/* 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/history_item_components.h" #include "api/api_text_entities.h" #include "base/qt/qt_key_modifiers.h" #include "lang/lang_keys.h" #include "ui/effects/ripple_animation.h" #include "ui/effects/spoiler_mess.h" #include "ui/image/image.h" #include "ui/toast/toast.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/view/history_view_message.h" // FromNameFg. #include "history/view/history_view_service_message.h" #include "history/view/media/history_view_document.h" #include "core/click_handler_types.h" #include "core/ui_integration.h" #include "layout/layout_position.h" #include "mainwindow.h" #include "media/audio/media_audio.h" #include "media/player/media_player_instance.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" #include "data/data_media_types.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_file_origin.h" #include "data/data_document.h" #include "data/data_web_page.h" #include "data/data_file_click_handler.h" #include "data/data_scheduled_messages.h" #include "data/data_session.h" #include "data/data_stories.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "api/api_bot.h" #include "styles/style_widgets.h" #include "styles/style_chat.h" #include "styles/style_dialogs.h" // dialogsMiniReplyStory. #include namespace { const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_"; } // namespace void HistoryMessageVia::create( not_null owner, UserId userId) { bot = owner->user(userId); maxWidth = st::msgServiceNameFont->width( tr::lng_inline_bot_via( tr::now, lt_inline_bot, '@' + bot->username())); link = std::make_shared([bot = this->bot]( ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { if (base::IsCtrlPressed()) { controller->showPeerInfo(bot); return; } else if (!bot->isBot() || bot->botInfo->inlinePlaceholder.isEmpty()) { controller->showPeerHistory( bot->id, Window::SectionShow::Way::Forward); return; } } const auto delegate = my.elementDelegate ? my.elementDelegate() : nullptr; if (delegate) { delegate->elementHandleViaClick(bot); } }); } void HistoryMessageVia::resize(int32 availw) const { if (availw < 0) { text = QString(); width = 0; } else { text = tr::lng_inline_bot_via( tr::now, lt_inline_bot, '@' + bot->username()); if (availw < maxWidth) { text = st::msgServiceNameFont->elided(text, availw); width = st::msgServiceNameFont->width(text); } else if (width < maxWidth) { width = maxWidth; } } } HiddenSenderInfo::HiddenSenderInfo( const QString &name, bool external, std::optional colorIndex) : name(name) , colorIndex(colorIndex.value_or( Data::DecideColorIndex(Data::FakePeerIdForJustName(name)))) , emptyUserpic( Ui::EmptyUserpic::UserpicColor(this->colorIndex), (external ? Ui::EmptyUserpic::ExternalName() : name)) { Expects(!name.isEmpty()); const auto parts = name.trimmed().split(' ', Qt::SkipEmptyParts); firstName = parts[0]; for (const auto &part : parts.mid(1)) { if (!lastName.isEmpty()) { lastName.append(' '); } lastName.append(part); } } const Ui::Text::String &HiddenSenderInfo::nameText() const { if (_nameText.isEmpty()) { _nameText.setText(st::msgNameStyle, name, Ui::NameTextOptions()); } return _nameText; } ClickHandlerPtr HiddenSenderInfo::ForwardClickHandler() { static const auto hidden = std::make_shared([]( ClickContext context) { const auto my = context.other.value(); const auto weak = my.sessionWindow; if (const auto strong = weak.get()) { strong->showToast(tr::lng_forwarded_hidden(tr::now)); } }); return hidden; } bool HiddenSenderInfo::paintCustomUserpic( Painter &p, Ui::PeerUserpicView &view, int x, int y, int outerWidth, int size) const { Expects(!customUserpic.empty()); auto valid = true; if (!customUserpic.isCurrentView(view.cloud)) { view.cloud = customUserpic.createView(); valid = false; } const auto image = *view.cloud; if (image.isNull()) { emptyUserpic.paintCircle(p, x, y, outerWidth, size); return valid; } Ui::ValidateUserpicCache( view, image.isNull() ? nullptr : &image, image.isNull() ? &emptyUserpic : nullptr, size * style::DevicePixelRatio(), false); p.drawImage(QRect(x, y, size, size), view.cached); return valid; } void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { auto phrase = TextWithEntities(); const auto fromChannel = originalSender && originalSender->isChannel() && !originalSender->isMegagroup(); const auto name = TextWithEntities{ .text = (originalSender ? originalSender->name() : originalHiddenSenderInfo->name) }; if (!originalPostAuthor.isEmpty()) { phrase = tr::lng_forwarded_signed( tr::now, lt_channel, name, lt_user, { .text = originalPostAuthor }, Ui::Text::WithEntities); } else { phrase = name; } if (story) { phrase = tr::lng_forwarded_story( tr::now, lt_user, Ui::Text::Link(phrase.text, QString()), // Link 1. Ui::Text::WithEntities); } else if (via && psaType.isEmpty()) { if (fromChannel) { phrase = tr::lng_forwarded_channel_via( tr::now, lt_channel, Ui::Text::Link(phrase.text, 1), // Link 1. lt_inline_bot, Ui::Text::Link('@' + via->bot->username(), 2), // Link 2. Ui::Text::WithEntities); } else { phrase = tr::lng_forwarded_via( tr::now, lt_user, Ui::Text::Link(phrase.text, 1), // Link 1. lt_inline_bot, Ui::Text::Link('@' + via->bot->username(), 2), // Link 2. Ui::Text::WithEntities); } } else { if (fromChannel || !psaType.isEmpty()) { auto custom = psaType.isEmpty() ? QString() : Lang::GetNonDefaultValue( kPsaForwardedPrefix + psaType.toUtf8()); if (!custom.isEmpty()) { custom = custom.replace("{channel}", phrase.text); const auto index = int(custom.indexOf(phrase.text)); const auto size = int(phrase.text.size()); phrase = TextWithEntities{ .text = custom, .entities = {{ EntityType::CustomUrl, index, size, {} }}, }; } else { phrase = (psaType.isEmpty() ? tr::lng_forwarded_channel : tr::lng_forwarded_psa_default)( tr::now, lt_channel, Ui::Text::Link(phrase.text, QString()), // Link 1. Ui::Text::WithEntities); } } else { phrase = tr::lng_forwarded( tr::now, lt_user, Ui::Text::Link(phrase.text, QString()), // Link 1. Ui::Text::WithEntities); } } text.setMarkedText(st::fwdTextStyle, phrase); text.setLink(1, fromChannel ? JumpToMessageClickHandler(originalSender, originalId) : originalSender ? originalSender->openLink() : HiddenSenderInfo::ForwardClickHandler()); if (via) { text.setLink(2, via->link); } } ReplyFields ReplyFields::clone(not_null parent) const { return { .quote = quote, .externalMedia = (externalMedia ? externalMedia->clone(parent) : nullptr), .externalSenderId = externalSenderId, .externalSenderName = externalSenderName, .externalPostAuthor = externalPostAuthor, .externalPeerId = externalPeerId, .messageId = messageId, .topMessageId = topMessageId, .storyId = storyId, .quoteOffset = quoteOffset, .manualQuote = manualQuote, .topicPost = topicPost, }; } ReplyFields ReplyFieldsFromMTP( not_null item, const MTPMessageReplyHeader &reply) { return reply.match([&](const MTPDmessageReplyHeader &data) { auto result = ReplyFields(); if (const auto peer = data.vreply_to_peer_id()) { result.externalPeerId = peerFromMTP(*peer); } const auto owner = &item->history()->owner(); if (const auto id = data.vreply_to_msg_id().value_or_empty()) { result.messageId = data.is_reply_to_scheduled() ? owner->scheduledMessages().localMessageId(id) : id; result.topMessageId = data.vreply_to_top_id().value_or(id); result.topicPost = data.is_forum_topic() ? 1 : 0; } if (const auto header = data.vreply_from()) { const auto &data = header->data(); result.externalPostAuthor = qs(data.vpost_author().value_or_empty()); result.externalSenderId = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(); result.externalSenderName = qs(data.vfrom_name().value_or_empty()); } if (const auto media = data.vreply_media()) { result.externalMedia = HistoryItem::CreateMedia(item, *media); } result.quote = TextWithEntities{ qs(data.vquote_text().value_or_empty()), Api::EntitiesFromMTP( &owner->session(), data.vquote_entities().value_or_empty()), }; result.quoteOffset = data.vquote_offset().value_or_empty(); result.manualQuote = data.is_quote() ? 1 : 0; return result; }, [&](const MTPDmessageReplyStoryHeader &data) { return ReplyFields{ .externalPeerId = peerFromUser(data.vuser_id()), .storyId = data.vstory_id().v, }; }); } FullReplyTo ReplyToFromMTP( not_null history, const MTPInputReplyTo &reply) { return reply.match([&](const MTPDinputReplyToMessage &data) { auto result = FullReplyTo{ .messageId = { history->peer->id, data.vreply_to_msg_id().v }, }; if (const auto peer = data.vreply_to_peer_id()) { const auto parsed = Data::PeerFromInputMTP( &history->owner(), *peer); if (!parsed) { return FullReplyTo(); } result.messageId.peer = parsed->id; } result.topicRootId = data.vtop_msg_id().value_or_empty(); result.quote = TextWithEntities{ qs(data.vquote_text().value_or_empty()), Api::EntitiesFromMTP( &history->session(), data.vquote_entities().value_or_empty()), }; result.quoteOffset = data.vquote_offset().value_or_empty(); return result; }, [&](const MTPDinputReplyToStory &data) { if (const auto parsed = Data::UserFromInputMTP( &history->owner(), data.vuser_id())) { return FullReplyTo{ .storyId = { parsed->id, data.vstory_id().v }, }; } return FullReplyTo(); }); } HistoryMessageReply::HistoryMessageReply() = default; HistoryMessageReply &HistoryMessageReply::operator=( HistoryMessageReply &&other) = default; HistoryMessageReply::~HistoryMessageReply() { // clearData() should be called by holder. Expects(resolvedMessage.empty()); _fields.externalMedia = nullptr; } void HistoryMessageReply::updateData( not_null holder, bool force) { const auto guard = gsl::finally([&] { refreshReplyToMedia(); }); if (!force) { if (resolvedMessage || resolvedStory || _unavailable) { _pendingResolve = 0; return; } } const auto peerId = _fields.externalPeerId ? _fields.externalPeerId : holder->history()->peer->id; if (!resolvedMessage && _fields.messageId) { resolvedMessage = holder->history()->owner().message( peerId, _fields.messageId); if (resolvedMessage) { if (resolvedMessage->isEmpty()) { // Really it is deleted. resolvedMessage = nullptr; force = true; } else { holder->history()->owner().registerDependentMessage( holder, resolvedMessage.get()); } } } if (!resolvedStory && _fields.storyId) { const auto maybe = holder->history()->owner().stories().lookup({ peerId, _fields.storyId, }); if (maybe) { resolvedStory = *maybe; holder->history()->owner().stories().registerDependentMessage( holder, resolvedStory.get()); } else if (maybe.error() == Data::NoStory::Deleted) { force = true; } } const auto asExternal = displayAsExternal(holder); const auto nonEmptyQuote = !_fields.quote.empty() && (asExternal || _fields.manualQuote); _multiline = !_fields.storyId && (asExternal || nonEmptyQuote); const auto displaying = resolvedMessage || resolvedStory || ((nonEmptyQuote || _fields.externalMedia) && (!_fields.messageId || force)); _displaying = displaying ? 1 : 0; const auto unavailable = !resolvedMessage && !resolvedStory && ((!_fields.storyId && !_fields.messageId) || force); _unavailable = unavailable ? 1 : 0; if (force) { if (!_displaying && (_fields.messageId || _fields.storyId)) { _unavailable = 1; } holder->history()->owner().requestItemResize(holder); } if (resolvedMessage || resolvedStory || (!_fields.messageId && !_fields.storyId && external()) || _unavailable) { _pendingResolve = 0; } else if (!force) { _pendingResolve = 1; _requestedResolve = 0; } } void HistoryMessageReply::set(ReplyFields fields) { _fields = std::move(fields); } void HistoryMessageReply::updateFields( not_null holder, MsgId messageId, MsgId topMessageId, bool topicPost) { _fields.topicPost = topicPost ? 1 : 0; if ((_fields.messageId != messageId) && !IsServerMsgId(_fields.messageId)) { _fields.messageId = messageId; updateData(holder); } if ((_fields.topMessageId != topMessageId) && !IsServerMsgId(_fields.topMessageId)) { _fields.topMessageId = topMessageId; } } bool HistoryMessageReply::acquireResolve() { if (!_pendingResolve || _requestedResolve) { return false; } _requestedResolve = 1; return true; } void HistoryMessageReply::setTopMessageId(MsgId topMessageId) { _fields.topMessageId = topMessageId; } void HistoryMessageReply::clearData(not_null holder) { if (resolvedMessage) { holder->history()->owner().unregisterDependentMessage( holder, resolvedMessage.get()); resolvedMessage = nullptr; } if (resolvedStory) { holder->history()->owner().stories().unregisterDependentMessage( holder, resolvedStory.get()); resolvedStory = nullptr; } _unavailable = 1; _displaying = 0; if (_multiline) { holder->history()->owner().requestItemResize(holder); _multiline = 0; } refreshReplyToMedia(); } bool HistoryMessageReply::external() const { return _fields.externalPeerId || _fields.externalSenderId || !_fields.externalSenderName.isEmpty(); } bool HistoryMessageReply::displayAsExternal( not_null holder) const { // Don't display replies that could be local as external. return external() && (!resolvedMessage || (holder->history() != resolvedMessage->history()) || (holder->topicRootId() != resolvedMessage->topicRootId())); } void HistoryMessageReply::itemRemoved( not_null holder, not_null removed) { if (resolvedMessage.get() == removed) { clearData(holder); holder->history()->owner().requestItemResize(holder); } } void HistoryMessageReply::storyRemoved( not_null holder, not_null removed) { if (resolvedStory.get() == removed) { clearData(holder); holder->history()->owner().requestItemResize(holder); } } void HistoryMessageReply::refreshReplyToMedia() { replyToDocumentId = 0; replyToWebPageId = 0; if (const auto media = resolvedMessage ? resolvedMessage->media() : nullptr) { if (const auto document = media->document()) { replyToDocumentId = document->id; } else if (const auto webpage = media->webpage()) { replyToWebPageId = webpage->id; } } } ReplyMarkupClickHandler::ReplyMarkupClickHandler( not_null owner, int row, int column, FullMsgId context) : _owner(owner) , _itemId(context) , _row(row) , _column(column) { } // Copy to clipboard support. QString ReplyMarkupClickHandler::copyToClipboardText() const { const auto button = getUrlButton(); return button ? QString::fromUtf8(button->data) : QString(); } QString ReplyMarkupClickHandler::copyToClipboardContextItemText() const { const auto button = getUrlButton(); return button ? tr::lng_context_copy_link(tr::now) : QString(); } // Finds the corresponding button in the items markup struct. // If the button is not found it returns nullptr. // Note: it is possible that we will point to the different button // than the one was used when constructing the handler, but not a big deal. const HistoryMessageMarkupButton *ReplyMarkupClickHandler::getButton() const { return HistoryMessageMarkupButton::Get(_owner, _itemId, _row, _column); } auto ReplyMarkupClickHandler::getUrlButton() const -> const HistoryMessageMarkupButton* { if (const auto button = getButton()) { using Type = HistoryMessageMarkupButton::Type; if (button->type == Type::Url || button->type == Type::Auth) { return button; } } return nullptr; } void ReplyMarkupClickHandler::onClick(ClickContext context) const { if (context.button != Qt::LeftButton) { return; } auto my = context.other.value(); my.itemId = _itemId; Api::ActivateBotCommand(my, _row, _column); } // Returns the full text of the corresponding button. QString ReplyMarkupClickHandler::buttonText() const { if (const auto button = getButton()) { return button->text; } return QString(); } QString ReplyMarkupClickHandler::tooltip() const { const auto button = getUrlButton(); const auto url = button ? QString::fromUtf8(button->data) : QString(); const auto text = _fullDisplayed ? QString() : buttonText(); if (!url.isEmpty() && !text.isEmpty()) { return QString("%1\n\n%2").arg(text, url); } else if (url.isEmpty() != text.isEmpty()) { return text + url; } else { return QString(); } } ReplyKeyboard::Button::Button() = default; ReplyKeyboard::Button::Button(Button &&other) = default; ReplyKeyboard::Button &ReplyKeyboard::Button::operator=( Button &&other) = default; ReplyKeyboard::Button::~Button() = default; ReplyKeyboard::ReplyKeyboard( not_null item, std::unique_ptr