/* 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_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 "apiwrap.h" #include "lang/lang_keys.h" #include "core/application.h" #include "ui/layers/generic_box.h" #include "ui/widgets/input_fields.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 kGeneralColorId = 0xA9A9A9; } // namespace Forum::Forum(not_null history) : _history(history) , _topicsList(&session(), FilterId(0), rpl::single(1)) { 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(); } const auto peerId = _history->peer->id; for (const auto &[rootId, topic] : _topics) { session().storage().unload(Storage::SharedMediaUnloadThread( peerId, rootId)); _history->setForwardDraft(rootId, {}); } } 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; } void Forum::unpinTopic() { const auto list = _topicsList.pinned(); while (!list->order().empty()) { list->setPinned(list->order().front(), false); } } 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) { applyReceivedTopics(result, _offset); const auto &list = result.data().vtopics().v; if (list.isEmpty() || list.size() == result.data().vcount().v) { _topicsList.setLoaded(); } _requestId = 0; _chatsListChanges.fire({}); if (_topicsList.loaded()) { _chatsListLoadedEvents.fire({}); } 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); _topicDestroyed.fire(raw); _topics.erase(i); _history->destroyMessagesByTopic(rootId); session().storage().unload(Storage::SharedMediaUnloadThread( _history->peer->id, rootId)); _history->setForwardDraft(rootId, {}); } } 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); const auto &list = data.vtopics().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 (callback) { callback(raw); } }); } if (!_staleRootIds.empty()) { requestSomeStale(); } } 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); if (_topicRequests.contains(rootId)) { continue; } 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) { applyReceivedTopics(result); call(); finish(); }).fail([=] { 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) { _staleRootIds.remove(rootId); auto &request = _topicRequests[rootId]; if (done) { request.callbacks.push_back(std::move(done)); } if (request.id) { return; } const auto call = [=] { finishTopicRequest(rootId); }; const auto type = Histories::RequestType::History; auto &histories = owner().histories(); request.id = histories.sendRequest(_history, type, [=]( Fn finish) { return session().api().request( MTPchannels_GetForumTopicsByID( channel()->inputChannel, MTP_vector(1, MTP_int(rootId.bare))) ).done([=](const MTPmessages_ForumTopics &result) { applyReceivedTopics(result); call(); finish(); }).fail([=] { call(); finish(); }).send(); }); } ForumTopic *Forum::applyTopicAdded( MsgId rootId, const QString &title, int32 colorId, DocumentId iconId) { 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); if (!creating(rootId)) { raw->addToChatList(FilterId(), topicsList()); _chatsListChanges.fire({}); } 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); 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); } 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(); } const auto result = applyTopicAdded(rootId, {}, {}, {}); requestTopic(rootId); return result; } 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