2
0
mirror of https://github.com/telegramdesktop/tdesktop synced 2025-08-23 02:37:11 +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_send_button_self" = "Buy a Gift for {cost}";
"lng_gift_buy_resale_title" = "Buy {name}"; "lng_gift_buy_resale_title" = "Buy {name}";
"lng_gift_buy_resale_button" = "Buy for {cost}"; "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" = "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_resale_confirm_self" = "Do you want to buy {name} for {price}?";
"lng_gift_buy_price_change_title" = "Price change!"; "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_title" = "Unlist {name}";
"lng_gift_sell_unlist_sure" = "Are you sure you want to unlist your gift?"; "lng_gift_sell_unlist_sure" = "Are you sure you want to unlist your gift?";
"lng_gift_sell_title" = "Price in Stars"; "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_about" = "You will receive {percent} of the selected amount.";
"lng_gift_sell_amount#one" = "You will receive **{count}** Star."; "lng_gift_sell_amount#one" = "You will receive **{count}** Star.";
"lng_gift_sell_amount#other" = "You will receive **{count}** Stars."; "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#one" = "Minimum price is {count} Star.";
"lng_gift_sell_min_price#other" = "Minimum price is {count} Stars."; "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_put" = "Put for Sale";
"lng_gift_sell_update" = "Update the Price"; "lng_gift_sell_update" = "Update the Price";
"lng_gift_sell_toast" = "{name} is now for sale!"; "lng_gift_sell_toast" = "{name} is now for sale!";

View File

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

View File

@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h" #include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_custom_emoji.h"
#include "history/admin_log/history_admin_log_item.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_media_generic.h"
#include "history/view/media/history_view_unique_gift.h" #include "history/view/media/history_view_unique_gift.h"
#include "history/view/history_view_element.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_style.h"
#include "ui/chat/chat_theme.h" #include "ui/chat/chat_theme.h"
#include "ui/controls/emoji_button.h" #include "ui/controls/emoji_button.h"
#include "ui/controls/ton_common.h"
#include "ui/controls/userpic_button.h" #include "ui/controls/userpic_button.h"
#include "ui/effects/path_shift_gradient.h" #include "ui/effects/path_shift_gradient.h"
#include "ui/effects/premium_graphics.h" #include "ui/effects/premium_graphics.h"
@ -233,6 +235,37 @@ struct SessionResalePrices {
crl::time lastReceived = 0; 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( [[nodiscard]] not_null<SessionResalePrices*> ResalePrices(
not_null<Main::Session*> session) { not_null<Main::Session*> session) {
static auto result = base::flat_map< static auto result = base::flat_map<
@ -4403,19 +4436,6 @@ void ShowUniqueGiftWearBox(
session, session,
st::creditsBoxButtonLabel, st::creditsBoxButtonLabel,
&st::giftBox.button.textFg); &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, {}); AddUniqueCloseButton(box, {});
})); }));
} }
@ -4473,12 +4493,14 @@ void UpdateGiftSellPrice(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique, std::shared_ptr<Data::UniqueGift> unique,
Data::SavedStarGiftId savedId, Data::SavedStarGiftId savedId,
int price) { CreditsAmount price) {
const auto was = unique->starsForResale; const auto was = unique->starsForResale;
const auto session = &show->session(); const auto session = &show->session();
session->api().request(MTPpayments_UpdateStarGiftPrice( session->api().request(MTPpayments_UpdateStarGiftPrice(
Api::InputSavedStarGiftId(savedId, unique), 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) { )).done([=](const MTPUpdates &result) {
session->api().applyUpdates(result); session->api().applyUpdates(result);
show->showToast((!price show->showToast((!price
@ -4489,8 +4511,26 @@ void UpdateGiftSellPrice(
tr::now, tr::now,
lt_name, lt_name,
Data::UniqueGiftName(*unique))); Data::UniqueGiftName(*unique)));
const auto setStars = [&](CreditsAmount amount) {
unique->starsForResale = price; 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({ session->data().notifyGiftUpdate({
.id = savedId, .id = savedId,
.slug = unique->slug, .slug = unique->slug,
@ -4517,80 +4557,103 @@ void UniqueGiftSellBox(
Data::SavedStarGiftId savedId, Data::SavedStarGiftId savedId,
int price, int price,
Settings::GiftWearBoxStyleOverride st) { 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->setStyle(st.box ? *st.box : st::upgradeGiftBox);
box->setWidth(st::boxWideWidth); box->setWidth(st::boxWideWidth);
box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] { box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] {
box->closeBox(); box->closeBox();
}); });
const auto priceNow = unique->starsForResale;
const auto name = Data::UniqueGiftName(*unique); const auto name = Data::UniqueGiftName(*unique);
const auto slug = unique->slug; const auto slug = unique->slug;
const auto session = &show->session(); const auto container = box->verticalLayout();
AddSubsectionTitle( auto priceInput = HistoryView::AddStarsTonPriceInput(container, {
box->verticalLayout(), .session = session,
tr::lng_gift_sell_placeholder(), .showTon = state->onlyTon.value(),
(st::boxRowPadding - QMargins( .price = state->price.current(),
st::defaultSubsectionTitlePadding.left(), .starsMin = starsMin,
0, .starsMax = appConfig.giftResaleStarsMax(),
st::defaultSubsectionTitlePadding.right(), .nanoTonMin = nanoTonMin,
st::defaultSubsectionTitlePadding.bottom()))); .nanoTonMax = appConfig.giftResaleNanoTonMax(),
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();
}); });
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( auto goods = rpl::merge(
rpl::single(rpl::empty) | rpl::map_to(true), rpl::single(rpl::empty) | rpl::map_to(true),
base::qt_signal_producer( std::move(priceInput.updates) | rpl::map_to(true),
field, state->errors.events() | rpl::map_to(false)
&Ui::NumberInput::changed
) | rpl::map_to(true),
errors->events() | rpl::map_to(false)
) | rpl::start_spawning(box->lifetime()); ) | rpl::start_spawning(box->lifetime());
auto text = rpl::duplicate(goods) | rpl::map([=](bool good) { auto text = rpl::duplicate(goods) | rpl::map([=](bool good) {
const auto value = field->getLastText().toInt(); const auto value = state->computePrice();
const auto receive = (int64(value) * thousandths) / 1000; const auto amount = value ? value->value() : 0.;
return !good const auto tonMin = nanoTonMin / float64(Ui::kNanosInOne);
? tr::lng_gift_sell_min_price( 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, tr::now,
lt_count, lt_count,
minimal, nanoTonMin / float64(Ui::kNanosInOne),
Ui::Text::RichLangValue) Ui::Text::RichLangValue)
: (value >= minimal) : tr::lng_gift_sell_min_price(
? tr::lng_gift_sell_amount( tr::now,
lt_count,
starsMin,
Ui::Text::RichLangValue))
: enough
? (value->ton()
? tr::lng_gift_sell_amount_ton(
tr::now, tr::now,
lt_count, lt_count,
receive, receive,
Ui::Text::RichLangValue) Ui::Text::RichLangValue)
: tr::lng_gift_sell_amount(
tr::now,
lt_count,
receive,
Ui::Text::RichLangValue))
: tr::lng_gift_sell_about( : tr::lng_gift_sell_about(
tr::now, tr::now,
lt_percent, lt_percent,
@ -4603,37 +4666,50 @@ void UniqueGiftSellBox(
box->verticalLayout()->resizeToWidth(box->width()); box->verticalLayout()->resizeToWidth(box->width());
}), }),
st::boxLabel)); 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) { rpl::duplicate(goods) | rpl::start_with_next([=](bool good) {
details->setTextColorOverride( details->setTextColorOverride(
good ? st::windowSubTextFg->c : st::boxTextFgError->c); good ? st::windowSubTextFg->c : st::boxTextFgError->c);
}, details->lifetime()); }, details->lifetime());
QObject::connect(field, &NumberInput::submitted, [=] { const auto submit = [=] {
const auto count = field->getLastText().toInt(); const auto value = state->computePrice();
if (count < minimal) { if (!value) {
field->showError(); state->errors.fire({});
errors->fire({});
return; return;
} }
box->closeBox(); box->closeBox();
UpdateGiftSellPrice(show, unique, savedId, count); UpdateGiftSellPrice(show, unique, savedId, *value);
}); };
const auto button = box->addButton(priceNow std::move(
priceInput.submits
) | rpl::start_with_next(submit, details->lifetime());
auto submitText = priceNow
? tr::lng_gift_sell_update() ? tr::lng_gift_sell_update()
: tr::lng_gift_sell_put(), [=] { field->submitted({}); }); : tr::lng_gift_sell_put();
rpl::combine( box->addButton(std::move(submitText), submit);
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());
} }
void ShowUniqueGiftSellBox( void ShowUniqueGiftSellBox(
@ -4874,17 +4950,6 @@ void UpgradeBox(
st::creditsBoxButtonLabel, st::creditsBoxButtonLabel,
&st::giftBox.button.textFg); &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, {}); AddUniqueCloseButton(box, {});
} }

View File

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

View File

@ -1533,6 +1533,18 @@ bool ResolveStarsSettings(
return true; 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 } // namespace
const std::vector<LocalUrlHandler> &LocalUrlHandlers() { const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
@ -1637,6 +1649,10 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
u"^stars/?(^\\?.*)?(#|$)"_q, u"^stars/?(^\\?.*)?(#|$)"_q,
ResolveStarsSettings ResolveStarsSettings
}, },
{
u"^ton/?(^\\?.*)?(#|$)"_q,
ResolveTonSettings
},
{ {
u"^([^\\?]+)(\\?|#|$)"_q, u"^([^\\?]+)(\\?|#|$)"_q,
HandleUnknown HandleUnknown

View File

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

View File

@ -48,6 +48,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_settings.h" #include "styles/style_settings.h"
namespace HistoryView { 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( void ChooseSuggestTimeBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
@ -112,6 +137,214 @@ void AddApproximateUsd(
usd->widthValue() | rpl::start_with_next(move, usd->lifetime()); 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( void ChooseSuggestPriceBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
SuggestPriceBoxArgs &&args) { SuggestPriceBoxArgs &&args) {
@ -135,42 +368,17 @@ void ChooseSuggestPriceBox(
state->date = args.value.date; state->date = args.value.date;
state->ton = (args.value.ton != 0); state->ton = (args.value.ton != 0);
state->price = args.value.price(); 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 peer = args.peer;
const auto admin = peer->amMonoforumAdmin(); const auto admin = peer->amMonoforumAdmin();
const auto broadcast = peer->monoforumBroadcast(); const auto broadcast = peer->monoforumBroadcast();
const auto usePeer = broadcast ? broadcast : peer; const auto usePeer = broadcast ? broadcast : peer;
const auto session = &peer->session(); const auto session = &peer->session();
const auto &appConfig = session->appConfig();
if (!admin) { if (!admin) {
session->credits().load(); session->credits().load();
session->credits().tonLoad(); 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(); const auto container = box->verticalLayout();
box->setStyle(st::suggestPriceBox); box->setStyle(st::suggestPriceBox);
@ -267,7 +475,6 @@ void ChooseSuggestPriceBox(
state->buttons[i].active = true; state->buttons[i].active = true;
state->buttons[1 - i].active = false; state->buttons[1 - i].active = false;
buttons->update(); buttons->update();
updatePrice();
break; break;
} }
} }
@ -299,64 +506,11 @@ void ChooseSuggestPriceBox(
Ui::AddSkip(container); Ui::AddSkip(container);
const auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding; const auto computePrice = [session](CreditsAmount amount) {
const auto manager = &session->data().customEmojiManager(); return PriceAfterCommission(session, amount).value();
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 formatCommission = [session](CreditsAmount amount) {
const auto starsWrap = container->add( return FormatAfterCommissionPercent(session, amount);
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 youGet = [=](rpl::producer<CreditsAmount> price, bool stars) { const auto youGet = [=](rpl::producer<CreditsAmount> price, bool stars) {
return (stars return (stars
@ -367,109 +521,34 @@ void ChooseSuggestPriceBox(
lt_percent, lt_percent,
rpl::duplicate(price) | rpl::map(formatCommission)); rpl::duplicate(price) | rpl::map(formatCommission));
}; };
Ui::AddDividerText(starsInner, admin auto starsAbout = admin
? rpl::combine( ? rpl::combine(
youGet(starsPrice(), true), youGet(StarsPriceValue(state->price.value()), true),
tr::lng_suggest_options_stars_warning(Ui::Text::RichLangValue) tr::lng_suggest_options_stars_warning(Ui::Text::RichLangValue)
) | rpl::map([=](const QString &t1, const TextWithEntities &t2) { ) | rpl::map([=](const QString &t1, const TextWithEntities &t2) {
return TextWithEntities{ t1 }.append("\n\n").append(t2); return TextWithEntities{ t1 }.append("\n\n").append(t2);
}) })
: tr::lng_suggest_options_stars_price_about(Ui::Text::WithEntities)); : tr::lng_suggest_options_stars_price_about(Ui::Text::WithEntities);
auto tonAbout = admin
const auto tonWrap = container->add( ? youGet(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( TonPriceValue(state->price.value()),
container, false
object_ptr<Ui::VerticalLayout>(container))); ) | Ui::Text::ToWithEntities()
const auto tonInner = tonWrap->entity(); : tr::lng_suggest_options_ton_price_about(Ui::Text::WithEntities);
auto priceInput = AddStarsTonPriceInput(container, {
Ui::AddSubsectionTitle( .session = session,
tonInner, .showTon = state->ton.value(),
tr::lng_suggest_options_ton_price(), .price = args.value.price(),
QMargins(added.left(), 0, added.right(), -st::defaultSubsectionTitlePadding.bottom())); .starsMin = appConfig.suggestedPostStarsMin(),
.starsMax = appConfig.suggestedPostStarsMax(),
const auto tonFieldWrap = tonInner->add( .nanoTonMin = appConfig.suggestedPostNanoTonMin(),
object_ptr<Ui::FixedHeightWidget>( .nanoTonMax = appConfig.suggestedPostNanoTonMax(),
box, .starsAbout = std::move(starsAbout),
st::editTagField.heightMin), .tonAbout = std::move(tonAbout),
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();
}
}); });
state->price = std::move(priceInput.result);
state->computePrice = std::move(priceInput.computeResult);
box->setFocusCallback(std::move(priceInput.focusCallback));
Ui::AddSkip(container); Ui::AddSkip(container);
@ -568,17 +647,13 @@ void ChooseSuggestPriceBox(
} }
}, box->lifetime()); }, box->lifetime());
QObject::connect( std::move(
starsField, priceInput.submits
&Ui::NumberInput::submitted, ) | rpl::start_with_next(state->save, box->lifetime());
box,
state->save);
tonField->submits(
) | rpl::start_with_next(state->save, tonField->lifetime());
const auto button = box->addButton(rpl::single(QString()), state->save); const auto button = box->addButton(rpl::single(QString()), state->save);
const auto coloredTonIcon = Ui::Text::SingleCustomEmoji( const auto coloredTonIcon = Ui::Text::SingleCustomEmoji(
manager->registerInternalEmoji( session->data().customEmojiManager().registerInternalEmoji(
Ui::Earn::IconCurrencyColored( Ui::Earn::IconCurrencyColored(
st::tonFieldIconSize, st::tonFieldIconSize,
st::currencyFg->c), st::currencyFg->c),

View File

@ -15,6 +15,7 @@ class Show;
namespace Ui { namespace Ui {
class GenericBox; class GenericBox;
class VerticalLayout;
} // namespace Ui } // namespace Ui
namespace Main { namespace Main {
@ -43,6 +44,30 @@ void ChooseSuggestTimeBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
SuggestTimeBoxArgs &&args); 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 { struct SuggestPriceBoxArgs {
not_null<PeerData*> peer; not_null<PeerData*> peer;
bool updating = false; bool updating = false;

View File

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

View File

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

View File

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

View File

@ -148,18 +148,32 @@ bool AppConfig::confcallPrioritizeVP8() const {
return get<bool>(u"confcall_use_vp8"_q, false); return get<bool>(u"confcall_use_vp8"_q, false);
} }
int AppConfig::giftResalePriceMax() const { int AppConfig::giftResaleStarsMin() const {
return get<int>(u"stars_stargift_resale_amount_max"_q, 35000);
}
int AppConfig::giftResalePriceMin() const {
return get<int>(u"stars_stargift_resale_amount_min"_q, 125); 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); 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 { int AppConfig::pollOptionsLimit() const {
return get<int>(u"poll_answers_max"_q, 12); return get<int>(u"poll_answers_max"_q, 12);
} }

View File

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

View File

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