Show upgrade price decrease information.

This commit is contained in:
John Preston
2025-09-09 19:21:48 +04:00
parent 5b02323487
commit 2ca63ae324
6 changed files with 417 additions and 24 deletions
+5
View File
@@ -3759,6 +3759,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_upgrade_tradable_about_user" = "{name} will be able to sell the gift on Telegram and NFT marketplaces.";
"lng_gift_upgrade_tradable_about_channel" = "Admins of {name} will be able to sell the gift on Telegram and NFT marketplaces.";
"lng_gift_upgrade_button" = "Upgrade for {price}";
"lng_gift_upgrade_decreases" = "Price decreases in {time}";
"lng_gift_upgrade_see_table" = "See how this price will decrease {arrow}";
"lng_gift_upgrade_prices_about" = "Upgrade cost drops every minute.";
"lng_gift_upgrade_prices_title" = "Upgrade Cost";
"lng_gift_upgrade_prices_subtitle" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
"lng_gift_upgrade_free" = "Upgrade for Free";
"lng_gift_upgrade_confirm" = "Confirm";
"lng_gift_upgrade_add_my" = "Add my name to the gift";
+376 -17
View File
@@ -74,10 +74,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/controls/button_labels.h"
#include "ui/controls/emoji_button.h"
#include "ui/controls/ton_common.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/effects/premium_bubble.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/effects/ripple_animation.h"
@@ -99,6 +101,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/shadow.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/table_layout.h"
#include "window/themes/window_theme.h"
#include "window/section_widget.h"
#include "window/window_controller.h"
@@ -107,6 +110,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_giveaway.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
@@ -500,6 +504,24 @@ int TextBubblePart::elisionLines() const {
return info.limitedCount && !info.limitedLeft;
}
struct UpgradePrice {
TimeId date = 0;
int stars = 0;
};
[[nodiscard]] std::vector<UpgradePrice> ParsePrices(
const MTPVector<MTPStarGiftUpgradePrice> &list) {
return ranges::views::all(
list.v
) | ranges::views::transform([](const MTPStarGiftUpgradePrice &price) {
const auto &data = price.data();
return UpgradePrice{
.date = data.vdate().v,
.stars = int(data.vupgrade_stars().v),
};
}) | ranges::to_vector;
}
PreviewDelegate::PreviewDelegate(
not_null<QWidget*> parent,
not_null<ChatStyle*> st,
@@ -4816,6 +4838,8 @@ struct UpgradeArgs : StarGiftUpgradeArgs {
std::vector<Data::UniqueGiftModel> models;
std::vector<Data::UniqueGiftPattern> patterns;
std::vector<Data::UniqueGiftBackdrop> backdrops;
std::vector<UpgradePrice> prices;
std::vector<UpgradePrice> nextPrices;
};
[[nodiscard]] rpl::producer<Data::UniqueGift> MakeUpgradeGiftStream(
@@ -4895,6 +4919,274 @@ void AddUpgradeGiftCover(
rpl::single(args.peer->shortName()))));
}
class UpgradePriceValue final {
public:
UpgradePriceValue(
not_null<Main::Session*> session,
uint64 stargiftId,
int cost,
std::vector<UpgradePrice> all,
std::vector<UpgradePrice> next);
[[nodiscard]] int cost() const;
[[nodiscard]] rpl::producer<int> costValue() const;
[[nodiscard]] rpl::producer<TimeId> tillNextValue() const;
private:
void update();
void refresh();
MTP::Sender _api;
uint64 _stargiftId = 0;
rpl::variable<int> _cost;
rpl::variable<TimeId> _tillNext;
std::vector<UpgradePrice> _all;
std::vector<UpgradePrice> _next;
base::Timer _timer;
bool _refreshing = false;
bool _finished = false;
};
UpgradePriceValue::UpgradePriceValue(
not_null<Main::Session*> session,
uint64 stargiftId,
int cost,
std::vector<UpgradePrice> all,
std::vector<UpgradePrice> next)
: _api(&session->mtp())
, _stargiftId(stargiftId)
, _cost(cost)
, _all(std::move(all))
, _next(std::move(next))
, _timer([=] { update(); })
, _finished(_next.size() < 2) {
update();
_timer.callEach(1000);
}
int UpgradePriceValue::cost() const {
return _cost.current();
}
rpl::producer<int> UpgradePriceValue::costValue() const {
return _cost.value();
}
rpl::producer<TimeId> UpgradePriceValue::tillNextValue() const {
return _tillNext.value();
}
void UpgradePriceValue::update() {
if (_all.empty() || _next.empty()) {
_timer.cancel();
return;
}
const auto now = base::unixtime::now();
const auto i = ranges::upper_bound(
_all,
now,
ranges::less(),
&UpgradePrice::date);
const auto j = ranges::upper_bound(
_next,
now,
ranges::less(),
&UpgradePrice::date);
const auto full = (i == begin(_all)) ? i->stars : (i - 1)->stars;
const auto part = (j == begin(_next)) ? j->stars : (j - 1)->stars;
const auto fullDate = (i != end(_all)) ? i->date : TimeId();
const auto partDate = (j != end(_next)) ? j->date : TimeId();
_cost = std::min({ _cost.current(), part, full});
if (int(end(_next) - j) < 3) {
refresh();
}
const auto next = std::min({
partDate ? partDate : TimeId(INT_MAX),
fullDate ? fullDate : TimeId(INT_MAX),
});
if (next != TimeId(INT_MAX)) {
_tillNext = next - now;
} else {
_tillNext = 0;
_timer.cancel();
}
}
void UpgradePriceValue::refresh() {
if (_refreshing || _finished) {
return;
}
_api.request(MTPpayments_GetStarGiftUpgradePreview(
MTP_long(_stargiftId)
)).done([=](const MTPpayments_StarGiftUpgradePreview &result) {
const auto &data = result.data();
_all = ParsePrices(data.vprices());
_next = ParsePrices(data.vnext_prices());
}).fail([=] {
_finished = true;
}).send();
}
void PricesBox(
not_null<Ui::GenericBox*> box,
const std::vector<UpgradePrice> &list,
rpl::producer<int> cost) {
const auto top = box->setPinnedToTopContent(
object_ptr<Ui::VerticalLayout>(box));
box->setWidth(st::boxWideWidth);
box->setStyle(st::boostBox);
struct State {
rpl::variable<int> cost;
};
const auto state = box->lifetime().make_state<State>();
state->cost = std::move(cost);
const auto min = list.back().stars;
const auto max = std::max(min + 2, list.front().stars);
const auto from = 0;
const auto till = max - min;
const auto addSkip = [&](int skip) {
top->add(object_ptr<Ui::FixedHeightWidget>(top, skip));
};
const auto ratio = [=](int current) {
current = std::clamp(current, from, till);
const auto count = (till - from);
const auto index = (current - from);
if (count <= 2) {
return 0.5;
}
const auto available = st::boxWideWidth
- st::boxPadding.left()
- st::boxPadding.right();
const auto average = available / float64(count);
const auto levelWidth = [&](int stars) {
return st::normalFont->width(Lang::FormatCountDecimal(stars));
};
const auto paddings = 2 * st::premiumLineTextSkip;
const auto labelLeftWidth = paddings + levelWidth(min);
const auto labelRightWidth = paddings + levelWidth(max);
const auto first = std::max(average, labelLeftWidth * 1.);
const auto last = std::max(average, labelRightWidth * 1.);
const auto other = (available - first - last) / (count - 2);
return (first + (index - 1) * other) / available;
};
auto bubbleRowState = state->cost.value(
) | rpl::map([=](int value) {
return Premium::BubbleRowState{
.counter = max - value,
.ratio = ratio(max - value),
.dynamic = true,
};
});
Premium::AddBubbleRow(
top,
st::starRatingBubble,
box->showFinishes(),
rpl::duplicate(bubbleRowState),
Ui::Premium::BubbleType::StarRating,
[=](int value) {
return Premium::BubbleText{
.counter = Lang::FormatCountDecimal(max - value),
};
},
&st::paidReactBubbleIcon,
st::boxRowPadding);
addSkip(st::premiumLineTextSkip);
auto limitState = std::move(
bubbleRowState
) | rpl::map([=](const Premium::BubbleRowState &state) {
return Premium::LimitRowState{
.ratio = state.ratio,
.dynamic = state.dynamic
};
});
auto left = rpl::single(Lang::FormatCountDecimal(max));
auto right = rpl::single(Lang::FormatCountDecimal(min));
Premium::AddLimitRow(
top,
st::upgradePriceLimits,
Premium::LimitRowLabels{
.leftLabel = std::move(left),
.rightLabel = std::move(right),
.activeLineBg = [=] { return st::windowBgActive->b; },
},
std::move(limitState),
st::boxRowPadding);
top->add(
object_ptr<Ui::FlatLabel>(
top,
tr::lng_gift_upgrade_prices_title(),
st::infoStarsTitle),
st::boxRowPadding + QMargins(0, st::boostTitleSkip / 2, 0, 0),
style::al_top);
top->add(
object_ptr<Ui::FlatLabel>(
top,
tr::lng_gift_upgrade_prices_subtitle(),
st::boostText),
(st::boxRowPadding
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),
style::al_top
)->setTryMakeSimilarLines(true);
auto helper = Ui::Text::CustomEmojiHelper();
const auto creditsIcon = helper.paletteDependent(
Ui::Earn::IconCreditsEmoji());
const auto context = helper.context();
const auto table = box->addRow(
object_ptr<Ui::TableLayout>(box, st::defaultTable),
st::boxPadding);
for (const auto &price : list) {
const auto parsed = base::unixtime::parse(price.date);
const auto time = QLocale().toString(
parsed.time(),
QLocale::ShortFormat);
const auto date = tr::lng_month_day(
tr::now,
lt_month,
Lang::MonthSmall(parsed.date().month())(tr::now),
lt_day,
QString::number(parsed.date().day()));
table->addRow(
object_ptr<Ui::FlatLabel>(
table,
time + u", "_q + date,
st::defaultTable.defaultLabel),
object_ptr<Ui::FlatLabel>(
table,
rpl::single(
base::duplicate(creditsIcon).append(' ').append(
Lang::FormatCountDecimal(price.stars))),
st::defaultTable.defaultValue,
st::defaultPopupMenu,
context),
st::giveawayGiftCodeLabelMargin,
st::giveawayGiftCodeValueMargin);
}
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_gift_upgrade_prices_about(),
st::resalePriceAbout),
style::al_top);
box->addSkip(st::boxPadding.top());
box->setMaxHeight(st::boxWideWidth);
box->addButton(rpl::single(QString()), [=] {
box->closeBox();
})->setText(rpl::single(Ui::Text::IconEmoji(
&st::infoStarsUnderstood
).append(' ').append(tr::lng_stars_rating_understood(tr::now))));
}
void UpgradeBox(
not_null<GenericBox*> box,
not_null<Window::SessionController*> controller,
@@ -4962,14 +5254,25 @@ void UpgradeBox(
&st::menuIconTradable);
struct State {
bool sent = false;
base::Timer timer;
std::unique_ptr<UpgradePriceValue> cost;
bool preserveDetails = false;
bool sent = false;
};
const auto state = std::make_shared<State>();
const auto gifting = !args.savedId
&& !args.giftPrepayUpgradeHash.isEmpty();
const auto preview = !args.savedId && !gifting;
const auto showPrices = !preview
&& (args.prices.size() > 1)
&& (args.nextPrices.size() > 1);
auto prices = args.prices;
state->cost = std::make_unique<UpgradePriceValue>(
&controller->session(),
args.stargiftId,
args.cost,
std::move(args.prices),
std::move(args.nextPrices));
if (!preview && !gifting) {
const auto skip = st::defaultVerticalListSkip;
container->add(
@@ -4993,12 +5296,15 @@ void UpgradeBox(
}, checkbox->lifetime());
}
box->setStyle(preview ? st::giftBox : st::upgradeGiftBox);
box->setStyle(preview
? st::giftBox
: showPrices
? st::upgradeGiftWithPricesBox
: st::upgradeGiftBox);
if (gifting) {
box->setWidth(st::boxWideWidth);
}
const auto cost = args.cost;
auto buttonText = preview ? tr::lng_box_ok() : rpl::single(QString());
const auto button = box->addButton(std::move(buttonText), [=] {
if (preview) {
@@ -5008,6 +5314,7 @@ void UpgradeBox(
return;
}
state->sent = true;
const auto cost = state->cost->cost();
const auto keepDetails = state->preserveDetails;
const auto weak = base::make_weak(box);
const auto done = [=](Payments::CheckoutResult result) {
@@ -5032,20 +5339,70 @@ void UpgradeBox(
}
});
if (!preview) {
SetButtonMarkedLabel(
auto costText = [=] {
auto costValue = state->cost->costValue(
) | rpl::map([](int cost) {
return Ui::Text::IconEmoji(
&st::starIconEmoji
).append(Lang::FormatCreditsAmountDecimal(
CreditsAmount{ cost }));
});
return tr::lng_gift_upgrade_button(
lt_price,
std::move(costValue),
Ui::Text::WithEntities);
};
auto tillNext = state->cost->tillNextValue(
) | rpl::map([](TimeId left) {
const auto hours = left / 3600;
const auto minutes = (left % 3600) / 60;
const auto seconds = left % 60;
const auto padded = [](int value) {
return u"%1"_q.arg(value, 2, 10, QChar('0'));
};
return hours
? u"%1:%2:%3"_q
.arg(hours).arg(padded(minutes)).arg(padded(seconds))
: u"%2:%3"_q.arg(padded(minutes)).arg(padded(seconds));
}) | Ui::Text::ToWithEntities();
Ui::SetButtonTwoLabels(
button,
(cost
? tr::lng_gift_upgrade_button(
lt_price,
rpl::single(Ui::Text::IconEmoji(
&st::starIconEmoji
).append(' ').append(Lang::FormatCreditsAmountDecimal(
CreditsAmount{ cost }))),
Ui::Text::WithEntities)
: tr::lng_gift_upgrade_confirm(Ui::Text::WithEntities)),
{},
st::creditsBoxButtonLabel,
&st::giftBox.button.textFg);
costText(),
tr::lng_gift_upgrade_decreases(
lt_time,
std::move(tillNext),
Ui::Text::WithEntities),
st::resaleButtonTitle,
st::resaleButtonSubtitle);
state->cost->tillNextValue() | rpl::filter([=](TimeId left) {
return !left;
}) | rpl::take(1) | rpl::start_with_next([=] {
while (!button->children().isEmpty()) {
delete button->children()[0];
}
button->setText(costText());
}, button->lifetime());
}
if (showPrices) {
const auto link = Ui::CreateChild<Ui::FlatLabel>(
button->parentWidget(),
tr::lng_gift_upgrade_see_table(
lt_arrow,
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
[](QString text) { return Ui::Text::Link(text); }),
st::resalePriceTableLink);
link->setTryMakeSimilarLines(true);
button->geometryValue() | rpl::start_with_next([=](QRect geometry) {
const auto outer = button->parentWidget()->height();
const auto top = geometry.y() + geometry.height();
const auto available = outer - top - st::boxRadius;
link->resizeToWidth(geometry.width());
link->move(geometry.x(), top + (available - link->height()) / 2);
}, button->lifetime());
link->setClickHandlerFilter([=, list = prices](const auto &...) {
controller->show(Box(PricesBox, list, state->cost->costValue()));
return false;
});
}
AddUniqueCloseButton(box, {});
@@ -5180,6 +5537,8 @@ void ShowStarGiftUpgradeBox(StarGiftUpgradeArgs &&args) {
}
const auto &data = result.data();
auto upgrade = UpgradeArgs{ args };
upgrade.prices = ParsePrices(data.vprices());
upgrade.nextPrices = ParsePrices(data.vnext_prices());
for (const auto &attribute : data.vsample_attributes().v) {
attribute.match([&](const MTPDstarGiftAttributeModel &data) {
upgrade.models.push_back(Api::FromTL(session, data));
+12 -1
View File
@@ -92,7 +92,7 @@ starIconEmojiLarge: IconEmoji {
tonIconEmoji: IconEmoji {
icon: icon{{ "chat/mini_ton_bold", currencyFg }};
padding: margins(1px, 3px, 1px, 0px);
padding: margins(1px, 2px, 1px, 0px);
}
tonIconEmojiLarge: IconEmoji {
icon: icon{{ "payments/ton_emoji-18x18", currencyFg }};
@@ -305,6 +305,9 @@ uniqueMenuButton: IconButton(uniqueCloseButton) {
upgradeGiftBox: Box(giftBox) {
buttonPadding: margins(22px, 3px, 22px, 22px);
}
upgradeGiftWithPricesBox: Box(giftBox) {
buttonPadding: margins(22px, 3px, 22px, 46px);
}
upgradeGiftSubtext: FlatLabel(boxDividerLabel) {
minWidth: 200px;
}
@@ -361,6 +364,14 @@ resaleButtonSubtitle: FlatLabel(defaultFlatLabel) {
textFg: activeButtonFg;
maxHeight: 20px;
}
resalePriceTableLink: FlatLabel(defaultFlatLabel) {
minWidth: 200px;
align: align(top);
}
resalePriceAbout: FlatLabel(boxLabel) {
textFg: windowSubTextFg;
align: align(top);
}
resaleConfirmTonOnly: FlatLabel(boxLabel) {
textFg: windowSubTextFg;
@@ -247,6 +247,9 @@ boostLimits: PremiumLimits(defaultPremiumLimits) {
negativeStarsLimits: PremiumLimits(boostLimits) {
gradientFromLeft: false;
}
upgradePriceLimits: PremiumLimits(boostLimits) {
gradientFromLeft: false;
}
boostBubble: PremiumBubble(defaultPremiumBubble) {
height: 32px;
padding: margins(7px, 0px, 11px, 0px);
@@ -117,6 +117,21 @@ int Bubble::countMaxWidth(int maxPossibleCounter) const {
+ _st.additionalStyle.font->width(textsMax.additional)));
}
int Bubble::countTargetWidth(int targetCounter) const {
auto numbers = Ui::NumbersAnimation(_st.font, [] {});
numbers.setDisabledMonospace(true);
numbers.setDuration(0);
const auto texts = _textFactory(targetCounter);
numbers.setText(texts.counter, targetCounter);
numbers.finishAnimating();
return filledWidth()
+ numbers.maxWidth()
+ (_additional.isEmpty()
? 0
: (_st.additionalSkip
+ _st.additionalStyle.font->width(texts.additional)));
}
void Bubble::setCounter(int value) {
if (_counter != value) {
_counter = value;
@@ -289,12 +304,12 @@ BubbleWidget::BubbleWidget(
}
void BubbleWidget::animateTo(BubbleRowState state) {
_maxBubbleWidth = _bubble.countMaxWidth(state.counter);
const auto targetWidth = _bubble.countTargetWidth(state.counter);
const auto parent = parentWidget();
const auto available = parent->width()
- _outerPadding.left()
- _outerPadding.right();
const auto halfWidth = (_maxBubbleWidth / 2);
const auto halfWidth = (targetWidth / 2);
const auto computeLeft = [=](float64 pointRatio, float64 animProgress) {
const auto delta = (pointRatio - _animatingFromResultRatio);
const auto center = available
@@ -303,9 +318,7 @@ void BubbleWidget::animateTo(BubbleRowState state) {
};
const auto moveEndPoint = state.ratio;
const auto computeRightEdge = [=] {
return parent->width()
- _outerPadding.right()
- _maxBubbleWidth;
return parent->width() - _outerPadding.right() - targetWidth;
};
struct Edge final {
float64 goodPointRatio = 0.;
@@ -456,6 +469,7 @@ void BubbleWidget::paintEvent(QPaintEvent *e) {
_bubble.paintBubble(p, bubbleRect, [&] {
switch (_type) {
case BubbleType::NoPremium:
case BubbleType::UpgradePrice:
case BubbleType::StarRating: return st::windowBgActive->b;
case BubbleType::NegativeRating: return st::attentionButtonFg->b;
case BubbleType::Premium: return QBrush(_cachedGradient);
@@ -55,6 +55,7 @@ public:
[[nodiscard]] int width() const;
[[nodiscard]] int bubbleRadius() const;
[[nodiscard]] int countMaxWidth(int maxPossibleCounter) const;
[[nodiscard]] int countTargetWidth(int targetCounter) const;
void setCounter(int value);
void setTailEdge(EdgeProgress edge);
@@ -94,6 +95,7 @@ struct BubbleRowState {
};
enum class BubbleType : uchar {
UpgradePrice,
StarRating,
NegativeRating,
NoPremium,
@@ -134,7 +136,6 @@ private:
float64 _animatingFromBubbleEdge = 0.;
rpl::variable<BubbleRowState> _state;
Bubble _bubble;
int _maxBubbleWidth = 0;
const BubbleType _type;
const style::margins _outerPadding;