/* 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/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) : 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()) { 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() : hiddenSenderInfo->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 ReplyFieldsFromMTP( not_null history, 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); if (result.externalPeerId == history->peer->id) { result.externalPeerId = 0; } } const auto owner = &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(); } if (const auto header = data.vreply_header()) { 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()); } result.quote = TextWithEntities{ qs(data.vquote_text().value_or_empty()), Api::EntitiesFromMTP( &owner->session(), data.vquote_entities().value_or_empty()), }; return result; }, [&](const MTPDmessageReplyStoryHeader &data) { return ReplyFields{ .externalPeerId = peerFromUser(data.vuser_id()), .storyId = data.vstory_id().v, }; }); } HistoryMessageReply::HistoryMessageReply() = default; HistoryMessageReply &HistoryMessageReply::operator=( HistoryMessageReply &&other) = default; HistoryMessageReply::~HistoryMessageReply() { // clearData() should be called by holder. Expects(resolvedMessage.empty()); Expects(originalVia == nullptr); } bool HistoryMessageReply::updateData( not_null holder, bool force) { const auto guard = gsl::finally([&] { refreshReplyToMedia(); }); if (!force) { if (resolvedMessage || resolvedStory || _unavailable) { return true; } } 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 external = _fields.externalSenderId || !_fields.externalSenderName.isEmpty(); if (resolvedMessage || resolvedStory || (external && (!_fields.messageId || force))) { const auto repaint = [=] { holder->customEmojiRepaint(); }; const auto context = Core::MarkedTextContext{ .session = &holder->history()->session(), .customEmojiRepaint = repaint, }; const auto text = !_fields.quote.empty() ? _fields.quote : resolvedMessage ? resolvedMessage->inReplyText() : resolvedStory ? resolvedStory->inReplyText() : TextWithEntities{ u"..."_q }; _text.setMarkedText( st::defaultTextStyle, text, Ui::DialogTextOptions(), context); updateName(holder); setLinkFrom(holder); if (resolvedMessage && !resolvedMessage->Has()) { if (const auto bot = resolvedMessage->viaBot()) { originalVia = std::make_unique(); originalVia->create( &holder->history()->owner(), peerToUser(bot->id)); } } if (resolvedMessage) { const auto peer = resolvedMessage->history()->peer; _colorKey = (!holder->out() && (peer->isMegagroup() || peer->isChat()) && resolvedMessage->from()->isUser()) ? resolvedMessage->from()->id : PeerId(); } else if (!resolvedStory) { _unavailable = true; } const auto media = resolvedMessage ? resolvedMessage->media() : nullptr; if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) { spoiler = nullptr; } else if (!spoiler) { spoiler = std::make_unique(repaint); } } else if (force) { if (_fields.messageId || _fields.storyId) { _unavailable = true; } _colorKey = 0; spoiler = nullptr; } if (force) { holder->history()->owner().requestItemResize(holder); } return resolvedMessage || resolvedStory || (external && !_fields.messageId) || _unavailable; } void HistoryMessageReply::set(ReplyFields fields) { _fields = std::move(fields); } void HistoryMessageReply::updateFields( not_null holder, MsgId messageId, MsgId topMessageId, bool topicPost) { _fields.topicPost = topicPost; if ((_fields.messageId != messageId) && !IsServerMsgId(_fields.messageId)) { _fields.messageId = messageId; if (!updateData(holder)) { RequestDependentMessageItem( holder, _fields.externalPeerId, _fields.messageId); } } if ((_fields.topMessageId != topMessageId) && !IsServerMsgId(_fields.topMessageId)) { _fields.topMessageId = topMessageId; } } void HistoryMessageReply::setLinkFrom( not_null holder) { const auto externalPeerId = _fields.externalSenderId; const auto external = externalPeerId || !_fields.externalSenderName.isEmpty(); const auto externalLink = [=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { if (externalPeerId) { controller->showPeerInfo( controller->session().data().peer(externalPeerId)); } else { controller->showToast(u"External reply"_q); } } }; _link = resolvedMessage ? JumpToMessageClickHandler(resolvedMessage.get(), holder->fullId()) : resolvedStory ? JumpToStoryClickHandler(resolvedStory.get()) : (external && !_fields.messageId) ? std::make_shared(externalLink) : nullptr; } void HistoryMessageReply::setTopMessageId(MsgId topMessageId) { _fields.topMessageId = topMessageId; } void HistoryMessageReply::clearData(not_null holder) { originalVia = nullptr; if (resolvedMessage) { holder->history()->owner().unregisterDependentMessage( holder, resolvedMessage.get()); resolvedMessage = nullptr; } if (resolvedStory) { holder->history()->owner().stories().unregisterDependentMessage( holder, resolvedStory.get()); resolvedStory = nullptr; } _unavailable = true; refreshReplyToMedia(); } PeerData *HistoryMessageReply::sender(not_null holder) const { if (resolvedStory) { return resolvedStory->peer(); } else if (!resolvedMessage) { if (!_externalSender && _fields.externalSenderId) { _externalSender = holder->history()->owner().peer( _fields.externalSenderId); } return _externalSender; } else if (holder->Has()) { // Forward of a reply. Show reply-to original sender. const auto forwarded = resolvedMessage->Get(); if (forwarded) { return forwarded->originalSender; } } if (const auto from = resolvedMessage->displayFrom()) { return from; } return resolvedMessage->author().get(); } QString HistoryMessageReply::senderName( not_null holder) const { if (const auto peer = sender(holder)) { return senderName(peer); } else if (!resolvedMessage) { return _fields.externalSenderName; } else if (holder->Has()) { // Forward of a reply. Show reply-to original sender. const auto forwarded = resolvedMessage->Get(); if (forwarded) { Assert(forwarded->hiddenSenderInfo != nullptr); return forwarded->hiddenSenderInfo->name; } } return QString(); } QString HistoryMessageReply::senderName(not_null peer) const { if (const auto user = originalVia ? peer->asUser() : nullptr) { return user->firstName; } return peer->name(); } bool HistoryMessageReply::isNameUpdated( not_null holder) const { if (const auto from = sender(holder)) { if (_nameVersion < from->nameVersion()) { updateName(holder, from); return true; } } return false; } void HistoryMessageReply::updateName( not_null holder, std::optional resolvedSender) const { const auto peer = resolvedSender.value_or(sender(holder)); const auto name = peer ? senderName(peer) : senderName(holder); if (!name.isEmpty()) { _name.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); if (peer) { _nameVersion = peer->nameVersion(); } bool hasPreview = (resolvedStory && resolvedStory->hasReplyPreview()) || (resolvedMessage && resolvedMessage->media() && resolvedMessage->media()->hasReplyPreview()); int32 previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; int32 w = _name.maxWidth(); if (originalVia) { w += st::msgServiceFont->spacew + originalVia->maxWidth; } _maxWidth = previewSkip + std::max( w, std::min(_text.maxWidth(), st::maxSignatureSize)) + (_fields.storyId ? (st::dialogsMiniReplyStory.skipText + st::dialogsMiniReplyStory.icon.icon.width()) : 0); } else { _maxWidth = st::msgDateFont->width(statePhrase()); } _maxWidth = st::msgReplyPadding.left() + st::msgReplyBarSkip + _maxWidth + st::msgReplyPadding.right(); } void HistoryMessageReply::resize(int width) const { if (originalVia) { bool hasPreview = (resolvedStory && resolvedStory->hasReplyPreview()) || (resolvedMessage && resolvedMessage->media() && resolvedMessage->media()->hasReplyPreview()); int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; originalVia->resize(width - st::msgReplyBarSkip - previewSkip - _name.maxWidth() - st::msgServiceFont->spacew); } } 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::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 opacity = p.opacity(); const auto outerWidth = w + 2 * x; const auto &bar = !inBubble ? st->msgImgReplyBarColor() : _colorKey ? HistoryView::FromNameFg(context, _colorKey) : stm->msgReplyBarColor; const auto rbar = style::rtlrect( x + st::msgReplyBarPos.x(), y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.width(), st::msgReplyBarSize.height(), outerWidth); if (ripple.animation) { const auto colorOverride = &stm->msgWaveformInactive->c; p.setOpacity(st::historyPollRippleOpacity); ripple.animation->paint( p, x - st::msgReplyPadding.left(), y, outerWidth, colorOverride); if (ripple.animation->empty()) { ripple.animation.reset(); } } p.setOpacity(opacity * kBarAlpha); p.fillRect(rbar, bar); p.setOpacity(opacity); } const auto pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler); if (w > st::msgReplyBarSkip) { if (resolvedMessage || resolvedStory || !_text.isEmpty()) { const auto media = resolvedMessage ? resolvedMessage->media() : nullptr; auto hasPreview = (media && media->hasReplyPreview()) || (resolvedStory && resolvedStory->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) { const auto image = media ? media->replyPreview() : resolvedStory->replyPreview(); if (image) { 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, pausedSpoiler))); } } } if (w > st::msgReplyBarSkip + previewSkip) { p.setPen(!inBubble ? st->msgImgReplyBarColor() : _colorKey ? HistoryView::FromNameFg(context, _colorKey) : stm->msgServiceFg); _name.drawLeftElided(p, x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top(), w - st::msgReplyBarSkip - previewSkip, w + 2 * x); if (originalVia && w > st::msgReplyBarSkip + previewSkip + _name.maxWidth() + st::msgServiceFont->spacew) { p.setFont(st::msgServiceFont); p.drawText(x + st::msgReplyBarSkip + previewSkip + _name.maxWidth() + st::msgServiceFont->spacew, y + st::msgReplyPadding.top() + st::msgServiceFont->ascent, originalVia->text); } p.setPen(inBubble ? stm->historyTextFg : st->msgImgReplyBarColor()); holder->prepareCustomEmojiPaint(p, context, _text); auto replyToTextPosition = QPoint( x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top() + st::msgServiceNameFont->height); const auto replyToTextPalette = &(inBubble ? stm->replyTextPalette : st->imgReplyTextPalette()); if (_fields.storyId) { st::dialogsMiniReplyStory.icon.icon.paint( p, replyToTextPosition, w - st::msgReplyBarSkip - previewSkip, replyToTextPalette->linkFg->c); replyToTextPosition += QPoint( st::dialogsMiniReplyStory.skipText + st::dialogsMiniReplyStory.icon.icon.width(), 0); } _text.draw(p, { .position = replyToTextPosition, .availableWidth = w - st::msgReplyBarSkip - previewSkip, .palette = replyToTextPalette, .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .pausedEmoji = (context.paused || On(PowerSaving::kEmojiChat)), .pausedSpoiler = pausedSpoiler, .elisionOneLine = true, }); 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(statePhrase(), w - st::msgReplyBarSkip)); } } } void HistoryMessageReply::unloadPersistentAnimation() { _text.unloadPersistentAnimation(); } QString HistoryMessageReply::statePhrase() const { return ((_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); } 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