From f3e84de5fb9eef7ffadd24af4c037d3b16535926 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 13 Jan 2022 13:13:08 +0300 Subject: [PATCH] Implement recent reaction userpics in groups. --- .../data/data_message_reactions.cpp | 44 +++++- .../SourceFiles/data/data_message_reactions.h | 8 +- Telegram/SourceFiles/history/history_item.cpp | 13 +- Telegram/SourceFiles/history/history_item.h | 2 + .../history/view/history_view_reactions.cpp | 129 +++++++++++++++++- .../history/view/history_view_reactions.h | 13 ++ Telegram/SourceFiles/ui/chat/chat.style | 7 + 7 files changed, 204 insertions(+), 12 deletions(-) diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 5730d5922..59b0a2ad9 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -438,19 +438,32 @@ void MessageReactions::add(const QString &reaction) { if (_chosen == reaction) { return; } + const auto history = _item->history(); + const auto self = history->session().user(); if (!_chosen.isEmpty()) { const auto i = _list.find(_chosen); Assert(i != end(_list)); --i->second; - if (!i->second) { + const auto removed = !i->second; + if (removed) { _list.erase(i); } + const auto j = _recent.find(_chosen); + if (j != end(_recent)) { + j->second.erase(ranges::remove(j->second, self), end(j->second)); + if (j->second.empty() || removed) { + _recent.erase(j); + } + } } _chosen = reaction; if (!reaction.isEmpty()) { + if (_item->canViewReactions()) { + _recent[reaction].push_back(self); + } ++_list[reaction]; } - auto &owner = _item->history()->owner(); + auto &owner = history->owner(); owner.reactions().send(_item, _chosen); owner.notifyItemDataChange(_item); } @@ -461,8 +474,10 @@ void MessageReactions::remove() { void MessageReactions::set( const QVector &list, + const QVector &recent, bool ignoreChosen) { - if (_item->history()->owner().reactions().sending(_item)) { + auto &owner = _item->history()->owner(); + if (owner.reactions().sending(_item)) { // We'll apply non-stale data from the request response. return; } @@ -499,8 +514,24 @@ void MessageReactions::set( _chosen = QString(); } } + auto parsed = base::flat_map< + QString, + std::vector>>(); + for (const auto &reaction : recent) { + reaction.match([&](const MTPDmessageUserReaction &data) { + const auto emoji = qs(data.vreaction()); + if (_list.contains(emoji)) { + parsed[emoji].push_back(owner.user(data.vuser_id())); + } + }); + } + if (_recent != parsed) { + _recent = std::move(parsed); + changed = true; + } + if (changed) { - _item->history()->owner().notifyItemDataChange(_item); + owner.notifyItemDataChange(_item); } } @@ -508,6 +539,11 @@ const base::flat_map &MessageReactions::list() const { return _list; } +auto MessageReactions::recent() const +-> const base::flat_map>> & { + return _recent; +} + bool MessageReactions::empty() const { return _list.empty(); } diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 9b0a020bd..c6ae98caa 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -125,8 +125,13 @@ public: void add(const QString &reaction); void remove(); - void set(const QVector &list, bool ignoreChosen); + void set( + const QVector &list, + const QVector &recent, + bool ignoreChosen); [[nodiscard]] const base::flat_map &list() const; + [[nodiscard]] auto recent() const + -> const base::flat_map>> &; [[nodiscard]] QString chosen() const; [[nodiscard]] bool empty() const; @@ -135,6 +140,7 @@ private: QString _chosen; base::flat_map _list; + base::flat_map>> _recent; }; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 141501212..94095aaf3 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -834,7 +834,10 @@ void HistoryItem::updateReactions(const MTPMessageReactions *reactions) { } else if (!_reactions) { _reactions = std::make_unique(this); } - _reactions->set(data.vresults().v, data.is_min()); + _reactions->set( + data.vresults().v, + data.vrecent_reactons().value_or_empty(), + data.is_min()); }); } @@ -847,6 +850,14 @@ const base::flat_map &HistoryItem::reactions() const { return _reactions ? _reactions->list() : kEmpty; } +auto HistoryItem::recentReactions() const +-> const base::flat_map>> & { + static const auto kEmpty = base::flat_map< + QString, + std::vector>>(); + return _reactions ? _reactions->recent() : kEmpty; +} + bool HistoryItem::canViewReactions() const { return (_flags & MessageFlag::CanViewReactions) && _reactions diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index c5d6cabcb..b2203f355 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -359,6 +359,8 @@ public: void updateReactions(const MTPMessageReactions *reactions); void updateReactionsUnknown(); [[nodiscard]] const base::flat_map &reactions() const; + [[nodiscard]] auto recentReactions() const + -> const base::flat_map>> &; [[nodiscard]] bool canViewReactions() const; [[nodiscard]] QString chosenReaction() const; [[nodiscard]] crl::time lastReactionsRefreshTime() const; diff --git a/Telegram/SourceFiles/history/view/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/history_view_reactions.cpp index 1b33b7448..bff4075ac 100644 --- a/Telegram/SourceFiles/history/view/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reactions.cpp @@ -12,7 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_message.h" #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 "data/data_message_reactions.h" +#include "data/data_user.h" #include "lang/lang_tag.h" #include "ui/chat/chat_style.h" #include "styles/style_chat.h" @@ -22,6 +24,7 @@ namespace { constexpr auto kInNonChosenOpacity = 0.12; constexpr auto kOutNonChosenOpacity = 0.18; +constexpr auto kMaxRecentUserpics = 3; [[nodiscard]] QColor AdaptChosenServiceFg(QColor serviceBg) { serviceBg.setAlpha(std::max(serviceBg.alpha(), 192)); @@ -92,7 +95,12 @@ void InlineList::layoutButtons() { ? std::move(*i) : prepareButtonWithEmoji(emoji)); const auto add = (emoji == _data.chosenReaction) ? 1 : 0; - setButtonCount(buttons.back(), count + add); + const auto j = _data.recent.find(emoji); + if (j != end(_data.recent) && !j->second.empty()) { + setButtonUserpics(buttons.back(), j->second); + } else { + setButtonCount(buttons.back(), count + add); + } } _buttons = std::move(buttons); } @@ -104,14 +112,53 @@ InlineList::Button InlineList::prepareButtonWithEmoji(const QString &emoji) { } void InlineList::setButtonCount(Button &button, int count) { - if (button.count == count) { + if (button.count == count && !button.userpics) { return; } + button.userpics = nullptr; button.count = count; button.countText = Lang::FormatCountToShort(count).string; button.countTextWidth = st::semiboldFont->width(button.countText); } +void InlineList::setButtonUserpics( + Button &button, + const std::vector> &users) { + if (!button.userpics) { + button.userpics = std::make_unique(); + } + const auto count = int(users.size()); + auto &list = button.userpics->list; + const auto regenerate = [&] { + if (list.size() != count) { + return true; + } + for (auto i = 0; i != count; ++i) { + if (users[i] != list[i].peer) { + return true; + } + } + return false; + }(); + if (!regenerate) { + return; + } + auto generated = std::vector(); + generated.reserve(count); + for (auto i = 0; i != count; ++i) { + if (i == list.size()) { + list.push_back(UserpicInRow{ + users[i] + }); + } else if (list[i].peer != users[i]) { + list[i].peer = users[i]; + } + } + while (list.size() > count) { + list.pop_back(); + } +} + QSize InlineList::countOptimalSize() { if (_buttons.empty()) { return _skipBlock; @@ -123,13 +170,26 @@ QSize InlineList::countOptimalSize() { const auto between = st::reactionInlineBetween; const auto padding = st::reactionInlinePadding; const auto size = st::reactionInlineSize; - const auto widthBase = padding.left() + const auto widthBaseCount = padding.left() + size + st::reactionInlineSkip + padding.right(); + const auto widthBaseUserpics = padding.left() + + size + + st::reactionInlineUserpicsPadding.left() + + st::reactionInlineUserpicsPadding.right(); + const auto userpicsWidth = [](const Button &button) { + const auto count = int(button.userpics->list.size()); + const auto single = st::reactionInlineUserpics.size; + const auto shift = st::reactionInlineUserpics.shift; + const auto width = single + (count - 1) * (single - shift); + return width; + }; const auto height = padding.top() + size + padding.bottom(); for (auto &button : _buttons) { - const auto width = widthBase + button.countTextWidth; + const auto width = button.userpics + ? (widthBaseUserpics + userpicsWidth(button)) + : (widthBaseCount + button.countTextWidth); button.geometry.setSize({ width, height }); x += width + between; } @@ -243,7 +303,16 @@ void InlineList::paint( return _animation->paintGetArea(p, QPoint(), image); }; } - if (!skipBubble) { + if (skipBubble) { + continue; + } + resolveUserpicsImage(button); + if (button.userpics) { + p.drawImage( + inner.x() + size + st::reactionInlineUserpicsPadding.left(), + geometry.y() + st::reactionInlineUserpicsPadding.top(), + button.userpics->image); + } else { p.setPen(!inbubble ? (chosen ? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c)) @@ -299,6 +368,35 @@ void InlineList::animateSend( st::reactionInlineImage); } +void InlineList::resolveUserpicsImage(const Button &button) const { + const auto userpics = button.userpics.get(); + const auto regenerate = [&] { + if (!userpics) { + return false; + } else if (userpics->image.isNull()) { + return true; + } + for (auto &entry : userpics->list) { + const auto peer = entry.peer; + auto &view = entry.view; + const auto wasView = view.get(); + if (peer->userpicUniqueKey(view) != entry.uniqueKey + || view.get() != wasView) { + return true; + } + } + return false; + }(); + if (!regenerate) { + return; + } + GenerateUserpicsInRow( + userpics->image, + userpics->list, + st::reactionInlineUserpics, + kMaxRecentUserpics); +} + std::unique_ptr InlineList::takeSendAnimation() { return std::move(_animation); } @@ -311,9 +409,28 @@ void InlineList::continueSendAnimation( InlineListData InlineListDataFromMessage(not_null message) { using Flag = InlineListData::Flag; const auto item = message->message(); - auto result = InlineListData(); result.reactions = item->reactions(); + const auto &recent = item->recentReactions(); + const auto showUserpics = [&] { + if (recent.size() != result.reactions.size()) { + return false; + } + auto b = begin(recent); + auto sum = 0; + for (const auto &[emoji, count] : result.reactions) { + sum += count; + if (emoji != b->first + || count != b->second.size() + || sum > kMaxRecentUserpics) { + return false; + } + } + return true; + }(); + if (showUserpics) { + result.recent = recent; + } result.chosenReaction = item->chosenReaction(); if (!result.chosenReaction.isEmpty()) { --result.reactions[result.chosenReaction]; diff --git a/Telegram/SourceFiles/history/view/history_view_reactions.h b/Telegram/SourceFiles/history/view/history_view_reactions.h index 5545000e3..fea50df95 100644 --- a/Telegram/SourceFiles/history/view/history_view_reactions.h +++ b/Telegram/SourceFiles/history/view/history_view_reactions.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { class Reactions; +class CloudImageView; } // namespace Data namespace Ui { @@ -22,6 +23,7 @@ using PaintContext = Ui::ChatPaintContext; class Message; struct TextState; struct SendReactionAnimationArgs; +struct UserpicInRow; } // namespace HistoryView namespace HistoryView::Reactions { @@ -37,6 +39,7 @@ struct InlineListData { using Flags = base::flags; base::flat_map reactions; + base::flat_map>> recent; QString chosenReaction; Flags flags = {}; }; @@ -74,10 +77,16 @@ public: void continueSendAnimation(std::unique_ptr animation); private: + struct Userpics { + QImage image; + std::vector list; + bool someNotLoaded = false; + }; struct Button { QRect geometry; mutable QImage image; mutable ClickHandlerPtr link; + std::unique_ptr userpics; QString emoji; QString countText; int count = 0; @@ -88,7 +97,11 @@ private: void layoutButtons(); void setButtonCount(Button &button, int count); + void setButtonUserpics( + Button &button, + const std::vector> &users); [[nodiscard]] Button prepareButtonWithEmoji(const QString &emoji); + void resolveUserpicsImage(const Button &button) const; QSize countOptimalSize() override; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index d08021ac3..205d645eb 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -969,6 +969,13 @@ reactionInlineImage: 28px; reactionInlineSkip: 3px; reactionInlineBetween: 4px; reactionInlineInBubbleLeft: -3px; +reactionInlineUserpicsPadding: margins(1px, 1px, 1px, 1px); +reactionInlineUserpics: GroupCallUserpics { + size: 18px; + shift: 7px; + stroke: 1px; + align: align(left); +} reactionInfoSize: 15px; reactionInfoImage: 30px;