info_statistics_inner_widget.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "info/statistics/info_statistics_inner_widget.h"
  8. #include "api/api_statistics.h"
  9. #include "apiwrap.h"
  10. #include "base/call_delayed.h"
  11. #include "base/event_filter.h"
  12. #include "data/data_peer.h"
  13. #include "data/data_session.h"
  14. #include "data/data_stories.h"
  15. #include "data/data_story.h"
  16. #include "history/history_item.h"
  17. #include "info/info_controller.h"
  18. #include "info/info_memento.h"
  19. #include "info/statistics/info_statistics_list_controllers.h"
  20. #include "info/statistics/info_statistics_recent_message.h"
  21. #include "info/statistics/info_statistics_widget.h"
  22. #include "lang/lang_keys.h"
  23. #include "lottie/lottie_icon.h"
  24. #include "main/main_session.h"
  25. #include "settings/settings_common.h" // CreateLottieIcon.
  26. #include "statistics/chart_widget.h"
  27. #include "statistics/statistics_common.h"
  28. #include "statistics/statistics_format_values.h"
  29. #include "statistics/widgets/chart_header_widget.h"
  30. #include "ui/layers/generic_box.h"
  31. #include "ui/rect.h"
  32. #include "ui/vertical_list.h"
  33. #include "ui/toast/toast.h"
  34. #include "ui/widgets/buttons.h"
  35. #include "ui/widgets/popup_menu.h"
  36. #include "ui/widgets/scroll_area.h"
  37. #include "ui/wrap/slide_wrap.h"
  38. #include "styles/style_boxes.h"
  39. #include "styles/style_menu_icons.h"
  40. #include "styles/style_settings.h"
  41. #include "styles/style_statistics.h"
  42. namespace Info::Statistics {
  43. namespace {
  44. struct Descriptor final {
  45. not_null<PeerData*> peer;
  46. not_null<Api::Statistics*> api;
  47. not_null<QWidget*> toastParent;
  48. };
  49. void AddContextMenu(
  50. not_null<Ui::RpWidget*> button,
  51. not_null<Controller*> controller,
  52. not_null<HistoryItem*> item) {
  53. const auto fullId = item->fullId();
  54. const auto contextMenu = button->lifetime()
  55. .make_state<base::unique_qptr<Ui::PopupMenu>>();
  56. const auto showMenu = [=] {
  57. *contextMenu = base::make_unique_q<Ui::PopupMenu>(
  58. button,
  59. st::popupMenuWithIcons);
  60. const auto go = [=] {
  61. const auto &session = controller->parentController();
  62. if (const auto item = session->session().data().message(fullId)) {
  63. session->showMessage(item);
  64. }
  65. };
  66. contextMenu->get()->addAction(
  67. tr::lng_context_to_msg(tr::now),
  68. crl::guard(controller, go),
  69. &st::menuIconShowInChat);
  70. contextMenu->get()->popup(QCursor::pos());
  71. };
  72. base::install_event_filter(button, [=](not_null<QEvent*> e) {
  73. if (e->type() == QEvent::ContextMenu) {
  74. showMenu();
  75. return base::EventFilterResult::Cancel;
  76. }
  77. return base::EventFilterResult::Continue;
  78. });
  79. }
  80. void ProcessZoom(
  81. const Descriptor &d,
  82. not_null<Statistic::ChartWidget*> widget,
  83. const QString &zoomToken,
  84. Statistic::ChartViewType type) {
  85. if (zoomToken.isEmpty()) {
  86. return;
  87. }
  88. widget->zoomRequests(
  89. ) | rpl::start_with_next([=](float64 x) {
  90. d.api->requestZoom(
  91. zoomToken,
  92. x
  93. ) | rpl::start_with_next_error_done([=](
  94. const Data::StatisticalGraph &graph) {
  95. if (graph.chart) {
  96. widget->setZoomedChartData(graph.chart, x, type);
  97. } else if (!graph.error.isEmpty()) {
  98. Ui::Toast::Show(d.toastParent, graph.error);
  99. }
  100. }, [=](const QString &error) {
  101. }, [=] {
  102. }, widget->lifetime());
  103. }, widget->lifetime());
  104. }
  105. void FillStatistic(
  106. not_null<Ui::VerticalLayout*> content,
  107. const Descriptor &descriptor,
  108. Data::AnyStatistics &stats,
  109. Fn<void()> done) {
  110. using Type = Statistic::ChartViewType;
  111. const auto &padding = st::statisticsChartEntryPadding;
  112. const auto &m = st::statisticsLayerMargins;
  113. const auto addSkip = [&](not_null<Ui::VerticalLayout*> c) {
  114. Ui::AddSkip(c, padding.bottom());
  115. Ui::AddDivider(c);
  116. Ui::AddSkip(c, padding.top());
  117. };
  118. struct State final {
  119. Fn<void()> done;
  120. int pendingCount = 0;
  121. };
  122. const auto state = content->lifetime().make_state<State>(
  123. State{ std::move(done) });
  124. const auto singlePendingDone = [=] {
  125. state->pendingCount--;
  126. if (!state->pendingCount && state->done) {
  127. base::take(state->done)();
  128. }
  129. };
  130. const auto addChart = [&](
  131. Data::StatisticalGraph &graphData,
  132. rpl::producer<QString> &&title,
  133. Statistic::ChartViewType type) {
  134. if (graphData.chart) {
  135. const auto widget = content->add(
  136. object_ptr<Statistic::ChartWidget>(content),
  137. m);
  138. widget->setChartData(graphData.chart, type);
  139. ProcessZoom(descriptor, widget, graphData.zoomToken, type);
  140. widget->setTitle(std::move(title));
  141. addSkip(content);
  142. } else if (!graphData.zoomToken.isEmpty()) {
  143. state->pendingCount++;
  144. const auto wrap = content->add(
  145. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  146. content,
  147. object_ptr<Ui::VerticalLayout>(content)));
  148. wrap->toggle(false, anim::type::instant);
  149. const auto widget = wrap->entity()->add(
  150. object_ptr<Statistic::ChartWidget>(content),
  151. m);
  152. descriptor.api->requestZoom(
  153. graphData.zoomToken,
  154. 0
  155. ) | rpl::start_with_next_error_done([=, graphPtr = &graphData](
  156. const Data::StatisticalGraph &graph) mutable {
  157. {
  158. // Save the loaded async data to cache.
  159. // Guarded by content->lifetime().
  160. *graphPtr = graph;
  161. }
  162. if (graph.chart) {
  163. widget->setChartData(graph.chart, type);
  164. wrap->toggle(true, anim::type::instant);
  165. ProcessZoom(descriptor, widget, graph.zoomToken, type);
  166. widget->setTitle(rpl::duplicate(title));
  167. } else if (!graph.error.isEmpty()) {
  168. }
  169. }, [=](const QString &error) {
  170. singlePendingDone();
  171. }, [=] {
  172. singlePendingDone();
  173. }, content->lifetime());
  174. addSkip(wrap->entity());
  175. }
  176. };
  177. addSkip(content);
  178. if (stats.channel) {
  179. addChart(
  180. stats.channel.memberCountGraph,
  181. tr::lng_chart_title_member_count(),
  182. Type::Linear);
  183. addChart(
  184. stats.channel.joinGraph,
  185. tr::lng_chart_title_join(),
  186. Type::Linear);
  187. addChart(
  188. stats.channel.muteGraph,
  189. tr::lng_chart_title_mute(),
  190. Type::Linear);
  191. addChart(
  192. stats.channel.viewCountByHourGraph,
  193. tr::lng_chart_title_view_count_by_hour(),
  194. Type::Linear);
  195. addChart(
  196. stats.channel.viewCountBySourceGraph,
  197. tr::lng_chart_title_view_count_by_source(),
  198. Type::StackBar);
  199. addChart(
  200. stats.channel.joinBySourceGraph,
  201. tr::lng_chart_title_join_by_source(),
  202. Type::StackBar);
  203. addChart(
  204. stats.channel.languageGraph,
  205. tr::lng_chart_title_language(),
  206. Type::StackLinear);
  207. addChart(
  208. stats.channel.messageInteractionGraph,
  209. tr::lng_chart_title_message_interaction(),
  210. Type::DoubleLinear);
  211. addChart(
  212. stats.channel.instantViewInteractionGraph,
  213. tr::lng_chart_title_instant_view_interaction(),
  214. Type::DoubleLinear);
  215. addChart(
  216. stats.channel.reactionsByEmotionGraph,
  217. tr::lng_chart_title_reactions_by_emotion(),
  218. Type::Bar);
  219. addChart(
  220. stats.channel.storyInteractionsGraph,
  221. tr::lng_chart_title_story_interactions(),
  222. Type::DoubleLinear);
  223. addChart(
  224. stats.channel.storyReactionsByEmotionGraph,
  225. tr::lng_chart_title_story_reactions_by_emotion(),
  226. Type::Bar);
  227. } else if (stats.supergroup) {
  228. addChart(
  229. stats.supergroup.memberCountGraph,
  230. tr::lng_chart_title_member_count(),
  231. Type::Linear);
  232. addChart(
  233. stats.supergroup.joinGraph,
  234. tr::lng_chart_title_group_join(),
  235. Type::Linear);
  236. addChart(
  237. stats.supergroup.joinBySourceGraph,
  238. tr::lng_chart_title_group_join_by_source(),
  239. Type::StackBar);
  240. addChart(
  241. stats.supergroup.languageGraph,
  242. tr::lng_chart_title_group_language(),
  243. Type::StackLinear);
  244. addChart(
  245. stats.supergroup.messageContentGraph,
  246. tr::lng_chart_title_group_message_content(),
  247. Type::StackBar);
  248. addChart(
  249. stats.supergroup.actionGraph,
  250. tr::lng_chart_title_group_action(),
  251. Type::DoubleLinear);
  252. addChart(
  253. stats.supergroup.dayGraph,
  254. tr::lng_chart_title_group_day(),
  255. Type::Linear);
  256. addChart(
  257. stats.supergroup.weekGraph,
  258. tr::lng_chart_title_group_week(),
  259. Type::StackLinear);
  260. } else {
  261. auto &messageOrStory = stats.message
  262. ? stats.message
  263. : stats.story;
  264. if (messageOrStory) {
  265. addChart(
  266. messageOrStory.messageInteractionGraph,
  267. tr::lng_chart_title_message_interaction(),
  268. Type::DoubleLinear);
  269. addChart(
  270. messageOrStory.reactionsByEmotionGraph,
  271. tr::lng_chart_title_reactions_by_emotion(),
  272. Type::Bar);
  273. }
  274. }
  275. if (!state->pendingCount) {
  276. ++state->pendingCount;
  277. singlePendingDone();
  278. }
  279. }
  280. void AddHeader(
  281. not_null<Ui::VerticalLayout*> content,
  282. tr::phrase<> text,
  283. const Data::AnyStatistics &stats) {
  284. const auto startDate = stats.channel
  285. ? stats.channel.startDate
  286. : stats.supergroup.startDate;
  287. const auto endDate = stats.channel
  288. ? stats.channel.endDate
  289. : stats.supergroup.endDate;
  290. const auto header = content->add(
  291. object_ptr<Statistic::Header>(content),
  292. st::statisticsLayerMargins + st::statisticsChartHeaderPadding);
  293. header->resizeToWidth(header->width());
  294. header->setTitle(text(tr::now));
  295. if (!endDate || !startDate) {
  296. header->setSubTitle({});
  297. return;
  298. }
  299. header->setSubTitle(Statistic::LangDayMonthYear(startDate)
  300. + ' '
  301. + QChar(8212)
  302. + ' '
  303. + Statistic::LangDayMonthYear(endDate));
  304. }
  305. void FillOverview(
  306. not_null<Ui::VerticalLayout*> content,
  307. const Data::AnyStatistics &stats,
  308. bool isChannelStoryStats) {
  309. using Value = Data::StatisticalValue;
  310. const auto &channel = stats.channel;
  311. const auto &supergroup = stats.supergroup;
  312. if (!isChannelStoryStats) {
  313. Ui::AddSkip(content, st::statisticsLayerOverviewMargins.top());
  314. AddHeader(content, tr::lng_stats_overview_title, stats);
  315. Ui::AddSkip(content);
  316. }
  317. struct Second final {
  318. QColor color;
  319. QString text;
  320. };
  321. const auto parseSecond = [&](const Value &v) -> Second {
  322. const auto diff = v.value - v.previousValue;
  323. if (!diff || !v.previousValue) {
  324. return {};
  325. }
  326. constexpr auto kTooMuchDiff = int(1'000'000);
  327. const auto diffAbs = std::abs(diff);
  328. const auto diffText = diffAbs > kTooMuchDiff
  329. ? Lang::FormatCountToShort(std::abs(diff)).string
  330. : QString::number(diffAbs);
  331. const auto percentage = std::abs(v.growthRatePercentage);
  332. const auto precision = (percentage == int(percentage)) ? 0 : 1;
  333. return {
  334. (diff < 0 ? st::menuIconAttentionColor : st::settingsIconBg2)->c,
  335. QString("%1%2 (%3%)")
  336. .arg((diff < 0) ? QChar(0x2212) : QChar(0x002B))
  337. .arg(diffText)
  338. .arg(QString::number(percentage, 'f', precision))
  339. };
  340. };
  341. const auto diffBetweenHeaders = 0
  342. + st::statisticsOverviewValue.style.font->height
  343. - st::statisticsHeaderTitleTextStyle.font->height;
  344. const auto container = content->add(
  345. object_ptr<Ui::RpWidget>(content),
  346. st::statisticsLayerMargins);
  347. const auto addPrimary = [&](const Value &v) {
  348. return Ui::CreateChild<Ui::FlatLabel>(
  349. container,
  350. (v.value >= 0)
  351. ? Lang::FormatCountToShort(v.value).string
  352. : QString(),
  353. st::statisticsOverviewValue);
  354. };
  355. const auto addSub = [&](
  356. not_null<Ui::RpWidget*> primary,
  357. const Value &v,
  358. tr::phrase<> text) {
  359. const auto data = parseSecond(v);
  360. const auto second = Ui::CreateChild<Ui::FlatLabel>(
  361. container,
  362. data.text,
  363. st::statisticsOverviewSecondValue);
  364. second->setTextColorOverride(data.color);
  365. const auto sub = Ui::CreateChild<Ui::FlatLabel>(
  366. container,
  367. text(),
  368. st::statisticsOverviewSubtext);
  369. sub->setTextColorOverride(st::windowSubTextFg->c);
  370. primary->geometryValue(
  371. ) | rpl::start_with_next([=](const QRect &g) {
  372. const auto &padding = st::statisticsOverviewSecondValuePadding;
  373. second->moveToLeft(
  374. rect::right(g) + padding.left(),
  375. g.y() + padding.top());
  376. sub->moveToLeft(
  377. g.x(),
  378. st::statisticsChartHeaderHeight
  379. - st::statisticsOverviewSubtext.style.font->height
  380. + g.y()
  381. + diffBetweenHeaders);
  382. if (container->height() < rect::bottom(sub)) {
  383. container->resize(container->width(), rect::bottom(sub));
  384. }
  385. }, primary->lifetime());
  386. };
  387. const auto isChannel = (!!channel);
  388. const auto &messageOrStory = stats.message ? stats.message : stats.story;
  389. const auto isMessage = (!!messageOrStory);
  390. const auto hasPostReactions = isChannel
  391. && (channel.meanReactionCount.value
  392. || channel.meanReactionCount.previousValue);
  393. const auto topLeftLabel = (isChannelStoryStats && isChannel)
  394. ? addPrimary(channel.meanShareCount)
  395. : isChannel
  396. ? addPrimary(channel.memberCount)
  397. : isMessage
  398. ? addPrimary({ .value = float64(messageOrStory.views) })
  399. : addPrimary(supergroup.memberCount);
  400. const auto topRightLabel = (isChannelStoryStats && isChannel)
  401. ? addPrimary(channel.meanStoryShareCount)
  402. : isChannel
  403. ? Ui::CreateChild<Ui::FlatLabel>(
  404. container,
  405. QString("%1%").arg(0.01
  406. * std::round(channel.enabledNotificationsPercentage * 100.)),
  407. st::statisticsOverviewValue)
  408. : isMessage
  409. ? addPrimary({ .value = float64(messageOrStory.publicForwards) })
  410. : addPrimary(supergroup.messageCount);
  411. const auto bottomLeftLabel = (isChannelStoryStats && isChannel)
  412. ? addPrimary(hasPostReactions
  413. ? channel.meanReactionCount
  414. : channel.meanStoryReactionCount)
  415. : isChannel
  416. ? addPrimary(channel.meanViewCount)
  417. : isMessage
  418. ? addPrimary({ .value = float64(messageOrStory.reactions) })
  419. : addPrimary(supergroup.viewerCount);
  420. const auto bottomRightLabel = (isChannelStoryStats && isChannel)
  421. ? addPrimary(!hasPostReactions
  422. ? Value{ .value = -1 }
  423. : channel.meanStoryReactionCount)
  424. : isChannel
  425. ? addPrimary(channel.meanStoryViewCount)
  426. : isMessage
  427. ? addPrimary({ .value = float64(messageOrStory.privateForwards) })
  428. : addPrimary(supergroup.senderCount);
  429. if (isChannelStoryStats && isChannel) {
  430. addSub(
  431. topLeftLabel,
  432. channel.meanShareCount,
  433. tr::lng_stats_overview_mean_share_count);
  434. addSub(
  435. topRightLabel,
  436. channel.meanStoryShareCount,
  437. tr::lng_stats_overview_mean_story_share_count);
  438. addSub(
  439. bottomLeftLabel,
  440. hasPostReactions
  441. ? channel.meanReactionCount
  442. : channel.meanStoryReactionCount,
  443. hasPostReactions
  444. ? tr::lng_stats_overview_mean_reactions_count
  445. : tr::lng_stats_overview_mean_story_reactions_count);
  446. if (hasPostReactions) {
  447. addSub(
  448. bottomRightLabel,
  449. channel.meanStoryReactionCount,
  450. tr::lng_stats_overview_mean_story_reactions_count);
  451. }
  452. } else if (const auto &s = channel) {
  453. addSub(
  454. topLeftLabel,
  455. s.memberCount,
  456. tr::lng_stats_overview_member_count);
  457. addSub(
  458. topRightLabel,
  459. {},
  460. tr::lng_stats_overview_enabled_notifications);
  461. addSub(
  462. bottomLeftLabel,
  463. s.meanViewCount,
  464. tr::lng_stats_overview_mean_view_count);
  465. addSub(
  466. bottomRightLabel,
  467. s.meanStoryViewCount,
  468. tr::lng_stats_overview_mean_story_view_count);
  469. } else if (const auto &s = supergroup) {
  470. addSub(
  471. topLeftLabel,
  472. s.memberCount,
  473. tr::lng_manage_peer_members);
  474. addSub(
  475. topRightLabel,
  476. s.messageCount,
  477. tr::lng_stats_overview_messages);
  478. addSub(
  479. bottomLeftLabel,
  480. s.viewerCount,
  481. tr::lng_stats_overview_group_mean_view_count);
  482. addSub(
  483. bottomRightLabel,
  484. s.senderCount,
  485. tr::lng_stats_overview_group_mean_post_count);
  486. } else if (const auto &s = messageOrStory) {
  487. if (s.views >= 0) {
  488. addSub(
  489. topLeftLabel,
  490. {},
  491. tr::lng_stats_overview_message_views);
  492. }
  493. if (s.publicForwards >= 0) {
  494. addSub(
  495. topRightLabel,
  496. {},
  497. tr::lng_stats_overview_message_public_shares);
  498. }
  499. if (s.reactions >= 0) {
  500. addSub(
  501. bottomLeftLabel,
  502. {},
  503. tr::lng_manage_peer_reactions);
  504. }
  505. if (s.privateForwards >= 0) {
  506. addSub(
  507. bottomRightLabel,
  508. {},
  509. tr::lng_stats_overview_message_private_shares);
  510. }
  511. }
  512. container->showChildren();
  513. container->sizeValue() | rpl::distinct_until_changed(
  514. ) | rpl::start_with_next([=](const QSize &s) {
  515. const auto halfWidth = s.width() / 2;
  516. {
  517. const auto &p = st::statisticsOverviewValuePadding;
  518. topLeftLabel->moveToLeft(p.left(), p.top());
  519. }
  520. topRightLabel->moveToLeft(
  521. topLeftLabel->x() + halfWidth + st::statisticsOverviewRightSkip,
  522. topLeftLabel->y());
  523. bottomLeftLabel->moveToLeft(
  524. topLeftLabel->x(),
  525. topLeftLabel->y() + st::statisticsOverviewMidSkip);
  526. bottomRightLabel->moveToLeft(
  527. topRightLabel->x(),
  528. bottomLeftLabel->y());
  529. }, container->lifetime());
  530. Ui::AddSkip(content, st::statisticsLayerOverviewMargins.bottom());
  531. }
  532. } // namespace
  533. void FillLoading(
  534. not_null<Ui::VerticalLayout*> container,
  535. LoadingType type,
  536. rpl::producer<bool> toggleOn,
  537. rpl::producer<> showFinished) {
  538. const auto emptyWrap = container->add(
  539. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  540. container,
  541. object_ptr<Ui::VerticalLayout>(container)));
  542. emptyWrap->toggleOn(std::move(toggleOn), anim::type::instant);
  543. const auto content = emptyWrap->entity();
  544. const auto iconName = (type == LoadingType::Boosts)
  545. ? u"stats_boosts"_q
  546. : (type == LoadingType::Earn)
  547. ? u"stats_earn"_q
  548. : u"stats"_q;
  549. auto icon = ::Settings::CreateLottieIcon(
  550. content,
  551. { .name = iconName, .sizeOverride = Size(st::changePhoneIconSize) },
  552. st::settingsBlockedListIconPadding);
  553. (
  554. std::move(showFinished) | rpl::take(1)
  555. ) | rpl::start_with_next([animate = std::move(icon.animate)] {
  556. animate(anim::repeat::loop);
  557. }, icon.widget->lifetime());
  558. content->add(std::move(icon.widget));
  559. content->add(
  560. object_ptr<Ui::CenterWrap<>>(
  561. content,
  562. object_ptr<Ui::FlatLabel>(
  563. content,
  564. (type == LoadingType::Boosts)
  565. ? tr::lng_stats_boosts_loading()
  566. : (type == LoadingType::Earn)
  567. ? tr::lng_stats_earn_loading()
  568. : tr::lng_stats_loading(),
  569. st::changePhoneTitle)),
  570. st::changePhoneTitlePadding + st::boxRowPadding);
  571. content->add(
  572. object_ptr<Ui::CenterWrap<>>(
  573. content,
  574. object_ptr<Ui::FlatLabel>(
  575. content,
  576. (type == LoadingType::Boosts)
  577. ? tr::lng_stats_boosts_loading_subtext()
  578. : (type == LoadingType::Earn)
  579. ? tr::lng_stats_earn_loading_subtext()
  580. : tr::lng_stats_loading_subtext(),
  581. st::statisticsLoadingSubtext)),
  582. st::changePhoneDescriptionPadding + st::boxRowPadding);
  583. Ui::AddSkip(content, st::settingsBlockedListIconPadding.top());
  584. }
  585. InnerWidget::InnerWidget(
  586. QWidget *parent,
  587. not_null<Controller*> controller,
  588. not_null<PeerData*> peer,
  589. FullMsgId contextId,
  590. FullStoryId storyId)
  591. : VerticalLayout(parent)
  592. , _controller(controller)
  593. , _peer(peer)
  594. , _contextId(contextId)
  595. , _storyId(storyId) {
  596. }
  597. void InnerWidget::load() {
  598. const auto inner = this;
  599. const auto descriptor = Descriptor{
  600. _peer,
  601. lifetime().make_state<Api::Statistics>(_peer->asChannel()),
  602. _controller->uiShow()->toastParent(),
  603. };
  604. FillLoading(
  605. inner,
  606. Info::Statistics::LoadingType::Statistic,
  607. _loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1),
  608. _showFinished.events());
  609. _showFinished.events(
  610. ) | rpl::take(1) | rpl::start_with_next([=] {
  611. if (!_contextId && !_storyId) {
  612. descriptor.api->request(
  613. ) | rpl::start_with_done([=] {
  614. _state.stats = Data::AnyStatistics{
  615. descriptor.api->channelStats(),
  616. descriptor.api->supergroupStats(),
  617. };
  618. fill();
  619. }, lifetime());
  620. } else {
  621. const auto lifetimeApi = lifetime().make_state<rpl::lifetime>();
  622. const auto api = _storyId
  623. ? lifetimeApi->make_state<Api::MessageStatistics>(
  624. descriptor.peer->asChannel(),
  625. _storyId)
  626. : lifetimeApi->make_state<Api::MessageStatistics>(
  627. descriptor.peer->asChannel(),
  628. _contextId);
  629. api->request([=](const Data::StoryStatistics &data) {
  630. _state.stats = Data::AnyStatistics{
  631. .message = _contextId ? data : Data::StoryStatistics(),
  632. .story = _storyId ? data : Data::StoryStatistics(),
  633. };
  634. if (_contextId || _storyId) {
  635. _state.publicForwardsFirstSlice = api->firstSlice();
  636. }
  637. fill();
  638. lifetimeApi->destroy();
  639. });
  640. }
  641. }, lifetime());
  642. }
  643. void InnerWidget::fill() {
  644. const auto wrap = this->add(
  645. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  646. this,
  647. object_ptr<Ui::VerticalLayout>(this)));
  648. wrap->toggle(false, anim::type::instant);
  649. const auto inner = wrap->entity();
  650. const auto descriptor = Descriptor{
  651. _peer,
  652. lifetime().make_state<Api::Statistics>(_peer->asChannel()),
  653. _controller->uiShow()->toastParent(),
  654. };
  655. const auto finishLoading = [=] {
  656. _loaded.fire(true);
  657. wrap->toggle(true, anim::type::instant);
  658. this->resizeToWidth(width());
  659. this->showChildren();
  660. };
  661. if (_state.stats.message) {
  662. if (const auto i = _peer->owner().message(_contextId)) {
  663. Ui::AddSkip(inner);
  664. const auto preview = inner->add(
  665. object_ptr<MessagePreview>(inner, i, QImage()));
  666. AddContextMenu(preview, _controller, i);
  667. Ui::AddSkip(inner);
  668. Ui::AddDivider(inner);
  669. }
  670. } else if (_state.stats.story) {
  671. if (const auto story = _peer->owner().stories().lookup(_storyId)) {
  672. Ui::AddSkip(inner);
  673. const auto preview = inner->add(
  674. object_ptr<MessagePreview>(inner, *story, QImage()));
  675. preview->setAttribute(Qt::WA_TransparentForMouseEvents);
  676. Ui::AddSkip(inner);
  677. Ui::AddDivider(inner);
  678. }
  679. }
  680. FillOverview(inner, _state.stats, false);
  681. if (_state.stats.channel) {
  682. FillOverview(inner, _state.stats, true);
  683. }
  684. FillStatistic(inner, descriptor, _state.stats, finishLoading);
  685. const auto &channel = _state.stats.channel;
  686. const auto &supergroup = _state.stats.supergroup;
  687. if (channel) {
  688. fillRecentPosts(inner);
  689. } else if (supergroup) {
  690. const auto showPeerInfo = [=](not_null<PeerData*> peer) {
  691. _showRequests.fire({ .info = peer->id });
  692. };
  693. const auto addSkip = [&](not_null<Ui::VerticalLayout*> c) {
  694. Ui::AddSkip(c);
  695. Ui::AddDivider(c);
  696. Ui::AddSkip(c);
  697. Ui::AddSkip(c);
  698. };
  699. if (!supergroup.topSenders.empty()) {
  700. AddMembersList(
  701. { .topSenders = supergroup.topSenders },
  702. inner,
  703. showPeerInfo,
  704. descriptor.peer,
  705. tr::lng_stats_members_title());
  706. }
  707. if (!supergroup.topAdministrators.empty()) {
  708. addSkip(inner);
  709. AddMembersList(
  710. { .topAdministrators
  711. = supergroup.topAdministrators },
  712. inner,
  713. showPeerInfo,
  714. descriptor.peer,
  715. tr::lng_stats_admins_title());
  716. }
  717. if (!supergroup.topInviters.empty()) {
  718. addSkip(inner);
  719. AddMembersList(
  720. { .topInviters = supergroup.topInviters },
  721. inner,
  722. showPeerInfo,
  723. descriptor.peer,
  724. tr::lng_stats_inviters_title());
  725. }
  726. } else if (_state.stats.message || _state.stats.story) {
  727. using namespace Data;
  728. AddPublicForwards(
  729. _state.publicForwardsFirstSlice,
  730. inner,
  731. [=](RecentPostId id) {
  732. _showRequests.fire({
  733. .info = (!id.messageId && !id.storyId)
  734. ? id.messageId.peer
  735. : PeerId(0),
  736. .history = id.messageId,
  737. .story = id.storyId,
  738. });
  739. },
  740. descriptor.peer,
  741. RecentPostId{ .messageId = _contextId, .storyId = _storyId });
  742. }
  743. }
  744. void InnerWidget::fillRecentPosts(not_null<Ui::VerticalLayout*> container) {
  745. const auto &stats = _state.stats.channel;
  746. if (!stats || stats.recentMessageInteractions.empty()) {
  747. return;
  748. }
  749. _messagePreviews.reserve(stats.recentMessageInteractions.size());
  750. const auto wrap = container->add(
  751. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  752. container,
  753. object_ptr<Ui::VerticalLayout>(container)));
  754. const auto content = wrap->entity();
  755. AddHeader(content, tr::lng_stats_recent_messages_title, { stats, {} });
  756. Ui::AddSkip(content);
  757. const auto addMessage = [=](
  758. not_null<Ui::VerticalLayout*> messageWrap,
  759. HistoryItem *maybeItem,
  760. Data::Story *maybeStory,
  761. const Data::StatisticsMessageInteractionInfo &info) {
  762. const auto button = messageWrap->add(
  763. object_ptr<Ui::SettingsButton>(
  764. messageWrap,
  765. rpl::never<QString>(),
  766. st::statisticsRecentPostButton));
  767. const auto fullRecentId = Data::RecentPostId{
  768. .messageId = maybeItem ? maybeItem->fullId() : FullMsgId(),
  769. .storyId = maybeStory ? maybeStory->fullId() : FullStoryId(),
  770. };
  771. auto it = _state.recentPostPreviews.find(fullRecentId);
  772. auto cachedPreview = (it != end(_state.recentPostPreviews))
  773. ? base::take(it->second)
  774. : QImage();
  775. const auto raw = maybeItem
  776. ? Ui::CreateChild<MessagePreview>(
  777. button,
  778. maybeItem,
  779. std::move(cachedPreview))
  780. : Ui::CreateChild<MessagePreview>(
  781. button,
  782. maybeStory,
  783. std::move(cachedPreview));
  784. raw->setInfo(
  785. info.viewsCount,
  786. info.forwardsCount,
  787. info.reactionsCount);
  788. if (maybeItem) {
  789. AddContextMenu(button, _controller, maybeItem);
  790. }
  791. _messagePreviews.push_back(raw);
  792. raw->show();
  793. button->sizeValue(
  794. ) | rpl::start_with_next([=](const QSize &s) {
  795. if (!s.isNull()) {
  796. raw->setGeometry(Rect(s)
  797. - st::statisticsRecentPostButton.padding);
  798. }
  799. }, raw->lifetime());
  800. button->setClickedCallback([=] {
  801. _showRequests.fire({
  802. .messageStatistic = fullRecentId.messageId,
  803. .storyStatistic = fullRecentId.storyId,
  804. });
  805. });
  806. Ui::AddSkip(messageWrap);
  807. if (!wrap->toggled()) {
  808. wrap->toggle(true, anim::type::normal);
  809. }
  810. };
  811. const auto buttonWrap = container->add(
  812. object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
  813. container,
  814. object_ptr<Ui::SettingsButton>(
  815. container,
  816. tr::lng_stories_show_more())));
  817. constexpr auto kFirstPage = int(10);
  818. constexpr auto kPerPage = int(30);
  819. const auto max = int(stats.recentMessageInteractions.size());
  820. if (_state.recentPostsExpanded) {
  821. _state.recentPostsExpanded = std::max(
  822. _state.recentPostsExpanded - kPerPage,
  823. 0);
  824. }
  825. const auto showMore = [=] {
  826. const auto from = _state.recentPostsExpanded;
  827. _state.recentPostsExpanded = std::min(
  828. max,
  829. _state.recentPostsExpanded
  830. ? (_state.recentPostsExpanded + kPerPage)
  831. : kFirstPage);
  832. if (_state.recentPostsExpanded == max) {
  833. buttonWrap->toggle(false, anim::type::instant);
  834. }
  835. for (auto i = from; i < _state.recentPostsExpanded; i++) {
  836. const auto &recent = stats.recentMessageInteractions[i];
  837. const auto messageWrap = content->add(
  838. object_ptr<Ui::VerticalLayout>(content));
  839. auto &data = _peer->owner();
  840. if (recent.messageId) {
  841. const auto fullId = FullMsgId(_peer->id, recent.messageId);
  842. if (const auto item = data.message(fullId)) {
  843. addMessage(messageWrap, item, nullptr, recent);
  844. continue;
  845. }
  846. const auto callback = crl::guard(content, [=] {
  847. if (const auto item = _peer->owner().message(fullId)) {
  848. addMessage(messageWrap, item, nullptr, recent);
  849. content->resizeToWidth(content->width());
  850. }
  851. });
  852. _peer->session().api().requestMessageData(
  853. _peer,
  854. fullId.msg,
  855. callback);
  856. } else if (recent.storyId) {
  857. const auto fullId = FullStoryId{ _peer->id, recent.storyId };
  858. if (const auto story = data.stories().lookup(fullId)) {
  859. addMessage(messageWrap, nullptr, *story, recent);
  860. continue;
  861. }
  862. }
  863. }
  864. container->resizeToWidth(container->width());
  865. };
  866. const auto delay = st::defaultRippleAnimation.hideDuration;
  867. buttonWrap->entity()->setClickedCallback([=] {
  868. base::call_delayed(delay, crl::guard(container, showMore));
  869. });
  870. showMore();
  871. if (_messagePreviews.empty()) {
  872. wrap->toggle(false, anim::type::instant);
  873. }
  874. }
  875. void InnerWidget::saveState(not_null<Memento*> memento) {
  876. for (const auto &message : _messagePreviews) {
  877. message->saveState(_state);
  878. }
  879. memento->setState(base::take(_state));
  880. }
  881. void InnerWidget::restoreState(not_null<Memento*> memento) {
  882. _state = memento->state();
  883. if (_state.stats.channel
  884. || _state.stats.supergroup
  885. || _state.stats.message
  886. || _state.stats.story) {
  887. fill();
  888. } else {
  889. load();
  890. }
  891. Ui::RpWidget::resizeToWidth(width());
  892. }
  893. rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
  894. return _scrollToRequests.events();
  895. }
  896. auto InnerWidget::showRequests() const -> rpl::producer<ShowRequest> {
  897. return _showRequests.events();
  898. }
  899. void InnerWidget::showFinished() {
  900. _showFinished.fire({});
  901. }
  902. } // namespace Info::Statistics