Allow suggesting with TON.

This commit is contained in:
John Preston 2025-06-24 13:39:24 +04:00
parent 0fa50f1951
commit 6272b79f70
12 changed files with 584 additions and 56 deletions

View file

@ -4422,8 +4422,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_suggest_bar_dated" = "Publish on {date}";
"lng_suggest_options_title" = "Suggest a Message";
"lng_suggest_options_change" = "Suggest Changes";
"lng_suggest_options_price" = "Enter Price in Stars";
"lng_suggest_options_price_about" = "Choose how many Stars to pay to publish this message.";
"lng_suggest_options_stars_offer" = "Offer Stars";
"lng_suggest_options_stars_price" = "Enter Price in Stars";
"lng_suggest_options_stars_price_about" = "Choose how many Stars to pay to publish this message.";
"lng_suggest_options_ton_offer" = "Offer TON";
"lng_suggest_options_ton_price" = "Enter Price in TON";
"lng_suggest_options_ton_price_about" = "Choose how many TON to pay to publish this message.";
"lng_suggest_options_date" = "Time";
"lng_suggest_options_date_any" = "Anytime";
"lng_suggest_options_date_about" = "Select the date and time you want the message to be published.";

View file

@ -18,7 +18,8 @@ MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest) {
using Flag = MTPDsuggestedPost::Flag;
return suggest.exists
? MTP_suggestedPost(
MTP_flags(suggest.date ? Flag::f_schedule_date : Flag()),
MTP_flags((suggest.date ? Flag::f_schedule_date : Flag())
| (suggest.price().empty() ? Flag() : Flag::f_price)),
StarsAmountToTL(suggest.price()),
MTP_int(suggest.date))
: MTPSuggestedPost();

View file

@ -2756,11 +2756,6 @@ void Updates::feedUpdate(const MTPUpdate &update) {
_session->credits().apply(data);
} break;
case mtpc_updateStarsTonBalance: {
const auto &data = update.c_updateStarsBalance();
_session->credits().apply(data);
} break;
case mtpc_updatePaidReactionPrivacy: {
const auto &data = update.c_updatePaidReactionPrivacy();
_session->api().globalPrivacy().updatePaidReactionShownPeer(

View file

@ -132,6 +132,9 @@ void Credits::invalidate() {
}
void Credits::apply(CreditsAmount balance) {
if (balance.ton()) {
_balanceTon = balance;
} else {
_balance = balance;
updateNonLockedValue();
@ -140,6 +143,7 @@ void Credits::apply(CreditsAmount balance) {
_loadedChanges.fire({});
}
}
}
void Credits::apply(PeerId peerId, CreditsAmount balance) {
_cachedPeerBalances[peerId] = balance;

View file

@ -56,6 +56,7 @@ private:
base::flat_map<PeerId, uint64> _cachedPeerCurrencyBalances;
CreditsAmount _balance;
CreditsAmount _balanceTon;
CreditsAmount _locked;
rpl::variable<CreditsAmount> _nonLockedBalance;
rpl::event_stream<> _loadedChanges;

View file

@ -8,9 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_suggest_options.h"
#include "base/unixtime.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_channel.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "history/history_item.h"
#include "info/channel_statistics/earn/earn_icons.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
@ -18,12 +22,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/controls/ton_common.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_channel_earn.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace HistoryView {
@ -37,6 +48,7 @@ void ChooseSuggestTimeBox(
const auto value = args.value
? std::clamp(args.value, now + min, now + max)
: (now + 86400);
const auto done = args.done;
Ui::ChooseDateTimeBox(box, {
.title = ((args.mode == SuggestMode::New)
? tr::lng_suggest_options_date()
@ -44,21 +56,34 @@ void ChooseSuggestTimeBox(
.submit = ((args.mode == SuggestMode::New)
? tr::lng_settings_save()
: tr::lng_suggest_options_update()),
.done = std::move(args.done),
.done = done,
.min = [=] { return now + min; },
.time = value,
.max = [=] { return now + max; },
});
box->addLeftButton(tr::lng_suggest_options_date_any(), [=] {
done(TimeId());
});
}
void ChooseSuggestPriceBox(
not_null<Ui::GenericBox*> box,
SuggestPriceBoxArgs &&args) {
struct Button {
QRect geometry;
Ui::Text::String text;
bool active = false;
};
struct State {
std::vector<Button> buttons;
rpl::variable<TimeId> date;
rpl::variable<bool> ton;
bool inButton = false;
};
const auto state = box->lifetime().make_state<State>();
state->date = args.value.date;
state->ton = (args.value.ton != 0);
const auto limit = args.session->appConfig().suggestedPostStarsMax();
@ -67,41 +92,214 @@ void ChooseSuggestPriceBox(
: tr::lng_suggest_options_change());
const auto container = box->verticalLayout();
Ui::AddSkip(container);
Ui::AddSubsectionTitle(container, tr::lng_suggest_options_price());
const auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::editTagField.heightMin));
auto owned = object_ptr<Ui::NumberInput>(
wrap,
st::editTagField,
tr::lng_paid_cost_placeholder(),
(args.value.price()
? QString::number(args.value.price().value())
: QString()),
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->buttons.push_back({
.text = Ui::Text::String(
st::semiboldTextStyle,
tr::lng_suggest_options_stars_offer(tr::now)),
.active = !state->ton.current(),
});
state->buttons.push_back({
.text = Ui::Text::String(
st::semiboldTextStyle,
tr::lng_suggest_options_ton_offer(tr::now)),
.active = state->ton.current(),
});
auto x = 0;
auto y = st::giftBoxTabsMargin.top();
const auto padding = st::giftBoxTabPadding;
for (auto &button : state->buttons) {
const auto width = button.text.maxWidth();
const auto height = st::semiboldTextStyle.font->height;
const auto r = QRect(0, 0, width, height).marginsAdded(padding);
button.geometry = QRect(QPoint(x, y), r.size());
x += r.width() + st::giftBoxTabSkip;
}
const auto buttons = box->addRow(object_ptr<Ui::RpWidget>(box));
const auto height = y
+ state->buttons.back().geometry.height()
+ st::giftBoxTabsMargin.bottom();
buttons->resize(buttons->width(), height);
buttons->setMouseTracking(true);
buttons->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto type = e->type();
switch (type) {
case QEvent::MouseMove: {
const auto in = [&] {
const auto me = static_cast<QMouseEvent*>(e.get());
const auto position = me->pos();
for (const auto &button : state->buttons) {
if (button.geometry.contains(position)) {
return true;
}
}
return false;
}();
if (state->inButton != in) {
state->inButton = in;
buttons->setCursor(in
? style::cur_pointer
: style::cur_default);
}
} break;
case QEvent::MouseButtonPress: {
const auto me = static_cast<QMouseEvent*>(e.get());
if (me->button() != Qt::LeftButton) {
break;
}
const auto position = me->pos();
for (auto i = 0, c = int(state->buttons.size()); i != c; ++i) {
if (state->buttons[i].geometry.contains(position)) {
state->ton = (i != 0);
state->buttons[i].active = true;
state->buttons[1 - i].active = false;
buttons->update();
break;
}
}
} break;
}
}, buttons->lifetime());
buttons->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(buttons);
auto hq = PainterHighQualityEnabler(p);
const auto padding = st::giftBoxTabPadding;
for (const auto &button : state->buttons) {
const auto geometry = button.geometry;
if (button.active) {
p.setBrush(st::giftBoxTabBgActive);
p.setPen(Qt::NoPen);
const auto radius = geometry.height() / 2.;
p.drawRoundedRect(geometry, radius, radius);
p.setPen(st::giftBoxTabFgActive);
} else {
p.setPen(st::giftBoxTabFg);
}
button.text.draw(p, {
.position = geometry.marginsRemoved(padding).topLeft(),
.availableWidth = button.text.maxWidth(),
});
}
}, buttons->lifetime());
Ui::AddSkip(container);
Ui::AddSkip(container);
Ui::AddDividerText(
const auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding;
const auto manager = &args.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 = args.session }));
};
const auto starsWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
tr::lng_suggest_options_price_about());
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(), 0));
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()),
limit);
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());
Ui::AddSkip(starsInner);
Ui::AddSkip(starsInner);
Ui::AddDividerText(
starsInner,
tr::lng_suggest_options_stars_price_about());
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(), 0));
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::windowActiveTextFg->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());
Ui::AddSkip(tonInner);
Ui::AddSkip(tonInner);
Ui::AddDividerText(
tonInner,
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);
box->setFocusCallback([=] {
if (state->ton.current()) {
tonField->selectAll();
tonField->setFocusFast();
} else {
starsField->selectAll();
starsField->setFocusFast();
}
});
Ui::AddSkip(container);
const auto time = Settings::AddButtonWithLabel(
@ -139,23 +337,37 @@ void ChooseSuggestPriceBox(
Ui::AddDividerText(container, tr::lng_suggest_options_date_about());
AssertIsDebug()//tr::lng_suggest_options_offer
const auto save = [=] {
const auto now = field->getLastText().toDouble();
if (now > limit) {
field->showError();
auto nanos = int64();
if (state->ton.current()) {
const auto now = Ui::ParseTonAmountString(
tonField->getLastText());
if (!now || (*now < 0) || (*now > limit * Ui::kNanosInOne)) {
tonField->showError();
return;
}
nanos = *now;
} else {
const auto now = starsField->getLastText().toLongLong();
if (now < 0 || now > limit) {
starsField->showError();
return;
}
nanos = now * Ui::kNanosInOne;
}
const auto value = CreditsAmount(
int(std::floor(now)),
int(base::SafeRound((now - std::floor(now)) * 1'000'000'000.)));
nanos / Ui::kNanosInOne,
nanos % Ui::kNanosInOne);
args.done({
.exists = true,
.priceWhole = uint32(value.whole()),
.priceNano = uint32(value.nano()),
.ton = uint32(state->ton.current() ? 1 : 0),
.date = state->date.current(),
});
};
QObject::connect(field, &Ui::NumberInput::submitted, box, save);
QObject::connect(starsField, &Ui::NumberInput::submitted, box, save);
tonField->submits() | rpl::start_with_next(save, tonField->lifetime());
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] {

View file

@ -47,10 +47,8 @@ namespace {
} // namespace
QImage IconCurrencyColored(
const style::font &font,
const QColor &c) {
const auto s = Size(font->ascent);
QImage IconCurrencyColored(int size, const QColor &c) {
const auto s = Size(size);
auto svg = QSvgRenderer(CurrencySvg(c));
auto image = QImage(
s * style::DevicePixelRatio(),
@ -64,6 +62,12 @@ QImage IconCurrencyColored(
return image;
}
QImage IconCurrencyColored(
const style::font &font,
const QColor &c) {
return IconCurrencyColored(font->ascent, c);
}
QByteArray CurrencySvgColored(const QColor &c) {
return CurrencySvg(c);
}

View file

@ -13,6 +13,7 @@ class CustomEmoji;
namespace Ui::Earn {
[[nodiscard]] QImage IconCurrencyColored(int size, const QColor &c);
[[nodiscard]] QImage IconCurrencyColored(
const style::font &font,
const QColor &c);

View file

@ -1352,3 +1352,21 @@ chatTabsOutlineHorizontal: ChatTabsOutline {
chatTabsOutlineVertical: ChatTabsOutline(chatTabsOutlineHorizontal) {
}
tonInput: InputField(defaultInputField) {
textBg: transparent;
textMargins: margins(0px, 7px, 0px, 7px);
placeholderFg: placeholderFg;
placeholderFgActive: placeholderFgActive;
placeholderFgError: placeholderFgActive;
placeholderMargins: margins(0px, 0px, 0px, 0px);
placeholderScale: 0.;
placeholderFont: boxTextFont;
heightMin: 34px;
heightMax: 100px;
}
starsFieldIconPosition: point(0px, 10px);
tonFieldIconSize: 16px;
tonFieldIconPosition: point(2px, 9px);

View file

@ -0,0 +1,240 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/controls/ton_common.h"
#include "base/qthelp_url.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/ui_utility.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
//#include "styles/style_wallet.h"
#include <QtCore/QLocale>
namespace Ui {
namespace {
constexpr auto kOneTon = kNanosInOne;
constexpr auto kNanoDigits = 9;
struct FixedAmount {
QString text;
int position = 0;
};
std::optional<int64> ParseAmountTons(const QString &trimmed) {
auto ok = false;
const auto grams = int64(trimmed.toLongLong(&ok));
return (ok
&& (grams <= std::numeric_limits<int64>::max() / kOneTon)
&& (grams >= std::numeric_limits<int64>::min() / kOneTon))
? std::make_optional(grams * kOneTon)
: std::nullopt;
}
std::optional<int64> ParseAmountNano(QString trimmed) {
while (trimmed.size() < kNanoDigits) {
trimmed.append('0');
}
auto zeros = 0;
for (const auto ch : trimmed) {
if (ch == '0') {
++zeros;
} else {
break;
}
}
if (zeros == trimmed.size()) {
return 0;
} else if (trimmed.size() > kNanoDigits) {
return std::nullopt;
}
auto ok = false;
const auto value = trimmed.mid(zeros).toLongLong(&ok);
return (ok && value > 0 && value < kOneTon)
? std::make_optional(value)
: std::nullopt;
}
[[nodiscard]] FixedAmount FixTonAmountInput(
const QString &was,
const QString &text,
int position) {
constexpr auto kMaxDigitsCount = 9;
const auto separator = FormatTonAmount(1).separator;
auto result = FixedAmount{ text, position };
if (text.isEmpty()) {
return result;
} else if (text.startsWith('.')
|| text.startsWith(',')
|| text.startsWith(separator)) {
result.text.prepend('0');
++result.position;
}
auto separatorFound = false;
auto digitsCount = 0;
for (auto i = 0; i != result.text.size();) {
const auto ch = result.text[i];
const auto atSeparator = result.text.midRef(i).startsWith(separator);
if (ch >= '0' && ch <= '9' && digitsCount < kMaxDigitsCount) {
++i;
++digitsCount;
continue;
} else if (!separatorFound
&& (atSeparator || ch == '.' || ch == ',')) {
separatorFound = true;
if (!atSeparator) {
result.text.replace(i, 1, separator);
}
digitsCount = 0;
i += separator.size();
continue;
}
result.text.remove(i, 1);
if (result.position > i) {
--result.position;
}
}
if (result.text == "0" && result.position > 0) {
if (was.startsWith('0')) {
result.text = QString();
result.position = 0;
} else {
result.text += separator;
result.position += separator.size();
}
}
return result;
}
} // namespace
FormattedTonAmount FormatTonAmount(int64 amount, TonFormatFlags flags) {
auto result = FormattedTonAmount();
const auto grams = amount / kOneTon;
const auto preciseNanos = std::abs(amount) % kOneTon;
auto roundedNanos = preciseNanos;
if (flags & TonFormatFlag::Rounded) {
if (std::abs(grams) >= 1'000'000 && (roundedNanos % 1'000'000)) {
roundedNanos -= (roundedNanos % 1'000'000);
} else if (std::abs(grams) >= 1'000 && (roundedNanos % 1'000)) {
roundedNanos -= (roundedNanos % 1'000);
}
}
const auto precise = (roundedNanos == preciseNanos);
auto nanos = preciseNanos;
auto zeros = 0;
while (zeros < kNanoDigits && nanos % 10 == 0) {
nanos /= 10;
++zeros;
}
const auto system = QLocale::system();
const auto locale = (flags & TonFormatFlag::Simple)
? QLocale::c()
: system;
const auto separator = system.decimalPoint();
result.wholeString = locale.toString(grams);
if ((flags & TonFormatFlag::Signed) && amount > 0) {
result.wholeString = locale.positiveSign() + result.wholeString;
} else if (amount < 0 && grams == 0) {
result.wholeString = locale.negativeSign() + result.wholeString;
}
result.full = result.wholeString;
if (zeros < kNanoDigits) {
result.separator = separator;
result.nanoString = QString("%1"
).arg(nanos, kNanoDigits - zeros, 10, QChar('0'));
if (!precise) {
const auto nanoLength = (std::abs(grams) >= 1'000'000)
? 3
: (std::abs(grams) >= 1'000)
? 6
: 9;
result.nanoString = result.nanoString.mid(0, nanoLength);
}
result.full += separator + result.nanoString;
}
return result;
}
std::optional<int64> ParseTonAmountString(const QString &amount) {
const auto trimmed = amount.trimmed();
const auto separator = QString(QLocale::system().decimalPoint());
const auto index1 = trimmed.indexOf('.');
const auto index2 = trimmed.indexOf(',');
const auto index3 = (separator == "." || separator == ",")
? -1
: trimmed.indexOf(separator);
const auto found = (index1 >= 0 ? 1 : 0)
+ (index2 >= 0 ? 1 : 0)
+ (index3 >= 0 ? 1 : 0);
if (found > 1) {
return std::nullopt;
}
const auto index = (index1 >= 0)
? index1
: (index2 >= 0)
? index2
: index3;
const auto used = (index1 >= 0)
? "."
: (index2 >= 0)
? ","
: separator;
const auto grams = ParseAmountTons(trimmed.mid(0, index));
const auto nano = ParseAmountNano(trimmed.mid(index + used.size()));
if (index < 0 || index == trimmed.size() - used.size()) {
return grams;
} else if (index == 0) {
return nano;
} else if (!nano || !grams) {
return std::nullopt;
}
return *grams + (*grams < 0 ? (-*nano) : (*nano));
}
QString TonAmountSeparator() {
return FormatTonAmount(1).separator;
}
not_null<Ui::InputField*> CreateTonAmountInput(
not_null<QWidget*> parent,
rpl::producer<QString> placeholder,
int64 amount) {
const auto result = Ui::CreateChild<Ui::InputField>(
parent.get(),
st::editTagField,
Ui::InputField::Mode::SingleLine,
std::move(placeholder),
(amount > 0
? FormatTonAmount(amount, TonFormatFlag::Simple).full
: QString()));
const auto lastAmountValue = std::make_shared<QString>();
result->changes() | rpl::start_with_next([=] {
Ui::PostponeCall(result, [=] {
const auto position = result->textCursor().position();
const auto now = result->getLastText();
const auto fixed = FixTonAmountInput(
*lastAmountValue,
now,
position);
*lastAmountValue = fixed.text;
if (fixed.text == now) {
return;
}
result->setText(fixed.text);
result->setFocusFast();
result->setCursorPosition(fixed.position);
});
}, result->lifetime());
return result;
}
} // namespace Wallet

View file

@ -0,0 +1,46 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/flags.h"
namespace Ui {
class InputField;
inline constexpr auto kNanosInOne = 1'000'000'000LL;
struct FormattedTonAmount {
QString wholeString;
QString separator;
QString nanoString;
QString full;
};
enum class TonFormatFlag {
Signed = 0x01,
Rounded = 0x02,
Simple = 0x04,
};
constexpr bool is_flag_type(TonFormatFlag) { return true; };
using TonFormatFlags = base::flags<TonFormatFlag>;
[[nodiscard]] FormattedTonAmount FormatTonAmount(
int64 amount,
TonFormatFlags flags = TonFormatFlags());
[[nodiscard]] std::optional<int64> ParseTonAmountString(
const QString &amount);
[[nodiscard]] QString TonAmountSeparator();
[[nodiscard]] not_null<Ui::InputField*> CreateTonAmountInput(
not_null<QWidget*> parent,
rpl::producer<QString> placeholder,
int64 amount = 0);
} // namespace Ui

View file

@ -395,6 +395,8 @@ PRIVATE
ui/controls/swipe_handler_data.h
ui/controls/tabbed_search.cpp
ui/controls/tabbed_search.h
ui/controls/ton_common.cpp
ui/controls/ton_common.h
ui/controls/who_reacted_context_action.cpp
ui/controls/who_reacted_context_action.h
ui/controls/window_outdated_bar.cpp