/* 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_reply.h" #include "core/click_handler_types.h" #include "core/ui_integration.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_story.h" #include "data/data_user.h" #include "history/view/history_view_item_preview.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_components.h" #include "history/history_item_helpers.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/chat/chat_style.h" #include "ui/effects/ripple_animation.h" #include "ui/effects/spoiler_mess.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" #include "styles/style_dialogs.h" namespace HistoryView { namespace { constexpr auto kNonExpandedLinesLimit = 5; } // namespace void ValidateBackgroundEmoji( DocumentId backgroundEmojiId, not_null data, not_null cache, not_null quote, not_null view) { if (data->firstFrameMask.isNull() && !data->emoji) { data->emoji = CreateBackgroundEmojiInstance( &view->history()->owner(), backgroundEmojiId, crl::guard(view, [=] { view->repaint(); })); } ValidateBackgroundEmoji(backgroundEmojiId, data, cache, quote); } void ValidateBackgroundEmoji( DocumentId backgroundEmojiId, not_null data, not_null cache, not_null quote) { Expects(!data->firstFrameMask.isNull() || data->emoji != nullptr); if (data->firstFrameMask.isNull()) { if (!cache->frames[0].isNull()) { for (auto &frame : cache->frames) { frame = QImage(); } } if (!data->emoji->ready()) { return; } const auto tag = Data::CustomEmojiSizeTag::Isolated; const auto size = Data::FrameSizeFromTag(tag); data->firstFrameMask = QImage( QSize(size, size), QImage::Format_ARGB32_Premultiplied); data->firstFrameMask.fill(Qt::transparent); data->firstFrameMask.setDevicePixelRatio(style::DevicePixelRatio()); auto p = Painter(&data->firstFrameMask); data->emoji->paint(p, { .textColor = QColor(255, 255, 255), .position = QPoint(0, 0), .internal = { .forceFirstFrame = true, }, }); p.end(); data->emoji = nullptr; } if (!cache->frames[0].isNull() && cache->color == quote->icon) { return; } cache->color = quote->icon; const auto ratio = style::DevicePixelRatio(); auto colorized = QImage( data->firstFrameMask.size(), QImage::Format_ARGB32_Premultiplied); colorized.setDevicePixelRatio(ratio); style::colorizeImage( data->firstFrameMask, cache->color, &colorized, QRect(), // src QPoint(), // dst true); // use alpha const auto make = [&](int size) { size = style::ConvertScale(size) * ratio; auto result = colorized.scaled( size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); result.setDevicePixelRatio(ratio); return result; }; constexpr auto kSize1 = 12; constexpr auto kSize2 = 16; constexpr auto kSize3 = 20; cache->frames[0] = make(kSize1); cache->frames[1] = make(kSize2); cache->frames[2] = make(kSize3); } auto CreateBackgroundEmojiInstance( not_null owner, DocumentId backgroundEmojiId, Fn repaint) -> std::unique_ptr { return owner->customEmojiManager().create( backgroundEmojiId, repaint, Data::CustomEmojiSizeTag::Isolated); } void FillBackgroundEmoji( QPainter &p, const QRect &rect, bool quote, const Ui::BackgroundEmojiCache &cache) { p.setClipRect(rect); const auto &frames = cache.frames; const auto right = rect.x() + rect.width(); const auto paint = [&](int x, int y, int index, float64 opacity) { y = style::ConvertScale(y); if (y >= rect.height()) { return; } p.setOpacity(opacity); p.drawImage( right - style::ConvertScale(x + (quote ? 12 : 0)), rect.y() + y, frames[index]); }; paint(28, 4, 2, 0.32); paint(51, 15, 1, 0.32); paint(64, -2, 0, 0.28); paint(87, 11, 1, 0.24); paint(125, -2, 2, 0.16); paint(28, 31, 1, 0.24); paint(72, 33, 2, 0.2); paint(46, 52, 1, 0.24); paint(24, 55, 2, 0.18); if (quote) { paint(4, 23, 1, 0.28); paint(0, 48, 0, 0.24); } p.setClipping(false); p.setOpacity(1.); } Reply::Reply() : _name(st::maxSignatureSize / 2) , _text(st::maxSignatureSize / 2) { } Reply &Reply::operator=(Reply &&other) = default; Reply::~Reply() = default; void Reply::update( not_null view, not_null data) { const auto item = view->data(); const auto &fields = data->fields(); const auto message = data->resolvedMessage.get(); const auto story = data->resolvedStory.get(); const auto externalMedia = fields.externalMedia.get(); if (!_externalSender) { if (const auto id = fields.externalSenderId) { _externalSender = view->history()->owner().peer(id); } } _colorPeer = message ? message->contentColorsFrom() : story ? story->peer().get() : _externalSender ? _externalSender : nullptr; _hiddenSenderColorIndexPlusOne = (!_colorPeer && message) ? (message->originalHiddenSenderInfo()->colorIndex + 1) : 0; const auto hasPreview = (story && story->hasReplyPreview()) || (message && message->media() && message->media()->hasReplyPreview()) || (externalMedia && externalMedia->hasReplyPreview()); _hasPreview = hasPreview ? 1 : 0; _displaying = data->displaying() ? 1 : 0; _multiline = data->multiline() ? 1 : 0; _replyToStory = (fields.storyId != 0); const auto hasQuoteIcon = _displaying && fields.manualQuote && !fields.quote.empty(); _hasQuoteIcon = hasQuoteIcon ? 1 : 0; const auto text = (!_displaying && data->unavailable()) ? TextWithEntities() : (message && (fields.quote.empty() || !fields.manualQuote)) ? message->inReplyText() : !fields.quote.empty() ? fields.quote : story ? story->inReplyText() : externalMedia ? externalMedia->toPreview({ .hideSender = true, .hideCaption = true, .ignoreMessageText = true, .generateImages = false, .ignoreGroup = true, .ignoreTopic = true, }).text : TextWithEntities(); const auto repaint = [=] { item->customEmojiRepaint(); }; const auto context = Core::MarkedTextContext{ .session = &view->history()->session(), .customEmojiRepaint = repaint, }; _text.setMarkedText( st::defaultTextStyle, text, _multiline ? Ui::ItemTextDefaultOptions() : Ui::DialogTextOptions(), context); updateName(view, data); if (_displaying) { setLinkFrom(view, data); const auto media = message ? message->media() : nullptr; if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) { _spoiler = nullptr; } else if (!_spoiler) { _spoiler = std::make_unique(repaint); } } else { _spoiler = nullptr; } } bool Reply::expand() { if (!_expandable || _expanded) { return false; } _expanded = true; return true; } void Reply::setLinkFrom( not_null view, not_null data) { const auto weak = base::make_weak(view); const auto &fields = data->fields(); const auto externalChannelId = peerToChannel(fields.externalPeerId); const auto messageId = fields.messageId; const auto quote = fields.manualQuote ? fields.quote : TextWithEntities(); const auto quoteOffset = fields.quoteOffset; const auto returnToId = view->data()->fullId(); const auto externalLink = [=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { auto error = QString(); const auto owner = &controller->session().data(); if (const auto view = weak.get()) { if (const auto reply = view->Get()) { if (reply->expand()) { owner->requestViewResize(view); return; } } } if (externalChannelId) { const auto channel = owner->channel(externalChannelId); if (!channel->isForbidden()) { if (messageId) { JumpToMessageClickHandler( channel, messageId, returnToId, quote, quoteOffset )->onClick(context); } else { controller->showPeerInfo(channel); } } else if (channel->isBroadcast()) { error = tr::lng_channel_not_accessible(tr::now); } else { error = tr::lng_group_not_accessible(tr::now); } } else { error = tr::lng_reply_from_private_chat(tr::now); } if (!error.isEmpty()) { controller->showToast(error); } } }; const auto message = data->resolvedMessage.get(); const auto story = data->resolvedStory.get(); _link = message ? JumpToMessageClickHandler(message, returnToId, quote, quoteOffset) : story ? JumpToStoryClickHandler(story) : (data->external() && (!fields.messageId || (data->unavailable() && externalChannelId))) ? std::make_shared(externalLink) : nullptr; } PeerData *Reply::sender( not_null view, not_null data) const { const auto message = data->resolvedMessage.get(); if (const auto story = data->resolvedStory.get()) { return story->peer(); } else if (!message) { return _externalSender; } else if (view->data()->Has()) { // Forward of a reply. Show reply-to original sender. const auto forwarded = message->Get(); if (forwarded) { return forwarded->originalSender; } } if (const auto from = message->displayFrom()) { return from; } return message->author().get(); } QString Reply::senderName( not_null view, not_null data, bool shorten) const { if (const auto peer = sender(view, data)) { return senderName(peer, shorten); } else if (!data->resolvedMessage) { return data->fields().externalSenderName; } else if (view->data()->Has()) { // Forward of a reply. Show reply-to original sender. const auto forwarded = data->resolvedMessage->Get(); if (forwarded) { Assert(forwarded->originalHiddenSenderInfo != nullptr); return forwarded->originalHiddenSenderInfo->name; } } return QString(); } QString Reply::senderName( not_null peer, bool shorten) const { const auto user = shorten ? peer->asUser() : nullptr; return user ? user->firstName : peer->name(); } bool Reply::isNameUpdated( not_null view, not_null data) const { if (const auto from = sender(view, data)) { if (_nameVersion < from->nameVersion()) { updateName(view, data, from); return true; } } return false; } void Reply::updateName( not_null view, not_null data, std::optional resolvedSender) const { auto viaBotUsername = QString(); const auto message = data->resolvedMessage.get(); const auto forwarded = message ? message->Get() : nullptr; if (message && !forwarded) { if (const auto bot = message->viaBot()) { viaBotUsername = bot->username(); } } const auto history = view->history(); const auto &fields = data->fields(); const auto sender = resolvedSender.value_or(this->sender(view, data)); const auto externalPeer = fields.externalPeerId ? history->owner().peer(fields.externalPeerId).get() : nullptr; const auto displayAsExternal = data->displayAsExternal(view->data()); const auto groupNameAdded = displayAsExternal && externalPeer && (externalPeer != sender) && (externalPeer->isChat() || externalPeer->isMegagroup()); const auto originalNameAdded = !displayAsExternal && forwarded && !message->isDiscussionPost() && (forwarded->forwardOfForward() || (!message->showForwardsFromSender(forwarded) && !view->data()->Has())); const auto shorten = !viaBotUsername.isEmpty() || groupNameAdded || originalNameAdded; const auto name = sender ? senderName(sender, shorten) : senderName(view, data, shorten); const auto previewSkip = _hasPreview ? (st::messageQuoteStyle.outline + st::historyReplyPreviewMargin.left() + st::historyReplyPreview + st::historyReplyPreviewMargin.right() - st::historyReplyPadding.left()) : 0; auto nameFull = TextWithEntities(); if (displayAsExternal && !groupNameAdded && !fields.storyId) { nameFull.append(PeerEmoji(history, sender)); } nameFull.append(name); if (groupNameAdded) { nameFull.append(' ').append(PeerEmoji(history, externalPeer)); nameFull.append(externalPeer->name()); } else if (originalNameAdded) { nameFull.append(' ').append(ForwardEmoji(&history->owner())); nameFull.append(forwarded->originalSender ? forwarded->originalSender->name() : forwarded->originalHiddenSenderInfo->name); } if (!viaBotUsername.isEmpty()) { nameFull.append(u" @"_q).append(viaBotUsername); } const auto context = Core::MarkedTextContext{ .session = &history->session(), .customEmojiRepaint = [] {}, .customEmojiLoopLimit = 1, }; _name.setMarkedText( st::fwdTextStyle, nameFull, Ui::NameTextOptions(), context); if (sender) { _nameVersion = sender->nameVersion(); } const auto nameMaxWidth = previewSkip + _name.maxWidth() + (_hasQuoteIcon ? st::messageTextStyle.blockquote.icon.width() : 0); const auto storySkip = fields.storyId ? (st::dialogsMiniReplyStory.skipText + st::dialogsMiniReplyStory.icon.icon.width()) : 0; const auto optimalTextSize = _multiline ? countMultilineOptimalSize(previewSkip) : QSize( (previewSkip + storySkip + std::min(_text.maxWidth(), st::maxSignatureSize)), st::normalFont->height); _maxWidth = std::max(nameMaxWidth, optimalTextSize.width()); if (!data->displaying()) { const auto unavailable = data->unavailable(); _stateText = ((fields.messageId || fields.storyId) && !unavailable) ? tr::lng_profile_loading(tr::now) : fields.storyId ? tr::lng_deleted_story(tr::now) : tr::lng_deleted_message(tr::now); const auto phraseWidth = st::msgDateFont->width(_stateText); _maxWidth = unavailable ? phraseWidth : std::max(_maxWidth, phraseWidth); } else { _stateText = QString(); } _maxWidth = st::historyReplyPadding.left() + _maxWidth + st::historyReplyPadding.right(); _minHeight = st::historyReplyPadding.top() + st::msgServiceNameFont->height + optimalTextSize.height() + st::historyReplyPadding.bottom(); } int Reply::resizeToWidth(int width) const { _ripple.animation = nullptr; const auto previewSkip = _hasPreview ? (st::messageQuoteStyle.outline + st::historyReplyPreviewMargin.left() + st::historyReplyPreview + st::historyReplyPreviewMargin.right() - st::historyReplyPadding.left()) : 0; if (width >= _maxWidth || !_multiline) { _nameTwoLines = 0; _expandable = _minHeightExpandable; _height = _minHeight; return height(); } const auto innerw = width - st::historyReplyPadding.left() - st::historyReplyPadding.right(); const auto namew = innerw - previewSkip; const auto desiredNameHeight = _name.countHeight(namew); _nameTwoLines = (desiredNameHeight > st::semiboldFont->height) ? 1 : 0; const auto nameh = (_nameTwoLines ? 2 : 1) * st::semiboldFont->height; const auto firstLineSkip = _nameTwoLines ? 0 : previewSkip; auto elided = false; const auto texth = _text.countDimensions( textGeometry(innerw, firstLineSkip, &elided)).height; _expandable = elided ? 1 : 0; _height = st::historyReplyPadding.top() + nameh + std::max(texth, st::normalFont->height) + st::historyReplyPadding.bottom(); return height(); } Ui::Text::GeometryDescriptor Reply::textGeometry( int available, int firstLineSkip, bool *outElided) const { return { .layout = [=](int line) { const auto skip = (line ? 0 : firstLineSkip); const auto elided = !_multiline || (!_expanded && (line + 1 >= kNonExpandedLinesLimit)); return Ui::Text::LineGeometry{ .left = skip, .width = available - skip, .elided = elided, }; }, .outElided = outElided }; } int Reply::height() const { return _height + st::historyReplyTop + st::historyReplyBottom; } QMargins Reply::margins() const { return QMargins(0, st::historyReplyTop, 0, st::historyReplyBottom); } QSize Reply::countMultilineOptimalSize( int previewSkip) const { auto elided = false; const auto max = previewSkip + _text.maxWidth(); const auto result = _text.countDimensions( textGeometry(max, previewSkip, &elided)); _minHeightExpandable = elided ? 1 : 0; return { result.width, std::max(result.height, st::normalFont->height), }; } void Reply::paint( Painter &p, not_null view, const Ui::ChatPaintContext &context, int x, int y, int w, bool inBubble) const { const auto st = context.st; const auto stm = context.messageStyle(); y += st::historyReplyTop; const auto rect = QRect(x, y, w, _height); const auto selected = context.selected(); const auto backgroundEmojiId = _colorPeer ? _colorPeer->backgroundEmojiId() : DocumentId(); const auto colorIndexPlusOne = _colorPeer ? (_colorPeer->colorIndex() + 1) : _hiddenSenderColorIndexPlusOne; const auto useColorIndex = colorIndexPlusOne && !context.outbg; const auto colorPattern = colorIndexPlusOne ? st->colorPatternIndex(colorIndexPlusOne - 1) : 0; const auto cache = !inBubble ? (_hasQuoteIcon ? st->serviceQuoteCache(colorPattern) : st->serviceReplyCache(colorPattern)).get() : useColorIndex ? (_hasQuoteIcon ? st->coloredQuoteCache(selected, colorIndexPlusOne - 1) : st->coloredReplyCache(selected, colorIndexPlusOne - 1)).get() : (_hasQuoteIcon ? stm->quoteCache[colorPattern] : stm->replyCache[colorPattern]).get(); const auto "eSt = _hasQuoteIcon ? st::messageTextStyle.blockquote : st::messageQuoteStyle; const auto backgroundEmoji = backgroundEmojiId ? st->backgroundEmojiData(backgroundEmojiId).get() : nullptr; const auto backgroundEmojiCache = backgroundEmoji ? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex( selected, context.outbg, inBubble, colorIndexPlusOne)] : nullptr; const auto rippleColor = cache->bg; if (!inBubble) { cache->bg = QColor(0, 0, 0, 0); } Ui::Text::ValidateQuotePaintCache(*cache, quoteSt); Ui::Text::FillQuotePaint(p, rect, *cache, quoteSt); if (backgroundEmoji) { ValidateBackgroundEmoji( backgroundEmojiId, backgroundEmoji, backgroundEmojiCache, cache, view); if (!backgroundEmojiCache->frames[0].isNull()) { FillBackgroundEmoji(p, rect, _hasQuoteIcon, *backgroundEmojiCache); } } if (!inBubble) { cache->bg = rippleColor; } if (_ripple.animation) { _ripple.animation->paint(p, x, y, w, &rippleColor); if (_ripple.animation->empty()) { _ripple.animation.reset(); } } auto hasPreview = (_hasPreview != 0); auto previewSkip = hasPreview ? (st::messageQuoteStyle.outline + st::historyReplyPreviewMargin.left() + st::historyReplyPreview + st::historyReplyPreviewMargin.right() - st::historyReplyPadding.left()) : 0; if (hasPreview && w <= st::historyReplyPadding.left() + previewSkip) { hasPreview = false; previewSkip = 0; } const auto pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler); auto textLeft = x + st::historyReplyPadding.left(); auto textTop = y + st::historyReplyPadding.top() + (st::msgServiceNameFont->height * (_nameTwoLines ? 2 : 1)); if (w > st::historyReplyPadding.left()) { if (_displaying) { if (hasPreview) { const auto data = view->data()->Get(); const auto message = data ? data->resolvedMessage.get() : nullptr; const auto media = message ? message->media() : nullptr; const auto image = media ? media->replyPreview() : !data ? nullptr : data->resolvedStory ? data->resolvedStory->replyPreview() : data->fields().externalMedia ? data->fields().externalMedia->replyPreview() : nullptr; if (image) { auto to = style::rtlrect( x + st::historyReplyPreviewMargin.left(), y + st::historyReplyPreviewMargin.top(), st::historyReplyPreview, st::historyReplyPreview, w + 2 * x); const auto preview = image->pixSingle( image->size() / style::DevicePixelRatio(), { .colored = (context.selected() ? &st->msgStickerOverlay() : nullptr), .options = Images::Option::RoundSmall, .outer = to.size(), }); p.drawPixmap(to.x(), to.y(), preview); if (_spoiler) { view->clearCustomEmojiRepaint(); Ui::FillSpoilerRect( p, to, Ui::DefaultImageSpoiler().frame( _spoiler->index( context.now, pausedSpoiler))); } } } const auto textw = w - st::historyReplyPadding.left() - st::historyReplyPadding.right(); const auto namew = textw - previewSkip - (_hasQuoteIcon ? st::messageTextStyle.blockquote.icon.width() : 0); auto firstLineSkip = _nameTwoLines ? 0 : previewSkip; if (namew > 0) { p.setPen(!inBubble ? st->msgImgReplyBarColor()->c : useColorIndex ? FromNameFg(context, colorIndexPlusOne - 1) : stm->msgServiceFg->c); _name.drawLeftElided( p, x + st::historyReplyPadding.left() + previewSkip, y + st::historyReplyPadding.top(), namew, w + 2 * x, _nameTwoLines ? 2 : 1); p.setPen(inBubble ? stm->historyTextFg : st->msgImgReplyBarColor()); view->prepareCustomEmojiPaint(p, context, _text); auto replyToTextPalette = &(!inBubble ? st->imgReplyTextPalette() : useColorIndex ? st->coloredTextPalette(selected, colorIndexPlusOne - 1) : stm->replyTextPalette); if (_replyToStory) { st::dialogsMiniReplyStory.icon.icon.paint( p, textLeft + firstLineSkip, textTop, w + 2 * x, replyToTextPalette->linkFg->c); firstLineSkip += st::dialogsMiniReplyStory.skipText + st::dialogsMiniReplyStory.icon.icon.width(); } auto owned = std::optional(); auto copy = std::optional(); if (inBubble && colorIndexPlusOne) { copy.emplace(*replyToTextPalette); owned.emplace(cache->icon); copy->linkFg = owned->color(); replyToTextPalette = &*copy; } _text.draw(p, { .position = { textLeft, textTop }, .geometry = textGeometry(textw, firstLineSkip), .palette = replyToTextPalette, .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .pausedEmoji = (context.paused || On(PowerSaving::kEmojiChat)), .pausedSpoiler = pausedSpoiler, .elisionLines = 1, }); p.setTextPalette(stm->textPalette); } } else { p.setFont(st::msgDateFont); p.setPen(cache->icon); p.drawTextLeft( textLeft, (y + st::historyReplyPadding.top() + (st::msgDateFont->height / 2)), w + 2 * x, st::msgDateFont->elided( _stateText, x + w - textLeft - st::historyReplyPadding.right())); } } } void Reply::createRippleAnimation( not_null view, QSize size) { _ripple.animation = std::make_unique( st::defaultRippleAnimation, Ui::RippleAnimation::RoundRectMask( size, st::messageQuoteStyle.radius), [=] { view->repaint(); }); } void Reply::saveRipplePoint(QPoint point) const { _ripple.lastPoint = point; } void Reply::addRipple() { if (_ripple.animation) { _ripple.animation->add(_ripple.lastPoint); } } void Reply::stopLastRipple() { if (_ripple.animation) { _ripple.animation->lastStop(); } } TextWithEntities Reply::PeerEmoji( not_null history, PeerData *peer) { return PeerEmoji(&history->owner(), peer); } TextWithEntities Reply::PeerEmoji( not_null owner, PeerData *peer) { using namespace std; const auto icon = !peer ? pair(&st::historyReplyUser, st::historyReplyUserPadding) : peer->isBroadcast() ? pair(&st::historyReplyChannel, st::historyReplyChannelPadding) : (peer->isChannel() || peer->isChat()) ? pair(&st::historyReplyGroup, st::historyReplyGroupPadding) : pair(&st::historyReplyUser, st::historyReplyUserPadding); return Ui::Text::SingleCustomEmoji( owner->customEmojiManager().registerInternalEmoji( *icon.first, icon.second)); } TextWithEntities Reply::ForwardEmoji(not_null owner) { return Ui::Text::SingleCustomEmoji( owner->customEmojiManager().registerInternalEmoji( st::historyReplyForward, st::historyReplyForwardPadding)); } TextWithEntities Reply::ComposePreviewName( not_null history, not_null to, bool quote) { const auto sender = [&] { if (const auto from = to->displayFrom()) { return not_null(from); } return to->author(); }(); const auto toPeer = to->history()->peer; const auto displayAsExternal = (to->history() != history); const auto groupNameAdded = displayAsExternal && (toPeer != sender) && (toPeer->isChat() || toPeer->isMegagroup()); const auto shorten = groupNameAdded || quote; auto nameFull = TextWithEntities(); using namespace HistoryView; if (displayAsExternal && !groupNameAdded) { nameFull.append(Reply::PeerEmoji(history, sender)); } nameFull.append(shorten ? sender->shortName() : sender->name()); if (groupNameAdded) { nameFull.append(' ').append(Reply::PeerEmoji(history, toPeer)); nameFull.append(toPeer->name()); } return (quote ? tr::lng_preview_reply_to_quote : tr::lng_preview_reply_to)( tr::now, lt_name, nameFull, Ui::Text::WithEntities); } void Reply::unloadPersistentAnimation() { _text.unloadPersistentAnimation(); } } // namespace HistoryView