/* 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_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 "ui/painter.h" #include "settings/settings_common.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_premium.h" #include "styles/style_settings.h" #include namespace { constexpr auto kPremiumShift = 21. / 240; constexpr auto kReactionsPerRow = 5; constexpr auto kDisabledOpacity = 0.5; constexpr auto kToggleStickerTimeout = 2 * crl::time(1000); constexpr auto kStarOpacityOff = 0.1; constexpr auto kStarOpacityOn = 1.; constexpr auto kStarPeriod = 3 * crl::time(1000); using Data::ReactionId; struct Descriptor { PremiumPreview section = PremiumPreview::Stickers; DocumentData *requestedSticker = nullptr; bool fromSettings = false; Fn hiddenCallback; Fn)> shownCallback; }; bool operator==(const Descriptor &a, const Descriptor &b) { return (a.section == b.section) && (a.requestedSticker == b.requestedSticker) && (a.fromSettings == b.fromSettings); } struct Preload { Descriptor descriptor; std::shared_ptr media; std::weak_ptr show; }; [[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::Stories: return tr::lng_premium_summary_subtitle_stories(); case PremiumPreview::DoubleLimits: return tr::lng_premium_summary_subtitle_double_limits(); 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::EmojiStatus: return tr::lng_premium_summary_subtitle_emoji_status(); case PremiumPreview::InfiniteReactions: return tr::lng_premium_summary_subtitle_infinite_reactions(); case PremiumPreview::Stickers: return tr::lng_premium_summary_subtitle_premium_stickers(); case PremiumPreview::AnimatedEmoji: return tr::lng_premium_summary_subtitle_animated_emoji(); 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(); case PremiumPreview::RealTimeTranslation: return tr::lng_premium_summary_subtitle_translation(); } Unexpected("PremiumPreview in SectionTitle."); } [[nodiscard]] rpl::producer SectionAbout(PremiumPreview section) { switch (section) { case PremiumPreview::Stories: return tr::lng_premium_summary_about_stories(); case PremiumPreview::DoubleLimits: return tr::lng_premium_summary_about_double_limits(); 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::EmojiStatus: return tr::lng_premium_summary_about_emoji_status(); case PremiumPreview::InfiniteReactions: return tr::lng_premium_summary_about_infinite_reactions(); case PremiumPreview::Stickers: return tr::lng_premium_summary_about_premium_stickers(); case PremiumPreview::AnimatedEmoji: return tr::lng_premium_summary_about_animated_emoji(); 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(); case PremiumPreview::RealTimeTranslation: return tr::lng_premium_summary_about_translation(); } 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, std::shared_ptr show, 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; style::owned_color pathFg = style::owned_color( QColor(255, 255, 255, 64)); 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) { show->session().downloaderTaskFinished( ) | rpl::take_while([=] { createLottieIfReady(); return !state->lottie || !state->effect; }) | rpl::start(result->lifetime()); } state->pathGradient = std::make_unique( st::shadowFg, state->pathFg.color(), [=] { result->update(); }, rpl::never<>()); 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(st::shadowFg); 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, std::shared_ptr show, 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 = &show->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, show, 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) || (section == PremiumPreview::AnimatedEmoji); } [[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::AnimatedEmoji: return "animated_emoji"; case PremiumPreview::AdvancedChatManagement: return "advanced_chat_management"; case PremiumPreview::EmojiStatus: return "emoji_status"; case PremiumPreview::InfiniteReactions: return "infinite_reactions"; case PremiumPreview::ProfileBadge: return "profile_badge"; case PremiumPreview::AnimatedUserpics: return "animated_userpics"; case PremiumPreview::RealTimeTranslation: return "translations"; } 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, std::shared_ptr show, 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 ready = state->instance.player().ready() && !state->instance.player().videoSize().isEmpty(); const auto size = QSize(width, height) * style::DevicePixelRatio(); using namespace Images; auto rounding = CornersMaskRef( Images::CornersMask(ImageRoundRadius::Large)); if (alignToBottom) { rounding.p[kBottomLeft] = rounding.p[kBottomRight] = nullptr; } else { rounding.p[kTopLeft] = rounding.p[kTopRight] = nullptr; } const auto frame = !ready ? state->blurred : state->instance.frame({ .resize = size, .outer = size, .rounding = rounding, }); 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, std::shared_ptr show, 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 = &show->session(); const auto state = lifetime.make_state(); const auto create = [=] { const auto document = LookupVideo(session, section); if (!document) { return; } state->single = VideoPreview( result, show, document, !VideoAlignToTop(section), readyCallback); }; create(); if (!state->single) { session->api().premium().videosUpdated( ) | rpl::take(1) | rpl::start_with_next(create, lifetime); } return result; } [[nodiscard]] not_null GenerateDefaultPreview( not_null parent, std::shared_ptr show, PremiumPreview section, Fn readyCallback) { switch (section) { case PremiumPreview::Stickers: return StickersPreview(parent, std::move(show), readyCallback); default: return GenericPreview( parent, std::move(show), 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 CreateSwitch( not_null parent, not_null*> selected, std::vector order) { 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(); const auto count = order.size(); for (auto i = 0; i != count; ++i) { const auto section = order[i]; const auto button = Ui::CreateChild(raw); parent->widthValue( ) | rpl::start_with_next([=](int outer) { const auto full = width * count; 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) / (count - 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, std::shared_ptr show, 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; bool preloadScheduled = false; bool showFinished = false; Ui::Animations::Simple animation; Fn preload; std::vector hiding; rpl::variable selected; std::vector order; }; const auto state = outer->lifetime().make_state(); state->selected = descriptor.section; state->order = Settings::PremiumPreviewOrder(&show->session()); const auto index = [=](PremiumPreview section) { const auto it = ranges::find(state->order, section); return (it == end(state->order)) ? 0 : std::distance(begin(state->order), it); }; const auto move = [=](int delta) { const auto count = int(state->order.size()); const auto now = state->selected.current(); state->selected = state->order[(index(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, show, PremiumPreview::Stickers, ready); state->stickersPreload->hide(); } }; switch (descriptor.section) { case PremiumPreview::Stickers: state->content = media ? StickerPreview(outer, show, media, state->preload) : StickersPreview(outer, show, state->preload); break; default: state->content = GenericPreview( outer, show, 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 = index(now) > index(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 { state->content = GenerateDefaultPreview( outer, show, 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(SectionTitle) | rpl::flatten_latest(); auto text = state->selected.value( ) | rpl::map(SectionAbout) | 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, state->order), st::premiumDotsMargin); const auto showFinished = [=] { state->showFinished = true; if (base::take(state->preloadScheduled)) { state->preload(); } }; if (descriptor.fromSettings && show->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::InfiniteReactions) ? tr::lng_premium_unlock_reactions() : (section == PremiumPreview::Stickers) ? tr::lng_premium_unlock_stickers() : (section == PremiumPreview::AnimatedEmoji) ? tr::lng_premium_unlock_emoji() : (section == PremiumPreview::EmojiStatus) ? tr::lng_premium_unlock_status() : tr::lng_premium_more_about(); }) | rpl::flatten_latest(); auto button = descriptor.fromSettings ? object_ptr::fromRaw( Settings::CreateSubscribeButton({ .parent = box, .computeRef = computeRef, .show = show, })) : CreateUnlockButton(box, std::move(unlock)); button->resizeToWidth(width); if (!descriptor.fromSettings) { button->setClickedCallback([=] { const auto window = show->resolveWindow( ChatHelpers::WindowUsage::PremiumPromo); if (!window) { return; } Settings::ShowPremium( window, Settings::LookupPremiumRef(state->selected.current())); }); } box->setShowFinishedCallback([=, raw = button.data()]{ showFinished(); raw->startGlareAnimation(); }); box->addButton(std::move(button)); } if (descriptor.fromSettings) { Data::AmPremiumValue( &show->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( std::shared_ptr show, const Descriptor &descriptor, const std::shared_ptr &media, QImage back) { auto box = Box(PreviewBox, show, descriptor, media, back); const auto raw = box.data(); show->showBox(std::move(box)); if (descriptor.shownCallback) { descriptor.shownCallback(raw); } } void Show(std::shared_ptr show, QImage back) { auto &list = Preloads(); for (auto i = begin(list); i != end(list);) { const auto already = i->show.lock(); if (!already) { i = list.erase(i); } else if (already == show) { Show(std::move(show), i->descriptor, i->media, back); i = list.erase(i); return; } else { ++i; } } } void DecorateListPromoBox( not_null box, std::shared_ptr show, const Descriptor &descriptor) { const auto session = &show->session(); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); Data::AmPremiumValue( session ) | rpl::skip(1) | rpl::start_with_next([=] { box->closeBox(); }, box->lifetime()); if (const auto &hidden = descriptor.hiddenCallback) { box->boxClosing() | rpl::start_with_next(hidden, box->lifetime()); } if (session->premium()) { box->addButton(tr::lng_close(), [=] { box->closeBox(); }); } else { const auto button = Settings::CreateSubscribeButton({ .parent = box, .computeRef = [] { return u"double_limits"_q; }, .show = show, }); box->setShowFinishedCallback([=] { button->startGlareAnimation(); }); box->setStyle(st::premiumPreviewDoubledLimitsBox); box->widthValue( ) | rpl::start_with_next([=](int width) { const auto &padding = st::premiumPreviewDoubledLimitsBox.buttonPadding; button->resizeToWidth(width - padding.left() - padding.right()); button->moveToLeft(padding.left(), padding.top()); }, button->lifetime()); box->addButton( object_ptr::fromRaw(button)); } } void Show( std::shared_ptr show, Descriptor &&descriptor) { if (!show->session().premiumPossible()) { auto box = Box(PremiumUnavailableBox); const auto raw = box.data(); show->showBox(std::move(box)); if (descriptor.shownCallback) { descriptor.shownCallback(raw); } return; } else if (descriptor.section == PremiumPreview::DoubleLimits) { show->showBox(Box([=](not_null box) { DoubledLimitsPreviewBox(box, &show->session()); DecorateListPromoBox(box, show, descriptor); })); return; } else if (descriptor.section == PremiumPreview::Stories) { show->showBox(Box([=](not_null box) { UpgradedStoriesPreviewBox(box, &show->session()); DecorateListPromoBox(box, show, descriptor); })); return; } auto &list = Preloads(); for (auto i = begin(list); i != end(list);) { const auto already = i->show.lock(); if (!already) { i = list.erase(i); } else if (already == show) { 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 = std::weak_ptr(show); list.push_back({ .descriptor = descriptor, .media = (descriptor.requestedSticker ? descriptor.requestedSticker->createMediaView() : nullptr), .show = 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 (auto strong = weak.lock()) { Show(std::move(strong), result); } }); }); } } // namespace void ShowStickerPreviewBox( std::shared_ptr show, not_null document) { Show(std::move(show), Descriptor{ .section = PremiumPreview::Stickers, .requestedSticker = document, }); } void ShowPremiumPreviewBox( not_null controller, PremiumPreview section, Fn)> shown) { ShowPremiumPreviewBox(controller->uiShow(), section, std::move(shown)); } void ShowPremiumPreviewBox( std::shared_ptr show, PremiumPreview section, Fn)> shown) { Show(std::move(show), Descriptor{ .section = section, .shownCallback = std::move(shown), }); } void ShowPremiumPreviewToBuy( not_null controller, PremiumPreview section, Fn hiddenCallback) { Show(controller->uiShow(), 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) { box->setTitle(tr::lng_premium_summary_subtitle_double_limits()); const auto limits = Data::PremiumLimits(session); auto entries = std::vector(); { const auto premium = limits.channelsPremium(); entries.push_back({ 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.dialogsPinnedPremium(); entries.push_back({ tr::lng_premium_double_limits_subtitle_pins(), tr::lng_premium_double_limits_about_pins( lt_count, rpl::single(float64(premium)), Ui::Text::RichLangValue), limits.dialogsPinnedDefault(), premium, }); } { const auto premium = limits.channelsPublicPremium(); entries.push_back({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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, st::defaultPremiumLimits, std::move(entries)); } void UpgradedStoriesPreviewBox( not_null box, not_null session) { using namespace Ui::Text; box->setTitle(tr::lng_premium_summary_subtitle_stories()); auto entries = std::vector(); entries.push_back({ .title = tr::lng_premium_stories_subtitle_order(), .about = tr::lng_premium_stories_about_order(WithEntities), .icon = &st::settingsStoriesIconOrder, }); entries.push_back({ .title = tr::lng_premium_stories_subtitle_stealth(), .about = tr::lng_premium_stories_about_stealth(WithEntities), .icon = &st::settingsStoriesIconStealth, }); entries.push_back({ .title = tr::lng_premium_stories_subtitle_views(), .about = tr::lng_premium_stories_about_views(WithEntities), .icon = &st::settingsStoriesIconViews, }); entries.push_back({ .title = tr::lng_premium_stories_subtitle_expiration(), .about = tr::lng_premium_stories_about_expiration(WithEntities), .icon = &st::settingsStoriesIconExpiration, }); entries.push_back({ .title = tr::lng_premium_stories_subtitle_download(), .about = tr::lng_premium_stories_about_download(WithEntities), .icon = &st::settingsStoriesIconDownload, }); entries.push_back({ .title = tr::lng_premium_stories_subtitle_caption(), .about = tr::lng_premium_stories_about_caption(WithEntities), .icon = &st::settingsStoriesIconCaption, }); entries.push_back({ .title = tr::lng_premium_stories_subtitle_links(), .about = tr::lng_premium_stories_about_links(WithEntities), .icon = &st::settingsStoriesIconLinks, }); Ui::Premium::ShowListBox( box, st::defaultPremiumLimits, std::move(entries)); Settings::AddDividerText( box->verticalLayout(), tr::lng_premium_stories_about_mobile()); } 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; }