1734 lines
		
	
	
	
		
			50 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1734 lines
		
	
	
	
		
			50 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| 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 "chat_helpers/stickers_gift_box_pack.h"
 | |
| #include "lang/lang_keys.h"
 | |
| #include "mainwidget.h"
 | |
| #include "main/main_session.h"
 | |
| #include "main/main_domain.h" // Core::App().domain().activate().
 | |
| #include "menu/menu_ttl_validator.h"
 | |
| #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 "data/data_folder.h"
 | |
| #include "data/data_forum.h"
 | |
| #include "data/data_forum_topic.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 "data/stickers/data_custom_emoji.h"
 | |
| #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_utilities.h"
 | |
| 
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kPinnedMessageTextLimit = 16;
 | |
| 
 | |
| using ItemPreview = HistoryView::ItemPreview;
 | |
| 
 | |
| [[nodiscard]] bool PeerCallKnown(not_null<PeerData*> 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<bool> PeerHasThisCallValue(
 | |
| 		not_null<PeerData*> 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<PeerData*> peer,
 | |
| 		CallId callId) {
 | |
| 	return std::make_shared<LambdaClickHandler>([=] {
 | |
| 		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;
 | |
| 	};
 | |
| 
 | |
| 	auto prepareGiftPremium = [&](
 | |
| 			const MTPDmessageActionGiftPremium &action) {
 | |
| 		auto result = PreparedText{};
 | |
| 		const auto isSelf = (_from->id == _from->session().userPeerId());
 | |
| 		const auto peer = isSelf ? history()->peer : _from;
 | |
| 		history()->session().giftBoxStickersPacks().load();
 | |
| 		const auto amount = action.vamount().v;
 | |
| 		const auto currency = qs(action.vcurrency());
 | |
| 		result.links.push_back(peer->createOpenLink());
 | |
| 		result.text = (isSelf
 | |
| 			? tr::lng_action_gift_received_me
 | |
| 			: tr::lng_action_gift_received)(
 | |
| 				tr::now,
 | |
| 				lt_user,
 | |
| 				Ui::Text::Link(peer->name(), 1), // Link 1.
 | |
| 				lt_cost,
 | |
| 				{ Ui::FillAmountAndCurrency(amount, currency) },
 | |
| 				Ui::Text::WithEntities);
 | |
| 		return result;
 | |
| 	};
 | |
| 
 | |
| 	auto prepareTopicCreate = [&](const MTPDmessageActionTopicCreate &action) {
 | |
| 		auto result = PreparedText{};
 | |
| 		// #TODO lang-forum
 | |
| 		result.text = { "topic created: " + qs(action.vtitle()) };
 | |
| 		return result;
 | |
| 	};
 | |
| 
 | |
| 	auto prepareTopicEdit = [&](const MTPDmessageActionTopicEdit &action) {
 | |
| 		auto result = PreparedText{};
 | |
| 		// #TODO lang-forum
 | |
| 		result.text = { "topic edited: " };
 | |
| 		if (const auto icon = action.vicon_emoji_id()) {
 | |
| 			result.text.append(TextWithEntities{
 | |
| 				"@",
 | |
| 				{ EntityInText(
 | |
| 					EntityType::CustomEmoji,
 | |
| 					0,
 | |
| 					1,
 | |
| 					Data::SerializeCustomEmojiId({ .id = icon->v }))
 | |
| 				},
 | |
| 			});
 | |
| 		}
 | |
| 		if (const auto &title = action.vtitle()) {
 | |
| 			result.text.append(qs(*title));
 | |
| 		}
 | |
| 		return result;
 | |
| 	};
 | |
| 
 | |
| 	setServiceText(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 MTPDmessageActionGiftPremium &data) {
 | |
| 		return prepareGiftPremium(data);
 | |
| 	}, [&](const MTPDmessageActionTopicCreate &data) {
 | |
| 		return prepareTopicCreate(data);
 | |
| 	}, [&](const MTPDmessageActionTopicEdit &data) {
 | |
| 		return prepareTopicEdit(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)
 | |
| 		};
 | |
| 	}));
 | |
| 
 | |
| 	// 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<Data::MediaPhoto>(
 | |
| 				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 MTPDmessageActionGiftPremium &data) {
 | |
| 		_media = std::make_unique<Data::MediaGiftBox>(
 | |
| 			this,
 | |
| 			_from,
 | |
| 			data.vmonths().v);
 | |
| 	}, [](const auto &) {
 | |
| 	});
 | |
| }
 | |
| 
 | |
| void HistoryService::setSelfDestruct(HistoryServiceSelfDestruct::Type type, int ttlSeconds) {
 | |
| 	UpdateComponents(HistoryServiceSelfDestruct::Bit());
 | |
| 	auto selfdestruct = Get<HistoryServiceSelfDestruct>();
 | |
| 	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<MTPlong> &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<HistoryServicePinned>();
 | |
| 	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<TTL>()) {
 | |
| 				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::Italic,
 | |
| 						EntityType::CustomEmoji,
 | |
| 					}),
 | |
| 				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<HistoryServiceGameScore>();
 | |
| 
 | |
| 	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<ReplyMarkupClickHandler>(
 | |
| 							&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<HistoryServicePayment>();
 | |
| 	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()) {
 | |
| 		if (payment->recurringUsed) {
 | |
| 			result.text = tr::lng_action_payment_used_recurring(
 | |
| 				tr::now,
 | |
| 				lt_amount,
 | |
| 				{ .text = payment->amount },
 | |
| 				Ui::Text::WithEntities);
 | |
| 		} else {
 | |
| 			result.text = (payment->recurringInit
 | |
| 				? tr::lng_action_payment_init_recurring
 | |
| 				: tr::lng_action_payment_done)(
 | |
| 					tr::now,
 | |
| 					lt_amount,
 | |
| 					{ .text = payment->amount },
 | |
| 					lt_user,
 | |
| 					{ .text = history()->peer->name() },
 | |
| 					Ui::Text::WithEntities);
 | |
| 		}
 | |
| 	} else {
 | |
| 		result.text = (payment->recurringInit
 | |
| 			? tr::lng_action_payment_init_recurring_for
 | |
| 			: 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<HistoryServiceOngoingCall>();
 | |
| 	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 = QLocale().toString(scheduled.time(), 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*> 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*> 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*> history,
 | |
| 	MsgId id,
 | |
| 	MessageFlags flags,
 | |
| 	TimeId date,
 | |
| 	PreparedText &&message,
 | |
| 	PeerId from,
 | |
| 	PhotoData *photo)
 | |
| : HistoryItem(history, id, flags, date, from) {
 | |
| 	setServiceText(std::move(message));
 | |
| 	if (photo) {
 | |
| 		_media = std::make_unique<Data::MediaPhoto>(
 | |
| 			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);
 | |
| }
 | |
| 
 | |
| MsgId HistoryService::replyToId() const {
 | |
| 	return 0; // Don't render replies info in service, only handle threads.
 | |
| }
 | |
| 
 | |
| MsgId HistoryService::replyToTop() const {
 | |
| 	if (const auto data = GetDependentData()) {
 | |
| 		return data->topId;
 | |
| 	}
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| MsgId HistoryService::topicRootId() const {
 | |
| 	if (const auto data = GetDependentData()
 | |
| 		; data && data->topicPost && data->topId) {
 | |
| 		return data->topId;
 | |
| 	} else if (Has<HistoryServiceTopicInfo>()) {
 | |
| 		return id;
 | |
| 	}
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| void HistoryService::setReplyFields(
 | |
| 		MsgId replyTo,
 | |
| 		MsgId replyToTop,
 | |
| 		bool isForumPost) {
 | |
| 	const auto data = GetDependentData();
 | |
| 	if (!data || IsServerMsgId(data->topId) || isScheduled()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	data->topId = replyToTop;
 | |
| }
 | |
| 
 | |
| std::unique_ptr<HistoryView::Element> HistoryService::createView(
 | |
| 		not_null<HistoryView::ElementDelegate*> 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(PreparedText &&prepared) {
 | |
| 	const auto had = !_text.empty();
 | |
| 	_text = std::move(prepared.text);
 | |
| 	_textLinks = std::move(prepared.links);
 | |
| 	if (had) {
 | |
| 		history()->owner().requestItemTextRefresh(this);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void HistoryService::markMediaAsReadHook() {
 | |
| 	if (const auto selfdestruct = Get<HistoryServiceSelfDestruct>()) {
 | |
| 		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<HistoryServiceSelfDestruct>()) {
 | |
| 		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_messageActionPinMessage) {
 | |
| 		UpdateComponents(HistoryServicePinned::Bit());
 | |
| 	} else if (type == mtpc_messageActionTopicCreate
 | |
| 		|| type == mtpc_messageActionTopicEdit) {
 | |
| 		UpdateComponents(HistoryServiceTopicInfo::Bit());
 | |
| 		Get<HistoryServiceTopicInfo>()->topicPost = true;
 | |
| 	} else if (type == mtpc_messageActionSetChatTheme) {
 | |
| 		setupChatThemeChange();
 | |
| 	} else if (type == mtpc_messageActionSetMessagesTTL) {
 | |
| 		setupTTLChange();
 | |
| 	} else if (type == mtpc_messageActionGameScore) {
 | |
| 		const auto &data = message.vaction().c_messageActionGameScore();
 | |
| 		UpdateComponents(HistoryServiceGameScore::Bit());
 | |
| 		Get<HistoryServiceGameScore>()->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<HistoryServicePayment>();
 | |
| 		const auto id = fullId();
 | |
| 		const auto owner = &history()->owner();
 | |
| 		payment->slug = data.vinvoice_slug().value_or_empty();
 | |
| 		payment->recurringInit = data.is_recurring_init();
 | |
| 		payment->recurringUsed = data.is_recurring_used();
 | |
| 		payment->amount = Ui::FillAmountAndCurrency(amount, currency);
 | |
| 		payment->invoiceLink = std::make_shared<LambdaClickHandler>([=](
 | |
| 				ClickContext context) {
 | |
| 			using namespace Payments;
 | |
| 			const auto my = context.other.value<ClickHandlerContext>();
 | |
| 			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<MTPint>();
 | |
| 		if (duration) {
 | |
| 			RemoveComponents(HistoryServiceOngoingCall::Bit());
 | |
| 		} else {
 | |
| 			UpdateComponents(HistoryServiceOngoingCall::Bit());
 | |
| 			const auto call = Get<HistoryServiceOngoingCall>();
 | |
| 			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<bool>();
 | |
| 		if (!hasLink) {
 | |
| 			RemoveComponents(HistoryServiceOngoingCall::Bit());
 | |
| 		} else {
 | |
| 			UpdateComponents(HistoryServiceOngoingCall::Bit());
 | |
| 			const auto call = Get<HistoryServiceOngoingCall>();
 | |
| 			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 (const auto dependent = GetDependentData()) {
 | |
| 				dependent->peerId = (peerId != history()->peer->id)
 | |
| 					? peerId
 | |
| 					: 0;
 | |
| 				dependent->msgId = data.vreply_to_msg_id().v;
 | |
| 				dependent->topId = data.vreply_to_top_id().value_or(
 | |
| 					data.vreply_to_msg_id().v);
 | |
| 				dependent->topicPost = data.is_forum_topic()
 | |
| 					|| Has<HistoryServiceTopicInfo>();
 | |
| 				if (!updateDependent()) {
 | |
| 					RequestDependentMessageData(
 | |
| 						this,
 | |
| 						dependent->peerId,
 | |
| 						dependent->msgId);
 | |
| 				}
 | |
| 			}
 | |
| 		});
 | |
| 	}
 | |
| 	setMessageByAction(message.vaction());
 | |
| }
 | |
| 
 | |
| const std::vector<ClickHandlerPtr> &HistoryService::customTextLinks() const {
 | |
| 	return _textLinks;
 | |
| }
 | |
| 
 | |
| 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();
 | |
| 	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<HistoryServicePinned>()) {
 | |
| 		text = preparePinnedText();
 | |
| 	} else if (Has<HistoryServiceGameScore>()) {
 | |
| 		text = prepareGameScoreText();
 | |
| 	} else if (Has<HistoryServicePayment>()) {
 | |
| 		text = preparePaymentSentText();
 | |
| 	} else {
 | |
| 		return;
 | |
| 	}
 | |
| 	updateText(std::move(text));
 | |
| }
 | |
| 
 | |
| void HistoryService::updateText(PreparedText &&text) {
 | |
| 	setServiceText(std::move(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::setupChatThemeChange() {
 | |
| 	if (const auto user = history()->peer->asUser()) {
 | |
| 		auto link = std::make_shared<LambdaClickHandler>([=](
 | |
| 				ClickContext context) {
 | |
| 			const auto my = context.other.value<ClickHandlerContext>();
 | |
| 			if (const auto controller = my.sessionWindow.get()) {
 | |
| 				controller->toggleChooseChatTheme(user);
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		UpdateComponents(HistoryServiceChatThemeChange::Bit());
 | |
| 		Get<HistoryServiceChatThemeChange>()->link = std::move(link);
 | |
| 	} else {
 | |
| 		RemoveComponents(HistoryServiceChatThemeChange::Bit());
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void HistoryService::setupTTLChange() {
 | |
| 	const auto peer = history()->peer;
 | |
| 	auto link = std::make_shared<LambdaClickHandler>([=](
 | |
| 			ClickContext context) {
 | |
| 		const auto my = context.other.value<ClickHandlerContext>();
 | |
| 		if (const auto controller = my.sessionWindow.get()) {
 | |
| 			const auto validator = TTLMenu::TTLValidator(
 | |
| 				std::make_shared<Window::Show>(controller),
 | |
| 				peer);
 | |
| 			if (validator.can()) {
 | |
| 				validator.showBox();
 | |
| 			}
 | |
| 		}
 | |
| 	});
 | |
| 
 | |
| 	UpdateComponents(HistoryServiceTTLChange::Bit());
 | |
| 	Get<HistoryServiceTTLChange>()->link = std::move(link);
 | |
| }
 | |
| 
 | |
| void HistoryService::dependencyItemRemoved(HistoryItem *dependency) {
 | |
| 	clearDependency();
 | |
| 	updateDependentText();
 | |
| }
 | |
| 
 | |
| HistoryService::~HistoryService() {
 | |
| 	clearDependency();
 | |
| 	_media.reset();
 | |
| }
 | |
| 
 | |
| HistoryService::PreparedText GenerateJoinedText(
 | |
| 		not_null<History*> history,
 | |
| 		not_null<UserData*> 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<HistoryService*> GenerateJoinedMessage(
 | |
| 		not_null<History*> history,
 | |
| 		TimeId inviteDate,
 | |
| 		not_null<UserData*> inviter,
 | |
| 		bool viaRequest) {
 | |
| 	return history->makeServiceMessage(
 | |
| 		history->owner().nextLocalMessageId(),
 | |
| 		MessageFlag::Local,
 | |
| 		inviteDate,
 | |
| 		GenerateJoinedText(history, inviter, viaRequest));
 | |
| }
 | |
| 
 | |
| std::optional<bool> PeerHasThisCall(
 | |
| 		not_null<PeerData*> peer,
 | |
| 		CallId id) {
 | |
| 	const auto call = peer->groupCall();
 | |
| 	return call
 | |
| 		? std::make_optional(call->id() == id)
 | |
| 		: PeerCallKnown(peer)
 | |
| 		? std::make_optional(false)
 | |
| 		: std::nullopt;
 | |
| }
 | 
