/* This file is part of Telegram Desktop, the official desktop version of Telegram messaging app, see https://telegram.org Telegram Desktop is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. In addition, as a special exception, the copyright holders give permission to link the code of portions of this program with the OpenSSL library. Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "history/history_message.h" #include "lang.h" #include "mainwidget.h" #include "mainwindow.h" #include "apiwrap.h" #include "history/history_location_manager.h" #include "history/history_service_layout.h" #include "history/history_media_types.h" #include "auth_session.h" #include "styles/style_dialogs.h" #include "styles/style_widgets.h" #include "styles/style_history.h" #include "window/notifications_manager.h" namespace { TextParseOptions _historySrvOptions = { TextParseLinks | TextParseMentions | TextParseHashtags/* | TextParseMultiline*/ | TextParseRichText, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // lang-dependent }; inline void initTextOptions() { _historySrvOptions.dir = _textNameOptions.dir = _textDlgOptions.dir = cLangDir(); _textDlgOptions.maxw = st::dialogsWidthMax * 2; } MediaOverviewType messageMediaToOverviewType(HistoryMedia *media) { switch (media->type()) { case MediaTypePhoto: return OverviewPhotos; case MediaTypeVideo: return OverviewVideos; case MediaTypeFile: return OverviewFiles; case MediaTypeMusicFile: return media->getDocument()->isMusic() ? OverviewMusicFiles : OverviewCount; case MediaTypeVoiceFile: return OverviewVoiceFiles; case MediaTypeGif: { auto document = media->getDocument(); return (document->isGifv() || document->isRoundVideo()) ? OverviewCount : OverviewFiles; } break; default: break; } return OverviewCount; } MediaOverviewType serviceMediaToOverviewType(HistoryMedia *media) { switch (media->type()) { case MediaTypePhoto: return OverviewChatPhotos; default: break; } return OverviewCount; } ApiWrap::RequestMessageDataCallback historyDependentItemCallback(const FullMsgId &msgId) { return [dependent = msgId](ChannelData *channel, MsgId msgId) { if (auto item = App::histItemById(dependent)) { item->updateDependencyItem(); } }; } constexpr auto kPinnedMessageTextLimit = 16; style::color fromNameFg(int index) { t_assert(index >= 0 && index < 8); style::color colors[] = { st::historyPeer1NameFg, st::historyPeer2NameFg, st::historyPeer3NameFg, st::historyPeer4NameFg, st::historyPeer5NameFg, st::historyPeer6NameFg, st::historyPeer7NameFg, st::historyPeer8NameFg, }; return colors[index]; } style::color fromNameFgSelected(int index) { t_assert(index >= 0 && index < 8); style::color colors[] = { st::historyPeer1NameFgSelected, st::historyPeer2NameFgSelected, st::historyPeer3NameFgSelected, st::historyPeer4NameFgSelected, st::historyPeer5NameFgSelected, st::historyPeer6NameFgSelected, st::historyPeer7NameFgSelected, st::historyPeer8NameFgSelected, }; return colors[index]; } } // namespace void historyInitMessages() { initTextOptions(); } void HistoryMessageVia::create(int32 userId) { _bot = App::user(peerFromUser(userId)); _maxWidth = st::msgServiceNameFont->width(lng_inline_bot_via(lt_inline_bot, '@' + _bot->username)); _lnk = MakeShared([bot = _bot] { App::insertBotCommand('@' + bot->username); }); } void HistoryMessageVia::resize(int32 availw) const { if (availw < 0) { _text = QString(); _width = 0; } else { _text = lng_inline_bot_via(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::create(UserData *from, const QDateTime &date) { QString time = qsl(", ") + date.toString(cTimeFormat()), name = App::peerName(from); int32 timew = st::msgDateFont->width(time), namew = st::msgDateFont->width(name); if (timew + namew > st::maxSignatureSize) { name = st::msgDateFont->elided(from->firstName, st::maxSignatureSize - timew); } _signature.setText(st::msgDateTextStyle, name + time, _textNameOptions); } int HistoryMessageSigned::maxWidth() const { return _signature.maxWidth(); } void HistoryMessageEdited::create(const QDateTime &editDate, const QDateTime &date) { _editDate = editDate; QString time = date.toString(cTimeFormat()); _edited.setText(st::msgDateTextStyle, lang(lng_edited) + ' ' + time, _textNameOptions); } int HistoryMessageEdited::maxWidth() const { return _edited.maxWidth(); } void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { QString text; if (_authorOriginal != _fromOriginal) { text = lng_forwarded_signed(lt_channel, App::peerName(_authorOriginal), lt_user, App::peerName(_fromOriginal)); } else { text = App::peerName(_authorOriginal); } if (via) { if (_authorOriginal->isChannel()) { text = lng_forwarded_channel_via(lt_channel, textcmdLink(1, text), lt_inline_bot, textcmdLink(2, '@' + via->_bot->username)); } else { text = lng_forwarded_via(lt_user, textcmdLink(1, text), lt_inline_bot, textcmdLink(2, '@' + via->_bot->username)); } } else { if (_authorOriginal->isChannel()) { text = lng_forwarded_channel(lt_channel, textcmdLink(1, text)); } else { text = lng_forwarded(lt_user, textcmdLink(1, text)); } } TextParseOptions opts = { TextParseRichText, 0, 0, Qt::LayoutDirectionAuto }; _text.setText(st::fwdTextStyle, text, opts); _text.setLink(1, (_originalId && _authorOriginal->isChannel()) ? goToMessageClickHandler(_authorOriginal, _originalId) : _authorOriginal->openLink()); if (via) { _text.setLink(2, via->_lnk); } } bool HistoryMessageReply::updateData(HistoryMessage *holder, bool force) { if (!force) { if (replyToMsg || !replyToMsgId) { return true; } } if (!replyToMsg) { replyToMsg = App::histItemById(holder->channelId(), replyToMsgId); if (replyToMsg) { App::historyRegDependency(holder, replyToMsg); } } if (replyToMsg) { replyToText.setText(st::messageTextStyle, textClean(replyToMsg->inReplyText()), _textDlgOptions); updateName(); replyToLnk = goToMessageClickHandler(replyToMsg); 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->setPendingInitDimensions(); } return (replyToMsg || !replyToMsgId); } void HistoryMessageReply::clearData(HistoryMessage *holder) { _replyToVia = nullptr; if (replyToMsg) { App::historyUnregDependency(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) { QString name = (_replyToVia && replyToMsg->author()->isUser()) ? replyToMsg->author()->asUser()->firstName : App::peerName(replyToMsg->author()); replyToName.setText(st::fwdTextStyle, name, _textNameOptions); replyToVersion = replyToMsg->author()->nameVersion; bool hasPreview = replyToMsg->getMedia() ? replyToMsg->getMedia()->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(lang(replyToMsgId ? lng_profile_loading : lng_deleted_message)); } _maxReplyWidth = st::msgReplyPadding.left() + st::msgReplyBarSkip + _maxReplyWidth + st::msgReplyPadding.right(); } void HistoryMessageReply::resize(int width) const { if (_replyToVia) { bool hasPreview = replyToMsg->getMedia() ? replyToMsg->getMedia()->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->setPendingInitDimensions(); } } void HistoryMessageReply::paint(Painter &p, const HistoryItem *holder, int x, int y, int w, PaintFlags flags) const { bool selected = (flags & PaintSelected), outbg = holder->hasOutLayout(); style::color bar = st::msgImgReplyBarColor; if (flags & PaintInBubble) { bar = (flags & PaintSelected) ? (outbg ? st::msgOutReplyBarSelColor : st::msgInReplyBarSelColor) : (outbg ? st::msgOutReplyBarColor : st::msgInReplyBarColor); } QRect rbar(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) { bool hasPreview = replyToMsg->getMedia() ? replyToMsg->getMedia()->hasReplyPreview() : false; int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; if (hasPreview) { ImagePtr replyPreview = replyToMsg->getMedia()->replyPreview(); if (!replyPreview->isNull()) { QRect to(rtlrect(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height(), w + 2 * x)); p.drawPixmap(to.x(), to.y(), replyPreview->pixSingle(replyPreview->width() / cIntRetinaFactor(), replyPreview->height() / cIntRetinaFactor(), to.width(), to.height(), ImageRoundRadius::Small)); if (selected) { App::roundRect(p, to, p.textPalette().selectOverlay, SelectedOverlaySmallCorners); } } } if (w > st::msgReplyBarSkip + previewSkip) { if (flags & PaintInBubble) { 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); } auto replyToAsMsg = replyToMsg->toHistoryMessage(); if (!(flags & PaintInBubble)) { } else if ((replyToAsMsg && replyToAsMsg->emptyText()) || replyToMsg->serviceMsg()) { p.setPen(outbg ? (selected ? st::msgOutDateFgSelected : st::msgOutDateFg) : (selected ? st::msgInDateFgSelected : st::msgInDateFg)); } else { p.setPen(outbg ? (selected ? st::historyTextOutFgSelected : st::historyTextOutFg) : (selected ? st::historyTextInFgSelected : st::historyTextInFg)); } replyToText.drawLeftElided(p, x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top() + st::msgServiceNameFont->height, w - st::msgReplyBarSkip - previewSkip, w + 2 * x); } } else { p.setFont(st::msgDateFont); auto &date = outbg ? (selected ? st::msgOutDateFgSelected : st::msgOutDateFg) : (selected ? st::msgInDateFgSelected : st::msgInDateFg); p.setPen((flags & PaintInBubble) ? 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(lang(replyToMsgId ? lng_profile_loading : lng_deleted_message), w - st::msgReplyBarSkip)); } } } void HistoryMessage::KeyboardStyle::startPaint(Painter &p) const { p.setPen(st::msgServiceFg); } const style::TextStyle &HistoryMessage::KeyboardStyle::textStyle() const { return st::serviceTextStyle; } void HistoryMessage::KeyboardStyle::repaint(const HistoryItem *item) const { Ui::repaintHistoryItem(item); } int HistoryMessage::KeyboardStyle::buttonRadius() const { return st::dateRadius; } void HistoryMessage::KeyboardStyle::paintButtonBg(Painter &p, const QRect &rect, float64 howMuchOver) const { App::roundRect(p, rect, st::msgServiceBg, StickerCorners); if (howMuchOver > 0) { auto o = p.opacity(); p.setOpacity(o * howMuchOver); App::roundRect(p, rect, st::msgBotKbOverBgAdd, BotKbOverCorners); p.setOpacity(o); } } void HistoryMessage::KeyboardStyle::paintButtonIcon(Painter &p, const QRect &rect, int outerWidth, HistoryMessageReplyMarkup::Button::Type type) const { using Button = HistoryMessageReplyMarkup::Button; auto getIcon = [](Button::Type type) -> const style::icon* { switch (type) { case Button::Type::Url: return &st::msgBotKbUrlIcon; case Button::Type::SwitchInlineSame: case Button::Type::SwitchInline: return &st::msgBotKbSwitchPmIcon; } return nullptr; }; if (auto icon = getIcon(type)) { icon->paint(p, rect.x() + rect.width() - icon->width() - st::msgBotKbIconPadding, rect.y() + st::msgBotKbIconPadding, outerWidth); } } void HistoryMessage::KeyboardStyle::paintButtonLoading(Painter &p, const QRect &rect) const { auto icon = &st::historySendingInvertedIcon; icon->paint(p, rect.x() + rect.width() - icon->width() - st::msgBotKbIconPadding, rect.y() + rect.height() - icon->height() - st::msgBotKbIconPadding, rect.x() * 2 + rect.width()); } int HistoryMessage::KeyboardStyle::minButtonWidth(HistoryMessageReplyMarkup::Button::Type type) const { using Button = HistoryMessageReplyMarkup::Button; int result = 2 * buttonPadding(), iconWidth = 0; switch (type) { case Button::Type::Url: iconWidth = st::msgBotKbUrlIcon.width(); break; case Button::Type::SwitchInlineSame: case Button::Type::SwitchInline: iconWidth = st::msgBotKbSwitchPmIcon.width(); break; case Button::Type::Callback: case Button::Type::Game: iconWidth = st::historySendingInvertedIcon.width(); break; } if (iconWidth > 0) { result = std::max(result, 2 * iconWidth + 4 * int(st::msgBotKbIconPadding)); } return result; } HistoryMessage::HistoryMessage(History *history, const MTPDmessage &msg) : HistoryItem(history, msg.vid.v, msg.vflags.v, ::date(msg.vdate), msg.has_from_id() ? msg.vfrom_id.v : 0) { CreateConfig config; if (msg.has_fwd_from() && msg.vfwd_from.type() == mtpc_messageFwdHeader) { auto &f = msg.vfwd_from.c_messageFwdHeader(); if (f.has_from_id() || f.has_channel_id()) { config.authorIdOriginal = f.has_channel_id() ? peerFromChannel(f.vchannel_id) : peerFromUser(f.vfrom_id); config.fromIdOriginal = f.has_from_id() ? peerFromUser(f.vfrom_id) : peerFromChannel(f.vchannel_id); if (f.has_channel_post()) config.originalId = f.vchannel_post.v; } } if (msg.has_reply_to_msg_id()) config.replyTo = msg.vreply_to_msg_id.v; if (msg.has_via_bot_id()) config.viaBotId = msg.vvia_bot_id.v; if (msg.has_views()) config.viewsCount = msg.vviews.v; if (msg.has_reply_markup()) config.mtpMarkup = &msg.vreply_markup; if (msg.has_edit_date()) config.editDate = ::date(msg.vedit_date); createComponents(config); initMedia(msg.has_media() ? (&msg.vmedia) : nullptr); TextWithEntities textWithEntities = { textClean(qs(msg.vmessage)), msg.has_entities() ? entitiesFromMTP(msg.ventities.v) : EntitiesInText(), }; setText(textWithEntities); } namespace { MTPDmessage::Flags newForwardedFlags(PeerData *p, int32 from, HistoryMessage *fwd) { MTPDmessage::Flags result = newMessageFlags(p) | MTPDmessage::Flag::f_fwd_from; if (from) { result |= MTPDmessage::Flag::f_from_id; } if (fwd->Has()) { result |= MTPDmessage::Flag::f_via_bot_id; } if (!p->isChannel()) { if (HistoryMedia *media = fwd->getMedia()) { if (media->type() == MediaTypeVoiceFile) { result |= MTPDmessage::Flag::f_media_unread; // } else if (media->type() == MediaTypeVideo) { // result |= MTPDmessage::flag_media_unread; } } } if (fwd->hasViews()) { result |= MTPDmessage::Flag::f_views; } return result; } } // namespace HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *fwd) : HistoryItem(history, id, newForwardedFlags(history->peer, from, fwd) | flags, date, from) { CreateConfig config; if (fwd->Has() || !fwd->history()->peer->isSelf()) { // Server doesn't add "fwd_from" to non-forwarded messages from chat with yourself. config.authorIdOriginal = fwd->authorOriginal()->id; config.fromIdOriginal = fwd->fromOriginal()->id; if (fwd->authorOriginal()->isChannel()) { config.originalId = fwd->idOriginal(); } } auto fwdViaBot = fwd->viaBot(); if (fwdViaBot) config.viaBotId = peerToUser(fwdViaBot->id); int fwdViewsCount = fwd->viewsCount(); if (fwdViewsCount > 0) { config.viewsCount = fwdViewsCount; } else if (isPost()) { config.viewsCount = 1; } // Copy inline keyboard when forwarding messages with a game. auto mediaOriginal = fwd->getMedia(); if (mediaOriginal && mediaOriginal->type() == MediaTypeGame) { config.inlineMarkup = fwd->inlineReplyMarkup(); } createComponents(config); if (mediaOriginal) { _media = mediaOriginal->clone(this); } setText(fwd->originalText()); } HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const TextWithEntities &textWithEntities) : HistoryItem(history, id, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, MTPnullMarkup); setText(textWithEntities); } HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, markup); initMediaFromDocument(doc, caption); setText(TextWithEntities()); } HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, markup); _media = std::make_unique(this, photo, caption); setText(TextWithEntities()); } HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, GameData *game, const MTPReplyMarkup &markup) : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, markup); _media = std::make_unique(this, game); setText(TextWithEntities()); } void HistoryMessage::createComponentsHelper(MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, const MTPReplyMarkup &markup) { CreateConfig config; if (flags & MTPDmessage::Flag::f_via_bot_id) config.viaBotId = viaBotId; if (flags & MTPDmessage::Flag::f_reply_to_msg_id) config.replyTo = replyTo; if (flags & MTPDmessage::Flag::f_reply_markup) config.mtpMarkup = &markup; if (isPost()) config.viewsCount = 1; createComponents(config); } void HistoryMessage::updateMediaInBubbleState() { if (!_media) { return; } if (!drawBubble()) { _media->setInBubbleState(MediaInBubbleState::None); return; } bool hasSomethingAbove = displayFromName() || displayForwardedFrom() || Has() || Has(); bool hasSomethingBelow = false; if (!emptyText()) { if (_media->isAboveMessage()) { hasSomethingBelow = true; } else { hasSomethingAbove = true; } } auto computeState = [hasSomethingAbove, hasSomethingBelow] { if (hasSomethingAbove) { if (hasSomethingBelow) { return MediaInBubbleState::Middle; } return MediaInBubbleState::Bottom; } else if (hasSomethingBelow) { return MediaInBubbleState::Top; } return MediaInBubbleState::None; }; _media->setInBubbleState(computeState()); } bool HistoryMessage::displayEditedBadge(bool hasViaBotOrInlineMarkup) const { if (hasViaBotOrInlineMarkup) { return false; } else if (!(_flags & MTPDmessage::Flag::f_edit_date)) { return false; } if (auto fromUser = from()->asUser()) { if (fromUser->botInfo) { return false; } } return true; } void HistoryMessage::createComponents(const CreateConfig &config) { uint64 mask = 0; if (config.replyTo) { mask |= HistoryMessageReply::Bit(); } if (config.viaBotId) { mask |= HistoryMessageVia::Bit(); } if (config.viewsCount >= 0) { mask |= HistoryMessageViews::Bit(); } if (isPost() && _from->isUser()) { mask |= HistoryMessageSigned::Bit(); } auto hasViaBot = (config.viaBotId != 0); auto hasInlineMarkup = [&config] { if (config.mtpMarkup) { return (config.mtpMarkup->type() == mtpc_replyInlineMarkup); } return (config.inlineMarkup != nullptr); }; if (displayEditedBadge(hasViaBot || hasInlineMarkup())) { mask |= HistoryMessageEdited::Bit(); } if (config.authorIdOriginal && config.fromIdOriginal) { mask |= HistoryMessageForwarded::Bit(); } if (config.mtpMarkup) { // optimization: don't create markup component for the case // MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag if (config.mtpMarkup->type() != mtpc_replyKeyboardHide || config.mtpMarkup->c_replyKeyboardHide().vflags.v != 0) { mask |= HistoryMessageReplyMarkup::Bit(); } } else if (config.inlineMarkup) { mask |= HistoryMessageReplyMarkup::Bit(); } UpdateComponents(mask); if (auto reply = Get()) { reply->replyToMsgId = config.replyTo; if (!reply->updateData(this) && App::api()) { App::api()->requestMessageData(history()->peer->asChannel(), reply->replyToMsgId, historyDependentItemCallback(fullId())); } } if (auto via = Get()) { via->create(config.viaBotId); } if (auto views = Get()) { views->_views = config.viewsCount; } if (auto msgsigned = Get()) { msgsigned->create(_from->asUser(), date); } if (auto edited = Get()) { edited->create(config.editDate, date); } if (auto fwd = Get()) { fwd->_authorOriginal = App::peer(config.authorIdOriginal); fwd->_fromOriginal = App::peer(config.fromIdOriginal); fwd->_originalId = config.originalId; } if (auto markup = Get()) { if (config.mtpMarkup) { markup->create(*config.mtpMarkup); } else if (config.inlineMarkup) { markup->create(*config.inlineMarkup); } if (markup->flags & MTPDreplyKeyboardMarkup_ClientFlag::f_has_switch_inline_button) { _flags |= MTPDmessage_ClientFlag::f_has_switch_inline_button; } } initTime(); } QString formatViewsCount(int32 views) { if (views > 999999) { views /= 100000; if (views % 10) { return QString::number(views / 10) + '.' + QString::number(views % 10) + 'M'; } return QString::number(views / 10) + 'M'; } else if (views > 9999) { views /= 100; if (views % 10) { return QString::number(views / 10) + '.' + QString::number(views % 10) + 'K'; } return QString::number(views / 10) + 'K'; } else if (views > 0) { return QString::number(views); } return qsl("1"); } void HistoryMessage::initTime() { if (auto msgsigned = Get()) { _timeWidth = msgsigned->maxWidth(); } else if (auto edited = Get()) { _timeWidth = edited->maxWidth(); } else { _timeText = date.toString(cTimeFormat()); _timeWidth = st::msgDateFont->width(_timeText); } if (auto views = Get()) { views->_viewsText = (views->_views >= 0) ? formatViewsCount(views->_views) : QString(); views->_viewsWidth = views->_viewsText.isEmpty() ? 0 : st::msgDateFont->width(views->_viewsText); } } void HistoryMessage::initMedia(const MTPMessageMedia *media) { switch (media ? media->type() : mtpc_messageMediaEmpty) { case mtpc_messageMediaContact: { auto &d = media->c_messageMediaContact(); _media = std::make_unique(this, d.vuser_id.v, qs(d.vfirst_name), qs(d.vlast_name), qs(d.vphone_number)); } break; case mtpc_messageMediaGeo: { auto &point = media->c_messageMediaGeo().vgeo; if (point.type() == mtpc_geoPoint) { _media = std::make_unique(this, LocationCoords(point.c_geoPoint())); } } break; case mtpc_messageMediaVenue: { auto &d = media->c_messageMediaVenue(); if (d.vgeo.type() == mtpc_geoPoint) { _media = std::make_unique(this, LocationCoords(d.vgeo.c_geoPoint()), qs(d.vtitle), qs(d.vaddress)); } } break; case mtpc_messageMediaPhoto: { auto &photo = media->c_messageMediaPhoto(); if (photo.vphoto.type() == mtpc_photo) { _media = std::make_unique(this, App::feedPhoto(photo.vphoto.c_photo()), qs(photo.vcaption)); } } break; case mtpc_messageMediaDocument: { auto &document = media->c_messageMediaDocument().vdocument; if (document.type() == mtpc_document) { return initMediaFromDocument(App::feedDocument(document), qs(media->c_messageMediaDocument().vcaption)); } } break; case mtpc_messageMediaWebPage: { auto &d = media->c_messageMediaWebPage().vwebpage; switch (d.type()) { case mtpc_webPageEmpty: break; case mtpc_webPagePending: { _media = std::make_unique(this, App::feedWebPage(d.c_webPagePending())); } break; case mtpc_webPage: { _media = std::make_unique(this, App::feedWebPage(d.c_webPage())); } break; case mtpc_webPageNotModified: LOG(("API Error: webPageNotModified is unexpected in message media.")); break; } } break; case mtpc_messageMediaGame: { auto &d = media->c_messageMediaGame().vgame; if (d.type() == mtpc_game) { _media = std::make_unique(this, App::feedGame(d.c_game())); } } break; case mtpc_messageMediaInvoice: { _media = std::make_unique(this, media->c_messageMediaInvoice()); if (static_cast(getMedia())->getReceiptMsgId()) { replaceBuyWithReceiptInMarkup(); } } break; }; } void HistoryMessage::replaceBuyWithReceiptInMarkup() { if (auto markup = inlineReplyMarkup()) { for (auto &row : markup->rows) { for (auto &button : row) { if (button.type == HistoryMessageReplyMarkup::Button::Type::Buy) { button.text = lang(lng_payments_receipt_button); } } } } } void HistoryMessage::initMediaFromDocument(DocumentData *doc, const QString &caption) { if (doc->sticker()) { _media = std::make_unique(this, doc); } else if (doc->isAnimation()) { _media = std::make_unique(this, doc, caption); } else if (doc->isVideo()) { _media = std::make_unique(this, doc, caption); } else { _media = std::make_unique(this, doc, caption); } } int32 HistoryMessage::plainMaxWidth() const { return st::msgPadding.left() + _text.maxWidth() + st::msgPadding.right(); } void HistoryMessage::initDimensions() { auto reply = Get(); if (reply) { reply->updateName(); } updateMediaInBubbleState(); if (drawBubble()) { auto fwd = Get(); auto via = Get(); if (fwd) { fwd->create(via); } auto mediaDisplayed = false; if (_media) { mediaDisplayed = _media->isDisplayed(); _media->initDimensions(); if (mediaDisplayed && _media->isBubbleBottom()) { if (_text.hasSkipBlock()) { _text.removeSkipBlock(); _textWidth = -1; _textHeight = 0; } } else if (!_text.hasSkipBlock()) { _text.setSkipBlock(skipBlockWidth(), skipBlockHeight()); _textWidth = -1; _textHeight = 0; } } _maxw = plainMaxWidth(); _minh = emptyText() ? 0 : _text.minHeight(); if (mediaDisplayed) { if (!_media->isBubbleTop()) { _minh += st::msgPadding.top() + st::mediaInBubbleSkip; } if (!_media->isBubbleBottom()) { _minh += st::msgPadding.bottom() + st::mediaInBubbleSkip; } auto maxw = _media->maxWidth(); if (maxw > _maxw) _maxw = maxw; _minh += _media->minHeight(); } else { _minh += st::msgPadding.top() + st::msgPadding.bottom(); if (displayFromName()) { auto namew = st::msgPadding.left() + author()->nameText.maxWidth() + st::msgPadding.right(); if (via && !fwd) { namew += st::msgServiceFont->spacew + via->_maxWidth; } if (namew > _maxw) _maxw = namew; } else if (via && !fwd) { if (st::msgPadding.left() + via->_maxWidth + st::msgPadding.right() > _maxw) { _maxw = st::msgPadding.left() + via->_maxWidth + st::msgPadding.right(); } } if (fwd) { auto _namew = st::msgPadding.left() + fwd->_text.maxWidth() + st::msgPadding.right(); if (via) { _namew += st::msgServiceFont->spacew + via->_maxWidth; } if (_namew > _maxw) _maxw = _namew; } } } else if (_media) { _media->initDimensions(); _maxw = _media->maxWidth(); _minh = _media->minHeight(); } else { _maxw = st::msgMinWidth; _minh = 0; } if (reply && !emptyText()) { int replyw = st::msgPadding.left() + reply->_maxReplyWidth - st::msgReplyPadding.left() - st::msgReplyPadding.right() + st::msgPadding.right(); if (reply->_replyToVia) { replyw += st::msgServiceFont->spacew + reply->_replyToVia->_maxWidth; } if (replyw > _maxw) _maxw = replyw; } if (auto markup = inlineReplyMarkup()) { if (!markup->inlineKeyboard) { markup->inlineKeyboard.reset(new ReplyKeyboard(this, std::make_unique(st::msgBotKbButton))); } // if we have a text bubble we can resize it to fit the keyboard // but if we have only media we don't do that if (!emptyText()) { _maxw = qMax(_maxw, markup->inlineKeyboard->naturalWidth()); } } } void HistoryMessage::countPositionAndSize(int32 &left, int32 &width) const { int32 maxwidth = qMin(int(st::msgMaxWidth), _maxw), hwidth = _history->width; if (_media && _media->currentWidth() < maxwidth) { maxwidth = qMax(_media->currentWidth(), qMin(maxwidth, plainMaxWidth())); } left = (!isPost() && out() && !Adaptive::ChatWide()) ? st::msgMargin.right() : st::msgMargin.left(); if (hasFromPhoto()) { left += st::msgPhotoSkip; // } else if (!Adaptive::Wide() && !out() && !fromChannel() && st::msgPhotoSkip - (hmaxwidth - hwidth) > 0) { // left += st::msgPhotoSkip - (hmaxwidth - hwidth); } width = hwidth - st::msgMargin.left() - st::msgMargin.right(); if (width > maxwidth) { if (!isPost() && out() && !Adaptive::ChatWide()) { left += width - maxwidth; } width = maxwidth; } } void HistoryMessage::fromNameUpdated(int32 width) const { _authorNameVersion = author()->nameVersion; if (!Has()) { if (auto via = Get()) { via->resize(width - st::msgPadding.left() - st::msgPadding.right() - author()->nameText.maxWidth() - st::msgServiceFont->spacew); } } } void HistoryMessage::applyEdition(const MTPDmessage &message) { int keyboardTop = -1; if (!pendingResize()) { if (auto keyboard = inlineReplyKeyboard()) { int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); keyboardTop = _height - h + st::msgBotKbButton.margin - marginBottom(); } } if (message.has_edit_date()) { _flags |= MTPDmessage::Flag::f_edit_date; auto hasViaBotId = Has(); auto hasInlineMarkup = (inlineReplyMarkup() != nullptr); if (displayEditedBadge(hasViaBotId || hasInlineMarkup)) { if (!Has()) { AddComponents(HistoryMessageEdited::Bit()); } Get()->create(::date(message.vedit_date), date); } else if (Has()) { RemoveComponents(HistoryMessageEdited::Bit()); } initTime(); } TextWithEntities textWithEntities = { qs(message.vmessage), EntitiesInText() }; if (message.has_entities()) { textWithEntities.entities = entitiesFromMTP(message.ventities.v); } setText(textWithEntities); setReplyMarkup(message.has_reply_markup() ? (&message.vreply_markup) : nullptr); setMedia(message.has_media() ? (&message.vmedia) : nullptr); setViewsCount(message.has_views() ? message.vviews.v : -1); finishEdition(keyboardTop); } void HistoryMessage::applyEdition(const MTPDmessageService &message) { if (message.vaction.type() == mtpc_messageActionHistoryClear) { applyEditionToEmpty(); } } void HistoryMessage::applyEditionToEmpty() { setEmptyText(); setMedia(nullptr); setReplyMarkup(nullptr); setViewsCount(-1); finishEditionToEmpty(); } void HistoryMessage::updateMedia(const MTPMessageMedia *media) { auto setMediaAllowed = [](HistoryMediaType type) { return (type == MediaTypeWebPage || type == MediaTypeGame || type == MediaTypeLocation); }; if (_flags & MTPDmessage_ClientFlag::f_from_inline_bot) { bool needReSet = true; if (media && _media) { needReSet = _media->needReSetInlineResultMedia(*media); } if (needReSet) { setMedia(media); } _flags &= ~MTPDmessage_ClientFlag::f_from_inline_bot; } else if (media && _media && !setMediaAllowed(_media->type())) { _media->updateSentMedia(*media); } else { setMedia(media); } setPendingInitDimensions(); } int32 HistoryMessage::addToOverview(AddToOverviewMethod method) { if (!indexInOverview()) return 0; int32 result = 0; if (HistoryMedia *media = getMedia()) { MediaOverviewType type = messageMediaToOverviewType(media); if (type != OverviewCount) { if (history()->addToOverview(type, id, method)) { result |= (1 << type); } } } if (hasTextLinks()) { if (history()->addToOverview(OverviewLinks, id, method)) { result |= (1 << OverviewLinks); } } return result; } void HistoryMessage::eraseFromOverview() { if (HistoryMedia *media = getMedia()) { MediaOverviewType type = messageMediaToOverviewType(media); if (type != OverviewCount) { history()->eraseFromOverview(type, id); } } if (hasTextLinks()) { history()->eraseFromOverview(OverviewLinks, id); } } TextWithEntities HistoryMessage::selectedText(TextSelection selection) const { TextWithEntities result, textResult, mediaResult; if (selection == FullSelection) { textResult = _text.originalTextWithEntities(AllTextSelection, ExpandLinksAll); } else { textResult = _text.originalTextWithEntities(selection, ExpandLinksAll); } if (_media) { mediaResult = _media->selectedText(toMediaSelection(selection)); } if (textResult.text.isEmpty()) { result = mediaResult; } else if (mediaResult.text.isEmpty()) { result = textResult; } else { result.text = textResult.text + qstr("\n\n"); result.entities = textResult.entities; appendTextWithEntities(result, std::move(mediaResult)); } if (auto fwd = Get()) { if (selection == FullSelection) { auto fwdinfo = fwd->_text.originalTextWithEntities(AllTextSelection, ExpandLinksAll); TextWithEntities wrapped; wrapped.text.reserve(fwdinfo.text.size() + 4 + result.text.size()); wrapped.entities.reserve(fwdinfo.entities.size() + result.entities.size()); wrapped.text.append('['); appendTextWithEntities(wrapped, std::move(fwdinfo)); wrapped.text.append(qsl("]\n")); appendTextWithEntities(wrapped, std::move(result)); result = wrapped; } } if (auto reply = Get()) { if (selection == FullSelection && reply->replyToMsg) { TextWithEntities wrapped; wrapped.text.reserve(lang(lng_in_reply_to).size() + reply->replyToMsg->author()->name.size() + 4 + result.text.size()); wrapped.text.append('[').append(lang(lng_in_reply_to)).append(' ').append(reply->replyToMsg->author()->name).append(qsl("]\n")); appendTextWithEntities(wrapped, std::move(result)); result = wrapped; } } return result; } void HistoryMessage::setMedia(const MTPMessageMedia *media) { if (!_media && (!media || media->type() == mtpc_messageMediaEmpty)) return; bool mediaRemovedSkipBlock = false; if (_media) { // Don't update Game media because we loose the consumed text of the message. if (_media->type() == MediaTypeGame) return; mediaRemovedSkipBlock = _media->isDisplayed() && _media->isBubbleBottom(); _media.reset(); } initMedia(media); auto mediaDisplayed = _media && _media->isDisplayed(); if (mediaDisplayed && _media->isBubbleBottom() && !mediaRemovedSkipBlock) { _text.removeSkipBlock(); _textWidth = -1; _textHeight = 0; } else if (mediaRemovedSkipBlock && (!mediaDisplayed || !_media->isBubbleBottom())) { _text.setSkipBlock(skipBlockWidth(), skipBlockHeight()); _textWidth = -1; _textHeight = 0; } } void HistoryMessage::setText(const TextWithEntities &textWithEntities) { for_const (auto &entity, textWithEntities.entities) { auto type = entity.type(); if (type == EntityInTextUrl || type == EntityInTextCustomUrl || type == EntityInTextEmail) { _flags |= MTPDmessage_ClientFlag::f_has_text_links; break; } } auto mediaDisplayed = _media && _media->isDisplayed(); if (mediaDisplayed && _media->consumeMessageText(textWithEntities)) { setEmptyText(); } else { if (_media && _media->isDisplayed() && !_media->isAboveMessage()) { _text.setMarkedText(st::messageTextStyle, textWithEntities, itemTextOptions(this)); } else { _text.setMarkedText(st::messageTextStyle, { textWithEntities.text + skipBlock(), textWithEntities.entities }, itemTextOptions(this)); } _textWidth = -1; _textHeight = 0; } } void HistoryMessage::setEmptyText() { _text.setMarkedText(st::messageTextStyle, { QString(), EntitiesInText() }, itemTextOptions(this)); _textWidth = -1; _textHeight = 0; } void HistoryMessage::setReplyMarkup(const MTPReplyMarkup *markup) { if (!markup) { if (_flags & MTPDmessage::Flag::f_reply_markup) { _flags &= ~MTPDmessage::Flag::f_reply_markup; if (Has()) { RemoveComponents(HistoryMessageReplyMarkup::Bit()); } setPendingInitDimensions(); Notify::replyMarkupUpdated(this); } return; } // optimization: don't create markup component for the case // MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag if (markup->type() == mtpc_replyKeyboardHide && markup->c_replyKeyboardHide().vflags.v == 0) { bool changed = false; if (Has()) { RemoveComponents(HistoryMessageReplyMarkup::Bit()); changed = true; } if (!(_flags & MTPDmessage::Flag::f_reply_markup)) { _flags |= MTPDmessage::Flag::f_reply_markup; changed = true; } if (changed) { setPendingInitDimensions(); Notify::replyMarkupUpdated(this); } } else { if (!(_flags & MTPDmessage::Flag::f_reply_markup)) { _flags |= MTPDmessage::Flag::f_reply_markup; } if (!Has()) { AddComponents(HistoryMessageReplyMarkup::Bit()); } Get()->create(*markup); setPendingInitDimensions(); Notify::replyMarkupUpdated(this); } } TextWithEntities HistoryMessage::originalText() const { if (emptyText()) { return { QString(), EntitiesInText() }; } return _text.originalTextWithEntities(); } bool HistoryMessage::textHasLinks() const { return emptyText() ? false : _text.hasLinks(); } int HistoryMessage::infoWidth() const { int result = _timeWidth; if (auto views = Get()) { result += st::historyViewsSpace + views->_viewsWidth + st::historyViewsWidth; } else if (id < 0 && history()->peer->isSelf()) { result += st::historySendStateSpace; } if (out() && !isPost()) { result += st::historySendStateSpace; } return result; } int HistoryMessage::timeLeft() const { int result = 0; if (auto views = Get()) { result += st::historyViewsSpace + views->_viewsWidth + st::historyViewsWidth; } else if (id < 0 && history()->peer->isSelf()) { result += st::historySendStateSpace; } return result; } void HistoryMessage::drawInfo(Painter &p, int32 right, int32 bottom, int32 width, bool selected, InfoDisplayType type) const { p.setFont(st::msgDateFont); bool outbg = out() && !isPost(); bool invertedsprites = (type == InfoDisplayOverImage || type == InfoDisplayOverBackground); int32 infoRight = right, infoBottom = bottom; switch (type) { case InfoDisplayDefault: infoRight -= st::msgPadding.right() - st::msgDateDelta.x(); infoBottom -= st::msgPadding.bottom() - st::msgDateDelta.y(); p.setPen(selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg)); break; case InfoDisplayOverImage: infoRight -= st::msgDateImgDelta + st::msgDateImgPadding.x(); infoBottom -= st::msgDateImgDelta + st::msgDateImgPadding.y(); p.setPen(st::msgDateImgFg); break; case InfoDisplayOverBackground: infoRight -= st::msgDateImgDelta + st::msgDateImgPadding.x(); infoBottom -= st::msgDateImgDelta + st::msgDateImgPadding.y(); p.setPen(st::msgServiceFg); break; } int32 infoW = HistoryMessage::infoWidth(); if (rtl()) infoRight = width - infoRight + infoW; int32 dateX = infoRight - infoW; int32 dateY = infoBottom - st::msgDateFont->height; if (type == InfoDisplayOverImage) { int32 dateW = infoW + 2 * st::msgDateImgPadding.x(), dateH = st::msgDateFont->height + 2 * st::msgDateImgPadding.y(); App::roundRect(p, dateX - st::msgDateImgPadding.x(), dateY - st::msgDateImgPadding.y(), dateW, dateH, selected ? st::msgDateImgBgSelected : st::msgDateImgBg, selected ? DateSelectedCorners : DateCorners); } else if (type == InfoDisplayOverBackground) { int32 dateW = infoW + 2 * st::msgDateImgPadding.x(), dateH = st::msgDateFont->height + 2 * st::msgDateImgPadding.y(); App::roundRect(p, dateX - st::msgDateImgPadding.x(), dateY - st::msgDateImgPadding.y(), dateW, dateH, selected ? st::msgServiceBgSelected : st::msgServiceBg, selected ? StickerSelectedCorners : StickerCorners); } dateX += HistoryMessage::timeLeft(); if (auto msgsigned = Get()) { msgsigned->_signature.drawElided(p, dateX, dateY, _timeWidth); } else if (auto edited = Get()) { edited->_edited.drawElided(p, dateX, dateY, _timeWidth); } else { p.drawText(dateX, dateY + st::msgDateFont->ascent, _timeText); } if (auto views = Get()) { auto icon = ([this, outbg, invertedsprites, selected] { if (id > 0) { if (outbg) { return &(invertedsprites ? st::historyViewsInvertedIcon : (selected ? st::historyViewsOutSelectedIcon : st::historyViewsOutIcon)); } return &(invertedsprites ? st::historyViewsInvertedIcon : (selected ? st::historyViewsInSelectedIcon : st::historyViewsInIcon)); } return &(invertedsprites ? st::historyViewsSendingInvertedIcon : st::historyViewsSendingIcon); })(); if (id > 0) { icon->paint(p, infoRight - infoW, infoBottom + st::historyViewsTop, width); p.drawText(infoRight - infoW + st::historyViewsWidth, infoBottom - st::msgDateFont->descent, views->_viewsText); } else if (!outbg) { // sending outbg icon will be painted below auto iconSkip = st::historyViewsSpace + views->_viewsWidth; icon->paint(p, infoRight - infoW + iconSkip, infoBottom + st::historyViewsTop, width); } } else if (id < 0 && history()->peer->isSelf()) { auto icon = &(invertedsprites ? st::historyViewsSendingInvertedIcon : st::historyViewsSendingIcon); icon->paint(p, infoRight - infoW, infoBottom + st::historyViewsTop, width); } if (outbg) { auto icon = ([this, invertedsprites, selected] { if (id > 0) { if (unread()) { return &(invertedsprites ? st::historySentInvertedIcon : (selected ? st::historySentSelectedIcon : st::historySentIcon)); } return &(invertedsprites ? st::historyReceivedInvertedIcon : (selected ? st::historyReceivedSelectedIcon : st::historyReceivedIcon)); } return &(invertedsprites ? st::historySendingInvertedIcon : st::historySendingIcon); })(); icon->paint(p, QPoint(infoRight, infoBottom) + st::historySendStatePosition, width); } } void HistoryMessage::setViewsCount(int32 count) { auto views = Get(); if (!views || views->_views == count || (count >= 0 && views->_views > count)) return; int32 was = views->_viewsWidth; views->_views = count; views->_viewsText = (views->_views >= 0) ? formatViewsCount(views->_views) : QString(); views->_viewsWidth = views->_viewsText.isEmpty() ? 0 : st::msgDateFont->width(views->_viewsText); if (was == views->_viewsWidth) { Ui::repaintHistoryItem(this); } else { if (_text.hasSkipBlock()) { _text.setSkipBlock(HistoryMessage::skipBlockWidth(), HistoryMessage::skipBlockHeight()); _textWidth = -1; _textHeight = 0; } setPendingInitDimensions(); } } void HistoryMessage::setId(MsgId newId) { bool wasPositive = (id > 0), positive = (newId > 0); HistoryItem::setId(newId); if (wasPositive == positive) { Ui::repaintHistoryItem(this); } else { if (_text.hasSkipBlock()) { _text.setSkipBlock(HistoryMessage::skipBlockWidth(), HistoryMessage::skipBlockHeight()); _textWidth = -1; _textHeight = 0; } setPendingInitDimensions(); } } void HistoryMessage::draw(Painter &p, const QRect &r, TextSelection selection, TimeMs ms) const { bool outbg = out() && !isPost(), bubble = drawBubble(), selected = (selection == FullSelection); int left = 0, width = 0, height = _height; countPositionAndSize(left, width); if (width < 1) return; int dateh = 0, unreadbarh = 0; if (auto date = Get()) { dateh = date->height(); } if (auto unreadbar = Get()) { unreadbarh = unreadbar->height(); if (r.intersects(QRect(0, dateh, _history->width, unreadbarh))) { p.translate(0, dateh); unreadbar->paint(p, 0, _history->width); p.translate(0, -dateh); } } auto fullAnimMs = App::main() ? App::main()->animActiveTimeStart(this) : 0LL; if (fullAnimMs > 0 && fullAnimMs <= ms) { int animms = ms - fullAnimMs; if (animms > st::activeFadeInDuration + st::activeFadeOutDuration) { App::main()->stopAnimActive(); } else { auto top = marginTop(); auto bottom = marginBottom(); auto fill = qMin(top, bottom); auto skiptop = top - fill; auto fillheight = fill + (height - top - bottom) + fill; auto dt = (animms > st::activeFadeInDuration) ? (1. - (animms - st::activeFadeInDuration) / float64(st::activeFadeOutDuration)) : (animms / float64(st::activeFadeInDuration)); auto o = p.opacity(); p.setOpacity(o * dt); p.fillRect(0, skiptop, _history->width, fillheight, st::defaultTextPalette.selectOverlay); p.setOpacity(o); } } p.setTextPalette(selected ? (outbg ? st::outTextPaletteSelected : st::inTextPaletteSelected) : (outbg ? st::outTextPalette : st::inTextPalette)); auto keyboard = inlineReplyKeyboard(); if (keyboard) { int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); height -= h; int top = height + st::msgBotKbButton.margin - marginBottom(); p.translate(left, top); keyboard->paint(p, width, r.translated(-left, -top), ms); p.translate(-left, -top); } if (bubble) { if (displayFromName() && author()->nameVersion > _authorNameVersion) { fromNameUpdated(width); } auto mediaDisplayed = _media && _media->isDisplayed(); auto top = marginTop(); auto r = QRect(left, top, width, height - top - marginBottom()); auto skipTail = isAttachedToNext() || (_media && _media->skipBubbleTail()) || (keyboard != nullptr); auto displayTail = skipTail ? HistoryLayout::BubbleTail::None : (outbg && !Adaptive::ChatWide()) ? HistoryLayout::BubbleTail::Right : HistoryLayout::BubbleTail::Left; HistoryLayout::paintBubble(p, r, _history->width, selected, outbg, displayTail); QRect trect(r.marginsAdded(-st::msgPadding)); if (mediaDisplayed && _media->isBubbleTop()) { trect.setY(trect.y() - st::msgPadding.top()); } else { paintFromName(p, trect, selected); paintForwardedInfo(p, trect, selected); paintReplyInfo(p, trect, selected); paintViaBotIdInfo(p, trect, selected); } if (mediaDisplayed && _media->isBubbleBottom()) { trect.setHeight(trect.height() + st::msgPadding.bottom()); } auto needDrawInfo = true; if (mediaDisplayed) { auto mediaAboveText = _media->isAboveMessage(); auto mediaHeight = _media->height(); auto mediaLeft = trect.x() - st::msgPadding.left(); auto mediaTop = mediaAboveText ? trect.y() : (trect.y() + trect.height() - mediaHeight); if (!mediaAboveText) { paintText(p, trect, selection); } p.translate(mediaLeft, mediaTop); _media->draw(p, r.translated(-mediaLeft, -mediaTop), toMediaSelection(selection), ms); p.translate(-mediaLeft, -mediaTop); if (mediaAboveText) { trect.setY(trect.y() + mediaHeight); paintText(p, trect, selection); } needDrawInfo = !_media->customInfoLayout(); } else { paintText(p, trect, selection); } if (needDrawInfo) { HistoryMessage::drawInfo(p, r.x() + r.width(), r.y() + r.height(), 2 * r.x() + r.width(), selected, InfoDisplayDefault); } } else if (_media) { auto top = marginTop(); p.translate(left, top); _media->draw(p, r.translated(-left, -top), toMediaSelection(selection), ms); p.translate(-left, -top); } p.restoreTextPalette(); auto reply = Get(); if (reply && reply->isNameUpdated()) { const_cast(this)->setPendingInitDimensions(); } } void HistoryMessage::paintFromName(Painter &p, QRect &trect, bool selected) const { if (displayFromName()) { p.setFont(st::msgNameFont); if (isPost()) { p.setPen(selected ? st::msgInServiceFgSelected : st::msgInServiceFg); } else { p.setPen(selected ? fromNameFgSelected(author()->colorIndex()) : fromNameFg(author()->colorIndex())); } author()->nameText.drawElided(p, trect.left(), trect.top(), trect.width()); auto fwd = Get(); auto via = Get(); if (via && !fwd && trect.width() > author()->nameText.maxWidth() + st::msgServiceFont->spacew) { bool outbg = out() && !isPost(); p.setPen(selected ? (outbg ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (outbg ? st::msgOutServiceFg : st::msgInServiceFg)); p.drawText(trect.left() + author()->nameText.maxWidth() + st::msgServiceFont->spacew, trect.top() + st::msgServiceFont->ascent, via->_text); } trect.setY(trect.y() + st::msgNameFont->height); } } void HistoryMessage::paintForwardedInfo(Painter &p, QRect &trect, bool selected) const { if (displayForwardedFrom()) { style::font serviceFont(st::msgServiceFont), serviceName(st::msgServiceNameFont); auto outbg = hasOutLayout(); p.setPen(selected ? (outbg ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (outbg ? st::msgOutServiceFg : st::msgInServiceFg)); p.setFont(serviceFont); auto fwd = Get(); bool breakEverywhere = (fwd->_text.countHeight(trect.width()) > 2 * serviceFont->height); p.setTextPalette(selected ? (outbg ? st::outFwdTextPaletteSelected : st::inFwdTextPaletteSelected) : (outbg ? st::outFwdTextPalette : st::inFwdTextPalette)); fwd->_text.drawElided(p, trect.x(), trect.y(), trect.width(), 2, style::al_left, 0, -1, 0, breakEverywhere); p.setTextPalette(selected ? (outbg ? st::outTextPaletteSelected : st::inTextPaletteSelected) : (outbg ? st::outTextPalette : st::inTextPalette)); trect.setY(trect.y() + (((fwd->_text.maxWidth() > trect.width()) ? 2 : 1) * serviceFont->height)); } } void HistoryMessage::paintReplyInfo(Painter &p, QRect &trect, bool selected) const { if (auto reply = Get()) { int32 h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); HistoryMessageReply::PaintFlags flags = HistoryMessageReply::PaintInBubble; if (selected) { flags |= HistoryMessageReply::PaintSelected; } reply->paint(p, this, trect.x(), trect.y(), trect.width(), flags); trect.setY(trect.y() + h); } } void HistoryMessage::paintViaBotIdInfo(Painter &p, QRect &trect, bool selected) const { if (!displayFromName() && !Has()) { if (auto via = Get()) { p.setFont(st::msgServiceNameFont); p.setPen(selected ? (hasOutLayout() ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (hasOutLayout() ? st::msgOutServiceFg : st::msgInServiceFg)); p.drawTextLeft(trect.left(), trect.top(), _history->width, via->_text); trect.setY(trect.y() + st::msgServiceNameFont->height); } } } void HistoryMessage::paintText(Painter &p, QRect &trect, TextSelection selection) const { auto outbg = out() && !isPost(); auto selected = (selection == FullSelection); p.setPen(outbg ? (selected ? st::historyTextOutFgSelected : st::historyTextOutFg) : (selected ? st::historyTextInFgSelected : st::historyTextInFg)); p.setFont(st::msgFont); _text.draw(p, trect.x(), trect.y(), trect.width(), style::al_left, 0, -1, selection); } void HistoryMessage::dependencyItemRemoved(HistoryItem *dependency) { if (auto reply = Get()) { reply->itemRemoved(this, dependency); } } int HistoryMessage::resizeGetHeight_(int width) { int result = performResizeGetHeight(width); auto keyboard = inlineReplyKeyboard(); if (auto markup = Get()) { int oldTop = markup->oldTop; if (oldTop >= 0) { markup->oldTop = -1; if (keyboard) { int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); int keyboardTop = _height - h + st::msgBotKbButton.margin - marginBottom(); if (keyboardTop != oldTop) { Notify::inlineKeyboardMoved(this, oldTop, keyboardTop); } } } } return result; } int HistoryMessage::performResizeGetHeight(int width) { if (width < st::msgMinWidth) return _height; width -= st::msgMargin.left() + st::msgMargin.right(); if (width < st::msgPadding.left() + st::msgPadding.right() + 1) { width = st::msgPadding.left() + st::msgPadding.right() + 1; } else if (width > st::msgMaxWidth) { width = st::msgMaxWidth; } if (drawBubble()) { auto fwd = Get(); auto reply = Get(); auto via = Get(); auto mediaDisplayed = false; auto mediaInBubbleState = MediaInBubbleState::None; if (_media) { mediaDisplayed = _media->isDisplayed(); mediaInBubbleState = _media->inBubbleState(); } if (width >= _maxw) { _height = _minh; if (mediaDisplayed) _media->resizeGetHeight(_maxw); } else { if (emptyText()) { _height = 0; } else { auto textWidth = qMax(width - st::msgPadding.left() - st::msgPadding.right(), 1); if (textWidth != _textWidth) { _textWidth = textWidth; _textHeight = _text.countHeight(textWidth); } _height = _textHeight; } if (mediaDisplayed) { if (!_media->isBubbleTop()) { _height += st::msgPadding.top() + st::mediaInBubbleSkip; } if (!_media->isBubbleBottom()) { _height += st::msgPadding.bottom() + st::mediaInBubbleSkip; } _height += _media->resizeGetHeight(width); } else { _height += st::msgPadding.top() + st::msgPadding.bottom(); } } if (displayFromName()) { int32 l = 0, w = 0; countPositionAndSize(l, w); fromNameUpdated(w); _height += st::msgNameFont->height; } else if (via && !fwd) { int32 l = 0, w = 0; countPositionAndSize(l, w); via->resize(w - st::msgPadding.left() - st::msgPadding.right()); _height += st::msgNameFont->height; } if (displayForwardedFrom()) { int32 l = 0, w = 0; countPositionAndSize(l, w); int32 fwdheight = ((fwd->_text.maxWidth() > (w - st::msgPadding.left() - st::msgPadding.right())) ? 2 : 1) * st::semiboldFont->height; _height += fwdheight; } if (reply) { int32 l = 0, w = 0; countPositionAndSize(l, w); reply->resize(w - st::msgPadding.left() - st::msgPadding.right()); _height += st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); } } else if (_media) { _height = _media->resizeGetHeight(width); } else { _height = 0; } if (auto keyboard = inlineReplyKeyboard()) { int32 l = 0, w = 0; countPositionAndSize(l, w); int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); _height += h; keyboard->resize(w, h - st::msgBotKbButton.margin); } _height += marginTop() + marginBottom(); return _height; } bool HistoryMessage::hasPoint(int x, int y) const { int left = 0, width = 0, height = _height; countPositionAndSize(left, width); if (width < 1) return false; if (drawBubble()) { int top = marginTop(); QRect r(left, top, width, height - top - marginBottom()); return r.contains(x, y); } else if (_media) { return _media->hasPoint(x - left, y - marginTop()); } else { return false; } } bool HistoryMessage::pointInTime(int32 right, int32 bottom, int x, int y, InfoDisplayType type) const { int32 infoRight = right, infoBottom = bottom; switch (type) { case InfoDisplayDefault: infoRight -= st::msgPadding.right() - st::msgDateDelta.x(); infoBottom -= st::msgPadding.bottom() - st::msgDateDelta.y(); break; case InfoDisplayOverImage: infoRight -= st::msgDateImgDelta + st::msgDateImgPadding.x(); infoBottom -= st::msgDateImgDelta + st::msgDateImgPadding.y(); break; } int32 dateX = infoRight - HistoryMessage::infoWidth() + HistoryMessage::timeLeft(); int32 dateY = infoBottom - st::msgDateFont->height; return QRect(dateX, dateY, HistoryMessage::timeWidth(), st::msgDateFont->height).contains(x, y); } HistoryTextState HistoryMessage::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; int left = 0, width = 0, height = _height; countPositionAndSize(left, width); if (width < 1) return result; auto keyboard = inlineReplyKeyboard(); if (keyboard) { int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); height -= h; } if (drawBubble()) { auto mediaDisplayed = _media && _media->isDisplayed(); auto top = marginTop(); QRect r(left, top, width, height - top - marginBottom()); QRect trect(r.marginsAdded(-st::msgPadding)); if (mediaDisplayed && _media->isBubbleTop()) { trect.setY(trect.y() - st::msgPadding.top()); } else { if (getStateFromName(x, y, trect, &result)) return result; if (getStateForwardedInfo(x, y, trect, &result, request)) return result; if (getStateReplyInfo(x, y, trect, &result)) return result; if (getStateViaBotIdInfo(x, y, trect, &result)) return result; } if (mediaDisplayed && _media->isBubbleBottom()) { trect.setHeight(trect.height() + st::msgPadding.bottom()); } auto needDateCheck = true; if (mediaDisplayed) { auto mediaAboveText = _media->isAboveMessage(); auto mediaHeight = _media->height(); auto mediaLeft = trect.x() - st::msgPadding.left(); auto mediaTop = mediaAboveText ? trect.y() : (trect.y() + trect.height() - mediaHeight); if (y >= mediaTop && y < mediaTop + mediaHeight) { result = _media->getState(x - mediaLeft, y - mediaTop, request); result.symbol += _text.length(); } else { if (mediaAboveText) { trect.setY(trect.y() + mediaHeight); } getStateText(x, y, trect, &result, request); } needDateCheck = !_media->customInfoLayout(); } else { getStateText(x, y, trect, &result, request); } if (needDateCheck) { if (HistoryMessage::pointInTime(r.x() + r.width(), r.y() + r.height(), x, y, InfoDisplayDefault)) { result.cursor = HistoryInDateCursorState; } } } else if (_media) { result = _media->getState(x - left, y - marginTop(), request); result.symbol += _text.length(); } if (keyboard) { int top = height + st::msgBotKbButton.margin - marginBottom(); if (x >= left && x < left + width && y >= top && y < _height - marginBottom()) { result.link = keyboard->getState(x - left, y - top); return result; } } return result; } // Forward to _media. void HistoryMessage::updatePressed(int x, int y) { if (!_media) return; auto left = 0, width = 0, height = _height; countPositionAndSize(left, width); auto keyboard = inlineReplyKeyboard(); if (keyboard) { auto h = st::msgBotKbButton.margin + keyboard->naturalHeight(); height -= h; } if (drawBubble()) { auto mediaDisplayed = _media && _media->isDisplayed(); auto top = marginTop(); QRect r(left, top, width, height - top - marginBottom()); QRect trect(r.marginsAdded(-st::msgPadding)); if (mediaDisplayed && _media->isBubbleTop()) { trect.setY(trect.y() - st::msgPadding.top()); } else { if (displayFromName()) trect.setTop(trect.top() + st::msgNameFont->height); if (displayForwardedFrom()) { auto fwd = Get(); auto fwdheight = ((fwd->_text.maxWidth() > trect.width()) ? 2 : 1) * st::semiboldFont->height; trect.setTop(trect.top() + fwdheight); } if (Get()) { auto h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); trect.setTop(trect.top() + h); } if (!displayFromName() && !Has()) { if (auto via = Get()) { trect.setTop(trect.top() + st::msgNameFont->height); } } } if (mediaDisplayed && _media->isBubbleBottom()) { trect.setHeight(trect.height() + st::msgPadding.bottom()); } auto needDateCheck = true; if (mediaDisplayed) { auto mediaAboveText = _media->isAboveMessage(); auto mediaHeight = _media->height(); auto mediaLeft = trect.x() - st::msgPadding.left(); auto mediaTop = mediaAboveText ? trect.y() : (trect.y() + trect.height() - mediaHeight); _media->updatePressed(x - mediaLeft, y - mediaTop); } } else { _media->updatePressed(x - left, y - marginTop()); } } bool HistoryMessage::getStateFromName(int x, int y, QRect &trect, HistoryTextState *outResult) const { if (displayFromName()) { if (y >= trect.top() && y < trect.top() + st::msgNameFont->height) { if (x >= trect.left() && x < trect.left() + trect.width() && x < trect.left() + author()->nameText.maxWidth()) { outResult->link = author()->openLink(); return true; } auto fwd = Get(); auto via = Get(); if (via && !fwd && x >= trect.left() + author()->nameText.maxWidth() + st::msgServiceFont->spacew && x < trect.left() + author()->nameText.maxWidth() + st::msgServiceFont->spacew + via->_width) { outResult->link = via->_lnk; return true; } } trect.setTop(trect.top() + st::msgNameFont->height); } return false; } bool HistoryMessage::getStateForwardedInfo(int x, int y, QRect &trect, HistoryTextState *outResult, const HistoryStateRequest &request) const { if (displayForwardedFrom()) { auto fwd = Get(); int32 fwdheight = ((fwd->_text.maxWidth() > trect.width()) ? 2 : 1) * st::semiboldFont->height; if (y >= trect.top() && y < trect.top() + fwdheight) { bool breakEverywhere = (fwd->_text.countHeight(trect.width()) > 2 * st::semiboldFont->height); auto textRequest = request.forText(); if (breakEverywhere) { textRequest.flags |= Text::StateRequest::Flag::BreakEverywhere; } *outResult = fwd->_text.getState(x - trect.left(), y - trect.top(), trect.width(), textRequest); outResult->symbol = 0; outResult->afterSymbol = false; if (breakEverywhere) { outResult->cursor = HistoryInForwardedCursorState; } else { outResult->cursor = HistoryDefaultCursorState; } return true; } trect.setTop(trect.top() + fwdheight); } return false; } bool HistoryMessage::getStateReplyInfo(int x, int y, QRect &trect, HistoryTextState *outResult) const { if (auto reply = Get()) { int32 h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); if (y >= trect.top() && y < trect.top() + h) { if (reply->replyToMsg && y >= trect.top() + st::msgReplyPadding.top() && y < trect.top() + st::msgReplyPadding.top() + st::msgReplyBarSize.height() && x >= trect.left() && x < trect.left() + trect.width()) { outResult->link = reply->replyToLink(); } return true; } trect.setTop(trect.top() + h); } return false; } bool HistoryMessage::getStateViaBotIdInfo(int x, int y, QRect &trect, HistoryTextState *outResult) const { if (!displayFromName() && !Has()) { if (auto via = Get()) { if (x >= trect.left() && y >= trect.top() && y < trect.top() + st::msgNameFont->height && x < trect.left() + via->_width) { outResult->link = via->_lnk; return true; } trect.setTop(trect.top() + st::msgNameFont->height); } } return false; } bool HistoryMessage::getStateText(int x, int y, QRect &trect, HistoryTextState *outResult, const HistoryStateRequest &request) const { if (trect.contains(x, y)) { *outResult = _text.getState(x - trect.x(), y - trect.y(), trect.width(), request.forText()); return true; } return false; } TextSelection HistoryMessage::adjustSelection(TextSelection selection, TextSelectType type) const { if (!_media || selection.to <= _text.length()) { return _text.adjustSelection(selection, type); } auto mediaSelection = _media->adjustSelection(toMediaSelection(selection), type); if (selection.from >= _text.length()) { return fromMediaSelection(mediaSelection); } auto textSelection = _text.adjustSelection(selection, type); return { textSelection.from, fromMediaSelection(mediaSelection).to }; } QString HistoryMessage::notificationHeader() const { return (!_history->peer->isUser() && !isPost()) ? from()->name : QString(); } bool HistoryMessage::displayFromPhoto() const { return hasFromPhoto() && !isAttachedToNext(); } bool HistoryMessage::hasFromPhoto() const { return (Adaptive::ChatWide() || (!out() && !history()->peer->isUser())) && !isPost() && !isEmpty(); } HistoryMessage::~HistoryMessage() { _media.reset(); if (auto reply = Get()) { reply->clearData(this); } } void HistoryService::setMessageByAction(const MTPmessageAction &action) { auto prepareChatAddUserText = [this](const MTPDmessageActionChatAddUser &action) { auto result = PreparedText {}; auto &users = action.vusers.v; if (users.size() == 1) { auto u = App::user(peerFromUser(users[0])); if (u == _from) { result.links.push_back(fromLink()); result.text = lng_action_user_joined(lt_from, fromLinkText()); } else { result.links.push_back(fromLink()); result.links.push_back(peerOpenClickHandler(u)); result.text = lng_action_add_user(lt_from, fromLinkText(), lt_user, textcmdLink(2, u->name)); } } else if (users.isEmpty()) { result.links.push_back(fromLink()); result.text = lng_action_add_user(lt_from, fromLinkText(), lt_user, "somebody"); } else { result.links.push_back(fromLink()); for (auto i = 0, l = users.size(); i != l; ++i) { auto user = App::user(peerFromUser(users[i])); result.links.push_back(peerOpenClickHandler(user)); auto linkText = textcmdLink(i + 2, user->name); if (i == 0) { result.text = linkText; } else if (i + 1 == l) { result.text = lng_action_add_users_and_last(lt_accumulated, result.text, lt_user, linkText); } else { result.text = lng_action_add_users_and_one(lt_accumulated, result.text, lt_user, linkText); } } result.text = lng_action_add_users_many(lt_from, fromLinkText(), lt_users, result.text); } return result; }; auto prepareChatJoinedByLink = [this](const MTPDmessageActionChatJoinedByLink &action) { auto result = PreparedText {}; result.links.push_back(fromLink()); result.text = lng_action_user_joined_by_link(lt_from, fromLinkText()); return result; }; auto prepareChatCreate = [this](const MTPDmessageActionChatCreate &action) { auto result = PreparedText {}; result.links.push_back(fromLink()); result.text = lng_action_created_chat(lt_from, fromLinkText(), lt_title, textClean(qs(action.vtitle))); return result; }; auto prepareChannelCreate = [this](const MTPDmessageActionChannelCreate &action) { auto result = PreparedText {}; if (isPost()) { result.text = lang(lng_action_created_channel); } else { result.links.push_back(fromLink()); result.text = lng_action_created_chat(lt_from, fromLinkText(), lt_title, textClean(qs(action.vtitle))); } return result; }; auto prepareChatDeletePhoto = [this] { auto result = PreparedText {}; if (isPost()) { result.text = lang(lng_action_removed_photo_channel); } else { result.links.push_back(fromLink()); result.text = lng_action_removed_photo(lt_from, fromLinkText()); } return result; }; auto prepareChatDeleteUser = [this](const MTPDmessageActionChatDeleteUser &action) { auto result = PreparedText {}; if (peerFromUser(action.vuser_id) == _from->id) { result.links.push_back(fromLink()); result.text = lng_action_user_left(lt_from, fromLinkText()); } else { auto user = App::user(peerFromUser(action.vuser_id)); result.links.push_back(fromLink()); result.links.push_back(peerOpenClickHandler(user)); result.text = lng_action_kick_user(lt_from, fromLinkText(), lt_user, textcmdLink(2, user->name)); } return result; }; auto prepareChatEditPhoto = [this](const MTPDmessageActionChatEditPhoto &action) { auto result = PreparedText {}; if (isPost()) { result.text = lang(lng_action_changed_photo_channel); } else { result.links.push_back(fromLink()); result.text = lng_action_changed_photo(lt_from, fromLinkText()); } return result; }; auto prepareChatEditTitle = [this](const MTPDmessageActionChatEditTitle &action) { auto result = PreparedText {}; if (isPost()) { result.text = lng_action_changed_title_channel(lt_title, textClean(qs(action.vtitle))); } else { result.links.push_back(fromLink()); result.text = lng_action_changed_title(lt_from, fromLinkText(), lt_title, textClean(qs(action.vtitle))); } return result; }; auto preparePhoneCallText = [this](const MTPDmessageActionPhoneCall &action) { auto result = PreparedText {}; auto timeText = date.toString(cTimeFormat()); auto duration = action.has_duration() ? qMax(action.vduration.v, 0) : 0; auto durationText = ([duration]() -> QString { if (!duration) { return QString(); } if (duration >= 60) { auto minutes = duration / 60; auto seconds = duration % 60; return lng_duration_minutes_seconds(lt_count_minutes, minutes, lt_count_seconds, seconds); } return lng_duration_seconds(lt_count, duration); })(); auto wasMissed = [&action] { if (action.has_reason()) switch (action.vreason.type()) { case mtpc_phoneCallDiscardReasonBusy: case mtpc_phoneCallDiscardReasonMissed: return true; } return false; }; if (out()) { if (wasMissed()) { result.text = lng_action_call_outgoing_missed(lt_time, timeText); } else if (duration) { result.text = lng_action_call_outgoing_duration(lt_duration, durationText, lt_time, timeText); } else { result.text = lng_action_call_outgoing(lt_time, timeText); } } else { if (wasMissed()) { result.text = lng_action_call_incoming_missed(lt_time, timeText); } else if (duration) { result.text = lng_action_call_incoming_duration(lt_duration, durationText, lt_time, timeText); } else { result.text = lng_action_call_incoming(lt_time, timeText); } } return result; }; auto messageText = PreparedText {}; switch (action.type()) { case mtpc_messageActionChatAddUser: messageText = prepareChatAddUserText(action.c_messageActionChatAddUser()); break; case mtpc_messageActionChatJoinedByLink: messageText = prepareChatJoinedByLink(action.c_messageActionChatJoinedByLink()); break; case mtpc_messageActionChatCreate: messageText = prepareChatCreate(action.c_messageActionChatCreate()); break; case mtpc_messageActionChannelCreate: messageText = prepareChannelCreate(action.c_messageActionChannelCreate()); break; case mtpc_messageActionHistoryClear: break; // Leave empty text. case mtpc_messageActionChatDeletePhoto: messageText = prepareChatDeletePhoto(); break; case mtpc_messageActionChatDeleteUser: messageText = prepareChatDeleteUser(action.c_messageActionChatDeleteUser()); break; case mtpc_messageActionChatEditPhoto: messageText = prepareChatEditPhoto(action.c_messageActionChatEditPhoto()); break; case mtpc_messageActionChatEditTitle: messageText = prepareChatEditTitle(action.c_messageActionChatEditTitle()); break; case mtpc_messageActionChatMigrateTo: messageText.text = lang(lng_action_group_migrate); break; case mtpc_messageActionChannelMigrateFrom: messageText.text = lang(lng_action_group_migrate); break; case mtpc_messageActionPinMessage: messageText = preparePinnedText(); break; case mtpc_messageActionGameScore: messageText = prepareGameScoreText(); break; case mtpc_messageActionPhoneCall: messageText = preparePhoneCallText(action.c_messageActionPhoneCall()); break; case mtpc_messageActionPaymentSent: messageText = preparePaymentSentText(); break; default: messageText.text = lang(lng_message_empty); break; } setServiceText(messageText); // Additional information. switch (action.type()) { case mtpc_messageActionChatAddUser: { if (auto channel = history()->peer->asMegagroup()) { auto &users = action.c_messageActionChatAddUser().vusers; for_const (auto &item, users.v) { if (item.v == AuthSession::CurrentUserId()) { channel->mgInfo->joinedMessageFound = true; break; } } } } break; case mtpc_messageActionChatJoinedByLink: { if (_from->isSelf() && history()->peer->isMegagroup()) { history()->peer->asChannel()->mgInfo->joinedMessageFound = true; } } break; case mtpc_messageActionChatEditPhoto: { auto &photo = action.c_messageActionChatEditPhoto().vphoto; if (photo.type() == mtpc_photo) { _media = std::make_unique(this, history()->peer, photo.c_photo(), st::msgServicePhotoWidth); } } break; case mtpc_messageActionChatMigrateTo: case mtpc_messageActionChannelMigrateFrom: { _flags |= MTPDmessage_ClientFlag::f_is_group_migrate; } break; } } bool HistoryService::updateDependent(bool force) { auto dependent = GetDependentData(); t_assert(dependent != nullptr); if (!force) { if (!dependent->msgId || dependent->msg) { return true; } } if (!dependent->lnk) { dependent->lnk = goToMessageClickHandler(history()->peer, dependent->msgId); } bool gotDependencyItem = false; if (!dependent->msg) { dependent->msg = App::histItemById(channelId(), dependent->msgId); if (dependent->msg) { App::historyRegDependency(this, dependent->msg); gotDependencyItem = true; } } if (dependent->msg) { updateDependentText(); } else if (force) { if (dependent->msgId > 0) { dependent->msgId = 0; gotDependencyItem = true; } updateDependentText(); } if (force && gotDependencyItem) { AuthSession::Current().notifications().checkDelayed(); } return (dependent->msg || !dependent->msgId); } HistoryService::PreparedText HistoryService::preparePinnedText() { auto result = PreparedText {}; auto pinned = Get(); if (pinned && pinned->msg) { auto mediaText = ([pinned]() -> QString { auto media = pinned->msg->getMedia(); switch (media ? media->type() : MediaTypeCount) { case MediaTypePhoto: return lang(lng_action_pinned_media_photo); case MediaTypeVideo: return lang(lng_action_pinned_media_video); case MediaTypeContact: return lang(lng_action_pinned_media_contact); case MediaTypeFile: return lang(lng_action_pinned_media_file); case MediaTypeGif: { if (auto document = media->getDocument()) { if (document->isRoundVideo()) { return lang(lng_action_pinned_media_video_message); } } return lang(lng_action_pinned_media_gif); } break; case MediaTypeSticker: { auto emoji = static_cast(media)->emoji(); if (emoji.isEmpty()) { return lang(lng_action_pinned_media_sticker); } return lng_action_pinned_media_emoji_sticker(lt_emoji, emoji); } break; case MediaTypeLocation: return lang(lng_action_pinned_media_location); case MediaTypeMusicFile: return lang(lng_action_pinned_media_audio); case MediaTypeVoiceFile: return lang(lng_action_pinned_media_voice); case MediaTypeGame: { auto title = static_cast(media)->game()->title; return lng_action_pinned_media_game(lt_game, title); } break; } return QString(); })(); result.links.push_back(fromLink()); result.links.push_back(pinned->lnk); if (mediaText.isEmpty()) { auto original = pinned->msg->originalText().text; auto cutAt = 0; auto limit = kPinnedMessageTextLimit; auto size = original.size(); for (; limit != 0;) { --limit; if (cutAt >= size) break; if (original.at(cutAt).isLowSurrogate() && cutAt + 1 < size && original.at(cutAt + 1).isHighSurrogate()) { cutAt += 2; } else { ++cutAt; } } if (!limit && cutAt + 5 < size) { original = original.mid(0, cutAt) + qstr("..."); } result.text = lng_action_pinned_message(lt_from, fromLinkText(), lt_text, textcmdLink(2, original)); } else { result.text = lng_action_pinned_media(lt_from, fromLinkText(), lt_media, textcmdLink(2, mediaText)); } } else if (pinned && pinned->msgId) { result.links.push_back(fromLink()); result.links.push_back(pinned->lnk); result.text = lng_action_pinned_media(lt_from, fromLinkText(), lt_media, textcmdLink(2, lang(lng_contacts_loading))); } else { result.links.push_back(fromLink()); result.text = lng_action_pinned_media(lt_from, fromLinkText(), lt_media, lang(lng_deleted_message)); } return result; } HistoryService::PreparedText HistoryService::prepareGameScoreText() { auto result = PreparedText {}; auto gamescore = Get(); auto computeGameTitle = [gamescore, &result]() -> QString { if (gamescore && gamescore->msg) { if (auto media = gamescore->msg->getMedia()) { if (media->type() == MediaTypeGame) { result.links.push_back(MakeShared(gamescore->msg, 0, 0)); auto titleText = static_cast(media)->game()->title; return textcmdLink(result.links.size(), titleText); } } return lang(lng_deleted_message); } else if (gamescore && gamescore->msgId) { return lang(lng_contacts_loading); } return QString(); }; auto scoreNumber = gamescore ? gamescore->score : 0; if (_from->isSelf()) { auto gameTitle = computeGameTitle(); if (gameTitle.isEmpty()) { result.text = lng_action_game_you_scored_no_game(lt_count, scoreNumber); } else { result.text = lng_action_game_you_scored(lt_count, scoreNumber, lt_game, gameTitle); } } else { result.links.push_back(fromLink()); auto gameTitle = computeGameTitle(); if (gameTitle.isEmpty()) { result.text = lng_action_game_score_no_game(lt_from, fromLinkText(), lt_count, scoreNumber); } else { result.text = lng_action_game_score(lt_from, fromLinkText(), lt_count, scoreNumber, lt_game, gameTitle); } } return result; } HistoryService::PreparedText HistoryService::preparePaymentSentText() { auto result = PreparedText {}; auto payment = Get(); auto invoiceTitle = ([payment]() -> QString { if (payment && payment->msg) { if (auto media = payment->msg->getMedia()) { if (media->type() == MediaTypeInvoice) { return static_cast(media)->getTitle(); } } return lang(lng_deleted_message); } else if (payment && payment->msgId) { return lang(lng_contacts_loading); } return QString(); })(); if (invoiceTitle.isEmpty()) { result.text = lng_action_payment_done(lt_amount, payment->amount, lt_user, history()->peer->name); } else { result.text = lng_action_payment_done_for(lt_amount, payment->amount, lt_user, history()->peer->name, lt_invoice, invoiceTitle); } return result; } HistoryService::HistoryService(History *history, const MTPDmessageService &msg) : HistoryItem(history, msg.vid.v, mtpCastFlags(msg.vflags.v), ::date(msg.vdate), msg.has_from_id() ? msg.vfrom_id.v : 0) { createFromMtp(msg); setMessageByAction(msg.vaction); } HistoryService::HistoryService(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags, int32 from) : HistoryItem(history, msgId, flags, date, from) { setServiceText({ msg }); } void HistoryService::initDimensions() { _maxw = _text.maxWidth() + st::msgServicePadding.left() + st::msgServicePadding.right(); _minh = _text.minHeight(); if (_media) _media->initDimensions(); } bool HistoryService::updateDependencyItem() { if (GetDependentData()) { return updateDependent(true); } return HistoryItem::updateDependencyItem(); } void HistoryService::countPositionAndSize(int32 &left, int32 &width) const { left = st::msgServiceMargin.left(); int32 maxwidth = _history->width; if (Adaptive::ChatWide()) { maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } width = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); } TextWithEntities HistoryService::selectedText(TextSelection selection) const { return _text.originalTextWithEntities((selection == FullSelection) ? AllTextSelection : selection); } QString HistoryService::inDialogsText() const { return textcmdLink(1, textClean(notificationText())); } QString HistoryService::inReplyText() const { QString result = HistoryService::notificationText(); return result.trimmed().startsWith(author()->name) ? result.trimmed().mid(author()->name.size()).trimmed() : result; } void HistoryService::setServiceText(const PreparedText &prepared) { _text.setText(st::serviceTextStyle, prepared.text, _historySrvOptions); auto linkIndex = 0; for_const (auto &link, prepared.links) { // Link indices start with 1. _text.setLink(++linkIndex, link); } setPendingInitDimensions(); _textWidth = -1; _textHeight = 0; } void HistoryService::draw(Painter &p, const QRect &r, TextSelection selection, TimeMs ms) const { int height = _height - st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); QRect clip(r); int dateh = 0, unreadbarh = 0; if (auto date = Get()) { dateh = date->height(); //if (clip.intersects(QRect(0, 0, _history->width, dateh))) { // date->paint(p, 0, _history->width); //} p.translate(0, dateh); clip.translate(0, -dateh); height -= dateh; } if (auto unreadbar = Get()) { unreadbarh = unreadbar->height(); if (clip.intersects(QRect(0, 0, _history->width, unreadbarh))) { unreadbar->paint(p, 0, _history->width); } p.translate(0, unreadbarh); clip.translate(0, -unreadbarh); height -= unreadbarh; } HistoryLayout::PaintContext context(ms, clip, selection); HistoryLayout::ServiceMessagePainter::paint(p, this, context, height); if (int skiph = dateh + unreadbarh) { p.translate(0, -skiph); } } int32 HistoryService::resizeGetHeight_(int32 width) { _height = displayedDateHeight(); if (auto unreadbar = Get()) { _height += unreadbar->height(); } if (_text.isEmpty()) { _textHeight = 0; } else { int32 maxwidth = _history->width; if (Adaptive::ChatWide()) { maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } if (width > maxwidth) width = maxwidth; width -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins if (width < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) width = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; int32 nwidth = qMax(width - st::msgServicePadding.left() - st::msgServicePadding.right(), 0); if (nwidth != _textWidth) { _textWidth = nwidth; _textHeight = _text.countHeight(nwidth); } if (width >= _maxw) { _height += _minh; } else { _height += _textHeight; } _height += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom(); if (_media) { _height += st::msgServiceMargin.top() + _media->resizeGetHeight(_media->currentWidth()); } } return _height; } bool HistoryService::hasPoint(int x, int y) const { int left = 0, width = 0, height = _height - st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); // two small margins countPositionAndSize(left, width); if (width < 1) return false; if (int dateh = displayedDateHeight()) { y -= dateh; height -= dateh; } if (auto unreadbar = Get()) { int unreadbarh = unreadbar->height(); y -= unreadbarh; height -= unreadbarh; } if (_media) { height -= st::msgServiceMargin.top() + _media->height(); } return QRect(left, st::msgServiceMargin.top(), width, height).contains(x, y); } HistoryTextState HistoryService::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; int left = 0, width = 0, height = _height - st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); // two small margins countPositionAndSize(left, width); if (width < 1) return result; if (int dateh = displayedDateHeight()) { y -= dateh; height -= dateh; } if (auto unreadbar = Get()) { int unreadbarh = unreadbar->height(); y -= unreadbarh; height -= unreadbarh; } if (_media) { height -= st::msgServiceMargin.top() + _media->height(); } auto outer = QRect(left, st::msgServiceMargin.top(), width, height); auto trect = outer.marginsAdded(-st::msgServicePadding); if (trect.contains(x, y)) { auto textRequest = request.forText(); textRequest.align = style::al_center; result = _text.getState(x - trect.x(), y - trect.y(), trect.width(), textRequest); if (auto gamescore = Get()) { if (!result.link && result.cursor == HistoryInTextCursorState && outer.contains(x, y)) { result.link = gamescore->lnk; } } else if (auto payment = Get()) { if (!result.link && result.cursor == HistoryInTextCursorState && outer.contains(x, y)) { result.link = payment->lnk; } } } else if (_media) { result = _media->getState(x - st::msgServiceMargin.left() - (width - _media->maxWidth()) / 2, y - st::msgServiceMargin.top() - height - st::msgServiceMargin.top(), request); } return result; } void HistoryService::createFromMtp(const MTPDmessageService &message) { if (message.vaction.type() == mtpc_messageActionGameScore) { UpdateComponents(HistoryServiceGameScore::Bit()); Get()->score = message.vaction.c_messageActionGameScore().vscore.v; } else if (message.vaction.type() == mtpc_messageActionPaymentSent) { UpdateComponents(HistoryServicePayment::Bit()); auto amount = message.vaction.c_messageActionPaymentSent().vtotal_amount.v; auto currency = qs(message.vaction.c_messageActionPaymentSent().vcurrency); Get()->amount = HistoryInvoice::fillAmountAndCurrency(amount, currency); } if (message.has_reply_to_msg_id()) { if (message.vaction.type() == mtpc_messageActionPinMessage) { UpdateComponents(HistoryServicePinned::Bit()); } if (auto dependent = GetDependentData()) { dependent->msgId = message.vreply_to_msg_id.v; if (!updateDependent() && App::api()) { App::api()->requestMessageData(history()->peer->asChannel(), dependent->msgId, historyDependentItemCallback(fullId())); } } } setMessageByAction(message.vaction); } void HistoryService::applyEdition(const MTPDmessageService &message) { clearDependency(); UpdateComponents(0); createFromMtp(message); if (message.vaction.type() == mtpc_messageActionHistoryClear) { removeMedia(); finishEditionToEmpty(); } else { finishEdition(-1); } } void HistoryService::removeMedia() { if (!_media) return; bool mediaWasDisplayed = _media->isDisplayed(); _media.reset(); if (mediaWasDisplayed) { _textWidth = -1; _textHeight = 0; } } int32 HistoryService::addToOverview(AddToOverviewMethod method) { if (!indexInOverview()) return 0; int32 result = 0; if (auto media = getMedia()) { MediaOverviewType type = serviceMediaToOverviewType(media); if (type != OverviewCount) { if (history()->addToOverview(type, id, method)) { result |= (1 << type); } } } return result; } void HistoryService::eraseFromOverview() { if (auto media = getMedia()) { MediaOverviewType type = serviceMediaToOverviewType(media); if (type != OverviewCount) { history()->eraseFromOverview(type, id); } } } void HistoryService::updateDependentText() { auto text = PreparedText {}; if (Has()) { text = preparePinnedText(); } else if (Has()) { text = prepareGameScoreText(); } else if (Has()) { text = preparePaymentSentText(); } else { return; } setServiceText(text); if (history()->textCachedFor == this) { history()->textCachedFor = nullptr; } if (App::main()) { App::main()->dlgUpdated(history()->peer, id); } App::historyUpdateDependent(this); } void HistoryService::clearDependency() { if (auto dependent = GetDependentData()) { if (dependent->msg) { App::historyUnregDependency(this, dependent->msg); } } } HistoryService::~HistoryService() { clearDependency(); _media.reset(); } HistoryJoined::HistoryJoined(History *history, const QDateTime &inviteDate, UserData *inviter, MTPDmessage::Flags flags) : HistoryService(history, clientMsgId(), inviteDate, QString(), flags) { auto prepared = PreparedText {}; prepared.text = ([history, inviter, &prepared]() { if (inviter->id == AuthSession::CurrentUserPeerId()) { return lang(history->isMegagroup() ? lng_action_you_joined_group : lng_action_you_joined); } prepared.links.push_back(peerOpenClickHandler(inviter)); if (history->isMegagroup()) { return lng_action_add_you_group(lt_from, textcmdLink(1, inviter->name)); } return lng_action_add_you(lt_from, textcmdLink(1, inviter->name)); })(); setServiceText(prepared); }