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_bar_dated" = "Publish on {date}";
|
||||||
"lng_suggest_options_title" = "Suggest a Message";
|
"lng_suggest_options_title" = "Suggest a Message";
|
||||||
"lng_suggest_options_change" = "Suggest Changes";
|
"lng_suggest_options_change" = "Suggest Changes";
|
||||||
"lng_suggest_options_price" = "Enter Price in Stars";
|
"lng_suggest_options_stars_offer" = "Offer Stars";
|
||||||
"lng_suggest_options_price_about" = "Choose how many Stars to pay to publish this message.";
|
"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" = "Time";
|
||||||
"lng_suggest_options_date_any" = "Anytime";
|
"lng_suggest_options_date_any" = "Anytime";
|
||||||
"lng_suggest_options_date_about" = "Select the date and time you want the message to be published.";
|
"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;
|
using Flag = MTPDsuggestedPost::Flag;
|
||||||
return suggest.exists
|
return suggest.exists
|
||||||
? MTP_suggestedPost(
|
? 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()),
|
StarsAmountToTL(suggest.price()),
|
||||||
MTP_int(suggest.date))
|
MTP_int(suggest.date))
|
||||||
: MTPSuggestedPost();
|
: MTPSuggestedPost();
|
||||||
|
|
|
@ -2756,11 +2756,6 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
||||||
_session->credits().apply(data);
|
_session->credits().apply(data);
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
case mtpc_updateStarsTonBalance: {
|
|
||||||
const auto &data = update.c_updateStarsBalance();
|
|
||||||
_session->credits().apply(data);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case mtpc_updatePaidReactionPrivacy: {
|
case mtpc_updatePaidReactionPrivacy: {
|
||||||
const auto &data = update.c_updatePaidReactionPrivacy();
|
const auto &data = update.c_updatePaidReactionPrivacy();
|
||||||
_session->api().globalPrivacy().updatePaidReactionShownPeer(
|
_session->api().globalPrivacy().updatePaidReactionShownPeer(
|
||||||
|
|
|
@ -132,6 +132,9 @@ void Credits::invalidate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Credits::apply(CreditsAmount balance) {
|
void Credits::apply(CreditsAmount balance) {
|
||||||
|
if (balance.ton()) {
|
||||||
|
_balanceTon = balance;
|
||||||
|
} else {
|
||||||
_balance = balance;
|
_balance = balance;
|
||||||
updateNonLockedValue();
|
updateNonLockedValue();
|
||||||
|
|
||||||
|
@ -140,6 +143,7 @@ void Credits::apply(CreditsAmount balance) {
|
||||||
_loadedChanges.fire({});
|
_loadedChanges.fire({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Credits::apply(PeerId peerId, CreditsAmount balance) {
|
void Credits::apply(PeerId peerId, CreditsAmount balance) {
|
||||||
_cachedPeerBalances[peerId] = balance;
|
_cachedPeerBalances[peerId] = balance;
|
||||||
|
|
|
@ -56,6 +56,7 @@ private:
|
||||||
base::flat_map<PeerId, uint64> _cachedPeerCurrencyBalances;
|
base::flat_map<PeerId, uint64> _cachedPeerCurrencyBalances;
|
||||||
|
|
||||||
CreditsAmount _balance;
|
CreditsAmount _balance;
|
||||||
|
CreditsAmount _balanceTon;
|
||||||
CreditsAmount _locked;
|
CreditsAmount _locked;
|
||||||
rpl::variable<CreditsAmount> _nonLockedBalance;
|
rpl::variable<CreditsAmount> _nonLockedBalance;
|
||||||
rpl::event_stream<> _loadedChanges;
|
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 "history/view/history_view_suggest_options.h"
|
||||||
|
|
||||||
#include "base/unixtime.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_channel.h"
|
||||||
#include "data/data_media_types.h"
|
#include "data/data_media_types.h"
|
||||||
|
#include "data/data_session.h"
|
||||||
#include "history/history_item.h"
|
#include "history/history_item.h"
|
||||||
|
#include "info/channel_statistics/earn/earn_icons.h"
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "main/main_app_config.h"
|
#include "main/main_app_config.h"
|
||||||
#include "main/main_session.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/layers/generic_box.h"
|
||||||
#include "ui/text/text_utilities.h"
|
#include "ui/text/text_utilities.h"
|
||||||
#include "ui/boxes/choose_date_time.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/number_input.h"
|
||||||
|
#include "ui/widgets/fields/input_field.h"
|
||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
|
#include "ui/wrap/slide_wrap.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
#include "ui/vertical_list.h"
|
#include "ui/vertical_list.h"
|
||||||
#include "window/window_session_controller.h"
|
#include "window/window_session_controller.h"
|
||||||
|
#include "styles/style_channel_earn.h"
|
||||||
#include "styles/style_chat.h"
|
#include "styles/style_chat.h"
|
||||||
#include "styles/style_chat_helpers.h"
|
#include "styles/style_chat_helpers.h"
|
||||||
|
#include "styles/style_credits.h"
|
||||||
|
#include "styles/style_layers.h"
|
||||||
#include "styles/style_settings.h"
|
#include "styles/style_settings.h"
|
||||||
|
|
||||||
namespace HistoryView {
|
namespace HistoryView {
|
||||||
|
@ -37,6 +48,7 @@ void ChooseSuggestTimeBox(
|
||||||
const auto value = args.value
|
const auto value = args.value
|
||||||
? std::clamp(args.value, now + min, now + max)
|
? std::clamp(args.value, now + min, now + max)
|
||||||
: (now + 86400);
|
: (now + 86400);
|
||||||
|
const auto done = args.done;
|
||||||
Ui::ChooseDateTimeBox(box, {
|
Ui::ChooseDateTimeBox(box, {
|
||||||
.title = ((args.mode == SuggestMode::New)
|
.title = ((args.mode == SuggestMode::New)
|
||||||
? tr::lng_suggest_options_date()
|
? tr::lng_suggest_options_date()
|
||||||
|
@ -44,21 +56,34 @@ void ChooseSuggestTimeBox(
|
||||||
.submit = ((args.mode == SuggestMode::New)
|
.submit = ((args.mode == SuggestMode::New)
|
||||||
? tr::lng_settings_save()
|
? tr::lng_settings_save()
|
||||||
: tr::lng_suggest_options_update()),
|
: tr::lng_suggest_options_update()),
|
||||||
.done = std::move(args.done),
|
.done = done,
|
||||||
.min = [=] { return now + min; },
|
.min = [=] { return now + min; },
|
||||||
.time = value,
|
.time = value,
|
||||||
.max = [=] { return now + max; },
|
.max = [=] { return now + max; },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
box->addLeftButton(tr::lng_suggest_options_date_any(), [=] {
|
||||||
|
done(TimeId());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChooseSuggestPriceBox(
|
void ChooseSuggestPriceBox(
|
||||||
not_null<Ui::GenericBox*> box,
|
not_null<Ui::GenericBox*> box,
|
||||||
SuggestPriceBoxArgs &&args) {
|
SuggestPriceBoxArgs &&args) {
|
||||||
|
struct Button {
|
||||||
|
QRect geometry;
|
||||||
|
Ui::Text::String text;
|
||||||
|
bool active = false;
|
||||||
|
};
|
||||||
struct State {
|
struct State {
|
||||||
|
std::vector<Button> buttons;
|
||||||
rpl::variable<TimeId> date;
|
rpl::variable<TimeId> date;
|
||||||
|
rpl::variable<bool> ton;
|
||||||
|
bool inButton = false;
|
||||||
};
|
};
|
||||||
const auto state = box->lifetime().make_state<State>();
|
const auto state = box->lifetime().make_state<State>();
|
||||||
state->date = args.value.date;
|
state->date = args.value.date;
|
||||||
|
state->ton = (args.value.ton != 0);
|
||||||
|
|
||||||
const auto limit = args.session->appConfig().suggestedPostStarsMax();
|
const auto limit = args.session->appConfig().suggestedPostStarsMax();
|
||||||
|
|
||||||
|
@ -67,41 +92,214 @@ void ChooseSuggestPriceBox(
|
||||||
: tr::lng_suggest_options_change());
|
: tr::lng_suggest_options_change());
|
||||||
|
|
||||||
const auto container = box->verticalLayout();
|
const auto container = box->verticalLayout();
|
||||||
|
state->buttons.push_back({
|
||||||
Ui::AddSkip(container);
|
.text = Ui::Text::String(
|
||||||
Ui::AddSubsectionTitle(container, tr::lng_suggest_options_price());
|
st::semiboldTextStyle,
|
||||||
|
tr::lng_suggest_options_stars_offer(tr::now)),
|
||||||
const auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>(
|
.active = !state->ton.current(),
|
||||||
box,
|
});
|
||||||
st::editTagField.heightMin));
|
state->buttons.push_back({
|
||||||
auto owned = object_ptr<Ui::NumberInput>(
|
.text = Ui::Text::String(
|
||||||
wrap,
|
st::semiboldTextStyle,
|
||||||
st::editTagField,
|
tr::lng_suggest_options_ton_offer(tr::now)),
|
||||||
tr::lng_paid_cost_placeholder(),
|
.active = state->ton.current(),
|
||||||
(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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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::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,
|
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);
|
Ui::AddSkip(container);
|
||||||
|
|
||||||
const auto time = Settings::AddButtonWithLabel(
|
const auto time = Settings::AddButtonWithLabel(
|
||||||
|
@ -139,23 +337,37 @@ void ChooseSuggestPriceBox(
|
||||||
Ui::AddDividerText(container, tr::lng_suggest_options_date_about());
|
Ui::AddDividerText(container, tr::lng_suggest_options_date_about());
|
||||||
AssertIsDebug()//tr::lng_suggest_options_offer
|
AssertIsDebug()//tr::lng_suggest_options_offer
|
||||||
const auto save = [=] {
|
const auto save = [=] {
|
||||||
const auto now = field->getLastText().toDouble();
|
auto nanos = int64();
|
||||||
if (now > limit) {
|
if (state->ton.current()) {
|
||||||
field->showError();
|
const auto now = Ui::ParseTonAmountString(
|
||||||
|
tonField->getLastText());
|
||||||
|
if (!now || (*now < 0) || (*now > limit * Ui::kNanosInOne)) {
|
||||||
|
tonField->showError();
|
||||||
return;
|
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(
|
const auto value = CreditsAmount(
|
||||||
int(std::floor(now)),
|
nanos / Ui::kNanosInOne,
|
||||||
int(base::SafeRound((now - std::floor(now)) * 1'000'000'000.)));
|
nanos % Ui::kNanosInOne);
|
||||||
args.done({
|
args.done({
|
||||||
.exists = true,
|
.exists = true,
|
||||||
.priceWhole = uint32(value.whole()),
|
.priceWhole = uint32(value.whole()),
|
||||||
.priceNano = uint32(value.nano()),
|
.priceNano = uint32(value.nano()),
|
||||||
|
.ton = uint32(state->ton.current() ? 1 : 0),
|
||||||
.date = state->date.current(),
|
.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_settings_save(), save);
|
||||||
box->addButton(tr::lng_cancel(), [=] {
|
box->addButton(tr::lng_cancel(), [=] {
|
||||||
|
|
|
@ -47,10 +47,8 @@ namespace {
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
QImage IconCurrencyColored(
|
QImage IconCurrencyColored(int size, const QColor &c) {
|
||||||
const style::font &font,
|
const auto s = Size(size);
|
||||||
const QColor &c) {
|
|
||||||
const auto s = Size(font->ascent);
|
|
||||||
auto svg = QSvgRenderer(CurrencySvg(c));
|
auto svg = QSvgRenderer(CurrencySvg(c));
|
||||||
auto image = QImage(
|
auto image = QImage(
|
||||||
s * style::DevicePixelRatio(),
|
s * style::DevicePixelRatio(),
|
||||||
|
@ -64,6 +62,12 @@ QImage IconCurrencyColored(
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QImage IconCurrencyColored(
|
||||||
|
const style::font &font,
|
||||||
|
const QColor &c) {
|
||||||
|
return IconCurrencyColored(font->ascent, c);
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray CurrencySvgColored(const QColor &c) {
|
QByteArray CurrencySvgColored(const QColor &c) {
|
||||||
return CurrencySvg(c);
|
return CurrencySvg(c);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ class CustomEmoji;
|
||||||
|
|
||||||
namespace Ui::Earn {
|
namespace Ui::Earn {
|
||||||
|
|
||||||
|
[[nodiscard]] QImage IconCurrencyColored(int size, const QColor &c);
|
||||||
[[nodiscard]] QImage IconCurrencyColored(
|
[[nodiscard]] QImage IconCurrencyColored(
|
||||||
const style::font &font,
|
const style::font &font,
|
||||||
const QColor &c);
|
const QColor &c);
|
||||||
|
|
|
@ -1352,3 +1352,21 @@ chatTabsOutlineHorizontal: ChatTabsOutline {
|
||||||
|
|
||||||
chatTabsOutlineVertical: ChatTabsOutline(chatTabsOutlineHorizontal) {
|
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/swipe_handler_data.h
|
||||||
ui/controls/tabbed_search.cpp
|
ui/controls/tabbed_search.cpp
|
||||||
ui/controls/tabbed_search.h
|
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.cpp
|
||||||
ui/controls/who_reacted_context_action.h
|
ui/controls/who_reacted_context_action.h
|
||||||
ui/controls/window_outdated_bar.cpp
|
ui/controls/window_outdated_bar.cpp
|
||||||
|
|
Loading…
Add table
Reference in a new issue