357 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			357 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
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/controls/userpic_button.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,
 | 
						|
		int size,
 | 
						|
		bool spoiler) {
 | 
						|
	if (original.width() * 10 < original.height()
 | 
						|
		|| original.height() * 10 < original.width()) {
 | 
						|
		return QImage();
 | 
						|
	}
 | 
						|
	const auto factor = style::DevicePixelRatio();
 | 
						|
	size *= 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<Ui::RpWidget*> parent,
 | 
						|
	not_null<HistoryItem*> 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<Ui::SpoilerAnimation>([=] { 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();
 | 
						|
		}
 | 
						|
		if ((!_documentMedia || _documentMedia->thumbnailSize().isNull())
 | 
						|
			&& !_photoMedia) {
 | 
						|
			const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
 | 
						|
				this,
 | 
						|
				item->history()->peer,
 | 
						|
				st::statisticsRecentPostUserpic);
 | 
						|
			userpic->move(st::peerListBoxItem.photoPosition);
 | 
						|
			userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
MessagePreview::MessagePreview(
 | 
						|
	not_null<Ui::RpWidget*> parent,
 | 
						|
	not_null<Data::Story*> 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();
 | 
						|
		const auto guard = gsl::finally([&] { update(); });
 | 
						|
		if (!computed.image) {
 | 
						|
			if (_documentMedia && !_documentMedia->owner()->hasThumbnail()) {
 | 
						|
				_preview = QImage();
 | 
						|
				_lifetimeDownload.destroy();
 | 
						|
			}
 | 
						|
			return;
 | 
						|
		} else if (computed.loaded) {
 | 
						|
			_lifetimeDownload.destroy();
 | 
						|
		}
 | 
						|
		if (_storyId) {
 | 
						|
			const auto line = st::dialogsStoriesFull.lineTwice;
 | 
						|
			const auto rect = Rect(Size(st::peerListBoxItem.photoSize));
 | 
						|
			const auto penWidth = line / 2.;
 | 
						|
			const auto offset = 1.5 * penWidth * 2;
 | 
						|
			const auto preview = PreparePreviewImage(
 | 
						|
				computed.image->original(),
 | 
						|
				ImageRoundRadius::Ellipse,
 | 
						|
				st::peerListBoxItem.photoSize - offset * 2,
 | 
						|
				!!_spoiler);
 | 
						|
			auto image = QImage(
 | 
						|
				rect.size() * style::DevicePixelRatio(),
 | 
						|
				QImage::Format_ARGB32_Premultiplied);
 | 
						|
			image.fill(Qt::transparent);
 | 
						|
			{
 | 
						|
				auto p = QPainter(&image);
 | 
						|
				p.drawImage(offset, offset, preview);
 | 
						|
				auto hq = PainterHighQualityEnabler(p);
 | 
						|
				auto gradient = Ui::UnreadStoryOutlineGradient();
 | 
						|
				gradient.setStart(rect.topRight());
 | 
						|
				gradient.setFinalStop(rect.bottomLeft());
 | 
						|
 | 
						|
				p.setPen(QPen(gradient, penWidth));
 | 
						|
				p.setBrush(Qt::NoBrush);
 | 
						|
				p.drawEllipse(rect - Margins(penWidth));
 | 
						|
			}
 | 
						|
			_preview = std::move(image);
 | 
						|
		} else {
 | 
						|
			_preview = PreparePreviewImage(
 | 
						|
				computed.image->original(),
 | 
						|
				ImageRoundRadius::Large,
 | 
						|
				st::peerListBoxItem.photoSize,
 | 
						|
				!!_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()
 | 
						|
				+ st::statisticsRecentPostIconSkip
 | 
						|
			: 0)
 | 
						|
		+ (_reactionsWidth
 | 
						|
			? _reactionsWidth
 | 
						|
				+ st::statisticsRecentPostReactionIcon.width()
 | 
						|
				+ st::statisticsChartRulerCaptionSkip
 | 
						|
				+ st::statisticsRecentPostIconSkip
 | 
						|
			: 0);
 | 
						|
	const auto rightWidth = std::max(_viewsWidth, rightSubTextWidth)
 | 
						|
		+ padding;
 | 
						|
	const auto left = (false && _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);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	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
 | 
						|
			- st::statisticsRecentPostIconSkip;
 | 
						|
		if (_sharesWidth) {
 | 
						|
			const auto &icon = st::statisticsRecentPostShareIcon;
 | 
						|
			const auto iconTop = bottomTextBottom - icon.height();
 | 
						|
			right -= st::statisticsRecentPostIconSkip + icon.width();
 | 
						|
			icon.paint(p, { right, 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();
 | 
						|
			right -= st::statisticsRecentPostIconSkip + icon.width();
 | 
						|
			icon.paint(p, { right, iconTop }, width());
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void MessagePreview::saveState(SavedState &state) const {
 | 
						|
	if (!_lifetimeDownload) {
 | 
						|
		const auto fullId = Data::RecentPostId{ _messageId, _storyId };
 | 
						|
		state.recentPostPreviews[fullId] = _preview;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
} // namespace Info::Statistics
 |