diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index acf322fc2..6841fda9f 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -550,6 +550,10 @@ PRIVATE data/data_replies_list.h data/data_reply_preview.cpp data/data_reply_preview.h + data/data_saved_messages.cpp + data/data_saved_messages.h + data/data_saved_sublist.cpp + data/data_saved_sublist.h data/data_search_controller.cpp data/data_search_controller.h data/data_send_action.cpp @@ -897,6 +901,8 @@ PRIVATE info/profile/info_profile_values.h info/profile/info_profile_widget.cpp info/profile/info_profile_widget.h + info/saved/info_saved_sublists_widget.cpp + info/saved/info_saved_sublists_widget.h info/settings/info_settings_widget.cpp info/settings/info_settings_widget.h info/similar_channels/info_similar_channels_widget.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d1ac8a25c..430a35692 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -395,6 +395,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_dlg_new_bot_name" = "Bot name"; "lng_no_chats" = "Your chats will be here"; "lng_no_chats_filter" = "No chats currently belong to this folder."; +"lng_no_saved_sublists" = "You can save messages from other chats here."; "lng_contacts_loading" = "Loading..."; "lng_contacts_not_found" = "No contacts found"; "lng_topics_not_found" = "No topics found."; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 00a5f30a1..c3070b99f 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -440,6 +440,26 @@ void ApiWrap::savePinnedOrder(not_null forum) { }).send(); } +void ApiWrap::savePinnedOrder(not_null saved) { + const auto &order = _session->data().pinnedChatsOrder(saved); + const auto input = [](Dialogs::Key key) { + if (const auto history = key.history()) { + return MTP_inputDialogPeer(history->peer->input); + } + Unexpected("Key type in pinnedDialogsOrder()."); + }; + auto peers = QVector(); + peers.reserve(order.size()); + ranges::transform( + order, + ranges::back_inserter(peers), + input); + request(MTPmessages_ReorderPinnedSavedDialogs( + MTP_flags(MTPmessages_ReorderPinnedSavedDialogs::Flag::f_force), + MTP_vector(peers) + )).send(); +} + void ApiWrap::toggleHistoryArchived( not_null history, bool archived, diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 70ed1affa..7e44d460c 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -34,6 +34,7 @@ class Forum; class ForumTopic; class Thread; class Story; +class SavedMessages; } // namespace Data namespace InlineBots { @@ -152,6 +153,7 @@ public: void savePinnedOrder(Data::Folder *folder); void savePinnedOrder(not_null forum); + void savePinnedOrder(not_null saved); void toggleHistoryArchived( not_null history, bool archived, diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp index bbd4ce987..76f599943 100644 --- a/Telegram/SourceFiles/data/data_folder.cpp +++ b/Telegram/SourceFiles/data/data_folder.cpp @@ -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(); } diff --git a/Telegram/SourceFiles/data/data_folder.h b/Telegram/SourceFiles/data/data_folder.h index 7008adac0..7f6347e93 100644 --- a/Telegram/SourceFiles/data/data_folder.h +++ b/Telegram/SourceFiles/data/data_folder.h @@ -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 &chatListNameWords() const override; const base::flat_set &chatListFirstLetters() const override; @@ -82,8 +82,6 @@ public: private: void indexNameParts(); - int chatListNameVersion() const override; - void reorderLastHistories(); void paintUserpic( diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h index 66430a0a3..275f38fcb 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.h +++ b/Telegram/SourceFiles/data/data_forum_topic.h @@ -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 &chatListNameWords() const override; const base::flat_set &chatListFirstLetters() const override; @@ -187,8 +188,6 @@ private: void allowChatListMessageResolve(); void resolveChatListMessageGroup(); - int chatListNameVersion() const override; - void subscribeToUnreadChanges(); [[nodiscard]] Dialogs::UnreadState unreadStateFor( int count, diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 746407a5f..a6f033924 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -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(); diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 907fec2e6..0252063f4 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -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); diff --git a/Telegram/SourceFiles/data/data_premium_limits.cpp b/Telegram/SourceFiles/data/data_premium_limits.cpp index 07a5124a6..443474040 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.cpp +++ b/Telegram/SourceFiles/data/data_premium_limits.cpp @@ -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); } diff --git a/Telegram/SourceFiles/data/data_premium_limits.h b/Telegram/SourceFiles/data/data_premium_limits.h index f9bd416d2..bc3c86d9f 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.h +++ b/Telegram/SourceFiles/data/data_premium_limits.h @@ -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; diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp new file mode 100644 index 000000000..d35beeab7 --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -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 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 SavedMessages::chatsList() { + return &_chatsList; +} + +not_null SavedMessages::sublist(not_null peer) { + const auto i = _sublists.find(peer); + if (i != end(_sublists)) { + return i->second.get(); + } + return _sublists.emplace( + peer, + std::make_unique(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*)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 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*)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>(); + 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 diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h new file mode 100644 index 000000000..dd68f4bb8 --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -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 owner); + ~SavedMessages(); + + [[nodiscard]] bool supported() const; + + [[nodiscard]] Session &owner() const; + [[nodiscard]] Main::Session &session() const; + + [[nodiscard]] not_null chatsList(); + [[nodiscard]] not_null sublist(not_null peer); + + void loadMore(); + void loadMore(not_null sublist); + +private: + const not_null _owner; + + Dialogs::MainList _chatsList; + base::flat_map< + not_null, + std::unique_ptr> _sublists; + + base::flat_map, mtpRequestId> _loadMoreRequests; + mtpRequestId _loadMoreRequestId = 0; + + TimeId _offsetDate = 0; + MsgId _offsetId = 0; + PeerData *_offsetPeer = nullptr; + + bool _unsupported = false; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp new file mode 100644 index 000000000..98624d6b0 --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -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 peer) +: Entry(&peer->owner(), Dialogs::Entry::Type::SavedSublist) +, _history(peer->owner().history(peer)) { +} + +SavedSublist::~SavedSublist() = default; + +not_null SavedSublist::history() const { + return _history; +} + +not_null 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> & { + return _items; +} + +void SavedSublist::applyMaybeLast(not_null item) { + 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)); + }; + + 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 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> &&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 &SavedSublist::chatListNameWords() const { + return _history->chatListNameWords(); +} + +const base::flat_set &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 diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h new file mode 100644 index 000000000..4f37b11e4 --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -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 peer); + ~SavedSublist(); + + [[nodiscard]] not_null history() const; + [[nodiscard]] not_null peer() const; + [[nodiscard]] bool isHiddenAuthor() const; + [[nodiscard]] bool isFullLoaded() const; + + [[nodiscard]] auto messages() const + -> const std::vector> &; + void applyMaybeLast(not_null item); + void removeOne(not_null item); + void append(std::vector> &&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 &chatListNameWords() const override; + const base::flat_set &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; + + bool hasOrphanMediaGroupPart() const; + void allowChatListMessageResolve(); + void resolveChatListMessageGroup(); + + const not_null _history; + + std::vector> _items; + Dialogs::Ui::MessageView _lastItemDialogsView; + Flags _flags; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 4ff49a02c..edc79bb35 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -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 session) , _forumIcons(std::make_unique(this)) , _notifySettings(std::make_unique(this)) , _customEmojiManager(std::make_unique(this)) -, _stories(std::make_unique(this)) { +, _stories(std::make_unique(this)) +, _savedMessages(std::make_unique(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); @@ -1712,6 +1715,11 @@ void Session::requestItemRepaint(not_null item) { topic->updateChatListEntry(); } } + if (const auto sublist = item->savedSublist()) { + if (sublist->lastItemDialogsView().dependsOn(item)) { + sublist->updateChatListEntry(); + } + } } rpl::producer> Session::itemRepaintRequest() const { @@ -2137,6 +2145,11 @@ int Session::pinnedChatsLimit(not_null forum) const { return limits.topicsPinnedCurrent(); } +int Session::pinnedChatsLimit(not_null saved) const { + const auto limits = Data::PremiumLimits(_session); + return limits.savedSublistsPinnedCurrent(); +} + rpl::producer Session::maxPinnedChatsLimitValue( Data::Folder *folder) const { // Premium limit from appconfig. @@ -2177,6 +2190,20 @@ rpl::producer Session::maxPinnedChatsLimitValue( }); } +rpl::producer Session::maxPinnedChatsLimitValue( + not_null 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 &Session::pinnedChatsOrder( Data::Folder *folder) const { return chatsList(folder)->pinned()->order(); @@ -2192,6 +2219,11 @@ const std::vector &Session::pinnedChatsOrder( return forum->topicsList()->pinned()->order(); } +const std::vector &Session::pinnedChatsOrder( + not_null saved) const { + return saved->chatsList()->pinned()->order(); +} + void Session::clearPinnedChats(Data::Folder *folder) { chatsList(folder)->pinned()->clear(); } @@ -4198,6 +4230,8 @@ not_null Session::chatsListFor( const auto topic = entry->asTopic(); return topic ? topic->forum()->topicsList() + : entry->asSublist() + ? _savedMessages->chatsList() : chatsList(entry->folder()); } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index ea93cc178..f1f31f1c2 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -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) const; + [[nodiscard]] int pinnedChatsLimit( + not_null saved) const; [[nodiscard]] rpl::producer maxPinnedChatsLimitValue( Folder *folder) const; [[nodiscard]] rpl::producer maxPinnedChatsLimitValue( FilterId filterId) const; [[nodiscard]] rpl::producer maxPinnedChatsLimitValue( not_null forum) const; + [[nodiscard]] rpl::producer maxPinnedChatsLimitValue( + not_null saved) const; [[nodiscard]] const std::vector &pinnedChatsOrder( Folder *folder) const; [[nodiscard]] const std::vector &pinnedChatsOrder( not_null forum) const; [[nodiscard]] const std::vector &pinnedChatsOrder( FilterId filterId) const; + [[nodiscard]] const std::vector &pinnedChatsOrder( + not_null 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; const std::unique_ptr _customEmojiManager; const std::unique_ptr _stories; + const std::unique_ptr _savedMessages; MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp index f2d5ebcd4..26f6e1fcb 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_forum_topic.h" #include "data/data_chat_filters.h" +#include "data/data_saved_sublist.h" #include "core/application.h" #include "core/core_settings.h" #include "mainwidget.h" @@ -83,6 +84,8 @@ Entry::Entry(not_null owner, Type type) ? (Flag::IsThread | Flag::IsHistory) : (type == Type::ForumTopic) ? Flag::IsThread + : (type == Type::SavedSublist) + ? Flag::IsSavedSublist : Flag(0)) { } @@ -109,7 +112,7 @@ Data::Forum *Entry::asForum() { } Data::Folder *Entry::asFolder() { - return (_flags & Flag::IsThread) + return (_flags & (Flag::IsThread | Flag::IsSavedSublist)) ? nullptr : static_cast(this); } @@ -126,6 +129,12 @@ Data::ForumTopic *Entry::asTopic() { : nullptr; } +Data::SavedSublist *Entry::asSublist() { + return (_flags & Flag::IsSavedSublist) + ? static_cast(this) + : nullptr; +} + const History *Entry::asHistory() const { return const_cast(this)->asHistory(); } @@ -146,6 +155,10 @@ const Data::ForumTopic *Entry::asTopic() const { return const_cast(this)->asTopic(); } +const Data::SavedSublist *Entry::asSublist() const { + return const_cast(this)->asSublist(); +} + void Entry::pinnedIndexChanged(FilterId filterId, int was, int now) { if (!filterId && session().supportMode()) { // Force reorder in support mode. diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.h b/Telegram/SourceFiles/dialogs/dialogs_entry.h index 3ad4281f3..56e84f48c 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.h +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.h @@ -25,6 +25,7 @@ class Session; class Forum; class Folder; class ForumTopic; +class SavedSublist; } // namespace Data namespace Ui { @@ -151,6 +152,7 @@ public: History, Folder, ForumTopic, + SavedSublist, }; Entry(not_null owner, Type type); virtual ~Entry(); @@ -163,12 +165,14 @@ public: Data::Folder *asFolder(); Data::Thread *asThread(); Data::ForumTopic *asTopic(); + Data::SavedSublist *asSublist(); const History *asHistory() const; const Data::Forum *asForum() const; const Data::Folder *asFolder() const; const Data::Thread *asThread() const; const Data::ForumTopic *asTopic() const; + const Data::SavedSublist *asSublist() const; PositionChange adjustByPosInChatList( FilterId filterId, @@ -206,27 +210,29 @@ public: void setChatListTimeId(TimeId date); virtual void updateChatListExistence(); bool needUpdateInChatList() const; - virtual TimeId adjustedChatListTimeId() const; + [[nodiscard]] virtual TimeId adjustedChatListTimeId() const; - virtual int fixedOnTopIndex() const = 0; + [[nodiscard]] virtual int fixedOnTopIndex() const = 0; static constexpr auto kArchiveFixOnTopIndex = 1; static constexpr auto kTopPromotionFixOnTopIndex = 2; - virtual bool shouldBeInChatList() const = 0; - virtual UnreadState chatListUnreadState() const = 0; - virtual BadgesState chatListBadgesState() const = 0; - virtual HistoryItem *chatListMessage() const = 0; - virtual bool chatListMessageKnown() const = 0; - virtual void requestChatListMessage() = 0; - virtual const QString &chatListName() const = 0; - virtual const QString &chatListNameSortKey() const = 0; - virtual const base::flat_set &chatListNameWords() const = 0; - virtual const base::flat_set &chatListFirstLetters() const = 0; + [[nodiscard]] virtual bool shouldBeInChatList() const = 0; + [[nodiscard]] virtual UnreadState chatListUnreadState() const = 0; + [[nodiscard]] virtual BadgesState chatListBadgesState() const = 0; + [[nodiscard]] virtual HistoryItem *chatListMessage() const = 0; + [[nodiscard]] virtual bool chatListMessageKnown() const = 0; + [[nodiscard]] virtual const QString &chatListName() const = 0; + [[nodiscard]] virtual const QString &chatListNameSortKey() const = 0; + [[nodiscard]] virtual int chatListNameVersion() const = 0; + [[nodiscard]] virtual auto chatListNameWords() const + -> const base::flat_set & = 0; + [[nodiscard]] virtual auto chatListFirstLetters() const + -> const base::flat_set & = 0; - virtual bool folderKnown() const { + [[nodiscard]] virtual bool folderKnown() const { return true; } - virtual Data::Folder *folder() const { + [[nodiscard]] virtual Data::Folder *folder() const { return nullptr; } @@ -255,8 +261,9 @@ private: enum class Flag : uchar { IsThread = (1 << 0), IsHistory = (1 << 1), - UpdatePostponed = (1 << 2), - InUnreadChangeBlock = (1 << 3), + IsSavedSublist = (1 << 2), + UpdatePostponed = (1 << 3), + InUnreadChangeBlock = (1 << 4), }; friend inline constexpr bool is_flag_type(Flag) { return true; } using Flags = base::flags; @@ -265,8 +272,6 @@ private: void pinnedIndexChanged(FilterId filterId, int was, int now); [[nodiscard]] uint64 computeSortPosition(FilterId filterId) const; - [[nodiscard]] virtual int chatListNameVersion() const = 0; - void setChatListExistence(bool exists); not_null mainChatListLink(FilterId filterId) const; Row *maybeMainChatListLink(FilterId filterId) const; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 5a9e98a69..c1a630b78 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_cloud_file.h" #include "data/data_changes.h" +#include "data/data_saved_messages.h" #include "data/data_stories.h" #include "data/stickers/data_stickers.h" #include "data/data_send_action.h" @@ -219,7 +220,9 @@ InnerWidget::InnerWidget( session().data().chatsListChanges(), session().data().chatsListLoadedEvents() ) | rpl::filter([=](Data::Folder *folder) { - return !_openedForum && (folder == _openedFolder); + return !_savedSublists + && !_openedForum + && (folder == _openedFolder); }) | rpl::start_with_next([=] { refresh(); }, lifetime()); @@ -499,6 +502,8 @@ int InnerWidget::searchInChatSkip() const { } void InnerWidget::changeOpenedFolder(Data::Folder *folder) { + Expects(!folder || !_savedSublists); + if (_openedFolder == folder) { return; } @@ -513,6 +518,8 @@ void InnerWidget::changeOpenedFolder(Data::Folder *folder) { } void InnerWidget::changeOpenedForum(Data::Forum *forum) { + Expects(!forum || !_savedSublists); + if (_openedForum == forum) { return; } @@ -553,12 +560,39 @@ void InnerWidget::changeOpenedForum(Data::Forum *forum) { } } +void InnerWidget::showSavedSublists() { + Expects(!_geometryInited); + Expects(!_savedSublists); + + _savedSublists = true; + + stopReorderPinned(); + clearSelection(); + + _filterId = 0; + _openedForum = nullptr; + _st = &st::defaultDialogRow; + refreshShownList(); + + _openedForumLifetime.destroy(); + + //session().data().savedMessages().chatsListChanges( + //) | rpl::start_with_next([=] { + // refresh(); + //}, lifetime()); + + refreshWithCollapsedRows(true); + if (_loadMoreCallback) { + _loadMoreCallback(); + } +} + void InnerWidget::paintEvent(QPaintEvent *e) { Painter p(this); p.setInactive( _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any)); - if (_controller->contentOverlapped(this, e)) { + if (!_savedSublists && _controller->contentOverlapped(this, e)) { return; } const auto activeEntry = _controller->activeChatEntryCurrent(); @@ -1416,11 +1450,14 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { } } const std::vector &InnerWidget::pinnedChatsOrder() const { - return _openedForum - ? session().data().pinnedChatsOrder(_openedForum) + const auto owner = &session().data(); + return _savedSublists + ? owner->pinnedChatsOrder(&owner->savedMessages()) + : _openedForum + ? owner->pinnedChatsOrder(_openedForum) : _filterId - ? session().data().pinnedChatsOrder(_filterId) - : session().data().pinnedChatsOrder(_openedFolder); + ? owner->pinnedChatsOrder(_filterId) + : owner->pinnedChatsOrder(_openedFolder); } void InnerWidget::checkReorderPinnedStart(QPoint localPosition) { @@ -1473,7 +1510,9 @@ void InnerWidget::savePinnedOrder() { return; // Something has changed in the set of pinned chats. } } - if (_openedForum) { + if (_savedSublists) { + session().api().savePinnedOrder(&session().data().savedMessages()); + } else if (_openedForum) { session().api().savePinnedOrder(_openedForum); } else if (_filterId) { Api::SaveNewFilterPinned(&session(), _filterId); @@ -1577,7 +1616,7 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) { const auto delta = [&] { if (localPosition.y() < _visibleTop) { return localPosition.y() - _visibleTop; - } else if ((_openedFolder || _openedForum || _filterId) + } else if ((_savedSublists || _openedFolder || _openedForum || _filterId) && localPosition.y() > _visibleBottom) { return localPosition.y() - _visibleBottom; } @@ -1832,6 +1871,8 @@ void InnerWidget::handleChatListEntryRefreshes() { return false; } else if (const auto topic = event.key.topic()) { return (topic->forum() == _openedForum); + } else if (event.key.sublist()) { + return _savedSublists; } else { return !_openedForum; } @@ -1848,6 +1889,8 @@ void InnerWidget::handleChatListEntryRefreshes() { && (_state == WidgetState::Default) && (key.topic() ? (key.topic()->forum() == _openedForum) + : key.sublist() + ? _savedSublists : (entry->folder() == _openedFolder))) { _dialogMoved.fire({ from, to }); } @@ -2051,7 +2094,11 @@ void InnerWidget::enterEventHook(QEnterEvent *e) { Row *InnerWidget::shownRowByKey(Key key) { const auto entry = key.entry(); - if (_openedForum) { + if (_savedSublists) { + if (!entry->asSublist()) { + return nullptr; + } + } else if (_openedForum) { const auto topic = entry->asTopic(); if (!topic || topic->forum() != _openedForum) { return nullptr; @@ -2114,7 +2161,9 @@ void InnerWidget::updateSelectedRow(Key key) { } void InnerWidget::refreshShownList() { - const auto list = _openedForum + const auto list = _savedSublists + ? session().data().savedMessages().chatsList()->indexed() + : _openedForum ? _openedForum->topicsList()->indexed() : _filterId ? session().data().chatsFilters().chatsList(_filterId)->indexed() @@ -2294,15 +2343,19 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) { } }; if (!_searchInChat && !_searchFromPeer && !words.isEmpty()) { - if (_openedForum) { + if (_savedSublists) { + const auto owner = &session().data(); + append(owner->savedMessages().chatsList()->indexed()); + } else if (_openedForum) { append(_openedForum->topicsList()->indexed()); } else { - append(session().data().chatsList()->indexed()); + const auto owner = &session().data(); + append(owner->chatsList()->indexed()); const auto id = Data::Folder::kId; - if (const auto add = session().data().folderLoaded(id)) { + if (const auto add = owner->folderLoaded(id)) { append(add->chatsList()->indexed()); } - append(session().data().contactsNoChatsList()); + append(owner->contactsNoChatsList()); } } refresh(true); @@ -2759,6 +2812,10 @@ void InnerWidget::refreshEmptyLabel() { const auto data = &session().data(); const auto state = !_shownList->empty() ? EmptyState::None + : _savedSublists + ? (data->savedMessages().chatsList()->loaded() + ? EmptyState::EmptySavedSublists + : EmptyState::Loading) : _openedForum ? (_openedForum->topicsList()->loaded() ? EmptyState::EmptyForum @@ -2783,6 +2840,8 @@ void InnerWidget::refreshEmptyLabel() { ? tr::lng_no_chats_filter() : (state == EmptyState::EmptyForum) ? tr::lng_forum_no_topics() + : (state == EmptyState::EmptySavedSublists) + ? tr::lng_no_saved_sublists() : tr::lng_contacts_loading(); auto link = (state == EmptyState::NoContacts) ? tr::lng_add_contact_button() diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 608513c85..55d257bb5 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -107,6 +107,7 @@ public: void changeOpenedFolder(Data::Folder *folder); void changeOpenedForum(Data::Forum *forum); + void showSavedSublists(); void selectSkip(int32 direction); void selectSkipPage(int32 pixels, int32 direction); @@ -198,6 +199,7 @@ private: NoContacts, EmptyFolder, EmptyForum, + EmptySavedSublists, }; struct PinnedRow { @@ -503,6 +505,8 @@ private: float64 _narrowRatio = 0.; bool _geometryInited = false; + bool _savedSublists = false; + base::unique_qptr _menu; }; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.cpp b/Telegram/SourceFiles/dialogs/dialogs_key.cpp index fe96a5a7d..dfa728141 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_key.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "history/history.h" namespace Dialogs { @@ -25,6 +26,9 @@ Key::Key(Data::Thread *thread) : _value(thread) { Key::Key(Data::ForumTopic *topic) : _value(topic) { } +Key::Key(Data::SavedSublist *sublist) : _value(sublist) { +} + Key::Key(not_null history) : _value(history) { } @@ -37,6 +41,9 @@ Key::Key(not_null folder) : _value(folder) { Key::Key(not_null topic) : _value(topic) { } +Key::Key(not_null sublist) : _value(sublist) { +} + not_null Key::entry() const { Expects(_value != nullptr); @@ -59,6 +66,10 @@ Data::Thread *Key::thread() const { return _value ? _value->asThread() : nullptr; } +Data::SavedSublist *Key::sublist() const { + return _value ? _value->asSublist() : nullptr; +} + History *Key::owningHistory() const { if (const auto thread = this->thread()) { return thread->owningHistory(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index 34fd9aa29..7ee381f4b 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -14,6 +14,7 @@ namespace Data { class Thread; class Folder; class ForumTopic; +class SavedSublist; } // namespace Data namespace Dialogs { @@ -29,12 +30,14 @@ public: Key(Data::Folder *folder); Key(Data::Thread *thread); Key(Data::ForumTopic *topic); + Key(Data::SavedSublist *sublist); Key(not_null entry) : _value(entry) { } Key(not_null history); Key(not_null thread); Key(not_null folder); Key(not_null topic); + Key(not_null sublist); explicit operator bool() const { return (_value != nullptr); @@ -46,6 +49,7 @@ public: [[nodiscard]] Data::Thread *thread() const; [[nodiscard]] History *owningHistory() const; [[nodiscard]] PeerData *peer() const; + [[nodiscard]] Data::SavedSublist *sublist() const; friend inline constexpr auto operator<=>(Key, Key) noexcept = default; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index aaa03035f..54247193c 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -261,8 +261,10 @@ void Row::recountHeight(float64 narrowRatio) { : st::defaultDialogRow.height; } else if (_id.folder()) { _height = st::defaultDialogRow.height; - } else { + } else if (_id.topic()) { _height = st::forumTopicRow.height; + } else { + _height = st::defaultDialogRow.height; } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h index e814e5ff1..738c9faa4 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.h +++ b/Telegram/SourceFiles/dialogs/dialogs_row.h @@ -131,6 +131,9 @@ public: [[nodiscard]] Data::Thread *thread() const { return _id.thread(); } + [[nodiscard]] Data::SavedSublist *sublist() const { + return _id.sublist(); + } [[nodiscard]] not_null entry() const { return _id.entry(); } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 5299b648c..a2086bdde 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_drafts.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "dialogs/dialogs_list.h" #include "dialogs/dialogs_three_state_icon.h" @@ -732,6 +733,7 @@ void RowPainter::Paint( const auto entry = row->entry(); const auto history = row->history(); const auto thread = row->thread(); + const auto sublist = row->sublist(); const auto peer = history ? history->peer.get() : nullptr; const auto badgesState = entry->chatListBadgesState(); entry->chatListPreloadData(); // Allow chat list message resolve. @@ -810,6 +812,8 @@ void RowPainter::Paint( ? nullptr : thread ? &thread->lastItemDialogsView() + : sublist + ? &sublist->lastItemDialogsView() : nullptr; if (view) { const auto forum = context.st->topicsHeight diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 13cdf046c..665024a11 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_channel_admins.h" @@ -609,6 +610,11 @@ not_null History::addNewItem( addNewToBack(item, unread); checkForLoadedAtTop(item); } + + if (const auto sublist = item->savedSublist()) { + sublist->applyMaybeLast(item); + } + return item; } @@ -1427,6 +1433,12 @@ void History::addCreatedOlderSlice( if (loadedAtBottom()) { // Add photos to overview and authors to lastAuthors. addItemsToLists(items); + + for (const auto &item : items) { + if (const auto sublist = item->savedSublist()) { + sublist->applyMaybeLast(item); + } + } } addToSharedMedia(items); } diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 909549684..adeb63fc2 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -365,6 +365,7 @@ public: void takeLocalDraft(not_null from); void applyCloudDraft(MsgId topicRootId); void draftSavedToCloud(MsgId topicRootId); + void requestChatListMessage(); [[nodiscard]] const Data::ForwardDraft &forwardDraft( MsgId topicRootId) const; @@ -383,9 +384,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 &chatListNameWords() const override; const base::flat_set &chatListFirstLetters() const override; void chatListPreloadData() override; @@ -589,8 +590,6 @@ private: [[nodiscard]] Dialogs::UnreadState computeUnreadState() const; void setFolderPointer(Data::Folder *folder); - int chatListNameVersion() const override; - void hasUnreadMentionChanged(bool has) override; void hasUnreadReactionChanged(bool has) override; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 876757e2f..ff4d4fdb4 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -41,6 +41,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_updates.h" #include "data/notify/data_notify_settings.h" #include "data/data_bot_app.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_scheduled_messages.h" #include "data/data_changes.h" #include "data/data_session.h" @@ -145,6 +147,8 @@ struct HistoryItem::CreateConfig { QString originalSenderName; QString originalPostAuthor; + PeerId savedSublistPeer = 0; + QString forwardPsaType; PeerId savedFromPeer = 0; MsgId savedFromMsgId = 0; @@ -769,6 +773,9 @@ HistoryItem::~HistoryItem() { if (const auto reply = Get()) { reply->clearData(this); } + if (const auto saved = Get()) { + saved->sublist->removeOne(this); + } clearDependencyMessage(); applyTTL(0); } @@ -1229,6 +1236,9 @@ void HistoryItem::invalidateChatListEntry() { if (const auto topic = this->topic()) { topic->lastItemDialogsView().itemInvalidated(this); } + if (const auto sublist = savedSublist()) { + sublist->lastItemDialogsView().itemInvalidated(this); + } } void HistoryItem::customEmojiRepaint() { @@ -3027,6 +3037,20 @@ bool HistoryItem::isEmpty() const { && !Has(); } +Data::SavedSublist *HistoryItem::savedSublist() const { + if (const auto saved = Get()) { + return saved->sublist; + } + return nullptr; +} + +PeerData *HistoryItem::savedSublistPeer() const { + if (const auto sublist = savedSublist()) { + return sublist->peer(); + } + return nullptr; +} + TextWithEntities HistoryItem::notificationText( NotificationTextOptions options) const { auto result = [&] { @@ -3178,9 +3202,28 @@ void HistoryItem::createComponents(CreateConfig &&config) { } else if (config.inlineMarkup) { mask |= HistoryMessageReplyMarkup::Bit(); } + if (_history->peer->isSelf()) { + mask |= HistoryMessageSaved::Bit(); + } UpdateComponents(mask); + if (const auto saved = Get()) { + if (!config.savedSublistPeer) { + if (config.savedFromPeer) { + config.savedSublistPeer = config.savedFromPeer; + } else if (config.originalSenderId) { + config.savedSublistPeer = config.originalSenderId; + } else if (!config.originalSenderName.isEmpty()) { + config.savedSublistPeer = PeerData::kSavedHiddenAuthorId; + } else { + config.savedSublistPeer = _history->session().userPeerId(); + } + } + const auto peer = _history->owner().peer(config.savedSublistPeer); + saved->sublist = _history->owner().savedMessages().sublist(peer); + } + if (const auto reply = Get()) { reply->set(std::move(config.reply)); if (!reply->updateData(this)) { @@ -3474,6 +3517,9 @@ void HistoryItem::applyTTL(const MTPDmessageService &data) { void HistoryItem::createComponents(const MTPDmessage &data) { auto config = CreateConfig(); + config.savedSublistPeer = data.vsaved_peer_id() + ? peerFromMTP(*data.vsaved_peer_id()) + : PeerId(); if (const auto forwarded = data.vfwd_from()) { forwarded->match([&](const MTPDmessageFwdHeader &data) { FillForwardedInfo(config, data); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index c14c65654..bfa73ac7e 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -56,6 +56,7 @@ class ForumTopic; class Thread; struct SponsoredFrom; class Story; +class SavedSublist; } // namespace Data namespace Main { @@ -485,6 +486,9 @@ public: [[nodiscard]] QString originalPostAuthor() const; [[nodiscard]] MsgId originalId() const; + [[nodiscard]] Data::SavedSublist *savedSublist() const; + [[nodiscard]] PeerData *savedSublistPeer() const; + [[nodiscard]] bool isEmpty() const; [[nodiscard]] MessageGroupId groupId() const; diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 77c51560a..69fe11eff 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -141,7 +141,7 @@ struct HistoryMessageForwarded : public RuntimeComponent { - PeerData *peer = nullptr; + Data::SavedSublist *sublist = nullptr; }; class ReplyToMessagePointer final { diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index c026ab9c7..ec687a9e8 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -689,11 +689,11 @@ void TopBarWidget::infoClicked() { return; } else if (const auto topic = key.topic()) { _controller->showSection(std::make_shared(topic)); - } else if (key.peer()->isSelf()) { + } else if (key.peer()->savedSublistsInfo()) { _controller->showSection(std::make_shared( key.peer(), - Info::Section(Storage::SharedMediaType::Photo))); - } else if (key.peer()->isRepliesChat()) { + Info::Section::Type::SavedSublists)); + } else if (key.peer()->sharedMediaInfo()) { _controller->showSection(std::make_shared( key.peer(), Info::Section(Storage::SharedMediaType::Photo))); diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index 046635020..bb5eca6bf 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -126,6 +126,7 @@ public: Media, CommonGroups, SimilarChannels, + SavedSublists, Members, Settings, Downloads, diff --git a/Telegram/SourceFiles/info/info_memento.cpp b/Telegram/SourceFiles/info/info_memento.cpp index b2f78b029..ec4eb246e 100644 --- a/Telegram/SourceFiles/info/info_memento.cpp +++ b/Telegram/SourceFiles/info/info_memento.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_widget.h" #include "info/members/info_members_widget.h" #include "info/common_groups/info_common_groups_widget.h" +#include "info/saved/info_saved_sublists_widget.h" #include "info/settings/info_settings_widget.h" #include "info/similar_channels/info_similar_channels_widget.h" #include "info/polls/info_polls_results_widget.h" @@ -111,7 +112,9 @@ std::vector> Memento::DefaultStack( } Section Memento::DefaultSection(not_null peer) { - if (peer->sharedMediaInfo()) { + if (peer->savedSublistsInfo()) { + return Section(Section::Type::SavedSublists); + } else if (peer->sharedMediaInfo()) { return Section(Section::MediaType::Photo); } return Section(Section::Type::Profile); @@ -145,6 +148,8 @@ std::shared_ptr Memento::DefaultContent( case Section::Type::SimilarChannels: return std::make_shared( peer->asChannel()); + case Section::Type::SavedSublists: + return std::make_shared(&peer->session()); case Section::Type::Members: return std::make_shared( peer, diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 02d1f75e1..bde368bd0 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -188,23 +188,32 @@ void WrapWidget::injectActivePeerProfile(not_null peer) { ? _historyStack.front().section->section().type() : _controller->section().type(); const auto firstSectionMediaType = [&] { - if (firstSectionType == Section::Type::Profile) { + if (firstSectionType == Section::Type::Profile + || firstSectionType == Section::Type::SavedSublists) { return Section::MediaType::kCount; } return hasStackHistory() ? _historyStack.front().section->section().mediaType() : _controller->section().mediaType(); }(); - const auto expectedType = peer->sharedMediaInfo() + const auto savedSublistsInfo = peer->savedSublistsInfo(); + const auto sharedMediaInfo = peer->sharedMediaInfo(); + const auto expectedType = savedSublistsInfo + ? Section::Type::SavedSublists + : sharedMediaInfo ? Section::Type::Media : Section::Type::Profile; - const auto expectedMediaType = peer->sharedMediaInfo() + const auto expectedMediaType = savedSublistsInfo + ? Section::MediaType::kCount + : sharedMediaInfo ? Section::MediaType::Photo : Section::MediaType::kCount; if (firstSectionType != expectedType || firstSectionMediaType != expectedMediaType || firstPeer != peer) { - auto section = peer->sharedMediaInfo() + auto section = savedSublistsInfo + ? Section(Section::Type::SavedSublists) + : sharedMediaInfo ? Section(Section::MediaType::Photo) : Section(Section::Type::Profile); injectActiveProfileMemento(std::move( @@ -545,6 +554,8 @@ void WrapWidget::removeFromStack(const std::vector
§ions) { const auto &s = item.section->section(); if (s.type() != section.type()) { return false; + } else if (s.type() == Section::Type::SavedSublists) { + return true; } else if (s.type() == Section::Type::Media) { return (s.mediaType() == section.mediaType()); } else if (s.type() == Section::Type::Settings) { diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp index dce97363c..ee9eb4dfd 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp @@ -43,7 +43,7 @@ InnerWidget::InnerWidget( // Allows showing additional shared media links and tabs. // Used for shared media in Saved Messages. void InnerWidget::setupOtherTypes() { - if (_controller->key().peer()->isSelf() && _isStackBottom) { + if (_controller->key().peer()->sharedMediaInfo() && _isStackBottom) { createOtherTypes(); } else { _otherTypes.destroy(); diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp new file mode 100644 index 000000000..28014e8a3 --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp @@ -0,0 +1,105 @@ +/* +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 "info/saved/info_saved_sublists_widget.h" +// +#include "data/data_saved_messages.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "dialogs/dialogs_inner_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "main/main_session.h" +#include "lang/lang_keys.h" + +namespace Info::Saved { + +SublistsMemento::SublistsMemento(not_null session) +: ContentMemento(session->user(), nullptr, PeerId()) { +} + +Section SublistsMemento::section() const { + return Section(Section::Type::SavedSublists); +} + +object_ptr SublistsMemento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr(parent, controller); + result->setInternalState(geometry, this); + return result; +} + +SublistsMemento::~SublistsMemento() = default; + +SublistsWidget::SublistsWidget( + QWidget *parent, + not_null controller) +: ContentWidget(parent, controller) { + _inner = setInnerWidget(object_ptr( + this, + controller->parentController(), + rpl::single(Dialogs::InnerWidget::ChildListShown()))); + _inner->showSavedSublists(); + _inner->setNarrowRatio(0.); + + const auto saved = &controller->session().data().savedMessages(); + _inner->heightValue() | rpl::start_with_next([=] { + if (!saved->supported()) { + crl::on_main(controller, [=] { + controller->showSection( + Memento::Default(controller->session().user()), + Window::SectionShow::Way::Backward); + }); + } + }, lifetime()); + + _inner->setLoadMoreCallback([=] { + saved->loadMore(); + }); +} + +rpl::producer SublistsWidget::title() { + return tr::lng_saved_messages(); +} + +bool SublistsWidget::showInternal(not_null memento) { + if (!controller()->validateMementoPeer(memento)) { + return false; + } + if (auto my = dynamic_cast(memento.get())) { + restoreState(my); + return true; + } + return false; +} + +void SublistsWidget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +std::shared_ptr SublistsWidget::doCreateMemento() { + auto result = std::make_shared( + &controller()->session()); + saveState(result.get()); + return result; +} + +void SublistsWidget::saveState(not_null memento) { + memento->setScrollTop(scrollTopSave()); +} + +void SublistsWidget::restoreState(not_null memento) { + scrollTopRestore(memento->scrollTop()); +} + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.h b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.h new file mode 100644 index 000000000..e7a875ba0 --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.h @@ -0,0 +1,64 @@ +/* +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 "info/info_content_widget.h" + +namespace Dialogs { +class InnerWidget; +} // namespace Dialogs + +namespace Main { +class Session; +} // namespace Main + +namespace Info::Saved { + +class SublistsMemento final : public ContentMemento { +public: + explicit SublistsMemento(not_null session); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) override; + + Section section() const override; + + ~SublistsMemento(); + +private: + +}; + +class SublistsWidget final : public ContentWidget { +public: + SublistsWidget( + QWidget *parent, + not_null controller); + + bool showInternal( + not_null memento) override; + + void setInternalState( + const QRect &geometry, + not_null memento); + + rpl::producer title() override; + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + std::shared_ptr doCreateMemento() override; + + Dialogs::InnerWidget *_inner = nullptr; + +}; + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp b/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp index 886a7869e..cfc911aaf 100644 --- a/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp +++ b/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp @@ -513,4 +513,3 @@ void Widget::restoreState(not_null memento) { } } // namespace Info::SimilarChannels - diff --git a/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.h b/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.h index 7c32a855c..74f15e64f 100644 --- a/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.h +++ b/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.h @@ -68,4 +68,3 @@ private: }; } // namespace Info::SimilarChannels - diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 5556864aa..fab799129 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -500,7 +500,7 @@ void Filler::addTogglePin() { } void Filler::addToggleMuteSubmenu(bool addSeparator) { - if (_thread->peer()->isSelf()) { + if (!_thread || _thread->peer()->isSelf()) { return; } PeerMenuAddMuteSubmenuAction(_controller, _thread, _addAction); @@ -526,6 +526,8 @@ void Filler::addSupportInfo() { void Filler::addInfo() { if (_peer && (_peer->isSelf() || _peer->isRepliesChat())) { return; + } else if (!_thread) { + return; } else if (_controller->adaptive().isThreeColumn()) { const auto thread = _controller->activeChatCurrent().thread(); if (thread && thread == _thread) { @@ -534,8 +536,6 @@ void Filler::addInfo() { return; } } - } else if (!_thread) { - return; } const auto controller = _controller; const auto weak = base::make_weak(_thread);