From 88d1a502a5693ca39f5d87f790ce185ca7c81fa6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 26 Oct 2022 13:30:10 +0400
Subject: [PATCH] Implement search in topics / forum messages.

---
 .../SourceFiles/api/api_messages_search.cpp   |   4 +-
 Telegram/SourceFiles/config.h                 |   4 -
 .../dialogs/dialogs_inner_widget.cpp          |  14 +-
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 261 ++++++++++++------
 Telegram/SourceFiles/dialogs/dialogs_widget.h |  11 +-
 .../view/history_view_top_bar_widget.cpp      | 130 ++++++++-
 .../view/history_view_top_bar_widget.h        |  26 +-
 .../SourceFiles/window/window_peer_menu.cpp   |  10 +-
 8 files changed, 357 insertions(+), 103 deletions(-)

diff --git a/Telegram/SourceFiles/api/api_messages_search.cpp b/Telegram/SourceFiles/api/api_messages_search.cpp
index 85e7076fb..d6c7c3029 100644
--- a/Telegram/SourceFiles/api/api_messages_search.cpp
+++ b/Telegram/SourceFiles/api/api_messages_search.cpp
@@ -19,6 +19,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Api {
 namespace {
 
+constexpr auto kSearchPerPage = 50;
+
 [[nodiscard]] MessageIdsList HistoryItemsFromTL(
 		not_null<Data::Session*> data,
 		const QVector<MTPMessage> &messages) {
@@ -94,7 +96,7 @@ void MessagesSearch::searchRequest() {
 			MTP_int(0), // max_date
 			MTP_int(_offsetId), // offset_id
 			MTP_int(0), // add_offset
-			MTP_int(SearchPerPage),
+			MTP_int(kSearchPerPage),
 			MTP_int(0), // max_id
 			MTP_int(0), // min_id
 			MTP_long(0) // hash
diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h
index 512ee948e..e034a118e 100644
--- a/Telegram/SourceFiles/config.h
+++ b/Telegram/SourceFiles/config.h
@@ -20,10 +20,6 @@ enum {
 	RecentInlineBotsLimit = 10,
 
 	AutoSearchTimeout = 900, // 0.9 secs
-	SearchPerPage = 50,
-	SearchManyPerPage = 100,
-	LinksOverviewPerPage = 12,
-	MediaOverviewStartPerPage = 5,
 
 	PreloadHeightsCount = 3, // when 3 screens to scroll left make a preload request
 
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index d3794d7ef..74cad5fde 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -2011,12 +2011,16 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) {
 					end(results));
 			};
 			if (!_searchInChat && !words.isEmpty()) {
-				append(session().data().chatsList()->indexed());
-				const auto id = Data::Folder::kId;
-				if (const auto folder = session().data().folderLoaded(id)) {
-					append(folder->chatsList()->indexed());
+				if (_openedForum) {
+					append(_openedForum->topicsList()->indexed());
+				} else {
+					append(session().data().chatsList()->indexed());
+					const auto id = Data::Folder::kId;
+					if (const auto add = session().data().folderLoaded(id)) {
+						append(add->chatsList()->indexed());
+					}
+					append(session().data().contactsNoChatsList());
 				}
-				append(session().data().contactsNoChatsList());
 			}
 			refresh(true);
 		}
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index fdffb40e7..d4c359870 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/event_filter.h"
 #include "core/application.h"
 #include "core/update_checker.h"
+#include "core/shortcuts.h"
 #include "boxes/peer_list_box.h"
 #include "boxes/peers/edit_participants_box.h"
 #include "window/window_adaptive.h"
@@ -72,8 +73,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Dialogs {
 namespace {
 
-QString SwitchToChooseFromQuery() {
-	return qsl("from:");
+constexpr auto kSearchPerPage = 50;
+
+[[nodiscard]] QString SwitchToChooseFromQuery() {
+	return u"from:"_q;
 }
 
 } // namespace
@@ -296,12 +299,12 @@ Widget::Widget(
 		Ui::PostponeCall(this, [=] { listScrollUpdated(); });
 	}, lifetime());
 
-	QObject::connect(_filter, &Ui::InputField::cancelled, [=] {
-		escape();
-	});
 	QObject::connect(_filter, &Ui::InputField::changed, [=] {
 		applyFilterUpdate();
 	});
+	QObject::connect(_filter, &Ui::InputField::submitted, [=] {
+		submit();
+	});
 	QObject::connect(
 		_filter->rawTextEdit().get(),
 		&QTextEdit::cursorPositionChanged,
@@ -339,6 +342,7 @@ Widget::Widget(
 	});
 
 	setupMainMenuToggle();
+	setupShortcuts();
 
 	_searchForNarrowFilters->setClickedCallback([=] { Ui::showChatsList(&session()); });
 
@@ -616,6 +620,29 @@ void Widget::setupMainMenuToggle() {
 	}, _mainMenuToggle->lifetime());
 }
 
+void Widget::setupShortcuts() {
+	Shortcuts::Requests(
+	) | rpl::filter([=] {
+		return isActiveWindow()
+			&& Ui::InFocusChain(this)
+			&& !Ui::isLayerShown()
+			&& !controller()->window().locked();
+	}) | rpl::start_with_next([=](not_null<Shortcuts::Request*> request) {
+		using Command = Shortcuts::Command;
+
+		if (controller()->selectingPeer()) {
+			return;
+		}
+		if (_openedForum && !controller()->activeChatCurrent()) {
+			request->check(Command::Search) && request->handle([=] {
+				const auto history = _openedForum->forum()->history();
+				controller()->content()->searchInChat(history);
+				return true;
+			});
+		}
+	}, lifetime());
+}
+
 void Widget::fullSearchRefreshOn(rpl::producer<> events) {
 	std::move(
 		events
@@ -642,14 +669,14 @@ void Widget::updateControlsVisibility(bool fast) {
 		_forwardCancel->show();
 	}
 	if ((_openedFolder || _openedForum) && _filter->hasFocus()) {
-		setFocus();
+		setInnerFocus();
 	}
 	if (_updateTelegram) {
 		_updateTelegram->show();
 	}
 	_searchControls->setVisible(!_openedFolder && !_openedForum);
 	if (_openedFolder || _openedForum) {
-		_folderTopBar->show();
+		_subsectionTopBar->show();
 		if (_forumTopShadow) {
 			_forumTopShadow->show();
 		}
@@ -694,6 +721,7 @@ void Widget::changeOpenedSubsection(
 	change();
 	refreshTopBars();
 	updateControlsVisibility(true);
+	_peerSearchRequest = 0;
 	if (animated == anim::type::normal) {
 		_connecting->setForceHidden(true);
 		_cacheOver = grabForFolderSlideAnimation();
@@ -721,23 +749,42 @@ void Widget::changeOpenedForum(ChannelData *forum, anim::type animated) {
 
 void Widget::refreshTopBars() {
 	if (_openedFolder || _openedForum) {
-		if (!_folderTopBar) {
-			_folderTopBar.create(this, controller());
+		if (!_subsectionTopBar) {
+			_subsectionTopBar.create(this, controller());
+			_subsectionTopBar->searchCancelled(
+			) | rpl::start_with_next([=] {
+				escape();
+			}, _subsectionTopBar->lifetime());
+			_subsectionTopBar->searchSubmitted(
+			) | rpl::start_with_next([=] {
+				submit();
+			}, _subsectionTopBar->lifetime());
+			_subsectionTopBar->searchQuery(
+			) | rpl::start_with_next([=](QString query) {
+				applyFilterUpdate();
+			}, lifetime());
 			updateControlsGeometry();
 		}
 		const auto history = _openedForum
 			? session().data().history(_openedForum).get()
 			: nullptr;
-		_folderTopBar->setActiveChat(
+		_subsectionTopBar->setActiveChat(
 			HistoryView::TopBarWidget::ActiveChat{
 				.key = (_openedForum
 					? Dialogs::Key(history)
 					: Dialogs::Key(_openedFolder)),
 				.section = Dialogs::EntryState::Section::ChatsList,
 			}, history ? history->sendActionPainter().get() : nullptr);
-	} else {
-		_folderTopBar.destroy();
+		if (_forumSearchRequested) {
+			_subsectionTopBar->toggleSearch(true, anim::type::instant);
+		}
+	} else if (_subsectionTopBar) {
+		if (_subsectionTopBar->searchHasFocus()) {
+			setFocus();
+		}
+		_subsectionTopBar.destroy();
 	}
+	_forumSearchRequested = false;
 	if (_openedForum) {
 		_openedForum->updateFull();
 
@@ -857,10 +904,10 @@ void Widget::checkUpdateStatus() {
 }
 
 void Widget::setInnerFocus() {
-	if (_openedFolder || _openedForum) {
-		setFocus();
-	} else {
+	if (!_openedFolder && !_openedForum) {
 		_filter->setFocus();
+	} else if (!_subsectionTopBar->searchSetFocus()) {
+		setFocus();
 	}
 }
 
@@ -868,7 +915,7 @@ void Widget::jumpToTop(bool belowPinned) {
 	if (session().supportMode()) {
 		return;
 	}
-	if ((_filter->getLastText().trimmed().isEmpty() && !_searchInChat)) {
+	if ((currentSearchQuery().trimmed().isEmpty() && !_searchInChat)) {
 		auto to = 0;
 		if (belowPinned) {
 			const auto list = _openedForum
@@ -981,8 +1028,8 @@ void Widget::startSlideAnimation() {
 		_forwardCancel->hide();
 	}
 	_searchControls->hide();
-	if (_folderTopBar) {
-		_folderTopBar->hide();
+	if (_subsectionTopBar) {
+		_subsectionTopBar->hide();
 	}
 	if (_forumTopShadow) {
 		_forumTopShadow->hide();
@@ -1018,19 +1065,20 @@ void Widget::animationCallback() {
 
 		updateControlsVisibility(true);
 
-		if (!_filter->hasFocus()) {
+		if ((!_subsectionTopBar || !_subsectionTopBar->searchHasFocus())
+			&& !_filter->hasFocus()) {
 			controller()->widget()->setInnerFocus();
 		}
 	}
 }
 
 void Widget::escape() {
-	if (controller()->openedFolder().current()) {
-		controller()->closeFolder();
-	} else if (controller()->openedForum().current()) {
-		controller()->closeForum();
-	} else if (!cancelSearch()) {
-		if (controller()->activeChatEntryCurrent().key) {
+	if (!cancelSearch()) {
+		if (controller()->openedFolder().current()) {
+			controller()->closeFolder();
+		} else if (controller()->openedForum().current()) {
+			controller()->closeForum();
+		} else if (controller()->activeChatEntryCurrent().key) {
 			controller()->content()->dialogsCancelled();
 		} else {
 			const auto filters = &session().data().chatsFilters();
@@ -1047,6 +1095,21 @@ void Widget::escape() {
 	}
 }
 
+void Widget::submit() {
+	if (_inner->chooseRow()) {
+		return;
+	}
+	const auto state = _inner->state();
+	if (state == WidgetState::Default
+		|| (state == WidgetState::Filtered
+			&& (!_inner->waitingForSearch() || _inner->hasFilteredResults()))) {
+		_inner->selectSkip(1);
+		_inner->chooseRow();
+	} else {
+		searchMessages();
+	}
+}
+
 void Widget::refreshLoadMoreButton(bool mayBlock, bool isBlocked) {
 	if (!mayBlock) {
 		if (_loadMoreChats) {
@@ -1084,7 +1147,7 @@ void Widget::loadMoreBlockedByDate() {
 
 bool Widget::searchMessages(bool searchCache) {
 	auto result = false;
-	auto q = _filter->getLastText().trimmed();
+	auto q = currentSearchQuery().trimmed();
 	if (q.isEmpty() && !_searchFromAuthor) {
 		cancelSearchRequest();
 		_api.request(base::take(_peerSearchRequest)).cancel();
@@ -1105,9 +1168,9 @@ bool Widget::searchMessages(bool searchCache) {
 			_searchFull = _searchFullMigrated = false;
 			cancelSearchRequest();
 			searchReceived(
-				_searchInChat
+				((_searchInChat || _openedForum)
 					? SearchRequestType::PeerFromStart
-					: SearchRequestType::FromStart,
+					: SearchRequestType::FromStart),
 				i->second,
 				0);
 			result = true;
@@ -1118,29 +1181,29 @@ bool Widget::searchMessages(bool searchCache) {
 		_searchNextRate = 0;
 		_searchFull = _searchFullMigrated = false;
 		cancelSearchRequest();
-		if (const auto peer = _searchInChat.peer()) {
+		if (const auto peer = searchInPeer()) {
+			const auto topic = searchInTopic();
 			auto &histories = session().data().histories();
 			const auto type = Data::Histories::RequestType::History;
 			const auto history = session().data().history(peer);
 			_searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) {
 				const auto type = SearchRequestType::PeerFromStart;
-				const auto flags = _searchQueryFrom
-					? MTP_flags(MTPmessages_Search::Flag::f_from_id)
-					: MTP_flags(0);
+				using Flag = MTPmessages_Search::Flag;
 				_searchRequest = session().api().request(MTPmessages_Search(
-					flags,
+					MTP_flags((topic ? Flag::f_top_msg_id : Flag())
+						| (_searchQueryFrom ? Flag::f_from_id : Flag())),
 					peer->input,
 					MTP_string(_searchQuery),
 					(_searchQueryFrom
 						? _searchQueryFrom->input
 						: MTP_inputPeerEmpty()),
-					MTPint(), // top_msg_id
+					MTP_int(topic ? topic->rootId() : 0),
 					MTP_inputMessagesFilterEmpty(),
 					MTP_int(0), // min_date
 					MTP_int(0), // max_date
 					MTP_int(0), // offset_id
 					MTP_int(0), // add_offset
-					MTP_int(SearchPerPage),
+					MTP_int(kSearchPerPage),
 					MTP_int(0), // max_id
 					MTP_int(0), // min_id
 					MTP_long(0) // hash
@@ -1172,7 +1235,7 @@ bool Widget::searchMessages(bool searchCache) {
 				MTP_int(0),
 				MTP_inputPeerEmpty(),
 				MTP_int(0),
-				MTP_int(SearchPerPage)
+				MTP_int(kSearchPerPage)
 			)).done([=](const MTPmessages_Messages &result) {
 				searchReceived(type, result, _searchRequest);
 			}).fail([=](const MTP::Error &error) {
@@ -1219,7 +1282,7 @@ bool Widget::searchMessages(bool searchCache) {
 }
 
 bool Widget::searchForPeersRequired(const QString &query) const {
-	if (_searchInChat || query.isEmpty()) {
+	if (_searchInChat || _openedForum || query.isEmpty()) {
 		return false;
 	}
 	return (query[0] != '#');
@@ -1238,11 +1301,17 @@ void Widget::showMainMenu() {
 void Widget::searchMessages(
 		const QString &query,
 		Key inChat) {
-	auto inChatChanged = [&] {
-		if (inChat == _searchInChat) {
+	const auto inChatChanged = [&] {
+		const auto inPeer = inChat.peer();
+		const auto inTopic = inChat.topic();
+		if (!inTopic && inPeer == _openedForum) {
+			return false;
+		} else if ((inTopic || (inPeer && !inPeer->isForum()))
+			&& (inChat == _searchInChat)) {
 			return false;
 		} else if (const auto inPeer = inChat.peer()) {
-			if (inPeer->migrateTo() == _searchInChat.peer()) {
+			if (inPeer->migrateTo() == _searchInChat.peer()
+				&& !_searchInChat.topic()) {
 				return false;
 			}
 		}
@@ -1269,19 +1338,19 @@ void Widget::searchMore() {
 	if (!_searchFull) {
 		auto offsetPeer = _inner->lastSearchPeer();
 		auto offsetId = _inner->lastSearchId();
-		if (const auto peer = _searchInChat.peer()) {
+		if (const auto peer = searchInPeer()) {
 			auto &histories = session().data().histories();
+			const auto topic = searchInTopic();
 			const auto type = Data::Histories::RequestType::History;
 			const auto history = session().data().history(peer);
 			_searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) {
 				const auto type = offsetId
 					? SearchRequestType::PeerFromOffset
 					: SearchRequestType::PeerFromStart;
-				auto flags = _searchQueryFrom
-					? MTP_flags(MTPmessages_Search::Flag::f_from_id)
-					: MTP_flags(0);
+				using Flag = MTPmessages_Search::Flag;
 				_searchRequest = session().api().request(MTPmessages_Search(
-					flags,
+					MTP_flags((topic ? Flag::f_top_msg_id : Flag())
+						| (_searchQueryFrom ? Flag::f_from_id : Flag())),
 					peer->input,
 					MTP_string(_searchQuery),
 					(_searchQueryFrom
@@ -1293,7 +1362,7 @@ void Widget::searchMore() {
 					MTP_int(0), // max_date
 					MTP_int(offsetId),
 					MTP_int(0), // add_offset
-					MTP_int(SearchPerPage),
+					MTP_int(kSearchPerPage),
 					MTP_int(0), // max_id
 					MTP_int(0), // min_id
 					MTP_long(0) // hash
@@ -1331,7 +1400,7 @@ void Widget::searchMore() {
 					? offsetPeer->input
 					: MTP_inputPeerEmpty(),
 				MTP_int(offsetId),
-				MTP_int(SearchPerPage)
+				MTP_int(kSearchPerPage)
 			)).done([=](const MTPmessages_Messages &result) {
 				searchReceived(type, result, _searchRequest);
 			}).fail([=](const MTP::Error &error) {
@@ -1366,7 +1435,7 @@ void Widget::searchMore() {
 				MTP_int(0), // max_date
 				MTP_int(offsetMigratedId),
 				MTP_int(0), // add_offset
-				MTP_int(SearchPerPage),
+				MTP_int(kSearchPerPage),
 				MTP_int(0), // max_id
 				MTP_int(0), // min_id
 				MTP_long(0) // hash
@@ -1451,7 +1520,7 @@ void Widget::searchReceived(
 
 	case mtpc_messages_channelMessages: {
 		auto &d = result.c_messages_channelMessages();
-		if (const auto peer = _searchInChat.peer()) {
+		if (const auto peer = _openedForum ? _openedForum : _searchInChat.peer()) {
 			if (const auto channel = peer->asChannel()) {
 				channel->ptsReceived(d.vpts().v);
 			} else {
@@ -1642,7 +1711,7 @@ void Widget::applyFilterUpdate(bool force) {
 		return;
 	}
 
-	auto filterText = _filter->getLastText();
+	const auto filterText = currentSearchQuery();
 	_inner->applyFilterUpdate(filterText, force);
 	if (filterText.isEmpty() && !_searchFromAuthor) {
 		clearSearchCache();
@@ -1677,6 +1746,16 @@ void Widget::searchInChat(Key chat) {
 }
 
 void Widget::setSearchInChat(Key chat, PeerData *from) {
+	const auto peer = chat.peer();
+	if (const auto forum = peer ? peer->forum() : nullptr) {
+		if (controller()->openedForum().current() == peer) {
+			_subsectionTopBar->toggleSearch(true, anim::type::normal);
+		} else {
+			_forumSearchRequested = true;
+			controller()->openForum(forum->channel());
+		}
+		return;
+	}
 	if (chat.folder()) {
 		chat = Key();
 	}
@@ -1685,7 +1764,9 @@ void Widget::setSearchInChat(Key chat, PeerData *from) {
 		if (const auto migrateTo = peer->migrateTo()) {
 			return setSearchInChat(peer->owner().history(migrateTo), from);
 		} else if (const auto migrateFrom = peer->migrateFrom()) {
-			_searchInMigrated = peer->owner().history(migrateFrom);
+			if (!chat.topic()) {
+				_searchInMigrated = peer->owner().history(migrateFrom);
+			}
 		}
 	}
 	const auto searchInPeerUpdated = (_searchInChat != chat);
@@ -1816,7 +1897,7 @@ void Widget::updateLoadMoreChatsVisibility() {
 	}
 	const auto hidden = (_openedFolder != nullptr)
 		|| (_openedForum != nullptr)
-		|| !_filter->getLastText().isEmpty();
+		|| !currentSearchQuery().isEmpty();
 	if (_loadMoreChats->isHidden() != hidden) {
 		_loadMoreChats->setVisible(!hidden);
 		updateControlsGeometry();
@@ -1866,8 +1947,8 @@ void Widget::updateControlsGeometry() {
 	auto filterWidth = qMax(width(), st::columnMinimalWidthLeft) - filterLeft - filterRight;
 	auto filterAreaHeight = st::topBarHeight;
 	_searchControls->setGeometry(0, filterAreaTop, width(), filterAreaHeight);
-	if (_folderTopBar) {
-		_folderTopBar->setGeometry(_searchControls->geometry());
+	if (_subsectionTopBar) {
+		_subsectionTopBar->setGeometry(_searchControls->geometry());
 	}
 
 	auto filterTop = (filterAreaHeight - _filter->height()) / 2;
@@ -1983,25 +2064,16 @@ RowDescriptor Widget::resolveChatPrevious(RowDescriptor from) const {
 
 void Widget::keyPressEvent(QKeyEvent *e) {
 	if (e->key() == Qt::Key_Escape) {
-		if (_openedForum) {
-			controller()->closeForum();
-		} else if (_openedFolder) {
-			controller()->closeFolder();
-		} else {
-			e->ignore();
-		}
+		escape();
+		//if (_openedForum) {
+		//	controller()->closeForum();
+		//} else if (_openedFolder) {
+		//	controller()->closeFolder();
+		//} else {
+		//	e->ignore();
+		//}
 	} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
-		if (!_inner->chooseRow()) {
-			const auto state = _inner->state();
-			if (state == WidgetState::Default
-				|| (state == WidgetState::Filtered
-					&& (!_inner->waitingForSearch() ||  _inner->hasFilteredResults()))) {
-				_inner->selectSkip(1);
-				_inner->chooseRow();
-			} else {
-				searchMessages();
-			}
-		}
+		submit();
 	} else if (e->key() == Qt::Key_Down) {
 		_inner->selectSkip(1);
 	} else if (e->key() == Qt::Key_Up) {
@@ -2082,22 +2154,51 @@ void Widget::cancelSearchRequest() {
 		base::take(_searchInHistoryRequest));
 }
 
+PeerData *Widget::searchInPeer() const {
+	return _openedForum ? _openedForum : _searchInChat.peer();
+}
+
+Data::ForumTopic *Widget::searchInTopic() const {
+	return _searchInChat.topic();
+}
+
+QString Widget::currentSearchQuery() const {
+	return _subsectionTopBar
+		? _subsectionTopBar->searchQueryCurrent()
+		: _filter->getLastText();
+}
+
+void Widget::clearSearchField() {
+	if (_subsectionTopBar) {
+		_subsectionTopBar->searchClear();
+	} else {
+		_filter->clear();
+	}
+}
+
 bool Widget::cancelSearch() {
-	bool clearing = !_filter->getLastText().isEmpty();
+	bool clearing = !currentSearchQuery().isEmpty();
 	cancelSearchRequest();
-	if (_searchInChat && !clearing) {
+	if (!clearing && _searchInChat) {
 		if (controller()->adaptive().isOneColumn()) {
-			if (const auto peer = _searchInChat.peer()) {
-				Ui::showPeerHistory(peer, ShowAtUnreadMsgId);
+			if (const auto topic = _searchInChat.topic()) {
+				//controller()->showTopic(topic); // #TODO forum search
+			} else if (const auto peer = _searchInChat.peer()) {
+				controller()->showPeer(peer, ShowAtUnreadMsgId);
 			} else {
 				Unexpected("Empty key in cancelSearch().");
 			}
 		}
 		setSearchInChat(Key());
 		clearing = true;
+	} else if (!clearing
+		&& _subsectionTopBar
+		&& _subsectionTopBar->toggleSearch(false, anim::type::normal)) {
+		setFocus();
+		return true;
 	}
 	_inner->clearFilter();
-	_filter->clear();
+	clearSearchField();
 	applyFilterUpdate();
 	return clearing;
 }
@@ -2108,8 +2209,10 @@ void Widget::cancelSearchInChat() {
 	if (_searchInChat) {
 		if (isOneColumn
 			&& !controller()->selectingPeer()
-			&& _filter->getLastText().trimmed().isEmpty()) {
-			if (const auto peer = _searchInChat.peer()) {
+			&& currentSearchQuery().trimmed().isEmpty()) {
+			if (const auto topic = _searchInChat.topic()) {
+				// #TODO forum search
+			} else if (const auto peer = _searchInChat.peer()) {
 				Ui::showPeerHistory(peer, ShowAtUnreadMsgId);
 			} else {
 				Unexpected("Empty key in cancelSearchInPeer().");
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index 2afb362e4..f6e975352 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -126,6 +126,8 @@ private:
 	void filterCursorMoved();
 	void completeHashtag(QString tag);
 
+	[[nodiscard]] QString currentSearchQuery() const;
+	void clearSearchField();
 	bool searchMessages(bool searchCache = false);
 	void needSearchMessages();
 
@@ -138,12 +140,16 @@ private:
 		const MTPcontacts_Found &result,
 		mtpRequestId requestId);
 	void escape();
+	void submit();
 	void cancelSearchRequest();
+	[[nodiscard]] PeerData *searchInPeer() const;
+	[[nodiscard]] Data::ForumTopic *searchInTopic() const;
 
 	void setupSupportMode();
 	void setupConnectingWidget();
 	void setupMainMenuToggle();
 	void setupDownloadBar();
+	void setupShortcuts();
 	bool searchForPeersRequired(const QString &query) const;
 	void setSearchInChat(Key chat, PeerData *from = nullptr);
 	void showCalendar();
@@ -192,7 +198,7 @@ private:
 
 	object_ptr<Ui::IconButton> _forwardCancel = { nullptr };
 	object_ptr<Ui::RpWidget> _searchControls;
-	object_ptr<HistoryView::TopBarWidget> _folderTopBar = { nullptr } ;
+	object_ptr<HistoryView::TopBarWidget> _subsectionTopBar = { nullptr } ;
 	object_ptr<Ui::IconButton> _mainMenuToggle;
 	object_ptr<Ui::IconButton> _searchForNarrowFilters;
 	object_ptr<Ui::InputField> _filter;
@@ -221,8 +227,9 @@ private:
 	ShowAnimation _showAnimationType = ShowAnimation::External;
 
 	Ui::Animations::Simple _scrollToTopShown;
-	bool _scrollToTopIsShown = false;
 	object_ptr<Ui::HistoryDownButton> _scrollToTop;
+	bool _scrollToTopIsShown = false;
+	bool _forumSearchRequested = false;
 
 	Data::Folder *_openedFolder = nullptr;
 	ChannelData *_openedForum = nullptr;
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 18b93a112..f540ace3d 100644
--- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/application.h"
 #include "core/core_settings.h"
 #include "ui/widgets/buttons.h"
+#include "ui/widgets/input_fields.h"
 #include "ui/widgets/popup_menu.h"
 #include "ui/widgets/menu/menu_add_action_callback_factory.h"
 #include "ui/effects/radial_animation.h"
@@ -426,12 +427,16 @@ void TopBarWidget::paintEvent(QPaintEvent *e) {
 	}
 	Painter p(this);
 
-	auto selectedButtonsTop = countSelectedButtonsTop(
+	const auto selectedButtonsTop = countSelectedButtonsTop(
 		_selectedShown.value(showSelectedActions() ? 1. : 0.));
+	const auto searchFieldTop = _searchField
+		? countSelectedButtonsTop(_searchShown.value(_searchMode ? 1. : 0.))
+		: -st::topBarHeight;
+	const auto slidingTop = std::max(selectedButtonsTop, searchFieldTop);
 
 	p.fillRect(QRect(0, 0, width(), st::topBarHeight), st::topBarBg);
-	if (selectedButtonsTop < 0) {
-		p.translate(0, selectedButtonsTop + st::topBarHeight);
+	if (slidingTop < 0) {
+		p.translate(0, slidingTop + st::topBarHeight);
 		paintTopBar(p);
 	}
 }
@@ -883,9 +888,19 @@ void TopBarWidget::updateControlsGeometry() {
 	if (!_activeChat.key) {
 		return;
 	}
-	auto hasSelected = showSelectedActions();
-	auto selectedButtonsTop = countSelectedButtonsTop(_selectedShown.value(hasSelected ? 1. : 0.));
-	auto otherButtonsTop = selectedButtonsTop + st::topBarHeight;
+	const auto hasSelected = showSelectedActions();
+	auto selectedButtonsTop = countSelectedButtonsTop(
+		_selectedShown.value(hasSelected ? 1. : 0.));
+	if (!_searchMode && !_searchShown.animating() && _searchField) {
+		_searchField.destroy();
+		_searchCancel.destroy();
+	}
+	auto searchFieldTop = _searchField
+		? countSelectedButtonsTop(_searchShown.value(_searchMode ? 1. : 0.))
+		: -st::topBarHeight;
+	const auto otherButtonsTop = std::max(selectedButtonsTop, searchFieldTop)
+		+ st::topBarHeight;
+	const auto backButtonTop = selectedButtonsTop + st::topBarHeight;
 	auto buttonsLeft = st::topBarActionSkip
 		+ (_controller->adaptive().isOneColumn() ? 0 : st::lineWidth);
 	auto buttonsWidth = (_forward->isHidden() ? 0 : _forward->contentWidth())
@@ -923,7 +938,7 @@ void TopBarWidget::updateControlsGeometry() {
 		_leftTaken = st::topBarArrowPadding.right();
 	} else {
 		_leftTaken = _narrowMode ? (width() - _back->width()) / 2 : 0;
-		_back->moveToLeft(_leftTaken, otherButtonsTop);
+		_back->moveToLeft(_leftTaken, backButtonTop);
 		_leftTaken += _back->width();
 	}
 	if (_info && !_info->isHidden()) {
@@ -934,6 +949,26 @@ void TopBarWidget::updateControlsGeometry() {
 		_leftTaken += st::normalFont->spacew;
 	}
 
+	if (_searchField) {
+		const auto fieldLeft = _leftTaken;
+		const auto fieldTop = searchFieldTop
+			+ (height() - _searchField->height()) / 2;
+		const auto fieldRight = st::dialogsFilterSkip
+			+ st::dialogsFilterPadding.x();
+		const auto fieldWidth = width() - fieldLeft - fieldRight;
+		_searchField->setGeometryToLeft(
+			fieldLeft,
+			fieldTop,
+			fieldWidth,
+			_searchField->height());
+
+		auto right = fieldLeft + fieldWidth;
+		_searchCancel->moveToLeft(
+			right - _searchCancel->width(),
+			_searchField->y());
+		right -= _searchCancel->width();
+	}
+
 	_rightTaken = 0;
 	_menuToggle->moveToRight(_rightTaken, otherButtonsTop);
 	if (_menuToggle->isHidden()) {
@@ -1146,9 +1181,86 @@ void TopBarWidget::showSelected(SelectedState state) {
 	}
 }
 
+bool TopBarWidget::toggleSearch(bool shown, anim::type animated) {
+	if (_searchMode == shown) {
+		if (animated == anim::type::instant) {
+			_searchShown.stop();
+		}
+		return false;
+	}
+	_searchMode = shown;
+	if (shown && !_searchField) {
+		_searchField.create(this, st::dialogsFilter, tr::lng_dlg_filter());
+		_searchField->setFocusPolicy(Qt::StrongFocus);
+		_searchField->customUpDown(true);
+		_searchField->show();
+		_searchCancel.create(this, st::dialogsCancelSearch);
+		_searchCancel->show(anim::type::instant);
+		_searchCancel->setClickedCallback([=] { _searchCancelled.fire({}); });
+		QObject::connect(_searchField, &Ui::InputField::submitted, [=] {
+			_searchSubmitted.fire({});
+		});
+		QObject::connect(_searchField, &Ui::InputField::changed, [=] {
+			_searchQuery = _searchField->getLastText();
+		});
+	} else {
+		Assert(_searchField != nullptr);
+	}
+	_searchQuery = shown ? _searchField->getLastText() : QString();
+	if (animated == anim::type::normal) {
+		_searchShown.start(
+			[=] { slideAnimationCallback(); },
+			shown ? 0. : 1.,
+			shown ? 1. : 0.,
+			st::slideWrapDuration,
+			anim::easeOutCirc);
+	} else {
+		_searchShown.stop();
+		slideAnimationCallback();
+	}
+	if (shown) {
+		_searchField->setFocusFast();
+	}
+	return true;
+}
+
+bool TopBarWidget::searchSetFocus() {
+	if (!_searchMode) {
+		return false;
+	}
+	_searchField->setFocus();
+	return true;
+}
+
+bool TopBarWidget::searchHasFocus() const {
+	return _searchMode && _searchField->hasFocus();
+}
+
+rpl::producer<> TopBarWidget::searchCancelled() const {
+	return _searchCancelled.events();
+}
+
+rpl::producer<> TopBarWidget::searchSubmitted() const {
+	return _searchSubmitted.events();
+}
+
+rpl::producer<QString> TopBarWidget::searchQuery() const {
+	return _searchQuery.value();
+}
+
+QString TopBarWidget::searchQueryCurrent() const {
+	return _searchQuery.current();
+}
+
+void TopBarWidget::searchClear() {
+	if (_searchMode) {
+		_searchField->clear();
+	}
+}
+
 void TopBarWidget::toggleSelectedControls(bool shown) {
 	_selectedShown.start(
-		[this] { selectedShowCallback(); },
+		[this] { slideAnimationCallback(); },
 		shown ? 0. : 1.,
 		shown ? 1. : 0.,
 		st::slideWrapDuration,
@@ -1159,7 +1271,7 @@ bool TopBarWidget::showSelectedActions() const {
 	return showSelectedState() && !_chooseForReportReason;
 }
 
-void TopBarWidget::selectedShowCallback() {
+void TopBarWidget::slideAnimationCallback() {
 	updateControlsGeometry();
 	update();
 }
diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h
index ee7bfe741..29333510b 100644
--- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h
+++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h
@@ -24,8 +24,12 @@ class RoundButton;
 class IconButton;
 class PopupMenu;
 class UnreadBadge;
+class InputField;
+class CrossButton;
 class InfiniteRadialAnimation;
 enum class ReportReason;
+template <typename Widget>
+class FadeWrapScaled;
 } // namespace Ui
 
 namespace Window {
@@ -72,6 +76,15 @@ public:
 	void showChooseMessagesForReport(Ui::ReportReason reason);
 	void clearChooseMessagesForReport();
 
+	bool toggleSearch(bool shown, anim::type animated);
+	bool searchSetFocus();
+	[[nodiscard]] bool searchHasFocus() const;
+	[[nodiscard]] rpl::producer<> searchCancelled() const;
+	[[nodiscard]] rpl::producer<> searchSubmitted() const;
+	[[nodiscard]] rpl::producer<QString> searchQuery() const;
+	[[nodiscard]] QString searchQueryCurrent() const;
+	void searchClear();
+
 	[[nodiscard]] rpl::producer<> forwardSelectionRequest() const {
 		return _forwardSelection.events();
 	}
@@ -104,7 +117,7 @@ private:
 	void refreshLang();
 	void updateSearchVisibility();
 	void updateControlsGeometry();
-	void selectedShowCallback();
+	void slideAnimationCallback();
 	void updateInfoToggleActive();
 
 	void call();
@@ -169,11 +182,22 @@ private:
 	bool _canDelete = false;
 	bool _canForward = false;
 	bool _canSendNow = false;
+	bool _searchMode = false;
 
 	Ui::Animations::Simple _selectedShown;
+	Ui::Animations::Simple _searchShown;
 
 	object_ptr<Ui::RoundButton> _clear;
 	object_ptr<Ui::RoundButton> _forward, _sendNow, _delete;
+	object_ptr<Ui::InputField> _searchField = { nullptr };
+	object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _chooseFromUser
+		= { nullptr };
+	object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _jumpToDate
+		= { nullptr };
+	object_ptr<Ui::CrossButton> _searchCancel = { nullptr };
+	rpl::variable<QString> _searchQuery;
+	rpl::event_stream<> _searchCancelled;
+	rpl::event_stream<> _searchSubmitted;
 
 	object_ptr<Ui::IconButton> _back;
 	object_ptr<Ui::IconButton> _cancelChoose;
diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp
index 547f8680e..06be3da9a 100644
--- a/Telegram/SourceFiles/window/window_peer_menu.cpp
+++ b/Telegram/SourceFiles/window/window_peer_menu.cpp
@@ -83,7 +83,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Window {
 namespace {
 
-constexpr auto kTopicsSearchMinCount = 10;
+constexpr auto kTopicsSearchMinCount = 1;
 
 } // namespace
 
@@ -1005,8 +1005,14 @@ void Filler::addViewAsMessages() {
 }
 
 void Filler::addSearchTopics() {
+	const auto forum = _peer ? _peer->forum() : nullptr;
+	if (!forum) {
+		return;
+	}
+	const auto history = forum->history();
+	const auto controller = _controller;
 	_addAction(tr::lng_dlg_filter(tr::now), [=] {
-
+		controller->content()->searchInChat(history);
 	}, &st::menuIconSearch);
 }