diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 315338f3c..93140d89f 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -884,6 +884,8 @@ PRIVATE info/profile/info_profile_widget.h info/settings/info_settings_widget.cpp info/settings/info_settings_widget.h + info/statistics/info_statistics_inner_widget.cpp + info/statistics/info_statistics_inner_widget.h info/statistics/info_statistics_list_controllers.cpp info/statistics/info_statistics_list_controllers.h info/statistics/info_statistics_recent_message.cpp diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp new file mode 100644 index 000000000..dc5312019 --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp @@ -0,0 +1,683 @@ +/* +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_inner_widget.h" + +#include "api/api_statistics.h" +#include "apiwrap.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "history/history_item.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "info/statistics/info_statistics_list_controllers.h" +#include "info/statistics/info_statistics_recent_message.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "statistics/chart_header_widget.h" +#include "statistics/chart_widget.h" +#include "statistics/statistics_common.h" +#include "ui/layers/generic_box.h" +#include "ui/rect.h" +#include "ui/toast/toast.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/scroll_area.h" +#include "ui/wrap/slide_wrap.h" +#include "styles/style_boxes.h" +#include "styles/style_settings.h" +#include "styles/style_statistics.h" + +namespace Info::Statistics { +namespace { + +struct Descriptor final { + not_null peer; + not_null api; + not_null toastParent; +}; + +struct AnyStats final { + Data::ChannelStatistics channel; + Data::SupergroupStatistics supergroup; + Data::MessageStatistics message; +}; + +void ProcessZoom( + const Descriptor &d, + not_null widget, + const QString &zoomToken, + Statistic::ChartViewType type) { + if (zoomToken.isEmpty()) { + return; + } + widget->zoomRequests( + ) | rpl::start_with_next([=](float64 x) { + d.api->requestZoom( + d.peer, + zoomToken, + x + ) | rpl::start_with_next_error_done([=]( + const Data::StatisticalGraph &graph) { + if (graph.chart) { + widget->setZoomedChartData(graph.chart, x, type); + } else if (!graph.error.isEmpty()) { + Ui::Toast::Show(d.toastParent, graph.error); + } + }, [=](const QString &error) { + }, [=] { + }, widget->lifetime()); + }, widget->lifetime()); +} + +void ProcessChart( + const Descriptor &d, + not_null*> wrap, + not_null widget, + const Data::StatisticalGraph &graphData, + rpl::producer &&title, + Statistic::ChartViewType type) { + wrap->toggle(false, anim::type::instant); + if (graphData.chart) { + widget->setChartData(graphData.chart, type); + wrap->toggle(true, anim::type::instant); + ProcessZoom(d, widget, graphData.zoomToken, type); + widget->setTitle(std::move(title)); + } else if (!graphData.zoomToken.isEmpty()) { + d.api->requestZoom( + d.peer, + graphData.zoomToken, + 0 + ) | rpl::start_with_next_error_done([=]( + const Data::StatisticalGraph &graph) { + if (graph.chart) { + widget->setChartData(graph.chart, type); + wrap->toggle(true, anim::type::normal); + ProcessZoom(d, widget, graph.zoomToken, type); + widget->setTitle(rpl::duplicate(title)); + } else if (!graph.error.isEmpty()) { + Ui::Toast::Show(d.toastParent, graph.error); + } + }, [=](const QString &error) { + }, [=] { + }, widget->lifetime()); + } +} + +void FillStatistic( + not_null content, + const Descriptor &descriptor, + const AnyStats &stats) { + using Type = Statistic::ChartViewType; + const auto &padding = st::statisticsChartEntryPadding; + const auto &m = st::statisticsLayerMargins; + const auto addSkip = [&](not_null c) { + ::Settings::AddSkip(c, padding.bottom()); + ::Settings::AddDivider(c); + ::Settings::AddSkip(c, padding.top()); + }; + const auto addChart = [&]( + const Data::StatisticalGraph &graphData, + rpl::producer &&title, + Statistic::ChartViewType type) { + const auto wrap = content->add( + object_ptr>( + content, + object_ptr(content))); + ProcessChart( + descriptor, + wrap, + wrap->entity()->add( + object_ptr(content), + m), + graphData, + std::move(title), + type); + addSkip(wrap->entity()); + }; + addSkip(content); + if (const auto s = stats.channel) { + addChart( + s.memberCountGraph, + tr::lng_chart_title_member_count(), + Type::Linear); + addChart( + s.joinGraph, + tr::lng_chart_title_join(), + Type::Linear); + addChart( + s.muteGraph, + tr::lng_chart_title_mute(), + Type::Linear); + addChart( + s.viewCountByHourGraph, + tr::lng_chart_title_view_count_by_hour(), + Type::Linear); + addChart( + s.viewCountBySourceGraph, + tr::lng_chart_title_view_count_by_source(), + Type::Stack); + addChart( + s.joinBySourceGraph, + tr::lng_chart_title_join_by_source(), + Type::Stack); + addChart( + s.languageGraph, + tr::lng_chart_title_language(), + Type::StackLinear); + addChart( + s.messageInteractionGraph, + tr::lng_chart_title_message_interaction(), + Type::DoubleLinear); + addChart( + s.instantViewInteractionGraph, + tr::lng_chart_title_instant_view_interaction(), + Type::DoubleLinear); + } else if (const auto s = stats.supergroup) { + addChart( + s.memberCountGraph, + tr::lng_chart_title_member_count(), + Type::Linear); + addChart( + s.joinGraph, + tr::lng_chart_title_group_join(), + Type::Linear); + addChart( + s.joinBySourceGraph, + tr::lng_chart_title_group_join_by_source(), + Type::Stack); + addChart( + s.languageGraph, + tr::lng_chart_title_group_language(), + Type::StackLinear); + addChart( + s.messageContentGraph, + tr::lng_chart_title_group_message_content(), + Type::Stack); + addChart( + s.actionGraph, + tr::lng_chart_title_group_action(), + Type::DoubleLinear); + addChart( + s.dayGraph, + tr::lng_chart_title_group_day(), + Type::Linear); + // addChart( + // s.weekGraph, + // tr::lng_chart_title_group_week(), + // Type::StackLinear); + } else if (const auto message = stats.message) { + addChart( + message.messageInteractionGraph, + tr::lng_chart_title_message_interaction(), + Type::DoubleLinear); + } +} + +void FillLoading( + not_null container, + rpl::producer toggleOn, + rpl::producer<> showFinished) { + const auto emptyWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + emptyWrap->toggleOn(std::move(toggleOn), anim::type::instant); + + const auto content = emptyWrap->entity(); + auto icon = ::Settings::CreateLottieIcon( + content, + { .name = u"stats"_q, .sizeOverride = Size(st::changePhoneIconSize) }, + st::settingsBlockedListIconPadding); + + ( + std::move(showFinished) | rpl::take(1) + ) | rpl::start_with_next([animate = std::move(icon.animate)] { + animate(anim::repeat::loop); + }, icon.widget->lifetime()); + content->add(std::move(icon.widget)); + + content->add( + object_ptr>( + content, + object_ptr( + content, + tr::lng_stats_loading(), + st::changePhoneTitle)), + st::changePhoneTitlePadding + st::boxRowPadding); + + content->add( + object_ptr>( + content, + object_ptr( + content, + tr::lng_stats_loading_subtext(), + st::statisticsLoadingSubtext)), + st::changePhoneDescriptionPadding + st::boxRowPadding); + + ::Settings::AddSkip(content, st::settingsBlockedListIconPadding.top()); +} + +void AddHeader( + not_null content, + tr::phrase<> text, + const AnyStats &stats) { + const auto startDate = stats.channel + ? stats.channel.startDate + : stats.supergroup.startDate; + const auto endDate = stats.channel + ? stats.channel.endDate + : stats.supergroup.endDate; + const auto header = content->add( + object_ptr(content), + st::statisticsLayerMargins + st::statisticsChartHeaderPadding); + header->resizeToWidth(header->width()); + header->setTitle(text(tr::now)); + if (!endDate || !startDate) { + header->setSubTitle({}); + return; + } + const auto formatter = u"d MMM yyyy"_q; + const auto from = QDateTime::fromSecsSinceEpoch(startDate); + const auto to = QDateTime::fromSecsSinceEpoch(endDate); + header->setSubTitle(QLocale().toString(from.date(), formatter) + + ' ' + + QChar(8212) + + ' ' + + QLocale().toString(to.date(), formatter)); +} + +void FillOverview( + not_null content, + const AnyStats &stats) { + using Value = Data::StatisticalValue; + + const auto &channel = stats.channel; + const auto &supergroup = stats.supergroup; + + ::Settings::AddSkip(content, st::statisticsLayerOverviewMargins.top()); + AddHeader(content, tr::lng_stats_overview_title, stats); + ::Settings::AddSkip(content); + + struct Second final { + QColor color; + QString text; + }; + + const auto parseSecond = [&](const Value &v) -> Second { + const auto diff = v.value - v.previousValue; + if (!diff) { + return {}; + } + constexpr auto kTooMuchDiff = int(1'000'000); + const auto diffAbs = std::abs(diff); + const auto diffText = diffAbs > kTooMuchDiff + ? Lang::FormatCountToShort(std::abs(diff)).string + : QString::number(diffAbs); + return { + (diff < 0 ? st::menuIconAttentionColor : st::settingsIconBg2)->c, + QString("%1%2 (%3%)") + .arg((diff < 0) ? QChar(0x2212) : QChar(0x002B)) + .arg(diffText) + .arg(std::abs(std::round(v.growthRatePercentage * 10.) / 10.)) + }; + }; + + const auto diffBetweenHeaders = 0 + + st::statisticsOverviewValue.style.font->height + - st::statisticsHeaderTitleTextStyle.font->height; + + const auto container = content->add( + object_ptr(content), + st::statisticsLayerMargins); + + const auto addPrimary = [&](const Value &v) { + return Ui::CreateChild( + container, + (v.value >= 0) + ? Lang::FormatCountToShort(v.value).string + : QString(), + st::statisticsOverviewValue); + }; + const auto addSub = [&]( + not_null primary, + const Value &v, + tr::phrase<> text) { + const auto data = parseSecond(v); + const auto second = Ui::CreateChild( + container, + data.text, + st::statisticsOverviewSecondValue); + second->setTextColorOverride(data.color); + const auto sub = Ui::CreateChild( + container, + text(), + st::statisticsOverviewSubtext); + sub->setTextColorOverride(st::windowSubTextFg->c); + + primary->geometryValue( + ) | rpl::start_with_next([=](const QRect &g) { + const auto &padding = st::statisticsOverviewSecondValuePadding; + second->moveToLeft( + rect::right(g) + padding.left(), + g.y() + padding.top()); + sub->moveToLeft( + g.x(), + st::statisticsChartHeaderHeight + - st::statisticsOverviewSubtext.style.font->height + + g.y() + + diffBetweenHeaders); + }, primary->lifetime()); + }; + + const auto isChannel = (!!channel); + const auto isMessage = (!!stats.message); + const auto topLeftLabel = isChannel + ? addPrimary(channel.memberCount) + : isMessage + ? addPrimary({ .value = float64(stats.message.views) }) + : addPrimary(supergroup.memberCount); + const auto topRightLabel = isChannel + ? Ui::CreateChild( + container, + QString("%1%").arg(0.01 + * std::round(channel.enabledNotificationsPercentage * 100.)), + st::statisticsOverviewValue) + : isMessage + ? addPrimary({ .value = float64(stats.message.publicForwards) }) + : addPrimary(supergroup.messageCount); + const auto bottomLeftLabel = isChannel + ? addPrimary(channel.meanViewCount) + : isMessage + ? addPrimary({ .value = float64(stats.message.privateForwards) }) + : addPrimary(supergroup.viewerCount); + const auto bottomRightLabel = isChannel + ? addPrimary(channel.meanShareCount) + : isMessage + ? addPrimary({ .value = -1. }) + : addPrimary(supergroup.senderCount); + if (const auto &s = channel) { + addSub( + topLeftLabel, + s.memberCount, + tr::lng_stats_overview_member_count); + addSub( + topRightLabel, + {}, + tr::lng_stats_overview_enabled_notifications); + addSub( + bottomLeftLabel, + s.meanViewCount, + tr::lng_stats_overview_mean_view_count); + addSub( + bottomRightLabel, + s.meanShareCount, + tr::lng_stats_overview_mean_share_count); + } else if (const auto &s = supergroup) { + addSub( + topLeftLabel, + s.memberCount, + tr::lng_manage_peer_members); + addSub( + topRightLabel, + s.messageCount, + tr::lng_stats_overview_messages); + addSub( + bottomLeftLabel, + s.viewerCount, + tr::lng_stats_overview_group_mean_view_count); + addSub( + bottomRightLabel, + s.senderCount, + tr::lng_stats_overview_group_mean_post_count); + } else if (const auto &s = stats.message) { + if (s.views >= 0) { + addSub( + topLeftLabel, + {}, + tr::lng_stats_overview_message_views); + } + if (s.publicForwards >= 0) { + addSub( + topRightLabel, + {}, + tr::lng_stats_overview_message_public_shares); + } + if (s.privateForwards >= 0) { + addSub( + bottomLeftLabel, + {}, + tr::lng_stats_overview_message_private_shares); + } + } + container->showChildren(); + container->resize(container->width(), topLeftLabel->height() * 5); + container->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + const auto halfWidth = s.width() / 2; + { + const auto &p = st::statisticsOverviewValuePadding; + topLeftLabel->moveToLeft(p.left(), p.top()); + } + topRightLabel->moveToLeft( + topLeftLabel->x() + halfWidth + st::statisticsOverviewRightSkip, + topLeftLabel->y()); + bottomLeftLabel->moveToLeft( + topLeftLabel->x(), + topLeftLabel->y() + st::statisticsOverviewMidSkip); + bottomRightLabel->moveToLeft( + topRightLabel->x(), + bottomLeftLabel->y()); + }, container->lifetime()); + ::Settings::AddSkip(content, st::statisticsLayerOverviewMargins.bottom()); +} + +void FillRecentPosts( + not_null container, + const Descriptor &descriptor, + const Data::ChannelStatistics &stats, + Fn showMessageStatistic) { + const auto wrap = container->add( + object_ptr>( + container, + object_ptr(container))); + wrap->toggle(false, anim::type::instant); + const auto content = wrap->entity(); + AddHeader(content, tr::lng_stats_recent_messages_title, { stats, {} }); + ::Settings::AddSkip(content); + + const auto addMessage = [=]( + not_null messageWrap, + not_null item, + const Data::StatisticsMessageInteractionInfo &info) { + const auto button = messageWrap->add( + object_ptr( + messageWrap, + rpl::never(), + st::statisticsRecentPostButton)); + const auto raw = Ui::CreateChild( + button, + item, + info.viewsCount, + info.forwardsCount); + raw->show(); + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + if (!s.isNull()) { + raw->setGeometry(Rect(s) + - st::statisticsRecentPostButton.padding); + } + }, raw->lifetime()); + button->setClickedCallback([=, fullId = item->fullId()] { + showMessageStatistic(fullId); + }); + ::Settings::AddSkip(messageWrap); + content->resizeToWidth(content->width()); + if (!wrap->toggled()) { + wrap->toggle(true, anim::type::normal); + } + }; + + const auto &peer = descriptor.peer; + for (const auto &recent : stats.recentMessageInteractions) { + const auto messageWrap = content->add( + object_ptr(content)); + const auto msgId = recent.messageId; + if (const auto item = peer->owner().message(peer, msgId)) { + addMessage(messageWrap, item, recent); + continue; + } + const auto callback = [=] { + if (const auto item = peer->owner().message(peer, msgId)) { + addMessage(messageWrap, item, recent); + } + }; + peer->session().api().requestMessageData(peer, msgId, callback); + } +} + +} // namespace + +InnerWidget::InnerWidget( + QWidget *parent, + not_null controller, + not_null peer, + FullMsgId contextId) +: VerticalLayout(parent) +, _controller(controller) +, _peer(peer) +, _contextId(contextId) { + const auto inner = this; + + const auto descriptor = Descriptor{ + peer, + lifetime().make_state(&peer->session().api()), + _controller->uiShow()->toastParent(), + }; + + FillLoading( + inner, + _loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1), + _showFinished.events()); + + const auto finishLoading = [=] { + _loaded.fire(true); + inner->resizeToWidth(width()); + inner->showChildren(); + }; + + _showFinished.events( + ) | rpl::take(1) | rpl::start_with_next([=] { + if (!contextId) { + descriptor.api->request( + descriptor.peer + ) | rpl::start_with_done([=] { + const auto anyStats = AnyStats{ + descriptor.api->channelStats(), + descriptor.api->supergroupStats(), + }; + + FillOverview(inner, anyStats); + FillStatistic(inner, descriptor, anyStats); + const auto &channel = anyStats.channel; + const auto &supergroup = anyStats.supergroup; + if (channel) { + auto showMessage = [=](FullMsgId fullId) { + _showRequests.fire({ .messageStatistic = fullId }); + }; + FillRecentPosts(inner, descriptor, channel, showMessage); + } else if (supergroup) { + const auto showPeerInfo = [=](not_null peer) { + _showRequests.fire({ .info = peer->id }); + }; + const auto addSkip = [&]( + not_null c) { + ::Settings::AddSkip(c); + ::Settings::AddDivider(c); + ::Settings::AddSkip(c); + ::Settings::AddSkip(c); + }; + if (!supergroup.topSenders.empty()) { + AddMembersList( + { .topSenders = supergroup.topSenders }, + inner, + showPeerInfo, + descriptor.peer, + tr::lng_stats_members_title()); + } + if (!supergroup.topAdministrators.empty()) { + addSkip(inner); + AddMembersList( + { .topAdministrators + = supergroup.topAdministrators }, + inner, + showPeerInfo, + descriptor.peer, + tr::lng_stats_admins_title()); + } + if (!supergroup.topInviters.empty()) { + addSkip(inner); + AddMembersList( + { .topInviters = supergroup.topInviters }, + inner, + showPeerInfo, + descriptor.peer, + tr::lng_stats_inviters_title()); + } + } + finishLoading(); + }, lifetime()); + } else { + const auto lifetimeApi = lifetime().make_state(); + const auto api = lifetimeApi->make_state( + descriptor.peer->asChannel(), + contextId); + + api->request([=](const Data::MessageStatistics &data) { + const auto stats = AnyStats{ .message = data }; + FillOverview(inner, stats); + FillStatistic(inner, descriptor, stats); + auto showPeerHistory = [=](FullMsgId fullId) { + _showRequests.fire({ .history = fullId }); + }; + AddPublicForwards( + *api, + inner, + std::move(showPeerHistory), + descriptor.peer, + contextId); + + finishLoading(); + lifetimeApi->destroy(); + }); + } + }, lifetime()); +} + +rpl::producer InnerWidget::scrollToRequests() const { + return _scrollToRequests.events(); +} + +auto InnerWidget::showRequests() const -> rpl::producer { + return _showRequests.events(); +} + +void InnerWidget::showFinished() { + _showFinished.fire({}); +} + +not_null InnerWidget::peer() const { + return _peer; +} + +FullMsgId InnerWidget::contextId() const { + return _contextId; +} + +} // namespace Info::Statistics + diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h new file mode 100644 index 000000000..ad7d09a69 --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h @@ -0,0 +1,54 @@ +/* +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 "base/object_ptr.h" +#include "ui/widgets/scroll_area.h" +#include "ui/wrap/vertical_layout.h" + +namespace Info { +class Controller; +} // namespace Info + +namespace Info::Statistics { + +class InnerWidget final : public Ui::VerticalLayout { +public: + struct ShowRequest final { + PeerId info = PeerId(0); + FullMsgId history; + FullMsgId messageStatistic; + }; + + InnerWidget( + QWidget *parent, + not_null controller, + not_null peer, + FullMsgId contextId); + + [[nodiscard]] not_null peer() const; + [[nodiscard]] FullMsgId contextId() const; + + [[nodiscard]] rpl::producer scrollToRequests() const; + [[nodiscard]] rpl::producer showRequests() const; + + void showFinished(); + +private: + not_null _controller; + not_null _peer; + FullMsgId _contextId; + + rpl::event_stream _scrollToRequests; + rpl::event_stream _showRequests; + rpl::event_stream<> _showFinished; + rpl::event_stream _loaded; + +}; + +} // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp index b5faca4e2..9ed88b555 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp @@ -7,541 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/statistics/info_statistics_widget.h" -#include "api/api_statistics.h" -#include "apiwrap.h" -#include "data/data_peer.h" -#include "data/data_session.h" -#include "history/history_item.h" +#include "info/statistics/info_statistics_inner_widget.h" #include "info/info_controller.h" #include "info/info_memento.h" -#include "info/statistics/info_statistics_list_controllers.h" -#include "info/statistics/info_statistics_recent_message.h" #include "lang/lang_keys.h" -#include "lottie/lottie_icon.h" -#include "main/main_session.h" -#include "settings/settings_common.h" -#include "statistics/chart_header_widget.h" -#include "statistics/chart_widget.h" -#include "statistics/statistics_common.h" -#include "ui/layers/generic_box.h" -#include "ui/rect.h" -#include "ui/toast/toast.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/scroll_area.h" -#include "ui/wrap/slide_wrap.h" -#include "styles/style_boxes.h" -#include "styles/style_settings.h" -#include "styles/style_statistics.h" namespace Info::Statistics { -namespace { - -struct Descriptor final { - not_null peer; - not_null api; - not_null toastParent; -}; - -struct AnyStats final { - Data::ChannelStatistics channel; - Data::SupergroupStatistics supergroup; - Data::MessageStatistics message; -}; - -void ProcessZoom( - const Descriptor &d, - not_null widget, - const QString &zoomToken, - Statistic::ChartViewType type) { - if (zoomToken.isEmpty()) { - return; - } - widget->zoomRequests( - ) | rpl::start_with_next([=](float64 x) { - d.api->requestZoom( - d.peer, - zoomToken, - x - ) | rpl::start_with_next_error_done([=]( - const Data::StatisticalGraph &graph) { - if (graph.chart) { - widget->setZoomedChartData(graph.chart, x, type); - } else if (!graph.error.isEmpty()) { - Ui::Toast::Show(d.toastParent, graph.error); - } - }, [=](const QString &error) { - }, [=] { - }, widget->lifetime()); - }, widget->lifetime()); -} - -void ProcessChart( - const Descriptor &d, - not_null*> wrap, - not_null widget, - const Data::StatisticalGraph &graphData, - rpl::producer &&title, - Statistic::ChartViewType type) { - wrap->toggle(false, anim::type::instant); - if (graphData.chart) { - widget->setChartData(graphData.chart, type); - wrap->toggle(true, anim::type::instant); - ProcessZoom(d, widget, graphData.zoomToken, type); - widget->setTitle(std::move(title)); - } else if (!graphData.zoomToken.isEmpty()) { - d.api->requestZoom( - d.peer, - graphData.zoomToken, - 0 - ) | rpl::start_with_next_error_done([=]( - const Data::StatisticalGraph &graph) { - if (graph.chart) { - widget->setChartData(graph.chart, type); - wrap->toggle(true, anim::type::normal); - ProcessZoom(d, widget, graph.zoomToken, type); - widget->setTitle(rpl::duplicate(title)); - } else if (!graph.error.isEmpty()) { - Ui::Toast::Show(d.toastParent, graph.error); - } - }, [=](const QString &error) { - }, [=] { - }, widget->lifetime()); - } -} - -void FillStatistic( - not_null content, - const Descriptor &descriptor, - const AnyStats &stats) { - using Type = Statistic::ChartViewType; - const auto &padding = st::statisticsChartEntryPadding; - const auto &m = st::statisticsLayerMargins; - const auto addSkip = [&](not_null c) { - ::Settings::AddSkip(c, padding.bottom()); - ::Settings::AddDivider(c); - ::Settings::AddSkip(c, padding.top()); - }; - const auto addChart = [&]( - const Data::StatisticalGraph &graphData, - rpl::producer &&title, - Statistic::ChartViewType type) { - const auto wrap = content->add( - object_ptr>( - content, - object_ptr(content))); - ProcessChart( - descriptor, - wrap, - wrap->entity()->add( - object_ptr(content), - m), - graphData, - std::move(title), - type); - addSkip(wrap->entity()); - }; - addSkip(content); - if (const auto s = stats.channel) { - addChart( - s.memberCountGraph, - tr::lng_chart_title_member_count(), - Type::Linear); - addChart( - s.joinGraph, - tr::lng_chart_title_join(), - Type::Linear); - addChart( - s.muteGraph, - tr::lng_chart_title_mute(), - Type::Linear); - addChart( - s.viewCountByHourGraph, - tr::lng_chart_title_view_count_by_hour(), - Type::Linear); - addChart( - s.viewCountBySourceGraph, - tr::lng_chart_title_view_count_by_source(), - Type::Stack); - addChart( - s.joinBySourceGraph, - tr::lng_chart_title_join_by_source(), - Type::Stack); - addChart( - s.languageGraph, - tr::lng_chart_title_language(), - Type::StackLinear); - addChart( - s.messageInteractionGraph, - tr::lng_chart_title_message_interaction(), - Type::DoubleLinear); - addChart( - s.instantViewInteractionGraph, - tr::lng_chart_title_instant_view_interaction(), - Type::DoubleLinear); - } else if (const auto s = stats.supergroup) { - addChart( - s.memberCountGraph, - tr::lng_chart_title_member_count(), - Type::Linear); - addChart( - s.joinGraph, - tr::lng_chart_title_group_join(), - Type::Linear); - addChart( - s.joinBySourceGraph, - tr::lng_chart_title_group_join_by_source(), - Type::Stack); - addChart( - s.languageGraph, - tr::lng_chart_title_group_language(), - Type::StackLinear); - addChart( - s.messageContentGraph, - tr::lng_chart_title_group_message_content(), - Type::Stack); - addChart( - s.actionGraph, - tr::lng_chart_title_group_action(), - Type::DoubleLinear); - addChart( - s.dayGraph, - tr::lng_chart_title_group_day(), - Type::Linear); - // addChart( - // s.weekGraph, - // tr::lng_chart_title_group_week(), - // Type::StackLinear); - } else if (const auto message = stats.message) { - addChart( - message.messageInteractionGraph, - tr::lng_chart_title_message_interaction(), - Type::DoubleLinear); - } -} - -void FillLoading( - not_null container, - rpl::producer toggleOn, - rpl::producer<> showFinished) { - const auto emptyWrap = container->add( - object_ptr>( - container, - object_ptr(container))); - emptyWrap->toggleOn(std::move(toggleOn), anim::type::instant); - - const auto content = emptyWrap->entity(); - auto icon = ::Settings::CreateLottieIcon( - content, - { .name = u"stats"_q, .sizeOverride = Size(st::changePhoneIconSize) }, - st::settingsBlockedListIconPadding); - - ( - std::move(showFinished) | rpl::take(1) - ) | rpl::start_with_next([animate = std::move(icon.animate)] { - animate(anim::repeat::loop); - }, icon.widget->lifetime()); - content->add(std::move(icon.widget)); - - content->add( - object_ptr>( - content, - object_ptr( - content, - tr::lng_stats_loading(), - st::changePhoneTitle)), - st::changePhoneTitlePadding + st::boxRowPadding); - - content->add( - object_ptr>( - content, - object_ptr( - content, - tr::lng_stats_loading_subtext(), - st::statisticsLoadingSubtext)), - st::changePhoneDescriptionPadding + st::boxRowPadding); - - ::Settings::AddSkip(content, st::settingsBlockedListIconPadding.top()); -} - -void AddHeader( - not_null content, - tr::phrase<> text, - const AnyStats &stats) { - const auto startDate = stats.channel - ? stats.channel.startDate - : stats.supergroup.startDate; - const auto endDate = stats.channel - ? stats.channel.endDate - : stats.supergroup.endDate; - const auto header = content->add( - object_ptr(content), - st::statisticsLayerMargins + st::statisticsChartHeaderPadding); - header->resizeToWidth(header->width()); - header->setTitle(text(tr::now)); - if (!endDate || !startDate) { - header->setSubTitle({}); - return; - } - const auto formatter = u"d MMM yyyy"_q; - const auto from = QDateTime::fromSecsSinceEpoch(startDate); - const auto to = QDateTime::fromSecsSinceEpoch(endDate); - header->setSubTitle(QLocale().toString(from.date(), formatter) - + ' ' - + QChar(8212) - + ' ' - + QLocale().toString(to.date(), formatter)); -} - -void FillOverview( - not_null content, - const AnyStats &stats) { - using Value = Data::StatisticalValue; - - const auto &channel = stats.channel; - const auto &supergroup = stats.supergroup; - - ::Settings::AddSkip(content, st::statisticsLayerOverviewMargins.top()); - AddHeader(content, tr::lng_stats_overview_title, stats); - ::Settings::AddSkip(content); - - struct Second final { - QColor color; - QString text; - }; - - const auto parseSecond = [&](const Value &v) -> Second { - const auto diff = v.value - v.previousValue; - if (!diff) { - return {}; - } - constexpr auto kTooMuchDiff = int(1'000'000); - const auto diffAbs = std::abs(diff); - const auto diffText = diffAbs > kTooMuchDiff - ? Lang::FormatCountToShort(std::abs(diff)).string - : QString::number(diffAbs); - return { - (diff < 0 ? st::menuIconAttentionColor : st::settingsIconBg2)->c, - QString("%1%2 (%3%)") - .arg((diff < 0) ? QChar(0x2212) : QChar(0x002B)) - .arg(diffText) - .arg(std::abs(std::round(v.growthRatePercentage * 10.) / 10.)) - }; - }; - - const auto diffBetweenHeaders = 0 - + st::statisticsOverviewValue.style.font->height - - st::statisticsHeaderTitleTextStyle.font->height; - - const auto container = content->add( - object_ptr(content), - st::statisticsLayerMargins); - - const auto addPrimary = [&](const Value &v) { - return Ui::CreateChild( - container, - (v.value >= 0) - ? Lang::FormatCountToShort(v.value).string - : QString(), - st::statisticsOverviewValue); - }; - const auto addSub = [&]( - not_null primary, - const Value &v, - tr::phrase<> text) { - const auto data = parseSecond(v); - const auto second = Ui::CreateChild( - container, - data.text, - st::statisticsOverviewSecondValue); - second->setTextColorOverride(data.color); - const auto sub = Ui::CreateChild( - container, - text(), - st::statisticsOverviewSubtext); - sub->setTextColorOverride(st::windowSubTextFg->c); - - primary->geometryValue( - ) | rpl::start_with_next([=](const QRect &g) { - const auto &padding = st::statisticsOverviewSecondValuePadding; - second->moveToLeft( - rect::right(g) + padding.left(), - g.y() + padding.top()); - sub->moveToLeft( - g.x(), - st::statisticsChartHeaderHeight - - st::statisticsOverviewSubtext.style.font->height - + g.y() - + diffBetweenHeaders); - }, primary->lifetime()); - }; - - const auto isChannel = (!!channel); - const auto isMessage = (!!stats.message); - const auto topLeftLabel = isChannel - ? addPrimary(channel.memberCount) - : isMessage - ? addPrimary({ .value = float64(stats.message.views) }) - : addPrimary(supergroup.memberCount); - const auto topRightLabel = isChannel - ? Ui::CreateChild( - container, - QString("%1%").arg(0.01 - * std::round(channel.enabledNotificationsPercentage * 100.)), - st::statisticsOverviewValue) - : isMessage - ? addPrimary({ .value = float64(stats.message.publicForwards) }) - : addPrimary(supergroup.messageCount); - const auto bottomLeftLabel = isChannel - ? addPrimary(channel.meanViewCount) - : isMessage - ? addPrimary({ .value = float64(stats.message.privateForwards) }) - : addPrimary(supergroup.viewerCount); - const auto bottomRightLabel = isChannel - ? addPrimary(channel.meanShareCount) - : isMessage - ? addPrimary({ .value = -1. }) - : addPrimary(supergroup.senderCount); - if (const auto &s = channel) { - addSub( - topLeftLabel, - s.memberCount, - tr::lng_stats_overview_member_count); - addSub( - topRightLabel, - {}, - tr::lng_stats_overview_enabled_notifications); - addSub( - bottomLeftLabel, - s.meanViewCount, - tr::lng_stats_overview_mean_view_count); - addSub( - bottomRightLabel, - s.meanShareCount, - tr::lng_stats_overview_mean_share_count); - } else if (const auto &s = supergroup) { - addSub( - topLeftLabel, - s.memberCount, - tr::lng_manage_peer_members); - addSub( - topRightLabel, - s.messageCount, - tr::lng_stats_overview_messages); - addSub( - bottomLeftLabel, - s.viewerCount, - tr::lng_stats_overview_group_mean_view_count); - addSub( - bottomRightLabel, - s.senderCount, - tr::lng_stats_overview_group_mean_post_count); - } else if (const auto &s = stats.message) { - if (s.views >= 0) { - addSub( - topLeftLabel, - {}, - tr::lng_stats_overview_message_views); - } - if (s.publicForwards >= 0) { - addSub( - topRightLabel, - {}, - tr::lng_stats_overview_message_public_shares); - } - if (s.privateForwards >= 0) { - addSub( - bottomLeftLabel, - {}, - tr::lng_stats_overview_message_private_shares); - } - } - container->showChildren(); - container->resize(container->width(), topLeftLabel->height() * 5); - container->sizeValue( - ) | rpl::start_with_next([=](const QSize &s) { - const auto halfWidth = s.width() / 2; - { - const auto &p = st::statisticsOverviewValuePadding; - topLeftLabel->moveToLeft(p.left(), p.top()); - } - topRightLabel->moveToLeft( - topLeftLabel->x() + halfWidth + st::statisticsOverviewRightSkip, - topLeftLabel->y()); - bottomLeftLabel->moveToLeft( - topLeftLabel->x(), - topLeftLabel->y() + st::statisticsOverviewMidSkip); - bottomRightLabel->moveToLeft( - topRightLabel->x(), - bottomLeftLabel->y()); - }, container->lifetime()); - ::Settings::AddSkip(content, st::statisticsLayerOverviewMargins.bottom()); -} - -void FillRecentPosts( - not_null container, - const Descriptor &descriptor, - const Data::ChannelStatistics &stats, - Fn)> showSection) { - const auto wrap = container->add( - object_ptr>( - container, - object_ptr(container))); - wrap->toggle(false, anim::type::instant); - const auto content = wrap->entity(); - AddHeader(content, tr::lng_stats_recent_messages_title, { stats, {} }); - ::Settings::AddSkip(content); - - const auto addMessage = [=]( - not_null messageWrap, - not_null item, - const Data::StatisticsMessageInteractionInfo &info) { - const auto button = messageWrap->add( - object_ptr( - messageWrap, - rpl::never(), - st::statisticsRecentPostButton)); - const auto raw = Ui::CreateChild( - button, - item, - info.viewsCount, - info.forwardsCount); - raw->show(); - button->sizeValue( - ) | rpl::start_with_next([=](const QSize &s) { - if (!s.isNull()) { - raw->setGeometry(Rect(s) - - st::statisticsRecentPostButton.padding); - } - }, raw->lifetime()); - button->setClickedCallback([=, fullId = item->fullId()] { - showSection(Info::Statistics::Make(descriptor.peer, fullId)); - }); - ::Settings::AddSkip(messageWrap); - content->resizeToWidth(content->width()); - if (!wrap->toggled()) { - wrap->toggle(true, anim::type::normal); - } - }; - - const auto &peer = descriptor.peer; - for (const auto &recent : stats.recentMessageInteractions) { - const auto messageWrap = content->add( - object_ptr(content)); - const auto msgId = recent.messageId; - if (const auto item = peer->owner().message(peer, msgId)) { - addMessage(messageWrap, item, recent); - continue; - } - const auto callback = [=] { - if (const auto item = peer->owner().message(peer, msgId)) { - addMessage(messageWrap, item, recent); - } - }; - peer->session().api().requestMessageData(peer, msgId, callback); - } -} - -} // namespace Memento::Memento(not_null controller) : ContentMemento(Tag{ @@ -571,129 +42,40 @@ object_ptr Memento::createWidget( Widget::Widget( QWidget *parent, not_null controller) -: ContentWidget(parent, controller) { - const auto peer = controller->statisticsPeer(); - const auto contextId = controller->statisticsContextId(); - if (!peer) { - return; - } - const auto inner = setInnerWidget(object_ptr(this)); - auto &lifetime = inner->lifetime(); - const auto loaded = lifetime.make_state>(); - - const auto descriptor = Descriptor{ - peer, - lifetime.make_state(&peer->session().api()), - controller->uiShow()->toastParent(), - }; - - FillLoading( - inner, - loaded->events_starting_with(false) | rpl::map(!rpl::mappers::_1), - _showFinished.events()); - - const auto finishLoading = [=] { - loaded->fire(true); - inner->resizeToWidth(width()); - inner->showChildren(); - }; - - _showFinished.events( - ) | rpl::take(1) | rpl::start_with_next([=] { - if (!contextId) { - descriptor.api->request( - descriptor.peer - ) | rpl::start_with_done([=] { - const auto anyStats = AnyStats{ - descriptor.api->channelStats(), - descriptor.api->supergroupStats(), - }; - - FillOverview(inner, anyStats); - FillStatistic(inner, descriptor, anyStats); - const auto &channel = anyStats.channel; - const auto &supergroup = anyStats.supergroup; - if (channel) { - const auto showSection = [controller]( - std::shared_ptr memento) { - controller->showSection(std::move(memento)); - }; - FillRecentPosts(inner, descriptor, channel, showSection); - } else if (supergroup) { - const auto showPeerInfo = [controller]( - not_null peer) { - controller->showSection( - std::make_shared(peer)); - }; - const auto addSkip = [&]( - not_null c) { - ::Settings::AddSkip(c); - ::Settings::AddDivider(c); - ::Settings::AddSkip(c); - ::Settings::AddSkip(c); - }; - if (!supergroup.topSenders.empty()) { - AddMembersList( - { .topSenders = supergroup.topSenders }, - inner, - showPeerInfo, - descriptor.peer, - tr::lng_stats_members_title()); - } - if (!supergroup.topAdministrators.empty()) { - addSkip(inner); - AddMembersList( - { .topAdministrators - = supergroup.topAdministrators }, - inner, - showPeerInfo, - descriptor.peer, - tr::lng_stats_admins_title()); - } - if (!supergroup.topInviters.empty()) { - addSkip(inner); - AddMembersList( - { .topInviters = supergroup.topInviters }, - inner, - showPeerInfo, - descriptor.peer, - tr::lng_stats_inviters_title()); - } - } - finishLoading(); - }, inner->lifetime()); - } else { - const auto weak = base::make_weak(controller); - const auto lifetime = inner->lifetime( - ).make_state(); - const auto api = lifetime->make_state( - descriptor.peer->asChannel(), - contextId); - - api->request([=](const Data::MessageStatistics &data) { - const auto stats = AnyStats{ .message = data }; - FillOverview(inner, stats); - FillStatistic(inner, descriptor, stats); - auto showPeerHistory = [=](FullMsgId fullId) { - if (const auto strong = weak.get()) { - controller->showPeerHistory( - fullId.peer, - Window::SectionShow::Way::Forward, - fullId.msg); - } - }; - AddPublicForwards( - *api, - inner, - std::move(showPeerHistory), - descriptor.peer, - contextId); - - finishLoading(); - lifetime->destroy(); - }); +: ContentWidget(parent, controller) +, _inner(setInnerWidget( + object_ptr( + this, + controller, + controller->statisticsPeer(), + controller->statisticsContextId()))) { + _inner->showRequests( + ) | rpl::start_with_next([=](InnerWidget::ShowRequest request) { + if (request.history) { + controller->showPeerHistory( + request.history.peer, + Window::SectionShow::Way::Forward, + request.history.msg); + } else if (request.info) { + controller->showPeerInfo(request.info); + } else if (request.messageStatistic) { + controller->showSection(Make( + controller->statisticsPeer(), + request.messageStatistic)); } - }, lifetime); + }, _inner->lifetime()); + _inner->scrollToRequests( + ) | rpl::start_with_next([=](const Ui::ScrollToRequest &request) { + scrollTo(request); + }, _inner->lifetime()); +} + +not_null Widget::peer() const { + return _inner->peer(); +} + +FullMsgId Widget::contextId() const { + return _inner->contextId(); } bool Widget::showInternal(not_null memento) { @@ -701,7 +83,7 @@ bool Widget::showInternal(not_null memento) { } rpl::producer Widget::title() { - return controller()->key().statisticsContextId() + return _inner->contextId() ? tr::lng_stats_message_title() : tr::lng_stats_title(); } @@ -711,7 +93,7 @@ rpl::producer Widget::desiredShadowVisibility() const { } void Widget::showFinished() { - _showFinished.fire({}); + _inner->showFinished(); } std::shared_ptr Widget::doCreateMemento() { diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_widget.h b/Telegram/SourceFiles/info/statistics/info_statistics_widget.h index b6c17cb33..de521d11a 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_widget.h +++ b/Telegram/SourceFiles/info/statistics/info_statistics_widget.h @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::Statistics { +class InnerWidget; + class Memento final : public ContentMemento { public: Memento(not_null controller); @@ -35,10 +37,13 @@ public: rpl::producer desiredShadowVisibility() const override; void showFinished() override; + [[nodiscard]] not_null peer() const; + [[nodiscard]] FullMsgId contextId() const; + private: std::shared_ptr doCreateMemento() override; - rpl::event_stream<> _showFinished; + const not_null _inner; };