diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index d4fee22c8..304749900 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -226,8 +226,9 @@ struct StoryUpdate { NewAdded = (1U << 2), ViewsAdded = (1U << 3), MarkRead = (1U << 4), + Reaction = (1U << 5), - LastUsedBit = (1U << 4), + LastUsedBit = (1U << 5), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index e4279cbb3..7aa820101 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -381,7 +381,7 @@ void Reactions::preloadImageFor(const ReactionId &id) { loadImage(set, document, !i->centerIcon); } else if (!_waitingForList) { _waitingForList = true; - refreshRecent(); + refreshDefault(); } } diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 586104855..d795b41e3 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -870,6 +870,21 @@ void Stories::activateStealthMode(Fn done) { }).send(); } +void Stories::sendReaction(FullStoryId id, Data::ReactionId reaction) { + if (const auto maybeStory = lookup(id)) { + const auto story = *maybeStory; + story->setReactionId(reaction); + + const auto api = &session().api(); + api->request(MTPstories_SendReaction( + MTP_flags(0), + story->peer()->asUser()->inputUser, + MTP_int(id.story), + ReactionToMTP(reaction) + )).send(); + } +} + std::shared_ptr Stories::resolveItem(not_null story) { auto &items = _items[story->peer()->id]; auto i = items.find(story->id()); diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index ae91d1ffc..8d8b23da4 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -240,6 +240,8 @@ public: [[nodiscard]] rpl::producer stealthModeValue() const; void activateStealthMode(Fn done = nullptr); + void sendReaction(FullStoryId id, Data::ReactionId reaction); + private: struct Saved { StoriesIds ids; diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index 3ac256ffa..d5faa4350 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -376,6 +376,17 @@ const TextWithEntities &Story::caption() const { return unsupported() ? empty : _caption; } +Data::ReactionId Story::sentReactionId() const { + return _sentReactionId; +} + +void Story::setReactionId(Data::ReactionId id) { + if (_sentReactionId != id) { + _sentReactionId = id; + session().changes().storyUpdated(this, UpdateFlag::Reaction); + } +} + const std::vector> &Story::recentViewers() const { return _recentViewers; } @@ -458,6 +469,9 @@ void Story::applyFields( bool initial) { _lastUpdateTime = now; + const auto reaction = data.vsent_reaction() + ? Data::ReactionFromMTP(*data.vsent_reaction()) + : Data::ReactionId(); const auto pinned = data.is_pinned(); const auto edited = data.is_edited(); const auto privacy = data.is_public() @@ -512,6 +526,7 @@ void Story::applyFields( || (_views.reactions != reactions) || (_recentViewers != viewers); const auto locationsChanged = (_locations != locations); + const auto reactionChanged = (_sentReactionId != reaction); _privacyPublic = (privacy == StoryPrivacy::Public); _privacyCloseFriends = (privacy == StoryPrivacy::CloseFriends); @@ -536,15 +551,19 @@ void Story::applyFields( if (locationsChanged) { _locations = std::move(locations); } + if (reactionChanged) { + _sentReactionId = reaction; + } const auto changed = editedChanged || captionChanged || mediaChanged || locationsChanged; - if (!initial && (changed || viewsChanged)) { + if (!initial && (changed || viewsChanged || reactionChanged)) { _peer->session().changes().storyUpdated(this, UpdateFlag() | (changed ? UpdateFlag::Edited : UpdateFlag()) - | (viewsChanged ? UpdateFlag::ViewsAdded : UpdateFlag())); + | (viewsChanged ? UpdateFlag::ViewsAdded : UpdateFlag()) + | (reactionChanged ? UpdateFlag::Reaction : UpdateFlag())); } if (!initial && (captionChanged || mediaChanged)) { if (const auto item = _peer->owner().stories().lookupItem(this)) { diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h index d288ee293..dcfe4495c 100644 --- a/Telegram/SourceFiles/data/data_story.h +++ b/Telegram/SourceFiles/data/data_story.h @@ -146,6 +146,9 @@ public: void setCaption(TextWithEntities &&caption); [[nodiscard]] const TextWithEntities &caption() const; + [[nodiscard]] Data::ReactionId sentReactionId() const; + void setReactionId(Data::ReactionId id); + [[nodiscard]] auto recentViewers() const -> const std::vector> &; [[nodiscard]] const StoryViews &viewsList() const; @@ -170,6 +173,7 @@ private: const StoryId _id = 0; const not_null _peer; + Data::ReactionId _sentReactionId; StoryMedia _media; TextWithEntities _caption; std::vector> _recentViewers; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 6990d5b22..7ad07e755 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1241,9 +1241,12 @@ bool ComposeControls::focus() { return true; } +bool ComposeControls::focused() const { + return Ui::InFocusChain(_wrap.get()); +} + rpl::producer ComposeControls::focusedValue() const { - return rpl::single(Ui::InFocusChain(_wrap.get())) - | rpl::then(_focusChanges.events()); + return rpl::single(focused()) | rpl::then(_focusChanges.events()); } rpl::producer ComposeControls::tabbedPanelShownValue() const { @@ -3022,7 +3025,7 @@ bool ComposeControls::handleCancelRequest() { } void ComposeControls::tryProcessKeyInput(not_null e) { - if (_field->isVisible()) { + if (_field->isVisible() && !e->text().isEmpty()) { _field->setFocusFast(); QCoreApplication::sendEvent(_field->rawTextEdit(), e); } @@ -3158,7 +3161,7 @@ rpl::producer ComposeControls::fieldMenuShownValue() const { return _field->menuShownValue(); } -not_null ComposeControls::likeAnimationTarget() const { +not_null ComposeControls::likeAnimationTarget() const { Expects(_like != nullptr); return _like; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 80ad5e7b2..925555832 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -147,6 +147,7 @@ public: [[nodiscard]] int heightCurrent() const; bool focus(); + [[nodiscard]] bool focused() const; [[nodiscard]] rpl::producer focusedValue() const; [[nodiscard]] rpl::producer tabbedPanelShownValue() const; [[nodiscard]] rpl::producer<> cancelRequests() const; @@ -222,7 +223,7 @@ public: [[nodiscard]] rpl::producer recordingActiveValue() const; [[nodiscard]] rpl::producer hasSendTextValue() const; [[nodiscard]] rpl::producer fieldMenuShownValue() const; - [[nodiscard]] not_null likeAnimationTarget() const; + [[nodiscard]] not_null likeAnimationTarget() const; void applyCloudDraft(); void applyDraft( diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp index 4aa2370a8..0f2874c4e 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -328,6 +328,10 @@ void Selector::updateShowState( update(); } +int Selector::countAppearedWidth(float64 progress) const { + return anim::interpolate(_skipx * 2 + _size, _inner.width(), progress); +} + void Selector::paintAppearing(QPainter &p) { Expects(_strip != nullptr); @@ -340,10 +344,7 @@ void Selector::paintAppearing(QPainter &p) { _paintBuffer.fill(_st.bg->c); auto q = QPainter(&_paintBuffer); const auto extents = extentsForShadow(); - const auto appearedWidth = anim::interpolate( - _skipx * 2 + _size, - _inner.width(), - _appearProgress); + const auto appearedWidth = countAppearedWidth(_appearProgress); const auto fullWidth = _inner.x() + appearedWidth + extents.right(); const auto size = QSize(fullWidth, _outer.height()); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h index b47dfa2d8..270644bdc 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h @@ -63,6 +63,7 @@ public: [[nodiscard]] QMargins extentsForShadow() const; [[nodiscard]] int extendTopForCategories() const; [[nodiscard]] int minimalHeight() const; + [[nodiscard]] int countAppearedWidth(float64 progress) const; void setSpecialExpandTopSkip(int skip); void initGeometry(int innerTop); void beforeDestroy(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index bdff3f368..b12e56585 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -7,19 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_controller.h" -#include "base/timer.h" #include "base/power_save_blocker.h" #include "base/qt_signal_producer.h" #include "base/unixtime.h" #include "boxes/peers/prepare_short_info_box.h" #include "chat_helpers/compose/compose_show.h" #include "core/application.h" +#include "core/core_settings.h" #include "core/update_checker.h" -#include "data/stickers/data_custom_emoji.h" #include "data/data_changes.h" #include "data/data_document.h" #include "data/data_file_origin.h" -#include "data/data_message_reactions.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" @@ -40,22 +38,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/report_box.h" -#include "ui/effects/emoji_fly_animation.h" -#include "ui/effects/message_sending_animation_common.h" -#include "ui/effects/reaction_fly_animation.h" -#include "ui/layers/box_content.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/round_rect.h" -#include "ui/rp_widget.h" #include "window/window_controller.h" #include "window/window_session_controller.h" -#include "styles/style_chat.h" -#include "styles/style_chat_helpers.h" +#include "styles/style_chat_helpers.h" // defaultReportBox #include "styles/style_media_view.h" -#include "styles/style_widgets.h" #include "styles/style_boxes.h" // UserpicButton #include @@ -114,10 +105,6 @@ struct SameDayRange { return result; } -[[nodiscard]] Data::ReactionId HeartReactionId() { - return { QString() + QChar(10084) }; -} - [[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) { if (std::abs(angle) < 1.) { return point; @@ -294,7 +281,7 @@ Controller::Controller(not_null delegate) rpl::combine( _replyArea->activeValue(), - _reactions->expandedValue(), + _reactions->activeValue(), _1 || _2 ) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](bool active) { @@ -302,38 +289,16 @@ Controller::Controller(not_null delegate) updateContentFaded(); }, _lifetime); - _replyArea->focusedValue( - ) | rpl::start_with_next([=](bool focused) { - _replyFocused = focused; - if (!_replyFocused) { - _reactions->hideIfCollapsed(); - } else if (!_hasSendText) { - _reactions->show(); - } - }, _lifetime); - - _replyArea->hasSendTextValue( - ) | rpl::start_with_next([=](bool has) { - _hasSendText = has; - if (_replyFocused) { - if (_hasSendText) { - _reactions->hide(); - } else { - _reactions->show(); - } - } - }, _lifetime); + _reactions->setReplyFieldState( + _replyArea->focusedValue(), + _replyArea->hasSendTextValue()); + if (const auto like = _replyArea->likeAnimationTarget()) { + _reactions->attachToReactionButton(like); + } _reactions->chosen( - ) | rpl::start_with_next([=](HistoryView::Reactions::ChosenReaction id) { - startReactionAnimation({ - .id = id.id, - .flyIcon = id.icon, - .flyFrom = _wrap->mapFromGlobal(id.globalGeometry), - .scaleOutDuration = st::fadeWrapDuration * 2, - }, _wrap.get()); - _replyArea->sendReaction(id.id); - unfocusReply(); + ) | rpl::start_with_next([=](Reactions::Chosen chosen) { + reactionChosen(chosen.mode, chosen.reaction); }, _lifetime); _delegate->storiesLayerShown( @@ -624,23 +589,17 @@ bool Controller::skipCaption() const { return _captionFullView != nullptr; } -bool Controller::liked() const { - return _liked.current(); +void Controller::toggleLiked() { + _reactions->toggleLiked(); } -rpl::producer Controller::likedValue() const { - return _liked.value(); -} - -void Controller::toggleLiked(bool liked) { - _liked = liked; - if (liked) { - startReactionAnimation({ - .id = HeartReactionId(), - .scaleOutDuration = st::fadeWrapDuration * 2, - .effectOnly = true, - }, _replyArea->likeAnimationTarget()); +void Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) { + if (mode == ReactionsMode::Message) { + _replyArea->sendReaction(chosen.id); + } else if (const auto user = shownUser()) { + user->owner().stories().sendReaction(_shown, chosen.id); } + unfocusReply(); } void Controller::showFullCaption() { @@ -902,14 +861,15 @@ void Controller::show( _viewed = false; invalidate_weak_ptrs(&_viewsLoadGuard); _reactions->hide(); - if (_replyFocused) { + if (_replyArea->focused()) { unfocusReply(); } _replyArea->show({ .user = unsupported ? nullptr : user, .id = story->id(), - }); + }, _reactions->likedValue()); + _recentViews->show({ .list = story->recentViewers(), .reactions = story->reactions(), @@ -949,7 +909,8 @@ bool Controller::changeShown(Data::Story *story) { story, Data::Stories::Polling::Viewer); } - _liked = false; + _reactions->showLikeFrom(story); + const auto &locations = story ? story->locations() : std::vector(); @@ -1099,8 +1060,7 @@ void Controller::ready() { } _started = true; updatePlayingAllowed(); - uiShow()->session().data().reactions().preloadAnimationsFor( - HeartReactionId()); + _reactions->ready(); } void Controller::updateVideoPlayback(const Player::TrackState &state) { @@ -1291,7 +1251,7 @@ void Controller::contentPressed(bool pressed) { _captionFullView->close(); } if (pressed) { - _reactions->collapse(); + _reactions->outsidePressed(); } } @@ -1607,28 +1567,6 @@ void Controller::updatePowerSaveBlocker(const Player::TrackState &state) { [=] { return _wrap->window()->windowHandle(); }); } -void Controller::startReactionAnimation( - Ui::ReactionFlyAnimationArgs args, - not_null target) { - Expects(shown()); - - _reactionAnimation = std::make_unique( - _wrap, - &shownUser()->owner().reactions(), - std::move(args), - [=] { _reactionAnimation->repaint(); }, - Data::CustomEmojiSizeTag::Isolated); - const auto layer = _reactionAnimation->layer(); - _wrap->paintRequest() | rpl::start_with_next([=] { - if (!_reactionAnimation->paintBadgeFrame(target)) { - InvokeQueued(layer, [=] { - _reactionAnimation = nullptr; - _wrap->update(); - }); - } - }, layer->lifetime()); -} - Ui::Toast::Config PrepareTogglePinnedToast(int count, bool pinned) { return { .text = (pinned diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index b4a37cc57..d7a9c37ff 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -26,17 +26,16 @@ struct FileChosen; namespace Data { struct FileOrigin; -struct ReactionId; +class DocumentMedia; } // namespace Data namespace HistoryView::Reactions { class CachedIconFactory; +struct ChosenReaction; } // namespace HistoryView::Reactions namespace Ui { class RpWidget; -struct ReactionFlyAnimationArgs; -class EmojiFlyAnimation; class BoxContent; } // namespace Ui @@ -66,6 +65,7 @@ struct SiblingView; enum class SiblingType; struct ContentLayout; class CaptionFullView; +enum class ReactionsMode; enum class HeaderLayout { Normal, @@ -118,9 +118,7 @@ public: [[nodiscard]] Data::FileOrigin fileOrigin() const; [[nodiscard]] TextWithEntities captionText() const; [[nodiscard]] bool skipCaption() const; - [[nodiscard]] bool liked() const; - [[nodiscard]] rpl::producer likedValue() const; - void toggleLiked(bool liked); + void toggleLiked(); void showFullCaption(); void captionClosing(); void captionClosed(); @@ -172,6 +170,9 @@ public: [[nodiscard]] rpl::lifetime &lifetime(); private: + class PhotoPlayback; + class Unsupported; + using ChosenReaction = HistoryView::Reactions::ChosenReaction; struct StoriesList { not_null user; Data::StoriesIds ids; @@ -194,8 +195,6 @@ private: float64 rotation = 0.; ClickHandlerPtr handler; }; - class PhotoPlayback; - class Unsupported; void initLayout(); bool changeShown(Data::Story *story); @@ -238,9 +237,7 @@ private: const std::vector &lists, int index); - void startReactionAnimation( - Ui::ReactionFlyAnimationArgs from, - not_null target); + void reactionChosen(ReactionsMode mode, ChosenReaction chosen); const not_null _delegate; @@ -260,9 +257,7 @@ private: bool _contentFaded = false; bool _windowActive = false; - bool _replyFocused = false; bool _replyActive = false; - bool _hasSendText = false; bool _layerShown = false; bool _menuShown = false; bool _tooltipShown = false; @@ -273,7 +268,6 @@ private: Data::StoriesContext _context; std::optional _source; std::optional _list; - rpl::variable _liked; FullStoryId _waitingForId; int _waitingForDelta = 0; int _index = 0; @@ -297,7 +291,6 @@ private: std::unique_ptr _siblingRight; std::unique_ptr _powerSaveBlocker; - std::unique_ptr _reactionAnimation; Main::Session *_session = nullptr; rpl::lifetime _sessionLifetime; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index 9d4265758..01cdcebec 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -7,13 +7,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_reactions.h" +#include "base/event_filter.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/compose/compose_show.h" +#include "data/data_changes.h" +#include "data/data_document.h" +#include "data/data_document_media.h" #include "data/data_message_reactions.h" #include "data/data_session.h" #include "history/view/reactions/history_view_reactions_selector.h" #include "main/main_session.h" #include "media/stories/media_stories_controller.h" +#include "ui/effects/emoji_fly_animation.h" +#include "ui/effects/reaction_fly_animation.h" +#include "ui/animated_icon.h" +#include "ui/painter.h" #include "styles/style_chat_helpers.h" #include "styles/style_media_view.h" #include "styles/style_widgets.h" @@ -21,6 +29,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Media::Stories { namespace { +constexpr auto kReactionScaleOutTarget = 0.7; +constexpr auto kReactionScaleOutDuration = crl::time(1000); +constexpr auto kMessageReactionScaleOutDuration = crl::time(400); + +[[nodiscard]] Data::ReactionId HeartReactionId() { + return { QString() + QChar(10084) }; +} + [[nodiscard]] Data::PossibleItemReactionsRef LookupPossibleReactions( not_null session) { auto result = Data::PossibleItemReactionsRef(); @@ -51,7 +67,50 @@ namespace { } // namespace -struct Reactions::Hiding { +class Reactions::Panel final { +public: + explicit Panel(not_null controller); + ~Panel(); + + [[nodiscard]] rpl::producer expandedValue() const { + return _expanded.value(); + } + [[nodiscard]] rpl::producer shownValue() const { + return _shown.value(); + } + + [[nodiscard]] rpl::producer chosen() const; + + void show(Mode mode); + void hide(Mode mode); + void hideIfCollapsed(Mode mode); + void collapse(Mode mode); + + void attachToReactionButton(not_null button); + +private: + struct Hiding; + + void create(); + void updateShowState(); + void fadeOutSelector(); + void startAnimation(); + + const not_null _controller; + + std::unique_ptr _parent; + std::unique_ptr _selector; + std::vector> _hiding; + rpl::event_stream _chosen; + Ui::Animations::Simple _showing; + rpl::variable _shownValue; + rpl::variable _expanded; + rpl::variable _mode; + rpl::variable _shown = false; + +}; + +struct Reactions::Panel::Hiding { explicit Hiding(not_null parent) : widget(parent) { } @@ -60,16 +119,24 @@ struct Reactions::Hiding { QImage frame; }; -Reactions::Reactions(not_null controller) +Reactions::Panel::Panel(not_null controller) : _controller(controller) { } -Reactions::~Reactions() = default; +Reactions::Panel::~Panel() = default; -void Reactions::show() { - if (_shown) { +auto Reactions::Panel::chosen() const -> rpl::producer { + return _chosen.events(); +} + +void Reactions::Panel::show(Mode mode) { + const auto was = _mode.current(); + if (_shown.current() && was == mode) { return; + } else if (_shown.current()) { + hide(was); } + _mode = mode; create(); if (!_selector) { return; @@ -82,8 +149,8 @@ void Reactions::show() { _parent->show(); } -void Reactions::hide() { - if (!_selector) { +void Reactions::Panel::hide(Mode mode) { + if (!_selector || _mode.current() != mode) { return; } _selector->beforeDestroy(); @@ -97,20 +164,32 @@ void Reactions::hide() { _parent = nullptr; } -void Reactions::hideIfCollapsed() { - if (!_expanded.current()) { - hide(); +void Reactions::Panel::hideIfCollapsed(Mode mode) { + if (!_expanded.current() && _mode.current() == mode) { + hide(mode); } } -void Reactions::collapse() { - if (_expanded.current()) { - hide(); - show(); +void Reactions::Panel::collapse(Mode mode) { + if (_expanded.current() && _mode.current() == mode) { + hide(mode); + show(mode); } } -void Reactions::create() { +void Reactions::Panel::attachToReactionButton(not_null button) { + base::install_event_filter(button, [=](not_null e) { + if (e->type() == QEvent::ContextMenu && !button->isHidden()) { + show(Reactions::Mode::Reaction); + return base::EventFilterResult::Cancel; + } else if (e->type() == QEvent::Hide) { + hide(Reactions::Mode::Reaction); + } + return base::EventFilterResult::Continue; + }); +} + +void Reactions::Panel::create() { auto reactions = LookupPossibleReactions( &_controller->uiShow()->session()); if (reactions.recent.empty() && !reactions.morePremiumAvailable) { @@ -119,13 +198,19 @@ void Reactions::create() { _parent = std::make_unique(_controller->wrap().get()); _parent->show(); + const auto mode = _mode.current(); + _parent->events() | rpl::start_with_next([=](not_null e) { if (e->type() == QEvent::MouseButtonPress) { const auto event = static_cast(e.get()); if (event->button() == Qt::LeftButton) { if (!_selector || !_selector->geometry().contains(event->pos())) { - collapse(); + if (mode == Mode::Message) { + collapse(mode); + } else { + hide(mode); + } } } } @@ -137,17 +222,17 @@ void Reactions::create() { _controller->uiShow(), std::move(reactions), _controller->cachedReactionIconFactory().createMethod(), - [=](bool fast) { hide(); }); + [=](bool fast) { hide(mode); }); _selector->chosen( ) | rpl::start_with_next([=]( HistoryView::Reactions::ChosenReaction reaction) { - _chosen.fire_copy(reaction); - hide(); + _chosen.fire({ .reaction = reaction, .mode = mode }); + hide(mode); }, _selector->lifetime()); _selector->premiumPromoChosen() | rpl::start_with_next([=] { - hide(); + hide(mode); ShowPremiumPreviewBox( _controller->uiShow(), PremiumPreview::InfiniteReactions); @@ -165,13 +250,23 @@ void Reactions::create() { _controller->layoutValue(), _shownValue.value() ) | rpl::start_with_next([=](const Layout &layout, float64 shown) { - const auto shift = int(base::SafeRound((full / 2.) * shown)); - _parent->setGeometry(QRect( - layout.reactions.x() + layout.reactions.width() / 2 - shift, - layout.reactions.y(), - full, - layout.reactions.height())); - const auto innerTop = layout.reactions.height() + const auto width = extents.left() + + _selector->countAppearedWidth(shown) + + extents.right(); + const auto height = layout.reactions.height(); + const auto shift = (width / 2); + const auto right = (mode == Mode::Message) + ? (layout.reactions.x() + layout.reactions.width() / 2 + shift) + : (layout.controlsBottomPosition.x() + + layout.controlsWidth + - st::storiesLikeReactionsPosition.x()); + const auto top = (mode == Mode::Message) + ? layout.reactions.y() + : (layout.controlsBottomPosition.y() + - height + - st::storiesLikeReactionsPosition.y()); + _parent->setGeometry(QRect((right - width), top, full, height)); + const auto innerTop = height - st::storiesReactionsBottomSkip - st::reactStripHeight; const auto maxAdded = innerTop - extents.top() - categoriesTop; @@ -186,11 +281,15 @@ void Reactions::create() { }, _selector->lifetime()); _selector->escapes() | rpl::start_with_next([=] { - collapse(); + if (mode == Mode::Message) { + collapse(mode); + } else { + hide(mode); + } }, _selector->lifetime()); } -void Reactions::fadeOutSelector() { +void Reactions::Panel::fadeOutSelector() { const auto wrap = _controller->wrap().get(); const auto geometry = Ui::MapFrom( wrap, @@ -226,8 +325,8 @@ void Reactions::fadeOutSelector() { }); } -void Reactions::updateShowState() { - const auto progress = _showing.value(_shown ? 1. : 0.); +void Reactions::Panel::updateShowState() { + const auto progress = _showing.value(_shown.current() ? 1. : 0.); const auto opacity = 1.; const auto appearing = _showing.animating(); const auto toggling = false; @@ -235,4 +334,355 @@ void Reactions::updateShowState() { _selector->updateShowState(progress, opacity, appearing, toggling); } +Reactions::Reactions(not_null controller) +: _controller(controller) +, _panel(std::make_unique(_controller)) { + _panel->chosen() | rpl::start_with_next([=](Chosen &&chosen) { + animateAndProcess(std::move(chosen)); + }, _lifetime); +} + +Reactions::~Reactions() = default; + +rpl::producer Reactions::activeValue() const { + using namespace rpl::mappers; + return rpl::combine( + _panel->expandedValue(), + _panel->shownValue(), + _1 || _2); +} + +auto Reactions::chosen() const -> rpl::producer { + return _chosen.events(); +} + +void Reactions::setReplyFieldState( + rpl::producer focused, + rpl::producer hasSendText) { + std::move( + focused + ) | rpl::start_with_next([=](bool focused) { + _replyFocused = focused; + if (!_replyFocused) { + _panel->hideIfCollapsed(Reactions::Mode::Message); + } else if (!_hasSendText) { + _panel->show(Reactions::Mode::Message); + } + }, _lifetime); + + std::move( + hasSendText + ) | rpl::start_with_next([=](bool has) { + _hasSendText = has; + if (_replyFocused) { + if (_hasSendText) { + _panel->hide(Reactions::Mode::Message); + } else { + _panel->show(Reactions::Mode::Message); + } + } + }, _lifetime); +} + +void Reactions::attachToReactionButton(not_null button) { + _likeButton = button; + _panel->attachToReactionButton(button); +} + +Data::ReactionId Reactions::liked() const { + return _liked.current(); +} + +rpl::producer Reactions::likedValue() const { + return _liked.value(); +} + +void Reactions::showLikeFrom(Data::Story *story) { + setLikedIdFrom(story); + + if (!story) { + _likeFromLifetime.destroy(); + return; + } + _likeFromLifetime = story->session().changes().storyUpdates( + story, + Data::StoryUpdate::Flag::Reaction + ) | rpl::start_with_next([=](const Data::StoryUpdate &update) { + setLikedIdFrom(update.story); + }); +} + +void Reactions::hide() { + _panel->hide(Reactions::Mode::Message); + _panel->hide(Reactions::Mode::Reaction); +} + +void Reactions::outsidePressed() { + _panel->hide(Reactions::Mode::Reaction); + _panel->collapse(Reactions::Mode::Message); +} + +void Reactions::toggleLiked() { + const auto liked = !_liked.current().empty(); + const auto now = liked ? Data::ReactionId() : HeartReactionId(); + if (_liked.current() != now) { + animateAndProcess({ { .id = now }, ReactionsMode::Reaction }); + } +} + +void Reactions::ready() { + if (const auto story = _controller->story()) { + story->owner().reactions().preloadAnimationsFor(HeartReactionId()); + } +} + +void Reactions::animateAndProcess(Chosen &&chosen) { + const auto like = (chosen.mode == Mode::Reaction); + const auto wrap = _controller->wrap(); + const auto target = like ? _likeButton : wrap.get(); + const auto story = _controller->story(); + if (!story || !target) { + return; + } + + auto done = like + ? setLikedIdIconInit(&story->owner(), chosen.reaction.id) + : Fn(); + const auto scaleOutDuration = like + ? kReactionScaleOutDuration + : kMessageReactionScaleOutDuration; + const auto scaleOutTarget = like ? kReactionScaleOutTarget : 0.; + + if (!chosen.reaction.id.empty()) { + startReactionAnimation({ + .id = chosen.reaction.id, + .flyIcon = chosen.reaction.icon, + .flyFrom = (chosen.reaction.globalGeometry.isEmpty() + ? QRect() + : wrap->mapFromGlobal(chosen.reaction.globalGeometry)), + .scaleOutDuration = scaleOutDuration, + .scaleOutTarget = scaleOutTarget, + }, target, std::move(done)); + } + + _chosen.fire(std::move(chosen)); +} + +void Reactions::assignLikedId(Data::ReactionId id) { + invalidate_weak_ptrs(&_likeIconGuard); + _likeIcon = nullptr; + _liked = id; +} + +Fn Reactions::setLikedIdIconInit( + not_null owner, + Data::ReactionId id, + bool force) { + if (_liked.current() != id) { + _likeIconMedia = nullptr; + } else if (!force) { + return nullptr; + } + assignLikedId(id); + if (id.empty() || !_likeButton) { + return nullptr; + } + return crl::guard(&_likeIconGuard, [=](Ui::ReactionFlyCenter center) { + if (!id.custom() && !center.icon && !_likeIconMedia) { + waitForLikeIcon(owner, id); + } else { + initLikeIcon(owner, id, std::move(center)); + } + }); +} + +void Reactions::initLikeIcon( + not_null owner, + Data::ReactionId id, + Ui::ReactionFlyCenter center) { + Expects(_likeButton != nullptr); + + _likeIcon = std::make_unique(_likeButton); + const auto icon = _likeIcon.get(); + icon->show(); + _likeButton->sizeValue() | rpl::start_with_next([=](QSize size) { + icon->setGeometry(QRect(QPoint(), size)); + }, icon->lifetime()); + + if (!id.custom() && !center.icon) { + return; + } + + struct State { + Ui::ReactionFlyCenter center; + QImage cache; + }; + const auto fly = icon->lifetime().make_state(State{ + .center = std::move(center), + }); + if (const auto customId = id.custom()) { + auto withCorrectCallback = owner->customEmojiManager().create( + customId, + [=] { icon->update(); }, + Data::CustomEmojiSizeTag::Isolated); + [[maybe_unused]] const auto load = withCorrectCallback->ready(); + fly->center.custom = std::move(withCorrectCallback); + fly->center.icon = nullptr; + } else { + fly->center.icon->jumpToStart(nullptr); + fly->center.custom = nullptr; + } + const auto paintNonCached = [=](QPainter &p) { + auto hq = PainterHighQualityEnabler(p); + + const auto size = fly->center.size; + const auto target = QRect( + (icon->width() - size) / 2, + (icon->height() - size) / 2, + size, + size); + const auto scale = fly->center.scale; + if (scale < 1.) { + const auto shift = QRectF(target).center(); + p.translate(shift); + p.scale(scale, scale); + p.translate(-shift); + } + const auto multiplier = fly->center.centerSizeMultiplier; + const auto inner = int(base::SafeRound(size * multiplier)); + if (const auto icon = fly->center.icon.get()) { + const auto rect = QRect( + target.x() + (target.width() - inner) / 2, + target.y() + (target.height() - inner) / 2, + inner, + inner); + p.drawImage(rect, icon->frame(st::windowFg->c)); + } else { + const auto customSize = fly->center.customSize; + const auto scaled = (inner != customSize); + fly->center.custom->paint(p, { + .textColor = st::windowFg->c, + .size = { customSize, customSize }, + .now = crl::now(), + .scale = (scaled ? (inner / float64(customSize)) : 1.), + .position = QPoint( + target.x() + (target.width() - customSize) / 2, + target.y() + (target.height() - customSize) / 2), + .scaled = scaled, + }); + } + }; + icon->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icon); + if (!fly->cache.isNull()) { + p.drawImage(0, 0, fly->cache); + } else if (fly->center.icon + || fly->center.custom->readyInDefaultState()) { + const auto ratio = style::DevicePixelRatio(); + fly->cache = QImage( + icon->size() * ratio, + QImage::Format_ARGB32_Premultiplied); + fly->cache.setDevicePixelRatio(ratio); + fly->cache.fill(Qt::transparent); + auto q = QPainter(&fly->cache); + paintNonCached(q); + q.end(); + + fly->center.icon = nullptr; + fly->center.custom = nullptr; + p.drawImage(0, 0, fly->cache); + } else { + paintNonCached(p); + } + }, icon->lifetime()); +} + +void Reactions::waitForLikeIcon( + not_null owner, + Data::ReactionId id) { + _likeIconWaitLifetime = rpl::single( + rpl::empty + ) | rpl::then( + owner->reactions().defaultUpdates() + ) | rpl::map([=]() -> rpl::producer { + const auto &list = owner->reactions().list( + Data::Reactions::Type::All); + const auto i = ranges::find(list, id, &Data::Reaction::id); + if (i == end(list)) { + return rpl::single(false); + } + const auto document = i->centerIcon + ? not_null(i->centerIcon) + : i->selectAnimation; + _likeIconMedia = document->createMediaView(); + _likeIconMedia->checkStickerLarge(); + return rpl::single( + rpl::empty + ) | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::map([=] { + return _likeIconMedia->loaded(); + }); + }) | rpl::flatten_latest( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + setLikedId(owner, id, true); + + crl::on_main(&_likeIconGuard, [=] { + _likeIconMedia = nullptr; + _likeIconWaitLifetime.destroy(); + }); + }); +} + +void Reactions::setLikedIdFrom(Data::Story *story) { + if (!story) { + assignLikedId({}); + } else { + setLikedId(&story->owner(), story->sentReactionId()); + } +} + +void Reactions::setLikedId( + not_null owner, + Data::ReactionId id, + bool force) { + if (const auto done = setLikedIdIconInit(owner, id, force)) { + const auto reactions = &owner->reactions(); + done(Ui::EmojiFlyAnimation(_controller->wrap(), reactions, { + .id = id, + .scaleOutDuration = kReactionScaleOutDuration, + .scaleOutTarget = kReactionScaleOutTarget, + }, [] {}, Data::CustomEmojiSizeTag::Isolated).grabBadgeCenter()); + } +} + +void Reactions::startReactionAnimation( + Ui::ReactionFlyAnimationArgs args, + not_null target, + Fn done) { + const auto wrap = _controller->wrap(); + const auto story = _controller->story(); + _reactionAnimation = std::make_unique( + wrap, + &story->owner().reactions(), + std::move(args), + [=] { _reactionAnimation->repaint(); }, + Data::CustomEmojiSizeTag::Isolated); + const auto layer = _reactionAnimation->layer(); + wrap->paintRequest() | rpl::start_with_next([=] { + if (!_reactionAnimation->paintBadgeFrame(target)) { + InvokeQueued(layer, [=] { + _reactionAnimation = nullptr; + wrap->update(); + }); + if (done) { + done(_reactionAnimation->grabBadgeCenter()); + } + } + }, layer->lifetime()); + wrap->update(); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.h b/Telegram/SourceFiles/media/stories/media_stories_reactions.h index ca2200d7e..5a16943a7 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.h @@ -7,10 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/data_message_reaction_id.h" #include "ui/effects/animations.h" namespace Data { +class DocumentMedia; struct ReactionId; +class Session; +class Story; } // namespace Data namespace HistoryView::Reactions { @@ -20,47 +24,96 @@ struct ChosenReaction; namespace Ui { class RpWidget; +struct ReactionFlyAnimationArgs; +struct ReactionFlyCenter; +class EmojiFlyAnimation; } // namespace Ui namespace Media::Stories { class Controller; +enum class ReactionsMode { + Message, + Reaction, +}; + class Reactions final { public: explicit Reactions(not_null controller); ~Reactions(); - using Chosen = HistoryView::Reactions::ChosenReaction; - [[nodiscard]] rpl::producer expandedValue() const { - return _expanded.value(); - } - [[nodiscard]] rpl::producer chosen() const { - return _chosen.events(); - } + using Mode = ReactionsMode; + + template + struct ChosenWrap { + Reaction reaction; + Mode mode; + }; + using Chosen = ChosenWrap; + + [[nodiscard]] rpl::producer activeValue() const; + [[nodiscard]] rpl::producer chosen() const; + + [[nodiscard]] Data::ReactionId liked() const; + [[nodiscard]] rpl::producer likedValue() const; + void showLikeFrom(Data::Story *story); - void show(); void hide(); - void hideIfCollapsed(); - void collapse(); + void outsidePressed(); + void toggleLiked(); + void ready(); + + void setReplyFieldState( + rpl::producer focused, + rpl::producer hasSendText); + void attachToReactionButton(not_null button); private: - struct Hiding; + class Panel; - void create(); - void updateShowState(); - void fadeOutSelector(); + void animateAndProcess(Chosen &&chosen); + + void assignLikedId(Data::ReactionId id); + [[nodiscard]] Fn setLikedIdIconInit( + not_null owner, + Data::ReactionId id, + bool force = false); + void setLikedIdFrom(Data::Story *story); + void setLikedId( + not_null owner, + Data::ReactionId id, + bool force = false); + void startReactionAnimation( + Ui::ReactionFlyAnimationArgs from, + not_null target, + Fn done = nullptr); + void waitForLikeIcon( + not_null owner, + Data::ReactionId id); + void initLikeIcon( + not_null owner, + Data::ReactionId id, + Ui::ReactionFlyCenter center); const not_null _controller; + const std::unique_ptr _panel; - std::unique_ptr _parent; - std::unique_ptr _selector; - std::vector> _hiding; rpl::event_stream _chosen; - Ui::Animations::Simple _showing; - rpl::variable _shownValue; - rpl::variable _expanded; - bool _shown = false; + bool _replyFocused = false; + bool _hasSendText = false; + + Ui::RpWidget *_likeButton = nullptr; + rpl::variable _liked; + base::has_weak_ptr _likeIconGuard; + std::unique_ptr _likeIcon; + std::shared_ptr _likeIconMedia; + + std::unique_ptr _reactionAnimation; + + rpl::lifetime _likeIconWaitLifetime; + rpl::lifetime _likeFromLifetime; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 53a5c3435..48ee62d2e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -623,7 +623,7 @@ void ReplyArea::initActions() { _controls->likeToggled( ) | rpl::start_with_next([=] { - _controller->toggleLiked(!_controller->liked()); + _controller->toggleLiked(); }, _lifetime); _controls->setMimeDataHook([=]( @@ -649,7 +649,9 @@ void ReplyArea::initActions() { _controls->showFinished(); } -void ReplyArea::show(ReplyAreaData data) { +void ReplyArea::show( + ReplyAreaData data, + rpl::producer likedValue) { if (_data == data) { return; } @@ -666,7 +668,11 @@ void ReplyArea::show(ReplyAreaData data) { const auto history = user ? user->owner().history(user).get() : nullptr; _controls->setHistory({ .history = history, - .liked = _controller->likedValue(), + .liked = std::move( + likedValue + ) | rpl::map([](const Data::ReactionId &id) { + return !id.empty(); + }), }); _controls->clear(); const auto hidden = user && user->isSelf(); @@ -697,6 +703,10 @@ Main::Session &ReplyArea::session() const { return _data.user->session(); } +bool ReplyArea::focused() const { + return _controls->focused(); +} + rpl::producer ReplyArea::focusedValue() const { return _controls->focusedValue(); } @@ -725,7 +735,7 @@ void ReplyArea::tryProcessKeyInput(not_null e) { _controls->tryProcessKeyInput(e); } -not_null ReplyArea::likeAnimationTarget() const { +not_null ReplyArea::likeAnimationTarget() const { return _controls->likeAnimationTarget(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index c3c13abdf..34f40b856 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -41,6 +41,7 @@ class Session; namespace Ui { struct PreparedList; class SendFilesWay; +class RpWidget; } // namespace Ui namespace Media::Stories { @@ -60,9 +61,12 @@ public: explicit ReplyArea(not_null controller); ~ReplyArea(); - void show(ReplyAreaData data); + void show( + ReplyAreaData data, + rpl::producer likedValue); void sendReaction(const Data::ReactionId &id); + [[nodiscard]] bool focused() const; [[nodiscard]] rpl::producer focusedValue() const; [[nodiscard]] rpl::producer activeValue() const; [[nodiscard]] rpl::producer hasSendTextValue() const; @@ -70,7 +74,7 @@ public: [[nodiscard]] bool ignoreWindowMove(QPoint position) const; void tryProcessKeyInput(not_null e); - [[nodiscard]] not_null likeAnimationTarget() const; + [[nodiscard]] not_null likeAnimationTarget() const; private: class Cant; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 5eb311f76..0d6f31042 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -682,7 +682,7 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { attach: storiesAttach; emoji: storiesAttachEmoji; like: storiesLike; - liked: icon{{ "chat/input_liked", settingsIconBg1 }}; + liked: icon{}; suggestions: EmojiSuggestions(defaultEmojiSuggestions) { dropdown: InnerDropdown(emojiSuggestionsDropdown) { animation: PanelAnimation(defaultPanelAnimation) { @@ -807,6 +807,7 @@ storiesReactionsPan: EmojiPan(storiesEmojiPan) { storiesReactionsWidth: 210px; storiesReactionsBottomSkip: 29px; storiesReactionsAddedTop: 200px; +storiesLikeReactionsPosition: point(85px, 30px); storiesUnsupportedLabel: FlatLabel(defaultFlatLabel) { textFg: mediaviewControlFg; diff --git a/Telegram/SourceFiles/ui/effects/emoji_fly_animation.cpp b/Telegram/SourceFiles/ui/effects/emoji_fly_animation.cpp index c655ab678..16269c3ac 100644 --- a/Telegram/SourceFiles/ui/effects/emoji_fly_animation.cpp +++ b/Telegram/SourceFiles/ui/effects/emoji_fly_animation.cpp @@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/emoji_fly_animation.h" #include "data/stickers/data_custom_emoji.h" +#include "ui/text/text_custom_emoji.h" +#include "ui/animated_icon.h" #include "styles/style_info.h" #include "styles/style_chat.h" @@ -100,4 +102,10 @@ bool EmojiFlyAnimation::paintBadgeFrame(not_null widget) { return !_fly.finished(); } +ReactionFlyCenter EmojiFlyAnimation::grabBadgeCenter() { + auto result = _fly.takeCenter(); + result.size = _flySize; + return result; +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/emoji_fly_animation.h b/Telegram/SourceFiles/ui/effects/emoji_fly_animation.h index f5dc8102b..f868c7267 100644 --- a/Telegram/SourceFiles/ui/effects/emoji_fly_animation.h +++ b/Telegram/SourceFiles/ui/effects/emoji_fly_animation.h @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { +struct ReactionFlyCenter; + class EmojiFlyAnimation { public: EmojiFlyAnimation( @@ -26,6 +28,7 @@ public: void repaint(); bool paintBadgeFrame(not_null widget); + [[nodiscard]] ReactionFlyCenter grabBadgeCenter(); private: const int _flySize = 0; diff --git a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp index e46820573..8bf354c17 100644 --- a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp +++ b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp @@ -68,7 +68,8 @@ ReactionFlyAnimation::ReactionFlyAnimation( : _owner(owner) , _repaint(std::move(repaint)) , _flyFrom(args.flyFrom) -, _scaleOutDuration(args.scaleOutDuration) { +, _scaleOutDuration(args.scaleOutDuration) +, _scaleOutTarget(args.scaleOutTarget) { const auto &list = owner->list(::Data::Reactions::Type::All); auto centerIcon = (DocumentData*)nullptr; auto aroundAnimation = (DocumentData*)nullptr; @@ -86,12 +87,14 @@ ReactionFlyAnimation::ReactionFlyAnimation( aroundAnimation = owner->chooseGenericAnimation(document); } else { const auto i = ranges::find(list, args.id, &::Data::Reaction::id); - if (i == end(list) || !i->centerIcon) { + if (i == end(list)/* || !i->centerIcon*/) { return; } - centerIcon = i->centerIcon; + centerIcon = i->centerIcon + ? not_null(i->centerIcon) + : i->selectAnimation; aroundAnimation = i->aroundAnimation; - _centerSizeMultiplier = 1.; + _centerSizeMultiplier = i->centerIcon ? 1. : 0.5; } const auto resolve = [&]( std::unique_ptr &icon, @@ -139,21 +142,31 @@ QRect ReactionFlyAnimation::paintGetArea( QRect clip, crl::time now) const { const auto scale = [&] { - const auto rate = _effect ? _effect->frameRate() : 0.; - if (!_scaleOutDuration || !rate) { + if (!_scaleOutDuration + || (!_effect && !_noEffectScaleStarted)) { return 1.; } - const auto left = _effect->framesCount() - _effect->frameIndex(); - const auto duration = left * 1000. / rate; - return (duration < _scaleOutDuration) - ? (duration / double(_scaleOutDuration)) - : 1.; + auto progress = _noEffectScaleAnimation.value(0.); + if (_effect) { + const auto rate = _effect->frameRate(); + if (!rate) { + return 1.; + } + const auto left = _effect->framesCount() - _effect->frameIndex(); + const auto duration = left * 1000. / rate; + progress = (duration < _scaleOutDuration) + ? (duration / double(_scaleOutDuration)) + : 1.; + } + return (1. * progress + _scaleOutTarget * (1. - progress)); }(); + auto hq = std::optional(); if (scale < 1.) { - const auto delta = ((1. - scale) / 2.) * target.size(); - target = QRect( - target.topLeft() + QPoint(delta.width(), delta.height()), - target.size() * scale); + hq.emplace(p); + const auto shift = QRectF(target).center(); + p.translate(shift); + p.scale(scale, scale); + p.translate(-shift); } if (!_valid) { return QRect(); @@ -169,8 +182,10 @@ QRect ReactionFlyAnimation::paintGetArea( if (clip.isEmpty() || area.intersects(clip)) { paintCenterFrame(p, target, colored, now); if (const auto effect = _effect.get()) { - // Must not be colored to text. - p.drawImage(wide, effect->frame(QColor())); + if (effect->animating()) { + // Must not be colored to text. + p.drawImage(wide, effect->frame(QColor())); + } } paintMiniCopies(p, target.center(), colored, now); } @@ -359,6 +374,9 @@ void ReactionFlyAnimation::startAnimations() { } if (const auto effect = _effect.get()) { _effect->animate(callback()); + } else if (_scaleOutDuration > 0) { + _noEffectScaleStarted = true; + _noEffectScaleAnimation.start(callback(), 1, 0, _scaleOutDuration); } if (!_miniCopies.empty()) { _minis.start(callback(), 0., 1., kMiniCopiesDurationMax); @@ -382,7 +400,19 @@ bool ReactionFlyAnimation::finished() const { || (_flyIcon.isNull() && (!_center || !_center->animating()) && (!_effect || !_effect->animating()) + && !_noEffectScaleAnimation.animating() && !_minis.animating()); } +ReactionFlyCenter ReactionFlyAnimation::takeCenter() { + _valid = false; + return { + .custom = std::move(_custom), + .icon = std::move(_center), + .scale = (_scaleOutDuration > 0) ? _scaleOutTarget : 1., + .centerSizeMultiplier = _centerSizeMultiplier, + .customSize = _customSize, + }; +} + } // namespace HistoryView::Reactions diff --git a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h index 00d9d2e8d..340deffa0 100644 --- a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h +++ b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h @@ -28,11 +28,21 @@ struct ReactionFlyAnimationArgs { QImage flyIcon; QRect flyFrom; crl::time scaleOutDuration = 0; + float64 scaleOutTarget = 0.; bool effectOnly = false; [[nodiscard]] ReactionFlyAnimationArgs translated(QPoint point) const; }; +struct ReactionFlyCenter { + std::unique_ptr custom; + std::unique_ptr icon; + float64 scale = 0.; + float64 centerSizeMultiplier = 0.; + int customSize = 0; + int size = 0; +}; + class ReactionFlyAnimation final { public: ReactionFlyAnimation( @@ -56,6 +66,8 @@ public: [[nodiscard]] float64 flyingProgress() const; [[nodiscard]] bool finished() const; + [[nodiscard]] ReactionFlyCenter takeCenter(); + private: struct Parabolic { float64 a = 0.; @@ -98,6 +110,7 @@ private: std::unique_ptr _custom; std::unique_ptr _center; std::unique_ptr _effect; + Animations::Simple _noEffectScaleAnimation; std::vector _miniCopies; Animations::Simple _fly; Animations::Simple _minis; @@ -105,6 +118,8 @@ private: float64 _centerSizeMultiplier = 0.; int _customSize = 0; crl::time _scaleOutDuration = 0; + float64 _scaleOutTarget = 0.; + bool _noEffectScaleStarted = false; bool _valid = false; mutable Parabolic _cached;