5456 lines
151 KiB
C++
5456 lines
151 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_item.h"
|
|
|
|
#include "lang/lang_keys.h"
|
|
#include "mainwidget.h"
|
|
#include "calls/calls_instance.h" // Core::App().calls().joinGroupCall.
|
|
#include "history/view/history_view_item_preview.h"
|
|
#include "history/view/history_view_message.h"
|
|
#include "history/view/history_view_service_message.h"
|
|
#include "history/view/media/history_view_media_grouped.h"
|
|
#include "history/history_item_components.h"
|
|
#include "history/history_item_helpers.h"
|
|
#include "history/history_unread_things.h"
|
|
#include "history/history.h"
|
|
#include "mtproto/mtproto_config.h"
|
|
#include "ui/text/format_values.h"
|
|
#include "ui/text/text_isolated_emoji.h"
|
|
#include "ui/text/text_utilities.h"
|
|
#include "storage/file_upload.h"
|
|
#include "storage/storage_shared_media.h"
|
|
#include "main/main_account.h"
|
|
#include "main/main_domain.h"
|
|
#include "main/main_session.h"
|
|
#include "main/main_session_settings.h"
|
|
#include "menu/menu_ttl_validator.h"
|
|
#include "apiwrap.h"
|
|
#include "media/audio/media_audio.h"
|
|
#include "core/application.h"
|
|
#include "window/window_controller.h"
|
|
#include "window/window_session_controller.h"
|
|
#include "core/click_handler_types.h"
|
|
#include "base/unixtime.h"
|
|
#include "base/timer_rpl.h"
|
|
#include "api/api_text_entities.h"
|
|
#include "api/api_updates.h"
|
|
#include "data/notify/data_notify_settings.h"
|
|
#include "data/data_bot_app.h"
|
|
#include "data/data_saved_messages.h"
|
|
#include "data/data_saved_sublist.h"
|
|
#include "data/data_scheduled_messages.h"
|
|
#include "data/data_changes.h"
|
|
#include "data/data_session.h"
|
|
#include "data/data_message_reactions.h"
|
|
#include "data/data_folder.h"
|
|
#include "data/data_forum.h"
|
|
#include "data/data_forum_topic.h"
|
|
#include "data/data_channel.h"
|
|
#include "data/data_chat.h"
|
|
#include "data/data_game.h"
|
|
#include "data/data_user.h"
|
|
#include "data/data_group_call.h" // Data::GroupCall::id().
|
|
#include "data/data_poll.h" // PollData::publicVotes.
|
|
#include "data/data_sponsored_messages.h"
|
|
#include "data/data_stories.h"
|
|
#include "data/data_web_page.h"
|
|
#include "chat_helpers/stickers_gift_box_pack.h"
|
|
#include "payments/payments_checkout_process.h" // CheckoutProcess::Start.
|
|
#include "platform/platform_notifications_manager.h"
|
|
#include "spellcheck/spellcheck_highlight_syntax.h"
|
|
#include "styles/style_dialogs.h"
|
|
|
|
namespace {
|
|
|
|
constexpr auto kNotificationTextLimit = 255;
|
|
constexpr auto kPinnedMessageTextLimit = 16;
|
|
|
|
using ItemPreview = HistoryView::ItemPreview;
|
|
|
|
template <typename T>
|
|
[[nodiscard]] PreparedServiceText PrepareEmptyText(const T &) {
|
|
return PreparedServiceText();
|
|
};
|
|
|
|
template <typename T>
|
|
[[nodiscard]] PreparedServiceText PrepareErrorText(const T &data) {
|
|
if constexpr (!std::is_same_v<T, MTPDmessageActionEmpty>) {
|
|
const auto name = QString::fromUtf8(typeid(data).name());
|
|
LOG(("API Error: %1 received.").arg(name));
|
|
}
|
|
return PreparedServiceText{ { tr::lng_message_empty(tr::now) } };
|
|
}
|
|
|
|
[[nodiscard]] TextWithEntities SpoilerLoginCode(TextWithEntities text) {
|
|
const auto r = QRegularExpression(u"([\\d\\-]{5,7})"_q);
|
|
const auto m = r.match(text.text);
|
|
if (!m.hasMatch()) {
|
|
return text;
|
|
}
|
|
const auto codeStart = int(m.capturedStart(1));
|
|
const auto codeLength = int(m.capturedLength(1));
|
|
auto i = text.entities.begin();
|
|
const auto e = text.entities.end();
|
|
while (i != e && i->offset() < codeStart) {
|
|
if (i->offset() + i->length() > codeStart) {
|
|
return text; // Entities should not intersect code.
|
|
}
|
|
++i;
|
|
}
|
|
text.entities.insert(i, { EntityType::Spoiler, codeStart, codeLength });
|
|
return text;
|
|
}
|
|
|
|
[[nodiscard]] bool HasNotEmojiAndSpaces(const QString &text) {
|
|
if (text.isEmpty()) {
|
|
return false;
|
|
}
|
|
auto emoji = 0;
|
|
auto start = text.data();
|
|
const auto end = start + text.size();
|
|
while (start < end) {
|
|
if (start->isSpace()) {
|
|
++start;
|
|
} else if (Ui::Emoji::Find(start, end, &emoji)) {
|
|
start += emoji;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void HistoryItem::HistoryItem::Destroyer::operator()(HistoryItem *value) {
|
|
if (value) {
|
|
value->destroy();
|
|
}
|
|
}
|
|
|
|
struct HistoryItem::CreateConfig {
|
|
ReplyFields reply;
|
|
|
|
UserId viaBotId = 0;
|
|
int viewsCount = -1;
|
|
int forwardsCount = -1;
|
|
int boostsApplied = 0;
|
|
QString postAuthor;
|
|
|
|
MsgId originalId = 0;
|
|
TimeId originalDate = 0;
|
|
PeerId originalSenderId = 0;
|
|
QString originalSenderName;
|
|
QString originalPostAuthor;
|
|
|
|
PeerId savedSublistPeer = 0;
|
|
|
|
QString forwardPsaType;
|
|
PeerId savedFromPeer = 0;
|
|
MsgId savedFromMsgId = 0;
|
|
|
|
PeerId savedFromSenderId = 0;
|
|
QString savedFromSenderName;
|
|
bool savedFromOutgoing = false;
|
|
|
|
TimeId editDate = 0;
|
|
HistoryMessageMarkupData markup;
|
|
HistoryMessageRepliesData replies;
|
|
bool imported = false;
|
|
|
|
// For messages created from existing messages (forwarded).
|
|
const HistoryMessageReplyMarkup *inlineMarkup = nullptr;
|
|
};
|
|
|
|
void HistoryItem::FillForwardedInfo(
|
|
CreateConfig &config,
|
|
const MTPDmessageFwdHeader &data) {
|
|
config.originalId = data.vchannel_post().value_or_empty();
|
|
config.originalDate = data.vdate().v;
|
|
if (const auto fromId = data.vfrom_id()) {
|
|
config.originalSenderId = peerFromMTP(*fromId);
|
|
}
|
|
config.originalSenderName = qs(data.vfrom_name().value_or_empty());
|
|
config.originalPostAuthor = qs(data.vpost_author().value_or_empty());
|
|
config.forwardPsaType = qs(data.vpsa_type().value_or_empty());
|
|
const auto savedFromPeer = data.vsaved_from_peer();
|
|
const auto savedFromMsgId = data.vsaved_from_msg_id();
|
|
if (savedFromPeer && savedFromMsgId) {
|
|
config.savedFromPeer = peerFromMTP(*savedFromPeer);
|
|
config.savedFromMsgId = savedFromMsgId->v;
|
|
}
|
|
config.savedFromSenderId = data.vsaved_from_id()
|
|
? peerFromMTP(*data.vsaved_from_id())
|
|
: PeerId();
|
|
config.savedFromSenderName = qs(
|
|
data.vsaved_from_name().value_or_empty());
|
|
config.savedFromOutgoing = data.is_saved_out();
|
|
|
|
config.imported = data.is_imported();
|
|
}
|
|
|
|
std::unique_ptr<Data::Media> HistoryItem::CreateMedia(
|
|
not_null<HistoryItem*> item,
|
|
const MTPMessageMedia &media) {
|
|
using Result = std::unique_ptr<Data::Media>;
|
|
return media.match([&](const MTPDmessageMediaContact &media) -> Result {
|
|
return std::make_unique<Data::MediaContact>(
|
|
item,
|
|
media.vuser_id().v,
|
|
qs(media.vfirst_name()),
|
|
qs(media.vlast_name()),
|
|
qs(media.vphone_number()));
|
|
}, [&](const MTPDmessageMediaGeo &media) -> Result {
|
|
return media.vgeo().match([&](const MTPDgeoPoint &point) -> Result {
|
|
return std::make_unique<Data::MediaLocation>(
|
|
item,
|
|
Data::LocationPoint(point));
|
|
}, [](const MTPDgeoPointEmpty &) -> Result {
|
|
return nullptr;
|
|
});
|
|
}, [&](const MTPDmessageMediaGeoLive &media) -> Result {
|
|
return media.vgeo().match([&](const MTPDgeoPoint &point) -> Result {
|
|
return std::make_unique<Data::MediaLocation>(
|
|
item,
|
|
Data::LocationPoint(point));
|
|
}, [](const MTPDgeoPointEmpty &) -> Result {
|
|
return nullptr;
|
|
});
|
|
}, [&](const MTPDmessageMediaVenue &media) -> Result {
|
|
return media.vgeo().match([&](const MTPDgeoPoint &point) -> Result {
|
|
return std::make_unique<Data::MediaLocation>(
|
|
item,
|
|
Data::LocationPoint(point),
|
|
qs(media.vtitle()),
|
|
qs(media.vaddress()));
|
|
}, [](const MTPDgeoPointEmpty &data) -> Result {
|
|
return nullptr;
|
|
});
|
|
}, [&](const MTPDmessageMediaPhoto &media) -> Result {
|
|
const auto photo = media.vphoto();
|
|
if (media.vttl_seconds()) {
|
|
LOG(("App Error: "
|
|
"Unexpected MTPMessageMediaPhoto "
|
|
"with ttl_seconds in CreateMedia."));
|
|
return nullptr;
|
|
} else if (!photo) {
|
|
LOG(("API Error: "
|
|
"Got MTPMessageMediaPhoto "
|
|
"without photo and without ttl_seconds."));
|
|
return nullptr;
|
|
}
|
|
return photo->match([&](const MTPDphoto &photo) -> Result {
|
|
return std::make_unique<Data::MediaPhoto>(
|
|
item,
|
|
item->history()->owner().processPhoto(photo),
|
|
media.is_spoiler());
|
|
}, [](const MTPDphotoEmpty &) -> Result {
|
|
return nullptr;
|
|
});
|
|
}, [&](const MTPDmessageMediaDocument &media) -> Result {
|
|
const auto document = media.vdocument();
|
|
if (media.vttl_seconds() && media.is_video()) {
|
|
LOG(("App Error: "
|
|
"Unexpected MTPMessageMediaDocument "
|
|
"with ttl_seconds in CreateMedia."));
|
|
return nullptr;
|
|
} else if (!document) {
|
|
LOG(("API Error: "
|
|
"Got MTPMessageMediaDocument "
|
|
"without document and without ttl_seconds."));
|
|
return nullptr;
|
|
}
|
|
return document->match([&](const MTPDdocument &document) -> Result {
|
|
return std::make_unique<Data::MediaFile>(
|
|
item,
|
|
item->history()->owner().processDocument(document),
|
|
media.is_nopremium(),
|
|
media.is_spoiler(),
|
|
media.vttl_seconds().value_or_empty());
|
|
}, [](const MTPDdocumentEmpty &) -> Result {
|
|
return nullptr;
|
|
});
|
|
}, [&](const MTPDmessageMediaWebPage &media) {
|
|
using Flag = MediaWebPageFlag;
|
|
const auto flags = Flag()
|
|
| (media.is_force_large_media()
|
|
? Flag::ForceLargeMedia
|
|
: Flag())
|
|
| (media.is_force_small_media()
|
|
? Flag::ForceSmallMedia
|
|
: Flag())
|
|
| (media.is_manual() ? Flag::Manual : Flag())
|
|
| (media.is_safe() ? Flag::Safe : Flag());
|
|
return media.vwebpage().match([](const MTPDwebPageEmpty &) -> Result {
|
|
return nullptr;
|
|
}, [&](const MTPDwebPagePending &webpage) -> Result {
|
|
return std::make_unique<Data::MediaWebPage>(
|
|
item,
|
|
item->history()->owner().processWebpage(webpage),
|
|
flags);
|
|
}, [&](const MTPDwebPage &webpage) -> Result {
|
|
return std::make_unique<Data::MediaWebPage>(
|
|
item,
|
|
item->history()->owner().processWebpage(webpage),
|
|
flags);
|
|
}, [](const MTPDwebPageNotModified &) -> Result {
|
|
LOG(("API Error: "
|
|
"webPageNotModified is unexpected in message media."));
|
|
return nullptr;
|
|
});
|
|
}, [&](const MTPDmessageMediaGame &media) -> Result {
|
|
return media.vgame().match([&](const MTPDgame &game) {
|
|
return std::make_unique<Data::MediaGame>(
|
|
item,
|
|
item->history()->owner().processGame(game));
|
|
});
|
|
}, [&](const MTPDmessageMediaInvoice &media) -> Result {
|
|
return std::make_unique<Data::MediaInvoice>(
|
|
item,
|
|
Data::ComputeInvoiceData(item, media));
|
|
}, [&](const MTPDmessageMediaPoll &media) -> Result {
|
|
return std::make_unique<Data::MediaPoll>(
|
|
item,
|
|
item->history()->owner().processPoll(media));
|
|
}, [&](const MTPDmessageMediaDice &media) -> Result {
|
|
return std::make_unique<Data::MediaDice>(
|
|
item,
|
|
qs(media.vemoticon()),
|
|
media.vvalue().v);
|
|
}, [&](const MTPDmessageMediaStory &media) -> Result {
|
|
return std::make_unique<Data::MediaStory>(item, FullStoryId{
|
|
peerFromMTP(media.vpeer()),
|
|
media.vid().v,
|
|
}, media.is_via_mention());
|
|
}, [&](const MTPDmessageMediaGiveaway &media) -> Result {
|
|
return std::make_unique<Data::MediaGiveawayStart>(
|
|
item,
|
|
Data::ComputeGiveawayStartData(item, media));
|
|
}, [&](const MTPDmessageMediaGiveawayResults &media) -> Result {
|
|
return std::make_unique<Data::MediaGiveawayResults>(
|
|
item,
|
|
Data::ComputeGiveawayResultsData(item, media));
|
|
}, [](const MTPDmessageMediaEmpty &) -> Result {
|
|
return nullptr;
|
|
}, [](const MTPDmessageMediaUnsupported &) -> Result {
|
|
return nullptr;
|
|
});
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
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)) {
|
|
_boostsApplied = data.vfrom_boosts_applied().value_or_empty();
|
|
|
|
const auto media = data.vmedia();
|
|
const auto checked = media
|
|
? CheckMessageMedia(*media)
|
|
: MediaCheckResult::Good;
|
|
if (checked == MediaCheckResult::Unsupported) {
|
|
_flags &= ~MessageFlag::HasPostAuthor;
|
|
_flags |= MessageFlag::Legacy;
|
|
createComponents(data);
|
|
setText(UnsupportedMessageText());
|
|
} else if (checked == MediaCheckResult::Empty) {
|
|
AddComponents(HistoryServiceData::Bit());
|
|
setServiceText({
|
|
tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
|
|
});
|
|
} else if ((checked == MediaCheckResult::HasUnsupportedTimeToLive)
|
|
|| (checked == MediaCheckResult::HasExpiredMediaTimeToLive)) {
|
|
createServiceFromMtp(data);
|
|
applyTTL(data);
|
|
} else if (checked == MediaCheckResult::HasStoryMention) {
|
|
setMedia(*data.vmedia());
|
|
createServiceFromMtp(data);
|
|
applyTTL(data);
|
|
} else {
|
|
createComponents(data);
|
|
if (const auto media = data.vmedia()) {
|
|
setMedia(*media);
|
|
}
|
|
auto textWithEntities = TextWithEntities{
|
|
qs(data.vmessage()),
|
|
Api::EntitiesFromMTP(
|
|
&history->session(),
|
|
data.ventities().value_or_empty())
|
|
};
|
|
setText(_media ? textWithEntities : EnsureNonEmpty(textWithEntities));
|
|
if (const auto groupedId = data.vgrouped_id()) {
|
|
setGroupId(
|
|
MessageGroupId::FromRaw(
|
|
history->peer->id,
|
|
groupedId->v,
|
|
_flags & MessageFlag::IsOrWasScheduled));
|
|
}
|
|
setReactions(data.vreactions());
|
|
applyTTL(data);
|
|
}
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
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)) {
|
|
if (data.vaction().type() != mtpc_messageActionPhoneCall) {
|
|
createServiceFromMtp(data);
|
|
} else {
|
|
createComponents(CreateConfig());
|
|
_media = std::make_unique<Data::MediaCall>(
|
|
this,
|
|
Data::ComputeCallData(data.vaction().c_messageActionPhoneCall()));
|
|
setTextValue({});
|
|
}
|
|
applyTTL(data);
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
not_null<History*> history,
|
|
MsgId id,
|
|
const MTPDmessageEmpty &data,
|
|
MessageFlags localFlags)
|
|
: HistoryItem(
|
|
history,
|
|
id,
|
|
localFlags,
|
|
TimeId(0),
|
|
PreparedServiceText{ tr::lng_message_empty(
|
|
tr::now,
|
|
Ui::Text::WithEntities) }) {
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
not_null<History*> history,
|
|
MsgId id,
|
|
MessageFlags flags,
|
|
TimeId date,
|
|
PreparedServiceText &&message,
|
|
PeerId from,
|
|
PhotoData *photo)
|
|
: HistoryItem(history, id, flags, date, from) {
|
|
setServiceText(std::move(message));
|
|
if (photo) {
|
|
_media = std::make_unique<Data::MediaPhoto>(
|
|
this,
|
|
history->peer,
|
|
photo);
|
|
}
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
not_null<History*> history,
|
|
MsgId id,
|
|
MessageFlags flags,
|
|
TimeId date,
|
|
PeerId from,
|
|
const QString &postAuthor,
|
|
not_null<HistoryItem*> original,
|
|
MsgId topicRootId)
|
|
: HistoryItem(
|
|
history,
|
|
id,
|
|
(NewForwardedFlags(history->peer, from, original) | flags),
|
|
date,
|
|
from) {
|
|
const auto peer = history->peer;
|
|
|
|
auto config = CreateConfig();
|
|
|
|
const auto originalMedia = original->media();
|
|
const auto dropForwardInfo = original->computeDropForwardedInfo();
|
|
config.reply.messageId = config.reply.topMessageId = topicRootId;
|
|
config.reply.topicPost = (topicRootId != 0) ? 1 : 0;
|
|
if (const auto originalReply = original->Get<HistoryMessageReply>()) {
|
|
if (originalReply->external()) {
|
|
config.reply = originalReply->fields().clone(this);
|
|
if (!config.reply.externalPeerId) {
|
|
config.reply.messageId = 0;
|
|
}
|
|
}
|
|
}
|
|
if (!dropForwardInfo) {
|
|
config.originalDate = original->originalDate();
|
|
if (const auto info = original->originalHiddenSenderInfo()) {
|
|
config.originalSenderName = info->name;
|
|
} else if (const auto originalSender = original->originalSender()) {
|
|
config.originalSenderId = originalSender->id;
|
|
if (originalSender->isChannel()) {
|
|
config.originalId = original->originalId();
|
|
}
|
|
} else {
|
|
Unexpected("Corrupt forwarded information in message.");
|
|
}
|
|
config.originalPostAuthor = original->originalPostAuthor();
|
|
}
|
|
if (peer->isSelf()) {
|
|
//
|
|
// iOS app sends you to the original post if we forward a forward from channel.
|
|
// But server returns not the original post but the forward in saved_from_...
|
|
//
|
|
//if (config.originalId) {
|
|
// config.savedFromPeer = config.senderOriginal;
|
|
// config.savedFromMsgId = config.originalId;
|
|
//} else {
|
|
config.savedFromPeer = original->history()->peer->id;
|
|
config.savedFromMsgId = original->id;
|
|
//}
|
|
|
|
config.savedFromOutgoing = original->out();
|
|
config.savedFromSenderId = original->Get<HistoryMessageForwarded>()
|
|
? original->author()->id
|
|
: PeerId();
|
|
}
|
|
if (flags & MessageFlag::HasPostAuthor) {
|
|
config.postAuthor = postAuthor;
|
|
}
|
|
if (const auto fwdViaBot = original->viaBot()) {
|
|
config.viaBotId = peerToUser(fwdViaBot->id);
|
|
} else if (originalMedia && originalMedia->game()) {
|
|
if (const auto sender = original->originalSender()) {
|
|
if (const auto user = sender->asUser()) {
|
|
if (user->isBot()) {
|
|
config.viaBotId = peerToUser(user->id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const auto fwdViewsCount = original->viewsCount();
|
|
if (fwdViewsCount > 0) {
|
|
config.viewsCount = fwdViewsCount;
|
|
} else if ((isPost() && !isScheduled())
|
|
|| (original->originalSender()
|
|
&& original->originalSender()->isChannel())) {
|
|
config.viewsCount = 1;
|
|
}
|
|
|
|
const auto mediaOriginal = original->media();
|
|
if (CopyMarkupToForward(original)) {
|
|
config.inlineMarkup = original->inlineReplyMarkup();
|
|
}
|
|
createComponents(std::move(config));
|
|
|
|
const auto ignoreMedia = [&] {
|
|
if (mediaOriginal && mediaOriginal->webpage()) {
|
|
if (peer->amRestricted(ChatRestriction::EmbedLinks)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
if (mediaOriginal && !ignoreMedia()) {
|
|
_media = mediaOriginal->clone(this);
|
|
if (original->invertMedia()) {
|
|
_flags |= MessageFlag::InvertMedia;
|
|
}
|
|
}
|
|
|
|
setText(dropForwardInfo
|
|
? DropDisallowedCustomEmoji(history->peer, original->originalText())
|
|
: original->originalText());
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
not_null<History*> history,
|
|
MsgId id,
|
|
MessageFlags flags,
|
|
FullReplyTo replyTo,
|
|
UserId viaBotId,
|
|
TimeId date,
|
|
PeerId from,
|
|
const QString &postAuthor,
|
|
const TextWithEntities &textWithEntities,
|
|
const MTPMessageMedia &media,
|
|
HistoryMessageMarkupData &&markup,
|
|
uint64 groupedId)
|
|
: HistoryItem(
|
|
history,
|
|
id,
|
|
flags,
|
|
date,
|
|
(flags & MessageFlag::HasFromId) ? from : 0) {
|
|
createComponentsHelper(
|
|
flags,
|
|
replyTo,
|
|
viaBotId,
|
|
postAuthor,
|
|
std::move(markup));
|
|
setMedia(media);
|
|
setText(textWithEntities);
|
|
if (groupedId) {
|
|
setGroupId(MessageGroupId::FromRaw(
|
|
history->peer->id,
|
|
groupedId,
|
|
flags & MessageFlag::IsOrWasScheduled));
|
|
}
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
not_null<History*> history,
|
|
MsgId id,
|
|
MessageFlags flags,
|
|
FullReplyTo replyTo,
|
|
UserId viaBotId,
|
|
TimeId date,
|
|
PeerId from,
|
|
const QString &postAuthor,
|
|
not_null<DocumentData*> document,
|
|
const TextWithEntities &caption,
|
|
HistoryMessageMarkupData &&markup)
|
|
: HistoryItem(
|
|
history,
|
|
id,
|
|
flags,
|
|
date,
|
|
(flags & MessageFlag::HasFromId) ? from : 0) {
|
|
createComponentsHelper(
|
|
flags,
|
|
replyTo,
|
|
viaBotId,
|
|
postAuthor,
|
|
std::move(markup));
|
|
|
|
const auto skipPremiumEffect = !history->session().premium();
|
|
const auto spoiler = false;
|
|
_media = std::make_unique<Data::MediaFile>(
|
|
this,
|
|
document,
|
|
skipPremiumEffect,
|
|
spoiler,
|
|
/*ttlSeconds = */0);
|
|
setText(caption);
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
not_null<History*> history,
|
|
MsgId id,
|
|
MessageFlags flags,
|
|
FullReplyTo replyTo,
|
|
UserId viaBotId,
|
|
TimeId date,
|
|
PeerId from,
|
|
const QString &postAuthor,
|
|
not_null<PhotoData*> photo,
|
|
const TextWithEntities &caption,
|
|
HistoryMessageMarkupData &&markup)
|
|
: HistoryItem(
|
|
history,
|
|
id,
|
|
flags,
|
|
date,
|
|
(flags & MessageFlag::HasFromId) ? from : 0) {
|
|
createComponentsHelper(
|
|
flags,
|
|
replyTo,
|
|
viaBotId,
|
|
postAuthor,
|
|
std::move(markup));
|
|
|
|
const auto spoiler = false;
|
|
_media = std::make_unique<Data::MediaPhoto>(this, photo, spoiler);
|
|
setText(caption);
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
not_null<History*> history,
|
|
MsgId id,
|
|
MessageFlags flags,
|
|
FullReplyTo replyTo,
|
|
UserId viaBotId,
|
|
TimeId date,
|
|
PeerId from,
|
|
const QString &postAuthor,
|
|
not_null<GameData*> game,
|
|
HistoryMessageMarkupData &&markup)
|
|
: HistoryItem(
|
|
history,
|
|
id,
|
|
flags,
|
|
date,
|
|
(flags & MessageFlag::HasFromId) ? from : 0) {
|
|
createComponentsHelper(
|
|
flags,
|
|
replyTo,
|
|
viaBotId,
|
|
postAuthor,
|
|
std::move(markup));
|
|
|
|
_media = std::make_unique<Data::MediaGame>(this, game);
|
|
setTextValue({});
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
not_null<History*> history,
|
|
MsgId id,
|
|
Data::SponsoredFrom from,
|
|
const TextWithEntities &textWithEntities,
|
|
HistoryItem *injectedAfter)
|
|
: HistoryItem(
|
|
history,
|
|
id,
|
|
((history->peer->isChannel() ? MessageFlag::Post : MessageFlag(0))
|
|
//| (from.peer ? MessageFlag::HasFromId : MessageFlag(0))
|
|
| MessageFlag::Local),
|
|
HistoryItem::NewMessageDate(injectedAfter
|
|
? injectedAfter->date()
|
|
: 0),
|
|
/*from.peer ? from.peer->id : */PeerId(0)) {
|
|
_flags |= MessageFlag::Sponsored;
|
|
|
|
const auto webPageType = !from.externalLink.isEmpty()
|
|
? WebPageType::None
|
|
: from.isExactPost
|
|
? WebPageType::Message
|
|
: (from.botLinkInfo && !from.botLinkInfo->botAppName.isEmpty())
|
|
? WebPageType::BotApp
|
|
: from.botLinkInfo
|
|
? WebPageType::Bot
|
|
: from.isBroadcast
|
|
? WebPageType::Channel
|
|
: (from.peer && from.peer->isUser())
|
|
? WebPageType::User
|
|
: WebPageType::Group;
|
|
|
|
const auto webpage = history->peer->owner().webpage(
|
|
history->peer->owner().nextLocalMessageId().bare,
|
|
webPageType,
|
|
from.externalLink,
|
|
from.externalLink,
|
|
from.isRecommended
|
|
? tr::lng_recommended_message_title(tr::now)
|
|
: tr::lng_sponsored_message_title(tr::now),
|
|
from.title,
|
|
textWithEntities,
|
|
(from.webpageOrBotPhotoId
|
|
? history->owner().photo(from.webpageOrBotPhotoId).get()
|
|
: nullptr),
|
|
nullptr,
|
|
WebPageCollage(),
|
|
0,
|
|
QString(),
|
|
false,
|
|
0);
|
|
auto webpageMedia = std::make_unique<Data::MediaWebPage>(
|
|
this,
|
|
webpage,
|
|
MediaWebPageFlag::Sponsored);
|
|
_media = std::move(webpageMedia);
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
not_null<History*> history,
|
|
MsgId id,
|
|
MessageFlags flags,
|
|
TimeId date,
|
|
PeerId from)
|
|
: id(id)
|
|
, _history(history)
|
|
, _from(from ? history->owner().peer(from) : history->peer)
|
|
, _flags(FinalizeMessageFlags(history, flags))
|
|
, _date(date) {
|
|
if (isHistoryEntry() && IsClientMsgId(id)) {
|
|
_history->registerClientSideMessage(this);
|
|
}
|
|
}
|
|
|
|
HistoryItem::HistoryItem(
|
|
not_null<History*> history,
|
|
not_null<Data::Story*> story)
|
|
: id(StoryIdToMsgId(story->id()))
|
|
, _history(history)
|
|
, _from(history->peer)
|
|
, _flags(MessageFlag::Local
|
|
| MessageFlag::Outgoing
|
|
| MessageFlag::FakeHistoryItem
|
|
| MessageFlag::StoryItem)
|
|
, _date(story->date()) {
|
|
setStoryFields(story);
|
|
}
|
|
|
|
HistoryItem::~HistoryItem() {
|
|
_media = nullptr;
|
|
clearSavedMedia();
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
reply->clearData(this);
|
|
}
|
|
if (const auto saved = Get<HistoryMessageSaved>()) {
|
|
saved->sublist->removeOne(this);
|
|
}
|
|
clearDependencyMessage();
|
|
applyTTL(0);
|
|
}
|
|
|
|
TimeId HistoryItem::date() const {
|
|
return _date;
|
|
}
|
|
|
|
TimeId HistoryItem::NewMessageDate(TimeId scheduled) {
|
|
return scheduled ? scheduled : base::unixtime::now();
|
|
}
|
|
|
|
HistoryServiceDependentData *HistoryItem::GetServiceDependentData() {
|
|
if (const auto pinned = Get<HistoryServicePinned>()) {
|
|
return pinned;
|
|
} else if (const auto gamescore = Get<HistoryServiceGameScore>()) {
|
|
return gamescore;
|
|
} else if (const auto payment = Get<HistoryServicePayment>()) {
|
|
return payment;
|
|
} else if (const auto info = Get<HistoryServiceTopicInfo>()) {
|
|
return info;
|
|
} else if (const auto same = Get<HistoryServiceSameBackground>()) {
|
|
return same;
|
|
} else if (const auto results = Get<HistoryServiceGiveawayResults>()) {
|
|
return results;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
auto HistoryItem::GetServiceDependentData() const
|
|
-> const HistoryServiceDependentData * {
|
|
return const_cast<HistoryItem*>(this)->GetServiceDependentData();
|
|
}
|
|
|
|
void HistoryItem::dependencyItemRemoved(not_null<HistoryItem*> dependency) {
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
const auto documentId = reply->replyToDocumentId;
|
|
reply->itemRemoved(this, dependency);
|
|
if (documentId != reply->replyToDocumentId
|
|
&& generateLocalEntitiesByReply()) {
|
|
_history->owner().requestItemTextRefresh(this);
|
|
}
|
|
} else {
|
|
clearDependencyMessage();
|
|
updateDependentServiceText();
|
|
}
|
|
}
|
|
|
|
void HistoryItem::dependencyStoryRemoved(
|
|
not_null<Data::Story*> dependency) {
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
const auto documentId = reply->replyToDocumentId;
|
|
reply->storyRemoved(this, dependency);
|
|
if (documentId != reply->replyToDocumentId
|
|
&& generateLocalEntitiesByReply()) {
|
|
_history->owner().requestItemTextRefresh(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::updateDependencyItem() {
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
const auto documentId = reply->replyToDocumentId;
|
|
const auto webpageId = reply->replyToWebPageId;
|
|
reply->updateData(this, true);
|
|
const auto mediaIdChanged = (documentId != reply->replyToDocumentId)
|
|
|| (webpageId != reply->replyToWebPageId);
|
|
if (mediaIdChanged && generateLocalEntitiesByReply()) {
|
|
_history->owner().requestItemTextRefresh(this);
|
|
}
|
|
} else if (GetServiceDependentData()) {
|
|
updateServiceDependent(true);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::updateDependentServiceText() {
|
|
if (Has<HistoryServicePinned>()) {
|
|
updateServiceText(preparePinnedText());
|
|
} else if (Has<HistoryServiceGameScore>()) {
|
|
updateServiceText(prepareGameScoreText());
|
|
} else if (Has<HistoryServicePayment>()) {
|
|
updateServiceText(preparePaymentSentText());
|
|
}
|
|
}
|
|
|
|
void HistoryItem::updateServiceDependent(bool force) {
|
|
auto dependent = GetServiceDependentData();
|
|
Assert(dependent != nullptr);
|
|
|
|
if (!force) {
|
|
if (!dependent->msgId || dependent->msg) {
|
|
dependent->pendingResolve = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!dependent->lnk) {
|
|
dependent->lnk = JumpToMessageClickHandler(
|
|
(dependent->peerId
|
|
? _history->owner().peer(dependent->peerId)
|
|
: _history->peer),
|
|
dependent->msgId,
|
|
fullId());
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Record resolve state for upcoming on-demand resolving.
|
|
if (dependent->msg || !dependent->msgId || force) {
|
|
dependent->pendingResolve = false;
|
|
} else {
|
|
dependent->pendingResolve = true;
|
|
dependent->requestedResolve = false;
|
|
}
|
|
|
|
// updateDependentServiceText may call UpdateComponents!
|
|
// So the `dependent` pointer becomes invalid.
|
|
if (dependent->msg) {
|
|
updateDependentServiceText();
|
|
} else if (force) {
|
|
if (dependent->msgId > 0) {
|
|
dependent->msgId = 0;
|
|
gotDependencyItem = true;
|
|
}
|
|
updateDependentServiceText();
|
|
}
|
|
if (force && gotDependencyItem) {
|
|
Core::App().notifications().checkDelayed();
|
|
}
|
|
}
|
|
|
|
MsgId HistoryItem::dependencyMsgId() const {
|
|
if (auto dependent = GetServiceDependentData()) {
|
|
return dependent->msgId;
|
|
}
|
|
return replyToId();
|
|
}
|
|
|
|
void HistoryItem::checkBuyButton() {
|
|
if (const auto invoice = _media ? _media->invoice() : nullptr) {
|
|
if (invoice->receiptMsgId) {
|
|
replaceBuyWithReceiptInMarkup();
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::resolveDependent(
|
|
not_null<HistoryServiceDependentData*> dependent) {
|
|
if (!dependent->pendingResolve || dependent->requestedResolve) {
|
|
return;
|
|
}
|
|
dependent->requestedResolve = true;
|
|
RequestDependentMessageItem(
|
|
this,
|
|
(dependent->peerId ? dependent->peerId : _history->peer->id),
|
|
dependent->msgId);
|
|
}
|
|
|
|
void HistoryItem::resolveDependent(not_null<HistoryMessageReply*> reply) {
|
|
if (!reply->acquireResolve()) {
|
|
return;
|
|
} else if (const auto messageId = reply->messageId()) {
|
|
RequestDependentMessageItem(
|
|
this,
|
|
reply->externalPeerId(),
|
|
reply->messageId());
|
|
} else if (reply->storyId()) {
|
|
RequestDependentMessageStory(
|
|
this,
|
|
reply->externalPeerId(),
|
|
reply->storyId());
|
|
}
|
|
}
|
|
|
|
void HistoryItem::resolveDependent() {
|
|
if (const auto dependent = GetServiceDependentData()) {
|
|
resolveDependent(dependent);
|
|
} else if (const auto reply = Get<HistoryMessageReply>()) {
|
|
resolveDependent(reply);
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::notificationReady() const {
|
|
if (const auto dependent = GetServiceDependentData()) {
|
|
if (dependent->msg || !dependent->msgId) {
|
|
return true;
|
|
}
|
|
const_cast<HistoryItem*>(this)->resolveDependent(
|
|
const_cast<HistoryServiceDependentData*>(dependent));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void HistoryItem::finishEdition(int oldKeyboardTop) {
|
|
if (const auto group = _history->owner().groups().find(this)) {
|
|
for (const auto &item : group->items) {
|
|
_history->owner().requestItemViewRefresh(item);
|
|
item->invalidateChatListEntry();
|
|
}
|
|
} else {
|
|
_history->owner().requestItemViewRefresh(this);
|
|
invalidateChatListEntry();
|
|
}
|
|
|
|
// Should be completely redesigned as the oldTop no longer exists.
|
|
//if (oldKeyboardTop >= 0) { // #TODO edit bot message
|
|
// if (auto keyboard = Get<HistoryMessageReplyMarkup>()) {
|
|
// keyboard->oldTop = oldKeyboardTop;
|
|
// }
|
|
//}
|
|
|
|
_history->owner().updateDependentMessages(this);
|
|
}
|
|
|
|
void HistoryItem::setGroupId(MessageGroupId groupId) {
|
|
Expects(!_groupId);
|
|
|
|
_groupId = groupId;
|
|
_history->owner().groups().registerMessage(this);
|
|
}
|
|
|
|
bool HistoryItem::checkCommentsLinkedChat(ChannelId id) const {
|
|
if (!id) {
|
|
return true;
|
|
} else if (const auto channel = _history->peer->asChannel()) {
|
|
if (channel->linkedChatKnown()
|
|
|| !(channel->flags() & ChannelDataFlag::HasLink)) {
|
|
const auto linked = channel->linkedChat();
|
|
if (!linked || peerToChannel(linked->id) != id) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void HistoryItem::setReplyMarkup(HistoryMessageMarkupData &&markup) {
|
|
const auto requestUpdate = [&] {
|
|
history()->owner().requestItemResize(this);
|
|
history()->session().changes().messageUpdated(
|
|
this,
|
|
Data::MessageUpdate::Flag::ReplyMarkup);
|
|
};
|
|
if (markup.isNull()) {
|
|
if (_flags & MessageFlag::HasReplyMarkup) {
|
|
_flags &= ~MessageFlag::HasReplyMarkup;
|
|
if (Has<HistoryMessageReplyMarkup>()) {
|
|
RemoveComponents(HistoryMessageReplyMarkup::Bit());
|
|
}
|
|
requestUpdate();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// optimization: don't create markup component for the case
|
|
// MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag
|
|
if (markup.isTrivial()) {
|
|
bool changed = false;
|
|
if (Has<HistoryMessageReplyMarkup>()) {
|
|
RemoveComponents(HistoryMessageReplyMarkup::Bit());
|
|
changed = true;
|
|
}
|
|
if (!(_flags & MessageFlag::HasReplyMarkup)) {
|
|
_flags |= MessageFlag::HasReplyMarkup;
|
|
changed = true;
|
|
}
|
|
if (changed) {
|
|
requestUpdate();
|
|
}
|
|
} else {
|
|
if (!(_flags & MessageFlag::HasReplyMarkup)) {
|
|
_flags |= MessageFlag::HasReplyMarkup;
|
|
}
|
|
if (!Has<HistoryMessageReplyMarkup>()) {
|
|
AddComponents(HistoryMessageReplyMarkup::Bit());
|
|
}
|
|
Get<HistoryMessageReplyMarkup>()->updateData(std::move(markup));
|
|
requestUpdate();
|
|
}
|
|
}
|
|
|
|
void HistoryItem::setCommentsInboxReadTill(MsgId readTillId) {
|
|
const auto views = Get<HistoryMessageViews>();
|
|
if (!views) {
|
|
return;
|
|
}
|
|
const auto newReadTillId = std::max(readTillId.bare, int64(1));
|
|
const auto ignore = (newReadTillId < views->commentsInboxReadTillId);
|
|
if (ignore) {
|
|
return;
|
|
}
|
|
const auto changed = (newReadTillId > views->commentsInboxReadTillId);
|
|
if (!changed) {
|
|
return;
|
|
}
|
|
const auto wasUnread = areCommentsUnread();
|
|
views->commentsInboxReadTillId = newReadTillId;
|
|
if (wasUnread && !areCommentsUnread()) {
|
|
_history->owner().requestItemRepaint(this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::setCommentsMaxId(MsgId maxId) {
|
|
if (const auto views = Get<HistoryMessageViews>()) {
|
|
if (views->commentsMaxId != maxId) {
|
|
const auto wasUnread = areCommentsUnread();
|
|
views->commentsMaxId = maxId;
|
|
if (wasUnread != areCommentsUnread()) {
|
|
_history->owner().requestItemRepaint(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::setCommentsPossibleMaxId(MsgId possibleMaxId) {
|
|
if (const auto views = Get<HistoryMessageViews>()) {
|
|
if (views->commentsMaxId < possibleMaxId) {
|
|
const auto wasUnread = areCommentsUnread();
|
|
views->commentsMaxId = possibleMaxId;
|
|
if (!wasUnread && areCommentsUnread()) {
|
|
_history->owner().requestItemRepaint(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::areCommentsUnread() const {
|
|
const auto views = Get<HistoryMessageViews>();
|
|
if (!views
|
|
|| !views->commentsMegagroupId
|
|
|| !checkCommentsLinkedChat(views->commentsMegagroupId)) {
|
|
return false;
|
|
}
|
|
const auto till = views->commentsInboxReadTillId;
|
|
if (views->commentsInboxReadTillId < 2 || views->commentsMaxId <= till) {
|
|
return false;
|
|
}
|
|
const auto group = views->commentsMegagroupId
|
|
? _history->owner().historyLoaded(
|
|
peerFromChannel(views->commentsMegagroupId))
|
|
: _history.get();
|
|
return !group || (views->commentsMaxId > group->inboxReadTillId());
|
|
}
|
|
|
|
FullMsgId HistoryItem::commentsItemId() const {
|
|
if (const auto views = Get<HistoryMessageViews>()) {
|
|
return FullMsgId(
|
|
PeerId(views->commentsMegagroupId),
|
|
views->commentsRootId);
|
|
}
|
|
return FullMsgId();
|
|
}
|
|
|
|
void HistoryItem::setCommentsItemId(FullMsgId id) {
|
|
if (id.peer == _history->peer->id) {
|
|
if (id.msg != this->id) {
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
reply->setTopMessageId(id.msg);
|
|
}
|
|
}
|
|
} else if (const auto views = Get<HistoryMessageViews>()) {
|
|
if (const auto channelId = peerToChannel(id.peer)) {
|
|
if (views->commentsMegagroupId != channelId) {
|
|
views->commentsMegagroupId = channelId;
|
|
_history->owner().requestItemResize(this);
|
|
}
|
|
views->commentsRootId = id.msg;
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::setServiceText(PreparedServiceText &&prepared) {
|
|
AddComponents(HistoryServiceData::Bit());
|
|
_flags &= ~MessageFlag::HasTextLinks;
|
|
const auto data = Get<HistoryServiceData>();
|
|
const auto had = !_text.empty();
|
|
_text = std::move(prepared.text);
|
|
data->textLinks = std::move(prepared.links);
|
|
if (had) {
|
|
_history->owner().requestItemTextRefresh(this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::updateServiceText(PreparedServiceText &&text) {
|
|
setServiceText(std::move(text));
|
|
_history->owner().requestItemResize(this);
|
|
invalidateChatListEntry();
|
|
_history->owner().updateDependentMessages(this);
|
|
}
|
|
|
|
void HistoryItem::updateStoryMentionText() {
|
|
setServiceText(prepareStoryMentionText());
|
|
}
|
|
|
|
HistoryMessageReplyMarkup *HistoryItem::inlineReplyMarkup() {
|
|
if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
|
|
if (markup->data.flags & ReplyMarkupFlag::Inline) {
|
|
return markup;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
ReplyKeyboard *HistoryItem::inlineReplyKeyboard() {
|
|
if (const auto markup = inlineReplyMarkup()) {
|
|
return markup->inlineKeyboard.get();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
ChannelData *HistoryItem::discussionPostOriginalSender() const {
|
|
if (!_history->peer->isMegagroup()) {
|
|
return nullptr;
|
|
}
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
const auto from = forwarded->savedFromPeer;
|
|
if (const auto result = from ? from->asChannel() : nullptr) {
|
|
return result;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool HistoryItem::isDiscussionPost() const {
|
|
return (discussionPostOriginalSender() != nullptr);
|
|
}
|
|
|
|
HistoryItem *HistoryItem::lookupDiscussionPostOriginal() const {
|
|
if (!_history->peer->isMegagroup()) {
|
|
return nullptr;
|
|
}
|
|
const auto forwarded = Get<HistoryMessageForwarded>();
|
|
if (!forwarded
|
|
|| !forwarded->savedFromPeer
|
|
|| !forwarded->savedFromMsgId) {
|
|
return nullptr;
|
|
}
|
|
return _history->owner().message(
|
|
forwarded->savedFromPeer->id,
|
|
forwarded->savedFromMsgId);
|
|
}
|
|
|
|
PeerData *HistoryItem::computeDisplayFrom() const {
|
|
if (const auto sender = discussionPostOriginalSender()) {
|
|
return sender;
|
|
} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
if (showForwardsFromSender(forwarded)) {
|
|
return forwarded->forwardOfForward()
|
|
? forwarded->savedFromSender
|
|
: forwarded->originalSender;
|
|
}
|
|
}
|
|
return author().get();
|
|
}
|
|
|
|
PeerData *HistoryItem::displayFrom() const {
|
|
if (!(_flags & MessageFlag::DisplayFromChecked)) {
|
|
_flags |= MessageFlag::DisplayFromChecked;
|
|
_displayFrom = computeDisplayFrom();
|
|
}
|
|
return _displayFrom;
|
|
}
|
|
|
|
uint8 HistoryItem::colorIndex() const {
|
|
if (const auto from = displayFrom()) {
|
|
return from->colorIndex();
|
|
} else if (const auto info = displayHiddenSenderInfo()) {
|
|
return info->colorIndex;
|
|
}
|
|
Unexpected("No displayFrom and no displayHiddenSenderInfo.");
|
|
}
|
|
|
|
PeerData *HistoryItem::contentColorsFrom() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->originalSender;
|
|
}
|
|
return displayFrom();
|
|
}
|
|
|
|
uint8 HistoryItem::contentColorIndex() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->originalSender
|
|
? forwarded->originalSender->colorIndex()
|
|
: forwarded->originalHiddenSenderInfo->colorIndex;
|
|
}
|
|
return colorIndex();
|
|
}
|
|
|
|
std::unique_ptr<HistoryView::Element> HistoryItem::createView(
|
|
not_null<HistoryView::ElementDelegate*> delegate,
|
|
HistoryView::Element *replacing) {
|
|
if (isService()) {
|
|
return std::make_unique<HistoryView::Service>(
|
|
delegate,
|
|
this,
|
|
replacing);
|
|
}
|
|
return std::make_unique<HistoryView::Message>(delegate, this, replacing);
|
|
}
|
|
|
|
void HistoryItem::invalidateChatListEntry() {
|
|
_history->session().changes().messageUpdated(
|
|
this,
|
|
Data::MessageUpdate::Flag::DialogRowRefresh);
|
|
_history->lastItemDialogsView().itemInvalidated(this);
|
|
if (const auto topic = this->topic()) {
|
|
topic->lastItemDialogsView().itemInvalidated(this);
|
|
}
|
|
if (const auto sublist = savedSublist()) {
|
|
sublist->lastItemDialogsView().itemInvalidated(this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::customEmojiRepaint() {
|
|
if (!(_flags & MessageFlag::CustomEmojiRepainting)) {
|
|
_flags |= MessageFlag::CustomEmojiRepainting;
|
|
_history->owner().requestItemRepaint(this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::finishEditionToEmpty() {
|
|
finishEdition(-1);
|
|
_history->itemVanished(this);
|
|
}
|
|
|
|
bool HistoryItem::hasUnreadMediaFlag() const {
|
|
if (_history->peer->isChannel()) {
|
|
const auto passed = base::unixtime::now() - date();
|
|
const auto &config = _history->session().serverConfig();
|
|
if (passed >= config.channelsReadMediaPeriod) {
|
|
return false;
|
|
}
|
|
}
|
|
return _flags & MessageFlag::MediaIsUnread;
|
|
}
|
|
|
|
bool HistoryItem::isUnreadMention() const {
|
|
return !out() && mentionsMe() && (_flags & MessageFlag::MediaIsUnread);
|
|
}
|
|
|
|
bool HistoryItem::hasUnreadReaction() const {
|
|
return (_flags & MessageFlag::HasUnreadReaction);
|
|
}
|
|
|
|
bool HistoryItem::mentionsMe() const {
|
|
if (Has<HistoryServicePinned>()
|
|
&& !Core::App().settings().notifyAboutPinned()) {
|
|
return false;
|
|
}
|
|
return _flags & MessageFlag::MentionsMe;
|
|
}
|
|
|
|
bool HistoryItem::isUnreadMedia() const {
|
|
if (!hasUnreadMediaFlag()) {
|
|
return false;
|
|
} else if (const auto media = this->media()) {
|
|
if (const auto document = media->document()) {
|
|
if (document->isVoiceMessage() || document->isVideoMessage()) {
|
|
return (media->webpage() == nullptr);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool HistoryItem::isIncomingUnreadMedia() const {
|
|
return !out() && isUnreadMedia();
|
|
}
|
|
|
|
void HistoryItem::markMediaAndMentionRead() {
|
|
_flags &= ~MessageFlag::MediaIsUnread;
|
|
|
|
if (mentionsMe()) {
|
|
_history->updateChatListEntry();
|
|
_history->unreadMentions().erase(id);
|
|
if (const auto topic = this->topic()) {
|
|
topic->updateChatListEntry();
|
|
topic->unreadMentions().erase(id);
|
|
}
|
|
}
|
|
|
|
if (const auto selfdestruct = Get<HistoryServiceSelfDestruct>()) {
|
|
if (selfdestruct->destructAt == crl::time()) {
|
|
const auto ttl = selfdestruct->timeToLive;
|
|
if (const auto maybeTime = std::get_if<crl::time>(&ttl)) {
|
|
const auto time = *maybeTime;
|
|
selfdestruct->destructAt = crl::now() + time;
|
|
_history->owner().selfDestructIn(this, time);
|
|
} else {
|
|
selfdestruct->destructAt = TimeToLiveSingleView();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::markReactionsRead() {
|
|
if (_reactions) {
|
|
_reactions->markRead();
|
|
}
|
|
_flags &= ~MessageFlag::HasUnreadReaction;
|
|
_history->updateChatListEntry();
|
|
_history->unreadReactions().erase(id);
|
|
if (const auto topic = this->topic()) {
|
|
topic->updateChatListEntry();
|
|
topic->unreadReactions().erase(id);
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::markContentsRead(bool fromThisClient) {
|
|
if (hasUnreadReaction()) {
|
|
if (fromThisClient) {
|
|
_history->owner().requestUnreadReactionsAnimation(this);
|
|
}
|
|
markReactionsRead();
|
|
return true;
|
|
} else if (isUnreadMention() || isIncomingUnreadMedia()) {
|
|
markMediaAndMentionRead();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void HistoryItem::setIsPinned(bool pinned) {
|
|
const auto changed = (isPinned() != pinned);
|
|
if (pinned) {
|
|
_flags |= MessageFlag::Pinned;
|
|
auto &storage = _history->session().storage();
|
|
storage.add(Storage::SharedMediaAddExisting(
|
|
_history->peer->id,
|
|
MsgId(0), // topicRootId
|
|
Storage::SharedMediaType::Pinned,
|
|
id,
|
|
{ id, id }));
|
|
_history->setHasPinnedMessages(true);
|
|
if (const auto topic = this->topic()) {
|
|
storage.add(Storage::SharedMediaAddExisting(
|
|
_history->peer->id,
|
|
topic->rootId(),
|
|
Storage::SharedMediaType::Pinned,
|
|
id,
|
|
{ id, id }));
|
|
topic->setHasPinnedMessages(true);
|
|
}
|
|
} else {
|
|
_flags &= ~MessageFlag::Pinned;
|
|
_history->session().storage().remove(Storage::SharedMediaRemoveOne(
|
|
_history->peer->id,
|
|
Storage::SharedMediaType::Pinned,
|
|
id));
|
|
}
|
|
if (changed) {
|
|
_history->owner().notifyItemDataChange(this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::returnSavedMedia() {
|
|
if (!isEditingMedia()) {
|
|
return;
|
|
}
|
|
const auto wasGrouped = history()->owner().groups().isGrouped(this);
|
|
const auto data = Get<HistoryMessageSavedMediaData>();
|
|
_media = std::move(data->media);
|
|
setText(data->text);
|
|
clearSavedMedia();
|
|
if (wasGrouped) {
|
|
history()->owner().groups().refreshMessage(this, true);
|
|
} else {
|
|
history()->owner().requestItemViewRefresh(this);
|
|
history()->owner().updateDependentMessages(this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::savePreviousMedia() {
|
|
Expects(_media != nullptr);
|
|
|
|
AddComponents(HistoryMessageSavedMediaData::Bit());
|
|
const auto data = Get<HistoryMessageSavedMediaData>();
|
|
data->text = originalText();
|
|
data->media = _media->clone(this);
|
|
}
|
|
|
|
bool HistoryItem::isEditingMedia() const {
|
|
return Has<HistoryMessageSavedMediaData>();
|
|
}
|
|
|
|
void HistoryItem::clearSavedMedia() {
|
|
RemoveComponents(HistoryMessageSavedMediaData::Bit());
|
|
}
|
|
|
|
bool HistoryItem::definesReplyKeyboard() const {
|
|
if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
|
|
if (markup->data.flags & ReplyMarkupFlag::Inline) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// optimization: don't create markup component for the case
|
|
// MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag
|
|
return (_flags & MessageFlag::HasReplyMarkup);
|
|
}
|
|
|
|
ReplyMarkupFlags HistoryItem::replyKeyboardFlags() const {
|
|
Expects(definesReplyKeyboard());
|
|
|
|
if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
|
|
return markup->data.flags;
|
|
}
|
|
|
|
// optimization: don't create markup component for the case
|
|
// MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag
|
|
return ReplyMarkupFlag::None;
|
|
}
|
|
|
|
void HistoryItem::addLogEntryOriginal(
|
|
WebPageId localId,
|
|
const QString &label,
|
|
const TextWithEntities &content) {
|
|
Expects(isAdminLogEntry());
|
|
|
|
AddComponents(HistoryMessageLogEntryOriginal::Bit());
|
|
Get<HistoryMessageLogEntryOriginal>()->page = _history->owner().webpage(
|
|
localId,
|
|
label,
|
|
content);
|
|
}
|
|
|
|
PeerData *HistoryItem::specialNotificationPeer() const {
|
|
return (mentionsMe() && !_history->peer->isUser())
|
|
? from().get()
|
|
: nullptr;
|
|
}
|
|
|
|
UserData *HistoryItem::viaBot() const {
|
|
if (const auto via = Get<HistoryMessageVia>()) {
|
|
return via->bot;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
UserData *HistoryItem::getMessageBot() const {
|
|
if (const auto bot = viaBot()) {
|
|
return bot;
|
|
}
|
|
auto bot = from()->asUser();
|
|
if (!bot) {
|
|
bot = _history->peer->asUser();
|
|
}
|
|
return (bot && bot->isBot()) ? bot : nullptr;
|
|
}
|
|
|
|
bool HistoryItem::isHistoryEntry() const {
|
|
return (_flags & MessageFlag::HistoryEntry);
|
|
}
|
|
|
|
bool HistoryItem::isAdminLogEntry() const {
|
|
return (_flags & MessageFlag::AdminLogEntry);
|
|
}
|
|
|
|
bool HistoryItem::isFromScheduled() const {
|
|
return isHistoryEntry()
|
|
&& (_flags & MessageFlag::IsOrWasScheduled);
|
|
}
|
|
|
|
bool HistoryItem::isScheduled() const {
|
|
return !isHistoryEntry()
|
|
&& !isAdminLogEntry()
|
|
&& (_flags & MessageFlag::IsOrWasScheduled);
|
|
}
|
|
|
|
bool HistoryItem::isSponsored() const {
|
|
return _flags & MessageFlag::Sponsored;
|
|
}
|
|
|
|
bool HistoryItem::skipNotification() const {
|
|
if (isSilent() && (_flags & MessageFlag::IsContactSignUp)) {
|
|
return true;
|
|
} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
if (forwarded->imported) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool HistoryItem::isUserpicSuggestion() const {
|
|
return (_flags & MessageFlag::IsUserpicSuggestion);
|
|
}
|
|
|
|
void HistoryItem::destroy() {
|
|
_history->destroyMessage(this);
|
|
}
|
|
|
|
not_null<Data::Thread*> HistoryItem::notificationThread() const {
|
|
if (const auto rootId = topicRootId()) {
|
|
if (const auto forum = _history->asForum()) {
|
|
return forum->enforceTopicFor(rootId);
|
|
}
|
|
}
|
|
return _history;
|
|
}
|
|
|
|
Data::ForumTopic *HistoryItem::topic() const {
|
|
if (const auto rootId = topicRootId()) {
|
|
if (const auto forum = _history->asForum()) {
|
|
return forum->topicFor(rootId);
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void HistoryItem::refreshMainView() {
|
|
if (const auto view = mainView()) {
|
|
_history->owner().notifyHistoryChangeDelayed(_history);
|
|
view->refreshInBlock();
|
|
}
|
|
}
|
|
|
|
void HistoryItem::removeMainView() {
|
|
if (const auto view = mainView()) {
|
|
_history->owner().notifyHistoryChangeDelayed(_history);
|
|
view->removeFromBlock();
|
|
}
|
|
}
|
|
|
|
void HistoryItem::clearMainView() {
|
|
_mainView = nullptr;
|
|
}
|
|
|
|
void HistoryItem::applyEdition(HistoryMessageEdition &&edition) {
|
|
int keyboardTop = -1;
|
|
//if (!pendingResize()) {// #TODO edit bot message
|
|
// if (auto keyboard = inlineReplyKeyboard()) {
|
|
// int h = st::msgBotKbButton.margin + keyboard->naturalHeight();
|
|
// keyboardTop = _height - h + st::msgBotKbButton.margin - marginBottom();
|
|
// }
|
|
//}
|
|
|
|
const auto editingMedia = isEditingMedia();
|
|
const auto updatingSavedLocalEdit = !edition.savePreviousMedia
|
|
&& editingMedia;
|
|
if (!editingMedia && edition.savePreviousMedia) {
|
|
savePreviousMedia();
|
|
}
|
|
Assert(!updatingSavedLocalEdit || !isLocalUpdateMedia());
|
|
|
|
if (edition.isEditHide) {
|
|
_flags |= MessageFlag::HideEdited;
|
|
} else {
|
|
_flags &= ~MessageFlag::HideEdited;
|
|
}
|
|
if (edition.invertMedia) {
|
|
_flags |= MessageFlag::InvertMedia;
|
|
} else {
|
|
_flags &= ~MessageFlag::InvertMedia;
|
|
}
|
|
|
|
if (edition.editDate != -1) {
|
|
//_flags |= MTPDmessage::Flag::f_edit_date;
|
|
if (!Has<HistoryMessageEdited>()) {
|
|
AddComponents(HistoryMessageEdited::Bit());
|
|
}
|
|
auto edited = Get<HistoryMessageEdited>();
|
|
edited->date = edition.editDate;
|
|
}
|
|
|
|
if (!edition.useSameMarkup) {
|
|
setReplyMarkup(base::take(edition.replyMarkup));
|
|
}
|
|
if (updatingSavedLocalEdit) {
|
|
Get<HistoryMessageSavedMediaData>()->media = edition.mtpMedia
|
|
? CreateMedia(this, *edition.mtpMedia)
|
|
: nullptr;
|
|
} else {
|
|
removeFromSharedMediaIndex();
|
|
refreshMedia(edition.mtpMedia);
|
|
}
|
|
if (!edition.useSameReactions) {
|
|
updateReactions(edition.mtpReactions);
|
|
}
|
|
if (!edition.useSameViews) {
|
|
changeViewsCount(edition.views);
|
|
}
|
|
if (!edition.useSameForwards) {
|
|
setForwardsCount(edition.forwards);
|
|
}
|
|
const auto &checkedMedia = updatingSavedLocalEdit
|
|
? Get<HistoryMessageSavedMediaData>()->media
|
|
: _media;
|
|
auto updatedText = checkedMedia
|
|
? edition.textWithEntities
|
|
: EnsureNonEmpty(edition.textWithEntities);
|
|
if (updatingSavedLocalEdit) {
|
|
Get<HistoryMessageSavedMediaData>()->text = std::move(updatedText);
|
|
} else {
|
|
setText(std::move(updatedText));
|
|
addToSharedMediaIndex();
|
|
}
|
|
if (!edition.useSameReplies) {
|
|
if (!edition.replies.isNull) {
|
|
if (checkRepliesPts(edition.replies)) {
|
|
setReplies(base::take(edition.replies));
|
|
}
|
|
} else {
|
|
clearReplies();
|
|
}
|
|
}
|
|
|
|
applyTTL(edition.ttl);
|
|
|
|
finishEdition(keyboardTop);
|
|
}
|
|
|
|
void HistoryItem::applyChanges(not_null<Data::Story*> story) {
|
|
Expects(_flags & MessageFlag::StoryItem);
|
|
Expects(StoryIdFromMsgId(id) == story->id());
|
|
|
|
_media = nullptr;
|
|
setStoryFields(story);
|
|
|
|
finishEdition(-1);
|
|
}
|
|
|
|
void HistoryItem::setStoryFields(not_null<Data::Story*> story) {
|
|
const auto spoiler = false;
|
|
if (const auto photo = story->photo()) {
|
|
_media = std::make_unique<Data::MediaPhoto>(this, photo, spoiler);
|
|
} else if (const auto document = story->document()) {
|
|
_media = std::make_unique<Data::MediaFile>(
|
|
this,
|
|
document,
|
|
/*skipPremiumEffect=*/false,
|
|
spoiler,
|
|
/*ttlSeconds = */0);
|
|
}
|
|
setText(story->caption());
|
|
}
|
|
|
|
void HistoryItem::applyEdition(const MTPDmessageService &message) {
|
|
if (message.vaction().type() == mtpc_messageActionHistoryClear) {
|
|
const auto wasGrouped = history()->owner().groups().isGrouped(this);
|
|
setReplyMarkup({});
|
|
removeFromSharedMediaIndex();
|
|
refreshMedia(nullptr);
|
|
setTextValue({});
|
|
changeViewsCount(-1);
|
|
setForwardsCount(-1);
|
|
if (wasGrouped) {
|
|
history()->owner().groups().unregisterMessage(this);
|
|
}
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
reply->clearData(this);
|
|
}
|
|
clearDependencyMessage();
|
|
UpdateComponents(0);
|
|
createServiceFromMtp(message);
|
|
applyServiceDateEdition(message);
|
|
finishEditionToEmpty();
|
|
_flags &= ~MessageFlag::DisplayFromChecked;
|
|
} else if (isService()) {
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
reply->clearData(this);
|
|
}
|
|
clearDependencyMessage();
|
|
UpdateComponents(0);
|
|
createServiceFromMtp(message);
|
|
applyServiceDateEdition(message);
|
|
finishEdition(-1);
|
|
_flags &= ~MessageFlag::DisplayFromChecked;
|
|
}
|
|
}
|
|
|
|
void HistoryItem::applyEdition(const MTPMessageExtendedMedia &media) {
|
|
if (const auto existing = this->media()) {
|
|
if (existing->updateExtendedMedia(this, media)) {
|
|
checkBuyButton();
|
|
finishEdition(-1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::applySentMessage(const MTPDmessage &data) {
|
|
updateSentContent({
|
|
qs(data.vmessage()),
|
|
Api::EntitiesFromMTP(
|
|
&_history->session(),
|
|
data.ventities().value_or_empty())
|
|
}, data.vmedia());
|
|
updateReplyMarkup(HistoryMessageMarkupData(data.vreply_markup()));
|
|
updateForwardedInfo(data.vfwd_from());
|
|
changeViewsCount(data.vviews().value_or(-1));
|
|
if (const auto replies = data.vreplies()) {
|
|
setReplies(HistoryMessageRepliesData(replies));
|
|
} else {
|
|
clearReplies();
|
|
}
|
|
setForwardsCount(data.vforwards().value_or(-1));
|
|
if (const auto reply = data.vreply_to()) {
|
|
reply->match([&](const MTPDmessageReplyHeader &data) {
|
|
const auto replyToPeer = data.vreply_to_peer_id()
|
|
? peerFromMTP(*data.vreply_to_peer_id())
|
|
: PeerId();
|
|
if (!replyToPeer || replyToPeer == history()->peer->id) {
|
|
if (const auto replyToId = data.vreply_to_msg_id()) {
|
|
setReplyFields(
|
|
replyToId->v,
|
|
data.vreply_to_top_id().value_or(replyToId->v),
|
|
data.is_forum_topic());
|
|
}
|
|
}
|
|
}, [](const MTPDmessageReplyStoryHeader &data) {
|
|
});
|
|
}
|
|
setPostAuthor(data.vpost_author().value_or_empty());
|
|
setIsPinned(data.is_pinned());
|
|
contributeToSlowmode(data.vdate().v);
|
|
addToSharedMediaIndex();
|
|
invalidateChatListEntry();
|
|
if (const auto period = data.vttl_period(); period && period->v > 0) {
|
|
applyTTL(data.vdate().v + period->v);
|
|
} else {
|
|
applyTTL(0);
|
|
}
|
|
_history->owner().notifyItemDataChange(this);
|
|
_history->owner().requestItemTextRefresh(this);
|
|
_history->owner().updateDependentMessages(this);
|
|
}
|
|
|
|
void HistoryItem::applySentMessage(
|
|
const QString &text,
|
|
const MTPDupdateShortSentMessage &data,
|
|
bool wasAlready) {
|
|
updateSentContent({
|
|
text,
|
|
Api::EntitiesFromMTP(
|
|
&_history->session(),
|
|
data.ventities().value_or_empty())
|
|
}, data.vmedia());
|
|
contributeToSlowmode(data.vdate().v);
|
|
if (!wasAlready) {
|
|
addToSharedMediaIndex();
|
|
}
|
|
invalidateChatListEntry();
|
|
if (const auto period = data.vttl_period(); period && period->v > 0) {
|
|
applyTTL(data.vdate().v + period->v);
|
|
} else {
|
|
applyTTL(0);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::updateSentContent(
|
|
const TextWithEntities &textWithEntities,
|
|
const MTPMessageMedia *media) {
|
|
if (isEditingMedia()) {
|
|
return;
|
|
}
|
|
setText(textWithEntities);
|
|
if (_flags & MessageFlag::FromInlineBot) {
|
|
if (!media || !_media || !_media->updateInlineResultMedia(*media)) {
|
|
refreshSentMedia(media);
|
|
}
|
|
_flags &= ~MessageFlag::FromInlineBot;
|
|
} else if (media || _media) {
|
|
if (!media || !_media || !_media->updateSentMedia(*media)) {
|
|
refreshSentMedia(media);
|
|
}
|
|
}
|
|
history()->owner().requestItemResize(this);
|
|
}
|
|
|
|
void HistoryItem::updateForwardedInfo(const MTPMessageFwdHeader *fwd) {
|
|
const auto forwarded = Get<HistoryMessageForwarded>();
|
|
if (!fwd) {
|
|
if (forwarded) {
|
|
LOG(("API Error: Server removed forwarded information."));
|
|
}
|
|
return;
|
|
} else if (!forwarded) {
|
|
LOG(("API Error: Server added forwarded information."));
|
|
return;
|
|
}
|
|
fwd->match([&](const MTPDmessageFwdHeader &data) {
|
|
auto config = CreateConfig();
|
|
FillForwardedInfo(config, data);
|
|
setupForwardedComponent(config);
|
|
history()->owner().requestItemResize(this);
|
|
});
|
|
}
|
|
|
|
void HistoryItem::applyEditionToHistoryCleared() {
|
|
applyEdition(
|
|
MTP_messageService(
|
|
MTP_flags(0),
|
|
MTP_int(id),
|
|
peerToMTP(PeerId(0)), // from_id
|
|
peerToMTP(_history->peer->id),
|
|
MTPMessageReplyHeader(),
|
|
MTP_int(date()),
|
|
MTP_messageActionHistoryClear(),
|
|
MTPint() // ttl_period
|
|
).c_messageService());
|
|
}
|
|
|
|
void HistoryItem::updateReplyMarkup(HistoryMessageMarkupData &&markup) {
|
|
setReplyMarkup(std::move(markup));
|
|
}
|
|
|
|
void HistoryItem::contributeToSlowmode(TimeId realDate) {
|
|
if (const auto channel = history()->peer->asChannel()) {
|
|
if (out() && isRegular() && !isService()) {
|
|
channel->growSlowmodeLastMessage(realDate ? realDate : date());
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::addToUnreadThings(HistoryUnreadThings::AddType type) {
|
|
if (!isRegular()) {
|
|
return;
|
|
}
|
|
const auto mention = isUnreadMention();
|
|
const auto reaction = hasUnreadReaction();
|
|
if (!mention && !reaction) {
|
|
return;
|
|
}
|
|
const auto topic = this->topic();
|
|
const auto history = this->history();
|
|
const auto changes = &history->session().changes();
|
|
if (mention) {
|
|
if (history->unreadMentions().add(id, type)) {
|
|
changes->historyUpdated(
|
|
history,
|
|
Data::HistoryUpdate::Flag::UnreadMentions);
|
|
}
|
|
if (topic && topic->unreadMentions().add(id, type)) {
|
|
changes->topicUpdated(
|
|
topic,
|
|
Data::TopicUpdate::Flag::UnreadMentions);
|
|
}
|
|
}
|
|
if (reaction) {
|
|
const auto toHistory = history->unreadReactions().add(id, type);
|
|
const auto toTopic = topic && topic->unreadReactions().add(id, type);
|
|
if (toHistory || toTopic) {
|
|
if (type == HistoryUnreadThings::AddType::New) {
|
|
changes->messageUpdated(
|
|
this,
|
|
Data::MessageUpdate::Flag::NewUnreadReaction);
|
|
}
|
|
if (hasUnreadReaction()) {
|
|
if (toHistory) {
|
|
changes->historyUpdated(
|
|
history,
|
|
Data::HistoryUpdate::Flag::UnreadReactions);
|
|
}
|
|
if (toTopic) {
|
|
changes->topicUpdated(
|
|
topic,
|
|
Data::TopicUpdate::Flag::UnreadReactions);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::destroyHistoryEntry() {
|
|
if (isUnreadMention()) {
|
|
history()->unreadMentions().erase(id);
|
|
if (const auto topic = this->topic()) {
|
|
topic->unreadMentions().erase(id);
|
|
}
|
|
}
|
|
if (hasUnreadReaction()) {
|
|
history()->unreadReactions().erase(id);
|
|
if (const auto topic = this->topic()) {
|
|
topic->unreadReactions().erase(id);
|
|
}
|
|
}
|
|
if (isRegular() && _history->peer->isMegagroup()) {
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
changeReplyToTopCounter(reply, -1);
|
|
}
|
|
}
|
|
}
|
|
|
|
Storage::SharedMediaTypesMask HistoryItem::sharedMediaTypes() const {
|
|
auto result = Storage::SharedMediaTypesMask{};
|
|
const auto saved = Get<HistoryMessageSavedMediaData>();
|
|
const auto media = saved ? saved->media.get() : _media.get();
|
|
if (media) {
|
|
result.set(media->sharedMediaTypes());
|
|
}
|
|
if (hasTextLinks()) {
|
|
result.set(Storage::SharedMediaType::Link);
|
|
}
|
|
if (isPinned()) {
|
|
result.set(Storage::SharedMediaType::Pinned);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void HistoryItem::indexAsNewItem() {
|
|
if (isRegular()) {
|
|
addToUnreadThings(HistoryUnreadThings::AddType::New);
|
|
}
|
|
addToSharedMediaIndex();
|
|
}
|
|
|
|
void HistoryItem::addToSharedMediaIndex() {
|
|
if (isRegular()) {
|
|
if (const auto types = sharedMediaTypes()) {
|
|
_history->session().storage().add(Storage::SharedMediaAddNew(
|
|
_history->peer->id,
|
|
topicRootId(),
|
|
types,
|
|
id));
|
|
if (types.test(Storage::SharedMediaType::Pinned)) {
|
|
_history->setHasPinnedMessages(true);
|
|
if (const auto topic = this->topic()) {
|
|
topic->setHasPinnedMessages(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::removeFromSharedMediaIndex() {
|
|
if (isRegular()) {
|
|
if (const auto types = sharedMediaTypes()) {
|
|
_history->session().storage().remove(
|
|
Storage::SharedMediaRemoveOne(
|
|
_history->peer->id,
|
|
types,
|
|
id));
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::incrementReplyToTopCounter() {
|
|
if (isRegular() && _history->peer->isMegagroup()) {
|
|
_history->session().changes().messageUpdated(
|
|
this,
|
|
Data::MessageUpdate::Flag::ReplyToTopAdded);
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
changeReplyToTopCounter(reply, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::changeReplyToTopCounter(
|
|
not_null<HistoryMessageReply*> reply,
|
|
int delta) {
|
|
const auto topId = reply->topMessageId();
|
|
if (!topId) {
|
|
return;
|
|
}
|
|
const auto top = _history->owner().message(_history->peer->id, topId);
|
|
if (!top) {
|
|
return;
|
|
}
|
|
const auto from = displayFrom();
|
|
const auto replier = from ? from->id : PeerId();
|
|
top->changeRepliesCount(delta, replier);
|
|
if (const auto original = top->lookupDiscussionPostOriginal()) {
|
|
original->changeRepliesCount(delta, replier);
|
|
}
|
|
}
|
|
|
|
QString HistoryItem::notificationHeader() const {
|
|
if (isService()) {
|
|
return QString();
|
|
} else if (out() && isFromScheduled() && !_history->peer->isSelf()) {
|
|
return tr::lng_from_you(tr::now);
|
|
} else if (!_history->peer->isUser() && !isPost()) {
|
|
return from()->name();
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
void HistoryItem::setRealId(MsgId newId) {
|
|
Expects(_flags & MessageFlag::BeingSent);
|
|
Expects(IsClientMsgId(id));
|
|
|
|
const auto oldId = std::exchange(id, newId);
|
|
_flags &= ~(MessageFlag::BeingSent | MessageFlag::Local);
|
|
if (isRegular()) {
|
|
_history->unregisterClientSideMessage(this);
|
|
}
|
|
_history->owner().notifyItemIdChange({ fullId(), oldId });
|
|
|
|
// We don't fire MessageUpdate::Flag::ReplyMarkup and update keyboard
|
|
// in history widget, because it can't exist for an outgoing message.
|
|
// Only inline keyboards can be in outgoing messages.
|
|
if (const auto markup = inlineReplyMarkup()) {
|
|
if (markup->inlineKeyboard) {
|
|
markup->inlineKeyboard->updateMessageId();
|
|
}
|
|
}
|
|
|
|
_history->owner().notifyItemDataChange(this);
|
|
_history->owner().groups().refreshMessage(this);
|
|
_history->owner().requestItemResize(this);
|
|
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
incrementReplyToTopCounter();
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::canPin() const {
|
|
if (!isRegular() || isService()) {
|
|
return false;
|
|
} else if (const auto m = media(); m && m->call()) {
|
|
return false;
|
|
}
|
|
return _history->peer->canPinMessages();
|
|
}
|
|
|
|
bool HistoryItem::allowsSendNow() const {
|
|
return !isService()
|
|
&& isScheduled()
|
|
&& !isSending()
|
|
&& !hasFailed()
|
|
&& !isEditingMedia();
|
|
}
|
|
|
|
bool HistoryItem::allowsForward() const {
|
|
return !isService()
|
|
&& isRegular()
|
|
&& !forbidsForward()
|
|
&& history()->peer->allowsForwarding()
|
|
&& (!_media || _media->allowsForward());
|
|
}
|
|
|
|
bool HistoryItem::isTooOldForEdit(TimeId now) const {
|
|
return !_history->peer->canEditMessagesIndefinitely()
|
|
&& !isScheduled()
|
|
&& (now - date() >= _history->session().serverConfig().editTimeLimit);
|
|
}
|
|
|
|
bool HistoryItem::allowsEdit(TimeId now) const {
|
|
return !isService()
|
|
&& canBeEdited()
|
|
&& !isTooOldForEdit(now)
|
|
&& (!_media || _media->allowsEdit())
|
|
&& !isLegacyMessage()
|
|
&& !isEditingMedia();
|
|
}
|
|
|
|
bool HistoryItem::canBeEdited() const {
|
|
if ((!isRegular() && !isScheduled())
|
|
|| Has<HistoryMessageVia>()
|
|
|| Has<HistoryMessageForwarded>()) {
|
|
return false;
|
|
}
|
|
|
|
const auto peer = _history->peer;
|
|
if (peer->isSelf()) {
|
|
return true;
|
|
} else if (const auto channel = peer->asChannel()) {
|
|
if (isPost() && channel->canEditMessages()) {
|
|
return true;
|
|
} else if (out()) {
|
|
if (isPost()) {
|
|
return channel->canPostMessages();
|
|
} else if (const auto topic = this->topic()) {
|
|
return Data::CanSendAnything(topic);
|
|
} else {
|
|
return Data::CanSendAnything(channel);
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return out();
|
|
}
|
|
|
|
bool HistoryItem::canStopPoll() const {
|
|
return canBeEdited() && isRegular();
|
|
}
|
|
|
|
bool HistoryItem::forbidsForward() const {
|
|
return (_flags & MessageFlag::NoForwards);
|
|
}
|
|
|
|
bool HistoryItem::forbidsSaving() const {
|
|
if (forbidsForward()) {
|
|
return true;
|
|
} else if (const auto invoice = _media ? _media->invoice() : nullptr) {
|
|
return (invoice->extendedMedia != nullptr);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool HistoryItem::canDelete() const {
|
|
if (isSponsored()) {
|
|
return false;
|
|
} else if (IsStoryMsgId(id)) {
|
|
return false;
|
|
} else if (isService() && !isRegular()) {
|
|
return false;
|
|
} else if (topicRootId() == id) {
|
|
return false;
|
|
} else if (!isHistoryEntry() && !isScheduled()) {
|
|
return false;
|
|
}
|
|
auto channel = _history->peer->asChannel();
|
|
if (!channel) {
|
|
return !isGroupMigrate();
|
|
}
|
|
|
|
if (id == 1) {
|
|
return false;
|
|
}
|
|
if (channel->canDeleteMessages()) {
|
|
return true;
|
|
} else if (out() && !isService()) {
|
|
return isPost() ? channel->canPostMessages() : true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool HistoryItem::canDeleteForEveryone(TimeId now) const {
|
|
const auto peer = _history->peer;
|
|
const auto &config = _history->session().serverConfig();
|
|
const auto messageToMyself = peer->isSelf();
|
|
const auto messageTooOld = messageToMyself
|
|
? false
|
|
: peer->isUser()
|
|
? (now - date() >= config.revokePrivateTimeLimit)
|
|
: (now - date() >= config.revokeTimeLimit);
|
|
if (!isRegular() || messageToMyself || messageTooOld || isPost()) {
|
|
return false;
|
|
}
|
|
if (peer->isChannel()) {
|
|
return false;
|
|
} else if (const auto user = peer->asUser()) {
|
|
// Bots receive all messages and there is no sense in revoking them.
|
|
// See https://github.com/telegramdesktop/tdesktop/issues/3818
|
|
if (user->isBot() && !user->isSupport()) {
|
|
return false;
|
|
}
|
|
}
|
|
if (const auto media = this->media()) {
|
|
if (!media->allowsRevoke(now)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (!out()) {
|
|
if (const auto chat = peer->asChat()) {
|
|
if (!chat->canDeleteMessages()) {
|
|
return false;
|
|
}
|
|
} else if (peer->isUser()) {
|
|
return config.revokePrivateInbox;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool HistoryItem::suggestReport() const {
|
|
if (out() || isService() || !isRegular()) {
|
|
return false;
|
|
} else if (const auto channel = _history->peer->asChannel()) {
|
|
return true;
|
|
} else if (const auto user = _history->peer->asUser()) {
|
|
return user->isBot();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool HistoryItem::suggestBanReport() const {
|
|
const auto channel = _history->peer->asChannel();
|
|
if (!channel || !channel->canRestrictParticipant(from())) {
|
|
return false;
|
|
}
|
|
return !isPost() && !out();
|
|
}
|
|
|
|
bool HistoryItem::suggestDeleteAllReport() const {
|
|
auto channel = _history->peer->asChannel();
|
|
if (!channel || !channel->canDeleteMessages()) {
|
|
return false;
|
|
}
|
|
return !isPost() && !out();
|
|
}
|
|
|
|
ChatRestriction HistoryItem::requiredSendRight() const {
|
|
const auto media = this->media();
|
|
if (media && media->game()) {
|
|
return ChatRestriction::SendGames;
|
|
}
|
|
const auto photo = (media && !media->webpage())
|
|
? media->photo()
|
|
: nullptr;
|
|
const auto document = (media && !media->webpage())
|
|
? media->document()
|
|
: nullptr;
|
|
if (photo) {
|
|
return ChatRestriction::SendPhotos;
|
|
} else if (document) {
|
|
return document->requiredSendRight();
|
|
} else if (media && media->poll()) {
|
|
return ChatRestriction::SendPolls;
|
|
}
|
|
return ChatRestriction::SendOther;
|
|
}
|
|
|
|
bool HistoryItem::requiresSendInlineRight() const {
|
|
return Has<HistoryMessageVia>();
|
|
}
|
|
|
|
std::optional<QString> HistoryItem::errorTextForForward(
|
|
not_null<Data::Thread*> to) const {
|
|
const auto requiredRight = requiredSendRight();
|
|
const auto requiresInline = requiresSendInlineRight();
|
|
const auto peer = to->peer();
|
|
constexpr auto kInline = ChatRestriction::SendInline;
|
|
if (const auto error = Data::RestrictionError(peer, requiredRight)) {
|
|
return *error;
|
|
} else if (requiresInline && !Data::CanSend(to, kInline)) {
|
|
return Data::RestrictionError(peer, kInline).value_or(
|
|
tr::lng_forward_cant(tr::now));
|
|
} else if (_media
|
|
&& _media->poll()
|
|
&& _media->poll()->publicVotes()
|
|
&& peer->isBroadcast()) {
|
|
return tr::lng_restricted_send_public_polls(tr::now);
|
|
} else if (!Data::CanSend(to, requiredRight, false)) {
|
|
return tr::lng_forward_cant(tr::now);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
const HistoryMessageTranslation *HistoryItem::translation() const {
|
|
return Get<HistoryMessageTranslation>();
|
|
}
|
|
|
|
bool HistoryItem::translationShowRequiresCheck(LanguageId to) const {
|
|
// Check if a call to translationShowRequiresRequest(to) is not a no-op.
|
|
if (!to) {
|
|
if (const auto translation = Get<HistoryMessageTranslation>()) {
|
|
return (!translation->failed && translation->text.empty())
|
|
|| translation->used;
|
|
}
|
|
return false;
|
|
} else if (const auto translation = Get<HistoryMessageTranslation>()) {
|
|
if (translation->to == to) {
|
|
return !translation->used && !translation->text.empty();
|
|
}
|
|
return true;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::translationShowRequiresRequest(LanguageId to) {
|
|
// When changing be sure to reflect in translationShowRequiresCheck(to).
|
|
if (!to) {
|
|
if (const auto translation = Get<HistoryMessageTranslation>()) {
|
|
if (!translation->failed && translation->text.empty()) {
|
|
Assert(!translation->used);
|
|
RemoveComponents(HistoryMessageTranslation::Bit());
|
|
} else {
|
|
translationToggle(translation, false);
|
|
}
|
|
}
|
|
return false;
|
|
} else if (const auto translation = Get<HistoryMessageTranslation>()) {
|
|
if (translation->to == to) {
|
|
translationToggle(translation, true);
|
|
return false;
|
|
}
|
|
translationToggle(translation, false);
|
|
translation->to = to;
|
|
translation->requested = true;
|
|
translation->failed = false;
|
|
translation->text = {};
|
|
return true;
|
|
} else {
|
|
AddComponents(HistoryMessageTranslation::Bit());
|
|
const auto added = Get<HistoryMessageTranslation>();
|
|
added->to = to;
|
|
added->requested = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void HistoryItem::translationToggle(
|
|
not_null<HistoryMessageTranslation*> translation,
|
|
bool used) {
|
|
if (translation->used != used && !translation->text.empty()) {
|
|
translation->used = used;
|
|
_history->owner().requestItemTextRefresh(this);
|
|
_history->owner().updateDependentMessages(this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::translationDone(LanguageId to, TextWithEntities result) {
|
|
const auto set = [&](not_null<HistoryMessageTranslation*> translation) {
|
|
if (result.empty()) {
|
|
translation->failed = true;
|
|
} else {
|
|
translation->text = std::move(result);
|
|
if (_history->translatedTo() == to) {
|
|
translationToggle(translation, true);
|
|
}
|
|
}
|
|
};
|
|
if (const auto translation = Get<HistoryMessageTranslation>()) {
|
|
if (translation->to == to && translation->text.empty()) {
|
|
translation->requested = false;
|
|
set(translation);
|
|
}
|
|
} else {
|
|
AddComponents(HistoryMessageTranslation::Bit());
|
|
const auto added = Get<HistoryMessageTranslation>();
|
|
added->to = to;
|
|
set(added);
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::canReact() const {
|
|
if (!isRegular() || isService()) {
|
|
return false;
|
|
} else if (const auto media = this->media()) {
|
|
if (media->call()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void HistoryItem::toggleReaction(
|
|
const Data::ReactionId &reaction,
|
|
ReactionSource source) {
|
|
if (!_reactions) {
|
|
_reactions = std::make_unique<Data::MessageReactions>(this);
|
|
const auto canViewReactions = !isDiscussionPost()
|
|
&& (_history->peer->isChat() || _history->peer->isMegagroup());
|
|
if (canViewReactions) {
|
|
_flags |= MessageFlag::CanViewReactions;
|
|
}
|
|
_reactions->add(reaction, (source == ReactionSource::Selector));
|
|
} else if (ranges::contains(_reactions->chosen(), reaction)) {
|
|
_reactions->remove(reaction);
|
|
if (_reactions->empty()) {
|
|
_reactions = nullptr;
|
|
_flags &= ~MessageFlag::CanViewReactions;
|
|
_history->owner().notifyItemDataChange(this);
|
|
}
|
|
} else {
|
|
_reactions->add(reaction, (source == ReactionSource::Selector));
|
|
}
|
|
_history->owner().notifyItemDataChange(this);
|
|
}
|
|
|
|
void HistoryItem::updateReactionsUnknown() {
|
|
_reactionsLastRefreshed = 1;
|
|
}
|
|
|
|
const std::vector<Data::MessageReaction> &HistoryItem::reactions() const {
|
|
static const auto kEmpty = std::vector<Data::MessageReaction>();
|
|
return _reactions ? _reactions->list() : kEmpty;
|
|
}
|
|
|
|
bool HistoryItem::reactionsAreTags() const {
|
|
return _flags & MessageFlag::ReactionsAreTags;
|
|
}
|
|
|
|
auto HistoryItem::recentReactions() const
|
|
-> const base::flat_map<
|
|
Data::ReactionId,
|
|
std::vector<Data::RecentReaction>> & {
|
|
static const auto kEmpty = base::flat_map<
|
|
Data::ReactionId,
|
|
std::vector<Data::RecentReaction>>();
|
|
return _reactions ? _reactions->recent() : kEmpty;
|
|
}
|
|
|
|
bool HistoryItem::canViewReactions() const {
|
|
return (_flags & MessageFlag::CanViewReactions)
|
|
&& _reactions
|
|
&& !_reactions->list().empty();
|
|
}
|
|
|
|
std::vector<Data::ReactionId> HistoryItem::chosenReactions() const {
|
|
return _reactions
|
|
? _reactions->chosen()
|
|
: std::vector<Data::ReactionId>();
|
|
}
|
|
|
|
Data::ReactionId HistoryItem::lookupUnreadReaction(
|
|
not_null<UserData*> from) const {
|
|
if (!_reactions) {
|
|
return {};
|
|
}
|
|
const auto recent = _reactions->recent();
|
|
for (const auto &[id, list] : _reactions->recent()) {
|
|
const auto i = ranges::find(
|
|
list,
|
|
from,
|
|
&Data::RecentReaction::peer);
|
|
if (i != end(list) && i->unread) {
|
|
return id;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
crl::time HistoryItem::lastReactionsRefreshTime() const {
|
|
return _reactionsLastRefreshed;
|
|
}
|
|
|
|
bool HistoryItem::hasDirectLink() const {
|
|
return isRegular() && _history->peer->isChannel();
|
|
}
|
|
|
|
bool HistoryItem::changesWallPaper() const {
|
|
if (const auto media = _media.get()) {
|
|
return media->paper() != nullptr;
|
|
}
|
|
return Has<HistoryServiceSameBackground>();
|
|
}
|
|
|
|
FullMsgId HistoryItem::fullId() const {
|
|
return FullMsgId(_history->peer->id, id);
|
|
}
|
|
|
|
GlobalMsgId HistoryItem::globalId() const {
|
|
return { fullId(), _history->session().uniqueId() };
|
|
}
|
|
|
|
Data::MessagePosition HistoryItem::position() const {
|
|
return { .fullId = fullId(), .date = date() };
|
|
}
|
|
|
|
bool HistoryItem::computeDropForwardedInfo() const {
|
|
const auto media = this->media();
|
|
return (media && media->dropForwardedInfo())
|
|
|| (_history->peer->isSelf()
|
|
&& !Has<HistoryMessageForwarded>()
|
|
&& (!media || !media->forceForwardedInfo()));
|
|
}
|
|
|
|
bool HistoryItem::inThread(MsgId rootId) const {
|
|
return (replyToTop() == rootId)
|
|
|| (topicRootId() == rootId);
|
|
}
|
|
|
|
not_null<PeerData*> HistoryItem::author() const {
|
|
return (isPost() && !isSponsored()) ? _history->peer : from();
|
|
}
|
|
|
|
TimeId HistoryItem::originalDate() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->originalDate;
|
|
}
|
|
return date();
|
|
}
|
|
|
|
PeerData *HistoryItem::originalSender() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->originalSender;
|
|
}
|
|
const auto peer = _history->peer;
|
|
return peer->isBroadcast() ? peer : from();
|
|
}
|
|
|
|
const HiddenSenderInfo *HistoryItem::originalHiddenSenderInfo() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->originalHiddenSenderInfo.get();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const HiddenSenderInfo *HistoryItem::displayHiddenSenderInfo() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->savedFromHiddenSenderInfo
|
|
? forwarded->savedFromHiddenSenderInfo.get()
|
|
: forwarded->originalHiddenSenderInfo.get();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool HistoryItem::showForwardsFromSender(
|
|
not_null<const HistoryMessageForwarded*> forwarded) const {
|
|
const auto peer = history()->peer;
|
|
return !forwarded->story
|
|
&& (peer->isSelf() || peer->isRepliesChat() || forwarded->imported);
|
|
}
|
|
|
|
not_null<PeerData*> HistoryItem::fromOriginal() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
if (forwarded->originalSender) {
|
|
if (const auto user = forwarded->originalSender->asUser()) {
|
|
return user;
|
|
}
|
|
}
|
|
}
|
|
return from();
|
|
}
|
|
|
|
QString HistoryItem::originalPostAuthor() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->originalPostAuthor;
|
|
} else if (const auto msgsigned = Get<HistoryMessageSigned>()) {
|
|
if (!msgsigned->isAnonymousRank) {
|
|
return msgsigned->postAuthor;
|
|
}
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
MsgId HistoryItem::originalId() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->originalId;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
const TextWithEntities &HistoryItem::originalText() const {
|
|
static const auto kEmpty = TextWithEntities();
|
|
return isService() ? kEmpty : _text;
|
|
}
|
|
|
|
const TextWithEntities &HistoryItem::translatedText() const {
|
|
if (isService()) {
|
|
static const auto kEmpty = TextWithEntities();
|
|
return kEmpty;
|
|
} else if (const auto translation = this->translation()
|
|
; translation
|
|
&& translation->used
|
|
&& (translation->to == history()->translatedTo())) {
|
|
return translation->text;
|
|
} else {
|
|
return originalText();
|
|
}
|
|
}
|
|
|
|
TextWithEntities HistoryItem::translatedTextWithLocalEntities() const {
|
|
if (isService()) {
|
|
return {};
|
|
} else {
|
|
return withLocalEntities(translatedText());
|
|
}
|
|
}
|
|
|
|
TextForMimeData HistoryItem::clipboardText() const {
|
|
return isService()
|
|
? TextForMimeData()
|
|
: TextForMimeData::WithExpandedLinks(translatedText());
|
|
}
|
|
|
|
bool HistoryItem::changeViewsCount(int count) {
|
|
const auto views = Get<HistoryMessageViews>();
|
|
if (!views
|
|
|| views->views.count == count
|
|
|| (count >= 0 && views->views.count > count)) {
|
|
return false;
|
|
}
|
|
|
|
views->views.count = count;
|
|
return true;
|
|
}
|
|
|
|
void HistoryItem::setForwardsCount(int count) {
|
|
const auto views = Get<HistoryMessageViews>();
|
|
if (!views
|
|
|| views->forwardsCount == count
|
|
|| (count >= 0 && views->forwardsCount > count)) {
|
|
return;
|
|
}
|
|
|
|
views->forwardsCount = count;
|
|
history()->owner().notifyItemDataChange(this);
|
|
}
|
|
|
|
void HistoryItem::setPostAuthor(const QString &postAuthor) {
|
|
auto msgsigned = Get<HistoryMessageSigned>();
|
|
if (postAuthor.isEmpty()) {
|
|
if (!msgsigned) {
|
|
return;
|
|
}
|
|
RemoveComponents(HistoryMessageSigned::Bit());
|
|
history()->owner().requestItemResize(this);
|
|
return;
|
|
}
|
|
if (!msgsigned) {
|
|
AddComponents(HistoryMessageSigned::Bit());
|
|
msgsigned = Get<HistoryMessageSigned>();
|
|
} else if (msgsigned->postAuthor == postAuthor) {
|
|
return;
|
|
}
|
|
msgsigned->postAuthor = postAuthor;
|
|
msgsigned->isAnonymousRank = !isDiscussionPost()
|
|
&& this->author()->isMegagroup();
|
|
history()->owner().requestItemResize(this);
|
|
}
|
|
|
|
void HistoryItem::setReplies(HistoryMessageRepliesData &&data) {
|
|
if (data.isNull) {
|
|
return;
|
|
}
|
|
auto views = Get<HistoryMessageViews>();
|
|
if (!views) {
|
|
AddComponents(HistoryMessageViews::Bit());
|
|
views = Get<HistoryMessageViews>();
|
|
}
|
|
const auto &repliers = data.recentRepliers;
|
|
const auto count = data.repliesCount;
|
|
const auto channelId = data.channelId;
|
|
const auto readTillId = data.readMaxId
|
|
? std::max({
|
|
views->commentsInboxReadTillId.bare,
|
|
data.readMaxId.bare,
|
|
int64(1),
|
|
})
|
|
: views->commentsInboxReadTillId;
|
|
const auto maxId = data.maxId ? data.maxId : views->commentsMaxId;
|
|
const auto countsChanged = (views->replies.count != count)
|
|
|| (views->commentsInboxReadTillId != readTillId)
|
|
|| (views->commentsMaxId != maxId);
|
|
const auto megagroupChanged = (views->commentsMegagroupId != channelId);
|
|
const auto recentChanged = (views->recentRepliers != repliers);
|
|
if (!countsChanged && !megagroupChanged && !recentChanged) {
|
|
return;
|
|
}
|
|
views->replies.count = count;
|
|
if (recentChanged) {
|
|
views->recentRepliers = repliers;
|
|
}
|
|
const auto wasUnread = areCommentsUnread();
|
|
views->commentsMegagroupId = channelId;
|
|
views->commentsInboxReadTillId = readTillId;
|
|
views->commentsMaxId = maxId;
|
|
if (wasUnread != areCommentsUnread()) {
|
|
history()->owner().requestItemRepaint(this);
|
|
}
|
|
refreshRepliesText(views, megagroupChanged);
|
|
}
|
|
|
|
void HistoryItem::clearReplies() {
|
|
auto views = Get<HistoryMessageViews>();
|
|
if (!views) {
|
|
return;
|
|
}
|
|
const auto viewsPart = views->views;
|
|
if (viewsPart.count < 0) {
|
|
RemoveComponents(HistoryMessageViews::Bit());
|
|
} else {
|
|
*views = HistoryMessageViews();
|
|
views->views = viewsPart;
|
|
}
|
|
history()->owner().requestItemResize(this);
|
|
}
|
|
|
|
void HistoryItem::refreshRepliesText(
|
|
not_null<HistoryMessageViews*> views,
|
|
bool forceResize) {
|
|
if (views->commentsMegagroupId) {
|
|
views->replies.text = (views->replies.count > 0)
|
|
? tr::lng_comments_open_count(
|
|
tr::now,
|
|
lt_count_short,
|
|
views->replies.count)
|
|
: tr::lng_comments_open_none(tr::now);
|
|
views->replies.textWidth = st::semiboldFont->width(
|
|
views->replies.text);
|
|
views->repliesSmall.text = (views->replies.count > 0)
|
|
? Lang::FormatCountToShort(views->replies.count).string
|
|
: QString();
|
|
const auto hadText = (views->repliesSmall.textWidth > 0);
|
|
views->repliesSmall.textWidth = (views->replies.count > 0)
|
|
? st::semiboldFont->width(views->repliesSmall.text)
|
|
: 0;
|
|
const auto hasText = (views->repliesSmall.textWidth > 0);
|
|
if (hasText != hadText) {
|
|
forceResize = true;
|
|
}
|
|
}
|
|
if (forceResize) {
|
|
history()->owner().requestItemResize(this);
|
|
} else {
|
|
history()->owner().requestItemRepaint(this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::changeRepliesCount(int delta, PeerId replier) {
|
|
const auto views = Get<HistoryMessageViews>();
|
|
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
|
|
if (!views) {
|
|
return;
|
|
}
|
|
|
|
// Update full count.
|
|
if (views->replies.count < 0) {
|
|
return;
|
|
}
|
|
views->replies.count = std::max(views->replies.count + delta, 0);
|
|
if (replier && views->commentsMegagroupId) {
|
|
if (delta < 0) {
|
|
views->recentRepliers.erase(
|
|
ranges::remove(views->recentRepliers, replier),
|
|
end(views->recentRepliers));
|
|
} else if (!ranges::contains(views->recentRepliers, replier)) {
|
|
views->recentRepliers.insert(views->recentRepliers.begin(), replier);
|
|
while (views->recentRepliers.size() > limit) {
|
|
views->recentRepliers.pop_back();
|
|
}
|
|
}
|
|
}
|
|
refreshRepliesText(views);
|
|
history()->owner().notifyItemDataChange(this);
|
|
}
|
|
|
|
void HistoryItem::setReplyFields(
|
|
MsgId replyTo,
|
|
MsgId replyToTop,
|
|
bool isForumPost) {
|
|
if (isScheduled()) {
|
|
return;
|
|
} else if (const auto data = GetServiceDependentData()) {
|
|
if ((data->topId != replyToTop) && !IsServerMsgId(data->topId)) {
|
|
data->topId = replyToTop;
|
|
if (isForumPost) {
|
|
data->topicPost = true;
|
|
}
|
|
}
|
|
} else if (const auto reply = Get<HistoryMessageReply>()) {
|
|
const auto increment = (reply->topMessageId() != replyToTop)
|
|
&& !IsServerMsgId(reply->topMessageId());
|
|
reply->updateFields(this, replyTo, replyToTop, isForumPost);
|
|
if (increment) {
|
|
incrementReplyToTopCounter();
|
|
}
|
|
}
|
|
if (const auto topic = this->topic()) {
|
|
topic->maybeSetLastMessage(this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::updateDate(TimeId newDate) {
|
|
if (canUpdateDate() && _date != newDate) {
|
|
_date = newDate;
|
|
_history->owner().requestItemViewRefresh(this);
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::canUpdateDate() const {
|
|
return isScheduled();
|
|
}
|
|
|
|
void HistoryItem::applyTTL(TimeId destroyAt) {
|
|
const auto previousDestroyAt = std::exchange(_ttlDestroyAt, destroyAt);
|
|
if (previousDestroyAt) {
|
|
_history->owner().unregisterMessageTTL(previousDestroyAt, this);
|
|
}
|
|
if (!_ttlDestroyAt) {
|
|
return;
|
|
} else if (base::unixtime::now() >= _ttlDestroyAt) {
|
|
const auto session = &_history->session();
|
|
crl::on_main(session, [session, id = fullId()]{
|
|
if (const auto item = session->data().message(id)) {
|
|
item->destroy();
|
|
}
|
|
});
|
|
} else {
|
|
_history->owner().registerMessageTTL(_ttlDestroyAt, this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::replaceBuyWithReceiptInMarkup() {
|
|
if (const auto markup = inlineReplyMarkup()) {
|
|
for (auto &row : markup->data.rows) {
|
|
for (auto &button : row) {
|
|
if (button.type == HistoryMessageMarkupButton::Type::Buy) {
|
|
const auto receipt = tr::lng_payments_receipt_button(tr::now);
|
|
if (button.text != receipt) {
|
|
button.text = receipt;
|
|
if (markup->inlineKeyboard) {
|
|
markup->inlineKeyboard = nullptr;
|
|
_history->owner().requestItemResize(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::isUploading() const {
|
|
return _media && _media->uploading();
|
|
}
|
|
|
|
bool HistoryItem::isRegular() const {
|
|
return isHistoryEntry() && !isLocal();
|
|
}
|
|
|
|
int HistoryItem::viewsCount() const {
|
|
if (const auto views = Get<HistoryMessageViews>()) {
|
|
return std::max(views->views.count, 0);
|
|
}
|
|
return hasViews() ? 1 : -1;
|
|
}
|
|
|
|
int HistoryItem::repliesCount() const {
|
|
if (const auto views = Get<HistoryMessageViews>()) {
|
|
if (!checkCommentsLinkedChat(views->commentsMegagroupId)) {
|
|
return 0;
|
|
}
|
|
return std::max(views->replies.count, 0);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool HistoryItem::repliesAreComments() const {
|
|
if (const auto views = Get<HistoryMessageViews>()) {
|
|
return (views->commentsMegagroupId != 0)
|
|
&& checkCommentsLinkedChat(views->commentsMegagroupId);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool HistoryItem::externalReply() const {
|
|
if (!_history->peer->isRepliesChat()) {
|
|
return false;
|
|
} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->savedFromPeer && forwarded->savedFromMsgId;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool HistoryItem::hasExtendedMediaPreview() const {
|
|
if (const auto media = _media.get()) {
|
|
if (const auto invoice = media->invoice()) {
|
|
return (invoice->extendedPreview && !invoice->extendedMedia);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void HistoryItem::sendFailed() {
|
|
Expects(_flags & MessageFlag::BeingSent);
|
|
Expects(!(_flags & MessageFlag::SendingFailed));
|
|
|
|
_flags = (_flags | MessageFlag::SendingFailed) & ~MessageFlag::BeingSent;
|
|
_history->owner().notifyItemDataChange(this);
|
|
_history->session().changes().historyUpdated(
|
|
_history,
|
|
Data::HistoryUpdate::Flag::ClientSideMessages);
|
|
}
|
|
|
|
bool HistoryItem::needCheck() const {
|
|
return (out() && !isEmpty())
|
|
|| (!isRegular() && _history->peer->isSelf());
|
|
}
|
|
|
|
bool HistoryItem::isService() const {
|
|
return Has<HistoryServiceData>();
|
|
}
|
|
|
|
bool HistoryItem::unread(not_null<Data::Thread*> thread) const {
|
|
// Messages from myself are always read, unless scheduled.
|
|
if (_history->peer->isSelf() && !isFromScheduled()) {
|
|
return false;
|
|
}
|
|
|
|
// All messages in converted chats are always read.
|
|
if (_history->peer->migrateTo()) {
|
|
return false;
|
|
}
|
|
|
|
if (isRegular()) {
|
|
if (!thread->isServerSideUnread(this)) {
|
|
return false;
|
|
}
|
|
if (out()) {
|
|
if (const auto user = _history->peer->asUser()) {
|
|
if (user->isBot() && !user->isSupport()) {
|
|
return false;
|
|
}
|
|
} else if (const auto channel = _history->peer->asChannel()) {
|
|
if (!channel->isMegagroup()) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return out() || (_flags & MessageFlag::ClientSideUnread);
|
|
}
|
|
|
|
MsgId HistoryItem::replyToId() const {
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
return reply->messageId();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
FullMsgId HistoryItem::replyToFullId() const {
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
const auto peer = reply->externalPeerId();
|
|
return { peer ? peer : history()->peer->id, reply->messageId() };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
MsgId HistoryItem::replyToTop() const {
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
return reply->topMessageId();
|
|
} else if (const auto data = GetServiceDependentData()) {
|
|
return data->topId;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
MsgId HistoryItem::topicRootId() const {
|
|
if (const auto reply = Get<HistoryMessageReply>()
|
|
; reply && reply->topicPost()) {
|
|
return reply->topMessageId();
|
|
} else if (const auto data = GetServiceDependentData()
|
|
; data && data->topicPost && data->topId) {
|
|
return data->topId;
|
|
} else if (const auto info = Get<HistoryServiceTopicInfo>()) {
|
|
if (info->created()) {
|
|
return id;
|
|
}
|
|
}
|
|
return Data::ForumTopic::kGeneralId;
|
|
}
|
|
|
|
FullStoryId HistoryItem::replyToStory() const {
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
if (reply->storyId()) {
|
|
const auto peerId = reply->externalPeerId()
|
|
? reply->externalPeerId()
|
|
: _history->peer->id;
|
|
return { .peer = peerId, .story = reply->storyId() };
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
FullReplyTo HistoryItem::replyTo() const {
|
|
auto result = FullReplyTo{
|
|
.topicRootId = topicRootId(),
|
|
};
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
const auto &fields = reply->fields();
|
|
const auto peer = fields.externalPeerId;
|
|
const auto replyToPeer = peer ? peer : _history->peer->id;
|
|
if (const auto id = fields.messageId) {
|
|
result.messageId = { replyToPeer, id };
|
|
result.quote = fields.quote;
|
|
result.quoteOffset = fields.quoteOffset;
|
|
}
|
|
if (const auto id = fields.storyId) {
|
|
result.storyId = { replyToPeer, id };
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void HistoryItem::setText(const TextWithEntities &textWithEntities) {
|
|
for (const auto &entity : textWithEntities.entities) {
|
|
auto type = entity.type();
|
|
if (type == EntityType::Url
|
|
|| type == EntityType::CustomUrl
|
|
|| type == EntityType::Email) {
|
|
_flags |= MessageFlag::HasTextLinks;
|
|
break;
|
|
}
|
|
}
|
|
setTextValue((_media && _media->consumeMessageText(textWithEntities))
|
|
? TextWithEntities()
|
|
: std::move(textWithEntities));
|
|
}
|
|
|
|
void HistoryItem::setTextValue(TextWithEntities text, bool force) {
|
|
if (const auto processId = Spellchecker::TryHighlightSyntax(text)) {
|
|
_flags |= MessageFlag::InHighlightProcess;
|
|
history()->owner().registerHighlightProcess(processId, this);
|
|
}
|
|
const auto had = !_text.empty();
|
|
_text = std::move(text);
|
|
RemoveComponents(HistoryMessageTranslation::Bit());
|
|
if (had || force) {
|
|
history()->owner().requestItemTextRefresh(this);
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::inHighlightProcess() const {
|
|
return _flags & MessageFlag::InHighlightProcess;
|
|
}
|
|
|
|
void HistoryItem::highlightProcessDone() {
|
|
Expects(inHighlightProcess());
|
|
|
|
_flags &= ~MessageFlag::InHighlightProcess;
|
|
if (!_text.empty()) {
|
|
setTextValue(base::take(_text), true);
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::showNotification() const {
|
|
const auto channel = _history->peer->asChannel();
|
|
if (channel && !channel->amIn()) {
|
|
return false;
|
|
}
|
|
return (out() || _history->peer->isSelf())
|
|
? isFromScheduled()
|
|
: unread(notificationThread());
|
|
}
|
|
|
|
void HistoryItem::markClientSideAsRead() {
|
|
_flags &= ~MessageFlag::ClientSideUnread;
|
|
}
|
|
|
|
MessageGroupId HistoryItem::groupId() const {
|
|
return _groupId;
|
|
}
|
|
|
|
bool HistoryItem::isEmpty() const {
|
|
return _text.empty()
|
|
&& !_media
|
|
&& !Has<HistoryMessageLogEntryOriginal>();
|
|
}
|
|
|
|
Data::SavedSublist *HistoryItem::savedSublist() const {
|
|
if (const auto saved = Get<HistoryMessageSaved>()) {
|
|
return saved->sublist;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
PeerData *HistoryItem::savedSublistPeer() const {
|
|
if (const auto sublist = savedSublist()) {
|
|
return sublist->peer();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
PeerData *HistoryItem::savedFromSender() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->savedFromSender;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const HiddenSenderInfo *HistoryItem::savedFromHiddenSenderInfo() const {
|
|
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
return forwarded->savedFromHiddenSenderInfo.get();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
TextWithEntities HistoryItem::notificationText(
|
|
NotificationTextOptions options) const {
|
|
auto result = [&] {
|
|
if (_media && !isService()) {
|
|
return _media->notificationText();
|
|
} else if (!emptyText()) {
|
|
return _text;
|
|
}
|
|
return TextWithEntities();
|
|
}();
|
|
if (options.spoilerLoginCode
|
|
&& !out()
|
|
&& history()->peer->isNotificationsUser()) {
|
|
result = SpoilerLoginCode(std::move(result));
|
|
}
|
|
if (result.text.size() <= kNotificationTextLimit) {
|
|
return result;
|
|
}
|
|
return Ui::Text::Mid(result, 0, kNotificationTextLimit).append(
|
|
Ui::kQEllipsis);
|
|
}
|
|
|
|
ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const {
|
|
if (isService()) {
|
|
const_cast<HistoryItem*>(this)->resolveDependent();
|
|
|
|
// Don't show small media for service messages (chat photo changed).
|
|
// Because larger version is shown exactly to the left of the small.
|
|
//auto media = _media ? _media->toPreview(options) : ItemPreview();
|
|
return {
|
|
.text = Ui::Text::Colorized(notificationText()),
|
|
//.images = std::move(media.images),
|
|
//.loadingContext = std::move(media.loadingContext),
|
|
};
|
|
}
|
|
|
|
auto result = [&]() -> ItemPreview {
|
|
if (_media) {
|
|
return _media->toPreview(options);
|
|
} else if (!emptyText()) {
|
|
return { .text = options.translated ? translatedText() : _text };
|
|
}
|
|
return {};
|
|
}();
|
|
if (options.spoilerLoginCode
|
|
&& !out()
|
|
&& history()->peer->isNotificationsUser()) {
|
|
result.text = SpoilerLoginCode(std::move(result.text));
|
|
}
|
|
const auto fromSender = [](not_null<PeerData*> sender) {
|
|
return sender->isSelf()
|
|
? tr::lng_from_you(tr::now)
|
|
: sender->shortName();
|
|
};
|
|
const auto forwarded = Get<HistoryMessageForwarded>();
|
|
const auto forwardFromSender = forwarded
|
|
&& showForwardsFromSender(forwarded);
|
|
result.icon = (forwarded
|
|
&& (!forwardFromSender || forwarded->forwardOfForward()))
|
|
? ItemPreview::Icon::ForwardedMessage
|
|
: replyToStory().valid()
|
|
? ItemPreview::Icon::ReplyToStory
|
|
: ItemPreview::Icon::None;
|
|
const auto fromForwarded = [&]() -> std::optional<QString> {
|
|
if (forwarded) {
|
|
const auto sender = forwarded->forwardOfForward()
|
|
? forwarded->savedFromSender
|
|
: forwarded->originalSender;
|
|
return sender
|
|
? fromSender(sender)
|
|
: forwarded->savedFromHiddenSenderInfo
|
|
? forwarded->savedFromHiddenSenderInfo->name
|
|
: forwarded->originalHiddenSenderInfo->name;
|
|
}
|
|
return {};
|
|
};
|
|
const auto sender = [&]() -> std::optional<QString> {
|
|
if (options.hideSender || isPost() || isEmpty()) {
|
|
return {};
|
|
} else if (!_history->peer->isUser()) {
|
|
if (const auto from = displayFrom()) {
|
|
return fromSender(from);
|
|
}
|
|
return fromForwarded();
|
|
} else if (_history->peer->isSelf()) {
|
|
return fromForwarded();
|
|
}
|
|
return {};
|
|
}();
|
|
if (!sender) {
|
|
return result;
|
|
}
|
|
const auto topic = options.ignoreTopic ? nullptr : this->topic();
|
|
return Dialogs::Ui::PreviewWithSender(
|
|
std::move(result),
|
|
*sender,
|
|
topic ? topic->titleWithIcon() : TextWithEntities());
|
|
}
|
|
|
|
TextWithEntities HistoryItem::inReplyText() const {
|
|
if (!isService()) {
|
|
return toPreview({
|
|
.hideSender = true,
|
|
.generateImages = false,
|
|
.translated = true,
|
|
}).text;
|
|
}
|
|
auto result = 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::Colorized(result);
|
|
}
|
|
|
|
const std::vector<ClickHandlerPtr> &HistoryItem::customTextLinks() const {
|
|
static const auto kEmpty = std::vector<ClickHandlerPtr>();
|
|
const auto service = Get<HistoryServiceData>();
|
|
return service ? service->textLinks : kEmpty;
|
|
}
|
|
|
|
void HistoryItem::createComponents(CreateConfig &&config) {
|
|
uint64 mask = 0;
|
|
if (config.reply.messageId
|
|
|| config.reply.externalSenderId
|
|
|| !config.reply.externalSenderName.isEmpty()
|
|
|| config.reply.storyId) {
|
|
mask |= HistoryMessageReply::Bit();
|
|
}
|
|
if (config.viaBotId) {
|
|
mask |= HistoryMessageVia::Bit();
|
|
}
|
|
if (config.viewsCount >= 0 || !config.replies.isNull) {
|
|
mask |= HistoryMessageViews::Bit();
|
|
}
|
|
if (!config.postAuthor.isEmpty()) {
|
|
mask |= HistoryMessageSigned::Bit();
|
|
} else if (_history->peer->isMegagroup() // Discussion posts signatures.
|
|
&& config.savedFromPeer
|
|
&& !config.originalPostAuthor.isEmpty()) {
|
|
const auto savedFrom = _history->owner().peerLoaded(
|
|
config.savedFromPeer);
|
|
if (savedFrom && savedFrom->isChannel()) {
|
|
mask |= HistoryMessageSigned::Bit();
|
|
}
|
|
} else if ((_history->peer->isSelf() || _history->peer->isRepliesChat())
|
|
&& !config.originalPostAuthor.isEmpty()) {
|
|
mask |= HistoryMessageSigned::Bit();
|
|
}
|
|
if (config.editDate != TimeId(0)) {
|
|
mask |= HistoryMessageEdited::Bit();
|
|
}
|
|
if (config.originalDate != 0) {
|
|
mask |= HistoryMessageForwarded::Bit();
|
|
}
|
|
if (!config.markup.isTrivial()) {
|
|
mask |= HistoryMessageReplyMarkup::Bit();
|
|
} else if (config.inlineMarkup) {
|
|
mask |= HistoryMessageReplyMarkup::Bit();
|
|
}
|
|
if (_history->peer->isSelf()) {
|
|
mask |= HistoryMessageSaved::Bit();
|
|
}
|
|
|
|
UpdateComponents(mask);
|
|
|
|
if (const auto saved = Get<HistoryMessageSaved>()) {
|
|
if (!config.savedSublistPeer) {
|
|
if (config.savedFromPeer) {
|
|
config.savedSublistPeer = config.savedFromPeer;
|
|
} else if (config.originalSenderId) {
|
|
config.savedSublistPeer = config.originalSenderId;
|
|
} else if (!config.originalSenderName.isEmpty()) {
|
|
config.savedSublistPeer = PeerData::kSavedHiddenAuthorId;
|
|
} else {
|
|
config.savedSublistPeer = _history->session().userPeerId();
|
|
}
|
|
}
|
|
const auto peer = _history->owner().peer(config.savedSublistPeer);
|
|
saved->sublist = _history->owner().savedMessages().sublist(peer);
|
|
}
|
|
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
reply->set(std::move(config.reply));
|
|
reply->updateData(this);
|
|
}
|
|
if (const auto via = Get<HistoryMessageVia>()) {
|
|
via->create(&_history->owner(), config.viaBotId);
|
|
}
|
|
if (const auto views = Get<HistoryMessageViews>()) {
|
|
changeViewsCount(config.viewsCount);
|
|
if (config.replies.isNull
|
|
&& isSending()
|
|
&& config.markup.isNull()) {
|
|
if (const auto broadcast = _history->peer->asBroadcast()) {
|
|
if (const auto linked = broadcast->linkedChat()) {
|
|
config.replies.isNull = false;
|
|
config.replies.channelId = peerToChannel(linked->id);
|
|
}
|
|
}
|
|
}
|
|
setForwardsCount(config.forwardsCount);
|
|
setReplies(std::move(config.replies));
|
|
}
|
|
if (const auto edited = Get<HistoryMessageEdited>()) {
|
|
edited->date = config.editDate;
|
|
}
|
|
if (const auto msgsigned = Get<HistoryMessageSigned>()) {
|
|
msgsigned->postAuthor = config.postAuthor.isEmpty()
|
|
? config.originalPostAuthor
|
|
: config.postAuthor;
|
|
msgsigned->isAnonymousRank = !isDiscussionPost()
|
|
&& author()->isMegagroup();
|
|
}
|
|
setupForwardedComponent(config);
|
|
if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
|
|
if (!config.markup.isTrivial()) {
|
|
markup->updateData(std::move(config.markup));
|
|
} else if (config.inlineMarkup) {
|
|
markup->createForwarded(*config.inlineMarkup);
|
|
}
|
|
if (markup->data.flags & ReplyMarkupFlag::HasSwitchInlineButton) {
|
|
_flags |= MessageFlag::HasSwitchInlineButton;
|
|
}
|
|
} else if (!config.markup.isNull()) {
|
|
_flags |= MessageFlag::HasReplyMarkup;
|
|
} else {
|
|
_flags &= ~MessageFlag::HasReplyMarkup;
|
|
}
|
|
|
|
if (out() && isSending()) {
|
|
if (const auto channel = _history->peer->asMegagroup()) {
|
|
_boostsApplied = channel->mgInfo->boostsApplied;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::checkRepliesPts(
|
|
const HistoryMessageRepliesData &data) const {
|
|
const auto channel = _history->peer->asChannel();
|
|
const auto pts = channel
|
|
? channel->pts()
|
|
: _history->session().updates().pts();
|
|
return (data.pts >= pts);
|
|
}
|
|
|
|
void HistoryItem::setupForwardedComponent(const CreateConfig &config) {
|
|
const auto forwarded = Get<HistoryMessageForwarded>();
|
|
if (!forwarded) {
|
|
return;
|
|
}
|
|
forwarded->originalDate = config.originalDate;
|
|
const auto originalSender = config.originalSenderId
|
|
? config.originalSenderId
|
|
: !config.originalSenderName.isEmpty()
|
|
? PeerId()
|
|
: from()->id;
|
|
forwarded->originalSender = originalSender
|
|
? _history->owner().peer(originalSender).get()
|
|
: nullptr;
|
|
if (!forwarded->originalSender) {
|
|
forwarded->originalHiddenSenderInfo
|
|
= std::make_unique<HiddenSenderInfo>(
|
|
config.originalSenderName,
|
|
config.imported);
|
|
}
|
|
forwarded->originalId = config.originalId;
|
|
forwarded->originalPostAuthor = config.originalPostAuthor;
|
|
forwarded->psaType = config.forwardPsaType;
|
|
forwarded->savedFromPeer = _history->owner().peerLoaded(
|
|
config.savedFromPeer);
|
|
forwarded->savedFromMsgId = config.savedFromMsgId;
|
|
forwarded->savedFromSender = _history->owner().peerLoaded(
|
|
config.savedFromSenderId);
|
|
forwarded->savedFromOutgoing = config.savedFromOutgoing;
|
|
if (!forwarded->savedFromSender
|
|
&& !config.savedFromSenderName.isEmpty()) {
|
|
forwarded->savedFromHiddenSenderInfo
|
|
= std::make_unique<HiddenSenderInfo>(config.savedFromSenderName, false);
|
|
}
|
|
forwarded->imported = config.imported;
|
|
}
|
|
|
|
bool HistoryItem::generateLocalEntitiesByReply() const {
|
|
using namespace HistoryView;
|
|
if (!_media) {
|
|
return true;
|
|
} else if (const auto document = _media->document()) {
|
|
return !DurationForTimestampLinks(document);
|
|
} else if (const auto webpage = _media->webpage()) {
|
|
return (webpage->type != WebPageType::Video)
|
|
&& !DurationForTimestampLinks(webpage);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
TextWithEntities HistoryItem::withLocalEntities(
|
|
const TextWithEntities &textWithEntities) const {
|
|
using namespace HistoryView;
|
|
if (!generateLocalEntitiesByReply()) {
|
|
if (!_media) {
|
|
} else if (const auto document = _media->document()) {
|
|
if (const auto duration = DurationForTimestampLinks(document)) {
|
|
return AddTimestampLinks(
|
|
textWithEntities,
|
|
duration,
|
|
TimestampLinkBase(document, fullId()));
|
|
}
|
|
} else if (const auto webpage = _media->webpage()) {
|
|
if (const auto duration = DurationForTimestampLinks(webpage)) {
|
|
return AddTimestampLinks(
|
|
textWithEntities,
|
|
duration,
|
|
TimestampLinkBase(webpage, fullId()));
|
|
}
|
|
}
|
|
return textWithEntities;
|
|
}
|
|
if (const auto reply = Get<HistoryMessageReply>()) {
|
|
const auto document = reply->replyToDocumentId
|
|
? _history->owner().document(reply->replyToDocumentId).get()
|
|
: nullptr;
|
|
const auto webpage = reply->replyToWebPageId
|
|
? _history->owner().webpage(reply->replyToWebPageId).get()
|
|
: nullptr;
|
|
if (document) {
|
|
if (const auto duration = DurationForTimestampLinks(document)) {
|
|
const auto context = reply->resolvedMessage->fullId();
|
|
return AddTimestampLinks(
|
|
textWithEntities,
|
|
duration,
|
|
TimestampLinkBase(document, context));
|
|
}
|
|
} else if (webpage) {
|
|
if (const auto duration = DurationForTimestampLinks(webpage)) {
|
|
const auto context = reply->resolvedMessage->fullId();
|
|
return AddTimestampLinks(
|
|
textWithEntities,
|
|
duration,
|
|
TimestampLinkBase(webpage, context));
|
|
}
|
|
}
|
|
}
|
|
return textWithEntities;
|
|
}
|
|
|
|
void HistoryItem::createComponentsHelper(
|
|
MessageFlags flags,
|
|
FullReplyTo replyTo,
|
|
UserId viaBotId,
|
|
const QString &postAuthor,
|
|
HistoryMessageMarkupData &&markup) {
|
|
auto config = CreateConfig();
|
|
config.viaBotId = viaBotId;
|
|
if (flags & MessageFlag::HasReplyInfo) {
|
|
config.reply.messageId = replyTo.messageId.msg;
|
|
config.reply.storyId = replyTo.storyId.story;
|
|
config.reply.externalPeerId = replyTo.storyId
|
|
? replyTo.storyId.peer
|
|
: (replyTo.messageId && replyTo.messageId.peer
|
|
!= history()->peer->id)
|
|
? replyTo.messageId.peer
|
|
: PeerId();
|
|
const auto to = LookupReplyTo(_history, replyTo.messageId);
|
|
const auto replyToTop = replyTo.topicRootId
|
|
? replyTo.topicRootId
|
|
: LookupReplyToTop(_history, to);
|
|
config.reply.topMessageId = replyToTop
|
|
? replyToTop
|
|
: (replyTo.messageId.peer == history()->peer->id)
|
|
? replyTo.messageId.msg
|
|
: MsgId();
|
|
const auto forum = _history->asForum();
|
|
const auto topic = forum
|
|
? forum->topicFor(replyTo.topicRootId)
|
|
: nullptr;
|
|
if (!config.reply.externalPeerId
|
|
&& topic
|
|
&& to
|
|
&& topic->rootId() != to->topicRootId()) {
|
|
config.reply.externalPeerId = replyTo.messageId.peer;
|
|
}
|
|
const auto topicPost = config.reply.externalPeerId
|
|
? (replyTo.topicRootId
|
|
&& (replyTo.topicRootId != Data::ForumTopic::kGeneralId))
|
|
: (LookupReplyIsTopicPost(to)
|
|
|| (to && to->Has<HistoryServiceTopicInfo>())
|
|
|| (forum && forum->creating(config.reply.topMessageId)));
|
|
config.reply.topicPost = topicPost ? 1 : 0;
|
|
config.reply.manualQuote = replyTo.quote.empty() ? 0 : 1;
|
|
config.reply.quoteOffset = replyTo.quoteOffset;
|
|
config.reply.quote = std::move(replyTo.quote);
|
|
}
|
|
config.markup = std::move(markup);
|
|
if (flags & MessageFlag::HasPostAuthor) config.postAuthor = postAuthor;
|
|
if (flags & MessageFlag::HasViews) config.viewsCount = 1;
|
|
|
|
createComponents(std::move(config));
|
|
}
|
|
|
|
void HistoryItem::setReactions(const MTPMessageReactions *reactions) {
|
|
Expects(!_reactions);
|
|
|
|
if (changeReactions(reactions) && _reactions->hasUnread()) {
|
|
_flags |= MessageFlag::HasUnreadReaction;
|
|
}
|
|
}
|
|
|
|
void HistoryItem::updateReactions(const MTPMessageReactions *reactions) {
|
|
const auto wasRecentUsers = LookupRecentUnreadReactedUsers(this);
|
|
const auto hadUnread = hasUnreadReaction();
|
|
const auto changed = changeReactions(reactions);
|
|
if (!changed) {
|
|
return;
|
|
}
|
|
const auto hasUnread = _reactions && _reactions->hasUnread();
|
|
if (hasUnread && !hadUnread) {
|
|
_flags |= MessageFlag::HasUnreadReaction;
|
|
|
|
addToUnreadThings(HistoryUnreadThings::AddType::New);
|
|
} else if (!hasUnread && hadUnread) {
|
|
markReactionsRead();
|
|
}
|
|
CheckReactionNotificationSchedule(this, wasRecentUsers);
|
|
_history->owner().notifyItemDataChange(this);
|
|
}
|
|
|
|
bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) {
|
|
if (reactions || _reactionsLastRefreshed) {
|
|
_reactionsLastRefreshed = crl::now();
|
|
}
|
|
if (!reactions) {
|
|
_flags &= ~MessageFlag::CanViewReactions;
|
|
if (_history->peer->isSelf()) {
|
|
_flags |= MessageFlag::ReactionsAreTags;
|
|
}
|
|
return (base::take(_reactions) != nullptr);
|
|
}
|
|
const auto &data = reactions->data();
|
|
const auto empty = data.vresults().v.isEmpty();
|
|
if (data.is_reactions_as_tags()
|
|
|| (empty && _history->peer->isSelf())) {
|
|
_flags |= MessageFlag::ReactionsAreTags;
|
|
} else {
|
|
_flags &= ~MessageFlag::ReactionsAreTags;
|
|
}
|
|
if (data.is_can_see_list()) {
|
|
_flags |= MessageFlag::CanViewReactions;
|
|
} else {
|
|
_flags &= ~MessageFlag::CanViewReactions;
|
|
}
|
|
if (empty) {
|
|
return (base::take(_reactions) != nullptr);
|
|
} else if (!_reactions) {
|
|
_reactions = std::make_unique<Data::MessageReactions>(this);
|
|
}
|
|
const auto min = data.is_min();
|
|
const auto &list = data.vresults().v;
|
|
const auto &recent = data.vrecent_reactions().value_or_empty();
|
|
if (min && hasUnreadReaction()) {
|
|
// We can't update reactions from min if we have unread.
|
|
if (_reactions->checkIfChanged(list, recent, min)) {
|
|
updateReactionsUnknown();
|
|
}
|
|
return false;
|
|
}
|
|
return _reactions->change(list, recent, min);
|
|
}
|
|
|
|
void HistoryItem::applyTTL(const MTPDmessage &data) {
|
|
if (const auto period = data.vttl_period()) {
|
|
if (period->v > 0) {
|
|
applyTTL(data.vdate().v + period->v);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::applyTTL(const MTPDmessageService &data) {
|
|
if (const auto period = data.vttl_period()) {
|
|
if (period->v > 0) {
|
|
applyTTL(data.vdate().v + period->v);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::createComponents(const MTPDmessage &data) {
|
|
auto config = CreateConfig();
|
|
config.savedSublistPeer = data.vsaved_peer_id()
|
|
? peerFromMTP(*data.vsaved_peer_id())
|
|
: PeerId();
|
|
if (const auto forwarded = data.vfwd_from()) {
|
|
forwarded->match([&](const MTPDmessageFwdHeader &data) {
|
|
FillForwardedInfo(config, data);
|
|
});
|
|
}
|
|
if (const auto reply = data.vreply_to()) {
|
|
config.reply = ReplyFieldsFromMTP(this, *reply);
|
|
}
|
|
config.viaBotId = data.vvia_bot_id().value_or_empty();
|
|
config.viewsCount = data.vviews().value_or(-1);
|
|
config.forwardsCount = data.vforwards().value_or(-1);
|
|
config.replies = isScheduled()
|
|
? HistoryMessageRepliesData()
|
|
: HistoryMessageRepliesData(data.vreplies());
|
|
config.markup = HistoryMessageMarkupData(data.vreply_markup());
|
|
config.editDate = data.vedit_date().value_or_empty();
|
|
config.postAuthor = qs(data.vpost_author().value_or_empty());
|
|
createComponents(std::move(config));
|
|
}
|
|
|
|
void HistoryItem::refreshMedia(const MTPMessageMedia *media) {
|
|
const auto was = (_media != nullptr);
|
|
if (const auto invoice = was ? _media->invoice() : nullptr) {
|
|
if (invoice->extendedMedia) {
|
|
return;
|
|
}
|
|
}
|
|
_media = nullptr;
|
|
if (media) {
|
|
setMedia(*media);
|
|
}
|
|
if (was || _media) {
|
|
if (const auto views = Get<HistoryMessageViews>()) {
|
|
refreshRepliesText(views);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::refreshSentMedia(const MTPMessageMedia *media) {
|
|
const auto wasGrouped = history()->owner().groups().isGrouped(this);
|
|
refreshMedia(media);
|
|
if (wasGrouped) {
|
|
history()->owner().groups().refreshMessage(this);
|
|
} else {
|
|
history()->owner().requestItemViewRefresh(this);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::createServiceFromMtp(const MTPDmessage &message) {
|
|
AddComponents(HistoryServiceData::Bit());
|
|
|
|
const auto unread = message.is_media_unread();
|
|
const auto media = message.vmedia();
|
|
Assert(media != nullptr);
|
|
|
|
media->match([&](const MTPDmessageMediaPhoto &data) {
|
|
if (unread) {
|
|
const auto ttl = data.vttl_seconds();
|
|
Assert(ttl != nullptr);
|
|
|
|
setSelfDestruct(HistoryServiceSelfDestruct::Type::Photo, *ttl);
|
|
if (out()) {
|
|
setServiceText({
|
|
tr::lng_ttl_photo_sent(tr::now, Ui::Text::WithEntities)
|
|
});
|
|
} else {
|
|
auto result = PreparedServiceText();
|
|
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)
|
|
});
|
|
}
|
|
}, [&](const MTPDmessageMediaDocument &data) {
|
|
if (unread) {
|
|
const auto ttl = data.vttl_seconds();
|
|
Assert(ttl != nullptr);
|
|
|
|
if (data.is_video()) {
|
|
setSelfDestruct(
|
|
HistoryServiceSelfDestruct::Type::Video,
|
|
*ttl);
|
|
if (out()) {
|
|
setServiceText({
|
|
tr::lng_ttl_video_sent(
|
|
tr::now,
|
|
Ui::Text::WithEntities)
|
|
});
|
|
} else {
|
|
auto result = PreparedServiceText();
|
|
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 if (out()) {
|
|
auto text = (data.is_voice()
|
|
? tr::lng_ttl_voice_sent
|
|
: data.is_round()
|
|
? tr::lng_ttl_round_sent
|
|
: tr::lng_message_empty)(tr::now, Ui::Text::WithEntities);
|
|
setServiceText({ std::move(text) });
|
|
}
|
|
} else {
|
|
auto text = (data.is_video()
|
|
? tr::lng_ttl_video_expired
|
|
: data.is_voice()
|
|
? tr::lng_ttl_voice_expired
|
|
: data.is_round()
|
|
? tr::lng_ttl_round_expired
|
|
: tr::lng_message_empty)(tr::now, Ui::Text::WithEntities);
|
|
setServiceText({ std::move(text) });
|
|
}
|
|
}, [&](const MTPDmessageMediaStory &data) {
|
|
setServiceText(prepareStoryMentionText());
|
|
}, [](const auto &) {
|
|
Unexpected("Media type in HistoryItem::createServiceFromMtp()");
|
|
});
|
|
|
|
if (const auto reactions = message.vreactions()) {
|
|
updateReactions(reactions);
|
|
}
|
|
}
|
|
|
|
void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
|
|
AddComponents(HistoryServiceData::Bit());
|
|
|
|
const auto &action = message.vaction();
|
|
const auto type = action.type();
|
|
if (type == mtpc_messageActionPinMessage) {
|
|
UpdateComponents(HistoryServicePinned::Bit());
|
|
} else if (type == mtpc_messageActionTopicCreate
|
|
|| type == mtpc_messageActionTopicEdit) {
|
|
UpdateComponents(HistoryServiceTopicInfo::Bit());
|
|
const auto info = Get<HistoryServiceTopicInfo>();
|
|
info->topicPost = true;
|
|
if (type == mtpc_messageActionTopicEdit) {
|
|
const auto &data = action.c_messageActionTopicEdit();
|
|
if (const auto title = data.vtitle()) {
|
|
info->title = qs(*title);
|
|
info->renamed = true;
|
|
}
|
|
if (const auto icon = data.vicon_emoji_id()) {
|
|
info->iconId = icon->v;
|
|
info->reiconed = true;
|
|
}
|
|
if (const auto closed = data.vclosed()) {
|
|
info->closed = mtpIsTrue(*closed);
|
|
info->reopened = !info->closed;
|
|
}
|
|
if (const auto hidden = data.vhidden()) {
|
|
info->hidden = mtpIsTrue(*hidden);
|
|
info->unhidden = !info->hidden;
|
|
}
|
|
} else {
|
|
const auto &data = action.c_messageActionTopicCreate();
|
|
info->title = qs(data.vtitle());
|
|
info->iconId = data.vicon_emoji_id().value_or_empty();
|
|
}
|
|
} else if (type == mtpc_messageActionSetChatTheme) {
|
|
setupChatThemeChange();
|
|
} else if (type == mtpc_messageActionSetMessagesTTL) {
|
|
setupTTLChange();
|
|
} else if (type == mtpc_messageActionGameScore) {
|
|
const auto &data = action.c_messageActionGameScore();
|
|
UpdateComponents(HistoryServiceGameScore::Bit());
|
|
Get<HistoryServiceGameScore>()->score = data.vscore().v;
|
|
} else if (type == mtpc_messageActionPaymentSent) {
|
|
const auto &data = action.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
|
|
? action.c_messageActionGroupCall().vcall()
|
|
: action.c_messageActionGroupCallScheduled().vcall();
|
|
const auto duration = started
|
|
? action.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 = action.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) {
|
|
updateServiceText(
|
|
prepareInvitedToCallText(
|
|
ParseInvitedToCallUsers(this, users),
|
|
has ? id : 0));
|
|
if (!has) {
|
|
RemoveComponents(HistoryServiceOngoingCall::Bit());
|
|
}
|
|
}, call->lifetime);
|
|
}
|
|
} else if (type == mtpc_messageActionSetChatWallPaper) {
|
|
if (action.c_messageActionSetChatWallPaper().is_same()) {
|
|
UpdateComponents(HistoryServiceSameBackground::Bit());
|
|
} else {
|
|
RemoveComponents(HistoryServiceSameBackground::Bit());
|
|
}
|
|
} else if (type == mtpc_messageActionGiveawayResults) {
|
|
UpdateComponents(HistoryServiceGiveawayResults::Bit());
|
|
}
|
|
if (const auto replyTo = message.vreply_to()) {
|
|
replyTo->match([&](const MTPDmessageReplyHeader &data) {
|
|
const auto peerId = data.vreply_to_peer_id()
|
|
? peerFromMTP(*data.vreply_to_peer_id())
|
|
: _history->peer->id;
|
|
if (const auto dependent = GetServiceDependentData()) {
|
|
const auto id = data.vreply_to_msg_id().value_or_empty();
|
|
if (id) {
|
|
dependent->peerId = (peerId != _history->peer->id)
|
|
? peerId
|
|
: 0;
|
|
dependent->msgId = id;
|
|
dependent->topId = data.vreply_to_top_id().value_or(id);
|
|
dependent->topicPost = data.is_forum_topic()
|
|
|| Has<HistoryServiceTopicInfo>();
|
|
updateServiceDependent();
|
|
}
|
|
}
|
|
}, [](const MTPDmessageReplyStoryHeader &data) {
|
|
});
|
|
}
|
|
setServiceMessageByAction(action);
|
|
}
|
|
|
|
void HistoryItem::setMedia(const MTPMessageMedia &media) {
|
|
_media = CreateMedia(this, media);
|
|
checkStoryForwardInfo();
|
|
checkBuyButton();
|
|
}
|
|
|
|
void HistoryItem::checkStoryForwardInfo() {
|
|
if (const auto storyId = _media ? _media->storyId() : FullStoryId()) {
|
|
const auto adding = !Has<HistoryMessageForwarded>();
|
|
if (adding) {
|
|
AddComponents(HistoryMessageForwarded::Bit());
|
|
}
|
|
const auto forwarded = Get<HistoryMessageForwarded>();
|
|
if (forwarded->story || adding) {
|
|
const auto peer = history()->owner().peer(storyId.peer);
|
|
forwarded->story = true;
|
|
forwarded->originalSender = peer;
|
|
}
|
|
} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
|
if (forwarded->story) {
|
|
RemoveComponents(HistoryMessageForwarded::Bit());
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryItem::applyServiceDateEdition(const MTPDmessageService &data) {
|
|
const auto date = data.vdate().v;
|
|
if (_date == date) {
|
|
return;
|
|
}
|
|
_date = date;
|
|
}
|
|
|
|
void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
|
|
auto prepareChatAddUserText = [this](const MTPDmessageActionChatAddUser &action) {
|
|
auto result = PreparedServiceText();
|
|
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 = u"somebody"_q },
|
|
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 = PreparedServiceText();
|
|
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 = PreparedServiceText();
|
|
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 = PreparedServiceText();
|
|
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 = [&](const MTPDmessageActionChatDeletePhoto &action) {
|
|
auto result = PreparedServiceText();
|
|
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 = PreparedServiceText();
|
|
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 = PreparedServiceText();
|
|
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 = PreparedServiceText();
|
|
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 preparePinMessage = [&](const MTPDmessageActionPinMessage &) {
|
|
return preparePinnedText();
|
|
};
|
|
|
|
auto prepareGameScore = [&](const MTPDmessageActionGameScore &) {
|
|
return prepareGameScoreText();
|
|
};
|
|
|
|
auto preparePhoneCall = [&](const MTPDmessageActionPhoneCall &) -> PreparedServiceText {
|
|
Unexpected("PhoneCall type in setServiceMessageFromMtp.");
|
|
};
|
|
|
|
auto preparePaymentSent = [&](const MTPDmessageActionPaymentSent &) {
|
|
return preparePaymentSentText();
|
|
};
|
|
|
|
auto prepareScreenshotTaken = [this](const MTPDmessageActionScreenshotTaken &) {
|
|
auto result = PreparedServiceText();
|
|
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 = PreparedServiceText();
|
|
result.text = { .text = qs(action.vmessage()) };
|
|
return result;
|
|
};
|
|
|
|
auto prepareBotAllowed = [&](const MTPDmessageActionBotAllowed &action) {
|
|
auto result = PreparedServiceText();
|
|
if (action.is_attach_menu()) {
|
|
result.text = {
|
|
tr::lng_action_attach_menu_bot_allowed(tr::now)
|
|
};
|
|
} else if (action.is_from_request()) {
|
|
result.text = {
|
|
tr::lng_action_webapp_bot_allowed(tr::now)
|
|
};
|
|
} else if (const auto app = action.vapp()) {
|
|
const auto bot = history()->peer->asUser();
|
|
const auto botId = bot ? bot->id : PeerId();
|
|
const auto info = history()->owner().processBotApp(botId, *app);
|
|
const auto url = (bot && info)
|
|
? history()->session().createInternalLinkFull(
|
|
bot->username() + '/' + info->shortName)
|
|
: QString();
|
|
result.text = tr::lng_action_bot_allowed_from_app(
|
|
tr::now,
|
|
lt_app,
|
|
(url.isEmpty()
|
|
? TextWithEntities{ u"App"_q }
|
|
: Ui::Text::Link(info->title, url)),
|
|
Ui::Text::WithEntities);
|
|
} else {
|
|
const auto domain = qs(action.vdomain().value_or_empty());
|
|
result.text = tr::lng_action_bot_allowed_from_domain(
|
|
tr::now,
|
|
lt_domain,
|
|
Ui::Text::Link(domain, u"http://"_q + domain),
|
|
Ui::Text::WithEntities);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
auto prepareSecureValuesSent = [&](const MTPDmessageActionSecureValuesSent &action) {
|
|
auto result = PreparedServiceText();
|
|
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](const MTPDmessageActionContactSignUp &data) {
|
|
auto result = PreparedServiceText();
|
|
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 = PreparedServiceText();
|
|
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 = PreparedServiceText();
|
|
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(
|
|
ParseInvitedToCallUsers(this, action.vusers().v),
|
|
linkCallId);
|
|
};
|
|
|
|
auto prepareSetMessagesTTL = [this](const MTPDmessageActionSetMessagesTTL &action) {
|
|
auto result = PreparedServiceText();
|
|
const auto period = action.vperiod().v;
|
|
const auto duration = (period == 5)
|
|
? u"5 seconds"_q
|
|
: Ui::FormatTTL(period);
|
|
if (const auto from = action.vauto_setting_from(); from && period) {
|
|
if (const auto peer = _from->owner().peer(peerFromUser(*from))) {
|
|
result.text = (peer->id == peer->session().userPeerId())
|
|
? tr::lng_action_ttl_global_me(
|
|
tr::now,
|
|
lt_duration,
|
|
{ .text = duration },
|
|
Ui::Text::WithEntities)
|
|
: tr::lng_action_ttl_global(
|
|
tr::now,
|
|
lt_from,
|
|
Ui::Text::Link(peer->name(), 1), // Link 1.
|
|
lt_duration,
|
|
{ .text = duration },
|
|
Ui::Text::WithEntities);
|
|
return result;
|
|
}
|
|
}
|
|
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 prepareGroupCallScheduled = [&](const MTPDmessageActionGroupCallScheduled &data) {
|
|
return prepareCallScheduledText(data.vschedule_date().v);
|
|
};
|
|
|
|
auto prepareSetChatTheme = [this](const MTPDmessageActionSetChatTheme &action) {
|
|
auto result = PreparedServiceText();
|
|
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 = PreparedServiceText();
|
|
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 = PreparedServiceText();
|
|
result.text = tr::lng_action_webview_data_done(
|
|
tr::now,
|
|
lt_text,
|
|
{ .text = qs(action.vtext()) },
|
|
Ui::Text::WithEntities);
|
|
return result;
|
|
};
|
|
|
|
auto prepareGiftPremium = [&](
|
|
const MTPDmessageActionGiftPremium &action) {
|
|
auto result = PreparedServiceText();
|
|
const auto isSelf = (_from->id == _from->session().userPeerId());
|
|
const auto peer = isSelf ? _history->peer : _from;
|
|
_history->session().giftBoxStickersPacks().load();
|
|
const auto amount = action.vamount().v;
|
|
const auto currency = qs(action.vcurrency());
|
|
result.links.push_back(peer->createOpenLink());
|
|
result.text = (isSelf
|
|
? tr::lng_action_gift_received_me
|
|
: tr::lng_action_gift_received)(
|
|
tr::now,
|
|
lt_user,
|
|
Ui::Text::Link(peer->name(), 1), // Link 1.
|
|
lt_cost,
|
|
{ Ui::FillAmountAndCurrency(amount, currency) },
|
|
Ui::Text::WithEntities);
|
|
return result;
|
|
};
|
|
|
|
auto prepareTopicCreate = [&](const MTPDmessageActionTopicCreate &action) {
|
|
auto result = PreparedServiceText();
|
|
const auto topicUrl = u"internal:url:https://t.me/c/%1/%2"_q
|
|
.arg(peerToChannel(_history->peer->id).bare)
|
|
.arg(id.bare);
|
|
result.text = tr::lng_action_topic_created(
|
|
tr::now,
|
|
lt_topic,
|
|
Ui::Text::Link(
|
|
Data::ForumTopicIconWithTitle(
|
|
id,
|
|
action.vicon_emoji_id().value_or_empty(),
|
|
qs(action.vtitle())),
|
|
topicUrl),
|
|
Ui::Text::WithEntities);
|
|
return result;
|
|
};
|
|
|
|
auto prepareTopicEdit = [&](const MTPDmessageActionTopicEdit &action) {
|
|
auto result = PreparedServiceText();
|
|
if (const auto closed = action.vclosed()) {
|
|
result.text = { mtpIsTrue(*closed)
|
|
? tr::lng_action_topic_closed_inside(tr::now)
|
|
: tr::lng_action_topic_reopened_inside(tr::now) };
|
|
} else if (const auto hidden = action.vhidden()) {
|
|
result.text = { mtpIsTrue(*hidden)
|
|
? tr::lng_action_topic_hidden_inside(tr::now)
|
|
: tr::lng_action_topic_unhidden_inside(tr::now) };
|
|
} else if (!action.vtitle()) {
|
|
if (const auto icon = action.vicon_emoji_id()) {
|
|
if (const auto iconId = icon->v) {
|
|
result.links.push_back(fromLink());
|
|
result.text = tr::lng_action_topic_icon_changed(
|
|
tr::now,
|
|
lt_from,
|
|
fromLinkText(), // Link 1.
|
|
lt_link,
|
|
{ tr::lng_action_topic_placeholder(tr::now) },
|
|
lt_emoji,
|
|
Data::SingleCustomEmoji(iconId),
|
|
Ui::Text::WithEntities);
|
|
} else {
|
|
result.links.push_back(fromLink());
|
|
result.text = tr::lng_action_topic_icon_removed(
|
|
tr::now,
|
|
lt_from,
|
|
fromLinkText(), // Link 1.
|
|
lt_link,
|
|
{ tr::lng_action_topic_placeholder(tr::now) },
|
|
Ui::Text::WithEntities);
|
|
}
|
|
}
|
|
} else {
|
|
result.links.push_back(fromLink());
|
|
result.text = tr::lng_action_topic_renamed(
|
|
tr::now,
|
|
lt_from,
|
|
fromLinkText(), // Link 1.
|
|
lt_link,
|
|
{ tr::lng_action_topic_placeholder(tr::now) },
|
|
lt_title,
|
|
Data::ForumTopicIconWithTitle(
|
|
topicRootId(),
|
|
action.vicon_emoji_id().value_or_empty(),
|
|
qs(*action.vtitle())),
|
|
Ui::Text::WithEntities);
|
|
}
|
|
if (result.text.empty()) {
|
|
result.text = { tr::lng_message_empty(tr::now) };
|
|
}
|
|
return result;
|
|
};
|
|
|
|
auto prepareSuggestProfilePhoto = [this](const MTPDmessageActionSuggestProfilePhoto &action) {
|
|
auto result = PreparedServiceText{};
|
|
const auto isSelf = (_from->id == _from->session().userPeerId());
|
|
const auto isVideo = action.vphoto().match([&](const MTPDphoto &data) {
|
|
return data.vvideo_sizes().has_value()
|
|
&& !data.vvideo_sizes()->v.isEmpty();
|
|
}, [](const MTPDphotoEmpty &) {
|
|
return false;
|
|
});
|
|
const auto peer = isSelf ? history()->peer : _from;
|
|
const auto user = peer->asUser();
|
|
const auto name = (user && !user->firstName.isEmpty())
|
|
? user->firstName
|
|
: peer->name();
|
|
result.links.push_back(peer->createOpenLink());
|
|
result.text = (isSelf
|
|
? (isVideo
|
|
? tr::lng_action_suggested_video_me
|
|
: tr::lng_action_suggested_photo_me)
|
|
: (isVideo
|
|
? tr::lng_action_suggested_video
|
|
: tr::lng_action_suggested_photo))(
|
|
tr::now,
|
|
lt_user,
|
|
Ui::Text::Link(name, 1), // Link 1.
|
|
Ui::Text::WithEntities);
|
|
return result;
|
|
};
|
|
|
|
auto prepareRequestedPeer = [&](
|
|
const MTPDmessageActionRequestedPeer &action) {
|
|
auto result = PreparedServiceText{};
|
|
result.links.push_back(fromLink());
|
|
|
|
const auto &list = action.vpeers().v;
|
|
for (auto i = 0, count = int(list.size()); i != count; ++i) {
|
|
const auto id = peerFromMTP(list[i]);
|
|
|
|
auto user = _history->owner().peer(id);
|
|
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 == count) {
|
|
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_shared_chat_with_bot(
|
|
tr::now,
|
|
lt_chat,
|
|
result.text,
|
|
lt_bot,
|
|
Ui::Text::Link(history()->peer->name(), 2),
|
|
Ui::Text::WithEntities);
|
|
return result;
|
|
};
|
|
|
|
auto prepareSetChatWallPaper = [&](
|
|
const MTPDmessageActionSetChatWallPaper &action) {
|
|
const auto isSelf = (_from->id == _from->session().userPeerId());
|
|
const auto same = action.is_same();
|
|
const auto both = action.is_for_both();
|
|
const auto peer = isSelf ? history()->peer : _from;
|
|
const auto user = peer->asUser();
|
|
const auto name = (user && !user->firstName.isEmpty())
|
|
? user->firstName
|
|
: peer->name();
|
|
auto result = PreparedServiceText{};
|
|
if (!isSelf) {
|
|
result.links.push_back(peer->createOpenLink());
|
|
}
|
|
result.text = isSelf
|
|
? ((!same && both)
|
|
? tr::lng_action_set_wallpaper_both_me(
|
|
tr::now,
|
|
lt_user,
|
|
Ui::Text::Link(Ui::Text::Bold(name), 1),
|
|
Ui::Text::WithEntities)
|
|
: (same
|
|
? tr::lng_action_set_same_wallpaper_me
|
|
: tr::lng_action_set_wallpaper_me)(
|
|
tr::now,
|
|
Ui::Text::WithEntities))
|
|
: (same
|
|
? tr::lng_action_set_same_wallpaper
|
|
: tr::lng_action_set_wallpaper)(
|
|
tr::now,
|
|
lt_user,
|
|
Ui::Text::Link(Ui::Text::Bold(name), 1),
|
|
Ui::Text::WithEntities);
|
|
return result;
|
|
};
|
|
|
|
auto prepareGiftCode = [&](const MTPDmessageActionGiftCode &action) {
|
|
auto result = PreparedServiceText();
|
|
_history->session().giftBoxStickersPacks().load();
|
|
if (const auto boosted = action.vboost_peer()) {
|
|
result.text = {
|
|
(action.is_unclaimed()
|
|
? tr::lng_prize_unclaimed_about
|
|
: action.is_via_giveaway()
|
|
? tr::lng_prize_about
|
|
: tr::lng_prize_gift_about)(
|
|
tr::now,
|
|
lt_channel,
|
|
_from->owner().peer(
|
|
peerFromMTP(*action.vboost_peer()))->name()),
|
|
};
|
|
} else {
|
|
const auto isSelf = (_from->id == _from->session().userPeerId());
|
|
const auto peer = isSelf ? _history->peer : _from;
|
|
result.links.push_back(peer->createOpenLink());
|
|
result.text = (isSelf
|
|
? tr::lng_action_gift_received_me
|
|
: tr::lng_action_gift_received)(
|
|
tr::now,
|
|
lt_user,
|
|
Ui::Text::Link(peer->name(), 1), // Link 1.
|
|
lt_cost,
|
|
{ Ui::FillAmountAndCurrency(
|
|
action.vamount().value_or_empty(),
|
|
qs(action.vcurrency().value_or_empty())) },
|
|
Ui::Text::WithEntities);
|
|
|
|
}
|
|
return result;
|
|
};
|
|
|
|
auto prepareGiveawayLaunch = [&](const MTPDmessageActionGiveawayLaunch &action) {
|
|
auto result = PreparedServiceText();
|
|
result.links.push_back(fromLink());
|
|
result.text = (_history->peer->isMegagroup()
|
|
? tr::lng_action_giveaway_started_group
|
|
: tr::lng_action_giveaway_started)(
|
|
tr::now,
|
|
lt_from,
|
|
fromLinkText(), // Link 1.
|
|
Ui::Text::WithEntities);
|
|
return result;
|
|
};
|
|
|
|
auto prepareGiveawayResults = [&](const MTPDmessageActionGiveawayResults &action) {
|
|
auto result = PreparedServiceText();
|
|
const auto winners = action.vwinners_count().v;
|
|
const auto unclaimed = action.vunclaimed_count().v;
|
|
result.text = {
|
|
(!winners
|
|
? tr::lng_action_giveaway_results_none(tr::now)
|
|
: unclaimed
|
|
? tr::lng_action_giveaway_results_some(tr::now)
|
|
: tr::lng_action_giveaway_results(
|
|
tr::now,
|
|
lt_count,
|
|
winners))
|
|
};
|
|
return result;
|
|
};
|
|
|
|
auto prepareBoostApply = [&](const MTPDmessageActionBoostApply &action) {
|
|
auto result = PreparedServiceText();
|
|
const auto boosts = action.vboosts().v;
|
|
result.links.push_back(fromLink());
|
|
result.text = tr::lng_action_boost_apply(
|
|
tr::now,
|
|
lt_count,
|
|
boosts,
|
|
lt_from,
|
|
fromLinkText(), // Link 1.
|
|
Ui::Text::WithEntities);
|
|
return result;
|
|
};
|
|
|
|
setServiceText(action.match(
|
|
prepareChatAddUserText,
|
|
prepareChatJoinedByLink,
|
|
prepareChatCreate,
|
|
PrepareEmptyText<MTPDmessageActionChatMigrateTo>,
|
|
PrepareEmptyText<MTPDmessageActionChannelMigrateFrom>,
|
|
PrepareEmptyText<MTPDmessageActionHistoryClear>,
|
|
prepareChannelCreate,
|
|
prepareChatDeletePhoto,
|
|
prepareChatDeleteUser,
|
|
prepareChatEditPhoto,
|
|
prepareChatEditTitle,
|
|
preparePinMessage,
|
|
prepareGameScore,
|
|
preparePhoneCall,
|
|
preparePaymentSent,
|
|
prepareScreenshotTaken,
|
|
prepareCustomAction,
|
|
prepareBotAllowed,
|
|
prepareSecureValuesSent,
|
|
prepareContactSignUp,
|
|
prepareProximityReached,
|
|
PrepareErrorText<MTPDmessageActionPaymentSentMe>,
|
|
PrepareErrorText<MTPDmessageActionSecureValuesSentMe>,
|
|
prepareGroupCall,
|
|
prepareInviteToGroupCall,
|
|
prepareSetMessagesTTL,
|
|
prepareGroupCallScheduled,
|
|
prepareSetChatTheme,
|
|
prepareChatJoinedByRequest,
|
|
prepareWebViewDataSent,
|
|
prepareGiftPremium,
|
|
prepareTopicCreate,
|
|
prepareTopicEdit,
|
|
PrepareErrorText<MTPDmessageActionWebViewDataSentMe>,
|
|
prepareSuggestProfilePhoto,
|
|
prepareRequestedPeer,
|
|
prepareSetChatWallPaper,
|
|
prepareGiftCode,
|
|
prepareGiveawayLaunch,
|
|
prepareGiveawayResults,
|
|
prepareBoostApply,
|
|
PrepareErrorText<MTPDmessageActionEmpty>));
|
|
|
|
// Additional information.
|
|
applyAction(action);
|
|
}
|
|
|
|
void HistoryItem::applyAction(const MTPMessageAction &action) {
|
|
action.match([&](const MTPDmessageActionChatAddUser &data) {
|
|
if (const auto channel = _history->peer->asMegagroup()) {
|
|
const auto selfUserId = _history->session().userId();
|
|
for (const auto &item : data.vusers().v) {
|
|
if (peerFromUser(item) == selfUserId) {
|
|
channel->mgInfo->joinedMessageFound = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}, [&](const MTPDmessageActionChatJoinedByLink &data) {
|
|
if (_from->isSelf()) {
|
|
if (const auto channel = _history->peer->asMegagroup()) {
|
|
channel->mgInfo->joinedMessageFound = true;
|
|
}
|
|
}
|
|
}, [&](const MTPDmessageActionChatEditPhoto &data) {
|
|
data.vphoto().match([&](const MTPDphoto &photo) {
|
|
_media = std::make_unique<Data::MediaPhoto>(
|
|
this,
|
|
_history->peer,
|
|
_history->owner().processPhoto(photo));
|
|
}, [](const MTPDphotoEmpty &) {
|
|
});
|
|
}, [&](const MTPDmessageActionChatCreate &) {
|
|
_flags |= MessageFlag::IsGroupEssential;
|
|
}, [&](const MTPDmessageActionChannelCreate &) {
|
|
_flags |= MessageFlag::IsGroupEssential;
|
|
}, [&](const MTPDmessageActionChatMigrateTo &) {
|
|
_flags |= MessageFlag::IsGroupEssential;
|
|
}, [&](const MTPDmessageActionChannelMigrateFrom &) {
|
|
_flags |= MessageFlag::IsGroupEssential;
|
|
}, [&](const MTPDmessageActionContactSignUp &) {
|
|
_flags |= MessageFlag::IsContactSignUp;
|
|
}, [&](const MTPDmessageActionChatJoinedByRequest &data) {
|
|
if (_from->isSelf()) {
|
|
if (const auto channel = _history->peer->asMegagroup()) {
|
|
channel->mgInfo->joinedMessageFound = true;
|
|
}
|
|
}
|
|
}, [&](const MTPDmessageActionGiftPremium &data) {
|
|
_media = std::make_unique<Data::MediaGiftBox>(
|
|
this,
|
|
_from,
|
|
data.vmonths().v);
|
|
}, [&](const MTPDmessageActionSuggestProfilePhoto &data) {
|
|
data.vphoto().match([&](const MTPDphoto &photo) {
|
|
_flags |= MessageFlag::IsUserpicSuggestion;
|
|
_media = std::make_unique<Data::MediaPhoto>(
|
|
this,
|
|
history()->peer,
|
|
history()->owner().processPhoto(photo));
|
|
}, [](const MTPDphotoEmpty &) {
|
|
});
|
|
}, [&](const MTPDmessageActionSetChatWallPaper &data) {
|
|
if (!data.is_same()) {
|
|
using namespace Data;
|
|
const auto session = &history()->session();
|
|
const auto &attached = data.vwallpaper();
|
|
if (const auto paper = WallPaper::Create(session, attached)) {
|
|
_media = std::make_unique<MediaWallPaper>(
|
|
this,
|
|
*paper,
|
|
data.is_for_both());
|
|
}
|
|
}
|
|
}, [&](const MTPDmessageActionGiftCode &data) {
|
|
const auto boostedId = data.vboost_peer()
|
|
? peerToChannel(peerFromMTP(*data.vboost_peer()))
|
|
: ChannelId();
|
|
_media = std::make_unique<Data::MediaGiftBox>(
|
|
this,
|
|
_from,
|
|
Data::GiftCode{
|
|
.slug = qs(data.vslug()),
|
|
.channel = (boostedId
|
|
? history()->owner().channel(boostedId).get()
|
|
: nullptr),
|
|
.months = data.vmonths().v,
|
|
.viaGiveaway = data.is_via_giveaway(),
|
|
.unclaimed = data.is_unclaimed(),
|
|
});
|
|
}, [](const auto &) {
|
|
});
|
|
}
|
|
|
|
void HistoryItem::setSelfDestruct(
|
|
HistoryServiceSelfDestruct::Type type,
|
|
MTPint mtpTTLvalue) {
|
|
UpdateComponents(HistoryServiceSelfDestruct::Bit());
|
|
const auto selfdestruct = Get<HistoryServiceSelfDestruct>();
|
|
if (mtpTTLvalue.v == TimeId(0x7FFFFFFF)) {
|
|
selfdestruct->timeToLive = TimeToLiveSingleView();
|
|
} else {
|
|
selfdestruct->timeToLive = mtpTTLvalue.v * crl::time(1000);
|
|
}
|
|
selfdestruct->type = type;
|
|
}
|
|
|
|
PreparedServiceText HistoryItem::prepareInvitedToCallText(
|
|
const std::vector<not_null<UserData*>> &users,
|
|
CallId linkCallId) {
|
|
auto chatText = tr::lng_action_invite_user_chat(
|
|
tr::now,
|
|
Ui::Text::WithEntities);
|
|
auto result = PreparedServiceText();
|
|
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 = users[0];
|
|
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.empty()) {
|
|
result.text = tr::lng_action_invite_user(
|
|
tr::now,
|
|
lt_from,
|
|
fromLinkText(), // Link 1.
|
|
lt_user,
|
|
{ .text = u"somebody"_q },
|
|
lt_chat,
|
|
chatText,
|
|
Ui::Text::WithEntities);
|
|
} else {
|
|
for (auto i = 0, l = int(users.size()); i != l; ++i) {
|
|
const auto user = users[i];
|
|
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;
|
|
}
|
|
|
|
PreparedServiceText HistoryItem::preparePinnedText() {
|
|
auto result = PreparedServiceText();
|
|
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->translatedText();
|
|
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::Link(
|
|
Ui::Text::Filtered(
|
|
std::move(original),
|
|
{
|
|
EntityType::Spoiler,
|
|
EntityType::StrikeOut,
|
|
EntityType::Italic,
|
|
EntityType::CustomEmoji,
|
|
}),
|
|
2);
|
|
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;
|
|
}
|
|
|
|
PreparedServiceText HistoryItem::prepareGameScoreText() {
|
|
auto result = PreparedServiceText();
|
|
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;
|
|
}
|
|
|
|
PreparedServiceText HistoryItem::preparePaymentSentText() {
|
|
auto result = PreparedServiceText();
|
|
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;
|
|
}
|
|
|
|
PreparedServiceText HistoryItem::prepareStoryMentionText() {
|
|
auto result = PreparedServiceText();
|
|
const auto peer = history()->peer;
|
|
result.links.push_back(peer->createOpenLink());
|
|
const auto phrase = (this->media() && this->media()->storyExpired(true))
|
|
? (out()
|
|
? tr::lng_action_story_mention_me_unavailable
|
|
: tr::lng_action_story_mention_unavailable)
|
|
: (out()
|
|
? tr::lng_action_story_mention_me
|
|
: tr::lng_action_story_mention);
|
|
result.text = phrase(
|
|
tr::now,
|
|
lt_user,
|
|
Ui::Text::Wrapped(
|
|
Ui::Text::Bold(peer->shortName()),
|
|
EntityType::CustomUrl,
|
|
u"internal:index"_q + QChar(1)),
|
|
Ui::Text::WithEntities);
|
|
return result;
|
|
}
|
|
|
|
PreparedServiceText HistoryItem::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 = PreparedServiceText();
|
|
const auto prepareWithDate = [&](const QString &date) {
|
|
if (_history->peer->isBroadcast()) {
|
|
result.text = tr::lng_action_group_call_scheduled_channel(
|
|
tr::now,
|
|
lt_date,
|
|
{ .text = date },
|
|
Ui::Text::WithEntities);
|
|
} else {
|
|
result.links.push_back(fromLink());
|
|
result.text = tr::lng_action_group_call_scheduled_group(
|
|
tr::now,
|
|
lt_from,
|
|
fromLinkText(), // Link 1.
|
|
lt_date,
|
|
{ .text = date },
|
|
Ui::Text::WithEntities);
|
|
}
|
|
};
|
|
const auto time = QLocale().toString(
|
|
scheduled.time(),
|
|
QLocale::ShortFormat);
|
|
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([=] {
|
|
updateServiceText(prepareCallScheduledText(scheduleDate));
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
TextWithEntities HistoryItem::fromLinkText() const {
|
|
return Ui::Text::Link(_from->name(), 1);
|
|
}
|
|
|
|
ClickHandlerPtr HistoryItem::fromLink() const {
|
|
return _from->createOpenLink();
|
|
}
|
|
|
|
crl::time HistoryItem::getSelfDestructIn(crl::time now) {
|
|
if (const auto selfdestruct = Get<HistoryServiceSelfDestruct>()) {
|
|
const auto at = std::get_if<crl::time>(&selfdestruct->destructAt);
|
|
if (at && (*at) > 0) {
|
|
const auto destruct = *at;
|
|
if (destruct <= now) {
|
|
auto text = [&] {
|
|
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 destruct - now;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void HistoryItem::cacheOnlyEmojiAndSpaces(bool only) {
|
|
_flags |= MessageFlag::OnlyEmojiAndSpacesSet;
|
|
if (only) {
|
|
_flags |= MessageFlag::OnlyEmojiAndSpaces;
|
|
} else {
|
|
_flags &= ~MessageFlag::OnlyEmojiAndSpaces;
|
|
}
|
|
}
|
|
|
|
bool HistoryItem::isOnlyEmojiAndSpaces() const {
|
|
if (!(_flags & MessageFlag::OnlyEmojiAndSpacesSet)) {
|
|
const_cast<HistoryItem*>(this)->cacheOnlyEmojiAndSpaces(
|
|
!HasNotEmojiAndSpaces(_text.text));
|
|
}
|
|
return (_flags & MessageFlag::OnlyEmojiAndSpaces);
|
|
}
|
|
|
|
void HistoryItem::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 HistoryItem::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(
|
|
controller->uiShow(),
|
|
peer);
|
|
if (validator.can()) {
|
|
validator.showBox();
|
|
}
|
|
}
|
|
});
|
|
|
|
UpdateComponents(HistoryServiceTTLChange::Bit());
|
|
Get<HistoryServiceTTLChange>()->link = std::move(link);
|
|
}
|
|
|
|
void HistoryItem::clearDependencyMessage() {
|
|
if (const auto dependent = GetServiceDependentData()) {
|
|
if (dependent->msg) {
|
|
_history->owner().unregisterDependentMessage(
|
|
this,
|
|
dependent->msg);
|
|
dependent->msg = nullptr;
|
|
dependent->msgId = 0;
|
|
}
|
|
}
|
|
}
|