/* 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_options.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 "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 "main/main_session.h" #include "window/window_session_controller.h" #include "facades.h" #include "styles/style_widgets.h" #include "styles/style_history.h" #include namespace { const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_"; } // namespace void HistoryMessageVia::create(UserId userId) { bot = Auth().data().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] { if (QGuiApplication::keyboardModifiers() == Qt::ControlModifier) { if (const auto window = App::wnd()) { if (const auto controller = window->sessionController()) { controller->showPeerInfo(bot); return; } } } 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) { 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) : name(name) , colorPeerId(Data::FakePeerIdForJustName(name)) , userpic(Data::PeerUserpicColor(colorPeerId), name) { nameText.setText(st::msgNameStyle, name, Ui::NameTextOptions()); const auto parts = name.trimmed().split(' ', QString::SkipEmptyParts); 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::Current().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( 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(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, int x, int y, int w, PaintFlags flags) const { bool selected = (flags & PaintFlag::Selected), outbg = holder->hasOutLayout(); style::color bar = st::msgImgReplyBarColor; if (flags & PaintFlag::InBubble) { bar = (flags & PaintFlag::Selected) ? (outbg ? st::msgOutReplyBarSelColor : st::msgInReplyBarSelColor) : (outbg ? st::msgOutReplyBarColor : st::msgInReplyBarColor); } 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(replyToMsg->fullId(), previewWidth, previewHeight, to.width(), to.height(), ImageRoundRadius::Small, RectPart::AllCorners, selected ? &st::msgStickerOverlay : nullptr); p.drawPixmap(to.x(), to.y(), preview); } } if (w > st::msgReplyBarSkip + previewSkip) { if (flags & PaintFlag::InBubble) { p.setPen(selected ? (outbg ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (outbg ? st::msgOutServiceFg : st::msgInServiceFg)); } else { p.setPen(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); } if (flags & PaintFlag::InBubble) { p.setPen(outbg ? (selected ? st::historyTextOutFgSelected : st::historyTextOutFg) : (selected ? st::historyTextInFgSelected : st::historyTextInFg)); p.setTextPalette(outbg ? (selected ? st::outReplyTextPaletteSelected : st::outReplyTextPalette) : (selected ? st::inReplyTextPaletteSelected : st::inReplyTextPalette)); } else { p.setTextPalette(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(selected ? (outbg ? st::outTextPaletteSelected : st::inTextPaletteSelected) : (outbg ? st::outTextPalette : st::inTextPalette)); } } else { p.setFont(st::msgDateFont); auto &date = outbg ? (selected ? st::msgOutDateFgSelected : st::msgOutDateFg) : (selected ? st::msgInDateFgSelected : st::msgInDateFg); p.setPen((flags & PaintFlag::InBubble) ? date : 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( int row, int column, FullMsgId context) : _itemId(context) , _row(row) , _column(column) { } // Copy to clipboard support. QString ReplyMarkupClickHandler::copyToClipboardText() const { if (const auto button = getButton()) { using Type = HistoryMessageMarkupButton::Type; if (button->type == Type::Url || button->type == Type::Auth) { return QString::fromUtf8(button->data); } } return QString(); } QString ReplyMarkupClickHandler::copyToClipboardContextItemText() const { if (const auto button = getButton()) { using Type = HistoryMessageMarkupButton::Type; if (button->type == Type::Url || button->type == Type::Auth) { return tr::lng_context_copy_link(tr::now); } } return 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(_itemId, _row, _column); } void ReplyMarkupClickHandler::onClickImpl() const { if (const auto item = Auth().data().message(_itemId)) { App::activateBotCommand(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(); } 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