diff --git a/Telegram/SourceFiles/chat_helpers/stickers.cpp b/Telegram/SourceFiles/chat_helpers/stickers.cpp index 859658bf2..aa96392f4 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers.cpp @@ -1093,6 +1093,36 @@ RecentStickerPack &GetRecentPack() { return cRefRecentStickers(); } +template +auto LottieCachedFromContent( + Method &&method, + Storage::Cache::Key baseKey, + LottieSize sizeTag, + not_null session, + const QByteArray &content, + QSize box) { + const auto key = Storage::Cache::Key{ + baseKey.high, + baseKey.low + int(sizeTag) + }; + const auto get = [=](FnMut handler) { + session->data().cacheBigFile().get( + key, + std::move(handler)); + }; + const auto weak = base::make_weak(session.get()); + const auto put = [=](QByteArray &&cached) { + crl::on_main(weak, [=, data = std::move(cached)]() mutable { + weak->data().cacheBigFile().put(key, std::move(data)); + }); + }; + return method( + get, + put, + content, + Lottie::FrameRequest{ box }); +} + template auto LottieFromDocument( Method &&method, @@ -1108,26 +1138,13 @@ auto LottieFromDocument( Lottie::FrameRequest{ box }); } if (const auto baseKey = document->bigFileBaseCacheKey()) { - const auto key = Storage::Cache::Key{ - baseKey->high, - baseKey->low + int(sizeTag) - }; - const auto get = [=](FnMut handler) { - document->session().data().cacheBigFile().get( - key, - std::move(handler)); - }; - const auto weak = base::make_weak(&document->session()); - const auto put = [=](QByteArray &&cached) { - crl::on_main(weak, [=, data = std::move(cached)]() mutable { - weak->data().cacheBigFile().put(key, std::move(data)); - }); - }; - return method( - get, - put, + return LottieCachedFromContent( + std::forward(method), + *baseKey, + sizeTag, + &document->session(), Lottie::ReadContent(data, filepath), - Lottie::FrameRequest{ box }); + box); } return method( Lottie::ReadContent(data, filepath), @@ -1156,4 +1173,98 @@ not_null LottieAnimationFromDocument( return LottieFromDocument(method, document, sizeTag, box); } +bool HasLottieThumbnail( + ImagePtr thumbnail, + not_null sticker) { + if (thumbnail) { + if (!thumbnail->loaded()) { + return false; + } + const auto &location = thumbnail->location(); + const auto &bytes = thumbnail->bytesForCache(); + return location.valid() + && location.type() == StorageFileLocation::Type::StickerSetThumb + && !bytes.isEmpty(); + } else if (const auto info = sticker->sticker()) { + if (!info->animated) { + return false; + } + sticker->automaticLoad(sticker->stickerSetOrigin(), nullptr); + if (!sticker->loaded()) { + return false; + } + return sticker->bigFileBaseCacheKey().has_value(); + } + return false; +} + +std::unique_ptr LottieThumbnail( + ImagePtr thumbnail, + not_null sticker, + LottieSize sizeTag, + QSize box, + std::shared_ptr renderer) { + const auto baseKey = thumbnail + ? thumbnail->location().file().bigFileBaseCacheKey() + : sticker->bigFileBaseCacheKey(); + if (!baseKey) { + return nullptr; + } + const auto content = (thumbnail + ? thumbnail->bytesForCache() + : Lottie::ReadContent(sticker->data(), sticker->filepath())); + if (content.isEmpty()) { + return nullptr; + } + const auto method = [](auto &&...args) { + return std::make_unique( + std::forward(args)...); + }; + return LottieCachedFromContent( + method, + *baseKey, + sizeTag, + &sticker->session(), + content, + box); +} + +ThumbnailSource::ThumbnailSource( + const StorageImageLocation &location, + int size) +: StorageSource(location, size) { +} + +QImage ThumbnailSource::takeLoaded() { + if (_bytesForAnimated.isEmpty() + && _loader + && _loader->finished() + && !_loader->cancelled()) { + _bytesForAnimated = _loader->bytes(); + } + auto result = StorageSource::takeLoaded(); + if (!_bytesForAnimated.isEmpty() + && !result.isNull() + && result.size() != Image::Empty()->original().size()) { + _bytesForAnimated = QByteArray(); + } + return result; +} + +QByteArray ThumbnailSource::bytesForCache() { + return _bytesForAnimated; +} + +std::unique_ptr ThumbnailSource::createLoader( + Data::FileOrigin origin, + LoadFromCloudSetting fromCloud, + bool autoLoading) { + auto result = StorageSource::createLoader( + origin, + fromCloud, + autoLoading); + _loader = result.get(); + return result; +} + } // namespace Stickers diff --git a/Telegram/SourceFiles/chat_helpers/stickers.h b/Telegram/SourceFiles/chat_helpers/stickers.h index 8f412b58f..d7ac190e0 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers.h +++ b/Telegram/SourceFiles/chat_helpers/stickers.h @@ -8,12 +8,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "mtproto/sender.h" +#include "ui/image/image_source.h" class DocumentData; +class AuthSession; + +namespace Storage { +namespace Cache { +struct Key; +} // namespace Cache +} // namespace Storage namespace Lottie { class SinglePlayer; class MultiPlayer; +class FrameRenderer; class Animation; } // namespace Lottie @@ -115,16 +124,50 @@ enum class LottieSize : uchar { MessageHistory, StickerSet, StickersPanel, + StickersFooter, + SetsListThumbnail, }; -std::unique_ptr LottiePlayerFromDocument( +[[nodiscard]] std::unique_ptr LottiePlayerFromDocument( not_null document, LottieSize sizeTag, QSize box); -not_null LottieAnimationFromDocument( +[[nodiscard]] not_null LottieAnimationFromDocument( not_null player, not_null document, LottieSize sizeTag, QSize box); +[[nodiscard]] bool HasLottieThumbnail( + ImagePtr thumbnail, + not_null sticker); +[[nodiscard]] std::unique_ptr LottieThumbnail( + ImagePtr thumbnail, + not_null sticker, + LottieSize sizeTag, + QSize box, + std::shared_ptr renderer = nullptr); + +class ThumbnailSource : public Images::StorageSource { +public: + ThumbnailSource( + const StorageImageLocation &location, + int size); + + QImage takeLoaded() override; + + QByteArray bytesForCache() override; + +protected: + std::unique_ptr createLoader( + Data::FileOrigin origin, + LoadFromCloudSetting fromCloud, + bool autoLoading) override; + +private: + QPointer _loader; + QByteArray _bytesForAnimated; + +}; + } // namespace Stickers diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 67254c64b..cfac87a28 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/ripple_animation.h" #include "ui/image/image.h" #include "lottie/lottie_multi_player.h" +#include "lottie/lottie_single_player.h" #include "lottie/lottie_animation.h" #include "boxes/stickers_box.h" #include "inline_bots/inline_bot_result.h" @@ -64,6 +65,7 @@ struct StickerIcon { } uint64 setId = 0; ImagePtr thumbnail; + mutable Lottie::SinglePlayer *lottie = nullptr; DocumentData *sticker = nullptr; ChannelData *megagroup = nullptr; int pixw = 0; @@ -71,9 +73,11 @@ struct StickerIcon { }; -class StickersListWidget::Footer : public TabbedSelector::InnerFooter, private base::Subscriber { +class StickersListWidget::Footer + : public TabbedSelector::InnerFooter + , private base::Subscriber { public: - Footer(not_null parent); + explicit Footer(not_null parent); void preloadImages(); void validateSelectedIcon( @@ -88,6 +92,8 @@ public: void returnFocus(); void setLoading(bool loading); + void clearLottieData(); + protected: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; @@ -106,6 +112,12 @@ private: }; using OverState = base::variant; + struct LottieIcon { + std::unique_ptr player; + bool stale = false; + rpl::lifetime lifetime; + }; + template void enumerateVisibleIcons(Callback callback); @@ -113,9 +125,13 @@ private: void setSelectedIcon( int newSelected, ValidateIconAnimations animations); + void validateIconLottieAnimation(const StickerIcon &icon); + QSize iconBox() const; void refreshIconsGeometry(ValidateIconAnimations animations); + void refillLottieData(); void updateSelected(); + void updateSetIcon(uint64 setId); void finishDragging(); void paintStickerSettingsIcon(Painter &p) const; void paintSearchIcon(Painter &p) const; @@ -134,6 +150,7 @@ private: static constexpr auto kVisibleIconsCount = 8; QList _icons; + mutable base::flat_map _lottieData; OverState _iconOver = SpecialOver::None; int _iconSel = 0; OverState _iconDown = SpecialOver::None; @@ -191,7 +208,8 @@ StickersListWidget::Set &StickersListWidget::Set::operator=( Set &&other) = default; StickersListWidget::Set::~Set() = default; -StickersListWidget::Footer::Footer(not_null parent) : InnerFooter(parent) +StickersListWidget::Footer::Footer(not_null parent) +: InnerFooter(parent) , _pan(parent) , _iconsAnimation([=](crl::time now) { return iconsAnimationCallback(now); @@ -205,6 +223,34 @@ StickersListWidget::Footer::Footer(not_null parent) : Inner }); } +void StickersListWidget::Footer::clearLottieData() { + for (auto &icon : _icons) { + icon.lottie = nullptr; + } + _lottieData.clear(); +} + +void StickersListWidget::Footer::refillLottieData() { + for (auto &item : _lottieData) { + item.second.stale = true; + } + for (auto &icon : _icons) { + const auto i = _lottieData.find(icon.setId); + if (i == end(_lottieData)) { + continue; + } + icon.lottie = i->second.player.get(); + i->second.stale = false; + } + for (auto i = begin(_lottieData); i != end(_lottieData);) { + if (i->second.stale) { + i = _lottieData.erase(i); + } else { + ++i; + } + } +} + void StickersListWidget::Footer::initSearch() { _searchField.create( this, @@ -282,7 +328,7 @@ void StickersListWidget::Footer::enumerateVisibleIcons(Callback callback) { } void StickersListWidget::Footer::preloadImages() { - enumerateVisibleIcons([](const StickerIcon & icon, int x) { + enumerateVisibleIcons([](const StickerIcon &icon, int x) { if (const auto sticker = icon.sticker) { const auto origin = sticker->stickerSetOrigin(); if (icon.thumbnail) { @@ -607,6 +653,7 @@ void StickersListWidget::Footer::updateSelected() { void StickersListWidget::Footer::refreshIcons( ValidateIconAnimations animations) { _pan->fillIcons(_icons); + refillLottieData(); refreshIconsGeometry(animations); } @@ -654,6 +701,49 @@ void StickersListWidget::Footer::paintFeaturedStickerSetsBadge(Painter &p, int i } } +QSize StickersListWidget::Footer::iconBox() const { + return QSize( + st::stickerIconWidth - 2 * st::stickerIconPadding, + st::emojiFooterHeight - 2 * st::stickerIconPadding); +} + +void StickersListWidget::Footer::validateIconLottieAnimation( + const StickerIcon &icon) { + if (icon.lottie + || !Stickers::HasLottieThumbnail(ImagePtr(), icon.sticker)) { + return; + } + auto player = Stickers::LottieThumbnail( + ImagePtr(), + icon.sticker, + Stickers::LottieSize::StickersFooter, + iconBox() * cIntRetinaFactor(), + _pan->getLottieRenderer()); + if (!player) { + return; + } + icon.lottie = player.get(); + const auto id = icon.setId; + const auto [i, ok] = _lottieData.emplace( + id, + LottieIcon{ std::move(player) }); + Assert(ok); + + icon.lottie->updates( + ) | rpl::start_with_next([=] { + updateSetIcon(id); + }, i->second.lifetime); +} + +void StickersListWidget::Footer::updateSetIcon(uint64 setId) { + enumerateVisibleIcons([&](const StickerIcon &icon, int x) { + if (icon.setId != setId) { + return; + } + update(x, _iconsTop, st::stickerIconWidth, st::emojiFooterHeight); + }); +} + void StickersListWidget::Footer::paintSetIcon( Painter &p, const StickerIcon &icon, @@ -663,11 +753,35 @@ void StickersListWidget::Footer::paintSetIcon( const auto thumb = icon.thumbnail ? icon.thumbnail.get() : icon.sticker->thumbnail(); - if (thumb) { - thumb->load(origin); + if (!thumb) { + return; + } + thumb->load(origin); + if (!thumb->loaded()) { + return; + } + const_cast(this)->validateIconLottieAnimation(icon); + if (!icon.lottie) { auto pix = thumb->pix(origin, icon.pixw, icon.pixh); p.drawPixmapLeft(x + (st::stickerIconWidth - icon.pixw) / 2, _iconsTop + (st::emojiFooterHeight - icon.pixh) / 2, width(), pix); + } else if (icon.lottie->ready()) { + auto request = Lottie::FrameRequest(); + request.box = iconBox() * cIntRetinaFactor(); + const auto frame = icon.lottie->frame(request); + const auto size = frame.size() / cIntRetinaFactor(); + p.drawImage( + QRect( + x + (st::stickerIconWidth - size.width()) / 2, + _iconsTop + (st::emojiFooterHeight - size.height()) / 2, + size.width(), + size.height()), + frame); + const auto paused = _pan->controller()->isGifPausedAtLeastFor( + Window::GifPauseReason::SavedGifs); + if (!paused) { + icon.lottie->markFrameShown(); + } } } else if (icon.megagroup) { icon.megagroup->paintUserpicLeft(p, x + (st::stickerIconWidth - st::stickerGroupCategorySize) / 2, _iconsTop + (st::emojiFooterHeight - st::stickerGroupCategorySize) / 2, width(), st::stickerGroupCategorySize); @@ -1901,11 +2015,17 @@ TabbedSelector::InnerFooter *StickersListWidget::getFooter() const { void StickersListWidget::processHideFinished() { clearSelection(); clearLottieData(); + if (_footer) { + _footer->clearLottieData(); + } } void StickersListWidget::processPanelHideFinished() { clearInstalledLocally(); clearLottieData(); + if (_footer) { + _footer->clearLottieData(); + } // Preserve panel state through visibility toggles. //// Reset to the recent stickers section. //if (_section == Section::Featured && (!_footer || !_footer->hasOnlyFeaturedSets())) { diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index a54674376..220855eed 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -69,6 +69,8 @@ public: void sendSearchRequest(); void searchForSets(const QString &query); + std::shared_ptr getLottieRenderer(); + ~StickersListWidget(); protected: @@ -290,8 +292,6 @@ private: void showPreview(); - std::shared_ptr getLottieRenderer(); - ChannelData *_megagroupSet = nullptr; uint64 _megagroupSetIdRequested = 0; std::vector _mySets; diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index d834027ad..e0b9266d8 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -214,6 +214,10 @@ class TabbedSelector::Inner : public Ui::RpWidget { public: Inner(QWidget *parent, not_null controller); + not_null controller() const { + return _controller; + } + int getVisibleTop() const { return _visibleTop; } @@ -246,10 +250,6 @@ protected: int minimalHeight() const; int resizeGetHeight(int newWidth) override final; - not_null controller() const { - return _controller; - } - virtual int countDesiredHeight(int newWidth) = 0; virtual InnerFooter *getFooter() const = 0; virtual void processHideFinished() { diff --git a/Telegram/SourceFiles/lottie/lottie_single_player.cpp b/Telegram/SourceFiles/lottie/lottie_single_player.cpp index aaa0e4a1d..e000881d5 100644 --- a/Telegram/SourceFiles/lottie/lottie_single_player.cpp +++ b/Telegram/SourceFiles/lottie/lottie_single_player.cpp @@ -13,20 +13,22 @@ namespace Lottie { SinglePlayer::SinglePlayer( const QByteArray &content, - const FrameRequest &request) + const FrameRequest &request, + std::shared_ptr renderer) : _animation(this, content, request) , _timer([=] { checkNextFrameRender(); }) -, _renderer(FrameRenderer::Instance()) { +, _renderer(renderer ? renderer : FrameRenderer::Instance()) { } SinglePlayer::SinglePlayer( FnMut)> get, // Main thread. FnMut put, // Unknown thread. const QByteArray &content, - const FrameRequest &request) + const FrameRequest &request, + std::shared_ptr renderer) : _animation(this, std::move(get), std::move(put), content, request) , _timer([=] { checkNextFrameRender(); }) -, _renderer(FrameRenderer::Instance()) { +, _renderer(renderer ? renderer : FrameRenderer::Instance()) { } SinglePlayer::~SinglePlayer() { diff --git a/Telegram/SourceFiles/lottie/lottie_single_player.h b/Telegram/SourceFiles/lottie/lottie_single_player.h index 0afe6c2d3..4d9f86f59 100644 --- a/Telegram/SourceFiles/lottie/lottie_single_player.h +++ b/Telegram/SourceFiles/lottie/lottie_single_player.h @@ -30,12 +30,14 @@ class SinglePlayer final : public Player { public: SinglePlayer( const QByteArray &content, - const FrameRequest &request); + const FrameRequest &request, + std::shared_ptr renderer = nullptr); SinglePlayer( FnMut)> get, // Main thread. FnMut put, // Unknown thread. const QByteArray &content, - const FrameRequest &request); + const FrameRequest &request, + std::shared_ptr renderer = nullptr); ~SinglePlayer(); void start( diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp index 09935dff5..28f26704d 100644 --- a/Telegram/SourceFiles/storage/localstorage.cpp +++ b/Telegram/SourceFiles/storage/localstorage.cpp @@ -3516,7 +3516,7 @@ void _readStickerSets(FileKey &stickersKey, Stickers::Order *outOrder = nullptr, setHash, MTPDstickerSet::Flags(setFlags), setInstallDate, - Images::Create(setThumbnail))); + Images::CreateStickerSetThumbnail(setThumbnail))); } auto &set = it.value(); auto inputSet = MTP_inputStickerSetID(MTP_long(set.id), MTP_long(set.access)); diff --git a/Telegram/SourceFiles/ui/image/image.cpp b/Telegram/SourceFiles/ui/image/image.cpp index ccfbff18b..21bc33331 100644 --- a/Telegram/SourceFiles/ui/image/image.cpp +++ b/Telegram/SourceFiles/ui/image/image.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/cache/storage_cache_database.h" #include "data/data_session.h" #include "data/data_file_origin.h" +#include "chat_helpers/stickers.h" #include "auth_session.h" using namespace Images; @@ -161,7 +162,11 @@ ImagePtr Create(int width, int height) { height))); } -ImagePtr Create(const StorageImageLocation &location, int size) { +template +ImagePtr Create( + const StorageImageLocation &location, + int size, + const QByteArray &bytes) { if (!location.valid()) { return ImagePtr(); } @@ -173,41 +178,63 @@ ImagePtr Create(const StorageImageLocation &location, int size) { : StorageImages.emplace( key, std::make_unique( - std::make_unique(location, size)) + std::make_unique(location, size)) ).first->second.get(); if (found) { image->refreshFileReference(location.fileReference()); } + if (!bytes.isEmpty()) { + image->setImageBytes(bytes); + } return ImagePtr(image); + +} + +ImagePtr Create(const StorageImageLocation &location, int size) { + return Create(location, size, QByteArray()); } ImagePtr Create( const StorageImageLocation &location, const QByteArray &bytes) { - const auto key = inMemoryKey(location); - const auto i = StorageImages.find(key); - const auto found = (i != end(StorageImages)); - const auto image = found - ? i->second.get() - : StorageImages.emplace( - key, - std::make_unique( - std::make_unique(location, bytes.size())) - ).first->second.get(); - if (found) { - image->refreshFileReference(location.fileReference()); - } - image->setImageBytes(bytes); - return ImagePtr(image); + return Create(location, bytes.size(), bytes); } -template +struct CreateStorageImage { + ImagePtr operator()( + const StorageImageLocation &location, + int size) { + return Create(location, size); + } + ImagePtr operator()( + const StorageImageLocation &location, + const QByteArray &bytes) { + return Create(location, bytes); + } +}; + +struct CreateSetThumbnail { + using Source = Stickers::ThumbnailSource; + ImagePtr operator()( + const StorageImageLocation &location, + int size) { + return Create(location, size, QByteArray()); + } + ImagePtr operator()( + const StorageImageLocation &location, + const QByteArray &bytes) { + return Create(location, bytes.size(), bytes); + } +}; + +template ImagePtr CreateFromPhotoSize( CreateLocation &&createLocation, - const MTPPhotoSize &size) { + const MTPPhotoSize &size, + Method method = Method()) { return size.match([&](const MTPDphotoSize &data) { const auto &location = data.vlocation.c_fileLocationToBeDeprecated(); - return Create( + return method( StorageImageLocation( createLocation(data.vtype, location), data.vw.v, @@ -216,7 +243,7 @@ ImagePtr CreateFromPhotoSize( }, [&](const MTPDphotoCachedSize &data) { const auto bytes = qba(data.vbytes); const auto &location = data.vlocation.c_fileLocationToBeDeprecated(); - return Create( + return method( StorageImageLocation( createLocation(data.vtype, location), data.vw.v, @@ -291,7 +318,11 @@ ImagePtr Create(const MTPDstickerSet &set, const MTPPhotoSize &size) { location.vvolume_id, location.vlocal_id)); }; - return CreateFromPhotoSize(create, size); + return CreateFromPhotoSize(create, size, CreateSetThumbnail()); +} + +ImagePtr CreateStickerSetThumbnail(const StorageImageLocation &location) { + return CreateSetThumbnail()(location, 0); } ImagePtr Create(const MTPDphoto &photo, const MTPPhotoSize &size) { diff --git a/Telegram/SourceFiles/ui/image/image.h b/Telegram/SourceFiles/ui/image/image.h index 74bf0266b..5d0352076 100644 --- a/Telegram/SourceFiles/ui/image/image.h +++ b/Telegram/SourceFiles/ui/image/image.h @@ -27,6 +27,7 @@ ImagePtr Create( QImage &&data); ImagePtr Create(int width, int height); ImagePtr Create(const StorageImageLocation &location, int size = 0); +ImagePtr CreateStickerSetThumbnail(const StorageImageLocation &location); ImagePtr Create( // photoCachedSize const StorageImageLocation &location, const QByteArray &bytes); diff --git a/Telegram/SourceFiles/ui/image/image_location.cpp b/Telegram/SourceFiles/ui/image/image_location.cpp index 123d60a37..7eb4c5857 100644 --- a/Telegram/SourceFiles/ui/image/image_location.cpp +++ b/Telegram/SourceFiles/ui/image/image_location.cpp @@ -407,9 +407,17 @@ Storage::Cache::Key StorageFileLocation::bigFileBaseCacheKey() const { return Storage::Cache::Key{ high, low }; } + case Type::StickerSetThumb: { + const auto high = (uint64(uint32(_localId)) << 24) + | ((uint64(_type) + 1) << 16) + | ((uint64(_dcId) & 0xFFULL) << 8) + | (_volumeId >> 56); + const auto low = (_volumeId << 8); + return Storage::Cache::Key{ high, low }; + } + case Type::Legacy: case Type::PeerPhoto: - case Type::StickerSetThumb: case Type::Encrypted: case Type::Secure: case Type::Photo: