From e148b5ff086912728758beae169e0b82dec55591 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 13 Dec 2021 18:03:47 +0400 Subject: [PATCH] Proof-of-concept reactions dropdown. --- .../boxes/peers/edit_peer_reactions.cpp | 4 +- .../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 | 62 +++- .../history/history_inner_widget.h | 11 +- .../history/view/history_view_cursor_state.h | 1 + .../history/view/history_view_element.cpp | 5 + .../history/view/history_view_element.h | 8 + .../history/view/history_view_list_widget.cpp | 5 + .../history/view/history_view_list_widget.h | 2 + .../history/view/history_view_message.cpp | 46 ++- .../history/view/history_view_message.h | 2 + .../history/view/history_view_reactions.cpp | 323 ++++++++++++++++++ .../history/view/history_view_reactions.h | 102 ++++++ .../info/media/info_media_list_widget.cpp | 2 +- Telegram/SourceFiles/ui/chat/chat.style | 7 + .../window/window_session_controller.cpp | 1 + .../window/window_session_controller.h | 5 + 19 files changed, 585 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp index 0df4cc658b..69a9ae1c99 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp @@ -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) { setImage(image); diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 6bdb282f9c..7a90b0946d 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -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; 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 c2504a29fa..0e123bef02 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -673,6 +673,11 @@ 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 4c4e73efad..7ec6b7fb8c 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -141,6 +141,8 @@ 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 b9def165e3..9ff9f1bd8b 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -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(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 photo) { const auto media = photo->activeMediaView(); if (photo->isNull() || !media || !media->loaded()) { @@ -2884,6 +2921,13 @@ 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 {}; @@ -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 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 b6324ef84d..9ae8b54519 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -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 scroll, not_null controller, not_null history); + ~HistoryInner(); [[nodiscard]] Main::Session &session() const; [[nodiscard]] not_null theme() const { @@ -117,6 +120,7 @@ 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); @@ -155,8 +159,6 @@ public: // HistoryView::ElementDelegate interface. static not_null 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, std::shared_ptr> _userpics, _userpicsCache; + std::vector _reactions; + std::unique_ptr _reactionsMenus; + MouseAction _mouseAction = MouseAction::None; TextSelectType _mouseSelectType = TextSelectType::Letters; QPoint _dragStartPosition; diff --git a/Telegram/SourceFiles/history/view/history_view_cursor_state.h b/Telegram/SourceFiles/history/view/history_view_cursor_state.h index d9ae018897..12004bb3cb 100644 --- a/Telegram/SourceFiles/history/view/history_view_cursor_state.h +++ b/Telegram/SourceFiles/history/view/history_view_cursor_state.h @@ -53,6 +53,7 @@ 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 144816711c..dcc96009d9 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -188,6 +188,11 @@ void SimpleElementDelegate::elementShowReactions( not_null view) { } +const Data::Reaction *SimpleElementDelegate::elementCornerReaction( + not_null view) { + return nullptr; +} + TextSelection UnshiftItemSelection( TextSelection selection, uint16 byLength) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 2141c84149..eed7fb5055 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -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 view) = 0; virtual void elementShowReactions(not_null view) = 0; + virtual const Data::Reaction *elementCornerReaction( + not_null view) = 0; virtual ~ElementDelegate() { } @@ -150,6 +156,8 @@ 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 { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index a3d17e04b0..29d0e95053 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1461,6 +1461,11 @@ 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 33a781691b..78341683ad 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -279,6 +279,8 @@ 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 8df8d76af5..d9f03fa691 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -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([=] { + 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; diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 0d4803fae8..8c6877344c 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -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; + mutable std::unique_ptr _react; std::unique_ptr _reactions; mutable std::unique_ptr _comments; diff --git a/Telegram/SourceFiles/history/view/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/history_view_reactions.cpp index 0c3aa7c717..321f18b27d 100644 --- a/Telegram/SourceFiles/history/view/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reactions.cpp @@ -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) { return result; } +ReactButton::ReactButton( + Fn update, + Fn react, + QRect bubble) +: _update(std::move(update)) +, _handler(std::make_shared(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 ReactButton::pointState(QPoint point) const { + if (!_geometry.contains(point)) { + return std::nullopt; + } + return PointState::Inside; +} + +std::optional 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 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); + } +} + +ReactionsMenu::ReactionsMenu( + QWidget *parent, + const std::vector &list) +: _dropdown(parent) { + _dropdown.setAutoHiding(false); + + const auto content = _dropdown.setOwnedWidget( + object_ptr(&_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(); + content->setMouseTracking(true); + content->events( + ) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type == QEvent::MouseMove) { + const auto position = static_cast(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 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 &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(_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 diff --git a/Telegram/SourceFiles/history/view/history_view_reactions.h b/Telegram/SourceFiles/history/view/history_view_reactions.h index af90755560..721e2e2999 100644 --- a/Telegram/SourceFiles/history/view/history_view_reactions.h +++ b/Telegram/SourceFiles/history/view/history_view_reactions.h @@ -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); +class ReactButton final { +public: + ReactButton(Fn update, Fn react, QRect bubble); + + void updateGeometry(QRect bubble); + [[nodiscard]] int bottomOutsideMargin(int fullHeight) const; + [[nodiscard]] std::optional pointState(QPoint point) const; + [[nodiscard]] std::optional 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 reaction); + +private: + const Fn _update; + const ClickHandlerPtr _handler; + QRect _geometry; + bool _shown = false; + Ui::Animations::Simple _shownAnimation; + + QImage _image; + QPoint _imagePosition; + std::shared_ptr _media; + rpl::lifetime _downloadTaskLifetime; + +}; + +class ReactionsMenu final { +public: + ReactionsMenu( + QWidget *parent, + const std::vector &list); + + void showAround(QRect area); + void toggle(bool shown, anim::type animated); + + [[nodiscard]] rpl::producer chosen() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + struct Element { + QString emoji; + QRect geometry; + }; + Ui::InnerDropdown _dropdown; + rpl::event_stream _chosen; + std::vector _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 &list); + void hideAll(anim::type animated); + + [[nodiscard]] rpl::producer chosen() const { + return _chosen.events(); + } + +private: + QWidget *_parent = nullptr; + rpl::event_stream _chosen; + + std::unique_ptr _menu; + FullMsgId _context; + std::vector _list; + std::vector> _hiding; + +}; + } // namespace HistoryView diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 4999f2ef9c..fa89173f20 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -962,7 +962,7 @@ void ListWidget::repaintItem(QRect itemGeometry) { } bool ListWidget::isMyItem(not_null item) const { - auto peer = item->history()->peer; + const auto peer = item->history()->peer; return (_peer == peer) || (_migrated == peer); } diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 0ad6913781..58e7ebbe6b 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -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); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 9be829b3a7..c443b910ad 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -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" diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 2cca76cdd0..05d81d4b7f 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -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;