From 7b7e18e752b79ba6e2065c71094a021b777e0d32 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 21 Feb 2025 21:29:10 +0400 Subject: [PATCH] Support paid sending in ShareBox. --- Telegram/Resources/langs/lang.strings | 5 + Telegram/SourceFiles/api/api_premium.cpp | 9 +- Telegram/SourceFiles/api/api_premium.h | 8 + .../SourceFiles/boxes/edit_privacy_box.cpp | 2 +- .../boxes/peer_list_controllers.cpp | 144 +++++++++++++----- .../SourceFiles/boxes/peer_list_controllers.h | 42 +++-- .../boxes/peers/edit_peer_invite_link.cpp | 3 + Telegram/SourceFiles/boxes/share_box.cpp | 138 +++++++++++++---- Telegram/SourceFiles/boxes/share_box.h | 3 + .../calls/group/calls_group_settings.cpp | 3 + .../chat_helpers/field_autocomplete.cpp | 3 +- .../history/history_item_helpers.cpp | 113 ++++++++++---- .../history/history_item_helpers.h | 21 ++- .../SourceFiles/history/history_widget.cpp | 20 --- Telegram/SourceFiles/history/history_widget.h | 1 - .../media/stories/media_stories_share.cpp | 5 +- .../payments/ui/payments_reaction_box.cpp | 114 ++++++++------ .../payments/ui/payments_reaction_box.h | 11 ++ .../settings/settings_credits_graphics.cpp | 15 +- .../SourceFiles/window/window_peer_menu.cpp | 1 + 20 files changed, 468 insertions(+), 193 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index aac02c54a..8f6a32ad8 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2795,6 +2795,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_small_balance_subscribe" = "Buy **Stars** and subscribe to **{channel}** and other channels."; "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_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}"; @@ -4834,6 +4835,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payment_confirm_text#other" = "{name} charges **{count}** Stars per message."; "lng_payment_confirm_amount#one" = "**{count}** Star"; "lng_payment_confirm_amount#other" = "**{count}** Stars"; +"lng_payment_confirm_users#one" = "You selected **{count}** user who charge Stars for messages."; +"lng_payment_confirm_users#other" = "You selected **{count}** users who charge Stars for messages."; +"lng_payment_confirm_chats#one" = "You selected **{count}** chat where you pay Stars for messages."; +"lng_payment_confirm_chats#other" = "You selected **{count}** chats where you pay Stars for messages."; "lng_payment_confirm_sure#one" = "Would you like to pay {amount} to send **{count}** message?"; "lng_payment_confirm_sure#other" = "Would you like to pay {amount} to send **{count}** messages?"; "lng_payment_confirm_dont_ask" = "Don't ask me again"; diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index b44ca20ed..fe3004cda 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "apiwrap.h" #include "base/random.h" +#include "data/data_channel.h" #include "data/data_document.h" #include "data/data_peer.h" #include "data/data_peer_values.h" @@ -714,12 +715,18 @@ rpl::producer SponsoredToggle::setToggled(bool v) { MessageMoneyRestriction ResolveMessageMoneyRestrictions( not_null peer, History *maybeHistory) { + if (const auto channel = peer->asChannel()) { + return { + .starsPerMessage = channel->starsPerMessageChecked(), + .known = true, + }; + } const auto user = peer->asUser(); if (!user) { return { .known = true }; } else if (user->messageMoneyRestrictionsKnown()) { return { - .starsPerMessage = user->starsPerMessage(), + .starsPerMessage = user->starsPerMessageChecked(), .premiumRequired = (user->requiresPremiumToWrite() && !user->session().premium()), .known = true, diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index 5f4d06d83..2b692a484 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -250,6 +250,14 @@ struct MessageMoneyRestriction { int starsPerMessage = 0; bool premiumRequired = false; bool known = false; + + explicit operator bool() const { + return starsPerMessage != 0 || premiumRequired; + } + + friend inline bool operator==( + const MessageMoneyRestriction &, + const MessageMoneyRestriction &) = default; }; [[nodiscard]] MessageMoneyRestriction ResolveMessageMoneyRestrictions( not_null peer, diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index f13b5bae0..d905a5d9a 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -46,7 +46,7 @@ constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value; constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value; constexpr auto kGetPercent = 85; constexpr auto kStarsMin = 1; -constexpr auto kStarsMax = 9000; +constexpr auto kStarsMax = 10000; constexpr auto kDefaultChargeStars = 10; using Exceptions = Api::UserPrivacy::Exceptions; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 95105ee60..f4f60bcc9 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "api/api_chat_participants.h" -#include "api/api_premium.h" +#include "api/api_premium.h" // MessageMoneyRestriction. #include "base/random.h" #include "boxes/filters/edit_filter_chats_list.h" #include "settings/settings_premium.h" @@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "dialogs/dialogs_main_list.h" +#include "payments/ui/payments_reaction_box.h" #include "ui/effects/outline_segments.h" #include "ui/wrap/slide_wrap.h" #include "window/window_separate_id.h" @@ -275,28 +276,56 @@ bool PeerListGlobalSearchController::isLoading() { return _timer.isActive() || _requestId; } +struct RecipientRow::Restriction { + Api::MessageMoneyRestriction value; + RestrictionBadgeCache cache; +}; + RecipientRow::RecipientRow( not_null peer, const style::PeerListItem *maybeLockedSt, History *maybeHistory) : PeerListRow(peer) , _maybeHistory(maybeHistory) -, _resolvePremiumRequired(maybeLockedSt != nullptr) { - if (maybeLockedSt - && Api::ResolveMessageMoneyRestrictions( +, _maybeLockedSt(maybeLockedSt) { + if (_maybeLockedSt) { + setRestriction(Api::ResolveMessageMoneyRestrictions( peer, - maybeHistory).premiumRequired) { - _lockedSt = maybeLockedSt; + maybeHistory)); } } +Api::MessageMoneyRestriction RecipientRow::restriction() const { + return _restriction + ? _restriction->value + : Api::MessageMoneyRestriction(); +} + +void RecipientRow::setRestriction(Api::MessageMoneyRestriction restriction) { + if (!restriction) { + _restriction = nullptr; + return; + } else if (!_restriction) { + _restriction = std::make_unique(); + } + _restriction->value = restriction; +} + PaintRoundImageCallback RecipientRow::generatePaintUserpicCallback( bool forceRound) { auto result = PeerListRow::generatePaintUserpicCallback(forceRound); - if (const auto st = _lockedSt) { + if (const auto &r = _restriction) { return [=](Painter &p, int x, int y, int outerWidth, int size) { result(p, x, y, outerWidth, size); - PaintPremiumRequiredLock(p, st, x, y, outerWidth, size); + PaintRestrictionBadge( + p, + _maybeLockedSt, + r->value.starsPerMessage, + r->cache, + x, + y, + outerWidth, + size); }; } return result; @@ -305,12 +334,14 @@ PaintRoundImageCallback RecipientRow::generatePaintUserpicCallback( bool RecipientRow::refreshLock( not_null maybeLockedSt) { if (const auto user = peer()->asUser()) { - const auto locked = _resolvePremiumRequired - && Api::ResolveMessageMoneyRestrictions( + using Restriction = Api::MessageMoneyRestriction; + const auto r = _maybeLockedSt + ? Api::ResolveMessageMoneyRestrictions( user, - _maybeHistory).premiumRequired; - if (this->locked() != locked) { - setLocked(locked ? maybeLockedSt.get() : nullptr); + _maybeHistory) + : Restriction(); + if ((_restriction ? _restriction->value : Restriction()) != r) { + setRestriction(r); return true; } } @@ -320,14 +351,20 @@ bool RecipientRow::refreshLock( void RecipientRow::preloadUserpic() { PeerListRow::preloadUserpic(); - if (!_resolvePremiumRequired) { + if (!_maybeLockedSt) { return; - } else if (!Api::ResolveMessageMoneyRestrictions( - peer(), - _maybeHistory).known) { - const auto user = peer()->asUser(); - user->session().api().premium().resolveMessageMoneyRestrictions( - user); + } + const auto peer = this->peer(); + const auto known = Api::ResolveMessageMoneyRestrictions( + peer, + _maybeHistory).known; + if (known) { + return; + } else if (const auto user = peer->asUser()) { + const auto api = &user->session().api(); + api->premium().resolveMessageMoneyRestrictions(user); + } else if (const auto group = peer->asChannel()) { + group->updateFull(); } } @@ -844,7 +881,8 @@ bool RecipientRow::ShowLockedError( not_null controller, not_null row, Fn)> error) { - if (!static_cast(row.get())->locked()) { + const auto recipient = static_cast(row.get()); + if (!recipient->restriction().premiumRequired) { return false; } ::Settings::ShowPremiumPromoToast( @@ -1100,25 +1138,61 @@ auto ChooseTopicBoxController::createRow(not_null topic) return skip ? nullptr : std::make_unique(topic); }; -void PaintPremiumRequiredLock( +void PaintRestrictionBadge( Painter &p, not_null st, + int stars, + RestrictionBadgeCache &cache, int x, int y, int outerWidth, int size) { - auto hq = PainterHighQualityEnabler(p); + const auto paletteVersion = style::PaletteVersion(); + const auto good = !cache.badge.isNull() + && (cache.stars == stars) + && (cache.paletteVersion == paletteVersion); const auto &check = st->checkbox.check; - auto pen = check.border->p; - pen.setWidthF(check.width); - p.setPen(pen); - p.setBrush(st::premiumButtonBg2); - const auto &icon = st::stickersPremiumLock; - const auto width = icon.width(); - const auto height = icon.height(); - const auto rect = QRect( - QPoint(x + size - width, y + size - height), - icon.size()); - p.drawEllipse(rect); - icon.paintInCenter(p, rect); + const auto add = check.width; + if (!good) { + cache.stars = stars; + cache.paletteVersion = paletteVersion; + if (stars) { + const auto text = (stars >= 1000) + ? (QString::number(stars / 1000) + 'K') + : QString::number(stars); + cache.badge = Ui::GenerateSmallBadgeImage( + text, + st::paidReactTopStarIcon, + check.bgActive->c, + st::premiumButtonFg->c, + &check); + } else { + auto hq = PainterHighQualityEnabler(p); + const auto &icon = st::stickersPremiumLock; + const auto width = icon.width(); + const auto height = icon.height(); + const auto rect = QRect( + QPoint(x + size - width, y + size - height), + icon.size()); + const auto added = QMargins(add, add, add, add); + const auto ratio = style::DevicePixelRatio(); + cache.badge = QImage( + (rect + added).size() * ratio, + QImage::Format_ARGB32_Premultiplied); + cache.badge.setDevicePixelRatio(ratio); + cache.badge.fill(Qt::transparent); + const auto inner = QRect(add, add, rect.width(), rect.height()); + auto q = QPainter(&cache.badge); + auto pen = check.border->p; + pen.setWidthF(check.width); + q.setPen(pen); + q.setBrush(st::premiumButtonBg2); + q.drawEllipse(inner); + icon.paintInCenter(q, inner); + } + } + const auto cached = cache.badge.size() / cache.badge.devicePixelRatio(); + const auto left = x + size + add - cached.width(); + const auto top = stars ? (y - add) : (y + size + add - cached.height()); + p.drawImage(left, top, cache.badge); } diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 9bec5a98e..bd97a408b 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -19,6 +19,10 @@ namespace style { struct PeerListItem; } // namespace style +namespace Api { +struct MessageMoneyRestriction; +} // namespace Api + namespace Data { class Thread; class Forum; @@ -100,6 +104,21 @@ struct RecipientMoneyRestrictionError { [[nodiscard]] RecipientMoneyRestrictionError WriteMoneyRestrictionError( not_null user); +struct RestrictionBadgeCache { + int paletteVersion = 0; + int stars = 0; + QImage badge; +}; +void PaintRestrictionBadge( + Painter &p, + not_null st, + int stars, + RestrictionBadgeCache &cache, + int x, + int y, + int outerWidth, + int size); + class RecipientRow : public PeerListRow { public: explicit RecipientRow( @@ -117,21 +136,20 @@ public: [[nodiscard]] History *maybeHistory() const { return _maybeHistory; } - [[nodiscard]] bool locked() const { - return _lockedSt != nullptr; - } - void setLocked(const style::PeerListItem *lockedSt) { - _lockedSt = lockedSt; - } PaintRoundImageCallback generatePaintUserpicCallback( bool forceRound) override; void preloadUserpic() override; + [[nodiscard]] Api::MessageMoneyRestriction restriction() const; + void setRestriction(Api::MessageMoneyRestriction restriction); + private: + struct Restriction; + History *_maybeHistory = nullptr; - const style::PeerListItem *_lockedSt = nullptr; - bool _resolvePremiumRequired = false; + const style::PeerListItem *_maybeLockedSt = nullptr; + std::shared_ptr _restriction; }; @@ -371,11 +389,3 @@ private: Fn)> _filter; }; - -void PaintPremiumRequiredLock( - Painter &p, - not_null st, - int x, - int y, - int outerWidth, - int size); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index f37bda892..288a09353 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -1486,6 +1486,7 @@ object_ptr ShareInviteLinkBox( }; auto submitCallback = [=]( std::vector> &&result, + Fn checkPaid, TextWithTags &&comment, Api::SendOptions options, Data::ForwardOptions) { @@ -1503,6 +1504,8 @@ object_ptr ShareInviteLinkBox( result.size() > 1)); } return; + } else if (!checkPaid(1)) { + return; } *sending = true; diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 3a890a91f..ff6733f22 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -118,12 +118,13 @@ private: Ui::RoundImageCheckbox checkbox; Ui::Text::String name; Ui::Animations::Simple nameActive; - bool locked = false; + Api::MessageMoneyRestriction restriction; + RestrictionBadgeCache badgeCache; }; void invalidateCache(); bool showLockedError(not_null chat); - void refreshLockedRows(); + void refreshRestrictedRows(); [[nodiscard]] int displayedChatsCount() const; [[nodiscard]] not_null chatThread( @@ -132,7 +133,7 @@ private: void paintChat(Painter &p, not_null chat, int index); void updateChat(not_null peer); void updateChatName(not_null chat); - void initChatLocked(not_null chat); + void initChatRestriction(not_null chat); void repaintChat(not_null peer); int chatIndex(not_null peer) const; void repaintChatAtIndex(int index); @@ -652,6 +653,67 @@ void ShareBox::innerSelectedChanged( } void ShareBox::submit(Api::SendOptions options) { + _submitLifetime.destroy(); + + auto threads = _inner->selected(); + const auto weak = Ui::MakeWeak(this); + const auto checkPaid = [=](int messagesCount) { + const auto withPaymentApproved = crl::guard(weak, [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + submit(copy); + }); + + const auto alreadyApproved = options.starsApproved; + auto paid = std::vector>(); + auto waiting = base::flat_set>(); + auto totalStars = 0; + for (const auto &thread : threads) { + const auto peer = thread->peer(); + const auto details = ComputePaymentDetails(peer, messagesCount); + if (!details) { + waiting.emplace(peer); + } else if (details->stars > 0) { + totalStars += details->stars; + paid.push_back(thread); + } + } + if (!waiting.empty()) { + _descriptor.session->changes().peerUpdates( + Data::PeerUpdate::Flag::FullInfo + ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { + if (waiting.contains(update.peer)) { + withPaymentApproved(alreadyApproved); + } + }, _submitLifetime); + + if (!_descriptor.session->credits().loaded()) { + _descriptor.session->credits().loadedValue( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + withPaymentApproved(alreadyApproved); + }, _submitLifetime); + } + return false; + } else if (totalStars > alreadyApproved) { + const auto show = uiShow(); + const auto session = _descriptor.session; + const auto sessionShow = Main::MakeSessionShow(show, session); + const auto scheduleBoxSt = _descriptor.st.scheduleBox.get(); + ShowSendPaidConfirm(sessionShow, paid, SendPaymentDetails{ + .messages = messagesCount, + .stars = totalStars, + }, [=] { withPaymentApproved(totalStars); }, PaidConfirmStyles{ + .label = (scheduleBoxSt + ? scheduleBoxSt->chooseDateTimeArgs.labelStyle + : nullptr), + .checkbox = _descriptor.st.checkbox, + }); + return false; + } + return true; + }; if (const auto onstack = _descriptor.submitCallback) { const auto forwardOptions = (_forwardOptions.captionsCount && _forwardOptions.dropCaptions) @@ -660,7 +722,8 @@ void ShareBox::submit(Api::SendOptions options) { ? Data::ForwardOptions::NoSenderNames : Data::ForwardOptions::PreserveInfo; onstack( - _inner->selected(), + std::move(threads), + checkPaid, _comment->entity()->getTextWithAppliedMarkdown(), options, forwardOptions); @@ -727,7 +790,7 @@ ShareBox::Inner::Inner( Data::AmPremiumValue(session) | rpl::to_empty, session->api().premium().someMessageMoneyRestrictionsResolved() ) | rpl::start_with_next([=] { - refreshLockedRows(); + refreshRestrictedRows(); }, lifetime()); } @@ -788,7 +851,7 @@ void ShareBox::Inner::invalidateCache() { } bool ShareBox::Inner::showLockedError(not_null chat) { - if (!chat->locked) { + if (!chat->restriction.premiumRequired) { return false; } ::Settings::ShowPremiumPromoToast( @@ -799,25 +862,25 @@ bool ShareBox::Inner::showLockedError(not_null chat) { return true; } -void ShareBox::Inner::refreshLockedRows() { +void ShareBox::Inner::refreshRestrictedRows() { auto changed = false; for (const auto &[peer, data] : _dataMap) { const auto history = data->history; - const auto locked = Api::ResolveMessageMoneyRestrictions( + const auto restriction = Api::ResolveMessageMoneyRestrictions( history->peer, - history).premiumRequired; - if (data->locked != locked) { - data->locked = locked; + history); + if (data->restriction != restriction) { + data->restriction = restriction; changed = true; } } for (const auto &data : d_byUsernameFiltered) { const auto history = data->history; - const auto locked = Api::ResolveMessageMoneyRestrictions( + const auto restriction = Api::ResolveMessageMoneyRestrictions( history->peer, - history).premiumRequired; - if (data->locked != locked) { - data->locked = locked; + history); + if (data->restriction != restriction) { + data->restriction = restriction; changed = true; } } @@ -884,13 +947,14 @@ void ShareBox::Inner::updateChatName(not_null chat) { chat->name.setText(_st.item.nameStyle, text, Ui::NameTextOptions()); } -void ShareBox::Inner::initChatLocked(not_null chat) { +void ShareBox::Inner::initChatRestriction(not_null chat) { if (_descriptor.moneyRestrictionError) { const auto history = chat->history; - if (Api::ResolveMessageMoneyRestrictions( - history->peer, - history).premiumRequired) { - chat->locked = true; + const auto restriction = Api::ResolveMessageMoneyRestrictions( + history->peer, + history); + if (restriction) { + chat->restriction = restriction; } } } @@ -1021,9 +1085,10 @@ void ShareBox::Inner::preloadUserpic(not_null entry) { } else if (!Api::ResolveMessageMoneyRestrictions( history->peer, history).known) { - const auto user = history->peer->asUser(); - _descriptor.session->api().premium().resolveMessageMoneyRestrictions( - user); + if (const auto user = history->peer->asUser()) { + const auto api = &_descriptor.session->api(); + api->premium().resolveMessageMoneyRestrictions(user); + } } } @@ -1046,7 +1111,7 @@ auto ShareBox::Inner::getChat(not_null row) repaintChat(peer); })); updateChatName(i->second.get()); - initChatLocked(i->second.get()); + initChatRestriction(i->second.get()); row->attached = i->second.get(); return i->second.get(); } @@ -1080,10 +1145,12 @@ void ShareBox::Inner::paintChat( auto photoTop = st::sharePhotoTop; chat->checkbox.paint(p, x + photoLeft, y + photoTop, outerWidth); - if (chat->locked) { - PaintPremiumRequiredLock( + if (chat->restriction) { + PaintRestrictionBadge( p, &_st.item, + chat->restriction.starsPerMessage, + chat->badgeCache, x + photoLeft, y + photoTop, outerWidth, @@ -1438,7 +1505,7 @@ void ShareBox::Inner::peopleReceived( _st.item, [=] { repaintChat(peer); })); updateChatName(d_byUsernameFiltered.back().get()); - initChatLocked(d_byUsernameFiltered.back().get()); + initChatRestriction(d_byUsernameFiltered.back().get()); } } }; @@ -1502,24 +1569,29 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( const auto state = std::make_shared(); return [=]( std::vector> &&result, - TextWithTags &&comment, + Fn checkPaid, + TextWithTags comment, Api::SendOptions options, Data::ForwardOptions forwardOptions) { if (!state->requests.empty()) { return; // Share clicked already. } + const auto items = history->owner().idsToItems(msgIds); const auto existingIds = history->owner().itemsToIds(items); if (existingIds.empty() || result.empty()) { return; } + auto messagesCount = int(items.size()) + (comment.empty() ? 0 : 1); const auto error = GetErrorForSending( result, { .forward = &items, .text = &comment }); if (error.error) { show->showBox(MakeSendErrorBox(error, result.size() > 1)); return; + } else if (!checkPaid(messagesCount)) { + return; } using Flag = MTPmessages_ForwardMessages::Flag; @@ -1614,7 +1686,11 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( } finish(); }).fail([=](const MTP::Error &error) { - if (error.type() == u"VOICE_MESSAGES_FORBIDDEN"_q) { + const auto type = error.type(); + if (type.startsWith(u"ALLOW_PAYMENT_REQUIRED_"_q)) { + show->showToast(u"Payment requirements changed. " + "Please, try again."_q); + } else if (type == u"VOICE_MESSAGES_FORBIDDEN"_q) { show->showToast( tr::lng_restricted_send_voice_messages( tr::now, @@ -1653,6 +1729,7 @@ ShareBoxStyleOverrides DarkShareBoxStyle() { .comment = &st::groupCallShareBoxComment, .peerList = &st::groupCallShareBoxList, .label = &st::groupCallField, + .checkbox = &st::groupCallCheckbox, .scheduleBox = std::make_shared(schedule()), }; } @@ -1765,6 +1842,7 @@ void FastShareLink( }; auto submitCallback = [=]( std::vector> &&result, + Fn checkPaid, TextWithTags &&comment, Api::SendOptions options, ::Data::ForwardOptions) { @@ -1781,6 +1859,8 @@ void FastShareLink( MakeSendErrorBox(error, result.size() > 1)); } return; + } else if (!checkPaid(1)) { + return; } *sending = true; diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index 83cdbcb4f..07227ee98 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -66,6 +66,7 @@ struct ShareBoxStyleOverrides { const style::InputField *comment = nullptr; const style::PeerList *peerList = nullptr; const style::InputField *label = nullptr; + const style::Checkbox *checkbox = nullptr; std::shared_ptr scheduleBox; }; [[nodiscard]] ShareBoxStyleOverrides DarkShareBoxStyle(); @@ -96,6 +97,7 @@ public: using CopyCallback = Fn; using SubmitCallback = Fn>&&, + Fn checkPaid, TextWithTags&&, Api::SendOptions, Data::ForwardOptions)>; @@ -196,5 +198,6 @@ private: PeopleQueries _peopleQueries; Ui::Animations::Simple _scrollAnimation; + rpl::lifetime _submitLifetime; }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp index 672d6d4a1..dec18b742 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -134,6 +134,7 @@ object_ptr ShareInviteLinkBox( }; auto submitCallback = [=]( std::vector> &&result, + Fn checkPaid, TextWithTags &&comment, Api::SendOptions options, Data::ForwardOptions) { @@ -150,6 +151,8 @@ object_ptr ShareInviteLinkBox( MakeSendErrorBox(error, result.size() > 1)); } return; + } else if (!checkPaid(1)) { + return; } *sending = true; diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 64db33d13..209dab9f5 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -1747,7 +1747,8 @@ void InitFieldAutocomplete( && peer->isUser() && !peer->asUser()->isBot() && (!shortcutMessages - || shortcutMessages->shortcuts().list.empty())) { + || shortcutMessages->shortcuts().list.empty() + || peer->starsPerMessageChecked() != 0)) { parsed = {}; } raw->showFiltered(peer, parsed.query, parsed.fromStart); diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 5fb0d9f76..4d15bf4ca 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -249,19 +249,42 @@ void ShowSendPaidConfirm( not_null navigation, not_null peer, SendPaymentDetails details, - Fn confirmed) { + Fn confirmed, + PaidConfirmStyles styles) { return ShowSendPaidConfirm( navigation->uiShow(), peer, details, - confirmed); + confirmed, + styles); } void ShowSendPaidConfirm( std::shared_ptr show, not_null peer, SendPaymentDetails details, - Fn confirmed) { + Fn confirmed, + PaidConfirmStyles styles) { + ShowSendPaidConfirm( + std::move(show), + std::vector>{ peer->owner().history(peer) }, + details, + confirmed, + styles); +} + +void ShowSendPaidConfirm( + std::shared_ptr show, + const std::vector> &threads, + SendPaymentDetails details, + Fn confirmed, + PaidConfirmStyles styles) { + Expects(!threads.empty()); + + const auto singlePeer = (threads.size() > 1) + ? (PeerData*)nullptr + : threads.front()->peer().get(); + const auto recipientId = singlePeer ? singlePeer->id : PeerId(); const auto check = [=] { const auto required = details.stars; if (!required) { @@ -276,57 +299,81 @@ void ShowSendPaidConfirm( Settings::MaybeRequestBalanceIncrease( show, required, - Settings::SmallBalanceForMessage{ .recipientId = peer->id }, + Settings::SmallBalanceForMessage{ .recipientId = recipientId }, done); }; - - const auto session = &peer->session(); - if (session->local().isPeerTrustedPayForMessage(peer->id)) { - check(); - return; + auto usersOnly = true; + for (const auto &thread : threads) { + if (!thread->peer()->isUser()) { + usersOnly = false; + break; + } + } + if (singlePeer) { + const auto session = &singlePeer->session(); + if (session->local().isPeerTrustedPayForMessage(recipientId)) { + check(); + return; + } } const auto messages = details.messages; const auto stars = details.stars; show->showBox(Box([=](not_null box) { const auto trust = std::make_shared>(); const auto proceed = [=](Fn close) { - if ((*trust)->checked()) { - session->local().markPeerTrustedPayForMessage(peer->id); + if (singlePeer && (*trust)->checked()) { + const auto session = &singlePeer->session(); + session->local().markPeerTrustedPayForMessage(recipientId); } check(); close(); }; Ui::ConfirmBox(box, { - .text = tr::lng_payment_confirm_text( - tr::now, - lt_count, - stars / messages, - lt_name, - Ui::Text::Bold(peer->shortName()), - Ui::Text::RichLangValue).append(' ').append( - tr::lng_payment_confirm_sure( + .text = (singlePeer + ? tr::lng_payment_confirm_text( + tr::now, + lt_count, + stars / messages, + lt_name, + Ui::Text::Bold(singlePeer->shortName()), + Ui::Text::RichLangValue) + : (usersOnly + ? tr::lng_payment_confirm_users + : tr::lng_payment_confirm_chats)( tr::now, lt_count, - messages, - lt_amount, - tr::lng_payment_confirm_amount( - tr::now, - lt_count, - stars, - Ui::Text::RichLangValue), - Ui::Text::RichLangValue)), + int(threads.size()), + Ui::Text::RichLangValue)).append(' ').append( + tr::lng_payment_confirm_sure( + tr::now, + lt_count, + messages, + lt_amount, + tr::lng_payment_confirm_amount( + tr::now, + lt_count, + stars, + Ui::Text::RichLangValue), + Ui::Text::RichLangValue)), .confirmed = proceed, .confirmText = tr::lng_payment_confirm_button( lt_count, rpl::single(messages * 1.)), + .labelStyle = styles.label, .title = tr::lng_payment_confirm_title(), }); - const auto skip = st::defaultCheckbox.margin.top(); - *trust = box->addRow( - object_ptr( - box, - tr::lng_payment_confirm_dont_ask(tr::now)), - st::boxRowPadding + QMargins(0, skip, 0, skip)); + if (singlePeer) { + const auto skip = st::defaultCheckbox.margin.top(); + *trust = box->addRow( + object_ptr( + box, + tr::lng_payment_confirm_dont_ask(tr::now), + false, + (styles.checkbox + ? *styles.checkbox + : st::defaultCheckbox)), + st::boxRowPadding + QMargins(0, skip, 0, skip)); + } })); } diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 34d8f5d18..4106df38f 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -11,6 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class History; +namespace style { +struct FlatLabel; +struct Checkbox; +} // namespace style + namespace Api { struct SendOptions; struct SendAction; @@ -144,16 +149,28 @@ struct SendPaymentDetails { not_null peer, int messagesCount); +struct PaidConfirmStyles { + const style::FlatLabel *label = nullptr; + const style::Checkbox *checkbox = nullptr; +}; void ShowSendPaidConfirm( not_null navigation, not_null peer, SendPaymentDetails details, - Fn confirmed); + Fn confirmed, + PaidConfirmStyles styles = {}); void ShowSendPaidConfirm( std::shared_ptr show, not_null peer, SendPaymentDetails details, - Fn confirmed); + Fn confirmed, + PaidConfirmStyles styles = {}); +void ShowSendPaidConfirm( + std::shared_ptr show, + const std::vector> &threads, + SendPaymentDetails details, + Fn confirmed, + PaidConfirmStyles styles = {}); class SendPaymentHelper final { public: diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 9609054cc..5ce0d2352 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -4575,26 +4575,6 @@ void HistoryWidget::reportSelectedMessages() { } } -void HistoryWidget::payForMessageSure(bool trust) { - const auto required = _peer->starsPerMessage(); - if (!required) { - return; - } - const auto done = [=](Settings::SmallBalanceResult result) { - if (result == Settings::SmallBalanceResult::Success - || result == Settings::SmallBalanceResult::Already) { - if (canWriteMessage()) { - setInnerFocus(); - } - } - }; - Settings::MaybeRequestBalanceIncrease( - controller()->uiShow(), - required, - Settings::SmallBalanceForMessage{ .recipientId = _peer->id }, - crl::guard(this, done)); -} - History *HistoryWidget::history() const { return _history; } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 425419530..f52dd3086 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -426,7 +426,6 @@ private: [[nodiscard]] int computeMaxFieldHeight() const; void toggleMuteUnmute(); void reportSelectedMessages(); - void payForMessageSure(bool trust = false); void showKeyboardHideButton(); void toggleKeyboard(bool manual = true); void startBotCommand(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 90f6ca1e2..d00da018c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -78,6 +78,7 @@ namespace Media::Stories { : Fn(); auto submitCallback = [=]( std::vector> &&result, + Fn checkPaid, TextWithTags &&comment, Api::SendOptions options, Data::ForwardOptions forwardOptions) { @@ -95,6 +96,8 @@ namespace Media::Stories { if (error.error) { show->showBox(MakeSendErrorBox(error, result.size() > 1)); return; + } else if (!checkPaid(comment.text.isEmpty() ? 1 : 2)) { + return; } const auto api = &story->owner().session().api(); @@ -133,7 +136,7 @@ namespace Media::Stories { sendFlags |= SendFlag::f_invert_media; } const auto starsPaid = std::min( - peer->starsPerMessageChecked(), + threadHistory->peer->starsPerMessageChecked(), options.starsApproved); if (starsPaid) { options.starsApproved -= starsPaid; diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index 7cc513f7b..1786d250c 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -147,53 +147,11 @@ void PaidReactionSlider( } [[nodiscard]] QImage GenerateBadgeImage(int count) { - const auto text = Lang::FormatCountDecimal(count); - const auto length = st::chatSimilarBadgeFont->width(text); - const auto contents = st::chatSimilarLockedIconPosition.x() - + st::paidReactTopStarIcon.width() - + st::paidReactTopStarSkip - + length; - const auto badge = QRect( - st::chatSimilarBadgePadding.left(), - st::chatSimilarBadgePadding.top(), - contents, - st::chatSimilarBadgeFont->height); - const auto rect = badge.marginsAdded(st::chatSimilarBadgePadding); - - auto result = QImage( - rect.size() * style::DevicePixelRatio(), - QImage::Format_ARGB32_Premultiplied); - result.setDevicePixelRatio(style::DevicePixelRatio()); - result.fill(Qt::transparent); - auto q = QPainter(&result); - - const auto &font = st::chatSimilarBadgeFont; - const auto textTop = badge.y() + font->ascent; - const auto icon = &st::paidReactTopStarIcon; - const auto position = st::chatSimilarLockedIconPosition; - - auto hq = PainterHighQualityEnabler(q); - q.setBrush(st::creditsBg3); - q.setPen(Qt::NoPen); - const auto radius = rect.height() / 2.; - q.drawRoundedRect(rect, radius, radius); - - auto textLeft = 0; - if (icon) { - icon->paint( - q, - badge.x() + position.x(), - badge.y() + position.y(), - rect.width()); - textLeft += position.x() + icon->width() + st::paidReactTopStarSkip; - } - - q.setFont(font); - q.setPen(st::premiumButtonFg); - q.drawText(textLeft, textTop, text); - q.end(); - - return result; + return GenerateSmallBadgeImage( + Lang::FormatCountDecimal(count), + st::paidReactTopStarIcon, + st::creditsBg3->c, + st::premiumButtonFg->c); } void AddArrowDown(not_null widget) { @@ -321,7 +279,6 @@ void SelectShownPeer( updateUserpic(); } (*menu)->popup(QCursor::pos()); - } void FillTopReactors( @@ -633,4 +590,65 @@ object_ptr MakePaidReactionBox(PaidReactionBoxArgs &&args) { return Box(PaidReactionsBox, std::move(args)); } +QImage GenerateSmallBadgeImage( + QString text, + const style::icon &icon, + QColor bg, + QColor fg, + const style::RoundCheckbox *borderSt) { + const auto length = st::chatSimilarBadgeFont->width(text); + const auto contents = st::chatSimilarLockedIconPosition.x() + + icon.width() + + st::paidReactTopStarSkip + + length; + const auto badge = QRect( + st::chatSimilarBadgePadding.left(), + st::chatSimilarBadgePadding.top(), + contents, + st::chatSimilarBadgeFont->height); + const auto rect = badge.marginsAdded(st::chatSimilarBadgePadding); + const auto add = borderSt ? borderSt->width : 0; + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + (rect + QMargins(add, add, add, add)).size() * ratio, + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + result.fill(Qt::transparent); + auto q = QPainter(&result); + + const auto &font = st::chatSimilarBadgeFont; + const auto textTop = badge.y() + font->ascent; + const auto position = st::chatSimilarLockedIconPosition; + + auto hq = PainterHighQualityEnabler(q); + q.translate(add, add); + q.setBrush(bg); + if (borderSt) { + q.setPen(QPen(borderSt->border->c, borderSt->width)); + } else { + q.setPen(Qt::NoPen); + } + const auto radius = rect.height() / 2.; + const auto shift = add / 2.; + q.drawRoundedRect( + QRectF(rect) + QMarginsF(shift, shift, shift, shift), + radius, + radius); + + auto textLeft = 0; + icon.paint( + q, + badge.x() + position.x(), + badge.y() + position.y(), + rect.width()); + textLeft += position.x() + icon.width() + st::paidReactTopStarSkip; + + q.setFont(font); + q.setPen(fg); + q.drawText(textLeft, textTop, text); + q.end(); + + return result; +} + } // namespace Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h index 034b04289..34f67e066 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" +namespace style { +struct RoundCheckbox; +} // namespace style + namespace Ui { class BoxContent; @@ -48,4 +52,11 @@ void PaidReactionsBox( [[nodiscard]] object_ptr MakePaidReactionBox( PaidReactionBoxArgs &&args); +[[nodiscard]] QImage GenerateSmallBadgeImage( + QString text, + const style::icon &icon, + QColor bg, + QColor fg, + const style::RoundCheckbox *borderSt = nullptr); + } // namespace Ui diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 3e54ba9cf..62075809f 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -2091,7 +2091,9 @@ void SmallBalanceBox( }, [&](SmallBalanceStarGift value) { return owner->peer(value.recipientId)->shortName(); }, [&](SmallBalanceForMessage value) { - return owner->peer(value.recipientId)->shortName(); + return value.recipientId + ? owner->peer(value.recipientId)->shortName() + : QString(); }); auto needed = show->session().credits().balanceValue( @@ -2131,10 +2133,13 @@ void SmallBalanceBox( rpl::single(Ui::Text::Bold(name)), Ui::Text::RichLangValue) : v::is(source) - ? tr::lng_credits_small_balance_for_message( - lt_user, - rpl::single(Ui::Text::Bold(name)), - Ui::Text::RichLangValue) + ? (name.isEmpty() + ? tr::lng_credits_small_balance_for_messages( + Ui::Text::RichLangValue) + : tr::lng_credits_small_balance_for_message( + lt_user, + rpl::single(Ui::Text::Bold(name)), + Ui::Text::RichLangValue)) : name.isEmpty() ? tr::lng_credits_small_balance_fallback( Ui::Text::RichLangValue) diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 8854d89a0..09193517b 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -2409,6 +2409,7 @@ QPointer ShowForwardMessagesBox( not_null peer) -> Controller::Chosen { return peer->owner().history(peer); }) | ranges::to_vector, + [](int messagesCount) { return true; }, comment->entity()->getTextWithAppliedMarkdown(), options, state->box->forwardOptionsData());