/* 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 "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/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "history/history.h" #include "history/history_message.h" #include "history/view/history_view_service_message.h" #include "history/view/media/history_view_document.h" #include "core/click_handler_types.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_file_click_handler.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "facades.h" #include "base/qt_adapters.h" #include "styles/style_widgets.h" #include "styles/style_chat.h" #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) { if (QGuiApplication::keyboardModifiers() == Qt::ControlModifier) { if (const auto window = App::wnd()) { if (const auto controller = window->sessionController()) { controller->showPeerInfo(bot); return; } } } const auto my = context.other.value(); if (const auto delegate = my.elementDelegate ? my.elementDelegate() : nullptr) { delegate->elementHandleViaClick(bot); } else { App::insertBotCommand('@' + bot->username); } }); } 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; } } } void HistoryMessageSigned::refresh(const QString &date) { Expects(!isAnonymousRank); auto name = author; const auto time = qsl(", ") + date; const auto timew = st::msgDateFont->width(time); const auto namew = st::msgDateFont->width(name); isElided = (timew + namew > st::maxSignatureSize); if (isElided) { name = st::msgDateFont->elided(author, st::maxSignatureSize - timew); } signature.setText( st::msgDateTextStyle, name + time, Ui::NameTextOptions()); } int HistoryMessageSigned::maxWidth() const { return signature.maxWidth(); } void HistoryMessageEdited::refresh(const QString &date, bool displayed) { const auto prefix = displayed ? (tr::lng_edited(tr::now) + ' ') : QString(); text.setText(st::msgDateTextStyle, prefix + date, Ui::NameTextOptions()); } int HistoryMessageEdited::maxWidth() const { return text.maxWidth(); } HiddenSenderInfo::HiddenSenderInfo(const QString &name, bool external) : name(name) , colorPeerId(Data::FakePeerIdForJustName(name)) , userpic( Data::PeerUserpicColor(colorPeerId), (external ? Ui::EmptyUserpic::ExternalName() : name)) { nameText.setText(st::msgNameStyle, name, Ui::NameTextOptions()); const auto parts = name.trimmed().split(' ', base::QStringSkipEmptyParts); firstName = parts[0]; for (const auto &part : parts.mid(1)) { if (!lastName.isEmpty()) { lastName.append(' '); } lastName.append(part); } } void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { auto phrase = QString(); const auto fromChannel = originalSender && originalSender->isChannel() && !originalSender->isMegagroup(); const auto name = originalSender ? originalSender->name : hiddenSenderInfo->name; if (!originalAuthor.isEmpty()) { phrase = tr::lng_forwarded_signed( tr::now, lt_channel, name, lt_user, originalAuthor); } else { phrase = name; } if (via && psaType.isEmpty()) { if (fromChannel) { phrase = tr::lng_forwarded_channel_via( tr::now, lt_channel, textcmdLink(1, phrase), lt_inline_bot, textcmdLink(2, '@' + via->bot->username)); } else { phrase = tr::lng_forwarded_via( tr::now, lt_user, textcmdLink(1, phrase), lt_inline_bot, textcmdLink(2, '@' + via->bot->username)); } } else { if (fromChannel || !psaType.isEmpty()) { auto custom = psaType.isEmpty() ? QString() : Lang::GetNonDefaultValue( kPsaForwardedPrefix + psaType.toUtf8()); phrase = !custom.isEmpty() ? custom.replace("{channel}", textcmdLink(1, phrase)) : (psaType.isEmpty() ? tr::lng_forwarded_channel : tr::lng_forwarded_psa_default)( tr::now, lt_channel, textcmdLink(1, phrase)); } else { phrase = tr::lng_forwarded( tr::now, lt_user, textcmdLink(1, phrase)); } } TextParseOptions opts = { TextParseRichText, 0, 0, Qt::LayoutDirectionAuto }; text.setText(st::fwdTextStyle, phrase, opts); static const auto hidden = std::make_shared([] { Ui::Toast::Show(tr::lng_forwarded_hidden(tr::now)); }); text.setLink(1, fromChannel ? goToMessageClickHandler(originalSender, originalId) : originalSender ? originalSender->openLink() : hidden); if (via) { text.setLink(2, via->link); } } bool HistoryMessageReply::updateData( not_null holder, bool force) { const auto guard = gsl::finally([&] { refreshReplyToDocument(); }); if (!force) { if (replyToMsg || !replyToMsgId) { return true; } } if (!replyToMsg) { replyToMsg = holder->history()->owner().message( (replyToPeerId ? peerToChannel(replyToPeerId) : holder->channelId()), replyToMsgId); if (replyToMsg) { if (replyToMsg->isEmpty()) { // Really it is deleted. replyToMsg = nullptr; force = true; } else { holder->history()->owner().registerDependentMessage( holder, replyToMsg); } } } if (replyToMsg) { replyToText.setText( st::messageTextStyle, replyToMsg->inReplyText(), Ui::DialogTextOptions()); updateName(); setReplyToLinkFrom(holder); if (!replyToMsg->Has()) { if (auto bot = replyToMsg->viaBot()) { replyToVia = std::make_unique(); replyToVia->create( &holder->history()->owner(), peerToUser(bot->id)); } } } else if (force) { replyToMsgId = 0; } if (force) { holder->history()->owner().requestItemResize(holder); } return (replyToMsg || !replyToMsgId); } void HistoryMessageReply::setReplyToLinkFrom( not_null holder) { replyToLnk = replyToMsg ? goToMessageClickHandler(replyToMsg, holder->fullId()) : nullptr; } void HistoryMessageReply::clearData(not_null holder) { replyToVia = nullptr; if (replyToMsg) { holder->history()->owner().unregisterDependentMessage( holder, replyToMsg); replyToMsg = nullptr; } replyToMsgId = 0; refreshReplyToDocument(); } bool HistoryMessageReply::isNameUpdated() const { if (replyToMsg && replyToMsg->author()->nameVersion > replyToVersion) { updateName(); return true; } return false; } void HistoryMessageReply::updateName() const { if (replyToMsg) { const auto from = [&] { if (const auto from = replyToMsg->displayFrom()) { return from; } return replyToMsg->author().get(); }(); const auto name = (replyToVia && from->isUser()) ? from->asUser()->firstName : from->name; replyToName.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); 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( HistoryMessage *holder, HistoryItem *removed) { if (replyToMsg == 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 ? stm->msgReplyBarColor : st->msgImgReplyBarColor(); QRect rbar(style::rtlrect(x + st::msgReplyBarPos.x(), y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.width(), st::msgReplyBarSize.height(), w + 2 * x)); p.fillRect(rbar, bar); if (w > st::msgReplyBarSkip) { if (replyToMsg) { auto hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false; 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 = replyToMsg->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); auto previewWidth = image->width() / cIntRetinaFactor(); auto previewHeight = image->height() / cIntRetinaFactor(); auto preview = image->pixSingle( previewWidth, previewHeight, to.width(), to.height(), ImageRoundRadius::Small, RectPart::AllCorners, context.selected() ? &st->msgStickerOverlay() : nullptr); p.drawPixmap(to.x(), to.y(), preview); } } if (w > st::msgReplyBarSkip + previewSkip) { p.setPen(inBubble ? stm->msgServiceFg : st->msgImgReplyBarColor()); 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()); p.setTextPalette(inBubble ? stm->replyTextPalette : st->imgReplyTextPalette()); replyToText.drawLeftElided(p, x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top() + st::msgServiceNameFont->height, w - st::msgReplyBarSkip - previewSkip, w + 2 * x); 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::refreshReplyToDocument() { replyToDocumentId = 0; if (const auto media = replyToMsg ? replyToMsg->media() : nullptr) { if (const auto document = media->document()) { replyToDocumentId = document->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; } if (const auto item = _owner->message(_itemId)) { const auto my = context.other.value(); App::activateBotCommand(my.sessionWindow.get(), item, _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