diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 64ce31f976..5ba5079c4f 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -127,6 +127,7 @@ constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000); constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000); constexpr auto kResaleGiftsPerPage = 50; constexpr auto kFiltersCount = 4; +constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000); using namespace HistoryView; using namespace Info::PeerGifts; @@ -220,6 +221,33 @@ struct GiftDetails { bool byStars = false; }; +struct SessionResalePrices { + explicit SessionResalePrices(not_null session) + : api(std::make_unique(session->user())) { + } + + std::unique_ptr api; + base::flat_map prices; + std::vector> waiting; + rpl::lifetime requestLifetime; + crl::time lastReceived = 0; +}; + +[[nodiscard]] not_null ResalePrices( + not_null session) { + static auto result = base::flat_map< + not_null, + std::unique_ptr>(); + if (const auto i = result.find(session); i != end(result)) { + return i->second.get(); + } + const auto i = result.emplace( + session, + std::make_unique(session)).first; + session->lifetime().add([session] { result.remove(session); }); + return i->second.get(); +} + class PeerRow final : public PeerListRow { public: using PeerListRow::PeerListRow; @@ -4381,6 +4409,55 @@ void ShowUniqueGiftWearBox( })); } +void PreloadUniqueGiftResellPrices(not_null session) { + const auto entry = ResalePrices(session); + const auto now = crl::now(); + const auto makeRequest = entry->prices.empty() + || (now - entry->lastReceived >= kResellPriceCacheLifetime); + if (!makeRequest || entry->requestLifetime) { + return; + } + const auto finish = [=] { + entry->requestLifetime.destroy(); + entry->lastReceived = crl::now(); + for (const auto &callback : base::take(entry->waiting)) { + callback(); + } + }; + entry->requestLifetime = entry->api->requestStarGifts( + ) | rpl::start_with_error_done(finish, [=] { + const auto &gifts = entry->api->starGifts(); + entry->prices.reserve(gifts.size()); + for (auto &gift : gifts) { + if (!gift.resellTitle.isEmpty() && gift.starsResellMin > 0) { + entry->prices[gift.resellTitle] = gift.starsResellMin; + } + } + finish(); + }); +} + +void InvokeWithUniqueGiftResellPrice( + not_null session, + const QString &title, + Fn callback) { + PreloadUniqueGiftResellPrices(session); + + const auto finish = [=] { + const auto entry = ResalePrices(session); + Assert(entry->lastReceived != 0); + + const auto i = entry->prices.find(title); + callback((i != end(entry->prices)) ? i->second : 0); + }; + const auto entry = ResalePrices(session); + if (entry->lastReceived) { + finish(); + } else { + entry->waiting.push_back(finish); + } +} + void UpdateGiftSellPrice( std::shared_ptr show, std::shared_ptr unique, @@ -4422,6 +4499,132 @@ void UpdateGiftSellPrice( }).send(); } +void UniqueGiftSellBox( + not_null box, + std::shared_ptr show, + std::shared_ptr unique, + Data::SavedStarGiftId savedId, + int price, + Settings::GiftWearBoxStyleOverride st) { + box->setTitle(tr::lng_gift_sell_title()); + box->setStyle(st.box ? *st.box : st::upgradeGiftBox); + box->setWidth(st::boxWideWidth); + + box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] { + box->closeBox(); + }); + const auto priceNow = unique->starsForResale; + const auto name = Data::UniqueGiftName(*unique); + const auto slug = unique->slug; + + const auto session = &show->session(); + AddSubsectionTitle( + box->verticalLayout(), + tr::lng_gift_sell_placeholder(), + (st::boxRowPadding - QMargins( + st::defaultSubsectionTitlePadding.left(), + 0, + st::defaultSubsectionTitlePadding.right(), + st::defaultSubsectionTitlePadding.bottom()))); + const auto &appConfig = session->appConfig(); + const auto limit = appConfig.giftResalePriceMax(); + const auto minimal = appConfig.giftResalePriceMin(); + const auto thousandths = appConfig.giftResaleReceiveThousandths(); + const auto wrap = box->addRow(object_ptr( + box, + st::editTagField.heightMin)); + auto owned = object_ptr( + wrap, + st::editTagField, + rpl::single(QString()), + QString::number(priceNow ? priceNow : price ? price : minimal), + limit); + const auto field = owned.data(); + wrap->widthValue() | rpl::start_with_next([=](int width) { + field->move(0, 0); + field->resize(width, field->height()); + wrap->resize(width, field->height()); + }, wrap->lifetime()); + field->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(field); + st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); + }, field->lifetime()); + field->selectAll(); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + + const auto errors = box->lifetime().make_state< + rpl::event_stream<> + >(); + auto goods = rpl::merge( + rpl::single(rpl::empty) | rpl::map_to(true), + base::qt_signal_producer( + field, + &Ui::NumberInput::changed + ) | rpl::map_to(true), + errors->events() | rpl::map_to(false) + ) | rpl::start_spawning(box->lifetime()); + auto text = rpl::duplicate(goods) | rpl::map([=](bool good) { + const auto value = field->getLastText().toInt(); + const auto receive = (int64(value) * thousandths) / 1000; + return !good + ? tr::lng_gift_sell_min_price( + tr::now, + lt_count, + minimal, + Ui::Text::RichLangValue) + : (value >= minimal) + ? tr::lng_gift_sell_amount( + tr::now, + lt_count, + receive, + Ui::Text::RichLangValue) + : tr::lng_gift_sell_about( + tr::now, + lt_percent, + TextWithEntities{ u"%1%"_q.arg(thousandths / 10.) }, + Ui::Text::RichLangValue); + }); + const auto details = box->addRow(object_ptr( + box, + std::move(text) | rpl::after_next([=] { + box->verticalLayout()->resizeToWidth(box->width()); + }), + st::boxLabel)); + Ui::AddSkip(box->verticalLayout()); + + rpl::duplicate(goods) | rpl::start_with_next([=](bool good) { + details->setTextColorOverride( + good ? st::windowSubTextFg->c : st::boxTextFgError->c); + }, details->lifetime()); + + QObject::connect(field, &NumberInput::submitted, [=] { + const auto count = field->getLastText().toInt(); + if (count < minimal) { + field->showError(); + errors->fire({}); + return; + } + box->closeBox(); + UpdateGiftSellPrice(show, unique, savedId, count); + }); + const auto button = box->addButton(priceNow + ? tr::lng_gift_sell_update() + : tr::lng_gift_sell_put(), [=] { field->submitted({}); }); + rpl::combine( + box->widthValue(), + button->widthValue() + ) | rpl::start_with_next([=](int outer, int inner) { + const auto padding = st::giftBox.buttonPadding; + const auto wanted = outer - padding.left() - padding.right(); + if (inner != wanted) { + button->resizeToWidth(wanted); + button->moveToLeft(padding.left(), padding.top()); + } + }, box->lifetime()); +} + void ShowUniqueGiftSellBox( std::shared_ptr show, std::shared_ptr unique, @@ -4430,125 +4633,11 @@ void ShowUniqueGiftSellBox( if (ShowResaleGiftLater(show, unique)) { return; } - show->show(Box([=](not_null box) { - box->setTitle(tr::lng_gift_sell_title()); - box->setStyle(st.box ? *st.box : st::upgradeGiftBox); - box->setWidth(st::boxWideWidth); - - box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] { - box->closeBox(); - }); - const auto priceNow = unique->starsForResale; - const auto name = Data::UniqueGiftName(*unique); - const auto slug = unique->slug; - - const auto session = &show->session(); - AddSubsectionTitle( - box->verticalLayout(), - tr::lng_gift_sell_placeholder(), - (st::boxRowPadding - QMargins( - st::defaultSubsectionTitlePadding.left(), - 0, - st::defaultSubsectionTitlePadding.right(), - st::defaultSubsectionTitlePadding.bottom()))); - const auto &appConfig = session->appConfig(); - const auto limit = appConfig.giftResalePriceMax(); - const auto minimal = appConfig.giftResalePriceMin(); - const auto thousandths = appConfig.giftResaleReceiveThousandths(); - const auto wrap = box->addRow(object_ptr( - box, - st::editTagField.heightMin)); - auto owned = object_ptr( - wrap, - st::editTagField, - rpl::single(QString()), - QString::number(priceNow ? priceNow : minimal), - limit); - const auto field = owned.data(); - wrap->widthValue() | rpl::start_with_next([=](int width) { - field->move(0, 0); - field->resize(width, field->height()); - wrap->resize(width, field->height()); - }, wrap->lifetime()); - field->paintRequest() | rpl::start_with_next([=](QRect clip) { - auto p = QPainter(field); - st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); - }, field->lifetime()); - field->selectAll(); - box->setFocusCallback([=] { - field->setFocusFast(); - }); - - const auto errors = box->lifetime().make_state< - rpl::event_stream<> - >(); - auto goods = rpl::merge( - rpl::single(rpl::empty) | rpl::map_to(true), - base::qt_signal_producer( - field, - &Ui::NumberInput::changed - ) | rpl::map_to(true), - errors->events() | rpl::map_to(false) - ) | rpl::start_spawning(box->lifetime()); - auto text = rpl::duplicate(goods) | rpl::map([=](bool good) { - const auto value = field->getLastText().toInt(); - const auto receive = (int64(value) * thousandths) / 1000; - return !good - ? tr::lng_gift_sell_min_price( - tr::now, - lt_count, - minimal, - Ui::Text::RichLangValue) - : (value >= minimal) - ? tr::lng_gift_sell_amount( - tr::now, - lt_count, - receive, - Ui::Text::RichLangValue) - : tr::lng_gift_sell_about( - tr::now, - lt_percent, - TextWithEntities{ u"%1%"_q.arg(thousandths / 10.) }, - Ui::Text::RichLangValue); - }); - const auto details = box->addRow(object_ptr( - box, - std::move(text) | rpl::after_next([=] { - box->verticalLayout()->resizeToWidth(box->width()); - }), - st::boxLabel)); - Ui::AddSkip(box->verticalLayout()); - - rpl::duplicate(goods) | rpl::start_with_next([=](bool good) { - details->setTextColorOverride( - good ? st::windowSubTextFg->c : st::boxTextFgError->c); - }, details->lifetime()); - - QObject::connect(field, &NumberInput::submitted, [=] { - const auto count = field->getLastText().toInt(); - if (count < minimal) { - field->showError(); - errors->fire({}); - return; - } - box->closeBox(); - UpdateGiftSellPrice(show, unique, savedId, count); - }); - const auto button = box->addButton(priceNow - ? tr::lng_gift_sell_update() - : tr::lng_gift_sell_put(), [=] { field->submitted({}); }); - rpl::combine( - box->widthValue(), - button->widthValue() - ) | rpl::start_with_next([=](int outer, int inner) { - const auto padding = st::giftBox.buttonPadding; - const auto wanted = outer - padding.left() - padding.right(); - if (inner != wanted) { - button->resizeToWidth(wanted); - button->moveToLeft(padding.left(), padding.top()); - } - }, box->lifetime()); - })); + const auto session = &show->session(); + const auto &title = unique->title; + InvokeWithUniqueGiftResellPrice(session, title, [=](int price) { + show->show(Box(UniqueGiftSellBox, show, unique, savedId, price, st)); + }); } void GiftReleasedByHandler(not_null peer) { diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index 78ee14a028..b3df5b5b87 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -21,6 +21,7 @@ class SavedStarGiftId; } // namespace Data namespace Main { +class Session; class SessionShow; } // namespace Main @@ -71,6 +72,8 @@ void ShowUniqueGiftWearBox( const Data::UniqueGift &gift, Settings::GiftWearBoxStyleOverride st); +void PreloadUniqueGiftResellPrices(not_null session); + void UpdateGiftSellPrice( std::shared_ptr show, std::shared_ptr unique, diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 86b7d7b527..8fd731a9e5 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium.h" #include "apiwrap.h" +#include "boxes/star_gift_box.h" #include "data/data_channel.h" #include "data/data_credits.h" #include "data/data_session.h" @@ -380,6 +381,7 @@ void InnerWidget::loadMore() { _entries.clear(); } _entries.reserve(_entries.size() + data.vgifts().v.size()); + auto hasUnique = false; for (const auto &gift : data.vgifts().v) { if (auto parsed = Api::FromTL(_peer, gift)) { auto descriptor = DescriptorForGift(_peer, *parsed); @@ -387,10 +389,15 @@ void InnerWidget::loadMore() { .gift = std::move(*parsed), .descriptor = std::move(descriptor), }); + hasUnique = (parsed->info.unique != nullptr); } } refreshButtons(); refreshAbout(); + + if (hasUnique) { + Ui::PreloadUniqueGiftResellPrices(&_peer->session()); + } }).fail([=] { _loadMoreRequestId = 0; _allLoaded = true; diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index afb9b9c64b..4cd259d2b6 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -1250,12 +1250,13 @@ void GenericCreditsEntryBox( EntryToSavedStarGiftId(session, e), style); }; + const auto canResell = CanResellGift(session, e); AddUniqueGiftCover( content, rpl::single(*uniqueGift), {}, std::move(price), - CanResellGift(session, e) ? std::move(change) : Fn()); + canResell ? std::move(change) : Fn()); AddSkip(content, st::defaultVerticalListSkip * 2); @@ -1263,6 +1264,10 @@ void GenericCreditsEntryBox( const auto type = SavedStarGiftMenuType::View; FillUniqueGiftMenu(show, menu, e, type, st); }); + + if (canResell) { + Ui::PreloadUniqueGiftResellPrices(session); + } } else if (const auto callback = Ui::PaintPreviewCallback(session, e)) { const auto thumb = content->add(object_ptr>( content,