/* 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_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 "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(); } } 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(); } } 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() { _allLoaded = false; session().api().request(base::take(_requestId)).cancel(); _offsetDate = 0; _offsetId = _offsetTopicId = 0; for (const auto &[rootId, topic] : _topics) { if (!topic->creating()) { _staleRootIds.emplace(topic->rootId()); } } requestTopics(); } void Forum::requestTopics() { if (_allLoaded || _requestId) { return; } const auto firstLoad = !_offsetDate; const auto loadCount = firstLoad ? kTopicsFirstLoad : kTopicsPerPage; _requestId = session().api().request(MTPchannels_GetForumTopics( MTP_flags(0), channel()->inputChannel, MTPstring(), // q MTP_int(_offsetDate), MTP_int(_offsetId), MTP_int(_offsetTopicId), MTP_int(loadCount) )).done([=](const MTPmessages_ForumTopics &result) { applyReceivedTopics(result, true); _requestId = 0; _chatsListChanges.fire({}); if (_allLoaded) { _chatsListLoadedEvents.fire({}); } requestSomeStale(); }).fail([=](const MTP::Error &error) { _allLoaded = true; _requestId = 0; if (error.type() == u"CHANNEL_FORUM_MISSING"_q) { const auto flags = channel()->flags() & ~ChannelDataFlag::Forum; channel()->setFlags(flags); } }).send(); } void Forum::applyReceivedTopics(const MTPmessages_ForumTopics &result) { applyReceivedTopics(result, false); } void Forum::applyTopicDeleted(MsgId 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); } } void Forum::applyReceivedTopics( const MTPmessages_ForumTopics &topics, bool updateOffset) { 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) { if (updateOffset) { LOG(("API Error: Got a deleted topic in getForumTopics.")); } applyTopicDeleted(rootId); }, [&](const MTPDforumTopic &data) { 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 (updateOffset) { if (const auto last = raw->lastServerMessage()) { _offsetDate = last->date(); _offsetId = last->id; } _offsetTopicId = rootId; } }); } if (updateOffset && (list.isEmpty() || list.size() == data.vcount().v)) { _allLoaded = true; } if (!_staleRootIds.empty()) { requestSomeStale(); } } void Forum::requestSomeStale() { if (_staleRequestId || (!_offsetId && _requestId)) { 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; } } 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; } rpl::producer<> Forum::chatsListChanges() const { return _chatsListChanges.events(); } rpl::producer<> Forum::chatsListLoadedEvents() const { return _chatsListLoadedEvents.events(); } } // namespace Data