From 284f1a521088b7a92ed77a622950fc95c21b0461 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 12 Aug 2024 17:51:31 +0200 Subject: [PATCH] Support anonymous paid reactions. --- Telegram/Resources/langs/lang.strings | 2 + .../data/data_message_reactions.cpp | 180 ++++++++++++++---- .../SourceFiles/data/data_message_reactions.h | 34 +++- .../history/history_inner_widget.cpp | 2 +- Telegram/SourceFiles/history/history_item.cpp | 70 +++++-- Telegram/SourceFiles/history/history_item.h | 13 +- .../history/view/history_view_message.cpp | 1 + .../history_view_reactions_selector.cpp | 2 +- .../payments/payments_reaction_process.cpp | 82 ++++++-- .../payments/payments_reaction_process.h | 9 +- .../payments/ui/payments_reaction_box.cpp | 102 ++++++++-- .../payments/ui/payments_reaction_box.h | 5 +- .../SourceFiles/ui/dynamic_thumbnails.cpp | 48 +++++ Telegram/SourceFiles/ui/dynamic_thumbnails.h | 1 + 14 files changed, 443 insertions(+), 108 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 0a858748c..65bdb834d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3477,6 +3477,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_paid_react_toast_text#one" = "You reacted with **{count} Star**."; "lng_paid_react_toast_text#other" = "You reacted with **{count} Stars**."; "lng_paid_react_undo" = "Undo"; +"lng_paid_react_show_in_top" = "Show me in Top Senders"; +"lng_paid_react_anonymous" = "Anonymous"; "lng_translate_show_original" = "Show Original"; "lng_translate_bar_to" = "Translate to {name}"; diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index dac04e0d7..bd743376e 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -155,7 +155,8 @@ constexpr auto kPaidAccumulatePeriod = 5 * crl::time(1000) + 500; } // namespace PossibleItemReactionsRef LookupPossibleReactions( - not_null item) { + not_null item, + bool paidInFront) { if (!item->canReact()) { return {}; } @@ -258,12 +259,15 @@ PossibleItemReactionsRef LookupPossibleReactions( && premiumPossible; } if (!item->reactionsAreTags()) { - const auto i = ranges::find( - result.recent, - reactions->favoriteId(), - &Reaction::id); - if (i != end(result.recent) && i != begin(result.recent)) { - std::rotate(begin(result.recent), i, i + 1); + const auto toFront = [&](Data::ReactionId id) { + const auto i = ranges::find(result.recent, id, &Reaction::id); + if (i != end(result.recent) && i != begin(result.recent)) { + std::rotate(begin(result.recent), i, i + 1); + } + }; + toFront(reactions->favoriteId()); + if (paidInFront) { + toFront(Data::ReactionId::Paid()); } } return result; @@ -1704,31 +1708,71 @@ void Reactions::sendPaid() { } bool Reactions::sendPaid(not_null item) { - const auto count = item->startPaidReactionSending(); - if (!count) { + const auto send = item->startPaidReactionSending(); + if (!send.valid) { return false; } - sendPaidRequest(item, count); + sendPaidRequest(item, send); return true; } -void Reactions::sendPaidRequest(not_null item, int count) { +void Reactions::sendPaidPrivacyRequest( + not_null item, + PaidReactionSend send) { Expects(!_sendingPaid.contains(item)); + Expects(!send.count); + + const auto id = item->fullId(); + auto &api = _owner->session().api(); + using Flag = MTPmessages_SendPaidReaction::Flag; + const auto requestId = api.request( + MTPmessages_TogglePaidReactionPrivacy( + item->history()->peer->input, + MTP_int(id.msg), + MTP_bool(send.anonymous)) + ).done([=] { + if (const auto item = _owner->message(id)) { + if (_sendingPaid.remove(item)) { + sendPaidFinish(item, send, true); + } + } + checkQuitPreventFinished(); + }).fail([=](const MTP::Error &error) { + if (const auto item = _owner->message(id)) { + if (_sendingPaid.remove(item)) { + sendPaidFinish(item, send, false); + } + } + checkQuitPreventFinished(); + }).send(); + _sendingPaid[item] = requestId; +} + +void Reactions::sendPaidRequest( + not_null item, + PaidReactionSend send) { + Expects(!_sendingPaid.contains(item)); + + if (!send.count) { + sendPaidPrivacyRequest(item, send); + return; + } const auto id = item->fullId(); const auto randomId = base::unixtime::mtproto_msg_id(); auto &api = _owner->session().api(); + using Flag = MTPmessages_SendPaidReaction::Flag; const auto requestId = api.request(MTPmessages_SendPaidReaction( - MTP_flags(0), + MTP_flags(send.anonymous ? Flag::f_private : Flag()), item->history()->peer->input, MTP_int(id.msg), - MTP_int(count), + MTP_int(send.count), MTP_long(randomId) )).done([=](const MTPUpdates &result) { if (const auto item = _owner->message(id)) { if (_sendingPaid.remove(item)) { - sendPaidFinish(item, count, true); + sendPaidFinish(item, send, true); } } _owner->session().api().applyUpdates(result); @@ -1737,9 +1781,9 @@ void Reactions::sendPaidRequest(not_null item, int count) { if (const auto item = _owner->message(id)) { _sendingPaid.remove(item); if (error.type() == u"RANDOM_ID_EXPIRED"_q) { - sendPaidRequest(item, count); + sendPaidRequest(item, send); } else { - sendPaidFinish(item, count, false); + sendPaidFinish(item, send, false); } } checkQuitPreventFinished(); @@ -1758,9 +1802,9 @@ void Reactions::checkQuitPreventFinished() { void Reactions::sendPaidFinish( not_null item, - int count, + PaidReactionSend send, bool success) { - item->finishPaidReactionSending(count, success); + item->finishPaidReactionSending(send, success); sendPaid(); } @@ -1772,7 +1816,9 @@ MessageReactions::~MessageReactions() { cancelScheduledPaid(); if (const auto paid = _paid.get()) { if (paid->sending > 0) { - finishPaidSending(paid->sending, false); + finishPaidSending( + { int(paid->sending), (paid->sendingAnonymous == 1) }, + false); } } } @@ -2063,7 +2109,7 @@ bool MessageReactions::change( if (paidTop.empty()) { if (_paid && !_paid->top.empty()) { changed = true; - if (localPaidCount()) { + if (localPaidData()) { _paid->top.clear(); } else { _paid = nullptr; @@ -2133,14 +2179,18 @@ void MessageReactions::markRead() { } } -void MessageReactions::scheduleSendPaid(int count) { - Expects(count > 0); +void MessageReactions::scheduleSendPaid(int count, bool anonymous) { + Expects(count >= 0); if (!_paid) { _paid = std::make_unique(); } _paid->scheduled += count; - _item->history()->session().credits().lock(count); + _paid->scheduledFlag = 1; + _paid->scheduledAnonymous = anonymous ? 1 : 0; + if (count > 0) { + _item->history()->session().credits().lock(count); + } _item->history()->owner().reactions().schedulePaid(_item); } @@ -2150,50 +2200,100 @@ int MessageReactions::scheduledPaid() const { void MessageReactions::cancelScheduledPaid() { if (_paid) { - if (_paid->scheduled > 0) { - _item->history()->session().credits().unlock( - base::take(_paid->scheduled)); + if (_paid->scheduledFlag) { + if (const auto amount = int(_paid->scheduled)) { + _item->history()->session().credits().unlock(amount); + } + _paid->scheduled = 0; + _paid->scheduledFlag = 0; + _paid->scheduledAnonymous = 0; } - if (!_paid->sending && _paid->top.empty()) { + if (!_paid->sendingFlag && _paid->top.empty()) { _paid = nullptr; } } } -int MessageReactions::startPaidSending() { - if (!_paid || !_paid->scheduled || _paid->sending) { - return 0; +PaidReactionSend MessageReactions::startPaidSending() { + if (!_paid || !_paid->scheduledFlag || _paid->sendingFlag) { + return {}; } _paid->sending = _paid->scheduled; + _paid->sendingFlag = _paid->scheduledFlag; + _paid->sendingAnonymous = _paid->scheduledAnonymous; _paid->scheduled = 0; - return _paid->sending; + _paid->scheduledFlag = 0; + _paid->scheduledAnonymous = 0; + return { + .count = int(_paid->sending), + .valid = true, + .anonymous = (_paid->sendingAnonymous == 1), + }; } -void MessageReactions::finishPaidSending(int count, bool success) { - Expects(count > 0); +void MessageReactions::finishPaidSending( + PaidReactionSend send, + bool success) { Expects(_paid != nullptr); - Expects(count == _paid->sending); + Expects(send.count == _paid->sending); + Expects(send.valid == (_paid->sendingFlag == 1)); + Expects(send.anonymous == (_paid->sendingAnonymous == 1)); _paid->sending = 0; - if (!_paid->scheduled && _paid->top.empty()) { + _paid->sendingFlag = 0; + _paid->sendingAnonymous = 0; + if (!_paid->scheduledFlag && _paid->top.empty()) { _paid = nullptr; + } else if (!send.count) { + const auto i = ranges::find_if(_paid->top, [](const TopPaid &top) { + return top.my; + }); + if (i != end(_paid->top)) { + i->peer = send.anonymous + ? nullptr + : _item->history()->session().user().get(); + } } - if (success) { - _item->history()->session().credits().withdrawLocked(count); - } else { - _item->history()->session().credits().unlock(count); + if (const auto amount = send.count) { + const auto credits = &_item->history()->session().credits(); + if (success) { + credits->withdrawLocked(amount); + } else { + credits->unlock(amount); + } } } +bool MessageReactions::localPaidData() const { + return _paid && (_paid->scheduledFlag || _paid->sendingFlag); +} + int MessageReactions::localPaidCount() const { return _paid ? (_paid->scheduled + _paid->sending) : 0; } +bool MessageReactions::localPaidAnonymous() const { + const auto minePaidAnonymous = [&] { + for (const auto &entry : _paid->top) { + if (entry.my) { + return !entry.peer; + } + } + return false; + }; + return _paid + && (_paid->scheduledFlag + ? (_paid->scheduledAnonymous == 1) + : _paid->sendingFlag + ? (_paid->sendingAnonymous == 1) + : minePaidAnonymous()); +} + bool MessageReactions::clearCloudData() { const auto result = !_list.empty(); _recent.clear(); _list.clear(); - if (localPaidCount()) { + if (localPaidData()) { _paid->top.clear(); } else { _paid = nullptr; diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index d3fa482b3..37c33047e 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -59,7 +59,8 @@ struct PossibleItemReactions { }; [[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions( - not_null item); + not_null item, + bool paidInFront = false); struct MyTagInfo { ReactionId id; @@ -67,6 +68,12 @@ struct MyTagInfo { int count = 0; }; +struct PaidReactionSend { + int count = 0; + bool valid = false; + bool anonymous = false; +}; + class Reactions final : private CustomEmojiManager::Listener { public: explicit Reactions(not_null owner); @@ -250,10 +257,15 @@ private: void sendPaid(); bool sendPaid(not_null item); - void sendPaidRequest(not_null item, int count); + void sendPaidRequest( + not_null item, + PaidReactionSend send); + void sendPaidPrivacyRequest( + not_null item, + PaidReactionSend send); void sendPaidFinish( not_null item, - int count, + PaidReactionSend send, bool success); void checkQuitPreventFinished(); @@ -397,21 +409,27 @@ public: [[nodiscard]] bool hasUnread() const; void markRead(); - void scheduleSendPaid(int count); + void scheduleSendPaid(int count, bool anonymous); [[nodiscard]] int scheduledPaid() const; void cancelScheduledPaid(); - int startPaidSending(); - void finishPaidSending(int count, bool success); + [[nodiscard]] PaidReactionSend startPaidSending(); + void finishPaidSending(PaidReactionSend send, bool success); + [[nodiscard]] bool localPaidData() const; [[nodiscard]] int localPaidCount() const; + [[nodiscard]] bool localPaidAnonymous() const; bool clearCloudData(); private: struct Paid { std::vector top; - int scheduled = 0; - int sending = 0; + uint32 scheduled: 30 = 0; + uint32 scheduledFlag : 1 = 0; + uint32 scheduledAnonymous : 1 = 0; + uint32 sending : 30 = 0; + uint32 sendingFlag : 1 = 0; + uint32 sendingAnonymous : 1 = 0; }; const not_null _item; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 7d2b3d821..b5b36e80d 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -492,7 +492,7 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) { return; } else if (reaction.id.paid()) { Payments::ShowPaidReactionDetails( - _controller->uiShow(), + _controller, item, viewByItem(item), HistoryReactionSource::Selector); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index a23be1277..f2688e517 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2521,15 +2521,17 @@ bool HistoryItem::canReact() const { return true; } -void HistoryItem::addPaidReaction(int count) { - Expects(count > 0); +void HistoryItem::addPaidReaction(int count, bool anonymous) { + Expects(count >= 0); Expects(_history->peer->isBroadcast()); if (!_reactions) { _reactions = std::make_unique(this); } - _reactions->scheduleSendPaid(count); - _history->owner().notifyItemDataChange(this); + _reactions->scheduleSendPaid(count, anonymous); + if (count > 0) { + _history->owner().notifyItemDataChange(this); + } } void HistoryItem::cancelScheduledPaidReaction() { @@ -2539,14 +2541,18 @@ void HistoryItem::cancelScheduledPaidReaction() { } } -int HistoryItem::startPaidReactionSending() { - return _reactions ? _reactions->startPaidSending() : 0; +Data::PaidReactionSend HistoryItem::startPaidReactionSending() { + return _reactions + ? _reactions->startPaidSending() + : Data::PaidReactionSend(); } -void HistoryItem::finishPaidReactionSending(int count, bool success) { +void HistoryItem::finishPaidReactionSending( + Data::PaidReactionSend send, + bool success) { Expects(_reactions != nullptr); - _reactions->finishPaidSending(count, success); + _reactions->finishPaidSending(send, success); _history->owner().notifyItemDataChange(this); } @@ -2586,12 +2592,15 @@ const std::vector &HistoryItem::reactions() const { } std::vector HistoryItem::reactionsWithLocal() const { - auto result = reactions(); + if (!_reactions) { + return {}; + } + auto result = _reactions->list(); const auto i = ranges::find( result, Data::ReactionId::Paid(), &Data::MessageReaction::id); - if (const auto local = _reactions ? _reactions->localPaidCount() : 0) { + if (const auto local = _reactions->localPaidCount()) { if (i != end(result)) { i->my = true; i->count += local; @@ -2629,10 +2638,41 @@ auto HistoryItem::recentReactions() const return _reactions ? _reactions->recent() : kEmpty; } -auto HistoryItem::topPaidReactions() const --> const std::vector & { - static const auto kEmpty = std::vector(); - return _reactions ? _reactions->topPaid() : kEmpty; +auto HistoryItem::topPaidReactionsWithLocal() const +-> std::vector { + if (!_reactions) { + return {}; + } + using TopPaid = Data::MessageReactionsTopPaid; + auto result = _reactions->topPaid(); + const auto i = ranges::find_if( + result, + [](const TopPaid &entry) { return entry.my != 0; }); + const auto peer = _reactions->localPaidAnonymous() + ? nullptr + : history()->session().user().get(); + if (const auto local = _reactions->localPaidCount()) { + const auto top = [&](int mine) { + return ranges::count_if(result, [&](const TopPaid &entry) { + return !entry.my && entry.count >= mine; + }) < 3; + }; + if (i != end(result)) { + i->count += local; + i->peer = peer; + i->top = top(i->count) ? 1 : 0; + } else { + result.push_back({ + .peer = peer, + .count = uint32(local), + .top = uint32(top(local) ? 1 : 0), + .my = uint32(1), + }); + } + } else if (i != end(result)) { + i->peer = peer; + } + return result; } bool HistoryItem::canViewReactions() const { @@ -3834,7 +3874,7 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) { const auto changeToEmpty = [&] { if (!_reactions) { return false; - } else if (!_reactions->localPaidCount()) { + } else if (!_reactions->localPaidData()) { _reactions = nullptr; return true; } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index e43481635..a11c6ce97 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -67,6 +67,7 @@ class Thread; struct SponsoredFrom; class Story; class SavedSublist; +struct PaidReactionSend; } // namespace Data namespace Main { @@ -445,10 +446,12 @@ public: void toggleReaction( const Data::ReactionId &reaction, HistoryReactionSource source); - void addPaidReaction(int count); + void addPaidReaction(int count, bool anonymous); void cancelScheduledPaidReaction(); - [[nodiscard]] int startPaidReactionSending(); - void finishPaidReactionSending(int count, bool success); + [[nodiscard]] Data::PaidReactionSend startPaidReactionSending(); + void finishPaidReactionSending( + Data::PaidReactionSend send, + bool success); void updateReactionsUnknown(); [[nodiscard]] auto reactions() const -> const std::vector &; @@ -458,8 +461,8 @@ public: -> const base::flat_map< Data::ReactionId, std::vector> &; - [[nodiscard]] auto topPaidReactions() const - -> const std::vector &; + [[nodiscard]] auto topPaidReactionsWithLocal() const + -> std::vector; [[nodiscard]] int reactionsPaidScheduled() const; [[nodiscard]] bool canViewReactions() const; [[nodiscard]] std::vector chosenReactions() const; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index cb408e7fc..610107a73 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -3283,6 +3283,7 @@ void Message::refreshReactions() { item, weak.get(), 1, + Payments::LookupMyPaidAnonymous(item), controller->uiShow()); return; } else { diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp index 2fc888c6f..1c1eff2f1 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -1358,7 +1358,7 @@ AttachSelectorResult AttachSelectorToMenu( desiredPosition, st::reactPanelEmojiPan, controller->uiShow(), - Data::LookupPossibleReactions(item), + Data::LookupPossibleReactions(item, true), std::move(about), std::move(iconFactory)); if (!result) { diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp index 0de8a9147..b18339a84 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.cpp +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/show.h" #include "ui/text/text_utilities.h" #include "ui/dynamic_thumbnails.h" +#include "window/window_session_controller.h" namespace Payments { namespace { @@ -41,6 +42,7 @@ void TryAddingPaidReaction( FullMsgId itemId, base::weak_ptr weakView, int count, + bool anonymous, std::shared_ptr show, Fn finished) { const auto checkItem = [=] { @@ -60,8 +62,8 @@ void TryAddingPaidReaction( const auto done = [=](Settings::SmallBalanceResult result) { if (result == Settings::SmallBalanceResult::Success) { if (const auto item = checkItem()) { - item->addPaidReaction(count); - if (const auto view = weakView.get()) { + item->addPaidReaction(count, anonymous); + if (const auto view = count ? weakView.get() : nullptr) { const auto history = view->history(); history->owner().notifyViewPaidReactionSent(view); view->animateReaction({ @@ -97,10 +99,20 @@ void TryAddingPaidReaction( } // namespace +bool LookupMyPaidAnonymous(not_null item) { + for (const auto &entry : item->topPaidReactionsWithLocal()) { + if (entry.my) { + return !entry.peer; + } + } + return false; +} + void TryAddingPaidReaction( not_null item, HistoryView::Element *view, int count, + bool anonymous, std::shared_ptr show, Fn finished) { TryAddingPaidReaction( @@ -108,17 +120,19 @@ void TryAddingPaidReaction( item->fullId(), view, count, + anonymous, std::move(show), std::move(finished)); } void ShowPaidReactionDetails( - std::shared_ptr show, + not_null controller, not_null item, HistoryView::Element *view, HistoryReactionSource source) { Expects(item->history()->peer->isBroadcast()); + const auto show = controller->uiShow(); const auto itemId = item->fullId(); const auto session = &item->history()->session(); const auto appConfig = &session->appConfig(); @@ -132,24 +146,26 @@ void ShowPaidReactionDetails( struct State { QPointer selectBox; + bool ignoreAnonymousSwitch = false; bool sending = false; }; const auto state = std::make_shared(); session->credits().load(true); const auto weakView = base::make_weak(view); - const auto send = [=](int count, auto resend) -> void { - Expects(count > 0); + const auto send = [=](int count, bool anonymous, auto resend) -> void { + Expects(count >= 0); const auto finish = [=](bool success) { state->sending = false; - if (success) { + if (success && count > 0) { + state->ignoreAnonymousSwitch = true; if (const auto strong = state->selectBox.data()) { strong->closeBox(); } } }; - if (state->sending) { + if (state->sending || (!count && state->ignoreAnonymousSwitch)) { return; } else if (const auto item = session->data().message(itemId)) { state->sending = true; @@ -157,6 +173,7 @@ void ShowPaidReactionDetails( item, weakView.get(), count, + anonymous, show, finish); } @@ -181,34 +198,57 @@ void ShowPaidReactionDetails( }; }); }; - auto already = 0; auto top = std::vector(); - const auto &topPaid = item->topPaidReactions(); - top.reserve(topPaid.size()); - for (const auto &entry : topPaid) { - if (entry.my) { - already = entry.count; - } - if (!entry.top) { - continue; - } + const auto add = [&](const Data::MessageReactionsTopPaid &entry) { + const auto peer = entry.peer; + const auto name = peer + ? peer->shortName() + : tr::lng_paid_react_anonymous(tr::now); + const auto open = [=] { + controller->showPeerInfo(peer); + }; top.push_back({ - .name = entry.peer->shortName(), - .photo = Ui::MakeUserpicThumbnail(entry.peer), + .name = name, + .photo = (peer + ? Ui::MakeUserpicThumbnail(peer) + : Ui::MakeHiddenAuthorThumbnail()), .count = int(entry.count), + .click = peer ? open : Fn(), + .my = (entry.my == 1), }); + }; + const auto topPaid = item->topPaidReactionsWithLocal(); + top.reserve(topPaid.size() + 2); + for (const auto &entry : topPaid) { + add(entry); + if (entry.my) { + auto copy = entry; + copy.peer = entry.peer ? nullptr : session->user().get(); + add(copy); + } + } + if (!ranges::contains(top, true, &Ui::PaidReactionTop::my)) { + auto entry = Data::MessageReactionsTopPaid{ + .peer = session->user(), + .count = 0, + .my = true, + }; + add(entry); + entry.peer = nullptr; + add(entry); } ranges::sort(top, ranges::greater(), &Ui::PaidReactionTop::count); state->selectBox = show->show(Ui::MakePaidReactionBox({ - .already = already + CountLocalPaid(item), .chosen = chosen, .max = max, .top = std::move(top), .channel = item->history()->peer->name(), .submit = std::move(submitText), .balanceValue = session->credits().balanceValue(), - .send = [=](int count) { send(count, send); }, + .send = [=](int count, bool anonymous) { + send(count, anonymous, send); + }, })); if (const auto strong = state->selectBox.data()) { diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.h b/Telegram/SourceFiles/payments/payments_reaction_process.h index 9bd6c0457..d9e945f33 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.h +++ b/Telegram/SourceFiles/payments/payments_reaction_process.h @@ -19,17 +19,24 @@ namespace Ui { class Show; } // namespace Ui +namespace Window { +class SessionController; +} // namespace Window + namespace Payments { +[[nodiscard]] bool LookupMyPaidAnonymous(not_null item); + void TryAddingPaidReaction( not_null item, HistoryView::Element *view, int count, + bool anonymous, std::shared_ptr show, Fn finished = nullptr); void ShowPaidReactionDetails( - std::shared_ptr show, + not_null controller, not_null item, HistoryView::Element *view, HistoryReactionSource source); diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index 57af38102..f05006b1a 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" #include "ui/widgets/continuous_sliders.h" #include "ui/dynamic_image.h" #include "ui/painter.h" @@ -178,8 +179,13 @@ void PaidReactionSlider( [[nodiscard]] not_null MakeTopReactor( not_null parent, const PaidReactionTop &data) { - const auto result = CreateChild(parent); + const auto result = CreateChild(parent); result->show(); + if (data.click && !data.my) { + result->setClickedCallback(data.click); + } else { + result->setAttribute(Qt::WA_TransparentForMouseEvents); + } struct State { QImage badge; @@ -224,7 +230,9 @@ void PaidReactionSlider( void FillTopReactors( not_null container, - std::vector top) { + std::vector top, + rpl::producer chosen, + rpl::producer anonymous) { container->add( MakeBoostFeaturesBadge( container, @@ -238,20 +246,53 @@ void FillTopReactors( st::paidReactTopMargin); struct State { std::vector> widgets; + rpl::event_stream<> updated; }; const auto state = wrap->lifetime().make_state(); - const auto topCount = std::min(int(top.size()), kMaxTopPaidShown); - for (auto i = 0; i != topCount; ++i) { - state->widgets.push_back(MakeTopReactor(wrap, top[i])); - } + rpl::combine( + std::move(chosen), + std::move(anonymous) + ) | rpl::start_with_next([=](int chosen, bool anonymous) { + for (const auto &widget : state->widgets) { + delete widget; + } + state->widgets.clear(); - wrap->widthValue() | rpl::start_with_next([=](int width) { + auto list = std::vector(); + list.reserve(kMaxTopPaidShown + 1); + for (const auto &entry : top) { + if (!entry.my) { + list.push_back(entry); + } else if (!entry.click == anonymous) { + auto copy = entry; + copy.count += chosen; + list.push_back(copy); + } + } + ranges::stable_sort( + list, + ranges::greater(), + &PaidReactionTop::count); + while (list.size() > kMaxTopPaidShown) { + list.pop_back(); + } + for (const auto &entry : list) { + state->widgets.push_back(MakeTopReactor(wrap, entry)); + } + state->updated.fire({}); + }, wrap->lifetime()); + + rpl::combine( + state->updated.events_starting_with({}), + wrap->widthValue() + ) | rpl::start_with_next([=](auto, int width) { const auto single = width / 4; if (single <= st::paidReactTopUserpic) { return; } - auto left = (width - single * topCount) / 2; + const auto count = int(state->widgets.size()); + auto left = (width - single * count) / 2; for (const auto widget : state->widgets) { widget->setGeometry(left, 0, single, height); left += single; @@ -264,6 +305,8 @@ void FillTopReactors( void PaidReactionsBox( not_null box, PaidReactionBoxArgs &&args) { + Expects(!args.top.empty()); + args.max = std::max(args.max, 2); args.chosen = std::clamp(args.chosen, 1, args.max); @@ -273,13 +316,22 @@ void PaidReactionsBox( struct State { rpl::variable chosen; + rpl::variable anonymous; }; const auto state = box->lifetime().make_state(); + state->chosen = args.chosen; const auto changed = [=](int count) { state->chosen = count; }; + const auto initialAnonymous = ranges::find( + args.top, + true, + &PaidReactionTop::my + )->click == nullptr; + state->anonymous = initialAnonymous; + const auto content = box->verticalLayout(); AddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop); @@ -307,6 +359,10 @@ void PaidReactionsBox( &st::paidReactBubbleIcon, st::boxRowPadding); + const auto already = ranges::find( + args.top, + true, + &PaidReactionTop::my)->count; PaidReactionSlider(content, args.chosen, args.max, changed); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); @@ -323,10 +379,10 @@ void PaidReactionsBox( + QMargins(0, st::lineWidth, 0, st::boostBottomSkip))); const auto label = CreateChild( labelWrap, - (args.already + (already ? tr::lng_paid_react_already( lt_count, - rpl::single(args.already) | tr::to_count(), + rpl::single(already) | tr::to_count(), Text::RichLangValue) : tr::lng_paid_react_about( lt_channel, @@ -343,13 +399,31 @@ void PaidReactionsBox( label->moveToLeft(0, skip); }, label->lifetime()); - if (!args.top.empty()) { - FillTopReactors(content, std::move(args.top)); - } + FillTopReactors( + content, + std::move(args.top), + state->chosen.value(), + state->anonymous.value()); + + const auto named = box->addRow(object_ptr>( + box, + object_ptr( + box, + tr::lng_paid_react_show_in_top(tr::now), + !state->anonymous.current()))); + state->anonymous = named->entity()->checkedValue( + ) | rpl::map(!rpl::mappers::_1); const auto button = box->addButton(rpl::single(QString()), [=] { - args.send(state->chosen.current()); + args.send(state->chosen.current(), !named->entity()->checked()); }); + + box->boxClosing() | rpl::filter([=] { + return state->anonymous.current() != initialAnonymous; + }) | rpl::start_with_next([=] { + args.send(0, state->anonymous.current()); + }, box->lifetime()); + { const auto buttonLabel = CreateChild( button, diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h index 7b830c974..8cd21f1cf 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -24,10 +24,11 @@ struct PaidReactionTop { QString name; std::shared_ptr photo; int count = 0; + Fn click; + bool my = false; }; struct PaidReactionBoxArgs { - int already = 0; int chosen = 0; int max = 0; @@ -36,7 +37,7 @@ struct PaidReactionBoxArgs { QString channel; Fn(rpl::producer amount)> submit; rpl::producer balanceValue; - Fn send; + Fn send; }; void PaidReactionsBox( diff --git a/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp b/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp index fdbd4b69f..2a472c152 100644 --- a/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp +++ b/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp @@ -157,6 +157,19 @@ private: }; +class HiddenAuthorUserpic final : public DynamicImage { +public: + std::shared_ptr clone() override; + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + QImage _frame; + int _paletteVersion = 0; + +}; + class IconThumbnail final : public DynamicImage { public: explicit IconThumbnail(const style::icon &icon); @@ -476,6 +489,37 @@ void RepliesUserpic::subscribeToUpdates(Fn callback) { } } +std::shared_ptr HiddenAuthorUserpic::clone() { + return std::make_shared(); +} + +QImage HiddenAuthorUserpic::image(int size) { + const auto good = (_frame.width() == size * _frame.devicePixelRatio()); + const auto paletteVersion = style::PaletteVersion(); + if (!good || _paletteVersion != paletteVersion) { + _paletteVersion = paletteVersion; + + const auto ratio = style::DevicePixelRatio(); + if (!good) { + _frame = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + _frame.setDevicePixelRatio(ratio); + } + _frame.fill(Qt::transparent); + + auto p = Painter(&_frame); + Ui::EmptyUserpic::PaintHiddenAuthor(p, 0, 0, size, size); + } + return _frame; +} + +void HiddenAuthorUserpic::subscribeToUpdates(Fn callback) { + if (!callback) { + _frame = {}; + } +} + IconThumbnail::IconThumbnail(const style::icon &icon) : _icon(icon) { } @@ -573,6 +617,10 @@ std::shared_ptr MakeRepliesThumbnail() { return std::make_shared(); } +std::shared_ptr MakeHiddenAuthorThumbnail() { + return std::make_shared(); +} + std::shared_ptr MakeStoryThumbnail( not_null story) { using Result = std::shared_ptr; diff --git a/Telegram/SourceFiles/ui/dynamic_thumbnails.h b/Telegram/SourceFiles/ui/dynamic_thumbnails.h index df36ae984..9876d3df8 100644 --- a/Telegram/SourceFiles/ui/dynamic_thumbnails.h +++ b/Telegram/SourceFiles/ui/dynamic_thumbnails.h @@ -23,6 +23,7 @@ class DynamicImage; bool forceRound = false); [[nodiscard]] std::shared_ptr MakeSavedMessagesThumbnail(); [[nodiscard]] std::shared_ptr MakeRepliesThumbnail(); +[[nodiscard]] std::shared_ptr MakeHiddenAuthorThumbnail(); [[nodiscard]] std::shared_ptr MakeStoryThumbnail( not_null story); [[nodiscard]] std::shared_ptr MakeIconThumbnail(