433 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			433 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| 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 "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(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();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| Session &Forum::owner() const {
 | |
| 	return _history->owner();
 | |
| }
 | |
| 
 | |
| Main::Session &Forum::session() const {
 | |
| 	return _history->session();
 | |
| }
 | |
| 
 | |
| not_null<History*> Forum::history() const {
 | |
| 	return _history;
 | |
| }
 | |
| 
 | |
| not_null<ChannelData*> Forum::channel() const {
 | |
| 	return _history->peer->asChannel();
 | |
| }
 | |
| 
 | |
| not_null<Dialogs::MainList*> 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<not_null<ForumTopic*>> 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);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Forum::applyReceivedTopics(
 | |
| 		const MTPmessages_ForumTopics &topics,
 | |
| 		ForumOffsets &updateOffsets) {
 | |
| 	applyReceivedTopics(topics, [&](not_null<ForumTopic*> 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<void(not_null<ForumTopic*>)> 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<ForumTopic>(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<MTPint>();
 | |
| 	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<void()> finish) {
 | |
| 		return session().api().request(
 | |
| 			MTPchannels_GetForumTopicsByID(
 | |
| 				channel()->inputChannel,
 | |
| 				MTP_vector<MTPint>(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<void()> 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<void()> finish) {
 | |
| 		return session().api().request(
 | |
| 			MTPchannels_GetForumTopicsByID(
 | |
| 				channel()->inputChannel,
 | |
| 				MTP_vector<MTPint>(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<ForumTopic>(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<void(not_null<ForumTopic*>)> 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
 | 
