From c260d9f0eab114edf35ef86cecbf45987ecd4cb4 Mon Sep 17 00:00:00 2001 From: upstream Date: Wed, 14 Oct 2026 18:12:56 +0200 Subject: [PATCH] Upstream telegramdesktop/tdesktop state of the touched files before 8f18ac2245 --- .../history/view/history_view_message.cpp | 7356 +++++++++++++++++ 1 file changed, 7356 insertions(+) create mode 100644 Telegram/SourceFiles/history/view/history_view_message.cpp diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp new file mode 100644 index 0000000..2b72afa --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -0,0 +1,7356 @@ +/* +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_message.h" + +#include "api/api_suggest_post.h" +#include "api/api_transcribes.h" +#include "base/options.h" +#include "base/qt/qt_key_modifiers.h" +#include "base/unixtime.h" +#include "core/application.h" +#include "core/click_handler_types.h" // ClickHandlerContext +#include "core/ui_integration.h" +#include "core/update_checker.h" +#include "history/view/history_view_cursor_state.h" +#include "history/history_item_components.h" +#include "history/history_item_helpers.h" +#include "history/view/media/history_view_media_generic.h" +#include "history/view/media/history_view_web_page.h" +#include "history/view/media/history_view_suggest_decision.h" +#include "history/view/reactions/history_view_reactions.h" +#include "history/view/reactions/history_view_reactions_button.h" +#include "history/view/history_view_reply_button.h" +#include "history/view/history_view_group_call_bar.h" // UserpicInRow. +#include "history/view/history_view_reply.h" +#include "history/view/history_view_transcribe_button.h" +#include "history/view/history_view_summary_header.h" +#include "history/view/history_view_view_button.h" // ViewButton. +#include "history/history.h" +#include "iv/markdown/iv_markdown_article_text.h" +#include "iv/markdown/iv_markdown_prepare_links.h" +#include "iv/iv_instance.h" +#include "iv/iv_rich_page.h" +#include "boxes/premium_preview_box.h" +#include "boxes/share_box.h" +#include "boxes/peers/tag_info_box.h" +#include "ui/chat/torn_edge.h" +#include "ui/effects/reaction_fly_animation.h" +#include "ui/effects/ripple_animation.h" +#include "ui/text/text_utilities.h" +#include "ui/text/text_custom_emoji.h" +#include "ui/text/text_extended_data.h" +#include "ui/power_saving.h" +#include "ui/rect.h" +//#include "ui/round_rect.h" +#include "data/components/factchecks.h" +#include "data/components/sponsored_messages.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "data/data_chat.h" +#include "data/data_channel.h" +#include "data/data_forum_topic.h" +#include "data/data_message_reactions.h" +#include "lang/lang_keys.h" +#include "mainwidget.h" +#include "main/main_session.h" +#include "settings/sections/settings_premium.h" +#include "ui/text/text_options.h" +#include "ui/painter.h" +#include "window/window_session_controller.h" +#include "apiwrap.h" +#include "api/api_rich_tasks.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_chat_style.h" +#include "styles/style_dialogs.h" +#include "styles/style_iv.h" +#include "styles/style_polls.h" + +namespace HistoryView { +namespace { + +base::options::toggle UnlimitedMessageWidth({ + .id = kOptionUnlimitedMessageWidth, + .name = "Unlimited message width", + .description = "Allow text-only message bubbles " + "to expand beyond the default maximum width.", + .restartRequired = true, +}); + +constexpr auto kPlayStatusLimit = 2; +constexpr auto kMaxWidth = (1 << 16) - 1; +constexpr auto kMaxNiceToReadLines = 6; +const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_"; +constexpr auto kFullLineAppearDuration = crl::time(300); +constexpr auto kFullLineAppearFinalDuration = crl::time(120); +constexpr auto kLineHeightAppearDuration = crl::time(100); +constexpr auto kLineHeightAppearFinalDuration = crl::time(60); +constexpr auto kMinWidthAppearDuration = crl::time(160); + +[[nodiscard]] int RevealLineRight(const Ui::Text::LineLayoutInfo &line) { + return line.left + line.width; +} + +void MergeTrailingSkipOnlyLine( + std::vector &lines, + int skipWidth, + int skipHeight) { + if (lines.size() < 2) { + return; + } + const auto &last = lines.back(); + const auto &prev = lines[lines.size() - 2]; + if (last.width == skipWidth && last.bottom - prev.bottom == skipHeight) { + const auto bottom = last.bottom; + lines.pop_back(); + lines.back().bottom = bottom; + } +} + +[[nodiscard]] int RichPageMediaEdgeSkip( + const Iv::Markdown::MarkdownArticleBubbleEdges &edges, + bool top) { + if (top) { + return edges.top + ? -st::msgPadding.top() + : edges.mediaAbove + ? 0 + : st::mediaInBubbleSkip; + } + return edges.infoBelow + ? st::mediaInBubbleSkip + : edges.bottom + ? -st::msgPadding.bottom() + : 0; +} + +// WHY: plain text keeps the quote verticalSkip between its text rect and a +// pre / blockquote frame; visual media sit where ordinary bubble media sit, +// and an info row below them starts mediaInBubbleSkip lower, like content. +[[nodiscard]] int RichPageEdgeSkip( + Iv::Markdown::MarkdownArticleEdgeBlock block, + const Iv::Markdown::MarkdownArticleBubbleEdges &edges, + bool top) { + using Block = Iv::Markdown::MarkdownArticleEdgeBlock; + switch (block) { + case Block::None: + case Block::Line: + return 0; + case Block::CodeFrame: + return st::messageTextStyle.pre.verticalSkip; + case Block::QuoteFrame: + return st::messageTextStyle.blockquote.verticalSkip; + case Block::VisualMedia: + return RichPageMediaEdgeSkip(edges, top); + } + Unexpected("Edge block in RichPageEdgeSkip."); +} + +using PreparedLink = Iv::Markdown::PreparedLink; +using PreparedLinkKind = Iv::Markdown::PreparedLinkKind; +using MediaActivation = Iv::Markdown::MediaActivation; +using MediaActivationKind = Iv::Markdown::MediaActivationKind; +using MarkdownArticleSelectionPosition = Iv::Markdown::MarkdownArticleSelectionPosition; +using MarkdownArticleSelection = Iv::Markdown::MarkdownArticleSelection; +using MarkdownArticleSelectionEndpoint = Iv::Markdown::MarkdownArticleSelectionEndpoint; +using MarkdownArticleSelectionEndpoints = Iv::Markdown::MarkdownArticleSelectionEndpoints; + +[[nodiscard]] auto FlatSelectionEndpointFromState(const TextState &state) +-> MessageSelectionFlatEndpoint { + return state.selectionCursor.isFlat() + ? state.selectionCursor.flat + : MessageSelectionFlatEndpoint{ + .symbol = state.symbol, + .afterSymbol = state.afterSymbol, + }; +} + +void SetRichPageSelectionCursor( + not_null state, + int segment, + int offset, + bool direct) { + state->selectionCursor = MessageSelectionEndpoint::RichPage( + { + .segment = segment, + .offset = offset, + }, + MarkdownArticleSelectionEndpoint{ + .segment = segment, + .direct = direct, + }); +} + +[[nodiscard]] MarkdownArticleSelection AdjustRichPageSelection( + const Iv::Markdown::MarkdownArticle &article, + MarkdownArticleSelectionPosition anchor, + MarkdownArticleSelectionPosition focus, + TextSelectType type) { + if (anchor.segment == focus.segment) { + const auto adjusted = article.adjustSelection( + anchor.segment, + TextSelection( + uint16(std::min(anchor.offset, focus.offset)), + uint16(std::max(anchor.offset, focus.offset))), + type); + return { + .from = { .segment = anchor.segment, .offset = adjusted.from }, + .to = { .segment = anchor.segment, .offset = adjusted.to }, + }; + } + const auto focusBeforeAnchor = CompareMessageSelectionPositions( + focus, + anchor) < 0; + const auto anchorExpanded = article.adjustSelection( + anchor.segment, + TextSelection(uint16(anchor.offset), uint16(anchor.offset)), + type); + const auto focusExpanded = article.adjustSelection( + focus.segment, + TextSelection(uint16(focus.offset), uint16(focus.offset)), + type); + return { + .from = { + .segment = anchor.segment, + .offset = focusBeforeAnchor + ? int(anchorExpanded.to) + : int(anchorExpanded.from), + }, + .to = { + .segment = focus.segment, + .offset = focusBeforeAnchor + ? int(focusExpanded.from) + : int(focusExpanded.to), + }, + }; +} + +[[nodiscard]] QString OpenableTargetForPreparedLink(const PreparedLink &link) { + return link.fragment.isEmpty() + ? link.target + : (link.target + u"#"_q + link.fragment); +} + +[[nodiscard]] ClickHandler::TextEntity TextEntityForPreparedLink( + const PreparedLink &link) { + if (const auto external = ExternalEntityLinkData(link)) { + return { + .type = external->type, + .data = external->data, + }; + } + return {}; +} + +[[nodiscard]] QString CopyTextForPreparedLink(const PreparedLink &link) { + if (const auto external = ExternalEntityLinkData(link)) { + return external->text; + } + switch (link.kind) { + case PreparedLinkKind::Anchor: + case PreparedLinkKind::FootnoteBacklink: + return link.target.isEmpty() ? QString() : (u"#"_q + link.target); + case PreparedLinkKind::Footnote: + return !link.copyText.isEmpty() ? link.copyText : link.target; + case PreparedLinkKind::LocalFile: + case PreparedLinkKind::InstantViewPage: + return OpenableTargetForPreparedLink(link); + case PreparedLinkKind::External: + return link.target; + case PreparedLinkKind::RejectedRelative: + case PreparedLinkKind::ToggleDetails: + case PreparedLinkKind::ToggleBlockquote: + return QString(); + } + return QString(); +} + +[[nodiscard]] QString CopyLabelForPreparedLink(const PreparedLink &link) { + if (const auto external = ExternalEntityLinkData(link)) { + return (external->type == EntityType::Email) + ? Ui::Integration::Instance().phraseContextCopyEmail() + : Ui::Integration::Instance().phraseContextCopyLink(); + } + switch (link.kind) { + case PreparedLinkKind::RejectedRelative: + case PreparedLinkKind::ToggleDetails: + case PreparedLinkKind::ToggleBlockquote: + return QString(); + case PreparedLinkKind::External: + case PreparedLinkKind::InstantViewPage: + case PreparedLinkKind::Anchor: + case PreparedLinkKind::Footnote: + case PreparedLinkKind::FootnoteBacklink: + case PreparedLinkKind::LocalFile: + return tr::lng_context_copy_link(tr::now); + } + return QString(); +} + +void CopyRichPageCodeBlockText(TextForMimeData text, ClickContext context) { + if (context.button != Qt::LeftButton || text.empty()) { + return; + } else if (!text.rich.text.endsWith('\n')) { + text.rich.text.append('\n'); + } + if (!text.expanded.endsWith('\n')) { + text.expanded.append('\n'); + } + if (Ui::Integration::Instance().copyPreOnClick(context.other)) { + TextUtilities::SetClipboardText(std::move(text)); + } +} + +[[nodiscard]] bool SamePreparedLink( + const std::optional &a, + const std::optional &b) { + if (!a || !b) { + return !a && !b; + } + return (a->index == b->index) + && (a->kind == b->kind) + && (a->target == b->target) + && (a->fragment == b->fragment) + && (a->copyText == b->copyText) + && (a->entityType == b->entityType) + && (a->shown == b->shown) + && (a->webpageId == b->webpageId); +} + +[[nodiscard]] bool SameEmbedRequest( + const Iv::Markdown::EmbedRequest &a, + const Iv::Markdown::EmbedRequest &b) { + return (a.html == b.html) + && (a.url == b.url) + && (a.width == b.width) + && (a.height == b.height) + && (a.fullWidth == b.fullWidth) + && (a.fixedHeight == b.fixedHeight) + && (a.allowScrolling == b.allowScrolling); +} + +[[nodiscard]] bool SameMediaActivation( + const MediaActivation &a, + const MediaActivation &b) { + return (a.kind == b.kind) + && (a.url == b.url) + && SameEmbedRequest(a.embed, b.embed) + && (a.placeholderId.value == b.placeholderId.value) + && (a.photo.get() == b.photo.get()) + && (a.document.get() == b.document.get()) + && (a.channel.get() == b.channel.get()); +} + +[[nodiscard]] std::optional +RichPageTaskMarkerHit( + const Iv::Markdown::MarkdownArticle &article, + QPoint point) { + const auto hit = article.editControlHitTest(point); + using Kind = Iv::Markdown::MarkdownArticleEditControlHitKind; + return (hit.kind == Kind::TaskMarker) ? hit.listItem : std::nullopt; +} + +class RichPageActionClickHandler final : public ClickHandler { +public: + RichPageActionClickHandler( + Fn callback, + std::optional link = std::nullopt) + : _callback(std::move(callback)) + , _link(std::move(link)) { + } + + void onClick(ClickContext context) const override { + if (_callback) { + _callback(std::move(context)); + } + } + + QString url() const override { + return _link ? _link->target : QString(); + } + + QString copyToClipboardText() const override { + return _link ? CopyTextForPreparedLink(*_link) : QString(); + } + + QString copyToClipboardContextItemText() const override { + return _link ? CopyLabelForPreparedLink(*_link) : QString(); + } + + TextEntity getTextEntity() const override { + return _link ? TextEntityForPreparedLink(*_link) : TextEntity(); + } + + QString tooltip() const override { + return _link + ? Iv::Markdown::TooltipForPreparedLink(*_link) + : QString(); + } + + [[nodiscard]] const std::optional &link() const { + return _link; + } + +private: + const Fn _callback; + const std::optional _link; +}; + +void ApplyRevealGradient( + not_null appearing, + QImage &cache, + int availableWidth) { + const auto ratio = style::DevicePixelRatio(); + const auto maskWidth = int(st::textRevealGradient) * ratio; + if (appearing->gradientMask.width() != maskWidth) { + auto mask = QImage( + QSize(maskWidth, 1), + QImage::Format_ARGB32_Premultiplied); + mask.setDevicePixelRatio(ratio); + mask.fill(Qt::transparent); + { + const auto logicalWidth = int(st::textRevealGradient); + auto p = QPainter(&mask); + auto gradient = QLinearGradient(0, 0, logicalWidth, 0); + gradient.setStops({ + { 0., QColor(255, 255, 255, 255) }, + { 1., QColor(255, 255, 255, 0) }, + }); + p.fillRect(0, 0, logicalWidth, 1, gradient); + } + appearing->gradientMask = std::move(mask); + } + + const auto cacheW = int(cache.width() / cache.devicePixelRatio()); + const auto cacheH = int(cache.height() / cache.devicePixelRatio()); + const auto revealedWidth = appearing->revealedLineWidth; + const auto gradientWidth = std::min( + int(st::textRevealGradient), + revealedWidth); + const auto lineRtl = appearing->lines[appearing->shownLine].rtl; + + auto p = QPainter(&cache); + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + if (lineRtl) { + const auto clampedAvailableWidth = std::min( + std::max(availableWidth, 0), + cacheW); + const auto rightEdge = clampedAvailableWidth - revealedWidth; + p.fillRect( + QRect(0, 0, rightEdge, cacheH), + Qt::transparent); + p.save(); + p.translate(rightEdge + gradientWidth, 0); + p.scale(-1, 1); + p.drawImage( + QRect(0, 0, gradientWidth, cacheH), + appearing->gradientMask); + p.restore(); + } else { + p.fillRect( + QRect(revealedWidth, 0, cacheW - revealedWidth, cacheH), + Qt::transparent); + p.drawImage( + QRect(revealedWidth - gradientWidth, 0, gradientWidth, cacheH), + appearing->gradientMask); + } +} + +struct SecondRightAction { + std::unique_ptr ripple; + ClickHandlerPtr link; +}; + +struct BadgePillGeometry { + int textWidth = 0; + int width = 0; + int height = 0; +}; + +[[nodiscard]] bool IsRippleLink(const ClickHandlerPtr &handler) { + switch (handler->getTextEntity().type) { + case EntityType::Url: + case EntityType::CustomUrl: + case EntityType::Email: + case EntityType::Hashtag: + case EntityType::Cashtag: + case EntityType::Mention: + case EntityType::MentionName: + case EntityType::BotCommand: + case EntityType::Phone: + case EntityType::BankCard: + case EntityType::TonAddress: + case EntityType::FormattedDate: + return true; + default: + return false; + } +} + +[[nodiscard]] ClickHandlerPtr MakeTopicButtonLink( + not_null topic, + MsgId messageId) { + const auto weak = base::make_weak(topic); + return std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + if (const auto strong = weak.get()) { + controller->showTopic( + strong, + messageId, + Window::SectionShow::Way::Forward); + } + } + }); +} + +[[nodiscard]] BadgePillGeometry ComputeBadgePillGeometry( + not_null badge) { + const auto &padding = st::msgTagBadgePadding; + const auto textWidth = badge->tag.maxWidth(); + const auto contentWidth = padding.left() + + textWidth + + padding.right(); + const auto height = padding.top() + + st::msgFont->height + + padding.bottom(); + return { + .textWidth = textWidth, + .width = std::max(contentWidth, height), + .height = height, + }; +} + +} // namespace + +const char kOptionUnlimitedMessageWidth[] + = "unlimited-message-width"; + +struct Message::CommentsButton { + std::unique_ptr ripple; + std::vector userpics; + QImage cachedUserpics; + ClickHandlerPtr link; + QPoint lastPoint; + int rippleShift = 0; +}; + +struct Message::FromNameStatus { + EmojiStatusId id; + std::unique_ptr custom; + ClickHandlerPtr link; + int skip = 0; +}; + +struct Message::RightAction { + std::unique_ptr ripple; + ClickHandlerPtr link; + QPoint lastPoint; + std::unique_ptr second; +}; + +struct Message::LinkRipple { + std::unique_ptr ripple; + ClickHandlerPtr link; + QPoint maskOffset; + int cachedWidth = 0; +}; + +LogEntryOriginal::LogEntryOriginal() = default; + +LogEntryOriginal::LogEntryOriginal(LogEntryOriginal &&other) +: page(std::move(other.page)) { +} + +LogEntryOriginal &LogEntryOriginal::operator=(LogEntryOriginal &&other) { + page = std::move(other.page); + return *this; +} + +LogEntryOriginal::~LogEntryOriginal() = default; + +Message::Message( + not_null delegate, + not_null data, + Element *replacing) +: Element(delegate, data, replacing, Flag(0)) +, _hideReply(delegate->elementHideReply(this)) +, _postShowingAuthor(data->isPostShowingAuthor() ? 1 : 0) +, _bottomInfo( + &data->history()->owner().reactions(), + BottomInfoDataFromMessage(this)) { + if (data->Get()) { + _hideReply = 1; + } else if (const auto media = data->media()) { + if (media->giveawayResults()) { + _hideReply = 1; + } + } + initLogEntryOriginal(); + initPsa(); + if (data->displayHiddenSenderInfo()) { + AddComponents(HiddenSenderTooltip::Bit()); + } + setupReactions(replacing); + auto animation = replacing ? replacing->takeEffectAnimation() : nullptr; + if (animation) { + _bottomInfo.continueEffectAnimation(std::move(animation)); + } + if (data->isSponsored()) { + const auto &session = data->history()->session(); + const auto details = session.sponsoredMessages().lookupDetails( + data->fullId()); + if (details.canReport) { + _rightAction = std::make_unique(); + _rightAction->second = std::make_unique(); + + _rightAction->second->link = ReportSponsoredClickHandler(data); + } + } + initPaidInformation(); + + if (data->textAppearing()) { + AddComponents(TextAppearing::Bit()); + const auto appearing = Get(); + if (replacing) { + if (const auto was = replacing->Get()) { + *appearing = std::move(*was); + appearing->widthAnimation.setCallback([=] { + textAppearWidthCallback(); + }); + appearing->heightAnimation.setCallback([=] { + textAppearHeightCallback(); + }); + } + } + if (data->textAppearingStarted() + && !appearing->widthAnimation.animating() + && !appearing->heightAnimation.animating()) { + skipInactiveTextAppearing(); + } + } +} + +Message::~Message() { + if (_comments || (_fromNameStatus && _fromNameStatus->custom)) { + _comments = nullptr; + _fromNameStatus = nullptr; + checkHeavyPart(); + } +} + +void HistoryMessageRichPage::Host::requestRepaint(QRect articleRect) { + if (const auto strong = owner.get()) { + crl::on_main(strong, [weak = owner, articleRect] { + if (const auto owner = weak.get()) { + owner->requestRichPageRepaint(articleRect); + } + }); + } +} + +void HistoryMessageRichPage::Host::requestRelayout(QRect articleRect) { + if (const auto strong = owner.get()) { + crl::on_main(strong, [weak = owner, articleRect] { + if (const auto owner = weak.get()) { + owner->requestRichPageRelayout(articleRect); + } + }); + } +} + +HistoryMessageRichPage::HistoryMessageRichPage() +: host(std::make_unique()) +, article(st::messageMarkdown) { +} + +QMargins HistoryMessageRichPage::edgeSkips() const { + const auto blocks = article.edgeBlocks(); + const auto edges = article.bubbleEdges(); + return { + 0, + RichPageEdgeSkip(blocks.top, edges, true), + 0, + RichPageEdgeSkip(blocks.bottom, edges, false), + }; +} + +void Message::setInstantViewMediaRuntime(QString pageUrl) { + Expects(Has() + || !Has()); + // We mustn't add the component here if we're in rich message, + // because we call this method where we remember RichPage reference. + + AddComponents(InstantViewMediaRuntime::Bit()); + Get()->pageUrl = std::move(pageUrl); +} + +bool Message::hasRichPage() const { + return (richpage() != nullptr); +} + +void Message::requestRichPageRepaint(QRect articleRect) const { + Q_UNUSED(articleRect); + repaint(); +} + +void Message::requestRichPageRelayout(QRect articleRect) { + Q_UNUSED(articleRect); + if (const auto rich = const_cast(this)->richpage()) { + rich->article.invalidateLayout(); + } + // The article layout was just invalidated, but textHeightFor() caches by + // _textWidth and would short-circuit when re-queried at the same (constant) + // max width, leaving the bubble sized from a stale, now-zeroed + // lastLayoutWidth(). Drop the text-size cache too so the article is really + // laid out again, the same way blockquoteExpandChanged() does for quotes. + invalidateTextSizeCache(); + setPendingResize(); + history()->owner().requestViewResize(this); +} + +void Message::activateRichPagePreparedLink( + const PreparedLink &link, + ClickContext context) const { + if (context.button != Qt::LeftButton + && context.button != Qt::MiddleButton) { + return; + } + const auto resolveInlineAnchor = [owner = const_cast(this)]( + const QString &anchorId) { + if (anchorId.isEmpty()) { + return false; + } + const auto rich = owner->richpage(); + if (!rich) { + return false; + } + auto top = rich->article.anchorTop(anchorId); + auto trect = QRect(); + auto haveTrect = false; + if (top < 0) { + const auto expansion = rich->article.expandDetailsToAnchor(anchorId); + if (expansion.changed) { + haveTrect = owner->prepareRichPageTextRect(trect); + if (!haveTrect) { + return false; + } + const auto rect = owner->richPageRect(trect); + rich->article.resizeGetHeight(rect.width()); + top = rich->article.anchorTop(anchorId); + owner->requestRichPageRelayout(QRect()); + } else { + top = rich->article.anchorTop(anchorId); + } + if (top < 0) { + return false; + } + } + if (!haveTrect && !owner->prepareRichPageTextRect(trect)) { + return false; + } + return owner->delegate()->elementScrollToLocalY( + owner, + owner->richPageRect(trect).top() + top); + }; + switch (link.kind) { + case PreparedLinkKind::External: + if (const auto data = ExternalEntityLinkData(link)) { + if (const auto handler = Ui::Integration::Instance() + .createLinkHandler(*data, Ui::Text::MarkedContext())) { + handler->onClick(std::move(context)); + } + } + break; + case PreparedLinkKind::InstantViewPage: { + const auto target = OpenableTargetForPreparedLink(link); + if (target.isEmpty()) { + return; + } + if (const auto controller = ExtractController(context)) { + Core::App().iv().openWithIvPreferred( + controller, + target, + context.other); + } else { + UrlClickHandler::Open(target, context.other); + } + } break; + case PreparedLinkKind::Anchor: + case PreparedLinkKind::Footnote: + case PreparedLinkKind::FootnoteBacklink: { + if (resolveInlineAnchor(link.target)) { + break; + } + const auto rich = richpage(); + if (rich && rich->page && rich->page->part) { + if (const auto controller = ExtractController(context)) { + Core::App().iv().showRichMessage( + controller, + data(), + link.target); + } + } else if (const auto controller = ExtractController(context)) { + controller->showToast( + tr::lng_iv_not_found_in_message(tr::now)); + } + break; + } + case PreparedLinkKind::LocalFile: { + const auto target = OpenableTargetForPreparedLink(link); + if (!target.isEmpty()) { + UrlClickHandler::Open(target, context.other); + } + } break; + case PreparedLinkKind::RejectedRelative: + break; + case PreparedLinkKind::ToggleDetails: + if (const auto rich = const_cast(this)->richpage() + ; rich && rich->article.toggleDetails(link.target)) { + const_cast(this)->requestRichPageRelayout(QRect()); + } + break; + case PreparedLinkKind::ToggleBlockquote: + if (const auto rich = const_cast(this)->richpage() + ; rich && rich->article.toggleBlockquote(link.target)) { + const_cast(this)->requestRichPageRelayout(QRect()); + } + break; + } +} + +QRect Message::richPageRect(QRect trect) const { + Expects(hasRichPage()); + + const auto skips = richpage()->edgeSkips(); + trect.setTop(trect.top() + skips.top()); + if (skips.bottom() < 0) { + trect.setHeight(trect.height() - skips.bottom()); + } + return trect.marginsAdded( + { st::msgPadding.left(), 0, st::msgPadding.right(), 0 }); +} + +bool Message::prepareRichPageTextRect(QRect &trect) const { + if (!hasVisibleText()) { + return false; + } + const auto item = data(); + const auto media = this->media(); + auto g = countGeometry(); + if (g.width() < 1 || isHidden() || !drawBubble()) { + return false; + } + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + const auto mediaDisplayed = media && media->isDisplayed(); + if (_reactions && !reactionsInBubble) { + g.setHeight(g.height() - st::mediaInBubbleSkip - _reactions->height()); + } + if (const auto keyboard = inlineReplyKeyboard()) { + g.setHeight( + g.height() + - st::msgBotKbButton.margin + - keyboard->naturalHeight()); + } + auto inner = g; + if (_comments) { + inner.setHeight(inner.height() - st::historyCommentsButtonHeight); + } + trect = inner.marginsRemoved(st::msgPadding); + const auto additionalInfoSkip = (mediaDisplayed + && !media->additionalInfoString().isEmpty()) + ? st::msgDateFont->height + : 0; + const auto reactionsTop = (reactionsInBubble && !_viewButton) + ? (additionalInfoSkip + st::mediaInBubbleSkip) + : additionalInfoSkip; + const auto reactionsHeight = reactionsInBubble + ? (reactionsTop + _reactions->height()) + : 0; + if (reactionsInBubble) { + trect.setHeight(trect.height() - reactionsHeight); + } + if (_viewButton) { + trect.setHeight(trect.height() - _viewButton->height()); + if (reactionsInBubble) { + trect.setHeight( + trect.height() + - st::mediaInBubbleSkip + + st::msgPadding.bottom()); + } else if (mediaDisplayed) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip); + } + } + const auto check = factcheckBlock(); + const auto entry = logEntryOriginal(); + const auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) + || check + || entry; + const auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) + || (entry && entry->isBubbleTop()); + if (mediaOnBottom) { + trect.setHeight(trect.height() + st::msgPadding.bottom()); + } + if (mediaOnTop) { + trect.setY(trect.y() - st::msgPadding.top()); + } else { + if (displayFromName()) { + trect.setTop(trect.top() + st::msgNameFont->height); + } + if (const auto badge = Get()) { + trect.setTop(trect.top() + badge->height); + } + if (displayedTopicButton()) { + trect.setTop(trect.top() + + st::topicButtonSkip + + st::topicButtonPadding.top() + + st::msgNameFont->height + + st::topicButtonPadding.bottom() + + st::topicButtonSkip); + } + if (displayForwardedFrom()) { + const auto forwarded = item->Get(); + const auto skip = forwarded->psaType.isEmpty() + ? 0 + : st::historyPsaIconSkip1; + const auto fits = (forwarded->text.maxWidth() + <= (trect.width() - skip)); + trect.setTop(trect.top() + + ((fits ? 1 : 2) * st::semiboldFont->height)); + } + if (item->Has() + && !displayFromName() + && !displayForwardedFrom()) { + trect.setTop(trect.top() + st::msgNameFont->height); + } + if (const auto reply = Get()) { + trect.setTop(trect.top() + reply->height()); + } + if (const auto summaryHeader = Get()) { + trect.setTop(trect.top() + summaryHeader->height()); + } + } + if (entry) { + trect.setHeight(trect.height() - entry->height()); + } + if (check) { + trect.setHeight( + trect.height() + - check->height() + - st::mediaInBubbleSkip); + } + if (mediaDisplayed && _invertMedia) { + const auto mediaTop = trect.y() + + (mediaOnTop ? 0 : st::mediaInBubbleSkip); + trect.setY(mediaTop + + media->height() + + (mediaOnBottom ? 0 : st::mediaInBubbleSkip)); + } + return true; +} + +QPoint Message::prepareRichPageStateRect(QPoint point, QRect &trect) const { + if (const auto botTop = Get()) { + trect.setY(trect.y() + botTop->height); + } + trect = richPageRect(trect); + return point - trect.topLeft(); +} + +void Message::activateRichPageMedia( + const MediaActivation &activation, + ClickContext context) const { + if (context.button != Qt::LeftButton + && context.button != Qt::MiddleButton) { + return; + } + switch (activation.kind) { + case MediaActivationKind::None: + break; + case MediaActivationKind::Embed: + if (!activation.embed.url.isEmpty()) { + HiddenUrlClickHandler::Open( + activation.embed.url, + context.other); + } + break; + case MediaActivationKind::ExternalUrl: + if (!activation.url.isEmpty()) { + HiddenUrlClickHandler::Open(activation.url, context.other); + } + break; + case MediaActivationKind::Photo: + if (activation.photo) { + activation.photo->open(context.button); + } + break; + case MediaActivationKind::Document: + if (activation.document) { + activation.document->open(context.button); + } + break; + case MediaActivationKind::OpenChannel: + if (activation.channel) { + activation.channel->open(context.button); + } + break; + case MediaActivationKind::JoinChannel: + if (activation.channel) { + activation.channel->join(context.button); + } + break; + case MediaActivationKind::UnsupportedBlock: + Core::UpdateApplication(); + break; + } +} + +void Message::refreshSuggestedInfo( + not_null item, + not_null suggest, + const HistoryMessageReply *replyData) { + const auto link = (replyData && replyData->resolvedMessage) + ? JumpToMessageClickHandler( + replyData->resolvedMessage.get(), + item->fullId()) + : ClickHandlerPtr(); + setServicePreMessage({}, link, std::make_unique( + this, + GenerateSuggestRequestMedia(this, suggest), + MediaGenericDescriptor{ + .maxWidth = st::chatSuggestWidth, + .fullAreaLink = link, + .service = true, + .hideServiceText = true, + })); +} + +void Message::initPaidInformation() { + const auto item = data(); + if (item->history()->peer->isMonoforum()) { + if (const auto suggest = item->Get()) { + const auto replyData = item->Get(); + refreshSuggestedInfo(item, suggest, replyData); + } + return; + } else if (!item->history()->peer->isUser()) { + return; + } + const auto media = this->media(); + const auto mine = PaidInformation{ + .messages = 1, + .stars = item->starsPaid(), + }; + auto info = media ? media->paidInformation().value_or(mine) : mine; + if (!info) { + return; + } + const auto action = [&] { + return (info.messages == 1) + ? tr::lng_action_paid_message_one( + tr::now, + tr::marked) + : tr::lng_action_paid_message_some( + tr::now, + lt_count, + info.messages, + tr::marked); + }; + auto text = PreparedServiceText{ + .text = item->out() + ? tr::lng_action_paid_message_sent( + tr::now, + lt_count, + info.stars, + lt_action, + action(), + tr::marked) + : tr::lng_action_paid_message_got( + tr::now, + lt_count, + info.stars, + lt_name, + tr::link(item->from()->shortName(), 1), + tr::marked), + }; + if (!item->out()) { + text.links.push_back(item ->from()->createOpenLink()); + } + setServicePreMessage(std::move(text)); +} + +void Message::refreshRightBadge() { + if (const auto badge = Get(); badge && badge->overridden) { + return; + } + if (hasOutLayout() || context() == Context::WelcomeMessages) { + if (Has()) { + RemoveComponents(RightBadge::Bit()); + } + return; + } + const auto item = data(); + const auto [text, role, special] = [&]() -> std::tuple { + if (item->isDiscussionPost()) { + return { + (delegate()->elementContext() == Context::Replies) + ? QString() + : tr::lng_channel_badge(tr::now), + BadgeRole::User, + true, + }; + } else if (item->author()->isMegagroup()) { + if (const auto msgsigned = item->Get()) { + if (!msgsigned->viaBusinessBot) { + Assert(msgsigned->isAnonymousRank); + return { msgsigned->author, BadgeRole::User, false }; + } + } + } + const auto channel = item->history()->peer->asMegagroup(); + const auto user = item->author()->asUser(); + if (!channel) { + if (const auto chat = item->history()->peer->asChat()) { + if (user) { + const auto j = chat->memberRanks.find( + peerToUser(user->id)); + if (j != chat->memberRanks.end()) { + const auto basicRole + = (peerToUser(user->id) == chat->creator) + ? BadgeRole::Creator + : chat->admins.contains(user) + ? BadgeRole::Admin + : BadgeRole::User; + return { j->second, basicRole, false }; + } + } + } + return { QString(), BadgeRole::User, false }; + } + if (!user) { + return { QString(), BadgeRole::User, false }; + } + const auto info = channel->mgInfo.get(); + const auto userId = peerToUser(user->id); + const auto isCreator = (info->creator == user); + const auto isAdmin = info->admins.contains(userId); + if (isCreator || isAdmin) { + const auto r = info->memberRanks.find(userId); + if (r != info->memberRanks.end() && !r->second.isEmpty()) { + return { + r->second, + isCreator ? BadgeRole::Creator : BadgeRole::Admin, + false, + }; + } + if (isCreator) { + return { tr::lng_owner_badge(tr::now), BadgeRole::Creator, false }; + } + return { tr::lng_admin_badge(tr::now), BadgeRole::Admin, false }; + } + const auto fromRank = item->fromRank(); + if (!fromRank.isEmpty()) { + return { fromRank, BadgeRole::User, false }; + } + return { QString(), BadgeRole::User, false }; + }(); + auto tagText = TextWithEntities{ + (text.isEmpty() + ? delegate()->elementAuthorRank(this) + : TextUtilities::RemoveEmoji(TextUtilities::SingleLine(text))) + }; + const auto boosts = item->boostsApplied(); + const auto needBadge = !tagText.empty() || boosts; + if (!needBadge) { + if (Has()) { + RemoveComponents(RightBadge::Bit()); + } + return; + } + if (!Has()) { + AddComponents(RightBadge::Bit()); + } + const auto badge = Get(); + badge->role = role; + badge->special = special || (text.isEmpty() && !tagText.empty()); + badge->tagLink = nullptr; + badge->ripple = nullptr; + if (tagText.empty()) { + badge->tag.clear(); + } else { + badge->tag.setMarkedText( + st::defaultTextStyle, + tagText, + Ui::NameTextOptions()); + } + if (boosts) { + const auto many = (boosts > 1); + auto boostText = Ui::Text::IconEmoji(many + ? &st::boostsMessageIcon + : &st::boostMessageIcon + ).append(many ? QString::number(boosts) : QString()); + badge->boosts.setMarkedText( + st::defaultTextStyle, + boostText, + Ui::NameTextOptions()); + } else { + badge->boosts.clear(); + } + const auto boostWidth = badge->boosts.isEmpty() + ? 0 + : (st::msgTagBadgeBoostSkip + badge->boosts.maxWidth()); + if (badge->role == BadgeRole::User) { + const auto tagWidth = badge->tag.isEmpty() + ? 0 + : badge->tag.maxWidth(); + badge->width = tagWidth + boostWidth; + } else { + const auto &padding = st::msgTagBadgePadding; + const auto textWidth = badge->tag.maxWidth(); + const auto contentWidth = padding.left() + + textWidth + + padding.right(); + const auto pillHeight = padding.top() + + st::msgFont->height + + padding.bottom(); + badge->width = std::max(contentWidth, pillHeight) + boostWidth; + } +} + +int Message::rightBadgeWidth() const { + const auto badge = Get(); + return badge ? badge->width : 0; +} + +void Message::applyGroupAdminChanges( + const base::flat_set &changes) { + if (!data()->out() + && changes.contains(peerToUser(data()->author()->id))) { + history()->owner().requestViewResize(this); + } +} + +void Message::animateReaction(Ui::ReactionFlyAnimationArgs &&args) { + const auto media = this->media(); + + auto g = countGeometry(); + if (g.width() < 1 || isHidden()) { + return; + } + const auto repainter = [=] { repaint(); }; + + const auto bubble = drawBubble(); + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + const auto mediaDisplayed = media && media->isDisplayed(); + + if (_reactions && !reactionsInBubble) { + const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height(); + const auto reactionsLeft = (!bubble && mediaDisplayed) + ? media->contentRectForReactions().x() + : 0; + g.setHeight(g.height() - reactionsHeight); + const auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip); + _reactions->animate(args.translated(-reactionsPosition), repainter); + return; + } + + const auto keyboard = inlineReplyKeyboard(); + auto keyboardHeight = 0; + if (keyboard) { + keyboardHeight = keyboard->naturalHeight(); + g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); + } + + if (bubble) { + // Entry page is always a bubble bottom. + auto inner = g; + if (_comments) { + inner.setHeight(inner.height() - st::historyCommentsButtonHeight); + } + auto trect = inner.marginsRemoved(st::msgPadding); + const auto reactionsTop = (reactionsInBubble && !_viewButton) + ? st::mediaInBubbleSkip + : 0; + const auto reactionsHeight = reactionsInBubble + ? (reactionsTop + _reactions->height()) + : 0; + if (reactionsInBubble) { + trect.setHeight(trect.height() - reactionsHeight); + const auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + reactionsTop); + _reactions->animate(args.translated(-reactionsPosition), repainter); + return; + } + } +} + +auto Message::takeEffectAnimation() +-> std::unique_ptr { + return _bottomInfo.takeEffectAnimation(); +} + +QRect Message::effectIconGeometry() const { + if (hidesBottomInfo()) { + return {}; + } + const auto media = this->media(); + + auto g = countGeometry(); + if (g.width() < 1 || isHidden()) { + return {}; + } + const auto bubble = drawBubble(); + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto keyboard = inlineReplyKeyboard(); + auto keyboardHeight = 0; + if (keyboard) { + keyboardHeight = keyboard->naturalHeight(); + g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); + } + + const auto fromBottomInfo = [&](QPoint bottomRight) { + const auto size = _bottomInfo.currentSize(); + return _bottomInfo.effectIconGeometry().translated( + bottomRight - QPoint(size.width(), size.height())); + }; + if (bubble) { + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + + // Entry page is always a bubble bottom. + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); + + auto inner = g; + if (_comments) { + inner.setHeight(inner.height() - st::historyCommentsButtonHeight); + } + auto trect = inner.marginsRemoved(st::msgPadding); + const auto reactionsTop = (reactionsInBubble && !_viewButton) + ? st::mediaInBubbleSkip + : 0; + const auto reactionsHeight = reactionsInBubble + ? (reactionsTop + _reactions->height()) + : 0; + if (_viewButton) { + const auto belowInfo = _viewButton->belowMessageInfo(); + const auto infoHeight = reactionsInBubble + ? (reactionsHeight + 2 * st::mediaInBubbleSkip) + : bottomInfoHeight(); + const auto heightMargins = QMargins(0, 0, 0, infoHeight); + if (belowInfo) { + inner -= heightMargins; + } + trect.setHeight(trect.height() - _viewButton->height()); + if (reactionsInBubble) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip + st::msgPadding.bottom()); + } else if (mediaDisplayed) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip); + } + } + if (mediaOnBottom) { + trect.setHeight(trect.height() + + st::msgPadding.bottom() + - viewButtonHeight()); + } + if (mediaOnTop) { + trect.setY(trect.y() - st::msgPadding.top()); + } + if (mediaDisplayed && mediaOnBottom && media->customInfoLayout()) { + auto mediaHeight = media->height(); + auto mediaLeft = trect.x() - st::msgPadding.left(); + auto mediaTop = (trect.y() + trect.height() - mediaHeight); + return fromBottomInfo(QPoint(mediaLeft, mediaTop) + media->resolveCustomInfoRightBottom()); + } else { + return fromBottomInfo({ + inner.left() + inner.width() - (st::msgPadding.right() - st::msgDateDelta.x()), + inner.top() + inner.height() - (st::msgPadding.bottom() - st::msgDateDelta.y()), + }); + } + } else if (mediaDisplayed) { + return fromBottomInfo(g.topLeft() + media->resolveCustomInfoRightBottom()); + } + return {}; +} + +QSize Message::performCountOptimalSize() { + const auto item = data(); + + const auto replyData = item->Get(); + const auto &summary = item->summaryEntry(); + const auto showSummaryReply = !summary.result.empty() && summary.shown; + + if (replyData && !_hideReply) { + AddComponents(Reply::Bit()); + } else { + RemoveComponents(Reply::Bit()); + } + if (showSummaryReply) { + AddComponents(SummaryHeader::Bit()); + } else { + RemoveComponents(SummaryHeader::Bit()); + } + + if (item->history()->peer->isMonoforum()) { + if (const auto suggest = item->Get()) { + if (const auto service = Get()) { + // Ok, we didn't have the message, but now we have. + // That means this is not a plain post suggestion, + // but a suggestion of changes to previous suggestion. + if (service->media + && !service->handler + && replyData + && replyData->resolvedMessage) { + refreshSuggestedInfo(item, suggest, replyData); + } + } + } + } + + if (const auto postSender = item->discussionPostOriginalSender()) { + if (!postSender->isFullLoaded()) { + // We need it for available reactions list. + postSender->updateFull(); + } + } + + const auto factcheck = item->Get(); + if (factcheck && !factcheck->data.text.empty()) { + AddComponents(Factcheck::Bit()); + Get()->page = history()->session().factchecks().makeMedia( + this, + factcheck); + } else { + RemoveComponents(Factcheck::Bit()); + } + refreshRightBadge(); + + const auto markup = item->inlineReplyMarkup(); + const auto reactionsKey = [&] { + return embedReactionsInBubble() ? 0 : 1; + }; + const auto oldKey = reactionsKey(); + if (_summarize) { + const auto &summary = item->summaryEntry(); + if (_summarize->loading() != summary.loading) { + _summarize->setLoading(summary.loading); + } + } + validateText(); + validateInlineKeyboard(markup); + updateViewButtonExistence(); + refreshTopicButton(); + + const auto media = this->media(); + const auto textItem = this->textItem(); + const auto defaultInvert = media && media->aboveTextByDefault(); + const auto invertDefault = textItem + && textItem->invertMedia() + && !textItem->emptyText(); + _invertMedia = invertDefault ? !defaultInvert : defaultInvert; + + updateMediaInBubbleState(); + if (oldKey != reactionsKey()) { + refreshReactions(); + } + refreshInfoSkipBlock(textItem); + + const auto botTop = item->isFakeAboutView() + ? Get() + : nullptr; + const auto ephemeralBadge = Get(); + const auto bubble = drawBubble(); + auto withVisibleText = false; + auto fullTextualWidth = 0; + if (botTop) { + botTop->init(); + } + if (ephemeralBadge) { + ephemeralBadge->init(this); + } + updateRichPageInBubbleState(); + + auto maxWidth = 0; + auto minHeight = 0; + + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + if (_reactions) { + _reactions->initDimensions(); + } + + const auto reply = Get(); + if (reply) { + reply->update(this, replyData); + } + const auto summaryHeader = Get(); + if (summaryHeader) { + if (showSummaryReply) { + summaryHeader->update(this); + } + } + + if (bubble) { + const auto forwarded = item->Get(); + const auto via = item->Get(); + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + if (forwarded) { + forwarded->create(via, item); + } + + auto mediaDisplayed = false; + if (media) { + mediaDisplayed = media->isDisplayed(); + media->initDimensions(); + } + if (check) { + check->initDimensions(); + } + if (entry) { + entry->initDimensions(); + } + + // Entry page is always a bubble bottom. + withVisibleText = hasVisibleText(); + fullTextualWidth = textualMaxWidth(); + const auto textualWidth = bubbleTextualWidth(); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); + maxWidth = textualWidth; + auto nonTextMax = 0; + if (isCommentsRootView()) { + maxWidth = std::max(maxWidth, st::msgMaxWidth); + accumulate_max(nonTextMax, st::msgMaxWidth); + } + minHeight = withVisibleText + ? hasRichPage() + ? textHeightFor(bubbleTextWidth(textualWidth)) + : text().minHeight() + : 0; + if (reactionsInBubble) { + const auto reactionsMaxWidth = st::msgPadding.left() + + _reactions->maxWidth() + + st::msgPadding.right(); + const auto reactionsLimited = std::min( + st::msgMaxWidth, + reactionsMaxWidth); + accumulate_max(maxWidth, reactionsLimited); + accumulate_max(nonTextMax, reactionsLimited); + if (mediaDisplayed + && !media->additionalInfoString().isEmpty()) { + // In round videos in a web page status text is painted + // in the bottom left corner, reactions should be below. + minHeight += st::msgDateFont->height; + } else { + minHeight += st::mediaInBubbleSkip; + } + if (maxWidth >= reactionsMaxWidth) { + minHeight += _reactions->minHeight(); + } else { + const auto widthForReactions = maxWidth + - st::msgPadding.left() + - st::msgPadding.right(); + minHeight += _reactions->resizeGetHeight(widthForReactions); + } + } + if (!mediaOnBottom && (!_viewButton || !reactionsInBubble)) { + minHeight += st::msgPadding.bottom(); + if (mediaDisplayed) { + minHeight += st::mediaInBubbleSkip; + } + } + if (!mediaOnTop) { + minHeight += st::msgPadding.top(); + if (mediaDisplayed) minHeight += st::mediaInBubbleSkip; + if (entry) minHeight += st::mediaInBubbleSkip; + } + if (check) minHeight += st::mediaInBubbleSkip; + if (mediaDisplayed) { + // Parts don't participate in maxWidth() in case of media message. + if (media->enforceBubbleWidth()) { + maxWidth = media->maxWidth(); + const auto innerWidth = maxWidth + - st::msgPadding.left() + - st::msgPadding.right(); + if (reactionsInBubble) { + minHeight -= _reactions->minHeight(); + minHeight + += _reactions->countCurrentSize(innerWidth).height(); + } + } else { + accumulate_max(maxWidth, media->maxWidth()); + } + minHeight += media->minHeight(); + } else { + // Count parts in maxWidth(), don't count them in minHeight(). + // They will be added in resizeGetHeight() anyway. + if (displayFromName()) { + const auto from = displayFrom(); + validateFromNameText(from); + const auto &name = from + ? _fromName + : item->displayHiddenSenderInfo()->nameText(); + auto namew = st::msgPadding.left() + + name.maxWidth() + + (_fromNameStatus + ? st::dialogsPremiumIcon.icon.width() + : 0) + + st::msgPadding.right(); + if (via && !displayForwardedFrom()) { + namew += st::msgServiceFont->spacew + via->maxWidth + + (_fromNameStatus ? st::msgServiceFont->spacew : 0); + } + if (const auto guestChat = item->Get()) { + namew += st::msgServiceFont->spacew + guestChat->maxWidth; + } + if (Has()) { + namew += st::msgPadding.right() + rightBadgeWidth(); + } + accumulate_max(maxWidth, namew); + accumulate_max(nonTextMax, namew); + } else if (via && !displayForwardedFrom()) { + const auto viaw = st::msgPadding.left() + via->maxWidth + st::msgPadding.right(); + accumulate_max(maxWidth, viaw); + accumulate_max(nonTextMax, viaw); + } + if (displayedTopicButton()) { + const auto padding = st::msgPadding + st::topicButtonPadding; + const auto topicw = padding.left() + + _topicButton->name.maxWidth() + + st::topicButtonArrowSkip + + padding.right(); + accumulate_max(maxWidth, topicw); + accumulate_max(nonTextMax, topicw); + } + if (displayForwardedFrom()) { + const auto skip1 = forwarded->psaType.isEmpty() + ? 0 + : st::historyPsaIconSkip1; + auto namew = st::msgPadding.left() + forwarded->text.maxWidth() + skip1 + st::msgPadding.right(); + if (via) { + namew += st::msgServiceFont->spacew + via->maxWidth; + } + accumulate_max(maxWidth, namew); + accumulate_max(nonTextMax, namew); + } + if (reply) { + const auto replyw = st::msgPadding.left() + + reply->maxWidth() + + st::msgPadding.right(); + accumulate_max(maxWidth, replyw); + accumulate_max(nonTextMax, replyw); + } + if (summaryHeader) { + const auto summaryHeaderWidth = st::msgPadding.left() + + summaryHeader->maxWidth() + + st::msgPadding.right(); + accumulate_max(maxWidth, summaryHeaderWidth); + accumulate_max(nonTextMax, summaryHeaderWidth); + } + if (check) { + accumulate_max(maxWidth, check->maxWidth()); + accumulate_max(nonTextMax, check->maxWidth()); + minHeight += check->minHeight(); + } + if (entry) { + accumulate_max(maxWidth, entry->maxWidth()); + accumulate_max(nonTextMax, entry->maxWidth()); + minHeight += entry->minHeight(); + } + } + if (withVisibleText && botTop) { + accumulate_max(maxWidth, botTop->maxWidth); + accumulate_max(nonTextMax, botTop->maxWidth); + minHeight += botTop->height; + } + if (ephemeralBadge) { + accumulate_max(maxWidth, ephemeralBadge->maxWidth); + accumulate_max(nonTextMax, ephemeralBadge->maxWidth); + } + accumulate_max(maxWidth, minWidthForMedia()); + accumulate_max(nonTextMax, minWidthForMedia()); + _nonTextMaxWidth = std::min(nonTextMax, kMaxWidth); + } else if (media) { + media->initDimensions(); + maxWidth = media->maxWidth(); + minHeight = media->isDisplayed() ? media->minHeight() : 0; + } else { + maxWidth = st::msgMinWidth; + minHeight = 0; + } + // if we have a text bubble we can resize it to fit the keyboard + // but if we have only media we don't do that + const auto keyboard = inlineReplyKeyboard(); + if (keyboard && hasVisibleText()) { + accumulate_max(maxWidth, keyboard->naturalWidth()); + if (bubble) { + const auto kbw = keyboard->naturalWidth(); + if (kbw > int(_nonTextMaxWidth)) { + _nonTextMaxWidth = std::min(kbw, kMaxWidth); + } + } + } + if (bubble + && withVisibleText + && (maxWidth < fullTextualWidth || hasRichPage())) { + minHeight -= hasRichPage() + ? textHeightFor(bubbleTextWidth(bubbleTextualWidth())) + : text().minHeight(); + minHeight += textHeightFor(bubbleTextWidth(maxWidth)); + } + if (const auto appearing = Get()) { + appearing->geometryValid = false; + appearing->startedForText = false; + appearing->finalizing = item->isRegular(); + } + return QSize(maxWidth, minHeight); +} + +void Message::refreshTopicButton() { + const auto item = data(); + if (isAttachedToPrevious() || delegate()->elementHideTopicButton(this)) { + _topicButton = nullptr; + } else if (const auto topic = item->topic()) { + if (topic->peer()->useSubsectionTabs()) { + _topicButton = nullptr; + return; + } + if (!_topicButton) { + _topicButton = std::make_unique(); + } + const auto jumpToId = IsServerMsgId(item->id) ? item->id : MsgId(); + _topicButton->link = MakeTopicButtonLink(topic, jumpToId); + if (_topicButton->nameVersion != topic->titleVersion()) { + _topicButton->nameVersion = topic->titleVersion(); + const auto context = Core::TextContext({ + .session = &history()->session(), + .repaint = [=] { customEmojiRepaint(); }, + .customEmojiLoopLimit = 1, + }); + _topicButton->name.setMarkedText( + st::fwdTextStyle, + topic->titleWithIcon(), + kMarkupTextOptions, + context); + } + } else { + _topicButton = nullptr; + } +} + +int Message::marginTop() const { + auto result = 0; + if (!isHidden()) { + if (isAttachedToPrevious()) { + result += st::msgMarginTopAttached; + } else { + result += st::msgMargin.top(); + } + } + result += displayedDateHeight(); + if (const auto bar = Get()) { + result += bar->height(); + } + if (const auto bar = Get()) { + result += bar->height(); + } + if (const auto service = Get()) { + if (!service->below) { + result += service->height; + } + } + if (const auto margins = Get()) { + result += margins->top; + } + return result; +} + +int Message::marginBottom() const { + if (isHidden()) { + return 0; + } + auto result = st::msgMargin.bottom(); + if (const auto service = Get()) { + if (service->below) { + result += service->height; + } + } + if (const auto margins = Get()) { + result += margins->bottom; + } + return result; +} + +void Message::draw(Painter &p, const PaintContext &context) const { + auto g = countGeometry(); + if (g.width() < 1) { + return; + } + + const auto item = data(); + const auto media = this->media(); + + const auto gestureShift = context.gestureHorizontal.visualTranslationFor( + item->id.bare); + if (gestureShift) { + p.translate(gestureShift, 0); + } + const auto selectionModeResult = delegate()->elementInSelectionMode(this); + const auto selectionTranslation = (selectionModeResult.progress > 0) + ? (selectionModeResult.progress + * AdditionalSpaceForSelectionCheckbox(this, g)) + : 0; + if (selectionTranslation) { + p.translate(selectionTranslation, 0); + } + + if (item->hasUnrequestedFactcheck() + && (Message::context() != Context::MediaEditor)) { + item->history()->session().factchecks().requestFor(item); + } + + const auto stm = context.messageStyle(); + const auto bubble = drawBubble(); + + if (const auto bar = Get()) { + auto unreadbarh = bar->height(); + auto aboveh = 0; + if (const auto date = Get()) { + aboveh += date->height(); + } + if (const auto bar = Get()) { + aboveh += bar->height(); + } + if (context.clip.intersects(QRect(0, aboveh, width(), unreadbarh))) { + p.translate(0, aboveh); + bar->paint(p, context, 0, width(), delegate()->elementChatMode()); + p.translate(0, -aboveh); + } + } + + if (const auto service = Get()) { + service->paint(p, context, g, delegate()->elementChatMode()); + } + + if (isHidden()) { + return; + } + + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + auto mediaDisplayed = media && media->isDisplayed(); + + // Entry page is always a bubble bottom. + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); + + const auto displayInfo = needInfoDisplay(); + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + + // We need to count geometry without keyboard and reactions + // for bubble selection intervals counting below. + auto gForIntervals = g; + if (_reactions && !reactionsInBubble) { + const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height(); + gForIntervals.setHeight(gForIntervals.height() - reactionsHeight); + } + const auto keyboard = inlineReplyKeyboard(); + if (keyboard) { + const auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight(); + gForIntervals.setHeight(gForIntervals.height() - keyboardHeight); + } + + auto mediaSelectionIntervals = (!context.selected() && mediaDisplayed) + ? media->getBubbleSelectionIntervals(context.selection) + : std::vector(); + auto localMediaTop = 0; + const auto customHighlight = mediaDisplayed && media->customHighlight(); + if (!mediaSelectionIntervals.empty() || customHighlight) { + auto localMediaBottom = gForIntervals.top() + gForIntervals.height(); + if (hasCommentsButton()) { + localMediaBottom -= st::historyCommentsButtonHeight; + } + if (_viewButton) { + localMediaBottom -= st::mediaInBubbleSkip + _viewButton->height(); + } + if (reactionsInBubble) { + localMediaBottom -= st::mediaInBubbleSkip + _reactions->height(); + } + if (!mediaOnBottom && (!_viewButton || !reactionsInBubble)) { + localMediaBottom -= st::msgPadding.bottom(); + if (mediaDisplayed) { + localMediaBottom -= st::mediaInBubbleSkip; + } + } + if (check) { + localMediaBottom -= check->height(); + } + if (entry) { + localMediaBottom -= entry->height(); + } + localMediaTop = localMediaBottom - media->height(); + for (auto &[top, height] : mediaSelectionIntervals) { + top += localMediaTop; + } + } + + { + if (selectionTranslation) { + p.translate(-selectionTranslation, 0); + } + if (customHighlight) { + media->drawHighlight(p, context, localMediaTop); + } else { + paintHighlight(p, context, g.height()); + } + if (selectionTranslation) { + p.translate(selectionTranslation, 0); + } + } + + const auto roll = media ? media->bubbleRoll() : Media::BubbleRoll(); + if (roll) { + p.save(); + p.translate(g.center()); + p.rotate(roll.rotate); + p.scale(roll.scale, roll.scale); + p.translate(-g.center()); + } + + p.setTextPalette(stm->textPalette); + + if (_reactions && !reactionsInBubble) { + const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height(); + const auto reactionsLeft = (!bubble && mediaDisplayed) + ? media->contentRectForReactions().x() + : 0; + g.setHeight(g.height() - reactionsHeight); + const auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip); + p.translate(reactionsPosition); + prepareCustomEmojiPaint(p, context, *_reactions); + _reactions->paint(p, context, g.width(), context.clip.translated(-reactionsPosition)); + if (context.reactionInfo) { + context.reactionInfo->position = reactionsPosition; + } + p.translate(-reactionsPosition); + } + + const auto messageRounding = countMessageRounding(); + if (keyboard) { + const auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight(); + g.setHeight(g.height() - keyboardHeight); + + const auto keyboardPosition = QPoint(g.left(), g.top() + g.height() + st::msgBotKbButton.margin); + p.translate(keyboardPosition); + keyboard->paint( + p, + context.st, + messageRounding, + g.width(), + context.clip.translated(-keyboardPosition), + context.paused); + p.translate(-keyboardPosition); + } + + if (context.highlightPathCache) { + context.highlightInterpolateTo = g; + context.highlightPathCache->clear(); + } + if (bubble) { + const auto from = displayFromName() ? displayFrom() : nullptr; + if (from && (_fromNameVersion < from->nameVersion())) { + fromNameUpdated(g.width()); + } + const auto simple = Ui::SimpleBubble{ + .st = context.st, + .geometry = g, + .pattern = context.bubblesPattern, + .patternViewport = context.viewport, + .outerWidth = width(), + .selected = context.selected(), + .outbg = context.outbg, + .rounding = countBubbleRounding(messageRounding), + }; + const auto rich = const_cast(this)->richpage(); + auto richPageGaps = std::vector(); + if (rich && rich->hasUnsupportedBlocks) { + auto richTrect = QRect(); + if (prepareRichPageTextRect(richTrect)) { + const auto origin = richPageRect(richTrect).topLeft(); + const auto rects = rich->article.unsupportedNoticeRects(); + richPageGaps.reserve(rects.size()); + for (const auto ¬ice : rects) { + const auto mapped = notice.translated(origin); + richPageGaps.push_back({ + mapped.y(), + mapped.height(), + }); + } + } + } + if (!richPageGaps.empty()) { + if (!rich->tornEdges) { + rich->tornEdges = std::make_unique(); + } + Ui::ValidateTornEdges(*rich->tornEdges, g.width()); + Ui::PaintBubble( + p, + Ui::BubbleWithGaps{ + .simple = simple, + .gaps = richPageGaps, + .torn = rich->tornEdges.get(), + }); + } else { + Ui::PaintBubble( + p, + Ui::ComplexBubble{ + .simple = simple, + .selection = mediaSelectionIntervals, + }); + } + + auto inner = g; + paintCommentsButton(p, inner, context); + + auto trect = inner.marginsRemoved(st::msgPadding); + + const auto additionalInfoSkip = (mediaDisplayed + && !media->additionalInfoString().isEmpty()) + ? st::msgDateFont->height + : 0; + const auto reactionsTop = (reactionsInBubble && !_viewButton) + ? (additionalInfoSkip + st::mediaInBubbleSkip) + : additionalInfoSkip; + const auto reactionsHeight = reactionsInBubble + ? (reactionsTop + _reactions->height()) + : 0; + if (reactionsInBubble) { + trect.setHeight(trect.height() - reactionsHeight); + const auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + reactionsTop); + p.translate(reactionsPosition); + prepareCustomEmojiPaint(p, context, *_reactions); + _reactions->paint(p, context, g.width(), context.clip.translated(-reactionsPosition)); + if (context.reactionInfo) { + context.reactionInfo->position = reactionsPosition; + } + p.translate(-reactionsPosition); + } + + if (_viewButton) { + const auto belowInfo = _viewButton->belowMessageInfo(); + const auto infoHeight = reactionsInBubble + ? (reactionsHeight + 2 * st::mediaInBubbleSkip) + : bottomInfoHeight(); + const auto heightMargins = QMargins(0, 0, 0, infoHeight); + _viewButton->draw( + p, + _viewButton->countRect(belowInfo + ? inner + : inner - heightMargins), + context); + if (belowInfo) { + inner.setHeight(inner.height() - _viewButton->height()); + } + trect.setHeight(trect.height() - _viewButton->height()); + if (reactionsInBubble) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip + st::msgPadding.bottom()); + } else if (mediaDisplayed) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip); + } + } + + if (mediaOnBottom) { + trect.setHeight(trect.height() + st::msgPadding.bottom()); + } + if (mediaOnTop) { + trect.setY(trect.y() - st::msgPadding.top()); + } else { + paintFromName(p, trect, context); + paintEphemeralBadge(p, trect, context); + paintTopicButton(p, trect, context); + paintForwardedInfo(p, trect, context); + paintViaBotIdInfo(p, trect, context); + paintReplyInfo(p, trect, context); + paintSummaryHeaderInfo(p, trect, context); + } + if (entry) { + trect.setHeight(trect.height() - entry->height()); + } + if (check) { + trect.setHeight(trect.height() - check->height() - st::mediaInBubbleSkip); + } + if (displayInfo) { + trect.setHeight(trect.height() + - (_bottomInfo.height() - st::msgDateFont->height)); + } + auto textSelection = context.selection; + auto highlightRange = context.highlight.range; + const auto mediaHeight = mediaDisplayed ? media->height() : 0; + const auto paintMedia = [&](int top) { + if (!mediaDisplayed) { + return; + } + const auto mediaSelection = _invertMedia + ? context.selection + : skipTextSelection(context.selection); + const auto maybeMediaHighlight = context.highlightPathCache + && context.highlightPathCache->isEmpty(); + auto mediaPosition = QPoint(inner.left(), top); + _lastMediaPosition = mediaPosition; + p.translate(mediaPosition); + media->draw(p, context.translated( + -mediaPosition + ).withSelection(mediaSelection)); + if (context.reactionInfo && !displayInfo && !_reactions) { + const auto add = QPoint(0, mediaHeight); + context.reactionInfo->position = mediaPosition + add; + if (context.reactionInfo->effectPaint) { + context.reactionInfo->effectOffset -= add; + } + } + if (maybeMediaHighlight + && !context.highlightPathCache->isEmpty()) { + context.highlightPathCache->translate(mediaPosition); + } + p.translate(-mediaPosition); + }; + if (mediaDisplayed && _invertMedia) { + if (!mediaOnTop) { + trect.setY(trect.y() + st::mediaInBubbleSkip); + } + paintMedia(trect.y()); + trect.setY(trect.y() + + mediaHeight + + (mediaOnBottom ? 0 : st::mediaInBubbleSkip)); + textSelection = media->skipSelection(textSelection); + highlightRange = media->skipSelection(highlightRange); + } + const auto drawText = context.skipDrawingParts + != PaintContext::SkipDrawingParts::Content; + const auto drawOnlyText = drawText + && (context.skipDrawingParts + != PaintContext::SkipDrawingParts::None); + if (drawOnlyText) { + p.save(); + p.setClipping(false); + } + if (drawText) { + auto copy = context; + copy.selection = textSelection; + copy.highlight.range = highlightRange; + paintText(p, trect, copy); + } + if (drawOnlyText) { + p.restore(); + } + if (mediaDisplayed && !_invertMedia) { + paintMedia(trect.y() + trect.height() - mediaHeight); + if (context.reactionInfo && !displayInfo && !_reactions) { + context.reactionInfo->position + = QPoint(inner.left(), trect.y() + trect.height()); + if (context.reactionInfo->effectPaint) { + context.reactionInfo->effectOffset -= QPoint(0, mediaHeight); + } + } + } + if (check) { + auto checkLeft = inner.left(); + auto checkTop = trect.y() + trect.height() + st::mediaInBubbleSkip; + p.translate(checkLeft, checkTop); + auto checkContext = context.translated(checkLeft, -checkTop); + checkContext.selection = skipTextSelection(context.selection); + if (mediaDisplayed) { + checkContext.selection = media->skipSelection( + checkContext.selection); + } + check->draw(p, checkContext); + p.translate(-checkLeft, -checkTop); + } + if (entry) { + auto entryLeft = inner.left(); + auto entryTop = trect.y() + trect.height(); + p.translate(entryLeft, entryTop); + auto entryContext = context.translated(-entryLeft, -entryTop); + entryContext.selection = skipTextSelection(context.selection); + if (mediaDisplayed) { + entryContext.selection = media->skipSelection( + entryContext.selection); + } + entry->draw(p, entryContext); + p.translate(-entryLeft, -entryTop); + } + if (displayInfo) { + const auto bottomSelected = context.selected() + || (!mediaSelectionIntervals.empty() + && (mediaSelectionIntervals.back().top + + mediaSelectionIntervals.back().height + >= inner.y() + inner.height())); + drawInfo( + p, + context.withSelection( + bottomSelected ? FullSelection : TextSelection()), + inner.left() + inner.width(), + inner.top() + inner.height(), + 2 * inner.left() + inner.width(), + InfoDisplayType::Default); + if (context.reactionInfo && !_reactions) { + const auto add = QPoint(0, inner.top() + inner.height()); + context.reactionInfo->position = add; + if (context.reactionInfo->effectPaint) { + context.reactionInfo->effectOffset -= add; + } + } + if (_comments) { + const auto o = p.opacity(); + p.setOpacity(0.3); + p.fillRect(g.left(), g.top() + g.height() - st::historyCommentsButtonHeight - st::lineWidth, g.width(), st::lineWidth, stm->msgDateFg); + p.setOpacity(o); + } + } + ensureSummarizeButton(); + if (const auto size = rightActionSize(); size || _summarize) { + const auto rightActionWidth = size + ? size->width() + : _summarize->size().width(); + const auto fastShareSkip = size + ? std::clamp( + (g.height() - size->height()) / 2, + 0, + st::historyFastShareBottom) + : st::historyFastShareBottom; + const auto fastShareLeft = hasRightLayout() + ? (g.left() + - (_summarize ? 0 : rightActionWidth) + - st::historyFastShareLeft) + : (g.left() + g.width() + st::historyFastShareLeft); + const auto fastShareTop = g.top() + (data()->isSponsored() + ? fastShareSkip + : g.height() - fastShareSkip - (size ? size->height() : 0)); + if (size) { + const auto o = p.opacity(); + if (selectionModeResult.progress > 0) { + p.setOpacity(1. - selectionModeResult.progress); + } + drawRightAction( + p, + context, + fastShareLeft, + fastShareTop, + width()); + if (selectionModeResult.progress > 0) { + p.setOpacity(o); + } + } + if (_summarize) { + paintSummarize( + p, + fastShareLeft, + fastShareTop, + !context.outbg, + context, + g); + } + } + + if (media) { + media->paintBubbleFireworks(p, g, context.now); + } + } else if (media && media->isDisplayed()) { + _lastMediaPosition = g.topLeft(); + p.translate(g.topLeft()); + media->draw(p, context.translated( + -g.topLeft() + ).withSelection(skipTextSelection(context.selection))); + if (context.reactionInfo && !_reactions) { + const auto add = QPoint(0, g.height()); + context.reactionInfo->position = g.topLeft() + add; + if (context.reactionInfo->effectPaint) { + context.reactionInfo->effectOffset -= add; + } + } + p.translate(-g.topLeft()); + } + + p.restoreTextPalette(); + + if (context.highlightPathCache + && !context.highlightPathCache->isEmpty()) { + const auto alpha = int(0.25 + * context.highlight.collapsion + * context.highlight.opacity + * 255); + if (alpha > 0) { + context.highlightPathCache->setFillRule(Qt::WindingFill); + auto color = context.messageStyle()->textPalette.linkFg->c; + color.setAlpha(alpha); + p.fillPath(*context.highlightPathCache, color); + } + } + + if (roll) { + p.restore(); + } + + if (const auto reply = Get()) { + if (const auto replyData = item->Get()) { + if (reply->isNameUpdated(this, replyData)) { + const_cast(this)->setPendingResize(); + } + } + } + if (gestureShift) { + p.translate(-gestureShift, 0); + if (context.reactionInfo && context.reactionInfo->effectPaint) { + context.reactionInfo->effectOffset += QPoint(gestureShift, 0); + } + paintSwipeReplyIcon(p, context, g); + } + if (selectionTranslation) { + p.translate(-selectionTranslation, 0); + } + if (selectionModeResult.progress) { + if (!context.skipSelectionCheck) { + const auto progress = selectionModeResult.progress; + if (progress <= 1.) { + if (context.selected()) { + if (!_selectionRoundCheckbox) { + _selectionRoundCheckbox + = std::make_unique( + st::msgSelectionCheck, + [this] { repaint(); }); + } + } + if (_selectionRoundCheckbox) { + _selectionRoundCheckbox->setChecked( + context.selected(), + anim::type::normal); + } + const auto o = ScopedPainterOpacity(p, progress); + const auto &st = st::msgSelectionCheck; + const auto right = (delegate()->elementChatMode() + == ElementChatMode::Wide) + ? std::min( + int(_bubbleWidthLimit + + st::msgPhotoSkip + + st::msgSelectionOffset + + st::msgPadding.left() + + st.size), + width()) + : width(); + const auto pos = QPoint( + (right + - (st::msgSelectionOffset * progress - st.size) / 2 + - st::msgPadding.right() / 2 + - st.size + - st::historyScroll.deltax), + rect::bottom(g) - st.size - st::msgSelectionBottomSkip); + { + p.setPen(QPen(st.border, st.width)); + p.setBrush(context.st->msgServiceBg()); + auto hq = PainterHighQualityEnabler(p); + p.drawEllipse(QRect(pos, Size(st.size))); + } + if (_selectionRoundCheckbox) { + _selectionRoundCheckbox->paint( + p, + pos.x(), + pos.y(), + width()); + } + } else { + _selectionRoundCheckbox = nullptr; + } + } + } else if (!context.skipSelectionCheck) { + _selectionRoundCheckbox = nullptr; + } +} + +void Message::paintCommentsButton( + Painter &p, + QRect &g, + const PaintContext &context) const { + if (!hasCommentsButton()) { + return; + } + if (!_comments) { + _comments = std::make_unique(); + history()->owner().registerHeavyViewPart(const_cast(this)); + } + const auto stm = context.messageStyle(); + const auto views = data()->Get(); + + g.setHeight(g.height() - st::historyCommentsButtonHeight); + const auto top = g.top() + g.height(); + auto left = g.left(); + auto width = g.width(); + + if (_comments->ripple) { + p.setOpacity(st::historyPollRippleOpacity); + const auto colorOverride = &stm->msgWaveformInactive->c; + _comments->ripple->paint( + p, + left - _comments->rippleShift, + top, + width, + colorOverride); + if (_comments->ripple->empty()) { + _comments->ripple.reset(); + } + p.setOpacity(1.); + } + + left += st::historyCommentsSkipLeft; + width -= st::historyCommentsSkipLeft + + st::historyCommentsSkipRight; + + const auto &open = stm->historyCommentsOpen; + open.paint(p, + left + width - open.width(), + top + (st::historyCommentsButtonHeight - open.height()) / 2, + width); + + if (!views || views->recentRepliers.empty()) { + const auto &icon = stm->historyComments; + icon.paint( + p, + left, + top + (st::historyCommentsButtonHeight - icon.height()) / 2, + width); + left += icon.width(); + } else { + auto &list = _comments->userpics; + const auto limit = HistoryMessageViews::kMaxRecentRepliers; + const auto count = std::min(int(views->recentRepliers.size()), limit); + const auto single = st::historyCommentsUserpics.size; + const auto shift = st::historyCommentsUserpics.shift; + const auto regenerate = [&] { + if (list.size() != count) { + return true; + } + for (auto i = 0; i != count; ++i) { + auto &entry = list[i]; + const auto peer = entry.peer; + auto &view = entry.view; + const auto wasView = view.cloud.get(); + if (views->recentRepliers[i] != peer->id + || peer->userpicUniqueKey(view) != entry.uniqueKey + || view.cloud.get() != wasView) { + return true; + } + } + return false; + }(); + if (regenerate) { + for (auto i = 0; i != count; ++i) { + const auto peerId = views->recentRepliers[i]; + if (i == list.size()) { + list.push_back(UserpicInRow{ + history()->owner().peer(peerId) + }); + } else if (list[i].peer->id != peerId) { + list[i].peer = history()->owner().peer(peerId); + } + } + while (list.size() > count) { + list.pop_back(); + } + GenerateUserpicsInRow( + _comments->cachedUserpics, + list, + st::historyCommentsUserpics, + limit); + } + p.drawImage( + left, + top + (st::historyCommentsButtonHeight - single) / 2, + _comments->cachedUserpics); + left += single + (count - 1) * (single - shift); + } + + left += st::historyCommentsSkipText; + p.setPen(stm->msgFileThumbLinkFg); + p.setFont(st::semiboldFont); + + const auto textTop = top + (st::historyCommentsButtonHeight - st::semiboldFont->height) / 2; + p.drawTextLeft( + left, + textTop, + width, + views ? views->replies.text : tr::lng_replies_view_original(tr::now), + views ? views->replies.textWidth : -1); + + if (views && data()->areCommentsUnread()) { + p.setPen(Qt::NoPen); + p.setBrush(stm->msgFileBg); + + { + PainterHighQualityEnabler hq(p); + p.drawEllipse(style::rtlrect(left + views->replies.textWidth + st::mediaUnreadSkip, textTop + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, width)); + } + } +} + +void Message::paintFromName( + Painter &p, + QRect &trect, + const PaintContext &context) const { + const auto item = data(); + if (!displayFromName()) { + return; + } + const auto badgeWidth = rightBadgeWidth(); + auto availableLeft = trect.left(); + auto availableWidth = trect.width(); + if (badgeWidth) { + availableWidth -= st::msgPadding.right() + badgeWidth; + } + + const auto stm = context.messageStyle(); + const auto from = displayFrom(); + const auto info = from ? nullptr : item->displayHiddenSenderInfo(); + Assert(from || info); + const auto nameFg = FromNameFg( + context, + colorIndex(), + colorCollectible()); + const auto nameText = [&] { + if (from) { + validateFromNameText(from); + return static_cast(&_fromName); + } + return &info->nameText(); + }(); + const auto statusWidth = _fromNameStatus + ? st::dialogsPremiumIcon.icon.width() + : 0; + const auto via = item->Get(); + const auto viaShown = via && !displayForwardedFrom() && via->width; + const auto viaSkipWidth = viaShown + ? (via->width + st::msgServiceFont->spacew) + : 0; + const auto nameAvailableWidth = std::max( + ((statusWidth && availableWidth > statusWidth) + ? (availableWidth - statusWidth) + : availableWidth) - viaSkipWidth, + 0); + if (statusWidth && availableWidth > statusWidth) { + const auto x = availableLeft + + std::min(nameAvailableWidth, nameText->maxWidth()); + const auto y = trect.top(); + auto color = nameFg; + color.setAlpha(115); + const auto id = from ? from->emojiStatusId() : EmojiStatusId(); + if (_fromNameStatus->id != id) { + const auto that = const_cast(this); + _fromNameStatus->custom = id + ? MakeWrappedEmoji( + history()->owner().customEmojiManager().create( + Data::EmojiStatusCustomId(id), + [=] { that->customEmojiRepaint(); }), + kPlayStatusLimit) + : nullptr; + if (id && !_fromNameStatus->id) { + history()->owner().registerHeavyViewPart(that); + } else if (!id && _fromNameStatus->id) { + that->checkHeavyPart(); + } + _fromNameStatus->id = id; + } + if (_fromNameStatus->custom) { + clearCustomEmojiRepaint(); + _fromNameStatus->custom->paint(p, { + .textColor = color, + .now = context.now, + .position = QPoint( + x - 2 * _fromNameStatus->skip, + y + _fromNameStatus->skip), + .paused = context.paused || On(PowerSaving::kEmojiStatus), + }); + } else { + st::dialogsPremiumIcon.icon.paint(p, x, y, width(), color); + } + } + p.setFont(st::msgNameFont); + p.setPen(nameFg); + const auto nameLinkHandler = fromLink(); + const auto nameWidth = std::min( + nameText->maxWidth(), + nameAvailableWidth); + if (!from) { + if (const auto tooltip = Get()) { + tooltip->linkRect = QRect( + availableLeft, + trect.top(), + nameWidth, + st::msgNameFont->height); + } + } + paintLinkRipple( + p, + nameLinkHandler, + QRect(availableLeft, trect.top(), nameWidth, st::msgNameFont->height), + trect.topLeft()); + nameText->draw(p, { + .position = { availableLeft, trect.top() }, + .availableWidth = nameAvailableWidth, + .elisionLines = 1, + }); + const auto skipWidth = nameWidth + + (_fromNameStatus + ? (st::dialogsPremiumIcon.icon.width() + + st::msgServiceFont->spacew) + : 0) + + st::msgServiceFont->spacew; + availableLeft += skipWidth; + availableWidth -= skipWidth; + + if (via && !displayForwardedFrom() && availableWidth > 0) { + p.setPen(stm->msgServiceFg); + paintLinkRipple( + p, + via->link, + QRect(availableLeft, trect.top(), via->width, st::msgServiceFont->height), + trect.topLeft()); + p.drawText(availableLeft, trect.top() + st::msgServiceFont->ascent, via->text); + auto skipWidth = via->width + st::msgServiceFont->spacew; + availableLeft += skipWidth; + availableWidth -= skipWidth; + } + if (const auto guestChat = item->Get()) { + if (availableWidth > 0) { + p.setPen(stm->msgServiceFg); + paintLinkRipple( + p, + guestChat->link, + QRect(availableLeft, trect.top(), guestChat->width, st::msgServiceFont->height), + trect.topLeft()); + p.drawText(availableLeft, trect.top() + st::msgServiceFont->ascent, guestChat->text); + auto skipWidth = guestChat->width + st::msgServiceFont->spacew; + availableLeft += skipWidth; + availableWidth -= skipWidth; + } + } + if (badgeWidth) { + p.setPen(stm->msgDateFg); + if (const auto badge = Get()) { + const auto badgeColor = (badge->role == BadgeRole::Creator) + ? st::rankOwnerFg->c + : (badge->role == BadgeRole::Admin) + ? st::rankAdminFg->c + : st::rankUserFg->c; + const auto badgeLeft = trect.left() + + trect.width() + - badge->width; + if (badge->role != BadgeRole::User) { + auto bgColor = badgeColor; + bgColor.setAlphaF(0.15); + const auto pill = ComputeBadgePillGeometry(badge); + const auto &padding = st::msgTagBadgePadding; + const auto badgeTop = trect.top() + + (st::msgNameFont->height - pill.height) / 2; + const auto pillRect = QRect( + badgeLeft, + badgeTop, + pill.width, + pill.height); + p.setPen(Qt::NoPen); + p.setBrush(bgColor); + { + auto hq = PainterHighQualityEnabler(p); + p.drawRoundedRect( + pillRect, + pill.height / 2., + pill.height / 2.); + } + if (badge->ripple) { + auto rippleColor = badgeColor; + rippleColor.setAlphaF(0.1); + badge->ripple->paint( + p, + badgeLeft, + badgeTop, + width(), + &rippleColor); + if (badge->ripple->empty()) { + badge->ripple.reset(); + } + } + p.setPen(badgeColor); + badge->tag.draw(p, { + .position = QPoint( + badgeLeft + (pill.width - pill.textWidth) / 2, + badgeTop + padding.top()), + .availableWidth = pill.textWidth, + .now = context.now, + }); + } else if (!badge->tag.isEmpty()) { + if (badge->ripple) { + const auto pill = ComputeBadgePillGeometry(badge); + const auto &padding = st::msgTagBadgePadding; + const auto pillLeft = badgeLeft + - (pill.width - pill.textWidth) / 2; + const auto pillTop = trect.top() - padding.top(); + auto rippleColor = badgeColor; + rippleColor.setAlphaF(0.1); + badge->ripple->paint( + p, + pillLeft, + pillTop, + width(), + &rippleColor); + if (badge->ripple->empty()) { + badge->ripple.reset(); + } + } + p.setPen(st::rankUserFg); + badge->tag.draw(p, { + .position = QPoint(badgeLeft, trect.top()), + .availableWidth = badge->tag.maxWidth(), + .now = context.now, + }); + } + if (!badge->boosts.isEmpty()) { + const auto boostWidth = badge->boosts.maxWidth(); + p.setPen(badgeColor); + badge->boosts.draw(p, { + .position = QPoint( + trect.left() + trect.width() - boostWidth, + trect.top()), + .availableWidth = boostWidth, + .now = context.now, + }); + } + } + } + trect.setY(trect.y() + st::msgNameFont->height); +} + +void Message::paintEphemeralBadge( + Painter &p, + QRect &trect, + const PaintContext &context) const { + const auto badge = Get(); + if (!badge || badge->text.isEmpty()) { + return; + } + const auto stm = context.messageStyle(); + const auto &icon = stm->historyEphemeralIcon; + const auto iconTop = trect.y() + + (st::msgNameStyle.font->height - icon.height()) / 2; + icon.paint(p, trect.x(), iconTop, width()); + const auto skip = icon.width() + st::historyEphemeralIconSkip; + p.setPen(stm->msgServiceFg); + badge->text.drawLeftElided( + p, + trect.x() + skip, + trect.y(), + trect.width() - skip, + width()); + trect.setY(trect.y() + badge->height); +} + +void Message::paintTopicButton( + Painter &p, + QRect &trect, + const PaintContext &context) const { + const auto button = displayedTopicButton(); + if (!button) { + return; + } + trect.setTop(trect.top() + st::topicButtonSkip); + const auto padding = st::topicButtonPadding; + const auto availableWidth = trect.width(); + const auto height = padding.top() + + st::msgNameFont->height + + padding.bottom(); + const auto width = std::max( + std::min( + availableWidth, + (padding.left() + + button->name.maxWidth() + + st::topicButtonArrowSkip + + padding.right())), + height); + const auto rect = QRect(trect.x(), trect.y(), width, height); + + const auto stm = context.messageStyle(); + const auto skip = padding.right() + st::topicButtonArrowSkip; + auto color = stm->msgServiceFg->c; + color.setAlpha(color.alpha() / 8); + p.setPen(Qt::NoPen); + p.setBrush(color); + { + auto hq = PainterHighQualityEnabler(p); + p.drawRoundedRect(rect, height / 2, height / 2); + } + if (button->ripple) { + button->ripple->paint( + p, + rect.x(), + rect.y(), + this->width(), + &color); + if (button->ripple->empty()) { + button->ripple.reset(); + } + } + clearCustomEmojiRepaint(); + p.setPen(stm->msgServiceFg); + p.setTextPalette(stm->fwdTextPalette); + button->name.drawElided( + p, + trect.x() + padding.left(), + trect.y() + padding.top(), + width - padding.left() - skip); + + const auto &icon = st::topicButtonArrow; + icon.paint( + p, + rect.x() + rect.width() - skip + st::topicButtonArrowPosition.x(), + rect.y() + padding.top() + st::topicButtonArrowPosition.y(), + this->width(), + stm->msgServiceFg->c); + + trect.setY(trect.y() + height + st::topicButtonSkip); +} + +void Message::paintForwardedInfo( + Painter &p, + QRect &trect, + const PaintContext &context) const { + if (displayForwardedFrom()) { + const auto item = data(); + const auto st = context.st; + const auto stm = context.messageStyle(); + const auto forwarded = item->Get(); + + const auto &serviceFont = st::msgServiceFont; + const auto skip1 = forwarded->psaType.isEmpty() + ? 0 + : st::historyPsaIconSkip1; + const auto skip2 = forwarded->psaType.isEmpty() + ? 0 + : st::historyPsaIconSkip2; + const auto fits = (forwarded->text.maxWidth() + skip1 <= trect.width()); + const auto skip = fits ? skip1 : skip2; + const auto useWidth = trect.width() - skip; + const auto countedHeight = forwarded->text.countHeight(useWidth); + const auto breakEverywhere = (countedHeight > 2 * serviceFont->height); + p.setPen(!forwarded->psaType.isEmpty() + ? st->boxTextFgGood() + : stm->msgServiceFg); + p.setFont(serviceFont); + const auto &fwdPalette = !forwarded->psaType.isEmpty() + ? st->historyPsaForwardPalette() + : stm->fwdTextPalette; + const auto rippleLinkRange = (_linkRipple && _linkRipple->link) + ? forwarded->text.linkRangeFor(_linkRipple->link) + : TextSelection(); + const auto rippleBelongsHere = !rippleLinkRange.empty(); + if (_linkRipple + && _linkRipple->ripple + && _linkRipple->cachedWidth != useWidth + && rippleBelongsHere) { + _linkRipple = nullptr; + } + if (_linkRipple && _linkRipple->ripple && rippleBelongsHere) { + auto color = p.pen().color(); + color.setAlphaF(0.1); + _linkRipple->ripple->paint( + p, + trect.x() + _linkRipple->maskOffset.x(), + trect.y() + _linkRipple->maskOffset.y(), + width(), + &color); + if (_linkRipple->ripple->empty()) { + _linkRipple = nullptr; + } + } + const auto needRippleMask = _linkRipple + && _linkRipple->link + && !_linkRipple->ripple + && rippleBelongsHere; + const auto hiddenTooltip = Get(); + const auto recomputeHidden = hiddenTooltip + && (hiddenTooltip->cachedWidth != useWidth); + const auto hiddenSenderRange = recomputeHidden + ? forwarded->text.linkRangeFor( + HiddenSenderInfo::ForwardClickHandler()) + : TextSelection(); + auto highlightPath = QPainterPath(); + auto highlightRequest = Ui::Text::HighlightInfoRequest{ + .range = needRippleMask ? rippleLinkRange : hiddenSenderRange, + .outPath = &highlightPath, + }; + const auto needHighlight = needRippleMask + || !hiddenSenderRange.empty(); + forwarded->text.draw(p, { + .position = { trect.x(), trect.y() }, + .availableWidth = useWidth, + .palette = &fwdPalette, + .paused = p.inactive(), + .highlight = needHighlight ? &highlightRequest : nullptr, + .elisionLines = 2, + .elisionBreakEverywhere = breakEverywhere, + }); + if (recomputeHidden) { + hiddenTooltip->cachedWidth = useWidth; + if (!hiddenSenderRange.empty() && !highlightPath.isEmpty()) { + hiddenTooltip->linkRect + = highlightPath.boundingRect().toRect(); + } + } + if (needRippleMask && !highlightPath.isEmpty()) { + createLinkRippleMask( + highlightPath, + trect.topLeft(), + useWidth, + st::nameRipplePadding, + st::nameRippleRadius); + } else if (needRippleMask) { + _linkRipple = nullptr; + } + p.setTextPalette(stm->textPalette); + + if (!forwarded->psaType.isEmpty() + && (Message::context() != Context::MediaEditor)) { + const auto entry = Get(); + Assert(entry != nullptr); + const auto shown = entry->buttonVisibleAnimation.value( + entry->buttonVisible ? 1. : 0.); + if (shown > 0) { + const auto &icon = stm->historyPsaIcon; + const auto position = fits + ? st::historyPsaIconPosition1 + : st::historyPsaIconPosition2; + const auto x = trect.x() + trect.width() - position.x() - icon.width(); + const auto y = trect.y() + position.y(); + if (shown == 1) { + icon.paint(p, x, y, trect.width()); + } else { + p.save(); + p.translate(x + icon.width() / 2, y + icon.height() / 2); + p.scale(shown, shown); + p.setOpacity(shown); + icon.paint(p, -icon.width() / 2, -icon.height() / 2, width()); + p.restore(); + } + } + } + + trect.setY(trect.y() + ((fits ? 1 : 2) * serviceFont->height)); + } +} + +void Message::paintReplyInfo( + Painter &p, + QRect &trect, + const PaintContext &context) const { + if (const auto reply = Get()) { + reply->paint( + p, + this, + context, + trect.x(), + trect.y(), + trect.width(), + true); + trect.setY(trect.y() + reply->height()); + } +} + +void Message::paintSummaryHeaderInfo( + Painter &p, + QRect &trect, + const PaintContext &context) const { + if (const auto summaryHeader = Get()) { + summaryHeader->paint( + p, + this, + context, + trect.x(), + trect.y(), + trect.width(), + true); + trect.setY(trect.y() + summaryHeader->height()); + } +} + +void Message::paintViaBotIdInfo( + Painter &p, + QRect &trect, + const PaintContext &context) const { + const auto item = data(); + if (!displayFromName() && !displayForwardedFrom()) { + if (auto via = item->Get()) { + const auto stm = context.messageStyle(); + p.setFont(st::msgServiceNameFont); + p.setPen(stm->msgServiceFg); + paintLinkRipple( + p, + via->link, + QRect(trect.x(), trect.y(), via->width, st::msgServiceNameFont->height), + trect.topLeft()); + p.drawTextLeft(trect.left(), trect.top(), width(), via->text); + trect.setY(trect.y() + st::msgServiceNameFont->height); + } + } +} + +void Message::paintText( + Painter &p, + QRect &trect, + const PaintContext &context) const { + if (!hasVisibleText()) { + return; + } + const auto stm = context.messageStyle(); + p.setPen(stm->historyTextFg); + p.setFont(st::msgFont); + if (const auto botTop = Get()) { + botTop->text.drawLeftElided( + p, + trect.x(), + trect.y(), + trect.width(), + width()); + trect.setY(trect.y() + botTop->height); + } + if (const auto rich = const_cast(this)->richpage()) { + paintRichText(p, rich, richPageRect(trect), context); + return; + } + + if (!context.clip.intersects(trect) + && context.skipDrawingParts == PaintContext::SkipDrawingParts::None + && !context.gestureHorizontal.translation) { + return; + } + prepareCustomEmojiPaint(p, context, text()); + + const auto rippleLinkRange = (_linkRipple && _linkRipple->link) + ? text().linkRangeFor(_linkRipple->link) + : TextSelection(); + const auto rippleBelongsHere = !rippleLinkRange.empty(); + if (_linkRipple + && _linkRipple->ripple + && _linkRipple->cachedWidth != trect.width() + && rippleBelongsHere) { + _linkRipple = nullptr; + } + if (_linkRipple && _linkRipple->ripple && rippleBelongsHere) { + auto color = stm->textPalette.linkFg->c; + color.setAlphaF(0.1); + _linkRipple->ripple->paint( + p, + trect.x() + _linkRipple->maskOffset.x(), + trect.y() + _linkRipple->maskOffset.y(), + width(), + &color); + if (_linkRipple->ripple->empty()) { + _linkRipple = nullptr; + } + } + const auto needRippleMask = _linkRipple + && _linkRipple->link + && !_linkRipple->ripple + && rippleBelongsHere; + auto ripplePath = QPainterPath(); + auto rippleRequest = Ui::Text::HighlightInfoRequest{ + .range = rippleLinkRange, + .outPath = &ripplePath, + }; + + const auto appearing = Get(); + const auto appearingClip = appearing + && appearing->use + && (Message::context() != Context::MediaEditor); + auto linePostprocess = std::optional(); + if (appearingClip) { + const auto shown = appearing->shownLine; + const auto &line = appearing->lines[shown]; + + p.save(); + p.setClipRect(QRect( + trect.x(), + trect.y(), + trect.width(), + line.bottom)); + + const auto revealedWidth = appearing->revealedLineWidth; + const auto lineWidth = line.width; + const auto availableWidth = textRealWidth(); + linePostprocess.emplace(Ui::Text::LinePostprocess{ + .method = [=](int lineIndex) -> Fn { + if (lineIndex != shown || revealedWidth >= lineWidth) { + return nullptr; + } + return [=](QImage &cache) { + ApplyRevealGradient(appearing, cache, availableWidth); + }; + }, + .cache = &appearing->lineCache, + }); + } + + auto highlightRequest = context.computeHighlightCache(); + text().draw(p, { + .position = trect.topLeft(), + .availableWidth = std::max(textRealWidth(), trect.width()), + .palette = &stm->textPalette, + .pre = stm->preCache.get(), + .blockquote = context.quoteCache( + contentColorCollectible(), + contentColorIndex()), + .colors = context.st->highlightColors(), + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = context.now, + .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), + .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), + .selection = context.selection, + .highlight = needRippleMask + ? &rippleRequest + : (highlightRequest ? &*highlightRequest : nullptr), + .useFullWidth = true, + .linePostprocess = linePostprocess ? &*linePostprocess : nullptr, + }); + if (needRippleMask && !ripplePath.isEmpty()) { + createLinkRippleMask( + ripplePath, + trect.topLeft(), + trect.width(), + st::linkRipplePadding, + st::linkRippleRadius); + } else if (needRippleMask) { + _linkRipple = nullptr; + } + if (appearingClip) { + p.restore(); + } +} + +void Message::paintRichText( + Painter &p, + not_null rich, + QRect rect, + const PaintContext &context) const { + const auto stm = context.messageStyle(); + const auto paletteVersion = context.st->paletteVersion(); + if (rich->paletteVersion != paletteVersion) { + rich->paletteVersion = paletteVersion; + rich->article.invalidatePaletteCache(); + } + const auto appearing = Get(); + const auto appearingClip = appearing + && appearing->use + && (appearing->shownLine < int(appearing->lines.size())); + const auto viewportClip = QRect( + QPoint(), + rect.size() + ).intersected(context.clip.translated(-rect.topLeft())); + auto articleClip = viewportClip; + if (appearingClip) { + const auto &line = appearing->lines[appearing->shownLine]; + const auto shownHeight = std::min( + std::max(appearing->shownHeight, 0), + std::max(line.bottom, 0)); + articleClip = articleClip.intersected(QRect( + 0, + 0, + rect.width(), + std::max(shownHeight - rich->edgeSkips().top(), 0))); + } + rich->article.setVisibleTopBottom( + std::clamp(viewportClip.top(), 0, rect.height()), + std::clamp(viewportClip.bottom() + 1, 0, rect.height())); + auto articleContext = Iv::Markdown::MarkdownArticlePaintContext( + context).translated(-rect.topLeft()); + if (context.messageSelection && context.messageSelection->isRichPage()) { + articleContext.selectionState.selection + = context.messageSelection->richPage.selection; + articleContext.selectionState.endpoints + = &context.messageSelection->richPage.endpoints; + } + articleContext.clip = articleClip; + articleContext.bubbleGradient = usesBubblePattern(context); + articleContext.buttonLoading.owner = &data()->history()->owner(); + articleContext.buttonLoading.itemId = data()->fullId(); + articleContext.caches = { + .pre = stm->preCache.get(), + .blockquote = context.quoteCache( + contentColorCollectible(), + contentColorIndex()), + .thinking = &rich->thinkingPaintCache, + .pathShiftGradient = delegate()->elementPathShiftGradient().get(), + .colors = context.st->highlightColors(), + .st = &stm->richPageStyle, + .repaint = [weak = base::make_weak(const_cast(this))] { + if (const auto owner = weak.get()) { + owner->requestRichPageRepaint(QRect()); + } + }, + .repaintRect = [weak = base::make_weak(const_cast(this))]( + QRect articleRect) { + if (const auto owner = weak.get()) { + owner->requestRichPageRepaint(articleRect); + } + }, + }; + auto revealPostprocess + = std::optional(); + auto revealState + = std::optional(); + if (appearingClip) { + revealPostprocess.emplace( + Iv::Markdown::MarkdownArticleRevealPostprocess{ + .method = [=]( + int lineIndex, + int availableWidth) -> Fn { + if (lineIndex != appearing->shownLine + || appearing->revealedLineWidth + >= appearing->lines[lineIndex].width) { + return nullptr; + } + return [=](QImage &cache) { + ApplyRevealGradient( + appearing, + cache, + availableWidth); + }; + }, + .cache = &appearing->lineCache, + }); + revealState.emplace(Iv::Markdown::MarkdownArticleRevealPaintState{ + .activeLine = appearing->shownLine, + .nextLine = 0, + .postprocess = &*revealPostprocess, + }); + articleContext.reveal = &*revealState; + } + p.save(); + p.translate(rect.topLeft()); + rich->article.paint(p, articleContext); + p.restore(); +} + +PointState Message::pointState(QPoint point) const { + auto g = countGeometry(); + if (g.width() < 1 || isHidden()) { + return PointState::Outside; + } + + const auto media = this->media(); + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + if (drawBubble()) { + if (!g.contains(point)) { + return PointState::Outside; + } + if (const auto mediaDisplayed = media && media->isDisplayed()) { + // Hack for grouped media point state. + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + + // Entry page is always a bubble bottom. + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); + + if (hasCommentsButton()) { + g.setHeight(g.height() - st::historyCommentsButtonHeight); + } + + auto trect = g.marginsRemoved(st::msgPadding); + if (reactionsInBubble) { + const auto reactionsHeight = (_viewButton ? 0 : st::mediaInBubbleSkip) + + _reactions->height(); + trect.setHeight(trect.height() - reactionsHeight); + } + if (_viewButton) { + trect.setHeight(trect.height() - _viewButton->height()); + if (reactionsInBubble) { + trect.setHeight(trect.height() + st::msgPadding.bottom()); + } else if (mediaDisplayed) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip); + } + } + if (mediaOnBottom) { + trect.setHeight(trect.height() + st::msgPadding.bottom()); + } + //if (mediaOnTop) { + // trect.setY(trect.y() - st::msgPadding.top()); + //} else { + // if (getStateFromName(point, trect, &result)) return result; + // if (getStateTopicButton(point, trect, &result)) return result; + // if (getStateForwardedInfo(point, trect, &result, request)) return result; + // if (getStateReplyInfo(point, trect, &result)) return result; + // if (getStateViaBotIdInfo(point, trect, &result)) return result; + //} + if (check) { + auto checkHeight = check->height(); + trect.setHeight(trect.height() - checkHeight - st::mediaInBubbleSkip); + } + if (entry) { + auto entryHeight = entry->height(); + trect.setHeight(trect.height() - entryHeight); + } + + const auto mediaHeight = mediaDisplayed ? media->height() : 0; + const auto mediaLeft = trect.x() - st::msgPadding.left(); + const auto mediaTop = (!mediaDisplayed || _invertMedia) + ? (trect.y() + (mediaOnTop ? 0 : st::mediaInBubbleSkip)) + : (trect.y() + trect.height() - mediaHeight); + if (mediaDisplayed && _invertMedia) { + trect.setY(mediaTop + + mediaHeight + + (mediaOnBottom ? 0 : st::mediaInBubbleSkip)); + } + if (point.y() >= mediaTop + && point.y() < mediaTop + mediaHeight) { + return media->pointState(point - QPoint(mediaLeft, mediaTop)); + } + } + return PointState::Inside; + } else if (media) { + return media->pointState(point - g.topLeft()); + } + return PointState::Outside; +} + +bool Message::displayFromPhoto() const { + return hasFromPhoto() && !isAttachedToNext(); +} + +void Message::clickHandlerPressedChanged( + const ClickHandlerPtr &handler, + bool pressed) { + const auto startLinkRipple = [&] { + if (!_linkRipple) { + if (!pressed) { + return; + } + _linkRipple = std::make_unique(); + } + _linkRipple->link = handler; + toggleLinkRipple(pressed); + }; + if (const auto markup = data()->Get()) { + if (const auto keyboard = markup->inlineKeyboard.get()) { + keyboard->clickHandlerPressedChanged( + handler, + pressed, + countMessageRounding()); + } + } + Element::clickHandlerPressedChanged(handler, pressed); + if (const auto check = factcheckBlock()) { + check->clickHandlerPressedChanged(handler, pressed); + } + if (!handler) { + return; + } else if (_rightAction && (handler == _rightAction->link)) { + toggleRightActionRipple(pressed); + } else if (_rightAction + && _rightAction->second + && (handler == _rightAction->second->link)) { + const auto rightSize = rightActionSize(); + Assert(rightSize != std::nullopt); + if (pressed) { + if (!_rightAction->second->ripple) { + // Create a ripple. + _rightAction->second->ripple + = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + Size(rightSize->width()), + rightSize->width() / 2), + [=] { repaint(); }); + } + _rightAction->second->ripple->add(_rightAction->lastPoint); + } else if (_rightAction->second->ripple) { + _rightAction->second->ripple->lastStop(); + } + } else if (_comments && (handler == _comments->link)) { + toggleCommentsButtonRipple(pressed); + } else if (_topicButton && (handler == _topicButton->link)) { + toggleTopicButtonRipple(pressed); + } else if (_viewButton) { + _viewButton->checkLink(handler, pressed); + } else if (const auto reply = Get() + ; reply && (handler == reply->link())) { + toggleReplyRipple(pressed); + } else if (const auto summaryHeader = Get() + ; summaryHeader && (handler == summaryHeader->link())) { + toggleSummaryHeaderRipple(pressed); + } else if (_summarize && (handler == _summarize->link())) { + if (pressed) { + _summarize->addRipple([=] { repaint(); }); + } else { + _summarize->stopRipple(); + } + } else if (const auto badge = Get() + ; badge && badge->tagLink && handler == badge->tagLink) { + toggleBadgeRipple(pressed); + } else if (displayFromName() && handler == fromLink()) { + if (_fromLinkRipplePointSet || !pressed) { + startLinkRipple(); + } + } else if (const auto via = data()->Get() + ; via + && (handler == via->link) + && !displayForwardedFrom()) { + startLinkRipple(); + } else if (const auto guestChat = data()->Get() + ; guestChat + && (handler == guestChat->link)) { + startLinkRipple(); + } else if (const auto forwarded = data()->Get() + ; forwarded + && displayForwardedFrom() + && !forwarded->text.linkRangeFor(handler).empty()) { + startLinkRipple(); + } else if (hasVisibleText() + && IsRippleLink(handler) + && !text().linkRangeFor(handler).empty()) { + startLinkRipple(); + } else if (const auto rich = richpage() + ; rich + && ((handler == rich->handler) + || (handler == rich->handlerHorizontalScrollPressed) + || (handler == rich->handlerButtonRowHandler) + || (handler == rich->pressedButtonRowHandler) + || (handler == rich->handlerInlineButtonHandler) + || (handler == rich->pressedInlineButtonHandler))) { + if (pressed) { + if ((handler == rich->handler) + && rich->handlerHorizontalScrollHit + && rich->article.beginHorizontalScroll( + rich->handlerHorizontalScrollPoint, + false)) { + rich->handlerHorizontalScrollActive = true; + rich->handlerHorizontalScrollPressed = handler; + } + } else if (rich->handlerHorizontalScrollActive + && (handler == rich->handlerHorizontalScrollPressed)) { + rich->article.endHorizontalScroll(); + rich->handlerHorizontalScrollActive = false; + rich->handlerHorizontalScrollPressed = nullptr; + } + if ((handler == rich->handler) && rich->handlerPlaceholderId) { + if (pressed) { + rich->article.addPlaceholderRipple( + rich->handlerPlaceholderId, + rich->handlerPlaceholderPoint); + } else { + rich->article.stopPlaceholderRipple(rich->handlerPlaceholderId); + } + } + if (pressed) { + if ((handler == rich->handlerButtonRowHandler) + && (rich->handlerButtonRow.index >= 0)) { + rich->pressedButtonRow = rich->handlerButtonRow; + rich->pressedButtonRowHandler = handler; + rich->article.addButtonRowRipple( + rich->pressedButtonRow.id, + rich->pressedButtonRow.index, + rich->pressedButtonRow.localPoint); + } + } else if (handler == rich->pressedButtonRowHandler) { + rich->article.stopButtonRowRipple(rich->pressedButtonRow.id); + rich->pressedButtonRow = {}; + rich->pressedButtonRowHandler = nullptr; + } + if (pressed) { + if ((handler == rich->handlerInlineButtonHandler) + && rich->handlerInlineButtonPoint) { + rich->pressedInlineButtonHandler = handler; + rich->article.addInlineButtonRipple( + *rich->handlerInlineButtonPoint); + } + } else if (handler == rich->pressedInlineButtonHandler) { + rich->article.stopInlineButtonRipple(); + rich->pressedInlineButtonHandler = nullptr; + } + } else if (_reactions) { + _reactions->clickHandlerPressedChanged( + handler, + pressed, + [=] { repaint(); }); + } +} + +void Message::toggleCommentsButtonRipple(bool pressed) { + Expects(_comments != nullptr); + + if (!drawBubble()) { + return; + } else if (pressed) { + if (!_comments->ripple) { + createCommentsButtonRipple(); + } + _comments->ripple->add(_comments->lastPoint + + QPoint(_comments->rippleShift, 0)); + } else if (_comments->ripple) { + _comments->ripple->lastStop(); + } +} + +void Message::toggleRightActionRipple(bool pressed) { + Expects(_rightAction != nullptr); + + const auto rightSize = rightActionSize(); + Assert(rightSize != std::nullopt); + + if (pressed) { + if (!_rightAction->ripple) { + // Create a ripple. + const auto size = _rightAction->second + ? Size(rightSize->width()) + : *rightSize; + _rightAction->ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask(size, size.width() / 2), + [=] { repaint(); }); + } + _rightAction->ripple->add(_rightAction->lastPoint); + } else if (_rightAction->ripple) { + _rightAction->ripple->lastStop(); + } +} + +void Message::toggleBadgeRipple(bool pressed) { + const auto badge = Get(); + if (!badge) { + return; + } else if (pressed) { + if (!badge->ripple) { + const auto pill = ComputeBadgePillGeometry(badge); + auto mask = Ui::RippleAnimation::RoundRectMask( + QSize(pill.width, pill.height), + pill.height / 2); + badge->ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [=] { repaint(); }); + } + badge->ripple->add(badge->lastPoint); + } else if (badge->ripple) { + badge->ripple->lastStop(); + } +} + +void Message::toggleReplyRipple(bool pressed) { + const auto reply = Get(); + if (!reply) { + return; + } + + if (pressed) { + if (!unwrapped()) { + const auto &padding = st::msgPadding; + const auto geometry = countGeometry(); + const auto margins = reply->margins(); + const auto size = QSize( + geometry.width() - padding.left() - padding.right(), + reply->height() - margins.top() - margins.bottom()); + reply->createRippleAnimation(this, size); + } + reply->addRipple(); + } else { + reply->stopLastRipple(); + } +} + +void Message::toggleSummaryHeaderRipple(bool pressed) { + const auto summaryHeader = Get(); + if (!summaryHeader) { + return; + } + + if (pressed) { + if (!unwrapped()) { + const auto size = QSize( + countGeometry().width() - rect::m::sum::h(st::msgPadding), + summaryHeader->height() + - rect::m::sum::v(summaryHeader->margins())); + summaryHeader->createRippleAnimation(this, size); + } + summaryHeader->addRipple(); + } else { + summaryHeader->stopLastRipple(); + } +} + +BottomRippleMask Message::bottomRippleMask(int buttonHeight) const { + using namespace Ui; + using namespace Images; + using Radius = CachedCornerRadius; + using Corner = BubbleCornerRounding; + const auto g = countGeometry(); + const auto buttonWidth = g.width(); + const auto &large = CachedCornersMasks(Radius::BubbleLarge); + const auto &small = CachedCornersMasks(Radius::BubbleSmall); + const auto rounding = countBubbleRounding(); + const auto icon = (rounding.bottomLeft == Corner::Tail) + ? &st::historyBubbleTailInLeft + : (rounding.bottomRight == Corner::Tail) + ? &st::historyBubbleTailInRight + : nullptr; + const auto shift = (rounding.bottomLeft == Corner::Tail) + ? icon->width() + : 0; + const auto added = shift ? shift : icon ? icon->width() : 0; + auto corners = CornersMaskRef(); + const auto set = [&](int index) { + corners.p[index] = (rounding[index] == Corner::Large) + ? &large[index] + : (rounding[index] == Corner::Small) + ? &small[index] + : nullptr; + }; + set(kBottomLeft); + set(kBottomRight); + const auto drawer = [&](QPainter &p) { + p.setCompositionMode(QPainter::CompositionMode_Source); + const auto ratio = style::DevicePixelRatio(); + const auto corner = [&](int index, bool right) { + if (const auto image = corners.p[index]) { + const auto width = image->width() / ratio; + const auto height = image->height() / ratio; + p.drawImage( + QRect( + shift + (right ? (buttonWidth - width) : 0), + buttonHeight - height, + width, + height), + *image); + } + }; + corner(kBottomLeft, false); + corner(kBottomRight, true); + if (icon) { + const auto left = shift ? 0 : buttonWidth; + p.fillRect( + QRect{ left, 0, added, buttonHeight }, + Qt::transparent); + icon->paint( + p, + left, + buttonHeight - icon->height(), + buttonWidth + shift, + Qt::white); + } + }; + return { + RippleAnimation::MaskByDrawer( + QSize(buttonWidth + added, buttonHeight), + true, + drawer), + shift, + }; +} + +void Message::createCommentsButtonRipple() { + auto mask = bottomRippleMask(st::historyCommentsButtonHeight); + _comments->ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask.image), + [=] { repaint(); }); + _comments->rippleShift = mask.shift; +} + +void Message::toggleTopicButtonRipple(bool pressed) { + Expects(_topicButton != nullptr); + + if (!drawBubble()) { + return; + } else if (pressed) { + if (!_topicButton->ripple) { + createTopicButtonRipple(); + } + _topicButton->ripple->add(_topicButton->lastPoint); + } else if (_topicButton->ripple) { + _topicButton->ripple->lastStop(); + } +} + +void Message::paintLinkRipple( + Painter &p, + const ClickHandlerPtr &handler, + QRect linkRect, + QPoint textPosition) const { + const auto raw = _linkRipple.get(); + if (!raw || raw->link != handler) { + return; + } + if (const auto ripple = raw->ripple.get()) { + auto color = p.pen().color(); + color.setAlpha(25); + ripple->paint( + p, + textPosition.x() + raw->maskOffset.x(), + textPosition.y() + raw->maskOffset.y(), + width(), + &color); + if (ripple->empty()) { + _linkRipple = nullptr; + } + } else { + createLinkRippleMask( + linkRect, + textPosition, + st::nameRipplePadding, + st::nameRippleRadius); + } +} + +void Message::toggleLinkRipple(bool pressed) { + if (!drawBubble()) { + return; + } else if (pressed) { + repaint(); + } else if (const auto ripple = _linkRipple + ? _linkRipple->ripple.get() + : nullptr) { + ripple->lastStop(); + } +} + +void Message::recordLinkRipplePoint( + QPoint point, + QPoint textOrigin) const { + _linkRippleLastPoint = point - textOrigin; +} + +void Message::createLinkRippleMask( + const QPainterPath &path, + QPoint textPosition, + int useWidth, + style::margins padding, + int radius) const { + auto rects = std::vector(); + for (const auto &polygon : path.toSubpathPolygons()) { + rects.push_back(polygon.boundingRect().toAlignedRect()); + } + auto boundingRect = QRect(); + for (auto &rect : rects) { + rect = rect.marginsAdded(padding); + if (boundingRect.isEmpty()) { + boundingRect = rect; + } else { + boundingRect = boundingRect.united(rect); + } + } + if (boundingRect.isEmpty()) { + return; + } + const auto topLeft = boundingRect.topLeft(); + const auto maskOrigin = topLeft - textPosition; + auto mask = Ui::RippleAnimation::MaskByDrawer( + boundingRect.size(), + false, + [&](QPainter &p) { + for (const auto &rect : rects) { + const auto shifted = rect.translated(-topLeft); + p.drawRoundedRect(shifted, radius, radius); + } + }); + _linkRipple->maskOffset = maskOrigin; + _linkRipple->cachedWidth = useWidth; + _linkRipple->ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [=] { repaint(); }); + _linkRipple->ripple->add(_linkRippleLastPoint - maskOrigin); +} + +void Message::createLinkRippleMask( + QRect linkRect, + QPoint textPosition, + style::margins padding, + int radius) const { + auto rect = linkRect.marginsAdded(padding); + const auto maskOrigin = rect.topLeft() - textPosition; + const auto size = rect.size(); + auto mask = Ui::RippleAnimation::MaskByDrawer( + size, + false, + [&](QPainter &p) { + p.drawRoundedRect(QRect(QPoint(), size), radius, radius); + }); + _linkRipple->maskOffset = maskOrigin; + _linkRipple->cachedWidth = 0; + _linkRipple->ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [=] { repaint(); }); + _linkRipple->ripple->add(_linkRippleLastPoint - maskOrigin); +} + +void Message::createTopicButtonRipple() { + const auto geometry = countGeometry().marginsRemoved(st::msgPadding); + const auto availableWidth = geometry.width(); + const auto padding = st::topicButtonPadding; + const auto height = padding.top() + + st::msgNameFont->height + + padding.bottom(); + const auto width = std::max( + std::min( + availableWidth, + (padding.left() + + _topicButton->name.maxWidth() + + st::topicButtonArrowSkip + + padding.right())), + height); + auto mask = Ui::RippleAnimation::RoundRectMask( + { width, height }, + height / 2); + _topicButton->ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [=] { repaint(); }); +} + +bool Message::hasHeavyPart() const { + return _comments + || (_fromNameStatus && _fromNameStatus->custom) + || Element::hasHeavyPart(); +} + +void Message::unloadHeavyPart() { + Element::unloadHeavyPart(); + _comments = nullptr; + if (_fromNameStatus) { + _fromNameStatus->custom = nullptr; + _fromNameStatus->id = EmojiStatusId(); + } + if (const auto summaryHeader = Get()) { + summaryHeader->unloadHeavyPart(); + } +} + +bool Message::hasFromPhoto() const { + if (isHidden()) { + return false; + } + switch (context()) { + case Context::AdminLog: + return true; + case Context::WelcomeMessages: + if (const auto channel = data()->history()->peer->asBroadcast()) { + return channel->signatureProfiles(); + } + return true; + case Context::Monoforum: + return (delegate()->elementChatMode() == ElementChatMode::Wide); + case Context::History: + case Context::ChatPreview: + case Context::TTLViewer: + case Context::MediaEditor: + case Context::Pinned: + case Context::Replies: + case Context::SavedSublist: + case Context::ScheduledTopic: { + const auto item = data(); + if (item->isSponsored()) { + return false; + } else if (item->isPost()) { + return (context() == Context::MediaEditor) + || !item->isPostHidingAuthor(); + } else if (item->isEmpty() + || item->isFakeAboutView() + || isCommentsRootView()) { + return false; + } + if (item->isEphemeral()) { + if (const auto channel = item->history()->peer->asBroadcast()) { + return channel->signatureProfiles(); + } + } + const auto mode = delegate()->elementChatMode(); + if (mode != ElementChatMode::Default) { + return (mode == ElementChatMode::Wide); + } else if (item->history()->peer->isVerifyCodes()) { + return !hasOutLayout(); + } else if (item->Has()) { + const auto peer = item->history()->peer; + if (peer->isSelf() || peer->isRepliesChat()) { + return !hasOutLayout(); + } + } + if (item->isGuestChatBotMessage()) { + return true; + } + return !item->out() && !item->history()->peer->isUser(); + } break; + case Context::ContactPreview: + case Context::ShortcutMessages: + return false; + } + Unexpected("Context in Message::hasFromPhoto."); +} + +TextState Message::textState( + QPoint point, + StateRequest request) const { + _fromLinkRipplePointSet = 0; + + const auto item = data(); + const auto media = this->media(); + + auto result = TextState(item); + const auto visibleMediaTextLen = visibleMediaTextLength(); + const auto visibleTextLen = visibleTextLength(); + const auto minSymbol = (_invertMedia && request.onlyMessageText) + ? visibleMediaTextLen + : 0; + SetTextStatePosition(&result, minSymbol, false); + + auto g = countGeometry(); + if (g.width() < 1 || isHidden()) { + return result; + } + + if (const auto service = Get()) { + result.link = service->textState(point, request, g); + if (result.link) { + return result; + } + } + + const auto bubble = drawBubble(); + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + const auto mediaDisplayed = media && media->isDisplayed(); + + if (_reactions && !reactionsInBubble) { + const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height(); + const auto reactionsLeft = (!bubble && mediaDisplayed) + ? media->contentRectForReactions().x() + : 0; + g.setHeight(g.height() - reactionsHeight); + const auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip); + if (_reactions->getState(point - reactionsPosition, &result)) { + AddTextStateOffset(&result, visibleMediaTextLen + visibleTextLen); + return result; + } + } + + const auto keyboard = inlineReplyKeyboard(); + auto keyboardHeight = 0; + if (keyboard) { + keyboardHeight = keyboard->naturalHeight(); + g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); + + if (item->isHistoryEntry() || item->isAdminLogEntry()) { + const auto keyboardPosition = QPoint(g.left(), g.top() + g.height() + st::msgBotKbButton.margin); + if (QRect(keyboardPosition, QSize(g.width(), keyboardHeight)).contains(point)) { + AddTextStateOffset(&result, visibleMediaTextLen + visibleTextLen); + result.link = keyboard->getLink(point - keyboardPosition); + return result; + } + } + } + + if (bubble) { + const auto inBubble = g.contains(point); + const auto check = factcheckBlock(); + const auto entry = logEntryOriginal(); + + // Entry page is always a bubble bottom. + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); + + auto inner = g; + if (getStateCommentsButton(point, inner, &result)) { + AddTextStateOffset(&result, visibleMediaTextLen + visibleTextLen); + return result; + } + auto trect = inner.marginsRemoved(st::msgPadding); + const auto additionalInfoSkip = (mediaDisplayed + && !media->additionalInfoString().isEmpty()) + ? st::msgDateFont->height + : 0; + const auto reactionsTop = (reactionsInBubble && !_viewButton) + ? (additionalInfoSkip + st::mediaInBubbleSkip) + : additionalInfoSkip; + const auto reactionsHeight = reactionsInBubble + ? (reactionsTop + _reactions->height()) + : 0; + if (reactionsInBubble) { + trect.setHeight(trect.height() - reactionsHeight); + const auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + reactionsTop); + if (_reactions->getState(point - reactionsPosition, &result)) { + AddTextStateOffset(&result, visibleMediaTextLen + visibleTextLen); + return result; + } + } + if (_viewButton) { + const auto belowInfo = _viewButton->belowMessageInfo(); + const auto infoHeight = reactionsInBubble + ? (reactionsHeight + 2 * st::mediaInBubbleSkip) + : bottomInfoHeight(); + const auto heightMargins = QMargins(0, 0, 0, infoHeight); + if (_viewButton->getState( + point, + _viewButton->countRect(belowInfo + ? inner + : inner - heightMargins), + &result)) { + AddTextStateOffset(&result, visibleMediaTextLen + visibleTextLen); + return result; + } + if (belowInfo) { + inner.setHeight(inner.height() - _viewButton->height()); + } + trect.setHeight(trect.height() - _viewButton->height()); + if (reactionsInBubble) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip + st::msgPadding.bottom()); + } else if (mediaDisplayed) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip); + } + } + if (mediaOnBottom) { + trect.setHeight(trect.height() + st::msgPadding.bottom()); + } + if (mediaOnTop) { + trect.setY(trect.y() - st::msgPadding.top()); + } else if (inBubble) { + if (getStateFromName(point, trect, &result)) { + return result; + } + if (const auto badge = Get()) { + trect.setTop(trect.top() + badge->height); + } + if (getStateTopicButton(point, trect, &result)) { + return result; + } + if (getStateForwardedInfo(point, trect, &result, request)) { + return result; + } + if (getStateViaBotIdInfo(point, trect, &result)) { + return result; + } + if (getStateReplyInfo(point, trect, &result)) { + return result; + } + if (getStateSummaryHeaderInfo(point, trect, &result)) { + return result; + } + } + if (entry) { + auto entryHeight = entry->height(); + trect.setHeight(trect.height() - entryHeight); + auto entryLeft = inner.left(); + auto entryTop = trect.y() + trect.height(); + if (point.y() >= entryTop && point.y() < entryTop + entryHeight) { + result = entry->textState( + point - QPoint(entryLeft, entryTop), + request); + AddTextStateOffset( + &result, + visibleTextLength() + visibleMediaTextLength()); + } + } + if (check) { + auto checkHeight = check->height(); + trect.setHeight(trect.height() - checkHeight - st::mediaInBubbleSkip); + auto checkLeft = inner.left(); + auto checkTop = trect.y() + trect.height() + st::mediaInBubbleSkip; + if (point.y() >= checkTop && point.y() < checkTop + checkHeight) { + result = check->textState( + point - QPoint(checkLeft, checkTop), + request); + AddTextStateOffset( + &result, + visibleTextLength() + visibleMediaTextLength()); + } + } + + auto checkBottomInfoState = [&] { + if (mediaOnBottom + && (check || entry || media->customInfoLayout())) { + return; + } + const auto bottomInfoResult = bottomInfoTextState( + inner.left() + inner.width(), + inner.top() + inner.height(), + point, + InfoDisplayType::Default); + if (bottomInfoResult.link + || bottomInfoResult.cursor != CursorState::None + || bottomInfoResult.customTooltip) { + result = bottomInfoResult; + } + }; + if (!inBubble) { + if (point.y() >= g.y() + g.height()) { + AddTextStateOffset(&result, visibleTextLen + visibleMediaTextLen); + } + } else if (result.symbol <= minSymbol) { + const auto mediaHeight = mediaDisplayed ? media->height() : 0; + const auto mediaLeft = trect.x() - st::msgPadding.left(); + const auto mediaTop = (!mediaDisplayed || _invertMedia) + ? (trect.y() + (mediaOnTop ? 0 : st::mediaInBubbleSkip)) + : (trect.y() + trect.height() - mediaHeight); + if (mediaDisplayed && _invertMedia) { + trect.setY(mediaTop + + mediaHeight + + (mediaOnBottom ? 0 : st::mediaInBubbleSkip)); + } + if (point.y() >= mediaTop + && point.y() < mediaTop + mediaHeight) { + result = media->textState( + point - QPoint(mediaLeft, mediaTop), + request); + if (_invertMedia) { + if (request.onlyMessageText) { + SetTextStatePosition(&result, minSymbol, false); + result.cursor = CursorState::None; + } + } else if (request.onlyMessageText) { + SetTextStatePosition(&result, visibleTextLen, false); + result.cursor = CursorState::None; + } else { + AddTextStateOffset(&result, visibleTextLen); + } + } else if (getStateText(point, trect, &result, request)) { + if (_invertMedia) { + AddTextStateOffset(&result, visibleMediaTextLen); + } + result.overMessageText = true; + checkBottomInfoState(); + return result; + } else if (point.y() >= trect.y() + trect.height()) { + result.symbol = visibleTextLen + visibleMediaTextLen; + } + } + checkBottomInfoState(); + if (const auto size = rightActionSize(); size && _rightAction) { + const auto fastShareSkip = std::clamp( + (g.height() - size->height()) / 2, + 0, + st::historyFastShareBottom); + const auto fastShareLeft = hasRightLayout() + ? (g.left() - size->width() - st::historyFastShareLeft) + : (g.left() + g.width() + st::historyFastShareLeft); + const auto fastShareTop = data()->isSponsored() + ? g.top() + fastShareSkip + : g.top() + g.height() - fastShareSkip - size->height(); + if (QRect( + fastShareLeft, + fastShareTop, + size->width(), + size->height() + ).contains(point)) { + result.link = rightActionLink(point + - QPoint(fastShareLeft, fastShareTop)); + } + } + if (_summarize && _summarize->contains(point)) { + result.link = _summarize->link(); + } + } else if (media && media->isDisplayed()) { + result = media->textState(point - g.topLeft(), request); + if (request.onlyMessageText) { + SetTextStatePosition(&result, 0, false); + result.cursor = CursorState::None; + } + AddTextStateOffset(&result, visibleTextLength()); + } + + return result; +} + +bool Message::getStateCommentsButton( + QPoint point, + QRect &g, + not_null outResult) const { + if (!_comments) { + return false; + } + g.setHeight(g.height() - st::historyCommentsButtonHeight); + if (data()->isSending() + || !QRect( + g.left(), + g.top() + g.height(), + g.width(), + st::historyCommentsButtonHeight).contains(point)) { + return false; + } + if (!_comments->link && data()->repliesAreComments()) { + _comments->link = createGoToCommentsLink(); + } else if (!_comments->link && data()->externalReply()) { + _comments->link = prepareRightActionLink(); + } + outResult->link = _comments->link; + _comments->lastPoint = point - QPoint(g.left(), g.top() + g.height()); + return true; +} + +ClickHandlerPtr Message::createGoToCommentsLink() const { + const auto fullId = data()->fullId(); + const auto sessionId = data()->history()->session().uniqueId(); + return std::make_shared([=](ClickContext context) { + const auto controller = ExtractController(context); + if (!controller || controller->session().uniqueId() != sessionId) { + return; + } + if (const auto item = controller->session().data().message(fullId)) { + const auto history = item->history(); + if (const auto channel = history->peer->asChannel()) { + if (channel->invitePeekExpires()) { + controller->showToast( + tr::lng_channel_invite_private(tr::now)); + return; + } + } + controller->showRepliesForMessage(history, item->id); + } + }); +} + +bool Message::getStateFromName( + QPoint point, + QRect &trect, + not_null outResult) const { + if (!displayFromName()) { + return false; + } + if (point.y() >= trect.top() && point.y() < trect.top() + st::msgNameFont->height) { + auto availableLeft = trect.left(); + auto availableWidth = trect.width(); + const auto badgeWidth = rightBadgeWidth(); + if (badgeWidth) { + availableWidth -= st::msgPadding.right() + badgeWidth; + } + const auto item = data(); + const auto from = displayFrom(); + const auto nameText = [&]() -> const Ui::Text::String * { + if (from) { + validateFromNameText(from); + return &_fromName; + } else if (const auto info = item->displayHiddenSenderInfo()) { + return &info->nameText(); + } else { + Unexpected("Corrupt forwarded information in message."); + } + }(); + + const auto statusWidth = (from && _fromNameStatus) + ? st::dialogsPremiumIcon.icon.width() + : 0; + const auto via = item->Get(); + const auto viaShown = via && !displayForwardedFrom() && via->width; + const auto viaSkipWidth = viaShown + ? (via->width + st::msgServiceFont->spacew) + : 0; + const auto nameAvailableWidth = std::max( + ((statusWidth && availableWidth > statusWidth) + ? (availableWidth - statusWidth) + : availableWidth) - viaSkipWidth, + 0); + const auto nameWidth = std::min( + nameText->maxWidth(), + nameAvailableWidth); + if (statusWidth && availableWidth > statusWidth) { + const auto x = availableLeft + + nameWidth + - (_fromNameStatus->custom ? (2 * _fromNameStatus->skip) : 0); + const auto checkWidth = _fromNameStatus->custom + ? (st::emojiSize - 2 * _fromNameStatus->skip) + : statusWidth; + if (point.x() >= x && point.x() < x + checkWidth) { + ensureFromNameStatusLink(from); + outResult->link = _fromNameStatus->link; + return true; + } + } + if (point.x() >= availableLeft + && point.x() < availableLeft + availableWidth + && point.x() < availableLeft + nameWidth) { + outResult->link = fromLink(); + recordLinkRipplePoint(point, trect.topLeft()); + _fromLinkRipplePointSet = 1; + return true; + } + + const auto skipWidth = nameWidth + + (_fromNameStatus + ? (st::dialogsPremiumIcon.icon.width() + + st::msgServiceFont->spacew) + : 0) + + st::msgServiceFont->spacew; + availableLeft += skipWidth; + availableWidth -= skipWidth; + + if (via + && !displayForwardedFrom() + && point.x() >= availableLeft + && point.x() < availableLeft + availableWidth + && point.x() < availableLeft + via->width) { + outResult->link = via->link; + recordLinkRipplePoint(point, trect.topLeft()); + return true; + } + if (const auto guestChat = item->Get()) { + auto guestChatLeft = availableLeft; + if (via && !displayForwardedFrom()) { + guestChatLeft += via->width + st::msgServiceFont->spacew; + } + if (point.x() >= guestChatLeft + && point.x() < availableLeft + availableWidth + && point.x() < guestChatLeft + guestChat->width) { + outResult->link = guestChat->link; + recordLinkRipplePoint(point, trect.topLeft()); + return true; + } + } + if (badgeWidth) { + const auto badge = Get(); + const auto badgeLeft = trect.left() + + trect.width() + - badgeWidth; + const auto badgeRight = trect.left() + + trect.width() + + st::msgPadding.right(); + const auto boostTextWidth = (badge && !badge->boosts.isEmpty()) + ? badge->boosts.maxWidth() + : 0; + const auto boostLeft = boostTextWidth + ? (trect.left() + trect.width() - boostTextWidth) + : 0; + if (boostTextWidth + && point.x() >= boostLeft + && point.x() < badgeRight) { + if (!badge->boostsLink) { + const auto fullId = item->fullId(); + badge->boostsLink = std::make_shared([ + fullId + ](ClickContext context) { + if (const auto controller = ExtractController(context)) { + if (const auto item = controller->session().data().message(fullId)) { + if (const auto channel = item->history()->peer->asChannel()) { + controller->resolveBoostState(channel); + } + } + } + }); + } + outResult->link = badge->boostsLink; + return true; + } + const auto tagRight = boostTextWidth + ? (boostLeft - st::msgTagBadgeBoostSkip) + : badgeRight; + if (point.x() >= badgeLeft && point.x() < tagRight) { + if (badge->special) { + return false; + } + if (!badge->tagLink) { + const auto weak = base::make_weak(this); + badge->tagLink = std::make_shared([ + weak + ](ClickContext context) { + if (const auto controller = ExtractController(context)) { + if (const auto view = weak.get()) { + const auto badge = view->Get(); + if (!badge) { + return; + } + const auto item = view->data(); + const auto peer = item->history()->peer; + const auto author = item->author(); + controller->uiShow()->show(Box( + TagInfoBox, + controller->uiShow(), + peer, + author, + badge->tag.toString(), + badge->role)); + } + } + }); + } + { + const auto pill = ComputeBadgePillGeometry(badge); + const auto &padding = st::msgTagBadgePadding; + if (badge->role != BadgeRole::User) { + const auto badgeTop = trect.top() + + (st::msgNameFont->height - pill.height) / 2; + badge->lastPoint = point + - QPoint(badgeLeft, badgeTop); + } else { + const auto pillLeft = badgeLeft + - (pill.width - pill.textWidth) / 2; + const auto pillTop = trect.top() - padding.top(); + badge->lastPoint = point + - QPoint(pillLeft, pillTop); + } + } + outResult->link = badge->tagLink; + return true; + } + } + } + trect.setTop(trect.top() + st::msgNameFont->height); + return false; +} + +void Message::ensureFromNameStatusLink(not_null peer) const { + Expects(_fromNameStatus != nullptr); + + if (_fromNameStatus->link) { + return; + } + _fromNameStatus->link = std::make_shared([=]( + ClickContext context) { + const auto controller = ExtractController(context); + if (controller) { + Settings::ShowEmojiStatusPremium(controller, peer); + } + }); +} + +bool Message::getStateTopicButton( + QPoint point, + QRect &trect, + not_null outResult) const { + if (!displayedTopicButton()) { + return false; + } + trect.setTop(trect.top() + st::topicButtonSkip); + const auto padding = st::topicButtonPadding; + const auto availableWidth = trect.width(); + const auto height = padding.top() + + st::msgNameFont->height + + padding.bottom(); + const auto width = std::max( + std::min( + availableWidth, + (padding.left() + + _topicButton->name.maxWidth() + + st::topicButtonArrowSkip + + padding.right())), + height); + const auto rect = QRect(trect.x(), trect.y(), width, height); + if (rect.contains(point)) { + outResult->link = _topicButton->link; + _topicButton->lastPoint = point - rect.topLeft(); + return true; + } + trect.setY(trect.y() + height + st::topicButtonSkip); + return false; +} + +bool Message::getStateForwardedInfo( + QPoint point, + QRect &trect, + not_null outResult, + StateRequest request) const { + if (!displayForwardedFrom()) { + return false; + } + const auto item = data(); + const auto forwarded = item->Get(); + const auto skip1 = forwarded->psaType.isEmpty() + ? 0 + : st::historyPsaIconSkip1; + const auto skip2 = forwarded->psaType.isEmpty() + ? 0 + : st::historyPsaIconSkip2; + const auto fits = (forwarded->text.maxWidth() <= (trect.width() - skip1)); + const auto fwdheight = (fits ? 1 : 2) * st::semiboldFont->height; + if (point.y() >= trect.top() && point.y() < trect.top() + fwdheight) { + if (skip1) { + const auto &icon = st::historyPsaIconIn; + const auto position = fits + ? st::historyPsaIconPosition1 + : st::historyPsaIconPosition2; + const auto iconRect = QRect( + trect.x() + trect.width() - position.x() - icon.width(), + trect.y() + position.y(), + icon.width(), + icon.height()); + if (iconRect.contains(point)) { + if (const auto link = psaTooltipLink()) { + outResult->link = link; + return true; + } + } + } + const auto useWidth = trect.width() - (fits ? skip1 : skip2); + const auto breakEverywhere = (forwarded->text.countHeight(useWidth) > 2 * st::semiboldFont->height); + auto textRequest = request.forText(); + if (breakEverywhere) { + textRequest.flags |= Ui::Text::StateRequest::Flag::BreakEverywhere; + } + *outResult = TextState(item, forwarded->text.getState( + point - trect.topLeft(), + useWidth, + textRequest)); + if (outResult->link) { + recordLinkRipplePoint(point, trect.topLeft()); + } + SetTextStatePosition(outResult, 0, false); + if (breakEverywhere) { + outResult->cursor = CursorState::Forwarded; + } else { + outResult->cursor = CursorState::None; + } + return true; + } + trect.setTop(trect.top() + fwdheight); + return false; +} + +ClickHandlerPtr Message::psaTooltipLink() const { + const auto state = Get(); + if (!state || !state->buttonVisible) { + return nullptr; + } else if (state->link) { + return state->link; + } + const auto type = state->type; + const auto handler = [=] { + const auto custom = type.isEmpty() + ? QString() + : Lang::GetNonDefaultValue(kPsaTooltipPrefix + type.toUtf8()); + auto text = tr::rich( + (custom.isEmpty() + ? tr::lng_tooltip_psa_default(tr::now) + : custom)); + TextUtilities::ParseEntities(text, 0); + psaTooltipToggled(true); + delegate()->elementShowTooltip(text, crl::guard(this, [=] { + psaTooltipToggled(false); + })); + }; + state->link = std::make_shared( + crl::guard(this, handler)); + return state->link; +} + +void Message::psaTooltipToggled(bool tooltipShown) const { + const auto visible = !tooltipShown; + const auto state = Get(); + if (state->buttonVisible == visible) { + return; + } + state->buttonVisible = visible; + history()->owner().notifyViewLayoutChange(this); + state->buttonVisibleAnimation.start( + [=] { repaint(); }, + visible ? 0. : 1., + visible ? 1. : 0., + st::fadeWrapDuration); +} + +bool Message::getStateReplyInfo( + QPoint point, + QRect &trect, + not_null outResult) const { + if (const auto reply = Get()) { + const auto margins = reply->margins(); + const auto height = reply->height(); + if (point.y() >= trect.top() && point.y() < trect.top() + height) { + const auto g = QRect( + trect.x(), + trect.y() + margins.top(), + trect.width(), + height - margins.top() - margins.bottom()); + if (g.contains(point)) { + if (const auto link = reply->link()) { + outResult->link = reply->link(); + reply->saveRipplePoint(point - g.topLeft()); + } + } + return true; + } + trect.setTop(trect.top() + height); + } + return false; +} + +bool Message::getStateSummaryHeaderInfo( + QPoint point, + QRect &trect, + not_null outResult) const { + if (const auto summaryHeader = Get()) { + const auto margins = summaryHeader->margins(); + const auto height = summaryHeader->height(); + if (point.y() >= trect.top() && point.y() < trect.top() + height) { + const auto g = QRect( + trect.x(), + trect.y() + margins.top(), + trect.width(), + height - margins.top() - margins.bottom()); + if (g.contains(point)) { + if (const auto link = summaryHeader->link()) { + outResult->link = summaryHeader->link(); + summaryHeader->saveRipplePoint(point - g.topLeft()); + } + } + return true; + } + trect.setTop(trect.top() + height); + } + return false; +} + +bool Message::getStateViaBotIdInfo( + QPoint point, + QRect &trect, + not_null outResult) const { + const auto item = data(); + if (const auto via = item->Get()) { + if (!displayFromName() && !displayForwardedFrom()) { + if (QRect(trect.x(), trect.y(), via->width, st::msgNameFont->height).contains(point)) { + outResult->link = via->link; + recordLinkRipplePoint(point, trect.topLeft()); + return true; + } + trect.setTop(trect.top() + st::msgNameFont->height); + } + } + return false; +} + +bool Message::getStateText( + QPoint point, + QRect &trect, + not_null outResult, + StateRequest request) const { + if (!hasVisibleText()) { + return false; + } + const auto item = this->textItem(); + if (const auto rich = richpage()) { + const auto local = prepareRichPageStateRect(point, trect); + if (!base::in_range(point.y(), trect.y(), trect.y() + trect.height())) { + return false; + } + const auto clearHorizontalScrollHandler = [&] { + rich->handlerHorizontalScrollHit = std::nullopt; + rich->handlerHorizontalScrollPoint = {}; + }; + const auto clearButtonRowHandler = [&] { + rich->handlerButtonRow = {}; + rich->handlerButtonRowHandler = nullptr; + }; + const auto clearInlineButtonHandler = [&] { + rich->handlerInlineButtonPoint = std::nullopt; + rich->handlerInlineButtonHandler = nullptr; + }; + if (const auto task = RichPageTaskMarkerHit(rich->article, local) + ; task && item->history()->session().api().richTasks() + .togglingAllowed(item)) { + *outResult = TextState(item); + rich->handlerCodeHeaderSegmentIndex = -1; + clearHorizontalScrollHandler(); + rich->handlerPreparedLink = std::nullopt; + rich->handlerMediaActivation = {}; + rich->handlerPlaceholderId = {}; + rich->handlerPlaceholderPoint = {}; + clearButtonRowHandler(); + clearInlineButtonHandler(); + if (!rich->handlerTaskItem || (*rich->handlerTaskItem != *task)) { + rich->handlerTaskItem = task; + rich->handler = std::make_shared( + [weak = base::make_weak(const_cast(this)), + source = *task](ClickContext context) { + const auto owner = weak.get(); + const auto item = owner + ? owner->data().get() + : nullptr; + if (item) { + item->history()->session().api().richTasks() + .toggle(item, source); + } + }); + } + outResult->link = rich->handler; + return true; + } + rich->handlerTaskItem = std::nullopt; + const auto horizontalScrollHit = rich->article.horizontalScrollHit(local); + *outResult = TextState(item); + outResult->horizontalScroll = horizontalScrollHit.scrollable; + const auto hit = rich->article.hitTest( + local, + request.flags | Ui::Text::StateRequest::Flag::LookupSymbol); + if (horizontalScrollHit.overScrollbar) { + rich->handlerCodeHeaderSegmentIndex = -1; + rich->handlerPreparedLink = std::nullopt; + rich->handlerMediaActivation = {}; + rich->handlerPlaceholderId = {}; + rich->handlerPlaceholderPoint = {}; + clearButtonRowHandler(); + clearInlineButtonHandler(); + if (!rich->handlerHorizontalScrollHit || !rich->handler) { + rich->handler = std::make_shared( + [](ClickContext) { + }); + } + rich->handlerHorizontalScrollHit = horizontalScrollHit; + rich->handlerHorizontalScrollPoint = local; + outResult->link = rich->handler; + return true; + } + if (!hit.valid()) { + rich->handlerCodeHeaderSegmentIndex = -1; + clearHorizontalScrollHandler(); + clearButtonRowHandler(); + clearInlineButtonHandler(); + return horizontalScrollHit.scrollable; + } + const auto offset = rich->article.selectionOffsetFromHit( + hit, + TextSelectType::Letters); + SetRichPageSelectionCursor( + outResult, + hit.segmentIndex, + offset, + hit.direct); + if (hit.codeHeaderCopy) { + const auto reuse = rich->handler + && (rich->handlerCodeHeaderSegmentIndex == hit.segmentIndex); + clearHorizontalScrollHandler(); + rich->handlerPreparedLink = std::nullopt; + rich->handlerMediaActivation = {}; + rich->handlerPlaceholderId = {}; + rich->handlerPlaceholderPoint = {}; + clearButtonRowHandler(); + clearInlineButtonHandler(); + if (!reuse) { + const auto text = rich->article.textForContext(hit); + rich->handlerCodeHeaderSegmentIndex = hit.segmentIndex; + rich->handler = std::make_shared( + [text](ClickContext context) { + CopyRichPageCodeBlockText(text, std::move(context)); + }); + } + outResult->link = rich->handler; + } else if (hit.preparedLink + && (hit.preparedLink->kind == PreparedLinkKind::External) + && (hit.mediaActivation.kind == MediaActivationKind::None) + && Iv::Markdown::ExtractPreparedLink(hit.state.link)) { + rich->handlerCodeHeaderSegmentIndex = -1; + clearHorizontalScrollHandler(); + rich->handlerPreparedLink = std::nullopt; + rich->handlerMediaActivation = {}; + rich->handlerPlaceholderId = {}; + rich->handlerPlaceholderPoint = {}; + clearButtonRowHandler(); + clearInlineButtonHandler(); + outResult->link = hit.state.link; + } else if (hit.preparedLink + || hit.mediaActivation.kind != MediaActivationKind::None) { + const auto prepared = hit.preparedLink; + const auto activation = hit.mediaActivation; + const auto reuse = SamePreparedLink( + rich->handlerPreparedLink, + prepared) + && SameMediaActivation( + rich->handlerMediaActivation, + activation); + rich->handlerCodeHeaderSegmentIndex = -1; + clearHorizontalScrollHandler(); + rich->handlerPlaceholderId = hit.mediaActivation.placeholderId; + rich->handlerPlaceholderPoint = hit.placeholderLocalPoint; + clearButtonRowHandler(); + clearInlineButtonHandler(); + if (!reuse) { + rich->handlerPreparedLink = prepared; + rich->handlerMediaActivation = activation; + rich->handler = std::make_shared( + [weak = base::make_weak(const_cast(this)), + prepared, + activation](ClickContext context) { + if (const auto owner = weak.get()) { + if (prepared) { + owner->activateRichPagePreparedLink( + *prepared, + std::move(context)); + } else { + owner->activateRichPageMedia( + activation, + std::move(context)); + } + } + }, + prepared); + } + outResult->link = rich->handler; + } else { + rich->handlerCodeHeaderSegmentIndex = -1; + clearHorizontalScrollHandler(); + if (hit.buttonRow.index >= 0) { + rich->handlerButtonRow = hit.buttonRow; + rich->handlerButtonRowHandler = hit.state.link; + } else { + clearButtonRowHandler(); + } + if (hit.inlineButton) { + rich->handlerInlineButtonPoint = hit.inlineButton; + rich->handlerInlineButtonHandler = hit.state.link; + } else { + clearInlineButtonHandler(); + } + if (!hit.customTooltip.isEmpty()) { + outResult->customTooltip = true; + using Flag = Ui::Text::StateRequest::Flag; + if (request.flags & Flag::LookupCustomTooltip) { + outResult->customTooltipText = hit.customTooltip; + } + } + outResult->link = hit.state.link; + } + outResult->cursor = (!outResult->link && hit.direct) + ? CursorState::Text + : CursorState::None; + return true; + } + if (const auto botTop = Get()) { + trect.setY(trect.y() + botTop->height); + } + if (base::in_range(point.y(), trect.y(), trect.y() + trect.height())) { + *outResult = TextState(item, text().getState( + point - trect.topLeft(), + std::max(textRealWidth(), trect.width()), + request.forText())); + if (outResult->link + && IsRippleLink(outResult->link) + && !text().linkRangeFor(outResult->link).empty()) { + recordLinkRipplePoint(point, trect.topLeft()); + } + return true; + } + return false; +} + +// Forward to media. +void Message::updatePressed(QPoint point) { + if (const auto rich = richpage()) { + auto trect = QRect(); + if (prepareRichPageTextRect(trect)) { + const auto local = prepareRichPageStateRect(point, trect); + rich->article.updatePressed(local); + if (rich->handlerHorizontalScrollActive + && (ClickHandler::getPressed() + == rich->handlerHorizontalScrollPressed)) { + rich->article.updateHorizontalScroll(local); + } + } + } + const auto item = data(); + const auto media = this->media(); + if (!media) { + return; + } + + auto g = countGeometry(); + + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + if (_reactions && !reactionsInBubble) { + const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height(); + g.setHeight(g.height() - reactionsHeight); + } + + const auto keyboard = inlineReplyKeyboard(); + if (keyboard) { + auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight(); + g.setHeight(g.height() - keyboardHeight); + } + + if (drawBubble()) { + auto mediaDisplayed = media && media->isDisplayed(); + auto trect = g.marginsAdded(-st::msgPadding); + if (mediaDisplayed && media->isBubbleTop()) { + trect.setY(trect.y() - st::msgPadding.top()); + } else { + if (displayFromName()) { + trect.setTop(trect.top() + st::msgNameFont->height); + } + if (displayedTopicButton()) { + trect.setTop(trect.top() + + st::topicButtonSkip + + st::topicButtonPadding.top() + + st::msgNameFont->height + + st::topicButtonPadding.bottom() + + st::topicButtonSkip); + } + if (displayForwardedFrom()) { + auto forwarded = item->Get(); + auto fwdheight = ((forwarded->text.maxWidth() > trect.width()) ? 2 : 1) * st::semiboldFont->height; + trect.setTop(trect.top() + fwdheight); + } + if (const auto reply = Get()) { + trect.setTop(trect.top() + reply->height()); + } + if (const auto summaryHeader = Get()) { + trect.setTop(trect.top() + summaryHeader->height()); + } + if (item->Has()) { + if (!displayFromName() && !displayForwardedFrom()) { + trect.setTop(trect.top() + st::msgNameFont->height); + } + } + } + if (mediaDisplayed && media->isBubbleBottom()) { + trect.setHeight(trect.height() + st::msgPadding.bottom()); + } + + if (mediaDisplayed) { + auto mediaHeight = media->height(); + auto mediaLeft = trect.x() - st::msgPadding.left(); + auto mediaTop = (trect.y() + trect.height() - mediaHeight); + media->updatePressed(point - QPoint(mediaLeft, mediaTop)); + } + } else { + media->updatePressed(point - g.topLeft()); + } +} + +bool Message::consumeHorizontalScroll( + QPoint position, + int delta, + Qt::ScrollPhase phase) { + const auto rich = richpage(); + auto trect = QRect(); + if (!rich || !prepareRichPageTextRect(trect)) { + return false; + } + return rich->article.consumeHorizontalScroll( + prepareRichPageStateRect(position, trect), + delta, + phase); +} + +bool Message::canConsumeHorizontalScroll(QPoint position, int delta) const { + const auto rich = richpage(); + auto trect = QRect(); + if (!rich || !prepareRichPageTextRect(trect)) { + return false; + } + return rich->article.canConsumeHorizontalScroll( + prepareRichPageStateRect(position, trect), + delta); +} + +MessageSelection Message::selectionFromStates( + const TextState &anchor, + const TextState ¤t, + TextSelectType type) const { + if (anchor.selectionCursor.isRichPage() + || current.selectionCursor.isRichPage()) { + if (!anchor.selectionCursor.valid() + || !current.selectionCursor.valid() + || !anchor.selectionCursor.isRichPage() + || !current.selectionCursor.isRichPage()) { + return {}; + } + auto selection = MarkdownArticleSelection{ + .from = anchor.selectionCursor.richPagePosition, + .to = current.selectionCursor.richPagePosition, + }; + const auto endpoints = MarkdownArticleSelectionEndpoints{ + .from = anchor.selectionCursor.richPage, + .to = current.selectionCursor.richPage, + }; + if (type != TextSelectType::Letters) { + const auto rich = richpage(); + if (!rich) { + return {}; + } + selection = AdjustRichPageSelection( + rich->article, + selection.from, + selection.to, + type); + if (selection.empty()) { + return {}; + } + } + return MessageSelection::RichPage( + selection, + endpoints, + anchor.selectionCursor.richPagePosition, + current.selectionCursor.richPagePosition, + anchor.selectionCursor.richPage, + current.selectionCursor.richPage); + } + const auto anchorEndpoint = FlatSelectionEndpointFromState(anchor); + const auto currentEndpoint = FlatSelectionEndpointFromState(current); + auto selection = TextSelection( + uint16(std::min(anchorEndpoint.offset(), currentEndpoint.offset())), + uint16(std::max(anchorEndpoint.offset(), currentEndpoint.offset()))); + if (type != TextSelectType::Letters) { + selection = adjustSelection(selection, type); + } + return MessageSelection::Flat( + selection, + anchorEndpoint, + currentEndpoint); +} + +TextForMimeData Message::selectedText(TextSelection selection) const { + const auto media = this->media(); + auto logEntryOriginalResult = TextForMimeData(); + auto factcheckResult = TextForMimeData(); + const auto mediaDisplayed = (media && media->isDisplayed()); + const auto mediaBefore = mediaDisplayed && invertMedia(); + const auto textSelection = mediaBefore + ? media->skipSelection(selection) + : selection; + const auto mediaSelection = !invertMedia() + ? skipTextSelection(selection) + : selection; + auto textResult = hasVisibleText() + ? text().toTextForMimeData(textSelection) + : TextForMimeData(); + auto mediaResult = (media && (mediaDisplayed || isHiddenByGroup())) + ? media->selectedText(mediaSelection) + : TextForMimeData(); + if (const auto check = factcheckBlock()) { + const auto checkSelection = mediaBefore + ? skipTextSelection(textSelection) + : mediaDisplayed + ? media->skipSelection(mediaSelection) + : skipTextSelection(selection); + factcheckResult = check->selectedText(checkSelection); + } + if (const auto entry = logEntryOriginal()) { + const auto originalSelection = mediaBefore + ? skipTextSelection(textSelection) + : mediaDisplayed + ? media->skipSelection(mediaSelection) + : skipTextSelection(selection); + logEntryOriginalResult = entry->selectedText(originalSelection); + } + const auto &first = mediaBefore ? mediaResult : textResult; + auto &second = mediaBefore ? textResult : mediaResult; + auto result = first; + if (result.empty()) { + result = std::move(second); + } else if (!second.empty()) { + result.append(u"\n\n"_q).append(std::move(second)); + } + if (result.empty()) { + result = std::move(factcheckResult); + } else if (!factcheckResult.empty()) { + result.append(u"\n\n"_q).append(std::move(factcheckResult)); + } + if (result.empty()) { + result = std::move(logEntryOriginalResult); + } else if (!logEntryOriginalResult.empty()) { + result.append(u"\n\n"_q).append(std::move(logEntryOriginalResult)); + } + return result; +} + +TextForMimeData Message::selectedText( + const MessageSelection &selection) const { + if (const auto flat = selection.flatSelection(); !flat.empty()) { + return selectedText(flat); + } else if (selection.isRichPage()) { + if (const auto rich = richpage()) { + return rich->article.textForSelection( + selection.richPage.selection, + &selection.richPage.endpoints); + } + } + return {}; +} + +SelectedQuote Message::selectedQuote(TextSelection selection) const { + const auto textItem = this->textItem(); + const auto item = textItem ? textItem : data().get(); + const auto &translated = item->translatedText(); + const auto &original = item->originalText(); + if (&translated != &original + || selection.empty() + || selection == FullSelection) { + return {}; + } else if (hasVisibleText()) { + const auto media = this->media(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto mediaBefore = mediaDisplayed && invertMedia(); + const auto textSelection = mediaBefore + ? media->skipSelection(selection) + : selection; + return FindSelectedQuote(text(), textSelection, item); + } else if (const auto media = this->media()) { + if (media->isDisplayed() || isHiddenByGroup()) { + return media->selectedQuote(selection); + } + } + return {}; +} + +SelectedQuote Message::selectedQuote( + const MessageSelection &selection) const { + if (const auto flat = selection.flatSelection(); !flat.empty()) { + return selectedQuote(flat); + } + return {}; +} + +TextSelection Message::selectionFromQuote( + const SelectedQuote "e) const { + Expects(quote.item != nullptr); + + if (quote.highlight.quote.empty()) { + return {}; + } + const auto item = quote.item; + const auto &translated = item->translatedText(); + const auto &original = item->originalText(); + if (&translated != &original) { + return {}; + } else if (hasVisibleText()) { + const auto media = this->media(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto mediaBefore = mediaDisplayed && invertMedia(); + const auto result = FindSelectionFromQuote(text(), quote); + return mediaBefore ? media->unskipSelection(result) : result; + } else if (const auto media = this->media()) { + if (media->isDisplayed() || isHiddenByGroup()) { + return media->selectionFromQuote(quote); + } + } + return {}; +} + +TextSelection Message::adjustSelection( + TextSelection selection, + TextSelectType type) const { + const auto media = this->media(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto mediaBefore = mediaDisplayed && invertMedia(); + const auto textSelection = mediaBefore + ? media->skipSelection(selection) + : selection; + const auto useSelection = [](TextSelection selection, bool skipped) { + return !skipped || (selection != TextSelection(uint16(), uint16())); + }; + auto textAdjusted = (hasVisibleText() + && useSelection(textSelection, mediaBefore)) + ? text().adjustSelection(textSelection, type) + : textSelection; + auto textResult = mediaBefore + ? media->unskipSelection(textAdjusted) + : textAdjusted; + auto mediaResult = TextSelection(); + auto mediaSelection = mediaBefore + ? selection + : skipTextSelection(selection); + if (mediaDisplayed) { + auto mediaAdjusted = useSelection(mediaSelection, !mediaBefore) + ? media->adjustSelection(mediaSelection, type) + : mediaSelection; + mediaResult = mediaBefore + ? mediaAdjusted + : unskipTextSelection(mediaAdjusted); + } + auto checkResult = TextSelection(); + if (const auto check = factcheckBlock()) { + auto checkSelection = !mediaDisplayed + ? skipTextSelection(selection) + : mediaBefore + ? skipTextSelection(textSelection) + : media->skipSelection(mediaSelection); + auto checkAdjusted = useSelection(checkSelection, true) + ? check->adjustSelection(checkSelection, type) + : checkSelection; + checkResult = unskipTextSelection(checkAdjusted); + if (mediaDisplayed) { + checkResult = media->unskipSelection(checkResult); + } + } + auto entryResult = TextSelection(); + if (const auto entry = logEntryOriginal()) { + auto entrySelection = !mediaDisplayed + ? skipTextSelection(selection) + : mediaBefore + ? skipTextSelection(textSelection) + : media->skipSelection(mediaSelection); + auto entryAdjusted = useSelection(entrySelection, true) + ? entry->adjustSelection(entrySelection, type) + : entrySelection; + entryResult = unskipTextSelection(entryAdjusted); + if (mediaDisplayed) { + entryResult = media->unskipSelection(entryResult); + } + } + auto result = textResult; + if (!mediaResult.empty()) { + result = result.empty() ? mediaResult : TextSelection{ + std::min(result.from, mediaResult.from), + std::max(result.to, mediaResult.to), + }; + } + if (!checkResult.empty()) { + result = result.empty() ? checkResult : TextSelection{ + std::min(result.from, checkResult.from), + std::max(result.to, checkResult.to), + }; + } + if (!entryResult.empty()) { + result = result.empty() ? entryResult : TextSelection{ + std::min(result.from, entryResult.from), + std::max(result.to, entryResult.to), + }; + } + return result; +} + +MessageSelection Message::adjustSelection( + const MessageSelection &selection, + TextSelectType type) const { + if (selection.isFlat()) { + const auto adjusted = adjustSelection(selection.flatSelection(), type); + if (adjusted.empty() || (adjusted == FullSelection)) { + return {}; + } + return MessageSelection::Flat( + adjusted, + selection.anchor.isFlat() + ? selection.anchor.flat + : MessageSelectionFlatEndpoint{ + .symbol = adjusted.from, + .afterSymbol = false, + }, + selection.focus.isFlat() + ? selection.focus.flat + : MessageSelectionFlatEndpoint{ + .symbol = adjusted.to, + .afterSymbol = false, + }); + } else if (selection.isRichPage()) { + if (type == TextSelectType::Letters + || !selection.anchor.valid() + || !selection.focus.valid() + || !selection.anchor.isRichPage() + || !selection.focus.isRichPage()) { + return selection; + } + const auto anchor = selection.anchor.richPagePosition; + const auto focus = selection.focus.richPagePosition; + if (!anchor.valid() || !focus.valid()) { + return selection; + } + const auto rich = richpage(); + if (!rich) { + return {}; + } + const auto adjusted = AdjustRichPageSelection( + rich->article, + anchor, + focus, + type); + if (adjusted.empty()) { + return {}; + } + return MessageSelection::RichPage( + adjusted, + MarkdownArticleSelectionEndpoints{ + .from = selection.anchor.richPage, + .to = selection.focus.richPage, + }, + anchor, + focus, + selection.anchor.richPage, + selection.focus.richPage); + } + return {}; +} + +TextSelection Message::selectionForEdit( + const MessageSelection &selection) const { + return selection.isFlat() + ? selection.flatRangeForEdit() + : TextSelection(); +} + +bool Message::selectionContains( + const MessageSelection &selection, + const TextState &state) const { + if (!selection.isRichPage()) { + return Element::selectionContains(selection, state); + } + const auto rich = richpage(); + if (!rich + || !state.overMessageText + || !state.selectionCursor.isRichPage()) { + return false; + } + auto hit = Iv::Markdown::MarkdownArticleHitTestResult(); + hit.segmentIndex = state.selectionCursor.richPagePosition.segment; + hit.forcedOffset = state.selectionCursor.richPagePosition.offset; + hit.direct = state.selectionCursor.richPage.direct; + return rich->article.selectionContains( + selection.richPage.selection, + &selection.richPage.endpoints, + hit); +} + +Reactions::ButtonParameters Message::reactionButtonParameters( + QPoint position, + const TextState &reactionState) const { + using namespace Reactions; + auto result = ButtonParameters{ .context = data()->fullId() }; + const auto outsideBubble = (!_comments && !embedReactionsInBubble()); + const auto geometry = countGeometry(); + result.pointer = position; + const auto onTheLeft = hasRightLayout(); + + const auto keyboard = inlineReplyKeyboard(); + const auto keyboardHeight = keyboard + ? (st::msgBotKbButton.margin + keyboard->naturalHeight()) + : 0; + const auto reactionsHeight = (_reactions && !embedReactionsInBubble()) + ? (st::mediaInBubbleSkip + _reactions->height()) + : 0; + result.reactionsHeight = reactionsHeight; + result.keyboardHeight = keyboardHeight; + const auto innerHeight = geometry.height() + - keyboardHeight + - reactionsHeight; + const auto maybeRelativeCenter = outsideBubble + ? media()->reactionButtonCenterOverride() + : std::nullopt; + const auto addOnTheRight = [&] { + return (maybeRelativeCenter + || !(displayFastShare() || displayGoToOriginal())) + ? st::reactionCornerCenter.x() + : 0; + }; + const auto relativeCenter = QPoint( + maybeRelativeCenter.value_or(onTheLeft + ? -st::reactionCornerCenter.x() + : (geometry.width() + addOnTheRight())), + innerHeight + st::reactionCornerCenter.y()); + result.center = geometry.topLeft() + relativeCenter; + if (reactionState.itemId != result.context + && !geometry.contains(position)) { + result.outside = true; + } + const auto minSkip = (st::reactionCornerShadow.left() + + st::reactionCornerSize.width() + + st::reactionCornerShadow.right()) / 2; + result.center = QPoint( + std::min(std::max(result.center.x(), minSkip), width() - minSkip), + result.center.y()); + return result; +} + +ReplyButton::ButtonParameters Message::replyButtonParameters( + QPoint position, + const TextState &replyState) const { + using namespace ReplyButton; + if (!displayFastReply() || unwrapped()) { + return {}; + } + auto result = ButtonParameters{ .context = data()->fullId() }; + const auto geometry = countGeometry(); + result.pointer = position; + const auto reactionInnerRight = st::reactionCornerCenter.x() + + st::reactionCornerSize.width() / 2; + const auto replyInnerWidth = ReplyButton::ComputeInnerWidth(); + const auto relativeCenter = QPoint( + geometry.width() + reactionInnerRight - replyInnerWidth, + st::replyCornerCenter.y()); + result.center = geometry.topLeft() + relativeCenter; + if (replyState.itemId != result.context + && !geometry.contains(position)) { + result.outside = true; + } + result.link = fastReplyLink(); + return result; +} + +int Message::reactionsOptimalWidth() const { + return _reactions ? _reactions->countNiceWidth() : 0; +} + +void Message::drawInfo( + Painter &p, + const PaintContext &context, + int right, + int bottom, + int width, + InfoDisplayType type) const { + if (hidesBottomInfo()) { + return; + } + p.setFont(st::msgDateFont); + + const auto st = context.st; + const auto sti = context.imageStyle(); + const auto stm = context.messageStyle(); + bool invertedsprites = (type == InfoDisplayType::Image) + || (type == InfoDisplayType::Background); + int32 infoRight = right, infoBottom = bottom; + switch (type) { + case InfoDisplayType::Default: + infoRight -= st::msgPadding.right() - st::msgDateDelta.x(); + infoBottom -= st::msgPadding.bottom() - st::msgDateDelta.y(); + p.setPen(stm->msgDateFg); + break; + case InfoDisplayType::Image: + infoRight -= st::msgDateImgDelta + st::msgDateImgPadding.x(); + infoBottom -= st::msgDateImgDelta + st::msgDateImgPadding.y(); + p.setPen(st->msgDateImgFg()); + break; + case InfoDisplayType::Background: + infoRight -= st::msgDateImgPadding.x(); + infoBottom -= st::msgDateImgPadding.y(); + p.setPen(st->msgServiceFg()); + break; + } + + const auto size = _bottomInfo.currentSize(); + const auto dateX = infoRight - size.width(); + const auto dateY = infoBottom - size.height(); + if (type == InfoDisplayType::Image) { + const auto dateW = size.width() + 2 * st::msgDateImgPadding.x(); + const auto dateH = size.height() + 2 * st::msgDateImgPadding.y(); + Ui::FillRoundRect(p, dateX - st::msgDateImgPadding.x(), dateY - st::msgDateImgPadding.y(), dateW, dateH, sti->msgDateImgBg, sti->msgDateImgBgCorners); + } else if (type == InfoDisplayType::Background) { + const auto dateW = size.width() + 2 * st::msgDateImgPadding.x(); + const auto dateH = size.height() + 2 * st::msgDateImgPadding.y(); + Ui::FillRoundRect(p, dateX - st::msgDateImgPadding.x(), dateY - st::msgDateImgPadding.y(), dateW, dateH, sti->msgServiceBg, sti->msgServiceBgCornersSmall); + } + _bottomInfo.paint( + p, + { dateX, dateY }, + width, + delegate()->elementShownUnread(this), + invertedsprites, + context); +} + +TextState Message::bottomInfoTextState( + int right, + int bottom, + QPoint point, + InfoDisplayType type) const { + if (hidesBottomInfo()) { + return {}; + } + auto infoRight = right; + auto infoBottom = bottom; + switch (type) { + case InfoDisplayType::Default: + infoRight -= st::msgPadding.right() - st::msgDateDelta.x(); + infoBottom -= st::msgPadding.bottom() - st::msgDateDelta.y(); + break; + case InfoDisplayType::Image: + infoRight -= st::msgDateImgDelta + st::msgDateImgPadding.x(); + infoBottom -= st::msgDateImgDelta + st::msgDateImgPadding.y(); + break; + case InfoDisplayType::Background: + infoRight -= st::msgDateImgPadding.x(); + infoBottom -= st::msgDateImgPadding.y(); + break; + } + const auto size = _bottomInfo.currentSize(); + const auto infoLeft = infoRight - size.width(); + const auto infoTop = infoBottom - size.height(); + return _bottomInfo.textState( + this, + point - QPoint{ infoLeft, infoTop }); +} + +int Message::infoWidth() const { + return hidesBottomInfo() ? 0 : _bottomInfo.maxWidth(); +} + +int Message::bottomInfoFirstLineWidth() const { + return hidesBottomInfo() ? 0 : _bottomInfo.firstLineWidth(); +} + +bool Message::bottomInfoIsWide() const { + if (hidesBottomInfo() + || (_reactions && embedReactionsInBubble())) { + return false; + } + return _bottomInfo.isWide(); +} + +bool Message::isSignedAuthorElided() const { + return _bottomInfo.isSignedAuthorElided(); +} + +bool Message::embedReactionsInBubble() const { + return usesMessageInfoLayout(); +} + +void Message::validateFromNameText(PeerData *from) const { + if (!from) { + if (_fromNameStatus) { + _fromNameStatus = nullptr; + } + return; + } + const auto version = from->nameVersion(); + if (_fromNameVersion < version) { + _fromNameVersion = version; + _fromName.setText( + st::msgNameStyle, + from->name(), + Ui::NameTextOptions()); + } + if (from->isPremium() + || (from->isChannel() + && from->emojiStatusId() + && from != history()->peer)) { + if (!_fromNameStatus) { + _fromNameStatus = std::make_unique(); + const auto size = st::emojiSize; + const auto emoji = Ui::Text::AdjustCustomEmojiSize(size); + _fromNameStatus->skip = (size - emoji) / 2; + } + } else if (_fromNameStatus) { + _fromNameStatus = nullptr; + } +} + +bool Message::updateBottomInfo() { + const auto wasInfo = _bottomInfo.currentSize(); + _bottomInfo.update(BottomInfoDataFromMessage(this), width()); + return (_bottomInfo.currentSize() != wasInfo); +} + +void Message::itemDataChanged() { + const auto infoChanged = updateBottomInfo(); + const auto reactionsChanged = updateReactions(); + const auto media = this->media(); + const auto mediaChanged = media && media->updateItemData(); + if (infoChanged || reactionsChanged || mediaChanged) { + history()->owner().requestViewResize(this); + } else { + repaint(); + } +} + +void Message::refreshDataIdHook() { + if (_rightAction && base::take(_rightAction->link)) { + _rightAction->link = rightActionLink(_rightAction->lastPoint); + } + if (base::take(_fastReplyLink)) { + _fastReplyLink = fastReplyLink(); + } + if (_viewButton) { + _viewButton = nullptr; + updateViewButtonExistence(); + } + if (_comments) { + _comments->link = nullptr; + } +} + +int Message::monospaceMaxWidth() const { + const auto textual = hasRichPage() ? bubbleTextualWidth() : 0; + const auto fromText = hasRichPage() + ? std::max({ + textualMaxWidth() + - st::msgPadding.left() + - st::msgPadding.right(), + textual - st::msgPadding.left() - st::msgPadding.right(), + int(richpage()->demandedTextWidth), + }) + : hasVisibleText() + ? text().countMaxMonospaceWidth() + : 0; + const auto fromMedia = this->media() + ? this->media()->contributedMaxMonospaceWidth() + : 0; + return st::msgPadding.left() + + std::max(fromText, fromMedia) + + st::msgPadding.right(); +} + +int Message::bubbleTextWidth(int bubbleWidth) const { + // For rich pages the article is laid out exactly at the bubble width, + // so richPageWidthFor(bubbleTextWidth(width)) must give width back. + // Clamping by msgMinWidth here would lay the article out wider than + // the bubble shrunk to its content, painting centered blocks (like + // display math formulas) shifted right and cut by the bubble edge. + const auto floored = hasRichPage() + ? bubbleWidth + : std::max(bubbleWidth, st::msgMinWidth); + return floored + - st::msgPadding.left() + - st::msgPadding.right(); +} + +int Message::richPageDemandedTextWidth() const { + const auto rich = richpage(); + return rich + ? std::min( + rich->article.contentDemandedWidth(), + kMaxWidth - st::msgPadding.left() - st::msgPadding.right()) + : 0; +} + +int Message::bubbleTextualWidth() const { + const auto full = textualMaxWidth(); + if (hasRichPage()) { + if (const auto cached = richpage()->textualWidth) { + return cached; + } + auto innerWidth = bubbleTextWidth(full); + [[maybe_unused]] auto laidOutHeight = textHeightFor(innerWidth); + // Horizontally scrolled blocks never fit at readable width. + const auto demanded = richPageDemandedTextWidth(); + if (demanded > innerWidth) { + innerWidth = demanded; + laidOutHeight = textHeightFor(innerWidth); + } + const auto rich = richpage(); + rich->textualWidth = st::msgPadding.left() + + std::max(rich->article.lastLayoutWidth(), 1) + + st::msgPadding.right(); + rich->demandedTextWidth = richPageDemandedTextWidth(); + return rich->textualWidth; + } + const auto media = this->media(); + if (!hasVisibleText() + || !media + || !media->allowsNarrowBubble()) { + return full; + } + const auto minimum = std::max( + media->minBubbleWidthForNarrowBubble(), + st::msgMinWidth); + if (_bubbleTextualWidthMinimum != minimum) { + _bubbleTextualWidthMinimum = minimum; + if (minimum >= full) { + _bubbleTextualWidthCache = minimum; + } else { + const auto lineHeight = text().style()->font->height; + const auto fullTextHeight = textHeightFor(bubbleTextWidth(full)); + if (fullTextHeight > kMaxNiceToReadLines * lineHeight) { + _bubbleTextualWidthCache = full; + } else { + auto left = minimum; + auto right = full; + while (left < right) { + const auto middle = left + (right - left) / 2; + const auto middleHeight = textHeightFor( + bubbleTextWidth(middle)); + if (middleHeight <= kMaxNiceToReadLines * lineHeight) { + right = middle; + } else { + left = middle + 1; + } + } + _bubbleTextualWidthCache = right; + [[maybe_unused]] const auto ensureRightCache + = textHeightFor(bubbleTextWidth(right)); + } + } + } + return _bubbleTextualWidthCache; +} + +int Message::viewButtonHeight() const { + return _viewButton ? _viewButton->height() : 0; +} + +void Message::updateViewButtonExistence() { + if (context() == Context::MediaEditor) { + _viewButton = nullptr; + return; + } + const auto item = data(); + const auto make = [=](auto &&from) { + return std::make_unique( + std::forward(from), + colorIndex(), + [=] { repaint(); }); + }; + if (const auto richPage = item->richPage(); richPage && richPage->part) { + const auto itemId = item->fullId(); + if (_viewButton && _viewButton->matches(itemId)) { + return; + } + _viewButton = make(itemId); + return; + } + const auto media = item->media(); + if (media && ViewButton::MediaHasViewButton(media)) { + if (_viewButton && _viewButton->matches(media)) { + return; + } + _viewButton = make(media); + return; + } + _viewButton = nullptr; +} + +void Message::initLogEntryOriginal() { + if (const auto log = data()->Get()) { + AddComponents(LogEntryOriginal::Bit()); + const auto entry = Get(); + using Flags = MediaWebPageFlags; + entry->page = std::make_unique(this, log->page, Flags()); + } +} + +void Message::initPsa() { + if (const auto forwarded = data()->Get()) { + if (!forwarded->psaType.isEmpty()) { + AddComponents(PsaTooltipState::Bit()); + Get()->type = forwarded->psaType; + } + } +} + +WebPage *Message::logEntryOriginal() const { + if (const auto entry = Get()) { + return entry->page.get(); + } + return nullptr; +} + +WebPage *Message::factcheckBlock() const { + if (const auto entry = Get()) { + return entry->page.get(); + } + return nullptr; +} + +bool Message::toggleSelectionByHandlerClick( + const ClickHandlerPtr &handler) const { + if (_comments && _comments->link == handler) { + return true; + } else if (_viewButton && _viewButton->link() == handler) { + return true; + } else if (const auto media = this->media()) { + if (media->toggleSelectionByHandlerClick(handler)) { + return true; + } + } + return false; +} + +bool Message::allowTextSelectionByHandler( + const ClickHandlerPtr &handler) const { + if (const auto media = this->media()) { + if (media->allowTextSelectionByHandler(handler)) { + return true; + } + } + if (dynamic_cast(handler.get())) { + return true; + } + if (const auto rich = dynamic_cast( + handler.get())) { + const auto &link = rich->link(); + if (link && link->kind == PreparedLinkKind::ToggleBlockquote) { + return true; + } + } + return false; +} + +bool Message::hasFromName() const { + switch (context()) { + case Context::AdminLog: + case Context::WelcomeMessages: + return true; + case Context::Monoforum: + return data()->out() || data()->from()->isChannel(); + case Context::History: + case Context::ChatPreview: + case Context::TTLViewer: + case Context::MediaEditor: + case Context::Pinned: + case Context::Replies: + case Context::SavedSublist: + case Context::ScheduledTopic: { + const auto item = data(); + const auto peer = item->history()->peer; + if (hasOutLayout() && !item->from()->isChannel()) { + if (peer->isSelf()) { + if (const auto forwarded = item->Get()) { + return forwarded->savedFromSender + && forwarded->savedFromSender->isChannel(); + } + } + return false; + } else if (!peer->isUser() || item->isGuestChatBotMessage()) { + if (const auto media = this->media()) { + return !media->hideFromName(); + } + return true; + } + if (const auto forwarded = item->Get()) { + if (forwarded->imported + && peer.get() == forwarded->originalSender) { + return false; + } else if (item->showForwardsFromSender(forwarded)) { + return true; + } + } + return false; + } break; + case Context::ContactPreview: + case Context::ShortcutMessages: + return false; + } + Unexpected("Context in Message::hasFromName."); +} + +bool Message::displayFromName() const { + if (!hasFromName() || isAttachedToPrevious() || data()->isSponsored()) { + return false; + } + return !Has(); +} + +bool Message::displayForwardedFrom() const { + return data()->displayForwardedFrom(); +} + +bool Message::hasOutLayout() const { + if (context() == Context::WelcomeMessages) { + return false; + } + const auto item = data(); + if (item->history()->peer->isSelf()) { + if (const auto forwarded = item->Get()) { + if (context() == Context::ShortcutMessages) { + return true; + } + return (context() == Context::SavedSublist + || context() == Context::History) + && (!forwarded->forwardOfForward() + ? (forwarded->originalSender + && forwarded->originalSender->isSelf()) + : ((forwarded->savedFromSender + && forwarded->savedFromSender->isSelf()) + || forwarded->savedFromOutgoing)); + } + return true; + } else if (const auto forwarded = item->Get()) { + if (!forwarded->imported + || !forwarded->originalSender + || !forwarded->originalSender->isSelf()) { + if (item->showForwardsFromSender(forwarded)) { + return false; + } + } + } + if (item->isGuestChatBotMessage()) { + return false; + } + return item->out() && !item->isPost(); +} + +bool Message::drawBubble() const { + const auto item = data(); + if (isHidden()) { + return false; + } else if (logEntryOriginal() + || factcheckBlock() + || item->isFakeAboutView()) { + return true; + } + const auto media = this->media(); + return media + ? (hasVisibleText() || media->needsBubble()) + : !item->isEmpty(); +} + +bool Message::hasBubble() const { + return drawBubble(); +} + +TopicButton *Message::displayedTopicButton() const { + return _topicButton.get(); +} + +bool Message::unwrapped() const { + const auto item = data(); + if (isHidden()) { + return true; + } else if (logEntryOriginal() || factcheckBlock()) { + return false; + } + const auto media = this->media(); + return media + ? (!hasVisibleText() && media->unwrapped()) + : item->isEmpty(); +} + +int Message::minWidthForMedia() const { + // InstantViewMediaRuntime without a rich page means a media view + // hosted inside an article, it doesn't draw a bubble with info. + // Rich page messages are real bubbles and need the minimal width. + if (Get() && !hasRichPage()) { + return 0; + } + auto result = hidesBottomInfo() + ? 0 + : infoWidth() + + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()); + const auto views = data()->Get(); + if (data()->repliesAreComments() && !views->replies.text.isEmpty()) { + const auto limit = HistoryMessageViews::kMaxRecentRepliers; + const auto single = st::historyCommentsUserpics.size; + const auto shift = st::historyCommentsUserpics.shift; + const auto added = single + + (limit - 1) * (single - shift) + + st::historyCommentsSkipLeft + + st::historyCommentsSkipRight + + st::historyCommentsSkipText + + st::historyCommentsOpenOutSelected.width() + + st::historyCommentsSkipRight + + st::mediaUnreadSkip + + st::mediaUnreadSize; + accumulate_max(result, added + views->replies.textWidth); + } else if (data()->externalReply()) { + const auto added = st::historyCommentsIn.width() + + st::historyCommentsSkipLeft + + st::historyCommentsSkipRight + + st::historyCommentsSkipText + + st::historyCommentsOpenOutSelected.width() + + st::historyCommentsSkipRight; + accumulate_max(result, added + st::semiboldFont->width( + tr::lng_replies_view_original(tr::now))); + } + if (const auto keyboard = inlineReplyKeyboard()) { + accumulate_max(result, keyboard->naturalWidth()); + } + return result; +} + +bool Message::hasFastReply() const { + if (context() == Context::Replies) { + if (isCommentsRootView()) { + return false; + } + } else if (context() != Context::History) { + return false; + } + const auto peer = data()->history()->peer; + return !hasOutLayout() && (peer->isChat() || peer->isMegagroup()); +} + +bool Message::displayFastReply() const { + const auto canSendAnything = [&] { + const auto item = data(); + const auto peer = item->history()->peer; + const auto topic = item->topic(); + return topic + ? Data::CanSendAnything(topic) + : Data::CanSendAnything(peer); + }; + + return hasFastReply() + && data()->isRegular() + && canSendAnything() + && !delegate()->elementInSelectionMode(this).inSelectionMode; +} + +bool Message::displayRightActionComments() const { + return !isPinnedContext() + && (context() != Context::SavedSublist) + && data()->repliesAreComments() + && media() + && media()->isDisplayed() + && !hasBubble(); +} + +std::optional Message::rightActionSize() const { + if (context() == Context::MediaEditor) { + return std::nullopt; + } else if (displayRightActionComments()) { + const auto views = data()->Get(); + Assert(views != nullptr); + return (views->repliesSmall.textWidth > 0) + ? QSize( + std::max( + st::historyFastShareSize, + 2 * st::historyFastShareBottom + views->repliesSmall.textWidth), + st::historyFastShareSize + st::historyFastShareBottom + st::semiboldFont->height) + : QSize(st::historyFastShareSize, st::historyFastShareSize); + } + return data()->isSponsored() + ? ((_rightAction && _rightAction->second) + ? QSize(st::historyFastCloseSize, st::historyFastCloseSize * 2) + : QSize(st::historyFastCloseSize, st::historyFastCloseSize)) + : (displayFastShare() || displayGoToOriginal()) + ? QSize(st::historyFastShareSize, st::historyFastShareSize) + : std::optional(); +} + +bool Message::displayFastShare() const { + const auto item = data(); + const auto peer = item->history()->peer; + if (!item->allowsForward() || IsAnchoredEphemeral(item)) { + return false; + } else if (peer->isChannel()) { + return !peer->isMegagroup(); + } else if (const auto user = peer->asUser()) { + if (const auto forwarded = item->Get()) { + return !item->out() + && forwarded->originalSender + && forwarded->originalSender->isBroadcast() + && !item->showForwardsFromSender(forwarded); + } else if (user->isBot() && !item->out()) { + if (const auto media = this->media()) { + return media->allowsFastShare(); + } + } + } + return false; +} + +bool Message::displayGoToOriginal() const { + if (isPinnedContext()) { + return !hasOutLayout(); + } else if (context() == Context::Monoforum) { + return false; + } + const auto item = data(); + if (const auto forwarded = item->Get()) { + return forwarded->savedFromPeer + && forwarded->savedFromMsgId + && (!item->externalReply() || !hasBubble()) + && (context() != Context::Replies); + } + return false; +} + +void Message::drawRightAction( + Painter &p, + const PaintContext &context, + int left, + int top, + int outerWidth) const { + ensureRightAction(); + + const auto size = rightActionSize(); + const auto st = context.st; + + if (_rightAction->ripple) { + const auto &stm = context.messageStyle(); + const auto colorOverride = &stm->msgWaveformInactive->c; + _rightAction->ripple->paint( + p, + left, + top, + size->width(), + colorOverride); + if (_rightAction->ripple->empty()) { + _rightAction->ripple.reset(); + } + } + if (_rightAction->second && _rightAction->second->ripple) { + const auto &stm = context.messageStyle(); + const auto colorOverride = &stm->msgWaveformInactive->c; + _rightAction->second->ripple->paint( + p, + left, + top + st::historyFastCloseSize, + size->width(), + colorOverride); + if (_rightAction->second->ripple->empty()) { + _rightAction->second->ripple.reset(); + } + } + + p.setPen(Qt::NoPen); + p.setBrush(st->msgServiceBg()); + { + PainterHighQualityEnabler hq(p); + const auto rect = style::rtlrect( + left, + top, + size->width(), + size->height(), + outerWidth); + const auto usual = st::historyFastShareSize; + if (size->width() == size->height() && size->width() == usual) { + p.drawEllipse(rect); + } else { + p.drawRoundedRect(rect, usual / 2, usual / 2); + } + } + if (displayRightActionComments()) { + const auto &icon = st->historyFastCommentsIcon(); + icon.paint( + p, + left + (size->width() - icon.width()) / 2, + top + (st::historyFastShareSize - icon.height()) / 2, + outerWidth); + const auto views = data()->Get(); + Assert(views != nullptr); + if (views->repliesSmall.textWidth > 0) { + p.setPen(st->msgServiceFg()); + p.setFont(st::semiboldFont); + p.drawTextLeft( + left + (size->width() - views->repliesSmall.textWidth) / 2, + top + st::historyFastShareSize, + outerWidth, + views->repliesSmall.text, + views->repliesSmall.textWidth); + } + } else if (_rightAction->second) { + st->historyFastCloseIcon().paintInCenter( + p, + QRect(left, top, size->width(), size->width())); + st->historyFastMoreIcon().paintInCenter( + p, + QRect(left, size->width() + top, size->width(), size->width())); + } else { + const auto &icon = data()->isSponsored() + ? st->historyFastCloseIcon() + : (displayFastShare() + && !isPinnedContext() + && this->context() != Context::SavedSublist) + ? st->historyFastShareIcon() + : st->historyGoToOriginalIcon(); + icon.paintInCenter(p, Rect(left, top, *size)); + } +} + +ClickHandlerPtr Message::rightActionLink( + std::optional pressPoint) const { + if (delegate()->elementInSelectionMode(this).progress > 0) { + return nullptr; + } + ensureRightAction(); + if (!_rightAction->link) { + _rightAction->link = prepareRightActionLink(); + } + if (pressPoint) { + _rightAction->lastPoint = *pressPoint; + } + if (_rightAction->second + && (_rightAction->lastPoint.y() > st::historyFastCloseSize)) { + return _rightAction->second->link; + } + return _rightAction->link; +} + +void Message::ensureRightAction() const { + if (_rightAction) { + return; + } + Assert(rightActionSize().has_value()); + _rightAction = std::make_unique(); +} + +ClickHandlerPtr Message::prepareRightActionLink() const { + if (data()->isSponsored()) { + return HideSponsoredClickHandler(); + } else if (isPinnedContext()) { + return JumpToMessageClickHandler(data()); + } else if ((context() != Context::SavedSublist) + && displayRightActionComments()) { + return createGoToCommentsLink(); + } + const auto sessionId = data()->history()->session().uniqueId(); + const auto owner = &data()->history()->owner(); + const auto itemId = data()->fullId(); + const auto forwarded = data()->Get(); + const auto savedFromPeer = forwarded + ? forwarded->savedFromPeer + : nullptr; + const auto savedFromMsgId = forwarded ? forwarded->savedFromMsgId : 0; + + using Callback = FnMut)>; + const auto showByThread = std::make_shared(); + const auto showByThreadWeak = std::weak_ptr(showByThread); + if (data()->externalReply()) { + *showByThread = [=, requested = 0]( + not_null controller) mutable { + const auto original = savedFromPeer->owner().message( + savedFromPeer, + savedFromMsgId); + if (original && original->replyToTop()) { + controller->showRepliesForMessage( + original->history(), + original->replyToTop(), + original->id, + Window::SectionShow::Way::Forward); + } else if (!requested) { + const auto prequested = &requested; + requested = 1; + savedFromPeer->session().api().requestMessageData( + savedFromPeer, + savedFromMsgId, + [=, weak = base::make_weak(controller)] { + if (const auto strong = showByThreadWeak.lock()) { + if (const auto strongController = weak.get()) { + *prequested = 2; + (*strong)(strongController); + } + } + }); + } else if (requested == 2) { + controller->showPeerHistory( + savedFromPeer, + Window::SectionShow::Way::Forward, + savedFromMsgId); + } + }; + }; + + class FastShareClickHandler : public LambdaClickHandler { + public: + FastShareClickHandler(Fn handler) + : LambdaClickHandler(std::move(handler)) {} + QString tooltip() const override { + return tr::lng_fast_share_tooltip(tr::now); + } + }; + + const auto result = std::make_shared([=]( + ClickContext context) { + const auto controller = ExtractController(context); + if (!controller || controller->session().uniqueId() != sessionId) { + return; + } + + if (const auto item = owner->message(itemId)) { + if (*showByThread) { + (*showByThread)(controller); + } else if (savedFromPeer && savedFromMsgId) { + controller->showPeerHistory( + savedFromPeer, + Window::SectionShow::Way::Forward, + savedFromMsgId); + } else if (base::IsCtrlPressed()) { + FastShareMessageToSelf(controller->uiShow(), item); + } else { + FastShareMessage(controller, item); + } + } + }); + const auto navigates = data()->externalReply() + || (savedFromPeer && savedFromMsgId); + if (!navigates) { + result->setProperty(kFastShareProperty, QVariant::fromValue(true)); + } + return result; +} + +ClickHandlerPtr Message::fastReplyLink() const { + if (_fastReplyLink) { + return _fastReplyLink; + } + const auto itemId = data()->fullId(); + _fastReplyLink = std::make_shared(crl::guard(this, [=] { + delegate()->elementReplyTo({ itemId }); + })); + return _fastReplyLink; +} + +bool Message::isPinnedContext() const { + return context() == Context::Pinned; +} + +void Message::updateMediaInBubbleState() { + const auto item = data(); + const auto media = this->media(); + + if (media) { + media->updateNeedBubbleState(); + } + const auto reactionsInBubble = (_reactions && embedReactionsInBubble()); + auto mediaHasSomethingBelow = (_viewButton != nullptr) + || reactionsInBubble + || (invertMedia() && hasVisibleText()); + auto mediaHasSomethingAbove = false; + auto getMediaHasSomethingAbove = [&] { + return displayFromName() + || displayedTopicButton() + || displayForwardedFrom() + || Has() + || Has() + || item->Has(); + }; + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + if (check) { + mediaHasSomethingBelow = true; + mediaHasSomethingAbove = getMediaHasSomethingAbove(); + auto checkState = (mediaHasSomethingAbove + || hasVisibleText() + || (media && media->isDisplayed())) + ? MediaInBubbleState::Bottom + : MediaInBubbleState::None; + check->setInBubbleState(checkState); + if (!media) { + check->setBubbleRounding(countBubbleRounding()); + return; + } + } else if (entry) { + mediaHasSomethingBelow = true; + mediaHasSomethingAbove = getMediaHasSomethingAbove(); + auto entryState = (mediaHasSomethingAbove + || hasVisibleText() + || (media && media->isDisplayed())) + ? MediaInBubbleState::Bottom + : MediaInBubbleState::None; + entry->setInBubbleState(entryState); + if (!media) { + entry->setBubbleRounding(countBubbleRounding()); + return; + } + } else if (!media) { + return; + } + + const auto guard = gsl::finally([&] { + media->setBubbleRounding(countBubbleRounding()); + }); + if (!drawBubble()) { + media->setInBubbleState(MediaInBubbleState::None); + return; + } + + if (!check && !entry) { + mediaHasSomethingAbove = getMediaHasSomethingAbove(); + } + if (!invertMedia() && hasVisibleText()) { + mediaHasSomethingAbove = true; + } + const auto state = [&] { + if (mediaHasSomethingAbove) { + if (mediaHasSomethingBelow) { + return MediaInBubbleState::Middle; + } + return MediaInBubbleState::Bottom; + } else if (mediaHasSomethingBelow) { + return MediaInBubbleState::Top; + } + return MediaInBubbleState::None; + }(); + media->setInBubbleState(state); +} + +void Message::updateRichPageInBubbleState() { + const auto rich = richpage(); + if (!rich) { + return; + } + auto edges = countRichPageBubbleEdges(); + using Corner = Ui::BubbleCornerRounding; + auto corners = countBubbleRounding(); + if (!edges.top) { + corners.topLeft = corners.topRight = Corner::None; + } + if (!edges.bottom || edges.infoBelow) { + corners.bottomLeft = corners.bottomRight = Corner::None; + } + edges.corners = corners; + if (rich->article.bubbleEdges() != edges) { + rich->article.setBubbleEdges(edges); + invalidateTextSizeCache(); + } +} + +auto Message::countRichPageBubbleEdges() const +-> Iv::Markdown::MarkdownArticleBubbleEdges { + const auto item = data(); + const auto media = this->media(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto badge = Get(); + const auto somethingAbove = displayFromName() + || (badge && badge->height > 0) + || displayedTopicButton() + || displayForwardedFrom() + || item->Has() + || Has() + || Has() + || Has() + || (mediaDisplayed && _invertMedia); + const auto somethingBelow = (_reactions && embedReactionsInBubble()) + || item->repliesAreComments() + || item->externalReply() + || (_viewButton != nullptr) + || factcheckBlock() + || logEntryOriginal() + || (mediaDisplayed && !_invertMedia); + const auto bubble = drawBubble(); + return Iv::Markdown::MarkdownArticleBubbleEdges{ + .top = bubble && !somethingAbove, + .bottom = bubble && !somethingBelow, + .mediaAbove = bubble && mediaDisplayed && _invertMedia, + .infoBelow = bubble && richPageInfoRow() && !_viewButton, + }; +} + +bool Message::richPageInfoRow() const { + const auto rich = richpage(); + return rich + && text().hasSkipBlock() + && !rich->article.hasSkipBlock(); +} + +void Message::fromNameUpdated(int width) const { + const auto item = data(); + if (Has()) { + width -= st::msgPadding.right() + rightBadgeWidth(); + } + const auto from = displayFrom(); + validateFromNameText(from); + const auto via = item->Get(); + const auto guestChat = item->Get(); + if (!via && !guestChat) { + return; + } + const auto available = width + - st::msgPadding.left() + - st::msgPadding.right() + - (_fromNameStatus + ? (st::dialogsPremiumIcon.icon.width() + + st::msgServiceFont->spacew) + : 0); + auto viaWidth = 0; + if (via && !displayForwardedFrom()) { + via->resize(available - st::msgServiceFont->spacew); + viaWidth = st::msgServiceFont->spacew + via->width; + } + if (guestChat) { + const auto nameText = [&]() -> const Ui::Text::String * { + if (from) { + return &_fromName; + } else if (const auto info = item->originalHiddenSenderInfo()) { + return &info->nameText(); + } else { + Unexpected("Corrupted forwarded information in message."); + } + }(); + const auto nameWidth = std::min( + nameText->maxWidth(), + std::max(available - viaWidth, 0)); + guestChat->resize(available + - viaWidth + - nameWidth + - st::msgServiceFont->spacew); + } +} + +TextSelection Message::skipTextSelection(TextSelection selection) const { + if (selection.from == 0xFFFF || !hasVisibleText()) { + return selection; + } + return HistoryView::UnshiftItemSelection(selection, text()); +} + +TextSelection Message::unskipTextSelection(TextSelection selection) const { + if (!hasVisibleText()) { + return selection; + } + return HistoryView::ShiftItemSelection(selection, text()); +} + +QRect Message::innerGeometry() const { + auto result = countGeometry(); + if (!hasOutLayout()) { + const auto w = std::max( + (media() ? media()->resolveCustomInfoRightBottom().x() : 0), + result.width()); + result.setWidth(std::min( + w + rightActionSize().value_or(QSize(0, 0)).width() * 2, + width())); + } + if (hasBubble()) { + const auto cut = [&](int amount) { + amount = std::min(amount, result.height()); + result.setTop(result.top() + amount); + }; + cut(st::msgPadding.top() + st::mediaInBubbleSkip); + + if (displayFromName()) { + // See paintFromName(). + cut(st::msgNameFont->height); + } + if (displayedTopicButton()) { + cut(st::topicButtonSkip + + st::topicButtonPadding.top() + + st::msgNameFont->height + + st::topicButtonPadding.bottom() + + st::topicButtonSkip); + } + if (!displayFromName() && !displayForwardedFrom()) { + // See paintViaBotIdInfo(). + if (data()->Has()) { + cut(st::msgServiceNameFont->height); + } + } + // Skip displayForwardedFrom() until there are no animations for it. + if (const auto reply = Get()) { + // See paintReplyInfo(). + cut(reply->height()); + } + } + return result; +} + +QPoint Message::mediaTopLeft() const { + return _lastMediaPosition; +} + +bool Message::isCommentsRootView() const { + return context() == Context::Replies + && data()->isDiscussionPost() + && !data()->history()->isForum(); +} + +QRect Message::countGeometry() const { + const auto item = data(); + const auto centeredView = item->isFakeAboutView() + || isCommentsRootView(); + const auto media = this->media(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto mediaWidth = mediaDisplayed ? media->width() : width(); + const auto outbg = hasOutLayout(); + const auto useMoreSpace = (delegate()->elementChatMode() + == ElementChatMode::Narrow); + const auto wideSkip = useMoreSpace + ? st::msgMargin.left() + : st::msgMargin.right(); + const auto availableWidth = width() + - st::msgMargin.left() + - (centeredView ? st::msgMargin.left() : wideSkip); + auto contentLeft = hasRightLayout() ? wideSkip : st::msgMargin.left(); + auto contentWidth = availableWidth; + if (hasFromPhoto()) { + contentLeft += st::msgPhotoSkip; + if (const auto size = rightActionSize()) { + contentWidth -= size->width() + (st::msgPhotoSkip - st::historyFastShareSize); + } + //} else if (!Adaptive::Wide() && !out() && !fromChannel() && st::msgPhotoSkip - (hmaxwidth - hwidth) > 0) { + // contentLeft += st::msgPhotoSkip - (hmaxwidth - hwidth); + } + accumulate_min(contentWidth, maxWidth()); + accumulate_min(contentWidth, int(_bubbleWidthLimit)); + if (mediaWidth < contentWidth) { + const auto textualWidth = bubbleTextualWidth(); + if (mediaWidth < textualWidth + && (!media || !media->enforceBubbleWidth())) { + accumulate_min(contentWidth, textualWidth); + } else { + contentWidth = mediaWidth; + } + } else if (!mediaDisplayed) { + const auto appearing = Get(); + const auto use = (appearing && appearing->use) + ? appearing->shownWidth + : textRealWidth(); + if (use > 0) { + const auto shrunk = std::max( + use + st::msgPadding.left() + st::msgPadding.right(), + int(_nonTextMaxWidth)); + accumulate_min(contentWidth, shrunk); + } + } + if (contentWidth < availableWidth + && delegate()->elementChatMode() != ElementChatMode::Wide) { + if (outbg) { + contentLeft += availableWidth - contentWidth; + } else if (centeredView) { + contentLeft += (availableWidth - contentWidth) / 2; + } + } else if (contentWidth < availableWidth && centeredView) { + contentLeft += std::max( + ((st::msgMaxWidth + 2 * st::msgPhotoSkip) - contentWidth) / 2, + 0); + } + + const auto contentTop = marginTop(); + return QRect( + contentLeft, + contentTop, + contentWidth, + height() - contentTop - marginBottom()); +} + +Ui::BubbleRounding Message::countMessageRounding() const { + const auto smallTop = isBubbleAttachedToPrevious(); + const auto smallBottom = isBubbleAttachedToNext(); + const auto media = smallBottom ? nullptr : this->media(); + const auto rich = smallBottom ? nullptr : richpage(); + const auto item = data(); + const auto keyboard = item->inlineReplyKeyboard(); + const auto richMediaAtBottom = [&] { + using Block = Iv::Markdown::MarkdownArticleEdgeBlock; + const auto edges = countRichPageBubbleEdges(); + return edges.bottom + && !edges.infoBelow + && (rich->article.edgeBlocks().bottom == Block::VisualMedia); + }; + const auto skipTail = smallBottom + || (media && media->skipBubbleTail()) + || (rich && richMediaAtBottom()) + || (keyboard != nullptr) + || item->isFakeAboutView() + || isCommentsRootView(); + const auto right = hasRightLayout(); + using Corner = Ui::BubbleCornerRounding; + return Ui::BubbleRounding{ + .topLeft = (smallTop && !right) ? Corner::Small : Corner::Large, + .topRight = (smallTop && right) ? Corner::Small : Corner::Large, + .bottomLeft = ((smallBottom && !right) + ? Corner::Small + : (!skipTail && !right) + ? Corner::Tail + : Corner::Large), + .bottomRight = ((smallBottom && right) + ? Corner::Small + : (!skipTail && right) + ? Corner::Tail + : Corner::Large), + }; +} + +Ui::BubbleRounding Message::countBubbleRounding( + Ui::BubbleRounding messageRounding) const { + if ([[maybe_unused]] const auto _ = inlineReplyKeyboard()) { + messageRounding.bottomLeft + = messageRounding.bottomRight + = Ui::BubbleCornerRounding::Small; + } + return messageRounding; +} + +Ui::BubbleRounding Message::countBubbleRounding() const { + return countBubbleRounding(countMessageRounding()); +} + +int Message::resizeContentGetHeight(int newWidth) { + if (isHidden()) { + return marginTop() + marginBottom(); + } else if (newWidth < st::msgMinWidth) { + return height(); + } + + const auto item = data(); + const auto postShowingAuthor = item->isPostShowingAuthor() ? 1 : 0; + if (_postShowingAuthor != postShowingAuthor) { + _postShowingAuthor = postShowingAuthor; + _fromNameVersion = 0; + previousInBlocksChanged(); + + const auto size = _bottomInfo.currentSize(); + _bottomInfo.update(BottomInfoDataFromMessage(this), newWidth); + if (size != _bottomInfo.currentSize()) { + // maxWidth may have changed, full recount required. + setPendingResize(); + return resizeGetHeight(newWidth); + } + } + + const auto media = this->media(); + const auto mediaDisplayed = media ? media->isDisplayed() : false; + const auto bubble = drawBubble(); + + item->resolveDependent(); + + // This code duplicates countGeometry() but also resizes media. + const auto centeredView = item->isFakeAboutView() + || isCommentsRootView(); + const auto useMoreSpace = (delegate()->elementChatMode() + == ElementChatMode::Narrow); + const auto wideSkip = useMoreSpace + ? st::msgMargin.left() + : st::msgMargin.right(); + auto contentWidth = newWidth + - st::msgMargin.left() + - (centeredView ? st::msgMargin.left() : wideSkip); + if (hasFromPhoto()) { + if (const auto size = rightActionSize()) { + contentWidth -= size->width() + (st::msgPhotoSkip - st::historyFastShareSize); + } + } + accumulate_min(contentWidth, maxWidth()); + _bubbleWidthLimit = (UnlimitedMessageWidth.value() && !mediaDisplayed) + ? 0x3FFFFFF + : std::max({ + st::msgMaxWidth, + monospaceMaxWidth(), + mediaDisplayed ? media->bubbleWidthLimit() : 0, + }); + accumulate_min(contentWidth, int(_bubbleWidthLimit)); + const auto textualWidth = bubbleTextualWidth(); + if (mediaDisplayed) { + media->resizeGetHeight(contentWidth); + if (media->width() < contentWidth) { + if (media->width() < textualWidth + && !media->enforceBubbleWidth()) { + accumulate_min(contentWidth, textualWidth); + } else { + contentWidth = media->width(); + } + } + } + if (!mediaDisplayed && bubble && hasVisibleText()) { + const auto probeTextWidth = bubbleTextWidth(contentWidth); + [[maybe_unused]] const auto probe = textHeightFor(probeTextWidth); + if (hasRichPage() || !Get()) { + const auto use = textRealWidth(); + if (use > 0) { + const auto shrunk = std::max( + use + st::msgPadding.left() + st::msgPadding.right(), + int(_nonTextMaxWidth)); + accumulate_min(contentWidth, shrunk); + } + } + } + const auto bottomInfoWidth = std::max( + contentWidth - st::msgPadding.left() - st::msgPadding.right(), + 1); + const auto textWidth = bubble + ? bubbleTextWidth(contentWidth) + : bottomInfoWidth; + if (bubble && hasRichPage() && hasVisibleText()) { + [[maybe_unused]] const auto laidOut = textHeightFor(textWidth); + } + + auto appearing = Get(); + if (appearing) { + if (appearing->textWidth != textWidth) { + appearing->geometryValid = false; + appearing->textWidth = textWidth; + } + // This may invalidate composer structure: by removing TextAppearing, + // or by adding / removing rich page components inside validateText(). + // Either way the pointer must be taken again. + appearing = textAppearValidate() ? Get() : nullptr; + } + + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + const auto bottomInfoHeight = _bottomInfo.resizeGetHeight( + std::min( + _bottomInfo.maxWidth(), + bottomInfoWidth - 2 * st::msgDateDelta.x())); + + auto newHeight = minHeight(); + + if (const auto service = Get()) { + service->resizeToWidth(newWidth, delegate()->elementChatMode()); + } + + const auto botTop = item->isFakeAboutView() + ? Get() + : nullptr; + const auto ephemeralBadge = Get(); + if (bubble) { + auto reply = Get(); + auto via = item->Get(); + const auto check = factcheckBlock(); + const auto entry = logEntryOriginal(); + + // Entry page is always a bubble bottom. + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); + + if (reactionsInBubble) { + _reactions->resizeGetHeight(textWidth); + } + if (contentWidth == maxWidth() && !appearing) { + if (mediaDisplayed) { + newHeight += media->height() - media->minHeight(); + if (check) { + newHeight += check->resizeGetHeight(contentWidth) + st::mediaInBubbleSkip; + } + if (entry) { + newHeight += entry->resizeGetHeight(contentWidth); + } + } else { + if (check) { + check->resizeGetHeight(contentWidth); + } + if (entry) { + // In case of text-only message it is counted in minHeight already. + entry->resizeGetHeight(contentWidth); + } + } + } else { + const auto withVisibleText = hasVisibleText(); + newHeight = 0; + if (withVisibleText) { + if (botTop) { + newHeight += botTop->height; + } + if (appearing) { + newHeight += appearing->shownHeight; + } else { + newHeight += textHeightFor(textWidth); + } + } + if (!mediaOnBottom && (!_viewButton || !reactionsInBubble)) { + newHeight += st::msgPadding.bottom(); + if (mediaDisplayed) { + newHeight += st::mediaInBubbleSkip; + } + } + if (!mediaOnTop) { + newHeight += st::msgPadding.top(); + if (mediaDisplayed) newHeight += st::mediaInBubbleSkip; + if (entry) newHeight += st::mediaInBubbleSkip; + } + if (mediaDisplayed) { + newHeight += media->height(); + } + if (check) { + newHeight += check->resizeGetHeight(contentWidth) + st::mediaInBubbleSkip; + } + if (entry) { + newHeight += entry->resizeGetHeight(contentWidth); + } + if (reactionsInBubble) { + if (mediaDisplayed + && !media->additionalInfoString().isEmpty()) { + // In round videos in a web page status text is painted + // in the bottom left corner, reactions should be below. + newHeight += st::msgDateFont->height; + } else { + newHeight += st::mediaInBubbleSkip; + } + newHeight += _reactions->height(); + } + } + + if (displayFromName()) { + fromNameUpdated(contentWidth); + newHeight += st::msgNameFont->height; + } else if (via && !displayForwardedFrom()) { + via->resize(contentWidth - st::msgPadding.left() - st::msgPadding.right()); + newHeight += st::msgNameFont->height; + } + + if (ephemeralBadge) { + newHeight += ephemeralBadge->height; + } + + if (displayedTopicButton()) { + newHeight += st::topicButtonSkip + + st::topicButtonPadding.top() + + st::msgNameFont->height + + st::topicButtonPadding.bottom() + + st::topicButtonSkip; + } + + if (displayForwardedFrom()) { + const auto forwarded = item->Get(); + const auto skip1 = forwarded->psaType.isEmpty() + ? 0 + : st::historyPsaIconSkip1; + const auto fwdheight = ((forwarded->text.maxWidth() > (contentWidth - st::msgPadding.left() - st::msgPadding.right() - skip1)) ? 2 : 1) * st::semiboldFont->height; + newHeight += fwdheight; + } + + if (reply) { + newHeight += reply->resizeToWidth(contentWidth + - st::msgPadding.left() + - st::msgPadding.right()); + } + if (const auto summaryHeader = Get()) { + newHeight += summaryHeader->resizeToWidth(contentWidth + - st::msgPadding.left() + - st::msgPadding.right()); + } + if (needInfoDisplay()) { + newHeight += (bottomInfoHeight - st::msgDateFont->height); + } + + if (hasCommentsButton()) { + newHeight += st::historyCommentsButtonHeight; + } else if (_comments) { + _comments = nullptr; + checkHeavyPart(); + } + newHeight += viewButtonHeight(); + } else if (mediaDisplayed) { + newHeight = media->height(); + } else { + newHeight = 0; + } + if (_reactions && !reactionsInBubble) { + const auto reactionsWidth = (!bubble && mediaDisplayed) + ? media->contentRectForReactions().width() + : contentWidth; + newHeight += st::mediaInBubbleSkip + + _reactions->resizeGetHeight(reactionsWidth); + if (hasRightLayout()) { + _reactions->flipToRight(); + } + } + + if (const auto keyboard = inlineReplyKeyboard()) { + const auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight(); + newHeight += keyboardHeight; + keyboard->resize(contentWidth, keyboardHeight - st::msgBotKbButton.margin); + } + + newHeight += marginTop() + marginBottom(); + return newHeight; +} + +void Message::invalidateTextDependentCache() { + _bubbleTextualWidthMinimum = -1; + _bubbleTextualWidthCache = 0; + if (const auto rich = richpage()) { + rich->textualWidth = 0; + rich->demandedTextWidth = 0; + } +} + +bool Message::textAppearValidate() { + while (true) { + if (!textAppearCheckLine()) { + return false; + } + // textAppearCheckLine() may have reallocated the composer block. + const auto appearing = Get(); + if (!appearing) { + return false; + } else if (!appearing->use + || appearing->widthAnimation.animating() + || appearing->heightAnimation.animating()) { + return true; + } + ++appearing->shownLine; + appearing->revealedLineWidth = 0; + appearing->startLineWidth = 0; + appearing->targetLineWidth = 0; + } +} + +bool Message::textAppearCheckLine() { + auto appearing = Get(); + if (!appearing) { + return false; + } + const auto recount = !appearing->geometryValid; + if (recount) { + appearing->geometryValid = true; + + // validateText() can add or remove runtime components, which + // reallocates the composer data block and frees the old one, so + // every TextAppearing pointer taken before it is dangling after. + validateText(); + appearing = Get(); + if (!appearing) { + return false; + } + if (const auto rich = richpage()) { + const auto articleWidth = richPageWidthFor(appearing->textWidth); + appearing->lines = rich->article.countRevealLinesGeometry( + articleWidth); + if (appearing->lines.empty()) { + // textHeightFor() calls validateText() as well. + const auto height = textHeightFor(appearing->textWidth); + const auto width = std::max(textRealWidth(), 1); + appearing = Get(); + if (!appearing) { + return false; + } + if (height > 0) { + appearing->lines.push_back({ + .left = 0, + .width = width, + .bottom = height, + .rtl = false, + .baseline = height, + }); + } + } else { + // Reveal heights are text-area based, article lines are not. + const auto skips = rich->edgeSkips(); + for (auto &line : appearing->lines) { + line.bottom += skips.top(); + } + if (rich->article.hasSkipBlock()) { + MergeTrailingSkipOnlyLine( + appearing->lines, + skipBlockWidth(), + skipBlockHeight()); + } + auto &last = appearing->lines.back(); + last.bottom = std::max( + last.bottom, + (skips.top() + + rich->article.resizeGetHeight(articleWidth) + + skips.bottom())); + } + } else { + appearing->lines = text().countLinesGeometry( + appearing->textWidth); + if (text().hasSkipBlock()) { + MergeTrailingSkipOnlyLine( + appearing->lines, + skipBlockWidth(), + skipBlockHeight()); + } + } + } + const auto lines = int(appearing->lines.size()); + const auto shown = appearing->shownLine; + const auto line = (shown < lines) ? &appearing->lines[shown] : nullptr; + const auto finalLineHeight = line + ? (richPageInfoRow() && (shown + 1 == lines) + ? line->bottom + skipBlockHeight() + : line->bottom) + : 0; + const auto use = appearing->use = !anim::Disabled() + && line + && ((shown + 1 < lines) + || (shown + 1 == lines + && ((appearing->revealedLineWidth < line->width) + || (appearing->shownHeight < finalLineHeight)))); + if (!use) { + if (data()->isRegular()) { + // We are inside these animations' tick, can't destroy them now. + crl::on_main(this, [=] { + if (Has() && !Get()->use) { + RemoveComponents(TextAppearing::Bit()); + } + }); + return false; + } else if (recount && lines) { + appearing->shownLine = lines - 1; + const auto &line = appearing->lines.back(); + appearing->revealedLineWidth = line.width; + appearing->shownWidth = textRealWidth(); + appearing->shownHeight = line.bottom + + (richPageInfoRow() ? skipBlockHeight() : 0); + appearing->widthAnimation.stop(); + appearing->heightAnimation.stop(); + } + return true; + } + if (appearing->targetLineWidth != line->width) { + if (appearing->revealedLineWidth >= line->width) { + appearing->widthAnimation.stop(); + appearing->revealedLineWidth + = appearing->targetLineWidth + = line->width; + } else { + textAppearStartWidthAnimation(appearing); + } + } + const auto targetHeight = textAppearTargetHeight(appearing); + if (appearing->targetHeight != targetHeight) { + if (!shown) { + appearing->shownHeight = appearing->lines.front().bottom + + (appearing->lines.size() > 1 ? skipBlockHeight() : 0); + } + if (appearing->shownHeight >= targetHeight) { + appearing->heightAnimation.stop(); + appearing->shownHeight = appearing->targetHeight = targetHeight; + } else { + const auto widthStart = appearing->startLineWidth; + const auto widthTarget = appearing->targetLineWidth; + const auto width = appearing->revealedLineWidth; + const auto progress = (width >= widthTarget) + ? 1. + : (width - widthStart) / float64(widthTarget - widthStart); + const auto left = (1. - progress) * appearing->widthDuration; + const auto duration = appearing->finalizing + ? kLineHeightAppearFinalDuration + : kLineHeightAppearDuration; + if (appearing->heightAnimation.animating() + || !appearing->widthAnimation.animating() + || left <= duration) { + textAppearStartHeightAnimation(appearing, targetHeight); + } + } + } + return true; +} + +void Message::textAppearStartWidthAnimation( + not_null appearing) { + Expects(appearing->use); + + const auto shown = appearing->shownLine; + const auto lines = int(appearing->lines.size()); + const auto lineWidth = appearing->lines[shown].width; + const auto lineDuration = appearing->finalizing + ? kFullLineAppearFinalDuration + : kFullLineAppearDuration; + const auto computed = (shown + 1 == lines) + ? lineDuration + : std::max( + lineDuration * lineWidth / st::msgMaxWidth, + crl::time(10)); + const auto duration = std::exchange(appearing->startedForText, true) + ? computed + : std::max(computed, kMinWidthAppearDuration); + appearing->widthDuration = duration; + const auto from + = appearing->startLineWidth + = appearing->revealedLineWidth; + const auto to = appearing->targetLineWidth = lineWidth; + + Assert(from < to); + appearing->widthAnimation.start([=] { + textAppearWidthCallback(); + }, from, to, duration, anim::linear); +} + +void Message::textAppearStartHeightAnimation( + not_null appearing, + int targetHeight) { + Expects(appearing->use); + + const auto from = appearing->shownHeight; + const auto to = appearing->targetHeight = targetHeight; + const auto duration = appearing->finalizing + ? kLineHeightAppearFinalDuration + : kLineHeightAppearDuration; + appearing->heightAnimation.start([=] { + textAppearHeightCallback(); + }, from, to, duration, anim::easeOutCubic); +} + +int Message::textAppearTargetHeight( + not_null appearing) const { + const auto next = appearing->shownLine + 1; + const auto lines = int(appearing->lines.size()); + if (next + 1 >= lines) { + return appearing->lines.back().bottom + + (richPageInfoRow() ? skipBlockHeight() : 0); + } + const auto &line = appearing->lines[next]; + const auto bottom = line.bottom; + const auto rich = hasRichPage(); + const auto nextWidth = rich ? RevealLineRight(line) : line.width; + const auto available = std::max( + (rich + ? RevealLineRight(appearing->lines[appearing->shownLine]) + : appearing->lines[appearing->shownLine].width), + appearing->shownWidth); + if (nextWidth + skipBlockWidth() <= available && !line.rtl) { + return bottom; + } + return bottom + skipBlockHeight(); +} + +void Message::textAppearWidthCallback() { + const auto appearing = Get(); + if (!appearing) { + return; + } + const auto now = int(base::SafeRound( + appearing->widthAnimation.value(appearing->targetLineWidth))); + if (now != appearing->revealedLineWidth) { + appearing->revealedLineWidth = now; + const auto &line = appearing->lines[appearing->shownLine]; + if (line.rtl) { + appearing->shownWidth = textRealWidth(); + } else { + const auto shownWidth = hasRichPage() + ? line.left + now + skipBlockWidth() + : now + skipBlockWidth(); + appearing->shownWidth = std::min( + std::max( + appearing->shownWidth, + shownWidth), + textRealWidth()); + } + repaint(); + } + textAppearValidate(); +} + +void Message::textAppearHeightCallback() { + const auto appearing = Get(); + if (!appearing) { + return; + } + const auto now = int(base::SafeRound( + appearing->heightAnimation.value(appearing->targetHeight))); + if (const auto delta = now - appearing->shownHeight) { + appearing->shownHeight = now; + adjustHeight(delta); + history()->viewHeightAdjusted(this, delta); + repaint(); + } + textAppearValidate(); +} + +int Message::bottomInfoHeight() const { + return hidesBottomInfo() ? 0 : _bottomInfo.height(); +} + +bool Message::usesMessageInfoLayout() const { + const auto media = this->media(); + const auto mediaDisplayed = media ? media->isDisplayed() : false; + const auto check = factcheckBlock(); + const auto entry = logEntryOriginal(); + return entry + ? !entry->customInfoLayout() + : check + ? !check->customInfoLayout() + : ((mediaDisplayed && media->isBubbleBottom()) + ? !media->customInfoLayout() + : true); +} + +bool Message::needInfoDisplay() const { + return !hidesBottomInfo() && usesMessageInfoLayout(); +} + +bool Message::invertMedia() const { + return _invertMedia; +} + +bool Message::hasVisibleText() const { + const auto textItem = this->textItem(); + if (!textItem) { + return false; + } + const auto media = this->media(); + if (hasRichPage()) { + return !media || !media->hideMessageText(); + } else if (textItem->emptyText()) { + if (const auto media = textItem->media()) { + return media->storyExpired() || media->storyUnsupported(); + } + return false; + } + return !media || !media->hideMessageText(); +} + +int Message::visibleTextLength() const { + return hasVisibleText() ? text().length() : 0; +} + +int Message::visibleMediaTextLength() const { + const auto media = this->media(); + return (media && media->isDisplayed()) + ? media->fullSelectionLength() + : 0; +} + +QSize Message::performCountCurrentSize(int newWidth) { + const auto newHeight = resizeContentGetHeight(newWidth); + + return { newWidth, newHeight }; +} + +void Message::refreshInfoSkipBlock(HistoryItem *textItem) { + const auto media = this->media(); + const auto hasTextSkipBlock = [&] { + if (!textItem || textItem->_text.empty()) { + if (const auto media = data()->media()) { + return media->storyExpired() || media->storyUnsupported(); + } + return false; + } else if (factcheckBlock() + || data()->Has()) { + return false; + } else if (media && media->isDisplayed() && !_invertMedia) { + return false; + } else if (_reactions) { + return false; + } + return true; + }(); + const auto skipWidth = skipBlockWidth(); + const auto skipHeight = skipBlockHeight(); + if (_reactions) { + if (needInfoDisplay()) { + _reactions->updateSkipBlock(skipWidth, skipHeight); + } else { + _reactions->removeSkipBlock(); + } + } + const auto has = !hidesBottomInfo() && hasTextSkipBlock; + validateTextSkipBlock(has, skipWidth, skipHeight); + if (const auto rich = richpage()) { + // The view button is placed above the info, so the info keeps its row. + const auto changed = (has && !_viewButton) + ? rich->article.updateSkipBlock(skipWidth, skipHeight) + : rich->article.removeSkipBlock(); + if (changed) { + invalidateTextSizeCache(); + } + } +} + +TimeId Message::displayedEditDate() const { + const auto item = data(); + const auto overrided = media() && media()->overrideEditedDate(); + if (item->hideEditedBadge() && !overrided) { + return TimeId(0); + } else if (const auto edited = displayedEditBadge()) { + return edited->date; + } + return TimeId(0); +} + +HistoryMessageEdited *Message::displayedEditBadge() { + if (const auto media = this->media()) { + if (media->overrideEditedDate()) { + return media->displayedEditBadge(); + } + } + return data()->Get(); +} + +const HistoryMessageEdited *Message::displayedEditBadge() const { + if (const auto media = this->media()) { + if (media->overrideEditedDate()) { + return media->displayedEditBadge(); + } + } + return data()->Get(); +} + +void Message::ensureSummarizeButton() const { + if (data()->canBeSummarized() + && (context() != Context::MediaEditor) + /*&& item->originalText().text.size() >= kSummarizeThreshold*/) { + if (!_summarize) { + _summarize + = std::make_unique(data(), false, true); + } + } else { + _summarize = nullptr; + } +} + +void Message::paintSummarize( + Painter &p, + int x, + int y, + bool right, + const PaintContext &context, + QRect g) const { + if (!_summarize) { + return; + } + const auto s = _summarize->size(); + const auto bottomY = y - s.height() - st::msgDateImgDelta; + if (bottomY < g.top()) { + return; + } + const auto topY = g.top(); + const auto buttonY = std::min( + std::max(topY, context.area.y() + st::msgDateImgDelta), + bottomY); + _summarize->paint( + p, + x - (right ? 0 : s.width()), + buttonY, + context); +} + +} // namespace HistoryView