Check amounts of stars/TON.

This commit is contained in:
John Preston 2025-06-26 22:30:55 +04:00
parent 6f305c8974
commit 4840a9094b
24 changed files with 474 additions and 150 deletions

View file

@ -2911,6 +2911,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_small_balance_star_gift" = "Buy **Stars** to send gifts to {user} and other contacts.";
"lng_credits_small_balance_for_message" = "Buy **Stars** to send messages to {user}.";
"lng_credits_small_balance_for_messages" = "Buy **Stars** to send messages.";
"lng_credits_small_balance_for_suggest" = "Buy **Stars** to suggest post to {channel}.";
"lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram.";
"lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars.";
"lng_credits_enough" = "You have enough stars at the moment. {link}";
@ -4478,6 +4479,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_suggest_warn_text_stars" = "You won't receive **Stars** for the post if you delete it now. The post must remain visible for at least **24 hours** after it was published.";
"lng_suggest_warn_text_ton" = "You won't receive **TON** for the post if you delete it now. The post must remain visible for at least **24 hours** after it was published.";
"lng_suggest_warn_delete_anyway" = "Delete Anyway";
"lng_suggest_low_ton_title" = "{amount} TON Needed";
"lng_suggest_low_ton_text" = "Buy **TON** to suggest message to {channel} and others.";
"lng_suggest_low_ton_fragment" = "Buy on Fragment";
"lng_suggest_low_ton_fragment_url" = "https://fragment.com/ads/topup";
"lng_reply_in_another_title" = "Reply in...";
"lng_reply_in_another_chat" = "Reply in Another Chat";

View file

@ -198,18 +198,8 @@ void SendSuggest(
SendSuggest(show, item, state, modify, done, stars);
}
};
const auto checked = state->sendPayment.check(
show,
item->history()->peer,
1,
starsApproved,
withPaymentApproved);
if (!checked) {
return;
}
const auto isForward = item->Get<HistoryMessageForwarded>();
auto action = SendAction(item->history());
action.options.suggest.exists = 1;
if (suggestion) {
action.options.suggest.date = suggestion->date;
@ -218,12 +208,22 @@ void SendSuggest(
action.options.suggest.ton = suggestion->price.ton() ? 1 : 0;
}
modify(action.options.suggest);
action.options.starsApproved = starsApproved;
action.replyTo.monoforumPeerId = item->history()->amMonoforumAdmin()
? item->sublistPeerId()
: PeerId();
action.replyTo.messageId = item->fullId();
const auto checked = state->sendPayment.check(
show,
item->history()->peer,
action.options,
1,
withPaymentApproved);
if (!checked) {
return;
}
show->session().api().sendAction(action);
show->session().api().forwardMessages({
.items = { item },
@ -302,7 +302,7 @@ void SuggestOfferForMessage(
};
using namespace HistoryView;
auto priceBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{
.session = &show->session(),
.peer = item->history()->peer,
.done = done,
.value = values,
.mode = mode,

View file

@ -101,8 +101,10 @@ public:
return result;
}
friend inline auto operator<=>(CreditsAmount, CreditsAmount) = default;
friend inline bool operator==(CreditsAmount, CreditsAmount) = default;
friend inline constexpr auto operator<=>(CreditsAmount, CreditsAmount)
= default;
friend inline constexpr bool operator==(CreditsAmount, CreditsAmount)
= default;
[[nodiscard]] CreditsAmount abs() const {
return (_whole < 0) ? CreditsAmount(-_whole, -_nano) : *this;

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/credits.h"
#include "apiwrap.h"
#include "api/api_credits.h"
#include "data/data_user.h"
#include "main/main_app_config.h"
@ -93,6 +94,54 @@ rpl::producer<CreditsAmount> Credits::balanceValue() const {
return _nonLockedBalance.value();
}
void Credits::tonLoad(bool force) {
if (_tonRequestId
|| (!force
&& _tonLastLoaded
&& _tonLastLoaded + kReloadThreshold > crl::now())) {
return;
}
_tonRequestId = _session->api().request(MTPpayments_GetStarsStatus(
MTP_flags(MTPpayments_GetStarsStatus::Flag::f_ton),
MTP_inputPeerSelf()
)).done([=](const MTPpayments_StarsStatus &result) {
_tonRequestId = 0;
const auto amount = CreditsAmountFromTL(result.data().vbalance());
if (amount.ton()) {
apply(amount);
} else if (amount.empty()) {
apply(CreditsAmount(0, CreditsType::Ton));
} else {
LOG(("API Error: Got weird balance."));
}
}).fail([=](const MTP::Error &error) {
_tonRequestId = 0;
LOG(("API Error: Couldn't get TON balance, error: %1"
).arg(error.type()));
}).send();
}
bool Credits::tonLoaded() const {
return _tonLastLoaded != 0;
}
rpl::producer<bool> Credits::tonLoadedValue() const {
if (tonLoaded()) {
return rpl::single(true);
}
return rpl::single(
false
) | rpl::then(_tonLoadedChanges.events() | rpl::map_to(true));
}
CreditsAmount Credits::tonBalance() const {
return _tonBalance.current();
}
rpl::producer<CreditsAmount> Credits::tonBalanceValue() const {
return _tonBalance.value();
}
void Credits::updateNonLockedValue() {
_nonLockedBalance = (_balance >= _locked)
? (_balance - _locked)
@ -133,7 +182,12 @@ void Credits::invalidate() {
void Credits::apply(CreditsAmount balance) {
if (balance.ton()) {
_balanceTon = balance;
_tonBalance = balance;
const auto was = std::exchange(_tonLastLoaded, crl::now());
if (!was) {
_tonLoadedChanges.fire({});
}
} else {
_balance = balance;
updateNonLockedValue();

View file

@ -19,12 +19,8 @@ public:
~Credits();
void load(bool force = false);
void apply(CreditsAmount balance);
void apply(PeerId peerId, CreditsAmount balance);
[[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<bool> loadedValue() const;
[[nodiscard]] CreditsAmount balance() const;
[[nodiscard]] CreditsAmount balance(PeerId peerId) const;
[[nodiscard]] rpl::producer<CreditsAmount> balanceValue() const;
@ -33,6 +29,15 @@ public:
[[nodiscard]] rpl::producer<> refreshedByPeerId(PeerId peerId);
void tonLoad(bool force = false);
[[nodiscard]] bool tonLoaded() const;
[[nodiscard]] rpl::producer<bool> tonLoadedValue() const;
[[nodiscard]] CreditsAmount tonBalance() const;
[[nodiscard]] rpl::producer<CreditsAmount> tonBalanceValue() const;
void apply(CreditsAmount balance);
void apply(PeerId peerId, CreditsAmount balance);
[[nodiscard]] bool statsEnabled() const;
void applyCurrency(PeerId peerId, uint64 balance);
@ -56,13 +61,17 @@ private:
base::flat_map<PeerId, uint64> _cachedPeerCurrencyBalances;
CreditsAmount _balance;
CreditsAmount _balanceTon;
CreditsAmount _locked;
rpl::variable<CreditsAmount> _nonLockedBalance;
rpl::event_stream<> _loadedChanges;
crl::time _lastLoaded = 0;
float64 _rate = 0.;
rpl::variable<CreditsAmount> _tonBalance;
rpl::event_stream<> _tonLoadedChanges;
crl::time _tonLastLoaded = false;
mtpRequestId _tonRequestId = 0;
bool _statsEnabled = false;
rpl::event_stream<PeerId> _refreshedByPeerId;

View file

@ -1262,7 +1262,9 @@ void ApplyChannelUpdate(
| Flag::PaidMediaAllowed
| Flag::CanViewCreditsRevenue
| Flag::StargiftsAvailable
| Flag::PaidMessagesAvailable;
| Flag::PaidMessagesAvailable
| Flag::HasStarsPerMessage
| Flag::StarsPerMessageKnown;
channel->setFlags((channel->flags() & ~mask)
| (update.is_can_set_username() ? Flag::CanSetUsername : Flag())
| (update.is_can_view_participants()
@ -1289,7 +1291,9 @@ void ApplyChannelUpdate(
: Flag())
| (update.is_paid_messages_available()
? Flag::PaidMessagesAvailable
: Flag()));
: Flag())
| (channel->starsPerMessage() ? Flag::HasStarsPerMessage : Flag())
| Flag::StarsPerMessageKnown);
channel->setUserpicPhoto(update.vchat_photo());
if (const auto migratedFrom = update.vmigrated_from_chat_id()) {
channel->addFlags(Flag::Megagroup);

View file

@ -83,6 +83,8 @@ enum class ChannelDataFlag : uint64 {
MonoforumAdmin = (1ULL << 40),
MonoforumDisabled = (1ULL << 41),
ForumTabs = (1ULL << 42),
HasStarsPerMessage = (1ULL << 43),
StarsPerMessageKnown = (1ULL << 44),
};
inline constexpr bool is_flag_type(ChannelDataFlag) { return true; };
using ChannelDataFlags = base::flags<ChannelDataFlag>;
@ -280,6 +282,12 @@ public:
[[nodiscard]] bool paidMessagesAvailable() const {
return flags() & Flag::PaidMessagesAvailable;
}
[[nodiscard]] bool hasStarsPerMessage() const {
return flags() & Flag::HasStarsPerMessage;
}
[[nodiscard]] bool starsPerMessageKnown() const {
return flags() & Flag::StarsPerMessageKnown;
}
[[nodiscard]] bool useSubsectionTabs() const;
[[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights(

View file

@ -992,7 +992,14 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
? Flag::StoriesHidden
: Flag())
| Flag::AutoTranslation
| Flag::Monoforum;
| Flag::Monoforum
| Flag::HasStarsPerMessage
| Flag::StarsPerMessageKnown;
const auto hasStarsPerMessage
= data.vsend_paid_messages_stars().has_value();
if (!hasStarsPerMessage) {
channel->setStarsPerMessage(0);
}
const auto storiesState = minimal
? std::optional<Data::Stories::PeerSourceState>()
: data.is_stories_unavailable()
@ -1034,7 +1041,13 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
? Flag::StoriesHidden
: Flag())
| (data.is_autotranslation() ? Flag::AutoTranslation : Flag())
| (data.is_monoforum() ? Flag::Monoforum : Flag());
| (data.is_monoforum() ? Flag::Monoforum : Flag())
| (hasStarsPerMessage
? (Flag::HasStarsPerMessage
| (channel->starsPerMessageKnown()
? Flag::StarsPerMessageKnown
: Flag()))
: Flag::StarsPerMessageKnown);
channel->setFlags((channel->flags() & ~flagsMask) | flagsSet);
channel->setBotVerifyDetailsIcon(
data.vbot_verification_icon().value_or_empty());
@ -1047,12 +1060,6 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
}
channel->setPhoto(data.vphoto());
const auto hasStarsPerMessage
= data.vsend_paid_messages_stars().has_value();
if (!hasStarsPerMessage) {
channel->setStarsPerMessage(0);
}
if (const auto monoforum = data.vlinked_monoforum_id()) {
if (const auto linked = channelLoaded(monoforum->v)) {
channel->setMonoforumLink(linked);

View file

@ -719,6 +719,7 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
| Flag::CanPinMessages
| Flag::VoiceMessagesForbidden
| Flag::ReadDatesPrivate
| Flag::HasStarsPerMessage
| Flag::MessageMoneyRestrictionsKnown
| Flag::RequiresPremiumToWrite;
user->setFlags((user->flags() & ~mask)
@ -732,6 +733,7 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
? Flag::VoiceMessagesForbidden
: Flag())
| (update.is_read_dates_private() ? Flag::ReadDatesPrivate : Flag())
| (user->starsPerMessage() ? Flag::HasStarsPerMessage : Flag())
| Flag::MessageMoneyRestrictionsKnown
| (update.is_contact_require_premium()
? Flag::RequiresPremiumToWrite

View file

@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_user.h"
#include "history/view/controls/history_view_suggest_options.h"
#include "history/history.h"
#include "history/history_item_components.h"
#include "main/main_account.h"
@ -189,20 +190,27 @@ Data::SendErrorWithThread GetErrorForSending(
std::optional<SendPaymentDetails> ComputePaymentDetails(
not_null<PeerData*> peer,
int messagesCount) {
if (const auto user = peer->asUser()) {
if (user->hasStarsPerMessage()
&& !user->messageMoneyRestrictionsKnown()) {
user->updateFull();
return {};
}
} else if (const auto channel = peer->asChannel()) {
if (!channel->isFullLoaded()) {
channel->updateFull();
return {};
}
const auto user = peer->asUser();
const auto channel = user ? nullptr : peer->asChannel();
const auto has = (user && user->hasStarsPerMessage())
|| (channel && channel->hasStarsPerMessage());
if (!has) {
return SendPaymentDetails();
}
if (!peer->session().credits().loaded()) {
const auto known1 = peer->session().credits().loaded();
if (!known1) {
peer->session().credits().load();
}
const auto known2 = user
? user->messageMoneyRestrictionsKnown()
: channel->starsPerMessageKnown();
if (!known2) {
peer->updateFull();
}
if (!known1 || !known2) {
return {};
} else if (const auto perMessage = peer->starsPerMessageChecked()) {
return SendPaymentDetails{
@ -213,6 +221,21 @@ std::optional<SendPaymentDetails> ComputePaymentDetails(
return SendPaymentDetails();
}
bool SuggestPaymentDataReady(
not_null<PeerData*> peer,
SuggestPostOptions suggest) {
if (!suggest.exists || !suggest.price()) {
return true;
} else if (suggest.ton && !peer->session().credits().tonLoaded()) {
peer->session().credits().tonLoad();
return false;
} else if (!suggest.ton && !peer->session().credits().loaded()) {
peer->session().credits().load();
return false;
}
return true;
}
object_ptr<Ui::BoxContent> MakeSendErrorBox(
const Data::SendErrorWithThread &error,
bool withTitle) {
@ -250,13 +273,15 @@ void ShowSendPaidConfirm(
not_null<PeerData*> peer,
SendPaymentDetails details,
Fn<void()> confirmed,
PaidConfirmStyles styles) {
PaidConfirmStyles styles,
int suggestStarsPrice) {
return ShowSendPaidConfirm(
navigation->uiShow(),
peer,
details,
confirmed,
styles);
styles,
suggestStarsPrice);
}
void ShowSendPaidConfirm(
@ -264,13 +289,15 @@ void ShowSendPaidConfirm(
not_null<PeerData*> peer,
SendPaymentDetails details,
Fn<void()> confirmed,
PaidConfirmStyles styles) {
PaidConfirmStyles styles,
int suggestStarsPrice) {
ShowSendPaidConfirm(
std::move(show),
std::vector<not_null<PeerData*>>{ peer },
details,
confirmed,
styles);
styles,
suggestStarsPrice);
}
void ShowSendPaidConfirm(
@ -278,7 +305,8 @@ void ShowSendPaidConfirm(
const std::vector<not_null<PeerData*>> &peers,
SendPaymentDetails details,
Fn<void()> confirmed,
PaidConfirmStyles styles) {
PaidConfirmStyles styles,
int suggestStarsPrice) {
Expects(!peers.empty());
const auto singlePeer = (peers.size() > 1)
@ -286,7 +314,7 @@ void ShowSendPaidConfirm(
: peers.front().get();
const auto singlePeerId = singlePeer ? singlePeer->id : PeerId();
const auto check = [=] {
const auto required = details.stars;
const auto required = details.stars + suggestStarsPrice;
if (!required) {
return;
}
@ -296,10 +324,13 @@ void ShowSendPaidConfirm(
confirmed();
}
};
Settings::MaybeRequestBalanceIncrease(
using namespace Settings;
MaybeRequestBalanceIncrease(
show,
required,
Settings::SmallBalanceForMessage{ .recipientId = singlePeerId },
(suggestStarsPrice
? SmallBalanceSource(SmallBalanceForSuggest{ singlePeerId })
: SmallBalanceForMessage{ singlePeerId }),
done);
};
auto usersOnly = true;
@ -388,15 +419,15 @@ void ShowSendPaidConfirm(
bool SendPaymentHelper::check(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Api::SendOptions options,
int messagesCount,
int starsApproved,
Fn<void(int)> resend,
PaidConfirmStyles styles) {
return check(
navigation->uiShow(),
peer,
options,
messagesCount,
starsApproved,
std::move(resend),
styles);
}
@ -404,17 +435,27 @@ bool SendPaymentHelper::check(
bool SendPaymentHelper::check(
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> peer,
Api::SendOptions options,
int messagesCount,
int starsApproved,
Fn<void(int)> resend,
PaidConfirmStyles styles) {
clear();
const auto suggest = options.suggest;
const auto starsApproved = options.starsApproved;
const auto suggestPriceStars = suggest.ton
? 0
: int(base::SafeRound(suggest.price().value()));
const auto suggestPriceTon = suggest.ton
? suggest.price()
: CreditsAmount();
const auto details = ComputePaymentDetails(peer, messagesCount);
if (!details) {
const auto suggestDetails = SuggestPaymentDataReady(peer, suggest);
if (!details || !suggestDetails) {
_resend = [=] { resend(starsApproved); };
if (!peer->session().credits().loaded()) {
if ((!details || !suggest.ton)
&& !peer->session().credits().loaded()) {
peer->session().credits().loadedValue(
) | rpl::filter(
rpl::mappers::_1
@ -425,6 +466,18 @@ bool SendPaymentHelper::check(
}, _lifetime);
}
if ((!suggestDetails && suggest.ton)
&& !peer->session().credits().tonLoaded()) {
peer->session().credits().tonLoadedValue(
) | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next([=] {
if (const auto callback = base::take(_resend)) {
callback();
}
}, _lifetime);
}
peer->session().changes().peerUpdates(
peer,
Data::PeerUpdate::Flag::FullInfo
@ -438,7 +491,32 @@ bool SendPaymentHelper::check(
} else if (const auto stars = details->stars; stars > starsApproved) {
ShowSendPaidConfirm(show, peer, *details, [=] {
resend(stars);
}, styles);
}, styles, suggestPriceStars);
return false;
} else if (suggestPriceStars
&& (CreditsAmount(details->stars + suggestPriceStars)
> peer->session().credits().balance())) {
const auto peerId = peer->id;
const auto forMessages = details->stars;
const auto required = forMessages + suggestPriceStars;
const auto done = [=](Settings::SmallBalanceResult result) {
if (result == Settings::SmallBalanceResult::Success
|| result == Settings::SmallBalanceResult::Already) {
resend(forMessages);
}
};
using namespace Settings;
MaybeRequestBalanceIncrease(
show,
required,
SmallBalanceForSuggest{ peerId },
done);
return false;
}
if (suggestPriceTon
&& suggestPriceTon > peer->session().credits().tonBalance()) {
show->show(
Box(HistoryView::InsufficientTonBox, peer, suggestPriceTon));
return false;
}
return true;

View file

@ -149,6 +149,10 @@ struct SendPaymentDetails {
not_null<PeerData*> peer,
int messagesCount);
[[nodiscard]] bool SuggestPaymentDataReady(
not_null<PeerData*> peer,
SuggestPostOptions suggest);
struct PaidConfirmStyles {
const style::FlatLabel *label = nullptr;
const style::Checkbox *checkbox = nullptr;
@ -158,34 +162,37 @@ void ShowSendPaidConfirm(
not_null<PeerData*> peer,
SendPaymentDetails details,
Fn<void()> confirmed,
PaidConfirmStyles styles = {});
PaidConfirmStyles styles = {},
int suggestStarsPrice = 0);
void ShowSendPaidConfirm(
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> peer,
SendPaymentDetails details,
Fn<void()> confirmed,
PaidConfirmStyles styles = {});
PaidConfirmStyles styles = {},
int suggestStarsPrice = 0);
void ShowSendPaidConfirm(
std::shared_ptr<Main::SessionShow> show,
const std::vector<not_null<PeerData*>> &peers,
SendPaymentDetails details,
Fn<void()> confirmed,
PaidConfirmStyles styles = {});
PaidConfirmStyles styles = {},
int suggestStarsPrice = 0);
class SendPaymentHelper final {
public:
[[nodiscard]] bool check(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Api::SendOptions options,
int messagesCount,
int starsApproved,
Fn<void(int)> resend,
PaidConfirmStyles styles = {});
[[nodiscard]] bool check(
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> peer,
Api::SendOptions options,
int messagesCount,
int starsApproved,
Fn<void(int)> resend,
PaidConfirmStyles styles = {});

View file

@ -4540,7 +4540,7 @@ void HistoryWidget::saveEditMessage(Api::SendOptions options) {
};
const auto checked = checkSendPayment(
1 + int(_forwardPanel->items().size()),
options.starsApproved,
options,
withPaymentApproved);
if (!checked) {
return;
@ -4629,15 +4629,15 @@ void HistoryWidget::sendVoice(const VoiceToSend &data) {
copy.options.starsApproved = approved;
sendVoice(copy);
};
auto action = prepareSendAction(data.options);
const auto checked = checkSendPayment(
1 + int(_forwardPanel->items().size()),
data.options.starsApproved,
action.options,
withPaymentApproved);
if (!checked) {
return;
}
auto action = prepareSendAction(data.options);
session().api().sendVoiceMessage(
data.bytes,
data.waveform,
@ -4678,7 +4678,7 @@ void HistoryWidget::send(Api::SendOptions options) {
message.textWithTags,
ignoreSlowmodeCountdown,
withPaymentApproved,
options.starsApproved)) {
message.action.options)) {
return;
}
@ -5011,14 +5011,14 @@ FullMsgId HistoryWidget::cornerButtonsCurrentId() {
bool HistoryWidget::checkSendPayment(
int messagesCount,
int starsApproved,
Api::SendOptions options,
Fn<void(int)> withPaymentApproved) {
return _peer
&& _sendPayment.check(
controller(),
_peer,
options,
messagesCount,
starsApproved,
std::move(withPaymentApproved));
}
@ -5209,9 +5209,11 @@ void HistoryWidget::sendBotCommand(
copy.starsApproved = approved;
sendBotCommand(request, copy);
};
const auto action = prepareSendAction(options);
const auto checked = checkSendPayment(
1,
options.starsApproved,
action.options,
withPaymentApproved);
if (!checked) {
return;
@ -5226,7 +5228,7 @@ void HistoryWidget::sendBotCommand(
? request.command
: Bot::WrapCommandInChat(_peer, request.command, request.context);
auto message = Api::MessageToSend(prepareSendAction(options));
auto message = Api::MessageToSend(action);
message.textWithTags = { toSend, TextWithTags::Tags() };
message.action.replyTo = request.replyTo
? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/)
@ -6233,7 +6235,7 @@ bool HistoryWidget::showSendMessageError(
const TextWithTags &textWithTags,
bool ignoreSlowmodeCountdown,
Fn<void(int starsApproved)> withPaymentApproved,
int starsApproved) {
Api::SendOptions options) {
if (!_canSendMessages) {
return false;
}
@ -6254,7 +6256,7 @@ bool HistoryWidget::showSendMessageError(
return withPaymentApproved
&& !checkSendPayment(
request.messagesCount,
starsApproved,
options,
withPaymentApproved);
}
@ -6369,6 +6371,11 @@ void HistoryWidget::sendingFilesConfirmed(
void HistoryWidget::sendingFilesConfirmed(
std::shared_ptr<Ui::PreparedBundle> bundle,
Api::SendOptions options) {
const auto compress = bundle->way.sendImagesAsPhotos();
const auto type = compress ? SendMediaType::Photo : SendMediaType::File;
auto action = prepareSendAction(options);
action.clearDraft = false;
const auto withPaymentApproved = [=](int approved) {
auto copy = options;
copy.starsApproved = approved;
@ -6376,16 +6383,12 @@ void HistoryWidget::sendingFilesConfirmed(
};
const auto checked = checkSendPayment(
bundle->totalCount,
options.starsApproved,
action.options,
withPaymentApproved);
if (!checked) {
return;
}
const auto compress = bundle->way.sendImagesAsPhotos();
const auto type = compress ? SendMediaType::Photo : SendMediaType::File;
auto action = prepareSendAction(options);
action.clearDraft = false;
if (bundle->sendComment) {
auto message = Api::MessageToSend(action);
message.textWithTags = base::take(bundle->caption);
@ -7715,9 +7718,13 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) {
copy.options.starsApproved = approved;
sendInlineResult(copy);
};
auto action = prepareSendAction(result.options);
action.generateLocal = true;
const auto checked = checkSendPayment(
1,
result.options.starsApproved,
action.options,
withPaymentApproved);
if (!checked) {
return;
@ -7725,9 +7732,6 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) {
controller()->sendingAnimation().appendSending(
result.messageSendingFrom);
auto action = prepareSendAction(result.options);
action.generateLocal = true;
session().api().sendInlineResult(
result.bot,
result.result.get(),
@ -8336,7 +8340,7 @@ bool HistoryWidget::sendExistingDocument(
};
const auto checked = checkSendPayment(
1,
messageToSend.action.options.starsApproved,
messageToSend.action.options,
withPaymentApproved);
if (!checked) {
return false;
@ -8375,6 +8379,7 @@ bool HistoryWidget::sendExistingPhoto(
} else if (showSlowmodeError()) {
return false;
}
const auto action = prepareSendAction(options);
const auto withPaymentApproved = [=](int approved) {
auto copy = options;
@ -8383,15 +8388,13 @@ bool HistoryWidget::sendExistingPhoto(
};
const auto checked = checkSendPayment(
1,
options.starsApproved,
action.options,
withPaymentApproved);
if (!checked) {
return false;
}
Api::SendExistingPhoto(
Api::MessageToSend(prepareSendAction(options)),
photo);
Api::SendExistingPhoto(Api::MessageToSend(action), photo);
hideSelectorControlsAnimated();

View file

@ -371,7 +371,7 @@ private:
[[nodiscard]] bool checkSendPayment(
int messagesCount,
int starsApproved,
Api::SendOptions options,
Fn<void(int)> withPaymentApproved);
void checkSuggestToGigagroup();
@ -489,7 +489,7 @@ private:
const TextWithTags &textWithTags,
bool ignoreSlowmodeCountdown,
Fn<void(int starsApproved)> withPaymentApproved = nullptr,
int starsApproved = 0);
Api::SendOptions options = {});
void sendingFilesConfirmed(
Ui::PreparedList &&list,

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "chat_helpers/compose/compose_show.h"
#include "core/ui_integration.h"
#include "data/components/credits.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_channel.h"
#include "data/data_media_types.h"
@ -19,9 +20,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item_components.h"
#include "info/channel_statistics/earn/earn_icons.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "settings/settings_common.h"
#include "settings/settings_credits_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/boxes/choose_date_time.h"
@ -30,8 +33,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/basic_click_handlers.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "styles/style_boxes.h"
#include "styles/style_channel_earn.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
@ -81,13 +87,19 @@ void ChooseSuggestPriceBox(
std::vector<Button> buttons;
rpl::variable<TimeId> date;
rpl::variable<bool> ton;
Fn<void()> save;
bool savePending = false;
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();
const auto peer = args.peer;
const auto session = &peer->session();
session->credits().load();
session->credits().tonLoad();
const auto limit = session->appConfig().suggestedPostStarsMax();
box->setTitle((args.mode == SuggestMode::New)
? tr::lng_suggest_options_title()
@ -196,7 +208,7 @@ void ChooseSuggestPriceBox(
Ui::AddSkip(container);
const auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding;
const auto manager = &args.session->data().customEmojiManager();
const auto manager = &session->data().customEmojiManager();
const auto makeIcon = [&](
not_null<QWidget*> parent,
TextWithEntities text) {
@ -205,7 +217,7 @@ void ChooseSuggestPriceBox(
rpl::single(text),
st::defaultFlatLabel,
st::defaultPopupMenu,
Core::TextContext({ .session = args.session }));
Core::TextContext({ .session = session }));
};
const auto starsWrap = container->add(
@ -333,7 +345,7 @@ void ChooseSuggestPriceBox(
}
};
auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{
.session = args.session,
.session = session,
.done = done,
.value = state->date.current(),
.mode = args.mode,
@ -344,8 +356,8 @@ void ChooseSuggestPriceBox(
Ui::AddSkip(container);
Ui::AddDividerText(container, tr::lng_suggest_options_date_about());
AssertIsDebug()//tr::lng_suggest_options_offer
const auto save = [=] {
AssertIsDebug();//tr::lng_suggest_options_offer
state->save = [=] {
auto nanos = int64();
if (state->ton.current()) {
const auto now = Ui::ParseTonAmountString(
@ -363,22 +375,74 @@ void ChooseSuggestPriceBox(
}
nanos = now * Ui::kNanosInOne;
}
const auto ton = uint32(state->ton.current() ? 1 : 0);
const auto value = CreditsAmount(
nanos / Ui::kNanosInOne,
nanos % Ui::kNanosInOne);
nanos % Ui::kNanosInOne,
ton ? CreditsType::Ton : CreditsType::Stars);
const auto credits = &session->credits();
if (ton) {
if (!credits->tonLoaded()) {
state->savePending = true;
return;
} else if (credits->tonBalance() < value) {
box->uiShow()->show(
Box(InsufficientTonBox, peer, value));
return;
}
} else {
if (!credits->loaded()) {
state->savePending = true;
return;
}
using namespace Settings;
const auto required = peer->starsPerMessageChecked()
+ int(base::SafeRound(value.value()));
const auto done = [=](SmallBalanceResult result) {
if (result == SmallBalanceResult::Success
|| result == SmallBalanceResult::Already) {
state->save();
}
};
MaybeRequestBalanceIncrease(
Main::MakeSessionShow(box->uiShow(), session),
required,
SmallBalanceForSuggest{ peer->id },
done);
return;
}
state->save = nullptr;
args.done({
.exists = true,
.priceWhole = uint32(value.whole()),
.priceNano = uint32(value.nano()),
.ton = uint32(state->ton.current() ? 1 : 0),
.ton = ton,
.date = state->date.current(),
});
};
QObject::connect(starsField, &Ui::NumberInput::submitted, box, save);
tonField->submits() | rpl::start_with_next(save, tonField->lifetime());
const auto credits = &session->credits();
rpl::combine(
credits->tonBalanceValue(),
credits->balanceValue()
) | rpl::filter([=] {
return state->savePending;
}) | rpl::start_with_next([=] {
state->savePending = false;
if (const auto onstack = state->save) {
onstack();
}
}, box->lifetime());
box->addButton(tr::lng_settings_save(), save);
QObject::connect(
starsField,
&Ui::NumberInput::submitted,
box,
state->save);
tonField->submits(
) | rpl::start_with_next(state->save, tonField->lifetime());
box->addButton(tr::lng_settings_save(), state->save);
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
@ -402,6 +466,52 @@ bool CanAddOfferToMessage(not_null<HistoryItem*> item) {
history->owner().history(broadcast)).has_value();
}
void InsufficientTonBox(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer,
CreditsAmount required) {
auto icon = Settings::CreateLottieIcon(
box->verticalLayout(),
{
.name = u"diamond"_q,
.sizeOverride = Size(st::changePhoneIconSize),
},
{});
box->setShowFinishedCallback([animate = std::move(icon.animate)] {
animate(anim::repeat::loop);
});
box->addRow(std::move(icon.widget), st::lowTonIconPadding);
const auto add = required - peer->session().credits().tonBalance();
const auto nano = add.whole() * Ui::kNanosInOne + add.nano();
const auto amount = Ui::FormatTonAmount(nano).full;
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_suggest_low_ton_title(tr::now, lt_amount, amount),
st::boxTitle)),
st::boxRowPadding + st::lowTonTitlePadding);
const auto label = box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_suggest_low_ton_text(
lt_channel,
rpl::single(Ui::Text::Bold(peer->name())),
Ui::Text::RichLangValue),
st::lowTonText),
st::boxRowPadding + st::lowTonTextPadding);
label->setTryMakeSimilarLines(true);
label->resizeToWidth(
st::boxWidth - st::boxRowPadding.left() - st::boxRowPadding.right());
box->addButton(tr::lng_suggest_low_ton_fragment(), [=] {
UrlClickHandler::Open(tr::lng_suggest_low_ton_fragment_url(tr::now));
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
SuggestOptions::SuggestOptions(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
@ -458,7 +568,7 @@ void SuggestOptions::edit() {
}
};
*weak = _show->show(Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{
.session = &_peer->session(),
.peer = _peer,
.done = apply,
.value = _values,
}));

View file

@ -44,7 +44,7 @@ void ChooseSuggestTimeBox(
SuggestTimeBoxArgs &&args);
struct SuggestPriceBoxArgs {
not_null<Main::Session*> session;
not_null<PeerData*> peer;
bool updating = false;
Fn<void(SuggestPostOptions)> done;
SuggestPostOptions value;
@ -58,6 +58,11 @@ void ChooseSuggestPriceBox(
[[nodiscard]] bool CanAddOfferToMessage(not_null<HistoryItem*> item);
void InsufficientTonBox(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer,
CreditsAmount required);
class SuggestOptions final {
public:
SuggestOptions(

View file

@ -1195,13 +1195,13 @@ void ChatWidget::sendingFilesConfirmed(
bool ChatWidget::checkSendPayment(
int messagesCount,
int starsApproved,
Api::SendOptions options,
Fn<void(int)> withPaymentApproved) {
return _sendPayment.check(
controller(),
_peer,
options,
messagesCount,
starsApproved,
std::move(withPaymentApproved));
}
@ -1215,7 +1215,7 @@ void ChatWidget::sendingFilesConfirmed(
};
const auto checked = checkSendPayment(
bundle->totalCount,
options.starsApproved,
options,
withPaymentApproved);
if (!checked) {
return;
@ -1386,7 +1386,7 @@ void ChatWidget::sendVoice(const ComposeControls::VoiceToSend &data) {
};
const auto checked = checkSendPayment(
1,
data.options.starsApproved,
data.options,
withPaymentApproved);
if (!checked) {
return;
@ -1438,7 +1438,7 @@ void ChatWidget::send(Api::SendOptions options) {
};
const auto checked = checkSendPayment(
request.messagesCount,
options.starsApproved,
options,
withPaymentApproved);
if (!checked) {
return;
@ -1669,7 +1669,7 @@ bool ChatWidget::sendExistingDocument(
};
const auto checked = checkSendPayment(
1,
messageToSend.action.options.starsApproved,
messageToSend.action.options,
withPaymentApproved);
if (!checked) {
return false;
@ -1709,7 +1709,7 @@ bool ChatWidget::sendExistingPhoto(
};
const auto checked = checkSendPayment(
1,
options.starsApproved,
options,
withPaymentApproved);
if (!checked) {
return false;
@ -1752,7 +1752,7 @@ void ChatWidget::sendInlineResult(
};
const auto checked = checkSendPayment(
1,
options.starsApproved,
options,
withPaymentApproved);
if (!checked) {
return;
@ -3158,7 +3158,7 @@ void ChatWidget::sendBotCommandWithOptions(
};
const auto checked = checkSendPayment(
1,
options.starsApproved,
options,
withPaymentApproved);
if (!checked) {
return;

View file

@ -227,7 +227,7 @@ private:
[[nodiscard]] bool checkSendPayment(
int messagesCount,
int starsApproved,
Api::SendOptions options,
Fn<void(int)> withPaymentApproved);
void markLoaded();

View file

@ -1910,8 +1910,8 @@ void WebViewInstance::botSendPreparedMessage(
const auto checked = state->sendPayment.check(
uiShow(),
strong->peer(),
options,
1,
options.starsApproved,
withPaymentApproved);
if (!checked) {
return;
@ -2545,8 +2545,8 @@ void ChooseAndSendLocation(
const auto checked = state->sendPayment.check(
strong,
action.history->peer,
action.options,
1,
action.options.starsApproved,
withPaymentApproved);
if (!checked) {
return;

View file

@ -257,7 +257,7 @@ bool ReplyArea::send(
};
const auto checked = checkSendPayment(
request.messagesCount,
message.action.options.starsApproved,
message.action.options,
withPaymentApproved);
if (!checked) {
return false;
@ -273,7 +273,7 @@ bool ReplyArea::send(
bool ReplyArea::checkSendPayment(
int messagesCount,
int starsApproved,
Api::SendOptions options,
Fn<void(int)> withPaymentApproved) {
const auto st1 = ::Settings::DarkCreditsEntryBoxStyle();
const auto st2 = st1.shareBox.get();
@ -282,8 +282,8 @@ bool ReplyArea::checkSendPayment(
&& _sendPayment.check(
_controller->uiShow(),
_data.peer,
options,
messagesCount,
starsApproved,
std::move(withPaymentApproved),
{
.label = st3 ? st3->chooseDateTimeArgs.labelStyle : nullptr,
@ -292,6 +292,8 @@ bool ReplyArea::checkSendPayment(
}
void ReplyArea::sendVoice(const VoiceToSend &data) {
auto action = prepareSendAction(data.options);
const auto withPaymentApproved = [=](int approved) {
auto copy = data;
copy.options.starsApproved = approved;
@ -299,13 +301,12 @@ void ReplyArea::sendVoice(const VoiceToSend &data) {
};
const auto checked = checkSendPayment(
1,
data.options.starsApproved,
action.options,
withPaymentApproved);
if (!checked) {
return;
}
auto action = prepareSendAction(data.options);
session().api().sendVoiceMessage(
data.bytes,
data.waveform,
@ -341,7 +342,7 @@ bool ReplyArea::sendExistingDocument(
};
const auto checked = checkSendPayment(
1,
messageToSend.action.options.starsApproved,
messageToSend.action.options,
withPaymentApproved);
if (!checked) {
return false;
@ -373,6 +374,8 @@ bool ReplyArea::sendExistingPhoto(
} else if (showSlowmodeError()) {
return false;
}
const auto action = prepareSendAction(options);
const auto withPaymentApproved = [=](int approved) {
auto copy = options;
copy.starsApproved = approved;
@ -380,15 +383,13 @@ bool ReplyArea::sendExistingPhoto(
};
const auto checked = checkSendPayment(
1,
options.starsApproved,
action.options,
withPaymentApproved);
if (!checked) {
return false;
}
Api::SendExistingPhoto(
Api::MessageToSend(prepareSendAction(options)),
photo);
Api::SendExistingPhoto(Api::MessageToSend(action), photo);
_controls->cancelReplyMessage();
finishSending();
@ -411,6 +412,9 @@ void ReplyArea::sendInlineResult(
not_null<UserData*> bot,
Api::SendOptions options,
std::optional<MsgId> localMessageId) {
auto action = prepareSendAction(options);
action.generateLocal = true;
const auto withPaymentApproved = [=](int approved) {
auto copy = options;
copy.starsApproved = approved;
@ -418,14 +422,12 @@ void ReplyArea::sendInlineResult(
};
const auto checked = checkSendPayment(
1,
options.starsApproved,
action.options,
withPaymentApproved);
if (!checked) {
return;
}
auto action = prepareSendAction(options);
action.generateLocal = true;
session().api().sendInlineResult(
bot,
result.get(),
@ -677,6 +679,11 @@ void ReplyArea::sendingFilesConfirmed(
void ReplyArea::sendingFilesConfirmed(
std::shared_ptr<Ui::PreparedBundle> bundle,
Api::SendOptions options) {
const auto compress = bundle->way.sendImagesAsPhotos();
const auto type = compress ? SendMediaType::Photo : SendMediaType::File;
auto action = prepareSendAction(options);
action.clearDraft = false;
const auto withPaymentApproved = [=](int approved) {
auto copy = options;
copy.starsApproved = approved;
@ -684,16 +691,12 @@ void ReplyArea::sendingFilesConfirmed(
};
const auto checked = checkSendPayment(
bundle->totalCount,
options.starsApproved,
action.options,
withPaymentApproved);
if (!checked) {
return;
}
const auto compress = bundle->way.sendImagesAsPhotos();
const auto type = compress ? SendMediaType::Photo : SendMediaType::File;
auto action = prepareSendAction(options);
action.clearDraft = false;
if (bundle->sendComment) {
auto message = Api::MessageToSend(action);
message.textWithTags = base::take(bundle->caption);

View file

@ -96,7 +96,7 @@ private:
[[nodiscard]] bool checkSendPayment(
int messagesCount,
int starsApproved,
Api::SendOptions options,
Fn<void(int)> withPaymentApproved);
void uploadFile(const QByteArray &fileContent, SendMediaType type);

View file

@ -2396,6 +2396,10 @@ void SmallBalanceBox(
return value.recipientId
? owner->peer(value.recipientId)->shortName()
: QString();
}, [&](SmallBalanceForSuggest value) {
return value.recipientId
? owner->peer(value.recipientId)->shortName()
: QString();
});
auto needed = show->session().credits().balanceValue(
@ -2442,6 +2446,11 @@ void SmallBalanceBox(
lt_user,
rpl::single(Ui::Text::Bold(name)),
Ui::Text::RichLangValue))
: v::is<SmallBalanceForSuggest>(source)
? tr::lng_credits_small_balance_for_suggest(
lt_channel,
rpl::single(Ui::Text::Bold(name)),
Ui::Text::RichLangValue)
: name.isEmpty()
? tr::lng_credits_small_balance_fallback(
Ui::Text::RichLangValue)

View file

@ -233,13 +233,17 @@ struct SmallBalanceStarGift {
struct SmallBalanceForMessage {
PeerId recipientId;
};
struct SmallBalanceForSuggest {
PeerId recipientId;
};
struct SmallBalanceSource : std::variant<
SmallBalanceBot,
SmallBalanceReaction,
SmallBalanceSubscription,
SmallBalanceDeepLink,
SmallBalanceStarGift,
SmallBalanceForMessage> {
SmallBalanceForMessage,
SmallBalanceForSuggest> {
using variant::variant;
};

View file

@ -1370,3 +1370,11 @@ tonInput: InputField(defaultInputField) {
starsFieldIconPosition: point(0px, 10px);
tonFieldIconSize: 16px;
tonFieldIconPosition: point(2px, 9px);
lowTonIconPadding: margins(12px, 20px, 12px, 0px);
lowTonTitlePadding: margins(0px, 12px, 0px, 12px);
lowTonTextPadding: margins(0px, 0px, 0px, 8px);
lowTonText: FlatLabel(defaultFlatLabel) {
minWidth: 100px;
align: align(top);
}

View file

@ -1798,6 +1798,10 @@ void PeerMenuShareContactBox(
state->share = nullptr;
return;
}
auto action = Api::SendAction(strong, options);
action.clearDraft = false;
const auto withPaymentApproved = [=](int stars) {
if (const auto onstack = state->share) {
auto copy = options;
@ -1808,8 +1812,8 @@ void PeerMenuShareContactBox(
const auto checked = state->sendPayment.check(
navigation,
peer,
action.options,
1,
options.starsApproved,
withPaymentApproved);
if (!checked) {
return;
@ -1818,8 +1822,6 @@ void PeerMenuShareContactBox(
strong,
ShowAtTheEndMsgId,
Window::SectionShow::Way::ClearStack);
auto action = Api::SendAction(strong, options);
action.clearDraft = false;
strong->session().api().shareContact(user, action);
state->share = nullptr;
};
@ -1887,6 +1889,12 @@ void PeerMenuCreatePoll(
const auto weak = QPointer<CreatePollBox>(box);
const auto state = box->lifetime().make_state<State>();
state->create = [=](const CreatePollBox::Result &result) {
auto action = Api::SendAction(
peer->owner().history(peer),
result.options);
action.replyTo = replyTo;
action.options.suggest = suggest;
const auto withPaymentApproved = crl::guard(weak, [=](int stars) {
if (const auto onstack = state->create) {
auto copy = result;
@ -1897,17 +1905,13 @@ void PeerMenuCreatePoll(
const auto checked = state->sendPayment.check(
controller,
peer,
action.options,
1,
result.options.starsApproved,
withPaymentApproved);
if (!checked || std::exchange(state->lock, true)) {
return;
}
auto action = Api::SendAction(
peer->owner().history(peer),
result.options);
action.replyTo = replyTo;
action.options.suggest = suggest;
const auto local = action.history->localDraft(
replyTo.topicRootId,
replyTo.monoforumPeerId);
@ -2002,20 +2006,22 @@ void PeerMenuCreateTodoList(
onstack(copy);
}
});
const auto checked = state->sendPayment.check(
controller,
peer,
1,
result.options.starsApproved,
withPaymentApproved);
if (!checked || std::exchange(state->lock, true)) {
return;
}
auto action = Api::SendAction(
peer->owner().history(peer),
result.options);
action.replyTo = replyTo;
action.options.suggest = suggest;
const auto checked = state->sendPayment.check(
controller,
peer,
action.options,
1,
withPaymentApproved);
if (!checked || std::exchange(state->lock, true)) {
return;
}
const auto local = action.history->localDraft(
replyTo.topicRootId,
replyTo.monoforumPeerId);