diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp index a5600c396..32f759d1c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp @@ -35,7 +35,6 @@ void AddReactionIcon( std::shared_ptr media; std::unique_ptr icon; QImage image; - rpl::lifetime downloadLifetime; }; const auto size = st::editPeerReactionsPreview; @@ -60,7 +59,6 @@ void AddReactionIcon( .sizeOverride = QSize(size, size), .frame = -1, }); - state->downloadLifetime.destroy(); state->media = nullptr; }; state->media->checkStickerLarge(); @@ -70,10 +68,10 @@ void AddReactionIcon( document->session().downloaderTaskFinished( ) | rpl::filter([=] { return state->media->loaded(); - }) | rpl::start_with_next([=] { + }) | rpl::take(1) | rpl::start_with_next([=] { initLottie(); icon->update(); - }, state->downloadLifetime); + }, icon->lifetime()); } icon->paintRequest( diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index bf669ac52..c1b041fb4 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -26,7 +26,7 @@ namespace { constexpr auto kRefreshFullListEach = 60 * 60 * crl::time(1000); constexpr auto kPollEach = 20 * crl::time(1000); -constexpr auto kSizeForDownscale = 128; +constexpr auto kSizeForDownscale = 64; } // namespace @@ -163,7 +163,7 @@ void Reactions::loadImage( } void Reactions::setLottie(ImageSet &set) { - const auto size = kSizeForDownscale / style::DevicePixelRatio(); + const auto size = style::ConvertScale(kSizeForDownscale); set.icon = std::make_unique(Lottie::IconDescriptor{ .path = set.media->owner()->filepath(true), .json = set.media->bytes(), diff --git a/Telegram/SourceFiles/history/view/history_view_react_button.cpp b/Telegram/SourceFiles/history/view/history_view_react_button.cpp index 36dd84ddc..dfc3db8f5 100644 --- a/Telegram/SourceFiles/history/view/history_view_react_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_react_button.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_message_reactions.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "lottie/lottie_icon.h" #include "main/main_session.h" #include "base/event_filter.h" #include "styles/style_chat.h" @@ -20,17 +21,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView::Reactions { namespace { -constexpr auto kToggleDuration = crl::time(80); +constexpr auto kToggleDuration = crl::time(120); constexpr auto kActivateDuration = crl::time(150); -constexpr auto kExpandDuration = crl::time(150); +constexpr auto kExpandDuration = crl::time(300); +constexpr auto kCollapseDuration = crl::time(250); constexpr auto kBgCacheIndex = 0; constexpr auto kShadowCacheIndex = 0; constexpr auto kEmojiCacheIndex = 1; constexpr auto kMaskCacheIndex = 2; constexpr auto kCacheColumsCount = 3; constexpr auto kButtonShowDelay = crl::time(300); -constexpr auto kButtonExpandDelay = crl::time(300); -constexpr auto kButtonHideDelay = crl::time(200); +constexpr auto kButtonExpandDelay = crl::time(25); +constexpr auto kButtonHideDelay = crl::time(300); +constexpr auto kButtonExpandedHideDelay = crl::time(0); +constexpr auto kSizeForDownscale = 96; [[nodiscard]] QPoint LocalPosition(not_null e) { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) @@ -55,15 +59,22 @@ constexpr auto kButtonHideDelay = crl::time(200); return int(base::SafeRound(st::reactionCornerImage * scale)); } -[[nodiscard]] QImage PrepareMaxOtherReaction(QImage image) { - const auto size = CornerImageSize(1.); - const auto factor = style::DevicePixelRatio(); - auto result = image.scaled( - QSize(size, size) * factor, - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - result.setDevicePixelRatio(factor); - return result; +[[nodiscard]] int MainReactionSize() { + return style::ConvertScale(kSizeForDownscale); +} + +[[nodiscard]] std::shared_ptr CreateIcon( + not_null media, + int startFrame, + int size) { + Expects(media->loaded()); + + return std::make_shared(Lottie::IconDescriptor{ + .path = media->owner()->filepath(true), + .json = media->bytes(), + .sizeOverride = QSize(size, size), + .frame = startFrame, + }); } } // namespace @@ -73,6 +84,7 @@ Button::Button( ButtonParameters parameters, Fn hideMe) : _update(std::move(update)) +, _finalScale(ScaleForState(_state)) , _collapsed(QPoint(), CountOuterSize()) , _finalHeight(_collapsed.height()) , _expandTimer([=] { applyState(State::Inside, _update); }) @@ -83,7 +95,7 @@ Button::Button( Button::~Button() = default; bool Button::isHidden() const { - return (_state == State::Hidden) && !_scaleAnimation.animating(); + return (_state == State::Hidden) && !_opacityAnimation.animating(); } QRect Button::geometry() const { @@ -148,6 +160,7 @@ void Button::applyParameters( } _lastGlobalPosition = parameters.globalPointer; } + const auto wasInside = (_state == State::Inside); const auto state = (inside && !delayInside) ? State::Inside : active @@ -155,7 +168,9 @@ void Button::applyParameters( : State::Shown; applyState(state, update); if (parameters.outside && _state == State::Shown) { - _hideTimer.callOnce(kButtonHideDelay); + _hideTimer.callOnce(wasInside + ? kButtonExpandedHideDelay + : kButtonHideDelay); } else { _hideTimer.cancel(); } @@ -211,30 +226,49 @@ void Button::applyState(State state, Fn update) { _expandTimer.cancel(); _hideTimer.cancel(); } - const auto finalHeight = (state == State::Inside) + const auto finalHeight = (state == State::Hidden) + ? _heightAnimation.value(_finalHeight) + : (state == State::Inside) ? _expandedHeight : _collapsed.height(); if (_finalHeight != finalHeight) { - _heightAnimation.start( - [=] { updateGeometry(_update); }, - _finalHeight, - finalHeight, - kExpandDuration); + if (state == State::Hidden) { + _heightAnimation.stop(); + } else { + _heightAnimation.start( + [=] { updateGeometry(_update); }, + _finalHeight, + finalHeight, + (state == State::Inside + ? kExpandDuration + : kCollapseDuration), + anim::easeOutCirc); + } _finalHeight = finalHeight; } updateGeometry(update); if (_state == state) { return; } - const auto duration = (state == State::Hidden - || _state == State::Hidden) + const auto duration = (state == State::Hidden || _state == State::Hidden) ? kToggleDuration : kActivateDuration; - _scaleAnimation.start( + const auto finalScale = ScaleForState(state); + _opacityAnimation.start( [=] { _update(_geometry); }, - ScaleForState(_state), - ScaleForState(state), - duration); + OpacityForScale(ScaleForState(_state)), + OpacityForScale(ScaleForState(state)), + duration, + anim::sineInOut); + if (state != State::Hidden && _finalScale != finalScale) { + _scaleAnimation.start( + [=] { _update(_geometry); }, + _finalScale, + finalScale, + duration, + anim::sineInOut); + _finalScale = finalScale; + } _state = state; } @@ -256,7 +290,11 @@ float64 Button::OpacityForScale(float64 scale) { } float64 Button::currentScale() const { - return _scaleAnimation.value(ScaleForState(_state)); + return _scaleAnimation.value(_finalScale); +} + +float64 Button::currentOpacity() const { + return _opacityAnimation.value(OpacityForScale(ScaleForState(_state))); } Manager::Manager( @@ -355,40 +393,65 @@ void Manager::showButtonDelayed() { } void Manager::applyList(std::vector list) { - constexpr auto proj = &Data::Reaction::emoji; - if (ranges::equal(_list, list, ranges::equal_to{}, proj, proj)) { + constexpr auto predicate = []( + const Data::Reaction &a, + const Data::Reaction &b) { + return (a.emoji == b.emoji) + && (a.appearAnimation == b.appearAnimation) + && (a.selectAnimation == b.selectAnimation); + }; + if (ranges::equal(_list, list, predicate)) { return; } _list = std::move(list); _links = std::vector(_list.size()); if (_list.empty()) { _mainReactionMedia = nullptr; + _mainReactionLifetime.destroy(); + _icons.clear(); return; } - const auto main = _list.front().staticIcon; - if (_mainReactionMedia && _mainReactionMedia->owner() == main) { + const auto main = _list.front().selectAnimation; + if (_mainReactionMedia + && _mainReactionMedia->owner() == main) { + if (!_mainReactionLifetime) { + loadIcons(); + } return; } + _mainReactionLifetime.destroy(); _mainReactionMedia = main->createMediaView(); - if (const auto image = _mainReactionMedia->getStickerLarge()) { - setMainReactionImage(image->original()); + _mainReactionMedia->checkStickerLarge(); + if (_mainReactionMedia->loaded()) { + setMainReactionIcon(); } else { main->session().downloaderTaskFinished( - ) | rpl::map([=] { - return _mainReactionMedia->getStickerLarge(); - }) | rpl::filter_nullptr() | rpl::take( - 1 - ) | rpl::start_with_next([=](not_null image) { - setMainReactionImage(image->original()); + ) | rpl::filter([=] { + return _mainReactionMedia->loaded(); + }) | rpl::take(1) | rpl::start_with_next([=] { + setMainReactionIcon(); }, _mainReactionLifetime); } } -void Manager::setMainReactionImage(QImage image) { - _mainReactionImage = std::move(image); +void Manager::setMainReactionIcon() { + _mainReactionLifetime.destroy(); ranges::fill(_validBg, false); ranges::fill(_validEmoji, false); - loadOtherReactions(); + loadIcons(); + const auto i = _loadCache.find(_mainReactionMedia->owner()); + if (i != end(_loadCache) && i->second.icon) { + const auto &icon = i->second.icon; + if (icon->frameIndex() == icon->framesCount() - 1 + && icon->width() == MainReactionSize()) { + _mainReactionImage = i->second.icon->frame(); + return; + } + } + _mainReactionImage = CreateIcon( + _mainReactionMedia.get(), + -1, + MainReactionSize())->frame(); } QMargins Manager::innerMargins() const { @@ -408,41 +471,56 @@ QRect Manager::buttonInner(not_null button) const { return button->geometry().marginsRemoved(innerMargins()); } -void Manager::loadOtherReactions() { - for (const auto &reaction : _list) { - const auto icon = reaction.staticIcon; - if (_otherReactions.contains(icon)) { - continue; +bool Manager::checkIconLoaded(ReactionDocument &entry) const { + if (!entry.media) { + return true; + } else if (!entry.media->loaded()) { + return false; + } + const auto size = (entry.media == _mainReactionMedia) + ? MainReactionSize() + : CornerImageSize(1.); + entry.icon = CreateIcon(entry.media.get(), entry.startFrame, size); + entry.media = nullptr; + return true; +} + +void Manager::loadIcons() { + const auto load = [&](not_null document, int frame) { + if (const auto i = _loadCache.find(document); i != end(_loadCache)) { + return i->second.icon; } - auto &entry = _otherReactions.emplace(icon, OtherReactionImage{ - .media = icon->createMediaView(), - }).first->second; - if (const auto image = entry.media->getStickerLarge()) { - entry.image = PrepareMaxOtherReaction(image->original()); - entry.media = nullptr; - } else if (!_otherReactionsLifetime) { - icon->session().downloaderTaskFinished( + auto &entry = _loadCache.emplace(document).first->second; + entry.media = document->createMediaView(); + entry.media->checkStickerLarge(); + entry.startFrame = frame; + if (!checkIconLoaded(entry) && !_loadCacheLifetime) { + document->session().downloaderTaskFinished( ) | rpl::start_with_next([=] { - checkOtherReactions(); - }, _otherReactionsLifetime); + checkIcons(); + }, _loadCacheLifetime); } + return entry.icon; + }; + _icons.clear(); + for (const auto &reaction : _list) { + _icons.push_back({ + .appear = load(reaction.appearAnimation, 1), + .select = load(reaction.selectAnimation, -1), + }); } } -void Manager::checkOtherReactions() { +void Manager::checkIcons() { auto all = true; - for (auto &[icon, entry] : _otherReactions) { - if (entry.media) { - if (const auto image = entry.media->getStickerLarge()) { - entry.image = PrepareMaxOtherReaction(image->original()); - entry.media = nullptr; - } else { - all = false; - } + for (auto &[document, entry] : _loadCache) { + if (!checkIconLoaded(entry)) { + all = false; } } if (all) { - _otherReactionsLifetime.destroy(); + _loadCacheLifetime.destroy(); + loadIcons(); } } @@ -549,7 +627,7 @@ void Manager::paintButton( not_null button, int frameIndex, float64 scale) { - const auto opacity = Button::OpacityForScale(scale); + const auto opacity = button->currentOpacity(); if (opacity == 0.) { return; } @@ -630,25 +708,31 @@ void Manager::paintAllEmoji( auto hq = PainterHighQualityEnabler(p); const auto between = st::reactionCornerSkip; const auto oneHeight = st::reactionCornerSize.height() + between; - const auto oneSize = CornerImageSize(scale); + const auto finalSize = CornerImageSize(1.); + const auto remove = finalSize * (1. - scale) / 2.; + const auto basicTarget = QRectF(QRect( + _inner.x() + (_inner.width() - finalSize) / 2, + _inner.y() + (_inner.height() - finalSize) / 2, + finalSize, + finalSize + )).marginsRemoved({ remove, remove, remove, remove }); const auto expandUp = button->expandUp(); const auto shift = QPoint(0, oneHeight * (expandUp ? -1 : 1)); auto emojiPosition = mainEmojiPosition + QPoint(0, button->scroll() * (expandUp ? 1 : -1)); - for (const auto &reaction : _list) { - const auto inner = _inner.translated(emojiPosition); - const auto target = QRect( - inner.x() + (inner.width() - oneSize) / 2, - inner.y() + (inner.height() - oneSize) / 2, - oneSize, - oneSize); - if (target.intersects(clip)) { - const auto i = _otherReactions.find(reaction.staticIcon); - if (i != end(_otherReactions) && !i->second.image.isNull()) { - p.drawImage(target, i->second.image); - } - } + for (const auto &icon : _icons) { + const auto target = basicTarget.translated(emojiPosition); emojiPosition += shift; + + if (!target.intersects(clip)) { + continue; + } else if (icon.appear) { + const auto size = int(base::SafeRound(target.width())); + const auto frame = icon.appear->frame( + { size, size }, + [=] { if (_button) _buttonUpdate(_button->geometry()); }); + p.drawImage(target, frame.image); + } } } diff --git a/Telegram/SourceFiles/history/view/history_view_react_button.h b/Telegram/SourceFiles/history/view/history_view_react_button.h index d27023e0e..91f106ed1 100644 --- a/Telegram/SourceFiles/history/view/history_view_react_button.h +++ b/Telegram/SourceFiles/history/view/history_view_react_button.h @@ -24,6 +24,10 @@ using PaintContext = Ui::ChatPaintContext; struct TextState; } // namespace HistoryView +namespace Lottie { +class Icon; +} // namespace Lottie + namespace HistoryView::Reactions { enum class ExpandDirection { @@ -74,6 +78,7 @@ public: [[nodiscard]] QRect geometry() const; [[nodiscard]] int scroll() const; [[nodiscard]] float64 currentScale() const; + [[nodiscard]] float64 currentOpacity() const; [[nodiscard]] bool consumeWheelEvent(not_null e); [[nodiscard]] static float64 ScaleForState(State state); @@ -89,7 +94,9 @@ private: const Fn _update; State _state = State::Hidden; + float64 _finalScale = 0.; Ui::Animations::Simple _scaleAnimation; + Ui::Animations::Simple _opacityAnimation; Ui::Animations::Simple _heightAnimation; QRect _collapsed; @@ -131,9 +138,14 @@ public: } private: - struct OtherReactionImage { - QImage image; + struct ReactionDocument { std::shared_ptr media; + std::shared_ptr icon; + int startFrame = 0; + }; + struct ReactionIcons { + std::shared_ptr appear; + std::shared_ptr select; }; static constexpr auto kFramesCount = 30; @@ -164,7 +176,7 @@ private: const QImage &image, QRect source); - void setMainReactionImage(QImage image); + void setMainReactionIcon(); void applyPatternedShadow(const QColor &shadow); [[nodiscard]] QRect cacheRect(int frameIndex, int columnIndex) const; QRect validateShadow( @@ -181,12 +193,15 @@ private: [[nodiscard]] QMargins innerMargins() const; [[nodiscard]] QRect buttonInner() const; [[nodiscard]] QRect buttonInner(not_null button) const; - void loadOtherReactions(); - void checkOtherReactions(); + [[nodiscard]] ClickHandlerPtr computeButtonLink(QPoint position) const; [[nodiscard]] ClickHandlerPtr resolveButtonLink( const Data::Reaction &reaction) const; + [[nodiscard]] bool checkIconLoaded(ReactionDocument &entry) const; + void loadIcons(); + void checkIcons(); + rpl::event_stream _chosen; std::vector _list; mutable std::vector _links; @@ -205,10 +220,9 @@ private: QImage _mainReactionImage; rpl::lifetime _mainReactionLifetime; - base::flat_map< - not_null, - OtherReactionImage> _otherReactions; - rpl::lifetime _otherReactionsLifetime; + base::flat_map, ReactionDocument> _loadCache; + std::vector _icons; + rpl::lifetime _loadCacheLifetime; std::optional _scheduledParameters; base::Timer _buttonShowTimer; diff --git a/Telegram/lib_lottie b/Telegram/lib_lottie index 5b576ba9b..ab022b57a 160000 --- a/Telegram/lib_lottie +++ b/Telegram/lib_lottie @@ -1 +1 @@ -Subproject commit 5b576ba9b90f621b3dd965d44caf8bd71c00f6ae +Subproject commit ab022b57a0a970a9a3ba73bc7fff7ea2cffc046b