diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index fee73590f..0890cfc55 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_recent_message.cpp + info/statistics/info_statistics_recent_message.h info/statistics/info_statistics_widget.cpp info/statistics/info_statistics_widget.h info/stories/info_stories_inner_widget.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e8650ebf6..1b801bea0 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4077,6 +4077,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stats_overview_group_mean_view_count" = "Viewing Members"; "lng_stats_overview_group_mean_post_count" = "Posting Members"; +"lng_stats_recent_messages_title" = "Recent posts"; +"lng_stats_recent_messages_views#one" = "{count} view"; +"lng_stats_recent_messages_views#other" = "{count} views"; +"lng_stats_recent_messages_shares#one" = "{count} share"; +"lng_stats_recent_messages_shares#other" = "{count} shares"; + "lng_stats_loading" = "Loading stats..."; "lng_stats_loading_subtext" = "Please wait a few moments while we generate your stats."; diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp new file mode 100644 index 000000000..976023556 --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp @@ -0,0 +1,227 @@ +/* +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 "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 "history/history.h" +#include "history/history_item.h" +#include "history/history_item_helpers.h" +#include "history/view/history_view_item_preview.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/effects/ripple_animation.h" +#include "ui/effects/spoiler_mess.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_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::statisticsRecentPostRowHeight * 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, + int views, + int shares) +: Ui::RpWidget(parent) +, _item(item) +, _date( + st::statisticsHeaderDatesTextStyle, + Ui::FormatDateTime(ItemDateTime(item))) +, _views( + st::statisticsDetailsPopupHeaderStyle, + tr::lng_stats_recent_messages_views( + tr::now, + lt_count_decimal, + views)) +, _shares( + st::statisticsHeaderDatesTextStyle, + tr::lng_stats_recent_messages_shares( + tr::now, + lt_count_decimal, + shares)) +, _viewsWidth(_views.maxWidth()) +, _sharesWidth(_shares.maxWidth()) { + _text.setMarkedText( + st::statisticsDetailsPopupHeaderStyle, + _item->toPreview({ .generateImages = false }).text, + Ui::DialogTextOptions(), + Core::MarkedTextContext{ + .session = &item->history()->session(), + .customEmojiRepaint = [=] { update(); }, + }); + processPreview(item); +} + +void MessagePreview::processPreview(not_null item) { + if (const auto media = item->media()) { + if (item->media()->hasSpoiler()) { + _spoiler = std::make_unique([=] { + update(); + }); + } + 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()); + } + } + 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 thumbnail = _photoMedia->image( + Size::Thumbnail)) { + return { false, thumbnail }; + } 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(), + ImageRoundRadius::Large, + !!_spoiler); + }, _lifetimeDownload); +} + +int MessagePreview::resizeGetHeight(int newWidth) { + return st::statisticsRecentPostRowHeight; +} + +void MessagePreview::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto padding = st::boxRowPadding.left() / 2; + const auto rightWidth = std::max(_viewsWidth, _sharesWidth) + padding; + const auto left = _preview.isNull() + ? 0 + : (_preview.width() / style::DevicePixelRatio()) + padding; + if (left) { + p.drawImage(0, 0, _preview); + if (_spoiler) { + const auto rect = Rect( + _preview.size() / _preview.devicePixelRatio()); + const auto paused = On(PowerSaving::kChatSpoiler); + FillSpoilerRect( + p, + rect, + Images::CornersMaskRef( + Images::CornersMask(st::roundRadiusLarge)), + Ui::DefaultImageSpoiler().frame( + _spoiler->index(crl::now(), paused)), + _cornerCache); + } + } + + p.setBrush(Qt::NoBrush); + p.setPen(st::boxTextFg); + _text.draw(p, { + .position = { left, 0 }, + .outerWidth = width() - left, + .availableWidth = width() - rightWidth - left, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = crl::now(), + .elisionHeight = st::statisticsDetailsPopupHeaderStyle.font->height, + }); + _views.draw(p, { + .position = { width() - _viewsWidth, 0 }, + .outerWidth = _viewsWidth, + .availableWidth = _viewsWidth, + }); + + p.setPen(st::windowSubTextFg); + _date.draw(p, { + .position = { left, height() / 2 }, + .outerWidth = width() - left, + .availableWidth = width() - rightWidth - left, + }); + _shares.draw(p, { + .position = { width() - _sharesWidth, height() / 2 }, + .outerWidth = _sharesWidth, + .availableWidth = _sharesWidth, + }); +} + +} // namespace Info::Statistics + diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.h b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.h new file mode 100644 index 000000000..5f858086e --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.h @@ -0,0 +1,61 @@ +/* +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 "ui/rp_widget.h" + +class HistoryItem; + +namespace Data { +class DocumentMedia; +class PhotoMedia; +} // namespace Data + +namespace Ui { +class SpoilerAnimation; +} // namespace Ui + +namespace Info::Statistics { + +class MessagePreview final : public Ui::RpWidget { +public: + MessagePreview( + not_null parent, + not_null item, + int views, + int shares); + +protected: + void paintEvent(QPaintEvent *e) override; + + int resizeGetHeight(int newWidth) override; + +private: + void processPreview(not_null item); + + not_null _item; + Ui::Text::String _text; + Ui::Text::String _date; + Ui::Text::String _views; + Ui::Text::String _shares; + + int _viewsWidth = 0; + int _sharesWidth = 0; + + QImage _cornerCache; + QImage _preview; + + std::shared_ptr _photoMedia; + std::shared_ptr _documentMedia; + std::unique_ptr _spoiler; + + rpl::lifetime _lifetimeDownload; + +}; + +} // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp index e5c300092..4a0c1b685 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp @@ -8,9 +8,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 "info/info_controller.h" #include "info/info_memento.h" +#include "info/statistics/info_statistics_recent_message.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "main/main_session.h" @@ -251,6 +254,31 @@ void FillLoading( ::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)); + 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) { @@ -258,25 +286,9 @@ void FillOverview( const auto &channel = stats.channel; const auto &supergroup = stats.supergroup; - const auto startDate = channel ? channel.startDate : supergroup.startDate; - const auto endDate = channel ? channel.endDate : supergroup.endDate; ::Settings::AddSkip(content, st::statisticsLayerOverviewMargins.top()); - { - const auto header = content->add( - object_ptr(content), - st::statisticsLayerMargins + st::statisticsChartHeaderPadding); - header->resizeToWidth(header->width()); - header->setTitle(tr::lng_stats_overview_title(tr::now)); - 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)); - } + AddHeader(content, tr::lng_stats_overview_title, stats); ::Settings::AddSkip(content); struct Second final { @@ -421,6 +433,54 @@ void FillOverview( ::Settings::AddSkip(content, st::statisticsLayerOverviewMargins.bottom()); } +void FillRecentPosts( + not_null container, + not_null peer, + const Data::ChannelStatistics &stats) { + 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 row = messageWrap->add( + object_ptr( + messageWrap, + item, + info.viewsCount, + info.forwardsCount), + st::boxRowPadding); + ::Settings::AddSkip(messageWrap); + content->resizeToWidth(content->width()); + if (!wrap->toggled()) { + wrap->toggle(true, anim::type::normal); + } + }; + + 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) @@ -481,6 +541,9 @@ Widget::Widget( } FillOverview(inner, anyStats); FillStatistic(inner, descriptor, anyStats); + if (anyStats.channel) { + FillRecentPosts(inner, descriptor.peer, anyStats.channel); + } loaded->fire(true); inner->resizeToWidth(width()); inner->showChildren(); diff --git a/Telegram/SourceFiles/statistics/statistics.style b/Telegram/SourceFiles/statistics/statistics.style index 0f73a60f2..b41d30c30 100644 --- a/Telegram/SourceFiles/statistics/statistics.style +++ b/Telegram/SourceFiles/statistics/statistics.style @@ -98,3 +98,5 @@ statisticsOverviewSubtext: FlatLabel(boxLabel) { } statisticsOverviewMidSkip: 50px; statisticsOverviewRightSkip: 14px; + +statisticsRecentPostRowHeight: 40px;