From a19e71324bdac60b0c74a08486887ca9721cea17 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 7 May 2024 14:58:03 +0400 Subject: [PATCH] Implement basic effect animation. --- .../data/data_message_reactions.cpp | 13 ++ .../SourceFiles/data/data_message_reactions.h | 1 + Telegram/SourceFiles/data/data_types.h | 2 + .../admin_log/history_admin_log_inner.cpp | 5 + .../admin_log/history_admin_log_inner.h | 3 + .../history/history_inner_widget.cpp | 23 +++- .../history/history_inner_widget.h | 3 + Telegram/SourceFiles/history/history_item.cpp | 18 +++ Telegram/SourceFiles/history/history_item.h | 2 + .../SourceFiles/history/history_widget.cpp | 7 +- .../history/view/history_view_bottom_info.cpp | 22 +-- .../history/view/history_view_bottom_info.h | 6 +- .../history/view/history_view_element.cpp | 5 + .../history/view/history_view_element.h | 6 + .../view/history_view_emoji_interactions.cpp | 126 +++++++++++++++++- .../view/history_view_emoji_interactions.h | 24 ++++ .../history/view/history_view_list_widget.cpp | 6 + .../history/view/history_view_list_widget.h | 3 + .../history/view/history_view_message.cpp | 2 +- 19 files changed, 258 insertions(+), 19 deletions(-) diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 539c55306..8682ba655 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_file_origin.h" #include "data/data_peer_values.h" #include "data/data_saved_sublist.h" #include "data/stickers/data_custom_emoji.h" @@ -594,6 +595,9 @@ void Reactions::preloadImageFor(const ReactionId &id) { } else { generateImage(set, i->title); } + if (set.effect) { + preloadEffect(*i); + } } else if (set.effect && !_waitingForEffects) { _waitingForEffects = true; refreshEffects(); @@ -603,6 +607,15 @@ void Reactions::preloadImageFor(const ReactionId &id) { } } +void Reactions::preloadEffect(const Reaction &effect) { + if (effect.aroundAnimation) { + effect.aroundAnimation->createMediaView()->checkStickerLarge(); + } else { + const auto premium = effect.selectAnimation; + premium->loadVideoThumbnail(premium->stickerSetOrigin()); + } +} + void Reactions::preloadAnimationsFor(const ReactionId &id) { const auto custom = id.custom(); const auto document = custom ? _owner->document(custom).get() : nullptr; diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 755b1c4d2..a1254a6d7 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -212,6 +212,7 @@ private: [[nodiscard]] std::optional parse( const MTPAvailableEffect &entry); + void preloadEffect(const Reaction &effect); void preloadImageFor(const ReactionId &id); [[nodiscard]] QImage resolveImageFor(const ReactionId &id); void loadImage( diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index acc5c8915..4d0e5b3d7 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -321,6 +321,8 @@ enum class MessageFlag : uint64 { ReactionsAreTags = (1ULL << 43), ShortcutMessage = (1ULL << 44), + + EffectWatchedLocal = (1ULL << 45), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index b7bb4b9e0..61cf9a889 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -674,6 +674,11 @@ void InnerWidget::elementStartPremium( void InnerWidget::elementCancelPremium(not_null view) { } +void InnerWidget::elementStartEffect( + not_null view, + Element *replacing) { +} + QString InnerWidget::elementAuthorRank(not_null view) { return {}; } diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index f47afddfa..bbe3c0381 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -136,6 +136,9 @@ public: HistoryView::Element *replacing) override; void elementCancelPremium( not_null view) override; + void elementStartEffect( + not_null view, + HistoryView::Element *replacing) override; QString elementAuthorRank( not_null view) override; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 9809f9c89..aed2480cc 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -304,12 +304,18 @@ public: _widget->elementStartPremium(view, replacing); } } - void elementCancelPremium(not_null view) override { if (_widget) { _widget->elementCancelPremium(view); } } + void elementStartEffect( + not_null view, + Element *replacing) override { + if (_widget) { + _widget->elementStartEffect(view, replacing); + } + } QString elementAuthorRank(not_null view) override { return {}; @@ -950,6 +956,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { _translateTracker->startBunch(); auto readTill = (HistoryItem*)nullptr; auto readContents = base::flat_set>(); + auto startEffects = base::flat_set>(); const auto markingAsViewed = _widget->markingContentsRead(); const auto guard = gsl::finally([&] { if (_pinnedItem) { @@ -958,6 +965,11 @@ void HistoryInner::paintEvent(QPaintEvent *e) { _translateTracker->finishBunch(); if (readTill && _widget->markingMessagesRead()) { session().data().histories().readInboxTill(readTill); + if (!startEffects.empty()) { + for (const auto &view : startEffects) { + _emojiInteractions->playEffectOnRead(view); + } + } } if (markingAsViewed && !readContents.empty()) { session().api().markContentsRead(readContents); @@ -991,6 +1003,9 @@ void HistoryInner::paintEvent(QPaintEvent *e) { session().sponsoredMessages().view(item->fullId()); } else if (isUnread) { readTill = item; + if (item->hasUnwatchedEffect()) { + startEffects.emplace(view); + } } if (markingAsViewed && item->hasViews()) { session().api().views().scheduleIncrement(item); @@ -3568,6 +3583,12 @@ void HistoryInner::elementCancelPremium(not_null view) { _emojiInteractions->cancelPremiumEffect(view); } +void HistoryInner::elementStartEffect( + not_null view, + Element *replacing) { + _emojiInteractions->playEffect(view); +} + auto HistoryInner::getSelectionState() const -> HistoryView::TopBarWidget::SelectedState { auto result = HistoryView::TopBarWidget::SelectedState {}; diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 11eb81a5a..2ab1207a1 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -170,6 +170,9 @@ public: not_null view, Element *replacing); void elementCancelPremium(not_null view); + void elementStartEffect( + not_null view, + Element *replacing); void updateBotInfo(bool recount = true); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index b65173f00..7e929a816 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -687,6 +687,9 @@ HistoryItem::HistoryItem( if (isHistoryEntry() && IsClientMsgId(id)) { _history->registerClientSideMessage(this); } + if (_effectId) { + _history->owner().reactions().preloadEffectImageFor(_effectId); + } } HistoryItem::HistoryItem( @@ -1283,6 +1286,21 @@ bool HistoryItem::hasUnreadReaction() const { return (_flags & MessageFlag::HasUnreadReaction); } +bool HistoryItem::hasUnwatchedEffect() const { + return !out() + && effectId() + && !(_flags & MessageFlag::EffectWatchedLocal) + && unread(history()); +} + +bool HistoryItem::markEffectWatched() { + if (!hasUnwatchedEffect()) { + return false; + } + _flags |= MessageFlag::EffectWatchedLocal; + return true; +} + bool HistoryItem::mentionsMe() const { if (Has() && !Core::App().settings().notifyAboutPinned()) { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index de437cfe4..1a276a447 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -241,6 +241,8 @@ public: [[nodiscard]] bool mentionsMe() const; [[nodiscard]] bool isUnreadMention() const; [[nodiscard]] bool hasUnreadReaction() const; + [[nodiscard]] bool hasUnwatchedEffect() const; + bool markEffectWatched(); [[nodiscard]] bool isUnreadMedia() const; [[nodiscard]] bool isIncomingUnreadMedia() const; [[nodiscard]] bool hasUnreadMediaFlag() const; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index ce14ce620..0f919d652 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1389,7 +1389,10 @@ int HistoryWidget::itemTopForHighlight( const auto itemTop = _list->itemTop(view); Assert(itemTop >= 0); - const auto reactionCenter = view->data()->hasUnreadReaction() + const auto item = view->data(); + const auto unwatchedEffect = item->hasUnwatchedEffect(); + const auto showReactions = item->hasUnreadReaction() || unwatchedEffect; + const auto reactionCenter = showReactions ? view->reactionButtonParameters({}, {}).center.y() : -1; @@ -2376,8 +2379,6 @@ 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 db8f49a27..c7461f3fc 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/view/history_view_message.h" #include "history/view/history_view_cursor_state.h" +#include "chat_helpers/emoji_interactions.h" #include "core/click_handler_types.h" #include "main/main_session.h" #include "lottie/lottie_icon.h" @@ -104,10 +105,11 @@ bool BottomInfo::isWide() const { } TextState BottomInfo::textState( - not_null item, + not_null view, QPoint position) const { + const auto item = view->data(); auto result = TextState(item); - if (const auto link = replayEffectLink(item, position)) { + if (const auto link = replayEffectLink(view, position)) { result.link = link; return result; } @@ -158,7 +160,7 @@ TextState BottomInfo::textState( } ClickHandlerPtr BottomInfo::replayEffectLink( - not_null item, + not_null view, QPoint position) const { if (!_effect) { return nullptr; @@ -189,7 +191,7 @@ ClickHandlerPtr BottomInfo::replayEffectLink( st::msgDateFont->height); if (image.contains(position)) { if (!_replayLink) { - _replayLink = replayEffectLink(item); + _replayLink = replayEffectLink(view); } return _replayLink; } @@ -200,15 +202,17 @@ ClickHandlerPtr BottomInfo::replayEffectLink( } ClickHandlerPtr BottomInfo::replayEffectLink( - not_null item) const { + not_null view) const { + const auto item = view->data(); const auto itemId = item->fullId(); const auto sessionId = item->history()->session().uniqueId(); - return std::make_shared([=]( - ClickContext context) { + const auto weak = base::make_weak(view); + return std::make_shared([=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { - controller->showToast("playing nice effect.."); - AssertIsDebug(); + if (const auto strong = weak.get()) { + strong->delegate()->elementStartEffect(strong, nullptr); + } } }); } diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index b4cc26f51..c585281be 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -62,7 +62,7 @@ public: [[nodiscard]] int firstLineWidth() const; [[nodiscard]] bool isWide() const; [[nodiscard]] TextState textState( - not_null item, + not_null view, QPoint position) const; [[nodiscard]] bool isSignedAuthorElided() const; @@ -106,10 +106,10 @@ private: [[nodiscard]] Effect prepareEffectWithId(EffectId id); [[nodiscard]] ClickHandlerPtr replayEffectLink( - not_null item, + not_null view, QPoint position) const; [[nodiscard]] ClickHandlerPtr replayEffectLink( - not_null item) const; + not_null view) const; const not_null<::Data::Reactions*> _reactionsOwner; Data _data; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 79fc506c1..ef5b0c830 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -192,6 +192,11 @@ void DefaultElementDelegate::elementCancelPremium( not_null view) { } +void DefaultElementDelegate::elementStartEffect( + not_null view, + Element *replacing) { +} + QString DefaultElementDelegate::elementAuthorRank( not_null view) { return {}; diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 4316806ce..324640b6d 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -113,6 +113,9 @@ public: not_null view, Element *replacing) = 0; virtual void elementCancelPremium(not_null view) = 0; + virtual void elementStartEffect( + not_null view, + Element *replacing) = 0; virtual QString elementAuthorRank(not_null view) = 0; virtual ~ElementDelegate() { @@ -163,6 +166,9 @@ public: not_null view, Element *replacing) override; void elementCancelPremium(not_null view) override; + void elementStartEffect( + not_null view, + Element *replacing) override; QString elementAuthorRank(not_null view) override; }; diff --git a/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp b/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp index 91e18f9d8..c9b299a72 100644 --- a/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp +++ b/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" #include "history/view/media/history_view_sticker.h" #include "history/history.h" +#include "history/history_item.h" #include "chat_helpers/stickers_emoji_pack.h" #include "chat_helpers/emoji_interactions.h" #include "chat_helpers/stickers_lottie.h" @@ -17,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_message_reactions.h" #include "lottie/lottie_common.h" #include "lottie/lottie_single_player.h" #include "base/random.h" @@ -45,8 +47,8 @@ constexpr auto kDropDelayedAfterDelay = crl::time(2000); EmojiInteractions::EmojiInteractions( not_null session, Fn)> itemTop) - : _session(session) - , _itemTop(std::move(itemTop)) { +: _session(session) +, _itemTop(std::move(itemTop)) { _session->data().viewRemoved( ) | rpl::filter([=] { return !_plays.empty() || !_delayed.empty(); @@ -56,6 +58,11 @@ EmojiInteractions::EmojiInteractions( ranges::remove(_delayed, view, &Delayed::view), end(_delayed)); }, _lifetime); + + _session->data().reactions().effectsUpdates( + ) | rpl::start_with_next([=] { + checkPendingEffects(); + }, _lifetime); } EmojiInteractions::~EmojiInteractions() = default; @@ -143,6 +150,121 @@ void EmojiInteractions::play( false); } +void EmojiInteractions::playEffectOnRead(not_null view) { + if (view->data()->markEffectWatched()) { + playEffect(view); + } +} + +void EmojiInteractions::playEffect(not_null view) { + if (const auto resolved = resolveEffect(view)) { + playEffect(view, resolved); + } else if (view->data()->effectId()) { + if (resolved.document && !_downloadLifetime) { + _downloadLifetime = _session->downloaderTaskFinished( + ) | rpl::start_with_next([=] { + checkPendingEffects(); + }); + } + addPendingEffect(view); + } +} + +EmojiInteractions::ResolvedEffect EmojiInteractions::resolveEffect( + not_null view) { + const auto item = view->data(); + const auto effectId = item->effectId(); + if (!effectId) { + return {}; + } + using Type = Data::Reactions::Type; + const auto &effects = _session->data().reactions().list(Type::Effects); + const auto i = ranges::find( + effects, + Data::ReactionId{ effectId }, + &Data::Reaction::id); + if (i == end(effects)) { + return {}; + } + auto document = (DocumentData*)nullptr; + auto content = QByteArray(); + auto filepath = QString(); + if ((document = i->aroundAnimation)) { + content = document->createMediaView()->bytes(); + filepath = document->filepath(); + } else { + document = i->selectAnimation; + content = document->createMediaView()->videoThumbnailContent(); + } + return { + .emoticon = i->title, + .document = document, + .content = content, + .filepath = filepath, + }; +} + +void EmojiInteractions::playEffect( + not_null view, + const ResolvedEffect &resolved) { + play( + resolved.emoticon, + view, + resolved.document, + resolved.content, + resolved.filepath, + false, + false); +} + +void EmojiInteractions::addPendingEffect(not_null view) { + auto found = false; + const auto predicate = [&](base::weak_ptr weak) { + const auto strong = weak.get(); + if (strong == view) { + found = true; + } + return !strong; + }; + _pendingEffects.erase( + ranges::remove_if(_pendingEffects, predicate), + end(_pendingEffects)); + if (!found) { + _pendingEffects.push_back(view); + } +} + +void EmojiInteractions::checkPendingEffects() { + auto waitingDownload = false; + const auto predicate = [&](base::weak_ptr weak) { + const auto strong = weak.get(); + if (!strong) { + return true; + } + const auto resolved = resolveEffect(strong); + if (resolved) { + playEffect(strong, resolved); + return true; + } else if (!strong->data()->effectId()) { + return true; + } else if (resolved.document) { + waitingDownload = true; + } + return false; + }; + _pendingEffects.erase( + ranges::remove_if(_pendingEffects, predicate), + end(_pendingEffects)); + if (!waitingDownload) { + _downloadLifetime.destroy(); + } else if (!_downloadLifetime) { + _downloadLifetime = _session->downloaderTaskFinished( + ) | rpl::start_with_next([=] { + checkPendingEffects(); + }); + } +} + void EmojiInteractions::play( QString emoticon, not_null view, diff --git a/Telegram/SourceFiles/history/view/history_view_emoji_interactions.h b/Telegram/SourceFiles/history/view/history_view_emoji_interactions.h index 4d38f38eb..b7af25997 100644 --- a/Telegram/SourceFiles/history/view/history_view_emoji_interactions.h +++ b/Telegram/SourceFiles/history/view/history_view_emoji_interactions.h @@ -43,6 +43,9 @@ public: void cancelPremiumEffect(not_null view); void visibleAreaUpdated(int visibleTop, int visibleBottom); + void playEffectOnRead(not_null view); + void playEffect(not_null view); + void paint(QPainter &p); [[nodiscard]] rpl::producer updateRequests() const; [[nodiscard]] rpl::producer playStarted() const; @@ -68,6 +71,16 @@ private: crl::time shouldHaveStartedAt = 0; bool incoming = false; }; + struct ResolvedEffect { + QString emoticon; + DocumentData *document = nullptr; + QByteArray content; + QString filepath; + + explicit operator bool() const { + return document && (!content.isEmpty() || !filepath.isEmpty()); + } + }; [[nodiscard]] QRect computeRect(const Play &play) const; @@ -85,6 +98,14 @@ private: bool incoming, bool premium); void checkDelayed(); + void addPendingEffect(not_null view); + + [[nodiscard]] ResolvedEffect resolveEffect( + not_null view); + void playEffect( + not_null view, + const ResolvedEffect &resolved); + void checkPendingEffects(); const not_null _session; const Fn)> _itemTop; @@ -97,6 +118,9 @@ private: rpl::event_stream _updateRequests; rpl::event_stream _playStarted; + std::vector> _pendingEffects; + rpl::lifetime _downloadLifetime; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index a320a67f7..59fb59534 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1857,6 +1857,12 @@ void ListWidget::elementCancelPremium(not_null view) { _emojiInteractions->cancelPremiumEffect(view); } +void ListWidget::elementStartEffect( + not_null view, + Element *replacing) { + _emojiInteractions->playEffect(view); +} + QString ListWidget::elementAuthorRank(not_null view) { return _delegate->listElementAuthorRank(view); } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index e5304bb44..b85bb7bed 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -419,6 +419,9 @@ public: not_null view, Element *replacing) override; void elementCancelPremium(not_null view) override; + void elementStartEffect( + not_null view, + Element *replacing) override; QString elementAuthorRank(not_null view) override; void setEmptyInfoWidget(base::unique_qptr &&w); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index ea17702ee..32cf1f5ad 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -3038,7 +3038,7 @@ TextState Message::bottomInfoTextState( const auto infoLeft = infoRight - size.width(); const auto infoTop = infoBottom - size.height(); return _bottomInfo.textState( - data(), + this, point - QPoint{ infoLeft, infoTop }); }