Start auction preview display.

This commit is contained in:
John Preston
2025-12-02 12:23:49 +04:00
parent 0cc21e5ca2
commit b08cf75f0b
12 changed files with 595 additions and 216 deletions

View File

@@ -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.";

View File

@@ -874,10 +874,12 @@ std::optional<Data::StarGift> 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<Data::StarGift> 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<Data::SavedStarGift> 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(),

View File

@@ -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<void()> MakeAuctionMenuCallback(
not_null<QWidget*> parent,
Fn<void(not_null<Ui::PopupMenu*>)> MakeAuctionFillMenuCallback(
std::shared_ptr<ChatHelpers::Show> 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<base::unique_qptr<PopupMenu>>();
return [=] {
*menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
(*menu)->addAction(tr::lng_auction_menu_about(tr::now), [=] {
return [=](not_null<Ui::PopupMenu*> 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<void()> MakeAuctionMenuCallback(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
const Data::GiftAuctionState &state) {
const auto menu = std::make_shared<base::unique_qptr<PopupMenu>>();
return [=, fill = MakeAuctionFillMenuCallback(show, state)] {
*menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
fill(menu->get());
(*menu)->popup(QCursor::pos());
};
}
@@ -906,6 +915,30 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
helper.context());
}
[[nodiscard]] std::vector<int> RandomIndicesSubset(int total, int subset) {
const auto take = std::min(total, subset);
if (!take) {
return {};
}
auto result = std::vector<int>();
auto taken = base::flat_set<int>();
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<TableLayout> AuctionInfoTable(
not_null<QWidget*> parent,
not_null<VerticalLayout*> container,
@@ -921,6 +954,7 @@ void AuctionBidBox(not_null<GenericBox*> 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<GenericBox*> 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<GenericBox*> 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>(
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<FlatLabel>(
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>(
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<FlatLabel>(
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<Data::UniqueGift> MakePreviewAuctionStream(
const Data::StarGift &info,
rpl::producer<Data::UniqueGiftAttributes> 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<Data::UniqueGift> {
if (values.backdrops.empty()
|| values.models.empty()
|| values.patterns.empty()) {
return rpl::never<Data::UniqueGift>();
}
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
struct State {
Data::UniqueGiftAttributes data;
std::vector<int> modelIndices;
std::vector<int> patternIndices;
std::vector<int> backdropIndices;
};
const auto state = lifetime.make_state<State>(State{
.data = values,
});
const auto put = [=] {
const auto index = [](
std::vector<int> &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<GenericBox*> box,
not_null<Window::SessionController*> window,
@@ -1101,105 +1263,160 @@ void AuctionInfoBox(
struct State {
explicit State(not_null<Main::Session*> session)
: delegate(session, GiftButtonMode::Minimal) {
: delegate(session, GiftButtonMode::Minimal) {
}
Delegate delegate;
rpl::variable<Data::GiftAuctionState> value;
rpl::variable<int> minutesLeft;
rpl::variable<int> minutesTillEnd;
rpl::variable<int> secondsTillStart;
rpl::variable<Data::UniqueGiftAttributes> attributes;
std::vector<Data::GiftAcquired> acquired;
bool acquiredRequested = false;
base::unique_qptr<PopupMenu> menu;
rpl::lifetime previewLifetime;
bool previewRequested = false;
};
const auto show = window->uiShow();
const auto state = box->lifetime().make_state<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<FixedHeightWidget>(box, size.height()),
st::auctionInfoPreviewMargin);
const auto gift = CreateChild<GiftButton>(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<FixedHeightWidget>(box, size.height()),
st::auctionInfoPreviewMargin);
const auto gift = CreateChild<GiftButton>(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<FlatLabel>(
box,
name,
st::uniqueGiftTitle),
style::al_top);
const auto about = box->addRow(
object_ptr<FlatLabel>(
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<FlatLabel>(
box,
name,
st::uniqueGiftTitle),
style::al_top);
const auto about = box->addRow(
object_ptr<FlatLabel>(
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<IconButton>(
box->verticalLayout(),
st::boxTitleClose);
close->setClickedCallback([=] { box->closeBox(); });
const auto menu = CreateChild<IconButton>(
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<QColor>());
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<FlatLabel>(
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<Data::GiftAcquired> 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<FlatLabel>(
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<IconButton>(
box->verticalLayout(),
st::boxTitleClose);
close->setClickedCallback([=] { box->closeBox(); });
const auto menu = CreateChild<IconButton>(
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<QColor>());
if (const auto strong = finished ? weakMenu.get() : nullptr) {
delete strong;
}
}, box->lifetime());
}
base::weak_qptr<BoxContent> ChooseAndShowAuctionBox(
@@ -1308,16 +1536,21 @@ base::weak_qptr<BoxContent> ChooseAndShowAuctionBox(
std::shared_ptr<rpl::variable<Data::GiftAuctionState>> state,
Fn<void()> 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 &current = 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<bool>(kAuctionAboutShownPref) || finished);
&& (!started
|| finished
|| local->readPref<bool>(kAuctionAboutShownPref));
auto box = base::weak_qptr<BoxContent>();
if (showBidBox) {
box = window->show(MakeAuctionBidBox({
@@ -1333,13 +1566,13 @@ base::weak_qptr<BoxContent> 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<BoxContent> 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<Data::GiftAuctionState> value) {
struct State {
rpl::variable<Data::GiftAuctionState> value;
rpl::variable<int> minutesLeft;
rpl::variable<int> minutesTillEnd;
rpl::variable<int> secondsTillStart;
};
const auto state = button->lifetime().make_state<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<TextWithEntities> {
if (state.finished() || minutes <= 0) {
const Data::GiftAuctionState &state,
int leftTill
) -> rpl::producer<TextWithEntities> {
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)));

View File

@@ -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<RoundButton*> button,

View File

@@ -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{

View File

@@ -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<Data::UniqueGiftAttributes> GiftAuctions::attributes(
uint64 giftId) const {
const auto i = _attributes.find(giftId);
return (i != end(_attributes) && i->second.waiters.empty())
? i->second.lists
: std::optional<Data::UniqueGiftAttributes>();
}
void GiftAuctions::requestAttributes(uint64 giftId, Fn<void()> 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<ActiveAuctions> 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<QString>();
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;

View File

@@ -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<void(std::vector<Data::GiftAcquired>)> done);
[[nodiscard]] std::optional<Data::UniqueGiftAttributes> attributes(
uint64 giftId) const;
void requestAttributes(uint64 giftId, Fn<void()> ready);
[[nodiscard]] rpl::producer<ActiveAuctions> active() const;
[[nodiscard]] rpl::producer<bool> 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<Fn<void()>> waiters;
};
void request(const QString &slug);
Entry *find(uint64 giftId) const;
@@ -126,6 +137,7 @@ private:
base::Timer _timer;
base::flat_map<QString, std::unique_ptr<Entry>> _map;
base::flat_map<uint64, Attributes> _attributes;
rpl::event_stream<> _activeChanged;
mtpRequestId _activeRequestId = 0;

View File

@@ -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;

View File

@@ -38,6 +38,12 @@ struct UniqueGiftBackdrop : UniqueGiftAttribute {
int id = 0;
};
struct UniqueGiftAttributes {
std::vector<UniqueGiftModel> models;
std::vector<UniqueGiftBackdrop> backdrops;
std::vector<UniqueGiftPattern> 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;

View File

@@ -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(),

View File

@@ -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)

View File

@@ -205,6 +205,11 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {
: Lang::FormatCountDecimal(number);
};
const auto auctionStartDate = v::is<GiftTypeStars>(descriptor)
? v::get<GiftTypeStars>(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)