From 388fe6adfb203f55fee0215819e660b0d15e870f Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 20 Sep 2022 22:12:30 +0400 Subject: [PATCH] Allow enabling forum, creating topics. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/data/data_channel.cpp | 8 +- Telegram/SourceFiles/data/data_channel.h | 2 +- Telegram/SourceFiles/data/data_folder.cpp | 6 +- Telegram/SourceFiles/data/data_folder.h | 8 +- Telegram/SourceFiles/data/data_forum.cpp | 166 ++++++++- Telegram/SourceFiles/data/data_forum.h | 36 +- .../SourceFiles/data/data_forum_topic.cpp | 340 ++++++++++++++++++ Telegram/SourceFiles/data/data_forum_topic.h | 115 ++++++ Telegram/SourceFiles/data/data_session.cpp | 5 + Telegram/SourceFiles/data/data_types.h | 31 +- .../SourceFiles/dialogs/dialogs_entry.cpp | 20 +- Telegram/SourceFiles/dialogs/dialogs_entry.h | 7 +- .../dialogs/dialogs_inner_widget.cpp | 34 +- .../dialogs/dialogs_inner_widget.h | 5 +- Telegram/SourceFiles/dialogs/dialogs_key.cpp | 12 + Telegram/SourceFiles/dialogs/dialogs_key.h | 4 + .../SourceFiles/dialogs/dialogs_widget.cpp | 10 +- Telegram/SourceFiles/history/history.cpp | 13 +- Telegram/SourceFiles/history/history_item.cpp | 3 +- Telegram/SourceFiles/history/history_item.h | 3 + 21 files changed, 781 insertions(+), 49 deletions(-) create mode 100644 Telegram/SourceFiles/data/data_forum_topic.cpp create mode 100644 Telegram/SourceFiles/data/data_forum_topic.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index f6952c4b9a..6e38a09d5e 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -469,6 +469,8 @@ PRIVATE data/data_folder.h data/data_forum.cpp data/data_forum.h + data/data_forum_topic.cpp + data/data_forum_topic.h data/data_file_click_handler.cpp data/data_file_click_handler.h data/data_file_origin.cpp diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 0cbebc4286..14903e079d 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -60,11 +60,11 @@ Data::ChatBotCommands::Changed MegagroupInfo::setBotCommands( return _botCommands.update(list); } -void MegagroupInfo::setIsForum(bool is) { +void MegagroupInfo::setIsForum(not_null that, bool is) { if (is == (_forum != nullptr)) { return; } else if (is) { - _forum = std::make_unique(); + _forum = std::make_unique(that->owner().history(that)); } else { _forum = nullptr; } @@ -97,10 +97,6 @@ ChannelData::ChannelData(not_null owner, PeerId id) mgInfo = nullptr; } } - if (change.diff & Flag::Forum) { - Assert(mgInfo != nullptr); - mgInfo->setIsForum(change.value & Flag::Forum); - } if (change.diff & Flag::CallNotEmpty) { if (const auto history = this->owner().historyLoaded(this)) { history->updateChatListEntry(); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 99f68bf272..6aa6210b64 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -100,7 +100,7 @@ public: return _botCommands; } - void setIsForum(bool is); + void setIsForum(not_null that, bool is); [[nodiscard]] Data::Forum *forum() const; std::deque> lastParticipants; diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp index 4c5e4016aa..0c9aa0fb78 100644 --- a/Telegram/SourceFiles/data/data_folder.cpp +++ b/Telegram/SourceFiles/data/data_folder.cpp @@ -41,8 +41,7 @@ Folder::Folder(not_null owner, FolderId id) &owner->session(), FilterId(), owner->maxPinnedChatsLimitValue(this, FilterId())) -, _name(tr::lng_archived_name(tr::now)) -, _chatListNameSortKey(owner->nameSortKey(_name)) { +, _name(tr::lng_archived_name(tr::now)) { indexNameParts(); session().changes().peerUpdates( @@ -374,7 +373,8 @@ const base::flat_set &Folder::chatListFirstLetters() const { } const QString &Folder::chatListNameSortKey() const { - return _chatListNameSortKey; + static const auto empty = QString(); + return empty; } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_folder.h b/Telegram/SourceFiles/data/data_folder.h index a79e8b8ed2..c589dd762b 100644 --- a/Telegram/SourceFiles/data/data_folder.h +++ b/Telegram/SourceFiles/data/data_folder.h @@ -21,7 +21,6 @@ class Session; namespace Data { class Session; -class Folder; class Folder final : public Dialogs::Entry, public base::has_weak_ptr { public: @@ -31,12 +30,12 @@ public: Folder(const Folder &) = delete; Folder &operator=(const Folder &) = delete; - FolderId id() const; + [[nodiscard]] FolderId id() const; void registerOne(not_null history); void unregisterOne(not_null history); void oneListMessageChanged(HistoryItem *from, HistoryItem *to); - not_null chatsList(); + [[nodiscard]] not_null chatsList(); void applyDialog(const MTPDdialogFolder &data); void applyPinnedUpdate(const MTPDupdateDialogPinned &data); @@ -94,13 +93,12 @@ private: const style::color *overrideBg, const style::color *overrideFg) const; - FolderId _id = 0; + const FolderId _id = 0; Dialogs::MainList _chatsList; QString _name; base::flat_set _nameWords; base::flat_set _nameFirstLetters; - QString _chatListNameSortKey; std::vector> _lastHistories; HistoryItem *_chatListMessage = nullptr; diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp index 527f63112c..225ace2006 100644 --- a/Telegram/SourceFiles/data/data_forum.cpp +++ b/Telegram/SourceFiles/data/data_forum.cpp @@ -7,10 +7,172 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_forum.h" -namespace Data { +#include "data/data_channel.h" +#include "data/data_session.h" +#include "data/data_forum_topic.h" +#include "history/history.h" +#include "history/history_item.h" +#include "main/main_session.h" +#include "base/random.h" +#include "apiwrap.h" +#include "lang/lang_keys.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/input_fields.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" -Forum::Forum() = default; +namespace Data { +namespace { + +constexpr auto kTopicsFirstLoad = 20; +constexpr auto kTopicsPerPage = 500; + +} // namespace + +Forum::Forum(not_null forum) +: _forum(forum) +, _topicsList(&forum->session(), FilterId(0), rpl::single(1)) { +} Forum::~Forum() = default; +not_null Forum::topicsList() { + return &_topicsList; +} + +void Forum::requestTopics() { + if (_allLoaded || _requestId) { + return; + } + const auto forum = _forum; + const auto firstLoad = !_offsetDate; + const auto loadCount = firstLoad ? kTopicsFirstLoad : kTopicsPerPage; + const auto api = &forum->session().api(); + _requestId = api->request(MTPchannels_GetForumTopics( + MTP_flags(0), + forum->peer->asChannel()->inputChannel, + MTPstring(), // q + MTP_int(_offsetDate), + MTP_int(_offsetId), + MTP_int(_offsetTopicId), + MTP_int(loadCount) + )).done([=](const MTPmessages_ForumTopics &result) { + if (!forum->peer->isForum()) { + return; + } + const auto &data = result.data(); + const auto owner = &forum->owner(); + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + owner->processMessages(data.vmessages(), NewMessageType::Existing); + forum->peer->asChannel()->ptsReceived(data.vpts().v); + const auto &list = data.vtopics().v; + for (const auto &topic : list) { + const auto rootId = MsgId(topic.data().vid().v); + if (const auto i = _topics.find(rootId); i != end(_topics)) { + i->second->applyTopic(topic); + } else { + const auto raw = _topics.emplace( + rootId, + std::make_unique(forum, rootId) + ).first->second.get(); + raw->applyTopic(topic); + raw->addToChatList(FilterId(), topicsList()); + } + } + if (list.isEmpty() || list.size() == data.vcount().v) { + _allLoaded = true; + } + if (const auto date = data.vnext_date()) { + _offsetDate = date->v; + } + _requestId = 0; + _chatsListChanges.fire({}); + if (_allLoaded) { + _chatsListLoadedEvents.fire({}); + } + }).fail([=](const MTP::Error &error) { + _allLoaded = true; + _requestId = 0; + }).send(); +} + +void Forum::topicAdded(not_null root) { + const auto rootId = root->id; + if (const auto i = _topics.find(rootId); i != end(_topics)) { + //i->second->applyTopic(topic); + } else { + const auto raw = _topics.emplace( + rootId, + std::make_unique(_forum, rootId) + ).first->second.get(); + //raw->applyTopic(topic); + raw->addToChatList(FilterId(), topicsList()); + _chatsListChanges.fire({}); + } +} + +rpl::producer<> Forum::chatsListChanges() const { + return _chatsListChanges.events(); +} + +rpl::producer<> Forum::chatsListLoadedEvents() const { + return _chatsListLoadedEvents.events(); +} + +void ShowAddForumTopic( + not_null controller, + not_null forum) { + controller->show(Box([=](not_null box) { + box->setTitle(rpl::single(u"New Topic"_q)); + + const auto title = box->addRow( + object_ptr( + box, + st::defaultInputField, + rpl::single(u"Topic Title"_q))); // #TODO forum + const auto message = box->addRow( + object_ptr( + box, + st::newGroupDescription, + Ui::InputField::Mode::MultiLine, + rpl::single(u"Message"_q))); // #TODO forum + box->setFocusCallback([=] { + title->setFocusFast(); + }); + box->addButton(tr::lng_create_group_create(), [=] { + if (!forum->isForum()) { + box->closeBox(); + return; + } else if (title->getLastText().trimmed().isEmpty()) { + title->setFocus(); + return; + } else if (message->getLastText().trimmed().isEmpty()) { + message->setFocus(); + return; + } + const auto randomId = base::RandomValue(); + const auto api = &forum->session().api(); + api->request(MTPchannels_CreateForumTopic( + MTP_flags(0), + forum->inputChannel, + MTP_string(title->getLastText().trimmed()), + MTPInputMedia(), + MTP_string(message->getLastText().trimmed()), + MTP_long(randomId), + MTPVector(), + MTPInputPeer() // send_as + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result, randomId); + box->closeBox(); + }).fail([=](const MTP::Error &error) { + api->sendMessageFail(error, forum, randomId); + }).send(); + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + }), Ui::LayerOption::KeepOther); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_forum.h b/Telegram/SourceFiles/data/data_forum.h index 437e87f42d..51fc36230b 100644 --- a/Telegram/SourceFiles/data/data_forum.h +++ b/Telegram/SourceFiles/data/data_forum.h @@ -7,15 +7,49 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "dialogs/dialogs_main_list.h" + +class History; +class ChannelData; + +namespace Window { +class SessionController; +} // namespace Window; + namespace Data { class Forum final { public: - Forum(); + explicit Forum(not_null forum); ~Forum(); + [[nodiscard]] not_null topicsList(); + + void requestTopics(); + [[nodiscard]] rpl::producer<> chatsListChanges() const; + [[nodiscard]] rpl::producer<> chatsListLoadedEvents() const; + + void topicAdded(not_null root); + private: + const not_null _forum; + + base::flat_map> _topics; + Dialogs::MainList _topicsList; + + mtpRequestId _requestId = 0; + TimeId _offsetDate = 0; + MsgId _offsetId = 0; + MsgId _offsetTopicId = 0; + bool _allLoaded = false; + + rpl::event_stream<> _chatsListChanges; + rpl::event_stream<> _chatsListLoadedEvents; }; +void ShowAddForumTopic( + not_null controller, + not_null forum); + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp new file mode 100644 index 0000000000..8970669334 --- /dev/null +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -0,0 +1,340 @@ +/* +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_forum_topic.h" + +#include "data/data_channel.h" +#include "data/data_forum.h" +#include "data/data_session.h" +#include "dialogs/dialogs_main_list.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "history/history.h" +#include "history/history_item.h" + +namespace Data { + +ForumTopic::ForumTopic(not_null forum, MsgId rootId) +: Entry(&forum->owner(), Type::ForumTopic) +, _forum(forum) +, _list(forum->peer->asChannel()->forum()->topicsList()) +, _rootId(rootId) { +} + +not_null ForumTopic::forum() const { + return _forum; +} + +MsgId ForumTopic::rootId() const { + return _rootId; +} + +void ForumTopic::applyTopic(const MTPForumTopic &topic) { + Expects(_rootId == topic.data().vid().v); + + const auto &data = topic.data(); + const auto title = qs(data.vtitle()); + if (_title != title) { + _title = title; + ++_titleVersion; + indexTitleParts(); + updateChatListEntry(); + } + + const auto pinned = _list->pinned(); + if (data.is_pinned()) { + pinned->addPinned(Dialogs::Key(this)); + } else { + pinned->setPinned(Dialogs::Key(this), false); + } + + applyTopicFields( + data.vunread_count().v, + data.vread_inbox_max_id().v, + data.vread_outbox_max_id().v); + applyTopicTopMessage(data.vtop_message().v); + //setUnreadMark(data.is_unread_mark()); +} + +void ForumTopic::indexTitleParts() { + _titleWords.clear(); + _titleFirstLetters.clear(); + auto toIndexList = QStringList(); + auto appendToIndex = [&](const QString &value) { + if (!value.isEmpty()) { + toIndexList.push_back(TextUtilities::RemoveAccents(value)); + } + }; + + appendToIndex(_title); + const auto appendTranslit = !toIndexList.isEmpty() + && cRussianLetters().match(toIndexList.front()).hasMatch(); + if (appendTranslit) { + appendToIndex(translitRusEng(toIndexList.front())); + } + auto toIndex = toIndexList.join(' '); + toIndex += ' ' + rusKeyboardLayoutSwitch(toIndex); + + const auto namesList = TextUtilities::PrepareSearchWords(toIndex); + for (const auto &name : namesList) { + _titleWords.insert(name); + _titleFirstLetters.insert(name[0]); + } +} + +int ForumTopic::chatListNameVersion() const { + return _titleVersion; +} + +void ForumTopic::applyTopicFields( + int unreadCount, + MsgId maxInboxRead, + MsgId maxOutboxRead) { + if (maxInboxRead + 1 >= _inboxReadBefore.value_or(1)) { + setUnreadCount(unreadCount); + setInboxReadTill(maxInboxRead); + } + setOutboxReadTill(maxOutboxRead); +} + +void ForumTopic::applyTopicTopMessage(MsgId topMessageId) { + if (topMessageId) { + const auto itemId = FullMsgId(_forum->peer->id, topMessageId); + if (const auto item = owner().message(itemId)) { + setLastServerMessage(item); + } else { + setLastServerMessage(nullptr); + } + } else { + setLastServerMessage(nullptr); + } +} + +void ForumTopic::setLastServerMessage(HistoryItem *item) { + _lastServerMessage = item; + if (_lastMessage + && *_lastMessage + && !(*_lastMessage)->isRegular() + && (!item || (*_lastMessage)->date() > item->date())) { + return; + } + setLastMessage(item); +} + +void ForumTopic::setLastMessage(HistoryItem *item) { + if (_lastMessage && *_lastMessage == item) { + return; + } + _lastMessage = item; + if (!item || item->isRegular()) { + _lastServerMessage = item; + } + setChatListMessage(item); +} + +void ForumTopic::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()); + +#if 0 // #TODO forum + // If we have a single message from a group, request the full album. + if (hasOrphanMediaGroupPart() + && !item->toPreview({ + .hideSender = true, + .hideCaption = true }).images.empty()) { + owner().histories().requestGroupAround(item); + } +#endif + } else if (!_chatListMessage || *_chatListMessage) { + _chatListMessage = nullptr; + updateChatListEntry(); + } +} + +void ForumTopic::setInboxReadTill(MsgId upTo) { + if (_inboxReadBefore) { + accumulate_max(*_inboxReadBefore, upTo + 1); + } else { + _inboxReadBefore = upTo + 1; + } +} + +void ForumTopic::setOutboxReadTill(MsgId upTo) { + if (_outboxReadBefore) { + accumulate_max(*_outboxReadBefore, upTo + 1); + } else { + _outboxReadBefore = upTo + 1; + } +} + +void ForumTopic::loadUserpic() { +} + +void ForumTopic::paintUserpic( + Painter &p, + std::shared_ptr &view, + int x, + int y, + int size) const { + // #TODO forum +} + +void ForumTopic::requestChatListMessage() { + if (!chatListMessageKnown()) { + // #TODO forum + } +} + +TimeId ForumTopic::adjustedChatListTimeId() const { + const auto result = chatListTimeId(); +#if 0 // #TODO forum + if (const auto draft = cloudDraft()) { + if (!Data::draftIsNull(draft) && !session().supportMode()) { + return std::max(result, draft->date); + } + } +#endif + return result; +} + +int ForumTopic::fixedOnTopIndex() const { + return kArchiveFixOnTopIndex; +} + +bool ForumTopic::shouldBeInChatList() const { + return isPinnedDialog(FilterId()) + || !lastMessageKnown() + || (lastMessage() != nullptr); +} + +HistoryItem *ForumTopic::lastMessage() const { + return _lastMessage.value_or(nullptr); +} + +bool ForumTopic::lastMessageKnown() const { + return _lastMessage.has_value(); +} + +HistoryItem *ForumTopic::lastServerMessage() const { + return _lastServerMessage.value_or(nullptr); +} + +bool ForumTopic::lastServerMessageKnown() const { + return _lastServerMessage.has_value(); +} + +int ForumTopic::unreadCount() const { + return _unreadCount ? *_unreadCount : 0; +} + +int ForumTopic::unreadCountForBadge() const { + const auto result = unreadCount(); + return (!result && unreadMark()) ? 1 : result; +} + +bool ForumTopic::unreadCountKnown() const { + return _unreadCount.has_value(); +} + +void ForumTopic::setUnreadCount(int newUnreadCount) { + if (_unreadCount == newUnreadCount) { + return; + } + const auto wasForBadge = (unreadCountForBadge() > 0); + const auto notifier = unreadStateChangeNotifier(true); + _unreadCount = newUnreadCount; +} + +void ForumTopic::setUnreadMark(bool unread) { + if (_unreadMark == unread) { + return; + } + const auto noUnreadMessages = !unreadCount(); + const auto refresher = gsl::finally([&] { + if (inChatList() && noUnreadMessages) { + updateChatListEntry(); + } + }); + const auto notifier = unreadStateChangeNotifier(noUnreadMessages); + _unreadMark = unread; +} + +bool ForumTopic::unreadMark() const { + return _unreadMark; +} + +int ForumTopic::chatListUnreadCount() const { + const auto state = chatListUnreadState(); + return state.marks + + (Core::App().settings().countUnreadMessages() + ? state.messages + : state.chats); +} + +Dialogs::UnreadState ForumTopic::chatListUnreadState() const { + auto result = Dialogs::UnreadState(); + const auto count = _unreadCount.value_or(0); + const auto mark = !count && _unreadMark; + const auto muted = _forum->mute(); + result.messages = count; + result.messagesMuted = muted ? count : 0; + result.chats = count ? 1 : 0; + result.chatsMuted = (count && muted) ? 1 : 0; + result.marks = mark ? 1 : 0; + result.marksMuted = (mark && muted) ? 1 : 0; + result.known = _unreadCount.has_value(); + return result; +} + +bool ForumTopic::chatListUnreadMark() const { + return false; +} + +bool ForumTopic::chatListMutedBadge() const { + return true; +} + +HistoryItem *ForumTopic::chatListMessage() const { + return _lastMessage.value_or(nullptr); +} + +bool ForumTopic::chatListMessageKnown() const { + return _lastMessage.has_value(); +} + +const QString &ForumTopic::chatListName() const { + return _title; +} + +const base::flat_set &ForumTopic::chatListNameWords() const { + return _titleWords; +} + +const base::flat_set &ForumTopic::chatListFirstLetters() const { + return _titleFirstLetters; +} + +const QString &ForumTopic::chatListNameSortKey() const { + static const auto empty = QString(); + return empty; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h new file mode 100644 index 0000000000..844dbd14e1 --- /dev/null +++ b/Telegram/SourceFiles/data/data_forum_topic.h @@ -0,0 +1,115 @@ +/* +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_entry.h" + +class ChannelData; + +namespace Dialogs { +class MainList; +} // namespace Dialogs + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class Session; + +class ForumTopic final : public Dialogs::Entry { +public: + ForumTopic(not_null forum, MsgId rootId); + + ForumTopic(const ForumTopic &) = delete; + ForumTopic &operator=(const ForumTopic &) = delete; + + [[nodiscard]] not_null forum() const; + [[nodiscard]] MsgId rootId() const; + + void applyTopic(const MTPForumTopic &topic); + + TimeId adjustedChatListTimeId() const override; + + int fixedOnTopIndex() const override; + bool shouldBeInChatList() const override; + int chatListUnreadCount() const override; + bool chatListUnreadMark() const override; + bool chatListMutedBadge() const override; + Dialogs::UnreadState chatListUnreadState() const override; + HistoryItem *chatListMessage() const override; + bool chatListMessageKnown() const override; + void requestChatListMessage() override; + const QString &chatListName() const override; + const QString &chatListNameSortKey() const override; + const base::flat_set &chatListNameWords() const override; + const base::flat_set &chatListFirstLetters() const override; + + [[nodiscard]] HistoryItem *lastMessage() const; + [[nodiscard]] HistoryItem *lastServerMessage() const; + [[nodiscard]] bool lastMessageKnown() const; + [[nodiscard]] bool lastServerMessageKnown() const; + + void loadUserpic() override; + void paintUserpic( + Painter &p, + std::shared_ptr &view, + int x, + int y, + int size) const override; + + [[nodiscard]] int unreadCount() const; + [[nodiscard]] bool unreadCountKnown() const; + + [[nodiscard]] int unreadCountForBadge() const; // unreadCount || unreadMark ? 1 : 0. + + void setUnreadCount(int newUnreadCount); + void setUnreadMark(bool unread); + [[nodiscard]] bool unreadMark() const; + +private: + void indexTitleParts(); + void applyTopicTopMessage(MsgId topMessageId); + void applyTopicFields( + int unreadCount, + MsgId maxInboxRead, + MsgId maxOutboxRead); + void applyChatListMessage(HistoryItem *item); + + void setLastMessage(HistoryItem *item); + void setLastServerMessage(HistoryItem *item); + void setChatListMessage(HistoryItem *item); + + void setInboxReadTill(MsgId upTo); + void setOutboxReadTill(MsgId upTo); + + int chatListNameVersion() const override; + + const not_null _forum; + const not_null _list; + const MsgId _rootId = 0; + + QString _title; + base::flat_set _titleWords; + base::flat_set _titleFirstLetters; + int _titleVersion = 0; + + std::optional _inboxReadBefore; + std::optional _outboxReadBefore; + std::optional _unreadCount; + std::optional _lastMessage; + std::optional _lastServerMessage; + std::optional _chatListMessage; + bool _unreadMark = false; + + rpl::lifetime _lifetime; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index ccea61bf5b..4651f4b8b1 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -805,7 +805,12 @@ not_null Session::processChat(const MTPChat &data) { | ((data.is_forum() && data.is_megagroup()) ? Flag::Forum : Flag()); + const auto wasForum = channel->isForum(); channel->setFlags((channel->flags() & ~flagsMask) | flagsSet); + if (const auto nowForum = channel->isForum(); nowForum != wasForum) { + Assert(channel->mgInfo != nullptr); + channel->mgInfo->setIsForum(channel, nowForum); + } channel->setName( qs(data.vtitle()), diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 3896ad2c6a..43c06cab85 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -239,52 +239,53 @@ enum class MessageFlag : uint64 { MentionsMe = (1ULL << 15), IsOrWasScheduled = (1ULL << 16), NoForwards = (1ULL << 17), + TopicStart = (1ULL << 18), // Needs to return back to inline mode. - HasSwitchInlineButton = (1ULL << 18), + HasSwitchInlineButton = (1ULL << 19), // For "shared links" indexing. - HasTextLinks = (1ULL << 19), + HasTextLinks = (1ULL << 20), // Group / channel create or migrate service message. - IsGroupEssential = (1ULL << 20), + IsGroupEssential = (1ULL << 21), // Edited media is generated on the client // and should not update media from server. - IsLocalUpdateMedia = (1ULL << 21), + IsLocalUpdateMedia = (1ULL << 22), // Sent from inline bot, need to re-set media when sent. - FromInlineBot = (1ULL << 22), + FromInlineBot = (1ULL << 23), // Generated on the client side and should be unread. - ClientSideUnread = (1ULL << 23), + ClientSideUnread = (1ULL << 24), // In a supergroup. - HasAdminBadge = (1ULL << 24), + HasAdminBadge = (1ULL << 25), // Outgoing message that is being sent. - BeingSent = (1ULL << 25), + BeingSent = (1ULL << 26), // Outgoing message and failed to be sent. - SendingFailed = (1ULL << 26), + SendingFailed = (1ULL << 27), // No media and only a several emoji or an only custom emoji text. - SpecialOnlyEmoji = (1ULL << 27), + SpecialOnlyEmoji = (1ULL << 28), // Message existing in the message history. - HistoryEntry = (1ULL << 28), + HistoryEntry = (1ULL << 29), // Local message, not existing on the server. - Local = (1ULL << 29), + Local = (1ULL << 30), // Fake message for some UI element. - FakeHistoryItem = (1ULL << 30), + FakeHistoryItem = (1ULL << 31), // Contact sign-up message, notification should be skipped for Silent. - IsContactSignUp = (1ULL << 31), + IsContactSignUp = (1ULL << 32), // Optimization for item text custom emoji repainting. - CustomEmojiRepainting = (1ULL << 32), + CustomEmojiRepainting = (1ULL << 33), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp index 2c85eb42fc..cea7af9ec1 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_session.h" #include "data/data_folder.h" +#include "data/data_forum_topic.h" #include "data/data_chat_filters.h" #include "mainwidget.h" #include "main/main_session.h" @@ -45,7 +46,7 @@ uint64 PinnedDialogPos(int pinnedIndex) { Entry::Entry(not_null owner, Type type) : _owner(owner) -, _isFolder(type == Type::Folder) { +, _type(type) { } Data::Session &Entry::owner() const { @@ -57,11 +58,19 @@ Main::Session &Entry::session() const { } History *Entry::asHistory() { - return _isFolder ? nullptr : static_cast(this); + return (_type == Type::History) ? static_cast(this) : nullptr; } Data::Folder *Entry::asFolder() { - return _isFolder ? static_cast(this) : nullptr; + return (_type == Type::Folder) + ? static_cast(this) + : nullptr; +} + +Data::ForumTopic *Entry::asForumTopic() { + return (_type == Type::ForumTopic) + ? static_cast(this) + : nullptr; } void Entry::pinnedIndexChanged(FilterId filterId, int was, int now) { @@ -177,6 +186,9 @@ const Ui::Text::String &Entry::chatListNameText() const { } void Entry::setChatListExistence(bool exists) { + if (asForumTopic()) { + return; + } if (exists && _sortKeyInChatList) { owner().refreshChatListEntry(this); updateChatListEntry(); @@ -251,7 +263,7 @@ not_null Entry::addToChatList( void Entry::removeFromChatList( FilterId filterId, not_null list) { - if (isPinnedDialog(filterId)) { + if (!asForumTopic() && isPinnedDialog(filterId)) { owner().setChatPinned(this, filterId, false); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.h b/Telegram/SourceFiles/dialogs/dialogs_entry.h index 9706376350..d18c5a2381 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.h +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.h @@ -19,6 +19,7 @@ class Session; namespace Data { class Session; class Folder; +class ForumTopic; class CloudImageView; } // namespace Data @@ -91,9 +92,10 @@ inline UnreadState operator-(const UnreadState &a, const UnreadState &b) { class Entry { public: - enum class Type { + enum class Type : uchar { History, Folder, + ForumTopic, }; Entry(not_null owner, Type type); Entry(const Entry &other) = delete; @@ -105,6 +107,7 @@ public: History *asHistory(); Data::Folder *asFolder(); + Data::ForumTopic *asForumTopic(); PositionChange adjustByPosInChatList( FilterId filterId, @@ -230,7 +233,7 @@ private: mutable int _chatListNameVersion = 0; TimeId _timeId = 0; bool _isTopPromoted = false; - const bool _isFolder = false; + const Type _type; }; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index d677a81d81..0b6b665fc3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "data/data_drafts.h" #include "data/data_folder.h" +#include "data/data_forum.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -409,6 +410,17 @@ void InnerWidget::changeOpenedForum(ChannelData *forum) { stopReorderPinned(); clearSelection(); _openedForum = forum; + + _openedForumLifetime.destroy(); + if (forum) { + rpl::merge( + forum->forum()->chatsListChanges(), + forum->forum()->chatsListLoadedEvents() + ) | rpl::start_with_next([=] { + refresh(); + }, _openedForumLifetime); + } + refreshWithCollapsedRows(true); if (_loadMoreCallback) { _loadMoreCallback(); @@ -1778,6 +1790,8 @@ void InnerWidget::updateSelectedRow(Key key) { not_null InnerWidget::shownDialogs() const { return _filterId ? session().data().chatsFilters().chatsList(_filterId)->indexed() + : _openedForum + ? _openedForum->forum()->topicsList()->indexed() : session().data().chatsList(_openedFolder)->indexed(); } @@ -2346,6 +2360,8 @@ void InnerWidget::refreshEmptyLabel() { const auto data = &session().data(); const auto state = !shownDialogs()->empty() ? EmptyState::None + : _openedForum + ? EmptyState::EmptyForum : (!_filterId && data->contactsLoaded().current()) ? EmptyState::NoContacts : (_filterId > 0) && data->chatsList()->loaded() @@ -2364,11 +2380,17 @@ void InnerWidget::refreshEmptyLabel() { ? tr::lng_no_chats() : (state == EmptyState::EmptyFolder) ? tr::lng_no_chats_filter() + : (state == EmptyState::EmptyForum) + // #TODO forum + ? rpl::single(u"No chats currently created in this forum."_q) : tr::lng_contacts_loading(); auto link = (state == EmptyState::NoContacts) ? tr::lng_add_contact_button() : (state == EmptyState::EmptyFolder) ? tr::lng_filters_context_edit() + : (state == EmptyState::EmptyForum) + // #TODO forum + ? rpl::single(u"Create topic"_q) : rpl::single(QString()); auto full = rpl::combine( std::move(phrase), @@ -2387,6 +2409,8 @@ void InnerWidget::refreshEmptyLabel() { _controller->showAddContact(); } else if (_emptyState == EmptyState::EmptyFolder) { editOpenedFilter(); + } else if (_emptyState == EmptyState::EmptyForum) { + Data::ShowAddForumTopic(_controller, _openedForum); } }); _empty->setVisible(_state == WidgetState::Default); @@ -3261,9 +3285,13 @@ void InnerWidget::setupShortcuts() { return jumpToDialogRow(last); }); request->check(Command::ChatSelf) && request->handle([=] { - _controller->content()->choosePeer( - session().userPeerId(), - ShowAtUnreadMsgId); + if (_openedForum) { + Data::ShowAddForumTopic(_controller, _openedForum); + } else { + _controller->content()->choosePeer( + session().userPeerId(), + ShowAtUnreadMsgId); + } return true; }); request->check(Command::ShowArchive) && request->handle([=] { diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index e37fc4a325..1098952205 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -331,7 +331,7 @@ private: void clearSearchResults(bool clearPeerSearchResults = true); void updateSelectedRow(Key key = Key()); - not_null shownDialogs() const; + [[nodiscard]] not_null shownDialogs() const; void checkReorderPinnedStart(QPoint localPosition); int updateReorderIndexGetCount(); @@ -343,7 +343,7 @@ private: bool pinnedShiftAnimationCallback(crl::time now); void handleChatListEntryRefreshes(); - not_null _controller; + const not_null _controller; FilterId _filterId = 0; bool _mouseSelection = false; @@ -352,6 +352,7 @@ private: Data::Folder *_openedFolder = nullptr; ChannelData *_openedForum = nullptr; + rpl::lifetime _openedForumLifetime; std::vector> _collapsedRows; int _collapsedSelected = -1; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.cpp b/Telegram/SourceFiles/dialogs/dialogs_key.cpp index a9013a29f1..0d6c570b09 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_key.cpp @@ -8,12 +8,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_key.h" #include "data/data_folder.h" +#include "data/data_forum_topic.h" #include "history/history.h" namespace Dialogs { namespace { using Folder = Data::Folder; +using ForumTopic = Data::ForumTopic; } // namespace @@ -23,12 +25,18 @@ Key::Key(History *history) : _value(history) { Key::Key(Data::Folder *folder) : _value(folder) { } +Key::Key(Data::ForumTopic *forumTopic) : _value(forumTopic) { +} + Key::Key(not_null history) : _value(history) { } Key::Key(not_null folder) : _value(folder) { } +Key::Key(not_null forumTopic) : _value(forumTopic) { +} + not_null Key::entry() const { Expects(_value != nullptr); @@ -43,6 +51,10 @@ Folder *Key::folder() const { return _value ? _value->asFolder() : nullptr; } +ForumTopic *Key::forumTopic() const { + return _value ? _value->asForumTopic() : nullptr; +} + PeerData *Key::peer() const { if (const auto history = this->history()) { return history->peer; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index 2f5967b45b..f77acc3bfc 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -14,6 +14,7 @@ class PeerData; namespace Data { class Folder; +class ForumTopic; } // namespace Data namespace Dialogs { @@ -27,10 +28,12 @@ public: } Key(History *history); Key(Data::Folder *folder); + Key(Data::ForumTopic *forumTopic); Key(not_null entry) : _value(entry) { } Key(not_null history); Key(not_null folder); + Key(not_null forumTopic); explicit operator bool() const { return (_value != nullptr); @@ -38,6 +41,7 @@ public: not_null entry() const; History *history() const; Data::Folder *folder() const; + Data::ForumTopic *forumTopic() const; PeerData *peer() const; inline bool operator<(const Key &other) const { diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 9da3065040..cb53901c33 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -46,6 +46,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat.h" #include "data/data_user.h" #include "data/data_folder.h" +#include "data/data_forum.h" +#include "data/data_forum_topic.h" #include "data/data_histories.h" #include "data/data_changes.h" #include "data/data_download_manager.h" @@ -261,7 +263,11 @@ Widget::Widget( const auto openSearchResult = !controller->selectingPeer() && row.filteredRow; const auto history = row.key.history(); - if (history && history->peer->isForum()) { + if (const auto forumTopic = row.key.forumTopic()) { + controller->showRepliesForMessage( + forumTopic->forum(), + forumTopic->rootId()); + } else if (history && history->peer->isForum()) { controller->openForum(history->peer->asChannel()); } else if (history) { const auto peer = history->peer; @@ -372,6 +378,8 @@ Widget::Widget( && _searchFull && !_searchFullMigrated))) { searchMore(); + } else if (_openedForum) { + _openedForum->forum()->requestTopics(); } else { const auto folder = _inner->shownFolder(); if (!folder || !folder->chatsList()->loaded()) { diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index af5d9f0ad4..a9fda3af7b 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_sponsored_messages.h" #include "data/data_send_action.h" #include "data/data_folder.h" +#include "data/data_forum.h" #include "data/data_photo.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -1123,6 +1124,11 @@ void History::newItemAdded(not_null item) { if (!folderKnown()) { owner().histories().requestDialogEntry(this); } + if (item->isTopicStart() && peer->isForum()) { + if (const auto forum = peer->asChannel()->forum()) { + forum->topicAdded(item); + } + } } void History::registerClientSideMessage(not_null item) { @@ -2019,12 +2025,13 @@ Dialogs::UnreadState History::chatListUnreadState() const { auto result = Dialogs::UnreadState(); const auto count = _unreadCount.value_or(0); const auto mark = !count && _unreadMark; + const auto muted = mute(); result.messages = count; - result.messagesMuted = mute() ? count : 0; + result.messagesMuted = muted ? count : 0; result.chats = count ? 1 : 0; - result.chatsMuted = (count && mute()) ? 1 : 0; + result.chatsMuted = (count && muted) ? 1 : 0; result.marks = mark ? 1 : 0; - result.marksMuted = (mark && mute()) ? 1 : 0; + result.marksMuted = (mark && muted) ? 1 : 0; result.known = _unreadCount.has_value(); return result; } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index ce3f4fe923..d593d7dbe9 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1424,7 +1424,8 @@ MessageFlags FlagsFromMTP( | ((flags & MTP::f_reply_markup) ? Flag::HasReplyMarkup : Flag()) | ((flags & MTP::f_from_scheduled) ? Flag::IsOrWasScheduled : Flag()) | ((flags & MTP::f_views) ? Flag::HasViews : Flag()) - | ((flags & MTP::f_noforwards) ? Flag::NoForwards : Flag()); + | ((flags & MTP::f_noforwards) ? Flag::NoForwards : Flag()) + | ((flags & MTP::f_topic_start) ? Flag::TopicStart : Flag()); } MessageFlags FlagsFromMTP( diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index c402150b42..b6e66fece1 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -145,6 +145,9 @@ public: [[nodiscard]] bool isPinned() const { return _flags & MessageFlag::Pinned; } + [[nodiscard]] bool isTopicStart() const { + return _flags & MessageFlag::TopicStart; + } [[nodiscard]] bool unread() const; [[nodiscard]] bool showNotification() const; void markClientSideAsRead();