909 lines
		
	
	
	
		
			26 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			909 lines
		
	
	
	
		
			26 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 "history/view/history_view_reply.h"
 | 
						|
 | 
						|
#include "core/click_handler_types.h"
 | 
						|
#include "core/ui_integration.h"
 | 
						|
#include "data/stickers/data_custom_emoji.h"
 | 
						|
#include "data/data_channel.h"
 | 
						|
#include "data/data_peer.h"
 | 
						|
#include "data/data_session.h"
 | 
						|
#include "data/data_story.h"
 | 
						|
#include "data/data_user.h"
 | 
						|
#include "history/view/history_view_item_preview.h"
 | 
						|
#include "history/history.h"
 | 
						|
#include "history/history_item.h"
 | 
						|
#include "history/history_item_components.h"
 | 
						|
#include "history/history_item_helpers.h"
 | 
						|
#include "lang/lang_keys.h"
 | 
						|
#include "main/main_session.h"
 | 
						|
#include "ui/chat/chat_style.h"
 | 
						|
#include "ui/effects/ripple_animation.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 "window/window_session_controller.h"
 | 
						|
#include "styles/style_chat.h"
 | 
						|
#include "styles/style_dialogs.h"
 | 
						|
 | 
						|
namespace HistoryView {
 | 
						|
namespace {
 | 
						|
 | 
						|
constexpr auto kNonExpandedLinesLimit = 5;
 | 
						|
 | 
						|
} // namespace
 | 
						|
 | 
						|
void ValidateBackgroundEmoji(
 | 
						|
		DocumentId backgroundEmojiId,
 | 
						|
		not_null<Ui::BackgroundEmojiData*> data,
 | 
						|
		not_null<Ui::BackgroundEmojiCache*> cache,
 | 
						|
		not_null<Ui::Text::QuotePaintCache*> quote,
 | 
						|
		not_null<const Element*> view) {
 | 
						|
	if (data->firstFrameMask.isNull() && !data->emoji) {
 | 
						|
		data->emoji = CreateBackgroundEmojiInstance(
 | 
						|
			&view->history()->owner(),
 | 
						|
			backgroundEmojiId,
 | 
						|
			crl::guard(view, [=] { view->repaint(); }));
 | 
						|
	}
 | 
						|
	ValidateBackgroundEmoji(backgroundEmojiId, data, cache, quote);
 | 
						|
}
 | 
						|
 | 
						|
void ValidateBackgroundEmoji(
 | 
						|
		DocumentId backgroundEmojiId,
 | 
						|
		not_null<Ui::BackgroundEmojiData*> data,
 | 
						|
		not_null<Ui::BackgroundEmojiCache*> cache,
 | 
						|
		not_null<Ui::Text::QuotePaintCache*> quote) {
 | 
						|
	Expects(!data->firstFrameMask.isNull() || data->emoji != nullptr);
 | 
						|
 | 
						|
	if (data->firstFrameMask.isNull()) {
 | 
						|
		if (!cache->frames[0].isNull()) {
 | 
						|
			for (auto &frame : cache->frames) {
 | 
						|
				frame = QImage();
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if (!data->emoji->ready()) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
		const auto tag = Data::CustomEmojiSizeTag::Isolated;
 | 
						|
		const auto size = Data::FrameSizeFromTag(tag);
 | 
						|
		data->firstFrameMask = QImage(
 | 
						|
			QSize(size, size),
 | 
						|
			QImage::Format_ARGB32_Premultiplied);
 | 
						|
		data->firstFrameMask.fill(Qt::transparent);
 | 
						|
		data->firstFrameMask.setDevicePixelRatio(style::DevicePixelRatio());
 | 
						|
		auto p = Painter(&data->firstFrameMask);
 | 
						|
		data->emoji->paint(p, {
 | 
						|
			.textColor = QColor(255, 255, 255),
 | 
						|
			.position = QPoint(0, 0),
 | 
						|
			.internal = {
 | 
						|
				.forceFirstFrame = true,
 | 
						|
			},
 | 
						|
		});
 | 
						|
		p.end();
 | 
						|
 | 
						|
		data->emoji = nullptr;
 | 
						|
	}
 | 
						|
	if (!cache->frames[0].isNull() && cache->color == quote->icon) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	cache->color = quote->icon;
 | 
						|
	const auto ratio = style::DevicePixelRatio();
 | 
						|
	auto colorized = QImage(
 | 
						|
		data->firstFrameMask.size(),
 | 
						|
		QImage::Format_ARGB32_Premultiplied);
 | 
						|
	colorized.setDevicePixelRatio(ratio);
 | 
						|
	style::colorizeImage(
 | 
						|
		data->firstFrameMask,
 | 
						|
		cache->color,
 | 
						|
		&colorized,
 | 
						|
		QRect(), // src
 | 
						|
		QPoint(), // dst
 | 
						|
		true); // use alpha
 | 
						|
	const auto make = [&](int size) {
 | 
						|
		size = style::ConvertScale(size) * ratio;
 | 
						|
		auto result = colorized.scaled(
 | 
						|
			size,
 | 
						|
			size,
 | 
						|
			Qt::IgnoreAspectRatio,
 | 
						|
			Qt::SmoothTransformation);
 | 
						|
		result.setDevicePixelRatio(ratio);
 | 
						|
		return result;
 | 
						|
	};
 | 
						|
 | 
						|
	constexpr auto kSize1 = 12;
 | 
						|
	constexpr auto kSize2 = 16;
 | 
						|
	constexpr auto kSize3 = 20;
 | 
						|
	cache->frames[0] = make(kSize1);
 | 
						|
	cache->frames[1] = make(kSize2);
 | 
						|
	cache->frames[2] = make(kSize3);
 | 
						|
}
 | 
						|
 | 
						|
auto CreateBackgroundEmojiInstance(
 | 
						|
	not_null<Data::Session*> owner,
 | 
						|
	DocumentId backgroundEmojiId,
 | 
						|
	Fn<void()> repaint)
 | 
						|
-> std::unique_ptr<Ui::Text::CustomEmoji> {
 | 
						|
	return owner->customEmojiManager().create(
 | 
						|
		backgroundEmojiId,
 | 
						|
		repaint,
 | 
						|
		Data::CustomEmojiSizeTag::Isolated);
 | 
						|
}
 | 
						|
 | 
						|
void FillBackgroundEmoji(
 | 
						|
		QPainter &p,
 | 
						|
		const QRect &rect,
 | 
						|
		bool quote,
 | 
						|
		const Ui::BackgroundEmojiCache &cache) {
 | 
						|
	p.setClipRect(rect);
 | 
						|
 | 
						|
	const auto &frames = cache.frames;
 | 
						|
	const auto right = rect.x() + rect.width();
 | 
						|
	const auto paint = [&](int x, int y, int index, float64 opacity) {
 | 
						|
		y = style::ConvertScale(y);
 | 
						|
		if (y >= rect.height()) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
		p.setOpacity(opacity);
 | 
						|
		p.drawImage(
 | 
						|
			right - style::ConvertScale(x + (quote ? 12 : 0)),
 | 
						|
			rect.y() + y,
 | 
						|
			frames[index]);
 | 
						|
	};
 | 
						|
 | 
						|
	paint(28, 4, 2, 0.32);
 | 
						|
	paint(51, 15, 1, 0.32);
 | 
						|
	paint(64, -2, 0, 0.28);
 | 
						|
	paint(87, 11, 1, 0.24);
 | 
						|
	paint(125, -2, 2, 0.16);
 | 
						|
 | 
						|
	paint(28, 31, 1, 0.24);
 | 
						|
	paint(72, 33, 2, 0.2);
 | 
						|
 | 
						|
	paint(46, 52, 1, 0.24);
 | 
						|
	paint(24, 55, 2, 0.18);
 | 
						|
 | 
						|
	if (quote) {
 | 
						|
		paint(4, 23, 1, 0.28);
 | 
						|
		paint(0, 48, 0, 0.24);
 | 
						|
	}
 | 
						|
 | 
						|
	p.setClipping(false);
 | 
						|
	p.setOpacity(1.);
 | 
						|
}
 | 
						|
 | 
						|
Reply::Reply()
 | 
						|
: _name(st::maxSignatureSize / 2)
 | 
						|
, _text(st::maxSignatureSize / 2) {
 | 
						|
}
 | 
						|
 | 
						|
Reply &Reply::operator=(Reply &&other) = default;
 | 
						|
 | 
						|
Reply::~Reply() = default;
 | 
						|
 | 
						|
void Reply::update(
 | 
						|
		not_null<Element*> view,
 | 
						|
		not_null<HistoryMessageReply*> data) {
 | 
						|
	const auto item = view->data();
 | 
						|
	const auto &fields = data->fields();
 | 
						|
	const auto message = data->resolvedMessage.get();
 | 
						|
	const auto story = data->resolvedStory.get();
 | 
						|
	const auto externalMedia = fields.externalMedia.get();
 | 
						|
	if (!_externalSender) {
 | 
						|
		if (const auto id = fields.externalSenderId) {
 | 
						|
			_externalSender = view->history()->owner().peer(id);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	_colorPeer = message
 | 
						|
		? message->contentColorsFrom()
 | 
						|
		: story
 | 
						|
		? story->peer().get()
 | 
						|
		: _externalSender
 | 
						|
		? _externalSender
 | 
						|
		: nullptr;
 | 
						|
	_hiddenSenderColorIndexPlusOne = (!_colorPeer && message)
 | 
						|
		? (message->originalHiddenSenderInfo()->colorIndex + 1)
 | 
						|
		: 0;
 | 
						|
 | 
						|
	const auto hasPreview = (story && story->hasReplyPreview())
 | 
						|
		|| (message
 | 
						|
			&& message->media()
 | 
						|
			&& message->media()->hasReplyPreview())
 | 
						|
		|| (externalMedia && externalMedia->hasReplyPreview());
 | 
						|
	_hasPreview = hasPreview ? 1 : 0;
 | 
						|
	_displaying = data->displaying() ? 1 : 0;
 | 
						|
	_multiline = data->multiline() ? 1 : 0;
 | 
						|
	_replyToStory = (fields.storyId != 0);
 | 
						|
	const auto hasQuoteIcon = _displaying
 | 
						|
		&& fields.manualQuote
 | 
						|
		&& !fields.quote.empty();
 | 
						|
	_hasQuoteIcon = hasQuoteIcon ? 1 : 0;
 | 
						|
 | 
						|
	const auto text = (!_displaying && data->unavailable())
 | 
						|
		? TextWithEntities()
 | 
						|
		: (message && (fields.quote.empty() || !fields.manualQuote))
 | 
						|
		? message->inReplyText()
 | 
						|
		: !fields.quote.empty()
 | 
						|
		? fields.quote
 | 
						|
		: story
 | 
						|
		? story->inReplyText()
 | 
						|
		: externalMedia
 | 
						|
		? externalMedia->toPreview({
 | 
						|
			.hideSender = true,
 | 
						|
			.hideCaption = true,
 | 
						|
			.ignoreMessageText = true,
 | 
						|
			.generateImages = false,
 | 
						|
			.ignoreGroup = true,
 | 
						|
			.ignoreTopic = true,
 | 
						|
		}).text
 | 
						|
		: TextWithEntities();
 | 
						|
	const auto repaint = [=] { item->customEmojiRepaint(); };
 | 
						|
	const auto context = Core::MarkedTextContext{
 | 
						|
		.session = &view->history()->session(),
 | 
						|
		.customEmojiRepaint = repaint,
 | 
						|
	};
 | 
						|
	_text.setMarkedText(
 | 
						|
		st::defaultTextStyle,
 | 
						|
		text,
 | 
						|
		_multiline ? Ui::ItemTextDefaultOptions() : Ui::DialogTextOptions(),
 | 
						|
		context);
 | 
						|
 | 
						|
	updateName(view, data);
 | 
						|
 | 
						|
	if (_displaying) {
 | 
						|
		setLinkFrom(view, data);
 | 
						|
		const auto media = message ? message->media() : nullptr;
 | 
						|
		if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) {
 | 
						|
			_spoiler = nullptr;
 | 
						|
		} else if (!_spoiler) {
 | 
						|
			_spoiler = std::make_unique<Ui::SpoilerAnimation>(repaint);
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		_spoiler = nullptr;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
bool Reply::expand() {
 | 
						|
	if (!_expandable || _expanded) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	_expanded = true;
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
void Reply::setLinkFrom(
 | 
						|
		not_null<Element*> view,
 | 
						|
		not_null<HistoryMessageReply*> data) {
 | 
						|
	const auto weak = base::make_weak(view);
 | 
						|
	const auto &fields = data->fields();
 | 
						|
	const auto externalChannelId = peerToChannel(fields.externalPeerId);
 | 
						|
	const auto messageId = fields.messageId;
 | 
						|
	const auto quote = fields.manualQuote
 | 
						|
		? fields.quote
 | 
						|
		: TextWithEntities();
 | 
						|
	const auto quoteOffset = fields.quoteOffset;
 | 
						|
	const auto returnToId = view->data()->fullId();
 | 
						|
	const auto externalLink = [=](ClickContext context) {
 | 
						|
		const auto my = context.other.value<ClickHandlerContext>();
 | 
						|
		if (const auto controller = my.sessionWindow.get()) {
 | 
						|
			auto error = QString();
 | 
						|
			const auto owner = &controller->session().data();
 | 
						|
			if (const auto view = weak.get()) {
 | 
						|
				if (const auto reply = view->Get<Reply>()) {
 | 
						|
					if (reply->expand()) {
 | 
						|
						owner->requestViewResize(view);
 | 
						|
						return;
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if (externalChannelId) {
 | 
						|
				const auto channel = owner->channel(externalChannelId);
 | 
						|
				if (!channel->isForbidden()) {
 | 
						|
					if (messageId) {
 | 
						|
						JumpToMessageClickHandler(
 | 
						|
							channel,
 | 
						|
							messageId,
 | 
						|
							returnToId,
 | 
						|
							quote,
 | 
						|
							quoteOffset
 | 
						|
						)->onClick(context);
 | 
						|
					} else {
 | 
						|
						controller->showPeerInfo(channel);
 | 
						|
					}
 | 
						|
				} else if (channel->isBroadcast()) {
 | 
						|
					error = tr::lng_channel_not_accessible(tr::now);
 | 
						|
				} else {
 | 
						|
					error = tr::lng_group_not_accessible(tr::now);
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				error = tr::lng_reply_from_private_chat(tr::now);
 | 
						|
			}
 | 
						|
			if (!error.isEmpty()) {
 | 
						|
				controller->showToast(error);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	};
 | 
						|
	const auto message = data->resolvedMessage.get();
 | 
						|
	const auto story = data->resolvedStory.get();
 | 
						|
	_link = message
 | 
						|
		? JumpToMessageClickHandler(message, returnToId, quote, quoteOffset)
 | 
						|
		: story
 | 
						|
		? JumpToStoryClickHandler(story)
 | 
						|
		: (data->external()
 | 
						|
			&& (!fields.messageId
 | 
						|
				|| (data->unavailable() && externalChannelId)))
 | 
						|
		? std::make_shared<LambdaClickHandler>(externalLink)
 | 
						|
		: nullptr;
 | 
						|
}
 | 
						|
 | 
						|
PeerData *Reply::sender(
 | 
						|
		not_null<const Element*> view,
 | 
						|
		not_null<HistoryMessageReply*> data) const {
 | 
						|
	const auto message = data->resolvedMessage.get();
 | 
						|
	if (const auto story = data->resolvedStory.get()) {
 | 
						|
		return story->peer();
 | 
						|
	} else if (!message) {
 | 
						|
		return _externalSender;
 | 
						|
	} else if (view->data()->Has<HistoryMessageForwarded>()) {
 | 
						|
		// Forward of a reply. Show reply-to original sender.
 | 
						|
		const auto forwarded = message->Get<HistoryMessageForwarded>();
 | 
						|
		if (forwarded) {
 | 
						|
			return forwarded->originalSender;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (const auto from = message->displayFrom()) {
 | 
						|
		return from;
 | 
						|
	}
 | 
						|
	return message->author().get();
 | 
						|
}
 | 
						|
 | 
						|
QString Reply::senderName(
 | 
						|
		not_null<const Element*> view,
 | 
						|
		not_null<HistoryMessageReply*> data,
 | 
						|
		bool shorten) const {
 | 
						|
	if (const auto peer = sender(view, data)) {
 | 
						|
		return senderName(peer, shorten);
 | 
						|
	} else if (!data->resolvedMessage) {
 | 
						|
		return data->fields().externalSenderName;
 | 
						|
	} else if (view->data()->Has<HistoryMessageForwarded>()) {
 | 
						|
		// Forward of a reply. Show reply-to original sender.
 | 
						|
		const auto forwarded
 | 
						|
			= data->resolvedMessage->Get<HistoryMessageForwarded>();
 | 
						|
		if (forwarded) {
 | 
						|
			Assert(forwarded->originalHiddenSenderInfo != nullptr);
 | 
						|
			return forwarded->originalHiddenSenderInfo->name;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return QString();
 | 
						|
}
 | 
						|
 | 
						|
QString Reply::senderName(
 | 
						|
		not_null<PeerData*> peer,
 | 
						|
		bool shorten) const {
 | 
						|
	const auto user = shorten ? peer->asUser() : nullptr;
 | 
						|
	return user ? user->firstName : peer->name();
 | 
						|
}
 | 
						|
 | 
						|
bool Reply::isNameUpdated(
 | 
						|
		not_null<const Element*> view,
 | 
						|
		not_null<HistoryMessageReply*> data) const {
 | 
						|
	if (const auto from = sender(view, data)) {
 | 
						|
		if (_nameVersion < from->nameVersion()) {
 | 
						|
			updateName(view, data, from);
 | 
						|
			return true;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false;
 | 
						|
}
 | 
						|
 | 
						|
void Reply::updateName(
 | 
						|
		not_null<const Element*> view,
 | 
						|
		not_null<HistoryMessageReply*> data,
 | 
						|
		std::optional<PeerData*> resolvedSender) const {
 | 
						|
	auto viaBotUsername = QString();
 | 
						|
	const auto message = data->resolvedMessage.get();
 | 
						|
	const auto forwarded = message
 | 
						|
		? message->Get<HistoryMessageForwarded>()
 | 
						|
		: nullptr;
 | 
						|
	if (message && !forwarded) {
 | 
						|
		if (const auto bot = message->viaBot()) {
 | 
						|
			viaBotUsername = bot->username();
 | 
						|
		}
 | 
						|
	}
 | 
						|
	const auto history = view->history();
 | 
						|
	const auto &fields = data->fields();
 | 
						|
	const auto sender = resolvedSender.value_or(this->sender(view, data));
 | 
						|
	const auto externalPeer = fields.externalPeerId
 | 
						|
		? history->owner().peer(fields.externalPeerId).get()
 | 
						|
		: nullptr;
 | 
						|
	const auto displayAsExternal = data->displayAsExternal(view->data());
 | 
						|
	const auto groupNameAdded = displayAsExternal
 | 
						|
		&& externalPeer
 | 
						|
		&& (externalPeer != sender)
 | 
						|
		&& (externalPeer->isChat() || externalPeer->isMegagroup());
 | 
						|
	const auto originalNameAdded = !displayAsExternal
 | 
						|
		&& forwarded
 | 
						|
		&& !message->isDiscussionPost()
 | 
						|
		&& (forwarded->forwardOfForward()
 | 
						|
			|| (!message->showForwardsFromSender(forwarded)
 | 
						|
				&& !view->data()->Has<HistoryMessageForwarded>()));
 | 
						|
	const auto shorten = !viaBotUsername.isEmpty()
 | 
						|
		|| groupNameAdded
 | 
						|
		|| originalNameAdded;
 | 
						|
	const auto name = sender
 | 
						|
		? senderName(sender, shorten)
 | 
						|
		: senderName(view, data, shorten);
 | 
						|
	const auto previewSkip = _hasPreview
 | 
						|
		? (st::messageQuoteStyle.outline
 | 
						|
			+ st::historyReplyPreviewMargin.left()
 | 
						|
			+ st::historyReplyPreview
 | 
						|
			+ st::historyReplyPreviewMargin.right()
 | 
						|
			- st::historyReplyPadding.left())
 | 
						|
		: 0;
 | 
						|
	auto nameFull = TextWithEntities();
 | 
						|
	if (displayAsExternal && !groupNameAdded && !fields.storyId) {
 | 
						|
		nameFull.append(PeerEmoji(history, sender));
 | 
						|
	}
 | 
						|
	nameFull.append(name);
 | 
						|
	if (groupNameAdded) {
 | 
						|
		nameFull.append(' ').append(PeerEmoji(history, externalPeer));
 | 
						|
		nameFull.append(externalPeer->name());
 | 
						|
	} else if (originalNameAdded) {
 | 
						|
		nameFull.append(' ').append(ForwardEmoji(&history->owner()));
 | 
						|
		nameFull.append(forwarded->originalSender
 | 
						|
			? forwarded->originalSender->name()
 | 
						|
			: forwarded->originalHiddenSenderInfo->name);
 | 
						|
	}
 | 
						|
	if (!viaBotUsername.isEmpty()) {
 | 
						|
		nameFull.append(u" @"_q).append(viaBotUsername);
 | 
						|
	}
 | 
						|
	const auto context = Core::MarkedTextContext{
 | 
						|
		.session = &history->session(),
 | 
						|
		.customEmojiRepaint = [] {},
 | 
						|
		.customEmojiLoopLimit = 1,
 | 
						|
	};
 | 
						|
	_name.setMarkedText(
 | 
						|
		st::fwdTextStyle,
 | 
						|
		nameFull,
 | 
						|
		Ui::NameTextOptions(),
 | 
						|
		context);
 | 
						|
	if (sender) {
 | 
						|
		_nameVersion = sender->nameVersion();
 | 
						|
	}
 | 
						|
	const auto nameMaxWidth = previewSkip
 | 
						|
		+ _name.maxWidth()
 | 
						|
		+ (_hasQuoteIcon
 | 
						|
			? st::messageTextStyle.blockquote.icon.width()
 | 
						|
			: 0);
 | 
						|
	const auto storySkip = fields.storyId
 | 
						|
		? (st::dialogsMiniReplyStory.skipText
 | 
						|
			+ st::dialogsMiniReplyStory.icon.icon.width())
 | 
						|
		: 0;
 | 
						|
	const auto optimalTextSize = _multiline
 | 
						|
		? countMultilineOptimalSize(previewSkip)
 | 
						|
		: QSize(
 | 
						|
			(previewSkip
 | 
						|
				+ storySkip
 | 
						|
				+ std::min(_text.maxWidth(), st::maxSignatureSize)),
 | 
						|
			st::normalFont->height);
 | 
						|
	_maxWidth = std::max(nameMaxWidth, optimalTextSize.width());
 | 
						|
	if (!data->displaying()) {
 | 
						|
		const auto unavailable = data->unavailable();
 | 
						|
		_stateText = ((fields.messageId || fields.storyId) && !unavailable)
 | 
						|
			? tr::lng_profile_loading(tr::now)
 | 
						|
			: fields.storyId
 | 
						|
			? tr::lng_deleted_story(tr::now)
 | 
						|
			: tr::lng_deleted_message(tr::now);
 | 
						|
		const auto phraseWidth = st::msgDateFont->width(_stateText);
 | 
						|
		_maxWidth = unavailable
 | 
						|
			? phraseWidth
 | 
						|
			: std::max(_maxWidth, phraseWidth);
 | 
						|
	} else {
 | 
						|
		_stateText = QString();
 | 
						|
	}
 | 
						|
	_maxWidth = st::historyReplyPadding.left()
 | 
						|
		+ _maxWidth
 | 
						|
		+ st::historyReplyPadding.right();
 | 
						|
	_minHeight = st::historyReplyPadding.top()
 | 
						|
		+ st::msgServiceNameFont->height
 | 
						|
		+ optimalTextSize.height()
 | 
						|
		+ st::historyReplyPadding.bottom();
 | 
						|
}
 | 
						|
 | 
						|
int Reply::resizeToWidth(int width) const {
 | 
						|
	_ripple.animation = nullptr;
 | 
						|
 | 
						|
	const auto previewSkip = _hasPreview
 | 
						|
		? (st::messageQuoteStyle.outline
 | 
						|
			+ st::historyReplyPreviewMargin.left()
 | 
						|
			+ st::historyReplyPreview
 | 
						|
			+ st::historyReplyPreviewMargin.right()
 | 
						|
			- st::historyReplyPadding.left())
 | 
						|
		: 0;
 | 
						|
	if (width >= _maxWidth || !_multiline) {
 | 
						|
		_nameTwoLines = 0;
 | 
						|
		_expandable = _minHeightExpandable;
 | 
						|
		_height = _minHeight;
 | 
						|
		return height();
 | 
						|
	}
 | 
						|
	const auto innerw = width
 | 
						|
		- st::historyReplyPadding.left()
 | 
						|
		- st::historyReplyPadding.right();
 | 
						|
	const auto namew = innerw - previewSkip;
 | 
						|
	const auto desiredNameHeight = _name.countHeight(namew);
 | 
						|
	_nameTwoLines = (desiredNameHeight > st::semiboldFont->height) ? 1 : 0;
 | 
						|
	const auto nameh = (_nameTwoLines ? 2 : 1) * st::semiboldFont->height;
 | 
						|
	const auto firstLineSkip = _nameTwoLines ? 0 : previewSkip;
 | 
						|
	auto elided = false;
 | 
						|
	const auto texth = _text.countDimensions(
 | 
						|
		textGeometry(innerw, firstLineSkip, &elided)).height;
 | 
						|
	_expandable = elided ? 1 : 0;
 | 
						|
	_height = st::historyReplyPadding.top()
 | 
						|
		+ nameh
 | 
						|
		+ std::max(texth, st::normalFont->height)
 | 
						|
		+ st::historyReplyPadding.bottom();
 | 
						|
	return height();
 | 
						|
}
 | 
						|
 | 
						|
Ui::Text::GeometryDescriptor Reply::textGeometry(
 | 
						|
		int available,
 | 
						|
		int firstLineSkip,
 | 
						|
		bool *outElided) const {
 | 
						|
	return { .layout = [=](int line) {
 | 
						|
		const auto skip = (line ? 0 : firstLineSkip);
 | 
						|
		const auto elided = !_multiline
 | 
						|
			|| (!_expanded && (line + 1 >= kNonExpandedLinesLimit));
 | 
						|
		return Ui::Text::LineGeometry{
 | 
						|
			.left = skip,
 | 
						|
			.width = available - skip,
 | 
						|
			.elided = elided,
 | 
						|
		};
 | 
						|
	}, .outElided = outElided };
 | 
						|
}
 | 
						|
 | 
						|
int Reply::height() const {
 | 
						|
	return _height + st::historyReplyTop + st::historyReplyBottom;
 | 
						|
}
 | 
						|
 | 
						|
QMargins Reply::margins() const {
 | 
						|
	return QMargins(0, st::historyReplyTop, 0, st::historyReplyBottom);
 | 
						|
}
 | 
						|
 | 
						|
QSize Reply::countMultilineOptimalSize(
 | 
						|
		int previewSkip) const {
 | 
						|
	auto elided = false;
 | 
						|
	const auto max = previewSkip + _text.maxWidth();
 | 
						|
	const auto result = _text.countDimensions(
 | 
						|
		textGeometry(max, previewSkip, &elided));
 | 
						|
	_minHeightExpandable = elided ? 1 : 0;
 | 
						|
	return {
 | 
						|
		result.width + st::historyReplyPadding.right(),
 | 
						|
		std::max(result.height, st::normalFont->height),
 | 
						|
	};
 | 
						|
}
 | 
						|
 | 
						|
void Reply::paint(
 | 
						|
		Painter &p,
 | 
						|
		not_null<const Element*> view,
 | 
						|
		const Ui::ChatPaintContext &context,
 | 
						|
		int x,
 | 
						|
		int y,
 | 
						|
		int w,
 | 
						|
		bool inBubble) const {
 | 
						|
	const auto st = context.st;
 | 
						|
	const auto stm = context.messageStyle();
 | 
						|
 | 
						|
	y += st::historyReplyTop;
 | 
						|
	const auto rect = QRect(x, y, w, _height);
 | 
						|
	const auto selected = context.selected();
 | 
						|
	const auto backgroundEmojiId = _colorPeer
 | 
						|
		? _colorPeer->backgroundEmojiId()
 | 
						|
		: DocumentId();
 | 
						|
	const auto colorIndexPlusOne = _colorPeer
 | 
						|
		? (_colorPeer->colorIndex() + 1)
 | 
						|
		: _hiddenSenderColorIndexPlusOne;
 | 
						|
	const auto useColorIndex = colorIndexPlusOne && !context.outbg;
 | 
						|
	const auto colorPattern = colorIndexPlusOne
 | 
						|
		? st->colorPatternIndex(colorIndexPlusOne - 1)
 | 
						|
		: 0;
 | 
						|
	const auto cache = !inBubble
 | 
						|
		? (_hasQuoteIcon
 | 
						|
			? st->serviceQuoteCache(colorPattern)
 | 
						|
			: st->serviceReplyCache(colorPattern)).get()
 | 
						|
		: useColorIndex
 | 
						|
		? (_hasQuoteIcon
 | 
						|
			? st->coloredQuoteCache(selected, colorIndexPlusOne - 1)
 | 
						|
			: st->coloredReplyCache(selected, colorIndexPlusOne - 1)).get()
 | 
						|
		: (_hasQuoteIcon
 | 
						|
			? stm->quoteCache[colorPattern]
 | 
						|
			: stm->replyCache[colorPattern]).get();
 | 
						|
	const auto "eSt = _hasQuoteIcon
 | 
						|
		? st::messageTextStyle.blockquote
 | 
						|
		: st::messageQuoteStyle;
 | 
						|
	const auto backgroundEmoji = backgroundEmojiId
 | 
						|
		? st->backgroundEmojiData(backgroundEmojiId).get()
 | 
						|
		: nullptr;
 | 
						|
	const auto backgroundEmojiCache = backgroundEmoji
 | 
						|
		? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex(
 | 
						|
			selected,
 | 
						|
			context.outbg,
 | 
						|
			inBubble,
 | 
						|
			colorIndexPlusOne)]
 | 
						|
		: nullptr;
 | 
						|
	const auto rippleColor = cache->bg;
 | 
						|
	if (!inBubble) {
 | 
						|
		cache->bg = QColor(0, 0, 0, 0);
 | 
						|
	}
 | 
						|
	Ui::Text::ValidateQuotePaintCache(*cache, quoteSt);
 | 
						|
	Ui::Text::FillQuotePaint(p, rect, *cache, quoteSt);
 | 
						|
	if (backgroundEmoji) {
 | 
						|
		ValidateBackgroundEmoji(
 | 
						|
			backgroundEmojiId,
 | 
						|
			backgroundEmoji,
 | 
						|
			backgroundEmojiCache,
 | 
						|
			cache,
 | 
						|
			view);
 | 
						|
		if (!backgroundEmojiCache->frames[0].isNull()) {
 | 
						|
			FillBackgroundEmoji(p, rect, _hasQuoteIcon, *backgroundEmojiCache);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (!inBubble) {
 | 
						|
		cache->bg = rippleColor;
 | 
						|
	}
 | 
						|
 | 
						|
	if (_ripple.animation) {
 | 
						|
		_ripple.animation->paint(p, x, y, w, &rippleColor);
 | 
						|
		if (_ripple.animation->empty()) {
 | 
						|
			_ripple.animation.reset();
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	auto hasPreview = (_hasPreview != 0);
 | 
						|
	auto previewSkip = hasPreview
 | 
						|
		? (st::messageQuoteStyle.outline
 | 
						|
			+ st::historyReplyPreviewMargin.left()
 | 
						|
			+ st::historyReplyPreview
 | 
						|
			+ st::historyReplyPreviewMargin.right()
 | 
						|
			- st::historyReplyPadding.left())
 | 
						|
		: 0;
 | 
						|
	if (hasPreview && w <= st::historyReplyPadding.left() + previewSkip) {
 | 
						|
		hasPreview = false;
 | 
						|
		previewSkip = 0;
 | 
						|
	}
 | 
						|
 | 
						|
	const auto pausedSpoiler = context.paused
 | 
						|
		|| On(PowerSaving::kChatSpoiler);
 | 
						|
	auto textLeft = x + st::historyReplyPadding.left();
 | 
						|
	auto textTop = y
 | 
						|
		+ st::historyReplyPadding.top()
 | 
						|
		+ (st::msgServiceNameFont->height * (_nameTwoLines ? 2 : 1));
 | 
						|
	if (w > st::historyReplyPadding.left()) {
 | 
						|
		if (_displaying) {
 | 
						|
			if (hasPreview) {
 | 
						|
				const auto data = view->data()->Get<HistoryMessageReply>();
 | 
						|
				const auto message = data
 | 
						|
					? data->resolvedMessage.get()
 | 
						|
					: nullptr;
 | 
						|
				const auto media = message ? message->media() : nullptr;
 | 
						|
				const auto image = media
 | 
						|
					? media->replyPreview()
 | 
						|
					: !data
 | 
						|
					? nullptr
 | 
						|
					: data->resolvedStory
 | 
						|
					? data->resolvedStory->replyPreview()
 | 
						|
					: data->fields().externalMedia
 | 
						|
					? data->fields().externalMedia->replyPreview()
 | 
						|
					: nullptr;
 | 
						|
				if (image) {
 | 
						|
					auto to = style::rtlrect(
 | 
						|
						x + st::historyReplyPreviewMargin.left(),
 | 
						|
						y + st::historyReplyPreviewMargin.top(),
 | 
						|
						st::historyReplyPreview,
 | 
						|
						st::historyReplyPreview,
 | 
						|
						w + 2 * x);
 | 
						|
					const auto preview = image->pixSingle(
 | 
						|
						image->size() / style::DevicePixelRatio(),
 | 
						|
						{
 | 
						|
							.colored = (context.selected()
 | 
						|
								? &st->msgStickerOverlay()
 | 
						|
								: nullptr),
 | 
						|
							.options = Images::Option::RoundSmall,
 | 
						|
							.outer = to.size(),
 | 
						|
						});
 | 
						|
					p.drawPixmap(to.x(), to.y(), preview);
 | 
						|
					if (_spoiler) {
 | 
						|
						view->clearCustomEmojiRepaint();
 | 
						|
						Ui::FillSpoilerRect(
 | 
						|
							p,
 | 
						|
							to,
 | 
						|
							Ui::DefaultImageSpoiler().frame(
 | 
						|
								_spoiler->index(
 | 
						|
									context.now,
 | 
						|
									pausedSpoiler)));
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
			const auto textw = w
 | 
						|
				- st::historyReplyPadding.left()
 | 
						|
				- st::historyReplyPadding.right();
 | 
						|
			const auto namew = textw
 | 
						|
				- previewSkip
 | 
						|
				- (_hasQuoteIcon
 | 
						|
					? st::messageTextStyle.blockquote.icon.width()
 | 
						|
					: 0);
 | 
						|
			auto firstLineSkip = _nameTwoLines ? 0 : previewSkip;
 | 
						|
			if (namew > 0) {
 | 
						|
				p.setPen(!inBubble
 | 
						|
					? st->msgImgReplyBarColor()->c
 | 
						|
					: useColorIndex
 | 
						|
					? FromNameFg(context, colorIndexPlusOne - 1)
 | 
						|
					: stm->msgServiceFg->c);
 | 
						|
				_name.drawLeftElided(
 | 
						|
					p,
 | 
						|
					x + st::historyReplyPadding.left() + previewSkip,
 | 
						|
					y + st::historyReplyPadding.top(),
 | 
						|
					namew,
 | 
						|
					w + 2 * x,
 | 
						|
					_nameTwoLines ? 2 : 1);
 | 
						|
 | 
						|
				p.setPen(inBubble
 | 
						|
					? stm->historyTextFg
 | 
						|
					: st->msgImgReplyBarColor());
 | 
						|
				view->prepareCustomEmojiPaint(p, context, _text);
 | 
						|
				auto replyToTextPalette = &(!inBubble
 | 
						|
					? st->imgReplyTextPalette()
 | 
						|
					: useColorIndex
 | 
						|
					? st->coloredTextPalette(selected, colorIndexPlusOne - 1)
 | 
						|
					: stm->replyTextPalette);
 | 
						|
				auto owned = std::optional<style::owned_color>();
 | 
						|
				auto copy = std::optional<style::TextPalette>();
 | 
						|
				if (inBubble && colorIndexPlusOne) {
 | 
						|
					copy.emplace(*replyToTextPalette);
 | 
						|
					owned.emplace(cache->icon);
 | 
						|
					copy->linkFg = owned->color();
 | 
						|
					replyToTextPalette = &*copy;
 | 
						|
				}
 | 
						|
				if (_replyToStory) {
 | 
						|
					st::dialogsMiniReplyStory.icon.icon.paint(
 | 
						|
						p,
 | 
						|
						textLeft + firstLineSkip,
 | 
						|
						textTop,
 | 
						|
						w + 2 * x,
 | 
						|
						replyToTextPalette->linkFg->c);
 | 
						|
					firstLineSkip += st::dialogsMiniReplyStory.skipText
 | 
						|
						+ st::dialogsMiniReplyStory.icon.icon.width();
 | 
						|
				}
 | 
						|
				_text.draw(p, {
 | 
						|
					.position = { textLeft, textTop },
 | 
						|
					.geometry = textGeometry(textw, firstLineSkip),
 | 
						|
					.palette = replyToTextPalette,
 | 
						|
					.spoiler = Ui::Text::DefaultSpoilerCache(),
 | 
						|
					.now = context.now,
 | 
						|
					.pausedEmoji = (context.paused
 | 
						|
						|| On(PowerSaving::kEmojiChat)),
 | 
						|
					.pausedSpoiler = pausedSpoiler,
 | 
						|
					.elisionLines = 1,
 | 
						|
				});
 | 
						|
				p.setTextPalette(stm->textPalette);
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			p.setFont(st::msgDateFont);
 | 
						|
			p.setPen(cache->icon);
 | 
						|
			p.drawTextLeft(
 | 
						|
				textLeft,
 | 
						|
				(y
 | 
						|
					+ st::historyReplyPadding.top()
 | 
						|
					+ (st::msgDateFont->height / 2)),
 | 
						|
				w + 2 * x,
 | 
						|
				st::msgDateFont->elided(
 | 
						|
					_stateText,
 | 
						|
					x + w - textLeft - st::historyReplyPadding.right()));
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Reply::createRippleAnimation(
 | 
						|
		not_null<const Element*> view,
 | 
						|
		QSize size) {
 | 
						|
	_ripple.animation = std::make_unique<Ui::RippleAnimation>(
 | 
						|
		st::defaultRippleAnimation,
 | 
						|
		Ui::RippleAnimation::RoundRectMask(
 | 
						|
			size,
 | 
						|
			st::messageQuoteStyle.radius),
 | 
						|
		[=] { view->repaint(); });
 | 
						|
}
 | 
						|
 | 
						|
void Reply::saveRipplePoint(QPoint point) const {
 | 
						|
	_ripple.lastPoint = point;
 | 
						|
}
 | 
						|
 | 
						|
void Reply::addRipple() {
 | 
						|
	if (_ripple.animation) {
 | 
						|
		_ripple.animation->add(_ripple.lastPoint);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Reply::stopLastRipple() {
 | 
						|
	if (_ripple.animation) {
 | 
						|
		_ripple.animation->lastStop();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
TextWithEntities Reply::PeerEmoji(
 | 
						|
		not_null<History*> history,
 | 
						|
		PeerData *peer) {
 | 
						|
	return PeerEmoji(&history->owner(), peer);
 | 
						|
}
 | 
						|
 | 
						|
TextWithEntities Reply::PeerEmoji(
 | 
						|
		not_null<Data::Session*> owner,
 | 
						|
		PeerData *peer) {
 | 
						|
	using namespace std;
 | 
						|
	const auto icon = !peer
 | 
						|
		? pair(&st::historyReplyUser, st::historyReplyUserPadding)
 | 
						|
		: peer->isBroadcast()
 | 
						|
		? pair(&st::historyReplyChannel, st::historyReplyChannelPadding)
 | 
						|
		: (peer->isChannel() || peer->isChat())
 | 
						|
		? pair(&st::historyReplyGroup, st::historyReplyGroupPadding)
 | 
						|
		: pair(&st::historyReplyUser, st::historyReplyUserPadding);
 | 
						|
	return Ui::Text::SingleCustomEmoji(
 | 
						|
		owner->customEmojiManager().registerInternalEmoji(
 | 
						|
			*icon.first,
 | 
						|
			icon.second));
 | 
						|
}
 | 
						|
 | 
						|
TextWithEntities Reply::ForwardEmoji(not_null<Data::Session*> owner) {
 | 
						|
	return Ui::Text::SingleCustomEmoji(
 | 
						|
		owner->customEmojiManager().registerInternalEmoji(
 | 
						|
			st::historyReplyForward,
 | 
						|
			st::historyReplyForwardPadding));
 | 
						|
}
 | 
						|
 | 
						|
TextWithEntities Reply::ComposePreviewName(
 | 
						|
		not_null<History*> history,
 | 
						|
		not_null<HistoryItem*> to,
 | 
						|
		bool quote) {
 | 
						|
	const auto sender = [&] {
 | 
						|
		if (const auto from = to->displayFrom()) {
 | 
						|
			return not_null(from);
 | 
						|
		}
 | 
						|
		return to->author();
 | 
						|
	}();
 | 
						|
	const auto toPeer = to->history()->peer;
 | 
						|
	const auto displayAsExternal = (to->history() != history);
 | 
						|
	const auto groupNameAdded = displayAsExternal
 | 
						|
		&& (toPeer != sender)
 | 
						|
		&& (toPeer->isChat() || toPeer->isMegagroup());
 | 
						|
	const auto shorten = groupNameAdded || quote;
 | 
						|
 | 
						|
	auto nameFull = TextWithEntities();
 | 
						|
	using namespace HistoryView;
 | 
						|
	if (displayAsExternal && !groupNameAdded) {
 | 
						|
		nameFull.append(Reply::PeerEmoji(history, sender));
 | 
						|
	}
 | 
						|
	nameFull.append(shorten ? sender->shortName() : sender->name());
 | 
						|
	if (groupNameAdded) {
 | 
						|
		nameFull.append(' ').append(Reply::PeerEmoji(history, toPeer));
 | 
						|
		nameFull.append(toPeer->name());
 | 
						|
	}
 | 
						|
	return (quote
 | 
						|
		? tr::lng_preview_reply_to_quote
 | 
						|
		: tr::lng_preview_reply_to)(
 | 
						|
			tr::now,
 | 
						|
			lt_name,
 | 
						|
			nameFull,
 | 
						|
			Ui::Text::WithEntities);
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
void Reply::unloadPersistentAnimation() {
 | 
						|
	_text.unloadPersistentAnimation();
 | 
						|
}
 | 
						|
 | 
						|
} // namespace HistoryView
 |