From e00c6ecfb8931358c050818f869e7e4f2e2f2a1c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 21 May 2024 13:16:08 +0400
Subject: [PATCH] Show empty / placeholder in chats search.

---
 .../dialogs/dialogs_inner_widget.cpp          | 118 +++++++++++++++---
 .../dialogs/dialogs_inner_widget.h            |  12 +-
 Telegram/SourceFiles/dialogs/dialogs_key.cpp  |   1 +
 Telegram/SourceFiles/dialogs/dialogs_key.h    |   2 -
 .../SourceFiles/dialogs/dialogs_widget.cpp    |  16 +--
 .../dialogs/ui/chat_search_empty.cpp          |  82 ++++++++++++
 .../dialogs/ui/chat_search_empty.h            |  43 +++++++
 .../dialogs/ui/dialogs_suggestions.cpp        |  50 +++-----
 .../dialogs/ui/dialogs_suggestions.h          |   4 +-
 Telegram/cmake/td_ui.cmake                    |   2 +
 10 files changed, 264 insertions(+), 66 deletions(-)
 create mode 100644 Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp
 create mode 100644 Telegram/SourceFiles/dialogs/ui/chat_search_empty.h

diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 274d889a9..6767c52f9 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "dialogs/dialogs_inner_widget.h"
 
 #include "dialogs/dialogs_three_state_icon.h"
+#include "dialogs/ui/chat_search_empty.h"
 #include "dialogs/ui/chat_search_tabs.h"
 #include "dialogs/ui/dialogs_layout.h"
 #include "dialogs/ui/dialogs_stories_content.h"
@@ -84,7 +85,7 @@ constexpr auto kHashtagResultsLimit = 5;
 constexpr auto kStartReorderThreshold = 30;
 constexpr auto kChatPreviewDelay = crl::time(1000);
 
-int FixedOnTopDialogsCount(not_null<Dialogs::IndexedList*> list) {
+[[nodiscard]] int FixedOnTopDialogsCount(not_null<Dialogs::IndexedList*> list) {
 	auto result = 0;
 	for (const auto &row : *list) {
 		if (!row->entry()->fixedOnTopIndex()) {
@@ -95,7 +96,7 @@ int FixedOnTopDialogsCount(not_null<Dialogs::IndexedList*> list) {
 	return result;
 }
 
-int PinnedDialogsCount(
+[[nodiscard]] int PinnedDialogsCount(
 		FilterId filterId,
 		not_null<Dialogs::IndexedList*> list) {
 	auto result = 0;
@@ -110,6 +111,52 @@ int PinnedDialogsCount(
 	return result;
 }
 
+[[nodiscard]] object_ptr<SearchEmpty> MakeSearchEmpty(
+		QWidget *parent,
+		SearchState state) {
+	const auto query = state.query.trimmed();
+	const auto hashtag = !query.isEmpty() && (query[0] == '#');
+	const auto trimmed = hashtag ? query.mid(1).trimmed() : query;
+	const auto waiting = trimmed.isEmpty()
+		&& state.tags.empty()
+		&& !state.fromPeer;
+	const auto icon = waiting
+		? SearchEmptyIcon::Search
+		: SearchEmptyIcon::NoResults;
+	auto text = TextWithEntities();
+	if (waiting) {
+		if (hashtag) {
+			text.append(tr::lng_search_tab_by_hashtag(tr::now));
+		} else {
+			text.append(
+				tr::lng_dlg_search_for_messages(tr::now)
+			).append('\n').append(Ui::Text::Link(tr::lng_cancel(tr::now)));
+		}
+	} else {
+		text.append(tr::lng_search_tab_no_results(
+			tr::now,
+			Ui::Text::Bold));
+		if (!trimmed.isEmpty()) {
+			text.append("\n").append(
+				tr::lng_search_tab_no_results_text(
+					tr::now,
+					lt_query,
+					trimmed));
+			if (hashtag) {
+				text.append("\n").append(
+					tr::lng_search_tab_no_results_retry(tr::now));
+			}
+		}
+	}
+	auto result = object_ptr<SearchEmpty>(
+		parent,
+		icon,
+		rpl::single(std::move(text)));
+	result->show();
+	result->resizeToWidth(parent->width());
+	return result;
+}
+
 } // namespace
 
 struct InnerWidget::CollapsedRow {
@@ -186,7 +233,7 @@ InnerWidget::InnerWidget(
 	session().data().contactsLoaded().changes(
 	) | rpl::start_with_next([=] {
 		refresh();
-		refreshEmptyLabel();
+		refreshEmpty();
 	}, lifetime());
 
 	session().data().itemRemoved(
@@ -1906,7 +1953,7 @@ void InnerWidget::resizeEvent(QResizeEvent *e) {
 	if (_searchTags) {
 		_searchTags->resizeToWidth(width() - 2 * _searchTagsLeft);
 	}
-	resizeEmptyLabel();
+	resizeEmpty();
 	moveCancelSearchButtons();
 }
 
@@ -2587,12 +2634,13 @@ void InnerWidget::applySearchState(SearchState state) {
 					append(owner->contactsNoChatsList());
 				}
 			}
-			refresh(true);
 		}
 		clearMouseSelection(true);
 	}
 	if (_state != WidgetState::Default) {
+		_searchLoading = true;
 		_searchMessages.fire({});
+		refresh(true);
 	}
 }
 
@@ -2791,6 +2839,10 @@ rpl::producer<> InnerWidget::cancelSearchFromUserRequests() const {
 	return _cancelSearchFromUser->clicks() | rpl::to_empty;
 }
 
+rpl::producer<> InnerWidget::cancelSearchRequests() const {
+	return _cancelSearch.events();
+}
+
 rpl::producer<Ui::ScrollToRequest> InnerWidget::mustScrollTo() const {
 	return _mustScrollTo.events();
 }
@@ -2888,6 +2940,8 @@ void InnerWidget::searchReceived(
 		HistoryItem *inject,
 		SearchRequestType type,
 		int fullCount) {
+	_searchLoading = false;
+
 	const auto uniquePeers = uniqueSearchResults();
 	if (type == SearchRequestType::FromStart
 		|| type == SearchRequestType::PeerFromStart) {
@@ -3018,7 +3072,7 @@ void InnerWidget::refresh(bool toTop) {
 	} else if (needCollapsedRowsRefresh()) {
 		return refreshWithCollapsedRows(toTop);
 	}
-	refreshEmptyLabel();
+	refreshEmpty();
 	if (_searchTags) {
 		_searchTagsLeft = st::dialogsFilterSkip
 			+ st::dialogsFilterPadding.x();
@@ -3032,7 +3086,11 @@ void InnerWidget::refresh(bool toTop) {
 			h = dialogsOffset() + _shownList->height();
 		}
 	} else if (_state == WidgetState::Filtered) {
-		if (_waitingForSearch) {
+		if (_searchEmpty && !_searchEmpty->isHidden()) {
+			h = searchedOffset() + st::recentPeersEmptyHeightMin;
+			_searchEmpty->setMinimalHeight(st::recentPeersEmptyHeightMin);
+			_searchEmpty->move(0, h - st::recentPeersEmptyHeightMin);
+		} else if (_waitingForSearch) {
 			h = searchedOffset() + (_searchResults.size() * _st->height) + ((_searchResults.empty() && !_searchState.inChat) ? -st::searchedBarHeight : 0);
 		} else {
 			h = searchedOffset() + (_searchResults.size() * _st->height);
@@ -3047,7 +3105,32 @@ void InnerWidget::refresh(bool toTop) {
 	update();
 }
 
-void InnerWidget::refreshEmptyLabel() {
+void InnerWidget::refreshEmpty() {
+	if (_state == WidgetState::Filtered) {
+		const auto empty = _filterResults.empty()
+			&& _searchResults.empty()
+			&& _peerSearchResults.empty()
+			&& _hashtagResults.empty();
+		if (_searchLoading || !empty) {
+			if (_searchEmpty) {
+				_searchEmpty->hide();
+			}
+		} else if (_searchEmptyState != _searchState) {
+			_searchEmptyState = _searchState;
+			_searchEmpty = MakeSearchEmpty(this, _searchState);
+			_searchEmpty->linkClicks() | rpl::start_with_next([=] {
+				_cancelSearch.fire({});
+			}, _searchEmpty->lifetime());
+			if (_controller->session().data().chatsListLoaded()) {
+				_searchEmpty->animate();
+			}
+		} else if (_searchEmpty) {
+			_searchEmpty->show();
+		}
+	} else {
+		_searchEmpty.destroy();
+	}
+
 	const auto data = &session().data();
 	const auto state = !_shownList->empty()
 		? EmptyState::None
@@ -3100,7 +3183,7 @@ void InnerWidget::refreshEmptyLabel() {
 		return result;
 	});
 	_empty.create(this, std::move(full), st::dialogsEmptyLabel);
-	resizeEmptyLabel();
+	resizeEmpty();
 	_empty->overrideLinkClickHandler([=] {
 		if (_emptyState == EmptyState::NoContacts) {
 			_controller->showAddContact();
@@ -3114,13 +3197,16 @@ void InnerWidget::refreshEmptyLabel() {
 	_empty->setVisible(_state == WidgetState::Default);
 }
 
-void InnerWidget::resizeEmptyLabel() {
-	if (!_empty) {
-		return;
+void InnerWidget::resizeEmpty() {
+	if (_empty) {
+		const auto skip = st::dialogsEmptySkip;
+		_empty->resizeToWidth(width() - 2 * skip);
+		_empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2);
+	}
+	if (_searchEmpty) {
+		_searchEmpty->resizeToWidth(width());
+		_searchEmpty->move(0, searchedOffset());
 	}
-	const auto skip = st::dialogsEmptySkip;
-	_empty->resizeToWidth(width() - 2 * skip);
-	_empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2);
 }
 
 void InnerWidget::clearMouseSelection(bool clearSelection) {
@@ -3481,7 +3567,7 @@ void InnerWidget::switchToFilter(FilterId filterId) {
 		refreshShownList();
 		refreshWithCollapsedRows(true);
 	}
-	refreshEmptyLabel();
+	refreshEmpty();
 	{
 		const auto skip = found
 			// Don't save a scroll state for very flexible chat filters.
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
index 8cde8de92..a7e3ae619 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
@@ -60,6 +60,7 @@ class Row;
 class FakeRow;
 class IndexedList;
 class SearchTags;
+class SearchEmpty;
 
 struct ChosenRow {
 	Key key;
@@ -119,8 +120,8 @@ public:
 
 	void clearFilter();
 	void refresh(bool toTop = false);
-	void refreshEmptyLabel();
-	void resizeEmptyLabel();
+	void refreshEmpty();
+	void resizeEmpty();
 
 	[[nodiscard]] bool isUserpicPress() const;
 	[[nodiscard]] bool isUserpicPressOnWide() const;
@@ -158,6 +159,7 @@ public:
 	void setLoadMoreFilteredCallback(Fn<void()> callback);
 	[[nodiscard]] rpl::producer<> listBottomReached() const;
 	[[nodiscard]] rpl::producer<> cancelSearchFromUserRequests() const;
+	[[nodiscard]] rpl::producer<> cancelSearchRequests() const;
 	[[nodiscard]] rpl::producer<ChosenRow> chosenRow() const;
 	[[nodiscard]] rpl::producer<> updated() const;
 
@@ -386,7 +388,6 @@ private:
 		const Ui::Text::String &text) const;
 	void refreshSearchInChatLabel();
 	void repaintSearchResult(int index);
-	void paintEmpty(QPainter &p, int top);
 
 	Ui::VideoUserpic *validateVideoUserpic(not_null<Row*> row);
 	Ui::VideoUserpic *validateVideoUserpic(not_null<History*> history);
@@ -395,7 +396,6 @@ private:
 	void clearSearchResults(bool clearPeerSearchResults = true);
 	void updateSelectedRow(Key key = Key());
 	void trackSearchResultsHistory(not_null<History*> history);
-	void trackSearchResultsForum(Data::Forum *forum);
 
 	[[nodiscard]] QBrush currentBg() const;
 	[[nodiscard]] Key computeChatPreviewRow() const;
@@ -485,8 +485,11 @@ private:
 
 	WidgetState _state = WidgetState::Default;
 
+	object_ptr<SearchEmpty> _searchEmpty = { nullptr };
+	SearchState _searchEmptyState;
 	object_ptr<Ui::FlatLabel> _empty = { nullptr };
 	object_ptr<Ui::IconButton> _cancelSearchFromUser;
+	rpl::event_stream<> _cancelSearch;
 
 	Ui::DraggingScrollManager _draggingScroll;
 
@@ -526,6 +529,7 @@ private:
 	bool _geometryInited = false;
 
 	bool _savedSublists = false;
+	bool _searchLoading = false;
 
 	base::unique_qptr<Ui::PopupMenu> _menu;
 
diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.cpp b/Telegram/SourceFiles/dialogs/dialogs_key.cpp
index 8a8cf398c..7c1be62aa 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_key.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_key.cpp
@@ -87,6 +87,7 @@ PeerData *Key::peer() const {
 
 [[nodiscard]] bool SearchState::empty() const {
 	return !inChat
+		&& tags.empty()
 		&& QStringView(query).trimmed().isEmpty();
 }
 
diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h
index a9278e08c..2f631288e 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_key.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_key.h
@@ -147,6 +147,4 @@ struct SearchState {
 		const SearchState&) = default;
 };
 
-;
-
 } // namespace Dialogs
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index db8724a61..67155eb3d 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -345,6 +345,10 @@ Widget::Widget(
 		}
 		applySearchState(std::move(copy));
 	}, lifetime());
+	_inner->cancelSearchRequests(
+	) | rpl::start_with_next([=] {
+		applySearchState({});
+	}, lifetime());
 	_inner->chosenRow(
 	) | rpl::start_with_next([=](const ChosenRow &row) {
 		chosenRow(row);
@@ -1988,6 +1992,9 @@ bool Widget::search(bool inCache) {
 
 	auto result = false;
 	const auto query = _searchState.query.trimmed();
+	const auto trimmed = (query.isEmpty() || query[0] != '#')
+		? query
+		: query.mid(1).trimmed();
 	const auto inPeer = searchInPeer();
 	const auto fromPeer = searchFromPeer();
 	const auto &inTags = searchInTags();
@@ -1995,9 +2002,7 @@ bool Widget::search(bool inCache) {
 	const auto fromStartType = inPeer
 		? SearchRequestType::PeerFromStart
 		: SearchRequestType::FromStart;
-	const auto skipRequest = (query.isEmpty() && !fromPeer && inTags.empty())
-		|| (tab == ChatSearchTab::PublicPosts && query.size() < 2);
-	if (skipRequest) {
+	if (trimmed.isEmpty() && !fromPeer && inTags.empty()) {
 		cancelSearchRequest();
 		searchApplyEmpty(fromStartType, 0);
 		_api.request(base::take(_peerSearchRequest)).cancel();
@@ -2021,10 +2026,7 @@ bool Widget::search(bool inCache) {
 			_searchNextRate = 0;
 			_searchFull = _searchFullMigrated = false;
 			cancelSearchRequest();
-			searchReceived(
-				fromStartType,
-				i->second,
-				0);
+			searchReceived(fromStartType, i->second, 0);
 			result = true;
 		}
 	} else if (_searchQuery != query
diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp
new file mode 100644
index 000000000..855112f85
--- /dev/null
+++ b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp
@@ -0,0 +1,82 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "dialogs/ui/chat_search_empty.h"
+
+#include "base/object_ptr.h"
+#include "lottie/lottie_icon.h"
+#include "settings/settings_common.h"
+#include "ui/widgets/labels.h"
+#include "styles/style_dialogs.h"
+
+namespace Dialogs {
+
+SearchEmpty::SearchEmpty(
+	QWidget *parent,
+	Icon icon,
+	rpl::producer<TextWithEntities> text)
+: RpWidget(parent) {
+	setup(icon, std::move(text));
+}
+
+void SearchEmpty::setMinimalHeight(int minimalHeight) {
+	const auto minimal = st::recentPeersEmptyHeightMin;
+	resize(width(), std::max(minimalHeight, minimal));
+}
+
+void SearchEmpty::setup(Icon icon, rpl::producer<TextWithEntities> text) {
+	const auto label = Ui::CreateChild<Ui::FlatLabel>(
+		this,
+		std::move(text),
+		st::defaultPeerListAbout);
+	label->setClickHandlerFilter([=](const auto &, Qt::MouseButton button) {
+		if (button == Qt::LeftButton) {
+			_linkClicks.fire({});
+		}
+		return false;
+	});
+	const auto size = st::recentPeersEmptySize;
+	const auto animation = [&] {
+		switch (icon) {
+		case Icon::Search: return u"search"_q;
+		case Icon::NoResults: return u"noresults"_q;
+		}
+		Unexpected("Icon in SearchEmpty::setup.");
+	}();
+	const auto [widget, animate] = Settings::CreateLottieIcon(
+		this,
+		{
+			.name = animation,
+			.sizeOverride = { size, size },
+		},
+		st::recentPeersEmptyMargin);
+	const auto animated = widget.data();
+
+	sizeValue() | rpl::start_with_next([=](QSize size) {
+		const auto padding = st::recentPeersEmptyMargin;
+		const auto paddings = padding.left() + padding.right();
+		label->resizeToWidth(size.width() - paddings);
+		const auto x = (size.width() - animated->width()) / 2;
+		const auto y = (size.height() - animated->height()) / 3;
+		const auto top = y + animated->height() + st::recentPeersEmptySkip;
+		const auto sub = std::max(top + label->height() - size.height(), 0);
+		animated->move(x, y - sub);
+		label->move((size.width() - label->width()) / 2, top - sub);
+	}, lifetime());
+
+	_animate = [animate] {
+		animate(anim::repeat::once);
+	};
+}
+
+void SearchEmpty::animate() {
+	if (const auto onstack = _animate) {
+		onstack();
+	}
+}
+
+} // namespace Dialogs
diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h
new file mode 100644
index 000000000..6ddf52676
--- /dev/null
+++ b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h
@@ -0,0 +1,43 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "ui/rp_widget.h"
+
+namespace Dialogs {
+
+enum class SearchEmptyIcon {
+	Search,
+	NoResults,
+};
+
+class SearchEmpty final : public Ui::RpWidget {
+public:
+	using Icon = SearchEmptyIcon;
+
+	SearchEmpty(
+		QWidget *parent,
+		Icon icon,
+		rpl::producer<TextWithEntities> text);
+
+	void setMinimalHeight(int minimalHeight);
+	[[nodiscard]] rpl::producer<> linkClicks() const {
+		return _linkClicks.events();
+	}
+
+	void animate();
+
+private:
+	void setup(Icon icon, rpl::producer<TextWithEntities> text);
+
+	Fn<void()> _animate;
+	rpl::event_stream<> _linkClicks;
+
+};
+
+} // namespace Dialogs
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
index 868b3b74c..9a86cca75 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
@@ -20,12 +20,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_peer_values.h"
 #include "data/data_session.h"
 #include "data/data_user.h"
+#include "dialogs/ui/chat_search_empty.h"
 #include "history/history.h"
 #include "lang/lang_keys.h"
-#include "lottie/lottie_icon.h"
 #include "main/main_session.h"
 #include "settings/settings_common.h"
 #include "ui/boxes/confirm_box.h"
+#include "ui/text/text_utilities.h"
 #include "ui/widgets/menu/menu_add_action_callback_factory.h"
 #include "ui/widgets/buttons.h"
 #include "ui/widgets/discrete_sliders.h"
@@ -1360,53 +1361,30 @@ object_ptr<Ui::SlideWrap<>> Suggestions::setupRecentPeers(
 }
 
 object_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyRecent() {
-	return setupEmpty(_chatsContent, "search", tr::lng_recent_none());
+	const auto icon = SearchEmptyIcon::Search;
+	return setupEmpty(_chatsContent, icon, tr::lng_recent_none());
 }
 
 object_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyChannels() {
-	return setupEmpty(
-		_channelsContent,
-		"noresults",
-		tr::lng_channels_none_about());
+	const auto icon = SearchEmptyIcon::NoResults;
+	return setupEmpty(_channelsContent, icon, tr::lng_channels_none_about());
 }
 
 object_ptr<Ui::SlideWrap<>> Suggestions::setupEmpty(
 		not_null<QWidget*> parent,
-		const QString &animation,
+		SearchEmptyIcon icon,
 		rpl::producer<QString> text) {
-	auto content = object_ptr<Ui::RpWidget>(parent);
+	auto content = object_ptr<SearchEmpty>(
+		parent,
+		icon,
+		std::move(text) | Ui::Text::ToWithEntities());
+
 	const auto raw = content.data();
-
-	const auto label = Ui::CreateChild<Ui::FlatLabel>(
-		raw,
-		std::move(text),
-		st::defaultPeerListAbout);
-	const auto size = st::recentPeersEmptySize;
-	const auto [widget, animate] = Settings::CreateLottieIcon(
-		raw,
-		{
-			.name = animation,
-			.sizeOverride = { size, size },
-		},
-		st::recentPeersEmptyMargin);
-	const auto icon = widget.data();
-
 	rpl::combine(
 		_chatsScroll->heightValue(),
 		_topPeersWrap->heightValue()
 	) | rpl::start_with_next([=](int height, int top) {
-		raw->resize(
-			raw->width(),
-			std::max(height - top, st::recentPeersEmptyHeightMin));
-	}, raw->lifetime());
-
-	raw->sizeValue() | rpl::start_with_next([=](QSize size) {
-		const auto x = (size.width() - icon->width()) / 2;
-		const auto y = (size.height() - icon->height()) / 3;
-		icon->move(x, y);
-		label->move(
-			(size.width() - label->width()) / 2,
-			y + icon->height() + st::recentPeersEmptySkip);
+		raw->setMinimalHeight(height - top);
 	}, raw->lifetime());
 
 	auto result = object_ptr<Ui::SlideWrap<>>(
@@ -1417,7 +1395,7 @@ object_ptr<Ui::SlideWrap<>> Suggestions::setupEmpty(
 	result->toggledValue() | rpl::filter([=](bool shown) {
 		return shown && _controller->session().data().chatsListLoaded();
 	}) | rpl::start_with_next([=] {
-		animate(anim::repeat::once);
+		raw->animate();
 	}, raw->lifetime());
 
 	return result;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
index 947bf2fa9..27b9b527f 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
@@ -34,6 +34,8 @@ class SessionController;
 
 namespace Dialogs {
 
+enum class SearchEmptyIcon;
+
 struct RecentPeersList {
 	std::vector<not_null<PeerData*>> list;
 };
@@ -112,7 +114,7 @@ private:
 		-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;
 	[[nodiscard]] object_ptr<Ui::SlideWrap<Ui::RpWidget>> setupEmpty(
 		not_null<QWidget*> parent,
-		const QString &animation,
+		SearchEmptyIcon icon,
 		rpl::producer<QString> text);
 
 	void switchTab(Tab tab);
diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake
index 533fe85ef..adf5b8f11 100644
--- a/Telegram/cmake/td_ui.cmake
+++ b/Telegram/cmake/td_ui.cmake
@@ -85,6 +85,8 @@ PRIVATE
     data/data_subscription_option.h
 
     dialogs/dialogs_three_state_icon.h
+    dialogs/ui/chat_search_empty.cpp
+    dialogs/ui/chat_search_empty.h
     dialogs/ui/chat_search_tabs.cpp
     dialogs/ui/chat_search_tabs.h
     dialogs/ui/dialogs_stories_list.cpp