2
0
mirror of https://github.com/kotatogram/kotatogram-desktop synced 2025-08-31 06:35:14 +00:00

Show saved messages sublists in profile.

This commit is contained in:
John Preston
2023-12-27 01:09:20 +00:00
parent ead40c759e
commit 18c4d210e5
41 changed files with 1030 additions and 65 deletions

View File

@@ -343,12 +343,6 @@ int Folder::storiesUnreadCount() const {
return _storiesUnreadCount;
}
void Folder::requestChatListMessage() {
if (!chatListMessageKnown()) {
owner().histories().requestDialogEntry(this);
}
}
TimeId Folder::adjustedChatListTimeId() const {
return chatListTimeId();
}

View File

@@ -49,9 +49,9 @@ public:
Dialogs::BadgesState chatListBadgesState() const override;
HistoryItem *chatListMessage() const override;
bool chatListMessageKnown() const override;
void requestChatListMessage() override;
const QString &chatListName() const override;
const QString &chatListNameSortKey() const override;
int chatListNameVersion() const override;
const base::flat_set<QString> &chatListNameWords() const override;
const base::flat_set<QChar> &chatListFirstLetters() const override;
@@ -82,8 +82,6 @@ public:
private:
void indexNameParts();
int chatListNameVersion() const override;
void reorderLastHistories();
void paintUserpic(

View File

@@ -98,6 +98,7 @@ public:
void setRealRootId(MsgId realId);
void readTillEnd();
void requestChatListMessage();
void applyTopic(const MTPDforumTopic &data);
@@ -109,9 +110,9 @@ public:
Dialogs::BadgesState chatListBadgesState() const override;
HistoryItem *chatListMessage() const override;
bool chatListMessageKnown() const override;
void requestChatListMessage() override;
const QString &chatListName() const override;
const QString &chatListNameSortKey() const override;
int chatListNameVersion() const override;
const base::flat_set<QString> &chatListNameWords() const override;
const base::flat_set<QChar> &chatListFirstLetters() const override;
@@ -187,8 +188,6 @@ private:
void allowChatListMessageResolve();
void resolveChatListMessageGroup();
int chatListNameVersion() const override;
void subscribeToUnreadChanges();
[[nodiscard]] Dialogs::UnreadState unreadStateFor(
int count,

View File

@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_folder.h"
#include "data/data_forum.h"
#include "data/data_forum_topic.h"
#include "data/data_saved_messages.h"
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "data/data_histories.h"
@@ -1029,6 +1030,10 @@ bool PeerData::sharedMediaInfo() const {
return isSelf() || isRepliesChat();
}
bool PeerData::savedSublistsInfo() const {
return isSelf() && owner().savedMessages().supported();
}
bool PeerData::hasStoriesHidden() const {
if (const auto user = asUser()) {
return user->hasStoriesHidden();

View File

@@ -203,6 +203,7 @@ public:
[[nodiscard]] bool isGigagroup() const;
[[nodiscard]] bool isRepliesChat() const;
[[nodiscard]] bool sharedMediaInfo() const;
[[nodiscard]] bool savedSublistsInfo() const;
[[nodiscard]] bool hasStoriesHidden() const;
void setStoriesHidden(bool hidden);

View File

@@ -141,6 +141,18 @@ int PremiumLimits::topicsPinnedCurrent() const {
return appConfigLimit("topics_pinned_limit", 5);
}
int PremiumLimits::savedSublistsPinnedDefault() const {
return appConfigLimit("saved_dialogs_pinned_limit_default", 5);
}
int PremiumLimits::savedSublistsPinnedPremium() const {
return appConfigLimit("saved_dialogs_pinned_limit_premium", 100);
}
int PremiumLimits::savedSublistsPinnedCurrent() const {
return isPremium()
? savedSublistsPinnedPremium()
: savedSublistsPinnedDefault();
}
int PremiumLimits::channelsPublicDefault() const {
return appConfigLimit("channels_public_limit_default", 10);
}

View File

@@ -59,6 +59,10 @@ public:
[[nodiscard]] int topicsPinnedCurrent() const;
[[nodiscard]] int savedSublistsPinnedDefault() const;
[[nodiscard]] int savedSublistsPinnedPremium() const;
[[nodiscard]] int savedSublistsPinnedCurrent() const;
[[nodiscard]] int channelsPublicDefault() const;
[[nodiscard]] int channelsPublicPremium() const;
[[nodiscard]] int channelsPublicCurrent() const;

View File

@@ -0,0 +1,181 @@
/*
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/data_saved_messages.h"
#include "apiwrap.h"
#include "data/data_peer.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "main/main_session.h"
namespace Data {
namespace {
constexpr auto kPerPage = 50;
constexpr auto kFirstPerPage = 10;
} // namespace
SavedMessages::SavedMessages(not_null<Session*> owner)
: _owner(owner)
, _chatsList(
&owner->session(),
FilterId(),
owner->maxPinnedChatsLimitValue(this)) {
}
SavedMessages::~SavedMessages() = default;
bool SavedMessages::supported() const {
return !_unsupported;
}
Session &SavedMessages::owner() const {
return *_owner;
}
Main::Session &SavedMessages::session() const {
return _owner->session();
}
not_null<Dialogs::MainList*> SavedMessages::chatsList() {
return &_chatsList;
}
not_null<SavedSublist*> SavedMessages::sublist(not_null<PeerData*> peer) {
const auto i = _sublists.find(peer);
if (i != end(_sublists)) {
return i->second.get();
}
return _sublists.emplace(
peer,
std::make_unique<SavedSublist>(peer)).first->second.get();
}
void SavedMessages::loadMore() {
if (_loadMoreRequestId || _chatsList.loaded()) {
return;
}
_loadMoreRequestId = _owner->session().api().request(
MTPmessages_GetSavedDialogs(
MTP_flags(0),
MTP_int(_offsetDate),
MTP_int(_offsetId),
_offsetPeer ? _offsetPeer->input : MTP_inputPeerEmpty(),
MTP_int(kPerPage),
MTP_long(0)) // hash
).done([=](const MTPmessages_SavedDialogs &result) {
auto list = (const QVector<MTPSavedDialog>*)nullptr;
result.match([](const MTPDmessages_savedDialogsNotModified &) {
LOG(("API Error: messages.savedDialogsNotModified."));
}, [&](const auto &data) {
_owner->processUsers(data.vusers());
_owner->processChats(data.vchats());
_owner->processMessages(
data.vmessages(),
NewMessageType::Existing);
list = &data.vdialogs().v;
});
_loadMoreRequestId = 0;
if (!list) {
_chatsList.setLoaded();
return;
}
auto lastValid = false;
const auto selfId = _owner->session().userPeerId();
for (const auto &dialog : *list) {
const auto &data = dialog.data();
const auto peer = _owner->peer(peerFromMTP(data.vpeer()));
const auto topId = MsgId(data.vtop_message().v);
if (const auto item = _owner->message(selfId, topId)) {
_offsetPeer = peer;
_offsetDate = item->date();
_offsetId = topId;
lastValid = true;
sublist(peer)->applyMaybeLast(item);
} else {
lastValid = false;
}
}
if (!lastValid) {
LOG(("API Error: Unknown message in the end of a slice."));
_chatsList.setLoaded();
} else if (result.type() == mtpc_messages_savedDialogs) {
_chatsList.setLoaded();
}
}).fail([=](const MTP::Error &error) {
if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) {
_unsupported = true;
}
_chatsList.setLoaded();
_loadMoreRequestId = 0;
}).send();
}
void SavedMessages::loadMore(not_null<SavedSublist*> sublist) {
if (_loadMoreRequests.contains(sublist) || sublist->isFullLoaded()) {
return;
}
const auto &list = sublist->messages();
const auto offsetId = list.empty() ? MsgId(0) : list.back()->id;
const auto offsetDate = list.empty() ? MsgId(0) : list.back()->date();
const auto limit = offsetId ? kPerPage : kFirstPerPage;
const auto requestId = _owner->session().api().request(
MTPmessages_GetSavedHistory(
sublist->peer()->input,
MTP_int(offsetId),
MTP_int(offsetDate),
MTP_int(0), // add_offset
MTP_int(limit),
MTP_int(0), // max_id
MTP_int(0), // min_id
MTP_long(0)) // hash
).done([=](const MTPmessages_Messages &result) {
auto list = (const QVector<MTPMessage>*)nullptr;
result.match([](const MTPDmessages_channelMessages &) {
LOG(("API Error: messages.channelMessages in sublist."));
}, [](const MTPDmessages_messagesNotModified &) {
LOG(("API Error: messages.messagesNotModified in sublist."));
}, [&](const auto &data) {
owner().processUsers(data.vusers());
owner().processChats(data.vchats());
list = &data.vmessages().v;
});
_loadMoreRequests.remove(sublist);
if (!list) {
sublist->setFullLoaded();
return;
}
auto items = std::vector<not_null<HistoryItem*>>();
items.reserve(list->size());
for (const auto &message : *list) {
const auto item = owner().addNewMessage(
message,
{},
NewMessageType::Existing);
if (item) {
items.push_back(item);
}
}
sublist->append(std::move(items));
if (result.type() == mtpc_messages_messages) {
sublist->setFullLoaded();
}
}).fail([=](const MTP::Error &error) {
if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) {
_unsupported = true;
}
sublist->setFullLoaded();
_loadMoreRequests.remove(sublist);
}).send();
}
} // namespace Data

View File

@@ -0,0 +1,56 @@
/*
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 "dialogs/dialogs_main_list.h"
namespace Main {
class Session;
} // namespace Main
namespace Data {
class Session;
class SavedSublist;
class SavedMessages final {
public:
explicit SavedMessages(not_null<Session*> owner);
~SavedMessages();
[[nodiscard]] bool supported() const;
[[nodiscard]] Session &owner() const;
[[nodiscard]] Main::Session &session() const;
[[nodiscard]] not_null<Dialogs::MainList*> chatsList();
[[nodiscard]] not_null<SavedSublist*> sublist(not_null<PeerData*> peer);
void loadMore();
void loadMore(not_null<SavedSublist*> sublist);
private:
const not_null<Session*> _owner;
Dialogs::MainList _chatsList;
base::flat_map<
not_null<PeerData*>,
std::unique_ptr<SavedSublist>> _sublists;
base::flat_map<not_null<SavedSublist*>, mtpRequestId> _loadMoreRequests;
mtpRequestId _loadMoreRequestId = 0;
TimeId _offsetDate = 0;
MsgId _offsetId = 0;
PeerData *_offsetPeer = nullptr;
bool _unsupported = false;
};
} // namespace Data

View File

@@ -0,0 +1,212 @@
/*
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/data_saved_sublist.h"
#include "data/data_histories.h"
#include "data/data_peer.h"
#include "data/data_saved_messages.h"
#include "data/data_session.h"
#include "history/view/history_view_item_preview.h"
#include "history/history.h"
#include "history/history_item.h"
namespace Data {
SavedSublist::SavedSublist(not_null<PeerData*> peer)
: Entry(&peer->owner(), Dialogs::Entry::Type::SavedSublist)
, _history(peer->owner().history(peer)) {
}
SavedSublist::~SavedSublist() = default;
not_null<History*> SavedSublist::history() const {
return _history;
}
not_null<PeerData*> SavedSublist::peer() const {
return _history->peer;
}
bool SavedSublist::isHiddenAuthor() const {
return peer()->isSavedHiddenAuthor();
}
bool SavedSublist::isFullLoaded() const {
return (_flags & Flag::FullLoaded) != 0;
}
auto SavedSublist::messages() const
-> const std::vector<not_null<HistoryItem*>> & {
return _items;
}
void SavedSublist::applyMaybeLast(not_null<HistoryItem*> item) {
const auto before = [](
not_null<HistoryItem*> a,
not_null<HistoryItem*> b) {
return IsServerMsgId(a->id)
? (IsServerMsgId(b->id) ? (a->id < b->id) : true)
: (IsServerMsgId(b->id) ? false : (a->id < b->id));
};
if (_items.empty()) {
_items.push_back(item);
} else if (_items.front() == item) {
return;
} else if (_items.size() == 1 && before(_items.front(), item)) {
_items[0] = item;
} else if (before(_items.back(), item)) {
for (auto i = begin(_items); i != end(_items); ++i) {
if (item == *i) {
break;
} else if (before(*i, item)) {
_items.insert(i, item);
break;
}
}
}
if (_items.front() == item) {
setChatListTimeId(item->date());
resolveChatListMessageGroup();
}
}
void SavedSublist::removeOne(not_null<HistoryItem*> item) {
if (_items.empty()) {
return;
}
const auto last = (_items.front() == item);
_items.erase(ranges::remove(_items, item), end(_items));
if (last) {
if (_items.empty()) {
if (isFullLoaded()) {
updateChatListExistence();
} else {
updateChatListEntry();
crl::on_main(this, [=] {
owner().savedMessages().loadMore(this);
});
}
} else {
setChatListTimeId(_items.front()->date());
}
}
}
void SavedSublist::append(std::vector<not_null<HistoryItem*>> &&items) {
if (items.empty()) {
setFullLoaded();
} else if (!_items.empty()) {
_items.insert(end(_items), begin(items), end(items));
} else {
_items = std::move(items);
setChatListTimeId(_items.front()->date());
}
}
void SavedSublist::setFullLoaded(bool loaded) {
if (loaded != isFullLoaded()) {
if (loaded) {
_flags |= Flag::FullLoaded;
if (_items.empty()) {
updateChatListExistence();
}
} else {
_flags &= ~Flag::FullLoaded;
}
}
}
int SavedSublist::fixedOnTopIndex() const {
return 0;
}
bool SavedSublist::shouldBeInChatList() const {
return isPinnedDialog(FilterId()) || !_items.empty();
}
Dialogs::UnreadState SavedSublist::chatListUnreadState() const {
return {};
}
Dialogs::BadgesState SavedSublist::chatListBadgesState() const {
return {};
}
HistoryItem *SavedSublist::chatListMessage() const {
return _items.empty() ? nullptr : _items.front().get();
}
bool SavedSublist::chatListMessageKnown() const {
return true;
}
const QString &SavedSublist::chatListName() const {
return _history->chatListName();
}
const base::flat_set<QString> &SavedSublist::chatListNameWords() const {
return _history->chatListNameWords();
}
const base::flat_set<QChar> &SavedSublist::chatListFirstLetters() const {
return _history->chatListFirstLetters();
}
const QString &SavedSublist::chatListNameSortKey() const {
return _history->chatListNameSortKey();
}
int SavedSublist::chatListNameVersion() const {
return _history->chatListNameVersion();
}
void SavedSublist::paintUserpic(
Painter &p,
Ui::PeerUserpicView &view,
const Dialogs::Ui::PaintContext &context) const {
_history->paintUserpic(p, view, context);
}
void SavedSublist::chatListPreloadData() {
peer()->loadUserpic();
allowChatListMessageResolve();
}
void SavedSublist::allowChatListMessageResolve() {
if (_flags & Flag::ResolveChatListMessage) {
return;
}
_flags |= Flag::ResolveChatListMessage;
resolveChatListMessageGroup();
}
bool SavedSublist::hasOrphanMediaGroupPart() const {
if (isFullLoaded() || _items.size() != 1) {
return false;
}
return (_items.front()->groupId() != MessageGroupId());
}
void SavedSublist::resolveChatListMessageGroup() {
const auto item = chatListMessage();
if (!(_flags & Flag::ResolveChatListMessage)
|| !item
|| !hasOrphanMediaGroupPart()) {
return;
}
// If we set a single album part, request the full album.
const auto withImages = !item->toPreview({
.hideSender = true,
.hideCaption = true }).images.empty();
if (withImages) {
owner().histories().requestGroupAround(item);
}
}
} // namespace Data

View File

@@ -0,0 +1,79 @@
/*
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 "dialogs/ui/dialogs_message_view.h"
#include "dialogs/dialogs_entry.h"
class PeerData;
class History;
namespace Data {
class Session;
class SavedSublist final : public Dialogs::Entry {
public:
explicit SavedSublist(not_null<PeerData*> peer);
~SavedSublist();
[[nodiscard]] not_null<History*> history() const;
[[nodiscard]] not_null<PeerData*> peer() const;
[[nodiscard]] bool isHiddenAuthor() const;
[[nodiscard]] bool isFullLoaded() const;
[[nodiscard]] auto messages() const
-> const std::vector<not_null<HistoryItem*>> &;
void applyMaybeLast(not_null<HistoryItem*> item);
void removeOne(not_null<HistoryItem*> item);
void append(std::vector<not_null<HistoryItem*>> &&items);
void setFullLoaded(bool loaded = true);
[[nodiscard]] Dialogs::Ui::MessageView &lastItemDialogsView() {
return _lastItemDialogsView;
}
int fixedOnTopIndex() const override;
bool shouldBeInChatList() const override;
Dialogs::UnreadState chatListUnreadState() const override;
Dialogs::BadgesState chatListBadgesState() const override;
HistoryItem *chatListMessage() const override;
bool chatListMessageKnown() const override;
const QString &chatListName() const override;
const QString &chatListNameSortKey() const override;
int chatListNameVersion() const override;
const base::flat_set<QString> &chatListNameWords() const override;
const base::flat_set<QChar> &chatListFirstLetters() const override;
void chatListPreloadData() override;
void paintUserpic(
Painter &p,
Ui::PeerUserpicView &view,
const Dialogs::Ui::PaintContext &context) const override;
private:
enum class Flag : uchar {
ResolveChatListMessage = (1 << 0),
FullLoaded = (1 << 1),
};
friend inline constexpr bool is_flag_type(Flag) { return true; }
using Flags = base::flags<Flag>;
bool hasOrphanMediaGroupPart() const;
void allowChatListMessageResolve();
void resolveChatListMessageGroup();
const not_null<History*> _history;
std::vector<not_null<HistoryItem*>> _items;
Dialogs::Ui::MessageView _lastItemDialogsView;
Flags _flags;
};
} // namespace Data

View File

@@ -60,6 +60,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_emoji_statuses.h"
#include "data/data_forum_icons.h"
#include "data/data_cloud_themes.h"
#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_stories.h"
#include "data/data_streaming.h"
#include "data/data_media_rotation.h"
@@ -261,7 +263,8 @@ Session::Session(not_null<Main::Session*> session)
, _forumIcons(std::make_unique<ForumIcons>(this))
, _notifySettings(std::make_unique<NotifySettings>(this))
, _customEmojiManager(std::make_unique<CustomEmojiManager>(this))
, _stories(std::make_unique<Stories>(this)) {
, _stories(std::make_unique<Stories>(this))
, _savedMessages(std::make_unique<SavedMessages>(this)) {
_cache->open(_session->local().cacheKey());
_bigFileCache->open(_session->local().cacheBigFileKey());
@@ -1712,6 +1715,11 @@ void Session::requestItemRepaint(not_null<const HistoryItem*> item) {
topic->updateChatListEntry();
}
}
if (const auto sublist = item->savedSublist()) {
if (sublist->lastItemDialogsView().dependsOn(item)) {
sublist->updateChatListEntry();
}
}
}
rpl::producer<not_null<const HistoryItem*>> Session::itemRepaintRequest() const {
@@ -2137,6 +2145,11 @@ int Session::pinnedChatsLimit(not_null<Data::Forum*> forum) const {
return limits.topicsPinnedCurrent();
}
int Session::pinnedChatsLimit(not_null<Data::SavedMessages*> saved) const {
const auto limits = Data::PremiumLimits(_session);
return limits.savedSublistsPinnedCurrent();
}
rpl::producer<int> Session::maxPinnedChatsLimitValue(
Data::Folder *folder) const {
// Premium limit from appconfig.
@@ -2177,6 +2190,20 @@ rpl::producer<int> Session::maxPinnedChatsLimitValue(
});
}
rpl::producer<int> Session::maxPinnedChatsLimitValue(
not_null<SavedMessages*> saved) const {
// Premium limit from appconfig.
// We always use premium limit in the MainList limit producer,
// because it slices the list to that limit. We don't want to slice
// premium-ly added chats from the pinned list because of sync issues.
return rpl::single(rpl::empty_value()) | rpl::then(
_session->account().appConfig().refreshed()
) | rpl::map([=] {
const auto limits = Data::PremiumLimits(_session);
return limits.savedSublistsPinnedPremium();
});
}
const std::vector<Dialogs::Key> &Session::pinnedChatsOrder(
Data::Folder *folder) const {
return chatsList(folder)->pinned()->order();
@@ -2192,6 +2219,11 @@ const std::vector<Dialogs::Key> &Session::pinnedChatsOrder(
return forum->topicsList()->pinned()->order();
}
const std::vector<Dialogs::Key> &Session::pinnedChatsOrder(
not_null<Data::SavedMessages*> saved) const {
return saved->chatsList()->pinned()->order();
}
void Session::clearPinnedChats(Data::Folder *folder) {
chatsList(folder)->pinned()->clear();
}
@@ -4198,6 +4230,8 @@ not_null<Dialogs::MainList*> Session::chatsListFor(
const auto topic = entry->asTopic();
return topic
? topic->forum()->topicsList()
: entry->asSublist()
? _savedMessages->chatsList()
: chatsList(entry->folder());
}

View File

@@ -61,6 +61,7 @@ class GroupCall;
class NotifySettings;
class CustomEmojiManager;
class Stories;
class SavedMessages;
struct RepliesReadTillUpdate {
FullMsgId id;
@@ -137,6 +138,9 @@ public:
[[nodiscard]] Stories &stories() const {
return *_stories;
}
[[nodiscard]] SavedMessages &savedMessages() const {
return *_savedMessages;
}
[[nodiscard]] MsgId nextNonHistoryEntryId() {
return ++_nonHistoryEntryId;
@@ -352,18 +356,24 @@ public:
[[nodiscard]] int pinnedChatsLimit(Folder *folder) const;
[[nodiscard]] int pinnedChatsLimit(FilterId filterId) const;
[[nodiscard]] int pinnedChatsLimit(not_null<Forum*> forum) const;
[[nodiscard]] int pinnedChatsLimit(
not_null<SavedMessages*> saved) const;
[[nodiscard]] rpl::producer<int> maxPinnedChatsLimitValue(
Folder *folder) const;
[[nodiscard]] rpl::producer<int> maxPinnedChatsLimitValue(
FilterId filterId) const;
[[nodiscard]] rpl::producer<int> maxPinnedChatsLimitValue(
not_null<Forum*> forum) const;
[[nodiscard]] rpl::producer<int> maxPinnedChatsLimitValue(
not_null<SavedMessages*> saved) const;
[[nodiscard]] const std::vector<Dialogs::Key> &pinnedChatsOrder(
Folder *folder) const;
[[nodiscard]] const std::vector<Dialogs::Key> &pinnedChatsOrder(
not_null<Forum*> forum) const;
[[nodiscard]] const std::vector<Dialogs::Key> &pinnedChatsOrder(
FilterId filterId) const;
[[nodiscard]] const std::vector<Dialogs::Key> &pinnedChatsOrder(
not_null<Data::SavedMessages*> saved) const;
void setChatPinned(Dialogs::Key key, FilterId filterId, bool pinned);
void setPinnedFromEntryList(Dialogs::Key key, bool pinned);
void clearPinnedChats(Folder *folder);
@@ -1041,6 +1051,7 @@ private:
const std::unique_ptr<NotifySettings> _notifySettings;
const std::unique_ptr<CustomEmojiManager> _customEmojiManager;
const std::unique_ptr<Stories> _stories;
const std::unique_ptr<SavedMessages> _savedMessages;
MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange;