info_statistics_recent_message.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  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_recent_message.h"
  8. #include "base/unixtime.h"
  9. #include "core/ui_integration.h"
  10. #include "data/data_document.h"
  11. #include "data/data_document_media.h"
  12. #include "data/data_file_origin.h"
  13. #include "data/data_photo.h"
  14. #include "data/data_photo_media.h"
  15. #include "data/data_session.h"
  16. #include "data/data_story.h"
  17. #include "history/history.h"
  18. #include "history/history_item.h"
  19. #include "history/history_item_helpers.h"
  20. #include "history/view/history_view_item_preview.h"
  21. #include "info/statistics/info_statistics_common.h"
  22. #include "lang/lang_keys.h"
  23. #include "main/main_session.h"
  24. #include "ui/controls/userpic_button.h"
  25. #include "ui/effects/outline_segments.h" // UnreadStoryOutlineGradient
  26. #include "ui/effects/ripple_animation.h"
  27. #include "ui/effects/spoiler_mess.h"
  28. #include "ui/painter.h"
  29. #include "ui/power_saving.h"
  30. #include "ui/rect.h"
  31. #include "ui/text/format_values.h"
  32. #include "ui/text/text_options.h"
  33. #include "styles/style_boxes.h"
  34. #include "styles/style_dialogs.h"
  35. #include "styles/style_layers.h"
  36. #include "styles/style_statistics.h"
  37. namespace Info::Statistics {
  38. namespace {
  39. [[nodiscard]] QImage PreparePreviewImage(
  40. QImage original,
  41. ImageRoundRadius radius,
  42. int size,
  43. bool spoiler) {
  44. if (original.width() * 10 < original.height()
  45. || original.height() * 10 < original.width()) {
  46. return QImage();
  47. }
  48. const auto factor = style::DevicePixelRatio();
  49. size *= factor;
  50. const auto scaled = original.scaled(
  51. QSize(size, size),
  52. Qt::KeepAspectRatioByExpanding,
  53. Qt::FastTransformation);
  54. auto square = scaled.copy(
  55. (scaled.width() - size) / 2,
  56. (scaled.height() - size) / 2,
  57. size,
  58. size
  59. ).convertToFormat(QImage::Format_ARGB32_Premultiplied);
  60. if (spoiler) {
  61. square = Images::BlurLargeImage(
  62. std::move(square),
  63. style::ConvertScale(3) * factor);
  64. }
  65. square = Images::Round(std::move(square), radius);
  66. square.setDevicePixelRatio(factor);
  67. return square;
  68. }
  69. } // namespace
  70. MessagePreview::MessagePreview(
  71. not_null<Ui::RpWidget*> parent,
  72. not_null<HistoryItem*> item,
  73. QImage cachedPreview)
  74. : Ui::RpWidget(parent)
  75. , _messageId(item->fullId())
  76. , _date(
  77. st::statisticsHeaderTitleTextStyle,
  78. Ui::FormatDateTime(ItemDateTime(item)))
  79. , _preview(std::move(cachedPreview)) {
  80. _text.setMarkedText(
  81. st::defaultPeerListItem.nameStyle,
  82. item->toPreview({ .generateImages = false }).text,
  83. Ui::DialogTextOptions(),
  84. Core::TextContext({
  85. .session = &item->history()->session(),
  86. .repaint = [=] { update(); },
  87. }));
  88. if (item->media() && item->media()->hasSpoiler()) {
  89. _spoiler = std::make_unique<Ui::SpoilerAnimation>([=] { update(); });
  90. }
  91. if (_preview.isNull()) {
  92. if (const auto media = item->media()) {
  93. if (const auto photo = media->photo()) {
  94. _photoMedia = photo->createMediaView();
  95. _photoMedia->wanted(Data::PhotoSize::Large, item->fullId());
  96. } else if (const auto document = media->document()) {
  97. _documentMedia = document->createMediaView();
  98. _documentMedia->thumbnailWanted(item->fullId());
  99. }
  100. processPreview();
  101. }
  102. if ((!_documentMedia || _documentMedia->thumbnailSize().isNull())
  103. && !_photoMedia) {
  104. const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
  105. this,
  106. item->history()->peer,
  107. st::statisticsRecentPostUserpic);
  108. userpic->move(st::peerListBoxItem.photoPosition);
  109. userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
  110. }
  111. }
  112. }
  113. MessagePreview::MessagePreview(
  114. not_null<Ui::RpWidget*> parent,
  115. not_null<Data::Story*> story,
  116. QImage cachedPreview)
  117. : Ui::RpWidget(parent)
  118. , _storyId(story->fullId())
  119. , _date(
  120. st::statisticsHeaderTitleTextStyle,
  121. Ui::FormatDateTime(base::unixtime::parse(story->date())))
  122. , _preview(std::move(cachedPreview)) {
  123. _text.setMarkedText(
  124. st::defaultPeerListItem.nameStyle,
  125. { tr::lng_in_dlg_story(tr::now) },
  126. Ui::DialogTextOptions(),
  127. Core::TextContext({
  128. .session = &story->peer()->session(),
  129. .repaint = [=] { update(); },
  130. }));
  131. if (_preview.isNull()) {
  132. if (const auto photo = story->photo()) {
  133. _photoMedia = photo->createMediaView();
  134. _photoMedia->wanted(Data::PhotoSize::Large, story->fullId());
  135. } else if (const auto document = story->document()) {
  136. _documentMedia = document->createMediaView();
  137. _documentMedia->thumbnailWanted(story->fullId());
  138. }
  139. processPreview();
  140. }
  141. }
  142. void MessagePreview::setInfo(int views, int shares, int reactions) {
  143. _views = Ui::Text::String(
  144. st::defaultPeerListItem.nameStyle,
  145. (views >= 0)
  146. ? tr::lng_stats_recent_messages_views(
  147. tr::now,
  148. lt_count_decimal,
  149. views)
  150. : QString());
  151. _shares = Ui::Text::String(
  152. st::statisticsHeaderTitleTextStyle,
  153. (shares > 0) ? Lang::FormatCountDecimal(shares) : QString());
  154. _reactions = Ui::Text::String(
  155. st::statisticsHeaderTitleTextStyle,
  156. (reactions > 0) ? Lang::FormatCountDecimal(reactions) : QString());
  157. _viewsWidth = (_views.maxWidth());
  158. _sharesWidth = (_shares.maxWidth());
  159. _reactionsWidth = (_reactions.maxWidth());
  160. }
  161. void MessagePreview::processPreview() {
  162. const auto session = _photoMedia
  163. ? &_photoMedia->owner()->session()
  164. : _documentMedia
  165. ? &_documentMedia->owner()->session()
  166. : nullptr;
  167. if (!session) {
  168. return;
  169. }
  170. struct ThumbInfo final {
  171. bool loaded = false;
  172. Image *image = nullptr;
  173. };
  174. const auto computeThumbInfo = [=]() -> ThumbInfo {
  175. using Size = Data::PhotoSize;
  176. if (_documentMedia) {
  177. return { true, _documentMedia->thumbnail() };
  178. } else if (const auto large = _photoMedia->image(Size::Large)) {
  179. return { true, large };
  180. } else if (const auto thumb = _photoMedia->image(Size::Thumbnail)) {
  181. return { false, thumb };
  182. } else if (const auto small = _photoMedia->image(Size::Small)) {
  183. return { false, small };
  184. } else {
  185. return { false, _photoMedia->thumbnailInline() };
  186. }
  187. };
  188. rpl::single(rpl::empty) | rpl::then(
  189. session->downloaderTaskFinished()
  190. ) | rpl::start_with_next([=] {
  191. const auto computed = computeThumbInfo();
  192. const auto guard = gsl::finally([&] { update(); });
  193. if (!computed.image) {
  194. if (_documentMedia && !_documentMedia->owner()->hasThumbnail()) {
  195. _preview = QImage();
  196. _lifetimeDownload.destroy();
  197. }
  198. return;
  199. } else if (computed.loaded) {
  200. _lifetimeDownload.destroy();
  201. }
  202. if (_storyId) {
  203. const auto line = st::dialogsStoriesFull.lineTwice;
  204. const auto rect = Rect(Size(st::peerListBoxItem.photoSize));
  205. const auto penWidth = line / 2.;
  206. const auto offset = 1.5 * penWidth * 2;
  207. const auto preview = PreparePreviewImage(
  208. computed.image->original(),
  209. ImageRoundRadius::Ellipse,
  210. st::peerListBoxItem.photoSize - offset * 2,
  211. !!_spoiler);
  212. auto image = QImage(
  213. rect.size() * style::DevicePixelRatio(),
  214. QImage::Format_ARGB32_Premultiplied);
  215. image.setDevicePixelRatio(style::DevicePixelRatio());
  216. image.fill(Qt::transparent);
  217. {
  218. auto p = QPainter(&image);
  219. p.drawImage(offset, offset, preview);
  220. auto hq = PainterHighQualityEnabler(p);
  221. auto gradient = Ui::UnreadStoryOutlineGradient();
  222. gradient.setStart(rect.topRight());
  223. gradient.setFinalStop(rect.bottomLeft());
  224. p.setPen(QPen(gradient, penWidth));
  225. p.setBrush(Qt::NoBrush);
  226. p.drawEllipse(rect - Margins(penWidth));
  227. }
  228. _preview = std::move(image);
  229. } else {
  230. _preview = PreparePreviewImage(
  231. computed.image->original(),
  232. ImageRoundRadius::Large,
  233. st::peerListBoxItem.photoSize,
  234. !!_spoiler);
  235. }
  236. }, _lifetimeDownload);
  237. }
  238. int MessagePreview::resizeGetHeight(int newWidth) {
  239. return st::peerListBoxItem.height;
  240. }
  241. void MessagePreview::paintEvent(QPaintEvent *e) {
  242. auto p = QPainter(this);
  243. const auto padding = st::boxRowPadding.left() / 2;
  244. const auto rightSubTextWidth = 0
  245. + (_sharesWidth
  246. ? _sharesWidth
  247. + st::statisticsRecentPostShareIcon.width()
  248. + st::statisticsRecentPostIconSkip
  249. : 0)
  250. + (_reactionsWidth
  251. ? _reactionsWidth
  252. + st::statisticsRecentPostReactionIcon.width()
  253. + st::statisticsChartRulerCaptionSkip
  254. + st::statisticsRecentPostIconSkip
  255. : 0);
  256. const auto rightWidth = std::max(_viewsWidth, rightSubTextWidth)
  257. + padding;
  258. const auto left = (false && _preview.isNull())
  259. ? st::peerListBoxItem.photoPosition.x()
  260. : st::peerListBoxItem.namePosition.x();
  261. if (left) {
  262. const auto rect = QRect(
  263. st::peerListBoxItem.photoPosition,
  264. Size(st::peerListBoxItem.photoSize));
  265. p.drawImage(rect.topLeft(), _preview);
  266. if (_spoiler) {
  267. const auto paused = On(PowerSaving::kChatSpoiler);
  268. FillSpoilerRect(
  269. p,
  270. rect,
  271. Images::CornersMaskRef(
  272. Images::CornersMask(st::roundRadiusLarge)),
  273. Ui::DefaultImageSpoiler().frame(
  274. _spoiler->index(crl::now(), paused)),
  275. _cornerCache);
  276. }
  277. }
  278. const auto topTextTop = st::peerListBoxItem.namePosition.y();
  279. const auto bottomTextTop = st::peerListBoxItem.statusPosition.y();
  280. p.setBrush(Qt::NoBrush);
  281. p.setPen(st::boxTextFg);
  282. _text.draw(p, {
  283. .position = { left, topTextTop },
  284. .outerWidth = width() - left,
  285. .availableWidth = width() - rightWidth - left,
  286. .spoiler = Ui::Text::DefaultSpoilerCache(),
  287. .now = crl::now(),
  288. .elisionHeight = st::statisticsDetailsPopupHeaderStyle.font->height,
  289. .elisionLines = 1,
  290. });
  291. _views.draw(p, {
  292. .position = { width() - _viewsWidth, topTextTop },
  293. .outerWidth = _viewsWidth,
  294. .availableWidth = _viewsWidth,
  295. });
  296. p.setPen(st::windowSubTextFg);
  297. _date.draw(p, {
  298. .position = { left, bottomTextTop },
  299. .outerWidth = width() - left,
  300. .availableWidth = width() - rightWidth - left,
  301. });
  302. {
  303. auto right = width() - _sharesWidth;
  304. _shares.draw(p, {
  305. .position = { right, bottomTextTop },
  306. .outerWidth = _sharesWidth,
  307. .availableWidth = _sharesWidth,
  308. });
  309. const auto bottomTextBottom = bottomTextTop
  310. + st::statisticsHeaderTitleTextStyle.font->height
  311. - st::statisticsRecentPostIconSkip;
  312. if (_sharesWidth) {
  313. const auto &icon = st::statisticsRecentPostShareIcon;
  314. const auto iconTop = bottomTextBottom - icon.height();
  315. right -= st::statisticsRecentPostIconSkip + icon.width();
  316. icon.paint(p, { right, iconTop }, width());
  317. }
  318. right -= _reactionsWidth + st::statisticsChartRulerCaptionSkip;
  319. _reactions.draw(p, {
  320. .position = { right, bottomTextTop },
  321. .outerWidth = _reactionsWidth,
  322. .availableWidth = _reactionsWidth,
  323. });
  324. if (_reactionsWidth) {
  325. const auto &icon = st::statisticsRecentPostReactionIcon;
  326. const auto iconTop = bottomTextBottom - icon.height();
  327. right -= st::statisticsRecentPostIconSkip + icon.width();
  328. icon.paint(p, { right, iconTop }, width());
  329. }
  330. }
  331. }
  332. void MessagePreview::saveState(SavedState &state) const {
  333. if (!_lifetimeDownload) {
  334. const auto fullId = Data::RecentPostId{ _messageId, _storyId };
  335. state.recentPostPreviews[fullId] = _preview;
  336. }
  337. }
  338. } // namespace Info::Statistics