diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f6bc102c25..2cb7d3ce07 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3736,6 +3736,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_stars_auction" = "auction"; "lng_gift_stars_auction_join" = "Join"; "lng_gift_stars_auction_view" = "View"; +"lng_gift_stars_auction_soon" = "soon"; +"lng_gift_stars_auction_upgraded" = "upgraded"; "lng_gift_stars_your_left#one" = "{count} left"; "lng_gift_stars_your_left#other" = "{count} left"; "lng_gift_stars_your_finished" = "none left"; @@ -4082,6 +4084,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_auction_text_link" = "Learn more {arrow}"; "lng_auction_text_ended" = "Auction ended."; "lng_auction_start_label" = "Started"; +"lng_auction_starts_label" = "Starts"; +"lng_auction_rounds_label" = "Rounds"; +"lng_auction_rounds_first" = "Round 1"; +"lng_auction_rounds_rest" = "Rounds 2-{last}"; +"lng_auction_rounds_rest_minutes#one" = "{count} minute each"; +"lng_auction_rounds_rest_minutes#other" = "{count} minutes each"; +"lng_auction_rounds_rest_hours#one" = "{count} hour each"; +"lng_auction_rounds_rest_hours#other" = "{count} hours each"; "lng_auction_end_label" = "Ends"; "lng_auction_round_label" = "Current Round"; "lng_auction_round_value" = "{n} of {amount}"; @@ -4096,10 +4106,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_auction_join_time_left" = "{time} left"; "lng_auction_join_time_medium" = "{hours} h {minutes} m"; "lng_auction_join_time_small" = "{minutes} m"; +"lng_auction_join_starts_in" = "starts in {time}"; +"lng_auction_join_early_bid" = "Place an Early Bid"; "lng_auction_menu_about" = "About"; "lng_auction_menu_copy_link" = "Copy Link"; "lng_auction_menu_share" = "Share"; "lng_auction_bid_title" = "Place a Bid"; +"lng_auction_bid_title_early" = "Place an Early Bid"; "lng_auction_bid_subtitle#one" = "Top {count} bidder will win"; "lng_auction_bid_subtitle#other" = "Top {count} bidders will win"; "lng_auction_bid_your" = "your bid"; @@ -4154,6 +4167,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_auction_change_to" = "Do you want to raise your bid and change the recipient to {name}?"; "lng_auction_change_already_me" = "You've already placed a bid on this gift for yourself."; "lng_auction_change_to_me" = "Do you want to raise your bid and change the recipient to yourself?"; +"lng_auction_preview_name" = "Upcoming Auction"; +"lng_auction_preview_learn_gifts" = "Learn more about Telegram Gifts {arrow}"; +"lng_auction_preview_variants#one" = "View {emoji} {count} Variant {arrow}"; +"lng_auction_preview_variants#other" = "View {emoji} {count} Variants {arrow}"; +"lng_auction_preview_random" = "Random Traits"; +"lng_auction_preview_selected" = "Selected Traits"; +"lng_auction_preview_randomize" = "Randomize Traits"; +"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_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_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_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."; "lng_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account."; diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 029280537c..316681746a 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -874,10 +874,12 @@ std::optional FromTL( .resellCount = int(data.vavailability_resale().value_or_empty()), .auctionSlug = qs(data.vauction_slug().value_or_empty()), .auctionGiftsPerRound = data.vgifts_per_round().value_or_empty(), + .auctionStartDate = data.vauction_start_date().value_or_empty(), .limitedLeft = remaining.value_or_empty(), .limitedCount = total.value_or_empty(), .perUserTotal = data.vper_user_total().value_or_empty(), .perUserRemains = data.vper_user_remains().value_or_empty(), + .upgradeVariants = data.vupgrade_variants().value_or_empty(), .firstSaleDate = data.vfirst_sale_date().value_or_empty(), .lastSaleDate = data.vlast_sale_date().value_or_empty(), .lockedUntilDate = data.vlocked_until_date().value_or_empty(), @@ -957,6 +959,8 @@ std::optional FromTL( data.vvalue_currency().value_or_empty()), .valuePrice = int64( data.vvalue_amount().value_or_empty()), + .valuePriceUsd = int64( + data.vvalue_usd_amount().value_or_empty()), }) : nullptr), .peerColor = colorCollectible, @@ -1024,6 +1028,7 @@ std::optional FromTL( ? peerFromMTP(*data.vfrom_id()) : PeerId()), .date = data.vdate().v, + .giftNum = data.vgift_num().value_or_empty(), .upgradeSeparate = data.is_upgrade_separate(), .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp index 6ffe58a6d5..02e59ee4fd 100644 --- a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp @@ -8,12 +8,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/star_gift_auction_box.h" #include "api/api_text_entities.h" +#include "base/random.h" #include "base/timer_rpl.h" #include "base/unixtime.h" #include "boxes/peers/replace_boost_box.h" #include "boxes/send_credits_box.h" // CreditsEmojiSmall #include "boxes/share_box.h" #include "boxes/star_gift_box.h" +#include "boxes/star_gift_resale_box.h" #include "calls/group/calls_group_common.h" #include "core/application.h" #include "core/credits_amount.h" @@ -74,6 +76,7 @@ namespace { constexpr auto kAuctionAboutShownPref = "gift_auction_about_shown"_cs; constexpr auto kBidPlacedToastDuration = 5 * crl::time(1000); +constexpr auto kSwitchPreviewCoverInterval = 3 * crl::time(1000); constexpr auto kMaxShownBid = 30'000; constexpr auto kShowTopPlaces = 3; @@ -288,33 +291,39 @@ struct BidSliderValues { return result; } -Fn MakeAuctionMenuCallback( - not_null parent, +Fn)> MakeAuctionFillMenuCallback( std::shared_ptr show, const Data::GiftAuctionState &state) { const auto url = show->session().createInternalLinkFull( u"auction/"_q + state.gift->auctionSlug); const auto rounds = state.totalRounds; const auto perRound = state.gift->auctionGiftsPerRound;; - const auto menu = std::make_shared>(); - return [=] { - *menu = base::make_unique_q( - parent, - st::popupMenuWithIcons); - - (*menu)->addAction(tr::lng_auction_menu_about(tr::now), [=] { + return [=](not_null menu) { + menu->addAction(tr::lng_auction_menu_about(tr::now), [=] { show->show(Box(AuctionAboutBox, rounds, perRound, nullptr)); }, &st::menuIconInfo); - (*menu)->addAction(tr::lng_auction_menu_copy_link(tr::now), [=] { + menu->addAction(tr::lng_auction_menu_copy_link(tr::now), [=] { QApplication::clipboard()->setText(url); show->showToast(tr::lng_username_copied(tr::now)); }, &st::menuIconLink); - (*menu)->addAction(tr::lng_auction_menu_share(tr::now), [=] { + menu->addAction(tr::lng_auction_menu_share(tr::now), [=] { FastShareLink(show, url); }, &st::menuIconShare); + }; +} +Fn MakeAuctionMenuCallback( + not_null parent, + std::shared_ptr show, + const Data::GiftAuctionState &state) { + const auto menu = std::make_shared>(); + return [=, fill = MakeAuctionFillMenuCallback(show, state)] { + *menu = base::make_unique_q( + parent, + st::popupMenuWithIcons); + fill(menu->get()); (*menu)->popup(QCursor::pos()); }; } @@ -906,6 +915,30 @@ void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { helper.context()); } +[[nodiscard]] std::vector RandomIndicesSubset(int total, int subset) { + const auto take = std::min(total, subset); + if (!take) { + return {}; + } + auto result = std::vector(); + auto taken = base::flat_set(); + result.reserve(take); + taken.reserve(take); + for (auto i = 0; i < take; ++i) { + auto index = base::RandomIndex(total - i); + for (const auto already : taken) { + if (index >= already) { + ++index; + } else { + break; + } + } + taken.emplace(index); + result.push_back(index); + } + return result; +} + [[nodiscard]] object_ptr AuctionInfoTable( not_null parent, not_null container, @@ -921,6 +954,7 @@ void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { state->value = std::move(value); const auto &now = state->value.current(); + const auto preview = (now.startDate > base::unixtime::now()); const auto name = now.gift->resellTitle; state->finished = now.finished() ? (rpl::single(true) | rpl::type_erased()) @@ -932,10 +966,12 @@ void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { }; AddTableRow( raw, - rpl::conditional( - state->finished.value(), - tr::lng_gift_link_label_first_sale(), - tr::lng_auction_start_label()), + (preview + ? tr::lng_auction_starts_label() + : rpl::conditional( + state->finished.value(), + tr::lng_gift_link_label_first_sale(), + tr::lng_auction_start_label())), date(now.startDate)); AddTableRow( raw, @@ -944,65 +980,104 @@ void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { tr::lng_gift_link_label_last_sale(), tr::lng_auction_end_label()), date(now.endDate)); - - auto roundText = state->value.value( - ) | rpl::map([](const Data::GiftAuctionState &state) { - const auto wrapped = [](int count) { - return rpl::single(tr::marked(Lang::FormatCountDecimal(count))); - }; - return tr::lng_auction_round_value( - lt_n, - wrapped(state.currentRound), - lt_amount, - wrapped(state.totalRounds), - tr::marked); - }) | rpl::flatten_latest(); - const auto round = AddTableRow( - raw, - tr::lng_auction_round_label(), - std::move(roundText)); - - auto availabilityText = state->value.value( - ) | rpl::map([](const Data::GiftAuctionState &state) { - const auto wrapped = [](int count) { - return rpl::single(tr::marked(Lang::FormatCountDecimal(count))); - }; - return tr::lng_auction_availability_value( - lt_n, - wrapped(state.giftsLeft), - lt_amount, - wrapped(state.gift->limitedCount), - tr::marked); - }) | rpl::flatten_latest(); - AddTableRow( - raw, - tr::lng_auction_availability_label(), - std::move(availabilityText)); - - const auto tooltip = std::make_shared( - TableRowTooltipData{ .parent = container }); - state->value.value( - ) | rpl::map([](const Data::GiftAuctionState &state) { - return state.averagePrice; - }) | rpl::filter( - rpl::mappers::_1 != 0 - ) | rpl::take( - 1 - ) | rpl::start_with_next([=](int64 price) { - delete round; - - raw->insertRow( - 2, - object_ptr( + if (preview) { + AddTableRow( + raw, + tr::lng_gift_unique_availability_label(), + rpl::single(tr::marked( + Lang::FormatCountDecimal(now.gift->limitedCount)))); + AddTableRow( + raw, + tr::lng_auction_rounds_label(), + rpl::single(tr::marked( + Lang::FormatCountDecimal(now.totalRounds)))); + AddTableRow( + raw, + tr::lng_auction_rounds_first(), + ((now.roundDurationFirst % 3600) + ? tr::lng_minutes( + lt_count, + rpl::single(now.roundDurationFirst / 60.), + tr::marked) + : tr::lng_hours( + lt_count, + rpl::single(now.roundDurationFirst / 3600.), + tr::marked))); + if (now.totalRounds > 1) { + AddTableRow( raw, - tr::lng_auction_average_label(), - raw->st().defaultLabel), - MakeAveragePriceValue(raw, tooltip, name, price), - st::giveawayGiftCodeLabelMargin, - st::giveawayGiftCodeValueMargin); - raw->resizeToWidth(raw->widthNoMargins()); - }, raw->lifetime()); + tr::lng_auction_rounds_rest( + lt_last, + rpl::single(QString::number(now.totalRounds))), + ((now.roundDurationRest % 3600) + ? tr::lng_auction_rounds_rest_minutes( + lt_count, + rpl::single(now.roundDurationRest / 60.), + tr::marked) + : tr::lng_auction_rounds_rest_hours( + lt_count, + rpl::single(now.roundDurationRest / 3600.), + tr::marked))); + } + } else { + auto roundText = state->value.value( + ) | rpl::map([](const Data::GiftAuctionState &state) { + const auto wrapped = [](int count) { + return rpl::single(tr::marked(Lang::FormatCountDecimal(count))); + }; + return tr::lng_auction_round_value( + lt_n, + wrapped(state.currentRound), + lt_amount, + wrapped(state.totalRounds), + tr::marked); + }) | rpl::flatten_latest(); + const auto round = AddTableRow( + raw, + tr::lng_auction_round_label(), + std::move(roundText)); + auto availabilityText = state->value.value( + ) | rpl::map([](const Data::GiftAuctionState &state) { + const auto wrapped = [](int count) { + return rpl::single(tr::marked(Lang::FormatCountDecimal(count))); + }; + return tr::lng_auction_availability_value( + lt_n, + wrapped(state.giftsLeft), + lt_amount, + wrapped(state.gift->limitedCount), + tr::marked); + }) | rpl::flatten_latest(); + AddTableRow( + raw, + tr::lng_auction_availability_label(), + std::move(availabilityText)); + + const auto tooltip = std::make_shared( + TableRowTooltipData{ .parent = container }); + state->value.value( + ) | rpl::map([](const Data::GiftAuctionState &state) { + return state.averagePrice; + }) | rpl::filter( + rpl::mappers::_1 != 0 + ) | rpl::take( + 1 + ) | rpl::start_with_next([=](int64 price) { + delete round; + + raw->insertRow( + 2, + object_ptr( + raw, + tr::lng_auction_average_label(), + raw->st().defaultLabel), + MakeAveragePriceValue(raw, tooltip, name, price), + st::giveawayGiftCodeLabelMargin, + st::giveawayGiftCodeValueMargin); + raw->resizeToWidth(raw->widthNoMargins()); + }, raw->lifetime()); + } return result; } @@ -1092,6 +1167,93 @@ void AuctionGotGiftsBox( } } +[[nodiscard]] rpl::producer MakePreviewAuctionStream( + const Data::StarGift &info, + rpl::producer attributes) { + Expects(attributes); + + auto initial = Data::UniqueGift{ + .title = info.resellTitle, + .model = Data::UniqueGiftModel{ + .document = info.document, + }, + .pattern = Data::UniqueGiftPattern{ + .document = info.document, + }, + .backdrop = Data::UniqueGiftBackdrop{ + .centerColor = QColor(0x3a, 0x76, 0xb4), + .edgeColor = QColor(0x10, 0x2d, 0x4d), + .patternColor = QColor(0, 0, 0, 0), + .textColor = QColor(0xff, 0xff, 0xff), + }, + }; + return rpl::single(initial) | rpl::then(std::move( + attributes + ) | rpl::map([=](const Data::UniqueGiftAttributes &values) + -> rpl::producer { + if (values.backdrops.empty() + || values.models.empty() + || values.patterns.empty()) { + return rpl::never(); + } + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + Data::UniqueGiftAttributes data; + std::vector modelIndices; + std::vector patternIndices; + std::vector backdropIndices; + }; + const auto state = lifetime.make_state(State{ + .data = values, + }); + + const auto put = [=] { + const auto index = []( + std::vector &indices, + const auto &v) { + const auto fill = [&] { + if (!indices.empty()) { + return; + } + indices = ranges::views::ints( + 0 + ) | ranges::views::take( + v.size() + ) | ranges::to_vector; + ranges::shuffle(indices); + }; + fill(); + const auto result = indices.back(); + indices.pop_back(); + fill(); + if (indices.back() == result) { + std::swap(indices.front(), indices.back()); + } + return result; + }; + auto &models = state->data.models; + auto &patterns = state->data.patterns; + auto &backdrops = state->data.backdrops; + consumer.put_next(Data::UniqueGift{ + .title = info.resellTitle, + .model = models[index(state->modelIndices, models)], + .pattern = patterns[index(state->patternIndices, patterns)], + .backdrop = backdrops[index(state->backdropIndices, backdrops)], + }); + }; + + put(); + base::timer_each( + kSwitchPreviewCoverInterval / 3 + ) | rpl::start_with_next(put, lifetime); + + return lifetime; + }; + }) | rpl::flatten_latest()); +} + void AuctionInfoBox( not_null box, not_null window, @@ -1101,105 +1263,160 @@ void AuctionInfoBox( struct State { explicit State(not_null session) - : delegate(session, GiftButtonMode::Minimal) { + : delegate(session, GiftButtonMode::Minimal) { } Delegate delegate; rpl::variable value; - rpl::variable minutesLeft; + rpl::variable minutesTillEnd; + rpl::variable secondsTillStart; + rpl::variable attributes; std::vector acquired; bool acquiredRequested = false; base::unique_qptr menu; + + rpl::lifetime previewLifetime; + bool previewRequested = false; }; const auto show = window->uiShow(); const auto state = box->lifetime().make_state(&show->session()); state->value = std::move(value); const auto &now = state->value.current(); - - state->minutesLeft = MinutesLeftTillValue(now.endDate); + const auto auctions = &show->session().giftAuctions(); + const auto giftId = now.gift->id; + if (auto attributes = auctions->attributes(giftId)) { + state->attributes = std::move(*attributes); + } else { + auctions->requestAttributes(giftId, crl::guard(box, [=] { + state->attributes.force_assign(*auctions->attributes(giftId)); + })); + } + state->minutesTillEnd = MinutesLeftTillValue(now.endDate); + state->secondsTillStart = SecondsLeftTillValue(now.startDate); + const auto started = !state->secondsTillStart.current(); + const auto perRound = now.gift->auctionGiftsPerRound; box->setStyle(st::giftBox); + box->setNoContentMargin(true); + if (!started) { + const auto container = box->verticalLayout(); + AddUniqueGiftCover( + container, + MakePreviewAuctionStream(*now.gift, state->attributes.value()), + tr::lng_gift_upgrade_about()); - const auto name = now.gift->resellTitle; - const auto extend = st::defaultDropdownMenu.wrap.shadow.extend; - const auto side = st::giftBoxGiftSmall; - const auto size = QSize(side, side).grownBy(extend); - const auto preview = box->addRow( - object_ptr(box, size.height()), - st::auctionInfoPreviewMargin); - const auto gift = CreateChild(preview, &state->delegate); - gift->setAttribute(Qt::WA_TransparentForMouseEvents); - gift->setDescriptor(GiftTypeStars{ - .info = *now.gift, - }, GiftButtonMode::Minimal); + AddSkip(container, st::defaultVerticalListSkip * 2); - preview->widthValue() | rpl::start_with_next([=](int width) { - const auto left = (width - size.width()) / 2; - gift->setGeometry( - QRect(QPoint(left, 0), size).marginsRemoved(extend), - extend); - }, gift->lifetime()); + AddUniqueCloseButton(box, {}, MakeAuctionFillMenuCallback(show, now)); + } else { + const auto name = now.gift->resellTitle; + const auto extend = st::defaultDropdownMenu.wrap.shadow.extend; + const auto side = st::giftBoxGiftSmall; + const auto size = QSize(side, side).grownBy(extend); + const auto preview = box->addRow( + object_ptr(box, size.height()), + st::auctionInfoPreviewMargin); + const auto gift = CreateChild(preview, &state->delegate); + gift->setAttribute(Qt::WA_TransparentForMouseEvents); + gift->setDescriptor(GiftTypeStars{ + .info = *now.gift, + }, GiftButtonMode::Minimal); - const auto rounds = state->value.current().totalRounds; - const auto perRound = state->value.current().gift->auctionGiftsPerRound; - auto aboutText = state->value.value( - ) | rpl::map([=](const Data::GiftAuctionState &state) { - if (state.finished()) { - return tr::lng_auction_text_ended(tr::now, tr::marked); - } - return tr::lng_auction_text( - tr::now, - lt_count, - perRound, - lt_name, - tr::bold(name), - lt_link, - tr::lng_auction_text_link( + preview->widthValue() | rpl::start_with_next([=](int width) { + const auto left = (width - size.width()) / 2; + gift->setGeometry( + QRect(QPoint(left, 0), size).marginsRemoved(extend), + extend); + }, gift->lifetime()); + + const auto rounds = state->value.current().totalRounds; + auto aboutText = state->value.value( + ) | rpl::map([=](const Data::GiftAuctionState &state) { + if (state.finished()) { + return tr::lng_auction_text_ended(tr::now, tr::marked); + } + return tr::lng_auction_text( tr::now, - lt_arrow, - Text::IconEmoji(&st::textMoreIconEmoji), - tr::link), - tr::rich); - }); - box->addRow( - object_ptr( - box, - name, - st::uniqueGiftTitle), - style::al_top); - const auto about = box->addRow( - object_ptr( - box, - std::move(aboutText), - st::uniqueGiftSubtitle), - st::boxRowPadding + QMargins(0, st::auctionInfoSubtitleSkip, 0, 0), - style::al_top); - about->setTryMakeSimilarLines(true); - box->resizeToWidth(box->widthNoMargins()); + lt_count, + perRound, + lt_name, + tr::bold(name), + lt_link, + tr::lng_auction_text_link( + tr::now, + lt_arrow, + Text::IconEmoji(&st::textMoreIconEmoji), + tr::link), + tr::rich); + }); + box->addRow( + object_ptr( + box, + name, + st::uniqueGiftTitle), + style::al_top); + const auto about = box->addRow( + object_ptr( + box, + std::move(aboutText), + st::uniqueGiftSubtitle), + (st::boxRowPadding + + QMargins(0, st::auctionInfoSubtitleSkip, 0, 0)), + style::al_top); + about->setTryMakeSimilarLines(true); - about->setClickHandlerFilter([=](const auto &...) { - show->show(Box(AuctionAboutBox, rounds, perRound, nullptr)); - return false; - }); + about->setClickHandlerFilter([=](const auto &...) { + show->show(Box(AuctionAboutBox, rounds, perRound, nullptr)); + return false; + }); + + const auto close = CreateChild( + box->verticalLayout(), + st::boxTitleClose); + close->setClickedCallback([=] { box->closeBox(); }); + + const auto menu = CreateChild( + box->verticalLayout(), + st::boxTitleMenu); + menu->setClickedCallback(MakeAuctionMenuCallback(menu, show, now)); + const auto weakMenu = base::make_weak(menu); + + box->verticalLayout()->widthValue() | rpl::start_with_next([=](int) { + close->moveToRight(0, 0); + if (const auto strong = weakMenu.get()) { + strong->moveToRight(close->width(), 0); + } + }, close->lifetime()); + + rpl::combine( + state->value.value(), + state->minutesTillEnd.value() + ) | rpl::start_with_next([=]( + const Data::GiftAuctionState &state, + int minutes) { + const auto finished = state.finished() || (minutes <= 0); + about->setTextColorOverride(finished + ? st::attentionButtonFg->c + : std::optional()); + if (const auto strong = finished ? weakMenu.get() : nullptr) { + delete strong; + } + }, box->lifetime()); + } box->addRow( AuctionInfoTable(box, box->verticalLayout(), state->value.value()), st::boxRowPadding + st::auctionInfoTableMargin); - state->value.value( - ) | rpl::map([=](const Data::GiftAuctionState &value) { - return value.my.gotCount; - }) | rpl::filter( - rpl::mappers::_1 > 0 - ) | rpl::take(1) | rpl::start_with_next([=](int count) { + if (const auto got = now.my.gotCount) { box->addRow( object_ptr( box, tr::lng_auction_bought( lt_count_decimal, - rpl::single(count * 1.), + rpl::single(1. * got), lt_emoji, rpl::single(Data::SingleCustomEmoji( state->value.current().gift->document)), @@ -1224,7 +1441,7 @@ void AuctionInfoBox( state->acquired)); } else if (!state->acquiredRequested) { state->acquiredRequested = true; - show->session().giftAuctions().requestAcquired( + auctions->requestAcquired( value.gift->id, crl::guard(box, [=]( std::vector result) { @@ -1241,11 +1458,56 @@ void AuctionInfoBox( } return false; }); - }, box->lifetime()); - + } else if (const auto variants = now.gift->upgradeVariants) { + using namespace Data; + state->attributes.value( + ) | rpl::filter([](const UniqueGiftAttributes &list) { + return !list.models.empty(); + }) | rpl::take( + 1 + ) | rpl::start_with_next([=](const UniqueGiftAttributes &list) { + auto emoji = tr::marked(); + const auto indices = RandomIndicesSubset(list.models.size(), 3); + for (const auto index : indices) { + emoji.append(Data::SingleCustomEmoji( + list.models[index].document)); + } + box->addRow( + object_ptr( + box, + tr::lng_auction_preview_variants( + lt_count_decimal, + rpl::single(1. * variants), + lt_emoji, + rpl::single(emoji), + lt_arrow, + rpl::single(Text::IconEmoji(&st::textMoreIconEmoji)), + tr::link), + st::uniqueGiftValueAvailableLink, + st::defaultPopupMenu, + Core::TextContext({ .session = &show->session() })), + 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; }); + return false; + }); + }, box->lifetime()); + } const auto button = box->addButton(rpl::single(QString()), [=] { if (state->value.current().finished() - || !state->minutesLeft.current()) { + || !state->minutesTillEnd.current()) { box->closeBox(); return; } @@ -1266,40 +1528,6 @@ void AuctionInfoBox( button, AuctionButtonCountdownType::Join, state->value.value()); - - box->setNoContentMargin(true); - const auto close = CreateChild( - box->verticalLayout(), - st::boxTitleClose); - close->setClickedCallback([=] { box->closeBox(); }); - - const auto menu = CreateChild( - box->verticalLayout(), - st::boxTitleMenu); - menu->setClickedCallback(MakeAuctionMenuCallback(menu, show, now)); - const auto weakMenu = base::make_weak(menu); - - box->verticalLayout()->widthValue() | rpl::start_with_next([=](int) { - close->moveToRight(0, 0); - if (const auto strong = weakMenu.get()) { - strong->moveToRight(close->width(), 0); - } - }, close->lifetime()); - - rpl::combine( - state->value.value(), - state->minutesLeft.value() - ) | rpl::start_with_next([=]( - const Data::GiftAuctionState &state, - int minutes) { - const auto finished = state.finished() || (minutes <= 0); - about->setTextColorOverride(finished - ? st::attentionButtonFg->c - : std::optional()); - if (const auto strong = finished ? weakMenu.get() : nullptr) { - delete strong; - } - }, box->lifetime()); } base::weak_qptr ChooseAndShowAuctionBox( @@ -1308,16 +1536,21 @@ base::weak_qptr ChooseAndShowAuctionBox( std::shared_ptr> state, Fn boxClosed) { const auto local = &peer->session().local(); - const auto &now = state->current(); - const auto finished = now.finished() - || (now.endDate <= base::unixtime::now()); - const auto showBidBox = now.my.bid + const auto ¤t = state->current(); + const auto now = base::unixtime::now(); + const auto started = (current.startDate <= now); + const auto finished = current.finished() || (current.endDate <= now); + const auto showBidBox = current.my.bid && !finished - && (!now.my.to || now.my.to == peer); - const auto showChangeRecipient = !showBidBox && now.my.bid && !finished; + && (!current.my.to || current.my.to == peer); + const auto showChangeRecipient = !showBidBox + && current.my.bid + && !finished; const auto showInfoBox = !showBidBox && !showChangeRecipient - && (local->readPref(kAuctionAboutShownPref) || finished); + && (!started + || finished + || local->readPref(kAuctionAboutShownPref)); auto box = base::weak_qptr(); if (showBidBox) { box = window->show(MakeAuctionBidBox({ @@ -1333,13 +1566,13 @@ base::weak_qptr ChooseAndShowAuctionBox( peer, nullptr, Info::PeerGifts::GiftTypeStars{ - .info = *now.gift, + .info = *current.gift, }, state->value())); sendBox->boxClosing( ) | rpl::start_with_next(close, sendBox->lifetime()); }; - const auto from = now.my.to; + const auto from = current.my.to; const auto text = (from->isSelf() ? tr::lng_auction_change_already_me(tr::now, tr::rich) : tr::lng_auction_change_already( @@ -1383,8 +1616,8 @@ base::weak_qptr ChooseAndShowAuctionBox( }; box = window->show(Box( AuctionAboutBox, - now.totalRounds, - now.gift->auctionGiftsPerRound, + current.totalRounds, + current.gift->auctionGiftsPerRound, understood)); } if (const auto strong = box.get()) { @@ -1457,35 +1690,60 @@ void SetAuctionButtonCountdownText( rpl::producer value) { struct State { rpl::variable value; - rpl::variable minutesLeft; + rpl::variable minutesTillEnd; + rpl::variable secondsTillStart; }; const auto state = button->lifetime().make_state(); state->value = std::move(value); - state->minutesLeft = MinutesLeftTillValue( - state->value.current().endDate); + + const auto &now = state->value.current(); + const auto preview = (now.startDate > base::unixtime::now()); + if (preview) { + state->secondsTillStart = SecondsLeftTillValue(now.startDate); + } else { + state->minutesTillEnd = MinutesLeftTillValue(now.endDate); + } auto buttonTitle = rpl::combine( state->value.value(), - state->minutesLeft.value() - ) | rpl::map([=](const Data::GiftAuctionState &state, int minutes) { - return (state.finished() || minutes <= 0) + (preview + ? state->secondsTillStart.value() + : state->minutesTillEnd.value()) + ) | rpl::map([=](const Data::GiftAuctionState &state, int leftTill) { + return (state.finished() || (!preview && leftTill <= 0)) ? tr::lng_box_ok(tr::marked) - : (type == AuctionButtonCountdownType::Join) - ? tr::lng_auction_join_button(tr::marked) - : tr::lng_auction_join_bid(tr::marked); + : (type == AuctionButtonCountdownType::Place) + ? tr::lng_auction_join_bid(tr::marked) + : tr::lng_auction_join_button(tr::marked); }) | rpl::flatten_latest(); auto buttonSubtitle = rpl::combine( state->value.value(), - state->minutesLeft.value() + (preview + ? state->secondsTillStart.value() + : state->minutesTillEnd.value()) ) | rpl::map([=]( - const Data::GiftAuctionState &state, - int minutes) -> rpl::producer { - if (state.finished() || minutes <= 0) { + const Data::GiftAuctionState &state, + int leftTill + ) -> rpl::producer { + if (state.finished() || leftTill <= 0) { return rpl::single(TextWithEntities()); + } else if (preview) { + const auto hours = (leftTill / 3600); + const auto minutes = (leftTill % 3600) / 60; + const auto seconds = (leftTill % 60); + const auto time = hours + ? u"%1:%2:%3"_q + .arg(hours).arg(minutes, 2, 10, QChar('0')) + .arg(seconds, 2, 10, QChar('0')) + : u"%1:%2"_q.arg(minutes).arg(seconds, 2, 10, QChar('0')); + return tr::lng_auction_join_starts_in( + lt_time, + rpl::single(tr::marked(time)), + tr::marked); } - const auto hours = (minutes / 60); - minutes -= (hours * 60); + const auto hours = (leftTill / 60); + const auto minutes = leftTill % 60; auto value = [](int count) { return rpl::single(tr::marked(QString::number(count))); diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.h b/Telegram/SourceFiles/boxes/star_gift_auction_box.h index e79d1e394e..880abafb89 100644 --- a/Telegram/SourceFiles/boxes/star_gift_auction_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.h @@ -14,6 +14,7 @@ class Show; namespace Data { struct GiftAuctionState; struct ActiveAuctions; +struct StarGift; } // namespace Data namespace Info::PeerGifts { @@ -49,6 +50,7 @@ struct AuctionBidBoxArgs { enum class AuctionButtonCountdownType { Join, Place, + Preview, }; void SetAuctionButtonCountdownText( not_null button, diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 882583150e..a594d6cbbe 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -3007,15 +3007,16 @@ void AddUniqueGiftCover( } p.drawImage(0, 0, gift.gradient); - Ui::PaintBgPoints( - p, - Ui::PatternBgPoints(), - gift.emojis, - gift.emoji.get(), - *gift.gift, - QRect(0, 0, width, pointsHeight), - shown); - + if (gift.gift->backdrop.patternColor.alpha() > 0) { + Ui::PaintBgPoints( + p, + Ui::PatternBgPoints(), + gift.emojis, + gift.emoji.get(), + *gift.gift, + QRect(0, 0, width, pointsHeight), + shown); + } const auto lottie = gift.lottie.get(); const auto factor = style::DevicePixelRatio(); const auto request = Lottie::FrameRequest{ diff --git a/Telegram/SourceFiles/data/components/gift_auctions.cpp b/Telegram/SourceFiles/data/components/gift_auctions.cpp index 61afcccacf..459536007c 100644 --- a/Telegram/SourceFiles/data/components/gift_auctions.cpp +++ b/Telegram/SourceFiles/data/components/gift_auctions.cpp @@ -112,6 +112,7 @@ void GiftAuctions::requestAcquired( .date = data.vdate().v, .bidAmount = int64(data.vbid_amount().v), .round = data.vround().v, + .number = data.vgift_num().value_or_empty(), .position = data.vpos().v, .nameHidden = data.is_name_hidden(), }); @@ -129,6 +130,49 @@ void GiftAuctions::requestAcquired( }).send(); } +std::optional GiftAuctions::attributes( + uint64 giftId) const { + const auto i = _attributes.find(giftId); + return (i != end(_attributes) && i->second.waiters.empty()) + ? i->second.lists + : std::optional(); +} + +void GiftAuctions::requestAttributes(uint64 giftId, Fn ready) { + auto &entry = _attributes[giftId]; + entry.waiters.push_back(std::move(ready)); + if (entry.waiters.size() > 1) { + return; + } + _session->api().request(MTPpayments_GetStarGiftUpgradeAttributes( + MTP_long(giftId) + )).done([=](const MTPpayments_StarGiftUpgradeAttributes &result) { + const auto &attributes = result.data().vattributes().v; + auto &entry = _attributes[giftId]; + auto &info = entry.lists; + info.models.reserve(attributes.size()); + info.patterns.reserve(attributes.size()); + info.backdrops.reserve(attributes.size()); + for (const auto &attribute : attributes) { + attribute.match([&](const MTPDstarGiftAttributeModel &data) { + info.models.push_back(Api::FromTL(_session, data)); + }, [&](const MTPDstarGiftAttributePattern &data) { + info.patterns.push_back(Api::FromTL(_session, data)); + }, [&](const MTPDstarGiftAttributeBackdrop &data) { + info.backdrops.push_back(Api::FromTL(data)); + }, [](const MTPDstarGiftAttributeOriginalDetails &data) { + }); + } + for (const auto &ready : base::take(entry.waiters)) { + ready(); + } + }).fail([=] { + for (const auto &ready : base::take(_attributes[giftId].waiters)) { + ready(); + } + }).send(); +} + rpl::producer GiftAuctions::active() const { return _activeChanged.events_starting_with_copy( rpl::empty @@ -233,6 +277,7 @@ void GiftAuctions::requestActive() { result.match([=](const MTPDpayments_starGiftActiveAuctions &data) { const auto owner = &_session->data(); owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); auto giftsFound = base::flat_set(); const auto &list = data.vauctions().v; @@ -294,6 +339,7 @@ void GiftAuctions::request(const QString &slug) { const auto &data = result.data(); _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); raw->state.gift = Api::FromTL(_session, data.vgift()); if (!raw->state.gift) { @@ -367,6 +413,7 @@ void GiftAuctions::apply( entry->giftsLeft = data.vgifts_left().v; entry->currentRound = data.vcurrent_round().v; entry->totalRounds = data.vtotal_rounds().v; + data.vrounds().v; entry->averagePrice = 0; }, [&](const MTPDstarGiftAuctionStateFinished &data) { entry->averagePrice = data.vaverage_price().v; diff --git a/Telegram/SourceFiles/data/components/gift_auctions.h b/Telegram/SourceFiles/data/components/gift_auctions.h index ba8012707d..881e3c5bff 100644 --- a/Telegram/SourceFiles/data/components/gift_auctions.h +++ b/Telegram/SourceFiles/data/components/gift_auctions.h @@ -42,6 +42,8 @@ struct GiftAuctionState { TimeId startDate = 0; TimeId endDate = 0; TimeId nextRoundAt = 0; + TimeId roundDurationFirst = 0; + TimeId roundDurationRest = 0; int currentRound = 0; int totalRounds = 0; int giftsLeft = 0; @@ -58,6 +60,7 @@ struct GiftAcquired { TimeId date = 0; int64 bidAmount = 0; int round = 0; + int number = 0; int position = 0; bool nameHidden = false; }; @@ -80,6 +83,10 @@ public: uint64 giftId, Fn)> done); + [[nodiscard]] std::optional attributes( + uint64 giftId) const; + void requestAttributes(uint64 giftId, Fn ready); + [[nodiscard]] rpl::producer active() const; [[nodiscard]] rpl::producer hasActiveChanges() const; [[nodiscard]] bool hasActive() const; @@ -100,6 +107,10 @@ private: } friend inline bool operator==(MyStateKey, MyStateKey) = default; }; + struct Attributes { + Data::UniqueGiftAttributes lists; + std::vector> waiters; + }; void request(const QString &slug); Entry *find(uint64 giftId) const; @@ -126,6 +137,7 @@ private: base::Timer _timer; base::flat_map> _map; + base::flat_map _attributes; rpl::event_stream<> _activeChanged; mtpRequestId _activeRequestId = 0; diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 78f1357b51..fcc046b016 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -162,6 +162,7 @@ struct GiftCode { int starsUpgradedBySender = 0; int starsForDetailsRemove = 0; int starsBid = 0; + int giftNum = 0; int limitedCount = 0; int limitedLeft = 0; int64 count = 0; diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index c9d78bb16a..643b2eba4e 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -38,6 +38,12 @@ struct UniqueGiftBackdrop : UniqueGiftAttribute { int id = 0; }; +struct UniqueGiftAttributes { + std::vector models; + std::vector backdrops; + std::vector patterns; +}; + struct UniqueGiftOriginalDetails { PeerId senderId = 0; PeerId recipientId = 0; @@ -48,6 +54,7 @@ struct UniqueGiftOriginalDetails { struct UniqueGiftValue { QString currency; int64 valuePrice = 0; + int64 valuePriceUsd = 0; CreditsAmount initialPriceStars; int64 initialSalePrice = 0; TimeId initialSaleDate = 0; @@ -130,10 +137,12 @@ struct StarGift { int resellCount = 0; QString auctionSlug; int auctionGiftsPerRound = 0; + TimeId auctionStartDate = 0; int limitedLeft = 0; int limitedCount = 0; int perUserTotal = 0; int perUserRemains = 0; + int upgradeVariants = 0; TimeId firstSaleDate = 0; TimeId lastSaleDate = 0; TimeId lockedUntilDate = 0; @@ -214,6 +223,7 @@ struct SavedStarGift { QString giftPrepayUpgradeHash; PeerId fromId = 0; TimeId date = 0; + int giftNum = 0; bool upgradeSeparate = false; bool upgradable = false; bool anonymous = false; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 6d9f56d2ce..465388f1c6 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -6807,6 +6807,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { .starsUpgradedBySender = int( data.vupgrade_stars().value_or_empty()), .starsBid = bid, + .giftNum = data.vgift_num().value_or_empty(), .type = Data::GiftType::StarGift, .upgradeSeparate = data.is_upgrade_separate(), .upgradeGifted = data.is_prepaid_upgrade(), diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index 27c7753284..2e5f849143 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -238,7 +238,8 @@ constexpr auto kSponsoredUserpicLines = 2; : (type == WebPageType::GiftCollection) ? tr::lng_view_button_collection(tr::now) : (type == WebPageType::Auction) - ? (page->auction && page->auction->endDate + ? (page->auction + && page->auction->endDate && page->auction->endDate <= base::unixtime::now()) ? tr::lng_auction_preview_view_results(tr::now) : tr::lng_auction_preview_join(tr::now) 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 cc948c9e4e..aa5f13f265 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -205,6 +205,11 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { : Lang::FormatCountDecimal(number); }; + const auto auctionStartDate = v::is(descriptor) + ? v::get(descriptor).info.auctionStartDate + : TimeId(); + const auto upcomingAuction = (auctionStartDate > base::unixtime::now()); + _descriptor = descriptor; _resalePrice = resalePrice; const auto resale = (_resalePrice > 0); @@ -276,7 +281,7 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { : unique ? tr::lng_gift_transfer_button(tr::now, tr::marked) : data.info.auction() - ? (data.info.soldOut + ? ((data.info.soldOut || upcomingAuction) ? tr::lng_gift_stars_auction_view : tr::lng_gift_stars_auction_join)(tr::now, tr::marked) : _delegate->star().append(' ' + format(data.info.stars))), @@ -789,6 +794,9 @@ void GiftButton::paintEvent(QPaintEvent *e) { }, [&](const GiftTypeStars &data) { const auto count = data.info.limitedCount; const auto pinned = data.pinned || data.pinnedSelection; + const auto now = base::unixtime::now(); + const auto upcomingAuction = (data.info.auctionStartDate > 0) + && (data.info.auctionStartDate > now); if (count || pinned) { const auto yourLeft = data.info.perUserTotal ? (data.info.perUserRemains @@ -808,7 +816,9 @@ void GiftButton::paintEvent(QPaintEvent *e) { : soldOut ? tr::lng_gift_stars_sold_out(tr::now) : (!unique && data.info.auction()) - ? tr::lng_gift_stars_auction(tr::now) + ? (upcomingAuction + ? tr::lng_gift_stars_auction_soon + : tr::lng_gift_stars_auction)(tr::now) : (!data.userpic && !data.info.unique && data.info.requirePremium)