/*
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/dialogs_widget.h"

#include "base/call_delayed.h"
#include "base/qt/qt_key_modifiers.h"
#include "base/options.h"
#include "dialogs/ui/chat_search_in.h"
#include "dialogs/ui/dialogs_stories_content.h"
#include "dialogs/ui/dialogs_stories_list.h"
#include "dialogs/ui/dialogs_suggestions.h"
#include "dialogs/dialogs_inner_widget.h"
#include "dialogs/dialogs_search_from_controllers.h"
#include "dialogs/dialogs_top_bar_suggestion.h"
#include "dialogs/dialogs_quick_action.h"
#include "dialogs/dialogs_key.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_chat_section.h"
#include "history/view/history_view_contact_status.h"
#include "history/view/history_view_group_call_bar.h"
#include "history/view/history_view_requests_bar.h"
#include "history/view/history_view_top_bar_widget.h"
#include "boxes/peers/edit_peer_requests_box.h"
#include "boxes/choose_filter_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/chat_filters_tabs_strip.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/effects/radial_animation.h"
#include "ui/chat/requests_bar.h"
#include "ui/chat/group_call_bar.h"
#include "ui/chat/more_chats_bar.h"
#include "ui/controls/download_bar.h"
#include "ui/controls/jump_down_button.h"
#include "ui/controls/swipe_handler.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "lang/lang_keys.h"
#include "mainwindow.h"
#include "mainwidget.h"
#include "main/main_domain.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "api/api_authorizations.h"
#include "api/api_chat_filters.h"
#include "apiwrap.h"
#include "chat_helpers/message_field.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "core/update_checker.h"
#include "core/shortcuts.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "window/window_slide_animation.h"
#include "window/window_connecting_widget.h"
#include "window/window_main_menu.h"
#include "storage/storage_media_prepare.h"
#include "storage/storage_account.h"
#include "storage/storage_domain.h"
#include "data/components/recent_peers.h"
#include "data/components/sponsored_messages.h"
#include "data/data_session.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_user.h"
#include "data/data_folder.h"
#include "data/data_forum.h"
#include "data/data_forum_topic.h"
#include "data/data_histories.h"
#include "data/data_changes.h"
#include "data/data_download_manager.h"
#include "data/data_chat_filters.h"
#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_stories.h"
#include "info/downloads/info_downloads_widget.h"
#include "info/info_memento.h"
#include "inline_bots/bot_attach_web_view.h"
#include "styles/style_dialogs.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include "styles/style_window.h"
#include "base/qt/qt_common_adapters.h"
#include "boxes/abstract_box.h"

#include <QtCore/QMimeData>
#include <QtGui/QTextBlock>
#include <QtWidgets/QScrollBar>
#include <QtWidgets/QTextEdit>

namespace Dialogs {
namespace {

constexpr auto kSearchPerPage = 50;
constexpr auto kStoriesExpandDuration = crl::time(200);
constexpr auto kSearchRequestDelay = crl::time(900);

base::options::toggle OptionForumHideChatsList({
	.id = kOptionForumHideChatsList,
	.name = "Hide chat list in forums",
	.description = "Don't keep a narrow column of chat list.",
});

[[nodiscard]] bool RedirectTextToSearch(const QString &text) {
	for (const auto &ch : text) {
		if (ch.unicode() >= 32) {
			return true;
		}
	}
	return false;
}

[[nodiscard]] QImage UpdateIcon() {
	const auto iconSize = st::dialogsInstallUpdateIconSize;
	const auto ratio = style::DevicePixelRatio();
	auto result = QImage(
		Size(iconSize) * ratio,
		QImage::Format_ARGB32_Premultiplied);
	result.setDevicePixelRatio(ratio);
	result.fill(Qt::transparent);
	{
		auto p = QPainter(&result);
		auto hq = PainterHighQualityEnabler(p);
		auto path = QPainterPath();

		const auto fullRect = QRectF(0, 0, iconSize, iconSize);
		const auto rect = fullRect
			- Margins(st::dialogsInstallUpdateIconInnerMargin);
		p.setPen(Qt::NoPen);
		p.setBrush(Qt::white);
		p.drawEllipse(fullRect);

		p.setCompositionMode(QPainter::CompositionMode_Clear);

		auto pen = QPen(Qt::black);
		pen.setWidthF(style::ConvertFloatScale(2.));
		pen.setCapStyle(Qt::RoundCap);
		p.setPen(pen);

		using namespace arc;
		constexpr auto kShift = int(20 * 16);
		p.drawArc(rect, -kShift, kQuarterLength + kShift);
		p.drawArc(rect, kHalfLength - kShift, kQuarterLength + kShift);

		const auto side1 = st::dialogsInstallUpdateIconSide1;
		const auto side2 = st::dialogsInstallUpdateIconSide2;
		const auto top = rect.y() - side1;
		const auto bottom = rect::bottom(rect) - side1;
		const auto centerX = rect::center(rect).x();
		path.moveTo(centerX, bottom + side1 + side2);
		path.lineTo(centerX, bottom + side1 - side2);
		path.lineTo(centerX + side2, bottom + side1);
		path.closeSubpath();

		path.moveTo(centerX, top + side1 + side2);
		path.lineTo(centerX, top + side1 - side2);
		path.lineTo(centerX - side2, top + side1);
		path.closeSubpath();

		p.fillPath(path, Qt::black);
	}
	return result;
}

} // namespace

const char kOptionForumHideChatsList[] = "forum-hide-chats-list";

class Widget::BottomButton : public Ui::RippleButton {
public:
	BottomButton(
		QWidget *parent,
		const QString &text,
		const style::FlatButton &st,
		const style::icon &icon,
		const style::icon &iconOver,
		bool hasTextIcon);

	void setText(const QString &text);

protected:
	void paintEvent(QPaintEvent *e) override;

	void onStateChanged(State was, StateChangeSource source) override;

private:
	void radialAnimationCallback();

	QString _text;
	const style::FlatButton &_st;
	const style::icon &_icon;
	const style::icon &_iconOver;
	const bool _hasTextIcon;
	std::unique_ptr<Ui::InfiniteRadialAnimation> _loading;

	QImage _textIcon;

};

Widget::BottomButton::BottomButton(
	QWidget *parent,
	const QString &text,
	const style::FlatButton &st,
	const style::icon &icon,
	const style::icon &iconOver,
	bool hasTextIcon)
: RippleButton(parent, st.ripple)
, _text(text)
, _st(st)
, _icon(icon)
, _iconOver(iconOver)
, _hasTextIcon(hasTextIcon) {
	resize(st::columnMinimalWidthLeft, _st.height);

	if (_hasTextIcon) {
		rpl::single(rpl::empty_value()) | rpl::then(
			style::PaletteChanged()
		) | rpl::on_next([this] {
			_textIcon = UpdateIcon();
		}, lifetime());
	}
}

void Widget::BottomButton::setText(const QString &text) {
	_text = text;
	update();
}

void Widget::BottomButton::radialAnimationCallback() {
	if (!anim::Disabled() && width() < st::columnMinimalWidthLeft) {
		update();
	}
}

void Widget::BottomButton::onStateChanged(
		State was,
		StateChangeSource source) {
	RippleButton::onStateChanged(was, source);
	if ((was & StateFlag::Disabled) != (state() & StateFlag::Disabled)) {
		_loading = isDisabled()
			? std::make_unique<Ui::InfiniteRadialAnimation>(
				[=] { radialAnimationCallback(); },
				st::dialogsLoadMoreLoading)
			: nullptr;
		if (_loading) {
			_loading->start();
		}
	}
	update();
}

void Widget::BottomButton::paintEvent(QPaintEvent *e) {
	auto p = QPainter(this);

	const auto over = isOver() && !isDisabled();

	auto r = QRect(0, height() - _st.height, width(), _st.height);

	if (_hasTextIcon) {
		auto gradient = QLinearGradient(0, 0, width(), 0);
		gradient.setStops({
			{ 0., st::groupCallLive1->c },
			{ 1., st::groupCallLive2->c },
		});
		p.fillRect(r, QBrush(std::move(gradient)));
		if (over) {
			p.fillRect(
				r,
				anim::with_alpha(st::universalRippleAnimation.color->c, .3));
		}

		if (!isDisabled()) {
			paintRipple(p, 0, 0, &st::universalRippleAnimation.color->c);
		}
	} else {
		p.fillRect(r, over ? _st.overBgColor : _st.bgColor);
		if (!isDisabled()) {
			paintRipple(p, 0, 0);
		}
	}

	const auto &font = over ? _st.overFont : _st.font;
	p.setFont(font);
	p.setRenderHint(QPainter::TextAntialiasing);
	p.setPen(over ? _st.overColor : _st.color);

	if (width() >= st::columnMinimalWidthLeft) {
		r.setTop(_st.textTop);
		if (_hasTextIcon) {
			const auto &icon = _textIcon;
			const auto iconSize = icon.size() / style::DevicePixelRatio();
			const auto skip = st::dialogsInstallUpdateIconSkip;
			const auto textWidth = font->width(_text);
			const auto rect = QRect(
				(width() - (iconSize.width() + textWidth + skip)) / 2,
				r.y(),
				textWidth,
				r.height());
			p.drawText(
				rect.translated(iconSize.width() + skip, 0),
				_text,
				style::al_top);
			p.drawImage(rect.x(), (height() - iconSize.height()) / 2, icon);
		} else {
			p.drawText(r, _text, style::al_top);
		}
	} else if (isDisabled() && _loading) {
		_loading->draw(
			p,
			QPoint(
				(width() - st::dialogsLoadMoreLoading.size.width()) / 2,
				(height() - st::dialogsLoadMoreLoading.size.height()) / 2),
			width());
	} else {
		if (_hasTextIcon) {
			const auto size = _textIcon.size() / style::DevicePixelRatio();
			p.drawImage(
				(width() - size.width()) / 2,
				(height() - size.height()) / 2,
				_textIcon);
		} else {
			(over ? _iconOver : _icon).paintInCenter(p, r);
		}
	}
}

Widget::Widget(
	QWidget *parent,
	not_null<Window::SessionController*> controller,
	Layout layout)
: Window::AbstractSectionWidget(parent, controller, nullptr)
, _api(&controller->session().mtp())
, _chooseByDragTimer([=] { _inner->chooseRow(); })
, _layout(layout)
, _narrowWidth(st::defaultDialogRow.padding.left()
	+ st::defaultDialogRow.photoSize
	+ st::defaultDialogRow.padding.left())
, _searchControls(this)
, _mainMenu({
	.toggle = object_ptr<Ui::IconButton>(
		_searchControls,
		st::dialogsMenuToggle),
	.under = object_ptr<Ui::AbstractButton>(_searchControls),
})
, _searchForNarrowLayout(_searchControls, st::dialogsSearchForNarrowFilters)
, _search(_searchControls, st::dialogsFilter, tr::lng_dlg_filter())
, _chooseFromUser(
	_searchControls,
	object_ptr<Ui::IconButton>(this, st::dialogsSearchFrom))
, _jumpToDate(
	_searchControls,
	object_ptr<Ui::IconButton>(this, st::dialogsCalendar))
, _cancelSearch(_searchControls, st::dialogsCancelSearch)
, _lockUnlock(
	_searchControls,
	object_ptr<Ui::IconButton>(this, st::dialogsLock))
, _scroll(this)
, _scrollToTop(_scroll, st::dialogsToUp)
, _stories((_layout != Layout::Child)
	? std::make_unique<Stories::List>(
		this,
		st::dialogsStoriesList,
		_storiesContents.events() | rpl::flatten_latest())
	: nullptr)
, _searchTimer([=] { search(); })
, _peerSearch(&controller->session(), Api::PeerSearch::Type::WithSponsored)
, _singleMessageSearch(&controller->session()) {
	const auto makeChildListShown = [](PeerId peerId, float64 shown) {
		return InnerWidget::ChildListShown{ peerId, shown };
	};
	using OverscrollType = Ui::ElasticScroll::OverscrollType;
	_scroll->setOverscrollTypes(
		_stories ? OverscrollType::Virtual : OverscrollType::Real,
		OverscrollType::Real);
	_innerList = _scroll->setOwnedWidget(
		object_ptr<Ui::VerticalLayout>(this));
	_inner = _innerList->add(object_ptr<InnerWidget>(
		_innerList,
		controller,
		rpl::combine(
			_childListPeerId.value(),
			_childListShown.value(),
			makeChildListShown)));
	rpl::combine(
		_scroll->heightValue(),
		_topBarSuggestionHeightChanged.events_starting_with(0)
	) | rpl::on_next([=](int height, int topBarHeight) {
		_innerList->setMinimumHeight(height);
		_inner->setMinimumHeight(height - topBarHeight);
		_inner->refresh();
	}, _innerList->lifetime());
	_scroll->widthValue() | rpl::on_next([=](int width) {
		_innerList->resizeToWidth(width);
	}, _innerList->lifetime());
	_scrollToTop->raise();
	_lockUnlock->toggle(false, anim::type::instant);

	_inner->updated(
	) | rpl::on_next([=] {
		listScrollUpdated();
	}, lifetime());

	rpl::combine(
		session().api().dialogsLoadMayBlockByDate(),
		session().api().dialogsLoadBlockedByDate()
	) | rpl::on_next([=](bool mayBlock, bool isBlocked) {
		refreshLoadMoreButton(mayBlock, isBlocked);
	}, lifetime());

	session().changes().historyUpdates(
		Data::HistoryUpdate::Flag::MessageSent
	) | rpl::filter([=](const Data::HistoryUpdate &update) {
		if (_openedForum) {
			return (update.history == _openedForum->history());
		} else if (_openedFolder) {
			return (update.history->folder() == _openedFolder)
				&& !update.history->isPinnedDialog(FilterId());
		} else {
			return !update.history->folder()
				&& !update.history->isPinnedDialog(
					controller->activeChatsFilterCurrent());
		}
	}) | rpl::on_next([=](const Data::HistoryUpdate &update) {
		jumpToTop(true);
	}, lifetime());

	fullSearchRefreshOn(session().settings().skipArchiveInSearchChanges(
	) | rpl::to_empty);

	_inner->scrollByDeltaRequests(
	) | rpl::on_next([=](int delta) {
		if (_scroll) {
			_scroll->scrollToY(_scroll->scrollTop() + delta);
		}
	}, lifetime());

	_inner->mustScrollTo(
	) | rpl::on_next([=](const Ui::ScrollToRequest &data) {
		if (_scroll) {
			_scroll->scrollToY(data.ymin, data.ymax);
		}
	}, lifetime());
	_inner->dialogMoved(
	) | rpl::on_next([=](const Ui::ScrollToRequest &data) {
		const auto movedFrom = data.ymin;
		const auto movedTo = data.ymax;
		const auto st = _scroll->scrollTop();
		if (st > movedTo && st < movedFrom) {
			_scroll->scrollToY(st + _inner->st()->height);
		}
	}, lifetime());
	_inner->searchRequests(
	) | rpl::on_next([=](SearchRequestDelay delay) {
		searchRequested(delay);
	}, lifetime());
	_inner->completeHashtagRequests(
	) | rpl::on_next([=](const QString &tag) {
		completeHashtag(tag);
	}, lifetime());
	_inner->refreshHashtagsRequests(
	) | rpl::on_next([=] {
		searchCursorMoved();
	}, lifetime());
	_inner->changeSearchTabRequests(
	) | rpl::filter([=](ChatSearchTab tab) {
		return _searchState.tab != tab;
	}) | rpl::on_next([=](ChatSearchTab tab) {
		auto copy = _searchState;
		copy.tab = tab;
		applySearchState(std::move(copy));
	}, lifetime());
	_inner->changeSearchFilterRequests(
	) | rpl::filter([=](ChatTypeFilter filter) {
		return (_searchState.filter != filter)
			&& (_searchState.tab == ChatSearchTab::MyMessages);
	}) | rpl::on_next([=](ChatTypeFilter filter) {
		auto copy = _searchState;
		copy.filter = filter;
		applySearchState(copy);
	}, lifetime());
	_inner->cancelSearchRequests(
	) | rpl::on_next([=] {
		cancelSearch({
			.forceFullCancel = true,
			.jumpBackToSearchedChat = true,
		});
		controller->widget()->setInnerFocus();
	}, lifetime());
	_inner->cancelSearchFromRequests(
	) | rpl::on_next([=] {
		auto copy = _searchState;
		copy.fromPeer = nullptr;
		if (copy.inChat.sublist()) {
			copy.inChat = session().data().history(session().user());
		}
		applySearchState(std::move(copy));
	}, lifetime());
	_inner->changeSearchFromRequests(
	) | rpl::on_next([=] {
		showSearchFrom();
	}, lifetime());
	_inner->chosenRow(
	) | rpl::on_next([=](const ChosenRow &row) {
		chosenRow(row);
	}, lifetime());
	_inner->openBotMainAppRequests(
	) | rpl::on_next([=](UserId userId) {
		if (const auto user = session().data().user(userId)) {
			openBotMainApp(user);
		}
	}, lifetime());

	_scroll->geometryChanged(
	) | rpl::on_next(crl::guard(_inner, [=] {
		_inner->parentGeometryChanged();
	}), lifetime());
	_scroll->scrolls(
	) | rpl::on_next([=] {
		listScrollUpdated();
	}, lifetime());

	session().data().chatsListChanges(
	) | rpl::filter([=](Data::Folder *folder) {
		return (folder == _inner->shownFolder());
	}) | rpl::on_next([=] {
		Ui::PostponeCall(this, [=] { listScrollUpdated(); });
	}, lifetime());

	setAttribute(Qt::WA_InputMethodEnabled);
	controller->widget()->imeCompositionStarts(
	) | rpl::filter([=] {
		return redirectImeToSearch();
	}) | rpl::on_next([=] {
		_search->setFocusFast();
	}, lifetime());

	_search->changes(
	) | rpl::on_next([=] {
		crl::on_main(this, [=] { applySearchUpdate(); });
	}, _search->lifetime());

	_search->submits(
	) | rpl::on_next([=] { submit(); }, _search->lifetime());

	_search->setMimeDataHook([=](
			not_null<const QMimeData*> data,
			Ui::InputField::MimeAction action) {
		if (data->hasFormat(u"application/x-telegram-dialog"_q)) {
			if (const auto history = HistoryFromMimeData(data, &session())) {
				if (action != Ui::InputField::MimeAction::Check) {
					controller->searchInChat(history);
				}
				return true;
			}
		}
		return false;
	});

	QObject::connect(
		_search->rawTextEdit().get(),
		&QTextEdit::cursorPositionChanged,
		this,
		[=] { searchCursorMoved(); },
		Qt::QueuedConnection); // So getLastText() works already.

	if (!Core::UpdaterDisabled()) {
		Core::UpdateChecker checker;
		rpl::merge(
			rpl::single(rpl::empty),
			checker.isLatest(),
			checker.failed(),
			checker.ready()
		) | rpl::on_next([=] {
			checkUpdateStatus();
		}, lifetime());
	}

	_cancelSearch->setClickedCallback([=] {
		cancelSearch({ .jumpBackToSearchedChat = true });
	});
	_cancelSearch->setAccessibleName(tr::lng_sr_cancel_search(tr::now));
	_jumpToDate->entity()->setClickedCallback([=] { showCalendar(); });
	_jumpToDate->entity()->setAccessibleName(
		tr::lng_sr_search_date(tr::now));
	_chooseFromUser->entity()->setClickedCallback([=] { showSearchFrom(); });
	_chooseFromUser->entity()->setAccessibleName(
		tr::lng_search_messages_from(tr::now));
	rpl::single(rpl::empty) | rpl::then(
		session().domain().local().localPasscodeChanged()
	) | rpl::on_next([=] {
		updateLockUnlockVisibility();
	}, lifetime());
	const auto lockUnlock = _lockUnlock->entity();
	lockUnlock->setAccessibleName(tr::lng_shortcuts_lock(tr::now));
	lockUnlock->setClickedCallback([=] {
		lockUnlock->setIconOverride(
			&st::dialogsUnlockIcon,
			&st::dialogsUnlockIconOver);
		Core::App().maybeLockByPasscode();
		lockUnlock->setIconOverride(nullptr);
	});

	setupMainMenuToggle();
	setupShortcuts();
	if (_stories) {
		setupStories();
	}

	_searchForNarrowLayout->setAccessibleName(tr::lng_dlg_filter(tr::now));
	_searchForNarrowLayout->setClickedCallback([=] {
		_search->setFocusFast();
		if (_childList) {
			controller->closeForum();
		}
	});

	setAcceptDrops(true);

	_inner->setLoadMoreFilteredCallback([=] {
		const auto state = _inner->state();
		if (state == WidgetState::Filtered
			&& !_topicSearchFull
			&& searchForTopicsRequired(_topicSearchQuery)) {
			searchTopics();
		}
	});
	_inner->setLoadMoreCallback([=] {
		const auto state = _inner->state();
		const auto process = currentSearchProcess();
		if (state == WidgetState::Filtered
			&& (!process->full
				|| (_searchInMigrated && !_migratedProcess.full))) {
			searchMore();
		} else if (_openedForum && state == WidgetState::Default) {
			_openedForum->requestTopics();
		} else {
			const auto folder = _inner->shownFolder();
			if (!folder || !folder->chatsList()->loaded()) {
				session().api().requestDialogs(folder);
			}
		}
	});
	_inner->listBottomReached(
	) | rpl::on_next([=] {
		loadMoreBlockedByDate();
	}, lifetime());

	_search->customUpDown(true);

	updateJumpToDateVisibility(true);
	updateSearchFromVisibility(true);
	setupSupportMode();
	setupScrollUpButton();
	setupTouchChatPreview();

	const auto overscrollBg = [=] {
		return anim::color(
			st::dialogsBg,
			st::dialogsBgOver,
			_childListShown.current());
	};
	_scroll->setOverscrollBg(overscrollBg());
	style::PaletteChanged(
	) | rpl::on_next([=] {
		_scroll->setOverscrollBg(overscrollBg());
	}, lifetime());

	if (_layout != Layout::Child) {
		setupConnectingWidget();

		changeOpenedFolder(
			controller->openedFolder().current(),
			anim::type::instant);

		controller->openedFolder().changes(
		) | rpl::on_next([=](Data::Folder *folder) {
			changeOpenedFolder(folder, anim::type::normal);
		}, lifetime());

		controller->shownForum().changes(
		) | rpl::filter(!rpl::mappers::_1) | rpl::on_next([=] {
			if (_openedForum) {
				changeOpenedForum(nullptr, anim::type::normal);
			} else if (_childList) {
				closeChildList(anim::type::normal);
			}
		}, lifetime());

		_childListShown.changes(
		) | rpl::on_next([=] {
			_scroll->setOverscrollBg(overscrollBg());
			updateControlsGeometry();
		}, lifetime());

		_childListShown.changes(
		) | rpl::filter((rpl::mappers::_1 == 0.) || (rpl::mappers::_1 == 1.)
		) | rpl::on_next([=](float64 shown) {
			const auto color = (shown > 0.) ? &st::dialogsRippleBg : nullptr;
			_mainMenu.toggle->setRippleColorOverride(color);
			_searchForNarrowLayout->setRippleColorOverride(color);
		}, lifetime());

		setupMoreChatsBar();
		setupDownloadBar();
		setupShortcuts(controller);
	}
	setupSwipeBack();

	if (session().settings().dialogsFiltersEnabled()
		&& (Core::App().settings().chatFiltersHorizontal()
			|| !controller->enoughSpaceForFilters())) {
		toggleFiltersMenu(true);
	}

	setupFrozenAccountBar();
	setupTopBarSuggestions();
}

void Widget::setupSwipeBack() {
	const auto isMainList = [=] {
		const auto current = controller()->activeChatsFilterCurrent();
		const auto &chatsFilters = session().data().chatsFilters();
		if (chatsFilters.has()) {
			return chatsFilters.defaultId() == current;
		}
		return !current;
	};

	auto update = [=](Ui::Controls::SwipeContextData data) {
		data.cursorTop -= _inner->y();
		if (data.translation != 0) {
			if (data.translation < 0
				&& _inner
				&& (Core::App().settings().quickDialogAction()
					!= Ui::QuickDialogAction::Disabled)) {
				_inner->setSwipeContextData(data.msgBareId, std::move(data));
			} else {
				if (!_swipeBackData.callback) {
					_swipeBackData = Ui::Controls::SetupSwipeBack(
						this,
						[]() -> std::pair<QColor, QColor> {
							return {
								st::historyForwardChooseBg->c,
								st::historyForwardChooseFg->c,
							};
						},
						_swipeBackMirrored,
						_swipeBackIconMirrored);
				}
				_swipeBackData.callback(data);
			}
			return;
		} else {
			if (_swipeBackData.lifetime) {
				_swipeBackData = {};
			}
			if (_inner) {
				_inner->setSwipeContextData(data.msgBareId, std::nullopt);
				_inner->update();
			}
		}
	};

	auto init = [=](int top, Qt::LayoutDirection direction) {
		top -= _inner->y();
		_swipeBackIconMirrored = false;
		_swipeBackMirrored = false;
		if (_childListShown.current()) {
			return Ui::Controls::SwipeHandlerFinishData();
		}
		const auto isRightToLeft = direction == Qt::RightToLeft;
		const auto action = Core::App().settings().quickDialogAction();
		const auto isDisabled = action == Ui::QuickDialogAction::Disabled;
		if (_inner) {
			_inner->clearQuickActions();
			if (!isRightToLeft) {
				if (const auto key = _inner->calcSwipeKey(top);
						key && !isDisabled) {
					_inner->prepareQuickAction(key, action);
					return Ui::Controls::SwipeHandlerFinishData{
						.callback = [=, session = &session()] {
							auto callback = [=, peerId = PeerId(key)] {
								PerformQuickDialogAction(
									controller(),
									session->data().peer(peerId),
									action,
									_inner->filterId());
							};
							base::call_delayed(
								st::slideWrapDuration,
								session,
								std::move(callback));
						},
						.msgBareId = key,
						.speedRatio = 1.,
						.reachRatioDuration = crl::time(
							st::slideWrapDuration),
						.provideReachOutRatio = true,
					};
				}
			}
		}
		if (controller()->openedFolder().current()) {
			if (!isRightToLeft) {
				return Ui::Controls::SwipeHandlerFinishData();
			}
			return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
				_swipeBackData = {};
				if (controller()->openedFolder().current()) {
					if (!controller()->windowId().folder()) {
						controller()->closeFolder();
					}
				}
			});
		}
		if (controller()->shownForum().current()) {
			if (!isRightToLeft) {
				return Ui::Controls::SwipeHandlerFinishData();
			}
			const auto id = controller()->windowId();
			const auto initial = id.forum();
			if (initial) {
				return Ui::Controls::SwipeHandlerFinishData();
			}
			return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
				_swipeBackData = {};
				if (controller()->shownForum().current()) {
					controller()->closeForum();
				}
			});
		}
		if (isRightToLeft && isMainList()) {
			_swipeBackIconMirrored = true;
			return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
				_swipeBackIconMirrored = false;
				_swipeBackData = {};
				if (isMainList()) {
					showMainMenu();
				}
			});
		}
		if (session().data().chatsFilters().has() && isDisabled) {
			_swipeBackMirrored = !isRightToLeft;
			using namespace Window;
			const auto next = !isRightToLeft;
			if (CheckAndJumpToNearChatsFilter(controller(), next, false)) {
				return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
					_swipeBackData = {};
					CheckAndJumpToNearChatsFilter(controller(), next, true);
				});
			}
		}

		return Ui::Controls::SwipeHandlerFinishData();
	};

	Ui::Controls::SetupSwipeHandler({
		.widget = _innerList,
		.scroll = _scroll.data(),
		.update = std::move(update),
		.init = std::move(init),
	});

}

void Widget::chosenRow(const ChosenRow &row) {
	storiesToggleExplicitExpand(false);

	if (!row.sponsoredRandomId.isEmpty()) {
		auto &messages = session().sponsoredMessages();
		messages.clicked(row.sponsoredRandomId, false, false);
	} else if (!_searchState.query.isEmpty()) {
		if (const auto history = row.key.history()) {
			session().recentPeers().bump(history->peer);
		}
	}

	const auto history = row.key.history();
	const auto topicJump = history
		? history->peer->forumTopicFor(row.topicJumpRootId)
		: nullptr;
	const auto sublistJump = history
		? history->peer->monoforumSublistFor(row.sublistJumpPeerId)
		: nullptr;

	if (topicJump) {
		if (controller()->shownForum().current() == topicJump->forum()) {
			controller()->closeForum();
		} else if (row.newWindow) {
			controller()->showInNewWindow(Window::SeparateId(topicJump));
		} else {
			if (!controller()->adaptive().isOneColumn()
				&& !topicJump->peer()->useSubsectionTabs()) {
				controller()->showForum(
					topicJump->forum(),
					Window::SectionShow().withChildColumn());
			}
			controller()->showThread(
				topicJump,
				ShowAtUnreadMsgId,
				Window::SectionShow::Way::ClearStack);
		}
		return;
	} else if (sublistJump) {
		if (row.newWindow) {
			controller()->showInNewWindow(Window::SeparateId(sublistJump));
		} else {
			controller()->showThread(
				sublistJump,
				ShowAtUnreadMsgId,
				Window::SectionShow::Way::ClearStack);
		}
		return;
	} else if (const auto topic = row.key.topic()) {
		auto params = Window::SectionShow(
			Window::SectionShow::Way::ClearStack);
		params.highlight = Window::SearchHighlightId(_searchState.query);
		if (row.newWindow) {
			controller()->showInNewWindow(
				Window::SeparateId(topic),
				row.message.fullId.msg);
		} else {
			session().data().saveViewAsMessages(topic->forum(), false);
			controller()->showThread(topic, row.message.fullId.msg, params);
		}
	} else if (!GetEnhancedBool("hide_stories")
		&& history
		&& row.userpicClick
		&& (row.message.fullId.msg == ShowAtUnreadMsgId)
		&& history->peer->hasActiveStories()
		&& !history->peer->isSelf()) {
		controller()->openPeerStories(history->peer->id);
		return;
	} else if (history
		&& history->isForum()
		&& !row.message.fullId
		&& (!controller()->adaptive().isOneColumn()
			|| !history->peer->viewForumAsMessages()
			|| history->peer->useSubsectionTabs())) {
		const auto forum = history->peer->forum();
		if (controller()->shownForum().current() == forum) {
			controller()->closeForum();
		} else if (row.newWindow) {
			const auto type = forum->peer()->useSubsectionTabs()
				? Window::SeparateType::Chat
				: Window::SeparateType::Forum;
			controller()->showInNewWindow(Window::SeparateId(type, history));
		} else {
			controller()->showForum(
				forum,
				Window::SectionShow(
					Window::SectionShow::Way::ClearStack).withChildColumn());
			if (controller()->shownForum().current() == forum
				&& forum->peer()->viewForumAsMessages()) {
				controller()->showThread(
					history,
					ShowAtUnreadMsgId,
					Window::SectionShow::Way::ClearStack);
			}
		}
		return;
	} else if (history
		&& history->amMonoforumAdmin()
		&& !row.message.fullId) {
		const auto monoforum = history->peer->monoforum();
		if (row.newWindow) {
			controller()->showInNewWindow(
				Window::SeparateId(Window::SeparateType::Chat, history));
		} else {
			if (const auto active = monoforum->activeSubsectionThread()) {
				controller()->showThread(
					active,
					ShowAtUnreadMsgId,
					Window::SectionShow::Way::ClearStack);
			} else {
				controller()->showPeerHistory(
					history,
					Window::SectionShow::Way::ClearStack);
			}
		}
		return;
	} else if (history) {
		const auto peer = history->peer;
		const auto showAtMsgId = controller()->uniqueChatsInSearchResults(
			_searchState
		) ? ShowAtUnreadMsgId : row.message.fullId.msg;
		auto params = Window::SectionShow(
			Window::SectionShow::Way::ClearStack);
		params.highlight = Window::SearchHighlightId(_searchState.query);
		if (row.newWindow) {
			controller()->showInNewWindow(peer, showAtMsgId);
			closeSuggestions();
		} else {
			controller()->showThread(history, showAtMsgId, params);
			hideChildList();
		}
	} else if (const auto folder = row.key.folder()) {
		if (!GetEnhancedBool("hide_stories") && row.userpicClick) {
			const auto list = Data::StorySourcesList::Hidden;
			const auto &sources = session().data().stories().sources(list);
			if (!sources.empty()) {
				controller()->openPeerStories(sources.front().id, list);
				return;
			}
		}
		if (row.newWindow) {
			controller()->showInNewWindow(Window::SeparateId(
				Window::SeparateType::Archive,
				&session()));
			return;
		}
		controller()->openFolder(folder);
		hideChildList();
	}
	if (row.filteredRow && !session().supportMode()) {
		if (_subsectionTopBar) {
			_subsectionTopBar->toggleSearch(false, anim::type::instant);
		} else {
			escape();
		}
	}
	updateForceDisplayWide();
}

void Widget::setGeometryWithTopMoved(
		const QRect &newGeometry,
		int topDelta) {
	_topDelta = topDelta;
	bool willBeResized = (size() != newGeometry.size());
	if (geometry() != newGeometry) {
		auto weak = base::make_weak(this);
		setGeometry(newGeometry);
		if (!weak) {
			return;
		}
	}
	if (!willBeResized) {
		resizeEvent(nullptr);
	}
	_topDelta = 0;
}

void Widget::scrollToDefaultChecked(bool verytop) {
	if (_scrollToAnimation.animating()) {
		return;
	}
	scrollToDefault(verytop);
}

void Widget::setupScrollUpButton() {
	_scrollToTop->setClickedCallback([=] { scrollToDefaultChecked(); });
	_scrollToTop->setAccessibleName(tr::lng_sr_scroll_to_top(tr::now));
	trackScroll(_scrollToTop);
	trackScroll(this);
	updateScrollUpVisibility();
}

void Widget::setupTouchChatPreview() {
	_scroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {
		return _inner->processTouchEvent(e);
	});
	_inner->touchCancelRequests() | rpl::on_next([=] {
		QTouchEvent ev(QEvent::TouchCancel);
		ev.setTimestamp(crl::now());
		QGuiApplication::sendEvent(_scroll, &ev);
	}, _inner->lifetime());
}

void Widget::setupFrozenAccountBar() {
	session().frozenValue(
	) | rpl::on_next([=] {
		updateFrozenAccountBar();
		updateControlsGeometry();
	}, lifetime());
}

void Widget::setupTopBarSuggestions() {
	if (_layout == Layout::Child) {
		return;
	}
	using namespace rpl::mappers;
	crl::on_main(_innerList, [=] {
		const auto owner = &session().data();
		session().api().authorizations().unreviewedChanges(
		) | rpl::on_next([=] {
			updateForceDisplayWide();
		}, lifetime());
		(owner->chatsListLoaded(nullptr)
			? rpl::single<Data::Folder*>(nullptr)
			: owner->chatsListLoadedEvents()
		) | rpl::filter(_1 == nullptr) | rpl::map([=] {
			auto on = rpl::combine(
				controller()->activeChatsFilter(),
				_openedFolderOrForumChanges.events_starting_with(false),
				widthValue() | rpl::map(
					_1 >= st::columnMinimalWidthLeft
				) | rpl::distinct_until_changed(),
				_searchStateForTopBarSuggestion.events_starting_with(
					!_searchState.query.isEmpty()),
				_jumpToDate->toggledValue()
			) | rpl::map([=](
					FilterId id,
					bool folderOrForum,
					bool wide,
					bool search,
					bool searchInPeer) {
				return !folderOrForum
					&& wide
					&& !search
					&& !searchInPeer
					&& (id == owner->chatsFilters().defaultId());
			});
			return TopBarSuggestionValue(_innerList, &session(), std::move(on));
		}) | rpl::flatten_latest() | rpl::on_next([=](
				Ui::SlideWrap<Ui::RpWidget> *raw) {
			if (raw) {
				_topBarSuggestion = _innerList->insert(
					0,
					object_ptr<Ui::SlideWrap<Ui::RpWidget>>::fromRaw(raw));
				_topBarSuggestion->heightValue(
				) | rpl::start_to_stream(
					_topBarSuggestionHeightChanged,
					_topBarSuggestion->entity()->lifetime());
				rpl::combine(
					_topBarSuggestion->entity()->desiredHeightValue(),
					_childListShown.value()
				) | rpl::on_next([=](
						int desiredHeight,
						float64 shown) {
					const auto newHeight = desiredHeight * (1. - shown);
					_topBarSuggestion->entity()->setMaximumHeight(newHeight);
					_topBarSuggestion->entity()->setMinimumWidth((shown > 0)
						? width()
						: 0);
					_topBarSuggestion->entity()->resize(width(), newHeight);
				}, _topBarSuggestion->lifetime());
			} else {
				if (_topBarSuggestion) {
					delete _topBarSuggestion;
				}
				_topBarSuggestion = nullptr;
			}
		}, lifetime());
	});
}

void Widget::updateFrozenAccountBar() {
	if (_layout == Layout::Child
		|| _openedForum
		|| _openedFolder
		|| !session().frozen()) {
		_frozenAccountBar = nullptr;
	} else if (!_frozenAccountBar) {
		_frozenAccountBar = FrozenWriteRestriction(
			this,
			controller()->uiShow(),
			FrozenWriteRestrictionType::DialogsList);
		_frozenAccountBar->show();
	}
}

void Widget::updateTopBarSuggestions() {
	if (_topBarSuggestion) {
		_openedFolderOrForumChanges.fire(_openedFolder || _openedForum);
	}
}

void Widget::setupMoreChatsBar() {
	if (_layout == Layout::Child) {
		return;
	}
	controller()->activeChatsFilter(
	) | rpl::on_next([=](FilterId id) {
		storiesToggleExplicitExpand(false);
		const auto cancelled = cancelSearch({ .forceFullCancel = true });
		const auto guard = gsl::finally([&] {
			if (cancelled) {
				controller()->content()->dialogsCancelled();
			}
		});

		if (!id) {
			_moreChatsBar = nullptr;
			updateControlsGeometry();
			return;
		}
		const auto filters = &session().data().chatsFilters();
		_moreChatsBar = std::make_unique<Ui::MoreChatsBar>(
			this,
			filters->moreChatsContent(id));

		trackScroll(_moreChatsBar->wrap());

		_moreChatsBar->barClicks(
		) | rpl::on_next([=] {
			if (const auto missing = filters->moreChats(id)
				; !missing.empty()) {
				Api::ProcessFilterUpdate(controller(), id, missing);
			}
		}, _moreChatsBar->lifetime());

		_moreChatsBar->closeClicks(
		) | rpl::on_next([=] {
			Api::ProcessFilterUpdate(controller(), id, {});
		}, _moreChatsBar->lifetime());

		if (_showAnimation) {
			_moreChatsBar->hide();
		} else {
			_moreChatsBar->show();
			_moreChatsBar->finishAnimating();
		}

		_moreChatsBar->heightValue(
		) | rpl::on_next([=] {
			updateControlsGeometry();
		}, _moreChatsBar->lifetime());
	}, lifetime());
}

void Widget::setupDownloadBar() {
	if (_layout == Layout::Child) {
		return;
	}

	Data::MakeDownloadBarContent(
	) | rpl::on_next([=](Ui::DownloadBarContent &&content) {
		const auto create = (content.count && !_downloadBar);
		if (create) {
			_downloadBar = std::make_unique<Ui::DownloadBar>(
				this,
				Data::MakeDownloadBarProgress());
		}
		if (_downloadBar) {
			_downloadBar->show(std::move(content));
		}
		if (create) {
			_downloadBar->heightValue(
			) | rpl::on_next([=] {
				updateControlsGeometry();
			}, _downloadBar->lifetime());

			_downloadBar->shownValue(
			) | rpl::filter(
				!rpl::mappers::_1
			) | rpl::on_next([=] {
				_downloadBar = nullptr;
				updateControlsGeometry();
			}, _downloadBar->lifetime());

			_downloadBar->clicks(
			) | rpl::on_next([=] {
				auto &&list = Core::App().downloadManager().loadingList();
				const auto guard = gsl::finally([] {
					Core::App().downloadManager().clearIfFinished();
				});
				auto first = (HistoryItem*)nullptr;
				for (const auto id : list) {
					if (!first) {
						first = id->object.item;
					} else {
						controller()->showSection(
							Info::Downloads::Make(
								controller()->session().user()));
						return;
					}
				}
				if (first) {
					controller()->showMessage(first);
				}
			}, _downloadBar->lifetime());

			if (_connecting) {
				_connecting->raise();
			}
		}
	}, lifetime());
}

void Widget::setupShortcuts(not_null<Window::SessionController *> controller) {
	Shortcuts::Requests(
	) | rpl::filter([=] {
		return isActiveWindow()
		       && !controller->isLayerShown()
		       && !controller->window().locked();
	}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {
		using Command = Shortcuts::Command;

		request->check(Command::GlobalSearch) && request->handle([=] {
			if (_openedFolder) {
				controller->closeFolder();
				setInnerFocus();
				return true;
			} else {
				if (controller->adaptive().isOneColumn()) {
					controller->clearSectionStack();
					controller->showBackFromStack();
				} else {
					setInnerFocus();
				}
				return true;
			}
			return false;
		});
	}, lifetime());
}

void Widget::updateScrollUpVisibility() {
	if (_scrollToAnimation.animating()) {
		return;
	}

	startScrollUpButtonAnimation(
		(_scroll->scrollTop() > (st::historyToDownShownAfter / 2))
		&& (_scroll->scrollTop() < _scroll->scrollTopMax()));
}

void Widget::startScrollUpButtonAnimation(bool shown) {
	const auto smallColumn = (width() < st::columnMinimalWidthLeft)
		|| _childList;
	shown &= !smallColumn;
	if (_scrollToTopIsShown == shown) {
		return;
	}
	_scrollToTopIsShown = shown;
	_scrollToTopShown.start(
		[=] { updateScrollUpPosition(); },
		_scrollToTopIsShown ? 0. : 1.,
		_scrollToTopIsShown ? 1. : 0.,
		smallColumn ? 0 : st::historyToDownDuration);
}

void Widget::updateScrollUpPosition() {
	// _scrollToTop is a child widget of _scroll, not me.
	auto top = anim::interpolate(
		0,
		_scrollToTop->height() + st::connectingMargin.top(),
		_scrollToTopShown.value(_scrollToTopIsShown ? 1. : 0.));
	_scrollToTop->moveToRight(
		st::historyToDownPosition.x(),
		_scroll->height() - top);
	const auto shouldBeHidden
		= !_scrollToTopIsShown && !_scrollToTopShown.animating();
	if (shouldBeHidden != _scrollToTop->isHidden()) {
		_scrollToTop->setVisible(!shouldBeHidden);
	}
}

void Widget::setupConnectingWidget() {
	_connecting = std::make_unique<Window::ConnectionState>(
		this,
		&session().account(),
		controller()->adaptive().oneColumnValue());
}

void Widget::setupSupportMode() {
	if (!session().supportMode()) {
		return;
	}

	fullSearchRefreshOn(session().settings().supportAllSearchResultsValue(
	) | rpl::to_empty);
}

void Widget::setupMainMenuToggle() {
	_mainMenu.under->setClickedCallback([=] {
		_mainMenu.toggle->clicked({}, Qt::LeftButton);
	});
	_mainMenu.under->stackUnder(_mainMenu.toggle);
	_mainMenu.toggle->setClickedCallback([=] { showMainMenu(); });
	_mainMenu.toggle->setIsMenuButton(true);
	_mainMenu.toggle->setAccessibleName(tr::lng_main_menu(tr::now));

	rpl::single(rpl::empty) | rpl::then(
		controller()->filtersMenuChanged()
	) | rpl::on_next([=] {
		const auto filtersHidden = !controller()->filtersWidth();
		_mainMenu.toggle->setVisible(filtersHidden);
		_mainMenu.under->setVisible(filtersHidden);
		_searchForNarrowLayout->setVisible(!filtersHidden);
		updateControlsGeometry();
	}, lifetime());

	Window::OtherAccountsUnreadState(
		&controller()->session().account()
	) | rpl::on_next([=](const Window::OthersUnreadState &state) {
		const auto icon = !state.count
			? nullptr
			: !state.allMuted
			? &st::dialogsMenuToggleUnread
			: &st::dialogsMenuToggleUnreadMuted;
		_mainMenu.toggle->setIconOverride(icon, icon);
	}, _mainMenu.toggle->lifetime());
}

void Widget::setupStories() {
	if (GetEnhancedBool("hide_stories")) {
		return;
	}
	_stories->verticalScrollEvents(
	) | rpl::on_next([=](not_null<QWheelEvent*> e) {
		_scroll->viewportEvent(e);
	}, _stories->lifetime());

	if (!Core::App().settings().storiesClickTooltipHidden()) {
		// Don't create tooltip
		// until storiesClickTooltipHidden can be returned to false.
		const auto hideTooltip = [=] {
			Core::App().settings().setStoriesClickTooltipHidden(true);
			Core::App().saveSettingsDelayed();
		};
		InvokeQueued(_stories.get(), [=] {
			_stories->setShowTooltip(
				controller()->content(),
				rpl::combine(
					Core::App().settings().storiesClickTooltipHiddenValue(),
					shownValue(),
					!rpl::mappers::_1 && rpl::mappers::_2),
				hideTooltip);
		});
	}

	_storiesContents.fire(Stories::ContentForSession(
		&controller()->session(),
		Data::StorySourcesList::NotHidden));

	const auto currentSource = [=] {
		using List = Data::StorySourcesList;
		return _openedFolder ? List::Hidden : List::NotHidden;
	};

	rpl::combine(
		_scroll->positionValue(),
		_scroll->movementValue(),
		_storiesExplicitExpandValue.value()
	) | rpl::on_next([=](
			Ui::ElasticScrollPosition position,
			Ui::ElasticScrollMovement movement,
			int explicitlyExpanded) {
		if (_stories->isHidden()) {
			return;
		}
		const auto overscrollTop = std::max(-position.overscroll, 0);
		if (overscrollTop > 0 && _storiesExplicitExpand) {
			_scroll->setOverscrollDefaults(
				-st::dialogsStoriesFull.height,
				0,
				true);
		}
		if (explicitlyExpanded > 0 && explicitlyExpanded < overscrollTop) {
			_storiesExplicitExpandAnimation.stop();
			_storiesExplicitExpand = false;
			_storiesExplicitExpandValue = 0;
			return;
		}
		const auto above = std::max(explicitlyExpanded, overscrollTop);
		if (_aboveScrollAdded != above) {
			_aboveScrollAdded = above;
			if (_updateScrollGeometryCached) {
				_updateScrollGeometryCached();
			}
		}
		using Phase = Ui::ElasticScrollMovement;
		_stories->setExpandedHeight(
			_aboveScrollAdded,
			((movement == Phase::Momentum || movement == Phase::Returning)
				&& (explicitlyExpanded < above)));
		if (position.overscroll > 0
			|| (position.value
				> (_storiesExplicitExpandScrollTop
					+ st::dialogsRowHeight))) {
			storiesToggleExplicitExpand(false);
		}
		updateLockUnlockPosition();
	}, lifetime());

	_stories->collapsedGeometryChanged(
	) | rpl::on_next([=] {
		updateLockUnlockPosition();
	}, lifetime());

	_stories->clicks(
	) | rpl::on_next([=](uint64 id) {
		controller()->openPeerStories(PeerId(int64(id)), currentSource());
	}, lifetime());

	_stories->showMenuRequests(
	) | rpl::on_next([=](const Stories::ShowMenuRequest &request) {
		FillSourceMenu(controller(), request);
	}, lifetime());

	_stories->loadMoreRequests(
	) | rpl::on_next([=] {
		session().data().stories().loadMore(currentSource());
	}, lifetime());

	_stories->toggleExpandedRequests(
	) | rpl::on_next([=](bool expanded) {
		const auto position = _scroll->position();
		if (!expanded) {
			_scroll->setOverscrollDefaults(0, 0);
		} else if (position.value > 0 || position.overscroll >= 0) {
			storiesToggleExplicitExpand(true);
			_scroll->setOverscrollDefaults(0, 0);
		} else {
			_scroll->setOverscrollDefaults(
				-st::dialogsStoriesFull.height,
				0);
		}
	}, lifetime());

	_stories->emptyValue() | rpl::skip(1) | rpl::on_next([=] {
		updateStoriesVisibility();
	}, lifetime());

	_stories->widthValue() | rpl::on_next([=] {
		updateLockUnlockPosition();
	}, lifetime());
}

void Widget::storiesToggleExplicitExpand(bool expand) {
	if (GetEnhancedBool("hide_stories")) {
		return;
	}
	if (_storiesExplicitExpand == expand) {
		return;
	}
	_storiesExplicitExpand = expand;
	if (!expand) {
		_scroll->setOverscrollDefaults(0, 0, true);
	}
	const auto height = st::dialogsStoriesFull.height;
	const auto duration = kStoriesExpandDuration;
	_storiesExplicitExpandScrollTop = _scroll->position().value;
	_storiesExplicitExpandAnimation.start([=](float64 value) {
		_storiesExplicitExpandValue = int(base::SafeRound(value));
	}, expand ? 0 : height, expand ? height : 0, duration, anim::sineInOut);
}

void Widget::trackScroll(not_null<Ui::RpWidget*> widget) {
	widget->events(
	) | rpl::on_next([=](not_null<QEvent*> e) {
		const auto type = e->type();
		if (type == QEvent::TouchBegin
			|| type == QEvent::TouchUpdate
			|| type == QEvent::TouchEnd
			|| type == QEvent::TouchCancel
			|| type == QEvent::Wheel) {
			_scroll->viewportEvent(e);
		}
	}, widget->lifetime());
}

void Widget::setupShortcuts() {
	Shortcuts::Requests(
	) | rpl::filter([=] {
		return isActiveWindow()
			&& Ui::InFocusChain(this)
			&& !_childList
			&& !controller()->isLayerShown()
			&& !controller()->window().locked();
	}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {
		using Command = Shortcuts::Command;

		if (!controller()->activeChatCurrent()) {
			request->check(Command::Search) && request->handle([=] {
				if (const auto forum = _openedForum) {
					const auto history = forum->history();
					controller()->searchInChat(history);
					return true;
				} else if (!_openedFolder
					&& !_childList
					&& _search->isVisible()) {
					_search->setFocus();
					return true;
				}
				return false;
			});
			request->check(Command::ShowChatMenu, 1) && request->handle([=] {
				if (_inner) {
					Window::ActivateWindow(controller());
					_inner->showPeerMenu();
				}
				return true;
			});
			request->check(Command::ShowChatPreview, 1)
			&& request->handle([=] {
				if (_inner) {
					Window::ActivateWindow(controller());
					return _inner->showChatPreview();
				}
				return true;
			});
		}
	}, lifetime());
}

void Widget::fullSearchRefreshOn(rpl::producer<> events) {
	std::move(
		events
	) | rpl::filter([=] {
		return !_searchQuery.isEmpty();
	}) | rpl::on_next([=] {
		_searchTimer.cancel();
		_searchProcess.cache.clear();
		const auto queries = base::take(_searchProcess.queries);
		for (const auto &[requestId, query] : queries) {
			session().api().request(requestId).cancel();
		}
		_singleMessageSearch.clear();
		_searchQuery = QString();
		_scroll->scrollToY(0);
		cancelSearchRequest();
		search();
	}, lifetime());
}

void Widget::updateControlsVisibility(bool fast) {
	updateLoadMoreChatsVisibility();
	_scroll->setVisible(!_suggestions && _hidingSuggestions.empty());
	updateStoriesVisibility();
	if ((_openedFolder || _openedForum) && _searchHasFocus) {
		setInnerFocus();
	}
	if (_updateTelegram) {
		_updateTelegram->show();
	}
	_searchControls->setVisible(!_openedFolder && !_openedForum);
	if (_moreChatsBar) {
		_moreChatsBar->show();
	}
	if (_frozenAccountBar) {
		_frozenAccountBar->show();
	}
	if (_chatFilters) {
		_chatFilters->setVisible(!_openedForum);
	}
	if (_openedFolder || _openedForum) {
		_subsectionTopBar->show();
		if (_forumTopShadow) {
			_forumTopShadow->show();
		}
		if (_forumGroupCallBar) {
			_forumGroupCallBar->show();
		}
		if (_forumRequestsBar) {
			_forumRequestsBar->show();
		}
		if (_forumReportBar) {
			_forumReportBar->show();
		}
	} else {
		updateLockUnlockVisibility(fast
			? anim::type::instant
			: anim::type::normal);
		updateJumpToDateVisibility(fast);
		updateSearchFromVisibility(fast);
	}
	if (_connecting) {
		_connecting->setForceHidden(false);
	}
	if (_childList) {
		_childList->show();
		_childListShadow->show();
	}
	if (_hideChildListCanvas) {
		_hideChildListCanvas->show();
	}
	if (_childList && _searchHasFocus) {
		setInnerFocus();
	}
	updateLockUnlockPosition();
}

void Widget::updateLockUnlockPosition() {
	if (_lockUnlock->isHidden()) {
		return;
	}
	const auto stories = (_stories && !_stories->isHidden())
		? _stories->collapsedGeometryCurrent()
		: Stories::List::CollapsedGeometry();
	const auto simple = _search->x() + _search->width();
	const auto right = stories.geometry.isEmpty()
		? simple
		: anim::interpolate(stories.geometry.x(), simple, stories.expanded);
	_lockUnlock->move(
		right - _lockUnlock->width(),
		st::dialogsFilterPadding.y());
}

void Widget::updateHasFocus(not_null<QWidget*> focused) {
	const auto has = (focused == _search.data())
		|| (focused == _search->rawTextEdit());
	if (_searchHasFocus != has) {
		_searchHasFocus = has;
		if (_postponeProcessSearchFocusChange) {
			return;
		} else if (has) {
			processSearchFocusChange();
		} else {
			// Search field may loose focus from the destructor of some
			// widget, in that case we don't want to destroy _suggestions
			// synchronously, because it may lead to a crash.
			crl::on_main(this, [=] { processSearchFocusChange(); });
		}
	}
}

void Widget::toggleFiltersMenu(bool enabled) {
	if (_layout == Layout::Child) {
		enabled = false;
	}
	if (const auto id = controller()->windowId(); id.forum() || id.folder()) {
		enabled = false;
	}
	if (!enabled == !_chatFilters) {
		return;
	} else if (enabled) {
		class NoScrollPropagationWidget final : public Ui::RpWidget {
		public:
			using Ui::RpWidget::RpWidget;

		protected:
			void touchEvent(QTouchEvent *e) {
				e->accept();
			}
			void wheelEvent(QWheelEvent *e) override final {
				e->accept();
			}

		};

		_chatFilters = base::make_unique_q<NoScrollPropagationWidget>(this);
		const auto raw = _chatFilters.get();
		const auto idBeforeTabs = controller()->activeChatsFilterCurrent();
		const auto inner = Ui::AddChatFiltersTabsStrip(
			_chatFilters.get(),
			&session(),
			[this](FilterId id) {
				_scroll->scrollToY(0);
				if (controller()->activeChatsFilterCurrent() != id) {
					controller()->setActiveChatsFilter(id);
				}
			},
			Window::GifPauseReason::Any,
			controller(),
			true);
		if (controller()->activeChatsFilterCurrent() != idBeforeTabs) {
			controller()->setActiveChatsFilter(idBeforeTabs);
		}
		raw->show();
		raw->stackUnder(_scroll);
		raw->resizeToWidth(width());
		const auto shadow = Ui::CreateChild<Ui::PlainShadow>(raw);
		shadow->show();
		inner->sizeValue() | rpl::on_next([=, this](const QSize &s) {
			raw->resize(s);
			shadow->setGeometry(
				0,
				s.height() - shadow->height(),
				s.width(),
				shadow->height());
			updateControlsGeometry();
		}, _chatFilters->lifetime());
		updateControlsGeometry();
	} else {
		_chatFilters = nullptr;
	}
}

bool Widget::cancelSearchByMouseBack() {
	return _searchHasFocus
		&& !_searchSuggestionsLocked
		&& !_searchState.inChat
		&& cancelSearch({ .jumpBackToSearchedChat = true });
}

void Widget::processSearchFocusChange() {
	_searchSuggestionsLocked = _suggestions && _suggestions->persist();
	updateCancelSearch();
	updateForceDisplayWide();
	updateSuggestions(anim::type::normal);
}

void Widget::updateSuggestions(anim::type animated) {
	const auto suggest = (_searchHasFocus || _searchSuggestionsLocked)
		&& !_searchState.inChat
		&& (_inner->state() == WidgetState::Default);
	if (anim::Disabled() || !session().data().chatsListLoaded()) {
		animated = anim::type::instant;
	}
	if (!suggest && _suggestions) {
		if (animated == anim::type::normal) {
			auto taken = base::take(_suggestions);
			taken->setVisible(false);
			storiesExplicitCollapse();
			updateStoriesVisibility();
			startWidthAnimation();
			taken->setVisible(true);
			_suggestions = base::take(taken);

			_suggestions->hide(animated, [=, raw = _suggestions.get()] {
				stopWidthAnimation();
				_hidingSuggestions.erase(
					ranges::remove(
						_hidingSuggestions,
						raw,
						&std::unique_ptr<Suggestions>::get),
					end(_hidingSuggestions));
				updateControlsVisibility();
			});
			_hidingSuggestions.push_back(std::move(_suggestions));
		} else {
			_suggestions = nullptr;
			_hidingSuggestions.clear();
			storiesExplicitCollapse();
			updateControlsVisibility();
			_scroll->show();
		}
	} else if (suggest && !_suggestions) {
		if (animated == anim::type::normal) {
			startWidthAnimation();
		}
		// Hides stories and passcode lock.
		updateStoriesVisibility();
		_suggestions = std::make_unique<Suggestions>(
			this,
			controller(),
			TopPeersContent(&session()),
			RecentPeersContent(&session()));
		_suggestions->clearSearchQueryRequests() | rpl::on_next([=] {
			setSearchQuery(QString());
		}, _suggestions->lifetime());
		_searchSuggestionsLocked = false;

		rpl::merge(
			_suggestions->topPeerChosen(),
			_suggestions->recentPeerChosen(),
			_suggestions->myChannelChosen(),
			_suggestions->recommendationChosen()
		) | rpl::on_next([=](not_null<PeerData*> peer) {
			if (_searchSuggestionsLocked
				&& (!_suggestions || !_suggestions->persist())) {
				processSearchFocusChange();
			}
			chosenRow({
				.key = peer->owner().history(peer),
				.newWindow = base::IsCtrlPressed(),
			});
			if (!_searchSuggestionsLocked && _searchHasFocus) {
				setFocus();
				controller()->widget()->setInnerFocus();
			}
		}, _suggestions->lifetime());

		rpl::merge(
			_suggestions->openBotMainAppRequests(),
			_suggestions->recentAppChosen()
		) | rpl::on_next([=](not_null<PeerData*> peer) {
			if (const auto user = peer->asUser()) {
				if (const auto info = user->botInfo.get()) {
					if (info->hasMainApp) {
						openBotMainApp(user);
						return;
					}
				}
			}
			chosenRow({
				.key = peer->owner().history(peer),
				.newWindow = base::IsCtrlPressed(),
			});
		}, _suggestions->lifetime());

		_suggestions->popularAppChosen(
		) | rpl::on_next([=](not_null<PeerData*> peer) {
			controller()->showPeerInfo(peer);
		}, _suggestions->lifetime());

		_suggestions->closeRequests() | rpl::on_next([=] {
			closeSuggestions();
		}, _suggestions->lifetime());

		updateControlsGeometry();

		_suggestions->show(animated, [=] {
			stopWidthAnimation();
		});
		_scroll->hide();
	} else {
		updateStoriesVisibility();
	}
}

void Widget::closeSuggestions() {
	_searchSuggestionsLocked = false;
	_searchHasFocus = false;
	setFocus();
	updateForceDisplayWide();
	updateCancelSearch();
	updateSuggestions(anim::type::normal);
}

void Widget::openBotMainApp(not_null<UserData*> bot) {
	session().attachWebView().open({
		.bot = bot,
		.context = {
			.controller = controller(),
			.maySkipConfirmation = true,
		},
		.source = InlineBots::WebViewSourceBotProfile(),
	});
}

void Widget::changeOpenedSubsection(
		FnMut<void()> change,
		bool fromRight,
		anim::type animated) {
	if (isHidden()) {
		animated = anim::type::instant;
	}
	auto oldContentCache = QPixmap();
	const auto showDirection = fromRight
		? Window::SlideDirection::FromRight
		: Window::SlideDirection::FromLeft;
	if (animated == anim::type::normal) {
		if (_connecting) {
			_connecting->setForceHidden(true);
		}
		oldContentCache = grabForFolderSlideAnimation();
	}
	//_scroll->verticalScrollBar()->setMinimum(0);
	_showAnimation = nullptr;
	destroyChildListCanvas();
	change();
	refreshTopBars();
	updateControlsVisibility(true);
	_peerSearch.clear();
	_api.request(base::take(_topicSearchRequest)).cancel();
	if (animated == anim::type::normal) {
		if (_connecting) {
			_connecting->setForceHidden(true);
		}
		auto newContentCache = grabForFolderSlideAnimation();
		if (_connecting) {
			_connecting->setForceHidden(false);
		}
		startSlideAnimation(
			std::move(oldContentCache),
			std::move(newContentCache),
			showDirection);
	}
}

void Widget::destroyChildListCanvas() {
	_childListShown = 0.;
	_hideChildListCanvas = nullptr;
}

void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) {
	if (_openedFolder == folder) {
		return;
	}
	changeOpenedSubsection([&] {
		cancelSearch({ .forceFullCancel = true });
		closeChildList(anim::type::instant);
		controller()->closeForum();
		_openedFolder = folder;
		_inner->changeOpenedFolder(folder);
		if (!GetEnhancedBool("hide_stories") && _stories) {
			storiesExplicitCollapse();
		}
		updateFrozenAccountBar();
		updateTopBarSuggestions();
	}, (folder != nullptr), animated);
}

void Widget::storiesExplicitCollapse() {
	if (_storiesExplicitExpand) {
		storiesToggleExplicitExpand(false);
	} else if (_stories) {
		using Type = Ui::ElasticScroll::OverscrollType;
		_scroll->setOverscrollDefaults(0, 0);
		_scroll->setOverscrollTypes(Type::None, Type::Real);
		_scroll->setOverscrollTypes(
			_stories->isHidden() ? Type::Real : Type::Virtual,
			Type::Real);
	}
	_storiesExplicitExpandAnimation.stop();
	_storiesExplicitExpandValue = 0;

	using List = Data::StorySourcesList;
	collectStoriesUserpicsViews(_openedFolder
		? List::NotHidden
		: List::Hidden);
	_storiesContents.fire(Stories::ContentForSession(
		&session(),
		_openedFolder ? List::Hidden : List::NotHidden));
}

void Widget::collectStoriesUserpicsViews(Data::StorySourcesList list) {
	auto &map = (list == Data::StorySourcesList::Hidden)
		? _storiesUserpicsViewsHidden
		: _storiesUserpicsViewsShown;
	map.clear();
	auto &owner = session().data();
	for (const auto &source : owner.stories().sources(list)) {
		if (const auto peer = owner.peerLoaded(source.id)) {
			if (auto view = peer->activeUserpicView(); view.cloud) {
				map.emplace(source.id, std::move(view));
			}
		}
	}
}

void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) {
	if (_openedForum == forum) {
		return;
	}
	changeOpenedSubsection([&] {
		cancelSearch({ .forceFullCancel = true });
		closeChildList(anim::type::instant);
		_openedForum = forum;
		_searchState.tab = forum
			? ChatSearchTab::ThisPeer
			: ChatSearchTab::MyMessages;
		_searchWithPostsPreview = computeSearchWithPostsPreview();
		_api.request(base::take(_topicSearchRequest)).cancel();
		_inner->changeOpenedForum(forum);
		storiesToggleExplicitExpand(false);
		updateFrozenAccountBar();
		updateTopBarSuggestions();
		updateStoriesVisibility();
	}, (forum != nullptr), animated);
}

void Widget::hideChildList() {
	if (_childList) {
		controller()->closeForum();
	}
}

void Widget::refreshTopBars() {
	if (_openedFolder || _openedForum) {
		if (!_subsectionTopBar) {
			_subsectionTopBar.create(this, controller());
			if (_stories) {
				_stories->raise();
			}
			_subsectionTopBar->searchCancelled(
			) | rpl::on_next([=] {
				escape();
			}, _subsectionTopBar->lifetime());
			_subsectionTopBar->searchSubmitted(
			) | rpl::on_next([=] {
				submit();
			}, _subsectionTopBar->lifetime());
			_subsectionTopBar->searchQuery(
			) | rpl::on_next([=](QString query) {
				applySearchUpdate();
			}, _subsectionTopBar->lifetime());
			_subsectionTopBar->jumpToDateRequest(
			) | rpl::on_next([=] {
				showCalendar();
			}, _subsectionTopBar->lifetime());
			_subsectionTopBar->chooseFromUserRequest(
			) | rpl::on_next([=] {
				showSearchFrom();
			}, _subsectionTopBar->lifetime());
			updateControlsGeometry();
		}
		const auto history = _openedForum
			? _openedForum->history().get()
			: nullptr;
		_subsectionTopBar->setActiveChat(
			HistoryView::TopBarWidget::ActiveChat{
				.key = (_openedForum
					? Dialogs::Key(history)
					: Dialogs::Key(_openedFolder)),
				.section = Dialogs::EntryState::Section::ChatsList,
			}, history ? history->sendActionPainter() : nullptr);
		if (_forumSearchRequested) {
			showSearchInTopBar(anim::type::instant);
		}
	} else if (_subsectionTopBar) {
		if (_subsectionTopBar->searchHasFocus()) {
			setFocus();
		}
		_subsectionTopBar.destroy();
		updateSearchFromVisibility(true);
	}
	_forumSearchRequested = false;
	if (_openedForum && _openedForum->peer()->isChannel()) {
		const auto peer = _openedForum->peer();
		peer->updateFull();

		_forumReportBar = std::make_unique<HistoryView::ContactStatus>(
			controller(),
			this,
			peer,
			true);
		_forumRequestsBar = std::make_unique<Ui::RequestsBar>(
			this,
			HistoryView::RequestsBarContentByPeer(
				peer,
				st::historyRequestsUserpics.size,
				true));
		_forumGroupCallBar = std::make_unique<Ui::GroupCallBar>(
			this,
			HistoryView::GroupCallBarContentByPeer(
				peer,
				st::historyGroupCallUserpics.size,
				true),
			Core::App().appDeactivatedValue());
		_forumTopShadow = std::make_unique<Ui::PlainShadow>(this);

		_forumRequestsBar->barClicks(
		) | rpl::on_next([=] {
			RequestsBoxController::Start(controller(), peer);
		}, _forumRequestsBar->lifetime());

		rpl::merge(
			_forumGroupCallBar->barClicks(),
			_forumGroupCallBar->joinClicks()
		) | rpl::on_next([=] {
			if (peer->groupCall()) {
				controller()->startOrJoinGroupCall(peer);
			}
		}, _forumGroupCallBar->lifetime());

		if (_showAnimation) {
			_forumTopShadow->hide();
			_forumGroupCallBar->hide();
			_forumRequestsBar->hide();
			_forumReportBar->bar().hide();
		} else {
			_forumTopShadow->show();
			_forumGroupCallBar->show();
			_forumRequestsBar->show();
			_forumReportBar->show();
			_forumGroupCallBar->finishAnimating();
			_forumRequestsBar->finishAnimating();
		}

		rpl::combine(
			_forumGroupCallBar->heightValue(),
			_forumRequestsBar->heightValue(),
			_forumReportBar->bar().heightValue()
		) | rpl::on_next([=] {
			updateControlsGeometry();
		}, _forumRequestsBar->lifetime());
	} else {
		_forumTopShadow = nullptr;
		_forumGroupCallBar = nullptr;
		_forumRequestsBar = nullptr;
		_forumReportBar = nullptr;
		updateControlsGeometry();
	}
}

void Widget::showSearchInTopBar(anim::type animated) {
	Expects(_subsectionTopBar != nullptr);

	_subsectionTopBar->toggleSearch(true, animated);
	updateForceDisplayWide();
}

QPixmap Widget::grabForFolderSlideAnimation() {
	const auto hidden = _scrollToTop->isHidden();
	if (!hidden) {
		_scrollToTop->hide();
	}

	const auto rect = QRect(0, 0, width(), rect::bottom(_scroll));
	auto result = Ui::GrabWidget(this, rect);

	if (!hidden) {
		_scrollToTop->show();
	}
	return result;
}

void Widget::checkUpdateStatus() {
	Expects(!Core::UpdaterDisabled());

	if (_layout == Layout::Child) {
		return;
	}

	using Checker = Core::UpdateChecker;
	if (Checker().state() == Checker::State::Ready) {
		if (_updateTelegram) {
			return;
		}
		_updateTelegram.create(
			this,
			tr::lng_update_telegram(tr::now),
			st::dialogsUpdateButton,
			st::dialogsInstallUpdate,
			st::dialogsInstallUpdateOver,
			true);
		_updateTelegram->show();
		_updateTelegram->setClickedCallback([] {
			Core::checkReadyUpdate();
			Core::Restart();
		});
		if (_connecting) {
			_connecting->raise();
		}
	} else {
		if (!_updateTelegram) {
			return;
		}
		_updateTelegram.destroy();
	}
	updateControlsGeometry();
}

void Widget::setInnerFocus(bool unfocusSearch) {
	if (_childList) {
		_childList->setInnerFocus();
	} else if (_subsectionTopBar && _subsectionTopBar->searchSetFocus()) {
		return;
	} else if (!unfocusSearch
		&& (!_search->getLastText().isEmpty()
			|| _searchState.inChat
			|| _searchHasFocus
			|| _searchSuggestionsLocked)) {
		_search->setFocus();
	} else {
		setFocus();
	}
}

bool Widget::searchHasFocus() const {
	return _searchHasFocus;
}

Data::Forum *Widget::openedForum() const {
	return _openedForum;
}

void Widget::jumpToTop(bool belowPinned) {
	if (session().supportMode()) {
		return;
	}
	if ((_searchState.query.trimmed().isEmpty() && !_searchState.inChat)) {
		auto to = 0;
		if (belowPinned) {
			const auto list = _openedForum
				? _openedForum->topicsList()
				: controller()->activeChatsFilterCurrent()
				? session().data().chatsFilters().chatsList(
					controller()->activeChatsFilterCurrent())
				: session().data().chatsList(_openedFolder);
			const auto count = int(list->pinned()->order().size());
			const auto row = _inner->st()->height;
			const auto min = (row * (count * 2 + 1) - _scroll->height()) / 2;
			if (_scroll->scrollTop() <= min) {
				return;
			}
			// Don't jump too high up, below the pinned chats.
			to = std::max(min, to);
		}
		_scrollToAnimation.stop();
		_scroll->scrollToY(to);
	}
}

void Widget::raiseWithTooltip() {
	raise();
	if (_stories) {
		Ui::PostponeCall(this, [=] {
			if (_stories) _stories->raiseTooltip();
		});
	}
}

void Widget::scrollToDefault(bool verytop) {
	if (verytop) {
		//_scroll->verticalScrollBar()->setMinimum(0);
	}
	_scrollToAnimation.stop();
	auto scrollTop = _scroll->scrollTop();
	const auto scrollTo = 0;
	if (scrollTop == scrollTo) {
		return;
	}
	const auto maxAnimatedDelta = _scroll->height();
	if (scrollTo + maxAnimatedDelta < scrollTop) {
		scrollTop = scrollTo + maxAnimatedDelta;
		_scroll->scrollToY(scrollTop);
	}

	startScrollUpButtonAnimation(false);

	const auto scroll = [=] {
		const auto animated = qRound(_scrollToAnimation.value(scrollTo));
		const auto animatedDelta = animated - scrollTo;
		const auto realDelta = _scroll->scrollTop() - scrollTo;
		if (base::OppositeSigns(realDelta, animatedDelta)) {
			// We scrolled manually to the other side of target 'scrollTo'.
			_scrollToAnimation.stop();
		} else if (std::abs(realDelta) > std::abs(animatedDelta)) {
			// We scroll by animation only if it gets us closer to target.
			_scroll->scrollToY(animated);
		}
	};

	_scrollAnimationTo = scrollTo;
	_scrollToAnimation.start(
		scroll,
		scrollTop,
		scrollTo,
		st::slideDuration,
		anim::sineInOut);
}

[[nodiscard]] QPixmap Widget::grabNonNarrowScrollFrame() {
	auto scrollGeometry = _scroll->geometry();
	const auto top = _searchControls->y() + _searchControls->height();
	const auto skip = scrollGeometry.y() - top;
	auto wideGeometry = QRect(
		scrollGeometry.x(),
		scrollGeometry.y(),
		std::max(scrollGeometry.width(), st::columnMinimalWidthLeft),
		scrollGeometry.height());
	_scroll->setGeometry(wideGeometry);
	_inner->resize(wideGeometry.width(), _inner->height());
	_inner->setNarrowRatio(0.);
	Ui::SendPendingMoveResizeEvents(_scroll);
	const auto grabSize = QSize(
		wideGeometry.width(),
		skip + wideGeometry.height());
	auto image = QImage(
		grabSize * style::DevicePixelRatio(),
		QImage::Format_ARGB32_Premultiplied);
	image.setDevicePixelRatio(style::DevicePixelRatio());
	image.fill(Qt::transparent);
	{
		QPainter p(&image);
		Ui::RenderWidget(
			p,
			this,
			QPoint(),
			QRect(0, top, wideGeometry.width(), skip));
		if (_chatFilters) {
			Ui::RenderWidget(
				p,
				_chatFilters,
				QPoint(0, skip - _chatFilters->height()));
		}
		Ui::RenderWidget(p, _scroll, QPoint(0, skip));
	}
	if (scrollGeometry != wideGeometry) {
		_scroll->setGeometry(scrollGeometry);
		updateControlsGeometry();
	}
	return Ui::PixmapFromImage(std::move(image));
}

void Widget::startWidthAnimation() {
	if (!_widthAnimationCache.isNull()) {
		return;
	}
	_widthAnimationCache = grabNonNarrowScrollFrame();
	_scroll->hide();
	if (_frozenAccountBar) {
		_frozenAccountBar->hide();
	}
	if (_chatFilters) {
		_chatFilters->hide();
	}
	updateStoriesVisibility();
}

void Widget::stopWidthAnimation() {
	_widthAnimationCache = QPixmap();
	if (!_showAnimation) {
		_scroll->setVisible(!_suggestions);
		if (_frozenAccountBar) {
			_frozenAccountBar->setVisible(!_suggestions);
		}
		if (_chatFilters) {
			_chatFilters->setVisible(!_suggestions);
		}
	}
	updateStoriesVisibility();
	update();
}

void Widget::updateStoriesVisibility() {
	if (GetEnhancedBool("hide_stories")) {
		_stories = nullptr;
	}
	
	updateLockUnlockVisibility(anim::type::normal);
	if (!_stories) {
		return;
	}
	const auto widthAnimation = !_widthAnimationCache.isNull();
	const auto suggestionsAnimation = widthAnimation
		&& (!_suggestions || !_hidingSuggestions.empty());
	const auto hiddenInstant = _showAnimation
		|| _openedForum
		|| (widthAnimation && !suggestionsAnimation)
		|| _childList
		|| _stories->empty()
		|| (_scroll->position().overscroll < -st::dialogsFilterSkip);
	const auto hiddenAnimated = _searchHasFocus
		|| _searchSuggestionsLocked
		|| !_searchState.query.isEmpty()
		|| _searchState.inChat
		|| suggestionsAnimation;
	const auto hidden = hiddenInstant || hiddenAnimated;
	const auto changed = (_stories->toggledHidden() != hidden);
	_stories->setToggledHidden(hiddenInstant, hiddenAnimated);
	if (changed) {
		using Type = Ui::ElasticScroll::OverscrollType;
		if (hidden) {
			_scroll->setOverscrollDefaults(0, 0);
			_scroll->setOverscrollTypes(Type::Real, Type::Real);
			if (_scroll->position().overscroll < 0) {
				_scroll->scrollToY(0);
			}
			_scroll->update();
		} else {
			_scroll->setOverscrollDefaults(0, 0);
			_scroll->setOverscrollTypes(Type::Virtual, Type::Real);
			_storiesExplicitExpandValue.force_assign(
				_storiesExplicitExpandValue.current());
		}
		if (_aboveScrollAdded > 0 && _updateScrollGeometryCached) {
			_updateScrollGeometryCached();
		}
		updateLockUnlockPosition();
	}
}

void Widget::showFast() {
	if (isHidden()) {
		_inner->clearSelection();
	}
	show();
}

rpl::producer<float64> Widget::shownProgressValue() const {
	return _shownProgressValue.value();
}

void Widget::showAnimated(
		Window::SlideDirection direction,
		const Window::SectionSlideParams &params) {
	_showAnimation = nullptr;

	auto oldContentCache = params.oldContentCache;
	showFast();
	auto newContentCache = Ui::GrabWidget(this);

	if (_updateTelegram) {
		_updateTelegram->hide();
	}
	if (_connecting) {
		_connecting->setForceHidden(true);
	}
	if (_childList) {
		_childList->hide();
		_childListShadow->hide();
	}
	_shownProgressValue = 0.;
	startSlideAnimation(
		std::move(oldContentCache),
		std::move(newContentCache),
		direction);
}

void Widget::startSlideAnimation(
		QPixmap oldContentCache,
		QPixmap newContentCache,
		Window::SlideDirection direction) {
	_scroll->hide();
	if (_stories) {
		_stories->setToggledHidden(true, false);
	}
	_searchControls->hide();
	if (_subsectionTopBar) {
		_subsectionTopBar->hide();
	}
	if (_moreChatsBar) {
		_moreChatsBar->hide();
	}
	if (_frozenAccountBar) {
		_frozenAccountBar->hide();
	}
	if (_chatFilters) {
		_chatFilters->hide();
	}
	if (_forumTopShadow) {
		_forumTopShadow->hide();
	}
	if (_forumGroupCallBar) {
		_forumGroupCallBar->hide();
	}
	if (_forumRequestsBar) {
		_forumRequestsBar->hide();
	}
	if (_forumReportBar) {
		_forumReportBar->bar().hide();
	}

	_showAnimation = std::make_unique<Window::SlideAnimation>();
	_showAnimation->setDirection(direction);
	_showAnimation->setRepaintCallback([=] {
		if (_shownProgressValue.current() < 1.) {
			_shownProgressValue = _showAnimation->progress();
		}
		update();
	});
	_showAnimation->setFinishedCallback([=] { slideFinished(); });
	_showAnimation->setPixmaps(oldContentCache, newContentCache);
	_showAnimation->start();
}

bool Widget::floatPlayerHandleWheelEvent(QEvent *e) {
	return _scroll->viewportEvent(e);
}

QRect Widget::floatPlayerAvailableRect() {
	return mapToGlobal(_scroll->geometry());
}

void Widget::slideFinished() {
	_showAnimation = nullptr;
	_shownProgressValue = 1.;
	updateControlsVisibility(true);
	if ((!_subsectionTopBar || !_subsectionTopBar->searchHasFocus())
		&& !_searchHasFocus) {
		controller()->widget()->setInnerFocus();
	}
}

void Widget::escape() {
	if (!cancelSearch({ .jumpBackToSearchedChat = true })) {
		if (const auto forum = controller()->shownForum().current()) {
			const auto id = controller()->windowId();
			const auto initial = id.forum();
			if (!initial) {
				controller()->closeForum();
			} else if (initial != forum) {
				controller()->showForum(initial);
			}
		} else if (controller()->openedFolder().current()) {
			if (!controller()->windowId().folder()) {
				controller()->closeFolder();
			}
		} else if (controller()->activeChatEntryCurrent().key) {
			controller()->content()->dialogsCancelled();
		} else if (controller()->isPrimary()) {
			const auto filters = &session().data().chatsFilters();
			const auto &list = filters->list();
			const auto first = list.empty() ? FilterId() : list.front().id();
			if (controller()->activeChatsFilterCurrent() != first) {
				controller()->setActiveChatsFilter(first);
			}
		}
	} else if (!_searchState.inChat
		&& controller()->activeChatEntryCurrent().key) {
		controller()->content()->dialogsCancelled();
	}
}

void Widget::submit() {
	if (_suggestions) {
		_suggestions->chooseRow();
		return;
	} else if (_inner->chooseRow()) {
		return;
	}
	const auto state = _inner->state();
	if (state == WidgetState::Default
		|| (state == WidgetState::Filtered
			&& _inner->hasFilteredResults())) {
		_inner->selectSkip(1);
		_inner->chooseRow();
	} else {
		search();
	}
}

void Widget::refreshLoadMoreButton(bool mayBlock, bool isBlocked) {
	if (_layout == Layout::Child) {
		return;
	}

	if (!mayBlock) {
		if (_loadMoreChats) {
			_loadMoreChats.destroy();
			updateControlsGeometry();
		}
		return;
	}
	if (!_loadMoreChats) {
		_loadMoreChats.create(
			this,
			"Load more",
			st::dialogsLoadMoreButton,
			st::dialogsLoadMore,
			st::dialogsLoadMore,
			false);
		_loadMoreChats->show();
		_loadMoreChats->addClickHandler([=] {
			loadMoreBlockedByDate();
		});
		updateControlsGeometry();
	}
	const auto loading = !isBlocked;
	_loadMoreChats->setDisabled(loading);
	_loadMoreChats->setText(loading ? "Loading..." : "Load more");
}

void Widget::loadMoreBlockedByDate() {
	if (!_loadMoreChats
		|| _loadMoreChats->isDisabled()
		|| _loadMoreChats->isHidden()) {
		return;
	}
	session().api().requestMoreBlockedByDateDialogs();
}

bool Widget::search(bool inCache, SearchRequestDelay delay) {
	_processingSearch = true;
	const auto guard = gsl::finally([&] {
		_processingSearch = false;
		listScrollUpdated();
	});

	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();
	const auto tab = _searchState.tab;
	const auto filter = _searchState.filter;
	const auto fromStartType = SearchRequestType{
		.start = true,
		.peer = (inPeer != nullptr),
	};
	if (trimmed.isEmpty() && !fromPeer && inTags.empty()) {
		cancelSearchRequest();

		// Otherwise inside first searchApplyEmpty we call searchMode(),
		// which tries to load migrated search results for empty query.
		_migratedProcess.full = true;

		searchApplyEmpty(fromStartType, currentSearchProcess());
		if (_searchInMigrated) {
			const auto type = SearchRequestType{
				.migrated = true,
				.start = true,
			};
			searchApplyEmpty(type, &_migratedProcess);
		}
		if (_searchWithPostsPreview) {
			searchApplyEmpty(
				{ .posts = true, .start = true },
				&_postsProcess);
		}
		_peerSearch.clear();
		_api.request(base::take(_topicSearchRequest)).cancel();
		peerSearchReceived({});
		return true;
	} else if (inCache) {
		const auto success = _singleMessageSearch.lookup(query, [=] {
			searchRequested(delay);
		});
		if (!success) {
			return false;
		}
		const auto process = currentSearchProcess();
		const auto i = process->cache.find(query);
		if (i != process->cache.end()) {
			_searchQuery = query;
			_searchQueryFrom = fromPeer;
			_searchQueryTags = inTags;
			_searchQueryTab = tab;
			_searchQueryFilter = filter;
			process->nextRate = 0;
			process->full = false;
			_migratedProcess.full = false;
			cancelSearchRequest();
			searchReceived(fromStartType, i->second, process, true);
			result = true;
		}
	} else if (_searchQuery != query
		|| _searchQueryFrom != fromPeer
		|| _searchQueryTags != inTags
		|| _searchQueryTab != tab
		|| _searchQueryFilter != filter) {
		const auto process = currentSearchProcess();
		_searchQuery = query;
		_searchQueryFrom = fromPeer;
		_searchQueryTags = inTags;
		_searchQueryTab = tab;
		_searchQueryFilter = filter;
		process->nextRate = 0;
		process->full = false;
		_migratedProcess.full = false;
		cancelSearchRequest();
		if (inPeer) {
			const auto topic = searchInTopic();
			auto &histories = session().data().histories();
			const auto type = Data::Histories::RequestType::History;
			const auto history = session().data().history(inPeer);
			const auto sublist = _openedForum
				? nullptr
				: _searchState.inChat.sublist();
			const auto fromPeer = sublist ? nullptr : _searchQueryFrom;
			const auto savedPeer = sublist
				? sublist->sublistPeer().get()
				: nullptr;
			_historiesRequest = histories.sendRequest(history, type, [=](
					Fn<void()> finish) {
				const auto type = SearchRequestType{
					.start = true,
					.peer = true,
				};
				using Flag = MTPmessages_Search::Flag;
				process->requestId = session().api().request(
					MTPmessages_Search(
						MTP_flags((topic ? Flag::f_top_msg_id : Flag())
							| (fromPeer ? Flag::f_from_id : Flag())
							| (savedPeer ? Flag::f_saved_peer_id : Flag())
							| (_searchQueryTags.empty()
								? Flag()
								: Flag::f_saved_reaction)),
						inPeer->input(),
						MTP_string(_searchQuery),
						(fromPeer ? fromPeer->input() : MTP_inputPeerEmpty()),
						(savedPeer ? savedPeer->input() : MTP_inputPeerEmpty()),
						MTP_vector_from_range(
							_searchQueryTags | ranges::views::transform(
								Data::ReactionToMTP
							)),
						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(kSearchPerPage),
						MTP_int(0), // max_id
						MTP_int(0), // min_id
						MTP_long(0)) // hash
				).done([=](const MTPmessages_Messages &result) {
					_historiesRequest = 0;
					searchReceived(type, result, process);
					finish();
				}).fail([=](const MTP::Error &error) {
					_historiesRequest = 0;
					searchFailed(type, error, process);
					finish();
				}).send();
				process->queries.emplace(process->requestId, _searchQuery);
				return process->requestId;
			});
		} else if (_searchState.tab == ChatSearchTab::PublicPosts) {
			requestPublicPosts(true);
		} else {
			requestMessages(true);
		}
		_inner->searchRequested(true);
	} else {
		_inner->searchRequested(false);
	}
	if (peerSearchRequired()) {
		const auto requestType = inCache
			? Api::PeerSearch::RequestType::CacheOnly
			: Api::PeerSearch::RequestType::CacheOrRemote;
		_peerSearch.request(query, [=](Api::PeerSearchResult result) {
			peerSearchReceived(result);
		}, requestType);
	} else {
		_peerSearch.clear();
		peerSearchReceived({});
	}
	const auto peerQuery = Api::ConvertPeerSearchQuery(query);
	if (searchForTopicsRequired(peerQuery)) {
		if (inCache) {
			if (_topicSearchQuery != peerQuery) {
				result = false;
			}
		} else if (_topicSearchQuery != peerQuery) {
			_topicSearchQuery = peerQuery;
			_topicSearchFull = false;
			searchTopics();
		}
	} else {
		_api.request(base::take(_topicSearchRequest)).cancel();
		_topicSearchQuery = peerQuery;
		_topicSearchFull = true;
	}
	return result;
}

bool Widget::peerSearchRequired() const {
	return !GetEnhancedBool("disable_global_search")
		&& _searchState.filterChatsList() && !_openedForum;
}

bool Widget::searchForTopicsRequired(const QString &query) const {
	return _searchState.filterChatsList()
		&& _openedForum
		&& !query.isEmpty()
		&& (IsHashOrCashtagSearchQuery(query) == HashOrCashtag::None)
		&& !_openedForum->topicsList()->loaded();
}

void Widget::searchRequested(SearchRequestDelay delay) {
	if (search(true, delay)) {
		return;
	} else if (delay == SearchRequestDelay::Instant) {
		_searchTimer.cancel();
		search();
	} else {
		_searchTimer.callOnce(kSearchRequestDelay);
	}
}

void Widget::showMainMenu() {
	controller()->widget()->showMainMenu();
}

void Widget::searchMessages(SearchState state) {
	if (const auto peer = state.inChat.peer()) {
		if (_openedForum && peer->forum() != _openedForum) {
			controller()->closeForum();
		}
	} else if (state.query.isEmpty()) {
		if (_childList) {
			hideChildList();
		}
		if (_openedForum) {
			controller()->closeForum();
		}
		if (_layout == Layout::Main) {
			controller()->closeFolder();
		}
	}
	applySearchState(std::move(state));
	session().local().saveRecentSearchHashtags(_searchState.query);

	if (_childList) {
		_childList->setInnerFocus();
	} else if (_subsectionTopBar) {
		if (!_subsectionTopBar->searchSetFocus()
			&& !_subsectionTopBar->searchHasFocus()) {
			_subsectionTopBar->toggleSearch(true, anim::type::normal);
		}
	} else {
		_search->setFocus();
	}
}

void Widget::searchTopics() {
	if (_topicSearchRequest || _topicSearchFull) {
		return;
	}
	_api.request(base::take(_topicSearchRequest)).cancel();
	_topicSearchRequest = _api.request(MTPmessages_GetForumTopics(
		MTP_flags(MTPmessages_GetForumTopics::Flag::f_q),
		_openedForum->peer()->input(),
		MTP_string(_topicSearchQuery),
		MTP_int(_topicSearchOffsetDate),
		MTP_int(_topicSearchOffsetId),
		MTP_int(_topicSearchOffsetTopicId),
		MTP_int(kSearchPerPage)
	)).done([=](const MTPmessages_ForumTopics &result) {
		_topicSearchRequest = 0;
		const auto savedTopicId = _topicSearchOffsetTopicId;
		const auto byCreation = result.data().is_order_by_create_date();
		_openedForum->applyReceivedTopics(result, [&](
				not_null<Data::ForumTopic*> topic) {
			_topicSearchOffsetTopicId = topic->rootId();
			if (byCreation) {
				_topicSearchOffsetDate = topic->creationDate();
				if (const auto last = topic->lastServerMessage()) {
					_topicSearchOffsetId = last->id;
				}
			} else if (const auto last = topic->lastServerMessage()) {
				_topicSearchOffsetId = last->id;
				_topicSearchOffsetDate = last->date();
			}
			_inner->appendToFiltered(topic);
		});
		if (_topicSearchOffsetTopicId != savedTopicId) {
			_inner->refresh();
		} else {
			_topicSearchFull = true;
		}
	}).fail([=] {
		_topicSearchFull = true;
	}).send();
}

void Widget::searchMore() {
	const auto process = currentSearchProcess();
	if (process->requestId
		|| _historiesRequest
		|| _searchTimer.isActive()) {
		return;
	} else if (!process->full) {
		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);
			const auto sublist = _openedForum
				? nullptr
				: _searchState.inChat.sublist();
			const auto fromPeer = sublist ? nullptr : _searchQueryFrom;
			const auto savedPeer = sublist
				? sublist->sublistPeer().get()
				: nullptr;
			_historiesRequest = histories.sendRequest(history, type, [=](
					Fn<void()> finish) {
				const auto type = SearchRequestType{
					.start = !process->lastId,
					.peer = true,
				};
				using Flag = MTPmessages_Search::Flag;
				process->requestId = session().api().request(
					MTPmessages_Search(
						MTP_flags((topic ? Flag::f_top_msg_id : Flag())
							| (fromPeer ? Flag::f_from_id : Flag())
							| (savedPeer ? Flag::f_saved_peer_id : Flag())
							| (_searchQueryTags.empty()
								? Flag()
								: Flag::f_saved_reaction)),
						peer->input(),
						MTP_string(_searchQuery),
						(fromPeer ? fromPeer->input() : MTP_inputPeerEmpty()),
						(savedPeer
							? savedPeer->input()
							: MTP_inputPeerEmpty()),
						MTP_vector_from_range(
							_searchQueryTags | ranges::views::transform(
								Data::ReactionToMTP
							)),
						MTP_int(topic ? topic->rootId() : 0),
						MTP_inputMessagesFilterEmpty(),
						MTP_int(0), // min_date
						MTP_int(0), // max_date
						MTP_int(process->lastId),
						MTP_int(0), // add_offset
						MTP_int(kSearchPerPage),
						MTP_int(0), // max_id
						MTP_int(0), // min_id
						MTP_long(0)) // hash
				).done([=](const MTPmessages_Messages &result) {
					searchReceived(type, result, process);
					_historiesRequest = 0;
					finish();
				}).fail([=](const MTP::Error &error) {
					searchFailed(type, error, process);
					_historiesRequest = 0;
					finish();
				}).send();
				if (!process->lastId) {
					process->queries.emplace(
						process->requestId,
						_searchQuery);
				}
				return process->requestId;
			});
		} else if (_searchState.tab == ChatSearchTab::PublicPosts) {
			requestPublicPosts(false);
		} else {
			requestMessages(false);
		}
	} else if (_searchInMigrated && !_migratedProcess.full) {
		auto &histories = session().data().histories();
		const auto type = Data::Histories::RequestType::History;
		const auto history = _searchInMigrated;
		_historiesRequest = histories.sendRequest(history, type, [=](
				Fn<void()> finish) {
			const auto type = SearchRequestType{
				.migrated = true,
				.start = !_migratedProcess.lastId,
			};
			const auto flags = _searchQueryFrom
				? MTP_flags(MTPmessages_Search::Flag::f_from_id)
				: MTP_flags(0);
			_migratedProcess.requestId = session().api().request(
				MTPmessages_Search(
					flags,
					_searchInMigrated->peer->input(),
					MTP_string(_searchQuery),
					(_searchQueryFrom
						? _searchQueryFrom->input()
						: MTP_inputPeerEmpty()),
					MTPInputPeer(), // saved_peer_id
					MTPVector<MTPReaction>(), // saved_reaction
					MTPint(), // top_msg_id
					MTP_inputMessagesFilterEmpty(),
					MTP_int(0), // min_date
					MTP_int(0), // max_date
					MTP_int(_migratedProcess.lastId),
					MTP_int(0), // add_offset
					MTP_int(kSearchPerPage),
					MTP_int(0), // max_id
					MTP_int(0), // min_id
					MTP_long(0)) // hash
			).done([=](const MTPmessages_Messages &result) {
				searchReceived(type, result, &_migratedProcess);
				_historiesRequest = 0;
				finish();
			}).fail([=](const MTP::Error &error) {
				searchFailed(type, error, &_migratedProcess);
				_historiesRequest = 0;
				finish();
			}).send();
			return _migratedProcess.requestId;
		});
	}
}

void Widget::requestPublicPosts(bool fromStart) {
	if (!_postsProcess.lastId || !_postsProcess.lastPeer) {
		fromStart = true;
	}
	const auto type = SearchRequestType{
		.posts = true,
		.start = fromStart,
	};
	using Flag = MTPchannels_SearchPosts::Flag;
	_postsProcess.requestId = session().api().request(
		MTPchannels_SearchPosts(
			MTP_flags(Flag::f_hashtag),
			MTP_string(_searchState.query.trimmed().mid(1)),
			MTP_string(), // query
			MTP_int(fromStart ? 0 : _postsProcess.nextRate),
			(fromStart
				? MTP_inputPeerEmpty()
				: _postsProcess.lastPeer->input()),
			MTP_int(fromStart ? 0 : _postsProcess.lastId),
			MTP_int(kSearchPerPage),
			MTP_long(0)) // allow_paid_stars
	).done([=](const MTPmessages_Messages &result) {
		searchReceived(type, result, &_postsProcess);
	}).fail([=](const MTP::Error &error) {
		searchFailed(type, error, &_postsProcess);
	}).send();
	if (fromStart) {
		_postsProcess.queries.emplace(_postsProcess.requestId, _searchQuery);
	}
}

void Widget::requestMessages(bool fromStart) {
	if (!_searchProcess.lastId || !_searchProcess.lastPeer) {
		fromStart = true;
	}
	const auto type = SearchRequestType{
		.start = fromStart,
	};
	using Flag = MTPmessages_SearchGlobal::Flag;
	const auto flags = Flag()
		| (session().settings().skipArchiveInSearch()
			? Flag::f_folder_id
			: Flag())
		| (_searchQueryFilter == ChatTypeFilter::Private
			? Flag::f_users_only
			: _searchQueryFilter == ChatTypeFilter::Groups
			? Flag::f_groups_only
			: _searchQueryFilter == ChatTypeFilter::Channels
			? Flag::f_broadcasts_only
			: Flag());
	const auto folderId = 0;
	_searchProcess.requestId = session().api().request(
		MTPmessages_SearchGlobal(
			MTP_flags(flags),
			MTP_int(folderId),
			MTP_string(_searchQuery),
			MTP_inputMessagesFilterEmpty(),
			MTP_int(0), // min_date
			MTP_int(0), // max_date
			MTP_int(fromStart ? 0 : _searchProcess.nextRate),
			(fromStart
				? MTP_inputPeerEmpty()
				: _searchProcess.lastPeer->input()),
			MTP_int(fromStart ? 0 : _searchProcess.lastId),
			MTP_int(kSearchPerPage))
	).done([=](const MTPmessages_Messages &result) {
		searchReceived(type, result, &_searchProcess);
	}).fail([=](const MTP::Error &error) {
		searchFailed(type, error, &_searchProcess);
	}).send();
	if (!_searchProcess.lastId) {
		_searchProcess.queries.emplace(
			_searchProcess.requestId,
			_searchQuery);
	}
	if (fromStart && _searchWithPostsPreview) {
		requestPublicPosts(true);
	}
}

auto Widget::currentSearchProcess() -> not_null<SearchProcessState*> {
	return (_searchState.tab == ChatSearchTab::PublicPosts)
		? &_postsProcess
		: &_searchProcess;
}

bool Widget::computeSearchWithPostsPreview() const {
	return 	(_searchHashOrCashtag != HashOrCashtag::None)
		&& (_searchState.tab == ChatSearchTab::MyMessages)
		&& !_searchState.inChat;
}

void Widget::searchReceived(
		SearchRequestType type,
		const MTPmessages_Messages &result,
		not_null<SearchProcessState*> process,
		bool cacheResults) {
	const auto state = _inner->state();
	if (!cacheResults
		&& (state == WidgetState::Filtered)
		&& type.start) {
		const auto i = process->queries.find(process->requestId);
		if (i != process->queries.end()) {
			process->cache[i->second] = result;
			process->queries.erase(i);
		}
	}
	const auto inject = (type.start && !type.posts && !type.migrated)
		? *_singleMessageSearch.lookup(_searchQuery)
		: nullptr;
	if (cacheResults && process->requestId) {
		return;
	}
	if (type.start) {
		process->lastPeer = nullptr;
		process->lastId = 0;
	}
	const auto processList = [&](const MTPVector<MTPMessage> &messages) {
		auto result = std::vector<not_null<HistoryItem*>>();
		for (const auto &message : messages.v) {
			const auto msgId = IdFromMessage(message);
			const auto peerId = PeerFromMessage(message);
			const auto lastDate = DateFromMessage(message);
			if (const auto peer = session().data().peerLoaded(peerId)) {
				if (lastDate) {
					const auto item = session().data().addNewMessage(
						message,
						MessageFlags(),
						NewMessageType::Existing);
					result.push_back(item);
				}
				process->lastPeer = peer;
			} else {
				LOG(("API Error: a search results with not loaded peer %1"
					).arg(peerId.value));
			}
			process->lastId = msgId;
		}
		return result;
	};
	auto fullCount = 0;
	auto messages = result.match([&](const MTPDmessages_messages &data) {
		if (!cacheResults) {
			// Don't apply cached data!
			session().data().processUsers(data.vusers());
			session().data().processChats(data.vchats());
			if (const auto peer = searchInPeer()) {
				peer->processTopics(data.vtopics());
			}
		}
		process->full = true;
		auto list = processList(data.vmessages());
		fullCount = list.size();
		return list;
	}, [&](const MTPDmessages_messagesSlice &data) {
		if (!cacheResults) {
			// Don't apply cached data!
			session().data().processUsers(data.vusers());
			session().data().processChats(data.vchats());
			if (const auto peer = searchInPeer()) {
				peer->processTopics(data.vtopics());
			}
		}
		auto list = processList(data.vmessages());
		const auto nextRate = data.vnext_rate();
		const auto rateUpdated = nextRate
			&& (nextRate->v != process->nextRate);
		const auto finished = (type.peer || type.migrated || type.posts)
			? list.empty()
			: !rateUpdated;
		if (rateUpdated) {
			process->nextRate = nextRate->v;
		}
		if (finished) {
			process->full = true;
		}
		fullCount = data.vcount().v;
		return list;
	}, [&](const MTPDmessages_channelMessages &data) {
		if (!cacheResults) {
			// Don't apply cached data!
			session().data().processUsers(data.vusers());
			session().data().processChats(data.vchats());
			if (const auto peer = searchInPeer()) {
				if (const auto channel = peer->asChannel()) {
					channel->ptsReceived(data.vpts().v);
				} else {
					LOG(("API Error: "
						"received messages.channelMessages when no channel "
						"was passed! (Widget::searchReceived)"));
				}
				peer->processTopics(data.vtopics());
			} else {
				LOG(("API Error: "
					"received messages.channelMessages when no channel "
					"was passed! (Widget::searchReceived)"));
			}
		}
		auto list = processList(data.vmessages());
		if (list.empty()) {
			process->full = true;
		}
		fullCount = data.vcount().v;
		return list;
	}, [&](const MTPDmessages_messagesNotModified &) {
		LOG(("API Error: received messages.messagesNotModified! "
			"(Widget::searchReceived)"));
		process->full = true;
		return std::vector<not_null<HistoryItem*>>();
	});
	_inner->searchReceived(messages, inject, type, fullCount);

	process->requestId = 0;
	listScrollUpdated();
	update();
}

void Widget::peerSearchReceived(Api::PeerSearchResult result) {
	_inner->peerSearchReceived(std::move(result));
	listScrollUpdated();
	update();
}

void Widget::searchApplyEmpty(
		SearchRequestType type,
		not_null<SearchProcessState*> process) {
	process->full = true;
	searchReceived(
		type,
		MTP_messages_messages(
			MTP_vector<MTPMessage>(),
			MTP_vector<MTPForumTopic>(),
			MTP_vector<MTPChat>(),
			MTP_vector<MTPUser>()),
		process);
}

void Widget::searchFailed(
		SearchRequestType type,
		const MTP::Error &error,
		not_null<SearchProcessState*> process) {
	if (error.type() == u"SEARCH_QUERY_EMPTY"_q) {
		searchApplyEmpty(type, process);
	} else {
		process->requestId = 0;
		process->full = true;
	}
}

void Widget::dragEnterEvent(QDragEnterEvent *e) {
	using namespace Storage;

	const auto data = e->mimeData();
	_dragInScroll = false;
	_dragForward = !controller()->adaptive().isOneColumn()
		&& data->hasFormat(u"application/x-td-forward"_q);
	if (_dragForward) {
		e->setDropAction(Qt::CopyAction);
		e->accept();
		updateDragInScroll(_scroll->geometry().contains(e->pos()));
	} else if (ComputeMimeDataState(data) != MimeDataState::None) {
		e->setDropAction(Qt::CopyAction);
		e->accept();
	}
	_chooseByDragTimer.cancel();
}

void Widget::dragMoveEvent(QDragMoveEvent *e) {
	if (_scroll->geometry().contains(e->pos())) {
		if (_dragForward) {
			updateDragInScroll(true);
		} else {
			_chooseByDragTimer.callOnce(ChoosePeerByDragTimeout);
		}
		const auto global = mapToGlobal(e->pos());
		const auto thread = _suggestions
			? _suggestions->updateFromParentDrag(global)
			: _inner->updateFromParentDrag(global);
		e->setDropAction(thread ? Qt::CopyAction : Qt::IgnoreAction);
	} else {
		if (_dragForward) {
			updateDragInScroll(false);
		}
		if (_suggestions) {
			_suggestions->dragLeft();
		}
		_inner->dragLeft();
		e->setDropAction(Qt::IgnoreAction);
	}
	e->accept();
}

void Widget::dragLeaveEvent(QDragLeaveEvent *e) {
	if (_dragForward) {
		updateDragInScroll(false);
	} else {
		_chooseByDragTimer.cancel();
	}
	if (_suggestions) {
		_suggestions->dragLeft();
	}
	_inner->dragLeft();
	e->accept();
}

void Widget::updateDragInScroll(bool inScroll) {
	if (_dragInScroll != inScroll) {
		_dragInScroll = inScroll;
		if (_dragInScroll) {
			controller()->content()->showDragForwardInfo();
		} else {
			controller()->content()->dialogsCancelled();
		}
	}
}

void Widget::dropEvent(QDropEvent *e) {
	_chooseByDragTimer.cancel();
	if (_scroll->geometry().contains(e->pos())) {
		const auto globalPosition = mapToGlobal(e->pos());
		const auto thread = _suggestions
			? _suggestions->updateFromParentDrag(globalPosition)
			: _inner->updateFromParentDrag(globalPosition);
		if (thread) {
			e->setDropAction(Qt::CopyAction);
			e->accept();
			controller()->content()->filesOrForwardDrop(
				thread,
				e->mimeData());
			if (!thread->owningHistory()->isForum()) {
				hideChildList();
			}
			controller()->widget()->raise();
			controller()->widget()->activateWindow();
		}
	}
}

void Widget::listScrollUpdated() {
	const auto scrollTop = _scroll->scrollTop();
	_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
	updateScrollUpVisibility();

	// Fix button rendering glitch, Qt bug with WA_OpaquePaintEvent widgets.
	_scrollToTop->update();
}

void Widget::updateCancelSearch() {
	const auto shown = !_searchState.query.isEmpty()
		|| (!_searchState.inChat
			&& (_searchHasFocus || _searchSuggestionsLocked));
	_cancelSearch->toggle(shown, anim::type::normal);
	if (_searchState.inChat) {
		_cancelSearch->setAccessibleName(shown
			? tr::lng_sr_clear_search(tr::now)
			: tr::lng_sr_cancel_search(tr::now));
	}
}

QString Widget::validateSearchQuery() {
	const auto query = currentSearchQuery();
	if (!_subsectionTopBar
		&& _suggestions
		&& _suggestions->consumeSearchQuery(query)) {
		return QString();
	} else if (_searchState.tab == ChatSearchTab::PublicPosts) {
		if (_searchHashOrCashtag == HashOrCashtag::None) {
			_searchHashOrCashtag = HashOrCashtag::Hashtag;
		}
		const auto fixed = FixHashtagSearchQuery(
			query,
			currentSearchQueryCursorPosition(),
			_searchHashOrCashtag);
		if (fixed.text != query) {
			setSearchQuery(fixed.text, fixed.cursorPosition);
		}
		return fixed.text;
	} else {
		_searchHashOrCashtag = IsHashOrCashtagSearchQuery(query);
	}
	_searchWithPostsPreview = computeSearchWithPostsPreview();
	return query;
}

void Widget::applySearchUpdate() {
	auto copy = _searchState;
	copy.query = validateSearchQuery();
	applySearchState(std::move(copy));

	if (_chooseFromUser->toggled()
		|| _searchState.fromPeer
		|| !_searchState.tags.empty()) {
		auto switchToChooseFrom = HistoryView::SwitchToChooseFromQuery();
		if (_lastSearchText != switchToChooseFrom
			&& switchToChooseFrom.startsWith(_lastSearchText)
			&& _searchState.query == switchToChooseFrom) {
			showSearchFrom();
		}
	}
	_lastSearchText = _searchState.query;
}

void Widget::updateForceDisplayWide() {
	if (_childList) {
		_childList->updateForceDisplayWide();
		return;
	}
	controller()->setChatsForceDisplayWide(_searchHasFocus
		|| (_subsectionTopBar && _subsectionTopBar->searchHasFocus())
		|| _searchSuggestionsLocked
		|| !_searchState.query.isEmpty()
		|| _searchState.inChat
		|| !session().api().authorizations().unreviewed().empty());
}

void Widget::showForum(
		not_null<Data::Forum*> forum,
		const Window::SectionShow &params) {
	if (_openedForum == forum) {
		return;
	}
	const auto nochat = !controller()->mainSectionShown();
	if (!params.childColumn
		|| (Core::App().settings().dialogsWidthRatio(nochat) == 0.)
		|| (_layout != Layout::Main)
		|| OptionForumHideChatsList.value()) {
		changeOpenedForum(forum, params.animated);
		return;
	}
	cancelSearch({ .forceFullCancel = true });
	openChildList(forum, params);
}

void Widget::openChildList(
		not_null<Data::Forum*> forum,
		const Window::SectionShow &params) {
	auto slide = Window::SectionSlideParams();
	const auto animated = !_childList
		&& (params.animated == anim::type::normal);
	if (animated) {
		destroyChildListCanvas();
		slide.oldContentCache = Ui::GrabWidget(
			this,
			QRect(_narrowWidth, 0, width() - _narrowWidth, height()));
	}
	auto copy = params;
	copy.childColumn = false;
	copy.animated = anim::type::instant;
	{
		if (_childList && InFocusChain(_childList.get())) {
			setFocus();
		}
		_childList = std::make_unique<Widget>(
			this,
			controller(),
			Layout::Child);
		_childList->showForum(forum, copy);
		_childListPeerId = forum->peer()->id;
	}

	_childListShadow = std::make_unique<Ui::RpWidget>(this);
	const auto shadow = _childListShadow.get();
	const auto opacity = shadow->lifetime().make_state<float64>(0.);
	shadow->setAttribute(Qt::WA_TransparentForMouseEvents);
	shadow->paintRequest(
	) | rpl::on_next([=](QRect clip) {
		auto p = QPainter(shadow);
		p.setOpacity(*opacity);
		p.fillRect(clip, st::shadowFg);
	}, shadow->lifetime());
	_childListShown.value() | rpl::on_next([=](float64 value) {
		*opacity = value;
		update();
		_inner->update();
		_search->setVisible(value < 1.);
		if (!value && _childListShadow.get() != shadow) {
			delete shadow;
		}
	}, shadow->lifetime());

	updateControlsGeometry();
	updateControlsVisibility(true);

	if (animated) {
		_childList->showAnimated(Window::SlideDirection::FromRight, slide);
		_childListShown = _childList->shownProgressValue();
	} else {
		_childListShown = 1.;
	}
	if (hasFocus()) {
		setInnerFocus();
	}
	updateForceDisplayWide();
}

void Widget::closeChildList(anim::type animated) {
	if (!_childList) {
		return;
	}
	const auto geometry = _childList->geometry();
	const auto shown = _childListShown.current();
	auto oldContentCache = QPixmap();
	auto animation = (Window::SlideAnimation*)nullptr;
	if (animated == anim::type::normal) {
		oldContentCache = Ui::GrabWidget(_childList.get());
		_hideChildListCanvas = std::make_unique<Ui::RpWidget>(this);
		_hideChildListCanvas->setAttribute(Qt::WA_TransparentForMouseEvents);
		_hideChildListCanvas->setGeometry(geometry);
		animation = _hideChildListCanvas->lifetime().make_state<
			Window::SlideAnimation
		>();
		_hideChildListCanvas->paintRequest(
		) | rpl::on_next([=] {
			QPainter p(_hideChildListCanvas.get());
			animation->paintContents(p);
		}, _hideChildListCanvas->lifetime());
	}
	if (InFocusChain(_childList.get())) {
		setFocus();
	}
	_childList = nullptr;
	_childListShown = 0.;
	if (hasFocus()) {
		setInnerFocus();
		_search->finishAnimating();
	}
	if (animated == anim::type::normal) {
		_hideChildListCanvas->hide();
		auto newContentCache = Ui::GrabWidget(this, geometry);
		_hideChildListCanvas->show();

		_childListShown = shown;
		_childListShadow.release();

		animation->setDirection(Window::SlideDirection::FromLeft);
		animation->setRepaintCallback([=] {
			_childListShown = (1. - animation->progress()) * shown;
			_hideChildListCanvas->update();
		});
		animation->setFinishedCallback([=] {
			destroyChildListCanvas();
		});
		animation->setPixmaps(oldContentCache, newContentCache);
		animation->start();
	} else {
		_childListShadow = nullptr;
	}
	updateStoriesVisibility();
	updateForceDisplayWide();
}

bool Widget::applySearchState(SearchState state) {
	if (_searchState == state) {
		return true;
	} else if (_childList) {
		if (_childList->applySearchState(state)) {
			return true;
		}
		hideChildList();
	}
	if (state.inChat && _layout == Layout::Main) {
		controller()->closeFolder();
	}

	// Adjust state to be consistent.
	if (const auto peer = state.inChat.peer()) {
		if (const auto to = peer->migrateTo()) {
			state.inChat = peer->owner().history(to);
		}
	}
	const auto peer = state.inChat.peer();
	const auto topic = state.inChat.topic();
	const auto forum = peer ? peer->forum() : nullptr;
	if (state.inChat.folder() || (forum && !topic)) {
		state.inChat = {};
	}
	if (!state.inChat && !forum && !_openedForum) {
		state.fromPeer = nullptr;
	}
	if (state.tab == ChatSearchTab::PublicPosts
		&& IsHashOrCashtagSearchQuery(state.query) == HashOrCashtag::None) {
		state.tab = (_openedForum && !state.inChat)
			? ChatSearchTab::ThisPeer
			: ChatSearchTab::MyMessages;
	} else if (!state.inChat
		&& _searchHashOrCashtag == HashOrCashtag::None) {
		state.tab = (forum || _openedForum)
			? ChatSearchTab::ThisPeer
			: ChatSearchTab::MyMessages;
	}
	if (!state.tags.empty()) {
		state.inChat = session().data().history(session().user());
	}

	const auto clearQuery = state.fromPeer
		&& (_lastSearchText == HistoryView::SwitchToChooseFromQuery());
	if (clearQuery) {
		state.query = _lastSearchText = QString();
	}

	const auto inChatChanged = (_searchState.inChat != state.inChat);
	const auto fromPeerChanged = (_searchState.fromPeer != state.fromPeer);
	const auto tagsChanged = (_searchState.tags != state.tags);
	const auto queryChanged = (_searchState.query != state.query);
	const auto tabChanged = (_searchState.tab != state.tab);
	const auto queryEmptyChanged = queryChanged
		? (_searchState.query.isEmpty() != state.query.isEmpty())
		: false;
	if (queryEmptyChanged || tabChanged) {
		state.filter = ChatTypeFilter::All;
	}
	const auto filterChanged = (_searchState.filter != state.filter);

	if (forum) {
		if (_openedForum == forum) {
			showSearchInTopBar(anim::type::normal);
		} else if (_layout == Layout::Main) {
			_forumSearchRequested = true;
			auto params = Window::SectionShow(
				Window::SectionShow::Way::ClearStack);
			params.forceTopicsList = true;
			controller()->showForum(forum, params);
		} else {
			return false;
		}
	} else if (peer && (_layout != Layout::Main)) {
		return false;
	}

	if ((state.tab == ChatSearchTab::ThisTopic
		&& !state.inChat.topic())
		|| (state.tab == ChatSearchTab::ThisPeer
			&& !state.inChat
			&& !_openedForum)
		|| (state.tab == ChatSearchTab::PublicPosts
			&& _searchHashOrCashtag == HashOrCashtag::None)) {
		state.tab = state.inChat.topic()
			? ChatSearchTab::ThisTopic
			: (state.inChat.owningHistory() || state.inChat.sublist())
			? ChatSearchTab::ThisPeer
			: ChatSearchTab::MyMessages;
	}

	const auto migrateFrom = (peer
		&& !topic
		&& state.tab == ChatSearchTab::ThisPeer)
		? peer->migrateFrom()
		: nullptr;
	_searchInMigrated = migrateFrom
		? peer->owner().history(migrateFrom).get()
		: nullptr;
	_searchState = state;
	if (_chatFilters && (queryEmptyChanged || inChatChanged)) {
		_chatFilters->setVisible(_searchState.query.isEmpty()
			&& !_openedForum
			&& !searchInPeer());
		updateControlsGeometry();
	}
	if (_topBarSuggestion && queryEmptyChanged) {
		_searchStateForTopBarSuggestion.fire(!_searchState.query.isEmpty());
	}
	_searchWithPostsPreview = computeSearchWithPostsPreview();
	if (queryChanged) {
		updateLockUnlockVisibility(anim::type::normal);
		updateLoadMoreChatsVisibility();
	}
	if (inChatChanged) {
		controller()->setSearchInChat(_searchState.inChat);
	}
	if (queryChanged || inChatChanged) {
		updateCancelSearch();
		updateStoriesVisibility();
	}
	updateJumpToDateVisibility();
	updateSearchFromVisibility();
	updateLockUnlockPosition();

	const auto searchCleared = state.query.isEmpty()
		&& !state.fromPeer
		&& state.tags.empty();
	if (searchCleared
		|| inChatChanged
		|| fromPeerChanged
		|| filterChanged
		|| tagsChanged
		|| tabChanged) {
		clearSearchCache(searchCleared);
	}
	if (state.query.isEmpty()) {
		_peerSearch.clear();
	}

	if (_searchState.query != currentSearchQuery()) {
		setSearchQuery(_searchState.query);
	}
	_inner->applySearchState(_searchState);

	if (!_postponeProcessSearchFocusChange) {
		// Suggestions depend on _inner->state(), not on _searchState.
		updateSuggestions(anim::type::instant);
	}

	_searchTagsLifetime = _inner->searchTagsChanges(
	) | rpl::on_next([=](std::vector<Data::ReactionId> &&list) {
		auto copy = _searchState;
		copy.tags = std::move(list);
		applySearchState(std::move(copy));
	});
	if (_subsectionTopBar) {
		_subsectionTopBar->searchEnableJumpToDate(
			_openedForum && _searchState.inChat);
	}
	if (!_searchState.inChat && _searchState.query.isEmpty()) {
		if (!_widthAnimationCache.isNull()) {
			stopWidthAnimation();
		}
		setInnerFocus();
	} else if (!_subsectionTopBar) {
		_search->setFocus();
	} else if (_openedForum && !_subsectionTopBar->searchSetFocus()) {
		_subsectionTopBar->toggleSearch(true, anim::type::normal);
	}
	updateForceDisplayWide();
	applySearchUpdate();
	return true;
}

void Widget::clearSearchCache(bool clearPosts) {
	_searchProcess.cache.clear();
	_singleMessageSearch.clear();
	const auto queries = base::take(_searchProcess.queries);
	for (const auto &[requestId, query] : queries) {
		session().api().request(requestId).cancel();
	}
	_searchQuery = QString();
	_searchQueryFrom = nullptr;
	_searchQueryTags.clear();
	if (clearPosts) {
		_postsProcess.cache.clear();
		const auto queries = base::take(_postsProcess.queries);
		for (const auto &[requestId, query] : queries) {
			session().api().request(requestId).cancel();
		}
	}
	_topicSearchQuery = QString();
	_topicSearchOffsetDate = 0;
	_topicSearchOffsetId = _topicSearchOffsetTopicId = 0;
	_api.request(base::take(_topicSearchRequest)).cancel();
	_peerSearch.clear();
	cancelSearchRequest();
}

void Widget::showCalendar() {
	if (_searchState.inChat) {
		controller()->showCalendar({ _searchState.inChat });
	}
}

void Widget::showSearchFrom() {
	if (const auto peer = searchInPeer()) {
		const auto weak = base::make_weak(_searchState.inChat.topic());
		const auto chat = (!_searchState.inChat && _openedForum)
			? Key(_openedForum->history())
			: _searchState.inChat;
		auto box = SearchFromBox(
			peer,
			crl::guard(this, [=](not_null<PeerData*> from) {
				controller()->hideLayer();
				auto copy = _searchState;
				if (!chat.topic()) {
					copy.inChat = chat;
					copy.fromPeer = from;
					applySearchState(std::move(copy));
				} else if (const auto strong = weak.get()) {
					copy.inChat = strong;
					copy.fromPeer = from;
					applySearchState(std::move(copy));
				}
			}),
			crl::guard(this, [=] { _search->setFocus(); }));
		if (box) {
			controller()->show(std::move(box));
		}
	}
}

void Widget::searchCursorMoved() {
	const auto to = _search->textCursor().position();
	const auto text = _search->getLastText();
	auto hashtag = QStringView();
	for (int start = to; start > 0;) {
		--start;
		if (text.size() <= start) {
			break;
		}
		const auto ch = text[start];
		if (ch == '#') {
			hashtag = base::StringViewMid(text, start, to - start);
			break;
		} else if (!ch.isLetterOrNumber() && ch != '_') {
			break;
		}
	}
	_inner->onHashtagFilterUpdate(hashtag);
}

void Widget::completeHashtag(QString tag) {
	const auto t = _search->getLastText();
	auto cur = _search->textCursor().position();
	auto hashtag = QString();
	for (int start = cur; start > 0;) {
		--start;
		if (t.size() <= start) {
			break;
		} else if (t.at(start) == '#') {
			if (cur == start + 1
				|| base::StringViewMid(t, start + 1, cur - start - 1)
					== base::StringViewMid(tag, 0, cur - start - 1)) {
				while (cur < t.size() && cur - start - 1 < tag.size()) {
					if (t.at(cur) != tag.at(cur - start - 1)) {
						break;
					}
					++cur;
				}
				if (cur - start - 1 == tag.size()
					&& cur < t.size()
					&& t.at(cur) == ' ') {
					++cur;
				}
				hashtag = t.mid(0, start + 1) + tag + ' ' + t.mid(cur);
				setSearchQuery(hashtag, start + 1 + tag.size() + 1);
				applySearchUpdate();
				return;
			}
			break;
		} else if (!t.at(start).isLetterOrNumber() && t.at(start) != '_') {
			break;
		}
	}
	setSearchQuery(
		t.mid(0, cur) + '#' + tag + ' ' + t.mid(cur),
		cur + 1 + tag.size() + 1);
	applySearchUpdate();
}

void Widget::resizeEvent(QResizeEvent *e) {
	updateControlsGeometry();
}

void Widget::updateLockUnlockVisibility(anim::type animated) {
	if (_showAnimation) {
		return;
	}
	const auto widthAnimation = !_widthAnimationCache.isNull();
	const auto suggestionsAnimation = widthAnimation
		&& (!_suggestions || !_hidingSuggestions.empty());
	const auto hiddenInstant = _showAnimation
		|| _openedForum
		|| (widthAnimation && !suggestionsAnimation)
		|| _childList
		|| !session().domain().local().hasLocalPasscode()
		|| (_stories
			&& !_stories->empty()
			&& _scroll->position().overscroll < -st::dialogsFilterSkip);
	const auto hiddenAnimated = _searchHasFocus
		|| _searchSuggestionsLocked
		|| !_searchState.query.isEmpty()
		|| _searchState.inChat
		|| suggestionsAnimation;
	const auto hidden = hiddenInstant || hiddenAnimated;
	const auto changed = (_lockUnlock->toggled() == hidden);
	_lockUnlock->toggle(
		!hidden,
		hiddenInstant ? anim::type::instant : animated);
	if (changed) {
		if (!hidden) {
			updateLockUnlockPosition();
		}
		updateControlsGeometry();
	}
}

void Widget::updateLoadMoreChatsVisibility() {
	if (_showAnimation || !_loadMoreChats) {
		return;
	}
	const auto hidden = (_openedFolder != nullptr)
		|| (_openedForum != nullptr)
		|| !_searchState.query.isEmpty();
	if (_loadMoreChats->isHidden() != hidden) {
		_loadMoreChats->setVisible(!hidden);
		updateControlsGeometry();
	}
}

void Widget::updateJumpToDateVisibility(bool fast) {
	if (_showAnimation) {
		return;
	}

	_jumpToDate->toggle(
		(searchInPeer() && _searchState.query.isEmpty()),
		fast ? anim::type::instant : anim::type::normal);
}

void Widget::updateSearchFromVisibility(bool fast) {
	auto visible = [&] {
		if (const auto peer = searchInPeer()) {
			if (peer->isChat() || peer->isMegagroup()) {
				return !_searchState.fromPeer;
			}
		}
		return false;
	}();
	const auto changed = (visible == !_chooseFromUser->toggled());
	_chooseFromUser->toggle(
		visible,
		fast ? anim::type::instant : anim::type::normal);
	if (_subsectionTopBar) {
		_subsectionTopBar->searchEnableChooseFromUser(true, visible);
	} else if (changed) {
		auto additional = QMargins();
		if (visible) {
			additional.setRight(_chooseFromUser->width());
		}
		_search->setAdditionalMargins(additional);
	}
}

void Widget::updateControlsGeometry() {
	if (width() < _narrowWidth) {
		return;
	}
	auto filterAreaTop = 0;

	const auto ratiow = anim::interpolate(
		width(),
		_narrowWidth,
		_childListShown.current());
	const auto smallw = st::columnMinimalWidthLeft - _narrowWidth;
	const auto narrowRatio = (ratiow < smallw)
		? ((smallw - ratiow) / float64(smallw - _narrowWidth))
		: 0.;

	auto filterLeft = (controller()->filtersWidth()
		? st::dialogsFilterSkip
		: (st::dialogsFilterPadding.x() + _mainMenu.toggle->width()))
		+ st::dialogsFilterPadding.x();
	const auto filterRight = st::dialogsFilterSkip
		+ st::dialogsFilterPadding.x();
	const auto filterWidth = qMax(ratiow, smallw) - filterLeft - filterRight;
	const auto filterAreaHeight = st::topBarHeight;
	_searchControls->setGeometry(0, filterAreaTop, ratiow, filterAreaHeight);
	if (_subsectionTopBar) {
		_subsectionTopBar->setGeometryWithNarrowRatio(
			_searchControls->geometry(),
			_narrowWidth,
			narrowRatio);
	}

	auto filterTop = (filterAreaHeight - _search->height()) / 2;
	filterLeft = anim::interpolate(filterLeft, _narrowWidth, narrowRatio);
	_search->setGeometryToLeft(
		filterLeft,
		filterTop,
		filterWidth,
		_search->height());

	auto mainMenuLeft = anim::interpolate(
		st::dialogsFilterPadding.x(),
		(_narrowWidth - _mainMenu.toggle->width()) / 2,
		narrowRatio);
	_mainMenu.toggle->moveToLeft(mainMenuLeft, st::dialogsFilterPadding.y());
	_mainMenu.under->setGeometry(
		0,
		0,
		filterLeft,
		_mainMenu.toggle->y()
			+ _mainMenu.toggle->height()
			+ st::dialogsFilterPadding.y());
	const auto searchLeft = anim::interpolate(
		-_searchForNarrowLayout->width(),
		(_narrowWidth - _searchForNarrowLayout->width()) / 2,
		narrowRatio);
	_searchForNarrowLayout->moveToLeft(
		searchLeft,
		st::dialogsFilterPadding.y());

	auto right = filterLeft + filterWidth;
	_cancelSearch->moveToLeft(right - _cancelSearch->width(), _search->y());
	right -= _jumpToDate->width();
	_jumpToDate->moveToLeft(right, _search->y());
	right -= _chooseFromUser->width();
	_chooseFromUser->moveToLeft(right, _search->y());

	const auto barw = width();
	const auto expandedStoriesTop = filterAreaTop + filterAreaHeight;
	const auto storiesHeight = 2 * st::dialogsStories.photoTop
		+ st::dialogsStories.photo;
	const auto added = (st::dialogsFilter.heightMin - storiesHeight) / 2;
	if (_stories) {
		_stories->setLayoutConstraints(
			{ filterLeft + filterWidth, filterTop + added },
			style::al_right,
			{ 0, expandedStoriesTop, barw, st::dialogsStoriesFull.height });
	}
	if (_forumTopShadow) {
		_forumTopShadow->setGeometry(
			0,
			expandedStoriesTop,
			barw,
			st::lineWidth);
	}

	updateLockUnlockPosition();

	auto bottomSkip = 0;
	const auto putBottomButton = [&](auto &button) {
		if (button && !button->isHidden()) {
			const auto buttonHeight = button->height();
			bottomSkip += buttonHeight;
			button->setGeometry(
				0,
				height() - bottomSkip,
				barw,
				buttonHeight);
		}
	};
	putBottomButton(_updateTelegram);
	putBottomButton(_downloadBar);
	putBottomButton(_loadMoreChats);
	if (_connecting) {
		_connecting->setBottomSkip(bottomSkip);
	}
	if (_layout != Layout::Child) {
		controller()->setConnectingBottomSkip(bottomSkip);
	}

	const auto wasScrollTop = _scroll->scrollTop();
	const auto newScrollTop = (wasScrollTop == 0)
		? wasScrollTop
		: (_topDelta < 0 && wasScrollTop <= 0)
		? wasScrollTop
		: (wasScrollTop + _topDelta);

	const auto scrollWidth = _childList ? _narrowWidth : barw;
	if (_moreChatsBar) {
		_moreChatsBar->resizeToWidth(barw);
	}
	if (_forumGroupCallBar) {
		_forumGroupCallBar->resizeToWidth(barw);
	}
	if (_forumRequestsBar) {
		_forumRequestsBar->resizeToWidth(barw);
	}
	if (_chatFilters) {
		_chatFilters->resizeToWidth(barw);
	}
	if (_frozenAccountBar) {
		_frozenAccountBar->resize(barw, _frozenAccountBar->height());
	}
	_updateScrollGeometryCached = [=] {
		const auto frozenBarTop = expandedStoriesTop
			+ ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded);
		if (_frozenAccountBar) {
			_frozenAccountBar->move(0, frozenBarTop);
		}
		const auto moreChatsBarTop = frozenBarTop
			+ (_frozenAccountBar ? _frozenAccountBar->height() : 0);
		if (_moreChatsBar) {
			_moreChatsBar->move(0, moreChatsBarTop);
		}
		const auto forumGroupCallTop = moreChatsBarTop
			+ (_moreChatsBar ? _moreChatsBar->height() : 0);
		if (_forumGroupCallBar) {
			_forumGroupCallBar->move(0, forumGroupCallTop);
		}
		const auto forumRequestsTop = forumGroupCallTop
			+ (_forumGroupCallBar ? _forumGroupCallBar->height() : 0);
		if (_forumRequestsBar) {
			_forumRequestsBar->move(0, forumRequestsTop);
		}
		const auto forumReportTop = forumRequestsTop
			+ (_forumRequestsBar ? _forumRequestsBar->height() : 0);
		if (_forumReportBar) {
			_forumReportBar->bar().move(0, forumReportTop);
		}
		const auto chatFiltersTop = forumReportTop
			+ (_forumReportBar ? _forumReportBar->bar().height() : 0);
		if (_chatFilters) {
			_chatFilters->move(0, chatFiltersTop);
		}
		const auto scrollTop = chatFiltersTop
			+ ((_chatFilters
				&& _searchState.query.isEmpty()
				&& !_openedForum && !searchInPeer())
				? (_chatFilters->height() * (1. - narrowRatio))
				: 0);
		const auto scrollHeight = height() - scrollTop - bottomSkip;
		const auto wasScrollHeight = _scroll->height();
		_scroll->setGeometry(0, scrollTop, scrollWidth, scrollHeight);
		if (scrollHeight != wasScrollHeight) {
			controller()->floatPlayerAreaUpdated();
		}
	};
	_updateScrollGeometryCached();

	if (_suggestions) {
		_suggestions->setGeometry(
			0,
			expandedStoriesTop,
			scrollWidth,
			height() - expandedStoriesTop - bottomSkip);
	}

	_inner->resize(scrollWidth, _inner->height());
	_inner->setNarrowRatio(narrowRatio);
	if (newScrollTop != wasScrollTop) {
		_scroll->scrollToY(newScrollTop);
	} else {
		listScrollUpdated();
	}
	if (_scrollToTopIsShown) {
		updateScrollUpPosition();
	}

	if (_childList) {
		const auto childw = std::max(_narrowWidth, width() - scrollWidth);
		const auto childh = _scroll->y() + _scroll->height();
		const auto childx = width() - childw;
		_childList->setGeometryWithTopMoved(
			{ childx, 0, childw, childh },
			_topDelta);
		const auto line = st::lineWidth;
		_childListShadow->setGeometry(childx - line, 0, line, childh);
	}
}

RowDescriptor Widget::resolveChatNext(RowDescriptor from) const {
	return _inner->resolveChatNext(from);
}

RowDescriptor Widget::resolveChatPrevious(RowDescriptor from) const {
	return _inner->resolveChatPrevious(from);
}

void Widget::keyPressEvent(QKeyEvent *e) {
	if (e->key() == Qt::Key_Escape) {
		escape();
		//if (_openedForum) {
		//	controller()->closeForum();
		//} else if (_openedFolder) {
		//	controller()->closeFolder();
		//} else {
		//	e->ignore();
		//}
	} else if ((e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Tab)
		&& _searchHasFocus
		&& !_searchState.inChat
		&& _searchState.query.isEmpty()) {
		escape();
	} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
		submit();
	} else if (_suggestions
		&& (e->key() == Qt::Key_Down
			|| e->key() == Qt::Key_Up
			|| e->key() == Qt::Key_Left
			|| e->key() == Qt::Key_Right)) {
		_suggestions->selectJump(Qt::Key(e->key()));
	} else if (e->key() == Qt::Key_Down) {
		_inner->selectSkip(1);
	} else if (e->key() == Qt::Key_Up) {
		_inner->selectSkip(-1);
	} else if (e->key() == Qt::Key_PageDown) {
		if (_suggestions) {
			_suggestions->selectJump(Qt::Key_Down, _scroll->height());
		} else {
			_inner->selectSkipPage(_scroll->height(), 1);
		}
	} else if (e->key() == Qt::Key_PageUp) {
		if (_suggestions) {
			_suggestions->selectJump(Qt::Key_Up, _scroll->height());
		} else {
			_inner->selectSkipPage(_scroll->height(), -1);
		}
	} else if (redirectKeyToSearch(e)) {
		// This delay in search focus processing allows us not to create
		// _suggestions in case the event inserts some non-whitespace search
		// query while still show _suggestions animated, if it is a space.
		_postponeProcessSearchFocusChange = true;
		_search->setFocusFast();
		if (e->key() != Qt::Key_Space) {
			QCoreApplication::sendEvent(_search->rawTextEdit(), e);
		}
		_postponeProcessSearchFocusChange = false;
		processSearchFocusChange();
	} else {
		e->ignore();
	}
}

void Widget::inputMethodEvent(QInputMethodEvent *e) {
	const auto cursor = _search->rawTextEdit()->textCursor();
	bool isGettingInput = !e->commitString().isEmpty()
		|| e->preeditString() != cursor.block().layout()->preeditAreaText()
		|| e->replacementLength() > 0;

	if (!isGettingInput || _postponeProcessSearchFocusChange) {
		Window::AbstractSectionWidget::inputMethodEvent(e);
		return;
	}

	// This delay in search focus processing allows us not to create
	// _suggestions in case the event inserts some non-whitespace search
	// query while still show _suggestions animated, if it is a space.
	_postponeProcessSearchFocusChange = true;
	_search->setFocusFast();
	QCoreApplication::sendEvent(_search->rawTextEdit(), e);
	_postponeProcessSearchFocusChange = false;
	processSearchFocusChange();
}

QVariant Widget::inputMethodQuery(Qt::InputMethodQuery query) const {
	return _search->rawTextEdit()->inputMethodQuery(query);
}

bool Widget::redirectToSearchPossible() const {
	return !_openedFolder
		&& !_openedForum
		&& !_childList
		&& _search->isVisible()
		&& !_search->hasFocus()
		&& hasFocus();
}

bool Widget::redirectKeyToSearch(QKeyEvent *e) const {
	if (!redirectToSearchPossible()) {
		return false;
	}
	const auto character = !(e->modifiers() & ~Qt::ShiftModifier)
		&& (e->key() != Qt::Key_Shift)
		&& RedirectTextToSearch(e->text());
	if (character) {
		return true;
	} else if (e != QKeySequence::Paste) {
		return false;
	}
	const auto useSelectionMode = (e->key() == Qt::Key_Insert)
		&& (e->modifiers() == (Qt::CTRL | Qt::SHIFT))
		&& QGuiApplication::clipboard()->supportsSelection();
	const auto pasteMode = useSelectionMode
		? QClipboard::Selection
		: QClipboard::Clipboard;
	const auto data = QGuiApplication::clipboard()->mimeData(pasteMode);
	return data && data->hasText();
}

bool Widget::redirectImeToSearch() const {
	return redirectToSearchPossible();
}

void Widget::paintEvent(QPaintEvent *e) {
	if (controller()->contentOverlapped(this, e)) {
		return;
	}

	Painter p(this);
	QRect r(e->rect());
	if (r != rect()) {
		p.setClipRect(r);
	}
	if (_showAnimation) {
		_showAnimation->paintContents(p);
		return;
	}
	const auto bg = anim::brush(
		st::dialogsBg,
		st::dialogsBgOver,
		_childListShown.current());
	auto above = QRect(0, 0, width(), _scroll->y());
	if (above.intersects(r)) {
		p.fillRect(above.intersected(r), bg);
	}

	auto belowTop = _scroll->y() + _scroll->height();
	if (!_widthAnimationCache.isNull()) {
		const auto suggestionsShown = _suggestions
			? _suggestions->shownOpacity()
			: !_hidingSuggestions.empty()
			? _hidingSuggestions.back()->shownOpacity()
			: 0.;
		const auto suggestionsSkip = suggestionsShown
			* (st::topPeers.height + st::searchedBarHeight);
		const auto top = _searchControls->y()
			+ _searchControls->height()
			+ suggestionsSkip;
		p.drawPixmapLeft(0, top, width(), _widthAnimationCache);
		belowTop = top
			+ (_widthAnimationCache.height() / style::DevicePixelRatio());
	}

	auto below = QRect(0, belowTop, width(), height() - belowTop);
	if (below.intersects(r)) {
		p.fillRect(below.intersected(r), bg);
	}
}

void Widget::scrollToEntry(const RowDescriptor &entry) {
	_inner->scrollToEntry(entry);
}

void Widget::cancelSearchRequest() {
	session().api().request(base::take(_searchProcess.requestId)).cancel();
	session().api().request(base::take(_migratedProcess.requestId)).cancel();
	session().api().request(base::take(_postsProcess.requestId)).cancel();
	session().data().histories().cancelRequest(
		base::take(_historiesRequest));
}

PeerData *Widget::searchInPeer() const {
	return (_searchState.tab == ChatSearchTab::MyMessages
		|| _searchState.tab == ChatSearchTab::PublicPosts)
		? nullptr
		: _openedForum
		? _openedForum->peer().get()
		: _searchState.inChat.sublist()
		? _searchState.inChat.sublist()->owningHistory()->peer.get()
		: _searchState.inChat.peer();
}

Data::ForumTopic *Widget::searchInTopic() const {
	return (_searchState.tab != ChatSearchTab::ThisTopic)
		? nullptr
		: _searchState.inChat.topic();
}

PeerData *Widget::searchFromPeer() const {
	if (const auto peer = searchInPeer()) {
		if (peer->isChat() || peer->isMegagroup()) {
			return _searchState.fromPeer;
		}
	}
	return nullptr;
}

const std::vector<Data::ReactionId> &Widget::searchInTags() const {
	if (const auto peer = searchInPeer()) {
		if (peer->isSelf() && _searchState.tab == ChatSearchTab::ThisPeer) {
			return _searchState.tags;
		}
	}
	static const auto kEmpty = std::vector<Data::ReactionId>();
	return kEmpty;
}

QString Widget::currentSearchQuery() const {
	return _subsectionTopBar
		? _subsectionTopBar->searchQueryCurrent()
		: _search->getLastText();
}

int Widget::currentSearchQueryCursorPosition() const {
	return _subsectionTopBar
		? _subsectionTopBar->searchQueryCursorPosition()
		: _search->textCursor().position();
}

void Widget::clearSearchField() {
	if (_subsectionTopBar) {
		_subsectionTopBar->searchClear();
	} else {
		_search->clear();
	}
}

void Widget::setSearchQuery(const QString &query, int cursorPosition) {
	if (query.isEmpty()) {
		clearSearchField();
		return;
	}
	if (cursorPosition < 0) {
		cursorPosition = query.size();
	}
	if (_subsectionTopBar) {
		_subsectionTopBar->searchSetText(query, cursorPosition);
	} else {
		_search->setText(query);
		_search->setCursorPosition(cursorPosition);
	}
}

bool Widget::cancelSearch(CancelSearchOptions options) {
	const auto clearingSuggestionsQuery = _suggestions
		&& _suggestions->consumeSearchQuery(QString());
	if (clearingSuggestionsQuery) {
		setSearchQuery(QString());
		if (!options.forceFullCancel) {
			return true;
		}
	}
	cancelSearchRequest();
	auto updatedState = _searchState;
	const auto clearingQuery = clearingSuggestionsQuery
		|| !updatedState.query.isEmpty();
	const auto forceFullCancel = options.forceFullCancel;
	auto clearingInChat = (forceFullCancel || !clearingQuery)
		&& (updatedState.inChat
			|| updatedState.fromPeer
			|| !updatedState.tags.empty());
	if (clearingQuery) {
		updatedState.query = QString();
	}
	if (clearingInChat) {
		if (options.jumpBackToSearchedChat
			&& updatedState.inChat
			&& controller()->adaptive().isOneColumn()) {
			if (const auto thread = updatedState.inChat.thread()) {
				controller()->showThread(thread);
			} else {
				Unexpected("Empty key in cancelSearch().");
			}
		}
		updatedState.inChat = {};
		updatedState.fromPeer = nullptr;
		updatedState.tags = {};
	}
	if (!clearingQuery
		&& _subsectionTopBar
		&& _subsectionTopBar->toggleSearch(false, anim::type::normal)) {
		setInnerFocus(true);
		clearingInChat = true;
	}
	const auto clearSearchFocus = (forceFullCancel || !updatedState.inChat)
		&& (_searchHasFocus || _searchSuggestionsLocked);
	if (!updatedState.inChat && _suggestions) {
		_suggestions->clearPersistance();
		_searchSuggestionsLocked = false;
	}
	if (!_suggestions && clearSearchFocus) {
		// Don't create suggestions in unfocus case.
		setInnerFocus(true);
	}
	_searchProcess.lastPeer = nullptr;
	_searchProcess.lastId = 0;
	_migratedProcess.lastPeer = nullptr;
	_migratedProcess.lastId = 0;
	_postsProcess.lastPeer = nullptr;
	_postsProcess.lastId = 0;
	_inner->clearFilter();
	applySearchState(std::move(updatedState));
	if (_suggestions && clearSearchFocus) {
		const auto clearLockedFocus = !_searchHasFocus;
		setInnerFocus(true);
		if (clearLockedFocus) {
			processSearchFocusChange();
		}
	}
	updateForceDisplayWide();
	if (clearingInChat) {
		if (const auto forum = controller()->shownForum().current()) {
			if (forum->peer()->useSubsectionTabs()) {
				const auto id = controller()->windowId();
				const auto initial = id.forum();
				if (!initial) {
					controller()->closeForum();
				} else if (initial != forum) {
					controller()->showForum(initial);
				}
			}
		}
	}
	return clearingQuery || clearingInChat || clearSearchFocus;
}

Widget::~Widget() {
	cancelSearchRequest();

	// Destructor may hide the bar and attempt to double-destroy it.
	base::take(_downloadBar);
}

} // namespace Dialogs
