/* 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 "main/main_session.h" #include "window/window_session_controller.h" #include "facades.h" #include "styles/style_widgets.h" #include "styles/style_history.h" #include <QtGui/QGuiApplication> 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<LambdaClickHandler>([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) { 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) { phrase = tr::lng_forwarded_channel( 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<LambdaClickHandler>([] { 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<HistoryMessage*> holder, bool force) { 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<HistoryMessageForwarded>()) { if (auto bot = replyToMsg->viaBot()) { replyToVia = std::make_unique<HistoryMessageVia>(); 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<HistoryMessage*> holder) { replyToLnk = replyToMsg ? goToMessageClickHandler(replyToMsg, holder->fullId()) : nullptr; } void HistoryMessageReply::clearData(not_null<HistoryMessage*> holder) { replyToVia = nullptr; if (replyToMsg) { holder->history()->owner().unregisterDependentMessage( holder, replyToMsg); replyToMsg = nullptr; } replyToMsgId = 0; } 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<const HistoryView::Element*> 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)); } } } 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<const HistoryItem*> item, std::unique_ptr<Style> &&s) : _item(item) , _selectedAnimation([=](crl::time now) { return selectedAnimationCallback(now); }) , _st(std::move(s)) { if (const auto markup = _item->Get<HistoryMessageReplyMarkup>()) { const auto context = _item->fullId(); const auto rowCount = int(markup->rows.size()); _rows.reserve(rowCount); for (auto i = 0; i != rowCount; ++i) { const auto &row = markup->rows.at(i); const auto rowSize = int(row.size()); auto newRow = std::vector<Button>(); newRow.reserve(rowSize); for (auto j = 0; j != rowSize; ++j) { auto button = Button(); const auto text = row[j].text; button.type = row.at(j).type; button.link = std::make_shared<ReplyMarkupClickHandler>( i, j, context); button.text.setText( _st->textStyle(), TextUtilities::SingleLine(text), _textPlainOptions); button.characters = text.isEmpty() ? 1 : text.size(); newRow.push_back(std::move(button)); } _rows.push_back(std::move(newRow)); } } } void ReplyKeyboard::updateMessageId() { const auto msgId = _item->fullId(); for (const auto &row : _rows) { for (const auto &button : row) { button.link->setMessageId(msgId); } } } void ReplyKeyboard::resize(int width, int height) { _width = width; auto markup = _item->Get<HistoryMessageReplyMarkup>(); auto y = 0.; auto buttonHeight = _rows.empty() ? float64(_st->buttonHeight()) : (float64(height + _st->buttonSkip()) / _rows.size()); for (auto &row : _rows) { int s = row.size(); int widthForButtons = _width - ((s - 1) * _st->buttonSkip()); int widthForText = widthForButtons; int widthOfText = 0; int maxMinButtonWidth = 0; for_const (auto &button, row) { widthOfText += qMax(button.text.maxWidth(), 1); int minButtonWidth = _st->minButtonWidth(button.type); widthForText -= minButtonWidth; accumulate_max(maxMinButtonWidth, minButtonWidth); } bool exact = (widthForText == widthOfText); bool enough = (widthForButtons - s * maxMinButtonWidth) >= widthOfText; float64 x = 0; for (Button &button : row) { int buttonw = qMax(button.text.maxWidth(), 1); float64 textw = buttonw, minw = _st->minButtonWidth(button.type); float64 w = textw; if (exact) { w += minw; } else if (enough) { w = (widthForButtons / float64(s)); textw = w - minw; } else { textw = (widthForText / float64(s)); w = minw + textw; accumulate_max(w, 2 * float64(_st->buttonPadding())); } int rectx = static_cast<int>(std::floor(x)); int rectw = static_cast<int>(std::floor(x + w)) - rectx; button.rect = QRect(rectx, qRound(y), rectw, qRound(buttonHeight - _st->buttonSkip())); if (rtl()) button.rect.setX(_width - button.rect.x() - button.rect.width()); x += w + _st->buttonSkip(); button.link->setFullDisplayed(textw >= buttonw); } y += buttonHeight; } } bool ReplyKeyboard::isEnoughSpace(int width, const style::BotKeyboardButton &st) const { for_const (auto &row, _rows) { int s = row.size(); int widthLeft = width - ((s - 1) * st.margin + s * 2 * st.padding); for_const (auto &button, row) { widthLeft -= qMax(button.text.maxWidth(), 1); if (widthLeft < 0) { if (row.size() > 3) { return false; } else { break; } } } } return true; } void ReplyKeyboard::setStyle(std::unique_ptr<Style> &&st) { _st = std::move(st); } int ReplyKeyboard::naturalWidth() const { auto result = 0; for (const auto &row : _rows) { auto maxMinButtonWidth = 0; for (const auto &button : row) { accumulate_max( maxMinButtonWidth, _st->minButtonWidth(button.type)); } auto rowMaxButtonWidth = 0; for (const auto &button : row) { accumulate_max( rowMaxButtonWidth, qMax(button.text.maxWidth(), 1) + maxMinButtonWidth); } const auto rowSize = int(row.size()); accumulate_max( result, rowSize * rowMaxButtonWidth + (rowSize - 1) * _st->buttonSkip()); } return result; } int ReplyKeyboard::naturalHeight() const { return (_rows.size() - 1) * _st->buttonSkip() + _rows.size() * _st->buttonHeight(); } void ReplyKeyboard::paint(Painter &p, int outerWidth, const QRect &clip) const { Assert(_st != nullptr); Assert(_width > 0); _st->startPaint(p); for (const auto &row : _rows) { for (const auto &button : row) { const auto rect = button.rect; if (rect.y() >= clip.y() + clip.height()) return; if (rect.y() + rect.height() < clip.y()) continue; // just ignore the buttons that didn't layout well if (rect.x() + rect.width() > _width) break; _st->paintButton(p, outerWidth, button); } } } ClickHandlerPtr ReplyKeyboard::getLink(QPoint point) const { Assert(_width > 0); for_const (auto &row, _rows) { for_const (auto &button, row) { QRect rect(button.rect); // just ignore the buttons that didn't layout well if (rect.x() + rect.width() > _width) break; if (rect.contains(point)) { _savedCoords = point; return button.link; } } } return ClickHandlerPtr(); } void ReplyKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (!p) return; _savedActive = active ? p : ClickHandlerPtr(); auto coords = findButtonCoordsByClickHandler(p); if (coords.i >= 0 && _savedPressed != p) { startAnimation(coords.i, coords.j, active ? 1 : -1); } } ReplyKeyboard::ButtonCoords ReplyKeyboard::findButtonCoordsByClickHandler(const ClickHandlerPtr &p) { for (int i = 0, rows = _rows.size(); i != rows; ++i) { auto &row = _rows[i]; for (int j = 0, cols = row.size(); j != cols; ++j) { if (row[j].link == p) { return { i, j }; } } } return { -1, -1 }; } void ReplyKeyboard::clickHandlerPressedChanged( const ClickHandlerPtr &handler, bool pressed) { if (!handler) return; _savedPressed = pressed ? handler : ClickHandlerPtr(); auto coords = findButtonCoordsByClickHandler(handler); if (coords.i >= 0) { auto &button = _rows[coords.i][coords.j]; if (pressed) { if (!button.ripple) { auto mask = Ui::RippleAnimation::roundRectMask( button.rect.size(), _st->buttonRadius()); button.ripple = std::make_unique<Ui::RippleAnimation>( _st->_st->ripple, std::move(mask), [this] { _st->repaint(_item); }); } button.ripple->add(_savedCoords - button.rect.topLeft()); } else { if (button.ripple) { button.ripple->lastStop(); } if (_savedActive != handler) { startAnimation(coords.i, coords.j, -1); } } } } void ReplyKeyboard::startAnimation(int i, int j, int direction) { auto notStarted = _animations.empty(); int indexForAnimation = (i * MatrixRowShift + j + 1) * direction; _animations.remove(-indexForAnimation); if (!_animations.contains(indexForAnimation)) { _animations.emplace(indexForAnimation, crl::now()); } if (notStarted && !_selectedAnimation.animating()) { _selectedAnimation.start(); } } bool ReplyKeyboard::selectedAnimationCallback(crl::time now) { if (anim::Disabled()) { now += st::botKbDuration; } for (auto i = _animations.begin(); i != _animations.end();) { const auto index = std::abs(i->first) - 1; const auto row = (index / MatrixRowShift); const auto col = index % MatrixRowShift; const auto dt = float64(now - i->second) / st::botKbDuration; if (dt >= 1) { _rows[row][col].howMuchOver = (i->first > 0) ? 1 : 0; i = _animations.erase(i); } else { _rows[row][col].howMuchOver = (i->first > 0) ? dt : (1 - dt); ++i; } } _st->repaint(_item); return !_animations.empty(); } void ReplyKeyboard::clearSelection() { for (const auto [relativeIndex, time] : _animations) { const auto index = std::abs(relativeIndex) - 1; const auto row = (index / MatrixRowShift); const auto col = index % MatrixRowShift; _rows[row][col].howMuchOver = 0; } _animations.clear(); _selectedAnimation.stop(); } int ReplyKeyboard::Style::buttonSkip() const { return _st->margin; } int ReplyKeyboard::Style::buttonPadding() const { return _st->padding; } int ReplyKeyboard::Style::buttonHeight() const { return _st->height; } void ReplyKeyboard::Style::paintButton( Painter &p, int outerWidth, const ReplyKeyboard::Button &button) const { const QRect &rect = button.rect; paintButtonBg(p, rect, button.howMuchOver); if (button.ripple) { button.ripple->paint(p, rect.x(), rect.y(), outerWidth); if (button.ripple->empty()) { button.ripple.reset(); } } paintButtonIcon(p, rect, outerWidth, button.type); if (button.type == HistoryMessageMarkupButton::Type::Callback || button.type == HistoryMessageMarkupButton::Type::Game) { if (auto data = button.link->getButton()) { if (data->requestId) { paintButtonLoading(p, rect); } } } int tx = rect.x(), tw = rect.width(); if (tw >= st::botKbStyle.font->elidew + _st->padding * 2) { tx += _st->padding; tw -= _st->padding * 2; } else if (tw > st::botKbStyle.font->elidew) { tx += (tw - st::botKbStyle.font->elidew) / 2; tw = st::botKbStyle.font->elidew; } button.text.drawElided(p, tx, rect.y() + _st->textTop + ((rect.height() - _st->height) / 2), tw, 1, style::al_top); } HistoryMessageMarkupButton::HistoryMessageMarkupButton( Type type, const QString &text, const QByteArray &data, const QString &forwardText, int32 buttonId) : type(type) , text(text) , forwardText(forwardText) , data(data) , buttonId(buttonId) { } HistoryMessageMarkupButton *HistoryMessageMarkupButton::Get( FullMsgId itemId, int row, int column) { if (const auto item = Auth().data().message(itemId)) { if (const auto markup = item->Get<HistoryMessageReplyMarkup>()) { if (row < markup->rows.size()) { auto &buttons = markup->rows[row]; if (column < buttons.size()) { return &buttons[column]; } } } } return nullptr; } void HistoryMessageReplyMarkup::createFromButtonRows( const QVector<MTPKeyboardButtonRow> &list) { rows.clear(); if (list.isEmpty()) { return; } rows.reserve(list.size()); for (const auto &row : list) { row.match([&](const MTPDkeyboardButtonRow &data) { auto row = std::vector<Button>(); row.reserve(data.vbuttons().v.size()); for (const auto &button : data.vbuttons().v) { using Type = Button::Type; button.match([&](const MTPDkeyboardButton &data) { row.emplace_back(Type::Default, qs(data.vtext())); }, [&](const MTPDkeyboardButtonCallback &data) { row.emplace_back( Type::Callback, qs(data.vtext()), qba(data.vdata())); }, [&](const MTPDkeyboardButtonRequestGeoLocation &data) { row.emplace_back(Type::RequestLocation, qs(data.vtext())); }, [&](const MTPDkeyboardButtonRequestPhone &data) { row.emplace_back(Type::RequestPhone, qs(data.vtext())); }, [&](const MTPDkeyboardButtonUrl &data) { row.emplace_back( Type::Url, qs(data.vtext()), qba(data.vurl())); }, [&](const MTPDkeyboardButtonSwitchInline &data) { const auto type = data.is_same_peer() ? Type::SwitchInlineSame : Type::SwitchInline; row.emplace_back(type, qs(data.vtext()), qba(data.vquery())); if (type == Type::SwitchInline) { // Optimization flag. // Fast check on all new messages if there is a switch button to auto-click it. flags |= MTPDreplyKeyboardMarkup_ClientFlag::f_has_switch_inline_button; } }, [&](const MTPDkeyboardButtonGame &data) { row.emplace_back(Type::Game, qs(data.vtext())); }, [&](const MTPDkeyboardButtonBuy &data) { row.emplace_back(Type::Buy, qs(data.vtext())); }, [&](const MTPDkeyboardButtonUrlAuth &data) { row.emplace_back( Type::Auth, qs(data.vtext()), qba(data.vurl()), qs(data.vfwd_text().value_or_empty()), data.vbutton_id().v); }, [&](const MTPDinputKeyboardButtonUrlAuth &data) { LOG(("API Error: inputKeyboardButtonUrlAuth received.")); // Should not get those for the users. }); } if (!row.empty()) { rows.push_back(std::move(row)); } }); } } void HistoryMessageReplyMarkup::create(const MTPReplyMarkup &markup) { flags = 0; rows.clear(); inlineKeyboard = nullptr; switch (markup.type()) { case mtpc_replyKeyboardMarkup: { auto &d = markup.c_replyKeyboardMarkup(); flags = d.vflags().v; createFromButtonRows(d.vrows().v); } break; case mtpc_replyInlineMarkup: { auto &d = markup.c_replyInlineMarkup(); flags = MTPDreplyKeyboardMarkup::Flags(0) | MTPDreplyKeyboardMarkup_ClientFlag::f_inline; createFromButtonRows(d.vrows().v); } break; case mtpc_replyKeyboardHide: { auto &d = markup.c_replyKeyboardHide(); flags = mtpCastFlags(d.vflags()) | MTPDreplyKeyboardMarkup_ClientFlag::f_zero; } break; case mtpc_replyKeyboardForceReply: { auto &d = markup.c_replyKeyboardForceReply(); flags = mtpCastFlags(d.vflags()) | MTPDreplyKeyboardMarkup_ClientFlag::f_force_reply; } break; } } void HistoryMessageReplyMarkup::create( const HistoryMessageReplyMarkup &markup) { flags = markup.flags; inlineKeyboard = nullptr; rows.clear(); rows.reserve(markup.rows.size()); using Type = HistoryMessageMarkupButton::Type; for (const auto &existing : markup.rows) { auto row = std::vector<Button>(); row.reserve(existing.size()); for (const auto &button : existing) { const auto newType = (button.type != Type::SwitchInlineSame) ? button.type : Type::SwitchInline; const auto text = button.forwardText.isEmpty() ? button.text : button.forwardText; row.emplace_back( newType, text, button.data, QString(), button.buttonId); } if (!row.empty()) { rows.push_back(std::move(row)); } } } HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal() = default; HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal( HistoryMessageLogEntryOriginal &&other) : page(std::move(other.page)) { } HistoryMessageLogEntryOriginal &HistoryMessageLogEntryOriginal::operator=( HistoryMessageLogEntryOriginal &&other) { page = std::move(other.page); return *this; } HistoryMessageLogEntryOriginal::~HistoryMessageLogEntryOriginal() = default; HistoryDocumentCaptioned::HistoryDocumentCaptioned() : _caption(st::msgFileMinWidth - st::msgPadding.left() - st::msgPadding.right()) { } HistoryDocumentVoicePlayback::HistoryDocumentVoicePlayback( const HistoryView::Document *that) : progress(0., 0.) , progressAnimation([=](crl::time now) { const auto nonconst = const_cast<HistoryView::Document*>(that); return nonconst->voiceProgressAnimationCallback(now); }) { } void HistoryDocumentVoice::ensurePlayback( const HistoryView::Document *that) const { if (!_playback) { _playback = std::make_unique<HistoryDocumentVoicePlayback>(that); } } void HistoryDocumentVoice::checkPlaybackFinished() const { if (_playback && !_playback->progressAnimation.animating()) { _playback.reset(); } } void HistoryDocumentVoice::startSeeking() { _seeking = true; _seekingCurrent = _seekingStart; Media::Player::instance()->startSeeking(AudioMsgId::Type::Voice); } void HistoryDocumentVoice::stopSeeking() { _seeking = false; Media::Player::instance()->cancelSeeking(AudioMsgId::Type::Voice); }