2
0
mirror of https://github.com/telegramdesktop/tdesktop synced 2025-08-22 10:17:10 +00:00

Allow settings gift price in TON.

This commit is contained in:
John Preston 2025-07-29 17:15:48 +04:00
parent 985324ac12
commit aa499eab61
14 changed files with 539 additions and 321 deletions

View File

@ -3559,6 +3559,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_send_button_self" = "Buy a Gift for {cost}";
"lng_gift_buy_resale_title" = "Buy {name}";
"lng_gift_buy_resale_button" = "Buy for {cost}";
"lng_gift_buy_resale_equals" = "Equals to {cost}";
"lng_gift_buy_resale_only_ton" = "The seller only accepts TON as payment";
"lng_gift_buy_resale_pay_stars" = "Pay in Stars";
"lng_gift_buy_resale_pay_ton" = "Pay in TON";
"lng_gift_buy_resale_confirm" = "Do you want to buy {name} for {price} and gift it to {user}?";
"lng_gift_buy_resale_confirm_self" = "Do you want to buy {name} for {price}?";
"lng_gift_buy_price_change_title" = "Price change!";
@ -3704,12 +3708,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_sell_unlist_title" = "Unlist {name}";
"lng_gift_sell_unlist_sure" = "Are you sure you want to unlist your gift?";
"lng_gift_sell_title" = "Price in Stars";
"lng_gift_sell_placeholder" = "Enter price in Stars";
"lng_gift_sell_about" = "You will receive {percent} of the selected amount.";
"lng_gift_sell_amount#one" = "You will receive **{count}** Star.";
"lng_gift_sell_amount#other" = "You will receive **{count}** Stars.";
"lng_gift_sell_min_price#one" = "Minimum price is {count} Star.";
"lng_gift_sell_min_price#other" = "Minimum price is {count} Stars.";
"lng_gift_sell_only_ton" = "Only Accept TON";
"lng_gift_sell_only_ton_about" = "If the buyer pays you in TON, there's no risk of refunds, unlike Stars payments.";
"lng_gift_sell_amount_ton#one" = "You will receive **{count}** TON.";
"lng_gift_sell_amount_ton#other" = "You will receive **{count}** TON.";
"lng_gift_sell_min_price_ton#one" = "Minimum price is {count} TON.";
"lng_gift_sell_min_price_ton#other" = "Minimum price is {count} TON.";
"lng_gift_sell_title_ton" = "Price in TON";
"lng_gift_sell_put" = "Put for Sale";
"lng_gift_sell_update" = "Update the Price";
"lng_gift_sell_toast" = "{name} is now for sale!";

View File

@ -895,9 +895,10 @@ std::optional<Data::StarGift> FromTL(
? peerFromMTP(*data.vowner_id())
: PeerId()),
.releasedBy = releasedBy,
.number = data.vnum().v,
.nanoTonForResale = FindTonForResale(data.vresell_amount()),
.starsForResale = FindStarsForResale(data.vresell_amount()),
.tonForResale = FindTonForResale(data.vresell_amount()),
.number = data.vnum().v,
.onlyAcceptTon = data.is_resale_ton_only(),
.model = *model,
.pattern = *pattern,
}),

View File

@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "history/admin_log/history_admin_log_item.h"
#include "history/view/controls/history_view_suggest_options.h"
#include "history/view/media/history_view_media_generic.h"
#include "history/view/media/history_view_unique_gift.h"
#include "history/view/history_view_element.h"
@ -71,6 +72,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.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_graphics.h"
@ -233,6 +235,37 @@ struct SessionResalePrices {
crl::time lastReceived = 0;
};
[[nodiscard]] CreditsAmount StarsFromTon(
not_null<Main::Session*> session,
CreditsAmount ton) {
const auto appConfig = &session->appConfig();
const auto starsRate = appConfig->starsWithdrawRate() / 100.;
const auto tonRate = appConfig->currencyWithdrawRate();
if (!starsRate) {
return {};
}
const auto count = (ton.value() * tonRate) / starsRate;
return CreditsAmount(int(base::SafeRound(count)));
}
[[nodiscard]] CreditsAmount TonFromStars(
not_null<Main::Session*> session,
CreditsAmount stars) {
const auto appConfig = &session->appConfig();
const auto starsRate = appConfig->starsWithdrawRate() / 100.;
const auto tonRate = appConfig->currencyWithdrawRate();
if (!tonRate) {
return {};
}
const auto count = (stars.value() * starsRate) / tonRate;
const auto whole = int(std::floor(count));
const auto cents = int(base::SafeRound((count - whole) * 100));
return CreditsAmount(
whole,
cents * (Ui::kNanosInOne / 100),
CreditsType::Ton);
}
[[nodiscard]] not_null<SessionResalePrices*> ResalePrices(
not_null<Main::Session*> session) {
static auto result = base::flat_map<
@ -4403,19 +4436,6 @@ void ShowUniqueGiftWearBox(
session,
st::creditsBoxButtonLabel,
&st::giftBox.button.textFg);
rpl::combine(
box->widthValue(),
button->widthValue()
) | rpl::start_with_next([=](int outer, int inner) {
const auto padding = st::giftBox.buttonPadding;
const auto wanted = outer - padding.left() - padding.right();
if (inner != wanted) {
button->resizeToWidth(wanted);
button->moveToLeft(padding.left(), padding.top());
}
}, box->lifetime());
AddUniqueCloseButton(box, {});
}));
}
@ -4473,12 +4493,14 @@ void UpdateGiftSellPrice(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,
Data::SavedStarGiftId savedId,
int price) {
CreditsAmount price) {
const auto was = unique->starsForResale;
const auto session = &show->session();
session->api().request(MTPpayments_UpdateStarGiftPrice(
Api::InputSavedStarGiftId(savedId, unique),
MTP_starsAmount(MTP_long(price), MTP_int(0))
(price
? StarsAmountToTL(price)
: MTP_starsAmount(MTP_long(0), MTP_int(0)))
)).done([=](const MTPUpdates &result) {
session->api().applyUpdates(result);
show->showToast((!price
@ -4489,8 +4511,26 @@ void UpdateGiftSellPrice(
tr::now,
lt_name,
Data::UniqueGiftName(*unique)));
unique->starsForResale = price;
const auto setStars = [&](CreditsAmount amount) {
unique->starsForResale = amount.whole();
};
const auto setTon = [&](CreditsAmount amount) {
unique->nanoTonForResale = amount.whole() * Ui::kNanosInOne
+ amount.nano();
};
if (!price) {
setStars({});
setTon({});
unique->onlyAcceptTon = false;
} else if (price.ton()) {
setStars(StarsFromTon(session, price));
setTon(price);
unique->onlyAcceptTon = true;
} else {
setStars(price);
setTon(TonFromStars(session, price));
unique->onlyAcceptTon = false;
}
session->data().notifyGiftUpdate({
.id = savedId,
.slug = unique->slug,
@ -4517,80 +4557,103 @@ void UniqueGiftSellBox(
Data::SavedStarGiftId savedId,
int price,
Settings::GiftWearBoxStyleOverride st) {
box->setTitle(tr::lng_gift_sell_title());
const auto session = &show->session();
const auto &appConfig = session->appConfig();
const auto starsMin = appConfig.giftResaleStarsMin();
const auto nanoTonMin = appConfig.giftResaleNanoTonMin();
const auto starsThousandths = appConfig.giftResaleStarsThousandths();
const auto nanoTonThousandths = appConfig.giftResaleNanoTonThousandths();
struct State {
rpl::variable<bool> onlyTon;
rpl::variable<CreditsAmount> price;
Fn<std::optional<CreditsAmount>()> computePrice;
rpl::event_stream<> errors;
};
const auto state = box->lifetime().make_state<State>();
state->onlyTon = unique->onlyAcceptTon;
const auto priceNow = unique->onlyAcceptTon
? CreditsAmount(
unique->nanoTonForResale / Ui::kNanosInOne,
unique->nanoTonForResale % Ui::kNanosInOne,
CreditsType::Ton)
: CreditsAmount(unique->starsForResale);
state->price = priceNow
? priceNow
: price
? CreditsAmount(price)
: CreditsAmount(starsMin);
box->setTitle(rpl::conditional(
state->onlyTon.value(),
tr::lng_gift_sell_title_ton(),
tr::lng_gift_sell_title()));
box->setStyle(st.box ? *st.box : st::upgradeGiftBox);
box->setWidth(st::boxWideWidth);
box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] {
box->closeBox();
});
const auto priceNow = unique->starsForResale;
const auto name = Data::UniqueGiftName(*unique);
const auto slug = unique->slug;
const auto session = &show->session();
AddSubsectionTitle(
box->verticalLayout(),
tr::lng_gift_sell_placeholder(),
(st::boxRowPadding - QMargins(
st::defaultSubsectionTitlePadding.left(),
0,
st::defaultSubsectionTitlePadding.right(),
st::defaultSubsectionTitlePadding.bottom())));
const auto &appConfig = session->appConfig();
const auto limit = appConfig.giftResalePriceMax();
const auto minimal = appConfig.giftResalePriceMin();
const auto thousandths = appConfig.giftResaleReceiveThousandths();
const auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::editTagField.heightMin));
auto owned = object_ptr<Ui::NumberInput>(
wrap,
st::editTagField,
rpl::single(QString()),
QString::number(priceNow ? priceNow : price ? price : minimal),
limit);
const auto field = owned.data();
wrap->widthValue() | rpl::start_with_next([=](int width) {
field->move(0, 0);
field->resize(width, field->height());
wrap->resize(width, field->height());
}, wrap->lifetime());
field->paintRequest() | rpl::start_with_next([=](QRect clip) {
auto p = QPainter(field);
st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width());
}, field->lifetime());
field->selectAll();
box->setFocusCallback([=] {
field->setFocusFast();
const auto container = box->verticalLayout();
auto priceInput = HistoryView::AddStarsTonPriceInput(container, {
.session = session,
.showTon = state->onlyTon.value(),
.price = state->price.current(),
.starsMin = starsMin,
.starsMax = appConfig.giftResaleStarsMax(),
.nanoTonMin = nanoTonMin,
.nanoTonMax = appConfig.giftResaleNanoTonMax(),
});
state->price = std::move(priceInput.result);
state->computePrice = std::move(priceInput.computeResult);
box->setFocusCallback(std::move(priceInput.focusCallback));
const auto errors = box->lifetime().make_state<
rpl::event_stream<>
>();
auto goods = rpl::merge(
rpl::single(rpl::empty) | rpl::map_to(true),
base::qt_signal_producer(
field,
&Ui::NumberInput::changed
) | rpl::map_to(true),
errors->events() | rpl::map_to(false)
std::move(priceInput.updates) | rpl::map_to(true),
state->errors.events() | rpl::map_to(false)
) | rpl::start_spawning(box->lifetime());
auto text = rpl::duplicate(goods) | rpl::map([=](bool good) {
const auto value = field->getLastText().toInt();
const auto receive = (int64(value) * thousandths) / 1000;
return !good
? tr::lng_gift_sell_min_price(
const auto value = state->computePrice();
const auto amount = value ? value->value() : 0.;
const auto tonMin = nanoTonMin / float64(Ui::kNanosInOne);
const auto enough = value
&& (amount >= (value->ton() ? tonMin : starsMin));
const auto receive = !value
? 0
: value->ton()
? ((amount * nanoTonThousandths) / 1000.)
: ((int64(amount) * starsThousandths) / 1000);
const auto thousandths = state->onlyTon.current()
? nanoTonThousandths
: starsThousandths;
return (!good || !value)
? (state->onlyTon.current()
? tr::lng_gift_sell_min_price_ton(
tr::now,
lt_count,
minimal,
nanoTonMin / float64(Ui::kNanosInOne),
Ui::Text::RichLangValue)
: (value >= minimal)
? tr::lng_gift_sell_amount(
: tr::lng_gift_sell_min_price(
tr::now,
lt_count,
starsMin,
Ui::Text::RichLangValue))
: enough
? (value->ton()
? tr::lng_gift_sell_amount_ton(
tr::now,
lt_count,
receive,
Ui::Text::RichLangValue)
: tr::lng_gift_sell_amount(
tr::now,
lt_count,
receive,
Ui::Text::RichLangValue))
: tr::lng_gift_sell_about(
tr::now,
lt_percent,
@ -4603,37 +4666,50 @@ void UniqueGiftSellBox(
box->verticalLayout()->resizeToWidth(box->width());
}),
st::boxLabel));
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(container);
Ui::AddSkip(container);
box->addRow(object_ptr<Ui::PlainShadow>(box));
Ui::AddSkip(container);
Ui::AddSkip(container);
const auto onlyTon = box->addRow(
object_ptr<Ui::Checkbox>(
box,
tr::lng_gift_sell_only_ton(tr::now),
state->onlyTon.current(),
st::defaultCheckbox));
state->onlyTon = onlyTon->checkedValue();
Ui::AddSkip(container);
box->addRow(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_gift_sell_only_ton_about(Ui::Text::RichLangValue),
st::boxDividerLabel));
Ui::AddSkip(container);
rpl::duplicate(goods) | rpl::start_with_next([=](bool good) {
details->setTextColorOverride(
good ? st::windowSubTextFg->c : st::boxTextFgError->c);
}, details->lifetime());
QObject::connect(field, &NumberInput::submitted, [=] {
const auto count = field->getLastText().toInt();
if (count < minimal) {
field->showError();
errors->fire({});
const auto submit = [=] {
const auto value = state->computePrice();
if (!value) {
state->errors.fire({});
return;
}
box->closeBox();
UpdateGiftSellPrice(show, unique, savedId, count);
});
const auto button = box->addButton(priceNow
UpdateGiftSellPrice(show, unique, savedId, *value);
};
std::move(
priceInput.submits
) | rpl::start_with_next(submit, details->lifetime());
auto submitText = priceNow
? tr::lng_gift_sell_update()
: tr::lng_gift_sell_put(), [=] { field->submitted({}); });
rpl::combine(
box->widthValue(),
button->widthValue()
) | rpl::start_with_next([=](int outer, int inner) {
const auto padding = st::giftBox.buttonPadding;
const auto wanted = outer - padding.left() - padding.right();
if (inner != wanted) {
button->resizeToWidth(wanted);
button->moveToLeft(padding.left(), padding.top());
}
}, box->lifetime());
: tr::lng_gift_sell_put();
box->addButton(std::move(submitText), submit);
}
void ShowUniqueGiftSellBox(
@ -4874,17 +4950,6 @@ void UpgradeBox(
st::creditsBoxButtonLabel,
&st::giftBox.button.textFg);
}
rpl::combine(
box->widthValue(),
button->widthValue()
) | rpl::start_with_next([=](int outer, int inner) {
const auto padding = st::giftBox.buttonPadding;
const auto wanted = outer - padding.left() - padding.right();
if (inner != wanted) {
button->resizeToWidth(wanted);
button->moveToLeft(padding.left(), padding.top());
}
}, box->lifetime());
AddUniqueCloseButton(box, {});
}

View File

@ -78,7 +78,7 @@ void UpdateGiftSellPrice(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,
Data::SavedStarGiftId savedId,
int price);
CreditsAmount price);
void ShowUniqueGiftSellBox(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,

View File

@ -1533,6 +1533,18 @@ bool ResolveStarsSettings(
return true;
}
bool ResolveTonSettings(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
controller->showSettings(::Settings::CurrencyId());
controller->window().activate();
return true;
}
} // namespace
const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
@ -1637,6 +1649,10 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
u"^stars/?(^\\?.*)?(#|$)"_q,
ResolveStarsSettings
},
{
u"^ton/?(^\\?.*)?(#|$)"_q,
ResolveTonSettings
},
{
u"^([^\\?]+)(\\?|#|$)"_q,
HandleUnknown

View File

@ -45,10 +45,11 @@ struct UniqueGift {
QString ownerName;
PeerId ownerId = 0;
PeerData *releasedBy = nullptr;
int number = 0;
int starsForTransfer = -1;
int64 nanoTonForResale = -1;
int starsForResale = -1;
int64 tonForResale = -1;
int starsForTransfer = -1;
int number = 0;
bool onlyAcceptTon = false;
TimeId exportAt = 0;
TimeId canTransferAt = 0;
TimeId canResellAt = 0;

View File

@ -48,6 +48,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_settings.h"
namespace HistoryView {
namespace {
[[nodiscard]] rpl::producer<CreditsAmount> StarsPriceValue(
rpl::producer<CreditsAmount> full) {
return rpl::single(
CreditsAmount()
) | rpl::then(std::move(
full
) | rpl::filter([=](CreditsAmount amount) {
return amount.stars();
}));
}
[[nodiscard]] rpl::producer<CreditsAmount> TonPriceValue(
rpl::producer<CreditsAmount> full) {
return rpl::single(
CreditsAmount()
) | rpl::then(std::move(
full
) | rpl::filter([=](CreditsAmount amount) {
return amount.ton();
}));
}
} // namespace
void ChooseSuggestTimeBox(
not_null<Ui::GenericBox*> box,
@ -112,6 +137,214 @@ void AddApproximateUsd(
usd->widthValue() | rpl::start_with_next(move, usd->lifetime());
}
StarsTonPriceInput AddStarsTonPriceInput(
not_null<Ui::VerticalLayout*> container,
StarsTonPriceArgs &&args) {
struct State {
rpl::variable<bool> ton;
rpl::variable<CreditsAmount> price;
rpl::event_stream<> updates;
rpl::event_stream<> submits;
};
const auto state = container->lifetime().make_state<State>();
state->ton = std::move(args.showTon);
state->price = args.price;
const auto session = args.session;
const auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding;
const auto manager = &session->data().customEmojiManager();
const auto makeIcon = [&](
not_null<QWidget*> parent,
TextWithEntities text) {
return Ui::CreateChild<Ui::FlatLabel>(
parent,
rpl::single(text),
st::defaultFlatLabel,
st::defaultPopupMenu,
Core::TextContext({ .session = session }));
};
const auto starsWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
const auto starsInner = starsWrap->entity();
Ui::AddSubsectionTitle(
starsInner,
tr::lng_suggest_options_stars_price(),
QMargins(
added.left(),
0,
added.right(),
-st::defaultSubsectionTitlePadding.bottom()));
const auto starsFieldWrap = starsInner->add(
object_ptr<Ui::FixedHeightWidget>(
starsInner,
st::editTagField.heightMin),
st::boxRowPadding);
auto ownedStarsField = object_ptr<Ui::NumberInput>(
starsFieldWrap,
st::editTagField,
rpl::single(u"0"_q),
((args.price && args.price.stars())
? QString::number(args.price.whole())
: QString()),
args.starsMax);
const auto starsField = ownedStarsField.data();
const auto starsIcon = makeIcon(starsField, manager->creditsEmoji());
starsFieldWrap->widthValue() | rpl::start_with_next([=](int width) {
starsIcon->move(st::starsFieldIconPosition);
starsField->move(0, 0);
starsField->resize(width, starsField->height());
starsFieldWrap->resize(width, starsField->height());
}, starsFieldWrap->lifetime());
AddApproximateUsd(
starsField,
session,
StarsPriceValue(state->price.value()));
Ui::AddSkip(starsInner);
Ui::AddSkip(starsInner);
if (args.starsAbout) {
Ui::AddDividerText(starsInner, std::move(args.starsAbout));
}
const auto tonWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
const auto tonInner = tonWrap->entity();
Ui::AddSubsectionTitle(
tonInner,
tr::lng_suggest_options_ton_price(),
QMargins(
added.left(),
0,
added.right(),
-st::defaultSubsectionTitlePadding.bottom()));
const auto tonFieldWrap = tonInner->add(
object_ptr<Ui::FixedHeightWidget>(
tonInner,
st::editTagField.heightMin),
st::boxRowPadding);
auto ownedTonField = object_ptr<Ui::InputField>::fromRaw(
Ui::CreateTonAmountInput(
tonFieldWrap,
rpl::single('0' + Ui::TonAmountSeparator() + '0'),
((args.price && args.price.ton())
? (args.price.whole() * Ui::kNanosInOne + args.price.nano())
: 0)));
const auto tonField = ownedTonField.data();
const auto tonIcon = makeIcon(tonField, Ui::Text::SingleCustomEmoji(
manager->registerInternalEmoji(
Ui::Earn::IconCurrencyColored(
st::tonFieldIconSize,
st::currencyFg->c),
st::channelEarnCurrencyCommonMargins,
false)));
tonFieldWrap->widthValue() | rpl::start_with_next([=](int width) {
tonIcon->move(st::tonFieldIconPosition);
tonField->move(0, 0);
tonField->resize(width, tonField->height());
tonFieldWrap->resize(width, tonField->height());
}, tonFieldWrap->lifetime());
AddApproximateUsd(
tonField,
session,
TonPriceValue(state->price.value()));
Ui::AddSkip(tonInner);
Ui::AddSkip(tonInner);
if (args.tonAbout) {
Ui::AddDividerText(tonInner, std::move(args.tonAbout));
}
tonWrap->toggleOn(state->ton.value(), anim::type::instant);
starsWrap->toggleOn(
state->ton.value() | rpl::map(!rpl::mappers::_1),
anim::type::instant);
auto computeResult = [=]() -> std::optional<CreditsAmount> {
auto nanos = int64();
const auto ton = uint32(state->ton.current() ? 1 : 0);
if (ton) {
const auto text = tonField->getLastText();
const auto now = Ui::ParseTonAmountString(text);
if (now
&& *now
&& ((*now < args.nanoTonMin) || (*now > args.nanoTonMax))) {
tonField->showError();
return {};
}
nanos = now.value_or(0);
} else {
const auto now = starsField->getLastText().toLongLong();
if (now && (now < args.starsMin || now > args.starsMax)) {
starsField->showError();
return {};
}
nanos = now * Ui::kNanosInOne;
}
return CreditsAmount(
nanos / Ui::kNanosInOne,
nanos % Ui::kNanosInOne,
ton ? CreditsType::Ton : CreditsType::Stars);
};
const auto updatePrice = [=] {
if (auto result = computeResult()) {
state->price = *result;
}
state->updates.fire({});
};
QObject::connect(
starsField,
&Ui::NumberInput::changed,
starsField,
updatePrice);
tonField->changes(
) | rpl::start_with_next(updatePrice, tonField->lifetime());
state->ton.changes(
) | rpl::start_with_next(updatePrice, container->lifetime());
QObject::connect(
starsField,
&Ui::NumberInput::submitted,
container,
[=] { state->submits.fire({}); });
tonField->submits(
) | rpl::to_empty | rpl::start_to_stream(
state->submits,
tonField->lifetime());
auto focusCallback = [=] {
if (state->ton.current()) {
tonField->selectAll();
tonField->setFocusFast();
} else {
starsField->selectAll();
starsField->setFocusFast();
}
};
return {
.focusCallback = std::move(focusCallback),
.computeResult = std::move(computeResult),
.submits = state->submits.events(),
.updates = state->updates.events(),
.result = state->price.value(),
};
}
void ChooseSuggestPriceBox(
not_null<Ui::GenericBox*> box,
SuggestPriceBoxArgs &&args) {
@ -135,42 +368,17 @@ void ChooseSuggestPriceBox(
state->date = args.value.date;
state->ton = (args.value.ton != 0);
state->price = args.value.price();
const auto updatePrice = [=] {
if (const auto price = state->computePrice()) {
state->price = *price;
}
};
const auto starsPrice = [=] {
return rpl::single(
CreditsAmount()
) | rpl::then(state->price.value(
) | rpl::filter([=](CreditsAmount amount) {
return amount.stars();
}));
};
const auto tonPrice = [=] {
return rpl::single(
CreditsAmount(0, 0, CreditsType::Ton)
) | rpl::then(state->price.value(
) | rpl::filter([=](CreditsAmount amount) {
return amount.ton();
}));
};
const auto peer = args.peer;
const auto admin = peer->amMonoforumAdmin();
const auto broadcast = peer->monoforumBroadcast();
const auto usePeer = broadcast ? broadcast : peer;
const auto session = &peer->session();
const auto &appConfig = session->appConfig();
if (!admin) {
session->credits().load();
session->credits().tonLoad();
}
const auto starsMin = session->appConfig().suggestedPostStarsMin();
const auto starsMax = session->appConfig().suggestedPostStarsMax();
const auto nanoTonMin = session->appConfig().suggestedPostNanoTonMin();
const auto nanoTonMax = session->appConfig().suggestedPostNanoTonMax();
const auto container = box->verticalLayout();
box->setStyle(st::suggestPriceBox);
@ -267,7 +475,6 @@ void ChooseSuggestPriceBox(
state->buttons[i].active = true;
state->buttons[1 - i].active = false;
buttons->update();
updatePrice();
break;
}
}
@ -299,64 +506,11 @@ void ChooseSuggestPriceBox(
Ui::AddSkip(container);
const auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding;
const auto manager = &session->data().customEmojiManager();
const auto makeIcon = [&](
not_null<QWidget*> parent,
TextWithEntities text) {
return Ui::CreateChild<Ui::FlatLabel>(
parent,
rpl::single(text),
st::defaultFlatLabel,
st::defaultPopupMenu,
Core::TextContext({ .session = session }));
const auto computePrice = [session](CreditsAmount amount) {
return PriceAfterCommission(session, amount).value();
};
const auto starsWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
const auto starsInner = starsWrap->entity();
Ui::AddSubsectionTitle(
starsInner,
tr::lng_suggest_options_stars_price(),
QMargins(added.left(), 0, added.right(), -st::defaultSubsectionTitlePadding.bottom()));
const auto starsFieldWrap = starsInner->add(
object_ptr<Ui::FixedHeightWidget>(
box,
st::editTagField.heightMin),
st::boxRowPadding);
auto ownedStarsField = object_ptr<Ui::NumberInput>(
starsFieldWrap,
st::editTagField,
rpl::single(u"0"_q),
((args.value.exists && args.value.priceWhole && !args.value.ton)
? QString::number(args.value.priceWhole)
: QString()),
starsMax);
const auto starsField = ownedStarsField.data();
const auto starsIcon = makeIcon(starsField, manager->creditsEmoji());
QObject::connect(starsField, &Ui::NumberInput::changed, updatePrice);
starsFieldWrap->widthValue() | rpl::start_with_next([=](int width) {
starsIcon->move(st::starsFieldIconPosition);
starsField->move(0, 0);
starsField->resize(width, starsField->height());
starsFieldWrap->resize(width, starsField->height());
}, starsFieldWrap->lifetime());
AddApproximateUsd(starsField, session, starsPrice());
Ui::AddSkip(starsInner);
Ui::AddSkip(starsInner);
const auto computePrice = [peer = args.peer](CreditsAmount amount) {
return PriceAfterCommission(&peer->session(), amount).value();
};
const auto formatCommission = [peer = args.peer](CreditsAmount amount) {
return FormatAfterCommissionPercent(&peer->session(), amount);
const auto formatCommission = [session](CreditsAmount amount) {
return FormatAfterCommissionPercent(session, amount);
};
const auto youGet = [=](rpl::producer<CreditsAmount> price, bool stars) {
return (stars
@ -367,109 +521,34 @@ void ChooseSuggestPriceBox(
lt_percent,
rpl::duplicate(price) | rpl::map(formatCommission));
};
Ui::AddDividerText(starsInner, admin
auto starsAbout = admin
? rpl::combine(
youGet(starsPrice(), true),
youGet(StarsPriceValue(state->price.value()), true),
tr::lng_suggest_options_stars_warning(Ui::Text::RichLangValue)
) | rpl::map([=](const QString &t1, const TextWithEntities &t2) {
return TextWithEntities{ t1 }.append("\n\n").append(t2);
})
: tr::lng_suggest_options_stars_price_about(Ui::Text::WithEntities));
const auto tonWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
const auto tonInner = tonWrap->entity();
Ui::AddSubsectionTitle(
tonInner,
tr::lng_suggest_options_ton_price(),
QMargins(added.left(), 0, added.right(), -st::defaultSubsectionTitlePadding.bottom()));
const auto tonFieldWrap = tonInner->add(
object_ptr<Ui::FixedHeightWidget>(
box,
st::editTagField.heightMin),
st::boxRowPadding);
auto ownedTonField = object_ptr<Ui::InputField>::fromRaw(
Ui::CreateTonAmountInput(
tonFieldWrap,
rpl::single('0' + Ui::TonAmountSeparator() + '0'),
((args.value.price() && args.value.ton)
? (int64(args.value.priceWhole) * Ui::kNanosInOne
+ int64(args.value.priceNano))
: 0)));
const auto tonField = ownedTonField.data();
const auto tonIcon = makeIcon(tonField, Ui::Text::SingleCustomEmoji(
manager->registerInternalEmoji(
Ui::Earn::IconCurrencyColored(
st::tonFieldIconSize,
st::currencyFg->c),
st::channelEarnCurrencyCommonMargins,
false)));
tonField->changes(
) | rpl::start_with_next(updatePrice, tonField->lifetime());
tonFieldWrap->widthValue() | rpl::start_with_next([=](int width) {
tonIcon->move(st::tonFieldIconPosition);
tonField->move(0, 0);
tonField->resize(width, tonField->height());
tonFieldWrap->resize(width, tonField->height());
}, tonFieldWrap->lifetime());
AddApproximateUsd(tonField, session, tonPrice());
Ui::AddSkip(tonInner);
Ui::AddSkip(tonInner);
Ui::AddDividerText(
tonInner,
(admin
? youGet(tonPrice(), false)
: tr::lng_suggest_options_ton_price_about()));
tonWrap->toggleOn(state->ton.value(), anim::type::instant);
starsWrap->toggleOn(
state->ton.value() | rpl::map(!rpl::mappers::_1),
anim::type::instant);
state->computePrice = [=]() -> std::optional<CreditsAmount> {
auto nanos = int64();
const auto ton = uint32(state->ton.current() ? 1 : 0);
if (ton) {
const auto text = tonField->getLastText();
const auto now = Ui::ParseTonAmountString(text);
if (now
&& *now
&& ((*now < nanoTonMin) || (*now > nanoTonMax))) {
tonField->showError();
return {};
}
nanos = now.value_or(0);
} else {
const auto now = starsField->getLastText().toLongLong();
if (now && (now < starsMin || now > starsMax)) {
starsField->showError();
return {};
}
nanos = now * Ui::kNanosInOne;
}
return CreditsAmount(
nanos / Ui::kNanosInOne,
nanos % Ui::kNanosInOne,
ton ? CreditsType::Ton : CreditsType::Stars);
};
box->setFocusCallback([=] {
if (state->ton.current()) {
tonField->selectAll();
tonField->setFocusFast();
} else {
starsField->selectAll();
starsField->setFocusFast();
}
: tr::lng_suggest_options_stars_price_about(Ui::Text::WithEntities);
auto tonAbout = admin
? youGet(
TonPriceValue(state->price.value()),
false
) | Ui::Text::ToWithEntities()
: tr::lng_suggest_options_ton_price_about(Ui::Text::WithEntities);
auto priceInput = AddStarsTonPriceInput(container, {
.session = session,
.showTon = state->ton.value(),
.price = args.value.price(),
.starsMin = appConfig.suggestedPostStarsMin(),
.starsMax = appConfig.suggestedPostStarsMax(),
.nanoTonMin = appConfig.suggestedPostNanoTonMin(),
.nanoTonMax = appConfig.suggestedPostNanoTonMax(),
.starsAbout = std::move(starsAbout),
.tonAbout = std::move(tonAbout),
});
state->price = std::move(priceInput.result);
state->computePrice = std::move(priceInput.computeResult);
box->setFocusCallback(std::move(priceInput.focusCallback));
Ui::AddSkip(container);
@ -568,17 +647,13 @@ void ChooseSuggestPriceBox(
}
}, box->lifetime());
QObject::connect(
starsField,
&Ui::NumberInput::submitted,
box,
state->save);
tonField->submits(
) | rpl::start_with_next(state->save, tonField->lifetime());
std::move(
priceInput.submits
) | rpl::start_with_next(state->save, box->lifetime());
const auto button = box->addButton(rpl::single(QString()), state->save);
const auto coloredTonIcon = Ui::Text::SingleCustomEmoji(
manager->registerInternalEmoji(
session->data().customEmojiManager().registerInternalEmoji(
Ui::Earn::IconCurrencyColored(
st::tonFieldIconSize,
st::currencyFg->c),

View File

@ -15,6 +15,7 @@ class Show;
namespace Ui {
class GenericBox;
class VerticalLayout;
} // namespace Ui
namespace Main {
@ -43,6 +44,30 @@ void ChooseSuggestTimeBox(
not_null<Ui::GenericBox*> box,
SuggestTimeBoxArgs &&args);
struct StarsTonPriceInput {
Fn<void()> focusCallback;
Fn<std::optional<CreditsAmount>()> computeResult;
rpl::producer<> submits;
rpl::producer<> updates;
rpl::producer<CreditsAmount> result;
};
struct StarsTonPriceArgs {
not_null<Main::Session*> session;
rpl::producer<bool> showTon;
CreditsAmount price;
int starsMin = 0;
int starsMax = 0;
int64 nanoTonMin = 0;
int64 nanoTonMax = 0;
rpl::producer<TextWithEntities> starsAbout;
rpl::producer<TextWithEntities> tonAbout;
};
[[nodiscard]] StarsTonPriceInput AddStarsTonPriceInput(
not_null<Ui::VerticalLayout*> container,
StarsTonPriceArgs &&args);
struct SuggestPriceBoxArgs {
not_null<PeerData*> peer;
bool updating = false;

View File

@ -96,6 +96,13 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {
_mediaLifetime.destroy();
unsubscribe();
const auto format = [=](int64 number) {
const auto onlyK = (number < 100'000'000);
return (number >= 1'000'000)
? Lang::FormatCountToShort(number, onlyK).string
: Lang::FormatCountDecimal(number);
};
_descriptor = descriptor;
_resalePrice = resalePrice;
const auto resale = (_resalePrice > 0);
@ -156,19 +163,18 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {
? (unique
? _delegate->monostar()
: _delegate->star()).append(' ').append(
Lang::FormatCountDecimal(unique
format(unique
? unique->starsForResale
: data.info.starsResellMin)
).append(data.info.resellCount > 1 ? "+" : "")
: (_small && unique && unique->starsForResale)
? _delegate->monostar().append(' ').append(
Lang::FormatCountDecimal(unique->starsForResale))
format(unique->starsForResale))
: unique
? tr::lng_gift_transfer_button(
tr::now,
Ui::Text::WithEntities)
: _delegate->star().append(
' ' + Lang::FormatCountDecimal(data.info.stars))),
: _delegate->star().append(' ' + format(data.info.stars))),
kMarkupTextOptions,
_delegate->textContext());
if (!_stars) {

View File

@ -917,7 +917,7 @@ int NonZeroPartToInt(QString value) {
: (value.isEmpty() ? 0 : value.toInt());
}
ShortenedCount FormatCountToShort(int64 number) {
ShortenedCount FormatCountToShort(int64 number, bool onlyK) {
auto result = ShortenedCount{ number };
const auto abs = std::abs(number);
const auto shorten = [&](int64 divider, char multiplier) {
@ -934,7 +934,7 @@ ShortenedCount FormatCountToShort(int64 number) {
result.number = rounded * divider;
result.shortened = true;
};
if (abs >= 1'000'000) {
if (!onlyK && abs >= 1'000'000) {
shorten(1'000'000, 'M');
} else if (abs >= 10'000) {
shorten(1'000, 'K');

View File

@ -26,7 +26,9 @@ struct ShortenedCount {
QString string;
bool shortened = false;
};
[[nodiscard]] ShortenedCount FormatCountToShort(int64 number);
[[nodiscard]] ShortenedCount FormatCountToShort(
int64 number,
bool onlyK = false);
[[nodiscard]] QString FormatCountDecimal(int64 number);
[[nodiscard]] QString FormatExactCountDecimal(float64 number);
[[nodiscard]] ShortenedCount FormatCreditsAmountToShort(

View File

@ -148,18 +148,32 @@ bool AppConfig::confcallPrioritizeVP8() const {
return get<bool>(u"confcall_use_vp8"_q, false);
}
int AppConfig::giftResalePriceMax() const {
return get<int>(u"stars_stargift_resale_amount_max"_q, 35000);
}
int AppConfig::giftResalePriceMin() const {
int AppConfig::giftResaleStarsMin() const {
return get<int>(u"stars_stargift_resale_amount_min"_q, 125);
}
int AppConfig::giftResaleReceiveThousandths() const {
int AppConfig::giftResaleStarsMax() const {
return get<int>(u"stars_stargift_resale_amount_max"_q, 35000);
}
int AppConfig::giftResaleStarsThousandths() const {
return get<int>(u"stars_stargift_resale_commission_permille"_q, 800);
}
int64 AppConfig::giftResaleNanoTonMin() const {
return get<int64>(u"ton_stargift_resale_amount_min"_q, 250'000'000LL);
}
int64 AppConfig::giftResaleNanoTonMax() const {
return get<int64>(
u"ton_stargift_resale_amount_max"_q,
1'000'000'000'000'000LL);
}
int AppConfig::giftResaleNanoTonThousandths() const {
return get<int>(u"ton_stargift_resale_commission_permille"_q, 800);
}
int AppConfig::pollOptionsLimit() const {
return get<int>(u"poll_answers_max"_q, 12);
}

View File

@ -85,9 +85,12 @@ public:
[[nodiscard]] int confcallSizeLimit() const;
[[nodiscard]] bool confcallPrioritizeVP8() const;
[[nodiscard]] int giftResalePriceMax() const;
[[nodiscard]] int giftResalePriceMin() const;
[[nodiscard]] int giftResaleReceiveThousandths() const;
[[nodiscard]] int giftResaleStarsMin() const;
[[nodiscard]] int giftResaleStarsMax() const;
[[nodiscard]] int giftResaleStarsThousandths() const;
[[nodiscard]] int64 giftResaleNanoTonMin() const;
[[nodiscard]] int64 giftResaleNanoTonMax() const;
[[nodiscard]] int giftResaleNanoTonThousandths() const;
[[nodiscard]] int pollOptionsLimit() const;
[[nodiscard]] int todoListItemsLimit() const;

View File

@ -1081,7 +1081,7 @@ void FillUniqueGiftMenu(
const auto name = UniqueGiftName(*unique);
const auto confirm = [=](Fn<void()> close) {
close();
Ui::UpdateGiftSellPrice(show, unique, savedId, 0);
Ui::UpdateGiftSellPrice(show, unique, savedId, {});
};
show->show(Ui::MakeConfirmBox({
.text = tr::lng_gift_sell_unlist_sure(),