From aa499eab611afac05b06cd12dbdb70633470dc51 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 29 Jul 2025 17:15:48 +0400 Subject: [PATCH] Allow settings gift price in TON. --- Telegram/Resources/langs/lang.strings | 12 +- Telegram/SourceFiles/api/api_premium.cpp | 5 +- Telegram/SourceFiles/boxes/star_gift_box.cpp | 281 ++++++----- Telegram/SourceFiles/boxes/star_gift_box.h | 2 +- .../SourceFiles/core/local_url_handlers.cpp | 16 + Telegram/SourceFiles/data/data_star_gift.h | 7 +- .../controls/history_view_suggest_options.cpp | 453 ++++++++++-------- .../controls/history_view_suggest_options.h | 25 + .../peer_gifts/info_peer_gifts_common.cpp | 14 +- Telegram/SourceFiles/lang/lang_tag.cpp | 4 +- Telegram/SourceFiles/lang/lang_tag.h | 4 +- Telegram/SourceFiles/main/main_app_config.cpp | 26 +- Telegram/SourceFiles/main/main_app_config.h | 9 +- .../settings/settings_credits_graphics.cpp | 2 +- 14 files changed, 539 insertions(+), 321 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 6e240e929e..058a3b68fd 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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!"; diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 99834cc949..a707c624b7 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -895,9 +895,10 @@ std::optional 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, }), diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index dd45785c99..fd46e1813a 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -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 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 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 ResalePrices( not_null 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 show, std::shared_ptr 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 onlyTon; + rpl::variable price; + Fn()> computePrice; + rpl::event_stream<> errors; + }; + const auto state = box->lifetime().make_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( - box, - st::editTagField.heightMin)); - auto owned = object_ptr( - 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( - tr::now, - lt_count, - minimal, - Ui::Text::RichLangValue) - : (value >= minimal) - ? tr::lng_gift_sell_amount( - tr::now, - lt_count, - receive, - Ui::Text::RichLangValue) + 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, + nanoTonMin / float64(Ui::kNanosInOne), + Ui::Text::RichLangValue) + : 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(box)); + Ui::AddSkip(container); + Ui::AddSkip(container); + + const auto onlyTon = box->addRow( + object_ptr( + 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( + 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, {}); } diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index b3df5b5b87..5b42d785d3 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -78,7 +78,7 @@ void UpdateGiftSellPrice( std::shared_ptr show, std::shared_ptr unique, Data::SavedStarGiftId savedId, - int price); + CreditsAmount price); void ShowUniqueGiftSellBox( std::shared_ptr show, std::shared_ptr unique, diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index a8b81f39c9..ec18169166 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -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 &LocalUrlHandlers() { @@ -1637,6 +1649,10 @@ const std::vector &LocalUrlHandlers() { u"^stars/?(^\\?.*)?(#|$)"_q, ResolveStarsSettings }, + { + u"^ton/?(^\\?.*)?(#|$)"_q, + ResolveTonSettings + }, { u"^([^\\?]+)(\\?|#|$)"_q, HandleUnknown diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index 750679b129..122f7b5b54 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -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; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp index 0784d82782..debbfc8a2b 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp @@ -48,6 +48,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_settings.h" namespace HistoryView { +namespace { + +[[nodiscard]] rpl::producer StarsPriceValue( + rpl::producer full) { + return rpl::single( + CreditsAmount() + ) | rpl::then(std::move( + full + ) | rpl::filter([=](CreditsAmount amount) { + return amount.stars(); + })); +} + +[[nodiscard]] rpl::producer TonPriceValue( + rpl::producer full) { + return rpl::single( + CreditsAmount() + ) | rpl::then(std::move( + full + ) | rpl::filter([=](CreditsAmount amount) { + return amount.ton(); + })); +} + +} // namespace void ChooseSuggestTimeBox( not_null box, @@ -112,6 +137,214 @@ void AddApproximateUsd( usd->widthValue() | rpl::start_with_next(move, usd->lifetime()); } +StarsTonPriceInput AddStarsTonPriceInput( + not_null container, + StarsTonPriceArgs &&args) { + struct State { + rpl::variable ton; + rpl::variable price; + rpl::event_stream<> updates; + rpl::event_stream<> submits; + }; + const auto state = container->lifetime().make_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 parent, + TextWithEntities text) { + return Ui::CreateChild( + parent, + rpl::single(text), + st::defaultFlatLabel, + st::defaultPopupMenu, + Core::TextContext({ .session = session })); + }; + + const auto starsWrap = container->add( + object_ptr>( + container, + object_ptr(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( + starsInner, + st::editTagField.heightMin), + st::boxRowPadding); + auto ownedStarsField = object_ptr( + 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>( + container, + object_ptr(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( + tonInner, + st::editTagField.heightMin), + st::boxRowPadding); + auto ownedTonField = object_ptr::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 { + 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 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 parent, - TextWithEntities text) { - return Ui::CreateChild( - 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>( - container, - object_ptr(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( - box, - st::editTagField.heightMin), - st::boxRowPadding); - auto ownedStarsField = object_ptr( - 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 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>( - container, - object_ptr(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( - box, - st::editTagField.heightMin), - st::boxRowPadding); - auto ownedTonField = object_ptr::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 { - 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), diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h index 7b270393fd..65eb6c7f9f 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h @@ -15,6 +15,7 @@ class Show; namespace Ui { class GenericBox; +class VerticalLayout; } // namespace Ui namespace Main { @@ -43,6 +44,30 @@ void ChooseSuggestTimeBox( not_null box, SuggestTimeBoxArgs &&args); +struct StarsTonPriceInput { + Fn focusCallback; + Fn()> computeResult; + rpl::producer<> submits; + rpl::producer<> updates; + rpl::producer result; +}; + +struct StarsTonPriceArgs { + not_null session; + rpl::producer showTon; + CreditsAmount price; + int starsMin = 0; + int starsMax = 0; + int64 nanoTonMin = 0; + int64 nanoTonMax = 0; + rpl::producer starsAbout; + rpl::producer tonAbout; +}; + +[[nodiscard]] StarsTonPriceInput AddStarsTonPriceInput( + not_null container, + StarsTonPriceArgs &&args); + struct SuggestPriceBoxArgs { not_null peer; bool updating = false; diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp index e25463f3f9..ef72cb7770 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -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) { diff --git a/Telegram/SourceFiles/lang/lang_tag.cpp b/Telegram/SourceFiles/lang/lang_tag.cpp index 5136a67a5e..d05d91709c 100644 --- a/Telegram/SourceFiles/lang/lang_tag.cpp +++ b/Telegram/SourceFiles/lang/lang_tag.cpp @@ -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'); diff --git a/Telegram/SourceFiles/lang/lang_tag.h b/Telegram/SourceFiles/lang/lang_tag.h index 3d0f231186..74088d9602 100644 --- a/Telegram/SourceFiles/lang/lang_tag.h +++ b/Telegram/SourceFiles/lang/lang_tag.h @@ -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( diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 21c6d1eb0c..5f9183797e 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -148,18 +148,32 @@ bool AppConfig::confcallPrioritizeVP8() const { return get(u"confcall_use_vp8"_q, false); } -int AppConfig::giftResalePriceMax() const { - return get(u"stars_stargift_resale_amount_max"_q, 35000); -} - -int AppConfig::giftResalePriceMin() const { +int AppConfig::giftResaleStarsMin() const { return get(u"stars_stargift_resale_amount_min"_q, 125); } -int AppConfig::giftResaleReceiveThousandths() const { +int AppConfig::giftResaleStarsMax() const { + return get(u"stars_stargift_resale_amount_max"_q, 35000); +} + +int AppConfig::giftResaleStarsThousandths() const { return get(u"stars_stargift_resale_commission_permille"_q, 800); } +int64 AppConfig::giftResaleNanoTonMin() const { + return get(u"ton_stargift_resale_amount_min"_q, 250'000'000LL); +} + +int64 AppConfig::giftResaleNanoTonMax() const { + return get( + u"ton_stargift_resale_amount_max"_q, + 1'000'000'000'000'000LL); +} + +int AppConfig::giftResaleNanoTonThousandths() const { + return get(u"ton_stargift_resale_commission_permille"_q, 800); +} + int AppConfig::pollOptionsLimit() const { return get(u"poll_answers_max"_q, 12); } diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index 78bde39f9c..11f6b66f34 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -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; diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index decb554373..a3d63a2ac0 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -1081,7 +1081,7 @@ void FillUniqueGiftMenu( const auto name = UniqueGiftName(*unique); const auto confirm = [=](Fn close) { close(); - Ui::UpdateGiftSellPrice(show, unique, savedId, 0); + Ui::UpdateGiftSellPrice(show, unique, savedId, {}); }; show->show(Ui::MakeConfirmBox({ .text = tr::lng_gift_sell_unlist_sure(),