Proof-of-concept reactions dropdown.

This commit is contained in:
John Preston 2021-12-13 18:03:47 +04:00
parent 9c18f7b0e3
commit e148b5ff08
19 changed files with 585 additions and 9 deletions

View File

@ -66,9 +66,7 @@ void AddReactionIcon(
document->session().downloaderTaskFinished(
) | rpl::map([=] {
return state->media->getStickerLarge();
}) | rpl::filter([=](Image *image) {
return (image != nullptr);
}) | rpl::take(
}) | rpl::filter_nullptr() | rpl::take(
1
) | rpl::start_with_next([=](not_null<Image*> image) {
setImage(image);

View File

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/basic_click_handlers.h"
constexpr auto kPeerLinkPeerIdProperty = 0x01;
constexpr auto kReactionIdProperty = 0x02;
namespace Main {
class Session;

View File

@ -673,6 +673,11 @@ void InnerWidget::elementStartInteraction(not_null<const Element*> view) {
void InnerWidget::elementShowReactions(not_null<const Element*> view) {
}
const Data::Reaction *InnerWidget::elementCornerReaction(
not_null<const Element*> view) {
return nullptr;
}
void InnerWidget::saveState(not_null<SectionMemento*> memento) {
memento->setFilter(std::move(_filter));
memento->setAdmins(std::move(_admins));

View File

@ -141,6 +141,8 @@ public:
not_null<const HistoryView::Element*> view) override;
void elementShowReactions(
not_null<const HistoryView::Element*> view) override;
const Data::Reaction *elementCornerReaction(
not_null<const HistoryView::Element*> view) override;
~InnerWidget();

View File

@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_service_message.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_context_menu.h"
#include "history/view/history_view_reactions.h"
#include "history/view/history_view_emoji_interactions.h"
#include "history/history_item_components.h"
#include "history/history_item_text.h"
@ -176,6 +177,8 @@ HistoryInner::HistoryInner(
HistoryView::MakePathShiftGradient(
controller->chatStyle(),
[=] { update(); }))
, _reactionsMenus(
std::make_unique<HistoryView::ReactionsMenuManager>(historyWidget))
, _touchSelectTimer([=] { onTouchSelect(); })
, _touchScrollTimer([=] { onTouchScrollTimer(); })
, _scrollDateCheck([this] { scrollDateCheck(); })
@ -222,6 +225,14 @@ HistoryInner::HistoryInner(
_controller->emojiInteractions().playStarted(_peer, std::move(emoji));
}, lifetime());
using ChosenReaction = HistoryView::ReactionsMenuManager::Chosen;
_reactionsMenus->chosen(
) | rpl::start_with_next([=](ChosenReaction reaction) {
if (const auto item = session().data().message(reaction.context)) {
item->addReaction(reaction.emoji);
}
}, lifetime());
session().data().itemRemoved(
) | rpl::start_with_next(
[this](auto item) { itemRemoved(item); },
@ -263,6 +274,18 @@ HistoryInner::HistoryInner(
update();
}, lifetime());
rpl::combine(
rpl::single(
rpl::empty_value()
) | rpl::then(session().data().reactions().updates()),
session().changes().peerFlagsValue(
_peer,
Data::PeerUpdate::Flag::Reactions)
) | rpl::start_with_next([=] {
_reactions = session().data().reactions().list(_peer);
repaintItem(App::mousedItem());
}, lifetime());
controller->adaptive().chatWideValue(
) | rpl::start_with_next([=](bool wide) {
_isChatWide = wide;
@ -1510,6 +1533,7 @@ void HistoryInner::mouseActionFinish(
.sessionWindow = base::make_weak(_controller.get()),
})
});
_reactionsMenus->hideAll(anim::type::normal);
return;
}
if ((_mouseAction == MouseAction::PrepareSelect)
@ -2125,6 +2149,19 @@ void HistoryInner::copySelectedText() {
}
}
void HistoryInner::showReactionsMenu(FullMsgId itemId, QRect area) {
const auto top = itemTop(session().data().message(itemId));
if (top < 0) {
area = QRect(); // Just hide.
}
const auto skip = st::reactionCornerOut.y();
area = area.marginsRemoved({ 0, skip, 0, skip });
_reactionsMenus->showReactionsMenu(
itemId,
{ mapToGlobal(area.translated(0, top).topLeft()), area.size() },
_reactions);
}
void HistoryInner::savePhotoToFile(not_null<PhotoData*> photo) {
const auto media = photo->activeMediaView();
if (photo->isNull() || !media || !media->loaded()) {
@ -2884,6 +2921,13 @@ void HistoryInner::elementShowReactions(not_null<const Element*> view) {
view->data()));
}
const Data::Reaction *HistoryInner::elementCornerReaction(
not_null<const Element*> view) {
return (view == App::mousedItem() && !_reactions.empty())
? &_reactions.front()
: nullptr;
}
auto HistoryInner::getSelectionState() const
-> HistoryView::TopBarWidget::SelectedState {
auto result = HistoryView::TopBarWidget::SelectedState {};
@ -2960,7 +3004,16 @@ void HistoryInner::mouseActionUpdate() {
view = block->messages[_curItem].get();
item = view->data();
App::mousedItem(view);
const auto was = App::mousedItem();
if (was != view) {
if (!_reactions.empty()) {
repaintItem(was);
}
App::mousedItem(view);
if (!_reactions.empty()) {
repaintItem(view);
}
}
m = mapPointToItem(point, view);
if (view->pointState(m) != PointState::Outside) {
if (App::hoveredItem() != view) {
@ -3095,6 +3148,7 @@ void HistoryInner::mouseActionUpdate() {
|| dragState.customTooltip) {
Ui::Tooltip::Show(1000, this);
}
showReactionsMenu(dragState.itemId, dragState.reactionArea);
Qt::CursorShape cur = style::cur_default;
if (_mouseAction == MouseAction::None) {
@ -3798,6 +3852,12 @@ not_null<HistoryView::ElementDelegate*> HistoryInner::ElementDelegate() {
Instance->elementShowReactions(view);
}
}
const Data::Reaction *elementCornerReaction(
not_null<const Element*> view) override {
Expects(Instance != nullptr);
return Instance->elementCornerReaction(view);
}
};

View File

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
struct Group;
class CloudImageView;
struct Reaction;
} // namespace Data
namespace HistoryView {
@ -28,6 +29,7 @@ enum class CursorState : char;
enum class PointState : char;
class EmptyPainter;
class Element;
class ReactionsMenuManager;
} // namespace HistoryView
namespace Window {
@ -57,6 +59,7 @@ public:
not_null<Ui::ScrollArea*> scroll,
not_null<Window::SessionController*> controller,
not_null<History*> history);
~HistoryInner();
[[nodiscard]] Main::Session &session() const;
[[nodiscard]] not_null<Ui::ChatTheme*> theme() const {
@ -117,6 +120,7 @@ public:
void elementReplyTo(const FullMsgId &to);
void elementStartInteraction(not_null<const Element*> view);
void elementShowReactions(not_null<const Element*> view);
const Data::Reaction *elementCornerReaction(not_null<const Element*> view);
void updateBotInfo(bool recount = true);
@ -155,8 +159,6 @@ public:
// HistoryView::ElementDelegate interface.
static not_null<HistoryView::ElementDelegate*> ElementDelegate();
~HistoryInner();
protected:
bool focusNextPrevChild(bool next) override;
@ -338,10 +340,10 @@ private:
void deleteAsGroup(FullMsgId itemId);
void reportItem(FullMsgId itemId);
void reportAsGroup(FullMsgId itemId);
void reportItems(MessageIdsList ids);
void blockSenderItem(FullMsgId itemId);
void blockSenderAsGroup(FullMsgId itemId);
void copySelectedText();
void showReactionsMenu(FullMsgId itemId, QRect area);
void setupSharingDisallowed();
[[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const;
@ -395,6 +397,9 @@ private:
not_null<PeerData*>,
std::shared_ptr<Data::CloudImageView>> _userpics, _userpicsCache;
std::vector<Data::Reaction> _reactions;
std::unique_ptr<HistoryView::ReactionsMenuManager> _reactionsMenus;
MouseAction _mouseAction = MouseAction::None;
TextSelectType _mouseSelectType = TextSelectType::Letters;
QPoint _dragStartPosition;

View File

@ -53,6 +53,7 @@ struct TextState {
bool customTooltip = false;
uint16 symbol = 0;
QString customTooltipText;
QRect reactionArea;
};

View File

@ -188,6 +188,11 @@ void SimpleElementDelegate::elementShowReactions(
not_null<const Element*> view) {
}
const Data::Reaction *SimpleElementDelegate::elementCornerReaction(
not_null<const Element*> view) {
return nullptr;
}
TextSelection UnshiftItemSelection(
TextSelection selection,
uint16 byLength) {

View File

@ -18,6 +18,10 @@ class HistoryMessage;
class HistoryService;
struct HistoryMessageReply;
namespace Data {
struct Reaction;
} // namespace Data
namespace Window {
class SessionController;
} // namespace Window
@ -92,6 +96,8 @@ public:
virtual void elementReplyTo(const FullMsgId &to) = 0;
virtual void elementStartInteraction(not_null<const Element*> view) = 0;
virtual void elementShowReactions(not_null<const Element*> view) = 0;
virtual const Data::Reaction *elementCornerReaction(
not_null<const Element*> view) = 0;
virtual ~ElementDelegate() {
}
@ -150,6 +156,8 @@ public:
void elementReplyTo(const FullMsgId &to) override;
void elementStartInteraction(not_null<const Element*> view) override;
void elementShowReactions(not_null<const Element*> view) override;
const Data::Reaction *elementCornerReaction(
not_null<const Element*> view) override;
protected:
[[nodiscard]] not_null<Window::SessionController*> controller() const {

View File

@ -1461,6 +1461,11 @@ void ListWidget::elementStartInteraction(not_null<const Element*> view) {
void ListWidget::elementShowReactions(not_null<const Element*> view) {
}
const Data::Reaction *ListWidget::elementCornerReaction(
not_null<const Element*> view) {
return nullptr; // #TODO reactions
}
void ListWidget::saveState(not_null<ListMemento*> memento) {
memento->setAroundPosition(_aroundPosition);
auto state = countScrollState();

View File

@ -279,6 +279,8 @@ public:
void elementReplyTo(const FullMsgId &to) override;
void elementStartInteraction(not_null<const Element*> view) override;
void elementShowReactions(not_null<const Element*> view) override;
const Data::Reaction *elementCornerReaction(
not_null<const Element*> view) override;
void setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w);

View File

@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/data_channel.h"
#include "data/data_message_reactions.h"
#include "lang/lang_keys.h"
#include "mainwidget.h"
#include "main/main_session.h"
@ -604,6 +605,28 @@ void Message::draw(Painter &p, const PaintContext &context) const {
p.translate(-reactionsPosition);
}
if (const auto reaction = delegate()->elementCornerReaction(this)) {
if (!_react) {
_react = std::make_unique<ReactButton>([=] {
history()->owner().requestViewRepaint(this);
}, [=] {
if (const auto reaction
= delegate()->elementCornerReaction(this)) {
data()->addReaction(reaction->emoji);
}
}, g);
_react->toggle(true);
} else {
_react->updateGeometry(g);
}
_react->show(reaction);
} else if (_react) {
_react->toggle(false);
if (_react->isHidden()) {
_react = nullptr;
}
}
if (bubble) {
if (displayFromName()
&& item->displayFrom()
@ -744,6 +767,10 @@ void Message::draw(Painter &p, const PaintContext &context) const {
drawRightAction(p, context, fastShareLeft, fastShareTop, width());
}
if (_react) {
_react->paint(p, context);
}
if (media) {
media->paintBubbleFireworks(p, g, context.now);
}
@ -1070,6 +1097,12 @@ PointState Message::pointState(QPoint point) const {
return PointState::Outside;
}
if (_react) {
if (const auto state = _react->pointState(point)) {
return *state;
}
}
const auto media = this->media();
const auto item = message();
const auto reactionsInBubble = _reactions && needInfoDisplay();
@ -1246,6 +1279,14 @@ TextState Message::textState(
return result;
}
if (_react) {
if (const auto state = _react->textState(point, request)) {
result.link = state->link;
result.reactionArea = state->reactionArea;
return result;
}
}
const auto reactionsInBubble = _reactions && needInfoDisplay();
auto keyboard = item->inlineReplyKeyboard();
auto keyboardHeight = 0;
@ -1923,9 +1964,11 @@ void Message::itemDataChanged() {
auto Message::verticalRepaintRange() const -> VerticalRepaintRange {
const auto media = this->media();
const auto add = media ? media->bubbleRollRepaintMargins() : QMargins();
const auto addBottom = add.bottom()
+ (_react ? std::max(_react->bottomOutsideMargin(height()), 0) : 0);
return {
.top = -add.top(),
.height = height() + add.top() + add.bottom()
.height = height() + add.top() + addBottom
};
}
@ -2669,6 +2712,7 @@ int Message::resizeContentGetHeight(int newWidth) {
if (_reactions && !reactionsInBubble) {
newHeight += st::mediaInBubbleSkip + _reactions->resizeGetHeight(contentWidth);
}
if (const auto keyboard = item->inlineReplyKeyboard()) {
const auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight();
newHeight += keyboardHeight;

View File

@ -19,6 +19,7 @@ struct HistoryMessageForwarded;
namespace HistoryView {
class ViewButton;
class ReactButton;
class Reactions;
class WebPage;
@ -233,6 +234,7 @@ private:
mutable ClickHandlerPtr _rightActionLink;
mutable ClickHandlerPtr _fastReplyLink;
mutable std::unique_ptr<ViewButton> _viewButton;
mutable std::unique_ptr<ReactButton> _react;
std::unique_ptr<Reactions> _reactions;
mutable std::unique_ptr<CommentsButton> _comments;

View File

@ -8,11 +8,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_reactions.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h"
#include "history/history_message.h"
#include "ui/chat/chat_style.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "data/data_message_reactions.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "core/click_handler_types.h"
#include "main/main_session.h"
#include "styles/style_chat.h"
#include "styles/palette.h"
namespace HistoryView {
namespace {
constexpr auto kItemsPerRow = 5;
} // namespace
Reactions::Reactions(Data &&data)
: _data(std::move(data))
@ -101,4 +115,313 @@ Reactions::Data ReactionsDataFromMessage(not_null<Message*> message) {
return result;
}
ReactButton::ReactButton(
Fn<void()> update,
Fn<void()> react,
QRect bubble)
: _update(std::move(update))
, _handler(std::make_shared<LambdaClickHandler>(react)) {
updateGeometry(bubble);
}
void ReactButton::updateGeometry(QRect bubble) {
const auto topLeft = bubble.topLeft()
+ QPoint(bubble.width(), bubble.height())
+ QPoint(st::reactionCornerOut.x(), st::reactionCornerOut.y())
- QPoint(
st::reactionCornerSize.width(),
st::reactionCornerSize.height());
_geometry = QRect(topLeft, st::reactionCornerSize);
_imagePosition = _geometry.topLeft() + QPoint(
(_geometry.width() - st::reactionCornerImage) / 2,
(_geometry.height() - st::reactionCornerImage) / 2);
}
int ReactButton::bottomOutsideMargin(int fullHeight) const {
return _geometry.y() + _geometry.height() - fullHeight;
}
std::optional<PointState> ReactButton::pointState(QPoint point) const {
if (!_geometry.contains(point)) {
return std::nullopt;
}
return PointState::Inside;
}
std::optional<TextState> ReactButton::textState(
QPoint point,
const StateRequest &request) const {
if (!_geometry.contains(point)) {
return std::nullopt;
}
auto result = TextState(nullptr, _handler);
result.reactionArea = _geometry;
return result;
}
void ReactButton::paint(Painter &p, const PaintContext &context) {
const auto shown = _shownAnimation.value(_shown ? 1. : 0.);
if (shown == 0.) {
return;
}
p.setOpacity(shown);
p.setBrush(context.messageStyle()->msgBg);
p.setPen(st::shadowFg);
const auto radius = _geometry.height() / 2;
p.drawRoundedRect(_geometry, radius, radius);
if (!_image.isNull()) {
p.drawImage(_imagePosition, _image);
}
p.setOpacity(1.);
}
void ReactButton::toggle(bool shown) {
if (_shown == shown) {
return;
}
_shown = shown;
_shownAnimation.start(_update, _shown ? 0. : 1., _shown ? 1. : 0., 120);
}
bool ReactButton::isHidden() const {
return !_shown && !_shownAnimation.animating();
}
void ReactButton::show(not_null<const Data::Reaction*> reaction) {
if (_media && _media->owner() == reaction->staticIcon) {
return;
}
_handler->setProperty(kReactionIdProperty, reaction->emoji);
_media = reaction->staticIcon->createMediaView();
const auto setImage = [=](not_null<Image*> image) {
const auto size = st::reactionCornerImage;
_image = Images::prepare(
image->original(),
size * style::DevicePixelRatio(),
size * style::DevicePixelRatio(),
Images::Option::Smooth | Images::Option::TransparentBackground,
size,
size);
_image.setDevicePixelRatio(style::DevicePixelRatio());
};
if (const auto image = _media->getStickerLarge()) {
setImage(image);
} else {
reaction->staticIcon->session().downloaderTaskFinished(
) | rpl::map([=] {
return _media->getStickerLarge();
}) | rpl::filter_nullptr() | rpl::take(
1
) | rpl::start_with_next([=](not_null<Image*> image) {
setImage(image);
_update();
}, _downloadTaskLifetime);
}
}
ReactionsMenu::ReactionsMenu(
QWidget *parent,
const std::vector<Data::Reaction> &list)
: _dropdown(parent) {
_dropdown.setAutoHiding(false);
const auto content = _dropdown.setOwnedWidget(
object_ptr<Ui::RpWidget>(&_dropdown));
const auto count = int(list.size());
const auto single = st::reactionPopupImage;
const auto padding = st::reactionPopupPadding;
const auto width = padding.left() + single + padding.right();
const auto height = padding.top() + single + padding.bottom();
const auto rows = (count + kItemsPerRow - 1) / kItemsPerRow;
const auto columns = (int(list.size()) + rows - 1) / rows;
const auto inner = QRect(0, 0, columns * width, rows * height);
const auto outer = inner.marginsAdded(padding);
content->resize(outer.size());
_elements.reserve(list.size());
auto x = padding.left();
auto y = padding.top();
auto row = -1;
auto perrow = 0;
while (_elements.size() != list.size()) {
if (!perrow) {
++row;
perrow = (list.size() - _elements.size()) / (rows - row);
x = (outer.width() - perrow * width) / 2;
}
auto &reaction = list[_elements.size()];
_elements.push_back({
.emoji = reaction.emoji,
.geometry = QRect(x, y + row * height, width, height),
});
x += width;
--perrow;
}
struct State {
int selected = -1;
int pressed = -1;
};
const auto state = content->lifetime().make_state<State>();
content->setMouseTracking(true);
content->events(
) | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::MouseMove) {
const auto position = static_cast<QMouseEvent*>(e.get())->pos();
const auto i = ranges::find_if(_elements, [&](const Element &e) {
return e.geometry.contains(position);
});
const auto selected = (i != end(_elements))
? int(i - begin(_elements))
: -1;
if (state->selected != selected) {
state->selected = selected;
content->update();
}
} else if (type == QEvent::MouseButtonPress) {
state->pressed = state->selected;
content->update();
} else if (type == QEvent::MouseButtonRelease) {
const auto pressed = std::exchange(state->pressed, -1);
if (pressed >= 0) {
content->update();
if (pressed == state->selected) {
_chosen.fire_copy(_elements[pressed].emoji);
}
}
}
}, content->lifetime());
content->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(content);
const auto radius = st::roundRadiusSmall;
{
auto hq = PainterHighQualityEnabler(p);
p.setBrush(st::emojiPanBg);
p.setPen(Qt::NoPen);
p.drawRoundedRect(content->rect(), radius, radius);
}
auto index = 0;
const auto activeIndex = (state->pressed >= 0)
? state->pressed
: state->selected;
const auto size = Ui::Emoji::GetSizeNormal();
for (const auto &element : _elements) {
const auto active = (index++ == activeIndex);
if (active) {
auto hq = PainterHighQualityEnabler(p);
p.setBrush(st::windowBgOver);
p.setPen(Qt::NoPen);
p.drawRoundedRect(element.geometry, radius, radius);
}
if (const auto emoji = Ui::Emoji::Find(element.emoji)) {
Ui::Emoji::Draw(
p,
emoji,
size,
element.geometry.x() + (width - size) / 2,
element.geometry.y() + (height - size) / 2);
}
}
}, content->lifetime());
_dropdown.resizeToContent();
}
void ReactionsMenu::showAround(QRect area) {
const auto parent = _dropdown.parentWidget();
const auto left = std::min(
std::max(area.x() + (area.width() - _dropdown.width()) / 2, 0),
parent->width() - _dropdown.width());
_fromTop = (area.y() >= _dropdown.height());
_fromLeft = (area.center().x() - left
<= left + _dropdown.width() - area.center().x());
const auto top = _fromTop
? (area.y() - _dropdown.height())
: (area.y() + area.height());
_dropdown.move(left, top);
}
void ReactionsMenu::toggle(bool shown, anim::type animated) {
if (animated == anim::type::normal) {
if (shown) {
using Origin = Ui::PanelAnimation::Origin;
_dropdown.showAnimated(_fromTop
? (_fromLeft ? Origin::BottomLeft : Origin::BottomRight)
: (_fromLeft ? Origin::TopLeft : Origin::TopRight));
} else {
_dropdown.hideAnimated();
}
} else if (shown) {
_dropdown.showFast();
} else {
_dropdown.hideFast();
}
}
[[nodiscard]] rpl::producer<QString> ReactionsMenu::chosen() const {
return _chosen.events();
}
[[nodiscard]] rpl::lifetime &ReactionsMenu::lifetime() {
return _dropdown.lifetime();
}
ReactionsMenuManager::ReactionsMenuManager(QWidget *parent)
: _parent(parent) {
}
ReactionsMenuManager::~ReactionsMenuManager() = default;
void ReactionsMenuManager::showReactionsMenu(
FullMsgId context,
QRect globalReactionArea,
const std::vector<Data::Reaction> &list) {
if (globalReactionArea.isEmpty()) {
context = FullMsgId();
}
const auto listsEqual = ranges::equal(
_list,
list,
ranges::equal_to(),
&Data::Reaction::emoji,
&Data::Reaction::emoji);
const auto changed = (_context != context || !listsEqual);
if (_menu && changed) {
_menu->toggle(false, anim::type::normal);
_hiding.push_back(std::move(_menu));
}
_context = context;
_list = list;
if (list.size() < 2 || !context || (!changed && !_menu)) {
return;
} else if (!_menu) {
_menu = std::make_unique<ReactionsMenu>(_parent, list);
_menu->chosen(
) | rpl::start_with_next([=](QString emoji) {
_menu->toggle(false, anim::type::normal);
_hiding.push_back(std::move(_menu));
_chosen.fire({ context, std::move(emoji) });
}, _menu->lifetime());
}
const auto area = QRect(
_parent->mapFromGlobal(globalReactionArea.topLeft()),
globalReactionArea.size());
_menu->showAround(area);
_menu->toggle(true, anim::type::normal);
}
void ReactionsMenuManager::hideAll(anim::type animated) {
if (animated == anim::type::instant) {
_hiding.clear();
_menu = nullptr;
} else if (_menu) {
_menu->toggle(false, anim::type::normal);
_hiding.push_back(std::move(_menu));
}
}
} // namespace HistoryView

View File

@ -8,13 +8,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "history/view/history_view_object.h"
#include "ui/effects/animations.h"
#include "ui/widgets/inner_dropdown.h"
class Image;
namespace Ui {
class ChatStyle;
struct ChatPaintContext;
} // namespace Ui
namespace Data {
struct Reaction;
class DocumentMedia;
} // namespace Data
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
enum class PointState : char;
struct TextState;
struct StateRequest;
class Message;
class Reactions final : public Object {
@ -52,4 +66,92 @@ private:
[[nodiscard]] Reactions::Data ReactionsDataFromMessage(
not_null<Message*> message);
class ReactButton final {
public:
ReactButton(Fn<void()> update, Fn<void()> react, QRect bubble);
void updateGeometry(QRect bubble);
[[nodiscard]] int bottomOutsideMargin(int fullHeight) const;
[[nodiscard]] std::optional<PointState> pointState(QPoint point) const;
[[nodiscard]] std::optional<TextState> textState(
QPoint point,
const StateRequest &request) const;
void paint(Painter &p, const PaintContext &context);
void toggle(bool shown);
[[nodiscard]] bool isHidden() const;
void show(not_null<const Data::Reaction*> reaction);
private:
const Fn<void()> _update;
const ClickHandlerPtr _handler;
QRect _geometry;
bool _shown = false;
Ui::Animations::Simple _shownAnimation;
QImage _image;
QPoint _imagePosition;
std::shared_ptr<Data::DocumentMedia> _media;
rpl::lifetime _downloadTaskLifetime;
};
class ReactionsMenu final {
public:
ReactionsMenu(
QWidget *parent,
const std::vector<Data::Reaction> &list);
void showAround(QRect area);
void toggle(bool shown, anim::type animated);
[[nodiscard]] rpl::producer<QString> chosen() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct Element {
QString emoji;
QRect geometry;
};
Ui::InnerDropdown _dropdown;
rpl::event_stream<QString> _chosen;
std::vector<Element> _elements;
bool _fromTop = true;
bool _fromLeft = true;
};
class ReactionsMenuManager final {
public:
explicit ReactionsMenuManager(QWidget *parent);
~ReactionsMenuManager();
struct Chosen {
FullMsgId context;
QString emoji;
};
void showReactionsMenu(
FullMsgId context,
QRect globalReactionArea,
const std::vector<Data::Reaction> &list);
void hideAll(anim::type animated);
[[nodiscard]] rpl::producer<Chosen> chosen() const {
return _chosen.events();
}
private:
QWidget *_parent = nullptr;
rpl::event_stream<Chosen> _chosen;
std::unique_ptr<ReactionsMenu> _menu;
FullMsgId _context;
std::vector<Data::Reaction> _list;
std::vector<std::unique_ptr<ReactionsMenu>> _hiding;
};
} // namespace HistoryView

View File

@ -962,7 +962,7 @@ void ListWidget::repaintItem(QRect itemGeometry) {
}
bool ListWidget::isMyItem(not_null<const HistoryItem*> item) const {
auto peer = item->history()->peer;
const auto peer = item->history()->peer;
return (_peer == peer) || (_migrated == peer);
}

View File

@ -954,3 +954,10 @@ sendAsButton: SendAsButton {
}
duration: 150;
}
reactionCornerSize: size(23px, 18px);
reactionCornerOut: point(7px, 5px);
reactionCornerImage: 14px;
reactionPopupImage: 25px;
reactionPopupPadding: margins(5px, 5px, 5px, 5px);

View File

@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_replies_section.h"
#include "history/view/history_view_reactions.h"
#include "media/player/media_player_instance.h"
#include "media/view/media_view_open_common.h"
#include "data/data_document_resolver.h"

View File

@ -55,8 +55,13 @@ struct ChatThemeBackgroundData;
namespace Data {
struct CloudTheme;
enum class CloudThemeType;
struct Reaction;
} // namespace Data
namespace HistoryView {
class ReactionsMenu;
} // namespace HistoryView
namespace Window {
class MainWindow;