From 0ab26f0c8222c188aac8815a4f951897ee86f7f8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 11 Jan 2022 17:13:04 +0300 Subject: [PATCH] Initial reaction effects implementation. --- Telegram/CMakeLists.txt | 2 + .../data/data_message_reactions.cpp | 20 ++- .../SourceFiles/data/data_message_reactions.h | 4 +- .../history/history_inner_widget.cpp | 17 +- .../history/view/history_view_bottom_info.cpp | 51 +++++- .../history/view/history_view_bottom_info.h | 15 ++ .../history/view/history_view_element.cpp | 26 ++- .../history/view/history_view_element.h | 19 +++ .../history/view/history_view_list_widget.cpp | 17 +- .../history/view/history_view_message.cpp | 154 ++++++++++++++++-- .../history/view/history_view_message.h | 4 + .../view/history_view_react_animation.cpp | 135 +++++++++++++++ .../view/history_view_react_animation.h | 58 +++++++ .../view/history_view_react_button.cpp | 108 +++++++++--- .../history/view/history_view_react_button.h | 9 + .../history/view/history_view_reactions.cpp | 44 ++++- .../history/view/history_view_reactions.h | 12 ++ .../view/media/history_view_document.cpp | 4 +- .../history/view/media/history_view_file.cpp | 12 +- .../history/view/media/history_view_file.h | 1 - .../history/view/media/history_view_gif.cpp | 54 +++--- .../history/view/media/history_view_gif.h | 1 + .../view/media/history_view_location.cpp | 6 + .../view/media/history_view_location.h | 1 + .../history/view/media/history_view_media.cpp | 5 + .../history/view/media/history_view_media.h | 5 + .../view/media/history_view_media_grouped.cpp | 6 + .../view/media/history_view_media_grouped.h | 2 + .../media/history_view_media_unwrapped.cpp | 9 + .../view/media/history_view_media_unwrapped.h | 2 + .../history/view/media/history_view_photo.cpp | 10 +- .../history/view/media/history_view_photo.h | 1 + .../history/view/media/history_view_poll.cpp | 26 +-- 33 files changed, 733 insertions(+), 107 deletions(-) create mode 100644 Telegram/SourceFiles/history/view/history_view_react_animation.cpp create mode 100644 Telegram/SourceFiles/history/view/history_view_react_animation.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 8746eda8d5..eea381f1b7 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -638,6 +638,8 @@ PRIVATE history/view/history_view_pinned_section.h history/view/history_view_pinned_tracker.cpp history/view/history_view_pinned_tracker.h + history/view/history_view_react_animation.cpp + history/view/history_view_react_animation.h history/view/history_view_react_button.cpp history/view/history_view_react_button.h history/view/history_view_reactions.cpp diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 0f4fcb532d..578723337f 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -224,19 +224,21 @@ void Reactions::request() { MTP_int(_hash) )).done([=](const MTPmessages_AvailableReactions &result) { _requestId = 0; + const auto oldCache = base::take(_iconsCache); + const auto toCache = [&](DocumentData *document) { + if (document) { + _iconsCache.emplace(document, document->createMediaView()); + } + }; result.match([&](const MTPDmessages_availableReactions &data) { _hash = data.vhash().v; const auto &list = data.vreactions().v; - const auto oldCache = base::take(_iconsCache); _active.clear(); _available.clear(); _active.reserve(list.size()); _available.reserve(list.size()); _iconsCache.reserve(list.size() * 2); - const auto toCache = [&](not_null document) { - _iconsCache.emplace(document, document->createMediaView()); - }; for (const auto &reaction : list) { if (const auto parsed = parse(reaction)) { _available.push_back(*parsed); @@ -244,6 +246,8 @@ void Reactions::request() { _active.push_back(*parsed); toCache(parsed->appearAnimation); toCache(parsed->selectAnimation); + toCache(parsed->centerIcon); + toCache(parsed->aroundAnimation); } } } @@ -277,10 +281,10 @@ std::optional Reactions::parse(const MTPAvailableReaction &entry) { .appearAnimation = _owner->processDocument( data.vappear_animation()), .selectAnimation = selectAnimation, - .activateAnimation = _owner->processDocument( - data.vactivate_animation()), - .activateEffects = _owner->processDocument( - data.veffect_animation()), + //.activateAnimation = _owner->processDocument( + // data.vactivate_animation()), + //.activateEffects = _owner->processDocument( + // data.veffect_animation()), .centerIcon = (data.vcenter_icon() ? _owner->processDocument(*data.vcenter_icon()).get() : nullptr), diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 36f4ed13a3..31a833071e 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -24,8 +24,8 @@ struct Reaction { not_null staticIcon; not_null appearAnimation; not_null selectAnimation; - not_null activateAnimation; - not_null activateEffects; + //not_null activateAnimation; + //not_null activateEffects; DocumentData *centerIcon = nullptr; DocumentData *aroundAnimation = nullptr; bool active = false; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 6f25a2a81a..6cf6f800ac 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -377,8 +377,21 @@ HistoryInner::HistoryInner( using ChosenReaction = HistoryView::Reactions::Manager::Chosen; _reactionsManager->chosen( ) | rpl::start_with_next([=](ChosenReaction reaction) { - if (const auto item = session().data().message(reaction.context)) { - item->toggleReaction(reaction.emoji); + const auto item = session().data().message(reaction.context); + if (!item) { + return; + } + item->toggleReaction(reaction.emoji); + if (item->chosenReaction() != reaction.emoji) { + return; + } else if (const auto view = item->mainView()) { + if (const auto top = itemTop(view); top >= 0) { + view->animateSendReaction({ + .emoji = reaction.emoji, + .flyIcon = reaction.icon, + .flyFrom = reaction.geometry.translated(0, -top), + }); + } } }, lifetime()); diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index 94d3431cde..f0f6d0326c 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -17,6 +17,8 @@ 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 "history/view/history_view_react_animation.h" +#include "lottie/lottie_icon.h" #include "data/data_message_reactions.h" #include "styles/style_chat.h" #include "styles/style_dialogs.h" @@ -31,6 +33,8 @@ BottomInfo::BottomInfo( layout(); } +BottomInfo::~BottomInfo() = default; + void BottomInfo::update(Data &&data, int availableWidth) { _data = std::move(data); layout(); @@ -221,19 +225,27 @@ void BottomInfo::paint( left += width() - available; top += st::msgDateFont->height; } - paintReactions(p, left, top, available); + paintReactions(p, position, left, top, available); } } void BottomInfo::paintReactions( Painter &p, + QPoint origin, int left, int top, int availableWidth) const { auto x = left; auto y = top; auto widthLeft = availableWidth; + const auto animated = _reactionAnimation + ? _reactionAnimation->playingAroundEmoji() + : QString(); + if (_reactionAnimation && animated.isEmpty()) { + _reactionAnimation = nullptr; + } for (const auto &reaction : _reactions) { + const auto animating = (reaction.emoji == animated); const auto add = (reaction.countTextWidth > 0) ? st::reactionInfoDigitSkip : st::reactionInfoBetween; @@ -251,11 +263,18 @@ void BottomInfo::paintReactions( reaction.emoji, ::Data::Reactions::ImageSize::BottomInfo); } - if (!reaction.image.isNull()) { - p.drawImage( - x + (st::reactionInfoSize - st::reactionInfoImage) / 2, - y + (st::msgDateFont->height - st::reactionInfoImage) / 2, - reaction.image); + 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 || !_reactionAnimation->flying()); + if (!reaction.image.isNull() && !skipImage) { + p.drawImage(image.topLeft(), reaction.image); + } + if (animating) { + _reactionAnimation->paint(p, origin, image); } if (reaction.countTextWidth > 0) { p.drawText( @@ -406,6 +425,26 @@ void BottomInfo::setReactionCount(Reaction &reaction, int count) { : 0; } +void BottomInfo::animateReactionSend( + SendReactionAnimationArgs &&args, + Fn repaint) { + _reactionAnimation = std::make_unique( + _reactionsOwner, + args.translated(QPoint(width(), height())), + std::move(repaint), + st::reactionInfoImage); +} + +auto BottomInfo::takeSendReactionAnimation() +-> std::unique_ptr { + return std::move(_reactionAnimation); +} + +void BottomInfo::continueSendReactionAnimation( + std::unique_ptr animation) { + _reactionAnimation = std::move(animation); +} + BottomInfo::Data BottomInfoDataFromMessage(not_null message) { using Flag = BottomInfo::Data::Flag; const auto item = message->message(); diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index a44b035d0d..09e2ebcb39 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -20,11 +20,15 @@ class Reactions; } // namespace Data namespace HistoryView { +namespace Reactions { +class SendAnimation; +} // namespace Reactions using PaintContext = Ui::ChatPaintContext; class Message; struct TextState; +struct SendReactionAnimationArgs; class BottomInfo final : public Object { public: @@ -49,6 +53,7 @@ public: Flags flags; }; BottomInfo(not_null<::Data::Reactions*> reactionsOwner, Data &&data); + ~BottomInfo(); void update(Data &&data, int availableWidth); @@ -67,6 +72,14 @@ public: bool inverted, const PaintContext &context) const; + void animateReactionSend( + SendReactionAnimationArgs &&args, + Fn repaint); + [[nodiscard]] auto takeSendReactionAnimation() + -> std::unique_ptr; + void continueSendReactionAnimation( + std::unique_ptr animation); + private: struct Reaction { mutable QImage image; @@ -86,6 +99,7 @@ private: [[nodiscard]] int countReactionsHeight(int newWidth) const; void paintReactions( Painter &p, + QPoint origin, int left, int top, int availableWidth) const; @@ -102,6 +116,7 @@ private: Ui::Text::String _views; Ui::Text::String _replies; std::vector _reactions; + mutable std::unique_ptr _reactionAnimation; int _reactionsMaxWidth = 0; int _dateWidth = 0; bool _authorElided = false; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 869118ac59..b4e2f679ca 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media_grouped.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_large_emoji.h" +#include "history/view/history_view_react_animation.h" #include "history/view/history_view_react_button.h" #include "history/view/history_view_cursor_state.h" #include "history/history.h" @@ -341,6 +342,15 @@ void DateBadge::paint( ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide); } +SendReactionAnimationArgs SendReactionAnimationArgs::translated( + QPoint point) const { + return { + .emoji = emoji, + .flyIcon = flyIcon, + .flyFrom = flyFrom.translated(point), + }; +} + Element::Element( not_null delegate, not_null data, @@ -392,6 +402,10 @@ void Element::setY(int y) { void Element::refreshDataIdHook() { } +void Element::repaint() const { + history()->owner().requestViewRepaint(this); +} + void Element::paintHighlight( Painter &p, const PaintContext &context, @@ -1020,7 +1034,7 @@ void Element::clickHandlerActiveChanged( } } App::hoveredLinkItem(active ? this : nullptr); - history()->owner().requestViewRepaint(this); + repaint(); if (const auto media = this->media()) { media->clickHandlerActiveChanged(handler, active); } @@ -1035,12 +1049,20 @@ void Element::clickHandlerPressedChanged( } } App::pressedLinkItem(pressed ? this : nullptr); - history()->owner().requestViewRepaint(this); + repaint(); if (const auto media = this->media()) { media->clickHandlerPressedChanged(handler, pressed); } } +void Element::animateSendReaction(SendReactionAnimationArgs &&args) { +} + +auto Element::takeSendReactionAnimation() +-> std::unique_ptr { + return nullptr; +} + Element::~Element() { // Delete media while owner still exists. base::take(_media); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 92f15bbf87..2aa1bb14b0 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -33,6 +33,10 @@ struct ChatPaintContext; class ChatStyle; } // namespace Ui +namespace Lottie { +class Icon; +} // namespace Lottie + namespace HistoryView { enum class PointState : char; @@ -45,6 +49,7 @@ using PaintContext = Ui::ChatPaintContext; namespace Reactions { struct ButtonParameters; +class SendAnimation; } // namespace Reactions enum class Context : char { @@ -224,6 +229,14 @@ struct DateBadge : public RuntimeComponent { }; +struct SendReactionAnimationArgs { + QString emoji; + std::shared_ptr flyIcon; + QRect flyFrom; + + [[nodiscard]] SendReactionAnimationArgs translated(QPoint point) const; +}; + class Element : public Object , public RuntimeComposer @@ -407,9 +420,15 @@ public: [[nodiscard]] bool markSponsoredViewed(int shownFromTop) const; + virtual void animateSendReaction(SendReactionAnimationArgs &&args); + [[nodiscard]] virtual auto takeSendReactionAnimation() + -> std::unique_ptr; + virtual ~Element(); protected: + void repaint() const; + void paintHighlight( Painter &p, const PaintContext &context, diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 3f6e962d5a..65cef54eba 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -342,8 +342,21 @@ ListWidget::ListWidget( using ChosenReaction = Reactions::Manager::Chosen; _reactionsManager->chosen( ) | rpl::start_with_next([=](ChosenReaction reaction) { - if (const auto item = session().data().message(reaction.context)) { - item->toggleReaction(reaction.emoji); + const auto item = session().data().message(reaction.context); + if (!item) { + return; + } + item->toggleReaction(reaction.emoji); + if (item->chosenReaction() != reaction.emoji) { + return; + } else if (const auto view = viewForItem(item)) { + if (const auto top = itemTop(view); top >= 0) { + view->animateSendReaction({ + .emoji = reaction.emoji, + .flyIcon = reaction.icon, + .flyFrom = reaction.geometry.translated(0, -top), + }); + } } }, lifetime()); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 11f26416be..11064e845d 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_message.h" #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_web_page.h" +#include "history/view/history_view_react_animation.h" #include "history/view/history_view_react_button.h" #include "history/view/history_view_reactions.h" #include "history/view/history_view_group_call_bar.h" // UserpicInRow. @@ -252,6 +253,17 @@ Message::Message( initLogEntryOriginal(); initPsa(); refreshReactions(); + auto animation = replacing + ? replacing->takeSendReactionAnimation() + : nullptr; + if (animation) { + animation->setRepaintCallback([=] { repaint(); }); + if (_reactions) { + _reactions->continueSendAnimation(std::move(animation)); + } else { + _bottomInfo.continueSendReactionAnimation(std::move(animation)); + } + } } Message::~Message() { @@ -314,6 +326,111 @@ void Message::applyGroupAdminChanges( } } +void Message::animateSendReaction(SendReactionAnimationArgs &&args) { + const auto item = message(); + 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(); + auto keyboard = item->inlineReplyKeyboard(); + auto keyboardHeight = 0; + if (keyboard) { + keyboardHeight = keyboard->naturalHeight(); + g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); + } + + if (_reactions && !reactionsInBubble) { + const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height(); + const auto reactionsLeft = (!bubble && mediaDisplayed) + ? media->contentRectForReactions().x() + : 0; + g.setHeight(g.height() - reactionsHeight); + const auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip); + _reactions->animateSend(args.translated(-reactionsPosition), repainter); + return; + } + + const auto animateInBottomInfo = [&](QPoint bottomRight) { + _bottomInfo.animateReactionSend(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 (reactionsInBubble) { + trect.setHeight(trect.height() - reactionsHeight); + const auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + reactionsTop); + _reactions->animateSend(args.translated(-reactionsPosition), repainter); + return; + } + if (_viewButton) { + const auto belowInfo = _viewButton->belowMessageInfo(); + const auto infoHeight = reactionsInBubble + ? (reactionsHeight + st::msgPadding.bottom()) + : _bottomInfo.height(); + const auto heightMargins = QMargins(0, 0, 0, infoHeight); + if (belowInfo) { + inner -= heightMargins; + } + trect.setHeight(trect.height() - _viewButton->height()); + if (reactionsInBubble) { + trect.setHeight(trect.height() + st::msgPadding.bottom()); + } else if (mediaDisplayed) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip); + } + } + if (mediaOnBottom) { + trect.setHeight(trect.height() + + st::msgPadding.bottom() + - viewButtonHeight()); + } + if (mediaOnTop) { + trect.setY(trect.y() - st::msgPadding.top()); + } + if (mediaDisplayed && mediaOnBottom && media->customInfoLayout()) { + auto mediaHeight = media->height(); + auto mediaLeft = trect.x() - st::msgPadding.left(); + auto mediaTop = (trect.y() + trect.height() - mediaHeight); + animateInBottomInfo(QPoint(mediaLeft, mediaTop) + media->resolveCustomInfoRightBottom()); + } else { + animateInBottomInfo({ + inner.left() + inner.width() - (st::msgPadding.right() - st::msgDateDelta.x()), + inner.top() + inner.height() - (st::msgPadding.bottom() - st::msgDateDelta.y()), + }); + } + } else if (mediaDisplayed) { + animateInBottomInfo(g.topLeft() + media->resolveCustomInfoRightBottom()); + } +} + +auto Message::takeSendReactionAnimation() +-> std::unique_ptr { + return _reactions + ? _reactions->takeSendAnimation() + : _bottomInfo.takeSendReactionAnimation(); +} + QSize Message::performCountOptimalSize() { const auto item = message(); const auto media = this->media(); @@ -516,12 +633,12 @@ void Message::draw(Painter &p, const PaintContext &context) const { const auto stm = context.messageStyle(); const auto bubble = drawBubble(); - auto dateh = 0; - if (const auto date = Get()) { - dateh = date->height(); - } if (const auto bar = Get()) { auto unreadbarh = bar->height(); + auto dateh = 0; + if (const auto date = Get()) { + dateh = date->height(); + } if (context.clip.intersects(QRect(0, dateh, width(), unreadbarh))) { p.translate(0, dateh); bar->paint( @@ -1207,7 +1324,7 @@ void Message::toggleCommentsButtonRipple(bool pressed) { _comments->ripple = std::make_unique( st::defaultRippleAnimation, std::move(mask), - [=] { history()->owner().requestViewRepaint(this); }); + [=] { repaint(); }); } _comments->ripple->add(_comments->lastPoint); } else if (_comments->ripple) { @@ -1653,7 +1770,7 @@ void Message::psaTooltipToggled(bool tooltipShown) const { state->buttonVisible = visible; history()->owner().notifyViewLayoutChange(this); state->buttonVisibleAnimation.start( - [=] { history()->owner().requestViewRepaint(this); }, + [=] { repaint(); }, visible ? 0. : 1., visible ? 1. : 0., st::fadeWrapDuration); @@ -2009,14 +2126,17 @@ void Message::refreshReactions() { auto reactionsData = InlineListDataFromMessage(this); if (!_reactions) { const auto handlerFactory = [=](QString emoji) { + const auto weak = base::make_weak(this); const auto fullId = data()->fullId(); - return std::make_shared([=]( - ClickContext context) { - const auto my = context.other.value(); - if (const auto controller = my.sessionWindow.get()) { - const auto &data = controller->session().data(); - if (const auto item = data.message(fullId)) { - item->toggleReaction(emoji); + return std::make_shared([=] { + if (const auto strong = weak.get()) { + strong->data()->toggleReaction(emoji); + if (const auto now = weak.get()) { + if (now->data()->chosenReaction() == emoji) { + now->animateSendReaction({ + .emoji = emoji, + }); + } } } }); @@ -2044,7 +2164,7 @@ void Message::itemDataChanged() { if (wasInfo != nowInfo || wasReactions != nowReactions) { history()->owner().requestViewResize(this); } else { - history()->owner().requestViewRepaint(this); + repaint(); } } @@ -2097,10 +2217,10 @@ void Message::updateViewButtonExistence() { } else if (_viewButton) { return; } - auto callback = [=] { history()->owner().requestViewRepaint(this); }; + auto repainter = [=] { repaint(); }; _viewButton = sponsored - ? std::make_unique(sponsored, std::move(callback)) - : std::make_unique(media, std::move(callback)); + ? std::make_unique(sponsored, std::move(repainter)) + : std::make_unique(media, std::move(repainter)); } void Message::initLogEntryOriginal() { diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 2d3ae61597..164a87b8e4 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -134,6 +134,10 @@ public: void applyGroupAdminChanges( const base::flat_set &changes) override; + void animateSendReaction(SendReactionAnimationArgs &&args) override; + auto takeSendReactionAnimation() + -> std::unique_ptr override; + protected: void refreshDataIdHook() override; diff --git a/Telegram/SourceFiles/history/view/history_view_react_animation.cpp b/Telegram/SourceFiles/history/view/history_view_react_animation.cpp new file mode 100644 index 0000000000..6ce30cee94 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_react_animation.cpp @@ -0,0 +1,135 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/history_view_react_animation.h" + +#include "history/view/history_view_element.h" +#include "lottie/lottie_icon.h" +#include "data/data_message_reactions.h" +#include "data/data_document.h" +#include "data/data_document_media.h" + +namespace HistoryView::Reactions { +namespace { + +constexpr auto kFlyDuration = crl::time(200); + +} // namespace + +SendAnimation::SendAnimation( + not_null<::Data::Reactions*> owner, + SendReactionAnimationArgs &&args, + Fn repaint, + int size) +: _owner(owner) +, _emoji(args.emoji) +, _repaint(std::move(repaint)) +, _flyFrom(args.flyFrom) { + const auto &list = owner->list(::Data::Reactions::Type::All); + const auto i = ranges::find(list, _emoji, &::Data::Reaction::emoji); + if (i == end(list) || !i->centerIcon) { + return; + } + const auto resolve = [&]( + std::unique_ptr &icon, + DocumentData *document, + int size) { + if (!document) { + return false; + } + const auto media = document->activeMediaView(); + if (!media || !media->loaded()) { + return false; + } + icon = std::make_unique(Lottie::IconDescriptor{ + .path = document->filepath(true), + .json = media->bytes(), + .sizeOverride = QSize(size, size), + }); + return true; + }; + _flyIcon = std::move(args.flyIcon); + if (!resolve(_center, i->centerIcon, size) + || !resolve(_effect, i->aroundAnimation, size * 2)) { + return; + } + if (_flyIcon) { + _fly.start([=] { flyCallback(); }, 0., 1., kFlyDuration); + } else { + startAnimations(); + } + _valid = true; +} + +SendAnimation::~SendAnimation() = default; + +void SendAnimation::paint(QPainter &p, QPoint origin, QRect target) const { + if (_flyIcon) { + const auto from = _flyFrom.translated(origin); + const auto lshift = target.width() / 4; + const auto rshift = target.width() / 2 - lshift; + const auto margins = QMargins{ lshift, lshift, rshift, rshift }; + target = target.marginsRemoved(margins); + const auto progress = _fly.value(1.); + const auto rect = QRect( + anim::interpolate(from.x(), target.x(), progress), + anim::interpolate(from.y(), target.y(), progress), + anim::interpolate(from.width(), target.width(), progress), + anim::interpolate(from.height(), target.height(), progress)); + auto hq = PainterHighQualityEnabler(p); + if (progress < 1.) { + p.setOpacity(1. - progress); + p.drawImage(rect, _flyIcon->frame()); + } + if (progress > 0.) { + p.setOpacity(progress); + p.drawImage(rect.marginsAdded(margins), _center->frame()); + } + p.setOpacity(1.); + } else { + p.drawImage(target, _center->frame()); + p.drawImage(QRect( + target.topLeft() - QPoint(target.width(), target.height()) / 2, + target.size() * 2 + ), _effect->frame()); + } +} + +void SendAnimation::startAnimations() { + _center->animate([=] { callback(); }, 0, _center->framesCount() - 1); + _effect->animate([=] { callback(); }, 0, _effect->framesCount() - 1); +} + +void SendAnimation::flyCallback() { + if (!_fly.animating()) { + _flyIcon = nullptr; + startAnimations(); + } + callback(); +} + +void SendAnimation::callback() { + if (_repaint) { + _repaint(); + } +} + +void SendAnimation::setRepaintCallback(Fn repaint) { + _repaint = std::move(repaint); +} + +bool SendAnimation::flying() const { + return (_flyIcon != nullptr); +} + +QString SendAnimation::playingAroundEmoji() const { + const auto finished = !_valid + || (!_flyIcon && !_center->animating() && !_effect->animating()); + return finished ? QString() : _emoji; +} + +} // namespace HistoryView::Reactions diff --git a/Telegram/SourceFiles/history/view/history_view_react_animation.h b/Telegram/SourceFiles/history/view/history_view_react_animation.h new file mode 100644 index 0000000000..35e1c25422 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_react_animation.h @@ -0,0 +1,58 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/effects/animations.h" + +namespace Lottie { +class Icon; +} // namespace Lottie + +namespace Data { +class Reactions; +} // namespace Data + +namespace HistoryView { +struct SendReactionAnimationArgs; +} // namespace HistoryView + +namespace HistoryView::Reactions { + +class SendAnimation final { +public: + SendAnimation( + not_null<::Data::Reactions*> owner, + SendReactionAnimationArgs &&args, + Fn repaint, + int size); + ~SendAnimation(); + + void setRepaintCallback(Fn repaint); + void paint(QPainter &p, QPoint origin, QRect target) const; + + [[nodiscard]] QString playingAroundEmoji() const; + [[nodiscard]] bool flying() const; + +private: + void flyCallback(); + void startAnimations(); + void callback(); + + const not_null<::Data::Reactions*> _owner; + const QString _emoji; + Fn _repaint; + std::shared_ptr _flyIcon; + std::unique_ptr _center; + std::unique_ptr _effect; + Ui::Animations::Simple _fly; + QRect _flyFrom; + bool _valid = false; + +}; + +} // namespace HistoryView::Reactions diff --git a/Telegram/SourceFiles/history/view/history_view_react_button.cpp b/Telegram/SourceFiles/history/view/history_view_react_button.cpp index 2f2dc59b2d..771df791ed 100644 --- a/Telegram/SourceFiles/history/view/history_view_react_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_react_button.cpp @@ -70,6 +70,20 @@ constexpr auto kHoverScale = 1.24; return style::ConvertScale(kSizeForDownscale); } +[[nodiscard]] std::shared_ptr CreateIcon( + not_null media, + int size, + int frame) { + Expects(media->loaded()); + + return std::make_shared(Lottie::IconDescriptor{ + .path = media->owner()->filepath(true), + .json = media->bytes(), + .sizeOverride = QSize(size, size), + .frame = frame, + }); +} + } // namespace Button::Button( @@ -383,17 +397,58 @@ Manager::Manager( _createChooseCallback = [=](QString emoji) { return [=] { - if (const auto context = _buttonContext) { + if (auto chosen = lookupChosen(emoji)) { updateButton({}); - _chosen.fire({ - .context = context, - .emoji = emoji, - }); + _chosen.fire(std::move(chosen)); } }; }; } +Manager::Chosen Manager::lookupChosen(const QString &emoji) const { + auto result = Chosen{ + .context = _buttonContext, + .emoji = emoji, + }; + const auto button = _button.get(); + const auto i = ranges::find(_icons, emoji, &ReactionIcons::emoji); + if (i == end(_icons) || !button) { + return result; + } + const auto &icon = *i; + if (const auto &appear = icon->appear; appear && appear->animating()) { + result.icon = CreateIcon( + icon->appearAnimation->activeMediaView().get(), + appear->width(), + appear->frameIndex()); + } else if (const auto &select = icon->select) { + result.icon = CreateIcon( + icon->selectAnimation->activeMediaView().get(), + select->width(), + select->frameIndex()); + } + const auto index = (i - begin(_icons)); + const auto between = st::reactionCornerSkip; + const auto oneHeight = (st::reactionCornerSize.height() + between); + const auto expanded = (_icons.size() > 1); + const auto skip = (expanded ? st::reactionExpandedSkip : 0); + const auto scroll = button->scroll(); + const auto local = skip + index * oneHeight - scroll; + const auto geometry = button->geometry(); + const auto top = button->expandUp() + ? (geometry.height() - local - _outer.height()) + : local; + const auto rect = QRect(geometry.topLeft() + QPoint(0, top), _outer); + const auto imageSize = int(base::SafeRound( + st::reactionCornerImage * kHoverScale)); + result.geometry = QRect( + rect.x() + (rect.width() - imageSize) / 2, + rect.y() + (rect.height() - imageSize) / 2, + imageSize, + imageSize); + return result; +} + void Manager::applyListFilters() { const auto limit = _uniqueLimit.current(); const auto applyUniqueLimit = _buttonContext @@ -480,15 +535,13 @@ void Manager::showButtonDelayed() { } void Manager::applyList(const std::vector &list) { - constexpr auto predicate = []( - const Data::Reaction &a, - const Data::Reaction &b) { - return (a.emoji == b.emoji) - && (a.appearAnimation == b.appearAnimation) - && (a.selectAnimation == b.selectAnimation); - }; const auto proj = [](const auto &obj) { - return std::tie(obj.emoji, obj.appearAnimation, obj.selectAnimation); + return std::tie( + obj.emoji, + obj.appearAnimation, + obj.selectAnimation, + obj.centerIcon, + obj.aroundAnimation); }; if (ranges::equal(_list, list, ranges::equal_to(), proj, proj)) { return; @@ -502,6 +555,8 @@ void Manager::applyList(const std::vector &list) { .emoji = reaction.emoji, .appearAnimation = reaction.appearAnimation, .selectAnimation = reaction.selectAnimation, + .centerIcon = reaction.centerIcon, + .aroundAnimation = reaction.aroundAnimation, }); } applyListFilters(); @@ -649,6 +704,7 @@ void Manager::loadIcons() { } return entry.icon; }; + auto all = true; for (const auto &icon : _icons) { if (!icon->appear) { icon->appear = load(icon->appearAnimation); @@ -656,6 +712,23 @@ void Manager::loadIcons() { if (!icon->select) { icon->select = load(icon->selectAnimation); } + if (!icon->appear || !icon->select) { + all = false; + } + } + if (all) { + const auto preload = [&](DocumentData *document) { + const auto view = document + ? document->activeMediaView() + : nullptr; + if (view) { + view->checkStickerLarge(); + } + }; + for (const auto &icon : _icons) { + preload(icon->centerIcon); + preload(icon->aroundAnimation); + } } } @@ -821,6 +894,7 @@ void Manager::paintButton( if (opacity == 0.) { return; } + const auto geometry = button->geometry(); const auto position = geometry.topLeft(); const auto size = geometry.size(); @@ -1469,13 +1543,7 @@ IconFactory CachedIconFactory::createMethod() { std::shared_ptr DefaultIconFactory( not_null media, int size) { - Expects(media->loaded()); - - return std::make_shared(Lottie::IconDescriptor{ - .path = media->owner()->filepath(true), - .json = media->bytes(), - .sizeOverride = QSize(size, size), - }); + return CreateIcon(media, size, 0); } } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_react_button.h b/Telegram/SourceFiles/history/view/history_view_react_button.h index cfb2518fd0..f875dc87e5 100644 --- a/Telegram/SourceFiles/history/view/history_view_react_button.h +++ b/Telegram/SourceFiles/history/view/history_view_react_button.h @@ -154,6 +154,12 @@ public: struct Chosen { FullMsgId context; QString emoji; + std::shared_ptr icon; + QRect geometry; + + explicit operator bool() const { + return context && !emoji.isNull(); + } }; [[nodiscard]] rpl::producer chosen() const { return _chosen.events(); @@ -172,6 +178,8 @@ private: QString emoji; not_null appearAnimation; not_null selectAnimation; + DocumentData *centerIcon = nullptr; + DocumentData *aroundAnimation = nullptr; std::shared_ptr appear; std::shared_ptr select; mutable ClickHandlerPtr link; @@ -190,6 +198,7 @@ private: void showButtonDelayed(); void stealWheelEvents(not_null target); + [[nodiscard]] Chosen lookupChosen(const QString &emoji) const; [[nodiscard]] bool overCurrentButton(QPoint position) const; void removeStaleButtons(); diff --git a/Telegram/SourceFiles/history/view/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/history_view_reactions.cpp index 311576f059..9495ef2a7c 100644 --- a/Telegram/SourceFiles/history/view/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reactions.cpp @@ -11,6 +11,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 "history/view/history_view_react_animation.h" #include "data/data_message_reactions.h" #include "lang/lang_tag.h" #include "ui/chat/chat_style.h" @@ -39,6 +40,8 @@ InlineList::InlineList( layout(); } +InlineList::~InlineList() = default; + void InlineList::update(Data &&data, int availableWidth) { _data = std::move(data); layout(); @@ -187,11 +190,19 @@ void InlineList::paint( const auto size = st::reactionBottomSize; const auto skip = (size - st::reactionBottomImage) / 2; const auto inbubble = (_data.flags & InlineListData::Flag::InBubble); + const auto animated = _animation + ? _animation->playingAroundEmoji() + : QString(); + if (_animation && animated.isEmpty()) { + _animation = nullptr; + } p.setFont(st::semiboldFont); for (const auto &button : _buttons) { + const auto animating = (animated == button.emoji); const auto &geometry = button.geometry; const auto inner = geometry.marginsRemoved(padding); - const auto chosen = (_data.chosenReaction == button.emoji); + const auto chosen = (_data.chosenReaction == button.emoji) + && (!animating || !_animation->flying()); { auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); @@ -216,8 +227,16 @@ void InlineList::paint( button.emoji, ::Data::Reactions::ImageSize::InlineList); } - if (!button.image.isNull()) { - p.drawImage(inner.topLeft() + QPoint(skip, skip), button.image); + const auto image = QRect( + inner.topLeft() + QPoint(skip, skip), + QSize(st::reactionBottomImage, st::reactionBottomImage)); + const auto skipImage = animating + && (button.count < 2 || !_animation->flying()); + if (!button.image.isNull() && !skipImage) { + p.drawImage(image.topLeft(), button.image); + } + if (animating) { + _animation->paint(p, QPoint(), image); } p.setPen(!inbubble ? (chosen @@ -262,6 +281,25 @@ bool InlineList::getState( return false; } +void InlineList::animateSend( + SendReactionAnimationArgs &&args, + Fn repaint) { + _animation = std::make_unique( + _owner, + std::move(args), + std::move(repaint), + st::reactionBottomImage); +} + +std::unique_ptr InlineList::takeSendAnimation() { + return std::move(_animation); +} + +void InlineList::continueSendAnimation( + std::unique_ptr animation) { + _animation = std::move(animation); +} + InlineListData InlineListDataFromMessage(not_null message) { using Flag = InlineListData::Flag; const auto item = message->message(); diff --git a/Telegram/SourceFiles/history/view/history_view_reactions.h b/Telegram/SourceFiles/history/view/history_view_reactions.h index 63d0342128..5545000e34 100644 --- a/Telegram/SourceFiles/history/view/history_view_reactions.h +++ b/Telegram/SourceFiles/history/view/history_view_reactions.h @@ -21,10 +21,13 @@ namespace HistoryView { using PaintContext = Ui::ChatPaintContext; class Message; struct TextState; +struct SendReactionAnimationArgs; } // namespace HistoryView namespace HistoryView::Reactions { +class SendAnimation; + struct InlineListData { enum class Flag : uchar { InBubble = 0x01, @@ -45,6 +48,7 @@ public: not_null<::Data::Reactions*> owner, Fn handlerFactory, Data &&data); + ~InlineList(); void update(Data &&data, int availableWidth); QSize countCurrentSize(int newWidth) override; @@ -63,6 +67,12 @@ public: QPoint point, not_null outResult) const; + void animateSend( + SendReactionAnimationArgs &&args, + Fn repaint); + [[nodiscard]] std::unique_ptr takeSendAnimation(); + void continueSendAnimation(std::unique_ptr animation); + private: struct Button { QRect geometry; @@ -88,6 +98,8 @@ private: std::vector