From 79a09a4510d833afb2109e98fca3873d7ed2898c Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 24 Dec 2021 13:51:53 +0000 Subject: [PATCH] Show latest reacted in context menu. --- Telegram/CMakeLists.txt | 4 +- .../{api_who_read.cpp => api_who_reacted.cpp} | 242 +++++++++++++++--- .../api/{api_who_read.h => api_who_reacted.h} | 2 +- .../history/history_inner_widget.cpp | 14 +- ...ion.cpp => who_reacted_context_action.cpp} | 62 +++-- ..._action.h => who_reacted_context_action.h} | 4 +- Telegram/cmake/td_ui.cmake | 4 +- 7 files changed, 262 insertions(+), 70 deletions(-) rename Telegram/SourceFiles/api/{api_who_read.cpp => api_who_reacted.cpp} (58%) rename Telegram/SourceFiles/api/{api_who_read.h => api_who_reacted.h} (92%) rename Telegram/SourceFiles/ui/controls/{who_read_context_action.cpp => who_reacted_context_action.cpp} (88%) rename Telegram/SourceFiles/ui/controls/{who_read_context_action.h => who_reacted_context_action.h} (89%) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 57f515dcb..ee60cd5dc 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -153,8 +153,8 @@ PRIVATE api/api_user_privacy.h api/api_views.cpp api/api_views.h - api/api_who_read.cpp - api/api_who_read.h + api/api_who_reacted.cpp + api/api_who_reacted.h boxes/filters/edit_filter_box.cpp boxes/filters/edit_filter_box.h boxes/filters/edit_filter_chats_list.cpp diff --git a/Telegram/SourceFiles/api/api_who_read.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp similarity index 58% rename from Telegram/SourceFiles/api/api_who_read.cpp rename to Telegram/SourceFiles/api/api_who_reacted.cpp index 6735facfe..cb47fc215 100644 --- a/Telegram/SourceFiles/api/api_who_read.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -5,7 +5,7 @@ the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ -#include "api/api_who_read.h" +#include "api/api_who_reacted.h" #include "history/history_item.h" #include "history/history.h" @@ -22,39 +22,71 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_account.h" #include "base/unixtime.h" #include "base/weak_ptr.h" -#include "ui/controls/who_read_context_action.h" +#include "ui/controls/who_reacted_context_action.h" #include "apiwrap.h" #include "styles/style_chat.h" namespace Api { namespace { -struct Cached { - explicit Cached(PeerId unknownFlag) +constexpr auto kContextReactionsLimit = 50; + +struct PeerWithReaction { + PeerId peer = 0; + QString reaction; +}; +bool operator==(const PeerWithReaction &a, const PeerWithReaction &b) { + return (a.peer == b.peer) && (a.reaction == b.reaction); +} + +struct CachedRead { + explicit CachedRead(PeerId unknownFlag) : list(std::vector{ unknownFlag }) { } rpl::variable> list; mtpRequestId requestId = 0; }; +struct CachedReacted { + explicit CachedReacted(PeerId unknownFlag) + : list( + std::vector{ PeerWithReaction{ unknownFlag } }) { + } + rpl::variable> list; + mtpRequestId requestId = 0; +}; + struct Context { - base::flat_map, Cached> cached; + base::flat_map, CachedRead> cachedRead; + base::flat_map, CachedReacted> cachedReacted; base::flat_map, rpl::lifetime> subscriptions; - [[nodiscard]] Cached &cache(not_null item) { - const auto i = cached.find(item); - if (i != end(cached)) { + [[nodiscard]] CachedRead &cacheRead(not_null item) { + const auto i = cachedRead.find(item); + if (i != end(cachedRead)) { return i->second; } - return cached.emplace( + return cachedRead.emplace( item, - Cached(item->history()->session().userPeerId()) + CachedRead(item->history()->session().userPeerId()) + ).first->second; + } + + [[nodiscard]] CachedReacted &cacheReacted(not_null item) { + const auto i = cachedReacted.find(item); + if (i != end(cachedReacted)) { + return i->second; + } + return cachedReacted.emplace( + item, + CachedReacted(item->history()->session().userPeerId()) ).first->second; } }; struct Userpic { not_null peer; + QString reaction; mutable std::shared_ptr view; mutable InMemoryKey uniqueKey; }; @@ -87,7 +119,12 @@ struct State { QObject::connect(key.get(), &QObject::destroyed, [=] { auto &contexts = Contexts(); const auto i = contexts.find(key); - for (auto &[item, entry] : i->second->cached) { + for (auto &[item, entry] : i->second->cachedRead) { + if (const auto requestId = entry.requestId) { + 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(); } @@ -97,6 +134,28 @@ struct State { return result; } +[[nodiscard]] not_null PreparedContextAt(not_null key, not_null session) { + const auto context = ContextAt(key); + if (context->subscriptions.contains(session)) { + return context; + } + session->changes().messageUpdates( + Data::MessageUpdate::Flag::Destroyed + ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { + const auto i = context->cachedRead.find(update.item); + if (i != end(context->cachedRead)) { + session->api().request(i->second.requestId).cancel(); + context->cachedRead.erase(i); + } + const auto j = context->cachedReacted.find(update.item); + if (j != end(context->cachedReacted)) { + session->api().request(j->second.requestId).cancel(); + context->cachedReacted.erase(j); + } + }, context->subscriptions[session]); + return context; +} + [[nodiscard]] QImage GenerateUserpic(Userpic &userpic, int size) { size *= style::DevicePixelRatio(); auto result = userpic.peer->generateUserpicImage(userpic.view, size); @@ -111,6 +170,14 @@ struct State { && (list.front() == item->history()->session().userPeerId()); } +[[nodiscard]] bool ListUnknown( + const std::vector &list, + not_null item) { + return (list.size() == 1) + && list.front().reaction.isEmpty() + && (list.front().peer == item->history()->session().userPeerId()); +} + [[nodiscard]] Ui::WhoReadType DetectType(not_null item) { if (const auto media = item->media()) { if (!media->webpage()) { @@ -135,20 +202,8 @@ struct State { if (!weak) { return rpl::lifetime(); } - const auto context = ContextAt(weak.data()); - if (!context->subscriptions.contains(session)) { - session->changes().messageUpdates( - Data::MessageUpdate::Flag::Destroyed - ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { - const auto i = context->cached.find(update.item); - if (i == end(context->cached)) { - return; - } - session->api().request(i->second.requestId).cancel(); - context->cached.erase(i); - }, context->subscriptions[session]); - } - auto &entry = context->cache(item); + const auto context = PreparedContextAt(weak.data(), session); + auto &entry = context->cacheRead(item); if (!entry.requestId) { entry.requestId = session->api().request( MTPmessages_GetMessageReadParticipants( @@ -156,7 +211,7 @@ struct State { MTP_int(item->id) ) ).done([=](const MTPVector &result) { - auto &entry = context->cache(item); + auto &entry = context->cacheRead(item); entry.requestId = 0; auto peers = std::vector(); peers.reserve(std::max(int(result.v.size()), 1)); @@ -165,7 +220,7 @@ struct State { } entry.list = std::move(peers); }).fail([=] { - auto &entry = context->cache(item); + auto &entry = context->cacheRead(item); entry.requestId = 0; if (ListUnknown(entry.list.current(), item)) { entry.list = std::vector(); @@ -176,33 +231,123 @@ struct State { }; } +[[nodiscard]] std::vector < PeerWithReaction> WithEmptyReactions( + const std::vector &peers) { + return peers | ranges::views::transform([](PeerId peer) { + return PeerWithReaction{ .peer = peer }; + }) | ranges::to_vector; +} + +[[nodiscard]] rpl::producer> WhoReactedIds( + not_null item, + not_null context) { + auto unknown = item->history()->session().userPeerId(); + auto weak = QPointer(context.get()); + const auto session = &item->history()->session(); + return [=](auto consumer) { + if (!weak) { + return rpl::lifetime(); + } + const auto context = PreparedContextAt(weak.data(), session); + auto &entry = context->cacheReacted(item); + if (!entry.requestId) { + entry.requestId = session->api().request( + MTPmessages_GetMessageReactionsList( + MTP_flags(0), + item->history()->peer->input, + MTP_int(item->id), + MTPstring(), // reaction + MTPstring(), // offset + MTP_int(kContextReactionsLimit) + ) + ).done([=](const MTPmessages_MessageReactionsList &result) { + auto &entry = context->cacheReacted(item); + entry.requestId = 0; + + result.match([&]( + const MTPDmessages_messageReactionsList &data) { + session->data().processUsers(data.vusers()); + + auto peers = std::vector(); + peers.reserve(data.vreactions().v.size()); + for (const auto &vote : data.vreactions().v) { + vote.match([&](const auto &data) { + peers.push_back(PeerWithReaction{ + .peer = peerFromUser(data.vuser_id()), + .reaction = qs(data.vreaction()), + }); + }); + } + entry.list = std::move(peers); + }); + }).fail([=] { + auto &entry = context->cacheReacted(item); + entry.requestId = 0; + if (ListUnknown(entry.list.current(), item)) { + entry.list = std::vector(); + } + }).send(); + } + return entry.list.value().start_existing(consumer); + }; +} + +[[nodiscard]] auto WhoReadOrReactedIds( + not_null item, + not_null context) +-> rpl::producer> { + return rpl::combine( + WhoReactedIds(item, context), + WhoReadIds(item, context) + ) | rpl::map([=]( + std::vector reacted, + std::vector read) { + if (ListUnknown(reacted, item) || ListUnknown(read, item)) { + return reacted; + } + for (const auto &peer : read) { + if (!ranges::contains(reacted, peer, &PeerWithReaction::peer)) { + reacted.push_back({ .peer = peer }); + } + } + return reacted; + }); +} + bool UpdateUserpics( not_null state, not_null item, - const std::vector &ids) { + const std::vector &ids) { auto &owner = item->history()->owner(); + struct ResolvedPeer { + PeerData *peer = nullptr; + QString reaction; + }; const auto peers = ranges::views::all( ids - ) | ranges::views::transform([&](PeerId id) { - return owner.peerLoaded(id); - }) | ranges::views::filter([](PeerData *peer) { - return peer != nullptr; - }) | ranges::views::transform([](PeerData *peer) { - return not_null(peer); + ) | ranges::views::transform([&](PeerWithReaction id) { + return ResolvedPeer{ + .peer = owner.peerLoaded(id.peer), + .reaction = id.reaction, + }; + }) | ranges::views::filter([](ResolvedPeer resolved) { + return resolved.peer != nullptr; }) | ranges::to_vector; const auto same = ranges::equal( state->userpics, peers, - ranges::less(), - &Userpic::peer); + ranges::equal_to(), + &Userpic::peer, + [](const ResolvedPeer &r) { return not_null{ r.peer }; }); if (same) { return false; } auto &was = state->userpics; auto now = std::vector(); - for (const auto &peer : peers) { + for (const auto &resolved : peers) { + const auto peer = not_null{ resolved.peer }; if (ranges::contains(now, peer, &Userpic::peer)) { continue; } @@ -213,6 +358,7 @@ bool UpdateUserpics( } now.push_back(Userpic{ .peer = peer, + .reaction = resolved.reaction, }); auto &userpic = now.back(); userpic.uniqueKey = peer->userpicUniqueKey(userpic.view); @@ -261,6 +407,7 @@ void RegenerateParticipants(not_null state, int small, int large) { } now.push_back({ .name = peer->name, + .reaction = userpic.reaction, .userpicLarge = GenerateUserpic(userpic, large), .userpicKey = userpic.uniqueKey, .id = id, @@ -313,7 +460,7 @@ bool WhoReactedExists(not_null item) { return item->canViewReactions() || WhoReadExists(item); } -rpl::producer WhoRead( +rpl::producer WhoReacted( not_null item, not_null context, const style::WhoRead &st) { @@ -341,10 +488,21 @@ rpl::producer WhoRead( consumer.put_next_copy(state->current); }; - WhoReadIds( - item, - context - ) | rpl::start_with_next([=](const std::vector &peers) { + const auto resolveWhoRead = WhoReadExists(item); + const auto resolveWhoReacted = item->canViewReactions(); + auto idsWithReactions = (resolveWhoRead && resolveWhoReacted) + ? WhoReadOrReactedIds(item, context) + : resolveWhoRead + ? (WhoReadIds(item, context) | rpl::map(WithEmptyReactions)) + : WhoReactedIds(item, context); + if (resolveWhoReacted) { + // #TODO reactions + state->current.mostPopularReaction = item->reactions().front().first; + } + std::move( + idsWithReactions + ) | rpl::start_with_next([=]( + const std::vector &peers) { if (ListUnknown(peers, item)) { state->userpics.clear(); consumer.put_next(Ui::WhoReadContent{ diff --git a/Telegram/SourceFiles/api/api_who_read.h b/Telegram/SourceFiles/api/api_who_reacted.h similarity index 92% rename from Telegram/SourceFiles/api/api_who_read.h rename to Telegram/SourceFiles/api/api_who_reacted.h index d77e24789..fa5e342eb 100644 --- a/Telegram/SourceFiles/api/api_who_read.h +++ b/Telegram/SourceFiles/api/api_who_reacted.h @@ -23,7 +23,7 @@ namespace Api { [[nodiscard]] bool WhoReactedExists(not_null item); // The context must be destroyed before the session holding this item. -[[nodiscard]] rpl::producer WhoRead( +[[nodiscard]] rpl::producer WhoReacted( not_null item, not_null context, const style::WhoRead &st); // Cache results for this lifetime. diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 616bc2670..304eaa28d 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -33,7 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/report_box.h" #include "ui/layers/generic_box.h" #include "ui/controls/delete_message_context_action.h" -#include "ui/controls/who_read_context_action.h" +#include "ui/controls/who_reacted_context_action.h" #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" #include "ui/inactive_press.h" @@ -60,7 +60,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "api/api_attached_stickers.h" #include "api/api_toggling_media.h" -#include "api/api_who_read.h" +#include "api/api_who_reacted.h" #include "api/api_views.h" #include "lang/lang_keys.h" #include "data/data_session.h" @@ -1701,11 +1701,11 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { isUponSelected = hasSelected; } - const auto hasWhoReadItem = _dragStateItem + const auto hasWhoReactedItem = _dragStateItem && Api::WhoReactedExists(_dragStateItem); _menu = base::make_unique_q( this, - hasWhoReadItem ? st::whoReadMenu : st::popupMenuWithIcons); + hasWhoReactedItem ? st::whoReadMenu : st::popupMenuWithIcons); const auto session = &this->session(); const auto controller = _controller; const auto groupLeaderOrSelf = [](HistoryItem *item) -> HistoryItem* { @@ -2079,16 +2079,16 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } } - if (hasWhoReadItem) { + if (hasWhoReactedItem) { const auto participantChosen = [=](uint64 id) { controller->showPeerInfo(PeerId(id)); }; if (!_menu->empty()) { _menu->addSeparator(); } - _menu->addAction(Ui::WhoReadContextAction( + _menu->addAction(Ui::WhoReactedContextAction( _menu.get(), - Api::WhoRead(_dragStateItem, this, st::defaultWhoRead), + Api::WhoReacted(_dragStateItem, this, st::defaultWhoRead), participantChosen)); } diff --git a/Telegram/SourceFiles/ui/controls/who_read_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp similarity index 88% rename from Telegram/SourceFiles/ui/controls/who_read_context_action.cpp rename to Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index ab49cda79..f233edc1b 100644 --- a/Telegram/SourceFiles/ui/controls/who_read_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -5,7 +5,7 @@ the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ -#include "ui/controls/who_read_context_action.h" +#include "ui/controls/who_reacted_context_action.h" #include "base/call_delayed.h" #include "ui/widgets/menu/menu_action.h" @@ -20,6 +20,7 @@ namespace { struct EntryData { QString text; + QString reaction; QImage userpic; Fn callback; }; @@ -46,6 +47,7 @@ private: const int _height = 0; Text::String _text; + EmojiPtr _emoji = nullptr; int _textWidth = 0; QImage _userpic; @@ -140,11 +142,17 @@ 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 - + padding.right(); + + rightSkip; const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax); _textWidth = w - (goodWidth - textWidth); setMinWidth(w); @@ -176,6 +184,18 @@ void EntryAction::paint(Painter &&p) { (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( @@ -260,6 +280,7 @@ void Action::resolveMinWidth() { return _st.itemStyle.font->width(text); }; const auto maxTextWidth = std::max({ + width(tr::lng_context_seen_text(tr::now, lt_count_short, 999999999)), width(tr::lng_context_seen_text(tr::now, lt_count, 999)), width(tr::lng_context_seen_listened(tr::now, lt_count, 999)), width(tr::lng_context_seen_watched(tr::now, lt_count, 999)) }); @@ -317,6 +338,7 @@ void Action::populateSubmenu() { }; auto data = EntryData{ .text = participant.name, + .reaction = participant.reaction, .userpic = participant.userpicLarge, .callback = chosen, }; @@ -345,18 +367,28 @@ void Action::paint(Painter &p) { if (enabled) { paintRipple(p, 0, 0); } - const auto &icon = (_content.type == WhoReadType::Seen) - ? (!enabled - ? st::whoReadChecksDisabled - : selected - ? st::whoReadChecksOver - : st::whoReadChecks) - : (!enabled - ? st::whoReadPlayedDisabled - : selected - ? st::whoReadPlayedOver - : st::whoReadPlayed); - icon.paint(p, st::defaultWhoRead.iconPosition, width()); + if (const auto emoji = Emoji::Find(_content.mostPopularReaction)) { + // #TODO reactions + const auto ratio = style::DevicePixelRatio(); + const auto size = Emoji::GetSizeNormal(); + const auto x = st::defaultWhoRead.iconPosition.x() + + (st::whoReadChecks.width() - (size / ratio)) / 2; + const auto y = (_height - (size / ratio)) / 2; + Emoji::Draw(p, emoji, size, x, y); + } else { + const auto &icon = (_content.type == WhoReadType::Seen) + ? (!enabled + ? st::whoReadChecksDisabled + : selected + ? st::whoReadChecksOver + : st::whoReadChecks) + : (!enabled + ? st::whoReadPlayedDisabled + : selected + ? st::whoReadPlayedOver + : st::whoReadPlayed); + icon.paint(p, st::defaultWhoRead.iconPosition, width()); + } p.setPen(!enabled ? _st.itemFgDisabled : selected @@ -457,7 +489,7 @@ bool operator!=(const WhoReadParticipant &a, const WhoReadParticipant &b) { return !(a == b); } -base::unique_qptr WhoReadContextAction( +base::unique_qptr WhoReactedContextAction( not_null menu, rpl::producer content, Fn participantChosen) { diff --git a/Telegram/SourceFiles/ui/controls/who_read_context_action.h b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h similarity index 89% rename from Telegram/SourceFiles/ui/controls/who_read_context_action.h rename to Telegram/SourceFiles/ui/controls/who_reacted_context_action.h index deb7b9736..4ffd6f14e 100644 --- a/Telegram/SourceFiles/ui/controls/who_read_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h @@ -18,6 +18,7 @@ class PopupMenu; struct WhoReadParticipant { QString name; + QString reaction; QImage userpicSmall; QImage userpicLarge; std::pair userpicKey = {}; @@ -38,10 +39,11 @@ enum class WhoReadType { struct WhoReadContent { std::vector participants; WhoReadType type = WhoReadType::Seen; + QString mostPopularReaction; bool unknown = false; }; -[[nodiscard]] base::unique_qptr WhoReadContextAction( +[[nodiscard]] base::unique_qptr WhoReactedContextAction( not_null menu, rpl::producer content, Fn participantChosen); diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 4f04ca58b..5ab2f6b1b 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -184,8 +184,8 @@ PRIVATE ui/controls/send_as_button.h ui/controls/send_button.cpp ui/controls/send_button.h - ui/controls/who_read_context_action.cpp - ui/controls/who_read_context_action.h + ui/controls/who_reacted_context_action.cpp + ui/controls/who_reacted_context_action.h ui/text/format_song_name.cpp ui/text/format_song_name.h ui/text/format_values.cpp