tdesktop/Telegram/SourceFiles/history/history_service.cpp

1570 lines
44 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 "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<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_group_call_duration_days(tr::now, lt_count, days)
: (hours > 1)
? tr::lng_group_call_duration_hours(tr::now, lt_count, hours)
: (minutes > 1)
? tr::lng_group_call_duration_minutes(tr::now, lt_count, minutes)
: tr::lng_group_call_duration_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
: (period < 2 * 86400)
? tr::lng_ttl_about_duration1(tr::now)
: (period < 8 * 86400)
? tr::lng_ttl_about_duration2(tr::now)
: tr::lng_ttl_about_duration3(tr::now);
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;
};
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 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<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 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::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()) {
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<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 = 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*> 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,
const PreparedText &message,
PeerId from,
PhotoData *photo)
: HistoryItem(history, id, flags, date, from) {
setServiceText(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);
}
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(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<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_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->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, [=] { 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 (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<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(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*> 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;
}