From 1060b04b1e657d32e90afbac2f6d731e52f4ca82 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 14 Jan 2022 18:42:24 +0300 Subject: [PATCH] Reacted users list on inline reaction right click. --- Telegram/SourceFiles/api/api_who_reacted.cpp | 81 ++-- Telegram/SourceFiles/api/api_who_reacted.h | 5 + .../history/history_inner_widget.cpp | 16 + .../history/history_inner_widget.h | 2 + .../view/history_view_context_menu.cpp | 62 +++ .../history/view/history_view_context_menu.h | 8 + .../history/view/history_view_list_widget.cpp | 30 +- .../history/view/history_view_list_widget.h | 2 + .../history/view/history_view_reactions.cpp | 4 + .../controls/who_reacted_context_action.cpp | 365 ++++++++++-------- .../ui/controls/who_reacted_context_action.h | 22 ++ 11 files changed, 396 insertions(+), 201 deletions(-) diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index deefd7f22..c493a3d82 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -80,7 +80,9 @@ struct CachedReacted { struct Context { base::flat_map, CachedRead> cachedRead; - base::flat_map, CachedReacted> cachedReacted; + base::flat_map< + not_null, + base::flat_map> cachedReacted; base::flat_map, rpl::lifetime> subscriptions; [[nodiscard]] CachedRead &cacheRead(not_null item) { @@ -91,12 +93,15 @@ struct Context { return cachedRead.emplace(item, CachedRead()).first->second; } - [[nodiscard]] CachedReacted &cacheReacted(not_null item) { - const auto i = cachedReacted.find(item); - if (i != end(cachedReacted)) { + [[nodiscard]] CachedReacted &cacheReacted( + not_null item, + const QString &reaction) { + auto &map = cachedReacted[item]; + const auto i = map.find(reaction); + if (i != end(map)) { return i->second; } - return cachedReacted.emplace(item, CachedReacted()).first->second; + return map.emplace(reaction, CachedReacted()).first->second; } }; @@ -140,9 +145,11 @@ struct State { item->history()->session().api().request(requestId).cancel(); } } - for (auto &[item, entry] : i->second->cachedReacted) { - if (const auto requestId = entry.requestId) { - item->history()->session().api().request(requestId).cancel(); + for (auto &[item, map] : i->second->cachedReacted) { + for (auto &[reaction, entry] : map) { + if (const auto requestId = entry.requestId) { + item->history()->session().api().request(requestId).cancel(); + } } } contexts.erase(i); @@ -150,7 +157,9 @@ struct State { return result; } -[[nodiscard]] not_null PreparedContextAt(not_null key, not_null session) { +[[nodiscard]] not_null PreparedContextAt( + not_null key, + not_null session) { const auto context = ContextAt(key); if (context->subscriptions.contains(session)) { return context; @@ -165,7 +174,9 @@ struct State { } const auto j = context->cachedReacted.find(update.item); if (j != end(context->cachedReacted)) { - session->api().request(j->second.requestId).cancel(); + for (auto &[reaction, entry] : j->second) { + session->api().request(entry.requestId).cancel(); + } context->cachedReacted.erase(j); } }, context->subscriptions[session]); @@ -244,6 +255,7 @@ struct State { [[nodiscard]] rpl::producer WhoReactedIds( not_null item, + const QString &reaction, not_null context) { auto weak = QPointer(context.get()); const auto session = &item->history()->session(); @@ -252,19 +264,22 @@ struct State { return rpl::lifetime(); } const auto context = PreparedContextAt(weak.data(), session); - auto &entry = context->cacheReacted(item); + auto &entry = context->cacheReacted(item, reaction); if (!entry.requestId) { + using Flag = MTPmessages_GetMessageReactionsList::Flag; entry.requestId = session->api().request( MTPmessages_GetMessageReactionsList( - MTP_flags(0), + MTP_flags(reaction.isEmpty() + ? Flag(0) + : Flag::f_reaction), item->history()->peer->input, MTP_int(item->id), - MTPstring(), // reaction + MTP_string(reaction), MTPstring(), // offset MTP_int(kContextReactionsLimit) ) ).done([=](const MTPmessages_MessageReactionsList &result) { - auto &entry = context->cacheReacted(item); + auto &entry = context->cacheReacted(item, reaction); entry.requestId = 0; result.match([&]( @@ -286,7 +301,7 @@ struct State { entry.data = std::move(parsed); }); }).fail([=] { - auto &entry = context->cacheReacted(item); + auto &entry = context->cacheReacted(item, reaction); entry.requestId = 0; if (entry.data.current().unknown) { entry.data = PeersWithReactions(); @@ -302,7 +317,7 @@ struct State { not_null context) -> rpl::producer { return rpl::combine( - WhoReactedIds(item, context), + WhoReactedIds(item, QString(), context), WhoReadIds(item, context) ) | rpl::map([=](PeersWithReactions reacted, Peers read) { if (reacted.unknown || read.unknown) { @@ -468,37 +483,52 @@ rpl::producer WhoReacted( not_null item, not_null context, const style::WhoRead &st) { + return WhoReacted(item, QString(), context, st); +} + +rpl::producer WhoReacted( + not_null item, + const QString &reaction, + not_null context, + const style::WhoRead &st) { const auto small = st.userpics.size; const auto large = st.photoSize; return [=](auto consumer) { auto lifetime = rpl::lifetime(); - const auto resolveWhoRead = WhoReadExists(item); + const auto resolveWhoRead = reaction.isEmpty() && WhoReadExists(item); const auto state = lifetime.make_state(); const auto pushNext = [=] { consumer.put_next_copy(state->current); }; - const auto resolveWhoReacted = item->canViewReactions(); + const auto resolveWhoReacted = !reaction.isEmpty() + || item->canViewReactions(); auto idsWithReactions = (resolveWhoRead && resolveWhoReacted) ? WhoReadOrReactedIds(item, context) : resolveWhoRead ? (WhoReadIds(item, context) | rpl::map(WithEmptyReactions)) - : WhoReactedIds(item, context); + : WhoReactedIds(item, reaction, context); state->current.type = resolveWhoRead ? DetectSeenType(item) : Ui::WhoReadType::Reacted; if (resolveWhoReacted) { const auto &list = item->reactions(); - state->current.fullReactionsCount = ranges::accumulate( - list, - 0, - ranges::plus{}, - [](const auto &pair) { return pair.second; }); + state->current.fullReactionsCount = reaction.isEmpty() + ? ranges::accumulate( + list, + 0, + ranges::plus{}, + [](const auto &pair) { return pair.second; }) + : list.contains(reaction) + ? list.find(reaction)->second + : 0; // #TODO reactions - state->current.singleReaction = (list.size() == 1) + state->current.singleReaction = !reaction.isEmpty() + ? reaction + : (list.size() == 1) ? list.front().first : QString(); } @@ -509,6 +539,7 @@ rpl::producer WhoReacted( state->userpics.clear(); consumer.put_next(Ui::WhoReadContent{ .type = state->current.type, + .fullReactionsCount = state->current.fullReactionsCount, .unknown = true, }); return; diff --git a/Telegram/SourceFiles/api/api_who_reacted.h b/Telegram/SourceFiles/api/api_who_reacted.h index fa5e342eb..fe05c3685 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.h +++ b/Telegram/SourceFiles/api/api_who_reacted.h @@ -27,5 +27,10 @@ namespace Api { not_null item, not_null context, const style::WhoRead &st); // Cache results for this lifetime. +[[nodiscard]] rpl::producer WhoReacted( + not_null item, + const QString &reaction, + not_null context, + const style::WhoRead &st); // Cache results for this lifetime. } // namespace Api diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 0690e0480..971963fc0 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -1892,6 +1892,22 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto hasWhoReactedItem = _dragStateItem && Api::WhoReactedExists(_dragStateItem); + const auto clickedEmoji = link + ? link->property(kReactionsCountEmojiProperty).toString() + : QString(); + _whoReactedMenuLifetime.destroy(); + if (hasWhoReactedItem && !clickedEmoji.isEmpty()) { + HistoryView::ShowWhoReactedMenu( + &_menu, + e->globalPos(), + this, + _dragStateItem, + clickedEmoji, + _controller, + _whoReactedMenuLifetime); + e->accept(); + return; + } _menu = base::make_unique_q( this, hasWhoReactedItem ? st::whoReadMenu : st::popupMenuWithIcons); diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 5645236a4..950f1be2e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -469,6 +469,8 @@ private: Ui::Animations::Simple _spoilerOpacity; + // _menu must be destroyed before _whoReactedMenuLifetime. + rpl::lifetime _whoReactedMenuLifetime; base::unique_qptr _menu; bool _scrollDateShown = false; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index cd7608c0d..aa0ed9631 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -44,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_scheduled_messages.h" +#include "data/data_message_reactions.h" #include "core/file_utilities.h" #include "core/click_handler_types.h" #include "base/platform/base_platform_info.h" @@ -1107,6 +1108,67 @@ void AddWhoReactedAction( showAllChosen)); } +void ShowWhoReactedMenu( + not_null*> menu, + QPoint position, + not_null context, + not_null item, + const QString &emoji, + not_null controller, + rpl::lifetime &lifetime) { + const auto participantChosen = [=](uint64 id) { + controller->showPeerInfo(PeerId(id)); + }; + const auto showAllChosen = [=, itemId = item->fullId()]{ + if (const auto item = controller->session().data().message(itemId)) { + controller->window().show(ReactionsListBox( + controller, + item, + emoji)); + } + }; + const auto reactions = &controller->session().data().reactions(); + const auto &list = reactions->list( + Data::Reactions::Type::Active); + const auto active = ranges::contains( + list, + emoji, + &Data::Reaction::emoji); + const auto filler = lifetime.make_state( + participantChosen, + showAllChosen); + Api::WhoReacted( + item, + emoji, + context, + st::defaultWhoRead + ) | rpl::filter([=](const Ui::WhoReadContent &content) { + return !content.unknown; + }) | rpl::start_with_next([=, &lifetime](Ui::WhoReadContent &&content) { + const auto creating = !*menu; + const auto refill = [=] { + if (active) { + (*menu)->addAction(tr::lng_context_set_as_quick(tr::now), [=] { + reactions->setFavorite(emoji); + }, &st::menuIconFave); + (*menu)->addSeparator(); + } + }; + if (creating) { + *menu = base::make_unique_q( + context, + st::whoReadMenu); + (*menu)->lifetime().add(base::take(lifetime)); + refill(); + } + filler->populate(menu->get(), content); + + if (creating) { + (*menu)->popup(position); + } + }, lifetime); +} + void ShowReportItemsBox(not_null peer, MessageIdsList ids) { const auto chosen = [=](Ui::ReportReason reason) { Ui::show(Box(Ui::ReportDetailsBox, [=](const QString &text) { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 54ec85d63..0861ebb27 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -65,6 +65,14 @@ void AddWhoReactedAction( not_null context, not_null item, not_null controller); +void ShowWhoReactedMenu( + not_null*> menu, + QPoint position, + not_null context, + not_null item, + const QString &emoji, + not_null controller, + rpl::lifetime &lifetime); void ShowReportItemsBox(not_null peer, MessageIdsList ids); void ShowReportPeerBox( diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 612d12405..13b58ca66 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "core/click_handler_types.h" #include "apiwrap.h" +#include "api/api_who_reacted.h" #include "layout/layout_selection.h" #include "window/window_adaptive.h" #include "window/window_session_controller.h" @@ -2098,16 +2099,35 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { && _reactionsManager->showContextMenu(this, e)) { return; } + const auto overItem = _overItemExact + ? _overItemExact + : _overElement + ? _overElement->data().get() + : nullptr; + const auto hasWhoReactedItem = overItem + && Api::WhoReactedExists(overItem); + const auto clickedEmoji = link + ? link->property(kReactionsCountEmojiProperty).toString() + : QString(); + _whoReactedMenuLifetime.destroy(); + if (hasWhoReactedItem && !clickedEmoji.isEmpty()) { + HistoryView::ShowWhoReactedMenu( + &_menu, + e->globalPos(), + this, + overItem, + clickedEmoji, + _controller, + _whoReactedMenuLifetime); + e->accept(); + return; + } auto request = ContextMenuRequest(_controller); request.link = link; request.view = _overElement; - request.item = _overItemExact - ? _overItemExact - : _overElement - ? _overElement->data().get() - : nullptr; + request.item = overItem; request.pointState = _overState.pointState; request.selectedText = _selectedText; request.selectedItems = collectSelectedItems(); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 0f5f1a9c7..2719cbb0d 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -614,6 +614,8 @@ private: bool _isChatWide = false; + // _menu must be destroyed before _whoReactedMenuLifetime. + rpl::lifetime _whoReactedMenuLifetime; base::unique_qptr _menu; QPoint _trippleClickPoint; diff --git a/Telegram/SourceFiles/history/view/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/history_view_reactions.cpp index bff4075ac..a4f2846f2 100644 --- a/Telegram/SourceFiles/history/view/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reactions.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_react_animation.h" #include "history/view/history_view_group_call_bar.h" +#include "core/click_handler_types.h" #include "data/data_message_reactions.h" #include "data/data_user.h" #include "lang/lang_tag.h" @@ -349,6 +350,9 @@ bool InlineList::getState( if (button.geometry.contains(point)) { if (!button.link) { button.link = _handlerFactory(button.emoji); + button.link->setProperty( + kReactionsCountEmojiProperty, + button.emoji); _owner->preloadAnimationsFor(button.emoji); } outResult->link = button.link; diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index 21a1b1ea1..1cfaa4ab5 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -26,34 +26,6 @@ struct EntryData { Fn callback; }; -class EntryAction final : public Menu::ItemBase { -public: - EntryAction( - not_null parent, - const style::Menu &st, - EntryData &&data); - - void setData(EntryData &&data); - - not_null action() const override; - bool isEnabled() const override; - -private: - int contentHeight() const override; - - void paint(Painter &&p); - - const not_null _dummyAction; - const style::Menu &_st; - const int _height = 0; - - Text::String _text; - EmojiPtr _emoji = nullptr; - int _textWidth = 0; - QImage _userpic; - -}; - class Action final : public Menu::ItemBase { public: Action( @@ -89,7 +61,7 @@ private: const std::unique_ptr _userpics; const style::Menu &_st; - std::vector> _submenuActions; + WhoReactedListMenu _submenu; Text::String _text; int _textWidth = 0; @@ -108,106 +80,6 @@ TextParseOptions MenuTextOptions = { Qt::LayoutDirectionAuto, // dir }; -EntryAction::EntryAction( - not_null parent, - const style::Menu &st, - EntryData &&data) -: ItemBase(parent, st) -, _dummyAction(CreateChild(parent.get())) -, _st(st) -, _height(st::defaultWhoRead.photoSkip * 2 + st::defaultWhoRead.photoSize) { - setAcceptBoth(true); - - initResizeHook(parent->sizeValue()); - setData(std::move(data)); - - paintRequest( - ) | rpl::start_with_next([=] { - paint(Painter(this)); - }, lifetime()); - - enableMouseSelecting(); -} - -not_null EntryAction::action() const { - return _dummyAction.get(); -} - -bool EntryAction::isEnabled() const { - return true; -} - -int EntryAction::contentHeight() const { - return _height; -} - -void EntryAction::setData(EntryData &&data) { - setClickedCallback(std::move(data.callback)); - _userpic = std::move(data.userpic); - _text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions); - _emoji = Emoji::Find(data.reaction); - const auto textWidth = _text.maxWidth(); - const auto &padding = _st.itemPadding; - const auto rightSkip = padding.right() - + (_emoji - ? ((Emoji::GetSizeNormal() / style::DevicePixelRatio()) - + padding.right()) - : 0); - const auto goodWidth = st::defaultWhoRead.nameLeft - + textWidth - + rightSkip; - const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax); - _textWidth = w - (goodWidth - textWidth); - setMinWidth(w); - update(); -} - -void EntryAction::paint(Painter &&p) { - const auto enabled = isEnabled(); - const auto selected = isSelected(); - if (selected && _st.itemBgOver->c.alpha() < 255) { - p.fillRect(0, 0, width(), _height, _st.itemBg); - } - p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg); - if (enabled) { - paintRipple(p, 0, 0); - } - const auto photoSize = st::defaultWhoRead.photoSize; - const auto photoLeft = st::defaultWhoRead.photoLeft; - const auto photoTop = (height() - photoSize) / 2; - if (!_userpic.isNull()) { - p.drawImage(photoLeft, photoTop, _userpic); - } else if (!_emoji) { - st::menuIconReactions.paintInCenter( - p, - QRect(photoLeft, photoTop, photoSize, photoSize)); - } - - p.setPen(selected - ? _st.itemFgOver - : enabled - ? _st.itemFg - : _st.itemFgDisabled); - _text.drawLeftElided( - p, - st::defaultWhoRead.nameLeft, - (height() - _st.itemStyle.font->height) / 2, - _textWidth, - width()); - - if (_emoji) { - // #TODO reactions - const auto size = Emoji::GetSizeNormal(); - const auto ratio = style::DevicePixelRatio(); - Emoji::Draw( - p, - _emoji, - size, - width() - _st.itemPadding.right() - (size / ratio), - (height() - (size / ratio)) / 2); - } -} - Action::Action( not_null parentMenu, rpl::producer content, @@ -223,6 +95,7 @@ Action::Action( rpl::never(), [=] { update(); })) , _st(parentMenu->menu()->st()) +, _submenu(_participantChosen, _showAllChosen) , _height(st::defaultWhoRead.itemPadding.top() + _st.itemStyle.font->height + st::defaultWhoRead.itemPadding.bottom()) { @@ -344,7 +217,7 @@ void Action::updateUserpicsFromContent() { void Action::populateSubmenu() { if (_content.participants.size() < 2) { - _submenuActions.clear(); + _submenu.clear(); _parentMenu->removeSubmenu(action()); if (!isEnabled()) { setSelected(false); @@ -353,47 +226,7 @@ void Action::populateSubmenu() { } const auto submenu = _parentMenu->ensureSubmenu(action()); - const auto reactions = ranges::count_if( - _content.participants, - [](const auto &p) { return !p.reaction.isEmpty(); }); - const auto addShowAll = (_content.fullReactionsCount > reactions); - const auto actionsCount = int(_content.participants.size()) - + (addShowAll ? 1 : 0); - if (_submenuActions.size() > actionsCount) { - _submenuActions.clear(); - submenu->clearActions(); - } - auto index = 0; - const auto append = [&](EntryData &&data) { - if (index < _submenuActions.size()) { - _submenuActions[index]->setData(std::move(data)); - } else { - auto item = base::make_unique_q( - submenu->menu(), - _st, - std::move(data)); - _submenuActions.push_back(item.get()); - submenu->addAction(std::move(item)); - } - ++index; - }; - for (const auto &participant : _content.participants) { - const auto chosen = [call = _participantChosen, id = participant.id] { - call(id); - }; - append({ - .text = participant.name, - .reaction = participant.reaction, - .userpic = participant.userpicLarge, - .callback = chosen, - }); - } - if (addShowAll) { - append({ - .text = tr::lng_context_seen_reacted_all(tr::now), - .callback = _showAllChosen, - }); - } + _submenu.populate(submenu, _content); _parentMenu->checkSubmenuShow(); } @@ -537,6 +370,134 @@ void Action::handleKeyPress(not_null e) { } // namespace +class WhoReactedListMenu::EntryAction final : public Menu::ItemBase { +public: + EntryAction( + not_null parent, + const style::Menu &st, + EntryData &&data); + + void setData(EntryData &&data); + + not_null action() const override; + bool isEnabled() const override; + +private: + int contentHeight() const override; + + void paint(Painter &&p); + + const not_null _dummyAction; + const style::Menu &_st; + const int _height = 0; + + Text::String _text; + EmojiPtr _emoji = nullptr; + int _textWidth = 0; + QImage _userpic; + +}; + +WhoReactedListMenu::EntryAction::EntryAction( + not_null parent, + const style::Menu &st, + EntryData &&data) +: ItemBase(parent, st) +, _dummyAction(CreateChild(parent.get())) +, _st(st) +, _height(st::defaultWhoRead.photoSkip * 2 + st::defaultWhoRead.photoSize) { + setAcceptBoth(true); + + initResizeHook(parent->sizeValue()); + setData(std::move(data)); + + paintRequest( + ) | rpl::start_with_next([=] { + paint(Painter(this)); + }, lifetime()); + + enableMouseSelecting(); +} + +not_null WhoReactedListMenu::EntryAction::action() const { + return _dummyAction.get(); +} + +bool WhoReactedListMenu::EntryAction::isEnabled() const { + return true; +} + +int WhoReactedListMenu::EntryAction::contentHeight() const { + return _height; +} + +void WhoReactedListMenu::EntryAction::setData(EntryData &&data) { + setClickedCallback(std::move(data.callback)); + _userpic = std::move(data.userpic); + _text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions); + _emoji = Emoji::Find(data.reaction); + const auto textWidth = _text.maxWidth(); + const auto &padding = _st.itemPadding; + const auto rightSkip = padding.right() + + (_emoji + ? ((Emoji::GetSizeNormal() / style::DevicePixelRatio()) + + padding.right()) + : 0); + const auto goodWidth = st::defaultWhoRead.nameLeft + + textWidth + + rightSkip; + const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax); + _textWidth = w - (goodWidth - textWidth); + setMinWidth(w); + update(); +} + +void WhoReactedListMenu::EntryAction::paint(Painter &&p) { + const auto enabled = isEnabled(); + const auto selected = isSelected(); + if (selected && _st.itemBgOver->c.alpha() < 255) { + p.fillRect(0, 0, width(), _height, _st.itemBg); + } + p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg); + if (enabled) { + paintRipple(p, 0, 0); + } + const auto photoSize = st::defaultWhoRead.photoSize; + const auto photoLeft = st::defaultWhoRead.photoLeft; + const auto photoTop = (height() - photoSize) / 2; + if (!_userpic.isNull()) { + p.drawImage(photoLeft, photoTop, _userpic); + } else if (!_emoji) { + st::menuIconReactions.paintInCenter( + p, + QRect(photoLeft, photoTop, photoSize, photoSize)); + } + + p.setPen(selected + ? _st.itemFgOver + : enabled + ? _st.itemFg + : _st.itemFgDisabled); + _text.drawLeftElided( + p, + st::defaultWhoRead.nameLeft, + (height() - _st.itemStyle.font->height) / 2, + _textWidth, + width()); + + if (_emoji) { + // #TODO reactions + const auto size = Emoji::GetSizeNormal(); + const auto ratio = style::DevicePixelRatio(); + Emoji::Draw( + p, + _emoji, + size, + width() - _st.itemPadding.right() - (size / ratio), + (height() - (size / ratio)) / 2); + } +} + bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b) { return (a.id == b.id) && (a.name == b.name) @@ -559,4 +520,66 @@ base::unique_qptr WhoReactedContextAction( std::move(showAllChosen)); } +WhoReactedListMenu::WhoReactedListMenu( + Fn participantChosen, + Fn showAllChosen) +: _participantChosen(std::move(participantChosen)) +, _showAllChosen(std::move(showAllChosen)) { +} + +void WhoReactedListMenu::clear() { + _actions.clear(); +} + +void WhoReactedListMenu::populate( + not_null menu, + const WhoReadContent &content, + Fn refillTopActions) { + const auto reactions = ranges::count_if( + content.participants, + [](const auto &p) { return !p.reaction.isEmpty(); }); + const auto addShowAll = (content.fullReactionsCount > reactions); + const auto actionsCount = int(content.participants.size()) + + (addShowAll ? 1 : 0); + if (_actions.size() > actionsCount) { + _actions.clear(); + menu->clearActions(); + if (refillTopActions) { + refillTopActions(); + } + } + auto index = 0; + const auto append = [&](EntryData &&data) { + if (index < _actions.size()) { + _actions[index]->setData(std::move(data)); + } else { + auto item = base::make_unique_q( + menu->menu(), + menu->menu()->st(), + std::move(data)); + _actions.push_back(item.get()); + menu->addAction(std::move(item)); + } + ++index; + }; + for (const auto &participant : content.participants) { + const auto chosen = [call = _participantChosen, id = participant.id]{ + call(id); + }; + append({ + .text = participant.name, + .reaction = participant.reaction, + .userpic = participant.userpicLarge, + .callback = chosen, + }); + } + if (addShowAll) { + append({ + .text = tr::lng_context_seen_reacted_all(tr::now), + .callback = _showAllChosen, + }); + } + +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h index 8874f8ea7..aa82c6fbc 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h @@ -51,4 +51,26 @@ struct WhoReadContent { Fn participantChosen, Fn showAllChosen); +class WhoReactedListMenu final { +public: + WhoReactedListMenu( + Fn participantChosen, + Fn showAllChosen); + + void clear(); + void populate( + not_null menu, + const WhoReadContent &content, + Fn refillTopActions = nullptr); + +private: + class EntryAction; + + const Fn _participantChosen; + const Fn _showAllChosen; + + std::vector> _actions; + +}; + } // namespace Ui