From 371c9c1bfef2d0c474ef9d61881f4e1076d3898e Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 15 Dec 2021 19:25:48 +0400 Subject: [PATCH] Implement a nice corner reaction button. --- .../SourceFiles/core/click_handler_types.h | 1 - .../admin_log/history_admin_log_inner.cpp | 5 - .../admin_log/history_admin_log_inner.h | 2 - .../history/history_inner_widget.cpp | 95 ++- .../history/history_inner_widget.h | 30 +- .../history/view/history_view_cursor_state.h | 1 - .../history/view/history_view_element.cpp | 11 +- .../history/view/history_view_element.h | 11 +- .../history/view/history_view_list_widget.cpp | 5 - .../history/view/history_view_list_widget.h | 2 - .../history/view/history_view_message.cpp | 77 +-- .../history/view/history_view_message.h | 12 +- .../history/view/history_view_reactions.cpp | 554 +++++++++++++----- .../history/view/history_view_reactions.h | 171 ++++-- Telegram/SourceFiles/ui/chat/chat.style | 8 +- .../window/window_session_controller.h | 5 - 16 files changed, 658 insertions(+), 332 deletions(-) diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 7a90b0946..6bdb282f9 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -10,7 +10,6 @@ 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; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index 0e123bef0..c2504a29f 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -673,11 +673,6 @@ void InnerWidget::elementStartInteraction(not_null view) { void InnerWidget::elementShowReactions(not_null view) { } -const Data::Reaction *InnerWidget::elementCornerReaction( - not_null view) { - return nullptr; -} - void InnerWidget::saveState(not_null memento) { memento->setFilter(std::move(_filter)); memento->setAdmins(std::move(_admins)); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index 7ec6b7fb8..4c4e73efa 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -141,8 +141,6 @@ public: not_null view) override; void elementShowReactions( not_null view) override; - const Data::Reaction *elementCornerReaction( - not_null view) override; ~InnerWidget(); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 9ff9f1bd8..d4c46c55d 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -177,8 +177,10 @@ HistoryInner::HistoryInner( HistoryView::MakePathShiftGradient( controller->chatStyle(), [=] { update(); })) -, _reactionsMenus( - std::make_unique(historyWidget)) +, _reactionsManager( + std::make_unique( + historyWidget, + [=](QRect updated) { update(updated); })) , _touchSelectTimer([=] { onTouchSelect(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); }) , _scrollDateCheck([this] { scrollDateCheck(); }) @@ -225,8 +227,8 @@ HistoryInner::HistoryInner( _controller->emojiInteractions().playStarted(_peer, std::move(emoji)); }, lifetime()); - using ChosenReaction = HistoryView::ReactionsMenuManager::Chosen; - _reactionsMenus->chosen( + using ChosenReaction = HistoryView::Reactions::Manager::Chosen; + _reactionsManager->chosen( ) | rpl::start_with_next([=](ChosenReaction reaction) { if (const auto item = session().data().message(reaction.context)) { item->addReaction(reaction.emoji); @@ -283,7 +285,7 @@ HistoryInner::HistoryInner( Data::PeerUpdate::Flag::Reactions) ) | rpl::start_with_next([=] { _reactions = session().data().reactions().list(_peer); - repaintItem(App::mousedItem()); + _reactionsManager->applyList(_reactions); }, lifetime()); controller->adaptive().chatWideValue( @@ -799,8 +801,8 @@ void HistoryInner::paintEvent(QPaintEvent *e) { view = block->messages[iItem].get(); item = view->data(); } - p.translate(0, -top); context.translate(0, top); + p.translate(0, -top); } if (htop >= 0) { auto iBlock = (_curHistory == _history ? _curBlock : 0); @@ -863,6 +865,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { view = block->messages[iItem].get(); item = view->data(); } + context.translate(0, top); p.translate(0, -top); if (readTill && _widget->doWeReadServerHistory()) { @@ -959,6 +962,9 @@ void HistoryInner::paintEvent(QPaintEvent *e) { return true; }); p.setOpacity(1.); + + _reactionsManager->paintButtons(p, context); + p.translate(0, _historyPaddingTop); _emojiInteractions->paint(p); } @@ -1533,7 +1539,7 @@ void HistoryInner::mouseActionFinish( .sessionWindow = base::make_weak(_controller.get()), }) }); - _reactionsMenus->hideAll(anim::type::normal); + _reactionsManager->hideSelectors(anim::type::normal); return; } if ((_mouseAction == MouseAction::PrepareSelect) @@ -1719,21 +1725,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { return; } const auto itemId = item->fullId(); - if (item->canReact()) { - auto reactionMenu = std::make_unique( - this, - st::reactionMenu); - auto &reactions = item->history()->owner().reactions(); - const auto &list = reactions.list(item->history()->peer); - if (!list.empty()) { - for (const auto &entry : list) { - reactionMenu->addAction(entry.emoji, [=] { - item->addReaction(entry.emoji); - }); - } - _menu->addAction("Reaction", std::move(reactionMenu), &st::menuIconReactions); - } - } if (canSendMessages) { _menu->addAction(tr::lng_context_reply_msg(tr::now), [=] { _widget->replyToMessage(itemId); @@ -2149,19 +2140,6 @@ 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 photo) { const auto media = photo->activeMediaView(); if (photo->isNull() || !media || !media->loaded()) { @@ -2696,6 +2674,7 @@ void HistoryInner::enterEventHook(QEnterEvent *e) { } void HistoryInner::leaveEventHook(QEvent *e) { + _reactionsManager->showButton({}); if (auto item = App::hoveredItem()) { repaintItem(item); App::hoveredItem(nullptr); @@ -2807,13 +2786,13 @@ bool HistoryInner::canCopySelected() const { } bool HistoryInner::canDeleteSelected() const { - auto selectedState = getSelectionState(); - return (selectedState.count > 0) && (selectedState.count == selectedState.canDeleteCount); + const auto selectedState = getSelectionState(); + return (selectedState.count > 0) + && (selectedState.count == selectedState.canDeleteCount); } bool HistoryInner::inSelectionMode() const { - if (!_selected.empty() - && (_selected.begin()->second == FullSelection)) { + if (hasSelectedItems()) { return true; } else if (_mouseAction == MouseAction::Selecting && _dragSelFrom @@ -2921,13 +2900,6 @@ void HistoryInner::elementShowReactions(not_null view) { view->data())); } -const Data::Reaction *HistoryInner::elementCornerReaction( - not_null view) { - return (view == App::mousedItem() && !_reactions.empty()) - ? &_reactions.front() - : nullptr; -} - auto HistoryInner::getSelectionState() const -> HistoryView::TopBarWidget::SelectedState { auto result = HistoryView::TopBarWidget::SelectedState {}; @@ -2955,10 +2927,14 @@ void HistoryInner::clearSelected(bool onlyTextSelection) { } } +bool HistoryInner::hasSelectedItems() const { + return !_selected.empty() && _selected.cbegin()->second == FullSelection; +} + MessageIdsList HistoryInner::getSelectedItems() const { using namespace ranges; - if (_selected.empty() || _selected.cbegin()->second != FullSelection) { + if (!hasSelectedItems()) { return {}; } @@ -2985,6 +2961,21 @@ void HistoryInner::onTouchSelect() { mouseActionStart(_touchPos, Qt::LeftButton); } +auto HistoryInner::reactionButtonParameters( + not_null view, + QPoint position) const +-> HistoryView::Reactions::ButtonParameters { + const auto top = itemTop(view); + if (top < 0 + || !view->data()->canReact() + || _mouseAction == MouseAction::Dragging + || inSelectionMode()) { + return {}; + } + const auto local = view->reactionButtonParameters(position); + return local.translated({ 0, itemTop(view) }); +} + void HistoryInner::mouseActionUpdate() { if (hasPendingResizedItems()) { return; @@ -3015,6 +3006,7 @@ void HistoryInner::mouseActionUpdate() { } } m = mapPointToItem(point, view); + _reactionsManager->showButton(reactionButtonParameters(view, m)); if (view->pointState(m) != PointState::Outside) { if (App::hoveredItem() != view) { repaintItem(App::hoveredItem()); @@ -3025,6 +3017,8 @@ void HistoryInner::mouseActionUpdate() { repaintItem(App::hoveredItem()); App::hoveredItem(nullptr); } + } else { + _reactionsManager->showButton({}); } if (_mouseActionItem && !_mouseActionItem->mainView()) { mouseActionCancel(); @@ -3148,7 +3142,6 @@ 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) { @@ -3852,12 +3845,6 @@ not_null HistoryInner::ElementDelegate() { Instance->elementShowReactions(view); } } - const Data::Reaction *elementCornerReaction( - not_null view) override { - Expects(Instance != nullptr); - - return Instance->elementCornerReaction(view); - } }; diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 9ae8b5451..988e93249 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -29,9 +29,13 @@ enum class CursorState : char; enum class PointState : char; class EmptyPainter; class Element; -class ReactionsMenuManager; } // namespace HistoryView +namespace HistoryView::Reactions { +class Manager; +struct ButtonParameters; +} // namespace HistoryView::Reactions + namespace Window { class SessionController; } // namespace Window @@ -69,7 +73,7 @@ public: void messagesReceived(PeerData *peer, const QVector &messages); void messagesReceivedDown(PeerData *peer, const QVector &messages); - TextForMimeData getSelectedText() const; + [[nodiscard]] TextForMimeData getSelectedText() const; void touchScrollUpdated(const QPoint &screenPos); @@ -82,14 +86,16 @@ public: void repaintItem(const HistoryItem *item); void repaintItem(const Element *view); - bool canCopySelected() const; - bool canDeleteSelected() const; + [[nodiscard]] bool canCopySelected() const; + [[nodiscard]] bool canDeleteSelected() const; - HistoryView::TopBarWidget::SelectedState getSelectionState() const; + [[nodiscard]] auto getSelectionState() const + -> HistoryView::TopBarWidget::SelectedState; void clearSelected(bool onlyTextSelection = false); - MessageIdsList getSelectedItems() const; - bool inSelectionMode() const; - bool elementIntersectsRange( + [[nodiscard]] MessageIdsList getSelectedItems() const; + [[nodiscard]] bool hasSelectedItems() const; + [[nodiscard]] bool inSelectionMode() const; + [[nodiscard]] bool elementIntersectsRange( not_null view, int from, int till) const; @@ -120,7 +126,6 @@ public: void elementReplyTo(const FullMsgId &to); void elementStartInteraction(not_null view); void elementShowReactions(not_null view); - const Data::Reaction *elementCornerReaction(not_null view); void updateBotInfo(bool recount = true); @@ -343,7 +348,10 @@ private: void blockSenderItem(FullMsgId itemId); void blockSenderAsGroup(FullMsgId itemId); void copySelectedText(); - void showReactionsMenu(FullMsgId itemId, QRect area); + + HistoryView::Reactions::ButtonParameters reactionButtonParameters( + not_null view, + QPoint position) const; void setupSharingDisallowed(); [[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const; @@ -398,7 +406,7 @@ private: std::shared_ptr> _userpics, _userpicsCache; std::vector _reactions; - std::unique_ptr _reactionsMenus; + std::unique_ptr _reactionsManager; MouseAction _mouseAction = MouseAction::None; TextSelectType _mouseSelectType = TextSelectType::Letters; diff --git a/Telegram/SourceFiles/history/view/history_view_cursor_state.h b/Telegram/SourceFiles/history/view/history_view_cursor_state.h index 12004bb3c..d9ae01889 100644 --- a/Telegram/SourceFiles/history/view/history_view_cursor_state.h +++ b/Telegram/SourceFiles/history/view/history_view_cursor_state.h @@ -53,7 +53,6 @@ struct TextState { bool customTooltip = false; uint16 symbol = 0; QString customTooltipText; - QRect reactionArea; }; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index dcc96009d..f2543b405 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media_grouped.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_large_emoji.h" +#include "history/view/history_view_reactions.h" #include "history/view/history_view_cursor_state.h" #include "history/history.h" #include "base/unixtime.h" @@ -188,11 +189,6 @@ void SimpleElementDelegate::elementShowReactions( not_null view) { } -const Data::Reaction *SimpleElementDelegate::elementCornerReaction( - not_null view) { - return nullptr; -} - TextSelection UnshiftItemSelection( TextSelection selection, uint16 byLength) { @@ -979,6 +975,11 @@ TextSelection Element::adjustSelection( return selection; } +Reactions::ButtonParameters Element::reactionButtonParameters( + QPoint position) const { + return {}; +} + void Element::clickHandlerActiveChanged( const ClickHandlerPtr &handler, bool active) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index eed7fb505..76b84a5b4 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -43,6 +43,10 @@ class Media; using PaintContext = Ui::ChatPaintContext; +namespace Reactions { +struct ButtonParameters; +} // namespace Reactions + enum class Context : char { History, Replies, @@ -96,8 +100,6 @@ public: virtual void elementReplyTo(const FullMsgId &to) = 0; virtual void elementStartInteraction(not_null view) = 0; virtual void elementShowReactions(not_null view) = 0; - virtual const Data::Reaction *elementCornerReaction( - not_null view) = 0; virtual ~ElementDelegate() { } @@ -156,8 +158,6 @@ public: void elementReplyTo(const FullMsgId &to) override; void elementStartInteraction(not_null view) override; void elementShowReactions(not_null view) override; - const Data::Reaction *elementCornerReaction( - not_null view) override; protected: [[nodiscard]] not_null controller() const { @@ -319,6 +319,9 @@ public: TextSelection selection, TextSelectType type) const; + [[nodiscard]] virtual auto reactionButtonParameters( + QPoint position) const -> Reactions::ButtonParameters; + // ClickHandlerHost interface. void clickHandlerActiveChanged( const ClickHandlerPtr &handler, diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 29d0e9505..a3d17e04b 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1461,11 +1461,6 @@ void ListWidget::elementStartInteraction(not_null view) { void ListWidget::elementShowReactions(not_null view) { } -const Data::Reaction *ListWidget::elementCornerReaction( - not_null view) { - return nullptr; // #TODO reactions -} - void ListWidget::saveState(not_null memento) { memento->setAroundPosition(_aroundPosition); auto state = countScrollState(); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 78341683a..33a781691 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -279,8 +279,6 @@ public: void elementReplyTo(const FullMsgId &to) override; void elementStartInteraction(not_null view) override; void elementShowReactions(not_null view) override; - const Data::Reaction *elementCornerReaction( - not_null view) override; void setEmptyInfoWidget(base::unique_qptr &&w); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index d9f03fa69..78103f3d2 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -605,28 +605,6 @@ 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([=] { - 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() @@ -767,10 +745,6 @@ 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); } @@ -1097,12 +1071,6 @@ 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(); @@ -1279,14 +1247,6 @@ 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; @@ -1828,6 +1788,28 @@ TextSelection Message::adjustSelection( return result; } +Reactions::ButtonParameters Message::reactionButtonParameters( + QPoint position) const { + const auto top = marginTop(); + if (!QRect(0, top, width(), height() - top).contains(position)) { + return {}; + } + auto result = Reactions::ButtonParameters{ .context = data()->fullId() }; + result.outbg = hasOutLayout(); + const auto geometry = countGeometry(); + result.center = geometry.topLeft() + + QPoint(geometry.width(), geometry.height()) + + st::reactionCornerCenter; + const auto size = st::reactionCornerSize; + const auto button = QRect( + result.center - QPoint(size.width() / 2, size.height() / 2), + size); + result.active = button.marginsAdded( + st::reactionCornerActiveAreaPadding + ).contains(position); + return result; +} + void Message::drawInfo( Painter &p, const PaintContext &context, @@ -1932,11 +1914,14 @@ void Message::refreshReactions() { const auto &list = item->reactions(); if (list.empty() || embedReactionsInBottomInfo()) { _reactions = nullptr; - } else if (!_reactions) { - _reactions = std::make_unique( - ReactionsDataFromMessage(this)); + return; + } + using namespace Reactions; + auto data = InlineListDataFromMessage(this); + if (!_reactions) { + _reactions = std::make_unique(std::move(data)); } else { - _reactions->update(ReactionsDataFromMessage(this), width()); + _reactions->update(std::move(data), width()); } } @@ -1964,11 +1949,9 @@ 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() + addBottom + .height = height() + add.top() + add.bottom() }; } diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 8c6877344..aeaca7fee 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -19,10 +19,12 @@ struct HistoryMessageForwarded; namespace HistoryView { class ViewButton; -class ReactButton; -class Reactions; class WebPage; +namespace Reactions { +class InlineList; +} // namespace Reactions + // Special type of Component for the channel actions log. struct LogEntryOriginal : public RuntimeComponent { @@ -85,6 +87,9 @@ public: TextSelection selection, TextSelectType type) const override; + Reactions::ButtonParameters reactionButtonParameters( + QPoint position) const override; + bool hasHeavyPart() const override; void unloadHeavyPart() override; @@ -234,8 +239,7 @@ private: mutable ClickHandlerPtr _rightActionLink; mutable ClickHandlerPtr _fastReplyLink; mutable std::unique_ptr _viewButton; - mutable std::unique_ptr _react; - std::unique_ptr _reactions; + std::unique_ptr _reactions; mutable std::unique_ptr _comments; Ui::Text::String _rightBadge; diff --git a/Telegram/SourceFiles/history/view/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/history_view_reactions.cpp index 321f18b27..adb9a55a5 100644 --- a/Telegram/SourceFiles/history/view/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reactions.cpp @@ -11,8 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_cursor_state.h" #include "history/history_message.h" #include "ui/chat/chat_style.h" +#include "ui/chat/message_bubble.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" +#include "ui/image/image_prepare.h" #include "data/data_message_reactions.h" #include "data/data_document.h" #include "data/data_document_media.h" @@ -21,20 +23,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat.h" #include "styles/palette.h" -namespace HistoryView { +namespace HistoryView::Reactions { namespace { constexpr auto kItemsPerRow = 5; +constexpr auto kToggleDuration = crl::time(80); +constexpr auto kActivateDuration = crl::time(150); +constexpr auto kInCacheIndex = 0; +constexpr auto kOutCacheIndex = 1; +constexpr auto kShadowCacheIndex = 0; +constexpr auto kEmojiCacheIndex = 1; +constexpr auto kMaskCacheIndex = 2; +constexpr auto kCacheColumsCount = 3; + +[[nodiscard]] QSize CountOuterSize() { + const auto extended = QRect( + QPoint(), + st::reactionCornerSize + ).marginsAdded(st::reactionCornerShadow); + const auto scale = Button::ScaleForState(ButtonState::Active); + return QSize( + int(base::SafeRound(extended.width() * scale)), + int(base::SafeRound(extended.height() * scale))); +} } // namespace -Reactions::Reactions(Data &&data) +InlineList::InlineList(Data &&data) : _data(std::move(data)) , _reactions(st::msgMinWidth / 2) { layout(); } -void Reactions::update(Data &&data, int availableWidth) { +void InlineList::update(Data &&data, int availableWidth) { _data = std::move(data); layout(); if (width() > 0) { @@ -42,20 +63,20 @@ void Reactions::update(Data &&data, int availableWidth) { } } -void Reactions::updateSkipBlock(int width, int height) { +void InlineList::updateSkipBlock(int width, int height) { _reactions.updateSkipBlock(width, height); } -void Reactions::removeSkipBlock() { +void InlineList::removeSkipBlock() { _reactions.removeSkipBlock(); } -void Reactions::layout() { +void InlineList::layout() { layoutReactionsText(); initDimensions(); } -void Reactions::layoutReactionsText() { +void InlineList::layoutReactionsText() { if (_data.reactions.empty()) { _reactions.clear(); return; @@ -87,18 +108,18 @@ void Reactions::layoutReactionsText() { Ui::NameTextOptions()); } -QSize Reactions::countOptimalSize() { +QSize InlineList::countOptimalSize() { return QSize(_reactions.maxWidth(), _reactions.minHeight()); } -QSize Reactions::countCurrentSize(int newWidth) { +QSize InlineList::countCurrentSize(int newWidth) { if (newWidth >= maxWidth()) { return optimalSize(); } return { newWidth, _reactions.countHeight(newWidth) }; } -void Reactions::paint( +void InlineList::paint( Painter &p, const Ui::ChatStyle *st, int outerWidth, @@ -106,8 +127,8 @@ void Reactions::paint( _reactions.draw(p, 0, 0, outerWidth); } -Reactions::Data ReactionsDataFromMessage(not_null message) { - auto result = Reactions::Data(); +InlineListData InlineListDataFromMessage(not_null message) { + auto result = InlineListData(); const auto item = message->message(); result.reactions = item->reactions(); @@ -115,111 +136,84 @@ Reactions::Data ReactionsDataFromMessage(not_null message) { return result; } -ReactButton::ReactButton( - Fn update, - Fn react, - QRect bubble) -: _update(std::move(update)) -, _handler(std::make_shared(react)) { - updateGeometry(bubble); +Button::Button( + Fn update, + ButtonParameters parameters) +: _update(std::move(update)) { + _geometry = QRect(QPoint(), CountOuterSize()); + _outbg = parameters.outbg; } -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); +Button::~Button() = default; + +bool Button::outbg() const { + return _outbg; } -int ReactButton::bottomOutsideMargin(int fullHeight) const { - return _geometry.y() + _geometry.height() - fullHeight; +bool Button::isHidden() const { + return (_state == State::Hidden) && !_scaleAnimation.animating(); } -std::optional ReactButton::pointState(QPoint point) const { - if (!_geometry.contains(point)) { - return std::nullopt; +QRect Button::geometry() const { + return _geometry; +} + +void Button::applyParameters(ButtonParameters parameters) { + const auto size = _geometry.size(); + const auto geometry = QRect( + parameters.center - QPoint(size.width(), size.height()) / 2, + size); + if (_outbg != parameters.outbg) { + _outbg = parameters.outbg; + _update(_geometry); } - return PointState::Inside; -} - -std::optional ReactButton::textState( - QPoint point, - const StateRequest &request) const { - if (!_geometry.contains(point)) { - return std::nullopt; + if (_geometry != geometry) { + if (!_geometry.isNull()) { + _update(_geometry); + } + _geometry = geometry; + _update(_geometry); } - auto result = TextState(nullptr, _handler); - result.reactionArea = _geometry; - return result; + applyState(parameters.active ? State::Active : State::Shown); } -void ReactButton::paint(Painter &p, const PaintContext &context) { - const auto shown = _shownAnimation.value(_shown ? 1. : 0.); - if (shown == 0.) { +void Button::applyState(State state) { + if (_state == state) { 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.); + const auto duration = (state == State::Hidden + || _state == State::Hidden) + ? kToggleDuration + : kActivateDuration; + _scaleAnimation.start( + [=] { _update(_geometry); }, + ScaleForState(_state), + ScaleForState(state), + duration); + _state = state; } -void ReactButton::toggle(bool shown) { - if (_shown == shown) { - return; +float64 Button::ScaleForState(State state) { + switch (state) { + case State::Hidden: return 0.7; + case State::Shown: return 1.; + case State::Active: return 1.4; } - _shown = shown; - _shownAnimation.start(_update, _shown ? 0. : 1., _shown ? 1. : 0., 120); + Unexpected("State in ReactionButton::ScaleForState."); } -bool ReactButton::isHidden() const { - return !_shown && !_shownAnimation.animating(); +float64 Button::OpacityForScale(float64 scale) { + return (scale >= 1.) + ? 1. + : ((scale - ScaleForState(State::Hidden)) + / (ScaleForState(State::Shown) - ScaleForState(State::Hidden))); } -void ReactButton::show(not_null reaction) { - if (_media && _media->owner() == reaction->staticIcon) { - return; - } - _handler->setProperty(kReactionIdProperty, reaction->emoji); - _media = reaction->staticIcon->createMediaView(); - const auto setImage = [=](not_null 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) { - setImage(image); - _update(); - }, _downloadTaskLifetime); - } +float64 Button::currentScale() const { + return _scaleAnimation.value(ScaleForState(_state)); } -ReactionsMenu::ReactionsMenu( +Selector::Selector( QWidget *parent, const std::vector &list) : _dropdown(parent) { @@ -331,7 +325,7 @@ ReactionsMenu::ReactionsMenu( _dropdown.resizeToContent(); } -void ReactionsMenu::showAround(QRect area) { +void Selector::showAround(QRect area) { const auto parent = _dropdown.parentWidget(); const auto left = std::min( std::max(area.x() + (area.width() - _dropdown.width()) / 2, 0), @@ -345,7 +339,7 @@ void ReactionsMenu::showAround(QRect area) { _dropdown.move(left, top); } -void ReactionsMenu::toggle(bool shown, anim::type animated) { +void Selector::toggle(bool shown, anim::type animated) { if (animated == anim::type::normal) { if (shown) { using Origin = Ui::PanelAnimation::Origin; @@ -362,65 +356,351 @@ void ReactionsMenu::toggle(bool shown, anim::type animated) { } } -[[nodiscard]] rpl::producer ReactionsMenu::chosen() const { +[[nodiscard]] rpl::producer Selector::chosen() const { return _chosen.events(); } -[[nodiscard]] rpl::lifetime &ReactionsMenu::lifetime() { +[[nodiscard]] rpl::lifetime &Selector::lifetime() { return _dropdown.lifetime(); } -ReactionsMenuManager::ReactionsMenuManager(QWidget *parent) -: _parent(parent) { +Manager::Manager(QWidget *selectorParent, Fn buttonUpdate) +: _outer(CountOuterSize()) +, _inner(QRectF( + (_outer.width() - st::reactionCornerSize.width()) / 2., + (_outer.height() - st::reactionCornerSize.height()) / 2., + st::reactionCornerSize.width(), + st::reactionCornerSize.height())) +, _buttonUpdate(std::move(buttonUpdate)) +, _selectorParent(selectorParent) { + const auto ratio = style::DevicePixelRatio(); + _cacheInOut = QImage( + _outer.width() * 2 * ratio, + _outer.height() * kFramesCount * ratio, + QImage::Format_ARGB32_Premultiplied); + _cacheInOut.setDevicePixelRatio(ratio); + _cacheInOut.fill(Qt::transparent); + _cacheParts = QImage( + _outer.width() * kCacheColumsCount * ratio, + _outer.height() * kFramesCount * ratio, + QImage::Format_ARGB32_Premultiplied); + _cacheParts.setDevicePixelRatio(ratio); + _cacheParts.fill(Qt::transparent); + _shadowBuffer = QImage( + _outer * ratio, + QImage::Format_ARGB32_Premultiplied); } -ReactionsMenuManager::~ReactionsMenuManager() = default; +Manager::~Manager() = default; -void ReactionsMenuManager::showReactionsMenu( - FullMsgId context, - QRect globalReactionArea, - const std::vector &list) { - if (globalReactionArea.isEmpty()) { +void Manager::showButton(ButtonParameters parameters) { + if (_button && _buttonContext != parameters.context) { + _button->applyState(ButtonState::Hidden); + _buttonHiding.push_back(std::move(_button)); + } + _buttonContext = parameters.context; + if (!_buttonContext || _list.size() < 2) { + return; + } + if (!_button) { + _button = std::make_unique