/* 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.h" #include "data/data_channel.h" #include "data/data_histories.h" #include "data/data_changes.h" #include "data/data_session.h" #include "data/data_forum_icons.h" #include "data/data_forum_topic.h" #include "data/notify/data_notify_settings.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_unread_things.h" #include "main/main_session.h" #include "base/random.h" #include "base/unixtime.h" #include "apiwrap.h" #include "lang/lang_keys.h" #include "core/application.h" #include "ui/layers/generic_box.h" #include "ui/widgets/fields/input_field.h" #include "storage/storage_facade.h" #include "storage/storage_shared_media.h" #include "window/window_session_controller.h" #include "window/notifications_manager.h" #include "styles/style_boxes.h" namespace Data { namespace { constexpr auto kTopicsFirstLoad = 20; constexpr auto kLoadedTopicsMinCount = 20; constexpr auto kTopicsPerPage = 500; constexpr auto kStalePerRequest = 100; constexpr auto kShowTopicNamesCount = 8; // constexpr auto kGeneralColorId = 0xA9A9A9; } // namespace Forum::Forum(not_null history) : _history(history) , _topicsList(&session(), {}, owner().maxPinnedChatsLimitValue(this)) { Expects(_history->peer->isChannel()); if (_history->inChatList()) { preloadTopics(); } if (channel()->canCreateTopics()) { owner().forumIcons().requestDefaultIfUnknown(); } } Forum::~Forum() { for (const auto &request : _topicRequests) { if (request.second.id != _staleRequestId) { owner().histories().cancelRequest(request.second.id); } } if (_staleRequestId) { session().api().request(_staleRequestId).cancel(); } if (_requestId) { session().api().request(_requestId).cancel(); } auto &storage = session().storage(); auto &changes = session().changes(); const auto peerId = _history->peer->id; for (const auto &[rootId, topic] : _topics) { storage.unload(Storage::SharedMediaUnloadThread(peerId, rootId)); _history->setForwardDraft(rootId, {}); const auto raw = topic.get(); changes.topicRemoved(raw); changes.entryRemoved(raw); } } Session &Forum::owner() const { return _history->owner(); } Main::Session &Forum::session() const { return _history->session(); } not_null Forum::history() const { return _history; } not_null Forum::channel() const { return _history->peer->asChannel(); } not_null Forum::topicsList() { return &_topicsList; } rpl::producer<> Forum::destroyed() const { return channel()->flagsValue( ) | rpl::filter([=](const ChannelData::Flags::Change &update) { using Flag = ChannelData::Flag; return (update.diff & Flag::Forum) && !(update.value & Flag::Forum); }) | rpl::take(1) | rpl::to_empty; } rpl::producer> Forum::topicDestroyed() const { return _topicDestroyed.events(); } void Forum::preloadTopics() { if (topicsList()->indexed()->size() < kLoadedTopicsMinCount) { requestTopics(); } } void Forum::reloadTopics() { _topicsList.setLoaded(false); session().api().request(base::take(_requestId)).cancel(); _offset = {}; for (const auto &[rootId, topic] : _topics) { if (!topic->creating()) { _staleRootIds.emplace(topic->rootId()); } } requestTopics(); } void Forum::requestTopics() { if (_topicsList.loaded() || _requestId) { return; } const auto firstLoad = !_offset.date; const auto loadCount = firstLoad ? kTopicsFirstLoad : kTopicsPerPage; _requestId = session().api().request(MTPchannels_GetForumTopics( MTP_flags(0), channel()->inputChannel, MTPstring(), // q MTP_int(_offset.date), MTP_int(_offset.id), MTP_int(_offset.topicId), MTP_int(loadCount) )).done([=](const MTPmessages_ForumTopics &result) { const auto previousOffset = _offset; applyReceivedTopics(result, _offset); const auto &list = result.data().vtopics().v; if (list.isEmpty() || list.size() == result.data().vcount().v || (_offset == previousOffset)) { _topicsList.setLoaded(); } _requestId = 0; _chatsListChanges.fire({}); if (_topicsList.loaded()) { _chatsListLoadedEvents.fire({}); } reorderLastTopics(); requestSomeStale(); }).fail([=](const MTP::Error &error) { _requestId = 0; _topicsList.setLoaded(); if (error.type() == u"CHANNEL_FORUM_MISSING"_q) { const auto flags = channel()->flags() & ~ChannelDataFlag::Forum; channel()->setFlags(flags); } }).send(); } void Forum::applyTopicDeleted(MsgId rootId) { _topicsDeleted.emplace(rootId); const auto i = _topics.find(rootId); if (i != end(_topics)) { const auto raw = i->second.get(); Core::App().notifications().clearFromTopic(raw); owner().removeChatListEntry(raw); if (ranges::contains(_lastTopics, not_null(raw))) { reorderLastTopics(); } _topicDestroyed.fire(raw); session().changes().topicUpdated( raw, Data::TopicUpdate::Flag::Destroyed); session().changes().entryUpdated( raw, Data::EntryUpdate::Flag::Destroyed); _topics.erase(i); _history->destroyMessagesByTopic(rootId); session().storage().unload(Storage::SharedMediaUnloadThread( _history->peer->id, rootId)); _history->setForwardDraft(rootId, {}); } } void Forum::reorderLastTopics() { // We want first kShowChatNamesCount histories, by last message date. const auto pred = [](not_null a, not_null b) { const auto aItem = a->chatListMessage(); const auto bItem = b->chatListMessage(); const auto aDate = aItem ? aItem->date() : TimeId(0); const auto bDate = bItem ? bItem->date() : TimeId(0); return aDate > bDate; }; _lastTopics.clear(); _lastTopics.reserve(kShowTopicNamesCount + 1); auto &&topics = ranges::views::all( *_topicsList.indexed() ) | ranges::views::transform([](not_null row) { return row->topic(); }); auto nonPinnedChecked = 0; for (const auto topic : topics) { const auto i = ranges::upper_bound( _lastTopics, not_null(topic), pred); if (size(_lastTopics) < kShowTopicNamesCount || i != end(_lastTopics)) { _lastTopics.insert(i, topic); } if (size(_lastTopics) > kShowTopicNamesCount) { _lastTopics.pop_back(); } if (!topic->isPinnedDialog(FilterId()) && ++nonPinnedChecked >= kShowTopicNamesCount) { break; } } ++_lastTopicsVersion; _history->updateChatListEntry(); } int Forum::recentTopicsListVersion() const { return _lastTopicsVersion; } void Forum::recentTopicsInvalidate(not_null topic) { if (ranges::contains(_lastTopics, topic)) { ++_lastTopicsVersion; _history->updateChatListEntry(); } } const std::vector> &Forum::recentTopics() const { return _lastTopics; } void Forum::listMessageChanged(HistoryItem *from, HistoryItem *to) { if (from || to) { reorderLastTopics(); } } void Forum::applyReceivedTopics( const MTPmessages_ForumTopics &topics, ForumOffsets &updateOffsets) { applyReceivedTopics(topics, [&](not_null topic) { if (const auto last = topic->lastServerMessage()) { updateOffsets.date = last->date(); updateOffsets.id = last->id; } updateOffsets.topicId = topic->rootId(); }); } void Forum::applyReceivedTopics( const MTPmessages_ForumTopics &topics, Fn)> callback) { const auto &data = topics.data(); owner().processUsers(data.vusers()); owner().processChats(data.vchats()); owner().processMessages(data.vmessages(), NewMessageType::Existing); channel()->ptsReceived(data.vpts().v); applyReceivedTopics(data.vtopics(), std::move(callback)); if (!_staleRootIds.empty()) { requestSomeStale(); } } void Forum::applyReceivedTopics( const MTPVector &topics, Fn)> callback) { const auto &list = topics.v; for (const auto &topic : list) { const auto rootId = topic.match([&](const auto &data) { return data.vid().v; }); _staleRootIds.remove(rootId); topic.match([&](const MTPDforumTopicDeleted &data) { applyTopicDeleted(rootId); }, [&](const MTPDforumTopic &data) { _topicsDeleted.remove(rootId); const auto i = _topics.find(rootId); const auto creating = (i == end(_topics)); const auto raw = creating ? _topics.emplace( rootId, std::make_unique(this, rootId) ).first->second.get() : i->second.get(); raw->applyTopic(data); if (creating) { if (const auto last = _history->chatListMessage() ; last && last->topicRootId() == rootId) { _history->lastItemDialogsView().itemInvalidated(last); _history->updateChatListEntry(); } } if (callback) { callback(raw); } }); } } void Forum::requestSomeStale() { if (_staleRequestId || (!_offset.id && _requestId) || _staleRootIds.empty()) { return; } const auto type = Histories::RequestType::History; auto rootIds = QVector(); rootIds.reserve(std::min(int(_staleRootIds.size()), kStalePerRequest)); for (auto i = begin(_staleRootIds); i != end(_staleRootIds);) { const auto rootId = *i; i = _staleRootIds.erase(i); rootIds.push_back(MTP_int(rootId)); if (rootIds.size() == kStalePerRequest) { break; } } if (rootIds.empty()) { return; } const auto call = [=] { for (const auto &id : rootIds) { finishTopicRequest(id.v); } }; auto &histories = owner().histories(); _staleRequestId = histories.sendRequest(_history, type, [=]( Fn finish) { return session().api().request( MTPchannels_GetForumTopicsByID( channel()->inputChannel, MTP_vector(rootIds)) ).done([=](const MTPmessages_ForumTopics &result) { _staleRequestId = 0; applyReceivedTopics(result); call(); finish(); }).fail([=] { _staleRequestId = 0; call(); finish(); }).send(); }); for (const auto &id : rootIds) { _topicRequests[id.v].id = _staleRequestId; } } void Forum::finishTopicRequest(MsgId rootId) { if (const auto request = _topicRequests.take(rootId)) { for (const auto &callback : request->callbacks) { callback(); } } } void Forum::requestTopic(MsgId rootId, Fn done) { auto &request = _topicRequests[rootId]; if (done) { request.callbacks.push_back(std::move(done)); } if (!request.id && _staleRootIds.emplace(rootId).second && (_staleRootIds.size() == 1)) { crl::on_main(&session(), [peer = channel()] { if (const auto forum = peer->forum()) { forum->requestSomeStale(); } }); } } ForumTopic *Forum::applyTopicAdded( MsgId rootId, const QString &title, int32 colorId, DocumentId iconId, PeerId creatorId, TimeId date, bool my) { Expects(rootId != 0); const auto i = _topics.find(rootId); const auto raw = (i != end(_topics)) ? i->second.get() : _topics.emplace( rootId, std::make_unique(this, rootId) ).first->second.get(); raw->applyTitle(title); raw->applyColorId(colorId); raw->applyIconId(iconId); raw->applyCreator(creatorId); raw->applyCreationDate(date); raw->applyIsMy(my); if (!creating(rootId)) { raw->addToChatList(FilterId(), topicsList()); _chatsListChanges.fire({}); reorderLastTopics(); } return raw; } MsgId Forum::reserveCreatingId( const QString &title, int32 colorId, DocumentId iconId) { const auto result = owner().nextLocalMessageId(); _creatingRootIds.emplace(result); applyTopicAdded( result, title, colorId, iconId, session().userPeerId(), base::unixtime::now(), true); return result; } void Forum::discardCreatingId(MsgId rootId) { Expects(creating(rootId)); const auto i = _topics.find(rootId); if (i != end(_topics)) { Assert(!i->second->inChatList()); _topics.erase(i); } _creatingRootIds.remove(rootId); } bool Forum::creating(MsgId rootId) const { return _creatingRootIds.contains(rootId); } void Forum::created(MsgId rootId, MsgId realId) { if (rootId == realId) { return; } _creatingRootIds.remove(rootId); const auto i = _topics.find(rootId); Assert(i != end(_topics)); auto topic = std::move(i->second); _topics.erase(i); const auto id = FullMsgId(_history->peer->id, realId); if (!_topics.contains(realId)) { _topics.emplace( realId, std::move(topic) ).first->second->setRealRootId(realId); reorderLastTopics(); } owner().notifyItemIdChange({ id, rootId }); } void Forum::clearAllUnreadMentions() { for (const auto &[rootId, topic] : _topics) { topic->unreadMentions().clear(); } } void Forum::clearAllUnreadReactions() { for (const auto &[rootId, topic] : _topics) { topic->unreadReactions().clear(); } } void Forum::enumerateTopics(Fn)> action) const { for (const auto &[rootId, topic] : _topics) { action(topic.get()); } } ForumTopic *Forum::topicFor(MsgId rootId) { if (!rootId) { return nullptr; } const auto i = _topics.find(rootId); return (i != end(_topics)) ? i->second.get() : nullptr; } ForumTopic *Forum::enforceTopicFor(MsgId rootId) { Expects(rootId != 0); const auto i = _topics.find(rootId); if (i != end(_topics)) { return i->second.get(); } requestTopic(rootId); return applyTopicAdded(rootId, {}, {}, {}, {}, {}, {}); } bool Forum::topicDeleted(MsgId rootId) const { return _topicsDeleted.contains(rootId); } rpl::producer<> Forum::chatsListChanges() const { return _chatsListChanges.events(); } rpl::producer<> Forum::chatsListLoadedEvents() const { return _chatsListLoadedEvents.events(); } } // namespace Data