diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 24b675f10..82573c229 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1682,6 +1682,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_similar_channels_title" = "Similar channels"; "lng_similar_channels_view_all" = "View all"; +"lng_similar_channels_more" = "More Channels"; +"lng_similar_channels_premium_all#one" = "Subscribe to {link} to unlock up to **{count}** similar channel."; +"lng_similar_channels_premium_all#other" = "Subscribe to {link} to unlock up to **{count}** similar channels."; +"lng_similar_channels_premium_all_link" = "Telegram Premium"; "lng_premium_gift_duration_months#one" = "for {count} month"; "lng_premium_gift_duration_months#other" = "for {count} months"; diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index ff3285aa5..35e9e2055 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -211,19 +211,24 @@ void ApplyBotsList( Data::PeerUpdate::Flag::FullInfo); } -[[nodiscard]] std::vector> ParseSimilar( +[[nodiscard]] ChatParticipants::Channels ParseSimilar( not_null channel, const MTPmessages_Chats &chats) { - auto result = std::vector>(); + auto result = ChatParticipants::Channels(); + std::vector>(); + auto total = 0; chats.match([&](const auto &data) { const auto &list = data.vchats().v; - result.reserve(list.size()); + result.list.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); + result.list.push_back(channel); } } + if constexpr (MTPDmessages_chatsSlice::Is()) { + result.more = data.vcount().v - data.vchats().v.size(); + } }); return result; } @@ -704,18 +709,25 @@ void ChatParticipants::unblock( } void ChatParticipants::loadSimilarChannels(not_null channel) { - if (!channel->isBroadcast() || _similar.contains(channel)) { + if (!channel->isBroadcast()) { return; + } else if (const auto i = _similar.find(channel); i != end(_similar)) { + if (i->second.requestId + || !i->second.channels.more + || !channel->session().premium()) { + return; + } } _similar[channel].requestId = _api.request( MTPchannels_GetChannelRecommendations(channel->inputChannel) ).done([=](const MTPmessages_Chats &result) { auto &similar = _similar[channel]; - auto list = ParseSimilar(channel, result); - if (similar.list == list) { + similar.requestId = 0; + auto parsed = ParseSimilar(channel, result); + if (similar.channels == parsed) { return; } - similar.list = std::move(list); + similar.channels = std::move(parsed); if (const auto history = channel->owner().historyLoaded(channel)) { if (const auto item = history->joinedMessageInstance()) { history->owner().requestItemResize(item); @@ -725,15 +737,15 @@ void ChatParticipants::loadSimilarChannels(not_null channel) { }).send(); } -const std::vector> &ChatParticipants::similar( - not_null channel) { +auto ChatParticipants::similar(not_null channel) +-> const Channels & { const auto i = channel->isBroadcast() ? _similar.find(channel) : end(_similar); if (i != end(_similar)) { - return i->second.list; + return i->second.channels; } - static const auto empty = std::vector>(); + static const auto empty = Channels(); return empty; } diff --git a/Telegram/SourceFiles/api/api_chat_participants.h b/Telegram/SourceFiles/api/api_chat_participants.h index 6140a5b3d..816cc6526 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.h +++ b/Telegram/SourceFiles/api/api_chat_participants.h @@ -122,14 +122,21 @@ public: void loadSimilarChannels(not_null channel); - [[nodiscard]] const std::vector> &similar( - not_null channel); + struct Channels { + std::vector> list; + int more = 0; + + friend inline bool operator==( + const Channels &, + const Channels &) = default; + }; + [[nodiscard]] const Channels &similar(not_null channel); [[nodiscard]] auto similarLoaded() const -> rpl::producer>; private: struct SimilarChannels { - std::vector> list; + Channels channels; mtpRequestId requestId = 0; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp index ea5b1771a..db20e3ae6 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp @@ -20,10 +20,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_app_config.h" #include "main/main_session.h" +#include "settings/settings_premium.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/effects/ripple_animation.h" +#include "ui/text/text_utilities.h" #include "ui/painter.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" @@ -31,11 +35,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { namespace { +using Channels = Api::ChatParticipants::Channels; + class SimilarChannelsController final : public PeerListController { public: SimilarChannelsController( not_null controller, - std::vector> channels); + Channels channels); void prepare() override; void loadMoreRows() override; @@ -44,19 +50,19 @@ public: private: const not_null _controller; - const std::vector> _channels; + const Channels _channels; }; SimilarChannelsController::SimilarChannelsController( not_null controller, - std::vector> channels) + Channels channels) : _controller(controller) , _channels(std::move(channels)) { } void SimilarChannelsController::prepare() { - for (const auto &channel : _channels) { + for (const auto &channel : _channels.list) { auto row = std::make_unique(channel); if (const auto count = channel->membersCount(); count > 1) { row->setCustomStatus(tr::lng_chat_status_subscribers( @@ -84,12 +90,12 @@ void SimilarChannelsController::rowClicked(not_null row) { } Main::Session &SimilarChannelsController::session() const { - return _channels.front()->session(); + return _channels.list.front()->session(); } [[nodiscard]] object_ptr SimilarChannelsBox( not_null controller, - const std::vector> &channels) { + const Channels &channels) { const auto initBox = [=](not_null box) { box->setTitle(tr::lng_similar_channels_title()); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); @@ -99,6 +105,43 @@ Main::Session &SimilarChannelsController::session() const { initBox); } +[[nodiscard]] ClickHandlerPtr MakeViewAllLink( + not_null channel, + bool promoForNonPremium) { + return std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + if (const auto strong = my.sessionWindow.get()) { + Assert(channel != nullptr); + if (promoForNonPremium && !channel->session().premium()) { + const auto account = &channel->session().account(); + const auto upto = account->appConfig().get( + u"recommended_channels_limit_premium"_q, + 100); + Settings::ShowPremiumPromoToast( + strong->uiShow(), + tr::lng_similar_channels_premium_all( + tr::now, + lt_count, + upto, + lt_link, + Ui::Text::Link( + Ui::Text::Bold( + tr::lng_similar_channels_premium_all_link( + tr::now))), + Ui::Text::RichLangValue), + u"similar_channels"_q); + return; + } + const auto api = &channel->session().api(); + const auto &list = api->chatParticipants().similar(channel); + if (list.list.empty()) { + return; + } + strong->show(SimilarChannelsBox(strong, list)); + } + }); +} + } // namespace SimilarChannels::SimilarChannels(not_null parent) @@ -180,14 +223,15 @@ void SimilarChannels::draw(Painter &p, const PaintContext &context) const { if (right <= 0) { return; } - if (!channel.subscribed) { - channel.subscribed = true; + const auto subscribing = !channel.subscribed; + if (subscribing) { + channel.subscribed = 1; 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; + channel.counterBgValid = 0; repaint(); } } @@ -203,7 +247,9 @@ void SimilarChannels::draw(Painter &p, const PaintContext &context) const { cachedp->translate(-geometry.topLeft()); } const auto q = cachedp ? &*cachedp : &p; - if (channel.ripple) { + if (channel.more) { + channel.ripple.reset(); + } else if (channel.ripple) { q->setOpacity(st::historyPollRippleOpacity); channel.ripple->paint( *q, @@ -216,30 +262,87 @@ void SimilarChannels::draw(Painter &p, const PaintContext &context) const { } q->setOpacity(1.); } + + auto pen = stm->msgBg->p; + auto left = geometry.x() + 2 * padding.left(); + const auto stroke = st::lineWidth * 2.; + const auto add = stroke / 2.; + const auto top = geometry.y() + padding.top(); + const auto size = st::chatSimilarChannelPhoto; + const auto paintCircle = [&] { + auto hq = PainterHighQualityEnabler(*q); + q->drawEllipse(QRectF(left, top, size, size).marginsAdded( + { add, add, add, add })); + }; + if (channel.more) { + pen.setWidthF(stroke); + p.setPen(pen); + for (auto i = 2; i != 0;) { + --i; + if (const auto &thumbnail = _moreThumbnails[i]) { + if (subscribing) { + thumbnail->subscribeToUpdates([=] { + repaint(); + }); + } + q->drawImage(left, top, thumbnail->image(size)); + q->setBrush(Qt::NoBrush); + } else { + q->setBrush(st::windowBgRipple->c); + } + if (!i || !_moreThumbnails[i]) { + paintCircle(); + } + left -= padding.left(); + } + } else { + left -= padding.left(); + } 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( + left, + top, + channel.thumbnail->image(size)); + if (channel.more) { + q->setBrush(Qt::NoBrush); + paintCircle(); + } + if (!channel.counter.isEmpty()) { + validateCounterBg(channel); + const auto participants = channel.counterRect.translated( geometry.topLeft()); - q->drawImage(participants.topLeft(), channel.participantsBg); + q->drawImage(participants.topLeft(), channel.counterBg); const auto badge = participants.marginsRemoved( st::chatSimilarBadgePadding); - const auto &icon = st::chatSimilarBadgeIcon; + auto textLeft = badge.x(); const auto &font = st::chatSimilarBadgeFont; - const auto position = st::chatSimilarBadgeIconPosition; const auto ascent = font->ascent; - icon.paint(*q, badge.topLeft() + position, width()); + const auto textTop = badge.y() + font->ascent; + const auto icon = !channel.more + ? &st::chatSimilarBadgeIcon + : channel.moreLocked + ? &st::chatSimilarLockedIcon + : nullptr; + const auto position = !channel.more + ? st::chatSimilarBadgeIconPosition + : st::chatSimilarLockedIconPosition; + if (icon) { + const auto skip = channel.more + ? (badge.width() - icon->width()) + : 0; + icon->paint( + *q, + badge.x() + position.x() + skip, + badge.y() + position.y(), + width()); + if (!channel.more) { + textLeft += position.x() + icon->width(); + } + } q->setFont(font); q->setPen(st::premiumButtonFg); - q->drawText( - badge.x() + position.x() + icon.width(), - badge.y() + font->ascent, - channel.participants); + q->drawText(textLeft, textTop, channel.counter); } - q->setPen(stm->historyTextFg); + q->setPen(channel.more ? st::windowSubTextFg : stm->historyTextFg); channel.name.drawLeftElided( *q, geometry.x() + st::normalFont->spacew, @@ -268,6 +371,7 @@ void SimilarChannels::draw(Painter &p, const PaintContext &context) const { } drawOne(channel); } + p.setPen(stm->historyTextFg); p.setFont(st::chatSimilarTitle); p.drawTextLeft( st::chatSimilarTitlePosition.x(), @@ -290,21 +394,23 @@ void SimilarChannels::draw(Painter &p, const PaintContext &context) const { p.setClipping(false); } -void SimilarChannels::validateParticipansBg(const Channel &channel) const { - if (channel.participantsBgValid) { +void SimilarChannels::validateCounterBg(const Channel &channel) const { + if (channel.counterBgValid) { return; } - channel.participantsBgValid = true; + channel.counterBgValid = 1; const auto photo = st::chatSimilarChannelPhoto; - const auto width = channel.participantsRect.width(); - const auto height = channel.participantsRect.height(); + const auto width = channel.counterRect.width(); + const auto height = channel.counterRect.height(); const auto ratio = style::DevicePixelRatio(); auto result = QImage( - channel.participantsRect.size() * ratio, + channel.counterRect.size() * ratio, QImage::Format_ARGB32_Premultiplied); - auto color = Ui::CountAverageColor( - channel.thumbnail->image(photo).copy( - QRect(photo / 3, photo / 3, photo / 3, photo / 3))); + auto color = channel.more + ? st::windowBgRipple->c + : Ui::CountAverageColor( + channel.thumbnail->image(photo).copy( + QRect(photo / 3, photo / 3, photo / 3, photo / 3))); const auto hsl = color.toHsl(); constexpr auto kMinSaturation = 0; @@ -334,7 +440,7 @@ void SimilarChannels::validateParticipansBg(const Channel &channel) const { height - radius, corners[Images::kBottomRight]); p.end(); - channel.participantsBg = std::move(result); + channel.counterBg = std::move(result); } ClickHandlerPtr SimilarChannels::ensureToggleLink() const { @@ -385,19 +491,7 @@ TextState SimilarChannels::textState( 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)); - } - }); + _viewAllLink = MakeViewAllLink(channel, false); } result.link = _viewAllLink; return result; @@ -419,53 +513,86 @@ QSize SimilarChannels::countOptimalSize() { Assert(channel != nullptr); _channels.clear(); + _moreThumbnails = {}; const auto api = &channel->session().api(); api->chatParticipants().loadSimilarChannels(channel); + const auto premium = channel->session().premium(); const auto similar = api->chatParticipants().similar(channel); - _empty = similar.empty() ? 1 : 0; + _empty = similar.list.empty() ? 1 : 0; using Flag = ChannelDataFlag; _toggled = (channel->flags() & Flag::SimilarExpanded) ? 1 : 0; if (_empty || !_toggled) { return {}; } - _channels.reserve(similar.size()); + _channels.reserve(similar.list.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(); + const auto limit = channel->session().account().appConfig().get( + u"recommended_channels_limit_default"_q, + 10); + const auto take = (similar.more > 0 || similar.list.size() > 2 * limit) + ? limit + : int(similar.list.size()); + const auto more = similar.more + int(similar.list.size() - take); + auto &&channels = ranges::views::all(similar.list) + | ranges::views::take(limit); + for (const auto &channel : channels) { + const auto moreCounter = (_channels.size() + 1 == take) ? more : 0; _channels.push_back({ .geometry = QRect(QPoint(x, y), outer.size()), .name = Ui::Text::String( st::chatSimilarName, - channel->name(), + (moreCounter + ? tr::lng_similar_channels_more(tr::now) + : channel->name()), kDefaultTextOptions, st::chatSimilarChannelPhoto), .thumbnail = Dialogs::Stories::MakeUserpicThumbnail(channel), - .link = channel->openLink(), - .participants = count, + .more = uint32(moreCounter), + .moreLocked = uint32((moreCounter && !premium) ? 1 : 0), }); - if (!count.isEmpty()) { - const auto length = st::chatSimilarBadgeFont->width(count); - const auto width = length + st::chatSimilarBadgeIcon.width(); + auto &last = _channels.back(); + last.link = moreCounter + ? MakeViewAllLink(parent()->history()->peer->asChannel(), true) + : channel->openLink(); + + const auto counter = moreCounter + ? moreCounter : + channel->membersCount(); + if (moreCounter || counter > 1) { + const auto text = (moreCounter ? u"+"_q : QString()) + + Lang::FormatCountToShort(counter).string; + const auto length = st::chatSimilarBadgeFont->width(text); + const auto width = length + + (!moreCounter + ? st::chatSimilarBadgeIcon.width() + : !premium + ? st::chatSimilarLockedIcon.width() + : 0); const auto delta = (outer.width() - width) / 2; const auto badge = QRect( delta, st::chatSimilarBadgeTop, outer.width() - 2 * delta, st::chatSimilarBadgeFont->height); - _channels.back().participantsRect = badge.marginsAdded( + last.counter = text; + last.counterRect = badge.marginsAdded( st::chatSimilarBadgePadding); } x += outer.width() + skip; } + for (auto i = 0, count = int(_moreThumbnails.size()); i != count; ++i) { + if (similar.list.size() <= _channels.size() + i) { + break; + } + _moreThumbnails[i] = Dialogs::Stories::MakeUserpicThumbnail( + similar.list[_channels.size() + i]); + } _title = tr::lng_similar_channels_title(tr::now); _titleWidth = st::chatSimilarTitle->width(_title); _viewAll = tr::lng_similar_channels_view_all(tr::now); @@ -507,9 +634,14 @@ bool SimilarChannels::hasHeavyPart() const { void SimilarChannels::unloadHeavyPart() { _hasHeavyPart = 0; for (const auto &channel : _channels) { - channel.subscribed = false; + channel.subscribed = 0; channel.thumbnail->subscribeToUpdates(nullptr); } + for (const auto &thumbnail : _moreThumbnails) { + if (thumbnail) { + thumbnail->subscribeToUpdates(nullptr); + } + } } bool SimilarChannels::consumeHorizontalScroll(QPoint position, int delta) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.h b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.h index 2b5ce0edb..b77caff90 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.h +++ b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.h @@ -64,16 +64,18 @@ private: Ui::Text::String name; std::shared_ptr thumbnail; ClickHandlerPtr link; - QString participants; - QRect participantsRect; - mutable QImage participantsBg; + QString counter; + QRect counterRect; + mutable QImage counterBg; mutable std::unique_ptr ripple; - mutable bool subscribed = false; - mutable bool participantsBgValid = false; + uint32 more : 29 = 0; + uint32 moreLocked : 1 = 0; + mutable uint32 subscribed : 1 = 0; + mutable uint32 counterBgValid : 1 = 0; }; void ensureCacheReady(QSize size) const; - void validateParticipansBg(const Channel &channel) const; + void validateCounterBg(const Channel &channel) const; [[nodiscard]] ClickHandlerPtr ensureToggleLink() const; QSize countOptimalSize() override; @@ -94,6 +96,7 @@ private: mutable uint32 _hasHeavyPart : 1 = 0; std::vector _channels; + std::array, 2> _moreThumbnails; mutable ClickHandlerPtr _viewAllLink; mutable ClickHandlerPtr _toggleLink; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 2385895f5..2fef5dfa0 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -991,6 +991,8 @@ chatSimilarBadgePadding: margins(2px, 0px, 3px, 1px); chatSimilarBadgeTop: 43px; chatSimilarBadgeIcon: icon{{ "chat/mini_subscribers", premiumButtonFg }}; chatSimilarBadgeIconPosition: point(0px, 1px); +chatSimilarLockedIcon: icon{{ "emoji/premium_lock", premiumButtonFg }}; +chatSimilarLockedIconPosition: point(0px, -1px); chatSimilarBadgeFont: font(10px bold); chatSimilarNameTop: 59px; chatSimilarName: TextStyle(defaultTextStyle) {