/* 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 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 struct ReplaceTag> { static TextWithTagOffset Call( TextWithTagOffset &&original, ushort tag, const TextWithTagOffset &replacement); }; template TextWithTagOffset ReplaceTag>::Call( TextWithTagOffset &&original, ushort tag, const TextWithTagOffset &replacement) { const auto replacementPosition = FindTagReplacementPosition( original.text.text, tag); if (replacementPosition < 0) { return std::move(original); } original.text = ReplaceTag::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::Colorized, }); for (auto &entity : result.entities) { if (entity.type() == EntityType::Pre) { entity = EntityInText( EntityType::Code, entity.offset(), entity.length()); } else if (entity.type() == EntityType::Colorized && !entity.data().isEmpty()) { // Drop 'data' so that only link-color colorization takes place. entity = EntityInText( EntityType::Colorized, 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 item) { if (_textCachedFor == item.get()) { _textCachedFor = nullptr; } } bool MessageView::dependsOn(not_null item) const { return (_textCachedFor == item.get()); } bool MessageView::prepared( not_null item, Data::Forum *forum) const { return (_textCachedFor == item.get()) && (!forum || (_topics && _topics->forum() == forum && _topics->prepared())); } void MessageView::prepare( not_null item, Data::Forum *forum, Fn customEmojiRepaint, ToPreviewOptions options) { if (!forum) { _topics = nullptr; } else if (!_topics || _topics->forum() != forum) { _topics = std::make_unique(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::Colorized); _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(customEmojiRepaint); } if (preview.loadingContext.has_value()) { if (!_loadingContext) { _loadingContext = std::make_unique(); 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, Fn 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 pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler); if (!_senderCache.isEmpty()) { _senderCache.draw(p, { .position = rect.topLeft(), .availableWidth = rect.width(), .palette = palette, .elisionHeight = rect.height(), }); 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, .elisionHeight = rect.height(), }); 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::FromString(sender) : tr::lng_dialogs_text_from_in_topic( tr::now, lt_from, { sender }, lt_topic, std::move(topic), TextWithTagOffset::FromString); auto wrappedWithOffset = tr::lng_dialogs_text_from_wrapped( tr::now, lt_from, std::move(senderWithOffset.text), TextWithTagOffset::FromString); const auto wrappedSize = wrappedWithOffset.text.text.size(); auto fullWithOffset = tr::lng_dialogs_text_with_from( tr::now, lt_from_part, Ui::Text::Colorized(std::move(wrappedWithOffset.text)), lt_message, std::move(preview.text), TextWithTagOffset::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