/* 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 "data/data_streaming.h" #include "data/data_peer_values.h" #include "data/data_premium_limits.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "main/main_domain.h" // kMaxAccounts #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/effects/gradient.h" #include "ui/text/text.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/gradient_round_button.h" #include "ui/wrap/padding_wrap.h" #include "ui/boxes/confirm_box.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 "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "window/window_session_controller.h" #include "api/api_premium.h" #include "apiwrap.h" #include "styles/style_layers.h" #include "styles/style_chat_helpers.h" #include "styles/style_settings.h" #include namespace { constexpr auto kPremiumShift = 21. / 240; constexpr auto kShiftDuration = crl::time(200); constexpr auto kReactionsPerRow = 5; constexpr auto kDisabledOpacity = 0.5; constexpr auto kPreviewsCount = int(PremiumPreview::kCount); constexpr auto kToggleStickerTimeout = 2 * crl::time(1000); constexpr auto kStarOpacityOff = 0.1; constexpr auto kStarOpacityOn = 1.; constexpr auto kStarPeriod = 3 * crl::time(1000); struct Descriptor { PremiumPreview section = PremiumPreview::Stickers; DocumentData *requestedSticker = nullptr; base::flat_map disabled; bool fromSettings = false; Fn hiddenCallback; }; bool operator==(const Descriptor &a, const Descriptor &b) { return (a.section == b.section) && (a.requestedSticker == b.requestedSticker) && (a.disabled == b.disabled) && (a.fromSettings == b.fromSettings); } [[nodiscard]] int ComputeX(int column, int columns) { const auto skip = st::premiumReactionWidthSkip; const auto fullWidth = columns * skip; const auto left = (st::boxWideWidth - fullWidth) / 2; return left + column * skip + (skip / 2); } [[nodiscard]] int ComputeY(int row, int rows) { const auto middle = (rows > 3) ? (st::premiumReactionInfoTop / 2) : st::premiumReactionsMiddle; const auto skip = st::premiumReactionHeightSkip; const auto fullHeight = rows * skip; const auto top = middle - (fullHeight / 2); return top + row * skip + (skip / 2); } 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]] rpl::producer SectionTitle(PremiumPreview section) { switch (section) { case PremiumPreview::MoreUpload: return tr::lng_premium_summary_subtitle_more_upload(); case PremiumPreview::FasterDownload: return tr::lng_premium_summary_subtitle_faster_download(); case PremiumPreview::VoiceToText: return tr::lng_premium_summary_subtitle_voice_to_text(); case PremiumPreview::NoAds: return tr::lng_premium_summary_subtitle_no_ads(); case PremiumPreview::Reactions: return tr::lng_premium_summary_subtitle_unique_reactions(); case PremiumPreview::Stickers: return tr::lng_premium_summary_subtitle_premium_stickers(); case PremiumPreview::AdvancedChatManagement: return tr::lng_premium_summary_subtitle_advanced_chat_management(); case PremiumPreview::ProfileBadge: return tr::lng_premium_summary_subtitle_profile_badge(); case PremiumPreview::AnimatedUserpics: return tr::lng_premium_summary_subtitle_animated_userpics(); } Unexpected("PremiumPreview in SectionTitle."); } [[nodiscard]] rpl::producer SectionAbout(PremiumPreview section) { switch (section) { case PremiumPreview::MoreUpload: return tr::lng_premium_summary_about_more_upload(); case PremiumPreview::FasterDownload: return tr::lng_premium_summary_about_faster_download(); case PremiumPreview::VoiceToText: return tr::lng_premium_summary_about_voice_to_text(); case PremiumPreview::NoAds: return tr::lng_premium_summary_about_no_ads(); case PremiumPreview::Reactions: return tr::lng_premium_summary_about_unique_reactions(); case PremiumPreview::Stickers: return tr::lng_premium_summary_about_premium_stickers(); case PremiumPreview::AdvancedChatManagement: return tr::lng_premium_summary_about_advanced_chat_management(); case PremiumPreview::ProfileBadge: return tr::lng_premium_summary_about_profile_badge(); case PremiumPreview::AnimatedUserpics: return tr::lng_premium_summary_about_animated_userpics(); } Unexpected("PremiumPreview in SectionTitle."); } [[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, Fn readyCallback = nullptr) { using namespace HistoryView; PreloadSticker(media); const auto document = media->owner(); const auto lottieSize = Sticker::Size(document); const auto effectSize = Sticker::PremiumEffectSize(document); const auto result = Ui::CreateChild(parent.get()); result->show(); 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; bool readyInvoked = false; }; 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 = [=] { if (!state->readyInvoked && readyCallback && state->lottie->ready() && state->effect->ready()) { state->readyInvoked = true; readyCallback(); } result->update(); }; auto &lifetime = result->lifetime(); state->lottie->updates() | rpl::start_with_next(update, lifetime); state->effect->updates() | rpl::start_with_next(update, lifetime); }; createLottieIfReady(); if (!state->lottie || !state->effect) { controller->session().downloaderTaskFinished( ) | rpl::take_while([=] { createLottieIfReady(); return !state->lottie || !state->effect; }) | rpl::start(result->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( QRect(QPoint(), effect.image.size() / factor), 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]] not_null StickersPreview( not_null parent, not_null controller, Fn readyCallback) { const auto result = Ui::CreateChild(parent.get()); result->show(); parent->sizeValue( ) | rpl::start_with_next([=](QSize size) { result->setGeometry(QRect(QPoint(), size)); }, result->lifetime()); auto &lifetime = result->lifetime(); struct State { std::vector> medias; Ui::RpWidget *previous = nullptr; Ui::RpWidget *current = nullptr; Ui::RpWidget *next = nullptr; Ui::Animations::Simple slide; base::Timer toggleTimer; bool toggleTimerPending = false; Fn singleReadyCallback; bool readyInvoked = false; bool timerFired = false; bool nextReady = false; int index = 0; }; const auto premium = &controller->session().api().premium(); const auto state = lifetime.make_state(); const auto create = [=](std::shared_ptr media) { const auto outer = Ui::CreateChild(result); outer->show(); result->sizeValue( ) | rpl::start_with_next([=](QSize size) { outer->resize(size); }, outer->lifetime()); [[maybe_unused]] const auto sticker = StickerPreview( outer, controller, media, state->singleReadyCallback); return outer; }; const auto createNext = [=] { state->nextReady = false; state->next = create(state->medias[state->index]); state->next->move(0, state->current->height()); }; const auto check = [=] { if (!state->timerFired || !state->nextReady) { return; } const auto animationCallback = [=] { const auto top = int(base::SafeRound(state->slide.value(0.))); state->previous->move(0, top - state->current->height()); state->current->move(0, top); if (!state->slide.animating()) { delete base::take(state->previous); state->timerFired = false; state->toggleTimer.callOnce(kToggleStickerTimeout); } }; state->timerFired = false; ++state->index; state->index %= state->medias.size(); delete std::exchange(state->previous, state->current); state->current = state->next; createNext(); state->slide.stop(); state->slide.start( animationCallback, state->current->height(), 0, st::premiumSlideDuration, anim::sineInOut); }; state->toggleTimer.setCallback([=] { state->timerFired = true; check(); }); state->singleReadyCallback = [=] { if (!state->readyInvoked && readyCallback) { state->readyInvoked = true; readyCallback(); } if (!state->next) { createNext(); if (result->isHidden()) { state->toggleTimerPending = true; } else { state->toggleTimer.callOnce(kToggleStickerTimeout); } } else { state->nextReady = true; check(); } }; result->shownValue( ) | rpl::filter([=](bool shown) { return shown && state->toggleTimerPending; }) | rpl::start_with_next([=] { state->toggleTimerPending = false; state->toggleTimer.callOnce(kToggleStickerTimeout); }, result->lifetime()); const auto fill = [=] { const auto &list = premium->stickers(); for (const auto &document : list) { state->medias.push_back(document->createMediaView()); } if (!state->medias.empty()) { state->current = create(state->medias.front()); state->index = 1 % state->medias.size(); state->current->move(0, 0); } }; fill(); if (state->medias.empty()) { premium->stickersUpdated( ) | rpl::take(1) | rpl::start_with_next(fill, lifetime); } return result; } struct VideoPreviewDocument { DocumentData *document = nullptr; RectPart align = RectPart::Bottom; }; [[nodiscard]] bool VideoAlignToTop(PremiumPreview section) { return (section == PremiumPreview::MoreUpload) || (section == PremiumPreview::NoAds); } [[nodiscard]] DocumentData *LookupVideo( not_null session, PremiumPreview section) { const auto name = [&] { switch (section) { case PremiumPreview::MoreUpload: return "more_upload"; case PremiumPreview::FasterDownload: return "faster_download"; case PremiumPreview::VoiceToText: return "voice_to_text"; case PremiumPreview::NoAds: return "no_ads"; case PremiumPreview::AdvancedChatManagement: return "advanced_chat_management"; case PremiumPreview::ProfileBadge: return "profile_badge"; case PremiumPreview::AnimatedUserpics: return "animated_userpics"; } return ""; }(); const auto &videos = session->api().premium().videos(); const auto i = videos.find(name); return (i != end(videos)) ? i->second.get() : nullptr; } [[nodiscard]] QPainterPath GenerateFrame( int left, int top, int width, int height, bool alignToBottom) { const auto radius = style::ConvertScaleExact(20.); const auto thickness = style::ConvertScaleExact(6.); const auto skip = thickness / 2.; auto path = QPainterPath(); if (alignToBottom) { path.moveTo(left - skip, top + height); path.lineTo(left - skip, top - skip + radius); path.arcTo( left - skip, top - skip, radius * 2, radius * 2, 180, -90); path.lineTo(left + width + skip - radius, top - skip); path.arcTo( left + width + skip - 2 * radius, top - skip, radius * 2, radius * 2, 90, -90); path.lineTo(left + width + skip, top + height); } else { path.moveTo(left - skip, top); path.lineTo(left - skip, top + height + skip - radius); path.arcTo( left - skip, top + height + skip - 2 * radius, radius * 2, radius * 2, 180, 90); path.lineTo(left + width + skip - radius, top + height + skip); path.arcTo( left + width + skip - 2 * radius, top + height + skip - 2 * radius, radius * 2, radius * 2, 270, 90); path.lineTo(left + width + skip, top); } return path; } [[nodiscard]] not_null VideoPreview( not_null parent, not_null controller, not_null document, bool alignToBottom, Fn readyCallback) { const auto result = Ui::CreateChild(parent.get()); result->show(); parent->sizeValue( ) | rpl::start_with_next([=](QSize size) { result->setGeometry(parent->rect()); }, result->lifetime()); auto &lifetime = result->lifetime(); auto shared = document->owner().streaming().sharedDocument( document, Data::FileOriginPremiumPreviews()); if (!shared) { return result; } struct State { State( std::shared_ptr shared, Fn waitingCallback) : instance(shared, std::move(waitingCallback)) , star(u":/gui/icons/settings/star.svg"_q) { } QImage blurred; Media::Streaming::Instance instance; std::shared_ptr media; Ui::Animations::Basic loading; QPainterPath frame; QSvgRenderer star; bool readyInvoked = false; }; const auto state = lifetime.make_state(std::move(shared), [] {}); state->media = document->createMediaView(); if (const auto image = state->media->thumbnailInline()) { if (image->width() > 0) { const auto width = st::premiumVideoWidth; const auto height = std::max( int(base::SafeRound( float64(width) * image->height() / image->width())), 1); using Option = Images::Option; const auto corners = alignToBottom ? (Option::RoundSkipBottomLeft | Option::RoundSkipBottomRight) : (Option::RoundSkipTopLeft | Option::RoundSkipTopRight); state->blurred = Images::Prepare( image->original(), QSize(width, height) * style::DevicePixelRatio(), { .options = (Option::Blur | Option::RoundLarge | corners) }); } } const auto width = st::premiumVideoWidth; const auto height = state->blurred.height() ? (state->blurred.height() / state->blurred.devicePixelRatio()) : width; const auto left = (st::boxWideWidth - width) / 2; const auto top = alignToBottom ? (st::premiumPreviewHeight - height) : 0; state->frame = GenerateFrame(left, top, width, height, alignToBottom); const auto check = [=] { if (state->instance.playerLocked()) { return; } else if (state->instance.paused()) { state->instance.resume(); } if (!state->instance.active() && !state->instance.failed()) { auto options = Media::Streaming::PlaybackOptions(); options.waitForMarkAsShown = true; options.mode = ::Media::Streaming::Mode::Video; options.loop = true; state->instance.play(options); } }; state->instance.player().updates( ) | rpl::start_with_next_error([=](Media::Streaming::Update &&update) { if (v::is(update.data) || v::is(update.data)) { if (!state->readyInvoked && readyCallback) { state->readyInvoked = true; readyCallback(); } result->update(); } }, [=](::Media::Streaming::Error &&error) { result->update(); }, state->instance.lifetime()); state->loading.init([=] { if (!anim::Disabled()) { result->update(); } }); result->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(result); const auto paintFrame = [&](QColor color, float64 thickness) { auto hq = PainterHighQualityEnabler(p); auto pen = QPen(color); pen.setWidthF(style::ConvertScaleExact(thickness)); p.setPen(pen); p.setBrush(Qt::NoBrush); p.drawPath(state->frame); }; check(); const auto corners = alignToBottom ? (RectPart::TopLeft | RectPart::TopRight) : (RectPart::BottomLeft | RectPart::BottomRight); const auto ready = state->instance.player().ready() && !state->instance.player().videoSize().isEmpty(); const auto size = QSize(width, height) * style::DevicePixelRatio(); const auto frame = !ready ? state->blurred : state->instance.frame({ .resize = size, .outer = size, .radius = ImageRoundRadius::Large, .corners = corners, }); paintFrame(QColor(0, 0, 0, 128), 12.); p.drawImage(QRect(left, top, width, height), frame); paintFrame(Qt::black, 6.6); if (ready) { state->loading.stop(); state->instance.markFrameShown(); } else { if (!state->loading.animating()) { state->loading.start(); } const auto progress = anim::Disabled() ? 1. : ((crl::now() % kStarPeriod) / float64(kStarPeriod)); const auto ratio = anim::Disabled() ? 1. : (1. + cos(progress * 2 * M_PI)) / 2.; const auto opacity = kStarOpacityOff + (kStarOpacityOn - kStarOpacityOff) * ratio; p.setOpacity(opacity); const auto starSize = st::premiumVideoStarSize; state->star.render(&p, QRectF( QPointF( left + (width - starSize.width()) / 2., top + (height - starSize.height()) / 2.), starSize)); } }, lifetime); return result; } [[nodiscard]] not_null GenericPreview( not_null parent, not_null controller, PremiumPreview section, Fn readyCallback) { const auto result = Ui::CreateChild(parent.get()); result->show(); parent->sizeValue( ) | rpl::start_with_next([=](QSize size) { result->setGeometry(QRect(QPoint(), size)); }, result->lifetime()); auto &lifetime = result->lifetime(); struct State { std::vector> medias; Ui::RpWidget *single = nullptr; }; const auto session = &controller->session(); const auto state = lifetime.make_state(); const auto create = [=] { const auto document = LookupVideo(session, section); if (!document) { return; } state->single = VideoPreview( result, controller, document, !VideoAlignToTop(section), readyCallback); }; create(); if (!state->single) { session->api().premium().videosUpdated( ) | rpl::take(1) | rpl::start_with_next(create, lifetime); } return result; } class ReactionPreview final { public: ReactionPreview( not_null controller, const Data::Reaction &reaction, ReactionDisableType type, Fn update, QPoint position); [[nodiscard]] bool playsEffect() const; void paint(Painter &p); void paintEffect(QPainter &p); void setOver(bool over); void startAnimations(); void cancelAnimations(); [[nodiscard]] bool ready() const; [[nodiscard]] bool disabled() const; [[nodiscard]] QRect geometry() const; private: void checkReady(); const not_null _controller; const Fn _update; const QPoint _position; Ui::Animations::Simple _scale; std::shared_ptr _centerMedia; std::shared_ptr _aroundMedia; std::unique_ptr _center; std::unique_ptr _around; std::unique_ptr _pathGradient; QImage _cache1; QImage _cache2; bool _over = false; bool _disabled = false; bool _playRequested = false; bool _aroundPlaying = false; bool _centerPlaying = false; rpl::lifetime _lifetime; }; ReactionPreview::ReactionPreview( not_null controller, const Data::Reaction &reaction, ReactionDisableType type, Fn update, QPoint position) : _controller(controller) , _update(std::move(update)) , _position(position) , _centerMedia(reaction.centerIcon->createMediaView()) , _aroundMedia(reaction.aroundAnimation->createMediaView()) , _pathGradient( HistoryView::MakePathShiftGradient( controller->chatStyle(), _update)) , _disabled(type != ReactionDisableType::None) { _centerMedia->checkStickerLarge(); _aroundMedia->checkStickerLarge(); checkReady(); if (!_center || !_around) { _controller->session().downloaderTaskFinished( ) | rpl::take_while([=] { checkReady(); return !_center || !_around; }) | rpl::start(_lifetime); } } QRect ReactionPreview::geometry() const { const auto xsize = st::premiumReactionWidthSkip; const auto ysize = st::premiumReactionHeightSkip; return { _position - QPoint(xsize / 2, ysize / 2), QSize(xsize, ysize) }; } 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::Default); 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::setOver(bool over) { if (_over == over || _disabled) { return; } _over = over; const auto from = st::premiumReactionScale; _scale.start( _update, over ? from : 1., over ? 1. : from, st::slideWrapDuration); } void ReactionPreview::startAnimations() { if (_disabled) { return; } _playRequested = true; if (!_center || !_center->ready() || !_around || !_around->ready()) { return; } _update(); } void ReactionPreview::cancelAnimations() { _playRequested = false; } bool ReactionPreview::ready() const { return _center && _center->ready(); } bool ReactionPreview::disabled() const { return _disabled; } void ReactionPreview::paint(Painter &p) { const auto center = st::premiumReactionSize; const auto scale = _scale.value(_over ? 1. : st::premiumReactionScale); const auto inner = QRect( -center / 2, -center / 2, center, center ).translated(_position); auto hq = PainterHighQualityEnabler(p); const auto centerReady = _center && _center->ready(); const auto staticCenter = centerReady && !_centerPlaying; const auto use1 = staticCenter && scale == 1.; const auto use2 = staticCenter && scale == st::premiumReactionScale; const auto useScale = (!use1 && !use2 && scale != 1.); if (useScale) { p.save(); p.translate(inner.center()); p.scale(scale, scale); p.translate(-inner.center()); } if (_disabled) { p.setOpacity(kDisabledOpacity); } checkReady(); if (centerReady) { if (use1 || use2) { auto &cache = use1 ? _cache1 : _cache2; const auto use = int(std::round(center * scale)); const auto rect = QRect(-use / 2, -use / 2, use, use).translated( _position); if (cache.isNull()) { cache = _center->frame().scaledToWidth( use * style::DevicePixelRatio(), Qt::SmoothTransformation); } p.drawImage(rect, cache); } else { p.drawImage(inner, _center->frame()); } 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(); } else if (_disabled) { p.setOpacity(1.); } } bool ReactionPreview::playsEffect() const { return _aroundPlaying; } void ReactionPreview::paintEffect(QPainter &p) { if (!_aroundPlaying) { return; } const auto size = st::premiumReactionAround; const auto outer = QRect(-size/2, -size/2, size, size).translated( _position); const auto scale = _scale.value(_over ? 1. : st::premiumReactionScale); 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) { const auto almost = (_around->frameIndex() + 1) == _around->framesCount(); const auto marked = _around->markFrameShown(); if (almost && marked) { _aroundPlaying = false; } } } [[nodiscard]] not_null ReactionsPreview( not_null parent, not_null controller, const base::flat_map &disabled, Fn readyCallback) { struct State { std::vector> entries; Ui::Text::String bottom; int selected = -1; bool readyInvoked = false; }; const auto result = Ui::CreateChild(parent.get()); result->show(); 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); const auto count = ranges::count(list, true, &Data::Reaction::premium); const auto rows = (count + kReactionsPerRow - 1) / kReactionsPerRow; const auto inrowmax = (count + rows - 1) / rows; const auto inrowless = (inrowmax * rows - count); const auto inrowmore = rows - inrowless; const auto inmaxrows = inrowmore * inrowmax; auto index = 0; auto disableType = ReactionDisableType::None; for (const auto &reaction : list) { if (!reaction.premium) { continue; } const auto inrow = (index < inmaxrows) ? inrowmax : (inrowmax - 1); const auto row = (index < inmaxrows) ? (index / inrow) : (inrowmore + ((index - inmaxrows) / inrow)); const auto column = (index < inmaxrows) ? (index % inrow) : ((index - inmaxrows) % inrow); ++index; if (!reaction.centerIcon || !reaction.aroundAnimation) { continue; } const auto i = disabled.find(reaction.emoji); const auto disable = (i != end(disabled)) ? i->second : ReactionDisableType::None; if (disable != ReactionDisableType::None) { disableType = disable; } state->entries.push_back(std::make_unique( controller, reaction, disable, [=] { result->update(); }, QPoint(ComputeX(column, inrow), ComputeY(row, rows)))); } const auto bottom1 = tr::lng_reaction_premium_info(tr::now); const auto bottom2 = (disableType == ReactionDisableType::None) ? QString() : (disableType == ReactionDisableType::Group) ? tr::lng_reaction_premium_no_group(tr::now) : tr::lng_reaction_premium_no_channel(tr::now); state->bottom.setText( st::defaultTextStyle, (bottom1 + '\n' + bottom2).trimmed()); result->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(result); auto effects = std::vector>(); auto ready = 0; for (const auto &entry : state->entries) { entry->paint(p); if (entry->ready()) { ++ready; } if (entry->playsEffect()) { effects.push_back([&] { entry->paintEffect(p); }); } } if (!state->readyInvoked && readyCallback && ready > 0 && ready == state->entries.size()) { state->readyInvoked = true; readyCallback(); } const auto padding = st::boxRowPadding; const auto available = parent->width() - padding.left() - padding.right(); const auto top = st::premiumReactionInfoTop + ((state->bottom.maxWidth() > available) ? st::normalFont->height : 0); p.setPen(st::premiumButtonFg); state->bottom.draw( p, padding.left(), top, available, style::al_top); for (const auto &paint : effects) { paint(); } }, lifetime); const auto lookup = [=](QPoint point) { auto index = 0; for (const auto &entry : state->entries) { if (entry->geometry().contains(point) && !entry->disabled()) { return index; } ++index; } return -1; }; const auto select = [=](int index) { const auto wasInside = (state->selected >= 0); const auto nowInside = (index >= 0); if (state->selected != index) { if (wasInside) { state->entries[state->selected]->setOver(false); } if (nowInside) { state->entries[index]->setOver(true); } state->selected = index; } if (wasInside != nowInside) { result->setCursor(nowInside ? style::cur_pointer : style::cur_default); } }; result->events( ) | rpl::start_with_next([=](not_null event) { if (event->type() == QEvent::MouseButtonPress) { const auto point = static_cast(event.get())->pos(); if (state->selected >= 0) { state->entries[state->selected]->cancelAnimations(); } if (const auto index = lookup(point); index >= 0) { state->entries[index]->startAnimations(); } } else if (event->type() == QEvent::MouseMove) { const auto point = static_cast(event.get())->pos(); select(lookup(point)); } else if (event->type() == QEvent::Leave) { select(-1); } }, lifetime); return result; } [[nodiscard]] not_null GenerateDefaultPreview( not_null parent, not_null controller, PremiumPreview section, Fn readyCallback) { switch (section) { case PremiumPreview::Reactions: return ReactionsPreview(parent, controller, {}, readyCallback); case PremiumPreview::Stickers: return StickersPreview(parent, controller, readyCallback); default: return GenericPreview(parent, controller, section, readyCallback); } } [[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, rpl::producer text) { auto result = CreatePremiumButton(parent); const auto &st = st::premiumPreviewBox.button; result->resize(result->width(), st.height); const auto label = Ui::CreateChild( result.data(), std::move(text), 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; } [[nodiscard]] object_ptr CreateSwitch( not_null parent, not_null*> selected) { const auto padding = st::premiumDotPadding; const auto width = padding.left() + st::premiumDot + padding.right(); const auto height = padding.top() + st::premiumDot + padding.bottom(); const auto stops = Ui::Premium::ButtonGradientStops(); auto result = object_ptr(parent.get(), height); const auto raw = result.data(); for (auto i = 0; i != kPreviewsCount; ++i) { const auto section = PremiumPreview(i); const auto button = Ui::CreateChild(raw); parent->widthValue( ) | rpl::start_with_next([=](int outer) { const auto full = width * kPreviewsCount; const auto left = (outer - full) / 2 + (i * width); button->setGeometry(left, 0, width, height); }, button->lifetime()); button->setClickedCallback([=] { *selected = section; }); button->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(button); auto hq = PainterHighQualityEnabler(p); p.setBrush((selected->current() == section) ? anim::gradient_color_at( stops, float64(i) / (kPreviewsCount - 1)) : st::windowBgRipple->c); p.setPen(Qt::NoPen); p.drawEllipse( button->rect().marginsRemoved(st::premiumDotPadding)); }, button->lifetime()); selected->changes( ) | rpl::start_with_next([=] { button->update(); }, button->lifetime()); } return result; } void PreviewBox( not_null box, not_null controller, const Descriptor &descriptor, const std::shared_ptr &media, const QImage &back) { const auto single = st::boxWideWidth; const auto size = QSize(single, st::premiumPreviewHeight); box->setWidth(size.width()); box->setNoContentMargin(true); const auto outer = box->addRow( ChatBackPreview(box, size.height(), back), {}); struct Hiding { not_null widget; int leftFrom = 0; int leftTill = 0; }; struct State { int leftFrom = 0; Ui::RpWidget *content = nullptr; Ui::RpWidget *stickersPreload = nullptr; bool stickersPreloadReady = false; Ui::RpWidget *reactionsPreload = nullptr; bool reactionsPreloadReady = false; bool preloadScheduled = false; bool showFinished = false; Ui::Animations::Simple animation; Fn preload; std::vector hiding; rpl::variable selected; }; const auto state = outer->lifetime().make_state(); state->selected = descriptor.section; const auto move = [=](int delta) { using Type = PremiumPreview; const auto count = int(Type::kCount); const auto now = state->selected.current(); state->selected = Type((int(now) + count + delta) % count); }; const auto buttonsParent = box->verticalLayout().get(); const auto close = Ui::CreateChild( buttonsParent, st::settingsPremiumTopBarClose); close->setClickedCallback([=] { box->closeBox(); }); const auto left = Ui::CreateChild( buttonsParent, st::settingsPremiumMoveLeft); left->setClickedCallback([=] { move(-1); }); const auto right = Ui::CreateChild( buttonsParent, st::settingsPremiumMoveRight); right->setClickedCallback([=] { move(1); }); buttonsParent->widthValue( ) | rpl::start_with_next([=](int width) { const auto outerHeight = st::premiumPreviewHeight; close->moveToRight(0, 0, width); left->moveToLeft(0, (outerHeight - left->height()) / 2, width); right->moveToRight(0, (outerHeight - right->height()) / 2, width); }, close->lifetime()); state->preload = [=] { if (!state->showFinished) { state->preloadScheduled = true; return; } const auto now = state->selected.current(); if (now != PremiumPreview::Stickers && !state->stickersPreload) { const auto ready = [=] { if (state->stickersPreload) { state->stickersPreloadReady = true; } else { state->preload(); } }; state->stickersPreload = GenerateDefaultPreview( outer, controller, PremiumPreview::Stickers, ready); state->stickersPreload->hide(); } if (now != PremiumPreview::Reactions && !state->reactionsPreload) { const auto ready = [=] { if (state->reactionsPreload) { state->reactionsPreloadReady = true; } else { state->preload(); } }; state->reactionsPreload = GenerateDefaultPreview( outer, controller, PremiumPreview::Reactions, ready); state->reactionsPreload->hide(); } }; switch (descriptor.section) { case PremiumPreview::Stickers: state->content = media ? StickerPreview(outer, controller, media, state->preload) : StickersPreview(outer, controller, state->preload); break; case PremiumPreview::Reactions: state->content = ReactionsPreview( outer, controller, descriptor.disabled, state->preload); break; default: state->content = GenericPreview( outer, controller, descriptor.section, state->preload); break; } state->selected.value( ) | rpl::combine_previous( ) | rpl::start_with_next([=](PremiumPreview was, PremiumPreview now) { const auto animationCallback = [=] { if (!state->animation.animating()) { for (const auto &hiding : base::take(state->hiding)) { delete hiding.widget; } state->leftFrom = 0; state->content->move(0, 0); } else { const auto progress = state->animation.value(1.); state->content->move( anim::interpolate(state->leftFrom, 0, progress), 0); for (const auto &hiding : state->hiding) { hiding.widget->move(anim::interpolate( hiding.leftFrom, hiding.leftTill, progress), 0); } } }; animationCallback(); const auto toLeft = int(now) > int(was); auto start = state->content->x() + (toLeft ? single : -single); for (const auto &hiding : state->hiding) { const auto left = hiding.widget->x(); if (toLeft && left + single > start) { start = left + single; } else if (!toLeft && left - single < start) { start = left - single; } } for (auto &hiding : state->hiding) { hiding.leftFrom = hiding.widget->x(); hiding.leftTill = hiding.leftFrom - start; } state->hiding.push_back({ .widget = state->content, .leftFrom = state->content->x(), .leftTill = state->content->x() - start, }); state->leftFrom = start; if (now == PremiumPreview::Stickers && state->stickersPreload) { state->content = base::take(state->stickersPreload); state->content->show(); if (base::take(state->stickersPreloadReady)) { state->preload(); } } else if (now == PremiumPreview::Reactions && state->reactionsPreload) { state->content = base::take(state->reactionsPreload); state->content->show(); if (base::take(state->reactionsPreloadReady)) { state->preload(); } } else { state->content = GenerateDefaultPreview( outer, controller, now, state->preload); } state->animation.stop(); state->animation.start( animationCallback, 0., 1., st::premiumSlideDuration, anim::sineInOut); }, outer->lifetime()); auto title = state->selected.value( ) | rpl::map([=](PremiumPreview section) { return SectionTitle(section); }) | rpl::flatten_latest(); auto text = state->selected.value( ) | rpl::map([=](PremiumPreview section) { return SectionAbout(section); }) | rpl::flatten_latest(); const auto padding = st::premiumPreviewAboutPadding; const auto available = size.width() - padding.left() - padding.right(); auto titleLabel = object_ptr( box, std::move(title), st::premiumPreviewAboutTitle); titleLabel->resizeToWidth(available); box->addRow( object_ptr>( box, std::move(titleLabel)), st::premiumPreviewAboutTitlePadding); auto textLabel = object_ptr( box, std::move(text), st::premiumPreviewAbout); textLabel->resizeToWidth(available); box->addRow( object_ptr>(box, std::move(textLabel)), padding); box->addRow( CreateSwitch(box->verticalLayout(), &state->selected), st::premiumDotsMargin); const auto showFinished = [=] { state->showFinished = true; if (base::take(state->preloadScheduled)) { state->preload(); } }; if (descriptor.fromSettings && controller->session().premium()) { box->setShowFinishedCallback(showFinished); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); } else { box->setStyle(st::premiumPreviewBox); const auto buttonPadding = st::premiumPreviewBox.buttonPadding; const auto width = size.width() - buttonPadding.left() - buttonPadding.right(); const auto computeRef = [=] { return Settings::LookupPremiumRef(state->selected.current()); }; auto unlock = state->selected.value( ) | rpl::map([=](PremiumPreview section) { return (section == PremiumPreview::Reactions) ? tr::lng_premium_unlock_reactions() : (section == PremiumPreview::Stickers) ? tr::lng_premium_unlock_stickers() : tr::lng_premium_more_about(); }) | rpl::flatten_latest(); auto button = descriptor.fromSettings ? object_ptr::fromRaw( Settings::CreateSubscribeButton(controller, box, computeRef)) : CreateUnlockButton(box, std::move(unlock)); button->resizeToWidth(width); if (!descriptor.fromSettings) { button->setClickedCallback([=] { Settings::ShowPremium( controller, Settings::LookupPremiumRef(state->selected.current())); }); } box->setShowFinishedCallback([=, raw = button.data()]{ showFinished(); raw->startGlareAnimation(); }); box->addButton(std::move(button)); } if (descriptor.fromSettings) { Data::AmPremiumValue( &controller->session() ) | rpl::skip(1) | rpl::start_with_next([=] { box->closeBox(); }, box->lifetime()); } box->events( ) | rpl::start_with_next([=](not_null e) { if (e->type() == QEvent::KeyPress) { const auto key = static_cast(e.get())->key(); if (key == Qt::Key_Left) { move(-1); } else if (key == Qt::Key_Right) { move(1); } } }, box->lifetime()); if (const auto &hidden = descriptor.hiddenCallback) { box->boxClosing() | rpl::start_with_next(hidden, box->lifetime()); } } void Show( not_null controller, const Descriptor &descriptor, const std::shared_ptr &media, QImage back) { controller->show(Box(PreviewBox, 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; } } } void Show( not_null controller, Descriptor &&descriptor) { if (!controller->session().premiumPossible()) { controller->show(Box(PremiumUnavailableBox)); return; } 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 stops = Ui::Premium::LimitGradientStops(); crl::async([=] { const auto factor = style::DevicePixelRatio(); auto cropped = QImage( fill * factor, QImage::Format_ARGB32_Premultiplied); cropped.setDevicePixelRatio(factor); auto p = QPainter(&cropped); auto gradient = QLinearGradient(0, fill.height(), fill.width(), 0); gradient.setStops(stops); p.fillRect(QRect(QPoint(), fill), gradient); p.end(); 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_map &disabled) { Show(controller, Descriptor{ .section = section, .disabled = disabled, }); } void ShowPremiumPreviewToBuy( not_null controller, PremiumPreview section, Fn hiddenCallback) { Show(controller, Descriptor{ .section = section, .fromSettings = true, .hiddenCallback = std::move(hiddenCallback), }); } void PremiumUnavailableBox(not_null box) { Ui::ConfirmBox(box, { .text = tr::lng_premium_unavailable( tr::now, Ui::Text::RichLangValue), .inform = true, }); } void DoubledLimitsPreviewBox( not_null box, not_null session) { const auto limits = Data::PremiumLimits(session); auto entries = std::vector(); { const auto premium = limits.channelsPremium(); entries.push_back(Ui::Premium::ListEntry{ tr::lng_premium_double_limits_subtitle_channels(), tr::lng_premium_double_limits_about_channels( lt_count, rpl::single(float64(premium)), Ui::Text::RichLangValue), limits.channelsDefault(), premium, }); } { const auto premium = limits.dialogsFolderPinnedPremium(); entries.push_back(Ui::Premium::ListEntry{ tr::lng_premium_double_limits_subtitle_pins(), tr::lng_premium_double_limits_about_pins( lt_count, rpl::single(float64(premium)), Ui::Text::RichLangValue), limits.dialogsFolderPinnedDefault(), premium, }); } { const auto premium = limits.channelsPublicPremium(); entries.push_back(Ui::Premium::ListEntry{ tr::lng_premium_double_limits_subtitle_links(), tr::lng_premium_double_limits_about_links( lt_count, rpl::single(float64(premium)), Ui::Text::RichLangValue), limits.channelsPublicDefault(), premium, }); } { const auto premium = limits.gifsPremium(); entries.push_back(Ui::Premium::ListEntry{ tr::lng_premium_double_limits_subtitle_gifs(), tr::lng_premium_double_limits_about_gifs( lt_count, rpl::single(float64(premium)), Ui::Text::RichLangValue), limits.gifsDefault(), premium, }); } { const auto premium = limits.stickersFavedPremium(); entries.push_back(Ui::Premium::ListEntry{ tr::lng_premium_double_limits_subtitle_stickers(), tr::lng_premium_double_limits_about_stickers( lt_count, rpl::single(float64(premium)), Ui::Text::RichLangValue), limits.stickersFavedDefault(), premium, }); } { const auto premium = limits.aboutLengthPremium(); entries.push_back(Ui::Premium::ListEntry{ tr::lng_premium_double_limits_subtitle_bio(), tr::lng_premium_double_limits_about_bio( Ui::Text::RichLangValue), limits.aboutLengthDefault(), premium, }); } { const auto premium = limits.captionLengthPremium(); entries.push_back(Ui::Premium::ListEntry{ tr::lng_premium_double_limits_subtitle_captions(), tr::lng_premium_double_limits_about_captions( Ui::Text::RichLangValue), limits.captionLengthDefault(), premium, }); } { const auto premium = limits.dialogFiltersPremium(); entries.push_back(Ui::Premium::ListEntry{ tr::lng_premium_double_limits_subtitle_folders(), tr::lng_premium_double_limits_about_folders( lt_count, rpl::single(float64(premium)), Ui::Text::RichLangValue), limits.dialogFiltersDefault(), premium, }); } { const auto premium = limits.dialogFiltersChatsPremium(); entries.push_back(Ui::Premium::ListEntry{ tr::lng_premium_double_limits_subtitle_folder_chats(), tr::lng_premium_double_limits_about_folder_chats( lt_count, rpl::single(float64(premium)), Ui::Text::RichLangValue), limits.dialogFiltersChatsDefault(), premium, }); } const auto nextMax = session->domain().maxAccounts() + 1; const auto till = (nextMax >= Main::Domain::kPremiumMaxAccounts) ? QString::number(Main::Domain::kPremiumMaxAccounts) : (QString::number(nextMax) + QChar('+')); entries.push_back(Ui::Premium::ListEntry{ tr::lng_premium_double_limits_subtitle_accounts(), tr::lng_premium_double_limits_about_accounts( lt_count, rpl::single(float64(Main::Domain::kPremiumMaxAccounts)), Ui::Text::RichLangValue), Main::Domain::kMaxAccounts, Main::Domain::kPremiumMaxAccounts, till, }); Ui::Premium::ShowListBox(box, std::move(entries)); }