diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg.png b/Telegram/Resources/icons/chat/reactions_expand_bg.png deleted file mode 100644 index 9c96eedd5..000000000 Binary files a/Telegram/Resources/icons/chat/reactions_expand_bg.png and /dev/null differ diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg@2x.png b/Telegram/Resources/icons/chat/reactions_expand_bg@2x.png deleted file mode 100644 index 827ac5f14..000000000 Binary files a/Telegram/Resources/icons/chat/reactions_expand_bg@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg@3x.png b/Telegram/Resources/icons/chat/reactions_expand_bg@3x.png deleted file mode 100644 index 3aaa0e272..000000000 Binary files a/Telegram/Resources/icons/chat/reactions_expand_bg@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/chat/reactions_round_big.png b/Telegram/Resources/icons/chat/reactions_round_big.png new file mode 100644 index 000000000..773b9f1c9 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_round_big.png differ diff --git a/Telegram/Resources/icons/chat/reactions_round_big@2x.png b/Telegram/Resources/icons/chat/reactions_round_big@2x.png new file mode 100644 index 000000000..e74f7fc48 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_round_big@2x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_round_big@3x.png b/Telegram/Resources/icons/chat/reactions_round_big@3x.png new file mode 100644 index 000000000..d0007a00b Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_round_big@3x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_round_small.png b/Telegram/Resources/icons/chat/reactions_round_small.png new file mode 100644 index 000000000..32df70aae Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_round_small.png differ diff --git a/Telegram/Resources/icons/chat/reactions_round_small@2x.png b/Telegram/Resources/icons/chat/reactions_round_small@2x.png new file mode 100644 index 000000000..548d984c8 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_round_small@2x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_round_small@3x.png b/Telegram/Resources/icons/chat/reactions_round_small@3x.png new file mode 100644 index 000000000..e5f648353 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_round_small@3x.png differ diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index b52c82b23..07ce80485 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -479,21 +479,26 @@ rpl::producer WhoReacted( : Ui::WhoReadType::Reacted; if (resolveWhoReacted) { const auto &list = item->reactions(); - state->current.fullReactionsCount = reaction.empty() - ? ranges::accumulate( + state->current.fullReactionsCount = [&] { + if (reaction.empty()) { + return ranges::accumulate( + list, + 0, + ranges::plus{}, + &Data::MessageReaction::count); + } + const auto i = ranges::find( list, - 0, - ranges::plus{}, - [](const auto &pair) { return pair.second; }) - : list.contains(reaction) - ? list.find(reaction)->second - : 0; + reaction, + &Data::MessageReaction::id); + return (i != end(list)) ? i->count : 0; + }(); // #TODO reactions state->current.singleReaction = (!reaction.empty() ? reaction : (list.size() == 1) - ? list.front().first + ? list.front().id : ReactionId()).emoji(); } std::move( diff --git a/Telegram/SourceFiles/data/data_message_reaction_id.h b/Telegram/SourceFiles/data/data_message_reaction_id.h index ed25fc721..ca64a6191 100644 --- a/Telegram/SourceFiles/data/data_message_reaction_id.h +++ b/Telegram/SourceFiles/data/data_message_reaction_id.h @@ -26,6 +26,12 @@ struct ReactionId { } }; +struct MessageReaction { + ReactionId id; + int count = 0; + bool my = false; +}; + inline bool operator<(const ReactionId &a, const ReactionId &b) { return a.data < b.data; } diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 9b8edc701..a3398422a 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -63,6 +63,18 @@ constexpr auto kTopReactionsLimit = 10; return result; } +[[nodiscard]] Reaction CustomReaction(not_null document) { + return Reaction{ + .id = { { document->id } }, + .title = "Custom reaction", + .appearAnimation = document, + .selectAnimation = document, + .centerIcon = document, + .active = true, + }; + +} + } // namespace PossibleItemReactions LookupPossibleReactions(not_null item) { @@ -74,22 +86,43 @@ PossibleItemReactions LookupPossibleReactions(not_null item) { const auto session = &peer->session(); const auto reactions = &session->data().reactions(); const auto &full = reactions->list(Reactions::Type::Active); + const auto &top = reactions->list(Reactions::Type::Top); + const auto &recent = reactions->list(Reactions::Type::Recent); const auto &all = item->reactions(); - const auto my = item->chosenReaction(); - auto myIsUnique = false; - for (const auto &[id, count] : all) { - if (count == 1 && id == my) { - myIsUnique = true; - } - } - const auto notMineCount = int(all.size()) - (myIsUnique ? 1 : 0); const auto limit = UniqueReactionsLimit(peer); - if (limit > 0 && notMineCount >= limit) { + const auto limited = (all.size() >= limit) && [&] { + const auto my = item->chosenReactions(); + if (my.empty()) { + return true; + } + return true; // #TODO reactions + }(); + auto added = base::flat_set(); + const auto addOne = [&](const Reaction &reaction) { + if (added.emplace(reaction.id).second) { + result.recent.push_back(&reaction); + } + }; + const auto add = [&](auto predicate) { + auto &&all = ranges::views::concat(top, recent, full); + for (const auto &reaction : all) { + if (predicate(reaction)) { + addOne(reaction); + } + } + }; + reactions->clearTemporary(); + if (limited) { result.recent.reserve(all.size()); - for (const auto &reaction : full) { + add([&](const Reaction &reaction) { + return ranges::contains(all, reaction.id, &MessageReaction::id); + }); + for (const auto &reaction : all) { const auto id = reaction.id; - if (all.contains(id)) { - result.recent.push_back(&reaction); + if (!added.contains(id)) { + if (const auto temp = reactions->lookupTemporary(id)) { + result.recent.push_back(temp); + } } } } else { @@ -97,22 +130,21 @@ PossibleItemReactions LookupPossibleReactions(not_null item) { result.recent.reserve((allowed.type == AllowedReactionsType::Some) ? allowed.some.size() : full.size()); - for (const auto &reaction : full) { + add([&](const Reaction &reaction) { const auto id = reaction.id; if ((allowed.type == AllowedReactionsType::Some) && !ranges::contains(allowed.some, id)) { - continue; + return false; } else if (reaction.premium && !session->premium() - && !all.contains(id)) { + && !ranges::contains(all, id, &MessageReaction::id)) { if (session->premiumPossible()) { result.morePremiumAvailable = true; } - continue; - } else { - result.recent.push_back(&reaction); + return false; } - } + return true; + }); result.customAllowed = (allowed.type == AllowedReactionsType::All); } const auto i = ranges::find( @@ -564,14 +596,7 @@ std::optional Reactions::resolveById(const ReactionId &id) { } else if (const auto customId = id.custom()) { const auto document = _owner->document(customId); if (document->sticker()) { - return Reaction{ - .id = id, - .title = "Custom reaction", - .appearAnimation = document, - .selectAnimation = document, - .centerIcon = document, - .active = true, - }; + return CustomReaction(document); } } return {}; @@ -637,7 +662,7 @@ std::optional Reactions::parse(const MTPAvailableReaction &entry) { }); } -void Reactions::send(not_null item, const ReactionId &chosen) { +void Reactions::send(not_null item, bool addToRecent) { const auto id = item->fullId(); auto &api = _owner->session().api(); auto i = _sentRequests.find(id); @@ -646,14 +671,17 @@ void Reactions::send(not_null item, const ReactionId &chosen) { } else { i = _sentRequests.emplace(id).first; } - const auto flags = chosen.empty() - ? MTPmessages_SendReaction::Flag(0) - : MTPmessages_SendReaction::Flag::f_reaction; + const auto chosen = item->chosenReactions(); + using Flag = MTPmessages_SendReaction::Flag; + const auto flags = (chosen.empty() ? Flag(0) : Flag::f_reaction) + | (addToRecent ? Flag::f_add_to_recent : Flag(0)); i->second = api.request(MTPmessages_SendReaction( MTP_flags(flags), item->history()->peer->input, MTP_int(id.msg), - MTP_vector(1, ReactionToMTP(chosen)) + MTP_vector(chosen | ranges::views::transform( + ReactionToMTP + ) | ranges::to>()) )).done([=](const MTPUpdates &result) { _sentRequests.remove(id); _owner->session().api().applyUpdates(result); @@ -693,6 +721,32 @@ void Reactions::updateAllInHistory(not_null peer, bool enabled) { } } +void Reactions::clearTemporary() { + _temporary.clear(); +} + +Reaction *Reactions::lookupTemporary(const ReactionId &id) { + if (const auto emoji = id.emoji(); !emoji.isEmpty()) { + const auto i = ranges::find(_available, id, &Reaction::id); + return (i != end(_available)) ? &*i : nullptr; + } else if (const auto customId = id.custom()) { + if (const auto i = _temporary.find(customId); i != end(_temporary)) { + return &i->second; + } + const auto document = _owner->document(customId); + if (document->sticker()) { + return &_temporary.emplace( + customId, + CustomReaction(document)).first->second; + } + _owner->customEmojiManager().resolve( + customId, + resolveListener()); + return nullptr; + } + return nullptr; +} + void Reactions::repaintCollected() { const auto now = crl::now(); auto closest = crl::time(); @@ -784,45 +838,84 @@ MessageReactions::MessageReactions(not_null item) : _item(item) { } -void MessageReactions::add(const ReactionId &reaction) { - if (_chosen == reaction) { - return; - } +void MessageReactions::add(const ReactionId &id, bool addToRecent) { + Expects(!id.empty()); + const auto history = _item->history(); const auto self = history->session().user(); - if (!_chosen.empty()) { - const auto i = _list.find(_chosen); - Assert(i != end(_list)); - --i->second; - const auto removed = !i->second; - if (removed) { - _list.erase(i); + const auto myLimit = self->isPremium() ? 5 : 1; // #TODO reactions + if (ranges::contains(chosen(), id)) { + return; + } + auto my = 0; + _list.erase(ranges::remove_if(_list, [&](MessageReaction &one) { + const auto removing = one.my && (my == myLimit || ++my == myLimit); + if (!removing) { + return false; } - const auto j = _recent.find(_chosen); + one.my = false; + const auto removed = !--one.count; + const auto j = _recent.find(one.id); if (j != end(_recent)) { j->second.erase( ranges::remove(j->second, self, &RecentReaction::peer), end(j->second)); - if (j->second.empty() || removed) { + if (j->second.empty()) { _recent.erase(j); + } else { + Assert(!removed); } } + return removed; + }), end(_list)); + if (_item->canViewReactions()) { + auto &list = _recent[id]; + list.insert(begin(list), RecentReaction{ self }); } - _chosen = reaction; - if (!reaction.empty()) { - if (_item->canViewReactions()) { - auto &list = _recent[reaction]; - list.insert(begin(list), RecentReaction{ self }); - } - ++_list[reaction]; + const auto i = ranges::find(_list, id, &MessageReaction::id); + if (i != end(_list)) { + i->my = true; + ++i->count; + std::rotate(i, i + 1, end(_list)); + } else { + _list.push_back({ .id = id, .count = 1, .my = true }); } auto &owner = history->owner(); - owner.reactions().send(_item, _chosen); + owner.reactions().send(_item, addToRecent); owner.notifyItemDataChange(_item); } -void MessageReactions::remove() { - add(ReactionId()); +void MessageReactions::remove(const ReactionId &id) { + const auto history = _item->history(); + const auto self = history->session().user(); + const auto i = ranges::find(_list, id, &MessageReaction::id); + const auto j = _recent.find(id); + if (i == end(_list)) { + Assert(j == end(_recent)); + return; + } else if (!i->my) { + Assert(j == end(_recent) + || !ranges::contains(j->second, self, &RecentReaction::peer)); + return; + } + i->my = false; + const auto removed = !--i->count; + if (removed) { + _list.erase(i); + } + if (j != end(_recent)) { + j->second.erase( + ranges::remove(j->second, self, &RecentReaction::peer), + end(j->second)); + if (j->second.empty()) { + _recent.erase(j); + } else { + Assert(!removed); + } + } + auto &owner = history->owner(); + owner.reactions().send(_item, false); + owner.notifyItemDataChange(_item); } bool MessageReactions::checkIfChanged( @@ -836,31 +929,31 @@ bool MessageReactions::checkIfChanged( auto existing = base::flat_set(); for (const auto &count : list) { const auto changed = count.match([&](const MTPDreactionCount &data) { - const auto reaction = ReactionFromMTP(data.vreaction()); + const auto id = ReactionFromMTP(data.vreaction()); const auto nowCount = data.vcount().v; - const auto i = _list.find(reaction); - const auto wasCount = (i != end(_list)) ? i->second : 0; + const auto i = ranges::find(_list, id, &MessageReaction::id); + const auto wasCount = (i != end(_list)) ? i->count : 0; if (wasCount != nowCount) { return true; } - existing.emplace(reaction); + existing.emplace(id); return false; }); if (changed) { return true; } } - for (const auto &[reaction, count] : _list) { - if (!existing.contains(reaction)) { + for (const auto &reaction : _list) { + if (!existing.contains(reaction.id)) { return true; } } auto parsed = base::flat_map>(); for (const auto &reaction : recent) { reaction.match([&](const MTPDmessagePeerReaction &data) { - const auto emoji = ReactionFromMTP(data.vreaction()); - if (_list.contains(emoji)) { - parsed[emoji].push_back(RecentReaction{ + const auto id = ReactionFromMTP(data.vreaction()); + if (ranges::contains(_list, id, &MessageReaction::id)) { + parsed[id].push_back(RecentReaction{ .peer = owner.peer(peerFromMTP(data.vpeer_id())), .unread = data.is_unread(), .big = data.is_big(), @@ -890,50 +983,79 @@ bool MessageReactions::change( } auto changed = false; auto existing = base::flat_set(); + auto order = base::flat_map(); for (const auto &count : list) { count.match([&](const MTPDreactionCount &data) { - const auto reaction = ReactionFromMTP(data.vreaction()); - if (!ignoreChosen) { - if (data.vchosen_order() && _chosen != reaction) { - _chosen = reaction; - changed = true; - } else if (!data.vchosen_order() && _chosen == reaction) { - _chosen = ReactionId(); + const auto id = ReactionFromMTP(data.vreaction()); + const auto &chosen = data.vchosen_order(); + if (!ignoreChosen && chosen) { + order[id] = chosen->v; + } + const auto i = ranges::find(_list, id, &MessageReaction::id); + const auto nowCount = data.vcount().v; + if (i == end(_list)) { + changed = true; + _list.push_back({ + .id = id, + .count = nowCount, + .my = (!ignoreChosen && chosen) + }); + } else { + const auto nowMy = ignoreChosen ? i->my : chosen.has_value(); + if (i->count != nowCount || i->my != nowMy) { + i->count = nowCount; + i->my = nowMy; changed = true; } } - const auto nowCount = data.vcount().v; - auto &wasCount = _list[reaction]; - if (wasCount != nowCount) { - wasCount = nowCount; - changed = true; - } - existing.emplace(reaction); + existing.emplace(id); }); } + if (!ignoreChosen && !order.empty()) { + const auto min = std::numeric_limits::min(); + const auto proj = [&](const MessageReaction &reaction) { + return reaction.my ? order[reaction.id] : min; + }; + const auto correctOrder = [&] { + auto previousOrder = min; + for (const auto &reaction : _list) { + const auto nowOrder = proj(reaction); + if (nowOrder < previousOrder) { + return false; + } + previousOrder = nowOrder; + } + return true; + }(); + if (!correctOrder) { + changed = true; + ranges::sort(_list, std::less(), proj); + } + } if (_list.size() != existing.size()) { changed = true; for (auto i = begin(_list); i != end(_list);) { - if (!existing.contains(i->first)) { + if (!existing.contains(i->id)) { i = _list.erase(i); } else { ++i; } } - if (!_chosen.empty() && !_list.contains(_chosen)) { - _chosen = ReactionId(); - } } auto parsed = base::flat_map>(); for (const auto &reaction : recent) { reaction.match([&](const MTPDmessagePeerReaction &data) { - const auto emoji = ReactionFromMTP(data.vreaction()); - if (_list.contains(emoji)) { - parsed[emoji].push_back(RecentReaction{ - .peer = owner.peer(peerFromMTP(data.vpeer_id())), - .unread = data.is_unread(), - .big = data.is_big(), - }); + const auto id = ReactionFromMTP(data.vreaction()); + const auto i = ranges::find(_list, id, &MessageReaction::id); + if (i != end(_list)) { + auto &list = parsed[id]; + if (list.size() < i->count) { + list.push_back(RecentReaction{ + .peer = owner.peer(peerFromMTP(data.vpeer_id())), + .unread = data.is_unread(), + .big = data.is_big(), + }); + } } }); } @@ -944,7 +1066,7 @@ bool MessageReactions::change( return changed; } -const base::flat_map &MessageReactions::list() const { +const std::vector &MessageReactions::list() const { return _list; } @@ -974,8 +1096,11 @@ void MessageReactions::markRead() { } } -ReactionId MessageReactions::chosen() const { - return _chosen; +std::vector MessageReactions::chosen() const { + return _list + | ranges::views::filter(&MessageReaction::my) + | ranges::views::transform(&MessageReaction::id) + | ranges::to_vector; } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index d5d00493a..84591c796 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -83,13 +83,16 @@ public: const ReactionId &emoji, ImageSize size); - void send(not_null item, const ReactionId &chosen); + void send(not_null item, bool addToRecent); [[nodiscard]] bool sending(not_null item) const; void poll(not_null item, crl::time now); void updateAllInHistory(not_null peer, bool enabled); + void clearTemporary(); + [[nodiscard]] Reaction *lookupTemporary(const ReactionId &id); + [[nodiscard]] static bool HasUnread(const MTPMessageReactions &data); static void CheckUnknownForUnread( not_null owner, @@ -160,6 +163,11 @@ private: rpl::event_stream<> _defaultUpdated; rpl::event_stream<> _favoriteUpdated; + // We need &i->second stay valid while inserting new items. + // So we use std::map instead of base::flat_map here. + // Otherwise we could use flat_map>. + std::map _temporary; + base::Timer _topRefreshTimer; mtpRequestId _topRequestId = 0; bool _topRequestScheduled = false; @@ -208,8 +216,8 @@ class MessageReactions final { public: explicit MessageReactions(not_null item); - void add(const ReactionId &reaction); - void remove(); + void add(const ReactionId &id, bool addToRecent); + void remove(const ReactionId &id); bool change( const QVector &list, const QVector &recent, @@ -217,10 +225,10 @@ public: [[nodiscard]] bool checkIfChanged( const QVector &list, const QVector &recent) const; - [[nodiscard]] const base::flat_map &list() const; + [[nodiscard]] const std::vector &list() const; [[nodiscard]] auto recent() const -> const base::flat_map> &; - [[nodiscard]] ReactionId chosen() const; + [[nodiscard]] std::vector chosen() const; [[nodiscard]] bool empty() const; [[nodiscard]] bool hasUnread() const; @@ -229,8 +237,7 @@ public: private: const not_null _item; - ReactionId _chosen; - base::flat_map _list; + std::vector _list; base::flat_map> _recent; }; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index f744617f9..745c4072e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -471,8 +471,8 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) { reaction.id)) { return; } - item->toggleReaction(reaction.id); - if (item->chosenReaction() != reaction.id) { + item->toggleReaction(reaction.id, HistoryItem::ReactionSource::Selector); + if (!ranges::contains(item->chosenReactions(), reaction.id)) { return; } else if (const auto view = item->mainView()) { if (const auto top = itemTop(view); top >= 0) { @@ -1948,12 +1948,12 @@ void HistoryInner::toggleFavoriteReaction(not_null view) const { &Data::Reaction::id) || Window::ShowReactPremiumError(_controller, item, favorite)) { return; - } else if (item->chosenReaction() != favorite) { + } else if (!ranges::contains(item->chosenReactions(), favorite)) { if (const auto top = itemTop(view); top >= 0) { view->animateReaction({ .id = favorite }); } } - item->toggleReaction(favorite); + item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); } void HistoryInner::contextMenuEvent(QContextMenuEvent *e) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 0576d1a5e..0c2df7c8f 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -883,15 +883,9 @@ bool HistoryItem::canReact() const { return true; } -void HistoryItem::addReaction(const Data::ReactionId &reaction) { - if (!_reactions) { - _reactions = std::make_unique(this); - } - _reactions->add(reaction); - history()->owner().notifyItemDataChange(this); -} - -void HistoryItem::toggleReaction(const Data::ReactionId &reaction) { +void HistoryItem::toggleReaction( + const Data::ReactionId &reaction, + ReactionSource source) { if (!_reactions) { _reactions = std::make_unique(this); const auto canViewReactions = !isDiscussionPost() @@ -899,16 +893,16 @@ void HistoryItem::toggleReaction(const Data::ReactionId &reaction) { if (canViewReactions) { _flags |= MessageFlag::CanViewReactions; } - _reactions->add(reaction); - } else if (_reactions->chosen() == reaction) { - _reactions->remove(); + _reactions->add(reaction, (source == ReactionSource::Selector)); + } else if (ranges::contains(_reactions->chosen(), reaction)) { + _reactions->remove(reaction); if (_reactions->empty()) { _reactions = nullptr; _flags &= ~MessageFlag::CanViewReactions; history()->owner().notifyItemDataChange(this); } } else { - _reactions->add(reaction); + _reactions->add(reaction, (source == ReactionSource::Selector)); } history()->owner().notifyItemDataChange(this); } @@ -977,8 +971,8 @@ void HistoryItem::updateReactionsUnknown() { _reactionsLastRefreshed = 1; } -const base::flat_map &HistoryItem::reactions() const { - static const auto kEmpty = base::flat_map(); +const std::vector &HistoryItem::reactions() const { + static const auto kEmpty = std::vector(); return _reactions ? _reactions->list() : kEmpty; } @@ -998,8 +992,10 @@ bool HistoryItem::canViewReactions() const { && !_reactions->list().empty(); } -Data::ReactionId HistoryItem::chosenReaction() const { - return _reactions ? _reactions->chosen() : Data::ReactionId(); +std::vector HistoryItem::chosenReactions() const { + return _reactions + ? _reactions->chosen() + : std::vector(); } Data::ReactionId HistoryItem::lookupUnreadReaction( diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index c7a8372f9..be1f33a6f 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -44,6 +44,7 @@ struct MessagePosition; struct RecentReaction; struct ReactionId; class Media; +struct MessageReaction; class MessageReactions; } // namespace Data @@ -373,18 +374,24 @@ public: [[nodiscard]] bool suggestDeleteAllReport() const; [[nodiscard]] bool canReact() const; - void addReaction(const Data::ReactionId &reaction); - void toggleReaction(const Data::ReactionId &reaction); + enum class ReactionSource { + Selector, + Quick, + Existing, + }; + void toggleReaction( + const Data::ReactionId &reaction, + ReactionSource source); void updateReactions(const MTPMessageReactions *reactions); void updateReactionsUnknown(); [[nodiscard]] auto reactions() const - -> const base::flat_map &; + -> const std::vector &; [[nodiscard]] auto recentReactions() const -> const base::flat_map< Data::ReactionId, std::vector> &; [[nodiscard]] bool canViewReactions() const; - [[nodiscard]] Data::ReactionId chosenReaction() const; + [[nodiscard]] std::vector chosenReactions() const; [[nodiscard]] Data::ReactionId lookupUnreadReaction( not_null from) const; [[nodiscard]] crl::time lastReactionsRefreshTime() const; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index e9518122a..8961fc87e 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -160,7 +160,7 @@ ClickHandlerPtr BottomInfo::revokeReactionLink( auto y = top; auto widthLeft = available; for (const auto &reaction : _reactions) { - const auto chosen = (reaction.id == _data.chosenReaction); + const auto chosen = reaction.chosen; const auto add = (reaction.countTextWidth > 0) ? st::reactionInfoDigitSkip : st::reactionInfoBetween; @@ -201,9 +201,11 @@ ClickHandlerPtr BottomInfo::revokeReactionLink( if (controller->session().uniqueId() == sessionId) { auto &owner = controller->session().data(); if (const auto item = owner.message(itemId)) { - const auto chosen = item->chosenReaction(); + const auto chosen = item->chosenReactions(); if (!chosen.empty()) { - item->toggleReaction(chosen); + item->toggleReaction( + chosen.front(), + HistoryItem::ReactionSource::Existing); } } } @@ -483,22 +485,23 @@ void BottomInfo::layoutReactionsText() { } auto sorted = ranges::view::all( _data.reactions - ) | ranges::view::transform([](const auto &pair) { - return std::make_pair(pair.first, pair.second); + ) | ranges::view::transform([](const MessageReaction &reaction) { + return not_null{ &reaction }; }) | ranges::to_vector; ranges::sort( sorted, std::greater<>(), - &std::pair::second); + &MessageReaction::count); auto reactions = std::vector(); reactions.reserve(sorted.size()); - for (const auto &[id, count] : sorted) { + for (const auto &reaction : sorted) { + const auto &id = reaction->id; const auto i = ranges::find(_reactions, id, &Reaction::id); reactions.push_back((i != end(_reactions)) ? std::move(*i) : prepareReactionWithId(id)); - setReactionCount(reactions.back(), count); + setReactionCount(reactions.back(), reaction->count); } _reactions = std::move(reactions); } @@ -593,7 +596,6 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null message) { result.date = message->dateTime(); if (message->embedReactionsInBottomInfo()) { result.reactions = item->reactions(); - result.chosenReaction = item->chosenReaction(); } if (message->hasOutLayout()) { result.flags |= Flag::OutLayout; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index 3d7a8d7a8..615fbccbd 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -45,6 +45,7 @@ struct TextState; class BottomInfo final : public Object { public: using ReactionId = ::Data::ReactionId; + using MessageReaction = ::Data::MessageReaction; struct Data { enum class Flag : uchar { Edited = 0x01, @@ -62,8 +63,7 @@ public: QDateTime date; QString author; - base::flat_map reactions; - ReactionId chosenReaction; + std::vector reactions; std::optional views; std::optional replies; Flags flags; @@ -105,6 +105,7 @@ private: QString countText; int count = 0; int countTextWidth = 0; + bool chosen = false; }; void layout(); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 660bd41ba..833f62b45 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -364,8 +364,10 @@ ListWidget::ListWidget( reaction.id)) { return; } - item->toggleReaction(reaction.id); - if (item->chosenReaction() != reaction.id) { + item->toggleReaction( + reaction.id, + HistoryItem::ReactionSource::Selector); + if (!ranges::contains(item->chosenReactions(), reaction.id)) { return; } else if (const auto view = viewForItem(item)) { if (const auto top = itemTop(view); top >= 0) { @@ -2129,12 +2131,12 @@ void ListWidget::toggleFavoriteReaction(not_null view) const { &Data::Reaction::id) || Window::ShowReactPremiumError(_controller, item, favorite)) { return; - } else if (item->chosenReaction() != favorite) { + } else if (!ranges::contains(item->chosenReactions(), favorite)) { if (const auto top = itemTop(view); top >= 0) { view->animateReaction({ .id = favorite }); } } - item->toggleReaction(favorite); + item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); } void ListWidget::trySwitchToWordSelection() { diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 1f4b962a5..30982b8ff 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2188,9 +2188,12 @@ void Message::refreshReactions() { const auto weak = base::make_weak(this); return std::make_shared([=] { if (const auto strong = weak.get()) { - strong->data()->toggleReaction(id); + strong->data()->toggleReaction( + id, + HistoryItem::ReactionSource::Existing); if (const auto now = weak.get()) { - if (now->data()->chosenReaction() == id) { + const auto chosen = now->data()->chosenReactions(); + if (ranges::contains(chosen, id)) { now->animateReaction({ .id = id, }); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index acb901bf9..ee43def46 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -45,6 +45,7 @@ struct InlineList::Button { QString countText; int count = 0; int countTextWidth = 0; + bool chosen = false; }; InlineList::InlineList( @@ -87,34 +88,39 @@ void InlineList::layoutButtons() { } auto sorted = ranges::view::all( _data.reactions - ) | ranges::view::transform([](const auto &pair) { - return std::make_pair(pair.first, pair.second); + ) | ranges::view::transform([](const MessageReaction &reaction) { + return not_null{ &reaction }; }) | ranges::to_vector; const auto &list = _owner->list(::Data::Reactions::Type::All); - ranges::sort(sorted, [&](const auto &p1, const auto &p2) { - if (p1.second > p2.second) { + ranges::sort(sorted, [&]( + not_null a, + not_null b) { + const auto acount = a->count - (a->my ? 1 : 0); + const auto bcount = b->count - (b->my ? 1 : 0); + if (acount > bcount) { return true; - } else if (p1.second < p2.second) { + } else if (acount < bcount) { return false; } - return ranges::find(list, p1.first, &::Data::Reaction::id) - < ranges::find(list, p2.first, &::Data::Reaction::id); + return ranges::find(list, a->id, &::Data::Reaction::id) + < ranges::find(list, b->id, &::Data::Reaction::id); }); auto buttons = std::vector