/* 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 "core/click_handler_types.h" // ClickHandlerContext #include "core/ui_integration.h" #include "history/view/history_view_cursor_state.h" #include "history/history_item.h" #include "history/history_item_components.h" #include "history/history_item_helpers.h" #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_web_page.h" #include "history/view/reactions/history_view_reactions.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/history_view_group_call_bar.h" // UserpicInRow. #include "history/view/history_view_view_button.h" // ViewButton. #include "history/history.h" #include "boxes/share_box.h" #include "ui/effects/glare.h" #include "ui/effects/ripple_animation.h" #include "ui/effects/reaction_fly_animation.h" #include "ui/chat/message_bubble.h" #include "ui/chat/chat_style.h" #include "ui/rect.h" #include "ui/text/text_utilities.h" #include "ui/text/text_entity.h" #include "ui/cached_round_corners.h" #include "ui/power_saving.h" #include "base/unixtime.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" #include "data/data_message_reactions.h" #include "data/data_sponsored_messages.h" #include "lang/lang_keys.h" #include "mainwidget.h" #include "main/main_session.h" #include "ui/text/text_options.h" #include "ui/painter.h" #include "window/window_session_controller.h" #include "apiwrap.h" #include "styles/style_widgets.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" namespace HistoryView { namespace { constexpr auto kPlayStatusLimit = 2; const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_"; [[nodiscard]] std::optional ExtractController( const ClickContext &context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { return controller; } return std::nullopt; } class KeyboardStyle : public ReplyKeyboard::Style { public: KeyboardStyle(const style::BotKeyboardButton &st); Images::CornersMaskRef buttonRounding( Ui::BubbleRounding outer, RectParts sides) const override; void startPaint( QPainter &p, const Ui::ChatStyle *st) const override; const style::TextStyle &textStyle() const override; void repaint(not_null item) const override; protected: void paintButtonBg( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, Ui::BubbleRounding rounding, float64 howMuchOver) const override; void paintButtonIcon( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, int outerWidth, HistoryMessageMarkupButton::Type type) const override; void paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, int outerWidth, Ui::BubbleRounding rounding) const override; int minButtonWidth(HistoryMessageMarkupButton::Type type) const override; private: using BubbleRoundingKey = uchar; mutable base::flat_map _cachedBg; mutable base::flat_map _cachedOutline; mutable std::unique_ptr _glare; rpl::lifetime _lifetime; }; KeyboardStyle::KeyboardStyle(const style::BotKeyboardButton &st) : ReplyKeyboard::Style(st) { style::PaletteChanged( ) | rpl::start_with_next([=] { _cachedBg = {}; _cachedOutline = {}; }, _lifetime); } void KeyboardStyle::startPaint( QPainter &p, const Ui::ChatStyle *st) const { Expects(st != nullptr); p.setPen(st->msgServiceFg()); } const style::TextStyle &KeyboardStyle::textStyle() const { return st::serviceTextStyle; } void KeyboardStyle::repaint(not_null item) const { item->history()->owner().requestItemRepaint(item); if (_glare && !_glare->glare.birthTime) { constexpr auto kTimeout = crl::time(0); constexpr auto kDuration = crl::time(1100); _glare->validate( st::premiumButtonFg->c, [=] { repaint(item); }, kTimeout, kDuration); } } Images::CornersMaskRef KeyboardStyle::buttonRounding( Ui::BubbleRounding outer, RectParts sides) const { using namespace Images; using namespace Ui; using Radius = CachedCornerRadius; using Corner = BubbleCornerRounding; auto result = CornersMaskRef(CachedCornersMasks(Radius::BubbleSmall)); if (sides & RectPart::Bottom) { const auto &large = CachedCornersMasks(Radius::BubbleLarge); auto round = [&](RectPart side, int index) { if ((sides & side) && (outer[index] == Corner::Large)) { result.p[index] = &large[index]; } }; round(RectPart::Left, kBottomLeft); round(RectPart::Right, kBottomRight); } return result; } void KeyboardStyle::paintButtonBg( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, Ui::BubbleRounding rounding, float64 howMuchOver) const { Expects(st != nullptr); using Corner = Ui::BubbleCornerRounding; auto &cachedBg = _cachedBg[rounding.key()]; if (cachedBg.isNull() || cachedBg.width() != (rect.width() * style::DevicePixelRatio())) { cachedBg = QImage( rect.size() * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied); cachedBg.setDevicePixelRatio(style::DevicePixelRatio()); cachedBg.fill(Qt::transparent); { auto painter = QPainter(&cachedBg); const auto sti = &st->imageStyle(false); const auto &small = sti->msgServiceBgCornersSmall; const auto &large = sti->msgServiceBgCornersLarge; auto corners = Ui::CornersPixmaps(); int radiuses[4]; for (auto i = 0; i != 4; ++i) { const auto isLarge = (rounding[i] == Corner::Large); corners.p[i] = (isLarge ? large : small).p[i]; radiuses[i] = Ui::CachedCornerRadiusValue(isLarge ? Ui::CachedCornerRadius::BubbleLarge : Ui::CachedCornerRadius::BubbleSmall); } const auto r = Rect(rect.size()); _cachedOutline[rounding.key()] = Ui::ComplexRoundedRectPath( r - Margins(st::lineWidth), radiuses[0], radiuses[1], radiuses[2], radiuses[3]); Ui::FillRoundRect(painter, r, sti->msgServiceBg, corners); } } p.drawImage(rect.topLeft(), cachedBg); if (howMuchOver > 0) { auto o = p.opacity(); p.setOpacity(o * howMuchOver); const auto &small = st->msgBotKbOverBgAddCornersSmall(); const auto &large = st->msgBotKbOverBgAddCornersLarge(); auto over = Ui::CornersPixmaps(); for (auto i = 0; i != 4; ++i) { over.p[i] = (rounding[i] == Corner::Large ? large : small).p[i]; } Ui::FillRoundRect(p, rect, st->msgBotKbOverBgAdd(), over); p.setOpacity(o); } } void KeyboardStyle::paintButtonIcon( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, int outerWidth, HistoryMessageMarkupButton::Type type) const { Expects(st != nullptr); using Type = HistoryMessageMarkupButton::Type; const auto icon = [&]() -> const style::icon* { switch (type) { case Type::Url: case Type::Auth: return &st->msgBotKbUrlIcon(); case Type::Buy: return &st->msgBotKbPaymentIcon(); case Type::SwitchInlineSame: case Type::SwitchInline: return &st->msgBotKbSwitchPmIcon(); case Type::WebView: case Type::SimpleWebView: return &st->msgBotKbWebviewIcon(); } return nullptr; }(); if (icon) { icon->paint(p, rect.x() + rect.width() - icon->width() - st::msgBotKbIconPadding, rect.y() + st::msgBotKbIconPadding, outerWidth); } } void KeyboardStyle::paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, int outerWidth, Ui::BubbleRounding rounding) const { Expects(st != nullptr); if (anim::Disabled()) { const auto &icon = st->historySendingInvertedIcon(); icon.paint( p, rect::right(rect) - icon.width() - st::msgBotKbIconPadding, rect::bottom(rect) - icon.height() - st::msgBotKbIconPadding, rect.x() * 2 + rect.width()); return; } const auto cacheKey = rounding.key(); auto &cachedBg = _cachedBg[cacheKey]; if (!cachedBg.isNull()) { if (_glare && _glare->glare.birthTime) { const auto progress = _glare->progress(crl::now()); const auto w = _glare->width; const auto h = rect.height(); const auto x = (-w) + (w * 2) * progress; auto frame = cachedBg; frame.fill(Qt::transparent); { auto painter = QPainter(&frame); auto hq = PainterHighQualityEnabler(painter); painter.setPen(Qt::NoPen); painter.drawTiledPixmap(x, 0, w, h, _glare->pixmap, 0, 0); auto path = QPainterPath(); path.addRect(Rect(rect.size())); path -= _cachedOutline[cacheKey]; constexpr auto kBgOutlineAlpha = 0.5; constexpr auto kFgOutlineAlpha = 0.8; const auto &c = st::premiumButtonFg->c; painter.setPen(Qt::NoPen); painter.setBrush(c); painter.setOpacity(kBgOutlineAlpha); painter.drawPath(path); auto gradient = QLinearGradient(-w, 0, w * 2, 0); { constexpr auto kShiftLeft = 0.01; constexpr auto kShiftRight = 0.99; auto stops = _glare->computeGradient(c).stops(); stops[1] = { std::clamp(progress, kShiftLeft, kShiftRight), QColor(c.red(), c.green(), c.blue(), kFgOutlineAlpha), }; gradient.setStops(std::move(stops)); } painter.setBrush(QBrush(gradient)); painter.setOpacity(1); painter.drawPath(path); painter.setCompositionMode( QPainter::CompositionMode_DestinationIn); painter.drawImage(0, 0, cachedBg); } p.drawImage(rect.x(), rect.y(), frame); } else { _glare = std::make_unique(); _glare->width = outerWidth; } } } int KeyboardStyle::minButtonWidth( HistoryMessageMarkupButton::Type type) const { using Type = HistoryMessageMarkupButton::Type; int result = 2 * buttonPadding(), iconWidth = 0; switch (type) { case Type::Url: case Type::Auth: iconWidth = st::msgBotKbUrlIcon.width(); break; case Type::Buy: iconWidth = st::msgBotKbPaymentIcon.width(); break; case Type::SwitchInlineSame: case Type::SwitchInline: iconWidth = st::msgBotKbSwitchPmIcon.width(); break; case Type::Callback: case Type::CallbackWithPassword: case Type::Game: iconWidth = st::historySendingInvertedIcon.width(); break; case Type::WebView: case Type::SimpleWebView: iconWidth = st::msgBotKbWebviewIcon.width(); break; } if (iconWidth > 0) { result = std::max(result, 2 * iconWidth + 4 * int(st::msgBotKbIconPadding)); } return result; } QString FastReplyText() { return tr::lng_fast_reply(tr::now); } [[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); } } }); } } // namespace struct Message::CommentsButton { std::unique_ptr ripple; std::vector userpics; QImage cachedUserpics; ClickHandlerPtr link; QPoint lastPoint; int rippleShift = 0; }; struct Message::FromNameStatus { DocumentId id = 0; std::unique_ptr custom; int skip = 0; }; struct Message::RightAction { std::unique_ptr ripple; ClickHandlerPtr link; QPoint lastPoint; }; 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)) , _invertMedia(data->invertMedia() && !data->emptyText()) , _bottomInfo( &data->history()->owner().reactions(), BottomInfoDataFromMessage(this)) { initLogEntryOriginal(); initPsa(); refreshReactions(); auto animations = replacing ? replacing->takeReactionAnimations() : base::flat_map< Data::ReactionId, std::unique_ptr>(); if (!animations.empty()) { const auto repainter = [=] { repaint(); }; for (const auto &[id, animation] : animations) { animation->setRepaintCallback(repainter); } if (_reactions) { _reactions->continueAnimations(std::move(animations)); } else { _bottomInfo.continueReactionAnimations(std::move(animations)); } } } Message::~Message() { if (_comments || (_fromNameStatus && _fromNameStatus->custom)) { _comments = nullptr; _fromNameStatus = nullptr; checkHeavyPart(); } } void Message::refreshRightBadge() { const auto text = [&] { if (data()->isDiscussionPost()) { return (delegate()->elementContext() == Context::Replies) ? QString() : tr::lng_channel_badge(tr::now); } else if (data()->author()->isMegagroup()) { if (const auto msgsigned = data()->Get()) { Assert(msgsigned->isAnonymousRank); return msgsigned->postAuthor; } } const auto channel = data()->history()->peer->asMegagroup(); const auto user = data()->author()->asUser(); if (!channel || !user) { return QString(); } const auto info = channel->mgInfo.get(); const auto i = info->admins.find(peerToUser(user->id)); const auto custom = (i != info->admins.end()) ? i->second : (info->creator == user) ? info->creatorRank : QString(); return !custom.isEmpty() ? custom : (info->creator == user) ? tr::lng_owner_badge(tr::now) : (i != info->admins.end()) ? tr::lng_admin_badge(tr::now) : QString(); }(); const auto badge = text.isEmpty() ? delegate()->elementAuthorRank(this) : TextUtilities::RemoveEmoji(TextUtilities::SingleLine(text)); if (badge.isEmpty()) { _rightBadge.clear(); } else { _rightBadge.setText(st::defaultTextStyle, badge); } } 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 item = data(); 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(); const auto keyboard = item->inlineReplyKeyboard(); auto keyboardHeight = 0; if (keyboard) { keyboardHeight = keyboard->naturalHeight(); g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); } 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 animateInBottomInfo = [&](QPoint bottomRight) { _bottomInfo.animateReaction(args.translated(-bottomRight), repainter); }; if (bubble) { auto entry = logEntryOriginal(); // Entry page is always a bubble bottom. auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (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 (reactionsInBubble) { trect.setHeight(trect.height() - reactionsHeight); const auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + reactionsTop); _reactions->animate(args.translated(-reactionsPosition), repainter); return; } if (_viewButton) { const auto belowInfo = _viewButton->belowMessageInfo(); const auto infoHeight = reactionsInBubble ? (reactionsHeight + 2 * st::mediaInBubbleSkip) : _bottomInfo.height(); 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); animateInBottomInfo(QPoint(mediaLeft, mediaTop) + media->resolveCustomInfoRightBottom()); } else { animateInBottomInfo({ inner.left() + inner.width() - (st::msgPadding.right() - st::msgDateDelta.x()), inner.top() + inner.height() - (st::msgPadding.bottom() - st::msgDateDelta.y()), }); } } else if (mediaDisplayed) { animateInBottomInfo(g.topLeft() + media->resolveCustomInfoRightBottom()); } } auto Message::takeReactionAnimations() -> base::flat_map< Data::ReactionId, std::unique_ptr> { return _reactions ? _reactions->takeAnimations() : _bottomInfo.takeReactionAnimations(); } QSize Message::performCountOptimalSize() { const auto item = data(); const auto markup = item->inlineReplyMarkup(); const auto reactionsKey = [&] { return embedReactionsInBottomInfo() ? 0 : embedReactionsInBubble() ? 1 : 2; }; const auto oldKey = reactionsKey(); refreshIsTopicRootReply(); validateText(); validateInlineKeyboard(markup); updateViewButtonExistence(); refreshTopicButton(); updateMediaInBubbleState(); if (oldKey != reactionsKey()) { refreshReactions(); } refreshRightBadge(); refreshInfoSkipBlock(); const auto media = this->media(); const auto botTop = item->isFakeBotAbout() ? Get() : nullptr; if (botTop) { botTop->init(); } auto maxWidth = 0; auto minHeight = 0; const auto reactionsInBubble = _reactions && embedReactionsInBubble(); if (_reactions) { _reactions->initDimensions(); } if (drawBubble()) { const auto forwarded = item->Get(); const auto reply = displayedReply(); const auto via = item->Get(); const auto entry = logEntryOriginal(); if (forwarded) { forwarded->create(via); } if (reply) { reply->updateName(item); } auto mediaDisplayed = false; if (media) { mediaDisplayed = media->isDisplayed(); media->initDimensions(); } if (entry) { entry->initDimensions(); } // Entry page is always a bubble bottom. const auto withVisibleText = hasVisibleText(); auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); maxWidth = plainMaxWidth(); if (context() == Context::Replies && item->isDiscussionPost()) { maxWidth = std::max(maxWidth, st::msgMaxWidth); } minHeight = withVisibleText ? text().minHeight() : 0; if (reactionsInBubble) { const auto reactionsMaxWidth = st::msgPadding.left() + _reactions->maxWidth() + st::msgPadding.right(); accumulate_max( maxWidth, std::min(st::msgMaxWidth, reactionsMaxWidth)); if (!mediaDisplayed || _viewButton) { 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 (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 (withVisibleText) { if (botTop) { minHeight += botTop->height; } if (maxWidth < plainMaxWidth()) { minHeight -= text().minHeight(); minHeight += text().countHeight(innerWidth); } } 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 = item->displayFrom(); validateFromNameText(from); const auto &name = from ? _fromName : item->hiddenSenderInfo()->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); } const auto replyWidth = hasFastReply() ? st::msgFont->width(FastReplyText()) : 0; if (!_rightBadge.isEmpty()) { const auto badgeWidth = _rightBadge.maxWidth(); namew += st::msgPadding.right() + std::max(badgeWidth, replyWidth); } else if (replyWidth) { namew += st::msgPadding.right() + replyWidth; } accumulate_max(maxWidth, namew); } else if (via && !displayForwardedFrom()) { accumulate_max(maxWidth, st::msgPadding.left() + via->maxWidth + st::msgPadding.right()); } if (displayedTopicButton()) { const auto padding = st::msgPadding + st::topicButtonPadding; accumulate_max( maxWidth, (padding.left() + _topicButton->name.maxWidth() + st::topicButtonArrowSkip + padding.right())); } 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); } if (reply) { auto replyw = st::msgPadding.left() + reply->maxWidth() + st::msgPadding.right(); if (reply->originalVia) { replyw += st::msgServiceFont->spacew + reply->originalVia->maxWidth; } accumulate_max(maxWidth, replyw); } if (entry) { accumulate_max(maxWidth, entry->maxWidth()); minHeight += entry->minHeight(); } if (withVisibleText && botTop) { accumulate_max(maxWidth, botTop->maxWidth); minHeight += botTop->height; } } accumulate_max(maxWidth, minWidthForMedia()); } 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 if (markup && markup->inlineKeyboard && hasVisibleText()) { accumulate_max(maxWidth, markup->inlineKeyboard->naturalWidth()); } return QSize(maxWidth, minHeight); } void Message::refreshTopicButton() { const auto item = data(); if (isAttachedToPrevious() || context() != Context::History) { _topicButton = nullptr; } else if (const auto topic = item->topic()) { 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::MarkedTextContext{ .session = &history()->session(), .customEmojiRepaint = [=] { 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 service = Get()) { result += service->height; } return result; } int Message::marginBottom() const { return isHidden() ? 0 : st::msgMargin.bottom(); } 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 stm = context.messageStyle(); const auto bubble = drawBubble(); if (const auto bar = Get()) { auto unreadbarh = bar->height(); auto dateh = 0; if (const auto date = Get()) { dateh = date->height(); } if (context.clip.intersects(QRect(0, dateh, width(), unreadbarh))) { p.translate(0, dateh); bar->paint( p, context, 0, width(), delegate()->elementIsChatWide()); p.translate(0, -dateh); } } if (const auto service = Get()) { service->paint(p, context, g, delegate()->elementIsChatWide()); } if (isHidden()) { return; } auto entry = logEntryOriginal(); auto mediaDisplayed = media && media->isDisplayed(); // Entry page is always a bubble bottom. auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); const auto displayInfo = needInfoDisplay(); const auto reactionsInBubble = _reactions && embedReactionsInBubble(); 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 = g.top() + g.height(); if (data()->repliesAreComments() || data()->externalReply()) { 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 (entry) { localMediaBottom -= entry->height(); } localMediaTop = localMediaBottom - media->height(); for (auto &[top, height] : mediaSelectionIntervals) { top += localMediaTop; } } if (customHighlight) { media->drawHighlight(p, context, localMediaTop); } else { paintHighlight(p, context, g.height()); } 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); const auto keyboard = item->inlineReplyKeyboard(); 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)); p.translate(-keyboardPosition); } 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); } if (context.highlightPathCache) { context.highlightInterpolateTo = g; context.highlightPathCache->clear(); } if (bubble) { if (displayFromName() && item->displayFrom() && (_fromNameVersion < item->displayFrom()->nameVersion())) { fromNameUpdated(g.width()); } Ui::PaintBubble( p, Ui::ComplexBubble{ .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), }, .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) : _bottomInfo.height(); 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); paintTopicButton(p, trect, context); paintForwardedInfo(p, trect, context); paintReplyInfo(p, trect, context); paintViaBotIdInfo(p, trect, context); } if (entry) { trect.setHeight(trect.height() - entry->height()); } 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); 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); } auto copy = context; copy.selection = textSelection; copy.highlight.range = highlightRange; paintText(p, trect, copy); 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 (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); } } if (const auto size = rightActionSize()) { const auto fastShareSkip = std::clamp( (g.height() - size->height()) / 2, 0, st::historyFastShareBottom); const auto fastShareLeft = g.left() + g.width() + st::historyFastShareLeft; const auto fastShareTop = g.top() + g.height() - fastShareSkip - size->height(); drawRightAction(p, context, fastShareLeft, fastShareTop, width()); } if (media) { media->paintBubbleFireworks(p, g, context.now); } } else if (media && media->isDisplayed()) { 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 = displayedReply()) { if (reply->isNameUpdated(data())) { const_cast(this)->setPendingResize(); } } } void Message::paintCommentsButton( Painter &p, QRect &g, const PaintContext &context) const { if (!data()->repliesAreComments() && !data()->externalReply()) { 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 = _rightBadge.isEmpty() ? 0 : _rightBadge.maxWidth(); const auto replyWidth = [&] { if (isUnderCursor() && displayFastReply()) { return st::msgFont->width(FastReplyText()); } return 0; }(); const auto rightWidth = replyWidth ? replyWidth : badgeWidth; auto availableLeft = trect.left(); auto availableWidth = trect.width(); if (rightWidth) { availableWidth -= st::msgPadding.right() + rightWidth; } const auto stm = context.messageStyle(); const auto from = item->displayFrom(); const auto info = from ? nullptr : item->hiddenSenderInfo(); Assert(from || info); const auto nameFg = !context.outbg ? FromNameFg(context, colorIndex()) : stm->msgServiceFg->c; const auto nameText = [&] { if (from) { validateFromNameText(from); return static_cast(&_fromName); } return &info->nameText(); }(); const auto statusWidth = _fromNameStatus ? st::dialogsPremiumIcon.icon.width() : 0; if (statusWidth && availableWidth > statusWidth) { const auto x = availableLeft + std::min(availableWidth - statusWidth, nameText->maxWidth()); const auto y = trect.top(); auto color = nameFg; color.setAlpha(115); const auto user = from->asUser(); const auto id = user ? user->emojiStatusId() : 0; if (_fromNameStatus->id != id) { const auto that = const_cast(this); _fromNameStatus->custom = id ? std::make_unique( user->owner().customEmojiManager().create( 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); } availableWidth -= statusWidth; } p.setFont(st::msgNameFont); p.setPen(nameFg); nameText->drawElided(p, availableLeft, trect.top(), availableWidth); const auto skipWidth = nameText->maxWidth() + (_fromNameStatus ? (st::dialogsPremiumIcon.icon.width() + st::msgServiceFont->spacew) : 0) + st::msgServiceFont->spacew; availableLeft += skipWidth; availableWidth -= skipWidth; auto via = item->Get(); if (via && !displayForwardedFrom() && availableWidth > 0) { p.setPen(stm->msgServiceFg); p.drawText(availableLeft, trect.top() + st::msgServiceFont->ascent, via->text); auto skipWidth = via->width + st::msgServiceFont->spacew; availableLeft += skipWidth; availableWidth -= skipWidth; } if (rightWidth) { p.setPen(stm->msgDateFg); p.setFont(ClickHandler::showAsActive(_fastReplyLink) ? st::msgFont->underline() : st::msgFont); if (replyWidth) { p.drawText( trect.left() + trect.width() - rightWidth, trect.top() + st::msgFont->ascent, FastReplyText()); } else { _rightBadge.draw( p, trect.left() + trect.width() - rightWidth, trect.top(), rightWidth); } } trect.setY(trect.y() + st::msgNameFont->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); p.setTextPalette(!forwarded->psaType.isEmpty() ? st->historyPsaForwardPalette() : stm->fwdTextPalette); forwarded->text.drawElided(p, trect.x(), trect.y(), useWidth, 2, style::al_left, 0, -1, 0, breakEverywhere); p.setTextPalette(stm->textPalette); if (!forwarded->psaType.isEmpty()) { 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 = displayedReply()) { reply->paint(p, this, context, trect.x(), trect.y(), trect.width(), true); trect.setY(trect.y() + reply->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); 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); prepareCustomEmojiPaint(p, context, text()); if (const auto botTop = Get()) { botTop->text.drawLeftElided( p, trect.x(), trect.y(), trect.width(), width()); trect.setY(trect.y() + botTop->height); } auto highlightRequest = context.computeHighlightCache(); text().draw(p, { .position = trect.topLeft(), .availableWidth = trect.width(), .palette = &stm->textPalette, .pre = stm->preCache.get(), .blockquote = context.quoteCache(colorIndex()), .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 = highlightRequest ? &*highlightRequest : nullptr, }); } PointState Message::pointState(QPoint point) const { auto g = countGeometry(); if (g.width() < 1 || isHidden()) { return PointState::Outside; } const auto media = this->media(); const auto item = data(); 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. auto entry = logEntryOriginal(); // Entry page is always a bubble bottom. auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); if (item->repliesAreComments() || item->externalReply()) { 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 (entry) { auto entryHeight = entry->height(); trect.setHeight(trect.height() - entryHeight); } auto mediaHeight = media->height(); auto mediaLeft = trect.x() - st::msgPadding.left(); auto mediaTop = (trect.y() + trect.height() - mediaHeight); 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) { if (const auto markup = data()->Get()) { if (const auto keyboard = markup->inlineKeyboard.get()) { keyboard->clickHandlerPressedChanged( handler, pressed, countMessageRounding()); } } Element::clickHandlerPressedChanged(handler, pressed); if (!handler) { return; } else if (_rightAction && (handler == _rightAction->link)) { toggleRightActionRipple(pressed); } 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 = displayedReply() ; reply && (handler == reply->link())) { toggleReplyRipple(pressed); } } 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 size = rightActionSize(); Assert(size != std::nullopt); if (pressed) { if (!_rightAction->ripple) { // Create a ripple. _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::toggleReplyRipple(bool pressed) { const auto reply = displayedReply(); if (!reply) { return; } if (pressed) { if (!reply->ripple.animation && !unwrapped()) { const auto &padding = st::msgPadding; const auto geometry = countGeometry(); const auto item = data(); const auto margins = reply->margins(); const auto size = QSize( geometry.width() - padding.left() - padding.right(), reply->height() - margins.top() - margins.bottom()); reply->ripple.animation = std::make_unique( st::defaultRippleAnimation, Ui::RippleAnimation::RoundRectMask( size, st::messageQuoteStyle.radius), [=] { item->history()->owner().requestItemRepaint(item); }); } if (reply->ripple.animation) { reply->ripple.animation->add(reply->ripple.lastPoint); } } else if (reply->ripple.animation) { reply->ripple.animation->lastStop(); } } 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::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(); if (_reactions) { _reactions->unloadCustomEmoji(); } _comments = nullptr; if (_fromNameStatus) { _fromNameStatus->custom = nullptr; _fromNameStatus->id = 0; } } bool Message::showForwardsFromSender( not_null forwarded) const { const auto peer = data()->history()->peer; return !forwarded->story && (peer->isSelf() || peer->isRepliesChat() || forwarded->imported); } bool Message::hasFromPhoto() const { if (isHidden()) { return false; } switch (context()) { case Context::AdminLog: return true; case Context::History: case Context::Pinned: case Context::Replies: { const auto item = data(); if (item->isPost()) { if (item->isSponsored()) { if (item->history()->peer->isMegagroup()) { return true; } if (const auto info = item->Get()) { return info->isForceUserpicDisplay; } } return false; } if (item->isEmpty() || (context() == Context::Replies && item->isDiscussionPost())) { return false; } else if (delegate()->elementIsChatWide()) { return true; } else if (const auto forwarded = item->Get()) { const auto peer = item->history()->peer; if (peer->isSelf() || peer->isRepliesChat()) { return true; } } return !item->out() && !item->history()->peer->isUser(); } break; case Context::ContactPreview: return false; } Unexpected("Context in Message::hasFromPhoto."); } TextState Message::textState( QPoint point, StateRequest request) const { const auto item = data(); const auto media = this->media(); auto result = TextState(item); const auto minSymbol = (_invertMedia && request.onlyMessageText) ? visibleMediaTextLength() : 0; result.symbol = minSymbol; auto g = countGeometry(); if (g.width() < 1 || isHidden()) { return result; } const auto bubble = drawBubble(); const auto reactionsInBubble = _reactions && embedReactionsInBubble(); const auto mediaDisplayed = media && media->isDisplayed(); auto keyboard = item->inlineReplyKeyboard(); auto keyboardHeight = 0; if (keyboard) { keyboardHeight = keyboard->naturalHeight(); g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); } 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)) { return result; } } if (bubble) { const auto inBubble = g.contains(point); auto entry = logEntryOriginal(); // Entry page is always a bubble bottom. auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); auto inner = g; if (getStateCommentsButton(point, inner, &result)) { 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)) { return result; } } if (_viewButton) { const auto belowInfo = _viewButton->belowMessageInfo(); const auto infoHeight = reactionsInBubble ? (reactionsHeight + 2 * st::mediaInBubbleSkip) : _bottomInfo.height(); const auto heightMargins = QMargins(0, 0, 0, infoHeight); if (_viewButton->getState( point, _viewButton->countRect(belowInfo ? inner : inner - heightMargins), &result)) { 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 (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 (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); result.symbol += visibleTextLength() + visibleMediaTextLength(); } } auto checkBottomInfoState = [&] { if (mediaOnBottom && (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 (result.symbol <= minSymbol && inBubble) { 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) { result.symbol = minSymbol; result.afterSymbol = false; result.cursor = CursorState::None; } } else if (request.onlyMessageText) { result.symbol = visibleTextLength(); result.afterSymbol = false; result.cursor = CursorState::None; } else { result.symbol += visibleTextLength(); } } else if (getStateText(point, trect, &result, request)) { if (_invertMedia) { result.symbol += visibleMediaTextLength(); } result.overMessageText = true; checkBottomInfoState(); return result; } else if (point.y() >= trect.y() + trect.height()) { result.symbol = visibleTextLength() + visibleMediaTextLength(); } } checkBottomInfoState(); if (const auto size = rightActionSize(); size && _rightAction) { const auto fastShareSkip = std::clamp( (g.height() - size->height()) / 2, 0, st::historyFastShareBottom); const auto fastShareLeft = g.left() + g.width() + st::historyFastShareLeft; const auto fastShareTop = g.top() + g.height() - fastShareSkip - size->height(); if (QRect( fastShareLeft, fastShareTop, size->width(), size->height() ).contains(point)) { result.link = rightActionLink(point - QPoint(fastShareLeft, fastShareTop)); } } } else if (media && media->isDisplayed()) { result = media->textState(point - g.topLeft(), request); if (request.onlyMessageText) { result.symbol = 0; result.afterSymbol = false; result.cursor = CursorState::None; } result.symbol += visibleTextLength(); } if (keyboard && item->isHistoryEntry()) { const auto keyboardTop = g.top() + g.height() + st::msgBotKbButton.margin + ((_reactions && !reactionsInBubble) ? (st::mediaInBubbleSkip + _reactions->height()) : 0); if (QRect(g.left(), keyboardTop, g.width(), keyboardHeight).contains(point)) { result.link = keyboard->getLink(point - QPoint(g.left(), keyboardTop)); } } 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).value_or(nullptr); if (!controller) { return; } if (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; } const auto replyWidth = [&] { if (isUnderCursor() && displayFastReply()) { return st::msgFont->width(FastReplyText()); } return 0; }(); if (replyWidth && point.x() >= trect.left() + trect.width() - replyWidth && point.x() < trect.left() + trect.width() + st::msgPadding.right() && point.y() >= trect.top() - st::msgPadding.top() && point.y() < trect.top() + st::msgServiceFont->height) { outResult->link = fastReplyLink(); return true; } if (point.y() >= trect.top() && point.y() < trect.top() + st::msgNameFont->height) { auto availableLeft = trect.left(); auto availableWidth = trect.width(); if (replyWidth) { availableWidth -= st::msgPadding.right() + replyWidth; } const auto item = data(); const auto from = item->displayFrom(); const auto nameText = [&]() -> const Ui::Text::String * { if (from) { validateFromNameText(from); return &_fromName; } else if (const auto info = item->hiddenSenderInfo()) { return &info->nameText(); } else { Unexpected("Corrupt forwarded information in message."); } }(); if (point.x() >= availableLeft && point.x() < availableLeft + availableWidth && point.x() < availableLeft + nameText->maxWidth()) { outResult->link = fromLink(); return true; } auto via = item->Get(); if (via && !displayForwardedFrom() && point.x() >= availableLeft + nameText->maxWidth() + st::msgServiceFont->spacew && point.x() < availableLeft + availableWidth && point.x() < availableLeft + nameText->maxWidth() + st::msgServiceFont->spacew + via->width) { outResult->link = via->link; return true; } } trect.setTop(trect.top() + st::msgNameFont->height); return false; } 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)); outResult->symbol = 0; outResult->afterSymbol = 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 = Ui::Text::RichLangValue( (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 = displayedReply()) { 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->ripple.lastPoint = 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; 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; } else if (const auto botTop = Get()) { trect.setY(trect.y() + botTop->height); } const auto item = data(); if (base::in_range(point.y(), trect.y(), trect.y() + trect.height())) { *outResult = TextState(item, text().getState( point - trect.topLeft(), trect.width(), request.forText())); return true; } return false; } // Forward to media. void Message::updatePressed(QPoint point) { const auto item = data(); const auto media = this->media(); if (!media) return; auto g = countGeometry(); auto keyboard = item->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 = item->Get()) { trect.setTop(trect.top() + reply->height()); } if (const auto via = item->Get()) { 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()); } } TextForMimeData Message::selectedText(TextSelection selection) const { const auto media = this->media(); auto logEntryOriginalResult = 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 = (mediaDisplayed || isHiddenByGroup()) ? media->selectedText(mediaSelection) : TextForMimeData(); if (auto entry = logEntryOriginal()) { const auto originalSelection = mediaBefore ? skipTextSelection(textSelection) : mediaDisplayed ? media->skipSelection(mediaSelection) : skipTextSelection(selection); logEntryOriginalResult = entry->selectedText(originalSelection); } 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(logEntryOriginalResult); } else if (!logEntryOriginalResult.empty()) { result.append(u"\n\n"_q).append(std::move(logEntryOriginalResult)); } return result; } SelectedQuote Message::selectedQuote(TextSelection selection) const { const auto item = data(); 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, data()); } else if (const auto media = this->media()) { if (media->isDisplayed() || isHiddenByGroup()) { return media->selectedQuote(selection); } } return {}; } TextSelection Message::selectionFromQuote( not_null item, const TextWithEntities "e) const { if (quote.empty()) { return {}; } 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(), item, quote); return mediaBefore ? media->unskipSelection(result) : result; } else if (const auto media = this->media()) { if (media->isDisplayed() || isHiddenByGroup()) { return media->selectionFromQuote(item, 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 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 (!entryResult.empty()) { result = result.empty() ? entryResult : TextSelection{ std::min(result.from, entryResult.from), std::max(result.to, entryResult.to), }; } return result; } Reactions::ButtonParameters Message::reactionButtonParameters( QPoint position, const TextState &reactionState) const { using namespace Reactions; auto result = ButtonParameters{ .context = data()->fullId() }; const auto outbg = hasOutLayout(); const auto outsideBubble = (!_comments && !embedReactionsInBubble()); const auto geometry = countGeometry(); result.pointer = position; const auto onTheLeft = (outbg && !delegate()->elementIsChatWide()); const auto keyboard = data()->inlineReplyKeyboard(); const auto keyboardHeight = keyboard ? (st::msgBotKbButton.margin + keyboard->naturalHeight()) : 0; const auto reactionsHeight = (_reactions && !embedReactionsInBubble()) ? (st::mediaInBubbleSkip + _reactions->height()) : 0; 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; } 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 { 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 { 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( data(), point - QPoint{ infoLeft, infoTop }); } int Message::infoWidth() const { return _bottomInfo.optimalSize().width(); } int Message::bottomInfoFirstLineWidth() const { return _bottomInfo.firstLineWidth(); } bool Message::bottomInfoIsWide() const { if (_reactions && embedReactionsInBubble()) { return false; } return _bottomInfo.isWide(); } bool Message::isSignedAuthorElided() const { return _bottomInfo.isSignedAuthorElided(); } bool Message::embedReactionsInBottomInfo() const { const auto item = data(); const auto user = item->history()->peer->asUser(); if (!user || user->isPremium() || user->session().premium()) { // Only in messages of a non premium user with a non premium user. return false; } auto seenMy = false; auto seenHis = false; for (const auto &reaction : item->reactions()) { if (reaction.id.custom()) { // Only in messages without any custom emoji reactions. return false; } // Only in messages without two reactions from the same person. if (reaction.my) { if (seenMy) { return false; } seenMy = true; } if (!reaction.my || (reaction.count > 1)) { if (seenHis) { return false; } seenHis = true; } } return true; } bool Message::embedReactionsInBubble() const { return needInfoDisplay(); } void Message::refreshReactions() { const auto item = data(); const auto &list = item->reactions(); if (list.empty() || embedReactionsInBottomInfo()) { _reactions = nullptr; return; } using namespace Reactions; auto reactionsData = InlineListDataFromMessage(this); if (!_reactions) { const auto handlerFactory = [=](ReactionId id) { const auto weak = base::make_weak(this); return std::make_shared([=] { if (const auto strong = weak.get()) { strong->data()->toggleReaction( id, HistoryItem::ReactionSource::Existing); if (const auto now = weak.get()) { const auto chosen = now->data()->chosenReactions(); if (ranges::contains(chosen, id)) { now->animateReaction({ .id = id, }); } } } }); }; _reactions = std::make_unique( &item->history()->owner().reactions(), handlerFactory, [=] { customEmojiRepaint(); }, std::move(reactionsData)); } else { _reactions->update(std::move(reactionsData), width()); } } void Message::validateInlineKeyboard(HistoryMessageReplyMarkup *markup) { if (!markup || markup->inlineKeyboard || markup->hiddenBy(data()->media())) { return; } markup->inlineKeyboard = std::make_unique( data(), std::make_unique(st::msgBotKbButton)); } 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()) { 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; } } void Message::itemDataChanged() { const auto wasInfo = _bottomInfo.currentSize(); const auto wasReactions = _reactions ? _reactions->currentSize() : QSize(); refreshReactions(); _bottomInfo.update(BottomInfoDataFromMessage(this), width()); const auto nowInfo = _bottomInfo.currentSize(); const auto nowReactions = _reactions ? _reactions->currentSize() : QSize(); if (wasInfo != nowInfo || wasReactions != nowReactions) { history()->owner().requestViewResize(this); } else { repaint(); } } auto Message::verticalRepaintRange() const -> VerticalRepaintRange { const auto media = this->media(); const auto add = media ? media->bubbleRollRepaintMargins() : QMargins(); return { .top = -add.top(), .height = height() + add.top() + add.bottom() }; } 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::plainMaxWidth() const { return st::msgPadding.left() + (hasVisibleText() ? text().maxWidth() : 0) + st::msgPadding.right(); } int Message::monospaceMaxWidth() const { return st::msgPadding.left() + (hasVisibleText() ? text().countMaxMonospaceWidth() : 0) + st::msgPadding.right(); } int Message::viewButtonHeight() const { return _viewButton ? _viewButton->height() : 0; } void Message::updateViewButtonExistence() { const auto item = data(); const auto sponsored = item->Get(); const auto media = sponsored ? nullptr : item->media(); const auto has = sponsored || (media && ViewButton::MediaHasViewButton(media)); if (!has) { _viewButton = nullptr; return; } else if (_viewButton) { return; } auto make = [=](auto &&from) { return std::make_unique( std::forward(from), colorIndex(), [=] { repaint(); }); }; _viewButton = sponsored ? make(sponsored) : make(media); } 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; } HistoryMessageReply *Message::displayedReply() const { if (const auto reply = data()->Get()) { return delegate()->elementHideReply(this) ? nullptr : reply; } 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::hasFromName() const { switch (context()) { case Context::AdminLog: return true; case Context::History: case Context::Pinned: case Context::Replies: { const auto item = data(); const auto peer = item->history()->peer; if (hasOutLayout() && !item->from()->isChannel()) { return false; } else if (!peer->isUser()) { 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 (showForwardsFromSender(forwarded)) { return true; } } return false; } break; case Context::ContactPreview: return false; } Unexpected("Context in Message::hasFromPhoto."); } bool Message::displayFromName() const { if (!hasFromName() || isAttachedToPrevious()) { return false; } return !Has(); } bool Message::displayForwardedFrom() const { const auto item = data(); if (const auto forwarded = item->Get()) { if (forwarded->story) { return true; } else if (showForwardsFromSender(forwarded)) { return false; } if (const auto sender = item->discussionPostOriginalSender()) { if (sender == forwarded->originalSender) { return false; } } const auto media = item->media(); return !media || !media->dropForwardedInfo(); } return false; } bool Message::hasOutLayout() const { const auto item = data(); if (item->history()->peer->isSelf()) { return !item->Has(); } else if (const auto forwarded = item->Get()) { if (!forwarded->imported || !forwarded->originalSender || !forwarded->originalSender->isSelf()) { if (showForwardsFromSender(forwarded)) { return false; } } } return item->out() && !item->isPost(); } bool Message::drawBubble() const { const auto item = data(); if (isHidden()) { return false; } else if (logEntryOriginal() || item->isFakeBotAbout()) { 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()) { return false; } const auto media = this->media(); return media ? (!hasVisibleText() && media->unwrapped()) : item->isEmpty(); } int Message::minWidthForMedia() const { auto result = 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))); } return result; } bool Message::hasFastReply() const { if (context() == Context::Replies) { if (data()->isDiscussionPost()) { 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(); } bool Message::displayRightActionComments() const { return !isPinnedContext() && data()->repliesAreComments() && media() && media()->isDisplayed() && !hasBubble(); } std::optional Message::rightActionSize() const { 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 (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()) { return false; } else if (peer->isChannel()) { return !peer->isMegagroup(); } else if (const auto user = peer->asUser()) { if (const auto forwarded = item->Get()) { return !showForwardsFromSender(forwarded) && !item->out() && forwarded->originalSender && forwarded->originalSender->isChannel() && !forwarded->originalSender->isMegagroup(); } 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(); } 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(); } } 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 { const auto &icon = (displayFastShare() && !isPinnedContext()) ? st->historyFastShareIcon() : st->historyGoToOriginalIcon(); icon.paintInCenter(p, { left, top, size->width(), size->height() }); } } ClickHandlerPtr Message::rightActionLink( std::optional pressPoint) const { ensureRightAction(); if (!_rightAction->link) { _rightAction->link = prepareRightActionLink(); } if (pressPoint) { _rightAction->lastPoint = *pressPoint; } return _rightAction->link; } void Message::ensureRightAction() const { if (_rightAction) { return; } Assert(rightActionSize().has_value()); _rightAction = std::make_unique(); } ClickHandlerPtr Message::prepareRightActionLink() const { if (isPinnedContext()) { return JumpToMessageClickHandler(data()); } else if (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); } }; }; return std::make_shared([=]( ClickContext context) { const auto controller = ExtractController(context).value_or(nullptr); if (!controller) { return; } if (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 { FastShareMessage(controller, item); } } }); } ClickHandlerPtr Message::fastReplyLink() const { if (_fastReplyLink) { return _fastReplyLink; } const auto itemId = data()->fullId(); _fastReplyLink = std::make_shared([=] { 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() || displayedReply() || item->Has(); }; auto entry = logEntryOriginal(); 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 (!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::fromNameUpdated(int width) const { const auto item = data(); const auto replyWidth = hasFastReply() ? st::msgFont->width(FastReplyText()) : 0; if (!_rightBadge.isEmpty()) { const auto badgeWidth = _rightBadge.maxWidth(); width -= st::msgPadding.right() + std::max(badgeWidth, replyWidth); } else if (replyWidth) { width -= st::msgPadding.right() + replyWidth; } const auto from = item->displayFrom(); validateFromNameText(from); if (const auto via = item->Get()) { if (!displayForwardedFrom()) { const auto nameText = [&]() -> const Ui::Text::String * { if (from) { return &_fromName; } else if (const auto info = item->hiddenSenderInfo()) { return &info->nameText(); } else { Unexpected("Corrupted forwarded information in message."); } }(); via->resize(width - st::msgPadding.left() - st::msgPadding.right() - nameText->maxWidth() + (_fromNameStatus ? (st::dialogsPremiumIcon.icon.width() + st::msgServiceFont->spacew) : 0) - 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()) { result.translate(0, st::msgPadding.top() + st::mediaInBubbleSkip); if (displayFromName()) { // See paintFromName(). result.translate(0, st::msgNameFont->height); } if (displayedTopicButton()) { result.translate(0, st::topicButtonSkip + st::topicButtonPadding.top() + st::msgNameFont->height + st::topicButtonPadding.bottom() + st::topicButtonSkip); } // Skip displayForwardedFrom() until there are no animations for it. if (const auto reply = displayedReply()) { // See paintReplyInfo(). result.translate(0, reply->height()); } if (!displayFromName() && !displayForwardedFrom()) { // See paintViaBotIdInfo(). if (data()->Has()) { result.translate(0, st::msgServiceNameFont->height); } } } return result; } QRect Message::countGeometry() const { const auto item = data(); const auto centeredView = item->isFakeBotAbout() || (context() == Context::Replies && item->isDiscussionPost()); const auto media = this->media(); const auto mediaWidth = (media && media->isDisplayed()) ? media->width() : width(); const auto outbg = hasOutLayout(); const auto availableWidth = width() - st::msgMargin.left() - (centeredView ? st::msgMargin.left() : st::msgMargin.right()); auto contentLeft = (outbg && !delegate()->elementIsChatWide()) ? st::msgMargin.right() : 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 = plainMaxWidth(); if (mediaWidth < textualWidth && (!media || !media->enforceBubbleWidth())) { accumulate_min(contentWidth, textualWidth); } else { contentWidth = mediaWidth; } } if (contentWidth < availableWidth && !delegate()->elementIsChatWide()) { 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 item = data(); const auto keyboard = item->inlineReplyKeyboard(); const auto skipTail = smallBottom || (media && media->skipBubbleTail()) || (keyboard != nullptr) || item->isFakeBotAbout() || (context() == Context::Replies && item->isDiscussionPost()); const auto right = !delegate()->elementIsChatWide() && hasOutLayout(); 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 (const auto keyboard = data()->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(); } auto newHeight = minHeight(); if (const auto service = Get()) { service->resizeToWidth(newWidth, delegate()->elementIsChatWide()); } const auto item = data(); const auto botTop = item->isFakeBotAbout() ? Get() : nullptr; const auto media = this->media(); const auto mediaDisplayed = media ? media->isDisplayed() : false; const auto bubble = drawBubble(); // This code duplicates countGeometry() but also resizes media. const auto centeredView = item->isFakeBotAbout() || (context() == Context::Replies && item->isDiscussionPost()); auto contentWidth = newWidth - st::msgMargin.left() - (centeredView ? st::msgMargin.left() : st::msgMargin.right()); if (hasFromPhoto()) { if (const auto size = rightActionSize()) { contentWidth -= size->width() + (st::msgPhotoSkip - st::historyFastShareSize); } } accumulate_min(contentWidth, maxWidth()); _bubbleWidthLimit = std::max(st::msgMaxWidth, monospaceMaxWidth()); accumulate_min(contentWidth, int(_bubbleWidthLimit)); if (mediaDisplayed) { media->resizeGetHeight(contentWidth); if (media->width() < contentWidth) { const auto textualWidth = plainMaxWidth(); if (media->width() < textualWidth && !media->enforceBubbleWidth()) { accumulate_min(contentWidth, textualWidth); } else { contentWidth = media->width(); } } } const auto textWidth = qMax(contentWidth - st::msgPadding.left() - st::msgPadding.right(), 1); const auto reactionsInBubble = _reactions && embedReactionsInBubble(); const auto bottomInfoHeight = _bottomInfo.resizeGetHeight( std::min( _bottomInfo.optimalSize().width(), textWidth - 2 * st::msgDateDelta.x())); if (bubble) { auto reply = displayedReply(); auto via = item->Get(); auto entry = logEntryOriginal(); // Entry page is always a bubble bottom. auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); if (reactionsInBubble) { _reactions->resizeGetHeight(textWidth); } if (contentWidth == maxWidth()) { if (mediaDisplayed) { if (entry) { newHeight += entry->resizeGetHeight(contentWidth); } } else 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; } 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 (entry) { newHeight += entry->resizeGetHeight(contentWidth); } } else if (entry) { newHeight += entry->resizeGetHeight(contentWidth); } if (reactionsInBubble) { if (!mediaDisplayed || _viewButton) { newHeight += st::mediaInBubbleSkip; } else if (!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; } 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 (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()); reply->ripple.animation = nullptr; } if (needInfoDisplay()) { newHeight += (bottomInfoHeight - st::msgDateFont->height); } if (item->repliesAreComments() || item->externalReply()) { 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 (hasOutLayout() && !delegate()->elementIsChatWide()) { _reactions->flipToRight(); } } if (const auto keyboard = item->inlineReplyKeyboard()) { const auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight(); newHeight += keyboardHeight; keyboard->resize(contentWidth, keyboardHeight - st::msgBotKbButton.margin); } newHeight += marginTop() + marginBottom(); return newHeight; } bool Message::needInfoDisplay() const { const auto media = this->media(); const auto mediaDisplayed = media ? media->isDisplayed() : false; const auto entry = logEntryOriginal(); return entry ? !entry->customInfoLayout() : ((mediaDisplayed && media->isBubbleBottom()) ? !media->customInfoLayout() : true); } bool Message::invertMedia() const { return _invertMedia; } bool Message::hasVisibleText() const { if (data()->emptyText()) { if (const auto media = data()->media()) { return media->storyExpired(); } return false; } const auto media = this->media(); 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() { const auto item = data(); const auto media = this->media(); const auto hasTextSkipBlock = [&] { if (item->_text.empty()) { if (const auto media = data()->media()) { return media->storyExpired(); } return false; } else if (item->Has()) { return false; } else if (media && media->isDisplayed()) { 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(); } } validateTextSkipBlock(hasTextSkipBlock, skipWidth, skipHeight); } 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(); } } // namespace HistoryView