From 36a8c49213f0f9d4d82c5452f29b3fe43b6e39d7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 21 Nov 2023 13:31:38 +0400 Subject: [PATCH] Show similar channels under join message. --- Telegram/CMakeLists.txt | 2 + .../Resources/icons/chat/mini_subscribers.png | Bin 0 -> 261 bytes .../icons/chat/mini_subscribers@2x.png | Bin 0 -> 399 bytes .../icons/chat/mini_subscribers@3x.png | Bin 0 -> 557 bytes Telegram/Resources/langs/lang.strings | 3 + .../SourceFiles/api/api_chat_participants.cpp | 52 ++ .../SourceFiles/api/api_chat_participants.h | 15 + Telegram/SourceFiles/data/data_types.h | 2 + Telegram/SourceFiles/dialogs/dialogs_row.cpp | 58 +- Telegram/SourceFiles/dialogs/dialogs_row.h | 1 + .../dialogs/ui/dialogs_stories_list.h | 1 + .../history/history_inner_widget.cpp | 19 +- .../history/history_inner_widget.h | 2 + Telegram/SourceFiles/history/history_item.h | 3 + .../history/history_item_helpers.cpp | 2 +- .../SourceFiles/history/history_widget.cpp | 10 + .../view/history_view_cursor_state.cpp | 8 +- .../history/view/history_view_cursor_state.h | 7 +- .../history/view/history_view_element.cpp | 4 + .../history/view/history_view_element.h | 7 +- .../history/view/history_view_reply.cpp | 6 +- .../view/history_view_service_message.cpp | 39 +- .../view/history_view_service_message.h | 2 + .../history/view/media/history_view_game.cpp | 3 +- .../view/media/history_view_giveaway.cpp | 8 +- .../history/view/media/history_view_media.cpp | 12 +- .../history/view/media/history_view_media.h | 8 +- .../media/history_view_similar_channels.cpp | 496 ++++++++++++++++++ .../media/history_view_similar_channels.h | 98 ++++ Telegram/SourceFiles/ui/chat/chat.style | 20 + 30 files changed, 814 insertions(+), 74 deletions(-) create mode 100644 Telegram/Resources/icons/chat/mini_subscribers.png create mode 100644 Telegram/Resources/icons/chat/mini_subscribers@2x.png create mode 100644 Telegram/Resources/icons/chat/mini_subscribers@3x.png create mode 100644 Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp create mode 100644 Telegram/SourceFiles/history/view/media/history_view_similar_channels.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 64e68bf60..d85a5e306 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -713,6 +713,8 @@ PRIVATE history/view/media/history_view_premium_gift.h history/view/media/history_view_service_box.cpp history/view/media/history_view_service_box.h + history/view/media/history_view_similar_channels.cpp + history/view/media/history_view_similar_channels.h history/view/media/history_view_slot_machine.cpp history/view/media/history_view_slot_machine.h history/view/media/history_view_sticker.cpp diff --git a/Telegram/Resources/icons/chat/mini_subscribers.png b/Telegram/Resources/icons/chat/mini_subscribers.png new file mode 100644 index 0000000000000000000000000000000000000000..d9cac3f6324409282abc7e00215aa947ef6d9322 GIT binary patch literal 261 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1SD^YpWXnZ7>k44ofy`glX(f`xTHpSruq6Z zXaU(A42G$TlC0TW!7X8|*U4N@q~^D7oe<$Jm~hDb;z zCoB*ZU}LUXW^G~dhl$zU-94q$=Y&jod3l&YL2Aif~@Ca1cK~ z)A;$%&(GJ#?~mJAv~D~*dHBp3o|Y47oUT_7iTy85}Sb4q9e07-a5*8l(j literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/mini_subscribers@2x.png b/Telegram/Resources/icons/chat/mini_subscribers@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1e6a9f0540a5c7766a36b41b4efbc1434c2d431b GIT binary patch literal 399 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlfU{q%Hk46zV= z8)VDp;vlkZ^}>LcEaDFsxWWtMrnJTo3(~#Q;gbj&7vz> z_k90*_6cv_?$_zk6&w78SPpK>y&&ScQHMLa=7dz+#qWQs*C$H&T(+6AsdLx5K(1eA zzK5?}jtmX`C~_rxmRq=SK%hvXW`$S}gIwf1_aipv3uPYXOj2QZbmHo~=RXxV;;LH{ zIUa{TuYaxL`N>BA^rmgO#|zJVw&_xey}p0<*2fmH*B|<~{mPsbbA9iu)JU<9A6yo_ z|2=)vs#QvJj_g>sTIaNSWY;1AvsV>zEcV`!(+bqC@;}&?n>lM<%=2X{rGGHIc*#At RFQOP6Y@V)uF6*2UngGNok9+_C literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/mini_subscribers@3x.png b/Telegram/Resources/icons/chat/mini_subscribers@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..1e4a68bc70291cd64b1d339ca83305f5fe611b7a GIT binary patch literal 557 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1SIp4_|F3=#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz07S-^~7gA{7<3F~oy6 zIYEMTv7&(#(-uBPw@Eq)iHRS-e>XQaW`1Q=`|HcGUg`S(|9mHL9?=pL5n+k?AkR6+ zve?bTqa)()k=2hMKejG^Co{=4;8`oT_>Z4IJI&iQrlzK*&MAa_jc($CI3d)vp=p>FJrwEzna}SD({6-*HW9__~;bsTZ!U4lneasJU+O;?Iwd z^PkCJ=6dz&)uc=fxm-Cpxr}d&JLk=tSFt5PB{eyDbMfmnq{YsH>|R^X-97$Jei4RaI09G7l^3 zsj0Pji!D!;k&)T)<$>V;udl@|r!p9no|$9$`Rr_SmczX39-N%4&dJGHF~>;DREnut aorR&Sa4VmA%Hse~RC&7kxvX> ParseSimilar( + not_null channel, + const MTPmessages_Chats &chats) { + auto result = std::vector>(); + chats.match([&](const auto &data) { + const auto &list = data.vchats().v; + result.reserve(list.size()); + for (const auto &chat : list) { + const auto peer = channel->owner().processChat(chat); + if (const auto channel = peer->asChannel()) { + result.push_back(channel); + } + } + }); + return result; +} + } // namespace ChatParticipant::ChatParticipant( @@ -559,6 +576,7 @@ void ChatParticipants::requestSelf(not_null channel) { UserId inviter = -1, TimeId inviteDate = 0, bool inviteViaRequest = false) { + const auto dateChanged = (channel->inviteDate != inviteDate); channel->inviter = inviter; channel->inviteDate = inviteDate; channel->inviteViaRequest = inviteViaRequest; @@ -569,6 +587,9 @@ void ChatParticipants::requestSelf(not_null channel) { } else { history->owner().histories().requestDialogEntry(history); } + if (dateChanged) { + loadSimilarChannels(channel); + } } }; _selfParticipantRequests.emplace(channel); @@ -685,4 +706,35 @@ void ChatParticipants::unblock( _kickRequests.emplace(kick, requestId); } +void ChatParticipants::loadSimilarChannels(not_null channel) { + if (!channel->isBroadcast() || _similar.contains(channel)) { + return; + } + _similar[channel].requestId = _api.request( + MTPchannels_GetChannelRecommendations(channel->inputChannel) + ).done([=](const MTPmessages_Chats &result) { + _similar[channel] = { + .list = ParseSimilar(channel, result), + }; + _similarLoaded.fire_copy(channel); + }).send(); +} + +const std::vector> &ChatParticipants::similar( + not_null channel) { + const auto i = channel->isBroadcast() + ? _similar.find(channel) + : end(_similar); + if (i != end(_similar)) { + return i->second.list; + } + static const auto empty = std::vector>(); + return empty; +} + +auto ChatParticipants::similarLoaded() const +-> rpl::producer> { + return _similarLoaded.events(); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_chat_participants.h b/Telegram/SourceFiles/api/api_chat_participants.h index 41eda6fe6..7b7a536bf 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.h +++ b/Telegram/SourceFiles/api/api_chat_participants.h @@ -120,7 +120,19 @@ public: not_null channel, not_null participant); + [[nodiscard]] const std::vector> &similar( + not_null channel); + [[nodiscard]] auto similarLoaded() const + -> rpl::producer>; + private: + struct SimilarChannels { + std::vector> list; + mtpRequestId requestId = 0; + }; + + void loadSimilarChannels(not_null channel); + MTP::Sender _api; using PeerRequests = base::flat_map; @@ -143,6 +155,9 @@ private: not_null>; base::flat_map _kickRequests; + base::flat_map, SimilarChannels> _similar; + rpl::event_stream> _similarLoaded; + }; } // namespace Api diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 8029c5eeb..a0e95eea5 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -309,6 +309,8 @@ enum class MessageFlag : uint64 { // If not set then we need to refresh _displayFrom value. DisplayFromChecked = (1ULL << 40), + + ShowSimilarChannels = (1ULL << 41), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index a6459bbc4..a48cf6daa 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -37,6 +37,7 @@ namespace { constexpr auto kTopLayer = 2; constexpr auto kBottomLayer = 1; constexpr auto kNoneLayer = 0; +constexpr auto kBlurRadius = 24; [[nodiscard]] QImage CornerBadgeTTL( not_null peer, @@ -46,38 +47,17 @@ constexpr auto kNoneLayer = 0; if (!ttl) { return QImage(); } - constexpr auto kBlurRadius = 24; - const auto ratio = style::DevicePixelRatio(); const auto fullSize = photoSize; - const auto blurredFull = Images::BlurLargeImage( - peer->generateUserpicImage(view, fullSize * ratio, 0), - kBlurRadius); const auto partRect = CornerBadgeTTLRect(fullSize); const auto &partSize = partRect.width(); - auto result = [&] { - auto blurredPart = blurredFull.copy( - blurredFull.width() - partSize * ratio, - blurredFull.height() - partSize * ratio, - partSize * ratio, - partSize * ratio); - blurredPart.setDevicePixelRatio(ratio); - - constexpr auto kMinAcceptableContrast = 4.5; - const auto averageColor = Ui::CountAverageColor(blurredPart); - const auto contrast = Ui::CountContrast( - averageColor, - st::premiumButtonFg->c); - if (contrast < kMinAcceptableContrast) { - constexpr auto kDarkerBy = 0.2; - auto painterPart = QPainter(&blurredPart); - painterPart.setOpacity(kDarkerBy); - painterPart.fillRect( - QRect(QPoint(), partRect.size()), - Qt::black); - } - return Images::Circle(std::move(blurredPart)); - }(); + const auto partSkip = fullSize - partSize; + auto result = Images::Circle(BlurredDarkenedPart( + peer->generateUserpicImage(view, fullSize * ratio, 0), + QRect( + QPoint(partSkip, partSkip) * ratio, + QSize(partSize, partSize) * ratio))); + result.setDevicePixelRatio(ratio); auto q = QPainter(&result); PainterHighQualityEnabler hq(q); @@ -125,6 +105,28 @@ QRect CornerBadgeTTLRect(int photoSize) { partSize); } +QImage BlurredDarkenedPart(QImage image, QRect part) { + const auto ratio = style::DevicePixelRatio(); + auto blurred = Images::BlurLargeImage( + std::move(image), + kBlurRadius).copy(part); + + constexpr auto kMinAcceptableContrast = 4.5; + const auto averageColor = Ui::CountAverageColor(blurred); + const auto contrast = Ui::CountContrast( + averageColor, + st::premiumButtonFg->c); + if (contrast < kMinAcceptableContrast) { + constexpr auto kDarkerBy = 0.2; + auto painterPart = QPainter(&blurred); + painterPart.setOpacity(kDarkerBy); + painterPart.fillRect(QRect(QPoint(), part.size()), Qt::black); + } + + blurred.setDevicePixelRatio(image.devicePixelRatio()); + return blurred; +} + Row::CornerLayersManager::CornerLayersManager() = default; bool Row::CornerLayersManager::isSameLayer(Layer layer) const { diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h index eecbd9c4f..e814e5ff1 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.h +++ b/Telegram/SourceFiles/dialogs/dialogs_row.h @@ -39,6 +39,7 @@ class Entry; enum class SortMode; [[nodiscard]] QRect CornerBadgeTTLRect(int photoSize); +[[nodiscard]] QImage BlurredDarkenedPart(QImage image, QRect part); class BasicRow { public: diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 03d8b1e73..71d4aca42 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/rp_widget.h" +#include "ui/effects/animations.h" class QPainter; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 7605fa91a..9f22817fe 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -1428,7 +1428,8 @@ void HistoryInner::onTouchScrollTimer() { } else if (_touchScrollState == Ui::TouchScrollState::Auto || _touchScrollState == Ui::TouchScrollState::Acceleration) { int32 elapsed = int32(nowTime - _touchTime); QPoint delta = _touchSpeed * elapsed / 1000; - bool hasScrolled = _widget->touchScroll(delta); + bool hasScrolled = consumeScrollAction(delta) + || _widget->touchScroll(delta); if (_touchSpeed.isNull() || !hasScrolled) { _touchScrollState = Ui::TouchScrollState::Manual; @@ -1625,7 +1626,9 @@ void HistoryInner::mouseActionUpdate(const QPoint &screenPos) { void HistoryInner::touchScrollUpdated(const QPoint &screenPos) { _touchPos = screenPos; - _widget->touchScroll(_touchPos - _touchPrevPos); + if (!consumeScrollAction(_touchPos - _touchPrevPos)) { + _widget->touchScroll(_touchPos - _touchPrevPos); + } touchUpdateSpeed(); } @@ -3834,6 +3837,7 @@ void HistoryInner::mouseActionUpdate() { } Qt::CursorShape cur = style::cur_default; + _acceptsHorizontalScroll = dragState.horizontalScroll; if (_mouseAction == MouseAction::None) { _mouseCursorState = dragState.cursor; if (dragState.link) { @@ -4447,6 +4451,17 @@ void HistoryInner::onParentGeometryChanged() { } } +bool HistoryInner::consumeScrollAction(QPoint delta) { + const auto horizontal = std::abs(delta.x()) > std::abs(delta.y()); + if (!horizontal || !_acceptsHorizontalScroll || !Element::Moused()) { + return false; + } + const auto position = mapPointToItem( + mapFromGlobal(_mousePosition), + Element::Moused()); + return Element::Moused()->consumeHorizontalScroll(position, delta.x()); +} + Fn HistoryInner::elementDelegateFactory( FullMsgId itemId) const { const auto weak = base::make_weak(_controller); diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 810521f50..512186688 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -203,6 +203,7 @@ public: bool tooltipWindowActive() const override; void onParentGeometryChanged(); + bool consumeScrollAction(QPoint delta); [[nodiscard]] Fn elementDelegateFactory( FullMsgId itemId) const; @@ -490,6 +491,7 @@ private: bool _recountedAfterPendingResizedItems = false; bool _useCornerReaction = false; bool _canHaveFromUserpicsSponsored = false; + bool _acceptsHorizontalScroll = false; QPoint _trippleClickPoint; base::Timer _trippleClickTimer; diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index a5206bdcc..50141aa93 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -317,6 +317,9 @@ public: [[nodiscard]] bool isFakeBotAbout() const { return _flags & MessageFlag::FakeBotAbout; } + [[nodiscard]] bool showSimilarChannels() const { + return _flags & MessageFlag::ShowSimilarChannels; + } [[nodiscard]] bool isRegular() const; [[nodiscard]] bool isUploading() const; void sendFailed(); diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index a9f663c63..c972558aa 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -557,7 +557,7 @@ not_null GenerateJoinedMessage( bool viaRequest) { return history->makeMessage( history->owner().nextLocalMessageId(), - MessageFlag::Local, + MessageFlag::Local | MessageFlag::ShowSimilarChannels, inviteDate, GenerateJoinedText(history, inviter, viaRequest)); } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4c8fdf547..3084637ea 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -135,6 +135,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_theme.h" #include "ui/chat/chat_style.h" #include "ui/chat/continuous_scroll.h" +#include "ui/widgets/elastic_scroll.h" #include "ui/widgets/popup_menu.h" #include "ui/item_text_options.h" #include "main/main_session.h" @@ -271,6 +272,15 @@ HistoryWidget::HistoryWidget( update(); }, lifetime()); + base::install_event_filter(_scroll.data(), [=](not_null e) { + const auto consumed = (e->type() == QEvent::Wheel) + && _list + && _list->consumeScrollAction( + Ui::ScrollDelta(static_cast(e.get()))); + return consumed + ? base::EventFilterResult::Cancel + : base::EventFilterResult::Continue; + }); _scroll->scrolls( ) | rpl::start_with_next([=] { handleScroll(); diff --git a/Telegram/SourceFiles/history/view/history_view_cursor_state.cpp b/Telegram/SourceFiles/history/view/history_view_cursor_state.cpp index afbd264d8..56ca495c3 100644 --- a/Telegram/SourceFiles/history/view/history_view_cursor_state.cpp +++ b/Telegram/SourceFiles/history/view/history_view_cursor_state.cpp @@ -24,8 +24,8 @@ TextState::TextState( ? CursorState::Text : CursorState::None) , link(state.link) -, afterSymbol(state.afterSymbol) -, symbol(state.symbol) { +, symbol(state.symbol) +, afterSymbol(state.afterSymbol) { } TextState::TextState( @@ -59,8 +59,8 @@ TextState::TextState( ? CursorState::Text : CursorState::None) , link(state.link) -, afterSymbol(state.afterSymbol) -, symbol(state.symbol) { +, symbol(state.symbol) +, afterSymbol(state.afterSymbol) { } TextState::TextState(std::nullptr_t, ClickHandlerPtr link) diff --git a/Telegram/SourceFiles/history/view/history_view_cursor_state.h b/Telegram/SourceFiles/history/view/history_view_cursor_state.h index d5bcaa6f9..3ad78e533 100644 --- a/Telegram/SourceFiles/history/view/history_view_cursor_state.h +++ b/Telegram/SourceFiles/history/view/history_view_cursor_state.h @@ -50,10 +50,11 @@ struct TextState { FullMsgId itemId; CursorState cursor = CursorState::None; ClickHandlerPtr link; - bool overMessageText = false; - bool afterSymbol = false; - bool customTooltip = false; uint16 symbol = 0; + bool afterSymbol = false; + bool overMessageText = false; + bool customTooltip = false; + bool horizontalScroll = false; QString customTooltipText; }; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 5d35cceda..38af0db75 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_message.h" #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_media_grouped.h" +#include "history/view/media/history_view_similar_channels.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_large_emoji.h" #include "history/view/media/history_view_custom_emoji.h" @@ -36,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/reaction_fly_animation.h" +#include "ui/effects/ripple_animation.h" #include "ui/chat/chat_style.h" #include "ui/toast/toast.h" #include "ui/text/text_options.h" @@ -723,6 +725,8 @@ void Element::refreshMedia(Element *replacing) { } } _media = media->createView(this, replacing); + } else if (item->showSimilarChannels()) { + _media = std::make_unique(this); } else if (isOnlyCustomEmoji() && Core::App().settings().largeEmoji() && !item->isSponsored()) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 2ba36333a..a619f15de 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -515,6 +515,7 @@ public: const Reactions::InlineList &reactions) const; void clearCustomEmojiRepaint() const; void hideSpoilers(); + void repaint() const; [[nodiscard]] ClickHandlerPtr fromPhotoLink() const { return fromLink(); @@ -531,6 +532,10 @@ public: void overrideMedia(std::unique_ptr media); + virtual bool consumeHorizontalScroll(QPoint position, int delta) { + return false; + } + virtual ~Element(); static void Hovered(Element *view); @@ -546,8 +551,6 @@ public: static void ClearGlobal(); protected: - void repaint() const; - void paintHighlight( Painter &p, const PaintContext &context, diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index 3da6c9952..98cefe4d8 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -52,10 +52,8 @@ void ValidateBackgroundEmoji( } const auto tag = Data::CustomEmojiSizeTag::Isolated; if (!data->emoji) { + const auto repaint = crl::guard(view, [=] { view->repaint(); }); const auto owner = &view->history()->owner(); - const auto repaint = crl::guard(view, [=] { - view->history()->owner().requestViewRepaint(view); - }); data->emoji = owner->customEmojiManager().create( backgroundEmojiId, repaint, @@ -779,7 +777,7 @@ void Reply::createRippleAnimation( Ui::RippleAnimation::RoundRectMask( size, st::messageQuoteStyle.radius), - [=] { view->history()->owner().requestViewRepaint(view); }); + [=] { view->repaint(); }); } void Reply::saveRipplePoint(QPoint point) const { diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 8c2ed651d..4f6c04cd3 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -411,6 +411,13 @@ QRect Service::innerGeometry() const { return countGeometry(); } +bool Service::consumeHorizontalScroll(QPoint position, int delta) { + if (const auto media = this->media()) { + return media->consumeHorizontalScroll(position, delta); + } + return false; +} + QRect Service::countGeometry() const { auto result = QRect(0, 0, width(), height()); if (delegate()->elementIsChatWide()) { @@ -429,7 +436,8 @@ QSize Service::performCountCurrentSize(int newWidth) { return { newWidth, newHeight }; } const auto media = this->media(); - if (media && media->hideServiceText()) { + const auto mediaDisplayed = media && media->isDisplayed(); + if (mediaDisplayed && media->hideServiceText()) { newHeight += st::msgServiceMargin.top() + media->resizeGetHeight(newWidth) + st::msgServiceMargin.bottom(); @@ -448,8 +456,10 @@ QSize Service::performCountCurrentSize(int newWidth) { ? minHeight() : textHeightFor(nwidth); newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom(); - if (media) { - newHeight += st::msgServiceMargin.top() + media->resizeGetHeight(media->maxWidth()); + if (mediaDisplayed) { + const auto mediaWidth = std::min(media->maxWidth(), nwidth); + newHeight += st::msgServiceMargin.top() + + media->resizeGetHeight(mediaWidth); } } @@ -527,10 +537,11 @@ void Service::draw(Painter &p, const PaintContext &context) const { p.setTextPalette(st->serviceTextPalette()); const auto media = this->media(); - const auto onlyMedia = (media && media->hideServiceText()); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto onlyMedia = (mediaDisplayed && media->hideServiceText()); if (!onlyMedia) { - if (media) { + if (mediaDisplayed) { height -= margin.top() + media->height(); } const auto trect = QRect(g.left(), margin.top(), g.width(), height) @@ -561,8 +572,8 @@ void Service::draw(Painter &p, const PaintContext &context) const { .fullWidthSelection = false, }); } - if (media) { - const auto left = margin.left() + (g.width() - media->maxWidth()) / 2; + if (mediaDisplayed) { + const auto left = margin.left() + (g.width() - media->width()) / 2; const auto top = margin.top() + (onlyMedia ? 0 : (height + margin.top())); p.translate(left, top); media->draw(p, context.translated(-left, -top).withSelection({})); @@ -576,6 +587,7 @@ void Service::draw(Painter &p, const PaintContext &context) const { PointState Service::pointState(QPoint point) const { const auto media = this->media(); + const auto mediaDisplayed = media && media->isDisplayed(); auto g = countGeometry(); if (g.width() < 1 || isHidden()) { @@ -588,7 +600,7 @@ PointState Service::pointState(QPoint point) const { if (const auto bar = Get()) { g.setTop(g.top() + bar->height()); } - if (media) { + if (mediaDisplayed) { const auto centerPadding = (g.width() - media->width()) / 2; const auto r = g - QMargins(centerPadding, 0, centerPadding, 0); if (!r.contains(point)) { @@ -602,7 +614,8 @@ PointState Service::pointState(QPoint point) const { TextState Service::textState(QPoint point, StateRequest request) const { const auto item = data(); const auto media = this->media(); - const auto onlyMedia = (media && media->hideServiceText()); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto onlyMedia = (mediaDisplayed && media->hideServiceText()); auto result = TextState(item); @@ -622,8 +635,8 @@ TextState Service::textState(QPoint point, StateRequest request) const { } if (onlyMedia) { - return media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->maxWidth()) / 2, st::msgServiceMargin.top()), request); - } else if (media) { + return media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->width()) / 2, st::msgServiceMargin.top()), request); + } else if (mediaDisplayed) { g.setHeight(g.height() - (st::msgServiceMargin.top() + media->height())); } auto trect = g.marginsAdded(-st::msgServicePadding); @@ -656,8 +669,8 @@ TextState Service::textState(QPoint point, StateRequest request) const { result.link = same->lnk; } } - } else if (media) { - result = media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->maxWidth()) / 2, st::msgServiceMargin.top() + g.height() + st::msgServiceMargin.top()), request); + } else if (mediaDisplayed) { + result = media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->width()) / 2, st::msgServiceMargin.top() + g.height() + st::msgServiceMargin.top()), request); } return result; } diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.h b/Telegram/SourceFiles/history/view/history_view_service_message.h index 30f422981..76bf73e02 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.h +++ b/Telegram/SourceFiles/history/view/history_view_service_message.h @@ -52,6 +52,8 @@ public: QRect innerGeometry() const override; + bool consumeHorizontalScroll(QPoint position, int delta) override; + private: [[nodiscard]] QRect countGeometry() const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_game.cpp b/Telegram/SourceFiles/history/view/media/history_view_game.cpp index 24ba5187c..57dd72167 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_game.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_game.cpp @@ -413,13 +413,12 @@ void Game::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { if (!_ripple) { const auto full = QRect(0, 0, width(), height()); const auto outer = full.marginsRemoved(inBubblePadding()); - const auto owner = &parent()->history()->owner(); _ripple = std::make_unique( st::defaultRippleAnimation, Ui::RippleAnimation::RoundRectMask( outer.size(), _st.radius), - [=] { owner->requestViewRepaint(parent()); }); + [=] { repaint(); }); } _ripple->add(_lastPoint); } else if (_ripple) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp index f65613ea3..81dbf0684 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp @@ -14,7 +14,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_document.h" #include "data/data_media_types.h" -#include "data/data_session.h" #include "dialogs/ui/dialogs_stories_content.h" #include "dialogs/ui/dialogs_stories_list.h" #include "history/history.h" @@ -348,9 +347,7 @@ void Giveaway::paintChannels( const auto &thumbnail = channel.thumbnail; const auto &geometry = channel.geometry; if (!_subscribedToThumbnails) { - thumbnail->subscribeToUpdates([view = parent()] { - view->history()->owner().requestViewRepaint(view); - }); + thumbnail->subscribeToUpdates([=] { repaint(); }); } const auto colorIndex = channel.colorIndex; @@ -487,13 +484,12 @@ void Giveaway::clickHandlerPressedChanged( } if (pressed) { if (!channel.ripple) { - const auto owner = &parent()->history()->owner(); channel.ripple = std::make_unique( st::defaultRippleAnimation, Ui::RippleAnimation::RoundRectMask( channel.geometry.size(), channel.geometry.height() / 2), - [=] { owner->requestViewRepaint(parent()); }); + [=] { repaint(); }); } channel.ripple->add(_lastPoint - channel.geometry.topLeft()); } else if (channel.ripple) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index 8f9923c36..048c43218 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -198,10 +198,6 @@ SelectedQuote Media::selectedQuote(TextSelection selection) const { return {}; } -bool Media::isDisplayed() const { - return true; -} - QSize Media::countCurrentSize(int newWidth) { return QSize(qMin(newWidth, maxWidth()), minHeight()); } @@ -285,7 +281,7 @@ void Media::fillImageSpoiler( void Media::createSpoilerLink(not_null spoiler) { const auto weak = base::make_weak(this); - spoiler->link = std::make_shared([=]( + spoiler->link = std::make_shared([weak, spoiler]( const ClickContext &context) { const auto button = context.button; const auto media = weak.get(); @@ -295,15 +291,15 @@ void Media::createSpoilerLink(not_null spoiler) { const auto view = media->parent(); spoiler->revealed = true; spoiler->revealAnimation.start([=] { - media->history()->owner().requestViewRepaint(view); + view->repaint(); }, 0., 1., st::fadeWrapDuration); - media->history()->owner().requestViewRepaint(view); + view->repaint(); media->history()->owner().registerShownSpoiler(view); }); } void Media::repaint() const { - history()->owner().requestViewRepaint(_parent); + _parent->repaint(); } Ui::Text::String Media::createCaption(not_null item) const { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index b8c04d0b5..1b835dcce 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -96,7 +96,9 @@ public: return {}; } - [[nodiscard]] virtual bool isDisplayed() const; + [[nodiscard]] virtual bool isDisplayed() const { + return true; + } virtual void updateNeedBubbleState() { } [[nodiscard]] virtual bool hasTextForCopy() const { @@ -335,6 +337,10 @@ public: virtual void parentTextUpdated() { } + virtual bool consumeHorizontalScroll(QPoint position, int delta) { + return false; + } + virtual ~Media() = default; protected: diff --git a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp new file mode 100644 index 000000000..01ef1d782 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp @@ -0,0 +1,496 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/media/history_view_similar_channels.h" + +#include "api/api_chat_participants.h" +#include "apiwrap.h" +#include "boxes/peer_lists_box.h" +#include "core/click_handler_types.h" +#include "data/data_channel.h" +#include "data/data_session.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" +#include "history/view/history_view_element.h" +#include "history/view/history_view_cursor_state.h" +#include "history/history.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" + +namespace HistoryView { +namespace { + +class SimilarChannelsController final : public PeerListController { +public: + SimilarChannelsController( + not_null controller, + std::vector> channels); + + void prepare() override; + void loadMoreRows() override; + void rowClicked(not_null row) override; + Main::Session &session() const override; + +private: + const not_null _controller; + const std::vector> _channels; + +}; + +SimilarChannelsController::SimilarChannelsController( + not_null controller, + std::vector> channels) +: _controller(controller) +, _channels(std::move(channels)) { +} + +void SimilarChannelsController::prepare() { + for (const auto &channel : _channels) { + auto row = std::make_unique(channel); + if (const auto count = channel->membersCount(); count > 1) { + row->setCustomStatus(tr::lng_chat_status_subscribers( + tr::now, + lt_count, + count)); + } + delegate()->peerListAppendRow(std::move(row)); + } + delegate()->peerListRefreshRows(); +} + +void SimilarChannelsController::loadMoreRows() { +} + +void SimilarChannelsController::rowClicked(not_null row) { + const auto other = ClickHandlerContext{ + .sessionWindow = _controller, + .show = _controller->uiShow(), + }; + row->peer()->openLink()->onClick({ + Qt::LeftButton, + QVariant::fromValue(other) + }); +} + +Main::Session &SimilarChannelsController::session() const { + return _channels.front()->session(); +} + +[[nodiscard]] object_ptr SimilarChannelsBox( + not_null controller, + const std::vector> &channels) { + const auto initBox = [=](not_null box) { + box->setTitle(tr::lng_similar_channels_title()); + box->addButton(tr::lng_close(), [=] { box->closeBox(); }); + }; + return Box( + std::make_unique(controller, channels), + initBox); +} + +} // namespace + +SimilarChannels::SimilarChannels(not_null parent) +: Media(parent) { +} + +SimilarChannels::~SimilarChannels() { + if (hasHeavyPart()) { + unloadHeavyPart(); + parent()->checkHeavyPart(); + } +} + +void SimilarChannels::clickHandlerActiveChanged( + const ClickHandlerPtr &p, + bool active) { +} + +void SimilarChannels::clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) { + for (auto &channel : _channels) { + if (channel.link != p) { + continue; + } + if (pressed) { + if (!channel.ripple) { + channel.ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + channel.geometry.size(), + st::roundRadiusLarge), + [=] { repaint(); }); + } + channel.ripple->add(_lastPoint); + } else if (channel.ripple) { + channel.ripple->lastStop(); + } + break; + } +} + +void SimilarChannels::draw(Painter &p, const PaintContext &context) const { + const auto large = Ui::BubbleCornerRounding::Large; + const auto geometry = QRect(0, 0, width(), height()); + Ui::PaintBubble( + p, + Ui::SimpleBubble{ + .st = context.st, + .geometry = geometry, + .pattern = context.bubblesPattern, + .patternViewport = context.viewport, + .outerWidth = width(), + .rounding = { large, large, large, large }, + }); + const auto stm = context.messageStyle(); + { + auto hq = PainterHighQualityEnabler(p); + auto path = QPainterPath(); + const auto x = geometry.center().x(); + const auto y = geometry.y(); + const auto size = st::chatSimilarArrowSize; + path.moveTo(x, y - size); + path.lineTo(x + size, y); + path.lineTo(x - size, y); + path.lineTo(x, y - size); + p.fillPath(path, stm->msgBg); + } + const auto photo = st::chatSimilarChannelPhoto; + const auto padding = st::chatSimilarChannelPadding; + p.setClipRect(geometry); + _hasHeavyPart = 1; + const auto drawOne = [&](const Channel &channel) { + const auto geometry = channel.geometry.translated(-_scrollLeft, 0); + const auto right = geometry.x() + geometry.width(); + if (right <= 0) { + return; + } + if (!channel.subscribed) { + channel.subscribed = true; + const auto raw = channel.thumbnail.get(); + const auto view = parent(); + channel.thumbnail->subscribeToUpdates([=] { + for (const auto &channel : _channels) { + if (channel.thumbnail.get() == raw) { + channel.participantsBgValid = false; + repaint(); + } + } + }); + } + auto cachedp = std::optional(); + const auto cached = (geometry.x() < padding.left()) + || (right > width() - padding.right()); + if (cached) { + ensureCacheReady(geometry.size()); + _roundedCache.fill(Qt::transparent); + cachedp.emplace(&_roundedCache); + cachedp->translate(-geometry.topLeft()); + } + const auto q = cachedp ? &*cachedp : &p; + if (channel.ripple) { + q->setOpacity(st::historyPollRippleOpacity); + channel.ripple->paint( + *q, + geometry.x(), + geometry.y(), + width(), + &stm->msgWaveformInactive->c); + if (channel.ripple->empty()) { + channel.ripple.reset(); + } + q->setOpacity(1.); + } + q->drawImage( + geometry.x() + padding.left(), + geometry.y() + padding.top(), + channel.thumbnail->image(st::chatSimilarChannelPhoto)); + if (!channel.participants.isEmpty()) { + validateParticipansBg(channel); + const auto participants = channel.participantsRect.translated( + QPoint(-_scrollLeft, 0)); + q->drawImage(participants.topLeft(), channel.participantsBg); + const auto badge = participants.marginsRemoved( + st::chatSimilarBadgePadding); + const auto &icon = st::chatSimilarBadgeIcon; + const auto &font = st::chatSimilarBadgeFont; + const auto position = st::chatSimilarBadgeIconPosition; + const auto ascent = font->ascent; + icon.paint(*q, badge.topLeft() + position, width()); + q->setFont(font); + q->setPen(st::premiumButtonFg); + q->drawText( + badge.x() + position.x() + icon.width(), + badge.y() + font->ascent, + channel.participants); + } + q->setPen(stm->historyTextFg); + channel.name.drawLeftElided( + *q, + geometry.x() + st::normalFont->spacew, + geometry.y() + st::chatSimilarNameTop, + (geometry.width() - 2 * st::normalFont->spacew), + width(), + 2, + style::al_top); + if (cachedp) { + q->setCompositionMode(QPainter::CompositionMode_DestinationIn); + const auto corners = _roundedCorners.data(); + const auto side = st::bubbleRadiusLarge; + q->drawImage(0, 0, corners[Images::kTopLeft]); + q->drawImage(width() - side, 0, corners[Images::kTopRight]); + q->drawImage(0, height() - side, corners[Images::kBottomLeft]); + q->drawImage( + QPoint(width() - side, height() - side), + corners[Images::kBottomRight]); + cachedp.reset(); + p.drawImage(geometry.topLeft(), _roundedCache); + } + }; + for (const auto &channel : _channels) { + if (channel.geometry.x() >= _scrollLeft + width()) { + break; + } + drawOne(channel); + } + p.setFont(st::chatSimilarTitle); + p.drawTextLeft( + st::chatSimilarTitlePosition.x(), + st::chatSimilarTitlePosition.y(), + width(), + _title); + if (!_hasViewAll) { + return; + } + p.setFont(ClickHandler::showAsActive(_viewAllLink) + ? st::normalFont->underline() + : st::normalFont); + p.setPen(stm->textPalette.linkFg); + const auto add = st::normalFont->ascent - st::chatSimilarTitle->ascent; + p.drawTextRight( + st::chatSimilarTitlePosition.x(), + st::chatSimilarTitlePosition.y() + add, + width(), + _viewAll); + p.setClipping(false); +} + +void SimilarChannels::validateParticipansBg(const Channel &channel) const { + if (channel.participantsBgValid) { + return; + } + channel.participantsBgValid = true; + const auto photo = st::chatSimilarChannelPhoto; + const auto width = channel.participantsRect.width(); + const auto height = channel.participantsRect.height(); + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + channel.participantsRect.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + auto color = Ui::CountAverageColor( + channel.thumbnail->image(photo).copy( + QRect(photo / 3, photo / 3, photo / 3, photo / 3))); + + const auto lightness = color.lightness(); + if (!base::in_range(lightness, 160, 208)) { + color = color.toHsl(); + color.setHsl( + color.hue(), + color.saturation(), + std::clamp(lightness, 160, 208)); + color = color.toRgb(); + } + + result.fill(color); + result.setDevicePixelRatio(ratio); + const auto radius = height / 2; + auto corners = Images::CornersMask(radius); + auto p = QPainter(&result); + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + p.drawImage(0, 0, corners[Images::kTopLeft]); + p.drawImage(width - radius, 0, corners[Images::kTopRight]); + p.drawImage(0, height - radius, corners[Images::kBottomLeft]); + p.drawImage( + width - radius, + height - radius, + corners[Images::kBottomRight]); + p.end(); + channel.participantsBg = std::move(result); +} + +void SimilarChannels::ensureCacheReady(QSize size) const { + const auto ratio = style::DevicePixelRatio(); + if (_roundedCache.size() != size * ratio) { + _roundedCache = QImage( + size * ratio, + QImage::Format_ARGB32_Premultiplied); + _roundedCache.setDevicePixelRatio(ratio); + } + const auto radius = st::bubbleRadiusLarge; + if (_roundedCorners.front().size() != QSize(radius, radius) * ratio) { + _roundedCorners = Images::CornersMask(radius); + } +} + +TextState SimilarChannels::textState( + QPoint point, + StateRequest request) const { + auto result = TextState(); + result.horizontalScroll = (_scrollMax > 0); + const auto skip = st::chatSimilarTitlePosition; + const auto viewWidth = _hasViewAll ? (_viewAllWidth + 2 * skip.x()) : 0; + const auto viewHeight = st::normalFont->height + 2 * skip.y(); + const auto viewLeft = width() - viewWidth; + if (QRect(viewLeft, 0, viewWidth, viewHeight).contains(point)) { + if (!_viewAllLink) { + const auto channel = parent()->history()->peer->asChannel(); + Assert(channel != nullptr); + _viewAllLink = std::make_shared([=]( + ClickContext context) { + Assert(channel != nullptr); + const auto api = &channel->session().api(); + const auto &list = api->chatParticipants().similar(channel); + if (list.empty()) { + return; + } + const auto my = context.other.value(); + if (const auto strong = my.sessionWindow.get()) { + strong->show(SimilarChannelsBox(strong, list)); + } + }); + } + result.link = _viewAllLink; + return result; + } + for (const auto &channel : _channels) { + if (channel.geometry.translated(-_scrollLeft, 0).contains(point)) { + result.link = channel.link; + _lastPoint = point + + QPoint(_scrollLeft, 0) + - channel.geometry.topLeft(); + break; + } + } + return result; +} + +QSize SimilarChannels::countOptimalSize() { + const auto channel = parent()->history()->peer->asChannel(); + Assert(channel != nullptr); + + _channels.clear(); + const auto api = &channel->session().api(); + const auto similar = api->chatParticipants().similar(channel); + if (similar.empty()) { + return {}; + } + + _channels.reserve(similar.size()); + auto x = st::chatSimilarPadding.left(); + auto y = st::chatSimilarPadding.top(); + const auto skip = st::chatSimilarSkip; + const auto photo = st::chatSimilarChannelPhoto; + const auto inner = QRect(0, 0, photo, photo); + const auto outer = inner.marginsAdded(st::chatSimilarChannelPadding); + for (const auto &channel : similar) { + const auto participants = channel->membersCount(); + const auto count = (participants > 1) + ? Lang::FormatCountToShort(participants).string + : QString(); + _channels.push_back({ + .geometry = QRect(QPoint(x, y), outer.size()), + .name = Ui::Text::String( + st::chatSimilarName, + channel->name(), + kDefaultTextOptions, + st::chatSimilarChannelPhoto), + .thumbnail = Dialogs::Stories::MakeUserpicThumbnail(channel), + .link = channel->openLink(), + .participants = count, + }); + if (!count.isEmpty()) { + const auto length = st::chatSimilarBadgeFont->width(count); + const auto width = length + st::chatSimilarBadgeIcon.width(); + const auto delta = (outer.width() - width) / 2; + const auto badge = QRect( + x + delta, + y + st::chatSimilarBadgeTop, + outer.width() - 2 * delta, + st::chatSimilarBadgeFont->height); + _channels.back().participantsRect = badge.marginsAdded( + st::chatSimilarBadgePadding); + } + x += outer.width() + skip; + } + _title = tr::lng_similar_channels_title(tr::now); + _titleWidth = st::chatSimilarTitle->width(_title); + _viewAll = tr::lng_similar_channels_view_all(tr::now); + _viewAllWidth = st::normalFont->width(_viewAll); + const auto count = int(_channels.size()); + const auto desired = (count ? (x - skip) : x) + - st::chatSimilarPadding.left(); + const auto full = QRect(0, 0, desired, outer.height()); + const auto bubble = full.marginsAdded(st::chatSimilarPadding); + _fullWidth = bubble.width(); + const auto titleSkip = st::chatSimilarTitlePosition.x(); + const auto min = _titleWidth + 2 * titleSkip; + const auto limited = std::max( + std::min(_fullWidth, st::chatSimilarWidthMax), + min); + if (limited > _fullWidth) { + const auto shift = (limited - _fullWidth) / 2; + for (auto &channel : _channels) { + channel.geometry.translate(shift, 0); + } + } + return { limited, bubble.height() }; +} + +QSize SimilarChannels::countCurrentSize(int newWidth) { + _scrollMax = std::max(_fullWidth - newWidth, 0); + _scrollLeft = std::clamp(_scrollLeft, uint32(), _scrollMax); + _hasViewAll = (_scrollMax != 0) ? 1 : 0; + return { newWidth, minHeight() }; +} + +bool SimilarChannels::hasHeavyPart() const { + return _hasHeavyPart != 0; +} + +void SimilarChannels::unloadHeavyPart() { + _hasHeavyPart = 0; + for (const auto &channel : _channels) { + channel.subscribed = false; + channel.thumbnail->subscribeToUpdates(nullptr); + } +} + +bool SimilarChannels::consumeHorizontalScroll(QPoint position, int delta) { + if (_scrollMax == 0) { + return false; + } + const auto left = _scrollLeft; + _scrollLeft = std::clamp( + int(_scrollLeft) - delta, + 0, + int(_scrollMax)); + if (_scrollLeft == left) { + return false; + } + repaint(); + return true; +} + +} // namespace HistoryView \ No newline at end of file diff --git a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.h b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.h new file mode 100644 index 000000000..594f86bb2 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.h @@ -0,0 +1,98 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "history/view/media/history_view_media.h" + +namespace Dialogs::Stories { +class Thumbnail; +} // namespace Dialogs::Stories + +namespace Ui { +class RippleAnimation; +} // namespace Ui + +namespace HistoryView { + +class SimilarChannels final : public Media { +public: + explicit SimilarChannels(not_null parent); + ~SimilarChannels(); + + void draw(Painter &p, const PaintContext &context) const override; + TextState textState(QPoint point, StateRequest request) const override; + + void clickHandlerActiveChanged( + const ClickHandlerPtr &p, + bool active) override; + void clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) override; + + bool toggleSelectionByHandlerClick( + const ClickHandlerPtr &p) const override { + return false; + } + bool dragItemByHandler(const ClickHandlerPtr &p) const override { + return false; + } + + bool needsBubble() const override { + return false; + } + bool customInfoLayout() const override { + return true; + } + bool isDisplayed() const override { + return !_channels.empty(); + } + + void unloadHeavyPart() override; + bool hasHeavyPart() const override; + + bool consumeHorizontalScroll(QPoint position, int delta) override; + +private: + using Thumbnail = Dialogs::Stories::Thumbnail; + struct Channel { + QRect geometry; + Ui::Text::String name; + std::shared_ptr thumbnail; + ClickHandlerPtr link; + QString participants; + QRect participantsRect; + mutable QImage participantsBg; + mutable std::unique_ptr ripple; + mutable bool subscribed = false; + mutable bool participantsBgValid = false; + }; + + void ensureCacheReady(QSize size) const; + void validateParticipansBg(const Channel &channel) const; + + QSize countOptimalSize() override; + QSize countCurrentSize(int newWidth) override; + + QString _title, _viewAll; + mutable QImage _roundedCache; + mutable std::array _roundedCorners; + mutable QPoint _lastPoint; + int _titleWidth = 0; + int _viewAllWidth = 0; + int _fullWidth = 0; + uint32 _scrollLeft : 15 = 0; + uint32 _scrollMax : 15 = 0; + uint32 _hasViewAll : 1 = 0; + mutable uint32 _hasHeavyPart : 1 = 0; + + std::vector _channels; + mutable ClickHandlerPtr _viewAllLink; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index f0062eff2..43ea6aa4b 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -977,3 +977,23 @@ chatGiveawayCountriesSkip: 16px; chatGiveawayDateTop: 6px; chatGiveawayDateSkip: 4px; chatGiveawayBottomSkip: 16px; + +chatSimilarRadius: 12px; +chatSimilarArrowSize: 6px; +chatSimilarTitle: semiboldFont; +chatSimilarTitlePosition: point(15px, 9px); +chatSimilarPadding: margins(8px, 32px, 8px, 4px); +chatSimilarChannelPadding: margins(8px, 5px, 8px, 37px); +chatSimilarChannelPhoto: 50px; +chatSimilarBadgePadding: margins(2px, 0px, 3px, 1px); +chatSimilarBadgeTop: 43px; +chatSimilarBadgeIcon: icon{{ "chat/mini_subscribers", premiumButtonFg }}; +chatSimilarBadgeIconPosition: point(0px, 1px); +chatSimilarBadgeFont: font(10px bold); +chatSimilarNameTop: 59px; +chatSimilarName: TextStyle(defaultTextStyle) { + font: font(12px); + lineHeight: 14px; +} +chatSimilarWidthMax: 424px; +chatSimilarSkip: 12px;