Nice bidding information display.

This commit is contained in:
John Preston
2025-11-14 13:51:56 +04:00
parent 9b599644d9
commit 4a5cdbcfb2
8 changed files with 963 additions and 462 deletions
+1 -1
View File
@@ -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";
@@ -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<QColor> 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<int> MinutesLeftTillValue(TimeId endDate) {
return [=](auto consumer) {
@@ -82,6 +113,131 @@ constexpr auto kMaxShownBid = 30'000;
};
}
[[nodiscard]] rpl::producer<int> 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<base::Timer>();
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<RpWidget> MakeBidRow(
not_null<RpWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
rpl::producer<BidRowData> data) {
auto result = object_ptr<RpWidget>(parent.get());
const auto raw = result.data();
struct State {
std::unique_ptr<FlatLabel> place;
std::unique_ptr<UserpicButton> userpic;
std::unique_ptr<FlatLabel> name;
std::unique_ptr<FlatLabel> stars;
UserData *user = nullptr;
};
const auto state = raw->lifetime().make_state<State>();
state->place = std::make_unique<FlatLabel>(
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<FlatLabel>(
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<FlatLabel>(
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<UserpicButton>(
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<PeerData*> peer;
std::shared_ptr<ChatHelpers::Show> show;
@@ -124,85 +280,318 @@ void PlaceAuctionBid(
});
}
object_ptr<RpWidget> MakeAuctionInfoBlocks(
not_null<RpWidget*> box,
not_null<Main::Session*> session,
rpl::producer<Data::GiftAuctionState> 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<GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
rpl::producer<Data::GiftAuctionState> value,
rpl::producer<int> chosen) {
struct My {
BidType type;
int position = 0;
inline bool operator==(const My &) const = default;
};
struct State {
rpl::variable<My> my;
rpl::variable<std::vector<BidRowData>> top;
std::vector<Ui::PeerUserpicView> cache;
};
const auto state = box->lifetime().make_state<State>();
rpl::duplicate(
value
) | rpl::start_with_next([=](const Data::GiftAuctionState &value) {
auto cache = std::vector<Ui::PeerUserpicView>();
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<BidRowData>();
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<BidRowData> &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<GenericBox*> box, AuctionBidBoxArgs &&args) {
const auto weak = base::make_weak(box);
const auto state = box->lifetime().make_state<
rpl::variable<Data::GiftAuctionState>
>(std::move(args.state));
auto submit = [=](rpl::producer<int> 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<Data::GiftAuctionState> value)
: value(std::move(value)) {
}
rpl::variable<Data::GiftAuctionState> value;
rpl::variable<int> chosen;
};
const auto state = box->lifetime().make_state<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>{
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 &current = 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<FlatLabel>(
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<BoxContent> MakeAuctionBidBox(
@@ -215,8 +604,8 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
std::shared_ptr<TableRowTooltipData> 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<GenericBox*> 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<GenericBox*> box, AuctionBidBoxArgs &&args) {
[[nodiscard]] object_ptr<TableLayout> AuctionInfoTable(
not_null<QWidget*> parent,
not_null<Ui::VerticalLayout*> container,
not_null<VerticalLayout*> container,
rpl::producer<Data::GiftAuctionState> value) {
auto result = object_ptr<TableLayout>(parent.get(), st::defaultTable);
const auto raw = result.data();
@@ -354,14 +743,14 @@ void AuctionAboutBox(
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
box->addRow(
object_ptr<Ui::FlatLabel>(
object_ptr<FlatLabel>(
box,
tr::lng_auction_about_title(),
st::boxTitle),
st::boxRowPadding + st::confcallLinkTitlePadding,
style::al_top);
box->addRow(
object_ptr<Ui::FlatLabel>(
object_ptr<FlatLabel>(
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<void()>([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<Ui::FlatLabel>(
object_ptr<FlatLabel>(
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,
@@ -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<Ui::RpWidget*> MakeInfoBlock(
not_null<Ui::RpWidget*> parent,
rpl::producer<QString> title,
rpl::producer<QString> subtext) {
const auto result = CreateChild<Ui::RpWidget>(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<Ui::FlatLabel>(
result,
std::move(title),
st::videoStreamInfoTitle);
const auto subtextLabel = CreateChild<Ui::FlatLabel>(
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<StarsColoring> &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<Ui::RpWidget> VideoStreamStarsLevel(
not_null<Ui::RpWidget*> box,
object_ptr<RpWidget> VideoStreamStarsLevel(
not_null<RpWidget*> box,
const std::vector<StarsColoring> &colorings,
rpl::producer<int> starsValue) {
auto result = object_ptr<Ui::RpWidget>(box.get());
const auto raw = result.data();
struct State {
rpl::variable<int> stars;
rpl::variable<StarsColoring> coloring;
std::vector<not_null<Ui::RpWidget*>> blocks;
};
const auto state = raw->lifetime().make_state<State>();
const auto state = box->lifetime().make_state<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<Ui::RpWidget> 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
@@ -65,187 +65,6 @@ struct TopReactorKey {
const TopReactorKey &) = default;
};
struct Discreter {
Fn<int(float64)> ratioToValue;
Fn<float64(int)> 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<float64, int>();
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<VerticalLayout*> container,
const style::MediaSlider &st,
int min,
int explicitlyAllowed,
int current,
int max,
Fn<void(int)> changed,
Fn<QColor(int)> activeFgOverride = nullptr) {
Expects(current >= 1 && current <= max);
Expects(explicitlyAllowed <= max);
if (!explicitlyAllowed) {
explicitlyAllowed = min;
}
const auto slider = container->add(
object_ptr<MediaSlider>(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<State>();
const auto stars = Ui::CreateChild<Ui::RpWidget>(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<Calls::Group::Ui::StarsColoring> &colorings,
int count,
@@ -572,6 +391,58 @@ void FillTopReactors(
}, wrap->lifetime());
}
[[nodiscard]] not_null<RpWidget*> MakeStarSelectInfoBlock(
not_null<RpWidget*> parent,
rpl::producer<TextWithEntities> title,
rpl::producer<QString> subtext,
Text::MarkedContext context,
bool dark) {
const auto result = CreateChild<RpWidget>(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<FlatLabel>(
result,
std::move(title),
dark ? st::videoStreamInfoTitle : st::starSelectInfoTitle,
st::defaultPopupMenu,
context);
const auto subtextLabel = CreateChild<FlatLabel>(
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<QColor(int)>());
const auto already = ranges::find(
args.top,
@@ -676,7 +522,7 @@ void PaidReactionsBox(
args.chosen,
args.max,
changed,
(videoStream || giftAuction) ? activeFgOverride : Fn<QColor(int)>());
videoStream ? activeFgOverride : Fn<QColor(int)>());
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<RpWidget>(box),
(st::boxRowPadding
+ QMargins(0, st::lineWidth, 0, st::boostBottomSkip)));
const auto label = CreateChild<FlatLabel>(
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<RpWidget>(box),
(st::boxRowPadding
+ QMargins(0, st::lineWidth, 0, st::boostBottomSkip)));
const auto label = CreateChild<FlatLabel>(
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<BoxContent> 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<float64, int>();
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<VerticalLayout*> container,
const style::MediaSlider &st,
int min,
int explicitlyAllowed,
int current,
int max,
Fn<void(int)> changed,
Fn<QColor(int)> activeFgOverride) {
Expects(current >= 1 && current <= max);
Expects(explicitlyAllowed <= max);
if (!explicitlyAllowed) {
explicitlyAllowed = min;
}
const auto slider = container->add(
object_ptr<MediaSlider>(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<State>();
const auto stars = Ui::CreateChild<Ui::RpWidget>(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<GenericBox*> box,
not_null<Main::Session*> session,
rpl::producer<CreditsAmount> 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<GenericBox*> box,
rpl::producer<int> value,
int max,
Fn<QColor(int)> 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<RpWidget> MakeStarSelectInfoBlocks(
not_null<RpWidget*> parent,
std::vector<StarSelectInfoBlock> blocks,
Text::MarkedContext context,
bool dark) {
Expects(!blocks.empty());
auto result = object_ptr<RpWidget>(parent.get());
const auto raw = result.data();
struct State {
std::vector<not_null<RpWidget*>> blocks;
};
const auto state = raw->lifetime().make_state<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
@@ -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<void(int, uint64)> 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<int(float64)> ratioToValue;
Fn<float64(int)> valueToRatio;
};
[[nodiscard]] StarSelectDiscreter StarSelectDiscreterForMax(int max);
void PaidReactionSlider(
not_null<VerticalLayout*> container,
const style::MediaSlider &st,
int min,
int explicitlyAllowed,
int current,
int max,
Fn<void(int)> changed,
Fn<QColor(int)> activeFgOverride = nullptr);
void AddStarSelectBalance(
not_null<GenericBox*> box,
not_null<Main::Session*> session,
rpl::producer<CreditsAmount> balanceValue,
bool dark = false);
void AddStarSelectBubble(
not_null<GenericBox*> box,
rpl::producer<int> value,
int max,
Fn<QColor(int)> activeFgOverride = nullptr);
struct StarSelectInfoBlock {
rpl::producer<TextWithEntities> title;
rpl::producer<QString> subtext;
};
[[nodiscard]] object_ptr<RpWidget> MakeStarSelectInfoBlocks(
not_null<RpWidget*> parent,
std::vector<StarSelectInfoBlock> blocks,
Text::MarkedContext context,
bool dark = false);
} // namespace Ui
@@ -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;
+12 -6
View File
@@ -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;
}