From 6997e165c6cf235ac894b1dba63fe43065e47a9e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 14 Oct 2022 17:25:05 +0400
Subject: [PATCH] Forum three-dot menu, except search.

---
 Telegram/Resources/langs/lang.strings         |  16 ++
 .../boxes/peers/edit_forum_topic_box.cpp      |   7 +-
 .../boxes/peers/edit_peer_info_box.cpp        |   4 +-
 .../SourceFiles/data/data_forum_topic.cpp     |  19 +-
 Telegram/SourceFiles/data/data_forum_topic.h  |   3 +
 Telegram/SourceFiles/data/data_peer.cpp       |  17 +-
 Telegram/SourceFiles/data/data_peer.h         |   2 +-
 .../SourceFiles/data/data_replies_list.cpp    |  79 +++++++-
 Telegram/SourceFiles/data/data_replies_list.h |  10 +
 .../dialogs/dialogs_inner_widget.cpp          |  23 +--
 Telegram/SourceFiles/dialogs/dialogs_key.h    |   1 +
 .../SourceFiles/history/history_service.cpp   |  63 ++++---
 .../view/history_view_replies_section.cpp     |  69 +------
 .../view/history_view_replies_section.h       |   5 -
 .../view/history_view_top_bar_widget.cpp      |  10 +-
 .../info/profile/info_profile_widget.cpp      |   2 +-
 Telegram/SourceFiles/ui/empty_userpic.cpp     |  19 +-
 Telegram/SourceFiles/ui/empty_userpic.h       |   3 +-
 Telegram/SourceFiles/ui/menu_icons.style      |   1 +
 .../SourceFiles/window/window_peer_menu.cpp   | 173 +++++++++++++-----
 20 files changed, 354 insertions(+), 172 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 62d2ca41d..f4399e84f 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1136,6 +1136,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_info_bot_title" = "Bot Info";
 "lng_info_group_title" = "Group Info";
 "lng_info_channel_title" = "Channel Info";
+"lng_info_topic_title" = "Topic Info";
 "lng_profile_enable_notifications" = "Notifications";
 "lng_profile_send_message" = "Send Message";
 "lng_info_add_as_contact" = "Add to contacts";
@@ -1484,6 +1485,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_action_webview_data_done" = "You have just successfully transferred data from the «{text}» button to the bot.";
 "lng_action_gift_received" = "{user} sent you a gift for {cost}";
 "lng_action_gift_received_me" = "You sent to {user} a gift for {cost}";
+"lng_action_topic_created" = "Topic created";
+"lng_action_topic_renamed" = "Topic renamed to «{title}»";
+"lng_action_topic_icon_changed" = "Topic icon changed to {emoji}";
+"lng_action_topic_icon_removed" = "Topic icon removed";
+"lng_action_topic_closed" = "Topic closed";
+"lng_action_topic_reopened" = "Topic reopened";
 
 "lng_premium_gift_duration_months#one" = "for {count} month";
 "lng_premium_gift_duration_months#other" = "for {count} months";
@@ -2116,6 +2123,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_context_send_message" = "Send message";
 "lng_context_view_group" = "View group info";
 "lng_context_view_channel" = "View channel info";
+"lng_context_view_topic" = "View topic info";
 "lng_context_hide_psa" = "Hide this announcement";
 "lng_context_pin_to_top" = "Pin to top";
 "lng_context_unpin_from_top" = "Unpin from top";
@@ -3470,6 +3478,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_ringtones_error_max_size" = "Sorry, but your file is too big. The maximum size for ringtones is {size}.";
 "lng_ringtones_error_max_duration" = "Sorry, but your file is too long. The maximum duration for ringtones is {duration}.";
 
+"lng_forum_topic_new" = "New Topic";
+"lng_forum_topic_edit" = "Edit Topic";
+"lng_forum_topic_title" = "Topic Title";
+"lng_forum_topics_switch" = "Topics";
+"lng_forum_no_topics" = "No topics currently created in this forum.";
+"lng_forum_create_topic" = "Create topic";
+"lng_forum_discard_sure" = "Discard sure?";
+
 // Wnd specific
 
 "lng_wnd_choose_program_menu" = "Choose Default Program...";
diff --git a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp
index 24cd4c736..422476289 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp
@@ -337,8 +337,9 @@ void EditForumTopicBox(
 	const auto topic = (!creating && forum->peer->forum())
 		? forum->peer->forum()->topicFor(rootId)
 		: nullptr;
-	// #TODO lang-forum
-	box->setTitle(rpl::single(creating ? u"New topic"_q : u"Edit topic"_q));
+	box->setTitle(creating
+		? tr::lng_forum_topic_new()
+		: tr::lng_forum_topic_edit());
 
 	box->setMaxHeight(st::editTopicMaxHeight);
 
@@ -365,7 +366,7 @@ void EditForumTopicBox(
 		object_ptr<Ui::InputField>(
 			box,
 			st::defaultInputField,
-			rpl::single(u"Topic Title"_q), // #TODO lang-forum
+			tr::lng_forum_topic_title(),
 			topic ? topic->title() : QString()),
 		st::editTopicTitleMargin);
 	box->setFocusCallback([=] {
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
index 0f8a54872..60ee64f10 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
@@ -812,13 +812,13 @@ void Controller::fillForumButton() {
 	Expects(_controls.buttonsLayout != nullptr);
 
 	const auto channel = _peer->asChannel();
-	if (!channel) {
+	if (!channel || !channel->amCreator()) {
 		return;
 	}
 
 	AddButtonWithText(
 		_controls.buttonsLayout,
-		rpl::single(u"Topics"_q), // #TODO lang-forum
+		tr::lng_forum_topics_switch(),
 		rpl::single(QString()),
 		[] {},
 		{ &st::settingsIconGroup, Settings::kIconPurple }
diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp
index 49be44b6e..bfb06ce5f 100644
--- a/Telegram/SourceFiles/data/data_forum_topic.cpp
+++ b/Telegram/SourceFiles/data/data_forum_topic.cpp
@@ -142,7 +142,8 @@ ForumTopic::ForumTopic(not_null<Forum*> forum, MsgId rootId)
 , _forum(forum)
 , _list(_forum->topicsList())
 , _replies(std::make_shared<RepliesList>(history(), rootId))
-, _rootId(rootId) {
+, _rootId(rootId)
+, _lastKnownServerMessageId(rootId) {
 	Thread::setMuted(owner().notifySettings().isMuted(this));
 
 	_replies->unreadCountValue(
@@ -193,6 +194,7 @@ MsgId ForumTopic::rootId() const {
 void ForumTopic::setRealRootId(MsgId realId) {
 	if (_rootId != realId) {
 		_rootId = realId;
+		_lastKnownServerMessageId = realId;
 		_replies = std::make_shared<RepliesList>(history(), _rootId);
 	}
 }
@@ -263,6 +265,7 @@ int ForumTopic::chatListNameVersion() const {
 
 void ForumTopic::applyTopicTopMessage(MsgId topMessageId) {
 	if (topMessageId) {
+		growLastKnownServerMessageId(topMessageId);
 		const auto itemId = FullMsgId(channel()->id, topMessageId);
 		if (const auto item = owner().message(itemId)) {
 			setLastServerMessage(item);
@@ -285,7 +288,14 @@ void ForumTopic::applyTopicTopMessage(MsgId topMessageId) {
 	}
 }
 
+void ForumTopic::growLastKnownServerMessageId(MsgId id) {
+	_lastKnownServerMessageId = std::max(_lastKnownServerMessageId, id);
+}
+
 void ForumTopic::setLastServerMessage(HistoryItem *item) {
+	if (item) {
+		growLastKnownServerMessageId(item->id);
+	}
 	_lastServerMessage = item;
 	if (_lastMessage
 		&& *_lastMessage
@@ -303,6 +313,9 @@ void ForumTopic::setLastMessage(HistoryItem *item) {
 	_lastMessage = item;
 	if (!item || item->isRegular()) {
 		_lastServerMessage = item;
+		if (item) {
+			growLastKnownServerMessageId(item->id);
+		}
 	}
 	setChatListMessage(item);
 }
@@ -413,6 +426,10 @@ bool ForumTopic::lastServerMessageKnown() const {
 	return _lastServerMessage.has_value();
 }
 
+MsgId ForumTopic::lastKnownServerMessageId() const {
+	return _lastKnownServerMessageId;
+}
+
 QString ForumTopic::title() const {
 	return _title;
 }
diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h
index 26aa4e32f..c687abef7 100644
--- a/Telegram/SourceFiles/data/data_forum_topic.h
+++ b/Telegram/SourceFiles/data/data_forum_topic.h
@@ -81,6 +81,7 @@ public:
 	[[nodiscard]] HistoryItem *lastServerMessage() const;
 	[[nodiscard]] bool lastMessageKnown() const;
 	[[nodiscard]] bool lastServerMessageKnown() const;
+	[[nodiscard]] MsgId lastKnownServerMessageId() const;
 
 	[[nodiscard]] QString title() const;
 	void applyTitle(const QString &title);
@@ -116,6 +117,7 @@ private:
 	void indexTitleParts();
 	void validateDefaultIcon() const;
 	void applyTopicTopMessage(MsgId topMessageId);
+	void growLastKnownServerMessageId(MsgId id);
 
 	void setLastMessage(HistoryItem *item);
 	void setLastServerMessage(HistoryItem *item);
@@ -131,6 +133,7 @@ private:
 	const not_null<Dialogs::MainList*> _list;
 	std::shared_ptr<RepliesList> _replies;
 	MsgId _rootId = 0;
+	MsgId _lastKnownServerMessageId = 0;
 
 	PeerNotifySettings _notify;
 
diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp
index 47cb364e1..ea8139e62 100644
--- a/Telegram/SourceFiles/data/data_peer.cpp
+++ b/Telegram/SourceFiles/data/data_peer.cpp
@@ -314,7 +314,15 @@ void PeerData::paintUserpic(
 			x,
 			y,
 			userpic->pix(size, size, { .options = rounding }));
-	} else {
+	} else if (isForum()) {
+		ensureEmptyUserpic()->paintRounded(
+			p,
+			x,
+			y,
+			x + size + x,
+			size,
+			st::roundRadiusLarge);
+	} else{
 		ensureEmptyUserpic()->paint(p, x, y, x + size + x, size);
 	}
 }
@@ -426,6 +434,9 @@ QImage PeerData::generateUserpicImage(
 			ensureEmptyUserpic()->paint(p, 0, 0, size, size);
 		} else if (radius == ImageRoundRadius::None) {
 			ensureEmptyUserpic()->paintSquare(p, 0, 0, size, size);
+		} else if (radius == ImageRoundRadius::Large) {
+			const auto radius = st::roundRadiusLarge;
+			ensureEmptyUserpic()->paintRounded(p, 0, 0, size, size, radius);
 		} else {
 			ensureEmptyUserpic()->paintRounded(p, 0, 0, size, size);
 		}
@@ -519,6 +530,10 @@ bool PeerData::canPinMessages() const {
 	Unexpected("Peer type in PeerData::canPinMessages.");
 }
 
+bool PeerData::canCreateTopics() const {
+	return isForum() && canPinMessages();
+}
+
 bool PeerData::canEditMessagesIndefinitely() const {
 	if (const auto user = asUser()) {
 		return user->isSelf();
diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h
index cf4bef19c..2c31944f9 100644
--- a/Telegram/SourceFiles/data/data_peer.h
+++ b/Telegram/SourceFiles/data/data_peer.h
@@ -339,7 +339,7 @@ public:
 
 	[[nodiscard]] bool canPinMessages() const;
 	[[nodiscard]] bool canEditMessagesIndefinitely() const;
-
+	[[nodiscard]] bool canCreateTopics() const;
 	[[nodiscard]] bool canExportChatHistory() const;
 
 	// Returns true if about text was changed.
diff --git a/Telegram/SourceFiles/data/data_replies_list.cpp b/Telegram/SourceFiles/data/data_replies_list.cpp
index 5863e59ee..292190b5e 100644
--- a/Telegram/SourceFiles/data/data_replies_list.cpp
+++ b/Telegram/SourceFiles/data/data_replies_list.cpp
@@ -18,6 +18,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_messages.h"
 #include "data/data_forum.h"
 #include "data/data_forum_topic.h"
+#include "window/notifications_manager.h"
+#include "core/application.h"
 #include "lang/lang_keys.h"
 #include "apiwrap.h"
 
@@ -25,6 +27,7 @@ namespace Data {
 namespace {
 
 constexpr auto kMessagesPerPage = 50;
+constexpr auto kReadRequestTimeout = 3 * crl::time(1000);
 
 [[nodiscard]] HistoryService *GenerateDivider(
 		not_null<History*> history,
@@ -59,7 +62,8 @@ struct RepliesList::Viewer {
 RepliesList::RepliesList(not_null<History*> history, MsgId rootId)
 : _history(history)
 , _rootId(rootId)
-, _creating(IsCreating(history, rootId)) {
+, _creating(IsCreating(history, rootId))
+, _readRequestTimer([=] { sendReadTillRequest(); }) {
 	_history->owner().repliesReadTillUpdates(
 	) | rpl::filter([=](const RepliesReadTillUpdate &update) {
 		return (update.id.msg == _rootId)
@@ -92,6 +96,9 @@ RepliesList::RepliesList(not_null<History*> history, MsgId rootId)
 RepliesList::~RepliesList() {
 	histories().cancelRequest(base::take(_beforeId));
 	histories().cancelRequest(base::take(_afterId));
+	if (_readRequestTimer.isActive()) {
+		sendReadTillRequest();
+	}
 	if (_divider) {
 		_divider->destroy();
 	}
@@ -753,6 +760,9 @@ MsgId RepliesList::computeOutboxReadTillFull() const {
 
 void RepliesList::setUnreadCount(std::optional<int> count) {
 	_unreadCount = count;
+	if (!count && !_readRequestTimer.isActive() && !_readRequestId) {
+		reloadUnreadCountIfNeeded();
+	}
 }
 
 void RepliesList::checkReadTillEnd() {
@@ -858,4 +868,71 @@ void RepliesList::requestUnreadCount() {
 	}).send();
 }
 
+void RepliesList::readTill(not_null<HistoryItem*> item) {
+	readTill(item->id, item);
+}
+
+void RepliesList::readTill(MsgId tillId) {
+	if (!IsServerMsgId(tillId)) {
+		return;
+	}
+	readTill(tillId, _history->owner().message(_history->peer->id, tillId));
+}
+
+void RepliesList::readTill(
+		MsgId tillId,
+		HistoryItem *tillIdItem) {
+	const auto was = computeInboxReadTillFull();
+	const auto now = tillId;
+	if (now < was) {
+		return;
+	}
+	const auto unreadCount = computeUnreadCountLocally(now);
+	const auto fast = (tillIdItem && tillIdItem->out()) || !unreadCount.has_value();
+	if (was < now || (fast && now == was)) {
+		setInboxReadTill(now, unreadCount);
+		const auto rootFullId = FullMsgId(_history->peer->id, _rootId);
+		if (const auto root = _history->owner().message(rootFullId)) {
+			if (const auto post = root->lookupDiscussionPostOriginal()) {
+				post->setCommentsInboxReadTill(now);
+			}
+		}
+		if (!_readRequestTimer.isActive()) {
+			_readRequestTimer.callOnce(fast ? 0 : kReadRequestTimeout);
+		} else if (fast && _readRequestTimer.remainingTime() > 0) {
+			_readRequestTimer.callOnce(0);
+		}
+	}
+	if (const auto topic = _history->peer->forumTopicFor(_rootId)) {
+		Core::App().notifications().clearIncomingFromTopic(topic);
+	}
+}
+
+void RepliesList::sendReadTillRequest() {
+	if (_readRequestTimer.isActive()) {
+		_readRequestTimer.cancel();
+	}
+	const auto api = &_history->session().api();
+	api->request(base::take(_readRequestId)).cancel();
+
+	_readRequestId = api->request(MTPmessages_ReadDiscussion(
+		_history->peer->input,
+		MTP_int(_rootId),
+		MTP_int(computeInboxReadTillFull())
+	)).done(crl::guard(this, [=] {
+		_readRequestId = 0;
+		reloadUnreadCountIfNeeded();
+	})).send();
+}
+
+void RepliesList::reloadUnreadCountIfNeeded() {
+	if (unreadCountKnown()) {
+		return;
+	} else if (inboxReadTillId() < computeInboxReadTillFull()) {
+		_readRequestTimer.callOnce(0);
+	} else {
+		requestUnreadCount();
+	}
+}
+
 } // namespace Data
diff --git a/Telegram/SourceFiles/data/data_replies_list.h b/Telegram/SourceFiles/data/data_replies_list.h
index 4fcb5d4e1..8c30c912d 100644
--- a/Telegram/SourceFiles/data/data_replies_list.h
+++ b/Telegram/SourceFiles/data/data_replies_list.h
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #pragma once
 
 #include "base/weak_ptr.h"
+#include "base/timer.h"
 
 class History;
 class HistoryService;
@@ -46,6 +47,9 @@ public:
 		MsgId afterId) const;
 	void requestUnreadCount();
 
+	void readTill(not_null<HistoryItem*> item);
+	void readTill(MsgId tillId);
+
 	[[nodiscard]] rpl::lifetime &lifetime() {
 		return _lifetime;
 	}
@@ -81,7 +85,10 @@ private:
 
 	void changeUnreadCountByPost(MsgId id, int delta);
 	void setUnreadCount(std::optional<int> count);
+	void readTill(MsgId tillId, HistoryItem *tillIdItem);
 	void checkReadTillEnd();
+	void sendReadTillRequest();
+	void reloadUnreadCountIfNeeded();
 
 	const not_null<History*> _history;
 	const MsgId _rootId = 0;
@@ -101,6 +108,9 @@ private:
 	int _beforeId = 0;
 	int _afterId = 0;
 
+	base::Timer _readRequestTimer;
+	mtpRequestId _readRequestId = 0;
+
 	mtpRequestId _reloadUnreadCountRequestId = 0;
 
 	rpl::lifetime _lifetime;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index b45a1ab39..340267d82 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -1935,7 +1935,7 @@ void InnerWidget::contextMenuEvent(QContextMenuEvent *e) {
 			_controller,
 			Dialogs::EntryState{
 				.key = row.key,
-				.section = Dialogs::EntryState::Section::ChatsList,
+				.section = Dialogs::EntryState::Section::ContextMenu,
 				.filterId = _filterId,
 			},
 			addAction);
@@ -2394,7 +2394,7 @@ void InnerWidget::refreshEmptyLabel() {
 	const auto data = &session().data();
 	const auto state = !shownDialogs()->empty()
 		? EmptyState::None
-		: _openedForum
+		: (_openedForum && _openedForum->topicsList()->loaded())
 		? EmptyState::EmptyForum
 		: (!_filterId && data->contactsLoaded().current())
 		? EmptyState::NoContacts
@@ -2415,16 +2415,14 @@ void InnerWidget::refreshEmptyLabel() {
 		: (state == EmptyState::EmptyFolder)
 		? tr::lng_no_chats_filter()
 		: (state == EmptyState::EmptyForum)
-		// #TODO lang-forum
-		? rpl::single(u"No chats currently created in this forum."_q)
+		? tr::lng_forum_no_topics()
 		: tr::lng_contacts_loading();
 	auto link = (state == EmptyState::NoContacts)
 		? tr::lng_add_contact_button()
 		: (state == EmptyState::EmptyFolder)
 		? tr::lng_filters_context_edit()
 		: (state == EmptyState::EmptyForum)
-		// #TODO lang-forum
-		? rpl::single(u"Create topic"_q)
+		? tr::lng_forum_create_topic()
 		: rpl::single(QString());
 	auto full = rpl::combine(
 		std::move(phrase),
@@ -3323,16 +3321,9 @@ void InnerWidget::setupShortcuts() {
 			return jumpToDialogRow(last);
 		});
 		request->check(Command::ChatSelf) && request->handle([=] {
-			if (_openedForum) {
-				_controller->show(Box(
-					NewForumTopicBox,
-					_controller,
-					_openedForum->history()));
-			} else {
-				_controller->content()->choosePeer(
-					session().userPeerId(),
-					ShowAtUnreadMsgId);
-			}
+			_controller->content()->choosePeer(
+				session().userPeerId(),
+				ShowAtUnreadMsgId);
 			return true;
 		});
 		request->check(Command::ShowArchive) && request->handle([=] {
diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h
index 620b81ddd..b644188b5 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_key.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_key.h
@@ -100,6 +100,7 @@ struct EntryState {
 		Scheduled,
 		Pinned,
 		Replies,
+		ContextMenu,
 	};
 
 	Key key;
diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp
index e3e42f172..ac209140f 100644
--- a/Telegram/SourceFiles/history/history_service.cpp
+++ b/Telegram/SourceFiles/history/history_service.cpp
@@ -639,27 +639,56 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
 	auto prepareTopicCreate = [&](const MTPDmessageActionTopicCreate &action) {
 		auto result = PreparedText{};
 		// #TODO lang-forum
-		result.text = { "topic created: " + qs(action.vtitle()) };
+		result.text = { tr::lng_action_topic_created(tr::now) };
 		return result;
 	};
 
 	auto prepareTopicEdit = [&](const MTPDmessageActionTopicEdit &action) {
 		auto result = PreparedText{};
-		// #TODO lang-forum
-		result.text = { "topic edited: " };
-		if (const auto icon = action.vicon_emoji_id()) {
-			result.text.append(TextWithEntities{
+		const auto wrapIcon = [](DocumentId id) {
+			return TextWithEntities{
 				"@",
 				{ EntityInText(
 					EntityType::CustomEmoji,
 					0,
 					1,
-					Data::SerializeCustomEmojiId({ .id = icon->v }))
+					Data::SerializeCustomEmojiId({ .id = id }))
 				},
-			});
+			};
+		};
+		if (const auto closed = action.vclosed()) {
+			result.text = { mtpIsTrue(*closed)
+				? tr::lng_action_topic_closed(tr::now)
+				: tr::lng_action_topic_reopened(tr::now) };
+		} else if (!action.vtitle()) {
+			if (const auto icon = action.vicon_emoji_id()) {
+				if (const auto iconId = icon->v) {
+					result.text = tr::lng_action_topic_icon_changed(
+						tr::now,
+						lt_emoji,
+						wrapIcon(iconId),
+						Ui::Text::WithEntities);
+				} else {
+					result.text = {
+						tr::lng_action_topic_icon_removed(tr::now)
+					};
+				}
+			}
+		} else {
+			auto title = TextWithEntities{
+				qs(*action.vtitle())
+			};
+			if (const auto icon = action.vicon_emoji_id().value_or_empty()) {
+				title = wrapIcon(icon).append(' ').append(std::move(title));
+			}
+			result.text = tr::lng_action_topic_renamed(
+				tr::now,
+				lt_title,
+				std::move(title),
+				Ui::Text::WithEntities);
 		}
-		if (const auto &title = action.vtitle()) {
-			result.text.append(qs(*title));
+		if (result.text.empty()) {
+			result.text = { tr::lng_message_empty(tr::now) };
 		}
 		return result;
 	};
@@ -709,14 +738,10 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
 		return prepareProximityReached(data);
 	}, [](const MTPDmessageActionPaymentSentMe &) {
 		LOG(("API Error: messageActionPaymentSentMe received."));
-		return PreparedText{
-			tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
-		};
+		return PreparedText{ { tr::lng_message_empty(tr::now) } };
 	}, [](const MTPDmessageActionSecureValuesSentMe &) {
 		LOG(("API Error: messageActionSecureValuesSentMe received."));
-		return PreparedText{
-			tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
-		};
+		return PreparedText{ { tr::lng_message_empty(tr::now) } };
 	}, [&](const MTPDmessageActionGroupCall &data) {
 		return prepareGroupCall(data);
 	}, [&](const MTPDmessageActionInviteToGroupCall &data) {
@@ -739,13 +764,9 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
 		return prepareTopicEdit(data);
 	}, [&](const MTPDmessageActionWebViewDataSentMe &data) {
 		LOG(("API Error: messageActionWebViewDataSentMe received."));
-		return PreparedText{
-			tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
-		};
+		return PreparedText{ { tr::lng_message_empty(tr::now) } };
 	}, [](const MTPDmessageActionEmpty &) {
-		return PreparedText{
-			tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
-		};
+		return PreparedText{ { tr::lng_message_empty(tr::now) } };
 	}));
 
 	// Additional information.
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
index aaa2cdada..72a7df19e 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
@@ -53,7 +53,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/call_delayed.h"
 #include "base/qt/qt_key_modifiers.h"
 #include "core/file_utilities.h"
-#include "core/application.h"
 #include "main/main_session.h"
 #include "data/data_session.h"
 #include "data/data_user.h"
@@ -70,7 +69,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "inline_bots/inline_bot_result.h"
 #include "lang/lang_keys.h"
 #include "facades.h"
-#include "window/notifications_manager.h"
 #include "styles/style_chat.h"
 #include "styles/style_window.h"
 #include "styles/style_info.h"
@@ -82,7 +80,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace HistoryView {
 namespace {
 
-constexpr auto kReadRequestTimeout = 3 * crl::time(1000);
 constexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200);
 
 bool CanSendFiles(not_null<const QMimeData*> data) {
@@ -218,8 +215,7 @@ RepliesWidget::RepliesWidget(
 , _cornerButtons(
 	_scroll.get(),
 	controller->chatStyle(),
-	static_cast<HistoryView::CornerButtonsDelegate*>(this))
-, _readRequestTimer([=] { sendReadTillRequest(); }) {
+	static_cast<HistoryView::CornerButtonsDelegate*>(this)) {
 	controller->chatStyle()->paletteChanged(
 	) | rpl::start_with_next([=] {
 		_scroll->updateBars();
@@ -359,9 +355,6 @@ RepliesWidget::~RepliesWidget() {
 		_topic->forum()->discardCreatingId(_rootId);
 		_topic = nullptr;
 	}
-	if (_readRequestTimer.isActive()) {
-		sendReadTillRequest();
-	}
 	base::take(_sendAction);
 	_history->owner().sendActionManager().repliesPainterRemoved(
 		_history,
@@ -380,23 +373,6 @@ void RepliesWidget::orderWidgets() {
 	_composeControls->raisePanels();
 }
 
-void RepliesWidget::sendReadTillRequest() {
-	if (_readRequestTimer.isActive()) {
-		_readRequestTimer.cancel();
-	}
-	const auto api = &_history->session().api();
-	api->request(base::take(_readRequestId)).cancel();
-
-	_readRequestId = api->request(MTPmessages_ReadDiscussion(
-		_history->peer->input,
-		MTP_int(_rootId),
-		MTP_int(_replies->computeInboxReadTillFull())
-	)).done(crl::guard(this, [=] {
-		_readRequestId = 0;
-		reloadUnreadCountIfNeeded();
-	})).send();
-}
-
 void RepliesWidget::setupRoot() {
 	if (!_root) {
 		const auto done = crl::guard(this, [=] {
@@ -1366,19 +1342,6 @@ MsgId RepliesWidget::replyToId() const {
 void RepliesWidget::refreshUnreadCountBadge(std::optional<int> count) {
 	if (count.has_value()) {
 		_cornerButtons.updateJumpDownVisibility(count);
-	} else if (!_readRequestTimer.isActive() && !_readRequestId) {
-		reloadUnreadCountIfNeeded();
-	}
-}
-
-void RepliesWidget::reloadUnreadCountIfNeeded() {
-	if (_replies->unreadCountKnown()) {
-		return;
-	} else if (_replies->inboxReadTillId()
-		< _replies->computeInboxReadTillFull()) {
-		_readRequestTimer.callOnce(0);
-	} else {
-		_replies->requestUnreadCount();
 	}
 }
 
@@ -1926,35 +1889,9 @@ void RepliesWidget::listSelectionChanged(SelectedItems &&items) {
 	_topBar->showSelected(state);
 }
 
-void RepliesWidget::readTill(not_null<HistoryItem*> item) {
-	const auto was = _replies->computeInboxReadTillFull();
-	const auto now = item->id;
-	if (now < was) {
-		return;
-	}
-	const auto unreadCount = _replies->computeUnreadCountLocally(now);
-	const auto fast = item->out() || !unreadCount.has_value();
-	if (was < now || (fast && now == was)) {
-		_replies->setInboxReadTill(now, unreadCount);
-		if (_root) {
-			if (const auto post = _root->lookupDiscussionPostOriginal()) {
-				post->setCommentsInboxReadTill(now);
-			}
-		}
-		if (!_readRequestTimer.isActive()) {
-			_readRequestTimer.callOnce(fast ? 0 : kReadRequestTimeout);
-		} else if (fast && _readRequestTimer.remainingTime() > 0) {
-			_readRequestTimer.callOnce(0);
-		}
-	}
-	if (_topic) {
-		Core::App().notifications().clearIncomingFromTopic(_topic);
-	}
-}
-
 void RepliesWidget::listMarkReadTill(not_null<HistoryItem*> item) {
 	if (true/*doWeReadServerHistory()*/) { // #TODO forum active
-		readTill(item);
+		_replies->readTill(item);
 	}
 }
 
@@ -1976,7 +1913,7 @@ MessagesBarData RepliesWidget::listMessagesBar(
 		const auto item = elements[i]->data();
 		if (item->isRegular() && item->id > till) {
 			if (item->out() || !item->replyToId()) {
-				readTill(item);
+				_replies->readTill(item);
 			} else {
 				return {
 					.bar = {
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h
index 9000c20d1..e91bf702e 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.h
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h
@@ -201,8 +201,6 @@ private:
 	void subscribeToTopic();
 	void setTopic(Data::ForumTopic *topic);
 	void setupDragArea();
-	void sendReadTillRequest();
-	void readTill(not_null<HistoryItem*> item);
 
 	void scrollDownAnimationFinish();
 	void updatePinnedVisibility();
@@ -312,9 +310,6 @@ private:
 
 	bool _choosingAttach = false;
 
-	base::Timer _readRequestTimer;
-	mtpRequestId _readRequestId = 0;
-
 	bool _loaded = false;
 
 };
diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
index 56ec46ba2..001cb9ed2 100644
--- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
@@ -980,9 +980,15 @@ void TopBarWidget::updateControlsVisibility() {
 	const auto hasPollsMenu = _activeChat.key.peer()
 		&& _activeChat.key.peer()->canSendPolls();
 	const auto hasMenu = !_activeChat.key.folder()
-		&& ((section == Section::Scheduled || section == Section::Replies)
+		&& (section == Section::History
+			? true
+			: (section == Section::Scheduled)
 			? hasPollsMenu
-			: historyMode);
+			: (section == Section::Replies)
+			? (hasPollsMenu || _activeChat.key.topic())
+			: (section == Section::ChatsList)
+			? (_activeChat.key.peer() && _activeChat.key.peer()->isForum())
+			: false);
 	updateSearchVisibility();
 	_menuToggle->setVisible(hasMenu && !_chooseForReportReason);
 	_infoToggle->setVisible(historyMode
diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp
index 39bf8491e..f610c2bfe 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp
@@ -90,7 +90,7 @@ void Widget::setInnerFocus() {
 
 rpl::producer<QString> Widget::title() {
 	if (const auto topic = controller()->key().topic()) {
-		return rpl::single(u"Topic Info"_q); // #TODO lang-forum
+		return tr::lng_info_topic_title();
 	}
 	const auto peer = controller()->key().peer();
 	if (const auto user = peer->asUser()) {
diff --git a/Telegram/SourceFiles/ui/empty_userpic.cpp b/Telegram/SourceFiles/ui/empty_userpic.cpp
index 992e18648..e7190c8db 100644
--- a/Telegram/SourceFiles/ui/empty_userpic.cpp
+++ b/Telegram/SourceFiles/ui/empty_userpic.cpp
@@ -224,19 +224,28 @@ void EmptyUserpic::paint(
 		int y,
 		int outerWidth,
 		int size) const {
-	paint(p, x, y, outerWidth, size, [&p, x, y, size] {
+	paint(p, x, y, outerWidth, size, [&] {
 		p.drawEllipse(x, y, size, size);
 	});
 }
 
-void EmptyUserpic::paintRounded(QPainter &p, int x, int y, int outerWidth, int size) const {
-	paint(p, x, y, outerWidth, size, [&p, x, y, size] {
-		p.drawRoundedRect(x, y, size, size, st::roundRadiusSmall, st::roundRadiusSmall);
+void EmptyUserpic::paintRounded(
+		QPainter &p,
+		int x,
+		int y,
+		int outerWidth,
+		int size,
+		int radius) const {
+	if (!radius) {
+		radius = st::roundRadiusSmall;
+	}
+	paint(p, x, y, outerWidth, size, [&] {
+		p.drawRoundedRect(x, y, size, size, radius, radius);
 	});
 }
 
 void EmptyUserpic::paintSquare(QPainter &p, int x, int y, int outerWidth, int size) const {
-	paint(p, x, y, outerWidth, size, [&p, x, y, size] {
+	paint(p, x, y, outerWidth, size, [&] {
 		p.fillRect(x, y, size, size, p.brush());
 	});
 }
diff --git a/Telegram/SourceFiles/ui/empty_userpic.h b/Telegram/SourceFiles/ui/empty_userpic.h
index ea57e4d94..c990a4e44 100644
--- a/Telegram/SourceFiles/ui/empty_userpic.h
+++ b/Telegram/SourceFiles/ui/empty_userpic.h
@@ -26,7 +26,8 @@ public:
 		int x,
 		int y,
 		int outerWidth,
-		int size) const;
+		int size,
+		int radius = 0) const;
 	void paintSquare(
 		QPainter &p,
 		int x,
diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style
index 10a503c13..a1c421d44 100644
--- a/Telegram/SourceFiles/ui/menu_icons.style
+++ b/Telegram/SourceFiles/ui/menu_icons.style
@@ -88,6 +88,7 @@ menuIconPhoto: icon {{ "menu/image", menuIconColor }};
 menuIconAddToFolder: icon {{ "menu/add_to_folder", menuIconColor }};
 menuIconLeave: icon {{ "menu/leave", menuIconColor }};
 menuIconGiftPremium: icon {{ "menu/gift_premium", menuIconColor }};
+menuIconSearch: icon {{ "menu/search", menuIconColor }};
 
 menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }};
 menuIconTTLAnyTextPosition: point(11px, 22px);
diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp
index 61061e111..1d3181c2a 100644
--- a/Telegram/SourceFiles/window/window_peer_menu.cpp
+++ b/Telegram/SourceFiles/window/window_peer_menu.cpp
@@ -63,7 +63,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_channel.h"
 #include "data/data_chat.h"
 #include "data/data_drafts.h"
+#include "data/data_forum.h"
 #include "data/data_forum_topic.h"
+#include "data/data_replies_list.h"
 #include "data/data_user.h"
 #include "data/data_scheduled_messages.h"
 #include "data/data_histories.h"
@@ -80,6 +82,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include <QAction>
 
 namespace Window {
+namespace {
+
+constexpr auto kTopicsSearchMinCount = 10;
+
+} // namespace
 
 const char kOptionViewProfileInChatsListContextMenu[] =
 	"view-profile-in-chats-list-context-menu";
@@ -104,20 +111,31 @@ void SetActionText(not_null<QAction*> action, rpl::producer<QString> &&text) {
 	}, *lifetime);
 }
 
-[[nodiscard]] bool IsUnreadHistory(not_null<History*> history) {
-	return (history->chatListUnreadCount() > 0)
-		|| (history->chatListUnreadMark());
+[[nodiscard]] bool IsUnreadThread(not_null<Data::Thread*> thread) {
+	return (thread->chatListUnreadCount() > 0)
+		|| (thread->chatListUnreadMark());
 }
 
-void MarkAsReadHistory(not_null<History*> history) {
-	const auto read = [&](not_null<History*> history) {
-		if (IsUnreadHistory(history)) {
-			history->peer->owner().histories().readInbox(history);
-		}
+void MarkAsReadThread(not_null<Data::Thread*> thread) {
+	const auto readHistory = [&](not_null<History*> history) {
+		history->owner().histories().readInbox(history);
 	};
-	read(history);
-	if (const auto migrated = history->migrateSibling()) {
-		read(migrated);
+	if (!IsUnreadThread(thread)) {
+		return;
+	} else if (const auto history = thread->asHistory()) {
+		if (const auto forum = history->peer->forum()) {
+			forum->enumerateTopics([](
+					not_null<Data::ForumTopic*> topic) {
+				MarkAsReadThread(topic);
+			});
+		} else {
+			readHistory(history);
+			if (const auto migrated = history->migrateSibling()) {
+				readHistory(migrated);
+			}
+		}
+	} else if (const auto topic = thread->asTopic()) {
+		topic->replies()->readTill(topic->lastKnownServerMessageId());
 	}
 }
 
@@ -128,7 +146,7 @@ void MarkAsReadChatList(not_null<Dialogs::MainList*> list) {
 			mark.push_back(history);
 		}
 	}
-	ranges::for_each(mark, MarkAsReadHistory);
+	ranges::for_each(mark, MarkAsReadThread);
 }
 
 void PeerMenuAddMuteSubmenuAction(
@@ -188,6 +206,7 @@ private:
 	void fillRepliesActions();
 	void fillScheduledActions();
 	void fillArchiveActions();
+	void fillContextMenuActions();
 
 	void addHidePromotion();
 	void addTogglePin();
@@ -200,6 +219,7 @@ private:
 	void addClearHistory();
 	void addDeleteChat();
 	void addLeaveChat();
+	void addJoinChat();
 	void addManageTopic();
 	void addManageChat();
 	void addCreatePoll();
@@ -216,10 +236,13 @@ private:
 	void addDeleteContact();
 	void addTTLSubmenu(bool addSeparator);
 	void addGiftPremium();
+	void addCreateTopic();
+	void addSearchTopics();
 
 	not_null<SessionController*> _controller;
 	Dialogs::EntryState _request;
 	Data::Thread *_thread = nullptr;
+	Data::ForumTopic *_topic = nullptr;
 	PeerData *_peer = nullptr;
 	Data::Folder *_folder = nullptr;
 	const PeerMenuCallback &_addAction;
@@ -340,6 +363,7 @@ Filler::Filler(
 : _controller(controller)
 , _request(request)
 , _thread(request.key.thread())
+, _topic(request.key.topic())
 , _peer(request.key.peer())
 , _folder(request.key.folder())
 , _addAction(addAction) {
@@ -347,7 +371,8 @@ Filler::Filler(
 
 void Filler::addHidePromotion() {
 	const auto history = _request.key.history();
-	if (!history
+	if (_topic
+		|| !history
 		|| !history->useTopPromotion()
 		|| history->topPromotionType().isEmpty()) {
 		return;
@@ -361,14 +386,14 @@ void Filler::addHidePromotion() {
 }
 
 void Filler::addTogglePin() {
-	if (!_peer) {
+	if (!_peer || _topic) {
 		// #TODO forum pinned
 		return;
 	}
 	const auto controller = _controller;
 	const auto filterId = _request.filterId;
 	const auto peer = _peer;
-	const auto history = peer->owner().historyLoaded(peer);
+	const auto history = _request.key.history();
 	if (!history || history->fixedOnTopIndex()) {
 		return;
 	}
@@ -435,7 +460,7 @@ void Filler::addInfo() {
 	const auto controller = _controller;
 	const auto weak = base::make_weak(_thread);
 	const auto text = _thread->asTopic()
-		? u"View Topic Info"_q // #TODO lang-forum
+		? tr::lng_context_view_topic(tr::now)
 		: (_peer->isChat() || _peer->isMegagroup())
 		? tr::lng_context_view_group(tr::now)
 		: _peer->isUser()
@@ -451,7 +476,7 @@ void Filler::addInfo() {
 void Filler::addToggleFolder() {
 	const auto controller = _controller;
 	const auto history = _request.key.history();
-	if (!history || !history->owner().chatsFilters().has()) {
+	if (_topic || !history || !history->owner().chatsFilters().has()) {
 		return;
 	}
 	_addAction(PeerMenuCallback::Args{
@@ -466,38 +491,37 @@ void Filler::addToggleFolder() {
 
 void Filler::addToggleUnreadMark() {
 	const auto peer = _peer;
-	const auto history = peer->owner().history(peer);
-	const auto label = [=] {
-		return IsUnreadHistory(history)
-			? tr::lng_context_mark_read(tr::now)
-			: tr::lng_context_mark_unread(tr::now);
-	};
-	auto action = _addAction(label(), [=] {
-		const auto markAsRead = IsUnreadHistory(history);
-		if (markAsRead) {
-			MarkAsReadHistory(history);
-		} else {
-			peer->owner().histories().changeDialogUnreadMark(
-				history,
-				!markAsRead);
+	const auto history = _request.key.history();
+	if (!_thread) {
+		return;
+	}
+	const auto unread = IsUnreadThread(_thread);
+	if (_thread->asTopic() && !unread) {
+		return;
+	}
+	const auto weak = base::make_weak(_thread);
+	const auto label = unread
+		? tr::lng_context_mark_read(tr::now)
+		: tr::lng_context_mark_unread(tr::now);
+	_addAction(label, [=] {
+		const auto thread = weak.get();
+		if (!thread) {
+			return;
 		}
-	}, (IsUnreadHistory(history)
-		? &st::menuIconMarkRead
-		: &st::menuIconMarkUnread));
-
-	auto actionText = history->session().changes().historyUpdates(
-		history,
-		Data::HistoryUpdate::Flag::UnreadView
-	) | rpl::map(label);
-	SetActionText(action, std::move(actionText));
+		if (unread) {
+			MarkAsReadThread(thread);
+		} else if (history) {
+			peer->owner().histories().changeDialogUnreadMark(history, true);
+		}
+	}, (unread ? &st::menuIconMarkRead : &st::menuIconMarkUnread));
 }
 
 void Filler::addToggleArchive() {
-	if (!_peer) {
+	if (!_peer || _topic) {
 		return;
 	}
 	const auto peer = _peer;
-	const auto history = peer->owner().historyLoaded(peer);
+	const auto history = _request.key.history();
 	if (history && history->useTopPromotion()) {
 		return;
 	} else if (peer->isNotificationsUser() || peer->isSelf()) {
@@ -529,6 +553,9 @@ void Filler::addToggleArchive() {
 }
 
 void Filler::addClearHistory() {
+	if (_topic) {
+		return;
+	}
 	const auto channel = _peer->asChannel();
 	const auto isGroup = _peer->isChat() || _peer->isMegagroup();
 	if (channel) {
@@ -546,7 +573,7 @@ void Filler::addClearHistory() {
 }
 
 void Filler::addDeleteChat() {
-	if (_peer->isChannel()) {
+	if (_topic || _peer->isChannel()) {
 		return;
 	}
 	_addAction({
@@ -561,7 +588,7 @@ void Filler::addDeleteChat() {
 
 void Filler::addLeaveChat() {
 	const auto channel = _peer->asChannel();
-	if (_thread->asTopic() || !channel || !channel->amIn()) {
+	if (_topic || !channel || !channel->amIn()) {
 		return;
 	}
 	_addAction({
@@ -574,6 +601,19 @@ void Filler::addLeaveChat() {
 	});
 }
 
+void Filler::addJoinChat() {
+	const auto channel = _peer->asChannel();
+	if (_topic || !channel || channel->amIn()) {
+		return;
+	}
+	const auto label = _peer->isMegagroup()
+		? tr::lng_profile_join_group(tr::now)
+		: tr::lng_profile_join_channel(tr::now);
+	_addAction(label, [=] {
+		channel->session().api().joinChannel(channel);
+	}, &st::menuIconAddToFolder);
+}
+
 void Filler::addBlockUser() {
 	const auto user = _peer->asUser();
 	if (!user
@@ -766,11 +806,10 @@ void Filler::addManageTopic() {
 	if (!topic) {
 		return;
 	}
-	// #TODO lang-forum
 	const auto history = topic->history();
 	const auto rootId = topic->rootId();
 	const auto navigation = _controller;
-	_addAction(u"Edit topic"_q, [=] {
+	_addAction(tr::lng_forum_topic_edit(tr::now), [=] {
 		navigation->show(
 			Box(EditForumTopicBox, navigation, history, rootId));
 	}, &st::menuIconEdit);
@@ -885,11 +924,53 @@ void Filler::fill() {
 	case Section::Profile: fillProfileActions(); break;
 	case Section::Replies: fillRepliesActions(); break;
 	case Section::Scheduled: fillScheduledActions(); break;
+	case Section::ContextMenu: fillContextMenuActions(); break;
 	default: Unexpected("_request.section in Filler::fill.");
 	}
 }
 
+void Filler::addCreateTopic() {
+	if (!_peer || !_peer->canCreateTopics()) {
+		return;
+	}
+	const auto peer = _peer;
+	const auto controller = _controller;
+	_addAction(tr::lng_forum_create_topic(tr::now), [=] {
+		if (const auto forum = peer->forum()) {
+			controller->show(Box(
+				NewForumTopicBox,
+				controller,
+				forum->history()));
+		}
+	}, &st::menuIconDiscussion);
+	_addAction(PeerMenuCallback::Args{ .isSeparator = true });
+}
+
+void Filler::addSearchTopics() {
+	_addAction(tr::lng_dlg_filter(tr::now), [=] {
+
+	}, &st::menuIconSearch);
+}
+
 void Filler::fillChatsListActions() {
+	if (!_peer || !_peer->isForum()) {
+		return;
+	}
+	addCreateTopic();
+	addInfo();
+	addNewMembers();
+	const auto &all = _peer->forum()->topicsList()->indexed()->all();
+	if (all.size() > kTopicsSearchMinCount) {
+		addSearchTopics();
+	}
+	if (_peer->asChannel()->amIn()) {
+		addLeaveChat();
+	} else {
+		addJoinChat();
+	}
+}
+
+void Filler::fillContextMenuActions() {
 	addHidePromotion();
 	addToggleArchive();
 	addTogglePin();