diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 4dbe1dc87..539c55306 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -264,6 +264,7 @@ Reactions::Reactions(not_null owner) kRefreshFullListEach ) | rpl::start_with_next([=] { refreshDefault(); + requestEffects(); }, _lifetime); _owner->session().changes().messageUpdates( @@ -343,6 +344,12 @@ void Reactions::refreshTags() { requestTags(); } +void Reactions::refreshEffects() { + if (_effects.empty()) { + requestEffects(); + } +} + const std::vector &Reactions::list(Type type) const { switch (type) { case Type::Active: return _active; @@ -352,6 +359,7 @@ const std::vector &Reactions::list(Type type) const { case Type::MyTags: return _myTags.find((SavedSublist*)nullptr)->second.tags; case Type::Tags: return _tags; + case Type::Effects: return _effects; } Unexpected("Type in Reactions::list."); } @@ -552,21 +560,45 @@ rpl::producer Reactions::myTagRenamed() const { return _myTagRenamed.events(); } +rpl::producer<> Reactions::effectsUpdates() const { + return _effectsUpdated.events(); +} + +void Reactions::preloadReactionImageFor(const ReactionId &emoji) { + if (!emoji.emoji().isEmpty()) { + preloadImageFor(emoji); + } +} + +void Reactions::preloadEffectImageFor(EffectId id) { + preloadImageFor({ DocumentId(id) }); +} + void Reactions::preloadImageFor(const ReactionId &id) { - if (_images.contains(id) || id.emoji().isEmpty()) { + if (_images.contains(id)) { return; } auto &set = _images.emplace(id).first->second; - const auto i = ranges::find(_available, id, &Reaction::id); - const auto document = (i == end(_available)) + set.effect = (id.custom() != 0); + const auto i = set.effect + ? ranges::find(_effects, id, &Reaction::id) + : ranges::find(_available, id, &Reaction::id); + const auto document = (i == end(set.effect ? _effects : _available)) ? nullptr : i->centerIcon ? i->centerIcon : i->selectAnimation.get(); - if (document) { - loadImage(set, document, !i->centerIcon); - } else if (!_waitingForList) { - _waitingForList = true; + if (document || (set.effect && i != end(_effects))) { + if (!set.effect || i->centerIcon) { + loadImage(set, document, !i->centerIcon); + } else { + generateImage(set, i->title); + } + } else if (set.effect && !_waitingForEffects) { + _waitingForEffects = true; + refreshEffects(); + } else if (!set.effect && !_waitingForReactions) { + _waitingForReactions = true; refreshDefault(); } } @@ -597,14 +629,24 @@ void Reactions::preloadAnimationsFor(const ReactionId &id) { preload(i->aroundAnimation); } -QImage Reactions::resolveImageFor( - const ReactionId &emoji, - ImageSize size) { - const auto i = _images.find(emoji); +QImage Reactions::resolveReactionImageFor(const ReactionId &emoji) { + Expects(!emoji.custom()); + + return resolveImageFor(emoji); +} + +QImage Reactions::resolveEffectImageFor(EffectId id) { + return resolveImageFor({ DocumentId(id) }); +} + +QImage Reactions::resolveImageFor(const ReactionId &id) { + const auto i = _images.find(id); if (i == end(_images)) { - preloadImageFor(emoji); + preloadImageFor(id); } - auto &set = (i != end(_images)) ? i->second : _images[emoji]; + auto &set = (i != end(_images)) ? i->second : _images[id]; + set.effect = (id.custom() != 0); + const auto resolve = [&](QImage &image, int size) { const auto factor = style::DevicePixelRatio(); const auto frameSize = set.fromSelectAnimation @@ -634,21 +676,18 @@ QImage Reactions::resolveImageFor( } image.setDevicePixelRatio(factor); }; - if (set.bottomInfo.isNull() && set.icon) { - resolve(set.bottomInfo, st::reactionInfoImage); - resolve(set.inlineList, st::reactionInlineImage); + if (set.image.isNull() && set.icon) { + resolve( + set.image, + set.effect ? st::effectInfoImage : st::reactionInlineImage); crl::async([icon = std::move(set.icon)]{}); } - switch (size) { - case ImageSize::BottomInfo: return set.bottomInfo; - case ImageSize::InlineList: return set.inlineList; - } - Unexpected("ImageSize in Reactions::resolveImageFor."); + return set.image; } -void Reactions::resolveImages() { +void Reactions::resolveReactionImages() { for (auto &[id, set] : _images) { - if (!set.bottomInfo.isNull() || set.icon || set.media) { + if (set.effect || !set.image.isNull() || set.icon || set.media) { continue; } const auto i = ranges::find(_available, id, &Reaction::id); @@ -666,14 +705,38 @@ void Reactions::resolveImages() { } } +void Reactions::resolveEffectImages() { + for (auto &[id, set] : _images) { + if (!set.effect || !set.image.isNull() || set.icon || set.media) { + continue; + } + const auto i = ranges::find(_effects, id, &Reaction::id); + const auto document = (i == end(_effects)) + ? nullptr + : i->centerIcon + ? i->centerIcon + : nullptr; + if (document) { + loadImage(set, document, false); + } else if (i != end(_effects)) { + generateImage(set, i->title); + } else { + LOG(("API Error: Effect '%1' not found!" + ).arg(ReactionIdToLog(id))); + } + } +} + void Reactions::loadImage( ImageSet &set, not_null document, bool fromSelectAnimation) { - if (!set.bottomInfo.isNull() || set.icon) { + if (!set.image.isNull() || set.icon) { return; } else if (!set.media) { - set.fromSelectAnimation = fromSelectAnimation; + if (!set.effect) { + set.fromSelectAnimation = fromSelectAnimation; + } set.media = document->createMediaView(); set.media->checkStickerLarge(); } @@ -687,6 +750,26 @@ void Reactions::loadImage( } } +void Reactions::generateImage(ImageSet &set, const QString &emoji) { + Expects(set.effect); + + const auto e = Ui::Emoji::Find(emoji); + Assert(e != nullptr); + + const auto large = Ui::Emoji::GetSizeLarge(); + const auto factor = style::DevicePixelRatio(); + auto image = QImage(large, large, QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(factor); + image.fill(Qt::transparent); + { + QPainter p(&image); + Ui::Emoji::Draw(p, e, large, 0, 0); + } + const auto size = st::effectInfoImage; + set.image = image.scaled(size * factor, size * factor); + set.image.setDevicePixelRatio(factor); +} + void Reactions::setAnimatedIcon(ImageSet &set) { const auto size = style::ConvertScale(kSizeForDownscale); set.icon = Ui::MakeAnimatedIcon({ @@ -840,6 +923,25 @@ void Reactions::requestTags() { } +void Reactions::requestEffects() { + if (_effectsRequestId) { + return; + } + auto &api = _owner->session().api(); + _effectsRequestId = api.request(MTPmessages_GetAvailableEffects( + MTP_int(_effectsHash) + )).done([=](const MTPmessages_AvailableEffects &result) { + _effectsRequestId = 0; + result.match([&](const MTPDmessages_availableEffects &data) { + updateEffects(data); + }, [&](const MTPDmessages_availableEffectsNotModified &) { + }); + }).fail([=] { + _effectsRequestId = 0; + _effectsHash = 0; + }).send(); +} + void Reactions::updateTop(const MTPDmessages_reactions &data) { _topHash = data.vhash().v; _topIds = ListFromMTP(data); @@ -881,9 +983,9 @@ void Reactions::updateDefault(const MTPDmessages_availableReactions &data) { } } } - if (_waitingForList) { - _waitingForList = false; - resolveImages(); + if (_waitingForReactions) { + _waitingForReactions = false; + resolveReactionImages(); } defaultUpdated(); } @@ -939,6 +1041,32 @@ void Reactions::updateTags(const MTPDmessages_reactions &data) { _tagsUpdated.fire({}); } +void Reactions::updateEffects(const MTPDmessages_availableEffects &data) { + _effectsHash = data.vhash().v; + + const auto &list = data.veffects().v; + const auto toCache = [&](DocumentData *document) { + if (document) { + _iconsCache.emplace(document, document->createMediaView()); + } + }; + for (const auto &document : data.vdocuments().v) { + toCache(_owner->processDocument(document)); + } + _effects.clear(); + _effects.reserve(list.size()); + for (const auto &effect : list) { + if (const auto parsed = parse(effect)) { + _effects.push_back(*parsed); + } + } + if (_waitingForEffects) { + _waitingForEffects = false; + resolveEffectImages(); + } + effectsUpdated(); +} + void Reactions::recentUpdated() { _topRefreshTimer.callOnce(kTopRequestDelay); _recentUpdated.fire({}); @@ -969,6 +1097,10 @@ void Reactions::tagsUpdated() { _tagsUpdated.fire({}); } +void Reactions::effectsUpdated() { + _effectsUpdated.fire({}); +} + not_null Reactions::resolveListener() { return static_cast(this); } @@ -1111,35 +1243,73 @@ void Reactions::resolve(const ReactionId &id) { } std::optional Reactions::parse(const MTPAvailableReaction &entry) { - return entry.match([&](const MTPDavailableReaction &data) { - const auto emoji = qs(data.vreaction()); - const auto known = (Ui::Emoji::Find(emoji) != nullptr); - if (!known) { - LOG(("API Error: Unknown emoji in reactions: %1").arg(emoji)); - } - return known - ? std::make_optional(Reaction{ - .id = ReactionId{ emoji }, - .title = qs(data.vtitle()), - //.staticIcon = _owner->processDocument(data.vstatic_icon()), - .appearAnimation = _owner->processDocument( - data.vappear_animation()), - .selectAnimation = _owner->processDocument( - data.vselect_animation()), - //.activateAnimation = _owner->processDocument( - // data.vactivate_animation()), - //.activateEffects = _owner->processDocument( - // data.veffect_animation()), - .centerIcon = (data.vcenter_icon() - ? _owner->processDocument(*data.vcenter_icon()).get() - : nullptr), - .aroundAnimation = (data.varound_animation() - ? _owner->processDocument( - *data.varound_animation()).get() - : nullptr), - .active = !data.is_inactive(), - }) - : std::nullopt; + const auto &data = entry.data(); + const auto emoji = qs(data.vreaction()); + const auto known = (Ui::Emoji::Find(emoji) != nullptr); + if (!known) { + LOG(("API Error: Unknown emoji in reactions: %1").arg(emoji)); + return std::nullopt; + } + return std::make_optional(Reaction{ + .id = ReactionId{ emoji }, + .title = qs(data.vtitle()), + //.staticIcon = _owner->processDocument(data.vstatic_icon()), + .appearAnimation = _owner->processDocument( + data.vappear_animation()), + .selectAnimation = _owner->processDocument( + data.vselect_animation()), + //.activateAnimation = _owner->processDocument( + // data.vactivate_animation()), + //.activateEffects = _owner->processDocument( + // data.veffect_animation()), + .centerIcon = (data.vcenter_icon() + ? _owner->processDocument(*data.vcenter_icon()).get() + : nullptr), + .aroundAnimation = (data.varound_animation() + ? _owner->processDocument(*data.varound_animation()).get() + : nullptr), + .active = !data.is_inactive(), + }); +} + +std::optional Reactions::parse(const MTPAvailableEffect &entry) { + const auto &data = entry.data(); + const auto emoji = qs(data.vemoticon()); + const auto known = (Ui::Emoji::Find(emoji) != nullptr); + if (!known) { + LOG(("API Error: Unknown emoji in effects: %1").arg(emoji)); + return std::nullopt; + } + const auto id = DocumentId(data.vid().v); + const auto document = _owner->document(id); + if (!document->sticker()) { + LOG(("API Error: Bad sticker in effects: %1").arg(id)); + return std::nullopt; + } + const auto aroundId = data.veffect_animation_id().value_or_empty(); + const auto around = aroundId + ? _owner->document(aroundId).get() + : nullptr; + if (around && !around->sticker()) { + LOG(("API Error: Bad sticker in effects around: %1").arg(aroundId)); + return std::nullopt; + } + const auto iconId = data.vstatic_icon_id().value_or_empty(); + const auto icon = iconId ? _owner->document(iconId).get() : nullptr; + if (icon && !icon->sticker()) { + LOG(("API Error: Bad sticker in effects icon: %1").arg(iconId)); + return std::nullopt; + } + return std::make_optional(Reaction{ + .id = ReactionId{ id }, + .title = emoji, + .appearAnimation = document, + .selectAnimation = document, + .centerIcon = icon, + .aroundAnimation = around, + .active = true, + .effect = true, + .premium = data.is_premium_required(), }); } diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 9d67e2e2c..755b1c4d2 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -37,6 +37,8 @@ struct Reaction { DocumentData *aroundAnimation = nullptr; int count = 0; bool active = false; + bool effect = false; + bool premium = false; }; struct PossibleItemReactionsRef { @@ -80,6 +82,7 @@ public: void refreshMyTags(SavedSublist *sublist = nullptr); void refreshMyTagsDelayed(); void refreshTags(); + void refreshEffects(); enum class Type { Active, @@ -88,6 +91,7 @@ public: All, MyTags, Tags, + Effects, }; [[nodiscard]] const std::vector &list(Type type) const; [[nodiscard]] const std::vector &myTagsInfo() const; @@ -108,16 +112,15 @@ public: [[nodiscard]] rpl::producer<> myTagsUpdates() const; [[nodiscard]] rpl::producer<> tagsUpdates() const; [[nodiscard]] rpl::producer myTagRenamed() const; + [[nodiscard]] rpl::producer<> effectsUpdates() const; + + void preloadReactionImageFor(const ReactionId &emoji); + [[nodiscard]] QImage resolveReactionImageFor(const ReactionId &emoji); + + void preloadEffectImageFor(EffectId id); + [[nodiscard]] QImage resolveEffectImageFor(EffectId id); - enum class ImageSize { - BottomInfo, - InlineList, - }; - void preloadImageFor(const ReactionId &emoji); void preloadAnimationsFor(const ReactionId &emoji); - [[nodiscard]] QImage resolveImageFor( - const ReactionId &emoji, - ImageSize size); void send(not_null item, bool addToRecent); [[nodiscard]] bool sending(not_null item) const; @@ -139,11 +142,11 @@ public: private: struct ImageSet { - QImage bottomInfo; - QImage inlineList; + QImage image; std::shared_ptr media; std::unique_ptr icon; bool fromSelectAnimation = false; + bool effect = false; }; struct TagsBySublist { TagsBySublist() = default; @@ -169,6 +172,7 @@ private: void requestGeneric(); void requestMyTags(SavedSublist *sublist = nullptr); void requestTags(); + void requestEffects(); void updateTop(const MTPDmessages_reactions &data); void updateRecent(const MTPDmessages_reactions &data); @@ -178,11 +182,13 @@ private: SavedSublist *sublist, const MTPDmessages_savedReactionTags &data); void updateTags(const MTPDmessages_reactions &data); + void updateEffects(const MTPDmessages_availableEffects &data); void recentUpdated(); void defaultUpdated(); void myTagsUpdated(); void tagsUpdated(); + void effectsUpdated(); [[nodiscard]] std::optional resolveById(const ReactionId &id); [[nodiscard]] std::vector resolveByIds( @@ -203,13 +209,19 @@ private: [[nodiscard]] std::optional parse( const MTPAvailableReaction &entry); + [[nodiscard]] std::optional parse( + const MTPAvailableEffect &entry); + void preloadImageFor(const ReactionId &id); + [[nodiscard]] QImage resolveImageFor(const ReactionId &id); void loadImage( ImageSet &set, not_null document, bool fromSelectAnimation); + void generateImage(ImageSet &set, const QString &emoji); void setAnimatedIcon(ImageSet &set); - void resolveImages(); + void resolveReactionImages(); + void resolveEffectImages(); void downloadTaskFinished(); void repaintCollected(); @@ -233,6 +245,7 @@ private: std::vector _topIds; base::flat_set _unresolvedTop; std::vector> _genericAnimations; + std::vector _effects; ReactionId _favoriteId; ReactionId _unresolvedFavoriteId; std::optional _favorite; @@ -249,6 +262,7 @@ private: rpl::event_stream _myTagsUpdated; rpl::event_stream<> _tagsUpdated; rpl::event_stream _myTagRenamed; + rpl::event_stream<> _effectsUpdated; // We need &i->second stay valid while inserting new items. // So we use std::map instead of base::flat_map here. @@ -271,9 +285,13 @@ private: mtpRequestId _tagsRequestId = 0; uint64 _tagsHash = 0; + mtpRequestId _effectsRequestId = 0; + int32 _effectsHash = 0; + base::flat_map _images; rpl::lifetime _imagesLoadLifetime; - bool _waitingForList = false; + bool _waitingForReactions = false; + bool _waitingForEffects = false; base::flat_map _sentRequests; diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp index 276d4e5b6..c7e85c94e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp @@ -168,7 +168,7 @@ void SearchTags::fill( .selected = ranges::contains(selected, id), }); if (!customId) { - _owner->reactions().preloadImageFor(id); + _owner->reactions().preloadReactionImageFor(id); } }; if (!premium) { @@ -335,9 +335,7 @@ void SearchTags::paint( paintBackground(p, geometry, tag); paintText(p, geometry, tag); if (!tag.custom && !tag.promo && tag.image.isNull()) { - tag.image = _owner->reactions().resolveImageFor( - tag.id, - ::Data::Reactions::ImageSize::InlineList); + tag.image = _owner->reactions().resolveReactionImageFor(tag.id); } const auto inner = geometry.marginsRemoved(padding); const auto image = QRect( diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 4326bed57..b65173f00 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -365,6 +365,7 @@ HistoryItem::HistoryItem( .from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0), .date = data.vdate().v, .shortcutId = data.vquick_reply_shortcut_id().value_or_empty(), + .effectId = data.veffect().value_or_empty(), }) { _boostsApplied = data.vfrom_boosts_applied().value_or_empty(); @@ -681,7 +682,8 @@ HistoryItem::HistoryItem( : history->peer) , _flags(FinalizeMessageFlags(history, fields.flags)) , _date(fields.date) -, _shortcutId(fields.shortcutId) { +, _shortcutId(fields.shortcutId) +, _effectId(fields.effectId) { if (isHistoryEntry() && IsClientMsgId(id)) { _history->registerClientSideMessage(this); } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 029da2623..ce14ce620 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -74,6 +74,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_histories.h" #include "data/data_group_call.h" +#include "data/data_message_reactions.h" #include "data/data_peer_values.h" // Data::AmPremiumValue. #include "data/data_premium_limits.h" // Data::PremiumLimits. #include "data/stickers/data_stickers.h" @@ -1398,7 +1399,7 @@ int HistoryWidget::itemTopForHighlight( if (heightLeft >= 0) { return std::max(itemTop - (heightLeft / 2), 0); } else if (reactionCenter >= 0) { - const auto maxSize = st::reactionInfoImage; + const auto maxSize = st::reactionInlineImage; // Show message right till the bottom. const auto forBottom = itemTop + viewHeight - visibleAreaHeight; @@ -2375,6 +2376,8 @@ void HistoryWidget::showHistory( } } + session().data().reactions().refreshEffects(); + _scroll->hide(); _list = _scroll->setOwnedWidget( object_ptr(this, _scroll, controller(), _history)); diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index a4000efe1..db8f49a27 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -30,14 +30,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { -struct BottomInfo::Reaction { +struct BottomInfo::Effect { mutable std::unique_ptr animation; mutable QImage image; - ReactionId id; - QString countText; - int count = 0; - int countTextWidth = 0; - bool chosen = false; + EffectId id = 0; }; BottomInfo::BottomInfo( @@ -58,17 +54,11 @@ void BottomInfo::update(Data &&data, int availableWidth) { } } -int BottomInfo::countReactionsMaxWidth() const { +int BottomInfo::countEffectMaxWidth() const { auto result = 0; - for (const auto &reaction : _reactions) { + if (_effect) { result += st::reactionInfoSize; - if (reaction.countTextWidth > 0) { - result += st::reactionInfoSkip - + reaction.countTextWidth - + st::reactionInfoDigitSkip; - } else { - result += st::reactionInfoBetween; - } + result += st::reactionInfoBetween; } if (result) { result += (st::reactionInfoSkip - st::reactionInfoBetween); @@ -76,19 +66,14 @@ int BottomInfo::countReactionsMaxWidth() const { return result; } -int BottomInfo::countReactionsHeight(int newWidth) const { +int BottomInfo::countEffectHeight(int newWidth) const { const auto left = 0; auto x = 0; auto y = 0; auto widthLeft = newWidth; - for (const auto &reaction : _reactions) { - const auto add = (reaction.countTextWidth > 0) - ? st::reactionInfoDigitSkip - : st::reactionInfoBetween; - const auto width = st::reactionInfoSize - + (reaction.countTextWidth > 0 - ? (st::reactionInfoSkip + reaction.countTextWidth) - : 0); + if (_effect) { + const auto add = st::reactionInfoBetween; + const auto width = st::reactionInfoSize; if (x > left && widthLeft < width) { x = left; y += st::msgDateFont->height; @@ -107,7 +92,7 @@ int BottomInfo::firstLineWidth() const { if (height() == minHeight()) { return width(); } - return maxWidth() - _reactionsMaxWidth; + return maxWidth() - _effectMaxWidth; } bool BottomInfo::isWide() const { @@ -115,14 +100,14 @@ bool BottomInfo::isWide() const { || !_data.author.isEmpty() || !_views.isEmpty() || !_replies.isEmpty() - || !_reactions.empty(); + || _effect; } TextState BottomInfo::textState( not_null item, QPoint position) const { auto result = TextState(item); - if (const auto link = revokeReactionLink(item, position)) { + if (const auto link = replayEffectLink(item, position)) { result.link = link; return result; } @@ -172,32 +157,26 @@ TextState BottomInfo::textState( return result; } -ClickHandlerPtr BottomInfo::revokeReactionLink( +ClickHandlerPtr BottomInfo::replayEffectLink( not_null item, QPoint position) const { - if (_reactions.empty()) { + if (!_effect) { return nullptr; } auto left = 0; auto top = 0; auto available = width(); if (height() != minHeight()) { - available = std::min(available, _reactionsMaxWidth); + available = std::min(available, _effectMaxWidth); left += width() - available; top += st::msgDateFont->height; } auto x = left; auto y = top; auto widthLeft = available; - for (const auto &reaction : _reactions) { - const auto chosen = reaction.chosen; - const auto add = (reaction.countTextWidth > 0) - ? st::reactionInfoDigitSkip - : st::reactionInfoBetween; - const auto width = st::reactionInfoSize - + (reaction.countTextWidth > 0 - ? (st::reactionInfoSkip + reaction.countTextWidth) - : 0); + if (_effect) { + const auto add = st::reactionInfoBetween; + const auto width = st::reactionInfoSize; if (x > left && widthLeft < width) { x = left; y += st::msgDateFont->height; @@ -208,11 +187,11 @@ ClickHandlerPtr BottomInfo::revokeReactionLink( y, st::reactionInfoSize, st::msgDateFont->height); - if (chosen && image.contains(position)) { - if (!_revokeLink) { - _revokeLink = revokeReactionLink(item); + if (image.contains(position)) { + if (!_replayLink) { + _replayLink = replayEffectLink(item); } - return _revokeLink; + return _replayLink; } x += width + add; widthLeft -= width + add; @@ -220,25 +199,16 @@ ClickHandlerPtr BottomInfo::revokeReactionLink( return nullptr; } -ClickHandlerPtr BottomInfo::revokeReactionLink( +ClickHandlerPtr BottomInfo::replayEffectLink( not_null item) const { const auto itemId = item->fullId(); const auto sessionId = item->history()->session().uniqueId(); return std::make_shared([=]( - ClickContext context) { + ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { - if (controller->session().uniqueId() == sessionId) { - auto &owner = controller->session().data(); - if (const auto item = owner.message(itemId)) { - const auto chosen = item->chosenReactions(); - if (!chosen.empty()) { - item->toggleReaction( - chosen.front(), - HistoryItem::ReactionSource::Existing); - } - } - } + controller->showToast("playing nice effect.."); + AssertIsDebug(); } }); } @@ -340,20 +310,20 @@ void BottomInfo::paint( firstLineBottom + st::historyViewsTop, outerWidth); } - if (!_reactions.empty()) { + if (_effect) { auto left = position.x(); auto top = position.y(); auto available = width(); if (height() != minHeight()) { - available = std::min(available, _reactionsMaxWidth); + available = std::min(available, _effectMaxWidth); left += width() - available; top += st::msgDateFont->height; } - paintReactions(p, position, left, top, available, context); + paintEffect(p, position, left, top, available, context); } } -void BottomInfo::paintReactions( +void BottomInfo::paintEffect( Painter &p, QPoint origin, int left, @@ -369,52 +339,33 @@ void BottomInfo::paintReactions( auto x = left; auto y = top; auto widthLeft = availableWidth; - for (const auto &reaction : _reactions) { - if (context.reactionInfo - && reaction.animation - && reaction.animation->finished()) { - reaction.animation = nullptr; - } - const auto animating = (reaction.animation != nullptr); - const auto add = (reaction.countTextWidth > 0) - ? st::reactionInfoDigitSkip - : st::reactionInfoBetween; - const auto width = st::reactionInfoSize - + (reaction.countTextWidth > 0 - ? (st::reactionInfoSkip + reaction.countTextWidth) - : 0); + if (_effect) { + const auto animating = (_effect->animation != nullptr); + const auto add = st::reactionInfoBetween; + const auto width = st::reactionInfoSize; if (x > left && widthLeft < width) { x = left; y += st::msgDateFont->height; widthLeft = availableWidth; } - if (reaction.image.isNull()) { - reaction.image = _reactionsOwner->resolveImageFor( - reaction.id, - ::Data::Reactions::ImageSize::BottomInfo); + if (_effect->image.isNull()) { + _effect->image = _reactionsOwner->resolveEffectImageFor( + _effect->id); } const auto image = QRect( - x + (st::reactionInfoSize - st::reactionInfoImage) / 2, - y + (st::msgDateFont->height - st::reactionInfoImage) / 2, - st::reactionInfoImage, - st::reactionInfoImage); - const auto skipImage = animating - && (reaction.count < 2 || !reaction.animation->flying()); - if (!reaction.image.isNull() && !skipImage) { - p.drawImage(image.topLeft(), reaction.image); + x + (st::reactionInfoSize - st::effectInfoImage) / 2, + y + (st::msgDateFont->height - st::effectInfoImage) / 2, + st::effectInfoImage, + st::effectInfoImage); + if (!_effect->image.isNull()) { + p.drawImage(image.topLeft(), _effect->image); } if (animating) { animations.push_back({ - .animation = reaction.animation.get(), + .animation = _effect->animation.get(), .target = image, }); } - if (reaction.countTextWidth > 0) { - p.drawText( - x + st::reactionInfoSize + st::reactionInfoSkip, - y + st::msgDateFont->ascent, - reaction.countText); - } x += width + add; widthLeft -= width + add; } @@ -448,18 +399,18 @@ QSize BottomInfo::countCurrentSize(int newWidth) { const auto dateHeight = (_data.flags & Data::Flag::Sponsored) ? 0 : st::msgDateFont->height; - const auto noReactionsWidth = maxWidth() - _reactionsMaxWidth; - accumulate_min(newWidth, std::max(noReactionsWidth, _reactionsMaxWidth)); + const auto noReactionsWidth = maxWidth() - _effectMaxWidth; + accumulate_min(newWidth, std::max(noReactionsWidth, _effectMaxWidth)); return QSize( newWidth, - dateHeight + countReactionsHeight(newWidth)); + dateHeight + countEffectHeight(newWidth)); } void BottomInfo::layout() { layoutDateText(); layoutViewsText(); layoutRepliesText(); - layoutReactionsText(); + layoutEffectText(); initDimensions(); } @@ -520,33 +471,12 @@ void BottomInfo::layoutRepliesText() { Ui::NameTextOptions()); } -void BottomInfo::layoutReactionsText() { - if (_data.reactions.empty()) { - _reactions.clear(); +void BottomInfo::layoutEffectText() { + if (!_data.effectId) { + _effect = nullptr; return; } - auto sorted = ranges::views::all( - _data.reactions - ) | ranges::views::transform([](const MessageReaction &reaction) { - return not_null{ &reaction }; - }) | ranges::to_vector; - ranges::sort( - sorted, - std::greater<>(), - &MessageReaction::count); - - auto reactions = std::vector(); - reactions.reserve(sorted.size()); - 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)); - reactions.back().chosen = reaction->my; - setReactionCount(reactions.back(), reaction->count); - } - _reactions = std::move(reactions); + _effect = std::make_unique(prepareEffectWithId(_data.effectId)); } QSize BottomInfo::countOptimalSize() { @@ -571,69 +501,42 @@ QSize BottomInfo::countOptimalSize() { if (_data.flags & Data::Flag::Pinned) { width += st::historyPinWidth; } - _reactionsMaxWidth = countReactionsMaxWidth(); - width += _reactionsMaxWidth; + _effectMaxWidth = countEffectMaxWidth(); + width += _effectMaxWidth; const auto dateHeight = (_data.flags & Data::Flag::Sponsored) ? 0 : st::msgDateFont->height; return QSize(width, dateHeight); } -BottomInfo::Reaction BottomInfo::prepareReactionWithId( - const ReactionId &id) { - auto result = Reaction{ .id = id }; - _reactionsOwner->preloadImageFor(id); +BottomInfo::Effect BottomInfo::prepareEffectWithId(EffectId id) { + auto result = Effect{ .id = id }; + _reactionsOwner->preloadEffectImageFor(id); return result; } -void BottomInfo::setReactionCount(Reaction &reaction, int count) { - if (reaction.count == count) { - return; - } - reaction.count = count; - reaction.countText = (count > 1) - ? Lang::FormatCountToShort(count).string - : QString(); - reaction.countTextWidth = (count > 1) - ? st::msgDateFont->width(reaction.countText) - : 0; -} - -void BottomInfo::animateReaction( - Ui::ReactionFlyAnimationArgs &&args, +void BottomInfo::animateEffect( + Ui::ReactionFlyAnimationArgs &&args, Fn repaint) { - const auto i = ranges::find(_reactions, args.id, &Reaction::id); - if (i == end(_reactions)) { + if (!_effect || args.id.custom() != _effect->id) { return; } - i->animation = std::make_unique( + _effect->animation = std::make_unique( _reactionsOwner, args.translated(QPoint(width(), height())), std::move(repaint), - st::reactionInfoImage); + st::effectInfoImage); } -auto BottomInfo::takeReactionAnimations() --> base::flat_map> { - auto result = base::flat_map< - ReactionId, - std::unique_ptr>(); - for (auto &reaction : _reactions) { - if (reaction.animation) { - result.emplace(reaction.id, std::move(reaction.animation)); - } - } - return result; +auto BottomInfo::takeEffectAnimation() +-> std::unique_ptr { + return _effect ? std::move(_effect->animation) : nullptr; } -void BottomInfo::continueReactionAnimations(base::flat_map< - ReactionId, - std::unique_ptr> animations) { - for (auto &[id, animation] : animations) { - const auto i = ranges::find(_reactions, id, &Reaction::id); - if (i != end(_reactions)) { - i->animation = std::move(animation); - } +void BottomInfo::continueEffectAnimation( + std::unique_ptr animation) { + if (_effect) { + _effect->animation = std::move(animation); } } @@ -643,9 +546,7 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null message) { auto result = BottomInfo::Data(); result.date = message->dateTime(); - if (message->embedReactionsInBottomInfo()) { - result.reactions = item->reactions(); - } + result.effectId = item->effectId(); 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 efdab3334..b4cc26f51 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -20,8 +20,6 @@ class ReactionFlyAnimation; namespace Data { class Reactions; -struct ReactionId; -struct MessageReaction; } // namespace Data namespace HistoryView { @@ -33,8 +31,6 @@ struct TextState; class BottomInfo final : public Object { public: - using ReactionId = ::Data::ReactionId; - using MessageReaction = ::Data::MessageReaction; struct Data { enum class Flag : uchar { Edited = 0x01, @@ -52,7 +48,7 @@ public: QDateTime date; QString author; - std::vector reactions; + EffectId effectId = 0; std::optional views; std::optional replies; std::optional forwardsCount; @@ -78,29 +74,26 @@ public: bool inverted, const PaintContext &context) const; - void animateReaction( + void animateEffect( Ui::ReactionFlyAnimationArgs &&args, Fn repaint); - [[nodiscard]] auto takeReactionAnimations() - -> base::flat_map< - ReactionId, - std::unique_ptr>; - void continueReactionAnimations(base::flat_map< - ReactionId, - std::unique_ptr> animations); + [[nodiscard]] auto takeEffectAnimation() + -> std::unique_ptr; + void continueEffectAnimation( + std::unique_ptr animation); private: - struct Reaction; + struct Effect; void layout(); void layoutDateText(); void layoutViewsText(); void layoutRepliesText(); - void layoutReactionsText(); + void layoutEffectText(); - [[nodiscard]] int countReactionsMaxWidth() const; - [[nodiscard]] int countReactionsHeight(int newWidth) const; - void paintReactions( + [[nodiscard]] int countEffectMaxWidth() const; + [[nodiscard]] int countEffectHeight(int newWidth) const; + void paintEffect( Painter &p, QPoint origin, int left, @@ -111,13 +104,11 @@ private: QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; - void setReactionCount(Reaction &reaction, int count); - [[nodiscard]] Reaction prepareReactionWithId( - const ReactionId &id); - [[nodiscard]] ClickHandlerPtr revokeReactionLink( + [[nodiscard]] Effect prepareEffectWithId(EffectId id); + [[nodiscard]] ClickHandlerPtr replayEffectLink( not_null item, QPoint position) const; - [[nodiscard]] ClickHandlerPtr revokeReactionLink( + [[nodiscard]] ClickHandlerPtr replayEffectLink( not_null item) const; const not_null<::Data::Reactions*> _reactionsOwner; @@ -125,9 +116,9 @@ private: Ui::Text::String _authorEditedDate; Ui::Text::String _views; Ui::Text::String _replies; - std::vector _reactions; - mutable ClickHandlerPtr _revokeLink; - int _reactionsMaxWidth = 0; + std::unique_ptr _effect; + mutable ClickHandlerPtr _replayLink; + int _effectMaxWidth = 0; bool _authorElided = false; }; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 559a98ccd..88d9ad183 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1038,7 +1038,7 @@ void EditTagBox( customId, [=] { field->update(); }); } else { - owner->reactions().preloadImageFor(id); + owner->reactions().preloadReactionImageFor(id); } field->paintRequest() | rpl::start_with_next([=](QRect clip) { auto p = QPainter(field); @@ -1053,9 +1053,8 @@ void EditTagBox( }); } else { if (state->image.isNull()) { - state->image = owner->reactions().resolveImageFor( - id, - ::Data::Reactions::ImageSize::InlineList); + state->image = owner->reactions().resolveReactionImageFor( + id); } if (!state->image.isNull()) { const auto size = st::reactionInlineSize; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index e687ec921..79fc506c1 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -1770,6 +1770,17 @@ auto Element::takeReactionAnimations() return {}; } +void Element::animateEffect(Ui::ReactionFlyAnimationArgs &&args) { +} + +void Element::animateUnreadEffect() { +} + +auto Element::takeEffectAnimation() +-> std::unique_ptr { + return nullptr; +} + Element::~Element() { // Delete media while owner still exists. clearSpecialOnlyEmoji(); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index a11d0831d..4316806ce 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -545,6 +545,11 @@ public: Data::ReactionId, std::unique_ptr>; + virtual void animateEffect(Ui::ReactionFlyAnimationArgs &&args); + void animateUnreadEffect(); + [[nodiscard]] virtual auto takeEffectAnimation() + -> std::unique_ptr; + void overrideMedia(std::unique_ptr media); virtual bool consumeHorizontalScroll(QPoint position, int delta) { diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index beaae32f1..ea17702ee 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -423,6 +423,7 @@ Message::Message( : base::flat_map< Data::ReactionId, std::unique_ptr>(); + auto animation = replacing ? replacing->takeEffectAnimation() : nullptr; if (!animations.empty()) { const auto repainter = [=] { repaint(); }; for (const auto &[id, animation] : animations) { @@ -430,10 +431,11 @@ Message::Message( } if (_reactions) { _reactions->continueAnimations(std::move(animations)); - } else { - _bottomInfo.continueReactionAnimations(std::move(animations)); } } + if (animation) { + _bottomInfo.continueEffectAnimation(std::move(animation)); + } if (data->isSponsored()) { const auto &session = data->history()->session(); const auto details = session.sponsoredMessages().lookupDetails( @@ -582,9 +584,6 @@ void Message::animateReaction(Ui::ReactionFlyAnimationArgs &&args) { return; } - const auto animateInBottomInfo = [&](QPoint bottomRight) { - _bottomInfo.animateReaction(args.translated(-bottomRight), repainter); - }; if (bubble) { auto entry = logEntryOriginal(); @@ -609,6 +608,50 @@ void Message::animateReaction(Ui::ReactionFlyAnimationArgs &&args) { _reactions->animate(args.translated(-reactionsPosition), repainter); return; } + } +} + +void Message::animateEffect(Ui::ReactionFlyAnimationArgs &&args) { + const auto item = data(); + const auto media = this->media(); + + auto g = countGeometry(); + if (g.width() < 1 || isHidden()) { + return; + } + const auto repainter = [=] { repaint(); }; + + const auto bubble = drawBubble(); + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto keyboard = item->inlineReplyKeyboard(); + auto keyboardHeight = 0; + if (keyboard) { + keyboardHeight = keyboard->naturalHeight(); + g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); + } + + const auto animateInBottomInfo = [&](QPoint bottomRight) { + _bottomInfo.animateEffect(args.translated(-bottomRight), repainter); + }; + if (bubble) { + auto entry = logEntryOriginal(); + + // Entry page is always a bubble bottom. + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); + + auto inner = g; + if (_comments) { + inner.setHeight(inner.height() - st::historyCommentsButtonHeight); + } + auto trect = inner.marginsRemoved(st::msgPadding); + const auto reactionsTop = (reactionsInBubble && !_viewButton) + ? st::mediaInBubbleSkip + : 0; + const auto reactionsHeight = reactionsInBubble + ? (reactionsTop + _reactions->height()) + : 0; if (_viewButton) { const auto belowInfo = _viewButton->belowMessageInfo(); const auto infoHeight = reactionsInBubble @@ -653,9 +696,15 @@ auto Message::takeReactionAnimations() -> base::flat_map< Data::ReactionId, std::unique_ptr> { - return _reactions - ? _reactions->takeAnimations() - : _bottomInfo.takeReactionAnimations(); + if (_reactions) { + return _reactions->takeAnimations(); + } + return {}; +} + +auto Message::takeEffectAnimation() +-> std::unique_ptr { + return _bottomInfo.takeEffectAnimation(); } QSize Message::performCountOptimalSize() { diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 62ab6c02b..77c7c5fad 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -158,6 +158,10 @@ public: Data::ReactionId, std::unique_ptr> override; + void animateEffect(Ui::ReactionFlyAnimationArgs &&args) override; + auto takeEffectAnimation() + -> std::unique_ptr override; + QRect innerGeometry() const override; [[nodiscard]] BottomRippleMask bottomRippleMask(int buttonHeight) const; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index 328a8749a..3a79d50b7 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -182,7 +182,7 @@ InlineList::Button InlineList::prepareButtonWithId(const ReactionId &id) { customId, _customEmojiRepaint); } else { - _owner->preloadImageFor(id); + _owner->preloadReactionImageFor(id); } return result; } @@ -439,9 +439,7 @@ void InlineList::paint( } } if (!button.custom && button.image.isNull()) { - button.image = _owner->resolveImageFor( - button.id, - ::Data::Reactions::ImageSize::InlineList); + button.image = _owner->resolveReactionImageFor(button.id); } const auto textFg = !inbubble diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 91ba60a37..302c5b593 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -904,6 +904,8 @@ reactionMainAppearShift: 20px; reactionCollapseFadeThreshold: 40px; reactionFlyUp: 50px; +effectInfoImage: 12px; + searchInChatMultiSelectItem: MultiSelectItem(defaultMultiSelectItem) { maxWidth: 200px; }