/* 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_element.h" #include "api/api_chat_invite.h" #include "history/view/history_view_service_message.h" #include "history/view/history_view_message.h" #include "history/history_item_components.h" #include "history/history_item.h" #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_media_grouped.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_large_emoji.h" #include "history/view/media/history_view_custom_emoji.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/reactions/history_view_reactions.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_spoiler_click_handler.h" #include "history/history.h" #include "base/unixtime.h" #include "core/application.h" #include "core/core_settings.h" #include "core/click_handler_types.h" #include "core/ui_integration.h" #include "main/main_session.h" #include "main/main_domain.h" #include "chat_helpers/stickers_emoji_pack.h" #include "window/window_session_controller.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/reaction_fly_animation.h" #include "ui/chat/chat_style.h" #include "ui/toast/toast.h" #include "ui/toasts/common_toasts.h" #include "ui/text/text_options.h" #include "ui/item_text_options.h" #include "ui/painter.h" #include "data/data_session.h" #include "data/data_groups.h" #include "data/data_media_types.h" #include "data/data_sponsored_messages.h" #include "data/data_message_reactions.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" namespace HistoryView { namespace { // A new message from the same sender is attached to previous within 15 minutes. constexpr int kAttachMessageToPreviousSecondsDelta = 900; Element *HoveredElement/* = nullptr*/; Element *PressedElement/* = nullptr*/; Element *HoveredLinkElement/* = nullptr*/; Element *PressedLinkElement/* = nullptr*/; Element *MousedElement/* = nullptr*/; [[nodiscard]] bool IsAttachedToPreviousInSavedMessages( not_null previous, HistoryMessageForwarded *prevForwarded, not_null item, HistoryMessageForwarded *forwarded) { const auto sender = previous->senderOriginal(); if ((prevForwarded != nullptr) != (forwarded != nullptr)) { return false; } else if (sender != item->senderOriginal()) { return false; } else if (!prevForwarded || sender) { return true; } const auto previousInfo = prevForwarded->hiddenSenderInfo.get(); const auto itemInfo = forwarded->hiddenSenderInfo.get(); Assert(previousInfo != nullptr); Assert(itemInfo != nullptr); return (*previousInfo == *itemInfo); } [[nodiscard]] Window::SessionController *ContextOrSessionWindow( const ClickHandlerContext &context, not_null session) { if (const auto controller = context.sessionWindow.get()) { if (&controller->session() == session) { return controller; } } return session->tryResolveWindow(); } } // namespace std::unique_ptr MakePathShiftGradient( not_null st, Fn update) { return std::make_unique( st->msgServiceBg(), st->msgServiceBgSelected(), std::move(update), st->paletteChanged()); } SimpleElementDelegate::SimpleElementDelegate( not_null controller, Fn update) : _controller(controller) , _pathGradient( MakePathShiftGradient( controller->chatStyle(), std::move(update))) { } SimpleElementDelegate::~SimpleElementDelegate() = default; std::unique_ptr SimpleElementDelegate::elementCreate( not_null message, Element *replacing) { return std::make_unique(this, message, replacing); } std::unique_ptr SimpleElementDelegate::elementCreate( not_null message, Element *replacing) { return std::make_unique(this, message, replacing); } bool SimpleElementDelegate::elementUnderCursor( not_null view) { return false; } float64 SimpleElementDelegate::elementHighlightOpacity( not_null item) const { return 0.; } bool SimpleElementDelegate::elementInSelectionMode() { return false; } bool SimpleElementDelegate::elementIntersectsRange( not_null view, int from, int till) { return true; } void SimpleElementDelegate::elementStartStickerLoop( not_null view) { } void SimpleElementDelegate::elementShowPollResults( not_null poll, FullMsgId context) { } void SimpleElementDelegate::elementOpenPhoto( not_null photo, FullMsgId context) { } void SimpleElementDelegate::elementOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { } void SimpleElementDelegate::elementCancelUpload(const FullMsgId &context) { } void SimpleElementDelegate::elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) { } bool SimpleElementDelegate::elementAnimationsPaused() { return _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any); } bool SimpleElementDelegate::elementHideReply(not_null view) { return false; } bool SimpleElementDelegate::elementShownUnread( not_null view) { return view->data()->unread(); } void SimpleElementDelegate::elementSendBotCommand( const QString &command, const FullMsgId &context) { } void SimpleElementDelegate::elementHandleViaClick(not_null bot) { } bool SimpleElementDelegate::elementIsChatWide() { return false; } auto SimpleElementDelegate::elementPathShiftGradient() -> not_null { return _pathGradient.get(); } void SimpleElementDelegate::elementReplyTo(const FullMsgId &to) { } void SimpleElementDelegate::elementStartInteraction( not_null view) { } void SimpleElementDelegate::elementStartPremium( not_null view, Element *replacing) { } void SimpleElementDelegate::elementCancelPremium( not_null view) { } TextSelection UnshiftItemSelection( TextSelection selection, uint16 byLength) { return (selection == FullSelection) ? selection : ::unshiftSelection(selection, byLength); } TextSelection ShiftItemSelection( TextSelection selection, uint16 byLength) { return (selection == FullSelection) ? selection : ::shiftSelection(selection, byLength); } TextSelection UnshiftItemSelection( TextSelection selection, const Ui::Text::String &byText) { return UnshiftItemSelection(selection, byText.length()); } TextSelection ShiftItemSelection( TextSelection selection, const Ui::Text::String &byText) { return ShiftItemSelection(selection, byText.length()); } QString DateTooltipText(not_null view) { const auto locale = QLocale(); const auto format = QLocale::LongFormat; auto dateText = locale.toString(view->dateTime(), format); if (const auto editedDate = view->displayedEditDate()) { dateText += '\n' + tr::lng_edited_date( tr::now, lt_date, locale.toString(base::unixtime::parse(editedDate), format)); } if (const auto forwarded = view->data()->Get()) { dateText += '\n' + tr::lng_forwarded_date( tr::now, lt_date, locale.toString(base::unixtime::parse(forwarded->originalDate), format)); if (forwarded->imported) { dateText = tr::lng_forwarded_imported(tr::now) + "\n\n" + dateText; } } if (view->isSignedAuthorElided()) { if (const auto msgsigned = view->data()->Get()) { dateText += '\n' + tr::lng_signed_author(tr::now, lt_user, msgsigned->author); } } return dateText; } void UnreadBar::init(const QString &string) { text = string; width = st::semiboldFont->width(text); } int UnreadBar::height() { return st::historyUnreadBarHeight + st::historyUnreadBarMargin; } int UnreadBar::marginTop() { return st::lineWidth + st::historyUnreadBarMargin; } void UnreadBar::paint( Painter &p, const PaintContext &context, int y, int w, bool chatWide) const { const auto st = context.st; const auto bottom = y + height(); y += marginTop(); p.fillRect( 0, y, w, height() - marginTop() - st::lineWidth, st->historyUnreadBarBg()); p.fillRect( 0, bottom - st::lineWidth, w, st::lineWidth, st->historyUnreadBarBorder()); p.setFont(st::historyUnreadBarFont); p.setPen(st->historyUnreadBarFg()); int maxwidth = w; if (chatWide) { maxwidth = qMin( maxwidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); } w = maxwidth; const auto skip = st::historyUnreadBarHeight - 2 * st::lineWidth - st::historyUnreadBarFont->height; p.drawText( (w - width) / 2, y + (skip / 2) + st::historyUnreadBarFont->ascent, text); } void DateBadge::init(const QString &date) { text = date; width = st::msgServiceFont->width(text); } int DateBadge::height() const { return st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom() + st::msgServiceMargin.bottom(); } void DateBadge::paint( Painter &p, not_null st, int y, int w, bool chatWide) const { ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide); } Element::Element( not_null delegate, not_null data, Element *replacing, Flag serviceFlag) : _delegate(delegate) , _data(data) , _dateTime(IsItemScheduledUntilOnline(data) ? QDateTime() : ItemDateTime(data)) , _text(st::msgMinWidth) , _flags(serviceFlag | Flag::NeedsResize | (IsItemScheduledUntilOnline(data) ? Flag::ScheduledUntilOnline : Flag())) , _context(delegate->elementContext()) { history()->owner().registerItemView(this); refreshMedia(replacing); if (_context == Context::History) { history()->setHasPendingResizedItems(); } } not_null Element::delegate() const { return _delegate; } not_null Element::data() const { return _data; } not_null Element::history() const { return _data->history(); } QDateTime Element::dateTime() const { return _dateTime; } Media *Element::media() const { return _media.get(); } Context Element::context() const { return _context; } int Element::y() const { return _y; } void Element::setY(int y) { _y = y; } void Element::refreshDataIdHook() { } void Element::clearSpecialOnlyEmoji() { if (!(_flags & Flag::SpecialOnlyEmoji)) { return; } history()->session().emojiStickersPack().remove(this); _flags &= ~Flag::SpecialOnlyEmoji; } void Element::checkSpecialOnlyEmoji() { if (history()->session().emojiStickersPack().add(this)) { _flags |= Flag::SpecialOnlyEmoji; } } void Element::hideSpoilers() { if (_text.hasSpoilers()) { _text.setSpoilerRevealed(false, anim::type::instant); } } void Element::customEmojiRepaint() { if (!(_flags & Flag::CustomEmojiRepainting)) { _flags |= Flag::CustomEmojiRepainting; history()->owner().requestViewRepaint(this); } } void Element::clearCustomEmojiRepaint() const { _flags &= ~Flag::CustomEmojiRepainting; data()->_flags &= ~MessageFlag::CustomEmojiRepainting; } void Element::prepareCustomEmojiPaint( Painter &p, const PaintContext &context, const Ui::Text::String &text) const { if (!text.hasPersistentAnimation()) { return; } clearCustomEmojiRepaint(); p.setInactive(context.paused); if (!_heavyCustomEmoji) { _heavyCustomEmoji = true; history()->owner().registerHeavyViewPart(const_cast(this)); } } void Element::prepareCustomEmojiPaint( Painter &p, const PaintContext &context, const Reactions::InlineList &reactions) const { if (!reactions.hasCustomEmoji()) { return; } clearCustomEmojiRepaint(); p.setInactive(context.paused); if (!_heavyCustomEmoji) { _heavyCustomEmoji = true; history()->owner().registerHeavyViewPart(const_cast(this)); } } void Element::repaint() const { history()->owner().requestViewRepaint(this); } void Element::paintHighlight( Painter &p, const PaintContext &context, int geometryHeight) const { const auto top = marginTop(); const auto bottom = marginBottom(); const auto fill = qMin(top, bottom); const auto skiptop = top - fill; const auto fillheight = fill + geometryHeight + fill; paintCustomHighlight(p, context, skiptop, fillheight, data()); } void Element::paintCustomHighlight( Painter &p, const PaintContext &context, int y, int height, not_null item) const { const auto opacity = delegate()->elementHighlightOpacity(item); if (opacity == 0.) { return; } const auto o = p.opacity(); p.setOpacity(o * opacity); p.fillRect( 0, y, width(), height, context.st->msgSelectOverlay()); p.setOpacity(o); } bool Element::isUnderCursor() const { return _delegate->elementUnderCursor(this); } bool Element::isLastAndSelfMessage() const { if (!hasOutLayout() || data()->_history->peer->isSelf()) { return false; } if (const auto last = data()->_history->lastMessage()) { return last == data(); } return false; } void Element::setPendingResize() { _flags |= Flag::NeedsResize; if (_context == Context::History) { data()->_history->setHasPendingResizedItems(); } } bool Element::pendingResize() const { return _flags & Flag::NeedsResize; } bool Element::isAttachedToPrevious() const { return _flags & Flag::AttachedToPrevious; } bool Element::isAttachedToNext() const { return _flags & Flag::AttachedToNext; } bool Element::isBubbleAttachedToPrevious() const { return _flags & Flag::BubbleAttachedToPrevious; } bool Element::isBubbleAttachedToNext() const { return _flags & Flag::BubbleAttachedToNext; } int Element::skipBlockWidth() const { return st::msgDateSpace + infoWidth() - st::msgDateDelta.x(); } int Element::skipBlockHeight() const { return st::msgDateFont->height - st::msgDateDelta.y(); } int Element::infoWidth() const { return 0; } int Element::bottomInfoFirstLineWidth() const { return 0; } bool Element::bottomInfoIsWide() const { return false; } bool Element::isHiddenByGroup() const { return _flags & Flag::HiddenByGroup; } bool Element::isHidden() const { return isHiddenByGroup(); } void Element::refreshMedia(Element *replacing) { _flags &= ~Flag::HiddenByGroup; const auto item = data(); if (const auto media = item->media()) { if (media->canBeGrouped()) { if (const auto group = history()->owner().groups().find(item)) { if (group->items.front() != item) { _media = nullptr; _flags |= Flag::HiddenByGroup; } else { _media = std::make_unique( this, group->items); if (!pendingResize()) { history()->owner().requestViewResize(this); } } return; } } _media = media->createView(this, replacing); } else if (isOnlyCustomEmoji() && Core::App().settings().largeEmoji()) { _media = std::make_unique( this, std::make_unique(this, onlyCustomEmoji())); } else if (isIsolatedEmoji() && Core::App().settings().largeEmoji()) { const auto emoji = isolatedEmoji(); const auto emojiStickers = &history()->session().emojiStickersPack(); const auto skipPremiumEffect = false; if (const auto sticker = emojiStickers->stickerForEmoji(emoji)) { _media = std::make_unique( this, std::make_unique( this, sticker.document, skipPremiumEffect, replacing, sticker.replacements)); } else { _media = std::make_unique( this, std::make_unique(this, emoji)); } } else { _media = nullptr; } } Ui::Text::IsolatedEmoji Element::isolatedEmoji() const { return _text.toIsolatedEmoji(); } Ui::Text::OnlyCustomEmoji Element::onlyCustomEmoji() const { return _text.toOnlyCustomEmoji(); } const Ui::Text::String &Element::text() const { return _text; } int Element::textHeightFor(int textWidth) { validateText(); if (_textWidth != textWidth) { _textWidth = textWidth; _textHeight = _text.countHeight(textWidth); } return _textHeight; } void Element::validateText() { const auto item = data(); const auto &text = item->_text; if (_text.isEmpty() == text.empty()) { return; } const auto context = Core::MarkedTextContext{ .session = &history()->session(), .customEmojiRepaint = [=] { customEmojiRepaint(); }, }; if (_flags & Flag::ServiceMessage) { _text.setMarkedText( st::serviceTextStyle, text, Ui::ItemTextServiceOptions(), context); auto linkIndex = 0; for (const auto &link : item->customTextLinks()) { // Link indices start with 1. _text.setLink(++linkIndex, link); } } else { clearSpecialOnlyEmoji(); const auto context = Core::MarkedTextContext{ .session = &history()->session(), .customEmojiRepaint = [=] { customEmojiRepaint(); }, }; _text.setMarkedText( st::messageTextStyle, item->originalTextWithLocalEntities(), Ui::ItemTextOptions(item), context); if (!text.empty() && _text.isEmpty()) { // If server has allowed some text that we've trim-ed entirely, // just replace it with something so that UI won't look buggy. _text.setMarkedText( st::messageTextStyle, { u":-("_q }, Ui::ItemTextOptions(item)); } if (!item->media()) { checkSpecialOnlyEmoji(); refreshMedia(nullptr); } } FillTextWithAnimatedSpoilers(this, _text); _textWidth = -1; _textHeight = 0; } void Element::validateTextSkipBlock(bool has, int width, int height) { validateText(); if (!has) { if (_text.removeSkipBlock()) { _textWidth = -1; _textHeight = 0; } } else if (_text.updateSkipBlock(width, height)) { _textWidth = -1; _textHeight = 0; } } void Element::previousInBlocksChanged() { recountDisplayDateInBlocks(); recountAttachToPreviousInBlocks(); } void Element::nextInBlocksRemoved() { setAttachToNext(false); } bool Element::markSponsoredViewed(int shownFromTop) const { const auto sponsoredTextTop = height() - st::msgPadding.bottom() - st::historyViewButtonHeight; return shownFromTop >= sponsoredTextTop; } void Element::refreshDataId() { if (const auto media = this->media()) { media->refreshParentId(data()); } refreshDataIdHook(); } bool Element::computeIsAttachToPrevious(not_null previous) { const auto mayBeAttached = [](not_null view) { const auto item = view->data(); return !item->isService() && !item->isEmpty() && !item->isPost() && (!item->history()->peer->isMegagroup() || !view->hasOutLayout() || !item->from()->isChannel()); }; const auto item = data(); if (!Has() && !Has()) { const auto prev = previous->data(); const auto previousMarkup = prev->inlineReplyMarkup(); const auto possible = (std::abs(prev->date() - item->date()) < kAttachMessageToPreviousSecondsDelta) && mayBeAttached(this) && mayBeAttached(previous) && (!previousMarkup || previousMarkup->hiddenBy(prev->media())); if (possible) { const auto forwarded = item->Get(); const auto prevForwarded = prev->Get(); if (item->history()->peer->isSelf() || item->history()->peer->isRepliesChat() || (forwarded && forwarded->imported) || (prevForwarded && prevForwarded->imported)) { return IsAttachedToPreviousInSavedMessages( prev, prevForwarded, item, forwarded); } else { return prev->from() == item->from(); } } } return false; } ClickHandlerPtr Element::fromLink() const { if (_fromLink) { return _fromLink; } const auto item = data(); if (item->isSponsored()) { const auto session = &item->history()->session(); _fromLink = std::make_shared([=]( ClickContext context) { if (context.button != Qt::LeftButton) { return; } const auto my = context.other.value(); if (const auto window = ContextOrSessionWindow(my, session)) { auto &sponsored = session->data().sponsoredMessages(); const auto itemId = my.itemId ? my.itemId : item->fullId(); const auto details = sponsored.lookupDetails(itemId); if (const auto &hash = details.hash) { Api::CheckChatInvite(window, *hash); } else if (const auto peer = details.peer) { window->showPeerInfo(peer); } } }); return _fromLink; } else if (const auto from = item->displayFrom()) { _fromLink = std::make_shared([=]( ClickContext context) { if (context.button != Qt::LeftButton) { return; } const auto my = context.other.value(); const auto session = &from->session(); if (const auto window = ContextOrSessionWindow(my, session)) { window->showPeerInfo(from); } }); _fromLink->setProperty(kPeerLinkPeerIdProperty, from->id.value); return _fromLink; } if (const auto forwarded = item->Get()) { if (forwarded->imported) { static const auto imported = std::make_shared([]( ClickContext context) { const auto my = context.other.value(); const auto weak = my.sessionWindow; if (const auto strong = weak.get()) { Ui::ShowMultilineToast({ .parentOverride = Window::Show(strong).toastParent(), .text = { tr::lng_forwarded_imported(tr::now) }, }); } }); return imported; } } _fromLink = HiddenSenderInfo::ForwardClickHandler(); return _fromLink; } void Element::createUnreadBar(rpl::producer text) { if (!AddComponents(UnreadBar::Bit())) { return; } const auto bar = Get(); std::move( text ) | rpl::start_with_next([=](const QString &text) { if (const auto bar = Get()) { bar->init(text); } }, bar->lifetime); if (data()->mainView() == this) { recountAttachToPreviousInBlocks(); } history()->owner().requestViewResize(this); } void Element::destroyUnreadBar() { if (!Has()) { return; } RemoveComponents(UnreadBar::Bit()); history()->owner().requestViewResize(this); if (data()->mainView() == this) { recountAttachToPreviousInBlocks(); } } int Element::displayedDateHeight() const { if (auto date = Get()) { return date->height(); } return 0; } bool Element::displayDate() const { return Has(); } bool Element::isInOneDayWithPrevious() const { return !data()->isEmpty() && !displayDate(); } void Element::recountAttachToPreviousInBlocks() { if (isHidden() || data()->isEmpty()) { if (const auto next = nextDisplayedInBlocks()) { next->recountAttachToPreviousInBlocks(); } else if (const auto previous = previousDisplayedInBlocks()) { previous->setAttachToNext(false); } return; } auto attachToPrevious = false; const auto previous = previousDisplayedInBlocks(); if (previous) { attachToPrevious = computeIsAttachToPrevious(previous); previous->setAttachToNext(attachToPrevious, this); } setAttachToPrevious(attachToPrevious, previous); } void Element::recountDisplayDateInBlocks() { setDisplayDate([&] { const auto item = data(); if (isHidden() || item->isEmpty()) { return false; } if (item->isSponsored()) { return false; } if (const auto previous = previousDisplayedInBlocks()) { const auto prev = previous->data(); return prev->isEmpty() || (previous->dateTime().date() != dateTime().date()); } return true; }()); } QSize Element::countOptimalSize() { return performCountOptimalSize(); } QSize Element::countCurrentSize(int newWidth) { if (_flags & Flag::NeedsResize) { _flags &= ~Flag::NeedsResize; initDimensions(); } return performCountCurrentSize(newWidth); } void Element::setDisplayDate(bool displayDate) { const auto item = data(); if (displayDate && !Has()) { AddComponents(DateBadge::Bit()); Get()->init( ItemDateText(item, (_flags & Flag::ScheduledUntilOnline))); setPendingResize(); } else if (!displayDate && Has()) { RemoveComponents(DateBadge::Bit()); setPendingResize(); } } void Element::setAttachToNext(bool attachToNext, Element *next) { Expects(next || !attachToNext); auto pending = false; if (attachToNext && !(_flags & Flag::AttachedToNext)) { _flags |= Flag::AttachedToNext; pending = true; } else if (!attachToNext && (_flags & Flag::AttachedToNext)) { _flags &= ~Flag::AttachedToNext; pending = true; } const auto bubble = attachToNext && !next->unwrapped(); if (bubble && !(_flags & Flag::BubbleAttachedToNext)) { _flags |= Flag::BubbleAttachedToNext; pending = true; } else if (!bubble && (_flags & Flag::BubbleAttachedToNext)) { _flags &= ~Flag::BubbleAttachedToNext; pending = true; } if (pending) { setPendingResize(); } } void Element::setAttachToPrevious(bool attachToPrevious, Element *previous) { Expects(previous || !attachToPrevious); auto pending = false; if (attachToPrevious && !(_flags & Flag::AttachedToPrevious)) { _flags |= Flag::AttachedToPrevious; pending = true; } else if (!attachToPrevious && (_flags & Flag::AttachedToPrevious)) { _flags &= ~Flag::AttachedToPrevious; pending = true; } const auto bubble = attachToPrevious && !previous->unwrapped(); if (bubble && !(_flags & Flag::BubbleAttachedToPrevious)) { _flags |= Flag::BubbleAttachedToPrevious; pending = true; } else if (!bubble && (_flags & Flag::BubbleAttachedToPrevious)) { _flags &= ~Flag::BubbleAttachedToPrevious; pending = true; } if (pending) { setPendingResize(); } } bool Element::displayFromPhoto() const { return false; } bool Element::hasFromPhoto() const { return false; } bool Element::hasFromName() const { return false; } bool Element::displayFromName() const { return false; } bool Element::displayForwardedFrom() const { return false; } bool Element::hasOutLayout() const { return false; } bool Element::drawBubble() const { return false; } bool Element::hasBubble() const { return false; } bool Element::unwrapped() const { return true; } bool Element::hasFastReply() const { return false; } bool Element::displayFastReply() const { return false; } std::optional Element::rightActionSize() const { return std::nullopt; } void Element::drawRightAction( Painter &p, const PaintContext &context, int left, int top, int outerWidth) const { } ClickHandlerPtr Element::rightActionLink() const { return ClickHandlerPtr(); } TimeId Element::displayedEditDate() const { return TimeId(0); } HistoryMessageReply *Element::displayedReply() const { return nullptr; } bool Element::toggleSelectionByHandlerClick( const ClickHandlerPtr &handler) const { return false; } bool Element::hasVisibleText() const { return false; } auto Element::verticalRepaintRange() const -> VerticalRepaintRange { return { .top = 0, .height = height() }; } bool Element::hasHeavyPart() const { return _heavyCustomEmoji; } void Element::checkHeavyPart() { if (!hasHeavyPart() && (!_media || !_media->hasHeavyPart())) { history()->owner().unregisterHeavyViewPart(this); } } bool Element::isSignedAuthorElided() const { return false; } void Element::itemDataChanged() { } void Element::itemTextUpdated() { if (const auto media = _media.get()) { media->parentTextUpdated(); } clearSpecialOnlyEmoji(); _text = Ui::Text::String(st::msgMinWidth); _textWidth = -1; _textHeight = 0; if (_media && !data()->media()) { refreshMedia(nullptr); } } void Element::unloadHeavyPart() { history()->owner().unregisterHeavyViewPart(this); if (_media) { _media->unloadHeavyPart(); } if (_heavyCustomEmoji) { _heavyCustomEmoji = false; _text.unloadPersistentAnimation(); if (const auto reply = data()->Get()) { reply->replyToText.unloadPersistentAnimation(); } } } HistoryBlock *Element::block() { return _block; } const HistoryBlock *Element::block() const { return _block; } void Element::attachToBlock(not_null block, int index) { Expects(_data->isHistoryEntry()); Expects(_block == nullptr); Expects(_indexInBlock < 0); Expects(index >= 0); _block = block; _indexInBlock = index; _data->setMainView(this); previousInBlocksChanged(); } void Element::removeFromBlock() { Expects(_block != nullptr); _block->remove(this); } void Element::refreshInBlock() { Expects(_block != nullptr); _block->refreshView(this); } void Element::setIndexInBlock(int index) { Expects(_block != nullptr); Expects(index >= 0); _indexInBlock = index; } int Element::indexInBlock() const { Expects((_indexInBlock >= 0) == (_block != nullptr)); Expects((_block == nullptr) || (_block->messages[_indexInBlock].get() == this)); return _indexInBlock; } Element *Element::previousInBlocks() const { if (_block && _indexInBlock >= 0) { if (_indexInBlock > 0) { return _block->messages[_indexInBlock - 1].get(); } if (auto previous = _block->previousBlock()) { Assert(!previous->messages.empty()); return previous->messages.back().get(); } } return nullptr; } Element *Element::previousDisplayedInBlocks() const { auto result = previousInBlocks(); while (result && (result->data()->isEmpty() || result->isHidden())) { result = result->previousInBlocks(); } return result; } Element *Element::nextInBlocks() const { if (_block && _indexInBlock >= 0) { if (_indexInBlock + 1 < _block->messages.size()) { return _block->messages[_indexInBlock + 1].get(); } if (auto next = _block->nextBlock()) { Assert(!next->messages.empty()); return next->messages.front().get(); } } return nullptr; } Element *Element::nextDisplayedInBlocks() const { auto result = nextInBlocks(); while (result && (result->data()->isEmpty() || result->isHidden())) { result = result->nextInBlocks(); } return result; } void Element::drawInfo( Painter &p, const PaintContext &context, int right, int bottom, int width, InfoDisplayType type) const { } TextState Element::bottomInfoTextState( int right, int bottom, QPoint point, InfoDisplayType type) const { return TextState(); } TextSelection Element::adjustSelection( TextSelection selection, TextSelectType type) const { return selection; } Reactions::ButtonParameters Element::reactionButtonParameters( QPoint position, const TextState &reactionState) const { return {}; } int Element::reactionsOptimalWidth() const { return 0; } void Element::clickHandlerActiveChanged( const ClickHandlerPtr &handler, bool active) { if (const auto markup = _data->Get()) { if (const auto keyboard = markup->inlineKeyboard.get()) { keyboard->clickHandlerActiveChanged(handler, active); } } HoveredLink(active ? this : nullptr); repaint(); if (const auto media = this->media()) { media->clickHandlerActiveChanged(handler, active); } } void Element::clickHandlerPressedChanged( const ClickHandlerPtr &handler, bool pressed) { PressedLink(pressed ? this : nullptr); repaint(); if (const auto media = this->media()) { media->clickHandlerPressedChanged(handler, pressed); } } void Element::animateReaction(Ui::ReactionFlyAnimationArgs &&args) { } void Element::animateUnreadReactions() { const auto &recent = data()->recentReactions(); for (const auto &[id, list] : recent) { if (ranges::contains(list, true, &Data::RecentReaction::unread)) { animateReaction({ .id = id }); } } } auto Element::takeReactionAnimations() -> base::flat_map< Data::ReactionId, std::unique_ptr> { return {}; } Element::~Element() { // Delete media while owner still exists. base::take(_media); if (_heavyCustomEmoji) { _heavyCustomEmoji = false; _text.unloadPersistentAnimation(); checkHeavyPart(); } if (_data->mainView() == this) { _data->clearMainView(); } if (_context == Context::History) { history()->owner().notifyViewRemoved(this); } history()->owner().unregisterItemView(this); } void Element::Hovered(Element *view) { HoveredElement = view; } Element *Element::Hovered() { return HoveredElement; } void Element::Pressed(Element *view) { PressedElement = view; } Element *Element::Pressed() { return PressedElement; } void Element::HoveredLink(Element *view) { HoveredLinkElement = view; } Element *Element::HoveredLink() { return HoveredLinkElement; } void Element::PressedLink(Element *view) { PressedLinkElement = view; } Element *Element::PressedLink() { return PressedLinkElement; } void Element::Moused(Element *view) { MousedElement = view; } Element *Element::Moused() { return MousedElement; } void Element::ClearGlobal() { HoveredElement = nullptr; PressedElement = nullptr; HoveredLinkElement = nullptr; PressedLinkElement = nullptr; MousedElement = nullptr; } } // namespace HistoryView