/* 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_recent_message.h" #include "base/unixtime.h" #include "core/ui_integration.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_origin.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_story.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/view/history_view_item_preview.h" #include "info/statistics/info_statistics_common.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/effects/outline_segments.h" // UnreadStoryOutlineGradient #include "ui/effects/ripple_animation.h" #include "ui/effects/spoiler_mess.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/rect.h" #include "ui/text/format_values.h" #include "ui/text/text_options.h" #include "styles/style_boxes.h" #include "styles/style_dialogs.h" #include "styles/style_layers.h" #include "styles/style_statistics.h" namespace Info::Statistics { namespace { [[nodiscard]] QImage PreparePreviewImage( QImage original, ImageRoundRadius radius, bool spoiler) { if (original.width() * 10 < original.height() || original.height() * 10 < original.width()) { return QImage(); } const auto factor = style::DevicePixelRatio(); const auto size = st::peerListBoxItem.photoSize * factor; const auto scaled = original.scaled( QSize(size, size), Qt::KeepAspectRatioByExpanding, Qt::FastTransformation); auto square = scaled.copy( (scaled.width() - size) / 2, (scaled.height() - size) / 2, size, size ).convertToFormat(QImage::Format_ARGB32_Premultiplied); if (spoiler) { square = Images::BlurLargeImage( std::move(square), style::ConvertScale(3) * factor); } square = Images::Round(std::move(square), radius); square.setDevicePixelRatio(factor); return square; } } // namespace MessagePreview::MessagePreview( not_null parent, not_null item, QImage cachedPreview) : Ui::RpWidget(parent) , _messageId(item->fullId()) , _date( st::statisticsHeaderTitleTextStyle, Ui::FormatDateTime(ItemDateTime(item))) , _preview(std::move(cachedPreview)) { _text.setMarkedText( st::defaultPeerListItem.nameStyle, item->toPreview({ .generateImages = false }).text, Ui::DialogTextOptions(), Core::MarkedTextContext{ .session = &item->history()->session(), .customEmojiRepaint = [=] { update(); }, }); if (item->media() && item->media()->hasSpoiler()) { _spoiler = std::make_unique([=] { update(); }); } if (_preview.isNull()) { if (const auto media = item->media()) { if (const auto photo = media->photo()) { _photoMedia = photo->createMediaView(); _photoMedia->wanted(Data::PhotoSize::Large, item->fullId()); } else if (const auto document = media->document()) { _documentMedia = document->createMediaView(); _documentMedia->thumbnailWanted(item->fullId()); } } processPreview(); } } MessagePreview::MessagePreview( not_null parent, not_null story, QImage cachedPreview) : Ui::RpWidget(parent) , _storyId(story->fullId()) , _date( st::statisticsHeaderTitleTextStyle, Ui::FormatDateTime(base::unixtime::parse(story->date()))) , _preview(std::move(cachedPreview)) { _text.setMarkedText( st::defaultPeerListItem.nameStyle, { tr::lng_in_dlg_story(tr::now) }, Ui::DialogTextOptions(), Core::MarkedTextContext{ .session = &story->peer()->session(), .customEmojiRepaint = [=] { update(); }, }); if (_preview.isNull()) { if (const auto photo = story->photo()) { _photoMedia = photo->createMediaView(); _photoMedia->wanted(Data::PhotoSize::Large, story->fullId()); } else if (const auto document = story->document()) { _documentMedia = document->createMediaView(); _documentMedia->thumbnailWanted(story->fullId()); } processPreview(); } } void MessagePreview::setInfo(int views, int shares, int reactions) { _views = Ui::Text::String( st::defaultPeerListItem.nameStyle, (views >= 0) ? tr::lng_stats_recent_messages_views( tr::now, lt_count_decimal, views) : QString()); _shares = Ui::Text::String( st::statisticsHeaderTitleTextStyle, (shares >= 0) ? Lang::FormatCountDecimal(shares) : QString()); _reactions = Ui::Text::String( st::statisticsHeaderTitleTextStyle, (reactions >= 0) ? Lang::FormatCountDecimal(reactions) : QString()); _viewsWidth = (_views.maxWidth()); _sharesWidth = (_shares.maxWidth()); _reactionsWidth = (_reactions.maxWidth()); } void MessagePreview::processPreview() { const auto session = _photoMedia ? &_photoMedia->owner()->session() : _documentMedia ? &_documentMedia->owner()->session() : nullptr; if (!session) { return; } struct ThumbInfo final { bool loaded = false; Image *image = nullptr; }; const auto computeThumbInfo = [=]() -> ThumbInfo { using Size = Data::PhotoSize; if (_documentMedia) { return { true, _documentMedia->thumbnail() }; } else if (const auto large = _photoMedia->image(Size::Large)) { return { true, large }; } else if (const auto thumb = _photoMedia->image(Size::Thumbnail)) { return { false, thumb }; } else if (const auto small = _photoMedia->image(Size::Small)) { return { false, small }; } else { return { false, _photoMedia->thumbnailInline() }; } }; rpl::single(rpl::empty) | rpl::then( session->downloaderTaskFinished() ) | rpl::start_with_next([=] { const auto computed = computeThumbInfo(); if (!computed.image) { if (_documentMedia && !_documentMedia->owner()->hasThumbnail()) { _preview = QImage(); _lifetimeDownload.destroy(); } return; } else if (computed.loaded) { _lifetimeDownload.destroy(); } _preview = PreparePreviewImage( computed.image->original(), _messageId ? ImageRoundRadius::Large : ImageRoundRadius::Ellipse, !!_spoiler); }, _lifetimeDownload); } int MessagePreview::resizeGetHeight(int newWidth) { return st::peerListBoxItem.height; } void MessagePreview::paintEvent(QPaintEvent *e) { auto p = QPainter(this); const auto padding = st::boxRowPadding.left() / 2; const auto rightSubTextWidth = 0 + (_sharesWidth ? _sharesWidth + st::statisticsRecentPostShareIcon.width() : 0) + (_reactionsWidth ? _reactionsWidth + st::statisticsRecentPostReactionIcon.width() + st::statisticsChartRulerCaptionSkip : 0); const auto rightWidth = std::max(_viewsWidth, rightSubTextWidth) + padding; const auto left = _preview.isNull() ? st::peerListBoxItem.photoPosition.x() : st::peerListBoxItem.namePosition.x(); if (left) { const auto rect = QRect( st::peerListBoxItem.photoPosition, Size(st::peerListBoxItem.photoSize)); p.drawImage(rect.topLeft(), _preview); if (_spoiler) { const auto paused = On(PowerSaving::kChatSpoiler); FillSpoilerRect( p, rect, Images::CornersMaskRef( Images::CornersMask(st::roundRadiusLarge)), Ui::DefaultImageSpoiler().frame( _spoiler->index(crl::now(), paused)), _cornerCache); } if (_storyId) { auto hq = PainterHighQualityEnabler(p); const auto line = st::dialogsStoriesFull.lineTwice / 2.; auto gradient = Ui::UnreadStoryOutlineGradient(); gradient.setStart(rect.topRight()); gradient.setFinalStop(rect.bottomLeft()); p.setPen(QPen(gradient, line)); p.setBrush(Qt::NoBrush); p.drawEllipse(rect + Margins(1.5 * line)); } } const auto topTextTop = st::peerListBoxItem.namePosition.y(); const auto bottomTextTop = st::peerListBoxItem.statusPosition.y(); p.setBrush(Qt::NoBrush); p.setPen(st::boxTextFg); _text.draw(p, { .position = { left, topTextTop }, .outerWidth = width() - left, .availableWidth = width() - rightWidth - left, .spoiler = Ui::Text::DefaultSpoilerCache(), .now = crl::now(), .elisionHeight = st::statisticsDetailsPopupHeaderStyle.font->height, .elisionLines = 1, }); _views.draw(p, { .position = { width() - _viewsWidth, topTextTop }, .outerWidth = _viewsWidth, .availableWidth = _viewsWidth, }); p.setPen(st::windowSubTextFg); _date.draw(p, { .position = { left, bottomTextTop }, .outerWidth = width() - left, .availableWidth = width() - rightWidth - left, }); { auto right = width() - _sharesWidth; _shares.draw(p, { .position = { right, bottomTextTop }, .outerWidth = _sharesWidth, .availableWidth = _sharesWidth, }); const auto bottomTextBottom = bottomTextTop + st::statisticsHeaderTitleTextStyle.font->height; if (_sharesWidth) { const auto &icon = st::statisticsRecentPostShareIcon; const auto iconTop = bottomTextBottom - icon.height(); icon.paint(p, { (right -= icon.width()), iconTop }, width()); } right -= _reactionsWidth + st::statisticsChartRulerCaptionSkip; _reactions.draw(p, { .position = { right, bottomTextTop }, .outerWidth = _reactionsWidth, .availableWidth = _reactionsWidth, }); if (_reactionsWidth) { const auto &icon = st::statisticsRecentPostReactionIcon; const auto iconTop = bottomTextBottom - icon.height(); icon.paint(p, { (right -= icon.width()), iconTop }, width()); } } } void MessagePreview::saveState(SavedState &state) const { if (!_lifetimeDownload) { const auto fullId = RecentPostId{ _messageId, _storyId }; state.recentPostPreviews[fullId] = _preview; } } } // namespace Info::Statistics