From 4a5cdbcfb2c844272cb55f8fe75ed576e7c8c7fe Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 14 Nov 2025 13:51:56 +0400 Subject: [PATCH] Nice bidding information display. --- Telegram/Resources/langs/lang.strings | 2 +- .../boxes/star_gift_auction_box.cpp | 553 +++++++++++--- .../group/ui/calls_group_stars_coloring.cpp | 119 +--- .../payments/ui/payments_reaction_box.cpp | 672 ++++++++++-------- .../payments/ui/payments_reaction_box.h | 42 +- Telegram/SourceFiles/ui/effects/credits.style | 17 + Telegram/SourceFiles/ui/effects/premium.style | 18 +- Telegram/lib_rpl | 2 +- 8 files changed, 963 insertions(+), 462 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2d8e25311b..cfdc7e4070 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4036,7 +4036,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_auction_bid_your_title" = "Your bid will be"; "lng_auction_bid_your_outbid" = "You've been outbid"; "lng_auction_bid_your_winning" = "You're winning"; -"lng_auction_bid_winners_title" = "Top 3 Winners"; +"lng_auction_bid_winners_title" = "Top winners"; "lng_auction_bid_place" = "Place a {stars} Bid"; "lng_auction_bid_increase" = "Add {stars} to Your Bid"; "lng_auction_bid_placed_title" = "Your bid has been placed"; diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp index 23a70db3d8..ba85e05772 100644 --- a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp @@ -24,17 +24,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "payments/ui/payments_reaction_box.h" #include "payments/payments_checkout_process.h" +#include "settings/settings_credits_graphics.h" #include "storage/storage_account.h" #include "ui/controls/button_labels.h" #include "ui/controls/feature_list.h" #include "ui/controls/table_rows.h" +#include "ui/controls/userpic_button.h" +#include "ui/effects/premium_bubble.h" #include "ui/layers/generic_box.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/wrap/table_layout.h" +#include "ui/color_int_conversion.h" #include "ui/dynamic_thumbnails.h" +#include "ui/vertical_list.h" #include "window/window_session_controller.h" #include "styles/style_calls.h" #include "styles/style_chat.h" @@ -42,13 +47,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" +#include "styles/style_premium.h" namespace Ui { namespace { constexpr auto kAuctionAboutShownPref = "gift_auction_about_shown"_cs; constexpr auto kBidPlacedToastDuration = 5 * crl::time(1000); -constexpr auto kMaxShownBid = 30'000; +constexpr auto kMaxShownBid = 30'000'00; AssertIsDebug() + +enum class BidType { + Setting, + Winning, + Loosing, +}; +struct BidRowData { + UserData *user = nullptr; + int stars = 0; + QString place; + BidType type = BidType::Setting; + + friend inline bool operator==( + const BidRowData &, + const BidRowData &) = default; +}; + +[[nodiscard]] std::optional BidColorOverride(BidType type) { + switch (type) { + case BidType::Setting: return {}; + case BidType::Winning: return st::boxTextFgGood->c; + case BidType::Loosing: return st::attentionButtonFg->c; + } + Unexpected("Type in BidType."); +} [[nodiscard]] rpl::producer MinutesLeftTillValue(TimeId endDate) { return [=](auto consumer) { @@ -82,6 +113,131 @@ constexpr auto kMaxShownBid = 30'000; }; } +[[nodiscard]] rpl::producer SecondsLeftTillValue(TimeId endDate) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + const auto now = base::unixtime::now(); + const auto left = (endDate > now) ? endDate - now : 0; + if (!left) { + consumer.put_next(0); + consumer.put_done(); + return lifetime; + } + + const auto starts = crl::now(); + const auto ends = starts + left * crl::time(1000); + const auto timer = lifetime.make_state(); + const auto callback = [=] { + const auto now = crl::now(); + const auto left = (ends > now) ? ends - now : 0; + const auto seconds = (left + 999) / 1000; + consumer.put_next_copy(seconds); + if (seconds) { + const auto next = left % 1000; + const auto wait = next ? next : 1000; + timer->callOnce(wait); + } else { + consumer.put_done(); + } + }; + timer->setCallback(callback); + callback(); + + return lifetime; + }; +} + +[[nodiscard]] object_ptr MakeBidRow( + not_null parent, + std::shared_ptr show, + rpl::producer data) { + auto result = object_ptr(parent.get()); + const auto raw = result.data(); + + struct State { + std::unique_ptr place; + std::unique_ptr userpic; + std::unique_ptr name; + std::unique_ptr stars; + UserData *user = nullptr; + }; + const auto state = raw->lifetime().make_state(); + state->place = std::make_unique( + raw, + rpl::duplicate(data) | rpl::map(&BidRowData::place), + st::auctionBidPlace); + + auto name = rpl::duplicate(data) | rpl::map([](const BidRowData &bid) { + return bid.user ? bid.user->name() : QString(); + }); + state->name = std::make_unique( + raw, + std::move(name), + st::auctionBidName); + + auto helper = Text::CustomEmojiHelper(Core::TextContext({ + .session = &show->session(), + })); + const auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji()); + auto stars = rpl::duplicate(data) | rpl::map([=](const BidRowData &bid) { + return TextWithEntities{ star }.append(' ').append( + Lang::FormatCountDecimal(bid.stars)); + }); + state->stars = std::make_unique( + raw, + std::move(stars), + st::auctionBidStars, + st::defaultPopupMenu, + helper.context()); + + const auto kHuge = u"99999"_q; + const auto userpicLeft = st::auctionBidPlace.style.font->width(kHuge); + + std::move(data) | rpl::start_with_next([=](BidRowData bid) { + state->place->setTextColorOverride(BidColorOverride(bid.type)); + if (state->user != bid.user) { + state->user = bid.user; + if (state->user) { + if (auto was = state->userpic.release()) { + was->hide(); + crl::on_main(was, [=] { delete was; }); + } + state->userpic = std::make_unique( + raw, + state->user, + st::auctionBidUserpic); + state->userpic->show(); + state->userpic->moveToLeft(userpicLeft, 0); + raw->resize(raw->width(), state->userpic->height()); + } else { + raw->resize(raw->width(), 0); + } + } + }, raw->lifetime()); + + rpl::combine( + raw->widthValue(), + state->stars->widthValue() + ) | rpl::start_with_next([=](int outer, int stars) { + const auto userpicHeight = st::auctionBidUserpic.size.height(); + const auto top = (userpicHeight - st::normalFont->height) / 2; + state->place->moveToLeft(0, top, outer); + if (state->userpic) { + state->userpic->moveToLeft(userpicLeft, 0, outer); + } + state->stars->moveToRight(0, top, outer); + + const auto userpicRight = userpicLeft + state->userpic->width(); + const auto nameLeft = userpicRight + st::auctionBidSkip; + const auto nameRight = stars + st::auctionBidSkip; + state->name->resizeToWidth(outer - nameLeft - nameRight); + state->name->moveToLeft(nameLeft, top, outer); + }, raw->lifetime()); + + return result; +} + struct AuctionBidBoxArgs { not_null peer; std::shared_ptr show; @@ -124,85 +280,318 @@ void PlaceAuctionBid( }); } +object_ptr MakeAuctionInfoBlocks( + not_null box, + not_null session, + rpl::producer stateValue) { + auto helper = Text::CustomEmojiHelper(Core::TextContext({ + .session = session, + })); + const auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji()); + + auto bidTitle = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return TextWithEntities{ + star + }.append(' ').append(Lang::FormatCountDecimal(state.minBidAmount)); + }); + auto minimal = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return state.minBidAmount; + }) | tr::to_count(); + auto untilTitle = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return SecondsLeftTillValue(state.nextRoundAt); + }) | rpl::flatten_latest() | rpl::map([=](int seconds) { + const auto minutes = seconds / 60; + const auto hours = minutes / 60; + return hours + ? u"%1:%2:%3"_q + .arg(hours) + .arg((minutes % 60), 2, 10, QChar('0')) + .arg((seconds % 60), 2, 10, QChar('0')) + : u"%1:%2"_q.arg(minutes).arg((seconds % 60), 2, 10, QChar('0')); + }) | Text::ToWithEntities(); + auto leftTitle = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return Data::SingleCustomEmoji( + state.gift->document + ).append(' ').append(Lang::FormatCountDecimal(state.giftsLeft)); + }); + auto left = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return state.giftsLeft; + }) | tr::to_count(); + return MakeStarSelectInfoBlocks(box, { + { + .title = std::move(bidTitle), + .subtext = tr::lng_auction_bid_minimal( + lt_count, + std::move(minimal)), + }, + { + .title = std::move(untilTitle), + .subtext = tr::lng_auction_bid_until(), + }, + { + .title = std::move(leftTitle), + .subtext = tr::lng_auction_bid_left(lt_count, std::move(left)) + }, + }, helper.context()); +} + +void AddBidPlaces( + not_null box, + std::shared_ptr show, + rpl::producer value, + rpl::producer chosen) { + struct My { + BidType type; + int position = 0; + + inline bool operator==(const My &) const = default; + }; + struct State { + rpl::variable my; + rpl::variable> top; + std::vector cache; + }; + const auto state = box->lifetime().make_state(); + + rpl::duplicate( + value + ) | rpl::start_with_next([=](const Data::GiftAuctionState &value) { + auto cache = std::vector(); + cache.reserve(value.topBidders.size()); + for (const auto &user : value.topBidders) { + cache.push_back(user->createUserpicView()); + } + state->cache = std::move(cache); + }, box->lifetime()); + + state->my = rpl::combine( + rpl::duplicate(value), + rpl::duplicate(chosen) + ) | rpl::map([=](const Data::GiftAuctionState &value, int chosen) { + const auto my = value.my.bid; + const auto &levels = value.bidLevels; + + auto top = std::vector(); + top.reserve(3); + const auto pushTop = [&](auto i) { + const auto index = int(i - begin(levels)); + if (top.size() < 3 && index < value.topBidders.size()) { + top.push_back({ value.topBidders[index], int(i->amount) }); + return true; + } + return false; + }; + + const auto setting = (chosen > my); + const auto finishWith = [&](int position) { + state->top = std::move(top); + const auto type = setting + ? BidType::Setting + : (position <= value.gift->auctionGiftsPerRound) + ? BidType::Winning + : BidType::Loosing; + return My{ type, position }; + }; + + for (auto i = begin(levels), e = end(levels); i != e; ++i) { + if (i->amount < chosen + || (!setting + && i->amount == chosen + && i->date > value.my.date)) { + top.push_back({ show->session().user(), chosen }); + for (auto j = i; j != e; ++j) { + if (!pushTop(j)) { + break; + } + } + return finishWith(i->position); + } + pushTop(i); + } + return finishWith((levels.empty() ? 0 : levels.back().position) + 1); + }); + auto myLabelText = state->my.value() | rpl::map([](My my) { + switch (my.type) { + case BidType::Setting: return tr::lng_auction_bid_your_title(); + case BidType::Winning: return tr::lng_auction_bid_your_winning(); + case BidType::Loosing: return tr::lng_auction_bid_your_outbid(); + } + Unexpected("Type in BidType."); + }) | rpl::flatten_latest(); + const auto myLabel = AddSubsectionTitle( + box->verticalLayout(), + std::move(myLabelText)); + state->my.value() | rpl::start_with_next([=](My my) { + myLabel->setTextColorOverride(BidColorOverride(my.type)); + }, myLabel->lifetime()); + + auto bid = rpl::combine( + state->my.value(), + rpl::duplicate(chosen) + ) | rpl::map([=, user = show->session().user()](My my, int stars) { + const auto place = QString::number(my.position); + return BidRowData{ user, stars, place, my.type }; + }); + box->addRow(MakeBidRow(box, show, std::move(bid))); + + AddSubsectionTitle( + box->verticalLayout(), + tr::lng_auction_bid_winners_title(), + { 0, st::paidReactTitleSkip / 2, 0, 0 }); + for (auto i = 0; i != 3; ++i) { + auto icon = QString::fromUtf8("\xf0\x9f\xa5\x87"); + icon.back().unicode() += i; + + auto bid = state->top.value( + ) | rpl::map([=](const std::vector &top) { + auto result = (i < top.size()) ? top[i] : BidRowData(); + result.place = icon; + return result; + }); + box->addRow(MakeBidRow(box, show, std::move(bid))); + } +} + void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { const auto weak = base::make_weak(box); - const auto state = box->lifetime().make_state< - rpl::variable - >(std::move(args.state)); - auto submit = [=](rpl::producer amount) { - return rpl::combine( - state->value(), - std::move(amount) - ) | rpl::map([=](const Data::GiftAuctionState &state, int count) { - return !state.my.bid - ? tr::lng_auction_bid_place( - lt_stars, - rpl::single(Ui::CreditsEmojiSmall().append( - Lang::FormatCountDecimal(count))), - tr::marked) - : (count <= state.my.bid) - ? tr::lng_box_ok(tr::marked) - : tr::lng_auction_bid_increase( - lt_stars, - rpl::single(Ui::CreditsEmojiSmall().append( - Lang::FormatCountDecimal(count - state.my.bid))), - tr::marked); - }) | rpl::flatten_latest(); + + struct State { + State(rpl::producer value) + : value(std::move(value)) { + } + + rpl::variable value; + rpl::variable chosen; }; + const auto state = box->lifetime().make_state( + std::move(args.state)); + const auto &now = state->value.current(); + const auto mine = int(now.my.bid); + const auto min = std::max( + int(mine ? now.my.minBidAmount : now.minBidAmount), + 1); + const auto max = std::max({ + min + 1, + kMaxShownBid, + mine, + }); + const auto chosen = mine ? mine : std::clamp(mine, min, max); + state->chosen = chosen; + const auto show = args.show; - const auto session = &show->session(); - auto top = std::vector{ - PaidReactionTop{ - .name = session->user()->shortName(), - .photo = MakeUserpicThumbnail(session->user()), - .barePeerId = uint64(session->userPeerId().value), - .count = int(state->current().my.bid), - .my = true, + const auto save = [=, peer = args.peer](int amount) { + const auto ¤t = state->value.current(); + if (amount > current.my.bid) { + const auto was = (current.my.bid > 0); + const auto perRound = current.gift->auctionGiftsPerRound; + const auto done = [=](Payments::CheckoutResult result) { + if (result == Payments::CheckoutResult::Paid) { + show->showToast({ + .title = (was + ? tr::lng_auction_bid_increased_title + : tr::lng_auction_bid_placed_title)( + tr::now), + .text = tr::lng_auction_bid_done_text( + tr::now, + lt_count, + perRound, + tr::rich), + .duration = kBidPlacedToastDuration, + }); + } + }; + PlaceAuctionBid(show, peer, amount, now, done); + } + if (const auto strong = weak.get()) { + strong->closeBox(); } }; - const auto save = [=, peer = args.peer](int amount) { - const auto &now = state->current(); - const auto was = (now.my.bid > 0); - const auto perRound = now.gift->auctionGiftsPerRound; - const auto done = [=](Payments::CheckoutResult result) { - if (result == Payments::CheckoutResult::Paid) { - show->showToast({ - .title = (was - ? tr::lng_auction_bid_increased_title - : tr::lng_auction_bid_placed_title)( - tr::now), - .text = tr::lng_auction_bid_done_text( - tr::now, - lt_count, - perRound, - tr::rich), - .duration = kBidPlacedToastDuration, - }); - } - }; - PlaceAuctionBid(show, peer, amount, now, done); + const auto colorings = show->session().appConfig().groupCallColorings(); + + box->setWidth(st::boxWideWidth); + box->setStyle(st::paidReactBox); + box->setNoContentMargin(true); + + const auto content = box->verticalLayout(); + AddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop); + + const auto activeFgOverride = [=](int count) { + const auto coloring = Calls::Group::Ui::StarsColoringForCount( + colorings, + count); + return ColorFromSerialized(coloring.bgLight); }; - const auto mine = state->current().my.bid; - PaidReactionsBox(box, { - .min = int(mine - ? state->current().my.minBidAmount - : state->current().minBidAmount), - .explicitlyAllowed = int(mine), - .chosen = int(state->current().my.bid), - .max = kMaxShownBid, - .top = std::move(top), - .session = session, - .submit = submit, - .colorings = session->appConfig().groupCallColorings(), - .balanceValue = session->credits().balanceValue(), - .send = [=](int count, uint64 barePeerId) { - save(count); - if (const auto strong = weak.get()) { - strong->closeBox(); - } - }, - .giftAuction = true, + AddStarSelectBubble(box, state->chosen.value(), max, activeFgOverride); + + PaidReactionSlider( + content, + st::paidReactSlider, + min, + mine, + chosen, + max, + [=](int count) { state->chosen = count; }, + activeFgOverride); + + box->addTopButton( + st::boxTitleClose, + [=] { box->closeBox(); }); + + const auto skip = st::paidReactTitleSkip; + box->addRow( + object_ptr( + box, + tr::lng_auction_bid_title(), + st::boostCenteredTitle), + st::boxRowPadding + QMargins(0, skip, 0, 0), + style::al_top); + + box->addRow( + MakeAuctionInfoBlocks(box, &show->session(), state->value.value()), + st::boxRowPadding + QMargins(0, skip / 2, 0, skip)); + + AddBidPlaces(box, show, state->value.value(), state->chosen.value()); + + AddSkip(content); + AddSkip(content); + + const auto button = box->addButton(rpl::single(QString()), [=] { + save(state->chosen.current()); }); + + button->setText(rpl::combine( + state->value.value(), + state->chosen.value() + ) | rpl::map([=](const Data::GiftAuctionState &state, int count) { + return !state.my.bid + ? tr::lng_auction_bid_place( + lt_stars, + rpl::single(CreditsEmojiSmall().append( + Lang::FormatCountDecimal(count))), + tr::marked) + : (count <= state.my.bid) + ? tr::lng_box_ok(tr::marked) + : tr::lng_auction_bid_increase( + lt_stars, + rpl::single(CreditsEmojiSmall().append( + Lang::FormatCountDecimal(count - state.my.bid))), + tr::marked); + }) | rpl::flatten_latest()); + + AddStarSelectBalance( + box, + &show->session(), + show->session().credits().balanceValue()); } [[nodiscard]] object_ptr MakeAuctionBidBox( @@ -215,8 +604,8 @@ void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { std::shared_ptr tooltip, const QString &name, int64 amount) { - auto helper = Ui::Text::CustomEmojiHelper(); - const auto price = helper.paletteDependent(Ui::Earn::IconCreditsEmoji( + auto helper = Text::CustomEmojiHelper(); + const auto price = helper.paletteDependent(Earn::IconCreditsEmoji( )).append(' ').append( Lang::FormatCreditsAmountDecimal(CreditsAmount{ amount })); return MakeTableValueWithTooltip( @@ -227,7 +616,7 @@ void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { tr::now, lt_amount, tr::bold( - Ui::Text::IconEmoji(&st::starIconEmojiInline).append( + Text::IconEmoji(&st::starIconEmojiInline).append( Lang::FormatCountDecimal(amount))), lt_gift, tr::bold(name), @@ -237,7 +626,7 @@ void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { [[nodiscard]] object_ptr AuctionInfoTable( not_null parent, - not_null container, + not_null container, rpl::producer value) { auto result = object_ptr(parent.get(), st::defaultTable); const auto raw = result.data(); @@ -354,14 +743,14 @@ void AuctionAboutBox( st::boxRowPadding + st::confcallLinkHeaderIconPadding); box->addRow( - object_ptr( + object_ptr( box, tr::lng_auction_about_title(), st::boxTitle), st::boxRowPadding + st::confcallLinkTitlePadding, style::al_top); box->addRow( - object_ptr( + object_ptr( box, tr::lng_auction_about_subtitle(tr::rich), st::confcallLinkCenteredText), @@ -410,7 +799,7 @@ void AuctionAboutBox( }, }; for (const auto &feature : features) { - box->addRow(Ui::MakeFeatureListEntry(box, feature)); + box->addRow(MakeFeatureListEntry(box, feature)); } const auto close = Fn([weak = base::make_weak(box)] { @@ -421,7 +810,7 @@ void AuctionAboutBox( box->addButton( rpl::single(QString()), understood ? [=] { understood(close); } : close - )->setText(rpl::single(Ui::Text::IconEmoji( + )->setText(rpl::single(Text::IconEmoji( &st::infoStarsUnderstood ).append(' ').append(tr::lng_auction_about_understood(tr::now)))); } @@ -525,7 +914,7 @@ void AuctionInfoBox( rpl::mappers::_1 > 0 ) | rpl::take(1) | rpl::start_with_next([=](int count) { box->addRow( - object_ptr( + object_ptr( box, tr::lng_auction_bought( lt_count_decimal, @@ -534,7 +923,7 @@ void AuctionInfoBox( rpl::single(Data::SingleCustomEmoji( state->value.current().gift->document)), lt_arrow, - rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + rpl::single(Text::IconEmoji(&st::textMoreIconEmoji)), tr::link), st::uniqueGiftValueAvailableLink, st::defaultPopupMenu, diff --git a/Telegram/SourceFiles/calls/group/ui/calls_group_stars_coloring.cpp b/Telegram/SourceFiles/calls/group/ui/calls_group_stars_coloring.cpp index 15d6e29e06..948c90ef33 100644 --- a/Telegram/SourceFiles/calls/group/ui/calls_group_stars_coloring.cpp +++ b/Telegram/SourceFiles/calls/group/ui/calls_group_stars_coloring.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" #include "lang/lang_keys.h" +#include "payments/ui/payments_reaction_box.h" #include "ui/widgets/labels.h" #include "ui/emoji_config.h" #include "ui/painter.h" @@ -18,57 +19,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_premium.h" namespace Calls::Group::Ui { -namespace { - -[[nodiscard]] not_null MakeInfoBlock( - not_null parent, - rpl::producer title, - rpl::producer subtext) { - const auto result = CreateChild(parent); - - const auto titleHeight = st::videoStreamInfoTitle.style.font->height; - const auto subtextHeight = st::videoStreamInfoSubtext.style.font->height; - const auto height = titleHeight + subtextHeight; - - result->paintRequest() | rpl::start_with_next([=] { - auto p = QPainter(result); - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - p.setBrush(st::groupCallMembersBgOver); - const auto radius = st::boxRadius; - p.drawRoundedRect(result->rect(), radius, radius); - }, result->lifetime()); - - result->resize( - result ->width(), - QSize(height, height).grownBy(st::videoStreamInfoPadding).height()); - - const auto titleLabel = CreateChild( - result, - std::move(title), - st::videoStreamInfoTitle); - const auto subtextLabel = CreateChild( - result, - std::move(subtext), - st::videoStreamInfoSubtext); - - rpl::combine( - result->widthValue(), - titleLabel->widthValue(), - subtextLabel->widthValue() - ) | rpl::start_with_next([=](int width, int titlew, int subtextw) { - const auto padding = st::videoStreamInfoPadding; - titleLabel->moveToLeft((width - titlew) / 2, padding.top(), width); - subtextLabel->moveToLeft( - (width - subtextw) / 2, - padding.top() + titleHeight, - width); - }, result->lifetime()); - - return result; -} - -} // namespace StarsColoring StarsColoringForCount( const std::vector &colorings, @@ -92,7 +42,7 @@ int StarsRequiredForMessage( auto view = QStringView(text.text); const auto length = int(view.size()); while (!view.isEmpty()) { - if (Ui::Emoji::Find(view, &outLength)) { + if (Emoji::Find(view, &outLength)) { view = view.mid(outLength); ++emojis; } else { @@ -107,26 +57,22 @@ int StarsRequiredForMessage( return colorings.back().fromStars + 1; } -object_ptr VideoStreamStarsLevel( - not_null box, +object_ptr VideoStreamStarsLevel( + not_null box, const std::vector &colorings, rpl::producer starsValue) { - auto result = object_ptr(box.get()); - const auto raw = result.data(); - struct State { rpl::variable stars; rpl::variable coloring; - std::vector> blocks; }; - const auto state = raw->lifetime().make_state(); + const auto state = box->lifetime().make_state(); state->stars = std::move(starsValue); state->coloring = state->stars.value( ) | rpl::map([=](int stars) { return StarsColoringForCount(colorings, stars); }); - state->blocks.push_back(MakeInfoBlock(raw, state->coloring.value( + auto pinTitle = state->coloring.value( ) | rpl::map([=](const StarsColoring &value) { const auto seconds = value.secondsPin; return (seconds >= 3600) @@ -134,46 +80,43 @@ object_ptr VideoStreamStarsLevel( : (seconds >= 60) ? tr::lng_minutes_tiny(tr::now, lt_count, seconds / 60) : tr::lng_seconds_tiny(tr::now, lt_count, seconds); - }), tr::lng_paid_comment_pin_about())); - - state->blocks.push_back(MakeInfoBlock(raw, state->coloring.value( + }); + auto limitTitle = state->coloring.value( ) | rpl::map([=](const StarsColoring &value) { return QString::number(value.charactersMax); - }), state->coloring.value() | rpl::map([=](const StarsColoring &value) { + }); + auto limitSubtext = state->coloring.value( + ) | rpl::map([=](const StarsColoring &value) { return tr::lng_paid_comment_limit_about( tr::now, lt_count, value.charactersMax); - }))); - - state->blocks.push_back(MakeInfoBlock(raw, state->coloring.value( + }); + auto emojiTitle = state->coloring.value( ) | rpl::map([=](const StarsColoring &value) { return QString::number(value.emojiLimit); - }), state->coloring.value() | rpl::map([=](const StarsColoring &value) { + }); + auto emojiSubtext = state->coloring.value( + ) | rpl::map([=](const StarsColoring &value) { return tr::lng_paid_comment_emoji_about( tr::now, lt_count, value.emojiLimit); - }))); - - raw->resize(raw->width(), state->blocks.front()->height()); - raw->widthValue() | rpl::start_with_next([=](int width) { - const auto count = int(state->blocks.size()); - const auto skip = (st::boxRowPadding.left() / 2); - const auto single = (width - skip * (count - 1)) / float64(count); - if (single < 1.) { - return; - } - auto x = 0.; - const auto w = int(base::SafeRound(single)); - for (const auto &block : state->blocks) { - block->resizeToWidth(w); - block->moveToLeft(int(base::SafeRound(x)), 0); - x += single + skip; - } - }, raw->lifetime()); - - return result; + }); + return MakeStarSelectInfoBlocks(box, { + { + .title = std::move(pinTitle) | Text::ToWithEntities(), + .subtext = tr::lng_paid_comment_pin_about(), + }, + { + .title = std::move(limitTitle) | Text::ToWithEntities(), + .subtext = std::move(limitSubtext), + }, + { + .title = std::move(emojiTitle) | Text::ToWithEntities(), + .subtext = std::move(emojiSubtext), + }, + }, {}, true); } } // namespace Calls::Group::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index aef5d4dcb8..164f2eb132 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -65,187 +65,6 @@ struct TopReactorKey { const TopReactorKey &) = default; }; -struct Discreter { - Fn ratioToValue; - Fn valueToRatio; -}; - -[[nodiscard]] Discreter DiscreterForMax(int max) { - Expects(max >= 2); - - // 1/8 of width is 1..10 - // 1/3 of width is 1..100 - // 2/3 of width is 1..1000 - - auto thresholds = base::flat_map(); - thresholds.emplace(0., 1); - if (max <= 40) { - thresholds.emplace(1., max); - } else if (max <= 300) { - thresholds.emplace(1. / 4, 10); - thresholds.emplace(1., max); - } else if (max <= 600) { - thresholds.emplace(1. / 8, 10); - thresholds.emplace(1. / 2, 100); - thresholds.emplace(1., max); - } else if (max <= 1900) { - thresholds.emplace(1. / 8, 10); - thresholds.emplace(1. / 3, 100); - thresholds.emplace(1., max); - } else if (max <= 10000) { - thresholds.emplace(1. / 8, 10); - thresholds.emplace(1. / 3, 100); - thresholds.emplace(2. / 3, 1000); - thresholds.emplace(1., max); - } else { - thresholds.emplace(1. / 10, 10); - thresholds.emplace(1. / 6, 100); - thresholds.emplace(1. / 3, 1000); - thresholds.emplace(1., max); - } - - const auto ratioToValue = [=](float64 ratio) { - ratio = std::clamp(ratio, 0., 1.); - const auto j = thresholds.lower_bound(ratio); - if (j == begin(thresholds)) { - return 1; - } - const auto i = j - 1; - const auto progress = (ratio - i->first) / (j->first - i->first); - const auto value = i->second + (j->second - i->second) * progress; - return int(base::SafeRound(value)); - }; - const auto valueToRatio = [=](int value) { - value = std::clamp(value, 1, max); - auto i = begin(thresholds); - auto j = i + 1; - while (j->second < value) { - i = j++; - } - const auto progress = (value - i->second) - / float64(j->second - i->second); - return i->first + (j->first - i->first) * progress; - }; - return { - .ratioToValue = ratioToValue, - .valueToRatio = valueToRatio, - }; -} - -void PaidReactionSlider( - not_null container, - const style::MediaSlider &st, - int min, - int explicitlyAllowed, - int current, - int max, - Fn changed, - Fn activeFgOverride = nullptr) { - Expects(current >= 1 && current <= max); - Expects(explicitlyAllowed <= max); - - if (!explicitlyAllowed) { - explicitlyAllowed = min; - } - const auto slider = container->add( - object_ptr(container, st), - st::boxRowPadding + QMargins(0, st::paidReactSliderTop, 0, 0)); - slider->resize(slider->width(), st::paidReactSlider.seekSize.height()); - - const auto update = [=](int count) { - if (activeFgOverride) { - const auto color = activeFgOverride(count); - slider->setColorOverrides({ - .activeBg = color, - .activeBorder = color, - .seekFg = st::groupCallMembersFg->c, - .seekBorder = color, - .inactiveBorder = Qt::transparent, - }); - } - }; - - const auto discreter = DiscreterForMax(max); - slider->setAlwaysDisplayMarker(true); - slider->setDirection(ContinuousSlider::Direction::Horizontal); - slider->setValue(discreter.valueToRatio(current)); - const auto ratioToValue = [=](float64 ratio) { - const auto value = discreter.ratioToValue(ratio); - return (value <= explicitlyAllowed && explicitlyAllowed < min) - ? explicitlyAllowed - : std::max(value, min); - }; - slider->setAdjustCallback([=](float64 ratio) { - return discreter.valueToRatio(ratioToValue(ratio)); - }); - const auto callback = [=](float64 ratio) { - const auto value = ratioToValue(ratio); - update(value); - changed(value); - }; - slider->setChangeProgressCallback(callback); - slider->setChangeFinishedCallback(callback); - update(current); - - struct State { - StarParticles particles = StarParticles( - StarParticles::Type::Right, - 200, - st::lineWidth * 7); - Ui::Animations::Basic animation; - }; - const auto state = slider->lifetime().make_state(); - - const auto stars = Ui::CreateChild(slider->parentWidget()); - stars->show(); - stars->raise(); - slider->geometryValue() | rpl::start_with_next([=](QRect rect) { - stars->setGeometry(rect); - }, stars->lifetime()); - - state->animation.init([=] { stars->update(); }); - stars->setAttribute(Qt::WA_TransparentForMouseEvents); - - const auto seekSize = st::paidReactSlider.seekSize.width(); - const auto seekRadius = seekSize / 2.; - stars->paintRequest() | rpl::start_with_next([=] { - if (!state->animation.animating()) { - state->animation.start(); - } - auto p = QPainter(stars); - auto hq = PainterHighQualityEnabler(p); - const auto progress = slider->value(); - const auto rect = stars->rect(); - const auto availableWidth = rect.width() - seekSize; - const auto seekCenter = seekRadius + availableWidth * progress; - - state->particles.setSpeed(.1 + progress * .3); - state->particles.setVisible(.25 + .65 * progress); - - auto fullPath = QPainterPath(); - fullPath.addRoundedRect(QRectF(rect), seekRadius, seekRadius); - auto circlePath = QPainterPath(); - circlePath.addEllipse( - QPointF(seekCenter, rect.height() / 2.), - seekRadius, - seekRadius); - auto rightRect = QPainterPath(); - rightRect.addRect( - QRectF(seekCenter, 0, rect.width() - seekCenter, rect.height())); - - p.setClipPath(fullPath.subtracted(circlePath)); - state->particles.setColor(Qt::white); - state->particles.paint(p, rect, crl::now()); - p.setClipping(false); - - p.setClipPath(fullPath.intersected(circlePath.united(rightRect))); - state->particles.setColor(activeFgOverride - ? st::groupCallMemberInactiveIcon->c - : st::creditsBg3->c); - state->particles.paint(p, rect, crl::now()); - }, stars->lifetime()); -} - [[nodiscard]] QImage GenerateBadgeImage( const std::vector &colorings, int count, @@ -572,6 +391,58 @@ void FillTopReactors( }, wrap->lifetime()); } +[[nodiscard]] not_null MakeStarSelectInfoBlock( + not_null parent, + rpl::producer title, + rpl::producer subtext, + Text::MarkedContext context, + bool dark) { + const auto result = CreateChild(parent); + + const auto titleHeight = st::starSelectInfoTitle.style.font->height; + const auto subtextHeight = st::starSelectInfoSubtext.style.font->height; + const auto height = titleHeight + subtextHeight; + + result->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(result); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(dark ? st::groupCallMembersBgOver : st::windowBgOver); + const auto radius = st::boxRadius; + p.drawRoundedRect(result->rect(), radius, radius); + }, result->lifetime()); + + result->resize( + result->width(), + QSize(height, height).grownBy(st::starSelectInfoPadding).height()); + + const auto titleLabel = CreateChild( + result, + std::move(title), + dark ? st::videoStreamInfoTitle : st::starSelectInfoTitle, + st::defaultPopupMenu, + context); + const auto subtextLabel = CreateChild( + result, + std::move(subtext), + dark ? st::videoStreamInfoSubtext : st::starSelectInfoSubtext); + + rpl::combine( + result->widthValue(), + titleLabel->widthValue(), + subtextLabel->widthValue() + ) | rpl::start_with_next([=](int width, int titlew, int subtextw) { + const auto padding = st::starSelectInfoPadding; + titleLabel->moveToLeft((width - titlew) / 2, padding.top(), width); + subtextLabel->moveToLeft( + (width - subtextw) / 2, + padding.top() + titleHeight, + width); + }, result->lifetime()); + + return result; +} + } // namespace void PaidReactionsBox( @@ -612,7 +483,6 @@ void PaidReactionsBox( const auto colorings = args.colorings; const auto videoStreamChoosing = args.videoStreamChoosing; const auto videoStreamSending = args.videoStreamSending; - const auto giftAuction = args.giftAuction; const auto videoStream = videoStreamChoosing || videoStreamSending; const auto initialShownPeer = ranges::find( args.top, @@ -628,41 +498,17 @@ void PaidReactionsBox( const auto content = box->verticalLayout(); AddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop); - const auto valueToRatio = DiscreterForMax(args.max).valueToRatio; - auto bubbleRowState = state->chosen.value() | rpl::map([=](int value) { - const auto full = st::boxWideWidth - - st::boxRowPadding.left() - - st::boxRowPadding.right(); - const auto marker = st::paidReactSlider.seekSize.width(); - const auto start = marker / 2; - const auto inner = full - marker; - const auto correct = start + inner * valueToRatio(value); - return Premium::BubbleRowState{ - .counter = value, - .ratio = correct / full, - }; - }); const auto activeFgOverride = [=](int count) { const auto coloring = Calls::Group::Ui::StarsColoringForCount( colorings, count); return Ui::ColorFromSerialized(coloring.bgLight); }; - - const auto bubble = Premium::AddBubbleRow( - content, - st::boostBubble, - BoxShowFinishes(box), - std::move(bubbleRowState), - Premium::BubbleType::Credits, - nullptr, - &st::paidReactBubbleIcon, - st::boxRowPadding); - if (videoStream || giftAuction) { - state->chosen.value() | rpl::start_with_next([=](int count) { - bubble->setBrushOverride(activeFgOverride(count)); - }, bubble->lifetime()); - } + AddStarSelectBubble( + box, + state->chosen.value(), + args.max, + videoStream ? activeFgOverride : Fn()); const auto already = ranges::find( args.top, @@ -676,7 +522,7 @@ void PaidReactionsBox( args.chosen, args.max, changed, - (videoStream || giftAuction) ? activeFgOverride : Fn()); + videoStream ? activeFgOverride : Fn()); box->addTopButton( dark ? st::darkEditStarsClose : st::boxTitleClose, @@ -700,7 +546,6 @@ void PaidReactionsBox( box->addRow( VideoStreamStarsLevel(box, colorings, state->chosen.value()), st::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0)); - } else if (giftAuction) { } else if (videoStreamSending) { addTopReactors(); } @@ -712,48 +557,46 @@ void PaidReactionsBox( ? tr::lng_paid_comment_title() : videoStreamSending ? tr::lng_paid_reaction_title() - : giftAuction - ? tr::lng_auction_bid_title() : tr::lng_paid_react_title()), dark ? st::darkEditStarsCenteredTitle : st::boostCenteredTitle), st::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0), style::al_top); - if (!giftAuction) { - const auto labelWrap = box->addRow( - object_ptr(box), - (st::boxRowPadding - + QMargins(0, st::lineWidth, 0, st::boostBottomSkip))); - const auto label = CreateChild( - labelWrap, - (videoStream - ? (videoStreamChoosing - ? tr::lng_paid_comment_about - : tr::lng_paid_reaction_about)( - lt_name, - rpl::single(Text::Bold(args.name)), - Text::RichLangValue) - : already - ? tr::lng_paid_react_already( - lt_count, - rpl::single(already) | tr::to_count(), - Text::RichLangValue) - : tr::lng_paid_react_about( - lt_channel, + + const auto labelWrap = box->addRow( + object_ptr(box), + (st::boxRowPadding + + QMargins(0, st::lineWidth, 0, st::boostBottomSkip))); + const auto label = CreateChild( + labelWrap, + (videoStream + ? (videoStreamChoosing + ? tr::lng_paid_comment_about + : tr::lng_paid_reaction_about)( + lt_name, rpl::single(Text::Bold(args.name)), - Text::RichLangValue)), - dark ? st::darkEditStarsText : st::boostText); - label->setTryMakeSimilarLines(true); - labelWrap->widthValue() | rpl::start_with_next([=](int width) { - label->resizeToWidth(width); - }, label->lifetime()); - label->heightValue() | rpl::start_with_next([=](int height) { - const auto min = 2 * st::normalFont->height; - const auto skip = std::max((min - height) / 2, 0); - labelWrap->resize(labelWrap->width(), 2 * skip + height); - label->moveToLeft(0, skip); - }, label->lifetime()); - } - if (!videoStream && !giftAuction) { + Text::RichLangValue) + : already + ? tr::lng_paid_react_already( + lt_count, + rpl::single(already) | tr::to_count(), + Text::RichLangValue) + : tr::lng_paid_react_about( + lt_channel, + rpl::single(Text::Bold(args.name)), + Text::RichLangValue)), + dark ? st::darkEditStarsText : st::boostText); + label->setTryMakeSimilarLines(true); + labelWrap->widthValue() | rpl::start_with_next([=](int width) { + label->resizeToWidth(width); + }, label->lifetime()); + label->heightValue() | rpl::start_with_next([=](int height) { + const auto min = 2 * st::normalFont->height; + const auto skip = std::max((min - height) / 2, 0); + labelWrap->resize(labelWrap->width(), 2 * skip + height); + label->moveToLeft(0, skip); + }, label->lifetime()); + + if (!videoStream) { addTopReactors(); const auto skip = st::defaultCheckbox.margin.bottom(); @@ -774,21 +617,19 @@ void PaidReactionsBox( AddSkip(content); AddSkip(content); - if (!giftAuction) { - AddDividerText( - content, - tr::lng_paid_react_agree( - lt_link, - rpl::combine( - tr::lng_paid_react_agree_link(), - tr::lng_group_invite_subscription_about_url() - ) | rpl::map([](const QString &text, const QString &url) { - return Ui::Text::Link(text, url); - }), - Ui::Text::RichLangValue), - st::defaultBoxDividerLabelPadding, - dark ? st::groupCallDividerLabel : st::defaultDividerLabel); - } + AddDividerText( + content, + tr::lng_paid_react_agree( + lt_link, + rpl::combine( + tr::lng_paid_react_agree_link(), + tr::lng_group_invite_subscription_about_url() + ) | rpl::map([](const QString &text, const QString &url) { + return Ui::Text::Link(text, url); + }), + Ui::Text::RichLangValue), + st::defaultBoxDividerLabelPadding, + dark ? st::groupCallDividerLabel : st::defaultDividerLabel); const auto button = box->addButton(rpl::single(QString()), [=] { args.send(state->chosen.current(), state->shownPeer.current()); @@ -802,24 +643,11 @@ void PaidReactionsBox( button->setText(args.submit(state->chosen.value())); - { - const auto balance = Settings::AddBalanceWidget( - content, - args.session, - std::move(args.balanceValue), - false, - nullptr, - dark); - rpl::combine( - balance->sizeValue(), - box->widthValue() - ) | rpl::start_with_next([=] { - balance->moveToLeft( - st::creditsHistoryRightSkip * 2, - st::creditsHistoryRightSkip); - balance->update(); - }, balance->lifetime()); - } + AddStarSelectBalance( + box, + args.session, + std::move(args.balanceValue), + dark); } object_ptr MakePaidReactionBox(PaidReactionBoxArgs &&args) { @@ -891,4 +719,282 @@ QImage GenerateSmallBadgeImage( return result; } +StarSelectDiscreter StarSelectDiscreterForMax(int max) { + Expects(max >= 2); + + // 1/8 of width is 1..10 + // 1/3 of width is 1..100 + // 2/3 of width is 1..1000 + + auto thresholds = base::flat_map(); + thresholds.emplace(0., 1); + if (max <= 40) { + thresholds.emplace(1., max); + } else if (max <= 300) { + thresholds.emplace(1. / 4, 10); + thresholds.emplace(1., max); + } else if (max <= 600) { + thresholds.emplace(1. / 8, 10); + thresholds.emplace(1. / 2, 100); + thresholds.emplace(1., max); + } else if (max <= 1900) { + thresholds.emplace(1. / 8, 10); + thresholds.emplace(1. / 3, 100); + thresholds.emplace(1., max); + } else if (max <= 10000) { + thresholds.emplace(1. / 8, 10); + thresholds.emplace(1. / 3, 100); + thresholds.emplace(2. / 3, 1000); + thresholds.emplace(1., max); + } else { + thresholds.emplace(1. / 10, 10); + thresholds.emplace(1. / 6, 100); + thresholds.emplace(1. / 3, 1000); + thresholds.emplace(1., max); + } + + const auto ratioToValue = [=](float64 ratio) { + ratio = std::clamp(ratio, 0., 1.); + const auto j = thresholds.lower_bound(ratio); + if (j == begin(thresholds)) { + return 1; + } + const auto i = j - 1; + const auto progress = (ratio - i->first) / (j->first - i->first); + const auto value = i->second + (j->second - i->second) * progress; + return int(base::SafeRound(value)); + }; + const auto valueToRatio = [=](int value) { + value = std::clamp(value, 1, max); + auto i = begin(thresholds); + auto j = i + 1; + while (j->second < value) { + i = j++; + } + const auto progress = (value - i->second) + / float64(j->second - i->second); + return i->first + (j->first - i->first) * progress; + }; + return { + .ratioToValue = ratioToValue, + .valueToRatio = valueToRatio, + }; +} + +void PaidReactionSlider( + not_null container, + const style::MediaSlider &st, + int min, + int explicitlyAllowed, + int current, + int max, + Fn changed, + Fn activeFgOverride) { + Expects(current >= 1 && current <= max); + Expects(explicitlyAllowed <= max); + + if (!explicitlyAllowed) { + explicitlyAllowed = min; + } + const auto slider = container->add( + object_ptr(container, st), + st::boxRowPadding + QMargins(0, st::paidReactSliderTop, 0, 0)); + slider->resize(slider->width(), st::paidReactSlider.seekSize.height()); + + const auto update = [=](int count) { + if (activeFgOverride) { + const auto color = activeFgOverride(count); + slider->setColorOverrides({ + .activeBg = color, + .activeBorder = color, + .seekFg = st::groupCallMembersFg->c, + .seekBorder = color, + .inactiveBorder = Qt::transparent, + }); + } + }; + + const auto discreter = StarSelectDiscreterForMax(max); + slider->setAlwaysDisplayMarker(true); + slider->setDirection(ContinuousSlider::Direction::Horizontal); + slider->setValue(discreter.valueToRatio(current)); + const auto ratioToValue = [=](float64 ratio) { + const auto value = discreter.ratioToValue(ratio); + return (value <= explicitlyAllowed && explicitlyAllowed < min) + ? explicitlyAllowed + : std::max(value, min); + }; + slider->setAdjustCallback([=](float64 ratio) { + return discreter.valueToRatio(ratioToValue(ratio)); + }); + const auto callback = [=](float64 ratio) { + const auto value = ratioToValue(ratio); + update(value); + changed(value); + }; + slider->setChangeProgressCallback(callback); + slider->setChangeFinishedCallback(callback); + update(current); + + struct State { + StarParticles particles = StarParticles( + StarParticles::Type::Right, + 200, + st::lineWidth * 7); + Ui::Animations::Basic animation; + }; + const auto state = slider->lifetime().make_state(); + + const auto stars = Ui::CreateChild(slider->parentWidget()); + stars->show(); + stars->raise(); + slider->geometryValue() | rpl::start_with_next([=](QRect rect) { + stars->setGeometry(rect); + }, stars->lifetime()); + + state->animation.init([=] { stars->update(); }); + stars->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto seekSize = st::paidReactSlider.seekSize.width(); + const auto seekRadius = seekSize / 2.; + stars->paintRequest() | rpl::start_with_next([=] { + if (!state->animation.animating()) { + state->animation.start(); + } + auto p = QPainter(stars); + auto hq = PainterHighQualityEnabler(p); + const auto progress = slider->value(); + const auto rect = stars->rect(); + const auto availableWidth = rect.width() - seekSize; + const auto seekCenter = seekRadius + availableWidth * progress; + + state->particles.setSpeed(.1 + progress * .3); + state->particles.setVisible(.25 + .65 * progress); + + auto fullPath = QPainterPath(); + fullPath.addRoundedRect(QRectF(rect), seekRadius, seekRadius); + auto circlePath = QPainterPath(); + circlePath.addEllipse( + QPointF(seekCenter, rect.height() / 2.), + seekRadius, + seekRadius); + auto rightRect = QPainterPath(); + rightRect.addRect( + QRectF(seekCenter, 0, rect.width() - seekCenter, rect.height())); + + p.setClipPath(fullPath.subtracted(circlePath)); + state->particles.setColor(Qt::white); + state->particles.paint(p, rect, crl::now()); + p.setClipping(false); + + p.setClipPath(fullPath.intersected(circlePath.united(rightRect))); + state->particles.setColor(activeFgOverride + ? st::groupCallMemberInactiveIcon->c + : st::creditsBg3->c); + state->particles.paint(p, rect, crl::now()); + }, stars->lifetime()); +} + +void AddStarSelectBalance( + not_null box, + not_null session, + rpl::producer balanceValue, + bool dark) { + const auto balance = Settings::AddBalanceWidget( + box->verticalLayout(), + session, + std::move(balanceValue), + false, + nullptr, + dark); + rpl::combine( + balance->sizeValue(), + box->widthValue() + ) | rpl::start_with_next([=] { + balance->moveToLeft( + st::creditsHistoryRightSkip * 2, + st::creditsHistoryRightSkip); + balance->update(); + }, balance->lifetime()); +} + +void AddStarSelectBubble( + not_null box, + rpl::producer value, + int max, + Fn activeFgOverride) { + const auto valueToRatio = StarSelectDiscreterForMax(max).valueToRatio; + auto bubbleRowState = rpl::duplicate(value) | rpl::map([=](int value) { + const auto full = st::boxWideWidth + - st::boxRowPadding.left() + - st::boxRowPadding.right(); + const auto marker = st::paidReactSlider.seekSize.width(); + const auto start = marker / 2; + const auto inner = full - marker; + const auto correct = start + inner * valueToRatio(value); + return Premium::BubbleRowState{ + .counter = value, + .ratio = correct / full, + }; + }); + + const auto bubble = Premium::AddBubbleRow( + box->verticalLayout(), + st::boostBubble, + BoxShowFinishes(box), + std::move(bubbleRowState), + Premium::BubbleType::Credits, + nullptr, + &st::paidReactBubbleIcon, + st::boxRowPadding); + if (activeFgOverride) { + std::move(value) | rpl::start_with_next([=](int count) { + bubble->setBrushOverride(activeFgOverride(count)); + }, bubble->lifetime()); + } +} + +object_ptr MakeStarSelectInfoBlocks( + not_null parent, + std::vector blocks, + Text::MarkedContext context, + bool dark) { + Expects(!blocks.empty()); + + auto result = object_ptr(parent.get()); + const auto raw = result.data(); + + struct State { + std::vector> blocks; + }; + const auto state = raw->lifetime().make_state(); + + for (auto &info : blocks) { + state->blocks.push_back(MakeStarSelectInfoBlock( + raw, + std::move(info.title), + std::move(info.subtext), + context, + dark)); + } + raw->resize(raw->width(), state->blocks.front()->height()); + raw->widthValue() | rpl::start_with_next([=](int width) { + const auto count = int(state->blocks.size()); + const auto skip = (st::boxRowPadding.left() / 2); + const auto single = (width - skip * (count - 1)) / float64(count); + if (single < 1.) { + return; + } + auto x = 0.; + const auto w = int(base::SafeRound(single)); + for (const auto &block : state->blocks) { + block->resizeToWidth(w); + block->moveToLeft(int(base::SafeRound(x)), 0); + x += single + skip; + } + }, raw->lifetime()); + + return result; +} + } // namespace Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h index 83b6dbe9c8..cd68333ac3 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace style { struct RoundCheckbox; +struct MediaSlider; } // namespace style namespace Main { @@ -23,6 +24,7 @@ namespace Ui { class BoxContent; class GenericBox; class DynamicImage; +class VerticalLayout; struct PaidReactionTop { QString name; @@ -49,7 +51,6 @@ struct PaidReactionBoxArgs { Fn send; bool videoStreamChoosing = false; bool videoStreamSending = false; - bool giftAuction = false; bool dark = false; }; @@ -69,4 +70,43 @@ void PaidReactionsBox( QColor fg, const style::RoundCheckbox *borderSt = nullptr); +struct StarSelectDiscreter { + Fn ratioToValue; + Fn valueToRatio; +}; + +[[nodiscard]] StarSelectDiscreter StarSelectDiscreterForMax(int max); + +void PaidReactionSlider( + not_null container, + const style::MediaSlider &st, + int min, + int explicitlyAllowed, + int current, + int max, + Fn changed, + Fn activeFgOverride = nullptr); + +void AddStarSelectBalance( + not_null box, + not_null session, + rpl::producer balanceValue, + bool dark = false); + +void AddStarSelectBubble( + not_null box, + rpl::producer value, + int max, + Fn activeFgOverride = nullptr); + +struct StarSelectInfoBlock { + rpl::producer title; + rpl::producer subtext; +}; +[[nodiscard]] object_ptr MakeStarSelectInfoBlocks( + not_null parent, + std::vector blocks, + Text::MarkedContext context, + bool dark = false); + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index a91f95fcba..28a780933c 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -446,3 +446,20 @@ auctionInfoSubtitleSkip: 8px; auctionInfoTableMargin: margins(0px, 12px, 0px, 12px); auctionAboutLogo: icon {{ "calls/group_call_logo", windowFgActive }}; auctionAboutLogoPadding: margins(8px, 8px, 8px, 8px); + +auctionBidPlace: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + textFg: windowActiveTextFg; +} +auctionBidUserpic: UserpicButton(defaultUserpicButton) { + size: size(28px, 36px); + photoSize: 28px; + photoPosition: point(0px, 4px); +} +auctionBidName: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; +} +auctionBidStars: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; +} +auctionBidSkip: 10px; diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index e58cfbfb79..2de0534ec7 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -560,20 +560,26 @@ starrefPopupMenu: PopupMenu(defaultPopupMenu) { } } -videoStreamInfoPadding: margins(12px, 8px, 12px, 8px); -videoStreamInfoTitle: FlatLabel(defaultFlatLabel) { +starSelectInfoPadding: margins(12px, 8px, 12px, 8px); +starSelectInfoTitle: FlatLabel(defaultFlatLabel) { minWidth: 40px; - textFg: groupCallMembersFg; + textFg: windowBoldFg; align: align(top); style: TextStyle(boxTextStyle) { font: font(18px semibold); } } -videoStreamInfoSubtext: FlatLabel(defaultFlatLabel) { +starSelectInfoSubtext: FlatLabel(defaultFlatLabel) { minWidth: 40px; - textFg: groupCallMembersFg; + textFg: windowFg; align: align(top); style: TextStyle(defaultTextStyle) { - font: font(12px); + font: font(10px); } } +videoStreamInfoTitle: FlatLabel(starSelectInfoTitle) { + textFg: groupCallMembersFg; +} +videoStreamInfoSubtext: FlatLabel(starSelectInfoSubtext) { + textFg: groupCallMembersFg; +} diff --git a/Telegram/lib_rpl b/Telegram/lib_rpl index 9a3ce435f4..f4eb411668 160000 --- a/Telegram/lib_rpl +++ b/Telegram/lib_rpl @@ -1 +1 @@ -Subproject commit 9a3ce435f4054e6cbd45e1c6e3e27cfff515c829 +Subproject commit f4eb411668573d32fc430e27260728be0376ea6c