From fe2df969538935b0fa1d7951b0bacbeecb8ff65c Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 25 Feb 2025 14:53:23 +0400 Subject: [PATCH] Improve paid peer-box multi-send. --- Telegram/SourceFiles/boxes/peer_list_box.cpp | 1 + Telegram/SourceFiles/boxes/peer_list_box.h | 7 + .../boxes/peer_list_controllers.cpp | 31 ++-- .../SourceFiles/boxes/peer_list_controllers.h | 8 +- Telegram/SourceFiles/boxes/share_box.cpp | 6 +- .../history/history_item_helpers.cpp | 16 +- .../history/history_item_helpers.h | 2 +- .../history/view/history_view_bottom_info.cpp | 2 +- .../inline_bots/bot_attach_web_view.cpp | 36 +++- .../SourceFiles/window/window_peer_menu.cpp | 155 ++++++++++++++++-- .../SourceFiles/window/window_peer_menu.h | 8 +- 11 files changed, 226 insertions(+), 46 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 8d4b60b1d..7a559faf3 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -873,6 +873,7 @@ void PeerListRow::paintUserpic( } else if (const auto callback = generatePaintUserpicCallback(false)) { callback(p, x, y, outerWidth, st.photoSize); } + paintUserpicOverlay(p, st, x, y, outerWidth); } // Emulates Ui::RoundImageCheckbox::paint() in a checked state. diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index c4a79c456..b8dbecb8c 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -95,6 +95,13 @@ public: [[nodiscard]] virtual QString generateShortName(); [[nodiscard]] virtual auto generatePaintUserpicCallback( bool forceRound) -> PaintRoundImageCallback; + virtual void paintUserpicOverlay( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int outerWidth) { + } [[nodiscard]] virtual auto generateNameFirstLetters() const -> const base::flat_set &; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index f4f60bcc9..3b2214753 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -311,24 +311,23 @@ void RecipientRow::setRestriction(Api::MessageMoneyRestriction restriction) { _restriction->value = restriction; } -PaintRoundImageCallback RecipientRow::generatePaintUserpicCallback( - bool forceRound) { - auto result = PeerListRow::generatePaintUserpicCallback(forceRound); +void RecipientRow::paintUserpicOverlay( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int outerWidth) { if (const auto &r = _restriction) { - return [=](Painter &p, int x, int y, int outerWidth, int size) { - result(p, x, y, outerWidth, size); - PaintRestrictionBadge( - p, - _maybeLockedSt, - r->value.starsPerMessage, - r->cache, - x, - y, - outerWidth, - size); - }; + PaintRestrictionBadge( + p, + _maybeLockedSt, + r->value.starsPerMessage, + r->cache, + x, + y, + outerWidth, + st.photoSize); } - return result; } bool RecipientRow::refreshLock( diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index bd97a408b..de9c67dbf 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -136,8 +136,12 @@ public: [[nodiscard]] History *maybeHistory() const { return _maybeHistory; } - PaintRoundImageCallback generatePaintUserpicCallback( - bool forceRound) override; + void paintUserpicOverlay( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int outerWidth) override; void preloadUserpic() override; diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index f5abb2e86..e23dd7165 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -675,7 +675,7 @@ void ShareBox::submit(Api::SendOptions options) { }); const auto alreadyApproved = options.starsApproved; - auto paid = std::vector>(); + auto paid = std::vector>(); auto waiting = base::flat_set>(); auto totalStars = 0; for (const auto &thread : threads) { @@ -685,7 +685,7 @@ void ShareBox::submit(Api::SendOptions options) { waiting.emplace(peer); } else if (details->stars > 0) { totalStars += details->stars; - paid.push_back(thread); + paid.push_back(peer); } } if (!waiting.empty()) { @@ -963,7 +963,7 @@ void ShareBox::Inner::initChatRestriction(not_null chat) { const auto restriction = Api::ResolveMessageMoneyRestrictions( history->peer, history); - if (restriction) { + if (restriction || restriction.known) { chat->restriction = restriction; } } diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 4d15bf4ca..49fcebcb2 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -267,7 +267,7 @@ void ShowSendPaidConfirm( PaidConfirmStyles styles) { ShowSendPaidConfirm( std::move(show), - std::vector>{ peer->owner().history(peer) }, + std::vector>{ peer }, details, confirmed, styles); @@ -275,15 +275,15 @@ void ShowSendPaidConfirm( void ShowSendPaidConfirm( std::shared_ptr show, - const std::vector> &threads, + const std::vector> &peers, SendPaymentDetails details, Fn confirmed, PaidConfirmStyles styles) { - Expects(!threads.empty()); + Expects(!peers.empty()); - const auto singlePeer = (threads.size() > 1) + const auto singlePeer = (peers.size() > 1) ? (PeerData*)nullptr - : threads.front()->peer().get(); + : peers.front().get(); const auto recipientId = singlePeer ? singlePeer->id : PeerId(); const auto check = [=] { const auto required = details.stars; @@ -303,8 +303,8 @@ void ShowSendPaidConfirm( done); }; auto usersOnly = true; - for (const auto &thread : threads) { - if (!thread->peer()->isUser()) { + for (const auto &peer : peers) { + if (!peer->isUser()) { usersOnly = false; break; } @@ -342,7 +342,7 @@ void ShowSendPaidConfirm( : tr::lng_payment_confirm_chats)( tr::now, lt_count, - int(threads.size()), + int(peers.size()), Ui::Text::RichLangValue)).append(' ').append( tr::lng_payment_confirm_sure( tr::now, diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 4106df38f..c5dc8795e 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -167,7 +167,7 @@ void ShowSendPaidConfirm( PaidConfirmStyles styles = {}); void ShowSendPaidConfirm( std::shared_ptr show, - const std::vector> &threads, + const std::vector> &peers, SendPaymentDetails details, Fn confirmed, PaidConfirmStyles styles = {}); diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index b7d216b71..c5fde09fd 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -436,7 +436,7 @@ void BottomInfo::layoutDateText() { auto marked = TextWithEntities{ full }; if (const auto count = _data.stars) { marked.append(Ui::Text::IconEmoji(&st::starIconEmoji)); - marked.append(QString::number(count)); + marked.append(Lang::FormatCountToShort(count).string); } _authorEditedDate.setMarkedText( st::msgDateTextStyle, diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index a56cba037..6d112e5d3 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -1809,11 +1809,15 @@ void WebViewInstance::botSendPreparedMessage( QPointer preview; QPointer choose; rpl::event_stream> recipient; + Fn send; + SendPaymentHelper sendPayment; bool sent = false; }; const auto state = std::make_shared(); auto recipient = state->recipient.events(); - const auto send = [=](std::vector> list) { + const auto send = [=]( + std::vector> list, + Api::SendOptions options) { if (state->sent) { return; } @@ -1852,7 +1856,7 @@ void WebViewInstance::botSendPreparedMessage( bot->session().api().sendInlineResult( bot, parsed.get(), - Api::SendAction(thread), + Api::SendAction(thread, options), std::nullopt, done); } @@ -1881,7 +1885,33 @@ void WebViewInstance::botSendPreparedMessage( state->choose = box.data(); panel->showBox(std::move(box)); }, [=](not_null thread) { - send({ thread }); + const auto weak = base::make_weak(thread); + state->send = [=](Api::SendOptions options) { + const auto strong = weak.get(); + if (!strong) { + state->send = nullptr; + return; + } + const auto withPaymentApproved = [=](int stars) { + if (const auto onstack = state->send) { + auto copy = options; + copy.starsApproved = stars; + onstack(copy); + } + }; + const auto checked = state->sendPayment.check( + uiShow(), + strong->peer(), + 1, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + state->send = nullptr; + send({ strong }, options); + }; + state->send({}); }); box->boxClosing() | rpl::start_with_next([=] { if (!state->sent) { diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 606a547f0..1e794a30f 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -1960,7 +1960,9 @@ object_ptr PrepareChooseRecipientBox( rpl::producer titleOverride, FnMut &&successCallback, InlineBots::PeerTypes typesRestriction, - Fn>)> sendMany) { + Fn>, + Api::SendOptions)> sendMany) { const auto weak = std::make_shared>(); const auto selectable = (sendMany != nullptr); class Controller final : public ChooseRecipientBoxController { @@ -2073,19 +2075,82 @@ object_ptr PrepareChooseRecipientBox( std::move(filter), selectable); const auto raw = controller.get(); + + struct State { + Fn submit; + rpl::lifetime submitLifetime; + }; + const auto state = std::make_shared(); auto initBox = [=](not_null box) { raw->hasSelectedChanges( ) | rpl::start_with_next([=](bool shown) { box->clearButtons(); if (shown) { - box->addButton(tr::lng_send_button(), [=] { + const auto weak = Ui::MakeWeak(box); + state->submit = [=](Api::SendOptions options) { + state->submitLifetime.destroy(); + const auto show = box->peerListUiShow(); const auto peers = box->collectSelectedRows(); + const auto withPaymentApproved = crl::guard(weak, [=]( + int approved) { + auto copy = options; + copy.starsApproved = approved; + if (const auto onstack = state->submit) { + onstack(copy); + } + }); + + const auto alreadyApproved = options.starsApproved; + auto paid = std::vector>(); + auto waiting = base::flat_set>(); + auto totalStars = 0; + for (const auto &peer : peers) { + const auto details = ComputePaymentDetails(peer, 1); + if (!details) { + waiting.emplace(peer); + } else if (details->stars > 0) { + totalStars += details->stars; + paid.push_back(peer); + } + } + if (!waiting.empty()) { + session->changes().peerUpdates( + Data::PeerUpdate::Flag::FullInfo + ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { + if (waiting.contains(update.peer)) { + withPaymentApproved(alreadyApproved); + } + }, state->submitLifetime); + + if (!session->credits().loaded()) { + session->credits().loadedValue( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + withPaymentApproved(alreadyApproved); + }, state->submitLifetime); + } + return; + } else if (totalStars > alreadyApproved) { + ShowSendPaidConfirm(show, paid, SendPaymentDetails{ + .messages = 1, + .stars = totalStars, + }, [=] { withPaymentApproved(totalStars); }); + return; + } + state->submit = nullptr; + sendMany(ranges::views::all( peers ) | ranges::views::transform([&]( not_null peer) -> Controller::Chosen { return peer->owner().history(peer); - }) | ranges::to_vector); + }) | ranges::to_vector, options); + }; + box->addButton(tr::lng_send_button(), [=] { + if (const auto onstack = state->submit) { + onstack({}); + } }); } box->addButton(tr::lng_cancel(), [=] { @@ -2257,6 +2322,8 @@ QPointer ShowForwardMessagesBox( not_null box; not_null controller; base::unique_qptr menu; + Fn submit; + rpl::lifetime submitLifetime; }; const auto applyFilter = [=](not_null box, FilterId id) { @@ -2401,8 +2468,62 @@ QPointer ShowForwardMessagesBox( session->data().message(msgIds.front())->history(), msgIds); - const auto submit = [=](Api::SendOptions options) { + const auto weak = Ui::MakeWeak(state->box); + state->submit = [=](Api::SendOptions options) { const auto peers = state->box->collectSelectedRows(); + const auto checkPaid = [=](int messagesCount) { + const auto withPaymentApproved = crl::guard(weak, [=]( + int approved) { + auto copy = options; + copy.starsApproved = approved; + if (const auto onstack = state->submit) { + onstack(copy); + } + }); + + const auto alreadyApproved = options.starsApproved; + auto paid = std::vector>(); + auto waiting = base::flat_set>(); + auto totalStars = 0; + for (const auto &peer : peers) { + const auto details = ComputePaymentDetails( + peer, + messagesCount); + if (!details) { + waiting.emplace(peer); + } else if (details->stars > 0) { + totalStars += details->stars; + paid.push_back(peer); + } + } + if (!waiting.empty()) { + session->changes().peerUpdates( + Data::PeerUpdate::Flag::FullInfo + ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { + if (waiting.contains(update.peer)) { + withPaymentApproved(alreadyApproved); + } + }, state->submitLifetime); + + if (!session->credits().loaded()) { + session->credits().loadedValue( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + withPaymentApproved(alreadyApproved); + }, state->submitLifetime); + } + return false; + } else if (totalStars > alreadyApproved) { + ShowSendPaidConfirm(show, paid, SendPaymentDetails{ + .messages = messagesCount, + .stars = totalStars, + }, [=] { withPaymentApproved(totalStars); }); + return false; + } + state->submit = nullptr; + return true; + }; send( ranges::views::all( peers @@ -2410,11 +2531,11 @@ QPointer ShowForwardMessagesBox( not_null peer) -> Controller::Chosen { return peer->owner().history(peer); }) | ranges::to_vector, - [](int messagesCount) { return true; }, + checkPaid, comment->entity()->getTextWithAppliedMarkdown(), options, state->box->forwardOptionsData()); - if (successCallback) { + if (!state->submit && successCallback) { successCallback(); } }; @@ -2483,7 +2604,12 @@ QPointer ShowForwardMessagesBox( state->menu.get(), show, SendMenu::Details{ sendMenuType() }, - SendMenu::DefaultCallback(show, crl::guard(parent, submit))); + SendMenu::DefaultCallback(show, crl::guard(parent, [=]( + Api::SendOptions options) { + if (const auto onstack = state->submit) { + onstack(options); + } + }))); if (showForwardOptions || !state->menu->empty()) { state->menu->popup(QCursor::pos()); } @@ -2505,7 +2631,11 @@ QPointer ShowForwardMessagesBox( const auto field = comment->entity(); field->submits( - ) | rpl::start_with_next([=] { submit({}); }, field->lifetime()); + ) | rpl::start_with_next([=] { + if (const auto onstack = state->submit) { + onstack({}); + } + }, field->lifetime()); InitMessageFieldHandlers({ .session = session, .show = show, @@ -2529,9 +2659,12 @@ QPointer ShowForwardMessagesBox( ) | rpl::start_with_next([=](bool shown) { state->box->clearButtons(); if (shown) { - const auto send = state->box->addButton( - tr::lng_send_button(), - [=] { submit({}); }); + auto text = tr::lng_send_button(); + const auto send = state->box->addButton(std::move(text), [=] { + if (const auto onstack = state->submit) { + onstack({}); + } + }); send->setAcceptBoth(); send->clicks( ) | rpl::start_with_next([=](Qt::MouseButton button) { diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 7e2314f59..97268366c 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -15,6 +15,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class History; +namespace Api { +struct SendOptions; +} // namespace Api + namespace Ui { class RpWidget; class BoxContent; @@ -146,7 +150,9 @@ object_ptr PrepareChooseRecipientBox( rpl::producer titleOverride = nullptr, FnMut &&successCallback = nullptr, InlineBots::PeerTypes typesRestriction = 0, - Fn>)> sendMany = nullptr); + Fn>, + Api::SendOptions)> sendMany = nullptr); QPointer ShowChooseRecipientBox( not_null navigation, FnMut)> &&chosen,