/* 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_service.h" #include "lang/lang_keys.h" #include "mainwidget.h" #include "apiwrap.h" #include "history/history_service_layout.h" #include "history/history_media_types.h" #include "history/history_message.h" #include "auth_session.h" #include "window/notifications_manager.h" namespace { constexpr auto kPinnedMessageTextLimit = 16; } // namespace TextParseOptions _historySrvOptions = { TextParseLinks | TextParseMentions | TextParseHashtags/* | TextParseMultiline*/ | TextParseRichText, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // lang-dependent }; 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 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: Unexpected("PhoneCall type in HistoryService."); 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_count, scoreNumber, lt_from, fromLinkText()); } else { result.text = lng_action_game_score(lt_count, scoreNumber, lt_from, fromLinkText(), 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(gsl::not_null history, const MTPDmessageService &message) : HistoryItem(history, message.vid.v, mtpCastFlags(message.vflags.v), ::date(message.vdate), message.has_from_id() ? message.vfrom_id.v : 0) { createFromMtp(message); setMessageByAction(message.vaction); } HistoryService::HistoryService(gsl::not_null history, MsgId msgId, QDateTime date, const PreparedText &message, MTPDmessage::Flags flags, int32 from, PhotoData *photo) : HistoryItem(history, msgId, flags, date, from) { setServiceText(message); if (photo) { _media = std::make_unique(this, history->peer, photo, st::msgServicePhotoWidth); } } 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(); } QRect HistoryService::countGeometry() const { auto result = QRect(0, 0, width(), _height); if (Adaptive::ChatWide()) { result.setWidth(qMin(result.width(), st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } return result.marginsRemoved(st::msgServiceMargin); } 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, QRect clip, TextSelection selection, TimeMs ms) const { auto height = _height - st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); auto dateh = 0; auto unreadbarh = 0; if (auto date = Get()) { dateh = date->height(); p.translate(0, dateh); clip.translate(0, -dateh); height -= dateh; } if (auto unreadbar = Get()) { unreadbarh = unreadbar->height(); if (clip.intersects(QRect(0, 0, width(), unreadbarh))) { unreadbar->paint(p, 0, 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 (auto skiph = dateh + unreadbarh) { p.translate(0, -skiph); } } int HistoryService::resizeContentGetHeight() { _height = displayedDateHeight(); if (auto unreadbar = Get()) { _height += unreadbar->height(); } if (_text.isEmpty()) { _textHeight = 0; } else { auto contentWidth = width(); if (Adaptive::ChatWide()) { accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); } contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; } auto nwidth = qMax(contentWidth - st::msgServicePadding.left() - st::msgServicePadding.right(), 0); if (nwidth != _textWidth) { _textWidth = nwidth; _textHeight = _text.countHeight(nwidth); } if (contentWidth >= _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(QPoint point) const { auto g = countGeometry(); if (g.width() < 1) { return false; } if (auto dateh = displayedDateHeight()) { g.setTop(g.top() + dateh); } if (auto unreadbar = Get()) { g.setTop(g.top() + unreadbar->height()); } if (_media) { g.setHeight(g.height() - (st::msgServiceMargin.top() + _media->height())); } return g.contains(point); } HistoryTextState HistoryService::getState(QPoint point, HistoryStateRequest request) const { HistoryTextState result; auto g = countGeometry(); if (g.width() < 1) { return result; } if (auto dateh = displayedDateHeight()) { point.setY(point.y() - dateh); g.setHeight(g.height() - dateh); } if (auto unreadbar = Get()) { auto unreadbarh = unreadbar->height(); point.setY(point.y() - unreadbarh); g.setHeight(g.height() - unreadbarh); } if (_media) { g.setHeight(g.height() - (st::msgServiceMargin.top() + _media->height())); } auto trect = g.marginsAdded(-st::msgServicePadding); if (trect.contains(point)) { auto textRequest = request.forText(); textRequest.align = style::al_center; result = _text.getState(point - trect.topLeft(), trect.width(), textRequest); if (auto gamescore = Get()) { if (!result.link && result.cursor == HistoryInTextCursorState && g.contains(point)) { result.link = gamescore->lnk; } } else if (auto payment = Get()) { if (!result.link && result.cursor == HistoryInTextCursorState && g.contains(point)) { result.link = payment->lnk; } } } else if (_media) { result = _media->getState(point - QPoint(st::msgServiceMargin.left() + (g.width() - _media->maxWidth()) / 2, st::msgServiceMargin.top() + g.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::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (_media) _media->clickHandlerActiveChanged(p, active); HistoryItem::clickHandlerActiveChanged(p, active); } void HistoryService::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { if (_media) _media->clickHandlerPressedChanged(p, pressed); HistoryItem::clickHandlerPressedChanged(p, pressed); } 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()) { result |= media->addToOverview(method); } return result; } void HistoryService::eraseFromOverview() { if (auto media = getMedia()) { media->eraseFromOverview(); } } 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(gsl::not_null history, const QDateTime &inviteDate, gsl::not_null inviter, MTPDmessage::Flags flags) : HistoryService(history, clientMsgId(), inviteDate, GenerateText(history, inviter), flags) { } HistoryJoined::PreparedText HistoryJoined::GenerateText(gsl::not_null history, gsl::not_null inviter) { if (inviter->id == AuthSession::CurrentUserPeerId()) { return { lang(history->isMegagroup() ? lng_action_you_joined_group : lng_action_you_joined) }; } auto result = PreparedText {}; result.links.push_back(peerOpenClickHandler(inviter)); if (history->isMegagroup()) { result.text = lng_action_add_you_group(lt_from, textcmdLink(1, inviter->name)); } result.text = lng_action_add_you(lt_from, textcmdLink(1, inviter->name)); return result; }