diff --git a/Telegram/SourceFiles/main/session/send_as_peers.cpp b/Telegram/SourceFiles/main/session/send_as_peers.cpp index 050c87f6e..3614cbcfd 100644 --- a/Telegram/SourceFiles/main/session/send_as_peers.cpp +++ b/Telegram/SourceFiles/main/session/send_as_peers.cpp @@ -73,6 +73,17 @@ const std::vector &SendAsPeers::list( return (i != end(_lists)) ? i->second : _onlyMe; } +std::vector> SendAsPeers::paidReactionList() const { + auto result = std::vector>(); + const auto owner = &_session->data(); + owner->enumerateBroadcasts([&](not_null channel) { + if (channel->amCreator() && !ranges::contains(result, channel)) { + result.push_back(channel); + } + }); + return result; +} + rpl::producer> SendAsPeers::updated() const { return _updates.events(); } diff --git a/Telegram/SourceFiles/main/session/send_as_peers.h b/Telegram/SourceFiles/main/session/send_as_peers.h index b047ca3ce..54de4522b 100644 --- a/Telegram/SourceFiles/main/session/send_as_peers.h +++ b/Telegram/SourceFiles/main/session/send_as_peers.h @@ -34,6 +34,8 @@ public: void setChosen(not_null peer, PeerId chosenId); [[nodiscard]] PeerId chosen(not_null peer) const; + [[nodiscard]] std::vector> paidReactionList() const; + // If !list(peer).empty() then the result will be from that list. [[nodiscard]] not_null resolveChosen( not_null peer) const; diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp index 674eca614..783e135d8 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.cpp +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "lang/lang_keys.h" #include "main/session/session_show.h" +#include "main/session/send_as_peers.h" #include "main/main_app_config.h" #include "main/main_session.h" #include "payments/ui/payments_reaction_box.h" @@ -206,35 +207,46 @@ void ShowPaidReactionDetails( .photo = (peer ? Ui::MakeUserpicThumbnail(peer) : Ui::MakeHiddenAuthorThumbnail()), + .barePeerId = peer ? uint64(peer->id.value) : 0, .count = int(entry.count), .click = peer ? open : Fn(), .my = (entry.my == 1), }); }; + const auto channels = session->sendAsPeers().paidReactionList(); const auto topPaid = item->topPaidReactionsWithLocal(); - top.reserve(topPaid.size() + 2); + top.reserve(topPaid.size() + 2 + channels.size()); 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, + auto myAdded = base::flat_set(); + const auto i = ranges::find(top, true, &Ui::PaidReactionTop::my); + if (i != end(top)) { + myAdded.emplace(i->barePeerId); + } + const auto myCount = uint32((i != end(top)) ? i->count : 0); + const auto myAdd = [&](PeerData *peer) { + const auto barePeerId = peer ? uint64(peer->id.value) : 0; + if (!myAdded.emplace(barePeerId).second) { + return; + } + add(Data::MessageReactionsTopPaid{ + .peer = peer, + .count = myCount, .my = true, - }; - add(entry); - entry.peer = nullptr; - add(entry); - if (session->api().globalPrivacy().paidReactionShownPeerCurrent()) { - std::swap(top.front(), top.back()); - } + }); + }; + const auto globalPrivacy = &session->api().globalPrivacy(); + const auto shown = globalPrivacy->paidReactionShownPeerCurrent(); + const auto owner = &session->data(); + const auto shownPeer = shown ? owner->peer(shown).get() : nullptr; + myAdd(shownPeer); + myAdd(session->user()); + myAdd(nullptr); + for (const auto &channel : channels) { + myAdd(channel); } - ranges::sort(top, ranges::greater(), &Ui::PaidReactionTop::count); + ranges::stable_sort(top, ranges::greater(), &Ui::PaidReactionTop::count); const auto linked = item->discussionPostOriginalSender(); const auto channel = (linked ? linked : item->history()->peer.get()); @@ -245,8 +257,8 @@ void ShowPaidReactionDetails( .channel = channel->name(), .submit = std::move(submitText), .balanceValue = session->credits().balanceValue(), - .send = [=](int count, bool anonymous) { - send(count, anonymous ? PeerId() : 0, send); + .send = [=](int count, uint64 barePeerId) { + send(count, PeerId(barePeerId), send); }, })); diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index 399b18e19..7cc513f7b 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -10,19 +10,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qt/qt_compare.h" #include "lang/lang_keys.h" #include "ui/boxes/boost_box.h" // MakeBoostFeaturesBadge. +#include "ui/controls/who_reacted_context_action.h" #include "ui/effects/premium_bubble.h" #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/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/dynamic_image.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_credits.h" #include "styles/style_layers.h" +#include "styles/style_media_player.h" #include "styles/style_premium.h" #include "styles/style_settings.h" @@ -192,13 +196,41 @@ void PaidReactionSlider( return result; } +void AddArrowDown(not_null widget) { + const auto arrow = CreateChild(widget); + const auto icon = &st::paidReactChannelArrow; + const auto skip = st::lineWidth * 4; + const auto size = icon->width() + skip * 2; + arrow->resize(size, size); + widget->widthValue() | rpl::start_with_next([=](int width) { + const auto left = (width - st::paidReactTopUserpic) / 2; + arrow->moveToRight(left - skip, -st::lineWidth, width); + }, widget->lifetime()); + arrow->paintRequest() | rpl::start_with_next([=] { + Painter p(arrow); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::activeButtonBg); + p.setPen(st::activeButtonFg); + const auto rect = arrow->rect(); + const auto line = st::lineWidth; + p.drawEllipse(rect.marginsRemoved({ line, line, line, line })); + icon->paint(p, skip, (size - icon->height()) / 2 + line, size); + }, widget->lifetime()); + arrow->setAttribute(Qt::WA_TransparentForMouseEvents); + arrow->show(); +} + [[nodiscard]] not_null MakeTopReactor( not_null parent, - const PaidReactionTop &data) { + const PaidReactionTop &data, + Fn selectShownPeer) { const auto result = CreateChild(parent); result->show(); if (data.click && !data.my) { result->setClickedCallback(data.click); + } else if (data.click && selectShownPeer) { + result->setClickedCallback(selectShownPeer); + AddArrowDown(result); } else { result->setAttribute(Qt::WA_TransparentForMouseEvents); } @@ -244,11 +276,60 @@ void PaidReactionSlider( return result; } +void SelectShownPeer( + std::shared_ptr> menu, + not_null parent, + const std::vector &mine, + uint64 selected, + Fn callback) { + if (*menu) { + (*menu)->hideMenu(); + } + (*menu) = CreateChild( + parent, + st::paidReactChannelMenu); + + struct Entry { + not_null action; + std::shared_ptr userpic; + }; + auto actions = std::make_shared>(); + actions->reserve(mine.size()); + for (const auto &entry : mine) { + auto action = base::make_unique_q( + (*menu)->menu(), + nullptr, + (*menu)->menu()->st(), + Ui::WhoReactedEntryData()); + const auto index = int(actions->size()); + actions->push_back({ action.get(), entry.photo->clone() }); + const auto id = entry.barePeerId; + const auto updateUserpic = [=] { + const auto size = st::defaultWhoRead.photoSize; + actions->at(index).action->setData({ + .text = entry.name, + .type = ((id == selected) + ? Ui::WhoReactedType::RefRecipientNow + : Ui::WhoReactedType::RefRecipient), + .userpic = actions->at(index).userpic->image(size), + .callback = [=] { callback(id); }, + }); + }; + actions->back().userpic->subscribeToUpdates(updateUserpic); + + (*menu)->addAction(std::move(action)); + updateUserpic(); + } + (*menu)->popup(QCursor::pos()); + +} + void FillTopReactors( not_null container, std::vector top, rpl::producer chosen, - rpl::producer anonymous) { + rpl::producer shownPeer, + Fn changeShownPeer) { container->add( MakeBoostFeaturesBadge( container, @@ -272,28 +353,33 @@ void FillTopReactors( bool chosenChanged = false; }; const auto state = wrap->lifetime().make_state(); + const auto menu = std::make_shared>(); rpl::combine( std::move(chosen), - std::move(anonymous) - ) | rpl::start_with_next([=](int chosen, bool anonymous) { + std::move(shownPeer) + ) | rpl::start_with_next([=](int chosen, uint64 barePeerId) { if (!state->initialChosen) { state->initialChosen = chosen; } else if (*state->initialChosen != chosen) { state->chosenChanged = true; } + auto mine = std::vector(); 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) { + } else if (entry.barePeerId == barePeerId) { auto copy = entry; if (state->chosenChanged) { copy.count += chosen; } list.push_back(copy); } + if (entry.my && entry.barePeerId) { + mine.push_back(entry); + } } ranges::stable_sort( list, @@ -303,6 +389,14 @@ void FillTopReactors( || (!list.empty() && !list.back().count)) { list.pop_back(); } + auto selectShownPeer = (mine.size() < 2) + ? Fn() + : [=] { SelectShownPeer( + menu, + parent, + mine, + barePeerId, + changeShownPeer); }; if (list.empty()) { wrap->hide(anim::type::normal); } else { @@ -319,7 +413,7 @@ void FillTopReactors( const auto i = state->cache.find(key); const auto widget = (i != end(state->cache)) ? i->second - : MakeTopReactor(parent, entry); + : MakeTopReactor(parent, entry, selectShownPeer); state->widgets.push_back(widget); widget->show(); } @@ -368,7 +462,8 @@ void PaidReactionsBox( struct State { rpl::variable chosen; - rpl::variable anonymous; + rpl::variable shownPeer; + uint64 savedShownPeer = 0; }; const auto state = box->lifetime().make_state(); @@ -377,12 +472,16 @@ void PaidReactionsBox( state->chosen = count; }; - const auto initialAnonymous = ranges::find( + const auto initialShownPeer = ranges::find( args.top, true, &PaidReactionTop::my - )->click == nullptr; - state->anonymous = initialAnonymous; + )->barePeerId; + state->shownPeer = initialShownPeer; + state->savedShownPeer = ranges::find_if(args.top, []( + const PaidReactionTop &entry) { + return entry.my && entry.barePeerId != 0; + })->barePeerId; const auto content = box->verticalLayout(); AddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop); @@ -455,25 +554,30 @@ void PaidReactionsBox( content, std::move(args.top), state->chosen.value(), - state->anonymous.value()); + state->shownPeer.value(), + [=](uint64 barePeerId) { + state->shownPeer = state->savedShownPeer = barePeerId; + }); 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); + state->shownPeer.current() != 0))); + named->entity()->checkedValue( + ) | rpl::start_with_next([=](bool show) { + state->shownPeer = show ? state->savedShownPeer : 0; + }, named->lifetime()); const auto button = box->addButton(rpl::single(QString()), [=] { - args.send(state->chosen.current(), !named->entity()->checked()); + args.send(state->chosen.current(), state->shownPeer.current()); }); box->boxClosing() | rpl::filter([=] { - return state->anonymous.current() != initialAnonymous; + return state->shownPeer.current() != initialShownPeer; }) | rpl::start_with_next([=] { - args.send(0, state->anonymous.current()); + args.send(0, state->shownPeer.current()); }, box->lifetime()); { diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h index 61a2b084b..034b04289 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -23,6 +23,7 @@ struct TextWithContext { struct PaidReactionTop { QString name; std::shared_ptr photo; + uint64 barePeerId = 0; int count = 0; Fn click; bool my = false; @@ -37,7 +38,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/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 5c52734e7..261beffaf 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -401,6 +401,13 @@ paidReactToastLabel: FlatLabel(defaultFlatLabel) { paidReactTopStarIcon: icon{{ "chat/mini_stars", premiumButtonFg }}; paidReactTopStarIconPosition: point(0px, 1px); paidReactTopStarSkip: 4px; +paidReactChannelArrow: icon{{ "intro_country_dropdown", activeButtonFg }}; +paidReactChannelMenu: PopupMenu(popupMenuWithIcons) { + menu: Menu(menuWithIcons) { + widthMax: 240px; + } + maxHeight: 345px; +} toastUndoStroke: 2px; toastUndoSpace: 8px;