From 9263c08068e6b6c30d03e11b3879d3f6e690fabd Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 12 Dec 2025 14:59:44 +0400 Subject: [PATCH] Started the gift upgrade animation. --- .../SourceFiles/boxes/gift_premium_box.cpp | 8 +- Telegram/SourceFiles/boxes/gift_premium_box.h | 2 + Telegram/SourceFiles/boxes/star_gift_box.cpp | 604 +++++++++++++++--- Telegram/SourceFiles/boxes/star_gift_box.h | 2 + Telegram/SourceFiles/data/data_star_gift.h | 18 + .../settings/settings_credits_graphics.cpp | 4 +- .../settings/settings_credits_graphics.h | 2 + .../ui/top_background_gradient.cpp | 31 +- .../SourceFiles/ui/top_background_gradient.h | 14 + 9 files changed, 575 insertions(+), 110 deletions(-) diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 97ee1effde..493ef991b2 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -279,7 +279,8 @@ using Ui::TableRowTooltipData; void AddUniqueGiftPropertyRows( not_null container, not_null table, - not_null unique) { + not_null unique, + std::shared_ptr spinner) { const auto tooltip = std::make_shared( TableRowTooltipData{ .parent = container }); const auto showTooltip = [=]( @@ -1204,6 +1205,7 @@ void AddStarGiftTable( not_null container, Settings::CreditsEntryBoxStyleOverrides st, const Data::CreditsHistoryEntry &entry, + std::shared_ptr spinner, Fn convertToStars, bool canStartUpgrade, Fn removed)> removeDetails) { @@ -1389,7 +1391,7 @@ void AddStarGiftTable( rpl::single(Ui::Text::WithEntities(langDateTime(entry.date)))); } if (unique) { - AddUniqueGiftPropertyRows(container, table, unique); + AddUniqueGiftPropertyRows(container, table, unique, spinner); } else { AddTableRow( table, @@ -1536,7 +1538,7 @@ void AddTransferGiftTable( container, st::giveawayGiftCodeTable), st::giveawayGiftCodeTableMargin); - AddUniqueGiftPropertyRows(container, table, unique.get()); + AddUniqueGiftPropertyRows(container, table, unique.get(), nullptr); if (const auto value = unique->value.get()) { AddTableRow( table, diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index 74535dbce6..39a3879fd3 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -24,6 +24,7 @@ struct GiveawayStart; struct GiveawayResults; struct SubscriptionEntry; struct UniqueGift; +struct GiftUpgradeSpinner; } // namespace Data namespace Main { @@ -77,6 +78,7 @@ void AddStarGiftTable( not_null container, Settings::CreditsEntryBoxStyleOverrides st, const Data::CreditsHistoryEntry &entry, + std::shared_ptr spinner, Fn convertToStars, bool canStartUpgrade, Fn removed)> removeDetails); diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 0768149e08..0c1a419350 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -142,6 +142,18 @@ constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000); constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000); constexpr auto kGradientButtonBgOpacity = 0.6; +constexpr auto kSpinnerBackdrops = 4; +constexpr auto kSpinnerPatterns = 4; +constexpr auto kSpinnerModels = 4; + +constexpr auto kBackdropSpinDuration = crl::time(300); +constexpr auto kBackdropStopsAt = 10 * 3 * crl::time(1000); +constexpr auto kPatternSpinDuration = crl::time(600); +constexpr auto kPatternStopsAt = 10 * 5 * crl::time(1000); +constexpr auto kModelSpinDuration = crl::time(300); +constexpr auto kModelStopsAt = 10 * 7 * crl::time(1000); +constexpr auto kModelScaleFrom = 0.7; + using namespace HistoryView; using namespace Info::PeerGifts; @@ -2833,6 +2845,8 @@ void AddUniqueGiftCover( not_null container, rpl::producer data, UniqueGiftCoverArgs &&args) { + using SpinnerState = Data::GiftUpgradeSpinner::State; + const auto cover = container->add(object_ptr(container)); struct Released { @@ -2852,70 +2866,282 @@ void AddUniqueGiftCover( }; const auto released = cover->lifetime().make_state(); - struct GiftView { + struct BackdropView { + Data::UniqueGiftBackdrop colors; QImage gradient; - std::optional gift; - std::shared_ptr media; - std::unique_ptr lottie; + }; + struct PatternView { + DocumentData *document = nullptr; std::unique_ptr emoji; base::flat_map emojis; - bool forced = false; + }; + struct ModelView { + std::shared_ptr media; + std::unique_ptr lottie; rpl::lifetime lifetime; }; + struct GiftView { + std::optional gift; + BackdropView backdrop; + PatternView pattern; + ModelView model; + bool spinned = false; + bool forced = false; + }; + struct AttributeSpin { + AttributeSpin(crl::time duration) : duration(duration) { + } + + Animations::Simple animation; + crl::time duration = 0; + int wasIndex = -1; + int nowIndex = -1; + int willIndex = -1; + + [[nodiscard]] float64 progress() const { + return animation.value(1.); + } + void startWithin(int count, Fn update) { + Expects(count > 0); + + wasIndex = nowIndex; + nowIndex = willIndex; + willIndex = (willIndex < 0 ? 1 : (willIndex + 1)) % count; + animation.start(update, 0., 1., duration); + } + void startToTarget(Fn update) { + if (willIndex != 0) { + wasIndex = nowIndex; + nowIndex = willIndex; + willIndex = 0; + animation.start(update, 0., 1., duration); + } + } + }; struct State { + std::shared_ptr spinner; + Fn checkSpinnerStart; GiftView now; GiftView next; Animations::Simple crossfade; Animations::Simple heightAnimation; + std::vector spinnerBackdrops; + std::vector spinnerPatterns; + std::vector spinnerModels; + AttributeSpin backdropSpin = kBackdropSpinDuration; + AttributeSpin patternSpin = kPatternSpinDuration; + AttributeSpin modelSpin = kModelSpinDuration; + crl::time spinStarted = 0; int heightFinal = 0; - bool animating = false; + bool crossfading = false; bool updateAttributesPending = false; }; const auto state = cover->lifetime().make_state(); + state->spinner = std::move(args.upgradeSpinner); + + const auto setupModel = [=]( + ModelView &to, + const Data::UniqueGiftModel &model) { + to.lifetime.destroy(); + + const auto document = model.document; + to.media = document->createMediaView(); + to.media->automaticLoad({}, nullptr); + rpl::single() | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::filter([&to] { + return to.media->loaded(); + }) | rpl::on_next([=, &to] { + const auto lottieSize = st::creditsHistoryEntryStarGiftSize; + to.lottie = ChatHelpers::LottiePlayerFromDocument( + to.media.get(), + ChatHelpers::StickerLottieSize::MessageHistory, + QSize(lottieSize, lottieSize), + Lottie::Quality::High); + + to.lifetime.destroy(); + const auto lottie = to.lottie.get(); + lottie->updates() | rpl::on_next([=] { + if (state->now.model.lottie.get() == lottie + || state->crossfade.animating()) { + cover->update(); + } + if (const auto onstack = state->checkSpinnerStart) { + onstack(); + } + }, to.lifetime); + }, to.lifetime); + }; + const auto setupPattern = [=]( + PatternView &to, + const Data::UniqueGiftPattern &pattern) { + const auto document = pattern.document; + const auto callback = [=] { + if (state->now.pattern.document == document + || state->crossfade.animating()) { + cover->update(); + } + if (const auto onstack = state->checkSpinnerStart) { + onstack(); + } + }; + to.document = document; + to.emoji = document->owner().customEmojiManager().create( + document, + callback, + Data::CustomEmojiSizeTag::Large); + [[maybe_unused]] const auto preload = to.emoji->ready(); + }; + + if (const auto spinner = state->spinner.get()) { + const auto fillBackdrops = [=] { + state->spinnerBackdrops.clear(); + state->spinnerBackdrops.reserve(kSpinnerBackdrops); + const auto push = [&](const Data::UniqueGiftBackdrop &backdrop) { + if (state->spinnerBackdrops.size() >= kSpinnerBackdrops) { + return false; + } + const auto already = ranges::contains( + state->spinnerBackdrops, + backdrop, + &BackdropView::colors); + if (!already) { + state->spinnerBackdrops.push_back({ backdrop }); + } + return true; + }; + push(spinner->target->backdrop); + for (const auto &backdrop : spinner->attributes.backdrops) { + if (!push(backdrop)) { + break; + } + } + }; + const auto fillPatterns = [=] { + state->spinnerPatterns.clear(); + state->spinnerPatterns.reserve(kSpinnerPatterns); + const auto push = [&](const Data::UniqueGiftPattern &pattern) { + if (state->spinnerPatterns.size() >= kSpinnerPatterns) { + return false; + } + const auto already = ranges::contains( + state->spinnerPatterns, + pattern.document.get(), + &PatternView::document); + if (!already) { + setupPattern( + state->spinnerPatterns.emplace_back(), + pattern); + } + return true; + }; + push(spinner->target->pattern); + for (const auto &pattern : spinner->attributes.patterns) { + if (!push(pattern)) { + break; + } + } + }; + const auto fillModels = [=] { + state->spinnerModels.clear(); + state->spinnerModels.reserve(kSpinnerModels); + const auto push = [&](const Data::UniqueGiftModel &model) { + if (state->spinnerModels.size() >= kSpinnerModels) { + return false; + } + const auto already = ranges::contains( + state->spinnerModels, + model.document, + [](const ModelView &view) { + return view.media->owner(); + }); + if (!already) { + setupModel( + state->spinnerModels.emplace_back(), + model); + } + return true; + }; + push(spinner->target->model); + for (const auto &model : spinner->attributes.models) { + if (!push(model)) { + break; + } + } + }; + state->checkSpinnerStart = [=] { + if (spinner->state.current() != SpinnerState::Loading + || state->crossfading) { + return; + } + for (const auto &pattern : state->spinnerPatterns) { + if (!pattern.emoji->ready()) { + return; + } + } + for (const auto &model : state->spinnerModels) { + if (!model.lottie || !model.lottie->ready()) { + return; + } + } + spinner->state = SpinnerState::Prepared; + }; + spinner->state.value() | rpl::on_next([=](SpinnerState now) { + if (now == SpinnerState::Preparing) { + fillBackdrops(); + fillPatterns(); + fillModels(); + spinner->state = SpinnerState::Loading; + if (!state->crossfading) { + state->next = {}; + } + state->checkSpinnerStart(); + } else if (now == SpinnerState::Started) { + const auto repaint = [=] { cover->update(); }; + state->backdropSpin.startWithin( + state->spinnerBackdrops.size(), + repaint); + state->patternSpin.startWithin( + state->spinnerPatterns.size(), + repaint); + state->modelSpin.startWithin( + state->spinnerModels.size(), + repaint); + state->spinStarted = crl::now(); + } + }, cover->lifetime()); + } const auto lottieSize = st::creditsHistoryEntryStarGiftSize; rpl::duplicate( data ) | rpl::on_next([=](const UniqueGiftCover &now) { const auto setup = [&](GiftView &to) { + to = {}; to.gift = now.values; to.forced = now.force; - const auto document = now.values.model.document; - to.media = document->createMediaView(); - to.media->automaticLoad({}, nullptr); - rpl::single() | rpl::then( - document->session().downloaderTaskFinished() - ) | rpl::filter([&to] { - return to.media->loaded(); - }) | rpl::on_next([=, &to] { - const auto lottieSize = st::creditsHistoryEntryStarGiftSize; - to.lottie = ChatHelpers::LottiePlayerFromDocument( - to.media.get(), - ChatHelpers::StickerLottieSize::MessageHistory, - QSize(lottieSize, lottieSize), - Lottie::Quality::High); - - to.lifetime.destroy(); - const auto lottie = to.lottie.get(); - lottie->updates() | rpl::on_next([=] { - if (state->now.lottie.get() == lottie - || state->crossfade.animating()) { - cover->update(); - } - }, to.lifetime); - }, to.lifetime); - to.emoji = document->owner().customEmojiManager().create( - now.values.pattern.document, - [=] { cover->update(); }, - Data::CustomEmojiSizeTag::Large); - [[maybe_unused]] const auto preload = to.emoji->ready(); + to.spinned = now.spinner; + to.backdrop.colors = now.values.backdrop; + setupModel(to.model, now.values.model); + setupPattern(to.pattern, now.values.pattern); }; + const auto spinner = state->spinner.get(); if (!state->now.gift) { setup(state->now); cover->update(); + } else if (now.spinner) { + Assert(spinner != nullptr); + Assert(spinner->state.current() == SpinnerState::Prepared); + + spinner->state = SpinnerState::Started; } else if (!state->next.gift || now.force) { - setup(state->next); + const auto spinnerState = spinner + ? spinner->state.current() + : SpinnerState::Initial; + if (spinnerState == SpinnerState::Initial) { + setup(state->next); + } } }, cover->lifetime()); @@ -3095,7 +3321,7 @@ void AddUniqueGiftCover( } else { released->subtitleButton = nullptr; } - updateColors(state->crossfade.value(state->animating ? 1. : 0.)); + updateColors(state->crossfade.value(state->crossfading ? 1. : 0.)); }, title->lifetime()); const auto attrs = args.attributesInfo @@ -3259,54 +3485,58 @@ void AddUniqueGiftCover( } }, cover->lifetime()); - cover->paintRequest() | rpl::on_next([=] { - auto p = QPainter(cover); - - auto progress = state->crossfade.value(state->animating ? 1. : 0.); + cover->paintOn([=](QPainter &p) { + auto progress = state->crossfade.value(state->crossfading ? 1. : 0.); if (state->updateAttributesPending && progress >= 0.5) { state->updateAttributesPending = false; updateAttrs(*state->next.gift); } else if (state->updateAttributesPending - && !state->animating && !state->crossfade.animating()) { state->updateAttributesPending = false; updateAttrs(*state->now.gift); } - if (state->animating) { + if (state->crossfading) { updateColors(progress); } if (progress == 1.) { - state->animating = false; + state->crossfading = false; state->now = base::take(state->next); progress = 0.; } - const auto paint = [&](GiftView &gift, float64 shown) { - Expects(gift.gift.has_value()); - - const auto width = cover->width(); - const auto pointsHeight = st::uniqueGiftSubtitleTop; + const auto width = cover->width(); + const auto getBackdrop = [&](BackdropView &backdrop) { const auto ratio = style::DevicePixelRatio(); const auto gradientSize = QSize( - cover->width(), + width, std::max(cover->height(), state->heightFinal)); - if (gift.gradient.size() != gradientSize * ratio) { - gift.gradient = Ui::CreateTopBgGradient( + auto &gradient = backdrop.gradient; + if (gradient.size() != gradientSize * ratio) { + gradient = Ui::CreateTopBgGradient( gradientSize, - *gift.gift); + backdrop.colors); } - p.drawImage(0, 0, gift.gradient); - - if (gift.gift->pattern.document != gift.gift->model.document) { - Ui::PaintBgPoints( - p, - Ui::PatternBgPoints(), - gift.emojis, - gift.emoji.get(), - *gift.gift, - QRect(0, 0, width, pointsHeight), - shown); - } - const auto lottie = gift.lottie.get(); + return gradient; + }; + const auto paintPattern = [&]( + QPainter &p, + PatternView &pattern, + const BackdropView &backdrop, + float64 shown) { + Ui::PaintBgPoints( + p, + Ui::PatternBgPoints(), + pattern.emojis, + pattern.emoji.get(), + backdrop.colors, + QRect(0, 0, width, st::uniqueGiftSubtitleTop), + shown); + }; + const auto paintModel = [&]( + QPainter &p, + ModelView &model, + float64 scale = 1., + bool paused = false) { + const auto lottie = model.lottie.get(); const auto factor = style::DevicePixelRatio(); const auto request = Lottie::FrameRequest{ .box = Size(lottieSize) * factor, @@ -3318,34 +3548,173 @@ void AddUniqueGiftCover( return false; } const auto size = frame.image.size() / factor; - const auto left = (width - size.width()) / 2; - p.drawImage( - QRect(QPoint(left, st::uniqueGiftModelTop), size), - frame.image); + const auto rect = QRect( + QPoint((width - size.width()) / 2, st::uniqueGiftModelTop), + size); + if (scale < 1.) { + const auto origin = rect.center(); + p.translate(origin); + p.scale(scale, scale); + p.translate(-origin); + } + p.drawImage(rect, frame.image); const auto count = lottie->framesCount(); const auto finished = lottie->frameIndex() == (count - 1); - lottie->markFrameShown(); + if (!paused) { + lottie->markFrameShown(); + } return finished; }; + const auto paint = [&](GiftView &gift, float64 shown) { + Expects(gift.gift.has_value()); - if (progress < 1.) { - const auto finished = paint(state->now, 1. - progress) - || (state->next.forced - && (!state->animating || !state->crossfade.animating())); - const auto next = finished ? state->next.lottie.get() : nullptr; - if (next && next->ready()) { - state->animating = true; - state->updateAttributesPending = true; - state->crossfade.start([=] { - cover->update(); - }, 0., 1., kCrossfadeDuration); + p.drawImage(0, 0, getBackdrop(gift.backdrop)); + if (gift.gift->pattern.document != gift.gift->model.document) { + paintPattern(p, gift.pattern, gift.backdrop, shown); + } + return paintModel(p, gift.model); + }; + + if (state->spinStarted) { + const auto select = [&](auto &&list, int index, auto &fallback) + -> decltype(auto) { + return (index >= 0) ? list[index] : fallback; + }; + + const auto now = crl::now(); + const auto elapsed = now - state->spinStarted; + auto current = state->spinner->state.current(); + const auto switchTo = [&](SpinnerState to, crl::time at) { + if (elapsed >= at && int(current) < int(to)) { + current = to; + state->spinner->state = to; + } + }; + switchTo(SpinnerState::FinishedBackdrop, kBackdropStopsAt); + switchTo(SpinnerState::FinishedPattern, kPatternStopsAt); + switchTo(SpinnerState::Finished, kModelStopsAt); + + const auto actualize = [&]( + AttributeSpin &spin, + auto &&list, + SpinnerState finishState) { + if (spin.progress() < 1.) { + return; + } else if (current >= finishState) { + spin.startToTarget([=] { cover->update(); }); + } else { + spin.startWithin(list.size(), [=] { cover->update(); }); + } + }; + actualize( + state->backdropSpin, + state->spinnerBackdrops, + SpinnerState::FinishedBackdrop); + actualize( + state->patternSpin, + state->spinnerPatterns, + SpinnerState::FinishedPattern); + actualize( + state->modelSpin, + state->spinnerModels, + SpinnerState::Finished); + + auto &backdropNow = select( + state->spinnerBackdrops, + state->backdropSpin.nowIndex, + state->now.backdrop); + auto &backdropWill = select( + state->spinnerBackdrops, + state->backdropSpin.willIndex, + state->now.backdrop); + const auto backdropProgress = state->backdropSpin.progress(); + if (backdropProgress >= 1.) { + p.drawImage(0, 0, getBackdrop(backdropWill)); + } else { + p.drawImage(0, 0, getBackdrop(backdropNow)); + if (backdropProgress > 0.) { + const auto w = base::SafeRound(width * backdropProgress); + p.setClipRect(0, 0, int(w), cover->height()); + p.drawImage(0, 0, getBackdrop(backdropWill)); + p.setClipping(false); + } + } + + auto &patternNow = select( + state->spinnerPatterns, + state->patternSpin.nowIndex, + state->now.pattern); + auto &patternWill = select( + state->spinnerPatterns, + state->patternSpin.willIndex, + state->now.pattern); + const auto patternProgress = state->patternSpin.progress(); + if (patternProgress >= 1.) { + paintPattern(p, patternWill, backdropWill, 1.); + } else { + paintPattern(p, patternNow, backdropWill, 1. - patternProgress); + if (patternProgress > 0.) { + paintPattern(p, patternWill, backdropWill, patternProgress); + } + } + + auto &modelWas = select( + state->spinnerModels, + state->modelSpin.wasIndex, + state->now.model); + auto &modelNow = select( + state->spinnerModels, + state->modelSpin.nowIndex, + state->now.model); + auto &modelWill = select( + state->spinnerModels, + state->modelSpin.willIndex, + state->now.model); + const auto modelProgress = state->modelSpin.progress(); + const auto paintOne = [&](ModelView &view, float64 progress) { + auto scale = 1.; + if (progress != 0.) { + const auto shift = progress * width / 2.; + const auto shown = 1. - std::abs(progress); + scale = kModelScaleFrom + shown * (1. - kModelScaleFrom); + p.save(); + p.setOpacity(shown); + p.translate(int(base::SafeRound(shift)), 0); + } + paintModel(p, view, scale, true); + if (progress != 0.) { + p.restore(); + } + }; + const auto willProgress = -1. + modelProgress * 2 / 3.; + const auto nowProgress = willProgress + 2 / 3.; + const auto wasProgress = 1. + (modelProgress - 1.) * 2 / 3.; + paintOne(modelWas, wasProgress); + paintOne(modelNow, nowProgress); + paintOne(modelWill, willProgress); + } else { + if (progress < 1.) { + const auto finished = paint(state->now, 1. - progress) + || (state->next.forced + && (!state->crossfading + || !state->crossfade.animating())); + const auto next = finished + ? state->next.model.lottie.get() + : nullptr; + if (next && next->ready()) { + state->crossfading = true; + state->updateAttributesPending = true; + state->crossfade.start([=] { + cover->update(); + }, 0., 1., kCrossfadeDuration); + } + } + if (progress > 0.) { + p.setOpacity(progress); + paint(state->next, progress); } } - if (progress > 0.) { - p.setOpacity(progress); - paint(state->next, progress); - } - }, cover->lifetime()); + }); } void AddWearGiftCover( @@ -4096,6 +4465,7 @@ void AddUpgradeGiftCover( std::shared_ptr show, not_null container, const UpgradeArgs &args, + std::shared_ptr upgradeSpinner, rpl::producer> upgraded) { struct State { rpl::variable> upgraded; @@ -4123,7 +4493,7 @@ void AddUpgradeGiftCover( return upgraded ? rpl::single(Ui::UniqueGiftCover{ .values = *upgraded->info.unique, - .force = true, + .spinner = true, }) : MakeUpgradeGiftStream(args); }) | rpl::flatten_latest(); @@ -4144,12 +4514,14 @@ void AddUpgradeGiftCover( .subtitle = std::move(subtitle), .resalePrice = std::move(resalePrice), .resaleClick = std::move(resaleClick), + .upgradeSpinner = upgradeSpinner, }); } void SwitchToUpgradedAnimation( not_null window, std::shared_ptr gift, + std::shared_ptr spinner, not_null box, int contentStartIndex, std::optional upgradeNext) { @@ -4229,7 +4601,8 @@ void SwitchToUpgradedAnimation( box, show, entry, - {}); + {}, + spinner); content->resizeToWidth(st::boxWideWidth); @@ -4570,7 +4943,18 @@ void UpgradeBox( const auto show = controller->uiShow(); const auto container = box->verticalLayout(); - AddUpgradeGiftCover(show, container, args, state->upgraded.events()); + const auto spinner = std::make_shared(); + spinner->attributes = Data::UniqueGiftAttributes{ + .models = args.models, + .backdrops = args.backdrops, + .patterns = args.patterns, + }; + AddUpgradeGiftCover( + show, + container, + args, + spinner, + state->upgraded.events()); const auto contentIndex = container->count(); AddSkip(container, st::defaultVerticalListSkip * 2); @@ -4713,7 +5097,6 @@ void UpgradeBox( const auto done = [=]( bool success, std::shared_ptr gift) { - state->upgrading = false; const auto ownerId = (gift && gift->info.unique) ? gift->info.unique->ownerId : PeerId(); @@ -4721,27 +5104,42 @@ void UpgradeBox( ? session->data().peer(ownerId).get() : nullptr; if (!success) { - state->sent = false; + if (weakBox) { + state->upgrading = false; + state->sent = false; + } } else if (owner) { + using State = Data::GiftUpgradeSpinner::State; if (const auto onstack = args.upgraded) { onstack(); } - if (const auto window = weakWindow.get()) { + if (weakBox.get()) { + spinner->target = gift->info.unique; + spinner->state = State::Preparing; + } + if (weakWindow.get()) { session->data().nextForUpgradeGiftRequest( owner, [=](std::optional next) { if (const auto box = weakBox.get()) { - delete base::take(state->pricesLink); - SwitchToUpgradedAnimation( - window, - gift, - box, - contentIndex, - next); - state->upgraded.fire_copy(gift); - } else { + spinner->state.value() | rpl::filter( + rpl::mappers::_1 == State::Prepared + ) | rpl::take(1) | rpl::on_next([=] { + state->upgrading = false; + state->upgraded.fire_copy(gift); + + delete base::take(state->pricesLink); + SwitchToUpgradedAnimation( + weakWindow.get(), + gift, + spinner, + box, + contentIndex, + next); + }, box->lifetime()); + } else if (const auto w = weakWindow.get()) { ShowGiftUpgradedToast( - window, + w, session, gift->info.unique); } diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index 3623351f63..6db7ac1df8 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -77,9 +77,11 @@ struct UniqueGiftCoverArgs { std::optional now, std::optional next, float64 progress)> repaintedHook; + std::shared_ptr upgradeSpinner; }; struct UniqueGiftCover { Data::UniqueGift values; + bool spinner = false; bool force = false; }; diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index b749356a80..e164e1abbd 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -249,6 +249,24 @@ struct SavedStarGift { bool mine = false; }; +struct GiftUpgradeSpinner { + enum class State { + Initial, // Didn't start the upgrade. + Preparing, // Upgraded, requested Cover to prepare the spinners. + Loading, // Cover started preparing the spinners. + Prepared, // Cover prepared the spinners, waiting for nextToUpgrade. + Started, // Started spinning animations. + FinishedBackdrop, // Backdrop finished spinning. + FinishedPattern, // Pattern finished spinning. + Finished, // All spinners finished animating or user pressed Skip. + Timeout, // Waited for Prepared for too long, skipping animations. + }; + + Data::UniqueGiftAttributes attributes; + std::shared_ptr target; + rpl::variable state; +}; + struct GiftUpgradeResult { StarGift info; SavedStarGiftId manageId; diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 617b6352f6..8ea550c4c6 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -1460,7 +1460,7 @@ void GenericCreditsEntryBox( const Data::SubscriptionEntry &s, CreditsEntryBoxStyleOverrides st) { GenericCreditsEntryCover(box, show, e, s, st); - GenericCreditsEntryBody(box, show, e, s, st); + GenericCreditsEntryBody(box, show, e, s, nullptr, st); } void GenericCreditsEntryBody( @@ -1468,6 +1468,7 @@ void GenericCreditsEntryBody( std::shared_ptr show, const Data::CreditsHistoryEntry &e, const Data::SubscriptionEntry &s, + std::shared_ptr upgradeSpinner, CreditsEntryBoxStyleOverrides st) { const auto session = &show->session(); const auto selfPeerId = session->userPeerId().value; @@ -2075,6 +2076,7 @@ void GenericCreditsEntryBody( content, st, e, + upgradeSpinner, canConvert ? convert : Fn(), canUpgrade, canRemoveDetails ? removeDetails : Fn)>()); diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index 1075d44468..ff1e7845f5 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -27,6 +27,7 @@ struct SavedStarGift; class SavedStarGiftId; struct StarGift; struct UniqueGift; +struct GiftUpgradeSpinner; } // namespace Data namespace HistoryView { @@ -155,6 +156,7 @@ void GenericCreditsEntryBody( std::shared_ptr show, const Data::CreditsHistoryEntry &e, const Data::SubscriptionEntry &s, + std::shared_ptr upgradeSpinner, CreditsEntryBoxStyleOverrides st = {}); void UniqueGiftValueBox( not_null box, diff --git a/Telegram/SourceFiles/ui/top_background_gradient.cpp b/Telegram/SourceFiles/ui/top_background_gradient.cpp index af54e5a96b..ac39872dc7 100644 --- a/Telegram/SourceFiles/ui/top_background_gradient.cpp +++ b/Telegram/SourceFiles/ui/top_background_gradient.cpp @@ -52,6 +52,7 @@ void PrepareImage( emoji->paint(p, { .textColor = patternColor, .position = QPoint(shift, shift), + .paused = true, }); } @@ -92,10 +93,16 @@ QImage CreateTopBgGradient( } QImage CreateTopBgGradient(QSize size, const Data::UniqueGift &gift) { + return CreateTopBgGradient(size, gift.backdrop); +} + +QImage CreateTopBgGradient( + QSize size, + const Data::UniqueGiftBackdrop &backdrop) { return CreateTopBgGradient( size, - gift.backdrop.centerColor, - gift.backdrop.edgeColor); + backdrop.centerColor, + backdrop.edgeColor); } QImage CreateTopBgGradient( @@ -211,7 +218,25 @@ void PaintBgPoints( points, cache, emoji, - gift.backdrop.patternColor, + gift.backdrop, + rect, + shown); +} + +void PaintBgPoints( + QPainter &p, + const std::vector &points, + base::flat_map &cache, + not_null emoji, + const Data::UniqueGiftBackdrop &backdrop, + const QRect &rect, + float64 shown) { + PaintBgPoints( + p, + points, + cache, + emoji, + backdrop.patternColor, rect, shown); } diff --git a/Telegram/SourceFiles/ui/top_background_gradient.h b/Telegram/SourceFiles/ui/top_background_gradient.h index 1f89745a88..9320a9c12c 100644 --- a/Telegram/SourceFiles/ui/top_background_gradient.h +++ b/Telegram/SourceFiles/ui/top_background_gradient.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { struct UniqueGift; +struct UniqueGiftBackdrop; } // namespace Data namespace Ui::Text { @@ -29,6 +30,10 @@ struct PatternPoint { QSize size, const Data::UniqueGift &gift); +[[nodiscard]] QImage CreateTopBgGradient( + QSize size, + const Data::UniqueGiftBackdrop &backdrop); + [[nodiscard]] QImage CreateTopBgGradient( QSize size, QColor centerColor, @@ -53,6 +58,15 @@ void PaintBgPoints( const QRect &rect, float64 shown = 1.); +void PaintBgPoints( + QPainter &p, + const std::vector &points, + base::flat_map &cache, + not_null emoji, + const Data::UniqueGiftBackdrop &backdrop, + const QRect &rect, + float64 shown = 1.); + void PaintBgPoints( QPainter &p, const std::vector &points,