479 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			479 lines
		
	
	
	
		
			13 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 "dialogs/ui/dialogs_message_view.h"
 | |
| 
 | |
| #include "history/history.h"
 | |
| #include "history/history_item.h"
 | |
| #include "history/view/history_view_item_preview.h"
 | |
| #include "main/main_session.h"
 | |
| #include "dialogs/dialogs_three_state_icon.h"
 | |
| #include "dialogs/ui/dialogs_layout.h"
 | |
| #include "dialogs/ui/dialogs_topics_view.h"
 | |
| #include "ui/effects/spoiler_mess.h"
 | |
| #include "ui/text/text_options.h"
 | |
| #include "ui/text/text_utilities.h"
 | |
| #include "ui/painter.h"
 | |
| #include "ui/power_saving.h"
 | |
| #include "core/ui_integration.h"
 | |
| #include "lang/lang_keys.h"
 | |
| #include "lang/lang_text_entity.h"
 | |
| #include "styles/style_dialogs.h"
 | |
| 
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kEmojiLoopCount = 2;
 | |
| 
 | |
| template <ushort kTag>
 | |
| struct TextWithTagOffset {
 | |
| 	TextWithTagOffset(TextWithEntities text) : text(std::move(text)) {
 | |
| 	}
 | |
| 	TextWithTagOffset(QString text) : text({ std::move(text) }) {
 | |
| 	}
 | |
| 	static TextWithTagOffset FromString(const QString &text) {
 | |
| 		return { { text } };
 | |
| 	}
 | |
| 
 | |
| 	TextWithEntities text;
 | |
| 	int offset = -1;
 | |
| };
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| namespace Lang {
 | |
| 
 | |
| template <ushort kTag>
 | |
| struct ReplaceTag<TextWithTagOffset<kTag>> {
 | |
| 	static TextWithTagOffset<kTag> Call(
 | |
| 		TextWithTagOffset<kTag> &&original,
 | |
| 		ushort tag,
 | |
| 		const TextWithTagOffset<kTag> &replacement);
 | |
| };
 | |
| 
 | |
| template <ushort kTag>
 | |
| TextWithTagOffset<kTag> ReplaceTag<TextWithTagOffset<kTag>>::Call(
 | |
| 		TextWithTagOffset<kTag> &&original,
 | |
| 		ushort tag,
 | |
| 		const TextWithTagOffset<kTag> &replacement) {
 | |
| 	const auto replacementPosition = FindTagReplacementPosition(
 | |
| 		original.text.text,
 | |
| 		tag);
 | |
| 	if (replacementPosition < 0) {
 | |
| 		return std::move(original);
 | |
| 	}
 | |
| 	original.text = ReplaceTag<TextWithEntities>::Replace(
 | |
| 		std::move(original.text),
 | |
| 		replacement.text,
 | |
| 		replacementPosition);
 | |
| 	if (tag == kTag) {
 | |
| 		original.offset = replacementPosition;
 | |
| 	} else if (original.offset > replacementPosition) {
 | |
| 		constexpr auto kReplaceCommandLength = 4;
 | |
| 		const auto replacementSize = replacement.text.text.size();
 | |
| 		original.offset += replacementSize - kReplaceCommandLength;
 | |
| 	}
 | |
| 	return std::move(original);
 | |
| }
 | |
| 
 | |
| } // namespace Lang
 | |
| 
 | |
| namespace Dialogs::Ui {
 | |
| 
 | |
| TextWithEntities DialogsPreviewText(TextWithEntities text) {
 | |
| 	auto result = Ui::Text::Filtered(
 | |
| 		std::move(text),
 | |
| 		{
 | |
| 			EntityType::Pre,
 | |
| 			EntityType::Code,
 | |
| 			EntityType::Spoiler,
 | |
| 			EntityType::StrikeOut,
 | |
| 			EntityType::Underline,
 | |
| 			EntityType::Italic,
 | |
| 			EntityType::CustomEmoji,
 | |
| 			EntityType::PlainLink,
 | |
| 		});
 | |
| 	for (auto &entity : result.entities) {
 | |
| 		if (entity.type() == EntityType::Pre) {
 | |
| 			entity = EntityInText(
 | |
| 				EntityType::Code,
 | |
| 				entity.offset(),
 | |
| 				entity.length());
 | |
| 		}
 | |
| 	}
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| struct MessageView::LoadingContext {
 | |
| 	std::any context;
 | |
| 	rpl::lifetime lifetime;
 | |
| };
 | |
| 
 | |
| MessageView::MessageView()
 | |
| : _senderCache(st::dialogsTextWidthMin)
 | |
| , _textCache(st::dialogsTextWidthMin) {
 | |
| }
 | |
| 
 | |
| MessageView::~MessageView() = default;
 | |
| 
 | |
| void MessageView::itemInvalidated(not_null<const HistoryItem*> item) {
 | |
| 	if (_textCachedFor == item.get()) {
 | |
| 		_textCachedFor = nullptr;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool MessageView::dependsOn(not_null<const HistoryItem*> item) const {
 | |
| 	return (_textCachedFor == item.get());
 | |
| }
 | |
| 
 | |
| bool MessageView::prepared(
 | |
| 		not_null<const HistoryItem*> item,
 | |
| 		Data::Forum *forum) const {
 | |
| 	return (_textCachedFor == item.get())
 | |
| 		&& (!forum
 | |
| 			|| (_topics
 | |
| 				&& _topics->forum() == forum
 | |
| 				&& _topics->prepared()));
 | |
| }
 | |
| 
 | |
| void MessageView::prepare(
 | |
| 		not_null<const HistoryItem*> item,
 | |
| 		Data::Forum *forum,
 | |
| 		Fn<void()> customEmojiRepaint,
 | |
| 		ToPreviewOptions options) {
 | |
| 	if (!forum) {
 | |
| 		_topics = nullptr;
 | |
| 	} else if (!_topics || _topics->forum() != forum) {
 | |
| 		_topics = std::make_unique<TopicsView>(forum);
 | |
| 		_topics->prepare(item->topicRootId(), customEmojiRepaint);
 | |
| 	} else if (!_topics->prepared()) {
 | |
| 		_topics->prepare(item->topicRootId(), customEmojiRepaint);
 | |
| 	}
 | |
| 	if (_textCachedFor == item.get()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	options.existing = &_imagesCache;
 | |
| 	options.ignoreTopic = true;
 | |
| 	options.spoilerLoginCode = true;
 | |
| 	auto preview = item->toPreview(options);
 | |
| 	_leftIcon = (preview.icon == ItemPreview::Icon::ForwardedMessage)
 | |
| 		? &st::dialogsMiniForward
 | |
| 		: (preview.icon == ItemPreview::Icon::ReplyToStory)
 | |
| 		? &st::dialogsMiniReplyStory
 | |
| 		: nullptr;
 | |
| 	const auto hasImages = !preview.images.empty();
 | |
| 	const auto history = item->history();
 | |
| 	const auto context = Core::MarkedTextContext{
 | |
| 		.session = &history->session(),
 | |
| 		.customEmojiRepaint = customEmojiRepaint,
 | |
| 		.customEmojiLoopLimit = kEmojiLoopCount,
 | |
| 	};
 | |
| 	const auto senderTill = (preview.arrowInTextPosition > 0)
 | |
| 		? preview.arrowInTextPosition
 | |
| 		: preview.imagesInTextPosition;
 | |
| 	if ((hasImages || _leftIcon) && senderTill > 0) {
 | |
| 		auto sender = Text::Mid(preview.text, 0, senderTill);
 | |
| 		TextUtilities::Trim(sender);
 | |
| 		_senderCache.setMarkedText(
 | |
| 			st::dialogsTextStyle,
 | |
| 			std::move(sender),
 | |
| 			DialogTextOptions());
 | |
| 		preview.text = Text::Mid(preview.text, senderTill);
 | |
| 	} else {
 | |
| 		_senderCache = { st::dialogsTextWidthMin };
 | |
| 	}
 | |
| 	TextUtilities::Trim(preview.text);
 | |
| 	auto textToCache = DialogsPreviewText(std::move(preview.text));
 | |
| 	_hasPlainLinkAtBegin = !textToCache.entities.empty()
 | |
| 		&& (textToCache.entities.front().type() == EntityType::PlainLink);
 | |
| 	_textCache.setMarkedText(
 | |
| 		st::dialogsTextStyle,
 | |
| 		std::move(textToCache),
 | |
| 		DialogTextOptions(),
 | |
| 		context);
 | |
| 	_textCachedFor = item;
 | |
| 	_imagesCache = std::move(preview.images);
 | |
| 	if (!ranges::any_of(_imagesCache, &ItemPreviewImage::hasSpoiler)) {
 | |
| 		_spoiler = nullptr;
 | |
| 	} else if (!_spoiler) {
 | |
| 		_spoiler = std::make_unique<SpoilerAnimation>(customEmojiRepaint);
 | |
| 	}
 | |
| 	if (preview.loadingContext.has_value()) {
 | |
| 		if (!_loadingContext) {
 | |
| 			_loadingContext = std::make_unique<LoadingContext>();
 | |
| 			item->history()->session().downloaderTaskFinished(
 | |
| 			) | rpl::start_with_next([=] {
 | |
| 				_textCachedFor = nullptr;
 | |
| 			}, _loadingContext->lifetime);
 | |
| 		}
 | |
| 		_loadingContext->context = std::move(preview.loadingContext);
 | |
| 	} else {
 | |
| 		_loadingContext = nullptr;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool MessageView::isInTopicJump(int x, int y) const {
 | |
| 	return _topics && _topics->isInTopicJumpArea(x, y);
 | |
| }
 | |
| 
 | |
| void MessageView::addTopicJumpRipple(
 | |
| 		QPoint origin,
 | |
| 		not_null<TopicJumpCache*> topicJumpCache,
 | |
| 		Fn<void()> updateCallback) {
 | |
| 	if (_topics) {
 | |
| 		_topics->addTopicJumpRipple(
 | |
| 			origin,
 | |
| 			topicJumpCache,
 | |
| 			std::move(updateCallback));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void MessageView::stopLastRipple() {
 | |
| 	if (_topics) {
 | |
| 		_topics->stopLastRipple();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void MessageView::clearRipple() {
 | |
| 	if (_topics) {
 | |
| 		_topics->clearRipple();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| int MessageView::countWidth() const {
 | |
| 	auto result = 0;
 | |
| 	if (!_senderCache.isEmpty()) {
 | |
| 		result += _senderCache.maxWidth();
 | |
| 		if (!_imagesCache.empty()) {
 | |
| 			result += st::dialogsMiniPreviewSkip
 | |
| 				+ st::dialogsMiniPreviewRight;
 | |
| 		}
 | |
| 	}
 | |
| 	if (!_imagesCache.empty()) {
 | |
| 		result += (_imagesCache.size()
 | |
| 			* (st::dialogsMiniPreview + st::dialogsMiniPreviewSkip))
 | |
| 			+ st::dialogsMiniPreviewRight;
 | |
| 	}
 | |
| 	return result + _textCache.maxWidth();
 | |
| }
 | |
| 
 | |
| void MessageView::paint(
 | |
| 		Painter &p,
 | |
| 		const QRect &geometry,
 | |
| 		const PaintContext &context) const {
 | |
| 	if (geometry.isEmpty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	p.setFont(st::dialogsTextFont);
 | |
| 	p.setPen(context.active
 | |
| 		? st::dialogsTextFgActive
 | |
| 		: context.selected
 | |
| 		? st::dialogsTextFgOver
 | |
| 		: st::dialogsTextFg);
 | |
| 	const auto withTopic = _topics && context.st->topicsHeight;
 | |
| 	const auto palette = &(withTopic
 | |
| 		? (context.active
 | |
| 			? st::dialogsTextPaletteInTopicActive
 | |
| 			: context.selected
 | |
| 			? st::dialogsTextPaletteInTopicOver
 | |
| 			: st::dialogsTextPaletteInTopic)
 | |
| 		: (context.active
 | |
| 			? st::dialogsTextPaletteActive
 | |
| 			: context.selected
 | |
| 			? st::dialogsTextPaletteOver
 | |
| 			: st::dialogsTextPalette));
 | |
| 
 | |
| 	auto rect = geometry;
 | |
| 	const auto checkJump = withTopic && !context.active;
 | |
| 	const auto jump1 = checkJump ? _topics->jumpToTopicWidth() : 0;
 | |
| 	if (jump1) {
 | |
| 		paintJumpToLast(p, rect, context, jump1);
 | |
| 	} else if (_topics) {
 | |
| 		_topics->clearTopicJumpGeometry();
 | |
| 	}
 | |
| 
 | |
| 	if (withTopic) {
 | |
| 		_topics->paint(p, rect, context);
 | |
| 		rect.setTop(rect.top() + context.st->topicsHeight);
 | |
| 	}
 | |
| 
 | |
| 	auto finalRight = rect.x() + rect.width();
 | |
| 	if (jump1) {
 | |
| 		rect.setWidth(rect.width() - st::forumDialogJumpArrowSkip);
 | |
| 		finalRight -= st::forumDialogJumpArrowSkip;
 | |
| 	}
 | |
| 	const auto lines = rect.height() / st::dialogsTextFont->height;
 | |
| 	const auto pausedSpoiler = context.paused
 | |
| 		|| On(PowerSaving::kChatSpoiler);
 | |
| 	if (!_senderCache.isEmpty()) {
 | |
| 		_senderCache.draw(p, {
 | |
| 			.position = rect.topLeft(),
 | |
| 			.availableWidth = rect.width(),
 | |
| 			.palette = palette,
 | |
| 			.elisionLines = lines,
 | |
| 		});
 | |
| 		rect.setLeft(rect.x() + _senderCache.maxWidth());
 | |
| 		if (!_imagesCache.empty() && !_leftIcon) {
 | |
| 			const auto skip = st::dialogsMiniPreviewSkip
 | |
| 				+ st::dialogsMiniPreviewRight;
 | |
| 			rect.setLeft(rect.x() + skip);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (_leftIcon) {
 | |
| 		const auto &icon = ThreeStateIcon(
 | |
| 			_leftIcon->icon,
 | |
| 			context.active,
 | |
| 			context.selected);
 | |
| 		const auto w = (icon.width());
 | |
| 		if (rect.width() > w) {
 | |
| 			if (_hasPlainLinkAtBegin && !context.active) {
 | |
| 				icon.paint(
 | |
| 					p,
 | |
| 					rect.topLeft(),
 | |
| 					rect.width(),
 | |
| 					palette->linkFg->c);
 | |
| 			} else {
 | |
| 				icon.paint(p, rect.topLeft(), rect.width());
 | |
| 			}
 | |
| 			rect.setLeft(rect.x()
 | |
| 				+ w
 | |
| 				+ (_imagesCache.empty()
 | |
| 					? _leftIcon->skipText
 | |
| 					: _leftIcon->skipMedia));
 | |
| 		}
 | |
| 	}
 | |
| 	for (const auto &image : _imagesCache) {
 | |
| 		const auto w = st::dialogsMiniPreview + st::dialogsMiniPreviewSkip;
 | |
| 		if (rect.width() < w) {
 | |
| 			break;
 | |
| 		}
 | |
| 		const auto mini = QRect(
 | |
| 			rect.x(),
 | |
| 			rect.y() + st::dialogsMiniPreviewTop,
 | |
| 			st::dialogsMiniPreview,
 | |
| 			st::dialogsMiniPreview);
 | |
| 		if (!image.data.isNull()) {
 | |
| 			p.drawImage(mini, image.data);
 | |
| 			if (image.hasSpoiler()) {
 | |
| 				const auto frame = DefaultImageSpoiler().frame(
 | |
| 					_spoiler->index(context.now, pausedSpoiler));
 | |
| 				FillSpoilerRect(p, mini, frame);
 | |
| 			}
 | |
| 		}
 | |
| 		rect.setLeft(rect.x() + w);
 | |
| 	}
 | |
| 	if (!_imagesCache.empty()) {
 | |
| 		rect.setLeft(rect.x() + st::dialogsMiniPreviewRight);
 | |
| 	}
 | |
| 	// Style of _textCache.
 | |
| 	static const auto ellipsisWidth = st::dialogsTextStyle.font->width(
 | |
| 		kQEllipsis);
 | |
| 	if (rect.width() > ellipsisWidth) {
 | |
| 		_textCache.draw(p, {
 | |
| 			.position = rect.topLeft(),
 | |
| 			.availableWidth = rect.width(),
 | |
| 			.palette = palette,
 | |
| 			.spoiler = Text::DefaultSpoilerCache(),
 | |
| 			.now = context.now,
 | |
| 			.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
 | |
| 			.pausedSpoiler = pausedSpoiler,
 | |
| 			.elisionLines = lines,
 | |
| 		});
 | |
| 		rect.setLeft(rect.x() + _textCache.maxWidth());
 | |
| 	}
 | |
| 	if (jump1) {
 | |
| 		const auto position = st::forumDialogJumpArrowPosition
 | |
| 			+ QPoint((rect.width() > 0) ? rect.x() : finalRight, rect.y());
 | |
| 		(context.selected
 | |
| 			? st::forumDialogJumpArrowOver
 | |
| 			: st::forumDialogJumpArrow).paint(p, position, context.width);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void MessageView::paintJumpToLast(
 | |
| 		Painter &p,
 | |
| 		const QRect &rect,
 | |
| 		const PaintContext &context,
 | |
| 		int width1) const {
 | |
| 	if (!context.topicJumpCache) {
 | |
| 		_topics->clearTopicJumpGeometry();
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto width2 = countWidth() + st::forumDialogJumpArrowSkip;
 | |
| 	const auto geometry = FillJumpToLastBg(p, {
 | |
| 		.st = context.st,
 | |
| 		.corners = (context.selected
 | |
| 			? &context.topicJumpCache->over
 | |
| 			: &context.topicJumpCache->corners),
 | |
| 		.geometry = rect,
 | |
| 		.bg = (context.selected
 | |
| 			? st::dialogsRippleBg
 | |
| 			: st::dialogsBgOver),
 | |
| 		.width1 = width1,
 | |
| 		.width2 = width2,
 | |
| 	});
 | |
| 	if (context.topicJumpSelected) {
 | |
| 		p.setOpacity(0.1);
 | |
| 		FillJumpToLastPrepared(p, {
 | |
| 			.st = context.st,
 | |
| 			.corners = &context.topicJumpCache->selected,
 | |
| 			.bg = st::dialogsTextFg,
 | |
| 			.prepared = geometry,
 | |
| 		});
 | |
| 		p.setOpacity(1.);
 | |
| 	}
 | |
| 	if (!_topics->changeTopicJumpGeometry(geometry)) {
 | |
| 		auto color = st::dialogsTextFg->c;
 | |
| 		color.setAlpha(color.alpha() / 10);
 | |
| 		if (color.alpha() > 0) {
 | |
| 			_topics->paintRipple(p, 0, 0, context.width, &color);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| HistoryView::ItemPreview PreviewWithSender(
 | |
| 		HistoryView::ItemPreview &&preview,
 | |
| 		const QString &sender,
 | |
| 		TextWithEntities topic) {
 | |
| 	auto senderWithOffset = topic.empty()
 | |
| 		? TextWithTagOffset<lt_from>::FromString(sender)
 | |
| 		: tr::lng_dialogs_text_from_in_topic(
 | |
| 			tr::now,
 | |
| 			lt_from,
 | |
| 			{ sender },
 | |
| 			lt_topic,
 | |
| 			std::move(topic),
 | |
| 			TextWithTagOffset<lt_from>::FromString);
 | |
| 	auto wrappedWithOffset = tr::lng_dialogs_text_from_wrapped(
 | |
| 		tr::now,
 | |
| 		lt_from,
 | |
| 		std::move(senderWithOffset.text),
 | |
| 		TextWithTagOffset<lt_from>::FromString);
 | |
| 	const auto wrappedSize = wrappedWithOffset.text.text.size();
 | |
| 	auto fullWithOffset = tr::lng_dialogs_text_with_from(
 | |
| 		tr::now,
 | |
| 		lt_from_part,
 | |
| 		Ui::Text::PlainLink(std::move(wrappedWithOffset.text)),
 | |
| 		lt_message,
 | |
| 		std::move(preview.text),
 | |
| 		TextWithTagOffset<lt_from_part>::FromString);
 | |
| 	preview.text = std::move(fullWithOffset.text);
 | |
| 	preview.arrowInTextPosition = (fullWithOffset.offset < 0
 | |
| 		|| wrappedWithOffset.offset < 0
 | |
| 		|| senderWithOffset.offset < 0)
 | |
| 		? -1
 | |
| 		: (fullWithOffset.offset
 | |
| 			+ wrappedWithOffset.offset
 | |
| 			+ senderWithOffset.offset
 | |
| 			+ sender.size());
 | |
| 	preview.imagesInTextPosition = (fullWithOffset.offset < 0)
 | |
| 		? 0
 | |
| 		: (fullWithOffset.offset + wrappedSize);
 | |
| 	return std::move(preview);
 | |
| }
 | |
| 
 | |
| } // namespace Dialogs::Ui
 | 
