/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/history_service.h" #include "lang/lang_keys.h" #include "mainwidget.h" #include "main/main_session.h" #include "main/main_domain.h" // Core::App().domain().activate(). #include "apiwrap.h" #include "history/history.h" #include "history/view/media/history_view_invoice.h" #include "history/history_message.h" #include "history/history_item_components.h" #include "history/view/history_view_service_message.h" #include "history/view/history_view_item_preview.h" #include "history/view/history_view_spoiler_click_handler.h" #include "data/data_folder.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_game.h" #include "data/data_channel.h" #include "data/data_user.h" #include "data/data_chat.h" #include "data/data_changes.h" #include "data/data_group_call.h" // Data::GroupCall::id(). #include "core/application.h" #include "core/click_handler_types.h" #include "base/unixtime.h" #include "base/timer_rpl.h" #include "calls/calls_instance.h" // Core::App().calls().joinGroupCall. #include "window/notifications_manager.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "storage/storage_shared_media.h" #include "payments/payments_checkout_process.h" // CheckoutProcess::Start. #include "ui/text/format_values.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" namespace { constexpr auto kPinnedMessageTextLimit = 16; using ItemPreview = HistoryView::ItemPreview; [[nodiscard]] bool PeerCallKnown(not_null peer) { if (peer->groupCall() != nullptr) { return true; } else if (const auto chat = peer->asChat()) { return !(chat->flags() & ChatDataFlag::CallActive); } else if (const auto channel = peer->asChannel()) { return !(channel->flags() & ChannelDataFlag::CallActive); } return true; } [[nodiscard]] rpl::producer PeerHasThisCallValue( not_null peer, CallId id) { return peer->session().changes().peerFlagsValue( peer, Data::PeerUpdate::Flag::GroupCall ) | rpl::filter([=] { return PeerCallKnown(peer); }) | rpl::map([=] { const auto call = peer->groupCall(); return (call && call->id() == id); }) | rpl::distinct_until_changed( ) | rpl::take_while([=](bool hasThisCall) { return hasThisCall; }) | rpl::then( rpl::single(false) ); } [[nodiscard]] CallId CallIdFromInput(const MTPInputGroupCall &data) { return data.match([&](const MTPDinputGroupCall &data) { return data.vid().v; }); } [[nodiscard]] ClickHandlerPtr GroupCallClickHandler( not_null peer, CallId callId) { return std::make_shared([=] { const auto call = peer->groupCall(); if (call && call->id() == callId) { const auto &windows = peer->session().windows(); if (windows.empty()) { Core::App().domain().activate(&peer->session().account()); if (windows.empty()) { return; } } windows.front()->startOrJoinGroupCall(peer, {}); } }); } } // namespace 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 = history()->owner().user(users[0].v); if (u == _from) { result.links.push_back(fromLink()); result.text = tr::lng_action_user_joined( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.links.push_back(u->createOpenLink()); result.text = tr::lng_action_add_user( tr::now, lt_from, fromLinkText(), // Link 1. lt_user, Ui::Text::Link(u->name, 2), // Link 2. Ui::Text::WithEntities); } } else if (users.isEmpty()) { result.links.push_back(fromLink()); result.text = tr::lng_action_add_user( tr::now, lt_from, fromLinkText(), // Link 1. lt_user, { .text = qsl("somebody") }, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); for (auto i = 0, l = int(users.size()); i != l; ++i) { auto user = history()->owner().user(users[i].v); result.links.push_back(user->createOpenLink()); auto linkText = Ui::Text::Link(user->name, 2 + i); if (i == 0) { result.text = linkText; } else if (i + 1 == l) { result.text = tr::lng_action_add_users_and_last( tr::now, lt_accumulated, result.text, lt_user, linkText, Ui::Text::WithEntities); } else { result.text = tr::lng_action_add_users_and_one( tr::now, lt_accumulated, result.text, lt_user, linkText, Ui::Text::WithEntities); } } result.text = tr::lng_action_add_users_many( tr::now, lt_from, fromLinkText(), // Link 1. lt_users, result.text, Ui::Text::WithEntities); } return result; }; auto prepareChatJoinedByLink = [this](const MTPDmessageActionChatJoinedByLink &action) { auto result = PreparedText{}; result.links.push_back(fromLink()); result.text = tr::lng_action_user_joined_by_link( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); return result; }; auto prepareChatCreate = [this](const MTPDmessageActionChatCreate &action) { auto result = PreparedText{}; result.links.push_back(fromLink()); result.text = tr::lng_action_created_chat( tr::now, lt_from, fromLinkText(), // Link 1. lt_title, { .text = qs(action.vtitle()) }, Ui::Text::WithEntities); return result; }; auto prepareChannelCreate = [this](const MTPDmessageActionChannelCreate &action) { auto result = PreparedText {}; if (isPost()) { result.text = tr::lng_action_created_channel( tr::now, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_created_chat( tr::now, lt_from, fromLinkText(), // Link 1. lt_title, { .text = qs(action.vtitle()) }, Ui::Text::WithEntities); } return result; }; auto prepareChatDeletePhoto = [this] { auto result = PreparedText{}; if (isPost()) { result.text = tr::lng_action_removed_photo_channel( tr::now, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_removed_photo( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); } 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 = tr::lng_action_user_left( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); } else { auto user = history()->owner().user(action.vuser_id().v); result.links.push_back(fromLink()); result.links.push_back(user->createOpenLink()); result.text = tr::lng_action_kick_user( tr::now, lt_from, fromLinkText(), // Link 1. lt_user, Ui::Text::Link(user->name, 2), // Link 2. Ui::Text::WithEntities); } return result; }; auto prepareChatEditPhoto = [this](const MTPDmessageActionChatEditPhoto &action) { auto result = PreparedText{}; if (isPost()) { result.text = tr::lng_action_changed_photo_channel( tr::now, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_changed_photo( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); } return result; }; auto prepareChatEditTitle = [this](const MTPDmessageActionChatEditTitle &action) { auto result = PreparedText{}; if (isPost()) { result.text = tr::lng_action_changed_title_channel( tr::now, lt_title, { .text = (qs(action.vtitle())) }, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_changed_title( tr::now, lt_from, fromLinkText(), // Link 1. lt_title, { .text = qs(action.vtitle()) }, Ui::Text::WithEntities); } return result; }; auto prepareScreenshotTaken = [this] { auto result = PreparedText{}; if (out()) { result.text = tr::lng_action_you_took_screenshot( tr::now, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_took_screenshot( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); } return result; }; auto prepareCustomAction = [&](const MTPDmessageActionCustomAction &action) { auto result = PreparedText{}; result.text = { .text = qs(action.vmessage()) }; return result; }; auto prepareBotAllowed = [&](const MTPDmessageActionBotAllowed &action) { auto result = PreparedText{}; const auto domain = qs(action.vdomain()); result.text = tr::lng_action_bot_allowed_from_domain( tr::now, lt_domain, Ui::Text::Link(domain, qstr("http://") + domain), Ui::Text::WithEntities); return result; }; auto prepareSecureValuesSent = [&](const MTPDmessageActionSecureValuesSent &action) { auto result = PreparedText{}; auto documents = QStringList(); for (const auto &type : action.vtypes().v) { documents.push_back([&] { switch (type.type()) { case mtpc_secureValueTypePersonalDetails: return tr::lng_action_secure_personal_details(tr::now); case mtpc_secureValueTypePassport: case mtpc_secureValueTypeDriverLicense: case mtpc_secureValueTypeIdentityCard: case mtpc_secureValueTypeInternalPassport: return tr::lng_action_secure_proof_of_identity(tr::now); case mtpc_secureValueTypeAddress: return tr::lng_action_secure_address(tr::now); case mtpc_secureValueTypeUtilityBill: case mtpc_secureValueTypeBankStatement: case mtpc_secureValueTypeRentalAgreement: case mtpc_secureValueTypePassportRegistration: case mtpc_secureValueTypeTemporaryRegistration: return tr::lng_action_secure_proof_of_address(tr::now); case mtpc_secureValueTypePhone: return tr::lng_action_secure_phone(tr::now); case mtpc_secureValueTypeEmail: return tr::lng_action_secure_email(tr::now); } Unexpected("Type in prepareSecureValuesSent."); }()); }; result.links.push_back(history()->peer->createOpenLink()); result.text = tr::lng_action_secure_values_sent( tr::now, lt_user, Ui::Text::Link(history()->peer->name, QString()), // Link 1. lt_documents, { .text = documents.join(", ") }, Ui::Text::WithEntities); return result; }; auto prepareContactSignUp = [this] { auto result = PreparedText{}; result.links.push_back(fromLink()); result.text = tr::lng_action_user_registered( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); return result; }; auto prepareProximityReached = [this](const MTPDmessageActionGeoProximityReached &action) { auto result = PreparedText{}; const auto fromId = peerFromMTP(action.vfrom_id()); const auto fromPeer = history()->owner().peer(fromId); const auto toId = peerFromMTP(action.vto_id()); const auto toPeer = history()->owner().peer(toId); const auto selfId = _from->session().userPeerId(); const auto distanceMeters = action.vdistance().v; const auto distance = [&] { if (distanceMeters >= 1000) { const auto km = (10 * (distanceMeters / 10)) / 1000.; return tr::lng_action_proximity_distance_km( tr::now, lt_count, km); } else { return tr::lng_action_proximity_distance_m( tr::now, lt_count, distanceMeters); } }(); result.text = [&] { if (fromId == selfId) { result.links.push_back(toPeer->createOpenLink()); return tr::lng_action_you_proximity_reached( tr::now, lt_distance, { .text = distance }, lt_user, Ui::Text::Link(toPeer->name, QString()), // Link 1. Ui::Text::WithEntities); } else if (toId == selfId) { result.links.push_back(fromPeer->createOpenLink()); return tr::lng_action_proximity_reached_you( tr::now, lt_from, Ui::Text::Link(fromPeer->name, QString()), // Link 1. lt_distance, { .text = distance }, Ui::Text::WithEntities); } else { result.links.push_back(fromPeer->createOpenLink()); result.links.push_back(toPeer->createOpenLink()); return tr::lng_action_proximity_reached( tr::now, lt_from, Ui::Text::Link(fromPeer->name, 1), // Link 1. lt_distance, { .text = distance }, lt_user, Ui::Text::Link(toPeer->name, 2), // Link 2. Ui::Text::WithEntities); } }(); return result; }; auto prepareGroupCall = [this](const MTPDmessageActionGroupCall &action) { auto result = PreparedText{}; if (const auto duration = action.vduration()) { const auto seconds = duration->v; const auto days = seconds / 86400; const auto hours = seconds / 3600; const auto minutes = seconds / 60; auto text = (days > 1) ? tr::lng_days(tr::now, lt_count, days) : (hours > 1) ? tr::lng_hours(tr::now, lt_count, hours) : (minutes > 1) ? tr::lng_minutes(tr::now, lt_count, minutes) : tr::lng_seconds(tr::now, lt_count, seconds); if (history()->peer->isBroadcast()) { result.text = tr::lng_action_group_call_finished( tr::now, lt_duration, { .text = text }, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_group_call_finished_group( tr::now, lt_from, fromLinkText(), // Link 1. lt_duration, { .text = text }, Ui::Text::WithEntities); } return result; } if (history()->peer->isBroadcast()) { result.text = tr::lng_action_group_call_started_channel( tr::now, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_group_call_started_group( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); } return result; }; auto prepareInviteToGroupCall = [this](const MTPDmessageActionInviteToGroupCall &action) { const auto callId = CallIdFromInput(action.vcall()); const auto owner = &history()->owner(); const auto peer = history()->peer; for (const auto &id : action.vusers().v) { const auto user = owner->user(id.v); if (callId) { owner->registerInvitedToCallUser(callId, peer, user); } }; const auto linkCallId = PeerHasThisCall(peer, callId).value_or(false) ? callId : 0; return prepareInvitedToCallText(action.vusers().v, linkCallId); }; auto prepareSetMessagesTTL = [this](const MTPDmessageActionSetMessagesTTL &action) { auto result = PreparedText{}; const auto period = action.vperiod().v; const auto duration = (period == 5) ? u"5 seconds"_q : Ui::FormatTTL(period); if (isPost()) { if (!period) { result.text = tr::lng_action_ttl_removed_channel( tr::now, Ui::Text::WithEntities); } else { result.text = tr::lng_action_ttl_changed_channel( tr::now, lt_duration, { .text = duration }, Ui::Text::WithEntities); } } else if (_from->isSelf()) { if (!period) { result.text = tr::lng_action_ttl_removed_you( tr::now, Ui::Text::WithEntities); } else { result.text = tr::lng_action_ttl_changed_you( tr::now, lt_duration, { .text = duration }, Ui::Text::WithEntities); } } else { result.links.push_back(fromLink()); if (!period) { result.text = tr::lng_action_ttl_removed( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); } else { result.text = tr::lng_action_ttl_changed( tr::now, lt_from, fromLinkText(), // Link 1. lt_duration, { .text = duration }, Ui::Text::WithEntities); } } return result; }; auto prepareSetChatTheme = [this](const MTPDmessageActionSetChatTheme &action) { auto result = PreparedText{}; const auto text = qs(action.vemoticon()); if (!text.isEmpty()) { if (_from->isSelf()) { result.text = tr::lng_action_you_theme_changed( tr::now, lt_emoji, { .text = text }, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_theme_changed( tr::now, lt_from, fromLinkText(), // Link 1. lt_emoji, { .text = text }, Ui::Text::WithEntities); } } else { if (_from->isSelf()) { result.text = tr::lng_action_you_theme_disabled( tr::now, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_theme_disabled( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); } } return result; }; auto prepareChatJoinedByRequest = [this](const MTPDmessageActionChatJoinedByRequest &action) { auto result = PreparedText{}; result.links.push_back(fromLink()); result.text = tr::lng_action_user_joined_by_request( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); return result; }; auto prepareWebViewDataSent = [](const MTPDmessageActionWebViewDataSent &action) { auto result = PreparedText{}; result.text = tr::lng_action_webview_data_done( tr::now, lt_text, { .text = qs(action.vtext()) }, Ui::Text::WithEntities); return result; }; const auto messageText = action.match([&]( const MTPDmessageActionChatAddUser &data) { return prepareChatAddUserText(data); }, [&](const MTPDmessageActionChatJoinedByLink &data) { return prepareChatJoinedByLink(data); }, [&](const MTPDmessageActionChatCreate &data) { return prepareChatCreate(data); }, [](const MTPDmessageActionChatMigrateTo &) { return PreparedText(); }, [](const MTPDmessageActionChannelMigrateFrom &) { return PreparedText(); }, [](const MTPDmessageActionHistoryClear &) { return PreparedText(); }, [&](const MTPDmessageActionChannelCreate &data) { return prepareChannelCreate(data); }, [&](const MTPDmessageActionChatDeletePhoto &) { return prepareChatDeletePhoto(); }, [&](const MTPDmessageActionChatDeleteUser &data) { return prepareChatDeleteUser(data); }, [&](const MTPDmessageActionChatEditPhoto &data) { return prepareChatEditPhoto(data); }, [&](const MTPDmessageActionChatEditTitle &data) { return prepareChatEditTitle(data); }, [&](const MTPDmessageActionPinMessage &) { return preparePinnedText(); }, [&](const MTPDmessageActionGameScore &) { return prepareGameScoreText(); }, [&](const MTPDmessageActionPhoneCall &) -> PreparedText { Unexpected("PhoneCall type in HistoryService."); }, [&](const MTPDmessageActionPaymentSent &) { return preparePaymentSentText(); }, [&](const MTPDmessageActionScreenshotTaken &) { return prepareScreenshotTaken(); }, [&](const MTPDmessageActionCustomAction &data) { return prepareCustomAction(data); }, [&](const MTPDmessageActionBotAllowed &data) { return prepareBotAllowed(data); }, [&](const MTPDmessageActionSecureValuesSent &data) { return prepareSecureValuesSent(data); }, [&](const MTPDmessageActionContactSignUp &data) { return prepareContactSignUp(); }, [&](const MTPDmessageActionGeoProximityReached &data) { return prepareProximityReached(data); }, [](const MTPDmessageActionPaymentSentMe &) { LOG(("API Error: messageActionPaymentSentMe received.")); return PreparedText{ tr::lng_message_empty(tr::now, Ui::Text::WithEntities) }; }, [](const MTPDmessageActionSecureValuesSentMe &) { LOG(("API Error: messageActionSecureValuesSentMe received.")); return PreparedText{ tr::lng_message_empty(tr::now, Ui::Text::WithEntities) }; }, [&](const MTPDmessageActionGroupCall &data) { return prepareGroupCall(data); }, [&](const MTPDmessageActionInviteToGroupCall &data) { return prepareInviteToGroupCall(data); }, [&](const MTPDmessageActionSetMessagesTTL &data) { return prepareSetMessagesTTL(data); }, [&](const MTPDmessageActionGroupCallScheduled &data) { return prepareCallScheduledText(data.vschedule_date().v); }, [&](const MTPDmessageActionSetChatTheme &data) { return prepareSetChatTheme(data); }, [&](const MTPDmessageActionChatJoinedByRequest &data) { return prepareChatJoinedByRequest(data); }, [&](const MTPDmessageActionWebViewDataSent &data) { return prepareWebViewDataSent(data); }, [&](const MTPDmessageActionWebViewDataSentMe &data) { LOG(("API Error: messageActionWebViewDataSentMe received.")); return PreparedText{ tr::lng_message_empty(tr::now, Ui::Text::WithEntities) }; }, [](const MTPDmessageActionEmpty &) { return PreparedText{ tr::lng_message_empty(tr::now, Ui::Text::WithEntities) }; }); setServiceText(messageText); // Additional information. applyAction(action); } void HistoryService::applyAction(const MTPMessageAction &action) { action.match([&](const MTPDmessageActionChatAddUser &data) { if (const auto channel = history()->peer->asMegagroup()) { const auto selfUserId = history()->session().userId(); for (const auto &item : data.vusers().v) { if (peerFromUser(item) == selfUserId) { channel->mgInfo->joinedMessageFound = true; break; } } } }, [&](const MTPDmessageActionChatJoinedByLink &data) { if (_from->isSelf()) { if (const auto channel = history()->peer->asMegagroup()) { channel->mgInfo->joinedMessageFound = true; } } }, [&](const MTPDmessageActionChatEditPhoto &data) { data.vphoto().match([&](const MTPDphoto &photo) { _media = std::make_unique( this, history()->peer, history()->owner().processPhoto(photo)); }, [](const MTPDphotoEmpty &) { }); }, [&](const MTPDmessageActionChatCreate &) { _flags |= MessageFlag::IsGroupEssential; }, [&](const MTPDmessageActionChannelCreate &) { _flags |= MessageFlag::IsGroupEssential; }, [&](const MTPDmessageActionChatMigrateTo &) { _flags |= MessageFlag::IsGroupEssential; }, [&](const MTPDmessageActionChannelMigrateFrom &) { _flags |= MessageFlag::IsGroupEssential; }, [&](const MTPDmessageActionContactSignUp &) { _flags |= MessageFlag::IsContactSignUp; }, [&](const MTPDmessageActionChatJoinedByRequest &data) { if (_from->isSelf()) { if (const auto channel = history()->peer->asMegagroup()) { channel->mgInfo->joinedMessageFound = true; } } }, [](const auto &) { }); } void HistoryService::setSelfDestruct(HistoryServiceSelfDestruct::Type type, int ttlSeconds) { UpdateComponents(HistoryServiceSelfDestruct::Bit()); auto selfdestruct = Get(); selfdestruct->timeToLive = ttlSeconds * 1000LL; selfdestruct->type = type; } bool HistoryService::updateDependent(bool force) { auto dependent = GetDependentData(); Assert(dependent != nullptr); if (!force) { if (!dependent->msgId || dependent->msg) { return true; } } if (!dependent->lnk) { dependent->lnk = goToMessageClickHandler( (dependent->peerId ? history()->owner().peer(dependent->peerId) : history()->peer), dependent->msgId); } auto gotDependencyItem = false; if (!dependent->msg) { dependent->msg = history()->owner().message( (dependent->peerId ? dependent->peerId : _history->peer->id), dependent->msgId); if (dependent->msg) { if (dependent->msg->isEmpty()) { // Really it is deleted. dependent->msg = nullptr; force = true; } else { history()->owner().registerDependentMessage( 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) { Core::App().notifications().checkDelayed(); } return (dependent->msg || !dependent->msgId); } HistoryService::PreparedText HistoryService::prepareInvitedToCallText( const QVector &users, CallId linkCallId) { const auto owner = &history()->owner(); auto chatText = tr::lng_action_invite_user_chat( tr::now, Ui::Text::WithEntities); auto result = PreparedText{}; result.links.push_back(fromLink()); auto linkIndex = 1; if (linkCallId) { const auto peer = history()->peer; result.links.push_back(GroupCallClickHandler(peer, linkCallId)); chatText = Ui::Text::Link(chatText.text, ++linkIndex); } if (users.size() == 1) { auto user = owner->user(users[0].v); result.links.push_back(user->createOpenLink()); result.text = tr::lng_action_invite_user( tr::now, lt_from, fromLinkText(), // Link 1. lt_user, Ui::Text::Link(user->name, ++linkIndex), // Link N. lt_chat, chatText, Ui::Text::WithEntities); } else if (users.isEmpty()) { result.text = tr::lng_action_invite_user( tr::now, lt_from, fromLinkText(), // Link 1. lt_user, { .text = qsl("somebody") }, lt_chat, chatText, Ui::Text::WithEntities); } else { for (auto i = 0, l = int(users.size()); i != l; ++i) { auto user = owner->user(users[i].v); result.links.push_back(user->createOpenLink()); auto linkText = Ui::Text::Link(user->name, ++linkIndex); if (i == 0) { result.text = linkText; } else if (i + 1 == l) { result.text = tr::lng_action_invite_users_and_last( tr::now, lt_accumulated, result.text, lt_user, linkText, Ui::Text::WithEntities); } else { result.text = tr::lng_action_invite_users_and_one( tr::now, lt_accumulated, result.text, lt_user, linkText, Ui::Text::WithEntities); } } result.text = tr::lng_action_invite_users_many( tr::now, lt_from, fromLinkText(), // Link 1. lt_users, result.text, lt_chat, chatText, Ui::Text::WithEntities); } return result; } HistoryService::PreparedText HistoryService::preparePinnedText() { auto result = PreparedText {}; auto pinned = Get(); if (pinned && pinned->msg) { const auto mediaText = [&] { using TTL = HistoryServiceSelfDestruct; if (const auto media = pinned->msg->media()) { return media->pinnedTextSubstring(); } else if (const auto selfdestruct = pinned->msg->Get()) { if (selfdestruct->type == TTL::Type::Photo) { return tr::lng_action_pinned_media_photo(tr::now); } else if (selfdestruct->type == TTL::Type::Video) { return tr::lng_action_pinned_media_video(tr::now); } } return QString(); }(); result.links.push_back(fromLink()); result.links.push_back(pinned->lnk); if (mediaText.isEmpty()) { auto original = pinned->msg->originalText(); auto cutAt = 0; auto limit = kPinnedMessageTextLimit; auto size = original.text.size(); for (; limit != 0;) { --limit; if (cutAt >= size) break; if (original.text.at(cutAt).isLowSurrogate() && (cutAt + 1 < size) && original.text.at(cutAt + 1).isHighSurrogate()) { cutAt += 2; } else { ++cutAt; } } if (!limit && cutAt + 5 < size) { original = Ui::Text::Mid(original, 0, cutAt).append( Ui::kQEllipsis); } original = Ui::Text::Wrapped( Ui::Text::Filtered( std::move(original), { EntityType::Spoiler, EntityType::StrikeOut }), EntityType::CustomUrl, Ui::Text::Link({}, 2).entities.front().data()); result.text = tr::lng_action_pinned_message( tr::now, lt_from, fromLinkText(), // Link 1. lt_text, std::move(original), // Link 2. Ui::Text::WithEntities); } else { result.text = tr::lng_action_pinned_media( tr::now, lt_from, fromLinkText(), // Link 1. lt_media, Ui::Text::Link(mediaText, 2), // Link 2. Ui::Text::WithEntities); } } else if (pinned && pinned->msgId) { result.links.push_back(fromLink()); result.links.push_back(pinned->lnk); result.text = tr::lng_action_pinned_media( tr::now, lt_from, fromLinkText(), // Link 1. lt_media, Ui::Text::Link(tr::lng_contacts_loading(tr::now), 2), // Link 2. Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_pinned_media( tr::now, lt_from, fromLinkText(), // Link 1. lt_media, { .text = tr::lng_deleted_message(tr::now) }, Ui::Text::WithEntities); } return result; } HistoryService::PreparedText HistoryService::prepareGameScoreText() { auto result = PreparedText {}; auto gamescore = Get(); auto computeGameTitle = [&]() -> TextWithEntities { if (gamescore && gamescore->msg) { if (const auto media = gamescore->msg->media()) { if (const auto game = media->game()) { const auto row = 0; const auto column = 0; result.links.push_back( std::make_shared( &history()->owner(), row, column, gamescore->msg->fullId())); auto titleText = game->title; return Ui::Text::Link(titleText, QString()); } } return tr::lng_deleted_message(tr::now, Ui::Text::WithEntities); } else if (gamescore && gamescore->msgId) { return tr::lng_contacts_loading(tr::now, Ui::Text::WithEntities); } return {}; }; const auto scoreNumber = gamescore ? gamescore->score : 0; if (_from->isSelf()) { auto gameTitle = computeGameTitle(); if (gameTitle.text.isEmpty()) { result.text = tr::lng_action_game_you_scored_no_game( tr::now, lt_count, scoreNumber, Ui::Text::WithEntities); } else { result.text = tr::lng_action_game_you_scored( tr::now, lt_count, scoreNumber, lt_game, gameTitle, Ui::Text::WithEntities); } } else { result.links.push_back(fromLink()); auto gameTitle = computeGameTitle(); if (gameTitle.text.isEmpty()) { result.text = tr::lng_action_game_score_no_game( tr::now, lt_count, scoreNumber, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); } else { result.text = tr::lng_action_game_score( tr::now, lt_count, scoreNumber, lt_from, fromLinkText(), // Link 1. lt_game, gameTitle, Ui::Text::WithEntities); } } return result; } HistoryService::PreparedText HistoryService::preparePaymentSentText() { auto result = PreparedText {}; const auto payment = Get(); Assert(payment != nullptr); auto invoiceTitle = [&] { if (payment->msg) { if (const auto media = payment->msg->media()) { if (const auto invoice = media->invoice()) { return Ui::Text::Link(invoice->title, QString()); } } } return TextWithEntities(); }(); if (invoiceTitle.text.isEmpty()) { result.text = tr::lng_action_payment_done( tr::now, lt_amount, { .text = payment->amount }, lt_user, { .text = history()->peer->name }, Ui::Text::WithEntities); } else { result.text = tr::lng_action_payment_done_for( tr::now, lt_amount, { .text = payment->amount }, lt_user, { .text = history()->peer->name }, lt_invoice, invoiceTitle, Ui::Text::WithEntities); if (payment->msg) { result.links.push_back(payment->lnk); } } return result; } HistoryService::PreparedText HistoryService::prepareCallScheduledText( TimeId scheduleDate) { const auto call = Get(); Assert(call != nullptr); const auto scheduled = base::unixtime::parse(scheduleDate); const auto date = scheduled.date(); const auto now = QDateTime::currentDateTime(); const auto secsToDateAddDays = [&](int days) { return now.secsTo(QDateTime(date.addDays(days), QTime(0, 0))); }; auto result = PreparedText(); const auto prepareWithDate = [&](const QString &date) { if (history()->peer->isBroadcast()) { result.text = tr::lng_action_group_call_scheduled_channel( tr::now, lt_date, { .text = date }, Ui::Text::WithEntities); } else { result.links.push_back(fromLink()); result.text = tr::lng_action_group_call_scheduled_group( tr::now, lt_from, fromLinkText(), // Link 1. lt_date, { .text = date }, Ui::Text::WithEntities); } }; const auto time = scheduled.time().toString(cTimeFormat()); const auto prepareGeneric = [&] { prepareWithDate(tr::lng_group_call_starts_date( tr::now, lt_date, langDayOfMonthFull(date), lt_time, time)); }; auto nextIn = TimeId(0); if (now.date().addDays(1) < scheduled.date()) { nextIn = secsToDateAddDays(-1); prepareGeneric(); } else if (now.date().addDays(1) == scheduled.date()) { nextIn = secsToDateAddDays(0); prepareWithDate( tr::lng_group_call_starts_tomorrow(tr::now, lt_time, time)); } else if (now.date() == scheduled.date()) { nextIn = secsToDateAddDays(1); prepareWithDate( tr::lng_group_call_starts_today(tr::now, lt_time, time)); } else { prepareGeneric(); } if (nextIn) { call->lifetime = base::timer_once( (nextIn + 2) * crl::time(1000) ) | rpl::start_with_next([=] { updateText(prepareCallScheduledText(scheduleDate)); }); } return result; } HistoryService::HistoryService( not_null history, MsgId id, const MTPDmessage &data, MessageFlags localFlags) : HistoryItem( history, id, FlagsFromMTP(id, data.vflags().v, localFlags), data.vdate().v, data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0)) { createFromMtp(data); applyTTL(data); } HistoryService::HistoryService( not_null history, MsgId id, const MTPDmessageService &data, MessageFlags localFlags) : HistoryItem( history, id, FlagsFromMTP(id, data.vflags().v, localFlags), data.vdate().v, data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0)) { createFromMtp(data); applyTTL(data); } HistoryService::HistoryService( not_null history, MsgId id, MessageFlags flags, TimeId date, const PreparedText &message, PeerId from, PhotoData *photo) : HistoryItem(history, id, flags, date, from) { setServiceText(message); if (photo) { _media = std::make_unique( this, history->peer, photo); } } bool HistoryService::updateDependencyItem() { if (GetDependentData()) { return updateDependent(true); } return HistoryItem::updateDependencyItem(); } bool HistoryService::needCheck() const { return out() && !isEmpty(); } ItemPreview HistoryService::toPreview(ToPreviewOptions options) const { // Don't show for service messages (chat photo changed). // Because larger version is shown exactly to the left of the preview. //auto media = _media ? _media->toPreview(options) : ItemPreview(); return { .text = Ui::Text::Wrapped(notificationText(), EntityType::PlainLink), //.images = std::move(media.images), //.loadingContext = std::move(media.loadingContext), }; } TextWithEntities HistoryService::inReplyText() const { auto result = HistoryService::notificationText(); const auto &name = author()->name; TextUtilities::Trim(result); if (result.text.startsWith(name)) { result = Ui::Text::Mid(result, name.size()); TextUtilities::Trim(result); } return Ui::Text::Wrapped(result, EntityType::PlainLink); } std::unique_ptr HistoryService::createView( not_null delegate, HistoryView::Element *replacing) { return delegate->elementCreate(this, replacing); } TextWithEntities HistoryService::fromLinkText() const { return Ui::Text::Link(_from->name, 1); } ClickHandlerPtr HistoryService::fromLink() const { return _from->createOpenLink(); } void HistoryService::setServiceText(const PreparedText &prepared) { _text.setMarkedText( st::serviceTextStyle, prepared.text, Ui::ItemTextServiceOptions()); HistoryView::FillTextWithAnimatedSpoilers(_text); auto linkIndex = 0; for (const auto &link : prepared.links) { // Link indices start with 1. _text.setLink(++linkIndex, link); } _textWidth = -1; _textHeight = 0; } void HistoryService::hideSpoilers() { HistoryView::HideSpoilers(_text); } void HistoryService::markMediaAsReadHook() { if (const auto selfdestruct = Get()) { if (!selfdestruct->destructAt) { selfdestruct->destructAt = crl::now() + selfdestruct->timeToLive; history()->owner().selfDestructIn(this, selfdestruct->timeToLive); } } } crl::time HistoryService::getSelfDestructIn(crl::time now) { if (auto selfdestruct = Get()) { if (selfdestruct->destructAt > 0) { if (selfdestruct->destructAt <= now) { auto text = [selfdestruct] { switch (selfdestruct->type) { case HistoryServiceSelfDestruct::Type::Photo: return tr::lng_ttl_photo_expired(tr::now); case HistoryServiceSelfDestruct::Type::Video: return tr::lng_ttl_video_expired(tr::now); } Unexpected("Type in HistoryServiceSelfDestruct::Type"); }; setServiceText({ TextWithEntities{ .text = text() } }); return 0; } return selfdestruct->destructAt - now; } } return 0; } void HistoryService::createFromMtp(const MTPDmessage &message) { const auto media = message.vmedia(); Assert(media != nullptr); const auto mediaType = media->type(); switch (mediaType) { case mtpc_messageMediaPhoto: { if (message.is_media_unread()) { const auto &photo = media->c_messageMediaPhoto(); const auto ttl = photo.vttl_seconds(); Assert(ttl != nullptr); setSelfDestruct(HistoryServiceSelfDestruct::Type::Photo, ttl->v); if (out()) { setServiceText({ tr::lng_ttl_photo_sent(tr::now, Ui::Text::WithEntities) }); } else { auto result = PreparedText(); result.links.push_back(fromLink()); result.text = tr::lng_ttl_photo_received( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); setServiceText(std::move(result)); } } else { setServiceText({ tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities) }); } } break; case mtpc_messageMediaDocument: { if (message.is_media_unread()) { const auto &document = media->c_messageMediaDocument(); const auto ttl = document.vttl_seconds(); Assert(ttl != nullptr); setSelfDestruct(HistoryServiceSelfDestruct::Type::Video, ttl->v); if (out()) { setServiceText({ tr::lng_ttl_video_sent(tr::now, Ui::Text::WithEntities) }); } else { auto result = PreparedText(); result.links.push_back(fromLink()); result.text = tr::lng_ttl_video_received( tr::now, lt_from, fromLinkText(), // Link 1. Ui::Text::WithEntities); setServiceText(std::move(result)); } } else { setServiceText({ tr::lng_ttl_video_expired(tr::now, Ui::Text::WithEntities) }); } } break; default: Unexpected("Media type in HistoryService::createFromMtp()"); } if (const auto reactions = message.vreactions()) { updateReactions(reactions); } } void HistoryService::createFromMtp(const MTPDmessageService &message) { const auto type = message.vaction().type(); if (type == mtpc_messageActionGameScore) { const auto &data = message.vaction().c_messageActionGameScore(); UpdateComponents(HistoryServiceGameScore::Bit()); Get()->score = data.vscore().v; } else if (type == mtpc_messageActionPaymentSent) { const auto &data = message.vaction().c_messageActionPaymentSent(); UpdateComponents(HistoryServicePayment::Bit()); const auto amount = data.vtotal_amount().v; const auto currency = qs(data.vcurrency()); const auto payment = Get(); const auto id = fullId(); const auto owner = &history()->owner(); payment->slug = data.vinvoice_slug().value_or_empty(); payment->amount = Ui::FillAmountAndCurrency(amount, currency); payment->invoiceLink = std::make_shared([=]( ClickContext context) { using namespace Payments; const auto my = context.other.value(); const auto weak = my.sessionWindow; if (const auto item = owner->message(id)) { CheckoutProcess::Start( item, Mode::Receipt, crl::guard(weak, [=](auto) { weak->window().activate(); })); } }); } else if (type == mtpc_messageActionGroupCall || type == mtpc_messageActionGroupCallScheduled) { const auto started = (type == mtpc_messageActionGroupCall); const auto &callData = started ? message.vaction().c_messageActionGroupCall().vcall() : message.vaction().c_messageActionGroupCallScheduled().vcall(); const auto duration = started ? message.vaction().c_messageActionGroupCall().vduration() : tl::conditional(); if (duration) { RemoveComponents(HistoryServiceOngoingCall::Bit()); } else { UpdateComponents(HistoryServiceOngoingCall::Bit()); const auto call = Get(); call->id = CallIdFromInput(callData); call->link = GroupCallClickHandler(history()->peer, call->id); } } else if (type == mtpc_messageActionInviteToGroupCall) { const auto &data = message.vaction().c_messageActionInviteToGroupCall(); const auto id = CallIdFromInput(data.vcall()); const auto peer = history()->peer; const auto has = PeerHasThisCall(peer, id); auto hasLink = !has.has_value() ? PeerHasThisCallValue(peer, id) : (*has) ? PeerHasThisCallValue( peer, id) | rpl::skip(1) | rpl::type_erased() : rpl::producer(); if (!hasLink) { RemoveComponents(HistoryServiceOngoingCall::Bit()); } else { UpdateComponents(HistoryServiceOngoingCall::Bit()); const auto call = Get(); call->id = id; call->lifetime.destroy(); const auto users = data.vusers().v; std::move(hasLink) | rpl::start_with_next([=](bool has) { updateText(prepareInvitedToCallText(users, has ? id : 0)); if (!has) { RemoveComponents(HistoryServiceOngoingCall::Bit()); } }, call->lifetime); } } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { const auto peerId = data.vreply_to_peer_id() ? peerFromMTP(*data.vreply_to_peer_id()) : history()->peer->id; if (message.vaction().type() == mtpc_messageActionPinMessage) { UpdateComponents(HistoryServicePinned::Bit()); } if (const auto dependent = GetDependentData()) { dependent->peerId = (peerId != history()->peer->id) ? peerId : 0; dependent->msgId = data.vreply_to_msg_id().v; if (!updateDependent()) { RequestDependentMessageData( this, dependent->peerId, dependent->msgId); } } }); } setMessageByAction(message.vaction()); } void HistoryService::applyEdition(const MTPDmessageService &message) { clearDependency(); UpdateComponents(0); createFromMtp(message); applyServiceDateEdition(message); if (message.vaction().type() == mtpc_messageActionHistoryClear) { removeMedia(); finishEditionToEmpty(); } else { finishEdition(-1); } } void HistoryService::removeMedia() { if (!_media) return; _media.reset(); _textWidth = -1; _textHeight = 0; history()->owner().requestItemResize(this); } Storage::SharedMediaTypesMask HistoryService::sharedMediaTypes() const { if (auto media = this->media()) { return media->sharedMediaTypes(); } return {}; } void HistoryService::updateDependentText() { auto text = PreparedText{}; if (Has()) { text = preparePinnedText(); } else if (Has()) { text = prepareGameScoreText(); } else if (Has()) { text = preparePaymentSentText(); } else { return; } updateText(std::move(text)); } void HistoryService::updateText(PreparedText &&text) { setServiceText(text); history()->owner().requestItemResize(this); invalidateChatListEntry(); history()->owner().updateDependentMessages(this); } void HistoryService::clearDependency() { if (const auto dependent = GetDependentData()) { if (dependent->msg) { history()->owner().unregisterDependentMessage( this, dependent->msg); dependent->msg = nullptr; dependent->msgId = 0; } } } void HistoryService::dependencyItemRemoved(HistoryItem *dependency) { clearDependency(); updateDependentText(); } HistoryService::~HistoryService() { clearDependency(); _media.reset(); } HistoryService::PreparedText GenerateJoinedText( not_null history, not_null inviter, bool viaRequest) { if (inviter->id != history->session().userPeerId()) { auto result = HistoryService::PreparedText{}; result.links.push_back(inviter->createOpenLink()); result.text = (history->peer->isMegagroup() ? tr::lng_action_add_you_group : tr::lng_action_add_you)( tr::now, lt_from, Ui::Text::Link(inviter->name, QString()), Ui::Text::WithEntities); return result; } else if (history->peer->isMegagroup()) { if (viaRequest) { return { tr::lng_action_you_joined_by_request( tr::now, Ui::Text::WithEntities) }; } auto self = history->session().user(); auto result = HistoryService::PreparedText{}; result.links.push_back(self->createOpenLink()); result.text = tr::lng_action_user_joined( tr::now, lt_from, Ui::Text::Link(self->name, QString()), Ui::Text::WithEntities); return result; } return { viaRequest ? tr::lng_action_you_joined_by_request_channel( tr::now, Ui::Text::WithEntities) : tr::lng_action_you_joined(tr::now, Ui::Text::WithEntities) }; } not_null GenerateJoinedMessage( not_null history, TimeId inviteDate, not_null inviter, bool viaRequest) { return history->makeServiceMessage( history->owner().nextLocalMessageId(), MessageFlag::Local, inviteDate, GenerateJoinedText(history, inviter, viaRequest)); } std::optional PeerHasThisCall( not_null peer, CallId id) { const auto call = peer->groupCall(); return call ? std::make_optional(call->id() == id) : PeerCallKnown(peer) ? std::make_optional(false) : std::nullopt; }