From 2362d6c6fbb2a2d9c307aa6d57f4b986d94b0381 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 27 May 2022 19:42:05 +0400 Subject: [PATCH] Introduce premium reactions preview box. --- Telegram/CMakeLists.txt | 4 +- .../SourceFiles/boxes/premium_preview_box.cpp | 782 ++++++++++++++++++ ...er_preview_box.h => premium_preview_box.h} | 11 + .../boxes/reactions_settings_box.cpp | 5 +- .../SourceFiles/boxes/sticker_preview_box.cpp | 324 -------- .../chat_helpers/chat_helpers.style | 11 + .../chat_helpers/stickers_lottie.h | 5 + .../data/data_message_reactions.cpp | 3 +- .../history/history_item_components.cpp | 6 +- .../view/media/history_view_sticker.cpp | 5 + .../history/view/media/history_view_sticker.h | 1 + .../SourceFiles/window/section_widget.cpp | 7 +- 12 files changed, 832 insertions(+), 332 deletions(-) create mode 100644 Telegram/SourceFiles/boxes/premium_preview_box.cpp rename Telegram/SourceFiles/boxes/{sticker_preview_box.h => premium_preview_box.h} (66%) delete mode 100644 Telegram/SourceFiles/boxes/sticker_preview_box.cpp diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 5df046df7d..124b3ac135 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -260,6 +260,8 @@ PRIVATE boxes/pin_messages_box.h boxes/premium_limits_box.cpp boxes/premium_limits_box.h + boxes/premium_preview_box.cpp + boxes/premium_preview_box.h boxes/reactions_settings_box.cpp boxes/reactions_settings_box.h boxes/report_messages_box.cpp @@ -274,8 +276,6 @@ PRIVATE boxes/sessions_box.h boxes/share_box.cpp boxes/share_box.h - boxes/sticker_preview_box.cpp - boxes/sticker_preview_box.h boxes/sticker_set_box.cpp boxes/sticker_set_box.h boxes/stickers_box.cpp diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp new file mode 100644 index 0000000000..c3b9eaa071 --- /dev/null +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -0,0 +1,782 @@ +/* +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 "boxes/premium_preview_box.h" + +#include "chat_helpers/stickers_lottie.h" +#include "chat_helpers/stickers_emoji_pack.h" +#include "data/data_file_origin.h" +#include "data/data_document.h" +#include "data/data_session.h" +#include "data/data_message_reactions.h" +#include "data/data_document_media.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/chat/chat_theme.h" +#include "ui/chat/chat_style.h" +#include "ui/layers/generic_box.h" +#include "ui/effects/path_shift_gradient.h" +#include "ui/effects/premium_graphics.h" +#include "ui/text/text.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/gradient_round_button.h" +#include "ui/wrap/padding_wrap.h" +#include "settings/settings_premium.h" +#include "lottie/lottie_single_player.h" +#include "history/view/media/history_view_sticker.h" +#include "history/view/history_view_element.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" +#include "styles/style_chat_helpers.h" + +namespace { + +constexpr auto kPremiumShift = 0.082; +constexpr auto kShiftDuration = crl::time(200); +constexpr auto kEnumerateCount = 3; + +struct Descriptor { + PremiumPreview section = PremiumPreview::Stickers; + DocumentData *requestedSticker = nullptr; + base::flat_set disabledReactions; +}; + +bool operator==(const Descriptor &a, const Descriptor &b) { + return (a.section == b.section) + && (a.requestedSticker == b.requestedSticker) + && (a.disabledReactions == b.disabledReactions); +} + +bool operator!=(const Descriptor &a, const Descriptor &b) { + return !(a == b); +} + +struct Preload { + Descriptor descriptor; + std::shared_ptr media; + base::weak_ptr controller; +}; + +[[nodiscard]] std::vector &Preloads() { + static auto result = std::vector(); + return result; +} + +void PreloadSticker(const std::shared_ptr &media) { + const auto origin = media->owner()->stickerSetOrigin(); + media->automaticLoad(origin, nullptr); + media->videoThumbnailWanted(origin); +} + +[[nodiscard]] object_ptr ChatBackPreview( + QWidget *parent, + int height, + const QImage &back) { + auto result = object_ptr(parent, height); + const auto raw = result.data(); + + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(raw); + p.drawImage(0, 0, back); + }, raw->lifetime()); + + return result; +} + +[[nodiscard]] not_null StickerPreview( + not_null parent, + not_null controller, + const std::shared_ptr &media) { + using namespace HistoryView; + const auto document = media->owner(); + const auto lottieSize = Sticker::Size(document); + const auto effectSize = Sticker::PremiumEffectSize(document); + const auto result = Ui::CreateChild(parent.get()); + parent->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + result->setGeometry(QRect( + QPoint( + (size.width() - effectSize.width()) / 2, + (size.height() - effectSize.height()) / 2), + effectSize)); + }, result->lifetime()); + auto &lifetime = result->lifetime(); + + struct State { + std::unique_ptr lottie; + std::unique_ptr effect; + std::unique_ptr pathGradient; + }; + const auto state = lifetime.make_state(); + const auto createLottieIfReady = [=] { + if (state->lottie) { + return; + } + const auto document = media->owner(); + const auto sticker = document->sticker(); + if (!sticker || !sticker->isLottie() || !media->loaded()) { + return; + } else if (media->videoThumbnailContent().isEmpty()) { + return; + } + + const auto factor = style::DevicePixelRatio(); + state->lottie = ChatHelpers::LottiePlayerFromDocument( + media.get(), + nullptr, + ChatHelpers::StickerLottieSize::MessageHistory, + lottieSize * factor, + Lottie::Quality::High); + state->effect = document->session().emojiStickersPack().effectPlayer( + document, + media->videoThumbnailContent(), + QString(), + true); + + const auto update = [=] { result->update(); }; + auto &lifetime = result->lifetime(); + state->lottie->updates() | rpl::start_with_next(update, lifetime); + state->effect->updates() | rpl::start_with_next(update, lifetime); + }; + state->pathGradient = MakePathShiftGradient( + controller->chatStyle(), + [=] { result->update(); }); + + result->paintRequest( + ) | rpl::start_with_next([=] { + createLottieIfReady(); + + auto p = QPainter(result); + + const auto left = effectSize.width() + - int(lottieSize.width() * (1. + kPremiumShift)); + const auto top = (effectSize.height() - lottieSize.height()) / 2; + const auto r = QRect(QPoint(left, top), lottieSize); + if (!state->lottie + || !state->lottie->ready() + || !state->effect->ready()) { + p.setBrush(controller->chatStyle()->msgServiceBg()); + ChatHelpers::PaintStickerThumbnailPath( + p, + media.get(), + r, + state->pathGradient.get()); + return; + } + + const auto factor = style::DevicePixelRatio(); + const auto frame = state->lottie->frameInfo({ lottieSize * factor }); + const auto effect = state->effect->frameInfo( + { effectSize * factor }); + //const auto framesCount = !frame.image.isNull() + // ? state->lottie->framesCount() + // : 1; + //const auto effectsCount = !effect.image.isNull() + // ? state->effect->framesCount() + // : 1; + + p.drawImage(r, frame.image); + p.drawImage(result->rect(), effect.image); + + if (!frame.image.isNull()/* + && ((frame.index % effectsCount) <= effect.index)*/) { + state->lottie->markFrameShown(); + } + if (!effect.image.isNull()/* + && ((effect.index % framesCount) <= frame.index)*/) { + state->effect->markFrameShown(); + } + }, lifetime); + + return result; +} + +class ReactionPreview final { +public: + ReactionPreview( + not_null controller, + const Data::Reaction &reaction, + Fn update); + + [[nodiscard]] bool playsEffect() const; + void paint(QPainter &p, int x, int y, float64 scale); + void paintEffect(QPainter &p, int x, int y, float64 scale); + + void startAnimations(); + void cancelAnimations(); + +private: + void checkReady(); + + const not_null _controller; + const Fn _update; + std::shared_ptr _centerMedia; + std::shared_ptr _aroundMedia; + std::unique_ptr _center; + std::unique_ptr _around; + std::unique_ptr _pathGradient; + QImage _cache1; + QImage _cache2; + QImage _cache3; + bool _playRequested = false; + bool _aroundPlaying = false; + bool _centerPlaying = false; + rpl::lifetime _lifetime; + +}; + +ReactionPreview::ReactionPreview( + not_null controller, + const Data::Reaction &reaction, + Fn update) +: _controller(controller) +, _update(std::move(update)) +, _centerMedia(reaction.centerIcon->createMediaView()) +, _aroundMedia(reaction.aroundAnimation->createMediaView()) +, _pathGradient( + HistoryView::MakePathShiftGradient( + controller->chatStyle(), + _update)) { + _centerMedia->checkStickerLarge(); + _aroundMedia->checkStickerLarge(); + checkReady(); +} + +void ReactionPreview::checkReady() { + const auto make = [&]( + const std::shared_ptr &media, + int size) { + const auto bytes = media->bytes(); + const auto filepath = media->owner()->filepath(); + auto result = ChatHelpers::LottiePlayerFromDocument( + media.get(), + nullptr, + ChatHelpers::StickerLottieSize::PremiumReactionPreview, + QSize(size, size) * style::DevicePixelRatio(), + Lottie::Quality::High); + result->updates() | rpl::start_with_next(_update, _lifetime); + return result; + }; + if (!_center && _centerMedia->loaded()) { + _center = make(_centerMedia, st::premiumReactionSize); + } + if (!_around && _aroundMedia->loaded()) { + _around = make(_aroundMedia, st::premiumReactionAround); + } +} + +void ReactionPreview::startAnimations() { + _playRequested = true; + if (!_center || !_center->ready() || !_around || !_around->ready()) { + return; + } + _update(); +} + +void ReactionPreview::cancelAnimations() { + _playRequested = false; +} + +void ReactionPreview::paint(QPainter &p, int x, int y, float64 scale) { + const auto size = st::premiumReactionAround; + const auto center = st::premiumReactionSize; + const auto inner = QRect( + x + (size - center) / 2, + y + (size - center) / 2, + center, + center); + auto hq = PainterHighQualityEnabler(p); + const auto centerReady = _center && _center->ready(); + const auto staticCenter = centerReady && !_centerPlaying; + const auto use1 = staticCenter && scale == st::premiumReactionScale1; + const auto use2 = staticCenter && scale == st::premiumReactionScale2; + const auto use3 = staticCenter && scale == st::premiumReactionScale3; + const auto useScale = (!use1 && !use2 && !use3 && scale != 1.); + if (useScale) { + p.save(); + p.translate(inner.center()); + p.scale(scale, scale); + p.translate(-inner.center()); + } + checkReady(); + if (centerReady) { + if (use1 || use2 || use3) { + auto &cache = use1 ? _cache1 : use2 ? _cache2 : _cache3; + const auto use = int(std::round(center * scale)); + const auto rect = QRect( + x + (size - use) / 2, + y + (size - use) / 2, + use, + use); + if (cache.isNull()) { + cache = _center->frame().scaledToWidth( + use * style::DevicePixelRatio(), + Qt::SmoothTransformation); + } + p.drawImage(rect, cache); + } else { + p.drawImage(inner, _center->frame()); + } + if (_aroundPlaying) { + const auto almost = (_around->frameIndex() + 1) + == _around->framesCount(); + const auto marked = _around->markFrameShown(); + if (almost && marked) { + _aroundPlaying = false; + } + } + if (_centerPlaying) { + const auto almost = (_center->frameIndex() + 1) + == _center->framesCount(); + const auto marked = _center->markFrameShown(); + if (almost && marked) { + _centerPlaying = false; + } + } + if (_around + && _around->ready() + && !_aroundPlaying + && !_centerPlaying + && _playRequested) { + _aroundPlaying = _centerPlaying = true; + _playRequested = false; + } + } else { + p.setBrush(_controller->chatStyle()->msgServiceBg()); + ChatHelpers::PaintStickerThumbnailPath( + p, + _centerMedia.get(), + inner, + _pathGradient.get()); + } + if (useScale) { + p.restore(); + } +} + +bool ReactionPreview::playsEffect() const { + return _aroundPlaying; +} + +void ReactionPreview::paintEffect(QPainter &p, int x, int y, float64 scale) { + if (!_aroundPlaying) { + return; + } + const auto size = st::premiumReactionAround; + const auto outer = QRect(x, y, size, size); + auto hq = PainterHighQualityEnabler(p); + if (scale != 1.) { + p.save(); + p.translate(outer.center()); + p.scale(scale, scale); + p.translate(-outer.center()); + } + p.drawImage(outer, _around->frame()); + if (scale != 1.) { + p.restore(); + } + if (_aroundPlaying + && (_around->frameIndex() + 1 == _around->framesCount()) + && _around->markFrameShown()) { + _aroundPlaying = false; + } +} + +[[nodiscard]] not_null ReactionsPreview( + not_null parent, + not_null controller) { + struct State { + std::vector> entries; + Ui::Animations::Simple shifting; + int shift = 2; + bool played = false; + bool inside = false; + }; + const auto result = Ui::CreateChild(parent.get()); + auto &lifetime = result->lifetime(); + const auto state = lifetime.make_state(); + + result->setMouseTracking(true); + + parent->sizeValue( + ) | rpl::start_with_next([=] { + result->setGeometry(parent->rect()); + }, result->lifetime()); + + using namespace HistoryView; + const auto list = controller->session().data().reactions().list( + Data::Reactions::Type::Active); + for (const auto &reaction : list) { + if (!reaction.premium + || !reaction.centerIcon + || !reaction.aroundAnimation) { + continue; + } + state->entries.push_back(std::make_unique( + controller, + reaction, + [=] { result->update(); })); + } + const auto enumerate = [=]( + Fn,int,float64,int)> callback) { + const auto count = int(state->entries.size()); + if (!count) { + return; + } + const auto computeLeft = [](int index) { + const auto skips = std::array{ + st::premiumReactionSkip1, + st::premiumReactionSkip2, + st::premiumReactionSkip3, + }; + const auto id = std::abs(index); + const auto delta = !id + ? 0 + : (id <= skips.size()) + ? std::accumulate(begin(skips), begin(skips) + id, 0) + : (ranges::accumulate(skips, 0) + + skips.back() * int(id - skips.size())); + return (st::boxWideWidth / 2) + + (index < 0 ? -delta : delta) + - st::premiumReactionAround / 2; + }; + const auto computeScale = [](int index) { + const auto id = std::abs(index); + const auto scales = std::array{ + st::premiumReactionScale1, + st::premiumReactionScale2, + st::premiumReactionScale3, + }; + return !id + ? 1. + : scales[std::min(id, int(scales.size())) - 1]; + }; + const auto shift = state->shifting.value(state->shift); + const auto delta = !state->shifting.animating() + ? state->shift + : (shift < 0) + ? -(int(std::floor(-shift)) + 1) + : int(std::floor(shift)); + const auto progress = shift - delta; + const auto start = delta - kEnumerateCount; + const auto from = ((start % count) + count) % count; + const auto till = from + kEnumerateCount * 2 + 1; + const auto outerSize = st::premiumReactionAround; + for (auto i = from; i != till; ++i) { + const auto index = (i - from) - kEnumerateCount; + auto left = computeLeft(index); + auto scale = computeScale(index); + if (progress > 0.) { + left = anim::interpolate( + left, + computeLeft(index - 1), + progress); + scale = scale + (computeScale(index - 1) - scale) * progress; + } + const auto entry = state->entries[i % count].get(); + const auto scaledSize = scale * st::premiumReactionSize; + const auto paintedLeft = left + (outerSize - scaledSize) / 2; + const auto paintedRight = left + (outerSize + scaledSize) / 2; + if (entry->playsEffect() + || (paintedRight > 0 && paintedLeft < st::boxWideWidth)) { + callback(entry, left, scale, delta + index); + } else { + int a = 0; + } + } + }; + + result->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(result); + const auto top = result->height() / 2 - st::premiumReactionTop; + auto effects = std::vector>(); + if (!state->played && !state->shifting.animating()) { + state->played = true; + if (const auto count = state->entries.size()) { + const auto index = ((state->shift % count) + count) % count; + state->entries[index]->startAnimations(); + } + } + enumerate([&]( + not_null entry, + int left, + float64 scale, + int index) { + entry->paint(p, left, top, scale); + if (entry->playsEffect()) { + effects.push_back([=, &p] { + entry->paintEffect(p, left, top, scale); + }); + } + }); + for (const auto &paint : effects) { + paint(); + } + }, lifetime); + + const auto lookup = [=](QPoint point) -> std::optional { + auto found = std::optional(); + const auto top = result->height() / 2 - st::premiumReactionTop; + enumerate([&](auto, int left, float64 scale, int index) { + const auto size = int(st::premiumReactionSize * scale) / 2; + const auto outer = st::premiumReactionAround / 2; + const auto rect = QRect( + left + outer - (size / 2), + top + outer - (size / 2), + size, + size); + if (rect.contains(point)) { + found = index; + } + }); + return found; + }; + result->events( + ) | rpl::start_with_next([=](not_null event) { + if (event->type() == QEvent::MouseButtonPress) { + const auto point = static_cast(event.get())->pos(); + if (const auto index = lookup(point)) { + state->shifting.start( + [=] { result->update(); }, + state->shift, + *index, + kShiftDuration, + anim::sineInOut); + state->shift = *index; + state->played = false; + } + + } else if (event->type() == QEvent::MouseMove) { + const auto point = static_cast(event.get())->pos(); + const auto inside = lookup(point).has_value(); + if (state->inside != inside) { + state->inside = inside; + result->setCursor(inside + ? style::cur_pointer + : style::cur_default); + } + } + }, lifetime); + + return result; +} + +[[nodiscard]] object_ptr CreateGradientButton( + QWidget *parent, + QGradientStops stops) { + return object_ptr(parent, std::move(stops)); +} + +[[nodiscard]] object_ptr CreatePremiumButton( + QWidget *parent) { + return CreateGradientButton(parent, Ui::Premium::ButtonGradientStops()); +} + +[[nodiscard]] object_ptr CreateUnlockButton( + QWidget *parent, + int width) { + auto result = CreatePremiumButton(parent); + const auto &st = st::premiumPreviewBox.button; + result->resize(width, st.height); + + const auto label = Ui::CreateChild( + result.data(), + tr::lng_sticker_premium_button(), + st::premiumPreviewButtonLabel); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + rpl::combine( + result->widthValue(), + label->widthValue() + ) | rpl::start_with_next([=](int outer, int width) { + label->moveToLeft( + (outer - width) / 2, + st::premiumPreviewBox.button.textTop, + outer); + }, label->lifetime()); + + return result; +} + +void StickerBox( + not_null box, + not_null controller, + const Descriptor &descriptor, + const std::shared_ptr &media, + const QImage &back) { + const auto size = QSize( + st::boxWideWidth, + HistoryView::Sticker::UsualPremiumEffectSize().height()); + box->setWidth(size.width()); + box->setNoContentMargin(true); + + const auto outer = box->addRow( + ChatBackPreview(box, size.height(), back), + {}); + struct State { + Ui::RpWidget *content = nullptr; + }; + const auto state = outer->lifetime().make_state(); + + switch (descriptor.section) { + case PremiumPreview::Stickers: + Assert(media != nullptr); + state->content = StickerPreview(outer, controller, media); + break; + case PremiumPreview::Reactions: + state->content = ReactionsPreview(outer, controller); + break; + case PremiumPreview::Avatars: + break; + } + + const auto padding = st::premiumPreviewAboutPadding; + auto label = object_ptr( + box, + tr::lng_sticker_premium_about(), + st::premiumPreviewAbout); + label->resizeToWidth(size.width() - padding.left() - padding.right()); + box->addRow( + object_ptr>( + box, + std::move(label)), + padding); + box->setStyle(st::premiumPreviewBox); + const auto buttonPadding = st::premiumPreviewBox.buttonPadding; + const auto width = size.width() + - buttonPadding.left() + - buttonPadding.right(); + auto button = CreateUnlockButton(box, width); + button->setClickedCallback([=] { + Settings::ShowPremium(controller, "premium_stickers"); + }); + box->addButton(std::move(button)); +} + +void Show( + not_null controller, + const Descriptor &descriptor, + const std::shared_ptr &media, + QImage back) { + controller->show(Box(StickerBox, controller, descriptor, media, back)); +} + +void Show(not_null controller, QImage back) { + auto &list = Preloads(); + for (auto i = begin(list); i != end(list);) { + const auto already = i->controller.get(); + if (!already) { + i = list.erase(i); + } else if (already == controller) { + Show(controller, i->descriptor, i->media, back); + i = list.erase(i); + return; + } else { + ++i; + } + } +} + +[[nodiscard]] QImage SolidColorImage(QSize size, QColor color) { + const auto ratio = style::DevicePixelRatio(); + auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + result.fill(color); + return result; +} + +void Show( + not_null controller, + Descriptor &&descriptor) { + auto &list = Preloads(); + for (auto i = begin(list); i != end(list);) { + const auto already = i->controller.get(); + if (!already) { + i = list.erase(i); + } else if (already == controller) { + if (i->descriptor == descriptor) { + return; + } + i->descriptor = descriptor; + i->media = descriptor.requestedSticker + ? descriptor.requestedSticker->createMediaView() + : nullptr; + if (const auto &media = i->media) { + PreloadSticker(media); + } + return; + } else { + ++i; + } + } + + const auto weak = base::make_weak(controller.get()); + list.push_back({ + .descriptor = descriptor, + .media = (descriptor.requestedSticker + ? descriptor.requestedSticker->createMediaView() + : nullptr), + .controller = weak, + }); + if (const auto &media = list.back().media) { + PreloadSticker(media); + } + + const auto fill = QSize(st::boxWideWidth, st::boxWideWidth); + const auto theme = controller->currentChatTheme(); + const auto color = theme->background().colorForFill; + const auto area = QSize(fill.width(), fill.height() * 2); + const auto request = theme->cacheBackgroundRequest(area); + crl::async([=] { + using Option = Images::Option; + auto back = color + ? SolidColorImage(area, *color) + : request.background.waitingForNegativePattern() + ? SolidColorImage(area, Qt::black) + : Ui::CacheBackground(request).image; + const auto factor = style::DevicePixelRatio(); + auto cropped = back.copy(QRect( + QPoint(0, fill.height() * factor / 2), + fill * factor)); + cropped.setDevicePixelRatio(factor); + const auto options = Images::Options() + | Option::RoundSkipBottomLeft + | Option::RoundSkipBottomRight + | Option::RoundLarge; + const auto result = Images::Round( + std::move(cropped), + Images::CornersMask(st::boxRadius), + RectPart::TopLeft | RectPart::TopRight); + crl::on_main([=] { + if (const auto strong = weak.get()) { + Show(strong, result); + } + }); + }); +} + +} // namespace + +void ShowStickerPreviewBox( + not_null controller, + not_null document) { + Show(controller, Descriptor{ + .section = PremiumPreview::Stickers, + .requestedSticker = document, + }); +} + +void ShowPremiumPreviewBox( + not_null controller, + PremiumPreview section, + const base::flat_set &disabledReactions) { + Show(controller, Descriptor{ + .section = section, + .disabledReactions = disabledReactions, + }); +} diff --git a/Telegram/SourceFiles/boxes/sticker_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h similarity index 66% rename from Telegram/SourceFiles/boxes/sticker_preview_box.h rename to Telegram/SourceFiles/boxes/premium_preview_box.h index 3e206c7bc1..ad9a847d63 100644 --- a/Telegram/SourceFiles/boxes/sticker_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -16,3 +16,14 @@ class SessionController; void ShowStickerPreviewBox( not_null controller, not_null document); + +enum class PremiumPreview { + Reactions, + Stickers, + Avatars, +}; + +void ShowPremiumPreviewBox( + not_null controller, + PremiumPreview section, + const base::flat_set &disabledReactions = {}); diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index 1bae05cd64..41479ecd97 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_react_button.h" // DefaultIconFactory #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" +#include "boxes/premium_preview_box.h" #include "main/main_session.h" #include "settings/settings_common.h" #include "settings/settings_premium.h" @@ -448,7 +449,9 @@ void ReactionsSettingsBox( button->setClickedCallback([=, emoji = r.emoji] { if (premium && !controller->session().premium()) { - Settings::ShowPremium(controller, "unique_reactions"); + ShowPremiumPreviewBox( + controller, + PremiumPreview::Reactions); return; } checkButton(button); diff --git a/Telegram/SourceFiles/boxes/sticker_preview_box.cpp b/Telegram/SourceFiles/boxes/sticker_preview_box.cpp deleted file mode 100644 index d40bff6756..0000000000 --- a/Telegram/SourceFiles/boxes/sticker_preview_box.cpp +++ /dev/null @@ -1,324 +0,0 @@ -/* -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 "boxes/sticker_preview_box.h" - -#include "chat_helpers/stickers_lottie.h" -#include "chat_helpers/stickers_emoji_pack.h" -#include "data/data_file_origin.h" -#include "data/data_document.h" -#include "data/data_document_media.h" -#include "lang/lang_keys.h" -#include "main/main_session.h" -#include "ui/chat/chat_theme.h" -#include "ui/chat/chat_style.h" -#include "ui/layers/generic_box.h" -#include "ui/effects/path_shift_gradient.h" -#include "ui/effects/premium_graphics.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/gradient_round_button.h" -#include "ui/wrap/padding_wrap.h" -#include "settings/settings_premium.h" -#include "lottie/lottie_single_player.h" -#include "history/view/media/history_view_sticker.h" -#include "history/view/history_view_element.h" -#include "window/window_session_controller.h" -#include "styles/style_layers.h" -#include "styles/style_chat_helpers.h" - -namespace { - -constexpr auto kPremiumShift = 0.082; - -struct Preload { - not_null document; - std::shared_ptr media; - base::weak_ptr controller; -}; - -[[nodiscard]] std::vector &Preloads() { - static auto result = std::vector(); - return result; -} - -void PreloadSticker(const std::shared_ptr &media) { - const auto origin = media->owner()->stickerSetOrigin(); - media->automaticLoad(origin, nullptr); - media->videoThumbnailWanted(origin); -} - -[[nodiscard]] object_ptr StickerPreview( - QWidget *parent, - not_null controller, - const std::shared_ptr &media, - const QImage &back, - QSize size) { - auto result = object_ptr(parent, size.height()); - const auto raw = result.data(); - auto &lifetime = raw->lifetime(); - - struct State { - std::unique_ptr lottie; - std::unique_ptr effect; - std::unique_ptr pathGradient; - }; - const auto state = lifetime.make_state(); - - using namespace HistoryView; - const auto document = media->owner(); - const auto lottieSize = Sticker::Size(document); - const auto effectSize = Sticker::PremiumEffectSize(document); - const auto createLottieIfReady = [=] { - if (state->lottie) { - return; - } - const auto document = media->owner(); - const auto sticker = document->sticker(); - if (!sticker || !sticker->isLottie() || !media->loaded()) { - return; - } else if (media->videoThumbnailContent().isEmpty()) { - return; - } - - const auto factor = style::DevicePixelRatio(); - state->lottie = ChatHelpers::LottiePlayerFromDocument( - media.get(), - nullptr, - ChatHelpers::StickerLottieSize::MessageHistory, - lottieSize * factor, - Lottie::Quality::High); - state->effect = document->session().emojiStickersPack().effectPlayer( - document, - media->videoThumbnailContent(), - QString(), - true); - - const auto update = [=] { raw->update(); }; - auto &lifetime = raw->lifetime(); - state->lottie->updates() | rpl::start_with_next(update, lifetime); - state->effect->updates() | rpl::start_with_next(update, lifetime); - }; - state->pathGradient = MakePathShiftGradient( - controller->chatStyle(), - [=] { raw->update(); }); - - raw->paintRequest( - ) | rpl::start_with_next([=] { - createLottieIfReady(); - - auto p = QPainter(raw); - p.drawImage(0, 0, back); - - const auto zero = (size.width() - effectSize.width()) / 2; - const auto left = zero - + effectSize.width() - - int(lottieSize.width() * (1. + kPremiumShift)); - const auto top = (effectSize.height() - lottieSize.height()) / 2; - const auto r = QRect(QPoint(left, top), lottieSize); - if (!state->lottie - || !state->lottie->ready() - || !state->effect->ready()) { - p.setBrush(controller->chatStyle()->msgServiceBg()); - ChatHelpers::PaintStickerThumbnailPath( - p, - media.get(), - r, - state->pathGradient.get()); - return; - } - - const auto factor = style::DevicePixelRatio(); - const auto frame = state->lottie->frameInfo({ lottieSize * factor }); - const auto effect = state->effect->frameInfo( - { effectSize * factor }); - //const auto framesCount = !frame.image.isNull() - // ? state->lottie->framesCount() - // : 1; - //const auto effectsCount = !effect.image.isNull() - // ? state->effect->framesCount() - // : 1; - - p.drawImage(r, frame.image); - p.drawImage(QRect(QPoint(zero, 0), effectSize), effect.image); - - if (!frame.image.isNull()/* - && ((frame.index % effectsCount) <= effect.index)*/) { - state->lottie->markFrameShown(); - } - if (!effect.image.isNull()/* - && ((effect.index % framesCount) <= frame.index)*/) { - state->effect->markFrameShown(); - } - }, lifetime); - - return result; -} - -[[nodiscard]] object_ptr CreateGradientButton( - QWidget *parent, - QGradientStops stops) { - return object_ptr(parent, std::move(stops)); -} - -[[nodiscard]] object_ptr CreatePremiumButton( - QWidget *parent) { - return CreateGradientButton(parent, Ui::Premium::ButtonGradientStops()); -} - -[[nodiscard]] object_ptr CreateUnlockButton( - QWidget *parent, - int width) { - auto result = CreatePremiumButton(parent); - const auto &st = st::premiumPreviewBox.button; - result->resize(width, st.height); - - const auto label = Ui::CreateChild( - result.data(), - tr::lng_sticker_premium_button(), - st::premiumPreviewButtonLabel); - label->setAttribute(Qt::WA_TransparentForMouseEvents); - rpl::combine( - result->widthValue(), - label->widthValue() - ) | rpl::start_with_next([=](int outer, int width) { - label->moveToLeft( - (outer - width) / 2, - st::premiumPreviewBox.button.textTop, - outer); - }, label->lifetime()); - - return result; -} - -void StickerBox( - not_null box, - not_null controller, - const std::shared_ptr &media, - const QImage &back) { - const auto size = QSize( - st::boxWideWidth, - HistoryView::Sticker::PremiumEffectSize(media->owner()).height()); - box->setWidth(size.width()); - box->setNoContentMargin(true); - box->addRow(StickerPreview(box, controller, media, back, size), {}); - const auto padding = st::premiumPreviewAboutPadding; - auto label = object_ptr( - box, - tr::lng_sticker_premium_about(), - st::premiumPreviewAbout); - label->resizeToWidth(size.width() - padding.left() - padding.right()); - box->addRow( - object_ptr>( - box, - std::move(label)), - padding); - box->setStyle(st::premiumPreviewBox); - const auto buttonPadding = st::premiumPreviewBox.buttonPadding; - const auto width = size.width() - - buttonPadding.left() - - buttonPadding.right(); - auto button = CreateUnlockButton(box, width); - button->setClickedCallback([=] { - Settings::ShowPremium(controller, "premium_stickers"); - }); - box->addButton(std::move(button)); -} - -void Show( - not_null controller, - const std::shared_ptr &media, - QImage back) { - controller->show(Box(StickerBox, controller, media, back)); -} - -void Show(not_null controller, QImage back) { - auto &list = Preloads(); - for (auto i = begin(list); i != end(list);) { - const auto already = i->controller.get(); - if (!already) { - i = list.erase(i); - } else if (already == controller) { - Show(controller, i->media, back); - i = list.erase(i); - return; - } else { - ++i; - } - } -} - -[[nodiscard]] QImage SolidColorImage(QSize size, QColor color) { - const auto ratio = style::DevicePixelRatio(); - auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); - result.setDevicePixelRatio(ratio); - result.fill(color); - return result; -} - -} // namespace - -void ShowStickerPreviewBox( - not_null controller, - not_null document) { - auto &list = Preloads(); - for (auto i = begin(list); i != end(list);) { - const auto already = i->controller.get(); - if (!already) { - i = list.erase(i); - } else if (already == controller) { - if (i->document == document) { - return; - } - i->document = document; - i->media = document->createMediaView(); - PreloadSticker(i->media); - return; - } else { - ++i; - } - } - - const auto weak = base::make_weak(controller.get()); - list.push_back({ - .document = document, - .media = document->createMediaView(), - .controller = weak, - }); - PreloadSticker(list.back().media); - - const auto fill = QSize(st::boxWideWidth, st::boxWideWidth); - const auto theme = controller->currentChatTheme(); - const auto color = theme->background().colorForFill; - const auto area = QSize(fill.width(), fill.height() * 2); - const auto request = theme->cacheBackgroundRequest(area); - crl::async([=] { - using Option = Images::Option; - auto back = color - ? SolidColorImage(area, *color) - : request.background.waitingForNegativePattern() - ? SolidColorImage(area, Qt::black) - : Ui::CacheBackground(request).image; - const auto factor = style::DevicePixelRatio(); - auto cropped = back.copy(QRect( - QPoint(0, fill.height() * factor / 2), - fill * factor)); - cropped.setDevicePixelRatio(factor); - const auto options = Images::Options() - | Option::RoundSkipBottomLeft - | Option::RoundSkipBottomRight - | Option::RoundLarge; - const auto result = Images::Round( - std::move(cropped), - Images::CornersMask(st::boxRadius), - RectPart::TopLeft | RectPart::TopRight); - crl::on_main([=] { - if (const auto strong = weak.get()) { - Show(strong, result); - } - }); - }); -} diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index fecbfcb7b6..4e7c893557 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -300,3 +300,14 @@ premiumPreviewButtonLabel: FlatLabel(defaultFlatLabel) { } stickersPremiumLock: icon{{ "emoji/premium_lock", premiumButtonFg }}; + +premiumReactionSize: 152px; +premiumReactionAround: 192px; +premiumReactionTop: 108px; +premiumReactionSkip1: 96px; +premiumReactionScale1: 0.78; +premiumReactionSkip2: 78px; +premiumReactionScale2: 0.62; +premiumReactionSkip3: 64px; +premiumReactionScale3: 0.5; + diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.h b/Telegram/SourceFiles/chat_helpers/stickers_lottie.h index 1f66515c62..c5fd606a00 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.h @@ -60,6 +60,11 @@ enum class StickerLottieSize : uchar { EmojiInteractionReserved1, EmojiInteractionReserved2, EmojiInteractionReserved3, + EmojiInteractionReserved4, + EmojiInteractionReserved5, + EmojiInteractionReserved6, + EmojiInteractionReserved7, + PremiumReactionPreview, }; [[nodiscard]] std::unique_ptr LottiePlayerFromDocument( diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 651e48b10b..a429e8fa8b 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -336,7 +336,6 @@ std::optional Reactions::parse(const MTPAvailableReaction &entry) { } const auto selectAnimation = _owner->processDocument( data.vselect_animation()); - static auto test = 0; AssertIsDebug(); return known ? std::make_optional(Reaction{ .emoji = emoji, @@ -357,7 +356,7 @@ std::optional Reactions::parse(const MTPAvailableReaction &entry) { *data.varound_animation()).get() : nullptr), .active = !data.is_inactive(), - .premium = (data.is_premium() || ((++test) % 2)), + .premium = data.is_premium(), }) : std::nullopt; }); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index cb6371c719..c6cb0c854f 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -344,7 +344,11 @@ void HistoryMessageReply::updateName( not_null holder) const { if (const auto name = replyToFromName(holder); !name.isEmpty()) { replyToName.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); - replyToVersion = replyToMsg->author()->nameVersion; + if (const auto from = replyToFrom(holder)) { + replyToVersion = from->nameVersion; + } else { + replyToVersion = replyToMsg->author()->nameVersion; + } bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false; int32 previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; int32 w = replyToName.maxWidth(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp index 10986eb7f2..b877bb300a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp @@ -155,6 +155,11 @@ QSize Sticker::PremiumEffectSize(not_null document) { return Size(document) * kPremiumMultiplier; } +QSize Sticker::UsualPremiumEffectSize() { + return DownscaledSize({ kMaxSizeFixed, kMaxSizeFixed }, Size()) + * kPremiumMultiplier; +} + QSize Sticker::EmojiEffectSize() { return EmojiSize() * kEmojiMultiplier; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.h b/Telegram/SourceFiles/history/view/media/history_view_sticker.h index f8916c40e9..b6e2695088 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.h +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.h @@ -80,6 +80,7 @@ public: [[nodiscard]] static QSize Size(not_null document); [[nodiscard]] static QSize PremiumEffectSize( not_null document); + [[nodiscard]] static QSize UsualPremiumEffectSize(); [[nodiscard]] static QSize EmojiEffectSize(); [[nodiscard]] static QSize EmojiSize(); [[nodiscard]] static ClickHandlerPtr ShowSetHandler( diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index cfe4c924ab..39218ac428 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "ui/chat/chat_theme.h" #include "ui/toasts/common_toasts.h" -#include "boxes/sticker_preview_box.h" +#include "boxes/premium_preview_box.h" #include "data/data_peer.h" #include "data/data_user.h" #include "data/data_document.h" @@ -355,7 +355,10 @@ bool ShowReactPremiumError( if (i == end(list) || !i->premium) { return false; } - Settings::ShowPremium(controller, "unique_reactions"); + ShowPremiumPreviewBox( + controller, + PremiumPreview::Reactions, + {}); return true; }