diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 8a368d58e2..253073f842 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -591,6 +591,8 @@ PRIVATE data/business/data_shortcut_messages.h data/components/credits.cpp data/components/credits.h + data/components/ephemeral_messages.cpp + data/components/ephemeral_messages.h data/components/factchecks.cpp data/components/factchecks.h data/components/gift_auctions.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 57e73e38df..2a20534075 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4917,6 +4917,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_from_you" = "You"; "lng_from_draft" = "Draft"; "lng_bot_description" = "What can this bot do?"; +"lng_ephemeral_visible_you" = "Only visible to you"; +"lng_ephemeral_visible_to" = "Only visible to {user}"; +"lng_ephemeral_about" = "Only you see this message. It will disappear after you restart the app."; +"lng_ephemeral_command_tooltip" = "Other members won't see this message"; +"lng_ephemeral_reply_text_only" = "Only text messages can be sent in reply to this message."; +"lng_ephemeral_cant_schedule" = "Ephemeral messages can't be scheduled."; "lng_unblock_button" = "Unblock"; "lng_restart_button" = "Restart"; "lng_channel_mute" = "Mute"; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 4dbe96861f..162228bfbc 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/stickers_dice_pack.h" #include "data/business/data_shortcut_messages.h" #include "data/components/credits.h" +#include "data/components/ephemeral_messages.h" #include "data/components/gift_auctions.h" #include "data/components/promo_suggestions.h" #include "data/components/scheduled_messages.h" @@ -1877,6 +1878,21 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().scheduledMessages().apply(d); } break; + case mtpc_updateNewEphemeralMessage: { + const auto &d = update.c_updateNewEphemeralMessage(); + session().ephemeralMessages().apply(d); + } break; + + case mtpc_updateEditEphemeralMessage: { + const auto &d = update.c_updateEditEphemeralMessage(); + session().ephemeralMessages().apply(d); + } break; + + case mtpc_updateDeleteEphemeralMessages: { + const auto &d = update.c_updateDeleteEphemeralMessages(); + session().ephemeralMessages().apply(d); + } break; + case mtpc_updateQuickReplies: { const auto &d = update.c_updateQuickReplies(); session().data().shortcutMessages().apply(d); diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 5046fd14ae..ebd0eda774 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_websites.h" #include "data/business/data_shortcut_messages.h" #include "data/components/credits.h" +#include "data/components/ephemeral_messages.h" #include "data/components/scheduled_messages.h" #include "data/notify/data_notify_settings.h" #include "data/data_changes.h" @@ -2342,6 +2343,12 @@ mtpRequestId ApiWrap::savePreparedDraftToCloud( }) : std::shared_ptr(); + auto reply = draft.reply; + const auto replyItem = _session->data().message(reply.messageId); + if (replyItem && replyItem->isEphemeral()) { + reply.messageId = FullMsgId(); + } + auto flags = MTPmessages_SaveDraft::Flags(0); const auto &textWithTags = draft.textWithTags; if (draft.webpage.removed) { @@ -2349,9 +2356,9 @@ mtpRequestId ApiWrap::savePreparedDraftToCloud( } else if (!draft.webpage.url.isEmpty()) { flags |= MTPmessages_SaveDraft::Flag::f_media; } - if (draft.reply.messageId - || draft.reply.topicRootId - || draft.reply.monoforumPeerId) { + if (reply.messageId + || reply.topicRootId + || reply.monoforumPeerId) { flags |= MTPmessages_SaveDraft::Flag::f_reply_to; } if (!textWithTags.tags.isEmpty()) { @@ -2454,7 +2461,7 @@ mtpRequestId ApiWrap::savePreparedDraftToCloud( bool refreshed) -> mtpRequestId { const auto requestId = request(MTPmessages_SaveDraft( MTP_flags(flags), - ReplyToForMTP(history, draft.reply), + ReplyToForMTP(history, reply), history->peer->input(), MTP_string(textWithTags.text), entities, @@ -4181,7 +4188,8 @@ void ApiWrap::sendFiles( Ui::PreparedList &&list, SendMediaType type, std::shared_ptr album, - const SendAction &action) { + SendAction action) { + StripEphemeralReply(_session, action.replyTo); const auto to = FileLoadTaskOptions(action); if (album) { album->options = to.options; @@ -4569,8 +4577,15 @@ void ApiWrap::sendMessage( ? replyTo->topicRootId() : Data::ForumTopic::kGeneralId; const auto topic = peer->forumTopicFor(topicRootId); - if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer)) - || Api::SendDice(message)) { + if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer))) { + return; + } else if (_session->ephemeralMessages().trySend(message)) { + if (clearCloudDraft) { + history->clearCloudDraft(draftTopicRootId, draftMonoforumPeerId); + } + return; + } + if (Api::SendDice(message)) { return; } local().saveRecentSentHashtags(textWithTags.text); @@ -4859,6 +4874,7 @@ void ApiWrap::sendInlineResult( SendAction action, std::optional localMessageId, Fn done) { + StripEphemeralReply(_session, action.replyTo); sendAction(action); const auto history = action.history; diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index cbc9a1328d..568841fde9 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -364,7 +364,7 @@ public: Ui::PreparedList &&list, SendMediaType type, std::shared_ptr album, - const SendAction &action); + SendAction action); void sendFile( const QByteArray &fileContent, SendMediaType type, diff --git a/Telegram/SourceFiles/data/components/ephemeral_messages.cpp b/Telegram/SourceFiles/data/components/ephemeral_messages.cpp new file mode 100644 index 0000000000..dd00701bf5 --- /dev/null +++ b/Telegram/SourceFiles/data/components/ephemeral_messages.cpp @@ -0,0 +1,462 @@ +/* +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 "data/components/ephemeral_messages.h" + +#include "api/api_common.h" +#include "api/api_text_entities.h" +#include "apiwrap.h" +#include "base/random.h" +#include "base/unixtime.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_peer_bot_command.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_edition.h" +#include "main/main_session.h" + +namespace Data { +namespace { + +constexpr auto kKeepDuration = TimeId(2 * 86400); +constexpr auto kPruneEach = 3600 * crl::time(1000); +constexpr auto kPendingFlushDelay = 2 * crl::time(1000); + +struct ParsedCommand { + QString command; + QString username; +}; + +[[nodiscard]] std::optional ParseCommand( + const QString &text) { + if (!text.startsWith(QChar('/'))) { + return std::nullopt; + } + const auto size = int(text.size()); + const auto good = [](QChar ch) { + return ch.isLetterOrNumber() || (ch == QChar('_')); + }; + auto i = 1; + while (i < size && good(text[i])) { + ++i; + } + auto result = ParsedCommand{ .command = text.mid(1, i - 1) }; + if (result.command.isEmpty()) { + return std::nullopt; + } + if (i < size && text[i] == QChar('@')) { + const auto from = ++i; + while (i < size && good(text[i])) { + ++i; + } + result.username = text.mid(from, i - from); + if (result.username.isEmpty()) { + return std::nullopt; + } + } + if (i < size && !text[i].isSpace()) { + return std::nullopt; + } + return result; +} + +} // namespace + +EphemeralMessages::EphemeralMessages(not_null session) +: _session(session) +, _pruneTimer([=] { pruneOld(); }) +, _pendingTimer([=] { drainPending(true); }) { + _session->data().itemRemoved( + ) | rpl::on_next([=](not_null item) { + if (item->isEphemeral()) { + itemRemoved(item); + } + }, _lifetime); + + _pruneTimer.callEach(kPruneEach); +} + +EphemeralMessages::~EphemeralMessages() = default; + +void EphemeralMessages::apply(const MTPDupdateNewEphemeralMessage &update) { + applyOrDefer(update.vmessage()); +} + +void EphemeralMessages::applyOrDefer(const MTPEphemeralMessage &message) { + if (replyTargetMissing(message.data())) { + _pending.push_back(message); + _pendingTimer.callOnce(kPendingFlushDelay); + return; + } + applyNew(message.data()); + drainPending(); +} + +bool EphemeralMessages::replyTargetMissing( + const MTPDephemeralMessage &data) const { + const auto header = data.vreply_to(); + if (!header) { + return false; + } + return header->match([&](const MTPDmessageReplyHeader &reply) { + const auto replyToId = reply.vreply_to_msg_id(); + if (!reply.is_reply_to_ephemeral() || !replyToId) { + return false; + } + const auto history = _session->data().history( + peerFromMTP(data.vpeer_id())); + return !lookupItem(history->peer, replyToId->v); + }, [](const MTPDmessageReplyStoryHeader &) { + return false; + }); +} + +void EphemeralMessages::drainPending(bool force) { + while (!_pending.empty()) { + const auto i = ranges::find_if(_pending, [&]( + const MTPEphemeralMessage &message) { + return force || !replyTargetMissing(message.data()); + }); + if (i == end(_pending)) { + return; + } + const auto message = *i; + _pending.erase(i); + applyNew(message.data()); + } +} + +void EphemeralMessages::apply(const MTPDupdateEditEphemeralMessage &update) { + const auto &data = update.vmessage().data(); + const auto history = _session->data().history( + peerFromMTP(data.vpeer_id())); + const auto item = lookupItem(history->peer, data.vid().v); + if (!item) { + applyNew(data); + return; + } + auto edition = HistoryMessageEdition(); + edition.isEditHide = true; + edition.editDate = -1; + edition.useSameViews = true; + edition.useSameForwards = true; + edition.useSameReplies = true; + edition.useSameReactions = true; + edition.useSameSuggest = true; + edition.textWithEntities = { + qs(data.vmessage()), + Api::EntitiesFromMTP( + _session, + data.ventities().value_or_empty()), + }; + edition.replyMarkup = HistoryMessageMarkupData(data.vreply_markup()); + edition.mtpMedia = data.vmedia(); + item->applyEdition(std::move(edition)); +} + +void EphemeralMessages::apply( + const MTPDupdateDeleteEphemeralMessages &update) { + const auto history = _session->data().historyLoaded( + peerFromMTP(update.vpeer())); + if (!history) { + return; + } + for (const auto &id : update.vids().v) { + if (const auto item = lookupItem(history->peer, id.v)) { + item->destroy(); + } + } +} + +HistoryItem *EphemeralMessages::applyNew(const MTPDephemeralMessage &data) { + const auto history = _session->data().history( + peerFromMTP(data.vpeer_id())); + const auto ephemeralId = data.vid().v; + if (const auto existing = lookupItem(history->peer, ephemeralId)) { + return existing; + } + const auto fromId = peerFromMTP(data.vfrom_id()); + auto replyTo = FullReplyTo(); + if (const auto topMsgId = data.vtop_msg_id()) { + replyTo.topicRootId = topMsgId->v; + } + if (const auto header = data.vreply_to()) { + header->match([&](const MTPDmessageReplyHeader &reply) { + const auto replyToId = reply.vreply_to_msg_id(); + if (reply.is_reply_to_ephemeral() && replyToId) { + const auto target = lookupItem(history->peer, replyToId->v); + if (target) { + replyTo.messageId = target->fullId(); + } + } + }, [](const MTPDmessageReplyStoryHeader &) { + }); + } + const auto item = history->addNewLocalMessage( + { + .id = _session->data().nextLocalMessageId(), + .flags = (MessageFlag::HistoryEntry + | MessageFlag::Ephemeral + | (data.is_out() ? MessageFlag::Outgoing : MessageFlag()) + | (fromId ? MessageFlag::HasFromId : MessageFlag()) + | (replyTo.messageId + ? MessageFlag::HasReplyInfo + : MessageFlag()) + | (data.vreply_markup() + ? MessageFlag::HasReplyMarkup + : MessageFlag())), + .from = fromId, + .replyTo = replyTo, + .date = data.vdate().v, + .markup = HistoryMessageMarkupData(data.vreply_markup()), + }, + TextWithEntities { + qs(data.vmessage()), + Api::EntitiesFromMTP( + _session, + data.ventities().value_or_empty()), + }, + data.vmedia() + ? *data.vmedia() + : MTPMessageMedia(MTP_messageMediaEmpty())); + _data[history].push_back({ + .ephemeralId = ephemeralId, + .receiverId = UserId(data.vreceiver_id()), + .item = item, + }); + _session->data().requestItemResize(item); + return item; +} + +HistoryItem *EphemeralMessages::lookupItem( + not_null peer, + int32 ephemeralId) const { + const auto history = _session->data().historyLoaded(peer); + if (!history) { + return nullptr; + } + const auto i = _data.find(history); + if (i == end(_data)) { + return nullptr; + } + const auto j = ranges::find( + i->second, + ephemeralId, + &Entry::ephemeralId); + return (j != end(i->second)) ? j->item.get() : nullptr; +} + +int32 EphemeralMessages::lookupId(not_null item) const { + const auto entry = findByItem(item); + return entry ? entry->ephemeralId : 0; +} + +UserData *EphemeralMessages::replyReceiver( + not_null item) const { + if (const auto entry = findByItem(item)) { + return botForSending(*entry); + } + if (item->out() && item->isEphemeral()) { + const auto target = _session->data().message( + item->replyTo().messageId); + if (target && target->isEphemeral() && !target->out()) { + const auto from = target->from(); + return from ? from->asUser() : nullptr; + } + } + return nullptr; +} + +bool EphemeralMessages::wouldSend(const Api::MessageToSend &message) const { + const auto history = message.action.history; + const auto peer = history->peer; + if (!peer->isChat() && !peer->isMegagroup()) { + return false; + } + const auto replyTo = _session->data().message( + message.action.replyTo.messageId); + if (replyTo && replyTo->isEphemeral()) { + return true; + } else if (message.action.replyTo) { + return false; + } + return findCommandBot(peer, message.textWithTags.text.trimmed()) + != nullptr; +} + +bool EphemeralMessages::trySend(const Api::MessageToSend &message) { + const auto history = message.action.history; + const auto peer = history->peer; + if (!peer->isChat() && !peer->isMegagroup()) { + return false; + } else if (message.action.options.scheduled + || message.action.options.shortcutId) { + if (wouldSend(message)) { + LOG(("API Error: " + "Dropping a scheduled ephemeral message send.")); + return true; + } + return false; + } + auto text = TextWithEntities{ + message.textWithTags.text, + TextUtilities::ConvertTextTagsToEntities(message.textWithTags.tags), + }; + TextUtilities::Trim(text); + if (text.text.isEmpty()) { + return false; + } + const auto replyToId = message.action.replyTo.messageId; + if (const auto replyTo = _session->data().message(replyToId)) { + if (replyTo->isEphemeral()) { + const auto entry = findByItem(replyTo); + const auto bot = entry ? botForSending(*entry) : nullptr; + if (bot) { + send(history, bot, std::move(text), entry->ephemeralId); + } + return true; + } + } + if (message.action.replyTo) { + return false; + } + const auto bot = findCommandBot(peer, text.text); + if (!bot) { + return false; + } + send(history, bot, std::move(text)); + return true; +} + +UserData *EphemeralMessages::findCommandBot( + not_null peer, + const QString &text) const { + const auto parsed = ParseCommand(text); + if (!parsed) { + return nullptr; + } + const auto &commands = peer->isChat() + ? peer->asChat()->botCommands() + : peer->asMegagroup()->mgInfo->botCommands(); + for (const auto &[userId, list] : commands) { + const auto user = _session->data().userLoaded(userId); + if (!user + || (!parsed->username.isEmpty() + && parsed->username.compare( + user->username(), + Qt::CaseInsensitive) != 0)) { + continue; + } + for (const auto &command : list) { + if (command.ephemeral + && !command.command.compare( + parsed->command, + Qt::CaseInsensitive)) { + return user; + } + } + } + return nullptr; +} + +void EphemeralMessages::send( + not_null history, + not_null bot, + TextWithEntities text, + int32 replyToEphemeralId) { + const auto session = _session; + const auto entities = Api::EntitiesToMTP( + session, + text.entities, + Api::ConvertOption::SkipLocal); + using Flag = MTPephemeral_SendMessage::Flag; + const auto flags = Flag(0) + | (entities.v.isEmpty() ? Flag(0) : Flag::f_entities) + | (replyToEphemeralId ? Flag::f_reply_to : Flag(0)); + session->api().request(MTPephemeral_SendMessage( + MTP_flags(flags), + history->peer->input(), + bot->inputUser(), + MTPlong(), // query_id + MTP_string(text.text), + entities, + MTPInputMedia(), + MTPReplyMarkup(), + MTPInputRichMessage(), + MTP_long(base::RandomValue()), + MTP_inputReplyToEphemeralMessage(MTP_int(replyToEphemeralId)) + )).done([=](const MTPUpdates &result) { + session->api().applyUpdates(result); + }).fail([=](const MTP::Error &error) { + LOG(("API Error: send ephemeral message - %1").arg(error.type())); + }).send(); +} + +void EphemeralMessages::deleteMessage(not_null item) { + if (const auto entry = findByItem(item)) { + const auto receiver = _session->data().user(entry->receiverId); + _session->api().request(MTPephemeral_DeleteMessage( + item->history()->peer->input(), + receiver->inputUser(), + MTP_int(entry->ephemeralId) + )).send(); + } + item->destroy(); +} + +const EphemeralMessages::Entry *EphemeralMessages::findByItem( + not_null item) const { + const auto i = _data.find(item->history()); + if (i == end(_data)) { + return nullptr; + } + const auto j = ranges::find(i->second, item.get(), &Entry::item); + return (j != end(i->second)) ? &*j : nullptr; +} + +UserData *EphemeralMessages::botForSending(const Entry &entry) const { + if (entry.item->out()) { + return _session->data().userLoaded(entry.receiverId); + } + const auto from = entry.item->from(); + return from ? from->asUser() : nullptr; +} + +void EphemeralMessages::itemRemoved(not_null item) { + const auto i = _data.find(item->history()); + if (i == end(_data)) { + return; + } + const auto j = ranges::find(i->second, item.get(), &Entry::item); + if (j == end(i->second)) { + return; + } + i->second.erase(j); + if (i->second.empty()) { + _data.erase(i); + } +} + +void EphemeralMessages::pruneOld() { + const auto till = base::unixtime::now() - kKeepDuration; + auto old = std::vector>(); + for (const auto &[history, list] : _data) { + for (const auto &entry : list) { + if (entry.item->date() <= till) { + old.push_back(entry.item); + } + } + } + for (const auto &item : old) { + item->destroy(); + } +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/ephemeral_messages.h b/Telegram/SourceFiles/data/components/ephemeral_messages.h new file mode 100644 index 0000000000..1965f29fe9 --- /dev/null +++ b/Telegram/SourceFiles/data/components/ephemeral_messages.h @@ -0,0 +1,87 @@ +/* +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 +*/ +#pragma once + +#include "base/timer.h" + +class History; +class HistoryItem; +class PeerData; +class UserData; + +namespace Api { +struct MessageToSend; +} // namespace Api + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class EphemeralMessages final { +public: + explicit EphemeralMessages(not_null session); + EphemeralMessages(const EphemeralMessages &other) = delete; + EphemeralMessages &operator=(const EphemeralMessages &other) = delete; + ~EphemeralMessages(); + + void apply(const MTPDupdateNewEphemeralMessage &update); + void apply(const MTPDupdateEditEphemeralMessage &update); + void apply(const MTPDupdateDeleteEphemeralMessages &update); + + [[nodiscard]] HistoryItem *lookupItem( + not_null peer, + int32 ephemeralId) const; + [[nodiscard]] int32 lookupId(not_null item) const; + [[nodiscard]] UserData *replyReceiver( + not_null item) const; + + [[nodiscard]] bool wouldSend(const Api::MessageToSend &message) const; + [[nodiscard]] bool trySend(const Api::MessageToSend &message); + void send( + not_null history, + not_null bot, + TextWithEntities text, + int32 replyToEphemeralId = 0); + void deleteMessage(not_null item); + +private: + struct Entry { + int32 ephemeralId = 0; + UserId receiverId; + not_null item; + }; + using List = std::vector; + + void applyOrDefer(const MTPEphemeralMessage &message); + HistoryItem *applyNew(const MTPDephemeralMessage &data); + [[nodiscard]] UserData *findCommandBot( + not_null peer, + const QString &text) const; + [[nodiscard]] bool replyTargetMissing( + const MTPDephemeralMessage &data) const; + void drainPending(bool force = false); + [[nodiscard]] const Entry *findByItem( + not_null item) const; + [[nodiscard]] UserData *botForSending(const Entry &entry) const; + void itemRemoved(not_null item); + void pruneOld(); + + const not_null _session; + + base::Timer _pruneTimer; + base::Timer _pendingTimer; + base::flat_map, List> _data; + std::vector _pending; + + rpl::lifetime _lifetime; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer_bot_command.cpp b/Telegram/SourceFiles/data/data_peer_bot_command.cpp index 9aef25403f..10093740a8 100644 --- a/Telegram/SourceFiles/data/data_peer_bot_command.cpp +++ b/Telegram/SourceFiles/data/data_peer_bot_command.cpp @@ -14,6 +14,7 @@ BotCommand BotCommandFromTL(const MTPBotCommand &result) { return BotCommand { .command = qs(data.vcommand().v), .description = qs(data.vdescription().v), + .ephemeral = data.is_ephemeral(), }; }); } diff --git a/Telegram/SourceFiles/data/data_peer_bot_command.h b/Telegram/SourceFiles/data/data_peer_bot_command.h index 3ee5ebee3a..1e4c34f2b8 100644 --- a/Telegram/SourceFiles/data/data_peer_bot_command.h +++ b/Telegram/SourceFiles/data/data_peer_bot_command.h @@ -12,6 +12,7 @@ namespace Data { struct BotCommand final { QString command; QString description; + bool ephemeral = false; friend inline bool operator==( const BotCommand &, diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 5c54b73a83..132c94e753 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -370,6 +370,8 @@ enum class MessageFlag : uint64 { TextAppearingStarted = (1ULL << 61), GuestChatViaFrom = (1ULL << 62), + + Ephemeral = (1ULL << 63), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index fe5a8e4b0c..73dc265df4 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -840,7 +840,9 @@ not_null History::addNewItem( } if (!loadedAtBottom() || peer->migrateTo()) { - setLastMessage(item); + if (!item->isEphemeral()) { + setLastMessage(item); + } if (unread) { const auto type = item->out() ? NewAddType::Outgoing @@ -1214,7 +1216,9 @@ not_null History::addNewToBack( } } - setLastMessage(item); + if (!item->isEphemeral()) { + setLastMessage(item); + } if (unread) { const auto type = item->out() ? NewAddType::Outgoing diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 0222e1d542..be35439676 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -345,6 +345,9 @@ public: [[nodiscard]] bool isLocal() const { return _flags & MessageFlag::Local; } + [[nodiscard]] bool isEphemeral() const { + return _flags & MessageFlag::Ephemeral; + } [[nodiscard]] bool isFakeAboutView() const { return _flags & MessageFlag::FakeAboutView; } diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index e31c3a82f0..2b172f5990 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -648,6 +648,27 @@ bool LookupReplyIsTopicPost(HistoryItem *replyTo) { && (replyTo->topicRootId() != Data::ForumTopic::kGeneralId); } +bool ShowEphemeralReplyTextOnlyError( + std::shared_ptr show, + not_null session, + FullMsgId replyToId) { + const auto item = session->data().message(replyToId); + if (!item || !item->isEphemeral()) { + return false; + } + show->showToast(tr::lng_ephemeral_reply_text_only(tr::now)); + return true; +} + +void StripEphemeralReply( + not_null session, + FullReplyTo &replyTo) { + const auto item = session->data().message(replyTo.messageId); + if (item && item->isEphemeral()) { + replyTo.messageId = FullMsgId(); + } +} + TextWithEntities DropDisallowedCustomEmoji( not_null to, TextWithEntities text) { diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 306767474b..bf0aaa18fd 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -128,6 +128,13 @@ void RequestDependentMessageStory( not_null history, FullReplyTo replyTo); [[nodiscard]] bool LookupReplyIsTopicPost(HistoryItem *replyTo); +[[nodiscard]] bool ShowEphemeralReplyTextOnlyError( + std::shared_ptr show, + not_null session, + FullMsgId replyToId); +void StripEphemeralReply( + not_null session, + FullReplyTo &replyTo); struct SendingErrorRequest { MsgId topicRootId = 0; diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index c73f3c7dc0..5191a6d754 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_account.h" #include "storage/storage_facade.h" #include "data/components/credits.h" +#include "data/components/ephemeral_messages.h" #include "data/components/factchecks.h" #include "data/components/gift_auctions.h" #include "data/components/location_pickers.h" @@ -119,6 +120,7 @@ Session::Session( , _recentSharedGifts(std::make_unique(this)) , _giftAuctions(std::make_unique(this)) , _scheduledMessages(std::make_unique(this)) +, _ephemeralMessages(std::make_unique(this)) , _sponsoredMessages(std::make_unique(this)) , _topPeers(std::make_unique(this, Data::TopPeerType::Chat)) , _topBotApps( diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index eadc0f6f22..56bdca404d 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -37,6 +37,7 @@ class RecentInlineBots; class RecentPeers; class RecentSharedMediaGifts; class ScheduledMessages; +class EphemeralMessages; class SponsoredMessages; class TopPeers; class Factchecks; @@ -157,6 +158,9 @@ public: [[nodiscard]] Data::ScheduledMessages &scheduledMessages() const { return *_scheduledMessages; } + [[nodiscard]] Data::EphemeralMessages &ephemeralMessages() const { + return *_ephemeralMessages; + } [[nodiscard]] Data::TopPeers &topPeers() const { return *_topPeers; } @@ -323,6 +327,7 @@ private: const std::unique_ptr _recentSharedGifts; const std::unique_ptr _giftAuctions; const std::unique_ptr _scheduledMessages; + const std::unique_ptr _ephemeralMessages; const std::unique_ptr _sponsoredMessages; const std::unique_ptr _topPeers; const std::unique_ptr _topBotApps;