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,
 | |
| 		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);
 | |
| 				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();
 | |
| 				}
 | |
| 				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;
 | |
| 				}
 | |
| 				_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
 | 
