/* 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 "base/qt/qt_key_modifiers.h" #include "lang/lang_keys.h" #include "ui/effects/ripple_animation.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 "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/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 "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 namespace { const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_"; constexpr auto kReplyBarAlpha = 230. / 255.; } // 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) : name(name) , colorPeerId(Data::FakePeerIdForJustName(name)) , emptyUserpic( Ui::EmptyUserpic::UserpicColor(Data::PeerColorIndex(colorPeerId)), (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()) { Ui::Toast::Show( Window::Show(strong).toastParent(), 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() : hiddenSenderInfo->name) }; if (!originalAuthor.isEmpty()) { phrase = tr::lng_forwarded_signed( tr::now, lt_channel, name, lt_user, { .text = originalAuthor }, Ui::Text::WithEntities); } else { phrase = name; } 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); } } bool HistoryMessageReply::updateData( not_null holder, bool force) { const auto guard = gsl::finally([&] { refreshReplyToMedia(); }); if (!force) { if (replyToMsg || !replyToMsgId) { return true; } } if (!replyToMsg) { replyToMsg = holder->history()->owner().message( (replyToPeerId ? replyToPeerId : holder->history()->peer->id), replyToMsgId); if (replyToMsg) { if (replyToMsg->isEmpty()) { // Really it is deleted. replyToMsg = nullptr; force = true; } else { holder->history()->owner().registerDependentMessage( holder, replyToMsg.get()); } } } if (replyToMsg) { const auto repaint = [=] { holder->customEmojiRepaint(); }; const auto context = Core::MarkedTextContext{ .session = &holder->history()->session(), .customEmojiRepaint = repaint, }; replyToText.setMarkedText( st::messageTextStyle, replyToMsg->inReplyText(), Ui::DialogTextOptions(), context); updateName(holder); setReplyToLinkFrom(holder); if (!replyToMsg->Has()) { if (auto bot = replyToMsg->viaBot()) { replyToVia = std::make_unique(); replyToVia->create( &holder->history()->owner(), peerToUser(bot->id)); } } { const auto peer = replyToMsg->history()->peer; replyToColorKey = (!holder->out() && (peer->isMegagroup() || peer->isChat())) ? replyToMsg->from()->id : PeerId(0); } const auto media = replyToMsg->media(); if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) { spoiler = nullptr; } else if (!spoiler) { spoiler = std::make_unique(repaint); } } else if (force) { replyToMsgId = 0; replyToColorKey = PeerId(0); spoiler = nullptr; } if (force) { holder->history()->owner().requestItemResize(holder); } return (replyToMsg || !replyToMsgId); } void HistoryMessageReply::setReplyToLinkFrom( not_null holder) { replyToLnk = replyToMsg ? JumpToMessageClickHandler(replyToMsg.get(), holder->fullId()) : nullptr; } void HistoryMessageReply::clearData(not_null holder) { replyToVia = nullptr; if (replyToMsg) { holder->history()->owner().unregisterDependentMessage( holder, replyToMsg.get()); replyToMsg = nullptr; } replyToMsgId = 0; refreshReplyToMedia(); } PeerData *HistoryMessageReply::replyToFrom( not_null holder) const { if (!replyToMsg) { return nullptr; } else if (holder->Has()) { if (const auto fwd = replyToMsg->Get()) { return fwd->originalSender; } } if (const auto from = replyToMsg->displayFrom()) { return from; } return replyToMsg->author().get(); } QString HistoryMessageReply::replyToFromName( not_null holder) const { if (!replyToMsg) { return QString(); } else if (holder->Has()) { if (const auto fwd = replyToMsg->Get()) { return fwd->originalSender ? replyToFromName(fwd->originalSender) : fwd->hiddenSenderInfo->name; } } if (const auto from = replyToMsg->displayFrom()) { return replyToFromName(from); } return replyToFromName(replyToMsg->author()); } QString HistoryMessageReply::replyToFromName( not_null peer) const { if (const auto user = replyToVia ? peer->asUser() : nullptr) { return user->firstName; } return peer->name(); } bool HistoryMessageReply::isNameUpdated( not_null holder) const { if (const auto from = replyToFrom(holder)) { if (replyToVersion < from->nameVersion()) { updateName(holder); return true; } } return false; } void HistoryMessageReply::updateName( not_null holder) const { if (const auto name = replyToFromName(holder); !name.isEmpty()) { replyToName.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); if (const auto from = replyToFrom(holder)) { replyToVersion = from->nameVersion(); } else { replyToVersion = replyToMsg->author()->nameVersion(); } bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false; int32 previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; int32 w = replyToName.maxWidth(); if (replyToVia) { w += st::msgServiceFont->spacew + replyToVia->maxWidth; } maxReplyWidth = previewSkip + qMax(w, qMin(replyToText.maxWidth(), int32(st::maxSignatureSize))); } else { maxReplyWidth = st::msgDateFont->width(replyToMsgId ? tr::lng_profile_loading(tr::now) : tr::lng_deleted_message(tr::now)); } maxReplyWidth = st::msgReplyPadding.left() + st::msgReplyBarSkip + maxReplyWidth + st::msgReplyPadding.right(); } void HistoryMessageReply::resize(int width) const { if (replyToVia) { bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false; int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; replyToVia->resize(width - st::msgReplyBarSkip - previewSkip - replyToName.maxWidth() - st::msgServiceFont->spacew); } } void HistoryMessageReply::itemRemoved( HistoryItem *holder, HistoryItem *removed) { if (replyToMsg.get() == removed) { clearData(holder); holder->history()->owner().requestItemResize(holder); } } void HistoryMessageReply::paint( Painter &p, not_null holder, const Ui::ChatPaintContext &context, int x, int y, int w, bool inBubble) const { const auto st = context.st; const auto stm = context.messageStyle(); { const auto &bar = !inBubble ? st->msgImgReplyBarColor() : replyToColorKey ? HistoryView::FromNameFg(context, replyToColorKey) : stm->msgReplyBarColor; const auto rbar = style::rtlrect( x + st::msgReplyBarPos.x(), y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.width(), st::msgReplyBarSize.height(), w + 2 * x); const auto opacity = p.opacity(); p.setOpacity(opacity * kReplyBarAlpha); p.fillRect(rbar, bar); p.setOpacity(opacity); } if (w > st::msgReplyBarSkip) { if (replyToMsg) { const auto media = replyToMsg->media(); auto hasPreview = media && media->hasReplyPreview(); if (hasPreview && w < st::msgReplyBarSkip + st::msgReplyBarSize.height()) { hasPreview = false; } auto previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; if (hasPreview) { if (const auto image = media->replyPreview()) { auto to = style::rtlrect(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height(), 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) { holder->clearCustomEmojiRepaint(); Ui::FillSpoilerRect( p, to, Ui::DefaultImageSpoiler().frame( spoiler->index( context.now, context.paused))); } } } if (w > st::msgReplyBarSkip + previewSkip) { p.setPen(!inBubble ? st->msgImgReplyBarColor() : replyToColorKey ? HistoryView::FromNameFg(context, replyToColorKey) : stm->msgServiceFg); replyToName.drawLeftElided(p, x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top(), w - st::msgReplyBarSkip - previewSkip, w + 2 * x); if (replyToVia && w > st::msgReplyBarSkip + previewSkip + replyToName.maxWidth() + st::msgServiceFont->spacew) { p.setFont(st::msgServiceFont); p.drawText(x + st::msgReplyBarSkip + previewSkip + replyToName.maxWidth() + st::msgServiceFont->spacew, y + st::msgReplyPadding.top() + st::msgServiceFont->ascent, replyToVia->text); } p.setPen(inBubble ? stm->historyTextFg : st->msgImgReplyBarColor()); holder->prepareCustomEmojiPaint(p, context, replyToText); replyToText.draw(p, { .position = QPoint( x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top() + st::msgServiceNameFont->height), .availableWidth = w - st::msgReplyBarSkip - previewSkip, .palette = &(inBubble ? stm->replyTextPalette : st->imgReplyTextPalette()), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .paused = context.paused, .elisionLines = 1, }); p.setTextPalette(stm->textPalette); } } else { p.setFont(st::msgDateFont); p.setPen(inBubble ? stm->msgDateFg : st->msgDateImgFg()); p.drawTextLeft(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2, w + 2 * x, st::msgDateFont->elided(replyToMsgId ? tr::lng_profile_loading(tr::now) : tr::lng_deleted_message(tr::now), w - st::msgReplyBarSkip)); } } } void HistoryMessageReply::refreshReplyToMedia() { replyToDocumentId = 0; replyToWebPageId = 0; if (const auto media = replyToMsg ? replyToMsg->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