/* 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/ui/dialogs_layout.h" #include "dialogs/ui/dialogs_topics_view.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/image/image.h" #include "ui/painter.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::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 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; auto preview = item->toPreview(options); 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 && senderTill > 0) { auto sender = Text::Mid(preview.text, 0, senderTill); TextUtilities::Trim(sender); _senderCache.setMarkedText( st::dialogsTextStyle, std::move(sender), DialogTextOptions()); const auto topicTill = preview.imagesInTextPosition; preview.text = Text::Mid(preview.text, senderTill); } else { _senderCache = { st::dialogsTextWidthMin }; } TextUtilities::Trim(preview.text); _textCache.setMarkedText( st::dialogsTextStyle, DialogsPreviewText(std::move(preview.text)), DialogTextOptions(), context); _textCachedFor = item; _imagesCache = std::move(preview.images); 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; } } 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); } 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; 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()) { const auto skip = st::dialogsMiniPreviewSkip + st::dialogsMiniPreviewRight; rect.setLeft(rect.x() + skip); } } for (const auto &image : _imagesCache) { if (rect.width() < st::dialogsMiniPreview) { break; } p.drawImage( rect.x(), rect.y() + st::dialogsMiniPreviewTop, image.data); rect.setLeft(rect.x() + st::dialogsMiniPreview + st::dialogsMiniPreviewSkip); } if (!_imagesCache.empty()) { rect.setLeft(rect.x() + st::dialogsMiniPreviewRight); } if (!rect.isEmpty()) { _textCache.draw(p, { .position = rect.topLeft(), .availableWidth = rect.width(), .palette = palette, .spoiler = Text::DefaultSpoilerCache(), .now = context.now, .paused = context.paused, .elisionLines = lines, }); rect.setLeft(rect.x() + _textCache.maxWidth()); } if (jump1) { const auto x = (rect.width() > st::forumDialogJumpArrowSkip) ? rect.x() : finalRight; const auto add = st::forumDialogJumpArrowLeft; const auto y = rect.y() + st::forumDialogJumpArrowTop; (context.selected ? st::forumDialogJumpArrowOver : st::forumDialogJumpArrow).paint(p, x + add, y, context.width); } } void MessageView::paintJumpToLast( Painter &p, const QRect &rect, const PaintContext &context, int width1) const { if (!context.topicJumpCache) { return; } 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 = countWidth() + st::forumDialogJumpArrowSkip, }); } void FillJumpToLastBg(QPainter &p, JumpToLastBg context) { const auto availableWidth = context.geometry.width(); const auto use1 = std::min(context.width1, availableWidth); const auto use2 = std::min(context.width2, availableWidth); const auto padding = st::forumDialogJumpPadding; const auto radius = st::forumDialogJumpRadius; const auto &bg = context.bg; auto &normal = context.corners->normal; auto &inverted = context.corners->inverted; auto &small = context.corners->small; auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(bg); const auto origin = context.geometry.topLeft(); const auto delta = std::abs(use1 - use2); if (delta <= context.st->topicsSkip / 2) { if (normal.p[0].isNull()) { normal = Ui::PrepareCornerPixmaps(radius, bg); } const auto w = std::max(use1, use2); const auto h = context.st->topicsHeight + st::normalFont->height; const auto fill = QRect(origin, QSize(w, h)); Ui::FillRoundRect(p, fill.marginsAdded(padding), bg, normal); } else { const auto h1 = context.st->topicsHeight; const auto h2 = st::normalFont->height; const auto hmin = std::min(h1, h2); const auto wantedInvertedRadius = hmin - radius; const auto invertedr = std::min(wantedInvertedRadius, delta / 2); const auto smallr = std::min(radius, delta - invertedr); const auto smallkey = (use1 < use2) ? smallr : (-smallr); if (normal.p[0].isNull()) { normal = Ui::PrepareCornerPixmaps(radius, bg); } if (inverted.p[0].isNull() || context.corners->invertedRadius != invertedr) { context.corners->invertedRadius = invertedr; inverted = Ui::PrepareInvertedCornerPixmaps(invertedr, bg); } if (smallr != radius && (small.isNull() || context.corners->smallKey != smallkey)) { context.corners->smallKey = smallr; auto pixmaps = Ui::PrepareCornerPixmaps(smallr, bg); small = pixmaps.p[(use1 < use2) ? 1 : 3]; } const auto rect1 = QRect(origin, QSize(use1, h1)); auto no1 = normal; no1.p[2] = QPixmap(); if (use1 < use2) { no1.p[3] = QPixmap(); } else if (smallr != radius) { no1.p[3] = small; } auto fill1 = rect1.marginsAdded({ padding.left(), padding.top(), padding.right(), (use1 < use2 ? -padding.top() : padding.bottom()), }); Ui::FillRoundRect(p, fill1, bg, no1); if (use1 < use2) { p.drawPixmap( fill1.x() + fill1.width(), fill1.y() + fill1.height() - invertedr, inverted.p[3]); } const auto add = QPoint(0, h1); const auto rect2 = QRect(origin + add, QSize(use2, h2)); const auto fill2 = rect2.marginsAdded({ padding.left(), (use2 < use1 ? -padding.bottom() : padding.top()), padding.right(), padding.bottom(), }); auto no2 = normal; no2.p[0] = QPixmap(); if (use2 < use1) { no2.p[1] = QPixmap(); } else if (smallr != radius) { no2.p[1] = small; } Ui::FillRoundRect(p, fill2, bg, no2); if (use2 < use1) { p.drawPixmap( fill2.x() + fill2.width(), fill2.y(), inverted.p[0]); } } } 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::PlainLink(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