1656 lines
47 KiB
C++
1656 lines
47 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 "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 "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 "core/ui_integration.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_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) {
|
|
return PreparedText{ .text = { "gift premium" }, }; // #TODO gifts
|
|
};
|
|
|
|
const auto messageText = action.match([&](
|
|
const MTPDmessageActionChatAddUser &data) {
|
|
return prepareChatAddUserText(data);
|
|
}, [&](const MTPDmessageActionChatJoinedByLink &data) {
|
|
return prepareChatJoinedByLink(data);
|
|
}, [&](const MTPDmessageActionChatCreate &data) {
|
|
return prepareChatCreate(data);
|
|
}, [](const MTPDmessageActionChatMigrateTo &) {
|
|
return PreparedText();
|
|
}, [](const MTPDmessageActionChannelMigrateFrom &) {
|
|
return PreparedText();
|
|
}, [](const MTPDmessageActionHistoryClear &) {
|
|
return PreparedText();
|
|
}, [&](const MTPDmessageActionChannelCreate &data) {
|
|
return prepareChannelCreate(data);
|
|
}, [&](const MTPDmessageActionChatDeletePhoto &) {
|
|
return prepareChatDeletePhoto();
|
|
}, [&](const MTPDmessageActionChatDeleteUser &data) {
|
|
return prepareChatDeleteUser(data);
|
|
}, [&](const MTPDmessageActionChatEditPhoto &data) {
|
|
return prepareChatEditPhoto(data);
|
|
}, [&](const MTPDmessageActionChatEditTitle &data) {
|
|
return prepareChatEditTitle(data);
|
|
}, [&](const MTPDmessageActionPinMessage &) {
|
|
return preparePinnedText();
|
|
}, [&](const MTPDmessageActionGameScore &) {
|
|
return prepareGameScoreText();
|
|
}, [&](const MTPDmessageActionPhoneCall &) -> PreparedText {
|
|
Unexpected("PhoneCall type in HistoryService.");
|
|
}, [&](const MTPDmessageActionPaymentSent &) {
|
|
return preparePaymentSentText();
|
|
}, [&](const MTPDmessageActionScreenshotTaken &) {
|
|
return prepareScreenshotTaken();
|
|
}, [&](const MTPDmessageActionCustomAction &data) {
|
|
return prepareCustomAction(data);
|
|
}, [&](const MTPDmessageActionBotAllowed &data) {
|
|
return prepareBotAllowed(data);
|
|
}, [&](const MTPDmessageActionSecureValuesSent &data) {
|
|
return prepareSecureValuesSent(data);
|
|
}, [&](const MTPDmessageActionContactSignUp &data) {
|
|
return prepareContactSignUp();
|
|
}, [&](const MTPDmessageActionGeoProximityReached &data) {
|
|
return prepareProximityReached(data);
|
|
}, [](const MTPDmessageActionPaymentSentMe &) {
|
|
LOG(("API Error: messageActionPaymentSentMe received."));
|
|
return PreparedText{
|
|
tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
|
|
};
|
|
}, [](const MTPDmessageActionSecureValuesSentMe &) {
|
|
LOG(("API Error: messageActionSecureValuesSentMe received."));
|
|
return PreparedText{
|
|
tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
|
|
};
|
|
}, [&](const MTPDmessageActionGroupCall &data) {
|
|
return prepareGroupCall(data);
|
|
}, [&](const MTPDmessageActionInviteToGroupCall &data) {
|
|
return prepareInviteToGroupCall(data);
|
|
}, [&](const MTPDmessageActionSetMessagesTTL &data) {
|
|
return prepareSetMessagesTTL(data);
|
|
}, [&](const MTPDmessageActionGroupCallScheduled &data) {
|
|
return prepareCallScheduledText(data.vschedule_date().v);
|
|
}, [&](const MTPDmessageActionSetChatTheme &data) {
|
|
return prepareSetChatTheme(data);
|
|
}, [&](const MTPDmessageActionChatJoinedByRequest &data) {
|
|
return prepareChatJoinedByRequest(data);
|
|
}, [&](const MTPDmessageActionWebViewDataSent &data) {
|
|
return prepareWebViewDataSent(data);
|
|
}, [&](const MTPDmessageActionGiftPremium &data) {
|
|
return prepareGiftPremium(data);
|
|
}, [&](const MTPDmessageActionWebViewDataSentMe &data) {
|
|
LOG(("API Error: messageActionWebViewDataSentMe received."));
|
|
return PreparedText{
|
|
tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
|
|
};
|
|
}, [](const MTPDmessageActionEmpty &) {
|
|
return PreparedText{
|
|
tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
|
|
};
|
|
});
|
|
|
|
setServiceText(messageText);
|
|
|
|
// Additional information.
|
|
applyAction(action);
|
|
}
|
|
|
|
void HistoryService::applyAction(const MTPMessageAction &action) {
|
|
action.match([&](const MTPDmessageActionChatAddUser &data) {
|
|
if (const auto channel = history()->peer->asMegagroup()) {
|
|
const auto selfUserId = history()->session().userId();
|
|
for (const auto &item : data.vusers().v) {
|
|
if (peerFromUser(item) == selfUserId) {
|
|
channel->mgInfo->joinedMessageFound = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}, [&](const MTPDmessageActionChatJoinedByLink &data) {
|
|
if (_from->isSelf()) {
|
|
if (const auto channel = history()->peer->asMegagroup()) {
|
|
channel->mgInfo->joinedMessageFound = true;
|
|
}
|
|
}
|
|
}, [&](const MTPDmessageActionChatEditPhoto &data) {
|
|
data.vphoto().match([&](const MTPDphoto &photo) {
|
|
_media = std::make_unique<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::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 = 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) {
|
|
const auto context = Core::MarkedTextContext{
|
|
.session = &history()->session(),
|
|
.customEmojiRepaint = [=] { customEmojiRepaint(); },
|
|
};
|
|
_text.setMarkedText(
|
|
st::serviceTextStyle,
|
|
prepared.text,
|
|
Ui::ItemTextServiceOptions(),
|
|
context);
|
|
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_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 (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::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;
|
|
}
|