From 51bbe4d2bbe8cb9ee51406f6490cf4c1ec72d134 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 3 Feb 2026 13:06:35 +0400 Subject: [PATCH] Load more saved gifts for crafting. --- Telegram/Resources/langs/lang.strings | 1 + .../boxes/star_gift_auction_box.cpp | 2 +- Telegram/SourceFiles/boxes/star_gift_box.cpp | 688 +++++++++--------- Telegram/SourceFiles/boxes/star_gift_box.h | 21 +- .../SourceFiles/boxes/star_gift_craft_box.cpp | 224 +++--- .../boxes/star_gift_resale_box.cpp | 23 +- Telegram/SourceFiles/data/data_star_gift.cpp | 42 ++ Telegram/SourceFiles/data/data_star_gift.h | 12 + .../peer_gifts/info_peer_gifts_common.cpp | 9 +- .../info/peer_gifts/info_peer_gifts_common.h | 1 + 10 files changed, 582 insertions(+), 441 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c78a3e0270..864c4a3244 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4119,6 +4119,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_collection_reorder_exit" = "Apply Reorder"; "lng_gift_collection_remove_from" = "Remove from Collection"; "lng_gift_locked_title" = "Gift Locked"; +"lng_gift_craft_menu_button" = "Craft"; "lng_gift_craft_info_title" = "Gift Crafting"; "lng_gift_craft_info_about" = "Turn your gifts into rare, epic\nand legendary versions."; "lng_gift_craft_rare_title" = "Get Rare Models"; diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp index 5379c37217..e91734b817 100644 --- a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp @@ -1304,7 +1304,7 @@ void AuctionInfoBox( struct State { explicit State(not_null session) - : delegate(session, GiftButtonMode::Minimal) { + : delegate(session, GiftButtonMode::Minimal) { } Delegate delegate; diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index a6d9f66b04..5730684d15 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -1594,315 +1594,6 @@ void CheckMaybeGiftLocked( })).send(); } -[[nodiscard]] object_ptr MakeGiftsList( - not_null window, - not_null peer, - rpl::producer gifts, - Fn loadMore, - Fn handler = nullptr) { - auto result = object_ptr((QWidget*)nullptr); - const auto raw = result.data(); - - Data::AmPremiumValue(&window->session()) | rpl::on_next([=] { - raw->update(); - }, raw->lifetime()); - - struct State { - Delegate delegate; - std::vector order; - std::vector validated; - std::vector list; - std::vector> buttons; - std::shared_ptr api; - std::shared_ptr transferRequested; - rpl::variable visibleRange; - uint64 resaleRequestingId = 0; - rpl::lifetime resaleLifetime; - bool sending = false; - int perRow = 1; - }; - const auto state = raw->lifetime().make_state(State{ - .delegate = Delegate(&window->session(), GiftButtonMode::Full), - }); - const auto single = state->delegate.buttonSize(); - const auto shadow = st::defaultDropdownMenu.wrap.shadow; - const auto extend = shadow.extend; - - auto &packs = window->session().giftBoxStickersPacks(); - packs.updated() | rpl::on_next([=] { - for (const auto &button : state->buttons) { - if (const auto raw = button.get()) { - raw->update(); - } - } - }, raw->lifetime()); - - const auto rebuild = [=] { - const auto width = st::boxWideWidth; - const auto padding = st::giftBoxPadding; - const auto available = width - padding.left() - padding.right(); - const auto range = state->visibleRange.current(); - const auto count = int(state->list.size()); - - auto &buttons = state->buttons; - if (buttons.size() < count) { - buttons.resize(count); - } - auto &validated = state->validated; - validated.resize(count); - - auto x = padding.left(); - auto y = padding.top(); - const auto perRow = state->perRow; - const auto singlew = single.width() + st::giftBoxGiftSkip.x(); - const auto singleh = single.height() + st::giftBoxGiftSkip.y(); - const auto rowFrom = std::max(range.top - y, 0) / singleh; - const auto rowTill = (std::max(range.bottom - y + st::giftBoxGiftSkip.y(), 0) + singleh - 1) - / singleh; - Assert(rowTill >= rowFrom); - const auto first = rowFrom * perRow; - const auto last = std::min(rowTill * perRow, count); - auto checkedFrom = 0; - auto checkedTill = int(buttons.size()); - const auto ensureButton = [&](int index) { - auto &button = buttons[index]; - if (!button) { - validated[index] = false; - for (; checkedFrom != first; ++checkedFrom) { - if (buttons[checkedFrom]) { - button = std::move(buttons[checkedFrom]); - break; - } - } - } - if (!button) { - for (; checkedTill != last; ) { - --checkedTill; - if (buttons[checkedTill]) { - button = std::move(buttons[checkedTill]); - break; - } - } - } - if (!button) { - button = std::make_unique(raw, &state->delegate); - } - const auto raw = button.get(); - if (validated[index]) { - return; - } - raw->show(); - validated[index] = true; - const auto &descriptor = state->list[state->order[index]]; - raw->setDescriptor(descriptor, GiftButton::Mode::Full); - raw->setClickedCallback([=] { - if (handler) { - handler(descriptor); - return; - } - const auto star = std::get_if(&descriptor); - const auto send = crl::guard(raw, [=] { - window->show(Box( - SendGiftBox, - window, - peer, - state->api, - descriptor, - nullptr)); - }); - const auto unique = star ? star->info.unique : nullptr; - const auto premiumNeeded = star && star->info.requirePremium; - if (unique && star->resale) { - window->show(Box( - Settings::GlobalStarGiftBox, - window->uiShow(), - star->info, - Settings::StarGiftResaleInfo{ - .recipientId = peer->id, - .forceTon = star->forceTon, - }, - Settings::CreditsEntryBoxStyleOverrides())); - } else if (unique && star->mine && !peer->isSelf()) { - if (ShowTransferGiftLater(window->uiShow(), unique)) { - return; - } - const auto done = [=] { - window->session().credits().load(true); - window->showPeerHistory(peer); - }; - if (state->transferRequested == unique) { - return; - } - state->transferRequested = unique; - const auto savedId = star->transferId; - using Payments::CheckoutResult; - const auto formReady = [=]( - uint64 formId, - CreditsAmount price, - std::optional failure) { - state->transferRequested = nullptr; - if (!failure && !price.stars()) { - LOG(("API Error: " - "Bad transfer invoice currenct.")); - } else if (!failure - || *failure == CheckoutResult::Free) { - unique->starsForTransfer = failure - ? 0 - : price.whole(); - ShowTransferToBox( - window, - peer, - unique, - savedId, - done); - } else if (*failure == CheckoutResult::Cancelled) { - done(); - } - }; - RequestOurForm( - window->uiShow(), - MTP_inputInvoiceStarGiftTransfer( - Api::InputSavedStarGiftId(savedId, unique), - peer->input()), - formReady); - } else if (star && star->resale) { - const auto id = star->info.id; - if (state->resaleRequestingId == id) { - return; - } - state->resaleRequestingId = id; - state->resaleLifetime = ShowStarGiftResale( - window, - peer, - id, - star->info.resellTitle, - [=] { state->resaleRequestingId = 0; }); - } else if (star && star->info.auction()) { - if (!IsSoldOut(star->info) - && premiumNeeded - && !peer->session().premium()) { - Settings::ShowPremiumGiftPremium(window, star->info); - } else { - const auto id = star->info.id; - if (state->resaleRequestingId == id) { - return; - } - state->resaleRequestingId = id; - state->resaleLifetime = ShowStarGiftAuction( - window, - peer, - id, - [=] { state->resaleRequestingId = 0; }, - crl::guard(raw, [=] { - state->resaleLifetime.destroy(); - })); - } - } else if (star && IsSoldOut(star->info)) { - window->show(Box(SoldOutBox, window, *star)); - } else if (premiumNeeded && !peer->session().premium()) { - Settings::ShowPremiumGiftPremium(window, star->info); - } else if (star - && star->info.lockedUntilDate - && star->info.lockedUntilDate > base::unixtime::now()) { - const auto ready = crl::guard(raw, [=] { - if (premiumNeeded && !peer->session().premium()) { - Settings::ShowPremiumGiftPremium( - window, - v::get(descriptor).info); - } else { - send(); - } - }); - CheckMaybeGiftLocked(window, star->info.id, ready); - } else if (star - && star->info.perUserTotal - && !star->info.perUserRemains) { - window->showToast({ - .text = tr::lng_gift_sent_finished( - tr::now, - lt_count, - star->info.perUserTotal, - tr::rich), - }); - } else { - send(); - } - }); - raw->setGeometry(QRect(QPoint(x, y), single), extend); - }; - y += rowFrom * singleh; - for (auto row = rowFrom; row != rowTill; ++row) { - for (auto col = 0; col != perRow; ++col) { - const auto index = row * perRow + col; - if (index >= count) { - break; - } - const auto last = !((col + 1) % perRow); - if (last) { - x = padding.left() + available - single.width(); - } - ensureButton(index); - if (last) { - x = padding.left(); - y += singleh; - } else { - x += singlew; - } - } - } - const auto till = std::min(int(buttons.size()), rowTill * perRow); - for (auto i = count; i < till; ++i) { - if (const auto button = buttons[i].get()) { - button->hide(); - } - } - - const auto page = range.bottom - range.top; - if (loadMore && page > 0 && range.bottom + page > raw->height()) { - loadMore(); - } - }; - - state->visibleRange = raw->visibleRange(); - state->visibleRange.value( - ) | rpl::on_next(rebuild, raw->lifetime()); - - std::move( - gifts - ) | rpl::on_next([=](const GiftsDescriptor &gifts) { - const auto width = st::boxWideWidth; - const auto padding = st::giftBoxPadding; - const auto available = width - padding.left() - padding.right(); - state->perRow = available / single.width(); - state->list = std::move(gifts.list); - state->api = gifts.api; - - const auto count = int(state->list.size()); - state->order = ranges::views::ints - | ranges::views::take(count) - | ranges::to_vector; - state->validated.clear(); - - if (SortForBirthday(peer)) { - ranges::stable_partition(state->order, [&](int i) { - const auto &gift = state->list[i]; - const auto stars = std::get_if(&gift); - return stars && stars->info.birthday && !stars->info.unique; - }); - } - - const auto rows = (count + state->perRow - 1) / state->perRow; - const auto height = padding.top() - + (rows * single.height()) - + ((rows - 1) * st::giftBoxGiftSkip.y()) - + padding.bottom(); - raw->resize(raw->width(), height); - rebuild(); - }, raw->lifetime()); - - return result; -} - void FillBg(not_null box) { box->paintRequest() | rpl::on_next([=] { auto p = QPainter(box); @@ -1957,13 +1648,18 @@ void AddBlock( state->gifts = GiftsPremium(&window->session(), peer); - auto result = MakeGiftsList(window, peer, state->gifts.value( + auto gifts = state->gifts.value( ) | rpl::map([=](const PremiumGiftsDescriptor &gifts) { return GiftsDescriptor{ gifts.list | ranges::to>, gifts.api, }; - }), nullptr); + }); + auto result = MakeGiftsList({ + .window = window, + .peer = peer, + .gifts = std::move(gifts), + }); result->lifetime().add([state = std::move(state)] {}); return result; } @@ -1996,7 +1692,8 @@ void AddBlock( tabSelected(tab); }, tabs.widget->lifetime()); result->add(std::move(tabs.widget)); - result->add(MakeGiftsList(window, peer, rpl::combine( + + auto gifts = rpl::combine( state->gifts.value(), state->priceTab.value(), rpl::single(rpl::empty) | rpl::then(state->myUpdated.events()) @@ -2042,7 +1739,8 @@ void AddBlock( return GiftsDescriptor{ gifts | ranges::to>(), }; - }), [=] { + }); + const auto loadMore = [=] { if (state->priceTab.current() == kPriceTabMy && !state->my.offset.isEmpty() && !state->myLoading) { @@ -2062,6 +1760,12 @@ void AddBlock( state->myUpdated.fire({}); }); } + }; + result->add(MakeGiftsList({ + .window = window, + .peer = peer, + .gifts = std::move(gifts), + .loadMore = loadMore, })); return result; @@ -4653,18 +4357,354 @@ CreditsAmount TonFromStars( CreditsType::Ton); } -object_ptr MakeGiftsSendList( +struct DefaultGiftHandlerState { + not_null window; + not_null peer; + std::shared_ptr api; + std::shared_ptr transferRequested; + uint64 resaleRequestingId = 0; + rpl::lifetime resaleLifetime; + + base::has_weak_ptr guard; +}; + +void DefaultGiftHandler( not_null window, - not_null peer, - rpl::producer gifts, - Fn loadMore, - Fn handler) { - return MakeGiftsList( - window, - peer, - std::move(gifts), - std::move(loadMore), - std::move(handler)); + not_null state, + Info::PeerGifts::GiftDescriptor descriptor) { + const auto star = std::get_if(&descriptor); + const auto send = crl::guard(&state->guard, [=] { + window->show(Box( + SendGiftBox, + window, + state->peer, + state->api, + descriptor, + nullptr)); + }); + const auto peer = state->peer; + const auto unique = star ? star->info.unique : nullptr; + const auto premiumNeeded = star && star->info.requirePremium; + if (unique && star->resale) { + window->show(Box( + Settings::GlobalStarGiftBox, + window->uiShow(), + star->info, + Settings::StarGiftResaleInfo{ + .recipientId = peer->id, + .forceTon = star->forceTon, + }, + Settings::CreditsEntryBoxStyleOverrides())); + } else if (unique && star->mine && !peer->isSelf()) { + if (ShowTransferGiftLater(window->uiShow(), unique)) { + return; + } + const auto done = [=] { + window->session().credits().load(true); + window->showPeerHistory(peer); + }; + if (state->transferRequested == unique) { + return; + } + state->transferRequested = unique; + const auto savedId = star->transferId; + using Payments::CheckoutResult; + const auto formReady = [=]( + uint64 formId, + CreditsAmount price, + std::optional failure) { + state->transferRequested = nullptr; + if (!failure && !price.stars()) { + LOG(("API Error: Bad transfer invoice currenct.")); + } else if (!failure + || *failure == CheckoutResult::Free) { + unique->starsForTransfer = failure + ? 0 + : price.whole(); + ShowTransferToBox( + window, + peer, + unique, + savedId, + done); + } else if (*failure == CheckoutResult::Cancelled) { + done(); + } + }; + RequestOurForm( + window->uiShow(), + MTP_inputInvoiceStarGiftTransfer( + Api::InputSavedStarGiftId(savedId, unique), + peer->input()), + formReady); + } else if (star && star->resale) { + const auto id = star->info.id; + if (state->resaleRequestingId == id) { + return; + } + state->resaleRequestingId = id; + state->resaleLifetime = ShowStarGiftResale( + window, + peer, + id, + star->info.resellTitle, + [=] { state->resaleRequestingId = 0; }); + } else if (star && star->info.auction()) { + if (!IsSoldOut(star->info) + && premiumNeeded + && !peer->session().premium()) { + Settings::ShowPremiumGiftPremium(window, star->info); + } else { + const auto id = star->info.id; + if (state->resaleRequestingId == id) { + return; + } + state->resaleRequestingId = id; + state->resaleLifetime = ShowStarGiftAuction( + window, + peer, + id, + [=] { state->resaleRequestingId = 0; }, + crl::guard(&state->guard, [=] { + state->resaleLifetime.destroy(); + })); + } + } else if (star && IsSoldOut(star->info)) { + window->show(Box(SoldOutBox, window, *star)); + } else if (premiumNeeded && !peer->session().premium()) { + Settings::ShowPremiumGiftPremium(window, star->info); + } else if (star + && star->info.lockedUntilDate + && star->info.lockedUntilDate > base::unixtime::now()) { + const auto ready = crl::guard(window, [=] { + if (premiumNeeded && !peer->session().premium()) { + Settings::ShowPremiumGiftPremium( + window, + v::get(descriptor).info); + } else { + send(); + } + }); + CheckMaybeGiftLocked(window, star->info.id, ready); + } else if (star + && star->info.perUserTotal + && !star->info.perUserRemains) { + window->showToast({ + .text = tr::lng_gift_sent_finished( + tr::now, + lt_count, + star->info.perUserTotal, + tr::rich), + }); + } else { + send(); + } +} + +object_ptr MakeGiftsList(GiftsListArgs &&args) { + using namespace Info::PeerGifts; + + auto result = object_ptr((QWidget*)nullptr); + const auto raw = result.data(); + + const auto mode = args.mode; + const auto peer = args.peer; + const auto window = args.window; + const auto session = &window->session(); + + Data::AmPremiumValue(session) | rpl::on_next([=] { + raw->update(); + }, raw->lifetime()); + + struct State { + Delegate delegate; + DefaultGiftHandlerState handlerState; + std::vector order; + std::vector validated; + std::vector list; + std::vector> buttons; + rpl::variable visibleRange; + bool sending = false; + int perRow = 1; + }; + const auto buttonMode = (mode == GiftsListMode::Craft) + ? GiftButtonMode::Craft + : GiftButtonMode::Full; + const auto state = raw->lifetime().make_state(State{ + .delegate = Delegate(session, buttonMode), + .handlerState = { + .window = window, + .peer = peer, + }, + }); + const auto single = state->delegate.buttonSize(); + const auto shadow = st::defaultDropdownMenu.wrap.shadow; + const auto extend = shadow.extend; + + const auto handler = args.handler + ? args.handler + : crl::guard(raw, [=](const GiftDescriptor &descriptor) { + DefaultGiftHandler(window, &state->handlerState, descriptor); + }); + + auto &packs = session->giftBoxStickersPacks(); + packs.updated() | rpl::on_next([=] { + for (const auto &button : state->buttons) { + if (const auto raw = button.get()) { + raw->update(); + } + } + }, raw->lifetime()); + + const auto loadMore = args.loadMore; + const auto alreadySelected = args.selected; + const auto rebuild = [=] { + const auto width = st::boxWideWidth; + const auto padding = st::giftBoxPadding; + const auto available = width - padding.left() - padding.right(); + const auto range = state->visibleRange.current(); + const auto count = int(state->list.size()); + + auto &buttons = state->buttons; + if (buttons.size() < count) { + buttons.resize(count); + } + auto &validated = state->validated; + validated.resize(count); + + auto x = padding.left(); + auto y = padding.top(); + const auto perRow = state->perRow; + const auto singlew = single.width() + st::giftBoxGiftSkip.x(); + const auto singleh = single.height() + st::giftBoxGiftSkip.y(); + const auto rangeFrom = range.top - y; + const auto rowFrom = std::max(rangeFrom, 0) / singleh; + const auto rangeTill = range.bottom - y + st::giftBoxGiftSkip.y(); + const auto rowTill = (std::max(rangeTill, 0) + singleh - 1) + / singleh; + Assert(rowTill >= rowFrom); + const auto first = rowFrom * perRow; + const auto last = std::min(rowTill * perRow, count); + auto checkedFrom = 0; + auto checkedTill = int(buttons.size()); + const auto ensureButton = [&](int index) { + auto &button = buttons[index]; + if (!button) { + validated[index] = false; + for (; checkedFrom != first; ++checkedFrom) { + if (buttons[checkedFrom]) { + button = std::move(buttons[checkedFrom]); + break; + } + } + } + if (!button) { + for (; checkedTill != last; ) { + --checkedTill; + if (buttons[checkedTill]) { + button = std::move(buttons[checkedTill]); + break; + } + } + } + if (!button) { + button = std::make_unique(raw, &state->delegate); + } + const auto raw = button.get(); + if (validated[index]) { + return; + } + raw->show(); + validated[index] = true; + const auto &descriptor = state->list[state->order[index]]; + if (mode == GiftsListMode::Craft) { + const auto already = ranges::contains( + alreadySelected, + v::get(descriptor).info.unique->slug, + &Data::UniqueGift::slug); + raw->toggleSelected( + already, + GiftSelectionMode::Inset, + anim::type::instant); + raw->setAttribute(Qt::WA_TransparentForMouseEvents, already); + } + raw->setDescriptor(descriptor, buttonMode); + raw->setClickedCallback([=] { + handler(descriptor); + }); + raw->setGeometry(QRect(QPoint(x, y), single), extend); + }; + y += rowFrom * singleh; + for (auto row = rowFrom; row != rowTill; ++row) { + for (auto col = 0; col != perRow; ++col) { + const auto index = row * perRow + col; + if (index >= count) { + break; + } + const auto last = !((col + 1) % perRow); + if (last) { + x = padding.left() + available - single.width(); + } + ensureButton(index); + if (last) { + x = padding.left(); + y += singleh; + } else { + x += singlew; + } + } + } + const auto till = std::min(int(buttons.size()), rowTill * perRow); + for (auto i = count; i < till; ++i) { + if (const auto button = buttons[i].get()) { + button->hide(); + } + } + + const auto page = range.bottom - range.top; + if (loadMore && page > 0 && range.bottom + page > raw->height()) { + loadMore(); + } + }; + + state->visibleRange = raw->visibleRange(); + state->visibleRange.value( + ) | rpl::on_next(rebuild, raw->lifetime()); + + std::move( + args.gifts + ) | rpl::on_next([=](const GiftsDescriptor &gifts) { + const auto width = st::boxWideWidth; + const auto padding = st::giftBoxPadding; + const auto available = width - padding.left() - padding.right(); + state->perRow = available / single.width(); + state->list = std::move(gifts.list); + state->handlerState.api = gifts.api; + + const auto count = int(state->list.size()); + state->order = ranges::views::ints + | ranges::views::take(count) + | ranges::to_vector; + state->validated.clear(); + + if (SortForBirthday(peer)) { + ranges::stable_partition(state->order, [&](int i) { + const auto &gift = state->list[i]; + const auto stars = std::get_if(&gift); + return stars && stars->info.birthday && !stars->info.unique; + }); + } + + const auto rows = (count + state->perRow - 1) / state->perRow; + const auto height = padding.top() + + (rows * single.height()) + + ((rows - 1) * st::giftBoxGiftSkip.y()) + + padding.bottom(); + raw->resize(raw->width(), height); + rebuild(); + }, raw->lifetime()); + + return result; } void SendGiftBox( diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index dccfaee088..a9e545d818 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -156,12 +156,21 @@ struct GiftsDescriptor { std::vector list; std::shared_ptr api; }; -[[nodiscard]] object_ptr MakeGiftsSendList( - not_null window, - not_null peer, - rpl::producer gifts, - Fn loadMore, - Fn handler = nullptr); +enum class GiftsListMode { + Send, + Craft, + CraftResale, +}; +struct GiftsListArgs { + not_null window; + GiftsListMode mode = GiftsListMode::Send; + not_null peer; + rpl::producer gifts; + std::vector> selected; + Fn loadMore; + Fn handler; +}; +[[nodiscard]] object_ptr MakeGiftsList(GiftsListArgs &&args); void SendGiftBox( not_null box, diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp index 8d204e9100..481c06c941 100644 --- a/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/widgets/buttons.h" #include "ui/widgets/gradient_round_button.h" +#include "ui/wrap/slide_wrap.h" #include "ui/painter.h" #include "ui/top_background_gradient.h" #include "ui/ui_utility.h" @@ -51,8 +52,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { namespace { -constexpr auto kCraftGiftsLimit = 30; - using namespace Info::PeerGifts; struct ColorScheme { @@ -562,6 +561,104 @@ AbstractButton *MakeRemoveButton( }; } +void AddCraftGiftsList( + not_null window, + not_null container, + Data::CraftGiftsDescriptor descriptor, + const std::vector &selected, + Fn)> chosen) { + struct State { + rpl::event_stream<> updated; + Data::CraftGiftsDescriptor data; + QString offset; + rpl::variable empty = true; + rpl::lifetime loading; + }; + const auto state = container->lifetime().make_state(); + state->data = std::move(descriptor); + + using Descriptor = Info::PeerGifts::GiftDescriptor; + using StarGift = Info::PeerGifts::GiftTypeStars; + auto handler = crl::guard(container, [=](Descriptor descriptor) { + Expects(v::is(descriptor)); + + const auto unique = v::get(descriptor).info.unique; + chosen(unique); + }); + + auto gifts = rpl::single( + rpl::empty + ) | rpl::then(state->updated.events()) | rpl::map([=] { + auto result = GiftsDescriptor(); + const auto selfId = window->session().userPeerId(); + for (const auto &gift : state->data.list) { + result.list.push_back(Info::PeerGifts::GiftTypeStars{ + .info = gift.info, + .resale = true, + .mine = (gift.info.unique->ownerId == selfId), + }); + } + state->empty = result.list.empty(); + return result; + }); + const auto peer = window->session().user(); + const auto loadMore = [=] { + if (!state->offset.isEmpty() && !state->loading) { + state->loading = Data::CraftGiftsSlice( + &peer->session(), + state->data.giftId, + state->data.offset + ) | rpl::on_next([=](Data::CraftGiftsDescriptor &&slice) { + state->loading.destroy(); + state->data.offset = slice.list.empty() + ? QString() + : slice.offset; + state->data.list.insert( + end(state->data.list), + std::make_move_iterator(begin(slice.list)), + std::make_move_iterator(end(slice.list))); + state->updated.fire({}); + }); + } + }; + container->add(MakeGiftsList({ + .window = window, + .mode = GiftsListMode::Craft, + .peer = peer, + .gifts = std::move(gifts), + .selected = (selected + | ranges::views::transform(&GiftForCraft::unique) + | ranges::to_vector), + .loadMore = loadMore, + .handler = handler, + })); + + const auto skip = st::defaultSubsectionTitlePadding.top(); + const auto wrap = container->add( + object_ptr>( + container, + object_ptr( + container, + tr::lng_gift_craft_select_none(), + st::craftYourListEmpty), + (st::boxRowPadding + QMargins(0, 0, 0, skip))), + style::al_top); + state->empty.value() | rpl::on_next([=](bool empty) { + // Scroll doesn't jump up if we show before rows are cleared, + // and we hide after rows are added. + if (empty) { + wrap->show(anim::type::instant); + } else { + crl::on_main(wrap, [=] { + if (!state->empty.current()) { + wrap->hide(anim::type::instant); + } + }); + } + }, wrap->lifetime()); + wrap->entity()->setTryMakeSimilarLines(true); +} + void ShowSelectGiftBox( not_null controller, uint64 giftId, @@ -573,24 +670,17 @@ void ShowSelectGiftBox( GiftButton *button = nullptr; }; struct State { - explicit State(not_null session) - : delegate(session, GiftButtonMode::Minimal) { - } - - Delegate delegate; std::vector entries; - std::vector myList; - QString myOffset; - bool myFinished = false; - + Data::CraftGiftsDescriptor craft; Data::ResaleGiftsDescriptor resale; + rpl::lifetime craftLifetime; rpl::lifetime resaleLifetime; }; const auto session = &controller->session(); - const auto state = std::make_shared(session); + const auto state = std::make_shared(); const auto make = [=](not_null box) { box->setTitle(tr::lng_gift_craft_select_title()); @@ -602,65 +692,18 @@ void ShowSelectGiftBox( box->verticalLayout(), tr::lng_gift_craft_select_your()); - if (state->myList.empty()) { - box->addRow( - object_ptr( - box, - tr::lng_gift_craft_select_none(), - st::craftYourListEmpty), - style::al_top - )->setTryMakeSimilarLines(true); - } + const auto got = crl::guard(box, [=]( + std::shared_ptr gift) { + chosen(GiftForCraft{ .unique = gift }); + box->closeBox(); + }); - const auto single = state->delegate.buttonSize(); - const auto extend = state->delegate.buttonExtend(); - const auto full = single.grownBy(extend).height(); - const auto skip = st::boxRowPadding.left() / 2; - const auto gifts = box->addRow( - object_ptr(box), - QMargins()); - for (auto &gift : state->myList) { - const auto row = (state->entries.size() / 3); - const auto col = (state->entries.size() % 3); - state->entries.push_back(Entry{ - .gift = gift, - .button = CreateChild(gifts, &state->delegate), - }); - const auto button = state->entries.back().button; - const auto proj = &GiftForCraft::slugId; - if (ranges::contains(selected, gift.info.unique->slug, proj)) { - button->toggleSelected( - true, - GiftSelectionMode::Inset, - anim::type::instant); - button->setAttribute(Qt::WA_TransparentForMouseEvents); - } else { - button->setClickedCallback([=] { - chosen({ gift.info.unique, gift.manageId }); - box->closeBox(); - }); - } - button->show(); - button->setDescriptor(GiftTypeStars{ - .info = { - .id = gift.info.id, - .unique = gift.info.unique, - .document = gift.info.unique->model.document, - }, - }, GiftButton::Mode::Minimal); - const auto width = (st::boxWideWidth - - 2 * skip - - st::boxRowPadding.left() - - st::boxRowPadding.right()) / 3; - const auto x = st::boxRowPadding.left() + (width + skip) * col; - const auto y = extend.top() + full * row; - button->setGeometry( - QRect(x, y, width, single.height()), - extend); - } - gifts->resize( - gifts->width(), - (state->myList.size() + 2) / 3 * full); + AddCraftGiftsList( + controller, + box->verticalLayout(), + state->craft, + selected, + got); if (const auto count = state->resale.count) { AddSubsectionTitle( @@ -669,18 +712,13 @@ void ShowSelectGiftBox( lt_count, rpl::single(count * 1.))); - const auto bought = crl::guard(box, [=]( - std::shared_ptr gift) { - chosen(GiftForCraft{ .unique = gift }); - box->closeBox(); - }); AddResaleGiftsList( controller, - controller->session().user(), + session->user(), box->verticalLayout(), state->resale, rpl::single(false), - bought, + got, true); } @@ -693,33 +731,17 @@ void ShowSelectGiftBox( controller->show(Box(make)); }); - session->api().request(MTPpayments_GetCraftStarGifts( - MTP_long(giftId), - MTP_string(), - MTP_int(kCraftGiftsLimit) - )).done([=](const MTPpayments_SavedStarGifts &result) { - const auto user = session->user(); - const auto owner = &session->data(); - const auto &data = result.data(); - owner->processUsers(data.vusers()); - owner->processChats(data.vchats()); - - auto list = std::vector(); - list.reserve(data.vgifts().v.size()); - for (const auto &gift : data.vgifts().v) { - if (auto parsed = Api::FromTL(user, gift)) { - list.push_back(std::move(*parsed)); - } - } - - state->myList = std::move(list); - state->myOffset = qs(data.vnext_offset().value_or_empty()); - state->myFinished = !data.vnext_offset(); + state->craftLifetime = Data::CraftGiftsSlice( + session, + giftId + ) | rpl::on_next([=](Data::CraftGiftsDescriptor &&info) { + state->craftLifetime.destroy(); + state->craft = std::move(info); if (state->resale.giftId) { show(); } - }).send(); + }); state->resaleLifetime = Data::ResaleGiftsSlice( session, @@ -729,7 +751,7 @@ void ShowSelectGiftBox( state->resaleLifetime.destroy(); state->resale = std::move(info); - if (!state->myList.empty() || state->myFinished) { + if (state->craft.giftId) { show(); } }); diff --git a/Telegram/SourceFiles/boxes/star_gift_resale_box.cpp b/Telegram/SourceFiles/boxes/star_gift_resale_box.cpp index 8d86cec534..bd1190c529 100644 --- a/Telegram/SourceFiles/boxes/star_gift_resale_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_resale_box.cpp @@ -652,7 +652,7 @@ void AddResaleGiftsList( }); } - container->add(MakeGiftsSendList(window, peer, rpl::single( + auto gifts = rpl::single( rpl::empty ) | rpl::then(rpl::merge( state->updated.events() | rpl::type_erased, @@ -662,16 +662,21 @@ void AddResaleGiftsList( const auto selfId = window->session().userPeerId(); const auto forceTon = state->ton.current(); for (const auto &gift : state->data.list) { + const auto mine = (gift.unique->ownerId == selfId); + if (mine && forCraft) { + continue; + } result.list.push_back(Info::PeerGifts::GiftTypeStars{ .info = gift, .forceTon = forceTon, .resale = true, - .mine = (gift.unique->ownerId == selfId), - }); + .mine = mine, + }); } state->empty = result.list.empty(); return result; - }), [=] { + }); + const auto loadMore = [=] { if (!state->data.offset.isEmpty() && !state->loading) { state->loading = ResaleGiftsSlice( @@ -691,7 +696,15 @@ void AddResaleGiftsList( state->updated.fire({}); }); } - }, customHandler)); + }; + container->add(MakeGiftsList({ + .window = window, + .mode = forCraft ? GiftsListMode::CraftResale : GiftsListMode::Send, + .peer = peer, + .gifts = std::move(gifts), + .loadMore = loadMore, + .handler = customHandler, + })); const auto skip = st::defaultSubsectionTitlePadding.top(); const auto wrap = container->add( diff --git a/Telegram/SourceFiles/data/data_star_gift.cpp b/Telegram/SourceFiles/data/data_star_gift.cpp index 3f6b98c751..3d8ce6c50d 100644 --- a/Telegram/SourceFiles/data/data_star_gift.cpp +++ b/Telegram/SourceFiles/data/data_star_gift.cpp @@ -24,6 +24,7 @@ namespace { constexpr auto kMyGiftsPerPage = 50; constexpr auto kResaleGiftsPerPage = 50; +constexpr auto kCraftGiftsPerPage = 50; [[nodiscard]] MTPStarGiftAttributeId AttributeToTL(GiftAttributeId id) { switch (id.type) { @@ -305,4 +306,45 @@ rpl::producer ResaleGiftsSlice( }; } +rpl::producer CraftGiftsSlice( + not_null session, + uint64 giftId, + QString offset) { + return [=](auto consumer) { + const auto requestId = session->api().request( + MTPpayments_GetCraftStarGifts( + MTP_long(giftId), + MTP_string(), + MTP_int(kCraftGiftsPerPage)) + ).done([=](const MTPpayments_SavedStarGifts &result) { + const auto user = session->user(); + const auto owner = &session->data(); + const auto &data = result.data(); + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + + auto info = CraftGiftsDescriptor{ + .giftId = giftId, + .offset = qs(data.vnext_offset().value_or_empty()), + .count = data.vcount().v, + }; + info.list.reserve(data.vgifts().v.size()); + for (const auto &gift : data.vgifts().v) { + if (auto parsed = Api::FromTL(user, gift)) { + info.list.push_back(std::move(*parsed)); + } + } + consumer.put_next(std::move(info)); + consumer.put_done(); + }).fail([=](const MTP::Error &error) { + consumer.put_next({}); + consumer.put_done(); + }).send(); + + auto lifetime = rpl::lifetime(); + lifetime.add([=] { session->api().request(requestId).cancel(); }); + return lifetime; + }; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index eb2bb6caba..1153dae5ce 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -408,4 +408,16 @@ struct ResaleGiftsFilter { ResaleGiftsFilter filter = {}, QString offset = QString()); +struct CraftGiftsDescriptor { + uint64 giftId = 0; + QString offset; + std::vector list; + int count = 0; +}; + +[[nodiscard]] rpl::producer CraftGiftsSlice( + not_null session, + uint64 giftId, + QString offset = QString()); + } // namespace Data 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 46475acc94..5415ea2c7b 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -270,7 +270,7 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { : data.from ? Ui::MakeUserpicThumbnail(data.from) : Ui::MakeHiddenAuthorThumbnail(); - if (small() && !resale) { + if (small() && !resale || (_mode == GiftButtonMode::Craft)) { _price = {}; _stars.reset(); return; @@ -337,7 +337,7 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { _uniquePatternEmoji = nullptr; _uniquePatternCache.clear(); - if (small() && !resale) { + if (_price.isEmpty()) { _button = QRect(); return; } @@ -817,7 +817,8 @@ void GiftButton::paint(QPainter &p, float64 craftProgress) { QRect( (width - size.width()) / 2, ((_mode == GiftButtonMode::CraftPreview - || _mode == GiftButtonMode::Minimal) + || _mode == GiftButtonMode::Minimal + || _mode == GiftButtonMode::Craft) ? (extend.top() + ((height - extend.top() @@ -970,7 +971,7 @@ void GiftButton::paint(QPainter &p, float64 craftProgress) { v::match(_descriptor, [](const GiftTypePremium &) { }, [&](const GiftTypeStars &data) { - if (!unique || _mode == Mode::CraftPreview) { + if (!unique || _mode == Mode::Craft || _mode == Mode::CraftPreview) { } else if (data.pinned && _mode != GiftButtonMode::Selection) { auto hq = PainterHighQualityEnabler(p); const auto &icon = st::giftBoxPinIcon; diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h index 7f57282f21..dd0bc042e4 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -138,6 +138,7 @@ struct GiftBadge { enum class GiftButtonMode : uint8 { Full, + Craft, Minimal, Selection, CraftPreview,