From a0764190f21096dad01d2fa50e85b827e019dd3e Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 20 Mar 2025 17:06:02 +0400 Subject: [PATCH] Support sponsored peers in search results. --- Telegram/Resources/langs/lang.strings | 5 + Telegram/SourceFiles/api/api_peer_search.cpp | 7 +- Telegram/SourceFiles/api/api_peer_search.h | 4 +- .../data/components/sponsored_messages.cpp | 82 ++-- .../data/components/sponsored_messages.h | 45 ++- Telegram/SourceFiles/dialogs/dialogs.style | 31 +- Telegram/SourceFiles/dialogs/dialogs_common.h | 9 + .../dialogs/dialogs_inner_widget.cpp | 367 +++++++++++++----- .../dialogs/dialogs_inner_widget.h | 33 +- .../SourceFiles/dialogs/dialogs_widget.cpp | 10 +- .../SourceFiles/dialogs/ui/dialogs_layout.cpp | 24 +- .../SourceFiles/dialogs/ui/dialogs_layout.h | 2 + .../dialogs/ui/dialogs_suggestions.cpp | 16 +- .../history/history_item_helpers.cpp | 3 +- .../earn/channel_earn.style | 2 +- Telegram/SourceFiles/menu/menu_sponsored.cpp | 112 ++++-- Telegram/SourceFiles/menu/menu_sponsored.h | 21 + 17 files changed, 546 insertions(+), 227 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 23e533b492..862ff5565f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4392,6 +4392,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_search_filter_private" = "Private chats"; "lng_search_filter_group" = "Group chats"; "lng_search_filter_channel" = "Channels"; +"lng_search_sponsored_button" = "Ad ⋮"; "lng_media_save_progress" = "{ready} of {total} {mb}"; "lng_mediaview_save_as" = "Save As..."; @@ -5806,6 +5807,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sponsored_revenued_info1_title" = "Respect Your Privacy"; "lng_sponsored_revenued_info1_description" = "Ads on Telegram do not use your personal information and are based on the channel in which you see them."; "lng_sponsored_revenued_info1_bot_description" = "Ads on Telegram do not use your personal information and are based on the mini app in which you see them."; +"lng_sponsored_revenued_info1_search_description" = "Ads on Telegram do not use your personal information and are based on the search query you entered."; "lng_sponsored_revenued_info2_title" = "Help the Channel Creator"; "lng_sponsored_revenued_info2_bot_title" = "Help the Bot Developer"; "lng_sponsored_revenued_info2_description" = "50% of the revenue from Telegram Ads goes to the owner of the channel where they are displayed."; @@ -5814,9 +5816,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sponsored_revenued_info3_description#one" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers."; "lng_sponsored_revenued_info3_description#other" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers."; "lng_sponsored_revenued_info3_bot_description" = "You can turn off ads in mini apps by subscribing to {link}."; +"lng_sponsored_revenued_info3_search_description" = "You can turn off ads by subscribing to Telegram Premium. {link}"; +"lng_sponsored_revenued_info3_search_link" = "Subscribe {arrow}"; "lng_sponsored_revenued_footer_title" = "Can I Launch an Ad?"; "lng_sponsored_revenued_footer_description" = "Anyone can create an ad to display in this channel — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}"; "lng_sponsored_revenued_footer_bot_description" = "Anyone can create an ad to display in this bot — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}"; +"lng_sponsored_revenued_footer_search_description" = "Anyone can create an ad to display in search results for any query. Check out the **Telegram Ad Platform** for details. {link}"; "lng_sponsored_top_bar_hide" = "remove"; "lng_telegram_features_url" = "https://t.me/TelegramTips"; diff --git a/Telegram/SourceFiles/api/api_peer_search.cpp b/Telegram/SourceFiles/api/api_peer_search.cpp index 4d7db9ccc8..789334681e 100644 --- a/Telegram/SourceFiles/api/api_peer_search.cpp +++ b/Telegram/SourceFiles/api/api_peer_search.cpp @@ -106,9 +106,10 @@ void PeerSearch::requestSponsored() { parsed.sponsored.push_back({ .peer = _session->data().peer(peerId), .randomId = data.vrandom_id().v, - .sponsorInfo = qs(data.vsponsor_info().value_or_empty()), - .additionalInfo = qs( - data.vadditional_info().value_or_empty()), + .sponsorInfo = TextWithEntities::Simple( + qs(data.vsponsor_info().value_or_empty())), + .additionalInfo = TextWithEntities::Simple( + qs(data.vadditional_info().value_or_empty())), }); } finishSponsored(requestId, std::move(parsed)); diff --git a/Telegram/SourceFiles/api/api_peer_search.h b/Telegram/SourceFiles/api/api_peer_search.h index e8f9f0253c..c706429521 100644 --- a/Telegram/SourceFiles/api/api_peer_search.h +++ b/Telegram/SourceFiles/api/api_peer_search.h @@ -16,8 +16,8 @@ namespace Api { struct SponsoredSearchResult { not_null peer; QByteArray randomId; - QString sponsorInfo; - QString additionalInfo; + TextWithEntities sponsorInfo; + TextWithEntities additionalInfo; }; struct PeerSearchResult { diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index 95ea5cb0ed..fa5dacf697 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/sponsored_messages.h" #include "api/api_text_entities.h" +#include "api/api_peer_search.h" // SponsoredSearchResult #include "apiwrap.h" #include "core/click_handler_types.h" #include "data/data_channel.h" @@ -33,6 +34,19 @@ constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000); return (received > 0) && (received + kRequestTimeLimit > crl::now()); } +template +[[nodiscard]] std::vector Prepare(const Fields &fields) { + using InfoList = std::vector; + return (!fields.sponsorInfo.text.isEmpty() + && !fields.additionalInfo.text.isEmpty()) + ? InfoList{ fields.sponsorInfo, fields.additionalInfo } + : !fields.sponsorInfo.text.isEmpty() + ? InfoList{ fields.sponsorInfo } + : !fields.additionalInfo.text.isEmpty() + ? InfoList{ fields.additionalInfo } + : InfoList{}; +} + } // namespace SponsoredMessages::SponsoredMessages(not_null session) @@ -535,18 +549,8 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails( return {}; } const auto &data = entryPtr->sponsored; - - using InfoList = std::vector; - auto info = (!data.sponsorInfo.text.isEmpty() - && !data.additionalInfo.text.isEmpty()) - ? InfoList{ data.sponsorInfo, data.additionalInfo } - : !data.sponsorInfo.text.isEmpty() - ? InfoList{ data.sponsorInfo } - : !data.additionalInfo.text.isEmpty() - ? InfoList{ data.additionalInfo } - : InfoList{}; return { - .info = std::move(info), + .info = Prepare(data), .link = data.link, .buttonText = data.from.buttonText, .photoId = data.from.photoId, @@ -559,6 +563,14 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails( }; } +SponsoredMessages::Details SponsoredMessages::lookupDetails( + const Api::SponsoredSearchResult &data) const { + return { + .info = Prepare(data), + .canReport = true, + }; +} + void SponsoredMessages::clicked( const FullMsgId &fullId, bool isMedia, @@ -583,9 +595,29 @@ void SponsoredMessages::clicked( )).send(); } +SponsoredReportAction SponsoredMessages::createReportCallback( + const FullMsgId &fullId) { + const auto entry = find(fullId); + if (!entry) { + return { .callback = [=](const auto &...) {} }; + } + const auto history = _session->data().history(fullId.peer); + const auto erase = [=] { + const auto it = _data.find(history); + if (it != end(_data)) { + auto &list = it->second.entries; + const auto proj = [&](const Entry &e) { + return e.itemFullId == fullId; + }; + list.erase(ranges::remove_if(list, proj), end(list)); + } + }; + return createReportCallback(entry->sponsored.randomId, erase); +} -auto SponsoredMessages::createReportCallback(const FullMsgId &fullId) --> Fn)> { +SponsoredReportAction SponsoredMessages::createReportCallback( + const QByteArray &randomId, + Fn erase) { using TLChoose = MTPDchannels_sponsoredMessageReportResultChooseOption; using TLAdsHidden = MTPDchannels_sponsoredMessageReportResultAdsHidden; using TLReported = MTPDchannels_sponsoredMessageReportResultReported; @@ -601,25 +633,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId) }; const auto state = std::make_shared(); - return [=](Result::Id optionId, Fn done) { - const auto entry = find(fullId); - if (!entry) { - return; - } - - const auto history = _session->data().history(fullId.peer); - - const auto erase = [=] { - const auto it = _data.find(history); - if (it != end(_data)) { - auto &list = it->second.entries; - const auto proj = [&](const Entry &e) { - return e.itemFullId == fullId; - }; - list.erase(ranges::remove_if(list, proj), end(list)); - } - }; - + return { .callback = [=](Result::Id optionId, Fn done) { if (optionId == Result::Id("-1")) { erase(); return; @@ -627,7 +641,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId) state->requestId = _session->api().request( MTPmessages_ReportSponsoredMessage( - MTP_bytes(entry->sponsored.randomId), + MTP_bytes(randomId), MTP_bytes(optionId)) ).done([=]( const MTPchannels_SponsoredMessageReportResult &result, @@ -664,7 +678,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId) done({ .error = error.type() }); } }).send(); - }; + } }; } SponsoredMessages::State SponsoredMessages::state( diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.h b/Telegram/SourceFiles/data/components/sponsored_messages.h index 194206e1d7..6c875c081c 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.h +++ b/Telegram/SourceFiles/data/components/sponsored_messages.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class History; +namespace Api { +struct SponsoredSearchResult; +} // namespace Api + namespace Main { class Session; } // namespace Main @@ -69,6 +73,25 @@ struct SponsoredMessage { TextWithEntities additionalInfo; }; +struct SponsoredMessageDetails { + std::vector info; + QString link; + QString buttonText; + PhotoId photoId = PhotoId(0); + PhotoId mediaPhotoId = PhotoId(0); + DocumentId mediaDocumentId = DocumentId(0); + uint64 backgroundEmojiId = 0; + uint8 colorIndex : 6 = 0; + bool isLinkInternal = false; + bool canReport = false; +}; + +struct SponsoredReportAction { + Fn)> callback; +}; + class SponsoredMessages final { public: enum class AppendResult { @@ -82,18 +105,7 @@ public: InjectToMiddle, AppendToTopBar, }; - struct Details { - std::vector info; - QString link; - QString buttonText; - PhotoId photoId = PhotoId(0); - PhotoId mediaPhotoId = PhotoId(0); - DocumentId mediaDocumentId = DocumentId(0); - uint64 backgroundEmojiId = 0; - uint8 colorIndex : 6 = 0; - bool isLinkInternal = false; - bool canReport = false; - }; + using Details = SponsoredMessageDetails; using RandomId = QByteArray; explicit SponsoredMessages(not_null session); ~SponsoredMessages(); @@ -103,6 +115,8 @@ public: void request(not_null history, Fn done); void clearItems(not_null history); [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; + [[nodiscard]] Details lookupDetails( + const Api::SponsoredSearchResult &data) const; void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen); void clicked( const QByteArray &randomId, @@ -125,8 +139,11 @@ public: [[nodiscard]] State state(not_null history) const; - [[nodiscard]] auto createReportCallback(const FullMsgId &fullId) - -> Fn)>; + [[nodiscard]] SponsoredReportAction createReportCallback( + const FullMsgId &fullId); + [[nodiscard]] SponsoredReportAction createReportCallback( + const QByteArray &randomId, + Fn erase); void clear(); diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 7426ca529b..9d4323f19c 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -24,6 +24,10 @@ DialogRow { unreadMarkDiameter: pixels; tagTop: pixels; } +DialogRightButton { + button: RoundButton; + margin: margins; +} ThreeStateIcon { icon: icon; @@ -115,11 +119,16 @@ dialogRowFilterTagSkip: 4px; dialogRowFilterTagStyle: TextStyle(defaultTextStyle) { font: font(10px); } -dialogRowOpenBotTextStyle: semiboldTextStyle; -dialogRowOpenBotHeight: 20px; -dialogRowOpenBotRight: 10px; -dialogRowOpenBotTop: 32px; -dialogRowOpenBotRecentTop: 28px; +dialogRowOpenBot: DialogRightButton { + button: RoundButton(defaultActiveButton) { + height: 20px; + textTop: 1px; + } + margin: margins(0px, 32px, 10px, 0px); +} +dialogRowOpenBotRecent: DialogRightButton(dialogRowOpenBot) { + margin: margins(0px, 32px, 28px, 0px); +} forumDialogJumpArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFg }}; forumDialogJumpArrowOver: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgOver }}; @@ -792,3 +801,15 @@ dialogsPopularAppsAbout: FlatLabel(boxDividerLabel) { dialogsQuickActionSize: 20px; dialogsQuickActionRippleSize: 80px; + +dialogsSponsoredButton: DialogRightButton(dialogRowOpenBot) { + button: RoundButton(defaultLightButton) { + textFg: windowActiveTextFg; + textFgOver: windowActiveTextFg; + textBg: lightButtonBgOver; + textBgOver: lightButtonBgOver; + height: 20px; + textTop: 1px; + } + margin: margins(0px, 9px, 10px, 0px); +} diff --git a/Telegram/SourceFiles/dialogs/dialogs_common.h b/Telegram/SourceFiles/dialogs/dialogs_common.h index 4d6523f975..c1e1cd755d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_common.h +++ b/Telegram/SourceFiles/dialogs/dialogs_common.h @@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +namespace style { +struct DialogRightButton; +} // namespace style + namespace Ui { class RippleAnimation; } // namespace Ui @@ -114,11 +118,16 @@ struct RowsByLetter { }; struct RightButton final { + const style::DialogRightButton *st = nullptr; QImage bg; QImage selectedBg; QImage activeBg; Ui::Text::String text; std::unique_ptr ripple; + + explicit operator bool() const { + return st != nullptr; + } }; } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 8f9f7d9cbe..7f30ceb753 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -63,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "menu/menu_sponsored.h" #include "window/notifications_manager.h" #include "window/window_controller.h" #include "window/window_session_controller.h" @@ -241,12 +242,17 @@ struct InnerWidget::HashtagResult { BasicRow row; }; +struct InnerWidget::SponsoredSearchResult { + Api::SponsoredSearchResult data; + RightButton button; +}; + struct InnerWidget::PeerSearchResult { explicit PeerSearchResult(not_null peer) : peer(peer) { } not_null peer; - std::unique_ptr sponsored; + std::unique_ptr sponsored; mutable Ui::Text::String name; mutable Ui::PeerBadge badge; BasicRow row; @@ -287,6 +293,12 @@ InnerWidget::InnerWidget( _topicJumpCache = nullptr; _chatsFilterTags.clear(); _rightButtons.clear(); + _pressedRightButtonData = nullptr; + for (const auto &result : _peerSearchResults) { + if (const auto sponsored = result->sponsored.get()) { + sponsored->button = {}; + } + } }, lifetime()); session().downloaderTaskFinished( @@ -1139,17 +1151,32 @@ void InnerWidget::paintEvent(QPaintEvent *e) { && r.y() <= (skip + from * st::dialogsRowHeight) && r.y() + r.height() >= (skip + (from + 1) * st::dialogsRowHeight)) { session().sponsoredMessages().view( - result->sponsored->randomId); + result->sponsored->data.randomId); } const auto peer = result->peer; const auto active = !activeEntry.fullId && activePeer && ((peer == activePeer) || (peer->migrateTo() == activePeer)); - const auto selected = (from == (isPressed() + const auto selected = (from == ((_peerSearchMenu >= 0) + ? _peerSearchMenu + : isPressed() ? _peerSearchPressed : _peerSearchSelected)); + if (result->sponsored + && result->sponsored->button.text.isEmpty()) { + fillRightButton( + result->sponsored->button, + tr::lng_search_sponsored_button( + tr::now, + Ui::Text::WithEntities), + st::dialogsSponsoredButton); + } + paintPeerSearchResult(p, result.get(), { + .rightButton = (result->sponsored + ? &result->sponsored->button + : nullptr), .st = &st::defaultDialogRow, .currentBg = currentBg(), .now = ms, @@ -1307,36 +1334,47 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } } +void InnerWidget::fillRightButton( + RightButton &button, + const TextWithEntities &text, + const style::DialogRightButton &st) { + button.st = &st; + button.text.setMarkedText(st.button.style, text); + const auto size = QSize( + button.text.maxWidth() + button.text.minHeight(), + st.button.height); + const auto generateBg = [&](const style::color &c) { + auto bg = QImage( + style::DevicePixelRatio() * size, + QImage::Format_ARGB32_Premultiplied); + bg.setDevicePixelRatio(style::DevicePixelRatio()); + bg.fill(Qt::transparent); + { + auto p = QPainter(&bg); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(c); + const auto r = size.height() / 2; + p.drawRoundedRect(Rect(size), r, r); + } + return bg; + }; + button.bg = generateBg(st.button.textBg); + button.selectedBg = generateBg(st.button.textBgOver); + button.activeBg = generateBg(st.button.textFg); +} + [[nodiscard]] RightButton *InnerWidget::maybeCacheRightButton(Row *row) { if (const auto user = MaybeBotWithApp(row)) { const auto it = _rightButtons.find(user->id); if (it == _rightButtons.end()) { auto rightButton = RightButton(); - const auto text = tr::lng_profile_open_app_short(tr::now); - rightButton.text.setText(st::dialogRowOpenBotTextStyle, text); - const auto size = QSize( - rightButton.text.maxWidth() - + rightButton.text.minHeight(), - st::dialogRowOpenBotHeight); - const auto generateBg = [&](const style::color &c) { - auto bg = QImage( - style::DevicePixelRatio() * size, - QImage::Format_ARGB32_Premultiplied); - bg.setDevicePixelRatio(style::DevicePixelRatio()); - bg.fill(Qt::transparent); - { - auto p = QPainter(&bg); - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - p.setBrush(c); - const auto r = size.height() / 2; - p.drawRoundedRect(Rect(size), r, r); - } - return bg; - }; - rightButton.bg = generateBg(st::activeButtonBg); - rightButton.selectedBg = generateBg(st::activeButtonBgOver); - rightButton.activeBg = generateBg(st::activeButtonFg); + fillRightButton( + rightButton, + tr::lng_profile_open_app_short( + tr::now, + Ui::Text::WithEntities), + st::dialogRowOpenBot); return &(_rightButtons.emplace( user->id, std::move(rightButton)).first->second); @@ -1459,7 +1497,11 @@ void InnerWidget::paintPeerSearchResult( context.st->photoSize); auto nameleft = context.st->nameLeft; - auto namewidth = context.width - nameleft - context.st->padding.right(); + auto available = context.width - nameleft - context.st->padding.right(); + auto namewidth = available; + if (const auto used = Ui::PaintRightButton(p, context)) { + namewidth -= used - st::dialogsUnreadPadding; + } QRect rectForName(nameleft, context.st->nameTop, namewidth, st::semiboldFont->height); if (result->name.isEmpty()) { @@ -1611,7 +1653,7 @@ void InnerWidget::clearIrrelevantState() { _filteredSelected = -1; setFilteredPressed(-1, false, false); _peerSearchSelected = -1; - setPeerSearchPressed(-1); + setPeerSearchPressed(-1, false); _previewSelected = -1; setPreviewPressed(-1); _searchedSelected = -1; @@ -1630,20 +1672,28 @@ bool InnerWidget::lookupIsInBotAppButton( if (const auto user = MaybeBotWithApp(row)) { const auto it = _rightButtons.find(user->id); if (it != _rightButtons.end()) { - const auto s = it->second.bg.size() / style::DevicePixelRatio(); - const auto r = QRect( - width() - s.width() - st::dialogRowOpenBotRight, - st::dialogRowOpenBotTop, - s.width(), - s.height()); - if (r.contains(localPosition)) { - return true; - } + return lookupIsInRightButton(it->second, localPosition); } } return false; } +bool InnerWidget::lookupIsInRightButton( + const RightButton &button, + QPoint localPosition) { + if (!button.st) { + return false; + } + + const auto s = button.bg.size() / style::DevicePixelRatio(); + const auto r = QRect( + width() - s.width() - button.st->margin.right(), + button.st->margin.top(), + s.width(), + s.height()); + return r.contains(localPosition); +} + void InnerWidget::selectByMouse(QPoint globalPosition) { const auto local = mapFromGlobal(globalPosition); if (updateReorderPinned(local)) { @@ -1687,16 +1737,16 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { const auto mappedY = selected ? mouseY - offset - selected->top() : 0; const auto selectedTopicJump = selected && selected->lookupIsInTopicJump(local.x(), mappedY); - const auto selectedBotApp = selected + const auto selectedRightButton = selected && lookupIsInBotAppButton(selected, QPoint(local.x(), mappedY)); if (_collapsedSelected != collapsedSelected || _selected != selected || _selectedTopicJump != selectedTopicJump - || _selectedBotApp != selectedBotApp) { + || _selectedRightButton != selectedRightButton) { updateSelectedRow(); _selected = selected; _selectedTopicJump = selectedTopicJump; - _selectedBotApp = selectedBotApp; + _selectedRightButton = selectedRightButton; _collapsedSelected = collapsedSelected; updateSelectedRow(); setCursor((_selected || _collapsedSelected >= 0) @@ -1736,29 +1786,39 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { && _filterResults[filteredSelected].row->lookupIsInTopicJump( local.x(), mappedY); - const auto selectedBotApp = (filteredSelected >= 0) + const auto selectedRightButton = (filteredSelected >= 0) && lookupIsInBotAppButton( _filterResults[filteredSelected].row, QPoint(local.x(), mappedY)); if (_filteredSelected != filteredSelected || _selectedTopicJump != selectedTopicJump - || _selectedBotApp != selectedBotApp) { + || _selectedRightButton != selectedRightButton) { updateSelectedRow(); _filteredSelected = filteredSelected; _selectedTopicJump = selectedTopicJump; - _selectedBotApp = selectedBotApp; + _selectedRightButton = selectedRightButton; updateSelectedRow(); } } if (!_peerSearchResults.empty()) { - auto skip = peerSearchOffset(); + const auto skip = peerSearchOffset(); auto peerSearchSelected = (mouseY >= skip) ? ((mouseY - skip) / st::dialogsRowHeight) : -1; if (peerSearchSelected < 0 || peerSearchSelected >= _peerSearchResults.size()) { peerSearchSelected = -1; } - if (_peerSearchSelected != peerSearchSelected) { + const auto mappedY = (peerSearchSelected >= 0) + ? mouseY - skip - (peerSearchSelected * st::dialogsRowHeight) + : 0; + const auto selectedRightButton = (peerSearchSelected >= 0) + && _peerSearchResults[peerSearchSelected]->sponsored + && lookupIsInRightButton( + _peerSearchResults[peerSearchSelected]->sponsored->button, + QPoint(local.x(), mappedY)); + if (_peerSearchSelected != peerSearchSelected + || _selectedRightButton != selectedRightButton) { updateSelectedRow(); _peerSearchSelected = peerSearchSelected; + _selectedRightButton = selectedRightButton; updateSelectedRow(); } } @@ -1845,12 +1905,15 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { selectByMouse(e->globalPos()); _pressButton = e->button(); - setPressed(_selected, _selectedTopicJump, _selectedBotApp); + setPressed(_selected, _selectedTopicJump, _selectedRightButton); setCollapsedPressed(_collapsedSelected); setHashtagPressed(_hashtagSelected); _hashtagDeletePressed = _hashtagDeleteSelected; - setFilteredPressed(_filteredSelected, _selectedTopicJump, _selectedBotApp); - setPeerSearchPressed(_peerSearchSelected); + setFilteredPressed( + _filteredSelected, + _selectedTopicJump, + _selectedRightButton); + setPeerSearchPressed(_peerSearchSelected, _selectedRightButton); setPreviewPressed(_previewSelected); setSearchedPressed(_searchedSelected); _pressedMorePosts = _selectedMorePosts; @@ -1881,7 +1944,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { - QPoint(0, dialogsOffset() + _pressed->top()); if ((_pressButton == Qt::MiddleButton) && addQuickActionRipple(row, updateCallback)) { - } else if (addBotAppRipple(origin, updateCallback)) { + } else if (addRightButtonRipple(origin, updateCallback)) { } else if (_pressedTopicJump) { row->addTopicJumpRipple( origin, @@ -1908,7 +1971,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { const auto origin = e->pos() - QPoint(0, filteredOffset() + result.top); const auto updateCallback = [=] { repaintDialogRow(filterId, row); }; - if (addBotAppRipple(origin, updateCallback)) { + if (addRightButtonRipple(origin, updateCallback)) { } else if (_pressedTopicJump) { row->addTopicJumpRipple( origin, @@ -1923,11 +1986,19 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { } } else if (base::in_range(_peerSearchPressed, 0, _peerSearchResults.size())) { auto &result = _peerSearchResults[_peerSearchPressed]; - auto row = &result->row; - row->addRipple( - e->pos() - QPoint(0, peerSearchOffset() + _peerSearchPressed * st::dialogsRowHeight), - QSize(width(), st::dialogsRowHeight), - [this, peer = result->peer] { updateSearchResult(peer); }); + const auto row = &result->row; + const auto origin = e->pos() + - QPoint(0, peerSearchOffset() + _peerSearchPressed * st::dialogsRowHeight); + const auto updateCallback = [this, peer = result->peer] { + updateSearchResult(peer); + }; + if (addRightButtonRipple(origin, updateCallback)) { + } else { + row->addRipple( + origin, + QSize(width(), st::dialogsRowHeight), + updateCallback); + } } else if (base::in_range(_searchedPressed, 0, _searchResults.size())) { auto &row = _searchResults[_searchedPressed]; row->addRipple( @@ -1943,22 +2014,22 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { } } -bool InnerWidget::addBotAppRipple(QPoint origin, Fn updateCallback) { - if (!(_pressedBotApp && _pressedBotAppData)) { +bool InnerWidget::addRightButtonRipple(QPoint origin, Fn updateCallback) { + if (!(_pressedRightButton && _pressedRightButtonData)) { return false; } - const auto size = _pressedBotAppData->bg.size() + const auto size = _pressedRightButtonData->bg.size() / style::DevicePixelRatio(); - if (!_pressedBotAppData->ripple) { - _pressedBotAppData->ripple = std::make_unique( - st::defaultRippleAnimation, + if (!_pressedRightButtonData->ripple) { + _pressedRightButtonData->ripple = std::make_unique( + _pressedRightButtonData->st->button.ripple, Ui::RippleAnimation::RoundRectMask(size, size.height() / 2), std::move(updateCallback)); } const auto shift = QPoint( - width() - size.width() - st::dialogRowOpenBotRight, - st::dialogRowOpenBotTop); - _pressedBotAppData->ripple->add(origin - shift); + width() - size.width() - _pressedRightButtonData->st->margin.right(), + _pressedRightButtonData->st->margin.top()); + _pressedRightButtonData->ripple->add(origin - shift); return true; } @@ -2051,7 +2122,7 @@ void InnerWidget::checkReorderPinnedStart(QPoint localPosition) { if (!_pressed || _dragging || (_state != WidgetState::Default) - || _pressedBotApp) { + || _pressedRightButtonData) { return; } else if (qAbs(localPosition.y() - _dragStart.y()) < style::ConvertScale(kStartReorderThreshold)) { @@ -2321,7 +2392,7 @@ void InnerWidget::mousePressReleased( setCollapsedPressed(-1); const auto pressedTopicRootId = _pressedTopicJumpRootId; const auto pressedTopicJump = _pressedTopicJump; - const auto pressedBotApp = _pressedBotApp; + const auto pressedRightButton = _pressedRightButton; auto pressed = _pressed; clearPressed(); auto hashtagPressed = _hashtagPressed; @@ -2331,7 +2402,7 @@ void InnerWidget::mousePressReleased( auto filteredPressed = _filteredPressed; setFilteredPressed(-1, false, false); auto peerSearchPressed = _peerSearchPressed; - setPeerSearchPressed(-1); + setPeerSearchPressed(-1, false); auto previewPressed = _previewPressed; setPreviewPressed(-1); auto searchedPressed = _searchedPressed; @@ -2343,8 +2414,8 @@ void InnerWidget::mousePressReleased( if (wasDragging) { selectByMouse(globalPosition); } - if (_pressedBotAppData && _pressedBotAppData->ripple) { - _pressedBotAppData->ripple->lastStop(); + if (_pressedRightButtonData && _pressedRightButtonData->ripple) { + _pressedRightButtonData->ripple->lastStop(); } if (_activeQuickAction && pressed && !_activeQuickAction->data) { if (const auto history = pressed->history()) { @@ -2372,13 +2443,14 @@ void InnerWidget::mousePressReleased( || (pressed && pressed == _selected && pressedTopicJump == _selectedTopicJump - && pressedBotApp == _selectedBotApp) + && pressedRightButton == _selectedRightButton) || (hashtagPressed >= 0 && hashtagPressed == _hashtagSelected && hashtagDeletePressed == _hashtagDeleteSelected) || (filteredPressed >= 0 && filteredPressed == _filteredSelected) || (peerSearchPressed >= 0 - && peerSearchPressed == _peerSearchSelected) + && peerSearchPressed == _peerSearchSelected + && pressedRightButton == _selectedRightButton) || (previewPressed >= 0 && previewPressed == _previewSelected) || (searchedPressed >= 0 @@ -2387,13 +2459,15 @@ void InnerWidget::mousePressReleased( && pressedMorePosts == _selectedMorePosts) || (pressedChatTypeFilter && pressedChatTypeFilter == _selectedChatTypeFilter)) { - if (pressedBotApp && (pressed || filteredPressed >= 0)) { + if (pressedRightButton && (pressed || filteredPressed >= 0)) { const auto &row = pressed ? pressed : _filterResults[filteredPressed].row.get(); if (const auto user = MaybeBotWithApp(row)) { _openBotMainAppRequests.fire(peerToUser(user->id)); } + } else if (pressedRightButton && peerSearchPressed >= 0) { + showSponsoredMenu(peerSearchPressed, globalPosition); } else { chooseRow(modifiers, pressedTopicRootId); } @@ -2420,25 +2494,25 @@ void InnerWidget::setCollapsedPressed(int pressed) { void InnerWidget::setPressed( Row *pressed, bool pressedTopicJump, - bool pressedBotApp) { + bool pressedRightButton) { if ((_pressed != pressed) || (pressed && _pressedTopicJump != pressedTopicJump) - || (pressed && _pressedBotApp != pressedBotApp)) { + || (pressed && _pressedRightButton != pressedRightButton)) { if (_pressed) { _pressed->stopLastRipple(); } - if (_pressedBotAppData && _pressedBotAppData->ripple) { - _pressedBotAppData->ripple->lastStop(); + if (_pressedRightButtonData && _pressedRightButtonData->ripple) { + _pressedRightButtonData->ripple->lastStop(); } _pressed = pressed; - if (pressed || !pressedTopicJump || !pressedBotApp) { + if (pressed || !pressedTopicJump || !pressedRightButton) { _pressedTopicJump = pressedTopicJump; - _pressedBotApp = pressedBotApp; - if (pressedBotApp) { + _pressedRightButton = pressedRightButton; + if (pressedRightButton) { if (const auto user = MaybeBotWithApp(pressed)) { const auto it = _rightButtons.find(user->id); if (it != _rightButtons.end()) { - _pressedBotAppData = &(it->second); + _pressedRightButtonData = &(it->second); } } } @@ -2465,26 +2539,26 @@ void InnerWidget::setHashtagPressed(int pressed) { void InnerWidget::setFilteredPressed( int pressed, bool pressedTopicJump, - bool pressedBotApp) { + bool pressedRightButton) { if (_filteredPressed != pressed || (pressed >= 0 && _pressedTopicJump != pressedTopicJump) - || (pressed >= 0 && _pressedBotApp != pressedBotApp)) { + || (pressed >= 0 && _pressedRightButton != pressedRightButton)) { if (base::in_range(_filteredPressed, 0, _filterResults.size())) { _filterResults[_filteredPressed].row->stopLastRipple(); } - if (_pressedBotAppData && _pressedBotAppData->ripple) { - _pressedBotAppData->ripple->lastStop(); + if (_pressedRightButtonData && _pressedRightButtonData->ripple) { + _pressedRightButtonData->ripple->lastStop(); } _filteredPressed = pressed; - if (pressed >= 0 || !pressedTopicJump || !pressedBotApp) { + if (pressed >= 0 || !pressedTopicJump || !pressedRightButton) { _pressedTopicJump = pressedTopicJump; - _pressedBotApp = pressedBotApp; - if (pressed >= 0 && pressedBotApp) { + _pressedRightButton = pressedRightButton; + if (pressed >= 0 && pressedRightButton) { const auto &row = _filterResults[pressed].row; if (const auto history = row->history()) { const auto it = _rightButtons.find(history->peer->id); if (it != _rightButtons.end()) { - _pressedBotAppData = &(it->second); + _pressedRightButtonData = &(it->second); } } } @@ -2497,11 +2571,26 @@ void InnerWidget::setFilteredPressed( } } -void InnerWidget::setPeerSearchPressed(int pressed) { - if (base::in_range(_peerSearchPressed, 0, _peerSearchResults.size())) { - _peerSearchResults[_peerSearchPressed]->row.stopLastRipple(); +void InnerWidget::setPeerSearchPressed(int pressed, bool pressedRightButton) { + if (_peerSearchPressed != pressed + || (pressed >= 0 && _pressedRightButton != pressedRightButton)) { + if (base::in_range(_peerSearchPressed, 0, _peerSearchResults.size())) { + _peerSearchResults[_peerSearchPressed]->row.stopLastRipple(); + } + if (_pressedRightButtonData && _pressedRightButtonData->ripple) { + _pressedRightButtonData->ripple->lastStop(); + } + _peerSearchPressed = pressed; + if (pressed >= 0 || !pressedRightButton) { + _pressedRightButton = pressedRightButton; + if (pressed >= 0 && pressedRightButton) { + const auto &entry = _peerSearchResults[pressed]; + if (entry->sponsored) { + _pressedRightButtonData = &entry->sponsored->button; + } + } + } } - _peerSearchPressed = pressed; } void InnerWidget::setPreviewPressed(int pressed) { @@ -2564,7 +2653,7 @@ void InnerWidget::dialogRowReplaced( _selected = newRow; } if (_pressed == oldRow) { - setPressed(newRow, _pressedTopicJump, _pressedBotApp); + setPressed(newRow, _pressedTopicJump, _pressedRightButton); } if (_dragging == oldRow) { if (newRow) { @@ -3081,6 +3170,62 @@ void InnerWidget::contextMenuEvent(QContextMenuEvent *e) { } } +void InnerWidget::showSponsoredMenu(int peerSearchIndex, QPoint globalPos) { + _menu = nullptr; + + const auto count = int(_peerSearchResults.size()); + const auto entry = (peerSearchIndex >= 0 && peerSearchIndex < count) + ? _peerSearchResults[peerSearchIndex].get() + : nullptr; + if (!entry || !entry->sponsored) { + return; + } + + _peerSearchMenu = peerSearchIndex; + _menu = base::make_unique_q( + this, + st::popupMenuExpandedSeparator); + const auto peer = entry->peer; + const auto remove = crl::guard(this, [=] { + _sponsoredRemoved.emplace(peer); + _peerSearchResults.erase( + ranges::remove( + _peerSearchResults, + peer, + &PeerSearchResult::peer), + end(_peerSearchResults)); + refresh(); + }); + Menu::FillSponsored( + this, + Ui::Menu::CreateAddActionCallback(_menu), + _controller->uiShow(), + Menu::SponsoredPhrases::Search, + session().sponsoredMessages().lookupDetails(entry->sponsored->data), + session().sponsoredMessages().createReportCallback( + entry->sponsored->data.randomId, + remove), + false, + false); + QObject::connect(_menu.get(), &QObject::destroyed, [=] { + if (_peerSearchMenu >= 0 + && _peerSearchMenu < _peerSearchResults.size()) { + const auto index = std::exchange(_peerSearchMenu, -1); + updateSearchResult(_peerSearchResults[index]->peer); + } + const auto globalPosition = QCursor::pos(); + if (rect().contains(mapFromGlobal(globalPosition))) { + setMouseTracking(true); + selectByMouse(globalPosition); + } + }); + if (_menu->empty()) { + _menu = nullptr; + } else { + _menu->popup(globalPos); + } +} + void InnerWidget::parentGeometryChanged() { const auto globalPosition = QCursor::pos(); if (rect().contains(mapFromGlobal(globalPosition))) { @@ -3362,14 +3507,23 @@ InnerWidget::~InnerWidget() { clearSearchResults(); } -void InnerWidget::clearSearchResults(bool clearPeerSearchResults) { - if (clearPeerSearchResults) { - _peerSearchResults.clear(); +void InnerWidget::clearSearchResults(bool alsoPeerSearchResults) { + if (alsoPeerSearchResults) { + clearPeerSearchResults(); } _searchResults.clear(); _searchedCount = _searchedMigratedCount = 0; } +void InnerWidget::clearPeerSearchResults() { + _peerSearchResults.clear(); + if (_pressedRightButtonSponsored) { + _pressedRightButtonData = nullptr; + _pressedRightButtonSponsored = false; + _pressedRightButton = false; + } +} + void InnerWidget::clearPreviewResults() { _previewResults.clear(); _previewCount = 0; @@ -3691,7 +3845,7 @@ void InnerWidget::peerSearchReceived(Api::PeerSearchResult result) { } _peerSearchQuery = result.query.toLower().trimmed(); - _peerSearchResults.clear(); + clearPeerSearchResults(); _peerSearchResults.reserve(result.peers.size() + result.sponsored.size()); for (const auto &peer : result.my) { @@ -3706,14 +3860,17 @@ void InnerWidget::peerSearchReceived(Api::PeerSearchResult result) { }; auto added = base::flat_set>(); for (const auto &sponsored : result.sponsored) { - if (inlist(sponsored.peer)) { + const auto peer = sponsored.peer; + if (inlist(peer) || _sponsoredRemoved.contains(peer)) { continue; } _peerSearchResults.push_back( - std::make_unique(sponsored.peer)); + std::make_unique(peer)); _peerSearchResults.back()->sponsored - = std::make_unique(sponsored); - added.emplace(sponsored.peer); + = std::make_unique(SponsoredSearchResult{ + .data = sponsored, + }); + added.emplace(peer); } for (const auto &peer : result.peers) { if (added.contains(peer) || inlist(peer)) { @@ -4054,7 +4211,7 @@ void InnerWidget::clearFilter() { _hashtagResults.clear(); _filterResults.clear(); _filterResultsGlobal.clear(); - _peerSearchResults.clear(); + clearPeerSearchResults(); _searchResults.clear(); _previewResults.clear(); _trackedHistories.clear(); @@ -4515,7 +4672,7 @@ ChosenRow InnerWidget::computeChosenRow() const { .key = session().data().history(row->peer), .message = Data::UnreadMessagePosition, .sponsoredRandomId = (row->sponsored - ? row->sponsored->randomId + ? row->sponsored->data.randomId : QByteArray()), }; } else if (base::in_range(_previewSelected, 0, _previewResults.size())) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 05cdfc9846..9842faf67a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace style { struct DialogRow; +struct DialogRightButton; } // namespace style namespace Api { @@ -242,6 +243,7 @@ protected: private: struct CollapsedRow; struct HashtagResult; + struct SponsoredSearchResult; struct PeerSearchResult; struct TagCache; @@ -299,6 +301,7 @@ private: void repaintDialogRow(RowDescriptor row); void refreshDialogRow(RowDescriptor row); bool updateEntryHeight(not_null entry); + void showSponsoredMenu(int peerSearchIndex, QPoint globalPos); void clearMouseSelection(bool clearSelection = false); void mousePressReleased( @@ -312,14 +315,17 @@ private: void scrollToItem(int top, int height); void scrollToDefaultSelected(); void setCollapsedPressed(int pressed); - void setPressed(Row *pressed, bool pressedTopicJump, bool pressedBotApp); + void setPressed( + Row *pressed, + bool pressedTopicJump, + bool pressedRightButton); void clearPressed(); void setHashtagPressed(int pressed); void setFilteredPressed( int pressed, bool pressedTopicJump, - bool pressedBotApp); - void setPeerSearchPressed(int pressed); + bool pressedRightButton); + void setPeerSearchPressed(int pressed, bool pressedRightButton); void setPreviewPressed(int pressed); void setSearchedPressed(int pressed); bool isPressed() const { @@ -357,6 +363,8 @@ private: bool addBotAppRipple(QPoint origin, Fn updateCallback); bool addQuickActionRipple(not_null row, Fn updateCallback); + bool addRightButtonRipple(QPoint origin, Fn updateCallback); + void setupShortcuts(); RowDescriptor computeJump( const RowDescriptor &to, @@ -459,7 +467,8 @@ private: Ui::VideoUserpic *validateVideoUserpic(not_null history); Row *shownRowByKey(Key key); - void clearSearchResults(bool clearPeerSearchResults = true); + void clearSearchResults(bool alsoPeerSearchResults = true); + void clearPeerSearchResults(); void clearPreviewResults(); void updateSelectedRow(Key key = Key()); void trackResultsHistory(not_null history); @@ -493,7 +502,14 @@ private: [[nodiscard]] bool lookupIsInBotAppButton( Row *row, QPoint localPosition); + [[nodiscard]] bool lookupIsInRightButton( + const RightButton &button, + QPoint localPosition); [[nodiscard]] RightButton *maybeCacheRightButton(Row *row); + void fillRightButton( + RightButton &button, + const TextWithEntities &text, + const style::DialogRightButton &st); [[nodiscard]] QImage *cacheChatsFilterTag( const Data::ChatFilter &filter, @@ -529,9 +545,10 @@ private: bool _selectedTopicJump = false; bool _pressedTopicJump = false; - RightButton *_pressedBotAppData = nullptr; - bool _selectedBotApp = false; - bool _pressedBotApp = false; + RightButton *_pressedRightButtonData = nullptr; + bool _pressedRightButtonSponsored = false; + bool _selectedRightButton = false; + bool _pressedRightButton = false; Row *_dragging = nullptr; int _draggingIndex = -1; @@ -566,9 +583,11 @@ private: rpl::lifetime _trackedLifetime; QString _peerSearchQuery; + base::flat_set> _sponsoredRemoved; std::vector> _peerSearchResults; int _peerSearchSelected = -1; int _peerSearchPressed = -1; + int _peerSearchMenu = -1; std::vector> _previewResults; int _previewCount = 0; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 9b1f0afe37..14cef4d46a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -835,7 +835,10 @@ void Widget::setupSwipeBack() { void Widget::chosenRow(const ChosenRow &row) { storiesToggleExplicitExpand(false); - if (!_searchState.query.isEmpty()) { + if (!row.sponsoredRandomId.isEmpty()) { + auto &messages = session().sponsoredMessages(); + messages.clicked(row.sponsoredRandomId, false, false); + } else if (!_searchState.query.isEmpty()) { if (const auto history = row.key.history()) { session().recentPeers().bump(history->peer); } @@ -846,11 +849,6 @@ void Widget::chosenRow(const ChosenRow &row) { ? history->peer->forumTopicFor(row.message.fullId.msg) : nullptr; - if (!row.sponsoredRandomId.isEmpty()) { - auto &messages = session().sponsoredMessages(); - messages.clicked(row.sponsoredRandomId, false, false); - } - if (topicJump) { if (controller()->shownForum().current() == topicJump->forum()) { controller()->closeForum(); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index dd8155f16b..2bcd9cb393 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -125,16 +125,18 @@ void PaintRowTopRight( text); } -int PaintRightButton(QPainter &p, const PaintContext &context) { +int PaintRightButtonImpl(QPainter &p, const PaintContext &context) { if (context.width < st::columnMinimalWidthLeft) { return 0; } if (const auto rightButton = context.rightButton) { + Assert(rightButton->st != nullptr); + const auto size = rightButton->bg.size() / style::DevicePixelRatio(); const auto left = context.width - size.width() - - st::dialogRowOpenBotRight; - const auto top = st::dialogRowOpenBotTop; + - rightButton->st->margin.right(); + const auto top = rightButton->st->margin.top(); p.drawImage( left, top, @@ -149,22 +151,22 @@ int PaintRightButton(QPainter &p, const PaintContext &context) { left, top, size.width() - size.height() / 2, - context.active + (context.active ? &st::universalRippleAnimation.color->c - : &st::activeButtonBgRipple->c); + : &rightButton->st->button.ripple.color->c)); if (rightButton->ripple->empty()) { rightButton->ripple.reset(); } } p.setPen(context.active - ? st::activeButtonBg + ? rightButton->st->button.textBg : context.selected - ? st::activeButtonFgOver - : st::activeButtonFg); + ? rightButton->st->button.textFgOver + : rightButton->st->button.textFg); rightButton->text.draw(p, { .position = QPoint( left + size.height() / 2, - top + (st::dialogRowOpenBotHeight - rightButton->text.minHeight()) / 2), + top + rightButton->st->button.textTop), .outerWidth = size.width() - size.height() / 2, .availableWidth = size.width() - size.height() / 2, .elisionLines = 1, @@ -1285,4 +1287,8 @@ void PaintCollapsedRow( } } +int PaintRightButton(QPainter &p, const PaintContext &context) { + return PaintRightButtonImpl(p, context); +} + } // namespace Dialogs::Ui diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h index 43122350ef..50002c4847 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h @@ -110,4 +110,6 @@ void PaintCollapsedRow( int unread, const PaintContext &context); +int PaintRightButton(QPainter &p, const PaintContext &context); + } // namespace Dialogs::Ui diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index 28511ebe55..4ae07e5a92 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -204,7 +204,7 @@ RecentRow::RecentRow(not_null peer) if (const auto user = peer->asUser()) { if (user->botInfo && user->botInfo->hasMainApp) { return std::make_unique( - st::dialogRowOpenBotTextStyle, + st::dialogRowOpenBotRecent.button.style, tr::lng_profile_open_app_short(tr::now)); } } @@ -275,20 +275,15 @@ QSize RecentRow::rightActionSize() const { if (_mainAppText && _badgeSize.isEmpty()) { return QSize( _mainAppText->maxWidth() + _mainAppText->minHeight(), - st::dialogRowOpenBotHeight); + st::dialogRowOpenBotRecent.button.height); } return _badgeSize; } QMargins RecentRow::rightActionMargins() const { if (_mainAppText && _badgeSize.isEmpty()) { - return QMargins( - 0, - st::dialogRowOpenBotRecentTop, - st::dialogRowOpenBotRight, - 0); - } - if (_badgeSize.isEmpty()) { + return st::dialogRowOpenBotRecent.margin; + } else if (_badgeSize.isEmpty()) { return {}; } const auto x = st::recentPeersItem.photoPosition.x(); @@ -321,8 +316,7 @@ void RecentRow::rightActionPaint( p.setPen(actionSelected ? st::activeButtonFgOver : st::activeButtonFg); - const auto top = 0 - + (st::dialogRowOpenBotHeight - _mainAppText->minHeight()) / 2; + const auto top = st::dialogRowOpenBotRecent.button.textTop; _mainAppText->draw(p, { .position = QPoint(x + size.height() / 2, y + top), .outerWidth = outerWidth, diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 1909f4943f..c0358488b4 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -699,7 +699,8 @@ ClickHandlerPtr HideSponsoredClickHandler() { if (session.premium()) { using Result = Data::SponsoredReportResult; session.sponsoredMessages().createReportCallback( - my.itemId)(Result::Id("-1"), [](const auto &) {}); + my.itemId + ).callback(Result::Id("-1"), [](const auto &) {}); } else { ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); } diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style b/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style index 5d68e501b0..86fa3772d8 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style +++ b/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style @@ -116,7 +116,7 @@ channelEarnFadeDuration: 60; channelEarnLearnDescription: FlatLabel(defaultFlatLabel) { maxHeight: 0px; - minWidth: 280px; + minWidth: 264px; align: align(top); } diff --git a/Telegram/SourceFiles/menu/menu_sponsored.cpp b/Telegram/SourceFiles/menu/menu_sponsored.cpp index 231da1ee00..0f431f538d 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.cpp +++ b/Telegram/SourceFiles/menu/menu_sponsored.cpp @@ -42,15 +42,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Menu { namespace { +[[nodiscard]] SponsoredPhrases PhrasesForMessage(FullMsgId fullId) { + return peerIsChannel(fullId.peer) + ? SponsoredPhrases::Channel + : SponsoredPhrases::Bot; +} + void AboutBox( not_null box, std::shared_ptr show, - const FullMsgId &fullId) { + SponsoredPhrases phrases, + const Data::SponsoredMessages::Details &details, + Data::SponsoredReportAction report) { constexpr auto kUrl = "https://promote.telegram.org"_cs; - box->setNoContentMargin(true); + box->setWidth(st::boxWideWidth); - const auto isChannel = peerIsChannel(fullId.peer); + const auto isChannel = (phrases == SponsoredPhrases::Channel); + const auto isSearch = (phrases == SponsoredPhrases::Search); const auto session = &show->session(); const auto content = box->verticalLayout().get(); @@ -138,20 +147,24 @@ void AboutBox( tr::lng_sponsored_revenued_info1_title(), (isChannel ? tr::lng_sponsored_revenued_info1_description + : isSearch + ? tr::lng_sponsored_revenued_info1_search_description : tr::lng_sponsored_revenued_info1_bot_description)( Ui::Text::RichLangValue), st::sponsoredAboutPrivacyIcon); - Ui::AddSkip(content); - Ui::AddSkip(content); - addEntry( - (isChannel - ? tr::lng_sponsored_revenued_info2_title - : tr::lng_sponsored_revenued_info2_bot_title)(), - (isChannel - ? tr::lng_sponsored_revenued_info2_description - : tr::lng_sponsored_revenued_info2_bot_description)( - Ui::Text::RichLangValue), - st::sponsoredAboutSplitIcon); + if (!isSearch) { + Ui::AddSkip(content); + Ui::AddSkip(content); + addEntry( + (isChannel + ? tr::lng_sponsored_revenued_info2_title + : tr::lng_sponsored_revenued_info2_bot_title)(), + (isChannel + ? tr::lng_sponsored_revenued_info2_description + : tr::lng_sponsored_revenued_info2_bot_description)( + Ui::Text::RichLangValue), + st::sponsoredAboutSplitIcon); + } Ui::AddSkip(content); Ui::AddSkip(content); auto link = tr::lng_settings_privacy_premium_link( @@ -160,17 +173,32 @@ void AboutBox( }); addEntry( tr::lng_sponsored_revenued_info3_title(), - isChannel + (isChannel ? tr::lng_sponsored_revenued_info3_description( lt_count, rpl::single(float64(levels)), lt_link, std::move(link), Ui::Text::RichLangValue) + : isSearch + ? tr::lng_sponsored_revenued_info3_search_description( + lt_link, + tr::lng_sponsored_revenued_info3_search_link( + lt_arrow, + rpl::single( + Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + Ui::Text::WithEntities + ) | rpl::map([](TextWithEntities &&link) { + return Ui::Text::Wrapped( + std::move(link), + EntityType::CustomUrl, + u"internal:"_q); + }), + Ui::Text::RichLangValue) : tr::lng_sponsored_revenued_info3_bot_description( lt_link, std::move(link), - Ui::Text::RichLangValue), + Ui::Text::RichLangValue)), st::sponsoredAboutRemoveIcon)->setClickHandlerFilter([=]( const auto &...) { ShowPremiumPreviewBox(show, PremiumFeature::NoAds); @@ -200,6 +228,8 @@ void AboutBox( content, (isChannel ? tr::lng_sponsored_revenued_footer_description + : isSearch + ? tr::lng_sponsored_revenued_footer_search_description : tr::lng_sponsored_revenued_footer_bot_description)( lt_link, tr::lng_channel_earn_about_link( @@ -256,7 +286,9 @@ void AboutBox( top, Ui::Menu::CreateAddActionCallback(menu->get()), show, - fullId, + phrases, + details, + report, false, true); const auto global = top->mapToGlobal( @@ -269,14 +301,11 @@ void AboutBox( return true; }); } - } void ShowReportSponsoredBox( std::shared_ptr show, - const FullMsgId &fullId) { - auto &sponsoredMessages = show->session().sponsoredMessages(); - const auto report = sponsoredMessages.createReportCallback(fullId); + Data::SponsoredReportAction report) { const auto guideLink = Ui::Text::Link( tr::lng_report_sponsored_reported_link(tr::now), u"https://promote.telegram.org/guidelines"_q); @@ -284,7 +313,7 @@ void ShowReportSponsoredBox( auto performRequest = [=]( const auto &repeatRequest, Data::SponsoredReportResult::Id id) -> void { - report(id, [=](const Data::SponsoredReportResult &result) { + report.callback(id, [=](const Data::SponsoredReportResult &result) { if (!result.error.isEmpty()) { show->showToast(result.error); } @@ -360,11 +389,12 @@ void FillSponsored( not_null parent, const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, - const FullMsgId &fullId, + SponsoredPhrases phrases, + const Data::SponsoredMessages::Details &details, + Data::SponsoredReportAction report, bool mediaViewer, bool skipAbout) { const auto session = &show->session(); - const auto details = session->sponsoredMessages().lookupDetails(fullId); const auto &info = details.info; if (!mediaViewer && !info.empty()) { @@ -408,12 +438,12 @@ void FillSponsored( if (details.canReport) { if (!skipAbout) { addAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] { - show->show(Box(AboutBox, show, fullId)); + show->show(Box(AboutBox, show, phrases, details, report)); }, (mediaViewer ? &st::mediaMenuIconInfo : &st::menuIconInfo)); } addAction(tr::lng_sponsored_menu_revenued_report(tr::now), [=] { - ShowReportSponsoredBox(show, fullId); + ShowReportSponsoredBox(show, report); }, (mediaViewer ? &st::mediaMenuIconBlock : &st::menuIconBlock)); addAction({ @@ -426,14 +456,32 @@ void FillSponsored( addAction(tr::lng_sponsored_hide_ads(tr::now), [=] { if (session->premium()) { using Result = Data::SponsoredReportResult; - session->sponsoredMessages().createReportCallback( - fullId)(Result::Id("-1"), [](const auto &) {}); + report.callback(Result::Id("-1"), [](const auto &) {}); } else { ShowPremiumPreviewBox(show, PremiumFeature::NoAds); } }, (mediaViewer ? &st::mediaMenuIconCancel : &st::menuIconCancel)); } +void FillSponsored( + not_null parent, + const Ui::Menu::MenuCallback &addAction, + std::shared_ptr show, + const FullMsgId &fullId, + bool mediaViewer, + bool skipAbout) { + const auto session = &show->session(); + FillSponsored( + parent, + addAction, + show, + PhrasesForMessage(fullId), + session->sponsoredMessages().lookupDetails(fullId), + session->sponsoredMessages().createReportCallback(fullId), + mediaViewer, + skipAbout); +} + void ShowSponsored( not_null parent, std::shared_ptr show, @@ -455,8 +503,14 @@ void ShowSponsored( void ShowSponsoredAbout( std::shared_ptr show, const FullMsgId &fullId) { + const auto session = &show->session(); show->showBox(Box([=](not_null box) { - AboutBox(box, show, fullId); + AboutBox( + box, + show, + PhrasesForMessage(fullId), + session->sponsoredMessages().lookupDetails(fullId), + session->sponsoredMessages().createReportCallback(fullId)); })); } diff --git a/Telegram/SourceFiles/menu/menu_sponsored.h b/Telegram/SourceFiles/menu/menu_sponsored.h index 4061271a98..106509e2e6 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.h +++ b/Telegram/SourceFiles/menu/menu_sponsored.h @@ -11,6 +11,11 @@ namespace ChatHelpers { class Show; } // namespace ChatHelpers +namespace Data { +struct SponsoredMessageDetails; +struct SponsoredReportAction; +} // namespace Data + namespace Ui { class RpWidget; namespace Menu { @@ -22,6 +27,22 @@ class HistoryItem; namespace Menu { +enum class SponsoredPhrases { + Channel, + Bot, + Search, +}; + +void FillSponsored( + not_null parent, + const Ui::Menu::MenuCallback &addAction, + std::shared_ptr show, + SponsoredPhrases phrases, + const Data::SponsoredMessageDetails &details, + Data::SponsoredReportAction report, + bool mediaViewer, + bool skipAbout); + void FillSponsored( not_null parent, const Ui::Menu::MenuCallback &addAction,