From 602ba5bba951986ee71b6e956411286fbf6325a6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 25 Oct 2022 11:20:22 +0400
Subject: [PATCH] Implement correct ForumTopic::canWrite logic.

---
 Telegram/SourceFiles/apiwrap.cpp              |  26 ++-
 .../boxes/peers/add_bot_to_chat_box.cpp       |   2 +-
 .../boxes/peers/edit_peer_invite_link.cpp     |   5 +-
 Telegram/SourceFiles/boxes/share_box.cpp      |   7 +-
 .../calls/group/calls_group_members.cpp       |   2 +-
 .../calls/group/calls_group_panel.cpp         |   2 +-
 .../calls/group/calls_group_settings.cpp      |   7 +-
 Telegram/SourceFiles/data/data_channel.cpp    |   7 +-
 Telegram/SourceFiles/data/data_channel.h      |   2 +-
 Telegram/SourceFiles/data/data_forum.cpp      |   7 +
 Telegram/SourceFiles/data/data_forum.h        |   2 +
 .../SourceFiles/data/data_forum_topic.cpp     |   7 +
 Telegram/SourceFiles/data/data_forum_topic.h  |   1 +
 Telegram/SourceFiles/data/data_peer.cpp       |   4 +-
 Telegram/SourceFiles/data/data_peer.h         |   2 +-
 .../SourceFiles/data/data_peer_values.cpp     |  51 ++++-
 Telegram/SourceFiles/data/data_peer_values.h  |  10 +-
 Telegram/SourceFiles/history/history.cpp      |   3 +
 .../history/history_inner_widget.cpp          |  15 +-
 Telegram/SourceFiles/history/history_item.cpp |   8 +-
 .../SourceFiles/history/history_message.cpp   |  51 ++---
 .../SourceFiles/history/history_message.h     |  16 +-
 .../SourceFiles/history/history_widget.cpp    | 183 +++++++++++++-----
 Telegram/SourceFiles/history/history_widget.h |   6 +
 .../view/history_view_context_menu.cpp        |   5 +-
 .../history/view/history_view_message.cpp     |   2 +-
 .../view/history_view_replies_section.cpp     |  11 +-
 .../inline_bots/bot_attach_web_view.cpp       |   2 +-
 .../main/session/send_as_peers.cpp            |   2 +-
 Telegram/SourceFiles/mainwidget.cpp           |  19 +-
 .../mac/touchbar/items/mac_scrubber_item.mm   |  14 +-
 .../mac/touchbar/mac_touchbar_manager.mm      |   7 +-
 .../window/notifications_manager.cpp          |  11 +-
 .../SourceFiles/window/window_peer_menu.cpp   |   6 +-
 Telegram/lib_ui                               |   2 +-
 35 files changed, 363 insertions(+), 144 deletions(-)

diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index e27fe368d..3d1c5b185 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -513,6 +513,12 @@ void ApiWrap::sendMessageFail(
 		Assert(randomId != 0);
 		_session->data().unregisterMessageRandomId(randomId);
 		item->sendFailed();
+
+		if (error == u"TOPIC_CLOSED"_q) {
+			if (const auto topic = item->topic()) {
+				topic->setClosed(true);
+			}
+		}
 	}
 }
 
@@ -3005,7 +3011,10 @@ void ApiWrap::finishForwarding(const SendAction &action) {
 	if (!toForward.items.empty()) {
 		const auto error = GetErrorTextForSending(
 			history->peer,
-			toForward.items);
+			{
+				.topicRootId = action.topicRootId,
+				.forward = &toForward.items,
+			});
 		if (!error.isEmpty()) {
 			return;
 		}
@@ -3074,6 +3083,9 @@ void ApiWrap::forwardMessages(
 	if (sendAs) {
 		sendFlags |= MTPmessages_ForwardMessages::Flag::f_send_as;
 	}
+	if (action.topicRootId) {
+		sendFlags |= MTPmessages_ForwardMessages::Flag::f_top_msg_id;
+	}
 
 	auto forwardFrom = draft.items.front()->history()->peer;
 	auto ids = QVector<MTPint>();
@@ -3093,7 +3105,7 @@ void ApiWrap::forwardMessages(
 				MTP_vector<MTPint>(ids),
 				MTP_vector<MTPlong>(randomIds),
 				peer->input,
-				MTPint(), // top_msg_id
+				MTP_int(action.topicRootId),
 				MTP_int(action.options.scheduled),
 				(sendAs ? sendAs->input : MTP_inputPeerEmpty())
 			)).done([=](const MTPUpdates &result) {
@@ -3145,7 +3157,7 @@ void ApiWrap::forwardMessages(
 				HistoryItem::NewMessageDate(action.options.scheduled),
 				messageFromId,
 				messagePostAuthor,
-				item);
+				item); // #TODO forum forward
 			_session->data().registerMessageRandomId(randomId, newId);
 			if (!localIds) {
 				localIds = std::make_shared<base::flat_map<uint64, FullMsgId>>();
@@ -3405,7 +3417,13 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 	action.generateLocal = true;
 	sendAction(action);
 
-	if (!peer->canWrite() || Api::SendDice(message)) {
+	const auto replyToId = message.action.replyTo;
+	const auto replyTo = replyToId
+		? peer->owner().message(peer, replyToId)
+		: nullptr;
+	const auto topic = replyTo ? replyTo->topic() : nullptr;
+	if (!(topic ? topic->canWrite() : peer->canWrite())
+		|| Api::SendDice(message)) {
 		return;
 	}
 	local().saveRecentSentHashtags(textWithTags.text);
diff --git a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp
index 4e636da93..86c41e28a 100644
--- a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp
@@ -342,7 +342,7 @@ auto AddBotToGroupBoxController::createRow(not_null<History*> history)
 bool AddBotToGroupBoxController::needToCreateRow(
 		not_null<PeerData*> peer) const {
 	if (sharingBotGame()) {
-		if (!peer->canWrite()
+		if (!peer->canWrite() // #TODO forum forward
 			|| peer->amRestricted(ChatRestriction::SendGames)) {
 			return false;
 		}
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
index 3f1e5b898..20acec9ad 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
@@ -1153,8 +1153,7 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
 			for (const auto peer : result) {
 				const auto error = GetErrorTextForSending(
 					peer,
-					{},
-					comment);
+					{ .text = &comment });
 				if (!error.isEmpty()) {
 					return std::make_pair(error, peer);
 				}
@@ -1205,7 +1204,7 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
 	auto object = Box<ShareBox>(ShareBox::Descriptor{
 		.session = &peer->session(),
 		.copyCallback = std::move(copyCallback),
-		.submitCallback = std::move(submitCallback),
+		.submitCallback = std::move(submitCallback), // #TODO forum forward
 		.filterCallback = [](auto peer) { return peer->canWrite(); },
 	});
 	*box = Ui::MakeWeak(object.data());
diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp
index f18ab9324..10817279a 100644
--- a/Telegram/SourceFiles/boxes/share_box.cpp
+++ b/Telegram/SourceFiles/boxes/share_box.cpp
@@ -1292,11 +1292,10 @@ void FastShareMessage(
 		}
 
 		const auto error = [&] {
-			for (const auto peer : result) {
+			for (const auto peer : result) { // #TODO forum forward
 				const auto error = GetErrorTextForSending(
 					peer,
-					items,
-					comment);
+					{ .forward = &items, .text = &comment });
 				if (!error.isEmpty()) {
 					return std::make_pair(error, peer);
 				}
@@ -1399,7 +1398,7 @@ void FastShareMessage(
 		}
 	};
 	auto filterCallback = [isGame](PeerData *peer) {
-		if (peer->canWrite()) {
+		if (peer->canWrite()) { // #TODO forum forward
 			if (auto channel = peer->asChannel()) {
 				return isGame ? (!channel->isBroadcast()) : true;
 			}
diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp
index f97807d6d..ebc047926 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp
@@ -1642,7 +1642,7 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
 			return rpl::single(false) | rpl::type_erased();
 		}
 		return rpl::combine(
-			Data::CanWriteValue(peer.get()),
+			Data::CanWriteValue(peer, false),
 			_call->joinAsValue()
 		) | rpl::map([=](bool can, not_null<PeerData*> joinAs) {
 			return can && joinAs->isSelf();
diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp
index 76e0d0ef3..9d8d38a38 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp
@@ -845,7 +845,7 @@ void Panel::setupMembers() {
 	_members->addMembersRequests(
 	) | rpl::start_with_next([=] {
 		if (!_peer->isBroadcast()
-			&& _peer->canWrite()
+			&& _peer->canWrite(false)
 			&& _call->joinAs()->isSelf()) {
 			addMembers();
 		} else if (const auto channel = _peer->asChannel()) {
diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp
index 260f353d3..c8add875a 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp
@@ -141,11 +141,10 @@ object_ptr<ShareBox> ShareInviteLinkBox(
 		}
 
 		const auto error = [&] {
-			for (const auto peer : result) {
+			for (const auto peer : result) { // #TODO forum forward
 				const auto error = GetErrorTextForSending(
 					peer,
-					{},
-					comment);
+					{ .text = &comment });
 				if (!error.isEmpty()) {
 					return std::make_pair(error, peer);
 				}
@@ -196,7 +195,7 @@ object_ptr<ShareBox> ShareInviteLinkBox(
 		showToast(tr::lng_share_done(tr::now));
 	};
 	auto filterCallback = [](PeerData *peer) {
-		return peer->canWrite();
+		return peer->canWrite(); // #TODO forum forward
 	};
 
 	const auto scheduleStyle = [&] {
diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp
index e9434c40a..6b99cb392 100644
--- a/Telegram/SourceFiles/data/data_channel.cpp
+++ b/Telegram/SourceFiles/data/data_channel.cpp
@@ -552,11 +552,14 @@ bool ChannelData::canPublish() const {
 		|| (adminRights() & AdminRight::PostMessages);
 }
 
-bool ChannelData::canWrite() const {
+bool ChannelData::canWrite(bool checkForForum) const {
 	// Duplicated in Data::CanWriteValue().
 	const auto allowed = amIn()
 		|| ((flags() & Flag::HasLink) && !(flags() & Flag::JoinToWrite));
-	return allowed && (canPublish()
+	const auto forumRestriction = checkForForum && isForum();
+	return allowed
+		&& !forumRestriction
+		&& (canPublish()
 			|| (!isBroadcast()
 				&& !amRestricted(Restriction::SendMessages)));
 }
diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h
index 73a7cc2b4..6e139bb0e 100644
--- a/Telegram/SourceFiles/data/data_channel.h
+++ b/Telegram/SourceFiles/data/data_channel.h
@@ -313,7 +313,7 @@ public:
 	void setDefaultRestrictions(ChatRestrictions rights);
 
 	// Like in ChatData.
-	[[nodiscard]] bool canWrite() const;
+	[[nodiscard]] bool canWrite(bool checkForForum = true) const;
 	[[nodiscard]] bool allowsForwarding() const;
 	[[nodiscard]] bool canEditInformation() const;
 	[[nodiscard]] bool canEditPermissions() const;
diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp
index 923739357..5a1779b5c 100644
--- a/Telegram/SourceFiles/data/data_forum.cpp
+++ b/Telegram/SourceFiles/data/data_forum.cpp
@@ -160,6 +160,8 @@ void Forum::applyReceivedTopics(const MTPmessages_ForumTopics &result) {
 }
 
 void Forum::applyTopicDeleted(MsgId rootId) {
+	_topicsDeleted.emplace(rootId);
+
 	const auto i = _topics.find(rootId);
 	if (i != end(_topics)) {
 		const auto raw = i->second.get();
@@ -192,6 +194,7 @@ void Forum::applyReceivedTopics(
 			}
 			applyTopicDeleted(rootId);
 		}, [&](const MTPDforumTopic &data) {
+			_topicsDeleted.remove(rootId);
 			const auto i = _topics.find(rootId);
 			const auto creating = (i == end(_topics));
 			const auto raw = creating
@@ -410,6 +413,10 @@ ForumTopic *Forum::enforceTopicFor(MsgId rootId) {
 	return result;
 }
 
+bool Forum::topicDeleted(MsgId rootId) const {
+	return _topicsDeleted.contains(rootId);
+}
+
 rpl::producer<> Forum::chatsListChanges() const {
 	return _chatsListChanges.events();
 }
diff --git a/Telegram/SourceFiles/data/data_forum.h b/Telegram/SourceFiles/data/data_forum.h
index fce162ec2..04aa3f896 100644
--- a/Telegram/SourceFiles/data/data_forum.h
+++ b/Telegram/SourceFiles/data/data_forum.h
@@ -55,6 +55,7 @@ public:
 	void applyTopicDeleted(MsgId rootId);
 	[[nodiscard]] ForumTopic *topicFor(MsgId rootId);
 	[[nodiscard]] ForumTopic *enforceTopicFor(MsgId rootId);
+	[[nodiscard]] bool topicDeleted(MsgId rootId) const;
 
 	void applyReceivedTopics(const MTPmessages_ForumTopics &topics);
 
@@ -90,6 +91,7 @@ private:
 	const not_null<History*> _history;
 
 	base::flat_map<MsgId, std::unique_ptr<ForumTopic>> _topics;
+	base::flat_set<MsgId> _topicsDeleted;
 	rpl::event_stream<not_null<ForumTopic*>> _topicDestroyed;
 	Dialogs::MainList _topicsList;
 
diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp
index 9bb0acefe..2416f10ce 100644
--- a/Telegram/SourceFiles/data/data_forum_topic.cpp
+++ b/Telegram/SourceFiles/data/data_forum_topic.cpp
@@ -205,6 +205,13 @@ bool ForumTopic::my() const {
 	return (_flags & Flag::My);
 }
 
+bool ForumTopic::canWrite() const {
+	const auto channel = this->channel();
+	return channel->amIn()
+		&& !channel->amRestricted(ChatRestriction::SendMessages)
+		&& (!closed() || canToggleClosed());
+}
+
 bool ForumTopic::canEdit() const {
 	return my() || channel()->canManageTopics();
 }
diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h
index 48ee8162b..7e3d9e4e5 100644
--- a/Telegram/SourceFiles/data/data_forum_topic.h
+++ b/Telegram/SourceFiles/data/data_forum_topic.h
@@ -62,6 +62,7 @@ public:
 	[[nodiscard]] MsgId rootId() const;
 
 	[[nodiscard]] bool my() const;
+	[[nodiscard]] bool canWrite() const;
 	[[nodiscard]] bool canEdit() const;
 	[[nodiscard]] bool canToggleClosed() const;
 	[[nodiscard]] bool canTogglePinned() const;
diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp
index 2b89ab335..8dbc5770b 100644
--- a/Telegram/SourceFiles/data/data_peer.cpp
+++ b/Telegram/SourceFiles/data/data_peer.cpp
@@ -937,11 +937,11 @@ Data::ForumTopic *PeerData::forumTopicFor(MsgId rootId) const {
 }
 
 
-bool PeerData::canWrite() const {
+bool PeerData::canWrite(bool checkForForum) const {
 	if (const auto user = asUser()) {
 		return user->canWrite();
 	} else if (const auto channel = asChannel()) {
-		return channel->canWrite();
+		return channel->canWrite(checkForForum);
 	} else if (const auto chat = asChat()) {
 		return chat->canWrite();
 	}
diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h
index f79f34e79..9c3e992c2 100644
--- a/Telegram/SourceFiles/data/data_peer.h
+++ b/Telegram/SourceFiles/data/data_peer.h
@@ -207,7 +207,7 @@ public:
 		return _notify;
 	}
 
-	[[nodiscard]] bool canWrite() const;
+	[[nodiscard]] bool canWrite(bool checkForForum = true) const;
 	[[nodiscard]] bool allowsForwarding() const;
 	[[nodiscard]] Data::RestrictionCheckResult amRestricted(
 		ChatRestriction right) const;
diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp
index 08feba690..ae46d9d6f 100644
--- a/Telegram/SourceFiles/data/data_peer_values.cpp
+++ b/Telegram/SourceFiles/data/data_peer_values.cpp
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_chat.h"
 #include "data/data_user.h"
 #include "data/data_changes.h"
+#include "data/data_forum_topic.h"
 #include "data/data_session.h"
 #include "data/data_message_reactions.h"
 #include "main/main_session.h"
@@ -201,10 +202,11 @@ rpl::producer<bool> CanWriteValue(ChatData *chat) {
 		});
 }
 
-rpl::producer<bool> CanWriteValue(ChannelData *channel) {
+rpl::producer<bool> CanWriteValue(ChannelData *channel, bool checkForForum) {
 	using Flag = ChannelDataFlag;
 	const auto mask = 0
 		| Flag::Left
+		| Flag::Forum
 		| Flag::JoinToWrite
 		| Flag::HasLink
 		| Flag::Forbidden
@@ -221,15 +223,19 @@ rpl::producer<bool> CanWriteValue(ChannelData *channel) {
 		DefaultRestrictionValue(
 			channel,
 			ChatRestriction::SendMessages),
-		[](
+		[=](
 				ChannelDataFlags flags,
 				bool postMessagesRight,
 				bool sendMessagesRestriction,
 				bool defaultSendMessagesRestriction) {
 			const auto notAmInFlags = Flag::Left | Flag::Forbidden;
+			const auto forumRestriction = checkForForum
+				&& (flags & Flag::Forum);
 			const auto allowed = !(flags & notAmInFlags)
 				|| ((flags & Flag::HasLink) && !(flags & Flag::JoinToWrite));
-			return allowed && (postMessagesRight
+			return allowed
+				&& !forumRestriction
+				&& (postMessagesRight
 					|| (flags & Flag::Creator)
 					|| (!(flags & Flag::Broadcast)
 						&& !sendMessagesRestriction
@@ -237,17 +243,52 @@ rpl::producer<bool> CanWriteValue(ChannelData *channel) {
 		});
 }
 
-rpl::producer<bool> CanWriteValue(not_null<PeerData*> peer) {
+rpl::producer<bool> CanWriteValue(
+		not_null<PeerData*> peer,
+		bool checkForForum) {
 	if (auto user = peer->asUser()) {
 		return CanWriteValue(user);
 	} else if (auto chat = peer->asChat()) {
 		return CanWriteValue(chat);
 	} else if (auto channel = peer->asChannel()) {
-		return CanWriteValue(channel);
+		return CanWriteValue(channel, checkForForum);
 	}
 	Unexpected("Bad peer value in CanWriteValue");
 }
 
+rpl::producer<bool> CanWriteValue(not_null<ForumTopic*> topic) {
+	using Flag = ChannelDataFlag;
+	const auto mask = 0
+		| Flag::Left
+		| Flag::JoinToWrite
+		| Flag::Forum
+		| Flag::Forbidden;
+	const auto channel = topic->channel();
+	return rpl::combine(
+		PeerFlagsValue(channel.get(), mask),
+		RestrictionValue(
+			channel,
+			ChatRestriction::SendMessages),
+		DefaultRestrictionValue(
+			channel,
+			ChatRestriction::SendMessages),
+		topic->session().changes().topicFlagsValue(
+			topic,
+			TopicUpdate::Flag::Closed),
+		[=](
+				ChannelDataFlags flags,
+				bool sendMessagesRestriction,
+				bool defaultSendMessagesRestriction,
+				auto) {
+			const auto notAmInFlags = Flag::Left | Flag::Forbidden;
+			const auto allowed = !(flags & notAmInFlags);
+			return allowed
+				&& !sendMessagesRestriction
+				&& !defaultSendMessagesRestriction
+				&& (!topic->closed() || topic->canToggleClosed());
+		});
+}
+
 // This is duplicated in PeerData::canPinMessages().
 rpl::producer<bool> CanPinMessagesValue(not_null<PeerData*> peer) {
 	using namespace rpl::mappers;
diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h
index 7a5911775..72374bf0c 100644
--- a/Telegram/SourceFiles/data/data_peer_values.h
+++ b/Telegram/SourceFiles/data/data_peer_values.h
@@ -21,6 +21,7 @@ class Session;
 namespace Data {
 
 struct Reaction;
+class ForumTopic;
 
 template <typename ChangeType, typename Error, typename Generator>
 inline auto FlagsValueWithMask(
@@ -102,8 +103,13 @@ inline auto PeerFullFlagValue(
 
 [[nodiscard]] rpl::producer<bool> CanWriteValue(UserData *user);
 [[nodiscard]] rpl::producer<bool> CanWriteValue(ChatData *chat);
-[[nodiscard]] rpl::producer<bool> CanWriteValue(ChannelData *channel);
-[[nodiscard]] rpl::producer<bool> CanWriteValue(not_null<PeerData*> peer);
+[[nodiscard]] rpl::producer<bool> CanWriteValue(
+	ChannelData *channel,
+	bool checkForForum = true);
+[[nodiscard]] rpl::producer<bool> CanWriteValue(
+	not_null<PeerData*> peer,
+	bool checkForForum = true);
+[[nodiscard]] rpl::producer<bool> CanWriteValue(not_null<ForumTopic*> topic);
 [[nodiscard]] rpl::producer<bool> CanPinMessagesValue(
 	not_null<PeerData*> peer);
 [[nodiscard]] rpl::producer<bool> CanManageGroupCallValue(
diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp
index 3d48d60bd..87cc62102 100644
--- a/Telegram/SourceFiles/history/history.cpp
+++ b/Telegram/SourceFiles/history/history.cpp
@@ -1133,6 +1133,9 @@ void History::applyServiceChanges(
 			if (const auto icon = data.vicon_emoji_id()) {
 				topic->applyIconId(icon->v);
 			}
+			if (const auto closed = data.vclosed()) {
+				topic->setClosed(mtpIsTrue(*closed));
+			}
 		}
 	}, [](const auto &) {
 	});
diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index 12d181bef..73033ce99 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -76,6 +76,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_message_reactions.h"
 #include "data/data_document.h"
 #include "data/data_channel.h"
+#include "data/data_forum_topic.h"
 #include "data/data_poll.h"
 #include "data/data_photo.h"
 #include "data/data_photo_media.h"
@@ -2003,7 +2004,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 		return;
 	}
 	auto selectedState = getSelectionState();
-	auto canSendMessages = _peer->canWrite();
 
 	// -2 - has full selected items, but not over, -1 - has selection, but no over, 0 - no selection, 1 - over text, 2 - over full selected items
 	auto isUponSelected = 0;
@@ -2079,7 +2079,18 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 			return;
 		}
 		const auto itemId = item->fullId();
-		if (canSendMessages) {
+		const auto canReply = [&] {
+			const auto peer = item->history()->peer;
+			if (const auto forum = item->history()->peer->forum()) {
+				const auto topicRootId = item->topicRootId();
+				const auto topic = item->topic();
+				return topic
+					? topic->canWrite()
+					: peer->canWrite(!topicRootId);
+			}
+			return peer->canWrite();
+		}();
+		if (canReply) {
 			_menu->addAction(tr::lng_context_reply_msg(tr::now), [=] {
 				_widget->replyToMessage(itemId);
 			}, &st::menuIconReply);
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index c7eed9029..61e82e580 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -798,7 +798,13 @@ bool HistoryItem::canBeEdited() const {
 		if (isPost() && channel->canEditMessages()) {
 			return true;
 		} else if (out()) {
-			return isPost() ? channel->canPublish() : channel->canWrite();
+			if (isPost()) {
+				return channel->canPublish();
+			} else if (const auto topic = this->topic()) {
+				return topic->canWrite();
+			} else {
+				return channel->canWrite();
+			}
 		} else {
 			return false;
 		}
diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp
index 023b4b782..aaf15e69e 100644
--- a/Telegram/SourceFiles/history/history_message.cpp
+++ b/Telegram/SourceFiles/history/history_message.cpp
@@ -107,44 +107,52 @@ namespace {
 
 QString GetErrorTextForSending(
 		not_null<PeerData*> peer,
-		const HistoryItemsList &items,
-		const TextWithTags &comment,
-		bool ignoreSlowmodeCountdown) {
-	if (!peer->canWrite()) {
+		SendingErrorRequest request) {
+	const auto forum = request.topicRootId ? peer->forum() : nullptr;
+	const auto topic = forum
+		? forum->topicFor(request.topicRootId)
+		: nullptr;
+	if (!(topic ? topic->canWrite() : peer->canWrite())) {
 		return tr::lng_forward_cant(tr::now);
 	}
 
-	for (const auto &item : items) {
-		if (const auto media = item->media()) {
-			const auto error = media->errorTextForForward(peer);
-			if (!error.isEmpty() && error != qstr("skip")) {
-				return error;
+	if (request.forward) {
+		for (const auto &item : *request.forward) {
+			if (const auto media = item->media()) {
+				const auto error = media->errorTextForForward(peer);
+				if (!error.isEmpty() && error != qstr("skip")) {
+					return error;
+				}
 			}
 		}
 	}
 	const auto error = Data::RestrictionError(
 		peer,
 		ChatRestriction::SendInline);
-	if (error && HasInlineItems(items)) {
+	if (error && request.forward && HasInlineItems(*request.forward)) {
 		return *error;
 	}
 
 	if (peer->slowmodeApplied()) {
+		const auto hasText = (request.text && !request.text->empty());
+		const auto count = (hasText ? 1 : 0)
+			+ (request.forward ? int(request.forward->size()) : 0);
 		if (const auto history = peer->owner().historyLoaded(peer)) {
-			if (!ignoreSlowmodeCountdown
+			if (!request.ignoreSlowmodeCountdown
 				&& (history->latestSendingMessage() != nullptr)
-				&& (!items.empty() || !comment.text.isEmpty())) {
+				&& (count > 0)) {
 				return tr::lng_slowmode_no_many(tr::now);
 			}
 		}
-		if (comment.text.size() > MaxMessageSize) {
+		if (request.text && request.text->text.size() > MaxMessageSize) {
 			return tr::lng_slowmode_too_long(tr::now);
-		} else if (!items.empty() && !comment.text.isEmpty()) {
+		} else if (hasText && count > 1) {
 			return tr::lng_slowmode_no_many(tr::now);
-		} else if (items.size() > 1) {
+		} else if (count > 1) {
 			const auto albumForward = [&] {
-				if (const auto groupId = items.front()->groupId()) {
-					for (const auto &item : items) {
+				const auto first = request.forward->front();
+				if (const auto groupId = first->groupId()) {
+					for (const auto &item : *request.forward) {
 						if (item->groupId() != groupId) {
 							return false;
 						}
@@ -159,7 +167,7 @@ QString GetErrorTextForSending(
 		}
 	}
 	if (const auto left = peer->slowmodeSecondsLeft()) {
-		if (!ignoreSlowmodeCountdown) {
+		if (!request.ignoreSlowmodeCountdown) {
 			return tr::lng_slowmode_enabled(
 				tr::now,
 				lt_left,
@@ -242,13 +250,6 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {
 	return MTPMessageReplyHeader();
 }
 
-QString GetErrorTextForSending(
-		not_null<PeerData*> peer,
-		const HistoryItemsList &items,
-		bool ignoreSlowmodeCountdown) {
-	return GetErrorTextForSending(peer, items, {}, ignoreSlowmodeCountdown);
-}
-
 TextWithEntities DropCustomEmoji(TextWithEntities text) {
 	text.entities.erase(
 		ranges::remove(
diff --git a/Telegram/SourceFiles/history/history_message.h b/Telegram/SourceFiles/history/history_message.h
index 5a23a25ba..e12778688 100644
--- a/Telegram/SourceFiles/history/history_message.h
+++ b/Telegram/SourceFiles/history/history_message.h
@@ -40,15 +40,17 @@ void RequestDependentMessageData(
 	MsgId replyToId);
 [[nodiscard]] MTPMessageReplyHeader NewMessageReplyHeader(
 	const Api::SendAction &action);
+
+struct SendingErrorRequest {
+	MsgId topicRootId = 0;
+	const HistoryItemsList *forward = nullptr;
+	const TextWithTags *text = nullptr;
+	bool ignoreSlowmodeCountdown = false;
+};
 [[nodiscard]] QString GetErrorTextForSending(
 	not_null<PeerData*> peer,
-	const HistoryItemsList &items,
-	bool ignoreSlowmodeCountdown = false);
-[[nodiscard]] QString GetErrorTextForSending(
-	not_null<PeerData*> peer,
-	const HistoryItemsList &items,
-	const TextWithTags &comment,
-	bool ignoreSlowmodeCountdown = false);
+	SendingErrorRequest request);
+
 [[nodiscard]] TextWithEntities DropCustomEmoji(TextWithEntities text);
 
 class HistoryMessage final : public HistoryItem {
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 0e32ce8a4..53d662ceb 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -65,6 +65,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_media_types.h"
 #include "data/data_channel.h"
 #include "data/data_chat.h"
+#include "data/data_forum.h"
+#include "data/data_forum_topic.h"
 #include "data/data_user.h"
 #include "data/data_chat_filters.h"
 #include "data/data_scheduled_messages.h"
@@ -1902,8 +1904,8 @@ void HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
 		auto fieldWillBeHiddenAfterEdit = (!fieldAvailable && _editMsgId != 0);
 		clearFieldText(0, fieldHistoryAction);
 		_field->setFocus();
-		_replyEditMsg = nullptr;
-		_replyToId = 0;
+		_processingReplyItem = _replyEditMsg = nullptr;
+		_processingReplyId = _replyToId = 0;
 		setEditMsgId(0);
 		if (fieldWillBeHiddenAfterEdit) {
 			updateControlsVisibility();
@@ -1926,23 +1928,25 @@ void HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
 	_parsedLinks = _fieldLinksParser->list().current();
 	_previewState = draft->previewState;
 
-	_replyEditMsg = nullptr;
+	_processingReplyItem = _replyEditMsg = nullptr;
+	_processingReplyId = _replyToId = 0;
 	if (const auto editDraft = _history->localEditDraft({})) {
 		setEditMsgId(editDraft->msgId);
-		_replyToId = 0;
 	} else {
-		_replyToId = readyToForward() ? 0 : _history->localDraft({})->msgId;
 		setEditMsgId(0);
 	}
 	updateCmdStartShown();
 	updateControlsVisibility();
 	updateControlsGeometry();
 	refreshTopBarActiveChat();
-	if (_editMsgId || _replyToId) {
+	if (_editMsgId) {
 		updateReplyEditTexts();
 		if (!_replyEditMsg) {
-			requestMessageData(_editMsgId ? _editMsgId : _replyToId);
+			requestMessageData(_editMsgId);
 		}
+	} else if (!readyToForward()) {
+		_processingReplyId = _history->localDraft({})->msgId;
+		processReply();
 	}
 }
 
@@ -2105,8 +2109,8 @@ void HistoryWidget::showHistory(
 	HistoryView::Element::ClearGlobal();
 
 	_saveEditMsgRequestId = 0;
-	_replyEditMsg = nullptr;
-	_editMsgId = _replyToId = 0;
+	_processingReplyItem = _replyEditMsg = nullptr;
+	_processingReplyId = _editMsgId = _replyToId = 0;
 	_previewData = nullptr;
 	_previewCache.clear();
 	_fieldBarCancel->hide();
@@ -2503,7 +2507,7 @@ void HistoryWidget::setupScheduledToggle() {
 
 void HistoryWidget::refreshScheduledToggle() {
 	const auto has = _history
-		&& _peer->canWrite()
+		&& _canSendMessages
 		&& (session().data().scheduledMessages().count(_history) > 0);
 	if (!_scheduled && has) {
 		_scheduled.create(this, st::historyScheduledToggle);
@@ -2564,9 +2568,15 @@ bool HistoryWidget::canWriteMessage() const {
 }
 
 std::optional<QString> HistoryWidget::writeRestriction() const {
-	return _peer
+	auto result = _peer
 		? Data::RestrictionError(_peer, ChatRestriction::SendMessages)
 		: std::nullopt;
+	if (result) {
+		return result;
+	} else if (_peer && _peer->isForum()) {
+		return u"You can reply to messages in topics."_q;
+	}
+	return std::nullopt;
 }
 
 void HistoryWidget::updateControlsVisibility() {
@@ -3699,11 +3709,17 @@ void HistoryWidget::send(Api::SendOptions options) {
 	message.webPageId = webPageId;
 
 	if (_canSendMessages) {
+		const auto topicRootId = _replyEditMsg
+			? _replyEditMsg->topicRootId()
+			: 0;
 		const auto error = GetErrorTextForSending(
 			_peer,
-			_toForward.items,
-			message.textWithTags,
-			options.scheduled);
+			{
+				.topicRootId = topicRootId,
+				.forward = &_toForward.items,
+				.text = &message.textWithTags,
+				.ignoreSlowmodeCountdown = (options.scheduled != 0),
+			});
 		if (!error.isEmpty()) {
 			Ui::ShowMultilineToast({
 				.parentOverride = Window::Show(controller()).toastParent(),
@@ -4027,7 +4043,7 @@ void HistoryWidget::chooseAttach(
 		return;
 	}
 
-	if (!_peer || !_peer->canWrite()) {
+	if (!_peer || !_canSendMessages) {
 		return;
 	} else if (const auto error = Data::RestrictionError(
 			_peer,
@@ -5248,6 +5264,10 @@ void HistoryWidget::itemRemoved(not_null<const HistoryItem*> item) {
 	if (item == _replyEditMsg && _replyToId) {
 		cancelReply();
 	}
+	if (item == _processingReplyItem) {
+		_processingReplyId = 0;
+		_processingReplyItem = nullptr;
+	}
 	if (_kbReplyTo && item == _kbReplyTo) {
 		toggleKeyboard();
 		_kbReplyTo = nullptr;
@@ -6080,7 +6100,7 @@ void HistoryWidget::fieldTabbed() {
 }
 
 void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) {
-	if (!_peer || !_peer->canWrite()) {
+	if (!_peer || !_canSendMessages) {
 		return;
 	} else if (showSlowmodeError()) {
 		return;
@@ -6547,7 +6567,7 @@ bool HistoryWidget::sendExistingDocument(
 			Ui::LayerOption::KeepOther);
 		return false;
 	} else if (!_peer
-		|| !_peer->canWrite()
+		|| !_canSendMessages
 		|| showSlowmodeError()
 		|| ShowSendPremiumError(controller(), document)) {
 		return false;
@@ -6583,7 +6603,7 @@ bool HistoryWidget::sendExistingPhoto(
 			Ui::MakeInformBox(*error),
 			Ui::LayerOption::KeepOther);
 		return false;
-	} else if (!_peer || !_peer->canWrite()) {
+	} else if (!_peer || !_canSendMessages) {
 		return false;
 	} else if (showSlowmodeError()) {
 		return false;
@@ -6657,24 +6677,80 @@ void HistoryWidget::replyToMessage(FullMsgId itemId) {
 }
 
 void HistoryWidget::replyToMessage(not_null<HistoryItem*> item) {
-	if (!item->isRegular() || !_canSendMessages) {
+	_processingReplyId = item->id;
+	_processingReplyItem = item;
+	processReply();
+}
+
+void HistoryWidget::processReply() {
+	const auto processContinue = [=] {
+		return crl::guard(_list, [=] {
+			if (!_peer || !_processingReplyId) {
+				return;
+			} else if (!_processingReplyItem) {
+				_processingReplyItem = _peer->owner().message(
+					_peer,
+					_processingReplyId);
+				if (!_processingReplyItem) {
+					_processingReplyId = 0;
+				} else {
+					processReply();
+				}
+			}
+		});
+	};
+	const auto processCancel = [=] {
+		_processingReplyId = 0;
+		_processingReplyItem = nullptr;
+	};
+
+	if (!_peer || !_processingReplyId) {
+		return processCancel();
+	} else if (!_processingReplyItem) {
+		session().api().requestMessageData(
+			_peer,
+			_processingReplyId,
+			processContinue());
 		return;
-	} else if (item->history() == _migrated) {
-		if (item->isService()) {
+	} else if (_processingReplyItem->history() == _migrated) {
+		if (_processingReplyItem->isService()) {
 			controller()->show(Ui::MakeInformBox(tr::lng_reply_cant()));
 		} else {
-			const auto itemId = item->fullId();
+			const auto itemId = _processingReplyItem->fullId();
 			controller()->show(
 				Ui::MakeConfirmBox({
 					.text = tr::lng_reply_cant_forward(),
 					.confirmed = crl::guard(this, [=] {
 						controller()->content()->setForwardDraft(
 							_peer->id,
-							{ .ids = { 1, itemId } });
+							{.ids = { 1, itemId } });
 					}),
 					.confirmText = tr::lng_selected_forward(),
-				}));
+					}));
 		}
+		return processCancel();
+	} else if (_processingReplyItem->history() != _history
+		|| !_processingReplyItem->isRegular()) {
+		return processCancel();
+	} else if (const auto forum = _peer->forum()) {
+		const auto topicRootId = _processingReplyItem->topicRootId();
+		if (!topicRootId || forum->topicDeleted(topicRootId)) {
+			return processCancel();
+		} else if (const auto topic = forum->topicFor(topicRootId)) {
+			if (!topic->canWrite()) {
+				return processCancel();
+			}
+		} else {
+			forum->requestTopic(topicRootId, processContinue());
+		}
+	} else if (!_peer->canWrite()) {
+		return processCancel();
+	}
+	setReplyFieldsFromProcessing();
+}
+
+void HistoryWidget::setReplyFieldsFromProcessing() {
+	if (!_processingReplyId || !_processingReplyItem) {
 		return;
 	}
 
@@ -6683,23 +6759,27 @@ void HistoryWidget::replyToMessage(not_null<HistoryItem*> item) {
 		_composeSearch->hideAnimated();
 	}
 
+	const auto id = base::take(_processingReplyId);
+	const auto item = base::take(_processingReplyItem);
 	if (_editMsgId) {
 		if (const auto localDraft = _history->localDraft({})) {
-			localDraft->msgId = item->id;
+			localDraft->msgId = id;
 		} else {
 			_history->setLocalDraft(std::make_unique<Data::Draft>(
 				TextWithTags(),
-				item->id,
-				MsgId(), // topicRootId
+				id,
+				MsgId(),
 				MessageCursor(),
 				Data::PreviewState::Allowed));
 		}
 	} else {
 		_replyEditMsg = item;
-		_replyToId = item->id;
+		_replyToId = id;
 		updateReplyEditText(_replyEditMsg);
+		updateCanSendMessage();
 		updateBotKeyboard();
 		updateReplyToName();
+		updateControlsVisibility();
 		updateControlsGeometry();
 		updateField();
 		refreshTopBarActiveChat();
@@ -6709,7 +6789,9 @@ void HistoryWidget::replyToMessage(not_null<HistoryItem*> item) {
 	_saveDraftStart = crl::now();
 	saveDraft();
 
-	_field->setFocus();
+	if (!_field->isHidden()) {
+		_field->setFocus();
+	}
 }
 
 void HistoryWidget::editMessage(FullMsgId itemId) {
@@ -6836,16 +6918,17 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) {
 	if (_replyToId) {
 		wasReply = true;
 
-		_replyEditMsg = nullptr;
-		_replyToId = 0;
+		_processingReplyItem = _replyEditMsg = nullptr;
+		_processingReplyId = _replyToId = 0;
 		mouseMoveEvent(0);
 		if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !_kbReplyTo) {
 			_fieldBarCancel->hide();
 			updateMouseTracking();
 		}
-
 		updateBotKeyboard();
 		refreshTopBarActiveChat();
+		updateCanSendMessage();
+		updateControlsVisibility();
 		updateControlsGeometry();
 		update();
 	} else if (const auto localDraft = (_history ? _history->localDraft({}) : nullptr)) {
@@ -7085,14 +7168,7 @@ void HistoryWidget::updatePreview() {
 void HistoryWidget::fullInfoUpdated() {
 	auto refresh = false;
 	if (_list) {
-		auto newCanSendMessages = _peer->canWrite();
-		if (newCanSendMessages != _canSendMessages) {
-			_canSendMessages = newCanSendMessages;
-			if (!_canSendMessages) {
-				cancelReply();
-			}
-			refreshScheduledToggle();
-			refreshSilentToggle();
+		if (updateCanSendMessage()) {
 			refresh = true;
 		}
 		checkFieldAutocomplete();
@@ -7134,14 +7210,7 @@ void HistoryWidget::handlePeerUpdate() {
 			|| (!isBlocked() && _joinChannel->isHidden() == isJoinChannel())) {
 			resize = true;
 		}
-		bool newCanSendMessages = _peer->canWrite();
-		if (newCanSendMessages != _canSendMessages) {
-			_canSendMessages = newCanSendMessages;
-			if (!_canSendMessages) {
-				cancelReply();
-			}
-			refreshScheduledToggle();
-			refreshSilentToggle();
+		if (updateCanSendMessage()) {
 			resize = true;
 		}
 		updateControlsVisibility();
@@ -7151,6 +7220,24 @@ void HistoryWidget::handlePeerUpdate() {
 	}
 }
 
+bool HistoryWidget::updateCanSendMessage() {
+	const auto replyTo = (_replyToId && !_editMsgId) ? _replyEditMsg : 0;
+	const auto topic = replyTo ? replyTo->topic() : nullptr;
+	const auto newCanSendMessages = topic
+		? topic->canWrite()
+		: _peer->canWrite();
+	if (_canSendMessages == newCanSendMessages) {
+		return false;
+	}
+	_canSendMessages = newCanSendMessages;
+	if (!_canSendMessages) {
+		cancelReply();
+	}
+	refreshScheduledToggle();
+	refreshSilentToggle();
+	return true;
+}
+
 void HistoryWidget::forwardSelected() {
 	if (!_list) {
 		return;
diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h
index f76b6e42f..845a70129 100644
--- a/Telegram/SourceFiles/history/history_widget.h
+++ b/Telegram/SourceFiles/history/history_widget.h
@@ -332,6 +332,8 @@ private:
 	bool cornerButtonsHas(HistoryView::CornerButtonType type) override;
 
 	void checkSuggestToGigagroup();
+	void processReply();
+	void setReplyFieldsFromProcessing();
 
 	void initTabbedSelector();
 	void initVoiceRecordBar();
@@ -382,6 +384,7 @@ private:
 	void toggleTabbedSelectorMode();
 	void recountChatWidth();
 	void handlePeerUpdate();
+	bool updateCanSendMessage();
 	void setMembersShowAreaActive(bool active);
 	void handleHistoryChange(not_null<const History*> history);
 	void showAboutTopPromotion();
@@ -616,6 +619,9 @@ private:
 	Ui::Text::String _replyToName;
 	int _replyToNameVersion = 0;
 
+	MsgId _processingReplyId = 0;
+	HistoryItem *_processingReplyItem = nullptr;
+
 	Data::ResolvedForwardDraft _toForward;
 	Ui::Text::String _toForwardFrom, _toForwardText;
 	int _toForwardNameVersion = 0;
diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
index 019c086c1..07c2630bc 100644
--- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
@@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_photo_media.h"
 #include "data/data_document.h"
 #include "data/data_media_types.h"
+#include "data/data_forum_topic.h"
 #include "data/data_session.h"
 #include "data/data_groups.h"
 #include "data/data_channel.h"
@@ -584,9 +585,11 @@ bool AddReplyToMessageAction(
 		not_null<ListWidget*> list) {
 	const auto context = list->elementContext();
 	const auto item = request.item;
+	const auto topic = item ? item->topic() : nullptr;
+	const auto peer = item ? item->history()->peer.get() : nullptr;
 	if (!item
 		|| !item->isRegular()
-		|| !item->history()->peer->canWrite()
+		|| (topic ? topic->canWrite() : !peer->canWrite())
 		|| (context != Context::History && context != Context::Replies)) {
 		return false;
 	}
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index 779527b6c..69669237f 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -2703,7 +2703,7 @@ bool Message::hasFastReply() const {
 bool Message::displayFastReply() const {
 	return hasFastReply()
 		&& data()->isRegular()
-		&& data()->history()->peer->canWrite()
+		&& data()->history()->peer->canWrite(false)
 		&& !delegate()->elementInSelectionMode();
 }
 
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
index 598c0798e..a0a7f0291 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
@@ -609,9 +609,11 @@ void RepliesWidget::setupComposeControls() {
 			ChatRestriction::SendMessages);
 		return restriction
 			? restriction
-			: _history->peer->canWrite()
+			: topicRestriction
 			? std::move(topicRestriction)
-			: tr::lng_group_not_accessible(tr::now);
+			: !(_topic ? _topic->canWrite() : _history->peer->canWrite())
+			? tr::lng_group_not_accessible(tr::now)
+			: std::optional<QString>();
 	});
 
 	_composeControls->setHistory({
@@ -1217,7 +1219,10 @@ void RepliesWidget::refreshJoinGroupButton() {
 		}
 	};
 	const auto channel = _history->peer->asChannel();
-	if (channel->amIn() || channel->canWrite()) {
+	const auto canWrite = !channel->isForum()
+		? channel->canWrite()
+		: (_topic && _topic->canWrite());
+	if (channel->amIn() || canWrite) {
 		set(nullptr);
 	} else {
 		if (!_joinGroup) {
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index 7126b925a..34a396086 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -129,7 +129,7 @@ void ShowChooseBox(
 		callback(peer);
 	};
 	auto filter = [=](not_null<PeerData*> peer) -> bool {
-		if (!peer->canWrite()) {
+		if (!peer->canWrite()) { // #TODO forum forward
 			return false;
 		} else if (const auto user = peer->asUser()) {
 			if (user->isBot()) {
diff --git a/Telegram/SourceFiles/main/session/send_as_peers.cpp b/Telegram/SourceFiles/main/session/send_as_peers.cpp
index 8f6f1ac26..49339cd7e 100644
--- a/Telegram/SourceFiles/main/session/send_as_peers.cpp
+++ b/Telegram/SourceFiles/main/session/send_as_peers.cpp
@@ -43,7 +43,7 @@ SendAsPeers::SendAsPeers(not_null<Session*> session)
 
 bool SendAsPeers::shouldChoose(not_null<PeerData*> peer) {
 	refresh(peer);
-	return peer->canWrite() && (list(peer).size() > 1);
+	return peer->canWrite(false) && (list(peer).size() > 1);
 }
 
 void SendAsPeers::refresh(not_null<PeerData*> peer, bool force) {
diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp
index 4b2f23f37..a213e2df7 100644
--- a/Telegram/SourceFiles/mainwidget.cpp
+++ b/Telegram/SourceFiles/mainwidget.cpp
@@ -335,7 +335,10 @@ MainWidget::MainWidget(
 	_controller->activeChatValue(
 	) | rpl::map([](Dialogs::Key key) {
 		const auto peer = key.peer();
-		auto canWrite = peer
+		const auto topic = key.topic();
+		auto canWrite = topic
+			? Data::CanWriteValue(topic)
+			: peer
 			? Data::CanWriteValue(peer)
 			: rpl::single(false);
 		return std::move(
@@ -516,10 +519,10 @@ bool MainWidget::setForwardDraft(PeerId peerId, Data::ForwardDraft &&draft) {
 	Expects(peerId != 0);
 
 	const auto peer = session().data().peer(peerId);
+	const auto items = session().data().idsToItems(draft.ids);
 	const auto error = GetErrorTextForSending(
-		peer,
-		session().data().idsToItems(draft.ids),
-		true);
+		peer, // #TODO forum forward
+		{ .forward = &items, .ignoreSlowmodeCountdown = true });
 	if (!error.isEmpty()) {
 		Ui::show(Ui::MakeInformBox(error), Ui::LayerOption::KeepOther);
 		return false;
@@ -541,7 +544,7 @@ bool MainWidget::shareUrl(
 	Expects(peerId != 0);
 
 	const auto peer = session().data().peer(peerId);
-	if (!peer->canWrite()) {
+	if (!peer->canWrite()) { // #TODO forum forward
 		_controller->show(Ui::MakeInformBox(tr::lng_share_cant()));
 		return false;
 	}
@@ -575,7 +578,7 @@ bool MainWidget::inlineSwitchChosen(
 	Expects(peerId != 0);
 
 	const auto peer = session().data().peer(peerId);
-	if (!peer->canWrite()) {
+	if (!peer->canWrite()) { // #TODO forum forward
 		Ui::show(Ui::MakeInformBox(tr::lng_inline_switch_cant()));
 		return false;
 	}
@@ -607,7 +610,7 @@ bool MainWidget::sendPaths(PeerId peerId) {
 	Expects(peerId != 0);
 
 	auto peer = session().data().peer(peerId);
-	if (!peer->canWrite()) {
+	if (!peer->canWrite()) { // #TODO forum forward
 		Ui::show(Ui::MakeInformBox(tr::lng_forward_send_files_cant()));
 		return false;
 	} else if (const auto error = Data::RestrictionError(
@@ -638,7 +641,7 @@ void MainWidget::onFilesOrForwardDrop(
 		}
 	} else {
 		auto peer = session().data().peer(peerId);
-		if (!peer->canWrite()) {
+		if (!peer->canWrite()) { // #TODO forum forward
 			Ui::show(Ui::MakeInformBox(tr::lng_forward_send_files_cant()));
 			return;
 		}
diff --git a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_scrubber_item.mm b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_scrubber_item.mm
index 2d402f3d7..515ea5941 100644
--- a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_scrubber_item.mm
+++ b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_scrubber_item.mm
@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_document.h"
 #include "data/data_document_media.h"
 #include "data/data_file_origin.h"
+#include "data/data_forum_topic.h"
 #include "data/data_session.h"
 #include "data/stickers/data_stickers.h"
 #include "history/history.h"
@@ -168,7 +169,9 @@ auto ActiveChat(not_null<Window::Controller*> controller) {
 }
 
 bool CanWriteToActiveChat(not_null<Window::Controller*> controller) {
-	if (const auto history = ActiveChat(controller).history()) {
+	if (const auto topic = ActiveChat(controller).topic()) {
+		return topic->canWrite();
+	} else if (const auto history = ActiveChat(controller).history()) {
 		return history->peer->canWrite();
 	}
 	return false;
@@ -557,10 +560,11 @@ void AppendEmojiPacks(
 
 	controller->sessionController()->activeChatValue(
 	) | rpl::map([](Dialogs::Key k) {
-		return k.peer()
-			&& k.history()
-			&& k.peer()->canWrite()
-			&& !RestrictionToSendStickers(k.peer());
+		const auto topic = k.topic();
+		const auto peer = k.peer();
+		return peer
+			&& !RestrictionToSendStickers(peer)
+			&& (topic ? topic->canWrite() : peer->canWrite());
 	}) | rpl::distinct_until_changed(
 	) | rpl::start_with_next([=](bool value) {
 		[self dismissPopover:nil];
diff --git a/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.mm b/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.mm
index febe82d82..3aa560faa 100644
--- a/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.mm
+++ b/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.mm
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "apiwrap.h" // ApiWrap::updateStickers()
 #include "core/application.h"
 #include "data/data_peer.h" // PeerData::canWrite()
+#include "data/data_forum_topic.h" // Data::ForumTopic::canWrite()
 #include "data/data_session.h"
 #include "data/stickers/data_stickers.h" // Stickers::setsRef()
 #include "main/main_domain.h"
@@ -144,7 +145,11 @@ const auto kAudioItemIdentifier = @"touchbarAudio";
 				_canApplyMarkdownLast),
 			_controller->sessionController()->activeChatValue(
 			) | rpl::map([](Dialogs::Key k) {
-				return k.peer() && k.history() && k.peer()->canWrite();
+				const auto topic = k.topic();
+				const auto peer = k.peer();
+				return topic
+					? topic->canWrite()
+					: (peer && peer->canWrite());
 			}) | rpl::distinct_until_changed()
 		) | rpl::start_with_next([=](
 				bool canApplyMarkdown,
diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp
index f2acc5db3..202d90e4f 100644
--- a/Telegram/SourceFiles/window/notifications_manager.cpp
+++ b/Telegram/SourceFiles/window/notifications_manager.cpp
@@ -811,6 +811,8 @@ Manager::DisplayOptions Manager::getNotificationOptions(
 	const auto hideEverything = Core::App().passcodeLocked()
 		|| forceHideDetails();
 	const auto view = Core::App().settings().notifyView();
+	const auto peer = item ? item->history()->peer.get() : nullptr;
+	const auto topic = item ? item->topic() : nullptr;
 
 	auto result = DisplayOptions();
 	result.hideNameAndPhoto = hideEverything
@@ -820,12 +822,11 @@ Manager::DisplayOptions Manager::getNotificationOptions(
 	result.hideMarkAsRead = result.hideMessageText
 		|| (type != Data::ItemNotificationType::Message)
 		|| !item
-		|| ((item->out() || item->history()->peer->isSelf())
-			&& item->isFromScheduled());
+		|| ((item->out() || peer->isSelf()) && item->isFromScheduled());
 	result.hideReplyButton = result.hideMarkAsRead
-		|| !item->history()->peer->canWrite()
-		|| item->history()->peer->isBroadcast()
-		|| (item->history()->peer->slowmodeSecondsLeft() > 0);
+		|| (!peer->canWrite() && (!topic || !topic->canWrite()))
+		|| peer->isBroadcast()
+		|| (peer->slowmodeSecondsLeft() > 0);
 	return result;
 }
 
diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp
index 8cfe2aa91..d9ecf509e 100644
--- a/Telegram/SourceFiles/window/window_peer_menu.cpp
+++ b/Telegram/SourceFiles/window/window_peer_menu.cpp
@@ -1219,7 +1219,7 @@ void PeerMenuShareContactBox(
 	// There is no async to make weak from controller.
 	const auto weak = std::make_shared<QPointer<Ui::BoxContent>>();
 	auto callback = [=](not_null<PeerData*> peer) {
-		if (!peer->canWrite()) {
+		if (!peer->canWrite()) { // #TODO forum forward
 			navigation->parentController()->show(
 				Ui::MakeInformBox(tr::lng_forward_share_cant()),
 				Ui::LayerOption::KeepOther);
@@ -1529,10 +1529,10 @@ QPointer<Ui::BoxContent> ShowSendNowMessagesBox(
 		? tr::lng_scheduled_send_now_many(tr::now, lt_count, items.size())
 		: tr::lng_scheduled_send_now(tr::now);
 
+	const auto list = session->data().idsToItems(items);
 	const auto error = GetErrorTextForSending(
 		history->peer,
-		session->data().idsToItems(items),
-		TextWithTags());
+		{ .forward = &list });
 	if (!error.isEmpty()) {
 		Ui::ShowMultilineToast({
 			.parentOverride = Window::Show(navigation).toastParent(),
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 4ba3000a2..c199a1722 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 4ba3000a288772752fcf9b41a618ce5df5a185a5
+Subproject commit c199a1722fae72e254753f3095444a3c82a2a704