mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-07-27 07:52:57 +02:00
Allow suggesting with TON.
This commit is contained in:
parent
0fa50f1951
commit
6272b79f70
12 changed files with 584 additions and 56 deletions
|
@ -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.";
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -132,6 +132,9 @@ void Credits::invalidate() {
|
|||
}
|
||||
|
||||
void Credits::apply(CreditsAmount balance) {
|
||||
if (balance.ton()) {
|
||||
_balanceTon = balance;
|
||||
} else {
|
||||
_balance = balance;
|
||||
updateNonLockedValue();
|
||||
|
||||
|
@ -139,6 +142,7 @@ void Credits::apply(CreditsAmount balance) {
|
|||
if (!was) {
|
||||
_loadedChanges.fire({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Credits::apply(PeerId peerId, CreditsAmount balance) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(), [=] {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
240
Telegram/SourceFiles/ui/controls/ton_common.cpp
Normal file
240
Telegram/SourceFiles/ui/controls/ton_common.cpp
Normal 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
|
46
Telegram/SourceFiles/ui/controls/ton_common.h
Normal file
46
Telegram/SourceFiles/ui/controls/ton_common.h
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue