From b2c01991a66887b18b26f4776e7bf292092c8d35 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 19 May 2025 14:59:57 +0400 Subject: [PATCH] Support unread state in sublists. --- Telegram/SourceFiles/api/api_updates.cpp | 26 + Telegram/SourceFiles/apiwrap.cpp | 10 + .../SourceFiles/data/data_forum_topic.cpp | 4 +- Telegram/SourceFiles/data/data_replies_list.h | 4 +- .../SourceFiles/data/data_saved_messages.cpp | 119 +- .../SourceFiles/data/data_saved_messages.h | 9 +- .../SourceFiles/data/data_saved_sublist.cpp | 1154 ++++++++++++++--- .../SourceFiles/data/data_saved_sublist.h | 126 +- Telegram/SourceFiles/data/data_session.cpp | 27 + Telegram/SourceFiles/data/data_session.h | 12 + Telegram/SourceFiles/history/history.cpp | 6 + Telegram/SourceFiles/history/history_item.cpp | 10 - .../view/history_view_chat_section.cpp | 111 +- .../history/view/history_view_chat_section.h | 1 + .../info/profile/info_profile_values.cpp | 4 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 3 +- 16 files changed, 1287 insertions(+), 339 deletions(-) diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index cade72e1b9..8efbfd32a1 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2442,6 +2442,32 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().data().updateRepliesReadTill({ id, readTillId, true }); } break; + case mtpc_updateReadMonoForumInbox: { + const auto &d = update.c_updateReadMonoForumInbox(); + const auto parentChatId = ChannelId(d.vchannel_id()); + const auto sublistPeerId = peerFromMTP(d.vsaved_peer_id()); + const auto readTillId = d.vread_max_id().v; + session().data().updateSublistReadTill({ + parentChatId, + sublistPeerId, + readTillId, + false, + }); + } break; + + case mtpc_updateReadMonoForumOutbox: { + const auto &d = update.c_updateReadMonoForumOutbox(); + const auto parentChatId = ChannelId(d.vchannel_id()); + const auto sublistPeerId = peerFromMTP(d.vsaved_peer_id()); + const auto readTillId = d.vread_max_id().v; + session().data().updateSublistReadTill({ + parentChatId, + sublistPeerId, + readTillId, + true, + }); + } break; + case mtpc_updateChannelAvailableMessages: { auto &d = update.c_updateChannelAvailableMessages(); if (const auto channel = session().data().channelLoaded(d.vchannel_id())) { diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 90d013cb60..352319e89e 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3197,11 +3197,21 @@ void ApiWrap::sendAction(const SendAction &action) { && !action.options.shortcutId && !action.replaceMediaOf) { const auto topicRootId = action.replyTo.topicRootId; + const auto monoforumPeerId = action.replyTo.monoforumPeerId; const auto topic = topicRootId ? action.history->peer->forumTopicFor(topicRootId) : nullptr; + const auto monoforum = monoforumPeerId + ? action.history->peer->monoforum() + : nullptr; + const auto sublist = monoforum + ? monoforum->sublistLoaded( + action.history->owner().peer(monoforumPeerId)) + : nullptr; if (topic) { topic->readTillEnd(); + } else if (sublist) { + sublist->readTillEnd(); } else { _session->data().histories().readInbox(action.history); } diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index 6e03125eb9..e3bf4757cb 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -362,8 +362,8 @@ void ForumTopic::subscribeToUnreadChanges() { ) | rpl::filter([=] { return inChatList(); }) | rpl::start_with_next([=]( - std::optional previous, - std::optional now) { + std::optional previous, + std::optional now) { if (previous.value_or(0) != now.value_or(0)) { _forum->recentTopicsInvalidate(this); } diff --git a/Telegram/SourceFiles/data/data_replies_list.h b/Telegram/SourceFiles/data/data_replies_list.h index 42f56c1aec..f5d32ddcfb 100644 --- a/Telegram/SourceFiles/data/data_replies_list.h +++ b/Telegram/SourceFiles/data/data_replies_list.h @@ -58,8 +58,6 @@ public: [[nodiscard]] bool isServerSideUnread( not_null item) const; - [[nodiscard]] std::optional computeUnreadCountLocally( - MsgId afterId) const; void requestUnreadCount(); void readTill(not_null item); @@ -79,6 +77,8 @@ private: void subscribeToUpdates(); void appendClientSideMessages(MessagesSlice &slice); + [[nodiscard]] std::optional computeUnreadCountLocally( + MsgId afterId) const; [[nodiscard]] bool buildFromData(not_null viewer); [[nodiscard]] bool applyItemDestroyed( diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 3645afe9e5..3bcc65ad4e 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "data/data_channel.h" -#include "data/data_peer.h" +#include "data/data_user.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "history/history.h" @@ -34,13 +34,15 @@ SavedMessages::SavedMessages( ChannelData *parentChat) : _owner(owner) , _parentChat(parentChat) -, _parentHistory(parentChat ? owner->history(parentChat).get() : nullptr) +, _owningHistory(parentChat ? owner->history(parentChat).get() : nullptr) , _chatsList( &_owner->session(), FilterId(), _owner->maxPinnedChatsLimitValue(this)) , _loadMore([=] { sendLoadMoreRequests(); }) { - if (_parentHistory && _parentHistory->inChatList()) { + // We don't assign _owningHistory for my Saved Messages here, + // because the data structures are not ready yet. + if (_owningHistory && _owningHistory->inChatList()) { preloadSublists(); } } @@ -51,10 +53,22 @@ bool SavedMessages::supported() const { return !_unsupported; } +void SavedMessages::markUnsupported() { + _unsupported = true; +} + ChannelData *SavedMessages::parentChat() const { return _parentChat; } +not_null SavedMessages::owningHistory() const { + if (!_owningHistory) { + const_cast(this)->_owningHistory + = _owner->history(_owner->session().user()); + } + return _owningHistory; +} + Session &SavedMessages::owner() const { return *_owner; } @@ -101,11 +115,6 @@ void SavedMessages::loadMore() { _loadMore.call(); } -void SavedMessages::loadMore(not_null sublist) { - _loadMoreSublistsScheduled.emplace(sublist); - _loadMore.call(); -} - void SavedMessages::sendLoadMore() { if (_loadMoreRequestId || _chatsList.loaded()) { return; @@ -132,7 +141,7 @@ void SavedMessages::sendLoadMore() { reorderLastSublists(); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { - _unsupported = true; + markUnsupported(); } _chatsList.setLoaded(); _loadMoreRequestId = 0; @@ -150,7 +159,7 @@ void SavedMessages::loadPinned() { _chatsListChanges.fire({}); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { - _unsupported = true; + markUnsupported(); } else { _pinnedLoaded = true; } @@ -158,82 +167,6 @@ void SavedMessages::loadPinned() { }).send(); } -void SavedMessages::sendLoadMore(not_null 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; - using Flag = MTPmessages_GetSavedHistory::Flag; - const auto requestId = _owner->session().api().request( - MTPmessages_GetSavedHistory( - MTP_flags(_parentChat ? Flag::f_parent_peer : Flag(0)), - _parentChat ? _parentChat->input : MTPInputPeer(), - sublist->sublistPeer()->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 count = 0; - auto list = (const QVector*)nullptr; - result.match([&](const MTPDmessages_channelMessages &data) { - if (const auto channel = _parentChat) { - channel->ptsReceived(data.vpts().v); - channel->processTopics(data.vtopics()); - list = &data.vmessages().v; - count = data.vcount().v; - } else { - 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; - if constexpr (MTPDmessages_messages::Is()) { - count = int(list->size()); - } else { - count = data.vcount().v; - } - }); - - _loadMoreRequests.remove(sublist); - if (!list) { - sublist->setFullLoaded(); - return; - } - auto items = std::vector>(); - 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), count); - 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(); - _loadMoreRequests[sublist] = requestId; -} - void SavedMessages::apply( const MTPmessages_SavedDialogs &result, bool pinned) { @@ -291,8 +224,7 @@ void SavedMessages::apply( offsetDate = item->date(); offsetId = topId; lastValid = true; - const auto entry = sublist(peer); - entry->applyMaybeLast(item); + sublist(peer)->applyMonoforumDialog(data, item); } else { lastValid = false; } @@ -321,9 +253,6 @@ void SavedMessages::sendLoadMoreRequests() { if (_loadMoreScheduled) { sendLoadMore(); } - for (const auto sublist : base::take(_loadMoreSublistsScheduled)) { - sendLoadMore(sublist); - } } void SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) { @@ -371,7 +300,7 @@ void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) { } void SavedMessages::reorderLastSublists() { - if (!_parentHistory) { + if (!_parentChat) { return; } @@ -411,7 +340,7 @@ void SavedMessages::reorderLastSublists() { } } ++_lastSublistsVersion; - _parentHistory->updateChatListEntry(); + owningHistory()->updateChatListEntry(); } void SavedMessages::listMessageChanged(HistoryItem *from, HistoryItem *to) { @@ -426,11 +355,11 @@ int SavedMessages::recentSublistsListVersion() const { void SavedMessages::recentSublistsInvalidate( not_null sublist) { - Expects(_parentHistory != nullptr); + Expects(_parentChat != nullptr); if (ranges::contains(_lastSublists, sublist)) { ++_lastSublistsVersion; - _parentHistory->updateChatListEntry(); + owningHistory()->updateChatListEntry(); } } diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index dd03e2d2a8..206b905f21 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -26,7 +26,10 @@ public: ~SavedMessages(); [[nodiscard]] bool supported() const; + void markUnsupported(); + [[nodiscard]] ChannelData *parentChat() const; + [[nodiscard]] not_null owningHistory() const; [[nodiscard]] Session &owner() const; [[nodiscard]] Main::Session &session() const; @@ -44,7 +47,6 @@ public: void preloadSublists(); void loadMore(); - void loadMore(not_null sublist); void apply(const MTPDupdatePinnedSavedDialogs &update); void apply(const MTPDupdateSavedDialogPinned &update); @@ -64,12 +66,11 @@ private: void reorderLastSublists(); void sendLoadMore(); - void sendLoadMore(not_null sublist); void sendLoadMoreRequests(); const not_null _owner; ChannelData *_parentChat = nullptr; - History *_parentHistory = nullptr; + History *_owningHistory = nullptr; rpl::event_stream> _sublistDestroyed; @@ -78,7 +79,6 @@ private: not_null, std::unique_ptr> _sublists; - base::flat_map, mtpRequestId> _loadMoreRequests; mtpRequestId _loadMoreRequestId = 0; mtpRequestId _pinnedRequestId = 0; @@ -87,7 +87,6 @@ private: PeerData *_offsetPeer = nullptr; SingleQueuedInvokation _loadMore; - base::flat_set> _loadMoreSublistsScheduled; bool _loadMoreScheduled = false; std::vector> _lastSublists; diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 367570cc0e..64a02b2d9d 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_saved_sublist.h" -#include "data/data_histories.h" +#include "apiwrap.h" +#include "data/data_changes.h" #include "data/data_channel.h" +#include "data/data_histories.h" +#include "data/data_messages.h" #include "data/data_peer.h" -#include "data/data_user.h" #include "data/data_saved_messages.h" #include "data/data_session.h" +#include "data/data_user.h" #include "history/view/history_view_item_preview.h" #include "history/history.h" #include "history/history_item.h" @@ -20,26 +23,143 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" namespace Data { +namespace { + +constexpr auto kMessagesPerPage = 50; +constexpr auto kReadRequestTimeout = 3 * crl::time(1000); + +} // namespace + +struct SavedSublist::Viewer { + MessagesSlice slice; + MsgId around = 0; + int limitBefore = 0; + int limitAfter = 0; + base::has_weak_ptr guard; + bool scheduled = false; +}; SavedSublist::SavedSublist( not_null parent, - not_null peer) -: Thread(&peer->owner(), Dialogs::Entry::Type::SavedSublist) + not_null sublistPeer) +: Thread(&sublistPeer->owner(), Dialogs::Entry::Type::SavedSublist) , _parent(parent) -, _history(peer->owner().history(peer)) { +, _sublistHistory(sublistPeer->owner().history(sublistPeer)) +, _readRequestTimer([=] { sendReadTillRequest(); }) { + if (parent->parentChat()) { + _flags |= Flag::InMonoforum; + } + subscribeToUnreadChanges(); } -SavedSublist::~SavedSublist() = default; +SavedSublist::~SavedSublist() { + histories().cancelRequest(base::take(_beforeId)); + histories().cancelRequest(base::take(_afterId)); + if (_readRequestTimer.isActive()) { + sendReadTillRequest(); + } + // session().api().unreadThings().cancelRequests(this); +} + +bool SavedSublist::inMonoforum() const { + return (_flags & Flag::InMonoforum) != 0; +} + +void SavedSublist::apply(const SublistReadTillUpdate &update) { + if (update.out) { + setOutboxReadTill(update.readTillId); + } else if (update.readTillId >= _inboxReadTillId) { + setInboxReadTill( + update.readTillId, + computeUnreadCountLocally(update.readTillId)); + } +} + +void SavedSublist::apply(const MessageUpdate &update) { + if (applyUpdate(update)) { + _instantChanges.fire({}); + } +} + +void SavedSublist::applyDifferenceTooLong() { + if (_skippedAfter.has_value()) { + _skippedAfter = std::nullopt; + _listChanges.fire({}); + } +} + +bool SavedSublist::removeOne(not_null item) { + const auto id = item->id; + const auto i = ranges::lower_bound(_list, id, std::greater<>()); + changeUnreadCountByMessage(id, -1); + if (i == end(_list) || *i != id) { + return false; + } + _list.erase(i); + if (_skippedBefore && _skippedAfter) { + _fullCount = *_skippedBefore + _list.size() + *_skippedAfter; + } else if (const auto known = _fullCount.current()) { + if (*known > 0) { + _fullCount = (*known - 1); + } + } + return true; +} + +rpl::producer SavedSublist::source( + MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto around = aroundId.fullId.msg; + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto viewer = lifetime.make_state(); + const auto push = [=] { + if (viewer->scheduled) { + viewer->scheduled = false; + if (buildFromData(viewer)) { + appendClientSideMessages(viewer->slice); + consumer.put_next_copy(viewer->slice); + } + } + }; + const auto pushInstant = [=] { + viewer->scheduled = true; + push(); + }; + const auto pushDelayed = [=] { + if (!viewer->scheduled) { + viewer->scheduled = true; + crl::on_main(&viewer->guard, push); + } + }; + viewer->around = around; + viewer->limitBefore = limitBefore; + viewer->limitAfter = limitAfter; + + const auto history = owningHistory(); + history->session().changes().historyUpdates( + history, + HistoryUpdate::Flag::ClientSideMessages + ) | rpl::start_with_next(pushDelayed, lifetime); + + _listChanges.events( + ) | rpl::start_with_next(pushDelayed, lifetime); + + _instantChanges.events( + ) | rpl::start_with_next(pushInstant, lifetime); + + pushInstant(); + return lifetime; + }; +} not_null SavedSublist::parent() const { return _parent; } not_null SavedSublist::owningHistory() { - const auto chat = parentChat(); - return _history->owner().history(chat - ? (PeerData*)chat - : _history->session().user().get()); + return _parent->owningHistory(); } ChannelData *SavedSublist::parentChat() const { @@ -47,17 +167,13 @@ ChannelData *SavedSublist::parentChat() const { } not_null SavedSublist::sublistPeer() const { - return _history->peer; + return _sublistHistory->peer; } bool SavedSublist::isHiddenAuthor() const { return sublistPeer()->isSavedHiddenAuthor(); } -bool SavedSublist::isFullLoaded() const { - return (_flags & Flag::FullLoaded) != 0; -} - rpl::producer<> SavedSublist::destroyed() const { using namespace rpl::mappers; return rpl::merge( @@ -67,133 +183,611 @@ rpl::producer<> SavedSublist::destroyed() const { ) | rpl::to_empty); } -auto SavedSublist::messages() const --> const std::vector> & { - return _items; +void SavedSublist::applyMaybeLast(not_null item, bool added) { + growLastKnownServerMessageId(item->id); + if (!_lastServerMessage || (*_lastServerMessage)->id < item->id) { + setLastServerMessage(item); + resolveChatListMessageGroup(); + } } -void SavedSublist::applyMaybeLast(not_null item, bool added) { - const auto before = []( - not_null a, - not_null b) { - return IsServerMsgId(a->id) - ? (IsServerMsgId(b->id) ? (a->id < b->id) : true) - : (IsServerMsgId(b->id) ? false : (a->id < b->id)); - }; +void SavedSublist::applyItemAdded(not_null item) { + if (item->isRegular()) { + setLastServerMessage(item); + } else { + setLastMessage(item); + } +} - const auto was = _items.empty() ? nullptr : _items.front().get(); - if (_items.empty()) { - _items.push_back(item); - } else if (_items.front() == item) { - return; - } else if (!isFullLoaded() - && _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) { - return; - } else if (before(*i, item)) { - _items.insert(i, item); - break; - } +void SavedSublist::applyItemRemoved(MsgId id) { + if (const auto lastItem = lastMessage()) { + if (lastItem->id == id) { + _lastMessage = std::nullopt; } } - if (added && _fullCount) { - ++*_fullCount; + if (const auto lastServerItem = lastServerMessage()) { + if (lastServerItem->id == id) { + _lastServerMessage = std::nullopt; + } } - if (_items.front() == item) { - setChatListTimeId(item->date()); - resolveChatListMessageGroup(); - - _parent->listMessageChanged(was, item.get()); + if (const auto chatListItem = _chatListMessage.value_or(nullptr)) { + if (chatListItem->id == id) { + _chatListMessage = std::nullopt; + requestChatListMessage(); + } } - _changed.fire({}); } -void SavedSublist::removeOne(not_null item) { - if (_items.empty()) { - return; +void SavedSublist::requestChatListMessage() { + if (!chatListMessageKnown()) { + //forum()->requestTopic(_rootId); // #TODO monoforum } - const auto last = (_items.front() == item); - const auto from = ranges::remove(_items, item); - const auto removed = end(_items) - from; - if (removed) { - _items.erase(from, end(_items)); +} + +void SavedSublist::readTillEnd() { + readTill(_lastKnownServerMessageId); +} + +bool SavedSublist::buildFromData(not_null viewer) { + if (_list.empty() && _skippedBefore == 0 && _skippedAfter == 0) { + viewer->slice.ids.clear(); + viewer->slice.nearestToAround = FullMsgId(); + viewer->slice.fullCount + = viewer->slice.skippedBefore + = viewer->slice.skippedAfter + = 0; + ranges::reverse(viewer->slice.ids); + return true; } - if (_fullCount) { - --*_fullCount; + const auto around = (viewer->around != ShowAtUnreadMsgId) + ? viewer->around + : computeInboxReadTillFull(); + if (_list.empty() + || (!around && _skippedAfter != 0) + || (around > _list.front() && _skippedAfter != 0) + || (around > 0 && around < _list.back() && _skippedBefore != 0)) { + loadAround(around); + return false; } - if (last) { - if (_items.empty()) { - if (isFullLoaded()) { - updateChatListExistence(); + const auto i = around + ? ranges::lower_bound(_list, around, std::greater<>()) + : end(_list); + const auto availableBefore = int(end(_list) - i); + const auto availableAfter = int(i - begin(_list)); + const auto useBefore = std::min(availableBefore, viewer->limitBefore + 1); + const auto useAfter = std::min(availableAfter, viewer->limitAfter); + const auto slice = &viewer->slice; + if (_skippedBefore.has_value()) { + slice->skippedBefore + = (*_skippedBefore + (availableBefore - useBefore)); + } + if (_skippedAfter.has_value()) { + slice->skippedAfter + = (*_skippedAfter + (availableAfter - useAfter)); + } + + const auto peerId = owningHistory()->peer->id; + slice->ids.clear(); + auto nearestToAround = std::optional(); + slice->ids.reserve(useAfter + useBefore); + for (auto j = i - useAfter, e = i + useBefore; j != e; ++j) { + const auto id = *j; + if (!nearestToAround && id < around) { + nearestToAround = (j == i - useAfter) + ? id + : *(j - 1); + } + slice->ids.emplace_back(peerId, id); + } + slice->nearestToAround = FullMsgId( + peerId, + nearestToAround.value_or( + slice->ids.empty() ? 0 : slice->ids.back().msg)); + slice->fullCount = _fullCount.current(); + + ranges::reverse(viewer->slice.ids); + + if (_skippedBefore != 0 && useBefore < viewer->limitBefore + 1) { + loadBefore(); + } + if (_skippedAfter != 0 && useAfter < viewer->limitAfter) { + loadAfter(); + } + + return true; +} + +bool SavedSublist::applyUpdate(const MessageUpdate &update) { + using Flag = MessageUpdate::Flag; + + if (update.item->history() != owningHistory() + || !update.item->isRegular() + || update.item->sublistPeerId() != sublistPeer()->id) { + return false; + } else if (update.flags & Flag::Destroyed) { + return removeOne(update.item); + } + const auto id = update.item->id; + if (update.flags & Flag::NewAdded) { + changeUnreadCountByMessage(id, 1); + } + const auto i = ranges::lower_bound(_list, id, std::greater<>()); + if (_skippedAfter != 0 + || (i != end(_list) && *i == id)) { + return false; + } + _list.insert(i, id); + if (_skippedBefore && _skippedAfter) { + _fullCount = *_skippedBefore + _list.size() + *_skippedAfter; + } else if (const auto known = _fullCount.current()) { + _fullCount = *known + 1; + } + return true; +} + +bool SavedSublist::processMessagesIsEmpty( + const MTPmessages_Messages &result) { + const auto guard = gsl::finally([&] { _listChanges.fire({}); }); + + const auto list = result.match([&]( + const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(HistoryWidget::messagesReceived)")); + return QVector(); + }, [&](const auto &data) { + owner().processUsers(data.vusers()); + owner().processChats(data.vchats()); + return data.vmessages().v; + }); + + const auto fullCount = result.match([&]( + const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(HistoryWidget::messagesReceived)")); + return 0; + }, [&](const MTPDmessages_messages &data) { + return int(data.vmessages().v.size()); + }, [&](const MTPDmessages_messagesSlice &data) { + return data.vcount().v; + }, [&](const MTPDmessages_channelMessages &data) { + if (const auto channel = owningHistory()->peer->asChannel()) { + channel->ptsReceived(data.vpts().v); + channel->processTopics(data.vtopics()); + } else { + LOG(("API Error: received messages.channelMessages when " + "no channel was passed! (HistoryWidget::messagesReceived)")); + } + return data.vcount().v; + }); + + if (list.isEmpty()) { + return true; + } + + const auto maxId = IdFromMessage(list.front()); + const auto wasSize = int(_list.size()); + const auto toFront = (wasSize > 0) && (maxId > _list.front()); + const auto localFlags = MessageFlags(); + const auto type = NewMessageType::Existing; + auto refreshed = std::vector(); + if (toFront) { + refreshed.reserve(_list.size() + list.size()); + } + auto skipped = 0; + for (const auto &message : list) { + if (const auto item = owner().addNewMessage(message, localFlags, type)) { + if (item->sublistPeerId() == sublistPeer()->id) { + if (toFront && item->id > _list.front()) { + refreshed.push_back(item->id); + } else if (_list.empty() || item->id < _list.back()) { + _list.push_back(item->id); + } } else { - updateChatListEntry(); - crl::on_main(this, [=] { _parent->loadMore(this); }); + ++skipped; } } else { - setChatListTimeId(_items.front()->date()); + ++skipped; } + } + if (toFront) { + refreshed.insert(refreshed.end(), _list.begin(), _list.end()); + _list = std::move(refreshed); + } - _parent->listMessageChanged(item.get(), chatListMessage()); + const auto nowSize = int(_list.size()); + auto &decrementFrom = toFront ? _skippedAfter : _skippedBefore; + if (decrementFrom.has_value()) { + *decrementFrom = std::max( + *decrementFrom - (nowSize - wasSize), + 0); } - if (removed || _fullCount) { - _changed.fire({}); + + const auto checkedCount = std::max(fullCount - skipped, nowSize); + if (_skippedBefore && _skippedAfter) { + auto &correct = toFront ? _skippedBefore : _skippedAfter; + *correct = std::max( + checkedCount - *decrementFrom - nowSize, + 0); + *decrementFrom = checkedCount - *correct - nowSize; + Assert(*decrementFrom >= 0); + } else if (_skippedBefore) { + *_skippedBefore = std::min(*_skippedBefore, checkedCount - nowSize); + _skippedAfter = checkedCount - *_skippedBefore - nowSize; + } else if (_skippedAfter) { + *_skippedAfter = std::min(*_skippedAfter, checkedCount - nowSize); + _skippedBefore = checkedCount - *_skippedAfter - nowSize; } + _fullCount = checkedCount; + + checkReadTillEnd(); + + Ensures(list.size() >= skipped); + return (list.size() == skipped); +} + +void SavedSublist::setInboxReadTill( + MsgId readTillId, + std::optional unreadCount) { + const auto newReadTillId = std::max(readTillId.bare, int64(1)); + const auto ignore = (newReadTillId < _inboxReadTillId); + if (ignore) { + return; + } + const auto changed = (newReadTillId > _inboxReadTillId); + if (changed) { + _inboxReadTillId = newReadTillId; + } + if (_skippedAfter == 0 + && !_list.empty() + && _inboxReadTillId >= _list.front()) { + unreadCount = 0; + } + const auto wasUnreadCount = _unreadCount; + if (_unreadCount.current() != unreadCount + && (changed || unreadCount.has_value())) { + setUnreadCount(unreadCount); + } +} + +MsgId SavedSublist::inboxReadTillId() const { + return _inboxReadTillId; +} + +MsgId SavedSublist::computeInboxReadTillFull() const { + return _inboxReadTillId; +} + +void SavedSublist::setOutboxReadTill(MsgId readTillId) { + const auto newReadTillId = std::max(readTillId.bare, int64(1)); + if (newReadTillId > _outboxReadTillId) { + _outboxReadTillId = newReadTillId; + const auto history = owningHistory(); + history->session().changes().historyUpdated( + history, + HistoryUpdate::Flag::OutboxRead); + } +} + +MsgId SavedSublist::computeOutboxReadTillFull() const { + return _outboxReadTillId; +} + +void SavedSublist::setUnreadCount(std::optional count) { + _unreadCount = count; + if (!count && !_readRequestTimer.isActive() && !_readRequestId) { + reloadUnreadCountIfNeeded(); + } +} + +bool SavedSublist::unreadCountKnown() const { + return !inMonoforum() || _unreadCount.current().has_value(); +} + +int SavedSublist::unreadCountCurrent() const { + return _unreadCount.current().value_or(0); +} + +rpl::producer> SavedSublist::unreadCountValue() const { + if (!inMonoforum()) { + return rpl::single(std::optional(0)); + } + return _unreadCount.value(); +} + +int SavedSublist::displayedUnreadCount() const { + return (_inboxReadTillId > 1) ? unreadCountCurrent() : 0; +} + +void SavedSublist::changeUnreadCountByMessage(MsgId id, int delta) { + if (!inMonoforum() || !_inboxReadTillId) { + setUnreadCount(std::nullopt); + return; + } + const auto count = _unreadCount.current(); + if (count.has_value() && (id > _inboxReadTillId)) { + setUnreadCount(std::max(*count + delta, 0)); + } +} + +bool SavedSublist::isServerSideUnread( + not_null item) const { + if (!inMonoforum()) { + return false; + } + const auto till = item->out() + ? computeOutboxReadTillFull() + : computeInboxReadTillFull(); + return (item->id > till); +} + +void SavedSublist::checkReadTillEnd() { + if (_unreadCount.current() != 0 + && _skippedAfter == 0 + && !_list.empty() + && _inboxReadTillId >= _list.front()) { + setUnreadCount(0); + } +} + +std::optional SavedSublist::computeUnreadCountLocally( + MsgId afterId) const { + Expects(afterId >= _inboxReadTillId); + + const auto currentUnreadCountAfter = _unreadCount.current(); + const auto startingMarkingAsRead = (currentUnreadCountAfter == 0) + && (_inboxReadTillId == 1) + && (afterId > 1); + const auto wasUnreadCountAfter = startingMarkingAsRead + ? _fullCount.current().value_or(0) + : currentUnreadCountAfter; + const auto readTillId = std::max(afterId, MsgId(1)); + const auto wasReadTillId = _inboxReadTillId; + const auto backLoaded = (_skippedBefore == 0); + const auto frontLoaded = (_skippedAfter == 0); + const auto fullLoaded = backLoaded && frontLoaded; + const auto allUnread = (readTillId == MsgId(1)) + || (fullLoaded && _list.empty()); + if (allUnread && fullLoaded) { + // Should not happen too often unless the list is empty. + return int(_list.size()); + } else if (frontLoaded && !_list.empty() && readTillId >= _list.front()) { + // Always "count by local data" if read till the end. + return 0; + } else if (wasReadTillId == readTillId) { + // Otherwise don't recount the same value over and over. + return wasUnreadCountAfter; + } else if (frontLoaded && !_list.empty() && readTillId >= _list.back()) { + // And count by local data if it is available and read-till changed. + return int(ranges::lower_bound(_list, readTillId, std::greater<>()) + - begin(_list)); + } else if (_list.empty()) { + return std::nullopt; + } else if (wasUnreadCountAfter.has_value() + && (frontLoaded || readTillId <= _list.front()) + && (backLoaded || wasReadTillId >= _list.back())) { + // Count how many were read since previous value. + const auto from = ranges::lower_bound( + _list, + readTillId, + std::greater<>()); + const auto till = ranges::lower_bound( + from, + end(_list), + wasReadTillId, + std::greater<>()); + return std::max(*wasUnreadCountAfter - int(till - from), 0); + } + return std::nullopt; +} + +void SavedSublist::requestUnreadCount() { + if (_reloadUnreadCountRequestId) { + return; + } + //const auto weak = base::make_weak(this); // #TODO monoforum + //const auto session = &_parent->session(); + //const auto apply = [weak](MsgId readTill, int unreadCount) { + // if (const auto strong = weak.get()) { + // strong->setInboxReadTill(readTill, unreadCount); + // } + //}; + //_reloadUnreadCountRequestId = session->api().request( + // ... + //).done([=](const ... &result) { + // if (weak) { + // _reloadUnreadCountRequestId = 0; + // } + // ... + //}).send(); +} + +void SavedSublist::readTill(not_null item) { + readTill(item->id, item); +} + +void SavedSublist::readTill(MsgId tillId) { + const auto parentChat = _parent->parentChat(); + if (!parentChat) { + return; + } + readTill(tillId, owner().message(parentChat->id, tillId)); +} + +void SavedSublist::readTill( + MsgId tillId, + HistoryItem *tillIdItem) { + if (!IsServerMsgId(tillId)) { + return; + } + const auto was = computeInboxReadTillFull(); + const auto now = tillId; + if (now < was) { + return; + } + const auto unreadCount = computeUnreadCountLocally(now); + const auto fast = (tillIdItem && tillIdItem->out()) + || !unreadCount.has_value(); + if (was < now || (fast && now == was)) { + setInboxReadTill(now, unreadCount); + if (!_readRequestTimer.isActive()) { + _readRequestTimer.callOnce(fast ? 0 : kReadRequestTimeout); + } else if (fast && _readRequestTimer.remainingTime() > 0) { + _readRequestTimer.callOnce(0); + } + } + // Core::App().notifications().clearIncomingFromSublist(this); // #TODO monoforum +} + +void SavedSublist::sendReadTillRequest() { + const auto parentChat = _parent->parentChat(); + if (!parentChat) { + return; + } + if (_readRequestTimer.isActive()) { + _readRequestTimer.cancel(); + } + const auto api = &_parent->session().api(); + api->request(base::take(_readRequestId)).cancel(); + + _readRequestId = api->request(MTPmessages_ReadSavedHistory( + parentChat->input, + sublistPeer()->input, + MTP_int(computeInboxReadTillFull()) + )).done(crl::guard(this, [=] { + _readRequestId = 0; + reloadUnreadCountIfNeeded(); + })).send(); +} + +void SavedSublist::reloadUnreadCountIfNeeded() { + if (unreadCountKnown()) { + return; + } else if (inboxReadTillId() < computeInboxReadTillFull()) { + _readRequestTimer.callOnce(0); + } else { + requestUnreadCount(); + } +} + +void SavedSublist::subscribeToUnreadChanges() { + if (!inMonoforum()) { + return; + } + _unreadCount.value( + ) | rpl::map([=](std::optional value) { + return value ? displayedUnreadCount() : value; + }) | rpl::distinct_until_changed( + ) | rpl::combine_previous( + ) | rpl::filter([=] { + return inChatList(); + }) | rpl::start_with_next([=]( + std::optional previous, + std::optional now) { + if (previous.value_or(0) != now.value_or(0)) { + _parent->recentSublistsInvalidate(this); + } + notifyUnreadStateChange(unreadStateFor( + previous.value_or(0), + previous.has_value())); + }, _lifetime); +} + +void SavedSublist::applyMonoforumDialog( + const MTPDmonoForumDialog &data, + not_null topItem) { + //if (const auto draft = data.vdraft()) { // #TODO monoforum + // draft->match([&](const MTPDdraftMessage &data) { + // Data::ApplyPeerCloudDraft( + // &session(), + // channel()->id, + // _rootId, + // data); + // }, [](const MTPDdraftMessageEmpty&) {}); + //} + + setInboxReadTill( + data.vread_inbox_max_id().v, + data.vunread_count().v); + setOutboxReadTill(data.vread_outbox_max_id().v); + applyMaybeLast(topItem); } rpl::producer<> SavedSublist::changes() const { - return _changed.events(); + return _listChanges.events(); +} + +void SavedSublist::loadFullCount() { + if (!_fullCount.current() && !_loadingAround) { + loadAround(0); + } +} + +void SavedSublist::appendClientSideMessages(MessagesSlice &slice) { + const auto &messages = owningHistory()->clientSideMessages(); + if (messages.empty()) { + return; + } else if (slice.ids.empty()) { + if (slice.skippedBefore != 0 || slice.skippedAfter != 0) { + return; + } + slice.ids.reserve(messages.size()); + const auto sublistPeerId = sublistPeer()->id; + for (const auto &item : messages) { + if (item->sublistPeerId() != sublistPeerId) { + continue; + } + slice.ids.push_back(item->fullId()); + } + ranges::sort(slice.ids); + return; + } + const auto sublistPeerId = sublistPeer()->id; + auto dates = std::vector(); + dates.reserve(slice.ids.size()); + for (const auto &id : slice.ids) { + const auto message = owner().message(id); + Assert(message != nullptr); + + dates.push_back(message->date()); + } + for (const auto &item : messages) { + if (item->sublistPeerId() != sublistPeerId) { + continue; + } + const auto date = item->date(); + if (date < dates.front()) { + if (slice.skippedBefore != 0) { + if (slice.skippedBefore) { + ++*slice.skippedBefore; + } + continue; + } + dates.insert(dates.begin(), date); + slice.ids.insert(slice.ids.begin(), item->fullId()); + } else { + auto to = dates.size(); + for (; to != 0; --to) { + const auto checkId = slice.ids[to - 1].msg; + if (dates[to - 1] > date) { + continue; + } else if (dates[to - 1] < date + || IsServerMsgId(checkId) + || checkId < item->id) { + break; + } + } + dates.insert(dates.begin() + to, date); + slice.ids.insert(slice.ids.begin() + to, item->fullId()); + } + } } std::optional SavedSublist::fullCount() const { - return isFullLoaded() ? int(_items.size()) : _fullCount; + return _fullCount.current(); } rpl::producer SavedSublist::fullCountValue() const { - return _changed.events_starting_with({}) | rpl::map([=] { - return fullCount(); - }) | rpl::filter_optional(); -} - -void SavedSublist::append( - std::vector> &&items, - int fullCount) { - _fullCount = fullCount; - if (items.empty()) { - setFullLoaded(); - } else if (_items.empty()) { - _items = std::move(items); - setChatListTimeId(_items.front()->date()); - _changed.fire({}); - } else if (_items.back()->id > items.front()->id) { - _items.insert(end(_items), begin(items), end(items)); - _changed.fire({}); - } else { - _items.insert(end(_items), begin(items), end(items)); - ranges::stable_sort( - _items, - ranges::greater(), - &HistoryItem::id); - ranges::unique(_items, ranges::greater(), &HistoryItem::id); - _changed.fire({}); - } -} - -void SavedSublist::setFullLoaded(bool loaded) { - if (loaded != isFullLoaded()) { - if (loaded) { - _flags |= Flag::FullLoaded; - if (_items.empty()) { - updateChatListExistence(); - } - } else { - _flags &= ~Flag::FullLoaded; - } - _changed.fire({}); - } + return _fullCount.value() | rpl::filter_optional(); } int SavedSublist::fixedOnTopIndex() const { @@ -206,50 +800,87 @@ bool SavedSublist::shouldBeInChatList() const { return false; } } - return isPinnedDialog(FilterId()) || !_items.empty(); + return isPinnedDialog(FilterId()) + || !lastMessageKnown() + || (lastMessage() != nullptr); +} + +HistoryItem *SavedSublist::lastMessage() const { + return _lastMessage.value_or(nullptr); +} + +bool SavedSublist::lastMessageKnown() const { + return _lastMessage.has_value(); +} + +HistoryItem *SavedSublist::lastServerMessage() const { + return _lastServerMessage.value_or(nullptr); +} + +bool SavedSublist::lastServerMessageKnown() const { + return _lastServerMessage.has_value(); +} + +MsgId SavedSublist::lastKnownServerMessageId() const { + return _lastKnownServerMessageId; } Dialogs::UnreadState SavedSublist::chatListUnreadState() const { - return {}; + if (!inMonoforum()) { + return {}; + } + return unreadStateFor(displayedUnreadCount(), unreadCountKnown()); } Dialogs::BadgesState SavedSublist::chatListBadgesState() const { - return {}; + if (!inMonoforum()) { + return {}; + } + auto result = Dialogs::BadgesForUnread( + chatListUnreadState(), + Dialogs::CountInBadge::Messages, + Dialogs::IncludeInBadge::All); + if (!result.unread && inboxReadTillId() < 2) { + result.unread = (_lastKnownServerMessageId + > _parent->owningHistory()->inboxReadTillId()); + result.unreadMuted = muted(); + } + return result; } HistoryItem *SavedSublist::chatListMessage() const { - return _items.empty() ? nullptr : _items.front().get(); + return _lastMessage.value_or(nullptr); } bool SavedSublist::chatListMessageKnown() const { - return true; + return _lastMessage.has_value(); } const QString &SavedSublist::chatListName() const { - return _history->chatListName(); + return _sublistHistory->chatListName(); } const base::flat_set &SavedSublist::chatListNameWords() const { - return _history->chatListNameWords(); + return _sublistHistory->chatListNameWords(); } const base::flat_set &SavedSublist::chatListFirstLetters() const { - return _history->chatListFirstLetters(); + return _sublistHistory->chatListFirstLetters(); } const QString &SavedSublist::chatListNameSortKey() const { - return _history->chatListNameSortKey(); + return _sublistHistory->chatListNameSortKey(); } int SavedSublist::chatListNameVersion() const { - return _history->chatListNameVersion(); + return _sublistHistory->chatListNameVersion(); } void SavedSublist::paintUserpic( Painter &p, Ui::PeerUserpicView &view, const Dialogs::Ui::PaintContext &context) const { - _history->paintUserpic(p, view, context); + _sublistHistory->paintUserpic(p, view, context); } HistoryView::SendActionPainter *SavedSublist::sendActionPainter() { @@ -277,17 +908,6 @@ void SavedSublist::hasUnreadReactionChanged(bool has) { notifyUnreadStateChange(was); } -bool SavedSublist::isServerSideUnread( - not_null item) const { - return false; -} - - -void SavedSublist::chatListPreloadData() { - sublistPeer()->loadUserpic(); - allowChatListMessageResolve(); -} - void SavedSublist::allowChatListMessageResolve() { if (_flags & Flag::ResolveChatListMessage) { return; @@ -296,27 +916,257 @@ void SavedSublist::allowChatListMessageResolve() { 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()) { + if (!(_flags & Flag::ResolveChatListMessage)) { 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); + const auto item = _lastServerMessage.value_or(nullptr); + if (item && item->groupId() != MessageGroupId()) { + if (owner().groups().isGroupOfOne(item) + && !item->toPreview({ + .hideSender = true, + .hideCaption = true }).images.empty() + && _requestedGroups.emplace(item->fullId()).second) { + owner().histories().requestGroupAround(item); + } } } +void SavedSublist::growLastKnownServerMessageId(MsgId id) { + _lastKnownServerMessageId = std::max(_lastKnownServerMessageId, id); +} + +void SavedSublist::setLastServerMessage(HistoryItem *item) { + if (item) { + growLastKnownServerMessageId(item->id); + } + _lastServerMessage = item; + if (_lastMessage + && *_lastMessage + && !(*_lastMessage)->isRegular() + && (!item + || (*_lastMessage)->date() > item->date() + || (*_lastMessage)->isSending())) { + return; + } + setLastMessage(item); +} + +void SavedSublist::setLastMessage(HistoryItem *item) { + if (_lastMessage && *_lastMessage == item) { + return; + } + _lastMessage = item; + if (!item || item->isRegular()) { + _lastServerMessage = item; + if (item) { + growLastKnownServerMessageId(item->id); + } + } + setChatListMessage(item); +} + +void SavedSublist::setChatListMessage(HistoryItem *item) { + if (_chatListMessage && *_chatListMessage == item) { + return; + } + const auto was = _chatListMessage.value_or(nullptr); + if (item) { + if (item->isSponsored()) { + return; + } + if (_chatListMessage + && *_chatListMessage + && !(*_chatListMessage)->isRegular() + && (*_chatListMessage)->date() > item->date()) { + return; + } + _chatListMessage = item; + setChatListTimeId(item->date()); + } else if (!_chatListMessage || *_chatListMessage) { + _chatListMessage = nullptr; + updateChatListEntry(); + } + _parent->listMessageChanged(was, item); +} + +void SavedSublist::chatListPreloadData() { + sublistPeer()->loadUserpic(); + allowChatListMessageResolve(); +} + +Dialogs::UnreadState SavedSublist::unreadStateFor( + int count, + bool known) const { + auto result = Dialogs::UnreadState(); + const auto muted = this->muted(); + result.messages = count; + result.chats = count ? 1 : 0; + result.chatsMuted = muted ? result.chats : 0; + result.known = known; + return result; +} + +Histories &SavedSublist::histories() { + return owner().histories(); +} + +void SavedSublist::loadAround(MsgId id) { + if (_loadingAround && *_loadingAround == id) { + return; + } + histories().cancelRequest(base::take(_beforeId)); + histories().cancelRequest(base::take(_afterId)); + + const auto send = [=](Fn finish) { + using Flag = MTPmessages_GetSavedHistory::Flag; + const auto parentChat = _parent->parentChat(); + return session().api().request(MTPmessages_GetSavedHistory( + MTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)), + parentChat ? parentChat->input : MTPInputPeer(), + sublistPeer()->input, + MTP_int(id), // offset_id + MTP_int(0), // offset_date + MTP_int(id ? (-kMessagesPerPage / 2) : 0), // add_offset + MTP_int(kMessagesPerPage), // limit + MTP_int(0), // max_id + MTP_int(0), // min_id + MTP_long(0)) // hash + ).done([=](const MTPmessages_Messages &result) { + _beforeId = 0; + _loadingAround = std::nullopt; + finish(); + + if (!id) { + _skippedAfter = 0; + } else { + _skippedAfter = std::nullopt; + } + _skippedBefore = std::nullopt; + _list.clear(); + if (processMessagesIsEmpty(result)) { + _fullCount = _skippedBefore = _skippedAfter = 0; + } else if (id) { + Assert(!_list.empty()); + if (_list.front() <= id) { + _skippedAfter = 0; + } else if (_list.back() >= id) { + _skippedBefore = 0; + } + } + checkReadTillEnd(); + }).fail([=](const MTP::Error &error) { + if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { + _parent->markUnsupported(); + } + _beforeId = 0; + _loadingAround = std::nullopt; + finish(); + }).send(); + }; + _loadingAround = id; + _beforeId = histories().sendRequest( + owningHistory(), + Histories::RequestType::History, + send); +} + +void SavedSublist::loadBefore() { + Expects(!_list.empty()); + + if (_loadingAround) { + histories().cancelRequest(base::take(_beforeId)); + } else if (_beforeId) { + return; + } + + const auto last = _list.back(); + const auto send = [=](Fn finish) { + using Flag = MTPmessages_GetSavedHistory::Flag; + const auto parentChat = _parent->parentChat(); + return session().api().request(MTPmessages_GetSavedHistory( + MTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)), + parentChat ? parentChat->input : MTPInputPeer(), + sublistPeer()->input, + MTP_int(last), // offset_id + MTP_int(0), // offset_date + MTP_int(0), // add_offset + MTP_int(kMessagesPerPage), // limit + MTP_int(0), // min_id + MTP_int(0), // max_id + MTP_long(0) // hash + )).done([=](const MTPmessages_Messages &result) { + _beforeId = 0; + finish(); + + if (_list.empty()) { + return; + } else if (_list.back() != last) { + loadBefore(); + } else if (processMessagesIsEmpty(result)) { + _skippedBefore = 0; + if (_skippedAfter == 0) { + _fullCount = _list.size(); + } + } + }).fail([=] { + _beforeId = 0; + finish(); + }).send(); + }; + _beforeId = histories().sendRequest( + owningHistory(), + Histories::RequestType::History, + send); +} + +void SavedSublist::loadAfter() { + Expects(!_list.empty()); + + if (_afterId) { + return; + } + + const auto first = _list.front(); + const auto send = [=](Fn finish) { + using Flag = MTPmessages_GetSavedHistory::Flag; + const auto parentChat = _parent->parentChat(); + return session().api().request(MTPmessages_GetSavedHistory( + MTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)), + parentChat ? parentChat->input : MTPInputPeer(), + sublistPeer()->input, + MTP_int(first + 1), // offset_id + MTP_int(0), // offset_date + MTP_int(-kMessagesPerPage), // add_offset + MTP_int(kMessagesPerPage), // limit + MTP_int(0), // min_id + MTP_int(0), // max_id + MTP_long(0) // hash + )).done([=](const MTPmessages_Messages &result) { + _afterId = 0; + finish(); + + if (_list.empty()) { + return; + } else if (_list.front() != first) { + loadAfter(); + } else if (processMessagesIsEmpty(result)) { + _skippedAfter = 0; + if (_skippedBefore == 0) { + _fullCount = _list.size(); + } + checkReadTillEnd(); + } + }).fail([=] { + _afterId = 0; + finish(); + }).send(); + }; + _afterId = histories().sendRequest( + owningHistory(), + Histories::RequestType::History, + send); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h index 4217a8fb67..468e4b647d 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.h +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/timer.h" #include "data/data_thread.h" #include "dialogs/ui/dialogs_message_view.h" @@ -16,31 +17,60 @@ class History; namespace Data { class Session; +class Histories; class SavedMessages; +struct MessagePosition; +struct MessageUpdate; +struct SublistReadTillUpdate; +struct MessagesSlice; class SavedSublist final : public Data::Thread { public: - SavedSublist(not_null parent, not_null peer); + SavedSublist( + not_null parent, + not_null sublistPeer); ~SavedSublist(); + [[nodiscard]] bool inMonoforum() const; + + void apply(const SublistReadTillUpdate &update); + void apply(const MessageUpdate &update); + void applyDifferenceTooLong(); + bool removeOne(not_null item); + + [[nodiscard]] rpl::producer source( + MessagePosition aroundId, + int limitBefore, + int limitAfter); + [[nodiscard]] not_null parent() const; [[nodiscard]] not_null owningHistory() override; [[nodiscard]] ChannelData *parentChat() const; [[nodiscard]] not_null sublistPeer() const; [[nodiscard]] bool isHiddenAuthor() const; - [[nodiscard]] bool isFullLoaded() const; [[nodiscard]] rpl::producer<> destroyed() const; - [[nodiscard]] auto messages() const - -> const std::vector> &; + void growLastKnownServerMessageId(MsgId id); void applyMaybeLast(not_null item, bool added = false); - void removeOne(not_null item); - void append(std::vector> &&items, int fullCount); - void setFullLoaded(bool loaded = true); + void applyItemAdded(not_null item); + void applyItemRemoved(MsgId id); [[nodiscard]] rpl::producer<> changes() const; [[nodiscard]] std::optional fullCount() const; [[nodiscard]] rpl::producer fullCountValue() const; + [[nodiscard]] rpl::producer> maybeFullCount() const; + void loadFullCount(); + + [[nodiscard]] bool unreadCountKnown() const; + [[nodiscard]] int unreadCountCurrent() const; + [[nodiscard]] int displayedUnreadCount() const; + [[nodiscard]] rpl::producer> unreadCountValue() const; + + void applyMonoforumDialog( + const MTPDmonoForumDialog &dialog, + not_null topItem); + void readTillEnd(); + void requestChatListMessage(); int fixedOnTopIndex() const override; bool shouldBeInChatList() const override; @@ -57,9 +87,27 @@ public: void hasUnreadMentionChanged(bool has) override; void hasUnreadReactionChanged(bool has) override; + [[nodiscard]] HistoryItem *lastMessage() const; + [[nodiscard]] HistoryItem *lastServerMessage() const; + [[nodiscard]] bool lastMessageKnown() const; + [[nodiscard]] bool lastServerMessageKnown() const; + [[nodiscard]] MsgId lastKnownServerMessageId() const; + + void setInboxReadTill(MsgId readTillId, std::optional unreadCount); + [[nodiscard]] MsgId inboxReadTillId() const; + [[nodiscard]] MsgId computeInboxReadTillFull() const; + + void setOutboxReadTill(MsgId readTillId); + [[nodiscard]] MsgId computeOutboxReadTillFull() const; + [[nodiscard]] bool isServerSideUnread( not_null item) const override; + void requestUnreadCount(); + + void readTill(not_null item); + void readTill(MsgId tillId); + void chatListPreloadData() override; void paintUserpic( Painter &p, @@ -70,25 +118,75 @@ public: -> HistoryView::SendActionPainter* override; private: + struct Viewer; + enum class Flag : uchar { ResolveChatListMessage = (1 << 0), - FullLoaded = (1 << 1), + InMonoforum = (1 << 1), }; friend inline constexpr bool is_flag_type(Flag) { return true; } using Flags = base::flags; - bool hasOrphanMediaGroupPart() const; + [[nodiscard]] Histories &histories(); + + void subscribeToUnreadChanges(); + [[nodiscard]] Dialogs::UnreadState unreadStateFor( + int count, + bool known) const; + void setLastMessage(HistoryItem *item); + void setLastServerMessage(HistoryItem *item); + void setChatListMessage(HistoryItem *item); void allowChatListMessageResolve(); void resolveChatListMessageGroup(); - const not_null _parent; - const not_null _history; + void changeUnreadCountByMessage(MsgId id, int delta); + void setUnreadCount(std::optional count); + void readTill(MsgId tillId, HistoryItem *tillIdItem); + void checkReadTillEnd(); + void sendReadTillRequest(); + void reloadUnreadCountIfNeeded(); - std::vector> _items; - std::optional _fullCount; - rpl::event_stream<> _changed; + [[nodiscard]] bool buildFromData(not_null viewer); + [[nodiscard]] bool applyUpdate(const MessageUpdate &update); + void appendClientSideMessages(MessagesSlice &slice); + [[nodiscard]] std::optional computeUnreadCountLocally( + MsgId afterId) const; + bool processMessagesIsEmpty(const MTPmessages_Messages &result); + void loadAround(MsgId id); + void loadBefore(); + void loadAfter(); + + const not_null _parent; + const not_null _sublistHistory; + + MsgId _lastKnownServerMessageId = 0; + + std::vector _list; + std::optional _skippedBefore; + std::optional _skippedAfter; + rpl::variable> _fullCount; + rpl::event_stream<> _listChanges; + rpl::event_stream<> _instantChanges; + std::optional _loadingAround; + rpl::variable> _unreadCount; + MsgId _inboxReadTillId = 0; + MsgId _outboxReadTillId = 0; Flags _flags; + std::optional _lastMessage; + std::optional _lastServerMessage; + std::optional _chatListMessage; + base::flat_set _requestedGroups; + int _beforeId = 0; + int _afterId = 0; + + base::Timer _readRequestTimer; + mtpRequestId _readRequestId = 0; + + mtpRequestId _reloadUnreadCountRequestId = 0; + + rpl::lifetime _lifetime; + }; } // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 82bda952cd..ec41e8ee17 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -341,6 +341,19 @@ void Session::subscribeForTopicRepliesLists() { } }, _lifetime); + sublistReadTillUpdates( + ) | rpl::start_with_next([=](const SublistReadTillUpdate &update) { + if (const auto parentChat = channelLoaded(update.parentChatId)) { + if (const auto monoforum = parentChat->monoforum()) { + const auto sublistPeerId = update.sublistPeerId; + const auto peer = monoforum->owner().peer(sublistPeerId); + if (const auto sublist = monoforum->sublistLoaded(peer)) { + sublist->apply(update); + } + } + } + }, _lifetime); + session().changes().messageUpdates( MessageUpdate::Flag::NewAdded | MessageUpdate::Flag::NewMaybeAdded @@ -349,6 +362,11 @@ void Session::subscribeForTopicRepliesLists() { ) | rpl::start_with_next([=](const MessageUpdate &update) { if (const auto topic = update.item->topic()) { topic->replies()->apply(update); + } else if (update.flags == MessageUpdate::Flag::ReplyToTopAdded) { + // Not interested in this one for sublist. + return; + } else if (const auto sublist = update.item->savedSublist()) { + sublist->apply(update); } }, _lifetime); @@ -2914,6 +2932,15 @@ auto Session::repliesReadTillUpdates() const return _repliesReadTillUpdates.events(); } +void Session::updateSublistReadTill(SublistReadTillUpdate update) { + _sublistReadTillUpdates.fire(std::move(update)); +} + +auto Session::sublistReadTillUpdates() const +-> rpl::producer { + return _sublistReadTillUpdates.events(); +} + int Session::computeUnreadBadge(const Dialogs::UnreadState &state) const { const auto all = Core::App().settings().includeMutedCounter(); return std::max(state.marks - (all ? 0 : state.marksMuted), 0) diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 2a32df2bc2..2ac7d93d75 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -80,6 +80,13 @@ struct RepliesReadTillUpdate { bool out = false; }; +struct SublistReadTillUpdate { + ChannelId parentChatId; + PeerId sublistPeerId; + MsgId readTillId; + bool out = false; +}; + struct GiftUpdate { enum class Action : uchar { Save, @@ -565,6 +572,10 @@ public: [[nodiscard]] auto repliesReadTillUpdates() const -> rpl::producer; + void updateSublistReadTill(SublistReadTillUpdate update); + [[nodiscard]] auto sublistReadTillUpdates() const + -> rpl::producer; + void selfDestructIn(not_null item, crl::time delay); [[nodiscard]] not_null photo(PhotoId id); @@ -1004,6 +1015,7 @@ private: rpl::event_stream _chatListEntryRefreshes; rpl::event_stream<> _unreadBadgeChanges; rpl::event_stream _repliesReadTillUpdates; + rpl::event_stream _sublistReadTillUpdates; rpl::event_stream _sentToScheduled; rpl::event_stream _sentFromScheduled; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 51bdcc9719..a67e60302b 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -163,6 +163,9 @@ void History::itemRemoved(not_null item) { if (const auto topic = item->topic()) { topic->applyItemRemoved(item->id); } + if (const auto sublist = item->savedSublist()) { + sublist->applyItemRemoved(item->id); + } if (const auto chat = peer->asChat()) { if (const auto to = chat->getMigrateToChannel()) { if (const auto history = owner().historyLoaded(to)) { @@ -1311,6 +1314,9 @@ void History::newItemAdded(not_null item) { if (const auto topic = item->topic()) { topic->applyItemAdded(item); } + if (const auto sublist = item->savedSublist()) { + sublist->applyItemAdded(item); + } if (const auto media = item->media()) { if (const auto gift = media->gift()) { if (const auto unique = gift->unique.get()) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 42f9127032..04c3f1076b 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -789,16 +789,6 @@ HistoryItem::~HistoryItem() { if (const auto reply = Get()) { reply->clearData(this); } - if (const auto saved = Get()) { - if (saved->savedMessagesSublist) { - saved->savedMessagesSublist->removeOne(this); - } else if (const auto monoforum = _history->peer->monoforum()) { - const auto peer = _history->owner().peer(saved->sublistPeerId); - if (const auto sublist = monoforum->sublistLoaded(peer)) { - sublist->removeOne(this); - } - } - } clearDependencyMessage(); applyTTL(0); } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index d789f7f07b..0cd5a0fe00 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -191,6 +191,13 @@ object_ptr ChatMemento::createWidget( _list.setScrollTopState(ListMemento::ScrollTopState{ Data::MinMessagePosition }); + } else if (!_list.aroundPosition().fullId + && _id.sublist + && _id.sublist->computeInboxReadTillFull() == MsgId(1)) { + _list.setAroundPosition(Data::MinMessagePosition); + _list.setScrollTopState(ListMemento::ScrollTopState{ + Data::MinMessagePosition + }); } auto result = object_ptr(parent, controller, _id); result->setInternalState(geometry, this); @@ -394,7 +401,9 @@ ChatWidget::ChatWidget( } }, lifetime()); - if (!_topic) { + if (_sublist) { + subscribeToSublist(); + } else if (!_topic) { _history->session().changes().historyUpdates( _history, Data::HistoryUpdate::Flag::OutboxRead @@ -2455,6 +2464,19 @@ void ChatWidget::setReplies(std::shared_ptr replies) { }, _repliesLifetime); } +void ChatWidget::subscribeToSublist() { + Expects(_sublist != nullptr); + + _sublist->unreadCountValue( + ) | rpl::start_with_next([=](std::optional count) { + refreshUnreadCountBadge(count); + }, lifetime()); + + refreshUnreadCountBadge(_sublist->unreadCountKnown() + ? _sublist->unreadCountCurrent() + : std::optional()); +} + void ChatWidget::restoreState(not_null memento) { if (auto replies = memento->getReplies()) { setReplies(std::move(replies)); @@ -2792,54 +2814,20 @@ rpl::producer ChatWidget::sublistSource( const auto messageId = aroundId.fullId.msg ? aroundId.fullId.msg : (ServerMaxMsgId - 1); - return [=](auto consumer) { - const auto pushSlice = [=] { - auto result = Data::MessagesSlice(); - result.fullCount = _sublist->fullCount(); - _topBar->setCustomTitle(result.fullCount - ? tr::lng_forum_messages( - tr::now, - lt_count_decimal, - *result.fullCount) - : tr::lng_contacts_loading(tr::now)); - const auto &messages = _sublist->messages(); - const auto i = ranges::lower_bound( - messages, - messageId, - ranges::greater(), - [](not_null item) { return item->id; }); - const auto before = int(end(messages) - i); - const auto useBefore = std::min(before, limitBefore); - const auto after = int(i - begin(messages)); - const auto useAfter = std::min(after, limitAfter); - const auto from = i - useAfter; - const auto till = i + useBefore; - auto nearestDistance = std::numeric_limits::max(); - result.ids.reserve(useAfter + useBefore); - for (auto j = till; j != from;) { - const auto item = *--j; - result.ids.push_back(item->fullId()); - const auto distance = std::abs((messageId - item->id).bare); - if (nearestDistance > distance) { - nearestDistance = distance; - result.nearestToAround = result.ids.back(); - } - } - result.skippedAfter = after - useAfter; - result.skippedBefore = result.fullCount - ? (*result.fullCount - after - useBefore) - : std::optional(); - if (!result.fullCount || useBefore < limitBefore) { - _sublist->parent()->loadMore(_sublist); - } - markLoaded(); - consumer.put_next(std::move(result)); - }; - auto lifetime = rpl::lifetime(); - _sublist->changes() | rpl::start_with_next(pushSlice, lifetime); - pushSlice(); - return lifetime; - }; + return _sublist->source( + aroundId, + limitBefore, + limitAfter + ) | rpl::before_next([=](const Data::MessagesSlice &result) { + // after_next makes a copy of value. + _topBar->setCustomTitle(result.fullCount + ? tr::lng_forum_messages( + tr::now, + lt_count_decimal, + *result.fullCount) + : tr::lng_contacts_loading(tr::now)); + markLoaded(); + }); } bool ChatWidget::listAllowsMultiSelect() { @@ -2882,6 +2870,8 @@ void ChatWidget::listSelectionChanged(SelectedItems &&items) { void ChatWidget::listMarkReadTill(not_null item) { if (_replies) { _replies->readTill(item); + } else if (_sublist) { + _sublist->readTill(item); } } @@ -2892,16 +2882,22 @@ void ChatWidget::listMarkContentsRead( MessagesBarData ChatWidget::listMessagesBar( const std::vector> &elements) { - if (_sublist || elements.empty()) { + if ((!_sublist && !_replies) || elements.empty()) { return {}; } - const auto till = _replies->computeInboxReadTillFull(); + const auto till = _replies + ? _replies->computeInboxReadTillFull() + : _sublist->computeInboxReadTillFull(); const auto hidden = (till < 2); for (auto i = 0, count = int(elements.size()); i != count; ++i) { const auto item = elements[i]->data(); if (item->isRegular() && item->id > till) { - if (item->out() || !item->replyToId()) { - _replies->readTill(item); + if (item->out() || (_replies && !item->replyToId())) { + if (_replies) { + _replies->readTill(item); + } else { + _sublist->readTill(item); + } } else { return { .bar = { @@ -2960,9 +2956,12 @@ bool ChatWidget::listElementHideReply(not_null view) { } bool ChatWidget::listElementShownUnread(not_null view) { + const auto item = view->data(); return _replies - ? _replies->isServerSideUnread(view->data()) - : view->data()->unread(view->data()->history()); + ? _replies->isServerSideUnread(item) + : _sublist + ? _sublist->isServerSideUnread(item) + : item->unread(item->history()); } bool ChatWidget::listIsGoodForAroundPosition( @@ -2973,7 +2972,7 @@ bool ChatWidget::listIsGoodForAroundPosition( void ChatWidget::listSendBotCommand( const QString &command, const FullMsgId &context) { - if (!_sublist) { + if (!_sublist || _sublist->parentChat()) { sendBotCommandWithOptions(command, context, {}); } } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index c38fe6dbe8..a62a30a2e9 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -265,6 +265,7 @@ private: void setupRootView(); void setupTopicViewer(); void subscribeToTopic(); + void subscribeToSublist(); void subscribeToPinnedMessages(); void setTopic(Data::ForumTopic *topic); diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index 8caddbf0c7..725ad58ad7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -588,8 +588,8 @@ rpl::producer SavedSublistCountValue( not_null peer) { const auto saved = &peer->owner().savedMessages(); const auto sublist = saved->sublist(peer); - if (!sublist->fullCount()) { - saved->loadMore(sublist); + if (!sublist->fullCount().has_value()) { + sublist->loadFullCount(); return rpl::single(0) | rpl::then(sublist->fullCountValue()); } return sublist->fullCountValue(); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 4e8faa448e..70a3f6f488 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -433,7 +433,7 @@ updateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Updat updatePaidReactionPrivacy#8b725fce private:PaidReactionPrivacy = Update; updateSentPhoneCode#504aa18f sent_code:auth.SentCode = Update; updateGroupCallChainBlocks#a477288f call:InputGroupCall sub_chain_id:int blocks:Vector next_offset:int = Update; -updateReadMonoForumInbox#bcf34712 flags:# channel_id:long saved_peer_id:Peer read_max_id:int = Update; +updateReadMonoForumInbox#77b0e372 channel_id:long saved_peer_id:Peer read_max_id:int = Update; updateReadMonoForumOutbox#a4a79376 channel_id:long saved_peer_id:Peer read_max_id:int = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -2395,6 +2395,7 @@ messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; messages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector offset:int limit:int hash:long = messages.FoundStickers; messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector = Bool; +messages.getSavedDialogsByID#6f6f9c96 flags:# parent_peer:flags.1?InputPeer ids:Vector = messages.SavedDialogs; messages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool; updates.getState#edd4882a = updates.State;