/* 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 "info/statistics/info_statistics_list_controllers.h" #include "api/api_statistics.h" #include "boxes/peer_list_controllers.h" #include "data/data_channel.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" #include "history/history_item.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/effects/outline_segments.h" // Ui::UnreadStoryOutlineGradient. #include "ui/effects/toggle_arrow.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "styles/style_dialogs.h" // dialogsStoriesFull. #include "styles/style_menu_icons.h" #include "styles/style_settings.h" #include "styles/style_statistics.h" #include "styles/style_window.h" namespace Info::Statistics { namespace { using BoostCallback = Fn; constexpr auto kColorIndexUnclaimed = int(3); constexpr auto kColorIndexPending = int(4); void AddArrow(not_null parent) { const auto arrow = Ui::CreateChild(parent.get()); arrow->paintRequest( ) | rpl::start_with_next([=](const QRect &r) { auto p = QPainter(arrow); const auto path = Ui::ToggleUpDownArrowPath( st::statisticsShowMoreButtonArrowSize, st::statisticsShowMoreButtonArrowSize, st::statisticsShowMoreButtonArrowSize, st::mainMenuToggleFourStrokes, 0.); auto hq = PainterHighQualityEnabler(p); p.fillPath(path, st::lightButtonFg); }, arrow->lifetime()); arrow->resize(Size(st::statisticsShowMoreButtonArrowSize * 2)); arrow->move(st::statisticsShowMoreButtonArrowPosition); arrow->show(); } void AddSubtitle( not_null container, rpl::producer title) { const auto &subtitlePadding = st::settingsButton.padding; Ui::AddSubsectionTitle( container, std::move(title), { 0, -subtitlePadding.top(), 0, -subtitlePadding.bottom() }); } [[nodiscard]] QString FormatText( int value1, tr::phrase phrase1, int value2, tr::phrase phrase2, int value3, tr::phrase phrase3) { const auto separator = u", "_q; auto resultText = QString(); if (value1 > 0) { resultText += phrase1(tr::now, lt_count, value1); } if (value2 > 0) { if (!resultText.isEmpty()) { resultText += separator; } resultText += phrase2(tr::now, lt_count, value2); } if (value3 > 0) { if (!resultText.isEmpty()) { resultText += separator; } resultText += phrase3(tr::now, lt_count, value3); } return resultText; } struct PublicForwardsDescriptor final { Data::PublicForwardsSlice firstSlice; Fn requestShow; not_null peer; Data::RecentPostId contextId; }; struct MembersDescriptor final { not_null session; Fn)> showPeerInfo; Data::SupergroupStatistics data; }; struct BoostsDescriptor final { Data::BoostsListSlice firstSlice; BoostCallback boostClickedCallback; not_null peer; }; class PeerListRowWithFullId : public PeerListRow { public: PeerListRowWithFullId( not_null peer, Data::RecentPostId contextId); [[nodiscard]] PaintRoundImageCallback generatePaintUserpicCallback( bool) override; [[nodiscard]] Data::RecentPostId contextId() const; private: const Data::RecentPostId _contextId; }; PeerListRowWithFullId::PeerListRowWithFullId( not_null peer, Data::RecentPostId contextId) : PeerListRow(peer) , _contextId(contextId) { } PaintRoundImageCallback PeerListRowWithFullId::generatePaintUserpicCallback( bool forceRound) { if (!_contextId.storyId) { return PeerListRow::generatePaintUserpicCallback(forceRound); } const auto peer = PeerListRow::peer(); auto userpic = PeerListRow::ensureUserpicView(); const auto line = st::dialogsStoriesFull.lineTwice; const auto penWidth = line / 2.; const auto offset = 1.5 * penWidth * 2; return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { const auto rect = QRect(QPoint(x, y), Size(size)); peer->paintUserpicLeft( p, userpic, x + offset, y + offset, outerWidth, size - offset * 2); auto hq = PainterHighQualityEnabler(p); auto gradient = Ui::UnreadStoryOutlineGradient(); gradient.setStart(rect.topRight()); gradient.setFinalStop(rect.bottomLeft()); p.setPen(QPen(gradient, penWidth)); p.setBrush(Qt::NoBrush); p.drawEllipse(rect - Margins(penWidth)); }; } Data::RecentPostId PeerListRowWithFullId::contextId() const { return _contextId; } class MembersController final : public PeerListController { public: MembersController(MembersDescriptor d); Main::Session &session() const override; void prepare() override; void rowClicked(not_null row) override; void loadMoreRows() override; void setLimit(int limit); private: void addRows(int from, int to); const not_null _session; Fn)> _showPeerInfo; Data::SupergroupStatistics _data; int _limit = 0; }; MembersController::MembersController(MembersDescriptor d) : _session(std::move(d.session)) , _showPeerInfo(std::move(d.showPeerInfo)) , _data(std::move(d.data)) { } Main::Session &MembersController::session() const { return *_session; } void MembersController::setLimit(int limit) { addRows(_limit, limit); _limit = limit; } void MembersController::addRows(int from, int to) { const auto addRow = [&](UserId userId, QString text) { const auto user = _session->data().user(userId); auto row = std::make_unique(user); row->setCustomStatus(std::move(text)); delegate()->peerListAppendRow(std::move(row)); }; if (!_data.topSenders.empty()) { for (auto i = from; i < to; i++) { const auto &member = _data.topSenders[i]; addRow( member.userId, FormatText( member.sentMessageCount, tr::lng_stats_member_messages, member.averageCharacterCount, tr::lng_stats_member_characters, 0, {})); } } else if (!_data.topAdministrators.empty()) { for (auto i = from; i < to; i++) { const auto &admin = _data.topAdministrators[i]; addRow( admin.userId, FormatText( admin.deletedMessageCount, tr::lng_stats_member_deletions, admin.bannedUserCount, tr::lng_stats_member_bans, admin.restrictedUserCount, tr::lng_stats_member_restrictions)); } } else if (!_data.topInviters.empty()) { for (auto i = from; i < to; i++) { const auto &inviter = _data.topInviters[i]; addRow( inviter.userId, FormatText( inviter.addedMemberCount, tr::lng_stats_member_invitations, 0, {}, 0, {})); } } } void MembersController::prepare() { } void MembersController::loadMoreRows() { } void MembersController::rowClicked(not_null row) { crl::on_main([=, peer = row->peer()] { _showPeerInfo(peer); }); } class PublicForwardsController final : public PeerListController { public: explicit PublicForwardsController(PublicForwardsDescriptor d); Main::Session &session() const override; void prepare() override; void rowClicked(not_null row) override; void loadMoreRows() override; base::unique_qptr rowContextMenu( QWidget *parent, not_null row) override; private: void appendRow(not_null peer, Data::RecentPostId contextId); void applySlice(const Data::PublicForwardsSlice &slice); const not_null _session; Fn _requestShow; Api::PublicForwards _api; Data::PublicForwardsSlice _firstSlice; Data::PublicForwardsSlice::OffsetToken _apiToken; bool _allLoaded = false; }; PublicForwardsController::PublicForwardsController(PublicForwardsDescriptor d) : _session(&d.peer->session()) , _requestShow(std::move(d.requestShow)) , _api(d.peer->asChannel(), d.contextId) , _firstSlice(std::move(d.firstSlice)) { } Main::Session &PublicForwardsController::session() const { return *_session; } void PublicForwardsController::prepare() { applySlice(base::take(_firstSlice)); delegate()->peerListRefreshRows(); } void PublicForwardsController::loadMoreRows() { if (_allLoaded) { return; } _api.request(_apiToken, [=](const Data::PublicForwardsSlice &slice) { applySlice(slice); }); } void PublicForwardsController::applySlice( const Data::PublicForwardsSlice &slice) { _allLoaded = slice.allLoaded; _apiToken = slice.token; for (const auto &item : slice.list) { if (const auto &full = item.messageId) { if (const auto peer = session().data().peerLoaded(full.peer)) { appendRow(peer, item); } } else if (const auto &full = item.storyId) { if (const auto story = session().data().stories().lookup(full)) { appendRow((*story)->peer(), item); } } } delegate()->peerListRefreshRows(); } void PublicForwardsController::rowClicked(not_null row) { const auto rowWithId = static_cast(row.get()); crl::on_main([=, id = rowWithId->contextId()] { _requestShow(id); }); } base::unique_qptr PublicForwardsController::rowContextMenu( QWidget *parent, not_null row) { auto menu = base::make_unique_q( parent, st::popupMenuWithIcons); const auto peer = row->peer(); const auto text = (peer->isChat() || peer->isMegagroup()) ? tr::lng_context_view_group(tr::now) : peer->isUser() ? tr::lng_context_view_profile(tr::now) : peer->isChannel() ? tr::lng_context_view_channel(tr::now) : QString(); if (text.isEmpty()) { return nullptr; } menu->addAction(text, crl::guard(parent, [=, peerId = peer->id] { _requestShow({ .messageId = { peerId, MsgId() } }); }), peer->isUser() ? &st::menuIconProfile : &st::menuIconInfo); return menu; } void PublicForwardsController::appendRow( not_null peer, Data::RecentPostId contextId) { if (delegate()->peerListFindRow(peer->id.value)) { return; } auto row = std::make_unique(peer, contextId); const auto members = peer->isChannel() ? peer->asChannel()->membersCount() : 0; const auto views = [&] { if (contextId.messageId) { const auto message = peer->owner().message(contextId.messageId); return message ? message->viewsCount() : 0; } else if (const auto &id = contextId.storyId) { const auto story = peer->owner().stories().lookup(id); return story ? (*story)->views() : 0; } return 0; }(); const auto membersText = !members ? QString() : peer->isMegagroup() ? tr::lng_chat_status_members(tr::now, lt_count_decimal, members) : tr::lng_chat_status_subscribers(tr::now, lt_count_decimal, members); const auto viewsText = views ? tr::lng_stats_recent_messages_views({}, lt_count_decimal, views) : QString(); const auto resultText = (membersText.isEmpty() && viewsText.isEmpty()) ? tr::lng_stories_no_views(tr::now) : (membersText.isEmpty() || viewsText.isEmpty()) ? membersText + viewsText : QString("%1, %2").arg(membersText, viewsText); row->setCustomStatus(resultText); delegate()->peerListAppendRow(std::move(row)); return; } class BoostRow final : public PeerListRow { public: BoostRow(not_null peer, const Data::Boost &boost); BoostRow(const Data::Boost &boost); [[nodiscard]] const Data::Boost &boost() const; [[nodiscard]] QString generateName() override; [[nodiscard]] PaintRoundImageCallback generatePaintUserpicCallback( bool forceRound) override; int paintNameIconGetWidth( Painter &p, Fn repaint, crl::time now, int nameLeft, int nameTop, int nameWidth, int availableWidth, int outerWidth, bool selected) override; QSize rightActionSize() const override; QMargins rightActionMargins() const override; bool rightActionDisabled() const override; void rightActionPaint( Painter &p, int x, int y, int outerWidth, bool selected, bool actionSelected) override; private: void init(); void invalidateBadges(); const Data::Boost _boost; Ui::EmptyUserpic _userpic; QImage _badge; QImage _rightBadge; }; BoostRow::BoostRow(not_null peer, const Data::Boost &boost) : PeerListRow(peer, UniqueRowIdFromString(boost.id)) , _boost(boost) , _userpic(Ui::EmptyUserpic::UserpicColor(0), QString()) { init(); } BoostRow::BoostRow(const Data::Boost &boost) : PeerListRow(UniqueRowIdFromString(boost.id)) , _boost(boost) , _userpic( Ui::EmptyUserpic::UserpicColor(boost.isUnclaimed ? kColorIndexUnclaimed : kColorIndexPending), QString()) { init(); } void BoostRow::init() { invalidateBadges(); auto status = !PeerListRow::special() ? tr::lng_boosts_list_status( tr::now, lt_date, langDayOfMonth(_boost.expiresAt.date())) : tr::lng_months_tiny(tr::now, lt_count, _boost.expiresAfterMonths) + ' ' + QChar(0x2022) + ' ' + langDayOfMonth(_boost.date.date()); PeerListRow::setCustomStatus(std::move(status)); } const Data::Boost &BoostRow::boost() const { return _boost; } QString BoostRow::generateName() { return !PeerListRow::special() ? PeerListRow::generateName() : _boost.isUnclaimed ? tr::lng_boosts_list_unclaimed(tr::now) : tr::lng_boosts_list_pending(tr::now); } PaintRoundImageCallback BoostRow::generatePaintUserpicCallback(bool force) { if (!PeerListRow::special()) { return PeerListRow::generatePaintUserpicCallback(force); } return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { _userpic.paintCircle(p, x, y, outerWidth, size); (_boost.isUnclaimed ? st::boostsListUnclaimedIcon : st::boostsListUnknownIcon).paintInCenter( p, { x, y, size, size }); }; } void BoostRow::invalidateBadges() { _badge = _boost.multiplier ? CreateBadge( st::statisticsDetailsBottomCaptionStyle, QString::number(_boost.multiplier), st::boostsListBadgeHeight, st::boostsListBadgeTextPadding, st::premiumButtonBg2, st::premiumButtonFg, 1., st::boostsListMiniIconPadding, st::boostsListMiniIcon) : QImage(); constexpr auto kBadgeBgOpacity = 0.2; const auto &rightColor = _boost.isGiveaway ? st::historyPeer4UserpicBg2 : st::historyPeer8UserpicBg2; const auto &rightIcon = _boost.isGiveaway ? st::boostsListGiveawayMiniIcon : st::boostsListGiftMiniIcon; _rightBadge = (_boost.isGift || _boost.isGiveaway) ? CreateBadge( st::boostsListRightBadgeTextStyle, _boost.isGiveaway ? tr::lng_gift_link_reason_giveaway(tr::now) : tr::lng_gift_link_label_gift(tr::now), st::boostsListRightBadgeHeight, st::boostsListRightBadgeTextPadding, rightColor, rightColor, kBadgeBgOpacity, st::boostsListGiftMiniIconPadding, rightIcon) : QImage(); } QSize BoostRow::rightActionSize() const { return _rightBadge.size() / style::DevicePixelRatio(); } QMargins BoostRow::rightActionMargins() const { return st::boostsListRightBadgePadding; } bool BoostRow::rightActionDisabled() const { return true; } void BoostRow::rightActionPaint( Painter &p, int x, int y, int outerWidth, bool selected, bool actionSelected) { if (!_rightBadge.isNull()) { p.drawImage(x, y, _rightBadge); } } int BoostRow::paintNameIconGetWidth( Painter &p, Fn repaint, crl::time now, int nameLeft, int nameTop, int nameWidth, int availableWidth, int outerWidth, bool selected) { if (_badge.isNull()) { return 0; } const auto badgew = _badge.width() / style::DevicePixelRatio(); const auto nameTooLarge = (nameWidth > availableWidth); const auto &padding = st::boostsListBadgePadding; const auto left = nameTooLarge ? ((nameLeft + availableWidth) - badgew - padding.left()) : (nameLeft + nameWidth + padding.right()); p.drawImage(left, nameTop + padding.top(), _badge); return badgew + (nameTooLarge ? padding.left() : 0); } class BoostsController final : public PeerListController { public: explicit BoostsController(BoostsDescriptor d); Main::Session &session() const override; void prepare() override; void rowClicked(not_null row) override; void loadMoreRows() override; [[nodiscard]] bool skipRequest() const; void requestNext(); [[nodiscard]] rpl::producer totalBoostsValue() const; private: void applySlice(const Data::BoostsListSlice &slice); const not_null _session; BoostCallback _boostClickedCallback; Api::Boosts _api; Data::BoostsListSlice _firstSlice; Data::BoostsListSlice::OffsetToken _apiToken; bool _allLoaded = false; bool _requesting = false; rpl::variable _totalBoosts; }; BoostsController::BoostsController(BoostsDescriptor d) : _session(&d.peer->session()) , _boostClickedCallback(std::move(d.boostClickedCallback)) , _api(d.peer) , _firstSlice(std::move(d.firstSlice)) { PeerListController::setStyleOverrides(&st::boostsListBox); } Main::Session &BoostsController::session() const { return *_session; } bool BoostsController::skipRequest() const { return _requesting || _allLoaded; } void BoostsController::requestNext() { _requesting = true; _api.requestBoosts(_apiToken, [=](const Data::BoostsListSlice &slice) { _requesting = false; applySlice(slice); }); } void BoostsController::prepare() { applySlice(base::take(_firstSlice)); delegate()->peerListRefreshRows(); } void BoostsController::loadMoreRows() { } void BoostsController::applySlice(const Data::BoostsListSlice &slice) { _allLoaded = slice.allLoaded; _apiToken = slice.token; auto sumFromSlice = 0; for (const auto &item : slice.list) { sumFromSlice += item.multiplier ? item.multiplier : 1; auto row = [&] { if (item.userId && !item.isUnclaimed) { const auto user = session().data().user(item.userId); return std::make_unique(user, item); } else { return std::make_unique(item); } }(); delegate()->peerListAppendRow(std::move(row)); } delegate()->peerListRefreshRows(); _totalBoosts = _totalBoosts.current() + sumFromSlice; } void BoostsController::rowClicked(not_null row) { if (_boostClickedCallback) { _boostClickedCallback( static_cast(row.get())->boost()); } } rpl::producer BoostsController::totalBoostsValue() const { return _totalBoosts.value(); } } // namespace void AddPublicForwards( const Data::PublicForwardsSlice &firstSlice, not_null container, Fn requestShow, not_null peer, Data::RecentPostId contextId) { if (!peer->isChannel()) { return; } struct State final { State(PublicForwardsDescriptor d) : controller(std::move(d)) { } PeerListContentDelegateSimple delegate; PublicForwardsController controller; }; auto d = PublicForwardsDescriptor{ firstSlice, std::move(requestShow), peer, contextId, }; const auto state = container->lifetime().make_state(std::move(d)); if (const auto total = firstSlice.total; total > 0) { AddSubtitle( container, tr::lng_stats_overview_message_public_share( lt_count_decimal, rpl::single(total))); } state->delegate.setContent(container->add( object_ptr(container, &state->controller))); state->controller.setDelegate(&state->delegate); } void AddMembersList( Data::SupergroupStatistics data, not_null container, Fn)> showPeerInfo, not_null peer, rpl::producer title) { if (!peer->isMegagroup()) { return; } const auto max = !data.topSenders.empty() ? data.topSenders.size() : !data.topAdministrators.empty() ? data.topAdministrators.size() : !data.topInviters.empty() ? data.topInviters.size() : 0; if (!max) { return; } constexpr auto kPerPage = 40; struct State final { State(MembersDescriptor d) : controller(std::move(d)) { } PeerListContentDelegateSimple delegate; MembersController controller; int limit = 0; }; auto d = MembersDescriptor{ &peer->session(), std::move(showPeerInfo), std::move(data), }; const auto state = container->lifetime().make_state(std::move(d)); AddSubtitle(container, std::move(title)); state->delegate.setContent(container->add( object_ptr(container, &state->controller))); state->controller.setDelegate(&state->delegate); const auto wrap = container->add( object_ptr>( container, object_ptr( container, tr::lng_stories_show_more())), { 0, -st::settingsButton.padding.top(), 0, 0 }); const auto showMore = [=] { state->limit = std::min(int(max), state->limit + kPerPage); state->controller.setLimit(state->limit); if (state->limit == max) { wrap->toggle(false, anim::type::instant); } container->resizeToWidth(container->width()); }; wrap->entity()->setClickedCallback(showMore); showMore(); } void AddBoostsList( const Data::BoostsListSlice &firstSlice, not_null container, BoostCallback boostClickedCallback, not_null peer, rpl::producer title) { const auto max = firstSlice.multipliedTotal; struct State final { State(BoostsDescriptor d) : controller(std::move(d)) { } PeerListContentDelegateSimple delegate; BoostsController controller; }; auto d = BoostsDescriptor{ firstSlice, boostClickedCallback, peer }; const auto state = container->lifetime().make_state(std::move(d)); state->delegate.setContent(container->add( object_ptr(container, &state->controller))); state->controller.setDelegate(&state->delegate); const auto wrap = container->add( object_ptr>( container, object_ptr( container, (firstSlice.token.gifts ? tr::lng_boosts_show_more_gifts : tr::lng_boosts_show_more_boosts)( lt_count, state->controller.totalBoostsValue( ) | rpl::map( max - rpl::mappers::_1 ) | tr::to_count()), st::statisticsShowMoreButton)), { 0, -st::settingsButton.padding.top(), 0, 0 }); const auto button = wrap->entity(); AddArrow(button); const auto showMore = [=] { if (!state->controller.skipRequest()) { state->controller.requestNext(); container->resizeToWidth(container->width()); } }; wrap->toggleOn( state->controller.totalBoostsValue( ) | rpl::map(rpl::mappers::_1 > 0 && rpl::mappers::_1 < max), anim::type::instant); button->setClickedCallback(showMore); } } // namespace Info::Statistics