diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index c9714d67e..dde1a68cd 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2508,6 +2508,10 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().data().reactions().refreshRecentDelayed(); } break; + case mtpc_updateSavedReactionTags: { + session().data().reactions().refreshMyTagsDelayed(); + } break; + ////// Cloud saved GIFs case mtpc_updateSavedGifs: { session().data().stickers().setLastSavedGifsUpdate(0); diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 9dc677fbf..b6eadde69 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -38,6 +38,7 @@ constexpr auto kPollEach = 20 * crl::time(1000); constexpr auto kSizeForDownscale = 64; constexpr auto kRecentRequestTimeout = 10 * crl::time(1000); constexpr auto kRecentReactionsLimit = 40; +constexpr auto kMyTagsRequestTimeout = crl::time(1000); constexpr auto kTopRequestDelay = 60 * crl::time(1000); constexpr auto kTopReactionsLimit = 14; @@ -64,6 +65,27 @@ constexpr auto kTopReactionsLimit = 14; return result; } +[[nodiscard]] std::vector ListFromMTP( + const MTPDmessages_savedReactionTags &data) { + const auto &list = data.vtags().v; + auto result = std::vector(); + result.reserve(list.size()); + for (const auto &reaction : list) { + const auto &data = reaction.data(); + const auto id = ReactionFromMTP(data.vreaction()); + if (id.empty()) { + LOG(("API Error: reactionEmpty in messages.reactions.")); + } else { + result.push_back({ + .id = id, + .title = qs(data.vtitle().value_or_empty()), + .count = data.vcount().v, + }); + } + } + return result; +} + [[nodiscard]] Reaction CustomReaction(not_null document) { return Reaction{ .id = { { document->id } }, @@ -121,6 +143,8 @@ PossibleItemReactionsRef LookupPossibleReactions( 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 &myTags = reactions->list(Reactions::Type::MyTags); + const auto &tags = reactions->list(Reactions::Type::Tags); const auto &all = item->reactions(); const auto limit = UniqueReactionsLimit(peer); const auto premiumPossible = session->premiumPossible(); @@ -143,7 +167,19 @@ PossibleItemReactionsRef LookupPossibleReactions( } }; reactions->clearTemporary(); - if (limited) { + if (item->reactionsAreTags()) { + auto &&all = ranges::views::concat(myTags, tags); + result.recent.reserve(myTags.size() + tags.size()); + for (const auto &reaction : all) { + if (premiumPossible + || ranges::contains(tags, reaction.id, &Reaction::id)) { + if (added.emplace(reaction.id).second) { + result.recent.push_back(&reaction); + } + } + } + result.customAllowed = premiumPossible; + } else if (limited) { result.recent.reserve(all.size()); add([&](const Reaction &reaction) { return ranges::contains(all, reaction.id, &MessageReaction::id); @@ -193,12 +229,14 @@ PossibleItemReactionsRef LookupPossibleReactions( result.customAllowed = (allowed.type == AllowedReactionsType::All) && premiumPossible; } - const auto i = ranges::find( - result.recent, - reactions->favoriteId(), - &Reaction::id); - if (i != end(result.recent) && i != begin(result.recent)) { - std::rotate(begin(result.recent), i, i + 1); + if (!item->reactionsAreTags()) { + const auto i = ranges::find( + result.recent, + reactions->favoriteId(), + &Reaction::id); + if (i != end(result.recent) && i != begin(result.recent)) { + std::rotate(begin(result.recent), i, i + 1); + } } return result; } @@ -280,16 +318,42 @@ void Reactions::refreshDefault() { requestDefault(); } +void Reactions::refreshMyTags() { + requestMyTags(); +} + +void Reactions::refreshMyTagsDelayed() { + if (_myTagsRequestId || _myTagsRequestScheduled) { + return; + } + _myTagsRequestScheduled = true; + base::call_delayed(kMyTagsRequestTimeout, &_owner->session(), [=] { + if (_myTagsRequestScheduled) { + requestMyTags(); + } + }); +} + +void Reactions::refreshTags() { + requestTags(); +} + const std::vector &Reactions::list(Type type) const { switch (type) { case Type::Active: return _active; case Type::Recent: return _recent; case Type::Top: return _top; case Type::All: return _available; + case Type::MyTags: return _myTags; + case Type::Tags: return _tags; } Unexpected("Type in Reactions::list."); } +const std::vector &Reactions::myTagsInfo() const { + return _myTagsInfo; +} + ReactionId Reactions::favoriteId() const { return _favoriteId; } @@ -375,6 +439,14 @@ rpl::producer<> Reactions::favoriteUpdates() const { return _favoriteUpdated.events(); } +rpl::producer<> Reactions::myTagsUpdates() const { + return _myTagsUpdated.events(); +} + +rpl::producer<> Reactions::tagsUpdates() const { + return _tagsUpdated.events(); +} + void Reactions::preloadImageFor(const ReactionId &id) { if (_images.contains(id) || id.emoji().isEmpty()) { return; @@ -617,6 +689,46 @@ void Reactions::requestGeneric() { }).send(); } +void Reactions::requestMyTags() { + if (_myTagsRequestId) { + return; + } + auto &api = _owner->session().api(); + _myTagsRequestScheduled = false; + _myTagsRequestId = api.request(MTPmessages_GetSavedReactionTags( + MTP_long(_myTagsHash) + )).done([=](const MTPmessages_SavedReactionTags &result) { + _myTagsRequestId = 0; + result.match([&](const MTPDmessages_savedReactionTags &data) { + updateMyTags(data); + }, [](const MTPDmessages_savedReactionTagsNotModified&) { + }); + }).fail([=] { + _myTagsRequestId = 0; + _myTagsHash = 0; + }).send(); +} + +void Reactions::requestTags() { + if (_tagsRequestId) { + return; + } + auto &api = _owner->session().api(); + _tagsRequestId = api.request(MTPmessages_GetDefaultTagReactions( + MTP_long(_tagsHash) + )).done([=](const MTPmessages_Reactions &result) { + _tagsRequestId = 0; + result.match([&](const MTPDmessages_reactions &data) { + updateTags(data); + }, [](const MTPDmessages_reactionsNotModified&) { + }); + }).fail([=] { + _tagsRequestId = 0; + _tagsHash = 0; + }).send(); + +} + void Reactions::updateTop(const MTPDmessages_reactions &data) { _topHash = data.vhash().v; _topIds = ListFromMTP(data); @@ -685,6 +797,23 @@ void Reactions::updateGeneric(const MTPDmessages_stickerSet &data) { } } +void Reactions::updateMyTags(const MTPDmessages_savedReactionTags &data) { + _myTagsHash = data.vhash().v; + _myTagsInfo = ListFromMTP(data); + _myTagsIds = _myTagsInfo | ranges::views::transform( + &MyTagInfo::id + ) | ranges::to_vector; + _myTags = resolveByIds(_myTagsIds, _unresolvedMyTags); + _myTagsUpdated.fire({}); +} + +void Reactions::updateTags(const MTPDmessages_reactions &data) { + _tagsHash = data.vhash().v; + _tagsIds = ListFromMTP(data); + _tags = resolveByIds(_tagsIds, _unresolvedTags); + _tagsUpdated.fire({}); +} + void Reactions::recentUpdated() { _topRefreshTimer.callOnce(kTopRequestDelay); _recentUpdated.fire({}); @@ -696,9 +825,25 @@ void Reactions::defaultUpdated() { if (_genericAnimations.empty()) { requestGeneric(); } + refreshMyTags(); + refreshTags(); _defaultUpdated.fire({}); } +void Reactions::myTagsUpdated() { + if (_genericAnimations.empty()) { + requestGeneric(); + } + _myTagsUpdated.fire({}); +} + +void Reactions::tagsUpdated() { + if (_genericAnimations.empty()) { + requestGeneric(); + } + _tagsUpdated.fire({}); +} + not_null Reactions::resolveListener() { return static_cast(this); } @@ -710,6 +855,10 @@ void Reactions::customEmojiResolveDone(not_null document) { const auto top = (i != end(_unresolvedTop)); const auto j = _unresolvedRecent.find(id); const auto recent = (j != end(_unresolvedRecent)); + const auto k = _unresolvedMyTags.find(id); + const auto myTag = (k != end(_unresolvedMyTags)); + const auto l = _unresolvedTags.find(id); + const auto tag = (l != end(_unresolvedTags)); if (favorite) { _unresolvedFavoriteId = ReactionId(); _favorite = resolveById(_favoriteId); @@ -722,6 +871,14 @@ void Reactions::customEmojiResolveDone(not_null document) { _unresolvedRecent.erase(j); _recent = resolveByIds(_recentIds, _unresolvedRecent); } + if (myTag) { + _unresolvedMyTags.erase(k); + _myTags = resolveByIds(_myTagsIds, _unresolvedMyTags); + } + if (tag) { + _unresolvedTags.erase(l); + _tags = resolveByIds(_tagsIds, _unresolvedTags); + } if (favorite) { _favoriteUpdated.fire({}); } @@ -731,6 +888,12 @@ void Reactions::customEmojiResolveDone(not_null document) { if (recent) { _recentUpdated.fire({}); } + if (myTag) { + _myTagsUpdated.fire({}); + } + if (tag) { + _tagsUpdated.fire({}); + } } std::optional Reactions::resolveById(const ReactionId &id) { diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 61b29107d..e43538a11 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -56,6 +56,12 @@ struct PossibleItemReactions { [[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions( not_null item); +struct MyTagInfo { + ReactionId id; + QString title; + int count = 0; +}; + class Reactions final : private CustomEmojiManager::Listener { public: explicit Reactions(not_null owner); @@ -70,14 +76,20 @@ public: void refreshRecent(); void refreshRecentDelayed(); void refreshDefault(); + void refreshMyTags(); + void refreshMyTagsDelayed(); + void refreshTags(); enum class Type { Active, Recent, Top, All, + MyTags, + Tags, }; [[nodiscard]] const std::vector &list(Type type) const; + [[nodiscard]] const std::vector &myTagsInfo() const; [[nodiscard]] ReactionId favoriteId() const; [[nodiscard]] const Reaction *favorite() const; void setFavorite(const ReactionId &id); @@ -88,6 +100,8 @@ public: [[nodiscard]] rpl::producer<> recentUpdates() const; [[nodiscard]] rpl::producer<> defaultUpdates() const; [[nodiscard]] rpl::producer<> favoriteUpdates() const; + [[nodiscard]] rpl::producer<> myTagsUpdates() const; + [[nodiscard]] rpl::producer<> tagsUpdates() const; enum class ImageSize { BottomInfo, @@ -130,14 +144,20 @@ private: void requestRecent(); void requestDefault(); void requestGeneric(); + void requestMyTags(); + void requestTags(); void updateTop(const MTPDmessages_reactions &data); void updateRecent(const MTPDmessages_reactions &data); void updateDefault(const MTPDmessages_availableReactions &data); void updateGeneric(const MTPDmessages_stickerSet &data); + void updateMyTags(const MTPDmessages_savedReactionTags &data); + void updateTags(const MTPDmessages_reactions &data); void recentUpdated(); void defaultUpdated(); + void myTagsUpdated(); + void tagsUpdated(); [[nodiscard]] std::optional resolveById(const ReactionId &id); [[nodiscard]] std::vector resolveByIds( @@ -167,6 +187,13 @@ private: std::vector _recent; std::vector _recentIds; base::flat_set _unresolvedRecent; + std::vector _myTags; + std::vector _myTagsIds; + std::vector _myTagsInfo; + base::flat_set _unresolvedMyTags; + std::vector _tags; + std::vector _tagsIds; + base::flat_set _unresolvedTags; std::vector _top; std::vector _topIds; base::flat_set _unresolvedTop; @@ -184,6 +211,8 @@ private: rpl::event_stream<> _recentUpdated; rpl::event_stream<> _defaultUpdated; rpl::event_stream<> _favoriteUpdated; + rpl::event_stream<> _myTagsUpdated; + rpl::event_stream<> _tagsUpdated; // We need &i->second stay valid while inserting new items. // So we use std::map instead of base::flat_map here. @@ -203,6 +232,13 @@ private: mtpRequestId _genericRequestId = 0; + mtpRequestId _myTagsRequestId = 0; + bool _myTagsRequestScheduled = false; + uint64 _myTagsHash = 0; + + mtpRequestId _tagsRequestId = 0; + uint64 _tagsHash = 0; + base::flat_map _images; rpl::lifetime _imagesLoadLifetime; bool _waitingForList = false; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp index 91ad93484..2081a85e7 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp @@ -895,7 +895,9 @@ void SetupManagerList( reactions.topUpdates(), reactions.recentUpdates(), reactions.defaultUpdates(), - reactions.favoriteUpdates() + reactions.favoriteUpdates(), + reactions.myTagsUpdates(), + reactions.tagsUpdates() ) | rpl::start_with_next([=] { if (!state->timer.isActive()) { state->timer.callOnce(kRefreshListDelay); diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index 47e1196ea..8da39b10e 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -529,14 +529,22 @@ bool ShowReactPremiumError( || ranges::contains(item->chosenReactions(), id) || item->history()->peer->isBroadcast()) { return false; - } - const auto &list = controller->session().data().reactions().list( - Data::Reactions::Type::Active); - const auto i = ranges::find(list, id, &Data::Reaction::id); - if (i == end(list) || !i->premium) { - if (!id.custom()) { + } else if (item->reactionsAreTags()) { + const auto &list = controller->session().data().reactions().list( + Data::Reactions::Type::Tags); + const auto i = ranges::find(list, id, &Data::Reaction::id); + if (i != end(list)) { return false; } + } else { + const auto &list = controller->session().data().reactions().list( + Data::Reactions::Type::Active); + const auto i = ranges::find(list, id, &Data::Reaction::id); + if (i == end(list) || !i->premium) { + if (!id.custom()) { + return false; + } + } } ShowPremiumPreviewBox(controller, PremiumPreview::InfiniteReactions); return true;