From 0b67fa65f2f436b7edbd13a75149595239a2c19a Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 2 Dec 2025 17:25:38 +0400 Subject: [PATCH] Full upgradable variants preview. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 3 + .../SourceFiles/api/api_toggling_media.cpp | 2 +- .../boxes/send_gif_with_caption_box.cpp | 2 +- Telegram/SourceFiles/boxes/share_box.cpp | 4 +- .../boxes/star_gift_auction_box.cpp | 29 +- Telegram/SourceFiles/boxes/star_gift_box.cpp | 194 ++- Telegram/SourceFiles/boxes/star_gift_box.h | 11 +- .../boxes/star_gift_preview_box.cpp | 1406 +++++++++++++++++ .../SourceFiles/boxes/star_gift_preview_box.h | 29 + .../SourceFiles/boxes/sticker_set_box.cpp | 4 +- .../chat_helpers/stickers_list_widget.cpp | 2 +- .../SourceFiles/data/data_document_media.cpp | 2 +- Telegram/SourceFiles/data/data_star_gift.h | 16 + Telegram/SourceFiles/data/data_story.cpp | 2 +- .../editor/scene/scene_item_sticker.cpp | 2 +- .../controls/history_view_compose_search.cpp | 2 +- .../peer_gifts/info_peer_gifts_common.cpp | 2 +- .../info/profile/info_profile_cover.cpp | 2 +- .../info/profile/info_profile_top_bar.cpp | 2 +- .../info_userpic_emoji_builder_preview.cpp | 4 +- .../media/stories/media_stories_reactions.cpp | 2 +- .../media/stories/media_stories_share.cpp | 2 +- .../menu_item_rate_transcribe_session.cpp | 6 +- .../settings/business/settings_chat_intro.cpp | 2 +- .../settings/settings_credits_graphics.cpp | 3 +- Telegram/SourceFiles/ui/effects/credits.style | 46 +- .../ui/effects/credits_graphics.cpp | 4 +- .../SourceFiles/ui/top_background_gradient.h | 2 +- Telegram/lib_ui | 2 +- 30 files changed, 1719 insertions(+), 72 deletions(-) create mode 100644 Telegram/SourceFiles/boxes/star_gift_preview_box.cpp create mode 100644 Telegram/SourceFiles/boxes/star_gift_preview_box.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index ff4ed33553..99f9c613dc 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -337,6 +337,8 @@ PRIVATE boxes/star_gift_auction_box.h boxes/star_gift_box.cpp boxes/star_gift_box.h + boxes/star_gift_preview_box.cpp + boxes/star_gift_preview_box.h boxes/star_gift_resale_box.cpp boxes/star_gift_resale_box.h boxes/sticker_set_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e14148e55a..f1b7abe128 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4180,12 +4180,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_auction_preview_model" = "model"; "lng_auction_preview_models#one" = "This collectible features **{count}** unique model."; "lng_auction_preview_models#other" = "This collectible features **{count}** unique models."; +"lng_auction_preview_models_button" = "Models"; "lng_auction_preview_backdrop" = "backdrop"; "lng_auction_preview_backdrops#one" = "This collectible features **{count}** unique backdrop."; "lng_auction_preview_backdrops#other" = "This collectible features **{count}** unique backdrops."; +"lng_auction_preview_backdrops_button" = "Backdrops"; "lng_auction_preview_symbol" = "symbol"; "lng_auction_preview_symbols#one" = "This collectible features **{count}** unique symbol."; "lng_auction_preview_symbols#other" = "This collectible features **{count}** unique symbols."; +"lng_auction_preview_symbols_button" = "Symbols"; "lng_auction_preview_wear" = "More about wearing gifts {arrow}"; "lng_auction_preview_free_upgrade" = "You can upgrade your gift for free, sell it on the market, or set it as your profile cover."; diff --git a/Telegram/SourceFiles/api/api_toggling_media.cpp b/Telegram/SourceFiles/api/api_toggling_media.cpp index 65d2ce317f..6c28df197d 100644 --- a/Telegram/SourceFiles/api/api_toggling_media.cpp +++ b/Telegram/SourceFiles/api/api_toggling_media.cpp @@ -24,7 +24,7 @@ void ToggleExistingMedia( Data::FileOrigin origin, ToggleRequestCallback toggleRequest, DoneCallback &&done) { - const auto api = &document->owner().session().api(); + const auto api = &document->session().api(); auto performRequest = [=](const auto &repeatRequest) -> void { const auto usedFileReference = document->fileReference(); diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp index ce08b9ff69..4e83fd7e72 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -121,7 +121,7 @@ namespace { return true; }; if (!updateThumbnail()) { - document->owner().session().downloaderTaskFinished( + document->session().downloaderTaskFinished( ) | rpl::start_with_next([=] { if (updateThumbnail()) { state->loadingLifetime.destroy(); diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index c9ee303c58..cd3274b6b3 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -1731,14 +1731,14 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( } return result; }; - auto &api = history->owner().session().api(); + auto &api = history->session().api(); auto &histories = history->owner().histories(); const auto donePhraseArgs = CreateForwardedMessagePhraseArgs( result, msgIds); const auto showRecentForwardsToSelf = result.size() == 1 && result.front()->peer()->isSelf() - && history->owner().session().premium(); + && history->session().premium(); const auto requestType = Data::Histories::RequestType::Send; for (const auto thread : result) { if (!comment.text.isEmpty()) { diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp index b272cddff8..ba8c8c5a2c 100644 --- a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/send_credits_box.h" // CreditsEmojiSmall #include "boxes/share_box.h" #include "boxes/star_gift_box.h" +#include "boxes/star_gift_preview_box.h" #include "boxes/star_gift_resale_box.h" #include "calls/group/calls_group_common.h" #include "core/application.h" @@ -1190,11 +1191,14 @@ void AuctionGotGiftsBox( } } -[[nodiscard]] rpl::producer MakePreviewAuctionStream( +[[nodiscard]] rpl::producer MakePreviewAuctionStream( const Data::StarGift &info, rpl::producer attributes) { Expects(attributes); + const auto cover = [](Data::UniqueGift gift) { + return UniqueGiftCover{ std::move(gift) }; + }; auto initial = Data::UniqueGift{ .title = info.resellTitle, .model = Data::UniqueGiftModel{ @@ -1207,14 +1211,14 @@ void AuctionGotGiftsBox( ? info.background->backdrop() : Data::UniqueGiftBackdrop()), }; - return rpl::single(initial) | rpl::then(std::move( + return rpl::single(cover(initial)) | rpl::then(std::move( attributes ) | rpl::map([=](const Data::UniqueGiftAttributes &values) - -> rpl::producer { + -> rpl::producer { if (values.backdrops.empty() || values.models.empty() || values.patterns.empty()) { - return rpl::never(); + return rpl::never(); } return [=](auto consumer) { auto lifetime = rpl::lifetime(); @@ -1256,12 +1260,12 @@ void AuctionGotGiftsBox( auto &models = state->data.models; auto &patterns = state->data.patterns; auto &backdrops = state->data.backdrops; - consumer.put_next(Data::UniqueGift{ + consumer.put_next(cover({ .title = info.resellTitle, .model = models[index(state->modelIndices, models)], .pattern = patterns[index(state->patternIndices, patterns)], .backdrop = backdrops[index(state->backdropIndices, backdrops)], - }); + })); }; put(); @@ -1520,18 +1524,7 @@ void AuctionInfoBox( st::boxRowPadding + st::uniqueGiftValueAvailableMargin, style::al_top )->setClickHandlerFilter([=](const auto &...) { - //if (state->previewRequested) { - // return false; - //} - //state->previewRequested = true; - //const auto &value = state->value.current(); - //const auto &gift = *value.gift; - //state->previewLifetime = ShowStarGiftResale( - // window, - // peer, - // gift.id, - // gift.resellTitle, - // [=] { state->previewRequested = false; }); + show->show(Box(StarGiftPreviewBox, window, *now.gift, list)); return false; }); }, box->lifetime()); diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 0b2f91125d..a888e621d0 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -2798,7 +2798,7 @@ void SetupResalePriceButton( void AddUniqueGiftCover( not_null container, - rpl::producer data, + rpl::producer data, UniqueGiftCoverArgs &&args) { const auto cover = container->add(object_ptr(container)); @@ -2812,16 +2812,18 @@ void AddUniqueGiftCover( style::FlatLabel st; PeerData *by = nullptr; QColor bg; + QColor fg; }; const auto released = cover->lifetime().make_state(); released->st = st::uniqueGiftReleasedBy; released->st.palette.linkFg = released->link.color(); + const auto repaintedHook = args.repaintedHook; if (args.resalePrice) { auto background = rpl::duplicate( data - ) | rpl::map([=](const Data::UniqueGift &unique) { - auto result = unique.backdrop.patternColor; + ) | rpl::map([=](const UniqueGiftCover &cover) { + auto result = cover.values.backdrop.patternColor; result.setAlphaF(kGradientButtonBgOpacity * result.alphaF()); return result; }); @@ -2870,12 +2872,15 @@ void AddUniqueGiftCover( cover, rpl::duplicate( data - ) | rpl::map([](const Data::UniqueGift &now) { return now.title; }), + ) | rpl::map([](const UniqueGiftCover &now) { + return now.values.title; + }), st::uniqueGiftTitle); title->setTextColorOverride(QColor(255, 255, 255)); released->subtitleText = args.subtitle ? std::move(args.subtitle) - : rpl::duplicate(data) | rpl::map([=](const Data::UniqueGift &gift) { + : rpl::duplicate(data) | rpl::map([=](const UniqueGiftCover &cover) { + const auto &gift = cover.values; released->by = gift.releasedBy; return gift.releasedBy ? tr::lng_gift_unique_number_by( @@ -2940,6 +2945,7 @@ void AddUniqueGiftCover( std::unique_ptr lottie; std::unique_ptr emoji; base::flat_map emojis; + bool forced = false; rpl::lifetime lifetime; }; struct State { @@ -2947,11 +2953,23 @@ void AddUniqueGiftCover( GiftView next; Animations::Simple crossfade; bool animating = false; + bool updateAttributesPending = false; }; const auto state = cover->lifetime().make_state(); const auto lottieSize = st::creditsHistoryEntryStarGiftSize; const auto updateLinkFg = args.subtitleLinkColored; const auto updateColors = [=](float64 progress) { + if (repaintedHook) { + repaintedHook(state->now.gift, state->next.gift, progress); + } + released->bg = (progress == 0.) + ? state->now.gift->backdrop.patternColor + : (progress == 1.) + ? state->next.gift->backdrop.patternColor + : anim::color( + state->now.gift->backdrop.patternColor, + state->next.gift->backdrop.patternColor, + progress); const auto color = (progress == 0.) ? state->now.gift->backdrop.textColor : (progress == 1.) @@ -2963,22 +2981,16 @@ void AddUniqueGiftCover( if (updateLinkFg) { released->link.update(color); } + released->fg = color; subtitle->setTextColorOverride(color); - released->bg = (progress == 0.) - ? state->now.gift->backdrop.patternColor - : (progress == 1.) - ? state->next.gift->backdrop.patternColor - : anim::color( - state->now.gift->backdrop.patternColor, - state->next.gift->backdrop.patternColor, - progress); }; - std::move( + rpl::duplicate( data - ) | rpl::start_with_next([=](const Data::UniqueGift &gift) { + ) | rpl::start_with_next([=](const UniqueGiftCover &now) { const auto setup = [&](GiftView &to) { - to.gift = gift; - const auto document = gift.model.document; + 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( @@ -3003,7 +3015,7 @@ void AddUniqueGiftCover( }, to.lifetime); }, to.lifetime); to.emoji = document->owner().customEmojiManager().create( - gift.pattern.document, + now.values.pattern.document, [=] { cover->update(); }, Data::CustomEmojiSizeTag::Large); [[maybe_unused]] const auto preload = to.emoji->ready(); @@ -3013,11 +3025,122 @@ void AddUniqueGiftCover( setup(state->now); cover->update(); updateColors(0.); - } else if (!state->next.gift) { + } else if (!state->next.gift || now.force) { setup(state->next); } }, cover->lifetime()); + const auto attrs = args.attributesInfo + ? CreateChild(cover) + : nullptr; + auto updateAttrs = Fn([](const auto &) { + }); + if (attrs) { + struct AttributeState { + Ui::Text::String name; + Ui::Text::String type; + Ui::Text::String percent; + }; + struct AttributesState { + AttributeState model; + AttributeState pattern; + AttributeState backdrop; + }; + const auto astate = cover->lifetime().make_state(); + const auto setType = [&](AttributeState &state, tr::phrase<> text) { + state.type = Ui::Text::String( + st::uniqueAttributeType, + text(tr::now)); + }; + setType(astate->model, tr::lng_auction_preview_model); + setType(astate->pattern, tr::lng_auction_preview_symbol); + setType(astate->backdrop, tr::lng_auction_preview_backdrop); + + updateAttrs = [=](const Data::UniqueGift &gift) { + const auto set = [&]( + AttributeState &state, + const Data::UniqueGiftAttribute &value) { + state.name = Ui::Text::String( + st::uniqueAttributeName, + value.name); + state.percent = Ui::Text::String( + st::uniqueAttributePercent, + QString::number(value.rarityPermille / 10.) + '%'); + }; + set(astate->model, gift.model); + set(astate->pattern, gift.pattern); + set(astate->backdrop, gift.backdrop); + attrs->update(); + }; + const auto height = st::uniqueAttributeTop + + st::uniqueAttributePadding.top() + + st::uniqueAttributeName.font->height + + st::uniqueAttributeType.font->height + + st::uniqueAttributePadding.bottom(); + attrs->resize(attrs->width(), height); + attrs->paintOn([=](QPainter &p) { + const auto skip = st::giftBoxGiftSkip.x(); + const auto available = attrs->width() - 2 * skip; + const auto single = available / 3; + if (single <= 0) { + return; + } + auto hq = PainterHighQualityEnabler(p); + auto bg = released->bg; + bg.setAlphaF(kGradientButtonBgOpacity * bg.alphaF()); + const auto innert = st::uniqueAttributeTop; + const auto innerh = height - innert; + const auto radius = innerh / 3.; + const auto paint = [&](int x, const AttributeState &state) { + p.setPen(Qt::NoPen); + p.setBrush(bg); + p.drawRoundedRect(x, innert, single, innerh, radius, radius); + p.setPen(QColor(255, 255, 255)); + const auto padding = st::uniqueAttributePadding; + const auto inner = single - padding.left() - padding.right(); + const auto namew = std::min(inner, state.name.maxWidth()); + state.name.draw(p, { + .position = QPoint( + x + (single - namew) / 2, + innert + padding.top()), + .availableWidth = namew, + .elisionLines = 1, + }); + p.setPen(released->fg); + const auto typew = std::min(inner, state.type.maxWidth()); + state.type.draw(p, { + .position = QPoint( + x + (single - typew) / 2, + innert + padding.top() + state.name.minHeight()), + .availableWidth = typew, + }); + p.setPen(Qt::NoPen); + p.setBrush(anim::color(released->bg, released->fg, 0.3)); + const auto r = st::uniqueAttributePercent.font->height / 2.; + const auto left = x + single - state.percent.maxWidth(); + const auto top = st::uniqueAttributePercentPadding.top(); + const auto percent = QRect( + left, + top, + state.percent.maxWidth(), + st::uniqueAttributeType.font->height); + p.drawRoundedRect( + percent.marginsAdded(st::uniqueAttributePercentPadding), + r, + r); + p.setPen(QColor(255, 255, 255)); + state.percent.draw(p, { + .position = percent.topLeft(), + }); + }; + auto left = 0; + paint(left, astate->model); + paint(left + single + skip, astate->backdrop); + paint(attrs->width() - single - left, astate->pattern); + }); + } + updateAttrs(*state->now.gift); + cover->widthValue() | rpl::start_with_next([=](int width) { const auto skip = st::uniqueGiftBottom; if (width <= 3 * skip) { @@ -3043,14 +3166,32 @@ void AddUniqueGiftCover( } subtitle->moveToLeft(skip, top); + top += subtitle->height() + skip; - cover->resize(width, subtitle->y() + subtitle->height() + skip); + if (attrs) { + attrs->resizeToWidth(width + - st::giftBoxPadding.left() + - st::giftBoxPadding.right()); + attrs->moveToLeft(st::giftBoxPadding.left(), top); + top += attrs->height() + (skip / 2); + } + + cover->resize(width, top); }, cover->lifetime()); cover->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(cover); auto progress = state->crossfade.value(state->animating ? 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) { updateColors(progress); } @@ -3105,10 +3246,13 @@ void AddUniqueGiftCover( }; if (progress < 1.) { - const auto finished = paint(state->now, 1. - progress); + 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); @@ -3803,12 +3947,12 @@ struct UpgradeArgs : StarGiftUpgradeArgs { std::vector nextPrices; }; -[[nodiscard]] rpl::producer MakeUpgradeGiftStream( +[[nodiscard]] rpl::producer MakeUpgradeGiftStream( const UpgradeArgs &args) { if (args.models.empty() || args.patterns.empty() || args.backdrops.empty()) { - return rpl::never(); + return rpl::never(); } return [=](auto consumer) { auto lifetime = rpl::lifetime(); @@ -3846,14 +3990,14 @@ struct UpgradeArgs : StarGiftUpgradeArgs { auto &models = state->data.models; auto &patterns = state->data.patterns; auto &backdrops = state->data.backdrops; - consumer.put_next(Data::UniqueGift{ + consumer.put_next(UniqueGiftCover{ Data::UniqueGift{ .title = (state->data.savedId ? tr::lng_gift_upgrade_title(tr::now) : tr::lng_gift_upgrade_preview_title(tr::now)), .model = models[index(state->modelIndices, models)], .pattern = patterns[index(state->patternIndices, patterns)], .backdrop = backdrops[index(state->backdropIndices, backdrops)], - }); + } }); }; put(); diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index 737ae4dde4..c108ce672e 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -72,11 +72,20 @@ struct UniqueGiftCoverArgs { bool subtitleLinkColored = false; rpl::producer resalePrice; Fn resaleClick; + bool attributesInfo = false; + Fn now, + std::optional next, + float64 progress)> repaintedHook; +}; +struct UniqueGiftCover { + Data::UniqueGift values; + bool force = false; }; void AddUniqueGiftCover( not_null container, - rpl::producer data, + rpl::producer data, UniqueGiftCoverArgs &&args); void AddWearGiftCover( not_null container, diff --git a/Telegram/SourceFiles/boxes/star_gift_preview_box.cpp b/Telegram/SourceFiles/boxes/star_gift_preview_box.cpp new file mode 100644 index 0000000000..9ba4b6140e --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_preview_box.cpp @@ -0,0 +1,1406 @@ +/* +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/star_gift_preview_box.h" + +#include "base/random.h" +#include "boxes/star_gift_box.h" +#include "chat_helpers/stickers_lottie.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_session.h" +#include "data/data_star_gift.h" +#include "history/view/media/history_view_sticker_player.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_custom_emoji.h" +#include "ui/widgets/buttons.h" +#include "ui/painter.h" +#include "ui/top_background_gradient.h" +#include "settings/settings_credits_graphics.h" +#include "window/window_session_controller.h" +#include "styles/style_credits.h" +#include "styles/style_layers.h" + +namespace Ui { +namespace { + +using Tab = Data::GiftAttributeIdType; +using GiftModel = Data::UniqueGiftModel; +using GiftPattern = Data::UniqueGiftPattern; +using GiftBackdrop = Data::UniqueGiftBackdrop; +constexpr auto kSwitchTimeout = 3 * crl::time(1000); +constexpr auto kGiftsPerRow = 3; + +struct AttributeDescriptor + : std::variant { + + friend inline bool operator==( + const AttributeDescriptor &, + const AttributeDescriptor &) = default; +}; + +[[nodiscard]] DocumentData *Sticker(const AttributeDescriptor &value) { + using namespace Data; + if (const auto pattern = std::get_if(&value)) { + return pattern->document; + } else if (const auto model = std::get_if(&value)) { + return model->document; + } + return nullptr; +} + +struct BackdropPlayers { + HistoryView::StickerPlayer *now = nullptr; + HistoryView::StickerPlayer *next = nullptr; + float64 progress = 0.; +}; + +struct PatternEmoji { + std::shared_ptr now; + std::shared_ptr next; + float64 progress = 0.; +}; + +class AttributeDelegate { +public: + [[nodiscard]] virtual QSize buttonSize() = 0; + [[nodiscard]] virtual QMargins buttonExtend() const = 0; + [[nodiscard]] virtual QImage background() = 0; + [[nodiscard]] virtual QImage cachePatternBackground( + int width, + int height, + QMargins extend) = 0; + [[nodiscard]] virtual QColor patternColor() = 0; + [[nodiscard]] virtual BackdropPlayers backdropPlayers() = 0; + [[nodiscard]] virtual PatternEmoji patternEmoji() = 0; +}; + +class AttributeButton final : public Ui::AbstractButton { +public: + AttributeButton( + QWidget *parent, + not_null delegate, + const AttributeDescriptor &descriptor); + + void toggleSelected(bool selected, anim::type animated); + + void setDescriptor(const AttributeDescriptor &descriptor); + void setGeometry(QRect inner, QMargins extend); + +private: + struct PatternCache { + base::flat_map map; + std::shared_ptr emoji; + }; + + void paintEvent(QPaintEvent *e) override; + void paintBackground(QPainter &p, const QImage &background); + + void setup(); + void validatePatternCache(); + void setDocument(not_null document); + + const not_null _delegate; + QImage _hiddenBgCache; + AttributeDescriptor _descriptor; + Ui::Text::String _name; + Ui::Text::String _percent; + QImage _backgroundCache; + PatternCache _patternNow; + PatternCache _patternNext; + QImage _patternFrame; + QColor _patternColor; + Ui::Animations::Simple _selectedAnimation; + bool _selected : 1 = false; + bool _patterned : 1 = false; + + QMargins _extend; + + DocumentData *_document = nullptr; + DocumentData *_playerDocument = nullptr; + std::unique_ptr _player; + rpl::lifetime _mediaLifetime; + +}; + +class Delegate final : public AttributeDelegate { +public: + explicit Delegate(Fn fullUpdate); + + Delegate(Delegate &&other) = default; + ~Delegate() = default; + + void update( + const std::optional &now, + const std::optional &next, + float64 progress); + + [[nodiscard]] QSize buttonSize() override; + [[nodiscard]] QMargins buttonExtend() const override; + [[nodiscard]] QImage background() override; + [[nodiscard]] QImage cachePatternBackground( + int width, + int height, + QMargins extend) override; + [[nodiscard]] QColor patternColor() override; + [[nodiscard]] BackdropPlayers backdropPlayers() override; + [[nodiscard]] PatternEmoji patternEmoji() override; + +private: + struct Sticker { + DocumentData *document = nullptr; + DocumentData *playerDocument = nullptr; + std::unique_ptr player; + rpl::lifetime mediaLifetime; + }; + struct Emoji { + DocumentData *document; + std::shared_ptr custom; + }; + + Fn _fullUpdate; + QSize _single; + QImage _bg; + + GiftBackdrop _nowBackdrop; + GiftBackdrop _nextBackdrop; + + Sticker _nowModel; + Sticker _nextModel; + + Emoji _nowPatternEmoji; + Emoji _nextPatternEmoji; + + float64 _progress = 0.; + + GiftBackdrop _backdrop; + QImage _backdropCache; + bool _backdropDirty = false; + +}; + +struct Selection { + int model = -1; + int pattern = -1; + int backdrop = -1; + + friend inline bool operator==(Selection, Selection) = default; +}; + +class AttributesList final : public Ui::BoxContentDivider { +public: + AttributesList( + QWidget *parent, + not_null delegate, + not_null window, + not_null attributes, + rpl::producer tab); + + [[nodiscard]] rpl::producer selected() const; + +private: + struct Entry { + AttributeDescriptor descriptor; + Selection value; + }; + struct Entries { + std::vector list; + }; + struct View { + std::unique_ptr button; + DocumentData *document = nullptr; + int index = 0; + }; + + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + void paintEvent(QPaintEvent *e) override; + + void fill(); + void refreshAbout(); + void refreshButtons(); + void validateButtons(); + + int resizeGetHeight(int width) override; + void clicked(int index); + + [[nodiscard]] View *find(const AttributeDescriptor &descriptor); + + const not_null _delegate; + const not_null _window; + const not_null _attributes; + rpl::variable _tab; + rpl::variable _selected; + + std::unique_ptr _about; + + Entries _models; + Entries _patterns; + Entries _backdrops; + not_null _entries; + not_null*> _list; + + std::vector _views; + int _viewsForWidth = 0; + int _viewsFromRow = 0; + int _viewsTillRow = 0; + + QSize _singleMin; + QSize _single; + int _perRow = 0; + int _visibleFrom = 0; + int _visibleTill = 0; + +}; + +void CacheBackdropBackground( + not_null backdrop, + int width, + int height, + QMargins extend, + QImage &cache, + bool force = false) { + const auto outer = QRect(0, 0, width, height); + const auto inner = outer.marginsRemoved( + extend + ).translated(-extend.left(), -extend.top()); + const auto ratio = style::DevicePixelRatio(); + if (cache.size() != inner.size() * ratio) { + cache = QImage( + inner.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + cache.setDevicePixelRatio(ratio); + force = true; + } + if (force) { + cache.fill(Qt::transparent); + + const auto radius = st::giftBoxGiftRadius; + auto p = QPainter(&cache); + auto hq = PainterHighQualityEnabler(p); + auto gradient = QRadialGradient(inner.center(), inner.width() / 2); + gradient.setStops({ + { 0., backdrop->centerColor }, + { 1., backdrop->edgeColor }, + }); + p.setBrush(gradient); + p.setPen(Qt::NoPen); + p.drawRoundedRect(inner, radius, radius); + } +} + +AttributeButton::AttributeButton( + QWidget *parent, + not_null delegate, + const AttributeDescriptor &descriptor) +: AbstractButton(parent) +, _delegate(delegate) +, _descriptor(descriptor) { + setup(); +} + +void AttributeButton::setup() { + v::match(_descriptor, [&](const auto &data) { + _name.setText(st::uniqueAttributeName, data.name); + }); + + v::match(_descriptor, [&](const GiftBackdrop &data) { + const auto destroyed = base::take(_player); + _document = nullptr; + _playerDocument = nullptr; + _mediaLifetime.destroy(); + _patternNow = {}; + _patternNext = {}; + _patternFrame = QImage(); + void validatePatternFrame(); + }, [&](const GiftModel &data) { + setDocument(data.document); + _patternNow = {}; + _patternNext = {}; + _patternFrame = QImage(); + }, [&](const GiftPattern &data) { + const auto document = data.document; + if (_document != document) { + setDocument(document); + const auto owner = &document->owner(); + _patternNow.emoji = owner->customEmojiManager().create( + document, + [=] { update(); }, + Data::CustomEmojiSizeTag::Large); + _patternNow.map.clear(); + _patternColor = QColor(); + } + }); +} + +void AttributeButton::setDescriptor(const AttributeDescriptor &descriptor) { + if (_descriptor == descriptor) { + return; + } + _descriptor = descriptor; + _backgroundCache = QImage(); + setup(); + update(); +} + +void AttributeButton::setDocument(not_null document) { + if (_document == document) { + return; + } + _document = document; + + const auto media = document->createMediaView(); + media->checkStickerLarge(); + media->goodThumbnailWanted(); + + const auto destroyed = base::take(_player); + _playerDocument = nullptr; + _mediaLifetime = rpl::single() | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::filter([=] { + return media->loaded(); + }) | rpl::start_with_next([=] { + _mediaLifetime.destroy(); + + auto result = std::unique_ptr(); + const auto sticker = document->sticker(); + if (sticker->isLottie()) { + result = std::make_unique( + ChatHelpers::LottiePlayerFromDocument( + media.get(), + ChatHelpers::StickerLottieSize::InlineResults, + st::uniqueAttributeStickerSize, + Lottie::Quality::High)); + } else if (sticker->isWebm()) { + result = std::make_unique( + media->owner()->location(), + media->bytes(), + st::uniqueAttributeStickerSize); + } else { + result = std::make_unique( + media->owner()->location(), + media->bytes(), + st::uniqueAttributeStickerSize); + } + result->setRepaintCallback([=] { update(); }); + _playerDocument = media->owner(); + _player = std::move(result); + update(); + }); + if (_playerDocument) { + _mediaLifetime.destroy(); + } +} + +void AttributeButton::setGeometry(QRect inner, QMargins extend) { + _extend = extend; + AbstractButton::setGeometry(inner.marginsAdded(extend)); +} + +void AttributeButton::toggleSelected(bool selected, anim::type animated) { + if (_selected == selected) { + if (animated == anim::type::instant) { + _selectedAnimation.stop(); + } + return; + } + + const auto duration = st::defaultRoundCheckbox.duration; + _selected = selected; + if (animated == anim::type::instant) { + _selectedAnimation.stop(); + return; + } + _selectedAnimation.start([=] { + update(); + }, selected ? 0. : 1., selected ? 1. : 0., duration, anim::easeOutCirc); +} + +void AttributeButton::paintBackground( + QPainter &p, + const QImage &background) { + const auto removed = QMargins(); + const auto x = removed.left(); + const auto y = removed.top(); + const auto width = this->width() - x - removed.right(); + const auto height = this->height() - y - removed.bottom(); + const auto dpr = int(background.devicePixelRatio()); + const auto bwidth = background.width() / dpr; + const auto bheight = background.height() / dpr; + const auto fillRow = [&](int yfrom, int ytill, int bfrom) { + const auto fill = [&](int xto, int wto, int xfrom, int wfrom = 0) { + const auto fheight = ytill - yfrom; + p.drawImage( + QRect(x + xto, y + yfrom, wto, fheight), + background, + QRect( + QPoint(xfrom, bfrom) * dpr, + QSize((wfrom ? wfrom : wto), fheight) * dpr)); + }; + if (width < bwidth) { + const auto xhalf = width / 2; + fill(0, xhalf, 0); + fill(xhalf, width - xhalf, bwidth - (width - xhalf)); + } else if (width == bwidth) { + fill(0, width, 0); + } else { + const auto half = bwidth / (2 * dpr); + fill(0, half, 0); + fill(width - half, half, bwidth - half); + fill(half, width - 2 * half, half, 1); + } + }; + if (height < bheight) { + fillRow(0, height / 2, 0); + fillRow(height / 2, height, bheight - (height - (height / 2))); + } else { + fillRow(0, height, 0); + } + + auto hq = PainterHighQualityEnabler(p); + const auto progress = _selectedAnimation.value(_selected ? 1. : 0.); + if (progress < 0.01) { + return; + } + const auto pwidth = progress * st::defaultRoundCheckbox.width; + p.setPen(QPen(st::defaultRoundCheckbox.bgActive->c, pwidth)); + p.setBrush(Qt::NoBrush); + const auto rounded = rect().marginsRemoved(_extend); + const auto phalf = pwidth / 2.; + const auto extended = QRectF(rounded).marginsRemoved( + { phalf, phalf, phalf, phalf }); + const auto xradius = removed.left() + st::giftBoxGiftRadius - phalf; + const auto yradius = removed.top() + st::giftBoxGiftRadius - phalf; + p.drawRoundedRect(extended, xradius, yradius); +} + +void AttributeButton::validatePatternCache() { + if (_patternNow.emoji && _patternNow.emoji->ready()) { + const auto inner = QRect(0, 0, + this->width() - _extend.left() - _extend.right(), + this->height() - _extend.top() - _extend.bottom()); + const auto size = inner.size(); + const auto ratio = style::DevicePixelRatio(); + if (_patternFrame.size() != size * ratio) { + _patternFrame = QImage( + size * ratio, + QImage::Format_ARGB32_Premultiplied); + _patternFrame.setDevicePixelRatio(ratio); + _patternColor = QColor(); + } + const auto color = _delegate->patternColor(); + if (_patternColor != color) { + _patternColor = color; + _patternNow.map.clear(); + _patternFrame.fill(Qt::transparent); + + auto p = QPainter(&_patternFrame); + const auto skip = inner.width() / 3; + Ui::PaintBgPoints( + p, + Ui::PatternBgPointsSmall(), + _patternNow.map, + _patternNow.emoji.get(), + color, + QRect(-skip, 0, inner.width() + 2 * skip, inner.height())); + } + } else if (!_patternFrame.isNull()) { + _patternFrame = QImage(); + } +} + +void AttributeButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + const auto model = std::get_if(&_descriptor); + const auto pattern = std::get_if(&_descriptor); + const auto backdrop = std::get_if(&_descriptor); + const auto position = QPoint(_extend.left(), _extend.top()); + const auto background = _delegate->background(); + const auto width = this->width(); + const auto dpr = int(background.devicePixelRatio()); + paintBackground(p, background); + if (backdrop) { + CacheBackdropBackground( + backdrop, + width, + background.height() / dpr, + _extend, + _backgroundCache); + p.drawImage(_extend.left(), _extend.top(), _backgroundCache); + + const auto inner = QRect( + _extend.left(), + _extend.top(), + this->width() - _extend.left() - _extend.right(), + this->height() - _extend.top() - _extend.bottom()); + const auto skip = inner.width() / 3; + const auto emoji = _delegate->patternEmoji(); + const auto paintSymbols = [&]( + PatternCache &cache, + std::shared_ptr emoji, + float64 shown) { + if (shown == 0. || !emoji) { + return; + } else if (cache.emoji != emoji) { + cache.map.clear(); + cache.emoji = emoji; + } + Ui::PaintBgPoints( + p, + Ui::PatternBgPointsSmall(), + cache.map, + emoji.get(), + backdrop->patternColor, + QRect(-skip, 0, inner.width() + 2 * skip, inner.height()), + shown); + }; + p.setClipRect(inner); + paintSymbols(_patternNow, emoji.now, 1. - emoji.progress); + paintSymbols(_patternNext, emoji.next, emoji.progress); + p.setClipping(false); + } else if (pattern) { + p.drawImage( + _extend.left(), + _extend.top(), + _delegate->cachePatternBackground( + width, + background.height() / dpr, + _extend)); + validatePatternCache(); + if (!_patternFrame.isNull()) { + p.drawImage(_extend.left(), _extend.top(), _patternFrame); + } + } + + auto inset = 0; + const auto progress = _selectedAnimation.value(_selected ? 1. : 0.); + if (progress > 0) { + auto hq = PainterHighQualityEnabler(p); + auto pen = st::boxBg->p; + const auto thickness = style::ConvertScaleExact(2.); + pen.setWidthF(progress * thickness); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + const auto height = background.height() / dpr; + const auto outer = QRectF(0, 0, width, height); + const auto shift = progress * thickness * 2; + const auto extend = QMarginsF(_extend) + + QMarginsF(shift, shift, shift, shift); + const auto radius = st::giftBoxGiftRadius - shift; + p.drawRoundedRect(outer.marginsRemoved(extend), radius, radius); + inset = int(std::ceil( + progress * (thickness * 2 + st::giftBoxUserpicSkip))); + } + + const auto paused = !isOver(); + const auto now = crl::now(); + const auto colored = v::is(_descriptor) + ? QColor(255, 255, 255) + : QColor(0, 0, 0, 0); + const auto frameSize = st::uniqueAttributeStickerSize; + const auto paintFrame = [&](HistoryView::StickerPlayer *player) { + if (!player || !player->ready()) { + return; + } + auto info = player->frame(frameSize, colored, false, now, paused); + const auto finished = (info.index + 1 == player->framesCount()); + if (!finished || !paused) { + player->markFrameShown(); + } + const auto size = info.image.size() / style::DevicePixelRatio(); + p.drawImage( + QRect( + (width - size.width()) / 2, + st::giftBoxSmallStickerTop, + size.width(), + size.height()), + info.image); + }; + if (backdrop) { + const auto backdropPlayers = _delegate->backdropPlayers(); + if (backdropPlayers.progress == 0.) { + paintFrame(backdropPlayers.now); + } else if (backdropPlayers.progress == 1.) { + paintFrame(backdropPlayers.next); + } else { + p.setOpacity(1. - backdropPlayers.progress); + paintFrame(backdropPlayers.now); + p.setOpacity(backdropPlayers.progress); + paintFrame(backdropPlayers.next); + p.setOpacity(1.); + } + } else { + paintFrame(_player.get()); + } + + const auto singlew = width - _extend.left() - _extend.right(); + if (model) { + p.setPen(st::windowBoldFg); + } else { + p.setPen(QColor(255, 255, 255)); + } + _name.draw(p, { + .position = (position + QPoint(0, st::giftBoxPremiumTextTop)), + .availableWidth = singlew, + .align = style::al_top, + }); +} + +Delegate::Delegate(Fn fullUpdate) +: _fullUpdate(std::move(fullUpdate)) { +} + +void Delegate::update( + const std::optional &now, + const std::optional &next, + float64 progress) { + const auto wasDirty = _backdropDirty; + const auto badSticker = [&]( + Sticker &model, + const std::optional &gift) { + return gift && (model.document != gift->model.document); + }; + const auto enforceSticker = [&]( + Sticker &model, + const std::optional &gift) { + Expects(gift.has_value()); + + const auto document = model.document = gift->model.document; + const auto media = document->createMediaView(); + media->checkStickerLarge(); + media->goodThumbnailWanted(); + + const auto destroyed = base::take(model.player); + model.playerDocument = nullptr; + model.mediaLifetime = rpl::single() | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::filter([=] { + return media->loaded(); + }) | rpl::start_with_next([=, &model] { + model.mediaLifetime.destroy(); + + auto result = std::unique_ptr(); + const auto sticker = document->sticker(); + if (sticker->isLottie()) { + result = std::make_unique( + ChatHelpers::LottiePlayerFromDocument( + media.get(), + ChatHelpers::StickerLottieSize::InlineResults, + st::uniqueAttributeStickerSize, + Lottie::Quality::High)); + } else if (sticker->isWebm()) { + result = std::make_unique( + media->owner()->location(), + media->bytes(), + st::uniqueAttributeStickerSize); + } else { + result = std::make_unique( + media->owner()->location(), + media->bytes(), + st::uniqueAttributeStickerSize); + } + result->setRepaintCallback(_fullUpdate); + model.playerDocument = media->owner(); + model.player = std::move(result); + _fullUpdate(); + }); + if (model.playerDocument) { + model.mediaLifetime.destroy(); + } + }; + const auto validateSticker = [&]( + Sticker &model, + const std::optional &gift) { + if (badSticker(model, gift)) { + enforceSticker(model, gift); + } + }; + const auto validatePatternEmoji = [&]( + Emoji &emoji, + const std::optional &gift) { + if (!gift) { + emoji = {}; + return; + } + const auto document = gift->pattern.document; + if (emoji.document != document) { + emoji.document = document; + emoji.custom = document->owner().customEmojiManager().create( + document, + _fullUpdate, + Data::CustomEmojiSizeTag::Large); + } + }; + + if (progress == 1.) { + Assert(next.has_value()); + if (_nextBackdrop != next->backdrop) { + _nextBackdrop = next->backdrop; + _backdropDirty = true; + } + validateSticker(_nextModel, next); + validatePatternEmoji(_nextPatternEmoji, next); + _nowModel = {}; + _nowPatternEmoji = {}; + } else { + Assert(now.has_value()); + if (_nowBackdrop != now->backdrop) { + if (_nextBackdrop == now->backdrop) { + _nowBackdrop = base::take(_nextBackdrop); + } else { + _nowBackdrop = now->backdrop; + _backdropDirty = true; + } + } + if (badSticker(_nowModel, now)) { + if (_nextModel.document == now->model.document + && !_nextModel.mediaLifetime) { + _nowModel = base::take(_nextModel); + } else { + enforceSticker(_nowModel, now); + } + } + validatePatternEmoji(_nowPatternEmoji, now); + if (progress > 0.) { + Assert(next.has_value()); + if (_nextBackdrop != next->backdrop) { + _nextBackdrop = next->backdrop; + _backdropDirty = true; + } + validateSticker(_nextModel, next); + validatePatternEmoji(_nextPatternEmoji, next); + } else { + if (_nextModel.document) { + _nextModel = Sticker(); + } + if (_nextPatternEmoji.document) { + _nextPatternEmoji = Emoji(); + } + } + } + if (_progress != progress) { + _progress = progress; + _backdropDirty = true; + } + if (!wasDirty && _backdropDirty) { + _fullUpdate(); + } +} + +QSize Delegate::buttonSize() { + if (!_single.isEmpty()) { + return _single; + } + const auto width = st::boxWideWidth; + const auto padding = st::giftBoxPadding; + const auto available = width - padding.left() - padding.right(); + const auto singlew = (available - 2 * st::giftBoxGiftSkip.x()) + / kGiftsPerRow; + _single = QSize(singlew, st::giftBoxGiftSmall); + return _single; +} + +QMargins Delegate::buttonExtend() const { + return st::defaultDropdownMenu.wrap.shadow.extend; +} + +QImage Delegate::background() { + if (!_bg.isNull()) { + return _bg; + } + const auto single = buttonSize(); + const auto extend = buttonExtend(); + const auto bgSize = single.grownBy(extend); + const auto ratio = style::DevicePixelRatio(); + auto bg = QImage( + bgSize * ratio, + QImage::Format_ARGB32_Premultiplied); + bg.setDevicePixelRatio(ratio); + bg.fill(Qt::transparent); + + const auto radius = st::giftBoxGiftRadius; + const auto rect = QRect(QPoint(), bgSize).marginsRemoved(extend); + + { + auto p = QPainter(&bg); + auto hq = PainterHighQualityEnabler(p); + p.setOpacity(0.3); + p.setPen(Qt::NoPen); + p.setBrush(st::windowShadowFg); + p.drawRoundedRect( + QRectF(rect).translated(0, radius / 12.), + radius, + radius); + } + bg = bg.scaled( + (bgSize * ratio) / 2, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + bg = Images::Blur(std::move(bg), true); + bg = bg.scaled( + bgSize * ratio, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + { + auto p = QPainter(&bg); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::windowBg); + p.drawRoundedRect(rect, radius, radius); + } + + _bg = std::move(bg); + return _bg; +} + +QImage Delegate::cachePatternBackground( + int width, + int height, + QMargins extend) { + if (_backdropDirty) { + _backdrop = GiftBackdrop(); + if (_progress == 0.) { + _backdrop = _nowBackdrop; + } else if (_progress == 1.) { + _backdrop = _nextBackdrop; + } else { + _backdrop.centerColor = anim::color( + _nowBackdrop.centerColor, + _nextBackdrop.centerColor, + _progress); + _backdrop.edgeColor = anim::color( + _nowBackdrop.edgeColor, + _nextBackdrop.edgeColor, + _progress); + _backdrop.patternColor = anim::color( + _nowBackdrop.patternColor, + _nextBackdrop.patternColor, + _progress); + } + } + CacheBackdropBackground( + &_backdrop, + width, + height, + extend, + _backdropCache, + _backdropDirty); + + _backdropDirty = false; + return _backdropCache; +} + +QColor Delegate::patternColor() { + Expects(!_backdropDirty); + + return _backdrop.patternColor; +} + +BackdropPlayers Delegate::backdropPlayers() { + return { + .now = _nowModel.player.get(), + .next = _nextModel.player.get(), + .progress = _progress, + }; +} + +PatternEmoji Delegate::patternEmoji() { + return { + .now = _nowPatternEmoji.custom, + .next = _nextPatternEmoji.custom, + .progress = _progress, + }; +} + +AttributesList::AttributesList( + QWidget *parent, + not_null delegate, + not_null window, + not_null attributes, + rpl::producer tab) +: BoxContentDivider(parent) +, _delegate(delegate) +, _window(window) +, _attributes(attributes) +, _tab(std::move(tab)) +, _entries(&_models) +, _list(&_entries->list) { + _singleMin = _delegate->buttonSize(); + + fill(); + + _tab.value( + ) | rpl::start_with_next([=](Tab tab) { + _entries = [&] { + switch (tab) { + case Tab::Model: return &_models; + case Tab::Pattern: return &_patterns; + case Tab::Backdrop: return &_backdrops; + } + Unexpected("Tab in AttributesList."); + }(); + _list = &_entries->list; + refreshButtons(); + refreshAbout(); + }, lifetime()); + + _selected.value() | rpl::combine_previous() | rpl::start_with_next([=]( + Selection was, + Selection now) { + const auto tab = _tab.current(); + const auto button = [&](int index) { + const auto i = ranges::find(_views, index, &View::index); + return (i != end(_views)) ? i->button.get() : nullptr; + }; + const auto update = [&](int was, int now) { + if (was != now) { + if (const auto deselect = button(was)) { + deselect->toggleSelected(false, anim::type::normal); + } + if (const auto select = button(now)) { + select->toggleSelected(true, anim::type::normal); + } + } + }; + if (tab == Tab::Model) { + update(was.model, now.model); + } else if (tab == Tab::Pattern) { + update(was.pattern, now.pattern); + } else { + update(was.backdrop, now.backdrop); + } + }, lifetime()); +} + +auto AttributesList::selected() const ->rpl::producer { + return _selected.value(); +} + +void AttributesList::fill() { + const auto addEntries = [&]( + const auto &source, + Entries &target, + const auto field) { + auto value = Selection(); + target.list.clear(); + target.list.reserve(source.size()); + for (const auto &item : source) { + ++(value.*field); + target.list.push_back({ .descriptor = item, .value = value }); + } + }; + addEntries(_attributes->models, _models, &Selection::model); + addEntries(_attributes->patterns, _patterns, &Selection::pattern); + addEntries(_attributes->backdrops, _backdrops, &Selection::backdrop); +} + +void AttributesList::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + _visibleFrom = visibleTop; + _visibleTill = visibleBottom; + validateButtons(); +} + +void AttributesList::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + p.fillRect(rect(), st::boxDividerBg->c); + paintTop(p); + paintBottom(p); +} + +void AttributesList::refreshButtons() { + _viewsForWidth = 0; + _viewsFromRow = 0; + _viewsTillRow = 0; + resizeToWidth(width()); + validateButtons(); +} + +void AttributesList::validateButtons() { + if (!_perRow || !_about) { + return; + } + const auto padding = st::giftBoxPadding; + const auto vskip = st::giftListAboutMargin.top() + + _about->height() + + st::giftListAboutMargin.bottom(); + const auto row = _single.height() + st::giftBoxGiftSkip.y(); + const auto fromRow = std::max(_visibleFrom - vskip, 0) / row; + const auto tillRow = (_visibleTill - vskip + row - 1) / row; + Assert(tillRow >= fromRow); + if (_viewsFromRow == fromRow + && _viewsTillRow == tillRow + && _viewsForWidth == width()) { + return; + } + _viewsFromRow = fromRow; + _viewsTillRow = tillRow; + _viewsForWidth = width(); + + const auto available = _viewsForWidth - padding.left() - padding.right(); + const auto skipw = st::giftBoxGiftSkip.x(); + const auto fullw = _perRow * (_single.width() + skipw) - skipw; + const auto left = padding.left() + (available - fullw) / 2; + const auto oneh = _single.height() + st::giftBoxGiftSkip.y(); + auto x = left; + auto y = vskip + fromRow * oneh; + auto views = std::vector(); + views.reserve((tillRow - fromRow) * _perRow); + const auto idUsed = [&](DocumentData *document, int column, int row) { + if (!document) { + return false; + } + for (auto j = row; j != tillRow; ++j) { + for (auto i = column; i != _perRow; ++i) { + const auto index = j * _perRow + i; + if (index >= _list->size()) { + return false; + } else if (Sticker((*_list)[index].descriptor) == document) { + return true; + } + } + column = 0; + } + return false; + }; + const auto selected = [&] { + const auto values = _selected.current(); + switch (_tab.current()) { + case Tab::Model: return values.model; + case Tab::Pattern: return values.pattern; + case Tab::Backdrop: return values.backdrop; + } + Unexpected("Tab in AttributesList::validateButtons."); + }(); + const auto add = [&](int column, int row) { + const auto index = row * _perRow + column; + if (index >= _list->size()) { + return false; + } + const auto &entry = (*_list)[index]; + const auto &descriptor = entry.descriptor; + const auto document = Sticker(descriptor); + const auto already = ranges::find_if(_views, [&](const View &view) { + return view.button && view.document == document; + }); + if (already != end(_views)) { + views.push_back(base::take(*already)); + views.back().button->setDescriptor(descriptor); + } else { + const auto unused = ranges::find_if(_views, [&](const View &v) { + return v.button && !idUsed(v.document, column, row); + }); + if (unused != end(_views)) { + views.push_back(base::take(*unused)); + views.back().button->setDescriptor(descriptor); + } else { + views.push_back({ + .button = std::make_unique( + this, + _delegate, + descriptor) + }); + views.back().button->show(); + } + } + auto &view = views.back(); + view.index = index; + view.button->toggleSelected(index == selected, anim::type::instant); + view.button->setClickedCallback([=] { + clicked(index); + }); + return true; + }; + for (auto j = fromRow; j != tillRow; ++j) { + for (auto i = 0; i != _perRow; ++i) { + if (!add(i, j)) { + break; + } + const auto &view = views.back(); + auto pos = QPoint(x, y); + + view.button->setGeometry( + QRect(pos, _single), + _delegate->buttonExtend()); + x += _single.width() + skipw; + } + x = left; + y += oneh; + } + + std::swap(_views, views); +} + +void AttributesList::clicked(int index) { + Expects(index >= 0 && index < _list->size()); + + auto now = _selected.current(); + const auto value = (*_list)[index].value; + const auto check = [&](const auto field) { + if (value.*field >= 0) { + now.*field = (now.*field == value.*field) ? -1 : value.*field; + } + }; + check(&Selection::model); + check(&Selection::pattern); + check(&Selection::backdrop); + _selected = now; +} + +AttributesList::View *AttributesList::find( + const AttributeDescriptor &descriptor) { + const auto i = ranges::find(*_list, descriptor, &Entry::descriptor); + if (i == end(*_list)) { + return nullptr; + } + const auto index = int(i - begin(*_list)); + const auto j = ranges::find(_views, index, &View::index); + return (j != end(_views)) ? &*j : nullptr; +} + +void AttributesList::refreshAbout() { + auto text = [&] { + switch (_tab.current()) { + case Tab::Model: return tr::lng_auction_preview_models; + case Tab::Pattern: return tr::lng_auction_preview_symbols; + case Tab::Backdrop: return tr::lng_auction_preview_backdrops; + } + Unexpected("Tab in AttributesList::refreshAbout."); + }()(lt_count_decimal, rpl::single(_list->size() * 1.), tr::rich); + auto about = std::make_unique( + this, + std::move(text), + st::giftListAbout); + about->show(); + _about = std::move(about); + resizeToWidth(width()); +} + +int AttributesList::resizeGetHeight(int width) { + const auto padding = st::giftBoxPadding; + const auto count = int(_list->size()); + const auto available = width - padding.left() - padding.right(); + const auto skipw = st::giftBoxGiftSkip.x(); + _perRow = std::min( + (available + skipw) / (_singleMin.width() + skipw), + std::max(count, 1)); + if (!_perRow || !_about) { + return 0; + } + auto result = 0; + + const auto margin = st::giftListAboutMargin; + _about->resizeToWidth(width - margin.left() - margin.right()); + _about->moveToLeft(margin.left(), result + margin.top()); + result += margin.top() + _about->height() + margin.bottom(); + + const auto singlew = std::min( + ((available + skipw) / _perRow) - skipw, + 2 * _singleMin.width()); + Assert(singlew >= _singleMin.width()); + const auto singleh = _singleMin.height(); + + _single = QSize(singlew, singleh); + const auto rows = (count + _perRow - 1) / _perRow; + const auto skiph = st::giftBoxGiftSkip.y(); + + result += rows + ? (padding.bottom() + rows * (singleh + skiph) - skiph) + : 0; + + return result; +} + +} // namespace + +void StarGiftPreviewBox( + not_null box, + not_null controller, + const Data::StarGift &gift, + const Data::UniqueGiftAttributes &attributes) { + Expects(!attributes.models.empty()); + Expects(!attributes.patterns.empty()); + Expects(!attributes.backdrops.empty()); + + box->setStyle(st::uniqueAttributesBox); + box->setWidth(st::boxWideWidth); + box->setNoContentMargin(true); + + struct State { + State(QString title, const Data::UniqueGiftAttributes &attributes) + : title(title) + , delegate([=] { + if (tab.current() != Tab::Model && list) { + list->update(); + } + }) + , attributes(attributes) + , gift(make()) + , pushNextTimer([=] { push(); }) { + } + + void randomize() { + const auto choose = [](const auto &list, auto &indices) { + if (indices.empty()) { + ranges::copy( + ranges::views::ints(0, int(list.size())), + std::back_inserter(indices)); + } + const auto which = base::RandomIndex(indices.size()); + const auto index = indices[which]; + indices.erase(begin(indices) + which); + return index; + }; + index = { + .model = choose(attributes.models, models), + .pattern = choose(attributes.patterns, patterns), + .backdrop = choose(attributes.backdrops, backdrops), + }; + } + [[nodiscard]] UniqueGiftCover make() { + randomize(); + + const auto choose = [](const auto &list, int index, int fixed) { + return list[(fixed >= 0) ? fixed : index]; + }; + return { + .values = { + .title = title, + .model = choose( + attributes.models, + index.model, + fixed.model), + .pattern = choose( + attributes.patterns, + index.pattern, + fixed.pattern), + .backdrop = choose( + attributes.backdrops, + index.backdrop, + fixed.backdrop), + }, + .force = paused.current(), + }; + } + void push() { + gift = make(); + } + + QString title; + Delegate delegate; + Data::UniqueGiftAttributes attributes; + std::vector models; + std::vector patterns; + std::vector backdrops; + rpl::variable tab = Tab::Model; + Selection index; + Selection fixed; + rpl::variable paused; + + rpl::variable gift; + + AttributesList *list = nullptr; + + base::Timer pushNextTimer; + }; + + const auto title = gift.resellTitle; + const auto state = box->lifetime().make_state(title, attributes); + + const auto repaintedHook = [=]( + std::optional now, + std::optional next, + float64 progress) { + state->delegate.update(now, next, progress); + }; + + const auto container = box->verticalLayout(); + AddUniqueGiftCover(container, state->gift.value(), { + .subtitle = rpl::conditional( + state->paused.value(), + tr::lng_auction_preview_selected(tr::marked), + tr::lng_auction_preview_random(tr::marked)), + .attributesInfo = true, + .repaintedHook = repaintedHook, + }); + AddUniqueCloseButton(box, {}); + + state->list = container->add(object_ptr( + box, + &state->delegate, + controller, + &state->attributes, + state->tab.value())); + state->list->selected( + ) | rpl::start_with_next([=](Selection value) { + state->fixed = value; + state->paused = (value.model >= 0) + || (value.pattern >= 0) + || (value.backdrop >= 0); + state->push(); + }, box->lifetime()); + + const auto button = box->addButton(rpl::single(u""_q), [] {}); + const auto buttonsParent = button->parentWidget(); + box->clearButtons(); + + const auto add = [&]( + tr::phrase<> text, + const style::RoundButton &st, + const style::icon &active, + Tab tab) { + auto owned = object_ptr(buttonsParent, text(), st); + const auto raw = owned.data(); + + raw->setTextTransform(RoundButton::TextTransform::NoTransform); + raw->setClickedCallback([=] { + state->tab = tab; + }); + const auto icon = &active; + state->tab.value() | rpl::start_with_next([=](Tab now) { + raw->setTextFgOverride((now == tab) + ? st::defaultActiveButton.textFg->c + : std::optional()); + raw->setBrushOverride((now == tab) + ? st::defaultActiveButton.textBg->c + : std::optional()); + raw->setIconOverride((now == tab) ? icon : nullptr); + }, raw->lifetime()); + box->addButton(std::move(owned)); + }; + add( + tr::lng_auction_preview_symbols_button, + st::uniqueAttributeSymbol, + st::uniqueAttributeSymbolActive, + Tab::Pattern); + add( + tr::lng_auction_preview_backdrops_button, + st::uniqueAttributeBackdrop, + st::uniqueAttributeBackdropActive, + Tab::Backdrop); + add( + tr::lng_auction_preview_models_button, + st::uniqueAttributeModel, + st::uniqueAttributeModelActive, + Tab::Model); + + state->paused.value() | rpl::start_with_next([=](bool paused) { + if (paused) { + state->pushNextTimer.cancel(); + } else { + state->pushNextTimer.callEach(kSwitchTimeout); + } + }, box->lifetime()); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_preview_box.h b/Telegram/SourceFiles/boxes/star_gift_preview_box.h new file mode 100644 index 0000000000..ee27a025b4 --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_preview_box.h @@ -0,0 +1,29 @@ +/* +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 +*/ +#pragma once + +namespace Data { +struct StarGift; +struct UniqueGiftAttributes; +} // namespace Data + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { + +class GenericBox; + +void StarGiftPreviewBox( + not_null box, + not_null controller, + const Data::StarGift &gift, + const Data::UniqueGiftAttributes &attributes); + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index bf3467880d..6d405550fb 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -1548,7 +1548,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox( const auto buttonWidth = state->saveButton ? state->saveButton->width() : 0; - state->requestId = document->owner().session().api().request( + state->requestId = document->session().api().request( MTPstickers_RemoveStickerFromSet(document->mtpInput() )).done([=](const TLStickerSet &result) { result.match([&](const MTPDmessages_stickerSet &d) { @@ -1590,7 +1590,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox( state->requestId.value() | rpl::map(rpl::mappers::_1 > 0)); } box->addButton(tr::lng_close(), [=] { - document->owner().session().api().request( + document->session().api().request( state->requestId.current()).cancel(); box->closeBox(); }); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index ecf20568ee..28761178a3 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -1742,7 +1742,7 @@ void StickersListWidget::showStickerSetBox( } lifetime->destroy(); }, *lifetime); - document->owner().session().api().requestSpecialStickersForce( + document->session().api().requestSpecialStickersForce( setId == Data::Stickers::FavedSetId, setId == Data::Stickers::RecentSetId, false); diff --git a/Telegram/SourceFiles/data/data_document_media.cpp b/Telegram/SourceFiles/data/data_document_media.cpp index e61030302b..6bf6bc22f1 100644 --- a/Telegram/SourceFiles/data/data_document_media.cpp +++ b/Telegram/SourceFiles/data/data_document_media.cpp @@ -431,7 +431,7 @@ void DocumentMedia::GenerateGoodThumbnail( document->setGoodThumbnailChecked(false); return; } - const auto guard = base::make_weak(&document->owner().session()); + const auto guard = base::make_weak(&document->session()); crl::async([=, location = std::move(location)] { const auto filepath = (location && location->accessEnable()) ? location->name() diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index 643b2eba4e..50cf3fad53 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -20,14 +20,26 @@ namespace Data { struct UniqueGiftAttribute { QString name; int rarityPermille = 0; + + friend inline bool operator==( + const UniqueGiftAttribute &, + const UniqueGiftAttribute &) = default; }; struct UniqueGiftModel : UniqueGiftAttribute { not_null document; + + friend inline bool operator==( + const UniqueGiftModel &, + const UniqueGiftModel &) = default; }; struct UniqueGiftPattern : UniqueGiftAttribute { not_null document; + + friend inline bool operator==( + const UniqueGiftPattern &, + const UniqueGiftPattern &) = default; }; struct UniqueGiftBackdrop : UniqueGiftAttribute { @@ -36,6 +48,10 @@ struct UniqueGiftBackdrop : UniqueGiftAttribute { QColor patternColor; QColor textColor; int id = 0; + + friend inline bool operator==( + const UniqueGiftBackdrop &, + const UniqueGiftBackdrop &) = default; }; struct UniqueGiftAttributes { diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index 7b7d7f19f4..a290131f6c 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -727,7 +727,7 @@ void Story::applyFields( auto caption = TextWithEntities{ data.vcaption().value_or_empty(), Api::EntitiesFromMTP( - &owner().session(), + &session(), data.ventities().value_or_empty()), }; if (const auto user = _peer->asUser()) { diff --git a/Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp b/Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp index 2cf42d10c0..5d67508679 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp +++ b/Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp @@ -84,7 +84,7 @@ ItemSticker::ItemSticker( return true; }; if (!updateThumbnail()) { - _document->owner().session().downloaderTaskFinished( + _document->session().downloaderTaskFinished( ) | rpl::start_with_next([=] { if (updateThumbnail()) { _loadingLifetime.destroy(); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp index 22d3ecba40..81710f3b5d 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp @@ -161,7 +161,7 @@ ListController::ListController(not_null history) } Main::Session &ListController::session() const { - return _history->owner().session(); + return _history->session(); } void ListController::prepare() { diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp index aa5f13f265..3f4e4d66fe 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -381,7 +381,7 @@ void GiftButton::setDocument(not_null document) { const auto destroyed = base::take(_player); _playerDocument = nullptr; _mediaLifetime = rpl::single() | rpl::then( - document->owner().session().downloaderTaskFinished() + document->session().downloaderTaskFinished() ) | rpl::filter([=] { return media->loaded(); }) | rpl::start_with_next([=] { diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index f0740636eb..893d6bd707 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -168,7 +168,7 @@ void TopicIconView::setupPlayer(not_null topic) { media->goodThumbnailWanted(); return rpl::single() | rpl::then( - document->owner().session().downloaderTaskFinished() + document->session().downloaderTaskFinished() ) | rpl::filter([=] { return media->loaded(); }) | rpl::take(1) | rpl::map([=] { diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp index d035665a92..e094d74d65 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp @@ -748,7 +748,7 @@ void TopBar::setupActions(not_null controller) { tr::lng_profile_action_short_join(tr::now), st::infoProfileTopBarActionJoin); join->setClickedCallback([=] { - channel->owner().session().api().joinChannel(channel); + channel->session().api().joinChannel(channel); }); buttons.push_back(join); _actions->add(join); diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp index 8ee29755da..de89ec82b9 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp +++ b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp @@ -76,7 +76,7 @@ void PreviewPainter::setDocument( } rpl::single() | rpl::then( - document->owner().session().downloaderTaskFinished() + document->session().downloaderTaskFinished() ) | rpl::start_with_next([=] { if (!_media->loaded()) { return; @@ -171,7 +171,7 @@ EmojiUserpic::EmojiUserpic( void EmojiUserpic::setDocument(not_null document) { if (!_playOnce.has_value()) { - const auto &c = document->owner().session().appConfig(); + const auto &c = document->session().appConfig(); _playOnce = !c.get(u"upload_markup_video"_q, false); } _painter.setDocument(document, [=] { update(); }); diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index 010156c48d..49347fd187 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -656,7 +656,7 @@ void WeatherView::setStickerFrom(not_null document) { media->goodThumbnailWanted(); rpl::single() | rpl::then( - document->owner().session().downloaderTaskFinished() + document->session().downloaderTaskFinished() ) | rpl::filter([=] { return media->loaded(); }) | rpl::take(1) | rpl::start_with_next([=] { diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 19028d249e..d19c81a926 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -132,7 +132,7 @@ namespace Media::Stories { return; } - const auto api = &story->owner().session().api(); + const auto api = &story->session().api(); auto &histories = story->owner().histories(); for (const auto thread : result) { const auto action = Api::SendAction(thread, options); diff --git a/Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.cpp b/Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.cpp index 95ce62c335..685f1b5b20 100644 --- a/Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.cpp +++ b/Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.cpp @@ -28,7 +28,7 @@ namespace Menu { Fn RateTranscribeCallbackFactory( not_null item) { return [item](bool good) { - item->history()->peer->owner().session().api().transcribes().rate( + item->history()->peer->session().api().transcribes().rate( item, good); }; @@ -36,9 +36,9 @@ Fn RateTranscribeCallbackFactory( bool HasRateTranscribeItem(not_null item) { const auto &peer = item->history()->peer; - if (!peer->owner().session().api().transcribes().entry( + if (!peer->session().api().transcribes().entry( item).result.isEmpty()) { - return !peer->owner().session().api().transcribes().isRated(item); + return !peer->session().api().transcribes().isRated(item); } return false; } diff --git a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp index 98cffdc6eb..45c7107505 100644 --- a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp @@ -175,7 +175,7 @@ rpl::producer> IconPlayerValue( media->goodThumbnailWanted(); return rpl::single() | rpl::then( - sticker->owner().session().downloaderTaskFinished() + sticker->session().downloaderTaskFinished() ) | rpl::filter([=] { return media->loaded(); }) | rpl::take(1) | rpl::map([=] { diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index f7018e6553..3f3677485c 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -1342,7 +1342,8 @@ void GenericCreditsEntryBox( style); }; const auto canResell = CanResellGift(session, e); - AddUniqueGiftCover(content, rpl::single(*uniqueGift), { + const auto cover = Ui::UniqueGiftCover{ *uniqueGift }; + AddUniqueGiftCover(content, rpl::single(cover), { .resalePrice = std::move(price), .resaleClick = canResell ? std::move(change) : Fn(), }); diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index 5e0ceb54de..b6ee310706 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -320,6 +320,50 @@ uniqueMenuButton: IconButton(uniqueCloseButton) { color: shadowFg; } } +uniqueAttributeTop: 10px; +uniqueAttributeSkip: 8px; +uniqueAttributePadding: margins(6px, 8px, 6px, 8px); +uniqueAttributeName: TextStyle(semiboldTextStyle) { + font: font(12px semibold); +} +uniqueAttributeType: TextStyle(defaultTextStyle) { + font: font(12px); +} +uniqueAttributePercent: uniqueAttributeType; +uniqueAttributePercentPadding: margins(4px, 0px, 4px, 0px); +uniqueAttributeModel: RoundButton(defaultLightButton) { + textFg: windowBoldFg; + textFgOver: windowBoldFg; + textBg: windowBgOver; + textBgOver: windowBgOver; + ripple: defaultRippleAnimation; + radius: 8px; + + width: 107px; + height: 34px; + textTop: 7px; + style: defaultBoxButtonTextStyle; + icon: icon {{ "menu/unique", windowBoldFg }}; + iconOver: icon {{ "menu/unique", windowBoldFg }}; + iconPosition: point(-1px, 4px); +} +uniqueAttributeModelActive: icon{{ "menu/unique", activeButtonFg }}; +uniqueAttributeBackdrop: RoundButton(uniqueAttributeModel) { + icon: icon {{ "menu/palette", windowBoldFg }}; + iconOver: icon {{ "menu/palette", windowBoldFg }}; +} +uniqueAttributeBackdropActive: icon{{ "menu/palette", activeButtonFg }}; +uniqueAttributeSymbol: RoundButton(uniqueAttributeModel) { + icon: icon {{ "menu/all_media", windowBoldFg }}; + iconOver: icon {{ "menu/all_media", windowBoldFg }}; +} +uniqueAttributeSymbolActive: icon{{ "menu/all_media", activeButtonFg }}; +uniqueAttributesBox: Box(giftBox) { + buttonWide: false; + buttonHeight: 34px; + buttonPadding: margins(10px, 10px, 11px, 10px); +} +uniqueAttributeStickerSize: size(64px, 64px); upgradeGiftBox: Box(giftBox) { buttonPadding: margins(22px, 3px, 22px, 22px); } @@ -341,7 +385,7 @@ darkUpgradeGiftBox: Box(upgradeGiftBox) { titleAdditionalFg: groupCallMemberNotJoinedStatus; } darkUpgradeGiftRadiant: icon{{ "menu/unique", groupCallMembersFg }}; -darkUpgradeGiftProfile: icon{{ "menu/profile", groupCallMembersFg }}; +darkUpgradeGiftProfile: icon{{ "settings/premium/features/feature_profile_cover", groupCallMembersFg }}; darkUpgradeGiftProof: icon{{ "menu/factcheck", groupCallMembersFg }}; darkUpgradeGiftInfoTitle: FlatLabel(defaultFlatLabel) { textFg: groupCallMembersFg; diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp index 358b0f3b97..95683ee064 100644 --- a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp @@ -341,7 +341,7 @@ PaintRoundImageCallback GenerateCreditsPaintEntryCallback( photo->load(Data::PhotoSize::Large, {}); rpl::single(rpl::empty_value()) | rpl::then( - photo->owner().session().downloaderTaskFinished() + photo->session().downloaderTaskFinished() ) | rpl::start_with_next([=] { using Size = Data::PhotoSize; if (const auto large = state->view->image(Size::Large)) { @@ -391,7 +391,7 @@ PaintRoundImageCallback GenerateCreditsPaintEntryCallback( video->loadThumbnail({}); rpl::single(rpl::empty_value()) | rpl::then( - video->owner().session().downloaderTaskFinished() + video->session().downloaderTaskFinished() ) | rpl::start_with_next([=] { if (const auto thumbnail = state->view->thumbnail()) { state->imagePtr = thumbnail; diff --git a/Telegram/SourceFiles/ui/top_background_gradient.h b/Telegram/SourceFiles/ui/top_background_gradient.h index 719a50e3de..1f89745a88 100644 --- a/Telegram/SourceFiles/ui/top_background_gradient.h +++ b/Telegram/SourceFiles/ui/top_background_gradient.h @@ -60,6 +60,6 @@ void PaintBgPoints( not_null emoji, const QColor &patternColor, const QRect &rect, - float64 shown); + float64 shown = 1.); } // namespace Ui diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 116b515e5c..258a928d83 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 116b515e5ccd322c72a63e92ec4ad3b16804754f +Subproject commit 258a928d83c9b69ee2e5ab65a2e1aa4f384d7a46