mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-07-11 00:02:54 +02:00
591 lines
16 KiB
C++
591 lines
16 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 "data/data_saved_messages.h"
|
|
|
|
#include "apiwrap.h"
|
|
#include "core/application.h"
|
|
#include "data/data_changes.h"
|
|
#include "data/data_channel.h"
|
|
#include "data/data_histories.h"
|
|
#include "data/data_user.h"
|
|
#include "data/data_saved_sublist.h"
|
|
#include "data/data_session.h"
|
|
#include "history/history.h"
|
|
#include "history/history_item.h"
|
|
#include "history/history_unread_things.h"
|
|
#include "main/main_session.h"
|
|
#include "storage/storage_facade.h"
|
|
#include "storage/storage_shared_media.h"
|
|
#include "window/notifications_manager.h"
|
|
|
|
namespace Data {
|
|
namespace {
|
|
|
|
constexpr auto kPerPage = 50;
|
|
constexpr auto kFirstPerPage = 10;
|
|
constexpr auto kListPerPage = 100;
|
|
constexpr auto kListFirstPerPage = 20;
|
|
constexpr auto kLoadedSublistsMinCount = 20;
|
|
constexpr auto kShowSublistNamesCount = 5;
|
|
constexpr auto kStalePerRequest = 100;
|
|
|
|
} // namespace
|
|
|
|
SavedMessages::SavedMessages(
|
|
not_null<Session*> owner,
|
|
ChannelData *parentChat)
|
|
: _owner(owner)
|
|
, _parentChat(parentChat)
|
|
, _owningHistory(parentChat ? owner->history(parentChat).get() : nullptr)
|
|
, _chatsList(
|
|
&_owner->session(),
|
|
FilterId(),
|
|
_owner->maxPinnedChatsLimitValue(this))
|
|
, _loadMore([=] { sendLoadMoreRequests(); }) {
|
|
// We don't assign _owningHistory for my Saved Messages here,
|
|
// because the data structures are not ready yet.
|
|
if (_owningHistory && _owningHistory->inChatList()) {
|
|
preloadSublists();
|
|
}
|
|
}
|
|
|
|
void SavedMessages::clear() {
|
|
for (const auto &request : base::take(_sublistRequests)) {
|
|
if (request.second.id != _staleRequestId) {
|
|
owner().histories().cancelRequest(request.second.id);
|
|
}
|
|
}
|
|
if (const auto requestId = base::take(_staleRequestId)) {
|
|
session().api().request(requestId).cancel();
|
|
}
|
|
|
|
auto &storage = session().storage();
|
|
auto &changes = session().changes();
|
|
if (_owningHistory) {
|
|
for (const auto &[peer, sublist] : base::take(_sublists)) {
|
|
storage.unload(Storage::SharedMediaUnloadThread(
|
|
_owningHistory->peer->id,
|
|
MsgId(),
|
|
peer->id));
|
|
_owningHistory->setForwardDraft(MsgId(), peer->id, {});
|
|
|
|
const auto raw = sublist.get();
|
|
changes.sublistRemoved(raw);
|
|
changes.entryRemoved(raw);
|
|
}
|
|
}
|
|
_owningHistory = nullptr;
|
|
}
|
|
|
|
void SavedMessages::saveActiveSubsectionThread(not_null<Thread*> thread) {
|
|
if (const auto sublist = thread->asSublist()) {
|
|
Assert(sublist->parent() == this);
|
|
_activeSubsectionSublist = sublist;
|
|
} else {
|
|
Assert(thread == _owningHistory);
|
|
_activeSubsectionSublist = nullptr;
|
|
}
|
|
}
|
|
|
|
Thread *SavedMessages::activeSubsectionThread() const {
|
|
return _activeSubsectionSublist;
|
|
}
|
|
|
|
Dialogs::UnreadState SavedMessages::unreadStateWithParentMuted() const {
|
|
auto result = _chatsList.unreadState();
|
|
if (_owningHistory->muted()) {
|
|
result.chatsMuted = result.chats;
|
|
result.marksMuted = result.marks;
|
|
result.messagesMuted = result.messages;
|
|
result.reactionsMuted = result.reactions;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
SavedMessages::~SavedMessages() {
|
|
clear();
|
|
}
|
|
|
|
bool SavedMessages::supported() const {
|
|
return !_unsupported;
|
|
}
|
|
|
|
void SavedMessages::markUnsupported() {
|
|
_unsupported = true;
|
|
}
|
|
|
|
ChannelData *SavedMessages::parentChat() const {
|
|
return _parentChat;
|
|
}
|
|
|
|
not_null<History*> SavedMessages::owningHistory() const {
|
|
if (!_owningHistory) {
|
|
const_cast<SavedMessages*>(this)->_owningHistory
|
|
= _owner->history(_owner->session().user());
|
|
}
|
|
return _owningHistory;
|
|
}
|
|
|
|
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) {
|
|
if (const auto loaded = sublistLoaded(peer)) {
|
|
return loaded;
|
|
}
|
|
return _sublists.emplace(
|
|
peer,
|
|
std::make_unique<SavedSublist>(this, peer)).first->second.get();
|
|
}
|
|
|
|
SavedSublist *SavedMessages::sublistLoaded(not_null<PeerData*> peer) {
|
|
const auto i = _sublists.find(peer);
|
|
return (i != end(_sublists)) ? i->second.get() : nullptr;
|
|
}
|
|
|
|
void SavedMessages::requestSomeStale() {
|
|
if (_staleRequestId
|
|
|| (!_offset.id && _loadMoreRequestId)
|
|
|| _stalePeers.empty()
|
|
|| !_parentChat) {
|
|
return;
|
|
}
|
|
const auto type = Histories::RequestType::History;
|
|
auto peers = std::vector<not_null<PeerData*>>();
|
|
auto peerIds = QVector<MTPInputPeer>();
|
|
peers.reserve(std::min(int(_stalePeers.size()), kStalePerRequest));
|
|
peerIds.reserve(std::min(int(_stalePeers.size()), kStalePerRequest));
|
|
for (auto i = begin(_stalePeers); i != end(_stalePeers);) {
|
|
const auto peer = *i;
|
|
i = _stalePeers.erase(i);
|
|
|
|
peers.push_back(peer);
|
|
peerIds.push_back(peer->input);
|
|
if (peerIds.size() == kStalePerRequest) {
|
|
break;
|
|
}
|
|
}
|
|
if (peerIds.empty()) {
|
|
return;
|
|
}
|
|
const auto call = [=] {
|
|
for (const auto &peer : peers) {
|
|
finishSublistRequest(peer);
|
|
}
|
|
};
|
|
auto &histories = owner().histories();
|
|
_staleRequestId = histories.sendRequest(_owningHistory, type, [=](
|
|
Fn<void()> finish) {
|
|
using Flag = MTPmessages_GetSavedDialogsByID::Flag;
|
|
return session().api().request(
|
|
MTPmessages_GetSavedDialogsByID(
|
|
MTP_flags(Flag::f_parent_peer),
|
|
_parentChat->input,
|
|
MTP_vector<MTPInputPeer>(peerIds))
|
|
).done([=](const MTPmessages_SavedDialogs &result) {
|
|
_staleRequestId = 0;
|
|
applyReceivedSublists(result);
|
|
call();
|
|
finish();
|
|
}).fail([=] {
|
|
_staleRequestId = 0;
|
|
call();
|
|
finish();
|
|
}).send();
|
|
});
|
|
for (const auto &peer : peers) {
|
|
_sublistRequests[peer].id = _staleRequestId;
|
|
}
|
|
}
|
|
|
|
void SavedMessages::finishSublistRequest(not_null<PeerData*> peer) {
|
|
if (const auto request = _sublistRequests.take(peer)) {
|
|
for (const auto &callback : request->callbacks) {
|
|
callback();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SavedMessages::requestSublist(
|
|
not_null<PeerData*> peer,
|
|
Fn<void()> done) {
|
|
if (!_parentChat) {
|
|
return;
|
|
}
|
|
auto &request = _sublistRequests[peer];
|
|
if (done) {
|
|
request.callbacks.push_back(std::move(done));
|
|
}
|
|
if (!request.id
|
|
&& _stalePeers.emplace(peer).second
|
|
&& (_stalePeers.size() == 1)) {
|
|
crl::on_main(&session(), [peer = _parentChat] {
|
|
if (const auto monoforum = peer->monoforum()) {
|
|
monoforum->requestSomeStale();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
rpl::producer<> SavedMessages::chatsListChanges() const {
|
|
return _chatsListChanges.events();
|
|
}
|
|
|
|
rpl::producer<> SavedMessages::chatsListLoadedEvents() const {
|
|
return _chatsListLoadedEvents.events();
|
|
}
|
|
|
|
void SavedMessages::preloadSublists() {
|
|
if (parentChat()
|
|
&& chatsList()->indexed()->size() < kLoadedSublistsMinCount) {
|
|
loadMore();
|
|
}
|
|
}
|
|
|
|
void SavedMessages::loadMore() {
|
|
_loadMoreScheduled = true;
|
|
_loadMore.call();
|
|
}
|
|
|
|
void SavedMessages::clearAllUnreadReactions() {
|
|
for (const auto &[peer, sublist] : _sublists) {
|
|
sublist->unreadReactions().clear();
|
|
}
|
|
}
|
|
|
|
void SavedMessages::sendLoadMore() {
|
|
if (_loadMoreRequestId || _chatsList.loaded()) {
|
|
return;
|
|
} else if (!_pinnedLoaded) {
|
|
loadPinned();
|
|
}
|
|
using Flag = MTPmessages_GetSavedDialogs::Flag;
|
|
_loadMoreRequestId = _owner->session().api().request(
|
|
MTPmessages_GetSavedDialogs(
|
|
MTP_flags(Flag::f_exclude_pinned
|
|
| (_parentChat ? Flag::f_parent_peer : Flag(0))),
|
|
_parentChat ? _parentChat->input : MTPInputPeer(),
|
|
MTP_int(_offset.date),
|
|
MTP_int(_offset.id),
|
|
_offset.peer ? _offset.peer->input : MTP_inputPeerEmpty(),
|
|
MTP_int(_offset.id ? kListPerPage : kListFirstPerPage),
|
|
MTP_long(0)) // hash
|
|
).done([=](const MTPmessages_SavedDialogs &result) {
|
|
const auto applied = applyReceivedSublists(result);
|
|
if (applied.allLoaded || _offset == applied.offset) {
|
|
_chatsList.setLoaded();
|
|
} else if (_offset.date > 0 && applied.offset.date > _offset.date) {
|
|
LOG(("API Error: Bad order in messages.savedDialogs."));
|
|
_chatsList.setLoaded();
|
|
} else {
|
|
_offset = applied.offset;
|
|
}
|
|
_loadMoreRequestId = 0;
|
|
_chatsListChanges.fire({});
|
|
if (_chatsList.loaded()) {
|
|
_chatsListLoadedEvents.fire({});
|
|
}
|
|
reorderLastSublists();
|
|
requestSomeStale();
|
|
}).fail([=](const MTP::Error &error) {
|
|
if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) {
|
|
markUnsupported();
|
|
}
|
|
_chatsList.setLoaded();
|
|
_loadMoreRequestId = 0;
|
|
}).send();
|
|
}
|
|
|
|
void SavedMessages::loadPinned() {
|
|
if (_pinnedRequestId || parentChat()) {
|
|
return;
|
|
}
|
|
_pinnedRequestId = _owner->session().api().request(
|
|
MTPmessages_GetPinnedSavedDialogs()
|
|
).done([=](const MTPmessages_SavedDialogs &result) {
|
|
_pinnedRequestId = 0;
|
|
_pinnedLoaded = true;
|
|
applyReceivedSublists(result, true);
|
|
_chatsListChanges.fire({});
|
|
}).fail([=](const MTP::Error &error) {
|
|
if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) {
|
|
markUnsupported();
|
|
} else {
|
|
_pinnedLoaded = true;
|
|
}
|
|
_pinnedRequestId = 0;
|
|
}).send();
|
|
}
|
|
|
|
SavedMessages::ApplyResult SavedMessages::applyReceivedSublists(
|
|
const MTPmessages_SavedDialogs &dialogs,
|
|
bool pinned) {
|
|
auto list = (const QVector<MTPSavedDialog>*)nullptr;
|
|
dialogs.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;
|
|
});
|
|
if (!list) {
|
|
return { .allLoaded = true };
|
|
}
|
|
auto lastValid = false;
|
|
auto result = ApplyResult();
|
|
const auto parentPeerId = _parentChat
|
|
? _parentChat->id
|
|
: _owner->session().userPeerId();
|
|
for (const auto &dialog : *list) {
|
|
dialog.match([&](const MTPDsavedDialog &data) {
|
|
const auto peer = _owner->peer(peerFromMTP(data.vpeer()));
|
|
const auto topId = MsgId(data.vtop_message().v);
|
|
if (const auto item = _owner->message(parentPeerId, topId)) {
|
|
result.offset.peer = peer;
|
|
result.offset.date = item->date();
|
|
result.offset.id = topId;
|
|
lastValid = true;
|
|
const auto entry = sublist(peer);
|
|
const auto entryPinned = pinned || data.is_pinned();
|
|
entry->applyMaybeLast(item);
|
|
_owner->setPinnedFromEntryList(entry, entryPinned);
|
|
} else {
|
|
lastValid = false;
|
|
}
|
|
}, [&](const MTPDmonoForumDialog &data) {
|
|
const auto peer = _owner->peer(peerFromMTP(data.vpeer()));
|
|
const auto topId = MsgId(data.vtop_message().v);
|
|
if (const auto item = _owner->message(parentPeerId, topId)) {
|
|
result.offset.peer = peer;
|
|
result.offset.date = item->date();
|
|
result.offset.id = topId;
|
|
lastValid = true;
|
|
sublist(peer)->applyMonoforumDialog(data, item);
|
|
} else {
|
|
lastValid = false;
|
|
}
|
|
});
|
|
}
|
|
if (pinned) {
|
|
} else if (!lastValid) {
|
|
LOG(("API Error: Unknown message in the end of a slice."));
|
|
result.allLoaded = true;
|
|
} else if (dialogs.type() == mtpc_messages_savedDialogs) {
|
|
result.allLoaded = true;
|
|
}
|
|
if (!_stalePeers.empty()) {
|
|
requestSomeStale();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void SavedMessages::sendLoadMoreRequests() {
|
|
if (_loadMoreScheduled) {
|
|
sendLoadMore();
|
|
}
|
|
}
|
|
|
|
void SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) {
|
|
Expects(!parentChat());
|
|
|
|
const auto list = update.vorder();
|
|
if (!list) {
|
|
loadPinned();
|
|
return;
|
|
}
|
|
const auto &order = list->v;
|
|
const auto notLoaded = [&](const MTPDialogPeer &peer) {
|
|
return peer.match([&](const MTPDdialogPeer &data) {
|
|
const auto peer = _owner->peer(peerFromMTP(data.vpeer()));
|
|
return !_sublists.contains(peer);
|
|
}, [&](const MTPDdialogPeerFolder &data) {
|
|
LOG(("API Error: "
|
|
"updatePinnedSavedDialogs has folders."));
|
|
return false;
|
|
});
|
|
};
|
|
if (!ranges::none_of(order, notLoaded)) {
|
|
loadPinned();
|
|
} else {
|
|
_chatsList.pinned()->applyList(this, order);
|
|
_owner->notifyPinnedDialogsOrderUpdated();
|
|
}
|
|
}
|
|
|
|
void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) {
|
|
Expects(!parentChat());
|
|
|
|
update.vpeer().match([&](const MTPDdialogPeer &data) {
|
|
const auto peer = _owner->peer(peerFromMTP(data.vpeer()));
|
|
const auto i = _sublists.find(peer);
|
|
if (i != end(_sublists)) {
|
|
const auto entry = i->second.get();
|
|
_owner->setChatPinned(entry, FilterId(), update.is_pinned());
|
|
} else {
|
|
loadPinned();
|
|
}
|
|
}, [&](const MTPDdialogPeerFolder &data) {
|
|
DEBUG_LOG(("API Error: Folder in updateSavedDialogPinned."));
|
|
});
|
|
}
|
|
|
|
void SavedMessages::applySublistDeleted(not_null<PeerData*> sublistPeer) {
|
|
const auto i = _sublists.find(sublistPeer);
|
|
if (i == end(_sublists)) {
|
|
return;
|
|
}
|
|
const auto raw = i->second.get();
|
|
Core::App().notifications().clearFromSublist(raw);
|
|
owner().removeChatListEntry(raw);
|
|
|
|
if (ranges::contains(_lastSublists, not_null(raw))) {
|
|
reorderLastSublists();
|
|
}
|
|
|
|
_sublistDestroyed.fire(raw);
|
|
session().changes().sublistUpdated(
|
|
raw,
|
|
Data::SublistUpdate::Flag::Destroyed);
|
|
session().changes().entryUpdated(
|
|
raw,
|
|
Data::EntryUpdate::Flag::Destroyed);
|
|
_sublists.erase(i);
|
|
|
|
const auto history = owningHistory();
|
|
history->destroyMessagesBySublist(sublistPeer);
|
|
session().storage().unload(Storage::SharedMediaUnloadThread(
|
|
_owningHistory->peer->id,
|
|
MsgId(),
|
|
sublistPeer->id));
|
|
history->setForwardDraft(MsgId(), sublistPeer->id, {});
|
|
}
|
|
|
|
void SavedMessages::reorderLastSublists() {
|
|
if (!_parentChat) {
|
|
return;
|
|
}
|
|
|
|
// We want first kShowChatNamesCount histories, by last message date.
|
|
const auto pred = [](
|
|
not_null<SavedSublist*> a,
|
|
not_null<SavedSublist*> b) {
|
|
const auto aItem = a->chatListMessage();
|
|
const auto bItem = b->chatListMessage();
|
|
const auto aDate = aItem ? aItem->date() : TimeId(0);
|
|
const auto bDate = bItem ? bItem->date() : TimeId(0);
|
|
return aDate > bDate;
|
|
};
|
|
_lastSublists.clear();
|
|
_lastSublists.reserve(kShowSublistNamesCount + 1);
|
|
auto &&sublists = ranges::views::all(
|
|
*_chatsList.indexed()
|
|
) | ranges::views::transform([](not_null<Dialogs::Row*> row) {
|
|
return row->sublist();
|
|
});
|
|
auto nonPinnedChecked = 0;
|
|
for (const auto sublist : sublists) {
|
|
const auto i = ranges::upper_bound(
|
|
_lastSublists,
|
|
not_null(sublist),
|
|
pred);
|
|
if (size(_lastSublists) < kShowSublistNamesCount
|
|
|| i != end(_lastSublists)) {
|
|
_lastSublists.insert(i, sublist);
|
|
}
|
|
if (size(_lastSublists) > kShowSublistNamesCount) {
|
|
_lastSublists.pop_back();
|
|
}
|
|
if (!sublist->isPinnedDialog(FilterId())
|
|
&& ++nonPinnedChecked >= kShowSublistNamesCount) {
|
|
break;
|
|
}
|
|
}
|
|
++_lastSublistsVersion;
|
|
owningHistory()->updateChatListEntry();
|
|
}
|
|
|
|
void SavedMessages::listMessageChanged(HistoryItem *from, HistoryItem *to) {
|
|
if (from || to) {
|
|
reorderLastSublists();
|
|
}
|
|
}
|
|
|
|
int SavedMessages::recentSublistsListVersion() const {
|
|
return _lastSublistsVersion;
|
|
}
|
|
|
|
void SavedMessages::recentSublistsInvalidate(
|
|
not_null<SavedSublist*> sublist) {
|
|
Expects(_parentChat != nullptr);
|
|
|
|
if (ranges::contains(_lastSublists, sublist)) {
|
|
++_lastSublistsVersion;
|
|
owningHistory()->updateChatListEntry();
|
|
}
|
|
}
|
|
|
|
auto SavedMessages::recentSublists() const
|
|
-> const std::vector<not_null<SavedSublist*>> & {
|
|
return _lastSublists;
|
|
}
|
|
|
|
void SavedMessages::markUnreadCountsUnknown(MsgId readTillId) {
|
|
for (const auto &[peer, sublist] : _sublists) {
|
|
if (sublist->unreadCountCurrent() > 0) {
|
|
sublist->setInboxReadTill(readTillId, std::nullopt);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SavedMessages::updateUnreadCounts(
|
|
MsgId readTillId,
|
|
const base::flat_map<not_null<SavedSublist*>, int> &counts) {
|
|
for (const auto &[peer, sublist] : _sublists) {
|
|
const auto raw = sublist.get();
|
|
const auto i = counts.find(raw);
|
|
const auto count = (i != end(counts)) ? i->second : 0;
|
|
if (raw->unreadCountCurrent() != count) {
|
|
raw->setInboxReadTill(readTillId, count);
|
|
}
|
|
}
|
|
}
|
|
|
|
rpl::producer<> SavedMessages::destroyed() const {
|
|
if (!_parentChat) {
|
|
return rpl::never<>();
|
|
}
|
|
return _parentChat->flagsValue(
|
|
) | rpl::filter([=](const ChannelData::Flags::Change &update) {
|
|
using Flag = ChannelData::Flag;
|
|
return (update.diff & Flag::MonoforumAdmin)
|
|
&& !(update.value & Flag::MonoforumAdmin);
|
|
}) | rpl::take(1) | rpl::to_empty;
|
|
}
|
|
|
|
auto SavedMessages::sublistDestroyed() const
|
|
-> rpl::producer<not_null<SavedSublist*>> {
|
|
return _sublistDestroyed.events();
|
|
}
|
|
|
|
rpl::lifetime &SavedMessages::lifetime() {
|
|
return _lifetime;
|
|
}
|
|
|
|
} // namespace Data
|