1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "dialogs/dialogs_widget.h"
9 
10 #include "dialogs/dialogs_inner_widget.h"
11 #include "dialogs/dialogs_search_from_controllers.h"
12 #include "dialogs/dialogs_key.h"
13 #include "dialogs/dialogs_entry.h"
14 #include "history/history.h"
15 #include "history/view/history_view_top_bar_widget.h"
16 #include "ui/widgets/buttons.h"
17 #include "ui/widgets/input_fields.h"
18 #include "ui/wrap/fade_wrap.h"
19 #include "ui/effects/radial_animation.h"
20 #include "ui/ui_utility.h"
21 #include "lang/lang_keys.h"
22 #include "mainwindow.h"
23 #include "mainwidget.h"
24 #include "main/main_domain.h"
25 #include "main/main_session.h"
26 #include "main/main_session_settings.h"
27 #include "apiwrap.h"
28 #include "base/event_filter.h"
29 #include "core/application.h"
30 #include "core/update_checker.h"
31 #include "boxes/peer_list_box.h"
32 #include "boxes/peers/edit_participants_box.h"
33 #include "window/window_adaptive.h"
34 #include "window/window_session_controller.h"
35 #include "window/window_slide_animation.h"
36 #include "window/window_connecting_widget.h"
37 #include "window/window_main_menu.h"
38 #include "storage/storage_media_prepare.h"
39 #include "storage/storage_account.h"
40 #include "storage/storage_domain.h"
41 #include "data/data_session.h"
42 #include "data/data_channel.h"
43 #include "data/data_chat.h"
44 #include "data/data_user.h"
45 #include "data/data_folder.h"
46 #include "data/data_histories.h"
47 #include "data/data_changes.h"
48 #include "facades.h"
49 #include "app.h"
50 #include "styles/style_dialogs.h"
51 #include "styles/style_chat.h"
52 #include "styles/style_info.h"
53 #include "styles/style_window.h"
54 #include "base/qt_adapters.h"
55 
56 #include <QtCore/QMimeData>
57 
58 namespace Dialogs {
59 namespace {
60 
SwitchToChooseFromQuery()61 QString SwitchToChooseFromQuery() {
62 	return qsl("from:");
63 }
64 
65 } // namespace
66 
67 class Widget::BottomButton : public Ui::RippleButton {
68 public:
69 	BottomButton(
70 		QWidget *parent,
71 		const QString &text,
72 		const style::FlatButton &st,
73 		const style::icon &icon,
74 		const style::icon &iconOver);
75 
76 	void setText(const QString &text);
77 
78 protected:
79 	void paintEvent(QPaintEvent *e) override;
80 
81 	void onStateChanged(State was, StateChangeSource source) override;
82 
83 private:
84 	void radialAnimationCallback();
85 
86 	QString _text;
87 	const style::FlatButton &_st;
88 	const style::icon &_icon;
89 	const style::icon &_iconOver;
90 	std::unique_ptr<Ui::InfiniteRadialAnimation> _loading;
91 
92 };
93 
BottomButton(QWidget * parent,const QString & text,const style::FlatButton & st,const style::icon & icon,const style::icon & iconOver)94 Widget::BottomButton::BottomButton(
95 	QWidget *parent,
96 	const QString &text,
97 	const style::FlatButton &st,
98 	const style::icon &icon,
99 	const style::icon &iconOver)
100 : RippleButton(parent, st.ripple)
101 , _text(text.toUpper())
102 , _st(st)
103 , _icon(icon)
104 , _iconOver(iconOver) {
105 	resize(st::columnMinimalWidthLeft, _st.height);
106 }
107 
setText(const QString & text)108 void Widget::BottomButton::setText(const QString &text) {
109 	_text = text.toUpper();
110 	update();
111 }
112 
radialAnimationCallback()113 void Widget::BottomButton::radialAnimationCallback() {
114 	if (!anim::Disabled() && width() < st::columnMinimalWidthLeft) {
115 		update();
116 	}
117 }
118 
onStateChanged(State was,StateChangeSource source)119 void Widget::BottomButton::onStateChanged(State was, StateChangeSource source) {
120 	RippleButton::onStateChanged(was, source);
121 	if ((was & StateFlag::Disabled) != (state() & StateFlag::Disabled)) {
122 		_loading = isDisabled()
123 			? std::make_unique<Ui::InfiniteRadialAnimation>(
124 				[=] { radialAnimationCallback(); },
125 				st::dialogsLoadMoreLoading)
126 			: nullptr;
127 		if (_loading) {
128 			_loading->start();
129 		}
130 	}
131 	update();
132 }
133 
paintEvent(QPaintEvent * e)134 void Widget::BottomButton::paintEvent(QPaintEvent *e) {
135 	Painter p(this);
136 
137 	const auto over = isOver() && !isDisabled();
138 
139 	QRect r(0, height() - _st.height, width(), _st.height);
140 	p.fillRect(r, over ? _st.overBgColor : _st.bgColor);
141 
142 	if (!isDisabled()) {
143 		paintRipple(p, 0, 0);
144 	}
145 
146 	p.setFont(over ? _st.overFont : _st.font);
147 	p.setRenderHint(QPainter::TextAntialiasing);
148 	p.setPen(over ? _st.overColor : _st.color);
149 
150 	if (width() >= st::columnMinimalWidthLeft) {
151 		r.setTop(_st.textTop);
152 		p.drawText(r, _text, style::al_top);
153 	} else if (isDisabled() && _loading) {
154 		_loading->draw(
155 			p,
156 			QPoint(
157 				(width() - st::dialogsLoadMoreLoading.size.width()) / 2,
158 				(height() - st::dialogsLoadMoreLoading.size.height()) / 2),
159 			width());
160 	} else {
161 		(over ? _iconOver : _icon).paintInCenter(p, r);
162 	}
163 }
164 
Widget(QWidget * parent,not_null<Window::SessionController * > controller)165 Widget::Widget(
166 	QWidget *parent,
167 	not_null<Window::SessionController*> controller)
168 : Window::AbstractSectionWidget(parent, controller, nullptr)
169 , _api(&controller->session().mtp())
170 , _searchControls(this)
171 , _mainMenuToggle(_searchControls, st::dialogsMenuToggle)
172 , _searchForNarrowFilters(_searchControls, st::dialogsSearchForNarrowFilters)
173 , _filter(_searchControls, st::dialogsFilter, tr::lng_dlg_filter())
174 , _chooseFromUser(
175 	_searchControls,
176 	object_ptr<Ui::IconButton>(this, st::dialogsSearchFrom))
177 , _jumpToDate(
178 	_searchControls,
179 	object_ptr<Ui::IconButton>(this, st::dialogsCalendar))
180 , _cancelSearch(_searchControls, st::dialogsCancelSearch)
181 , _lockUnlock(_searchControls, st::dialogsLock)
182 , _scroll(this)
183 , _scrollToTop(_scroll, st::dialogsToUp)
184 , _singleMessageSearch(&controller->session()) {
185 	_inner = _scroll->setOwnedWidget(object_ptr<InnerWidget>(this, controller));
186 
187 	_inner->updated(
188 	) | rpl::start_with_next([=] {
189 		onListScroll();
190 	}, lifetime());
191 
192 	rpl::combine(
193 		session().api().dialogsLoadMayBlockByDate(),
194 		session().api().dialogsLoadBlockedByDate()
195 	) | rpl::start_with_next([=](bool mayBlock, bool isBlocked) {
196 		refreshLoadMoreButton(mayBlock, isBlocked);
197 	}, lifetime());
198 
199 	session().changes().historyUpdates(
200 		Data::HistoryUpdate::Flag::MessageSent
201 	) | rpl::start_with_next([=] {
202 		jumpToTop();
203 	}, lifetime());
204 
205 	fullSearchRefreshOn(session().settings().skipArchiveInSearchChanges(
206 	) | rpl::to_empty);
207 
208 	connect(_inner, SIGNAL(draggingScrollDelta(int)), this, SLOT(onDraggingScrollDelta(int)));
209 	connect(_inner, &InnerWidget::mustScrollTo, [=](int top, int bottom) {
210 		if (_scroll) {
211 			_scroll->scrollToY(top, bottom);
212 		}
213 	});
214 	connect(_inner, SIGNAL(dialogMoved(int,int)), this, SLOT(onDialogMoved(int,int)));
215 	connect(_inner, SIGNAL(searchMessages()), this, SLOT(onNeedSearchMessages()));
216 	connect(_inner, SIGNAL(completeHashtag(QString)), this, SLOT(onCompleteHashtag(QString)));
217 	connect(_inner, SIGNAL(refreshHashtags()), this, SLOT(onFilterCursorMoved()));
218 	connect(_inner, SIGNAL(cancelSearchInChat()), this, SLOT(onCancelSearchInChat()));
219 	_inner->cancelSearchFromUserRequests(
220 	) | rpl::start_with_next([=] {
221 		setSearchInChat(_searchInChat, nullptr);
222 		applyFilterUpdate(true);
223 	}, lifetime());
224 	_inner->chosenRow(
225 	) | rpl::start_with_next([=](const ChosenRow &row) {
226 		const auto openSearchResult = !controller->selectingPeer()
227 			&& row.filteredRow;
228 		if (const auto history = row.key.history()) {
229 			controller->content()->choosePeer(
230 				history->peer->id,
231 				(controller->uniqueChatsInSearchResults()
232 					? ShowAtUnreadMsgId
233 					: row.message.fullId.msg));
234 		} else if (const auto folder = row.key.folder()) {
235 			controller->openFolder(folder);
236 		}
237 		if (openSearchResult && !session().supportMode()) {
238 			escape();
239 		}
240 	}, lifetime());
241 
242 	_scroll->geometryChanged(
243 	) | rpl::start_with_next(crl::guard(_inner, [=] {
244 		_inner->onParentGeometryChanged();
245 	}), lifetime());
246 	_scroll->scrolls(
247 	) | rpl::start_with_next([=] {
248 		onListScroll();
249 	}, lifetime());
250 
251 	session().data().chatsListChanges(
252 	) | rpl::filter([=](Data::Folder *folder) {
253 		return (folder == _inner->shownFolder());
254 	}) | rpl::start_with_next([=] {
255 		Ui::PostponeCall(this, [=] { onListScroll(); });
256 	}, lifetime());
257 
258 	connect(_filter, &Ui::FlatInput::cancelled, [=] {
259 		escape();
260 	});
261 	connect(_filter, &Ui::FlatInput::changed, [=] {
262 		applyFilterUpdate();
263 	});
264 	connect(
265 		_filter,
266 		&Ui::FlatInput::cursorPositionChanged,
267 		[=](int from, int to) { onFilterCursorMoved(from, to); });
268 
269 	if (!Core::UpdaterDisabled()) {
270 		Core::UpdateChecker checker;
271 		rpl::merge(
272 			rpl::single(rpl::empty_value()),
273 			checker.isLatest(),
274 			checker.failed(),
275 			checker.ready()
276 		) | rpl::start_with_next([=] {
277 			checkUpdateStatus();
278 		}, lifetime());
279 	}
280 
281 	controller->adaptive().changes(
282 	) | rpl::start_with_next([=] {
283 		updateForwardBar();
284 	}, lifetime());
285 
286 	_cancelSearch->setClickedCallback([this] { onCancelSearch(); });
287 	_jumpToDate->entity()->setClickedCallback([this] { showJumpToDate(); });
288 	_chooseFromUser->entity()->setClickedCallback([this] { showSearchFrom(); });
289 	rpl::single(
290 		rpl::empty_value()
291 	) | rpl::then(
292 		session().domain().local().localPasscodeChanged()
293 	) | rpl::start_with_next([=] {
294 		updateLockUnlockVisibility();
295 	}, lifetime());
296 	_lockUnlock->setClickedCallback([this] {
297 		_lockUnlock->setIconOverride(&st::dialogsUnlockIcon, &st::dialogsUnlockIconOver);
298 		Core::App().lockByPasscode();
299 		_lockUnlock->setIconOverride(nullptr);
300 	});
301 
302 	setupMainMenuToggle();
303 
304 	_searchForNarrowFilters->setClickedCallback([=] { Ui::showChatsList(&session()); });
305 
306 	_chooseByDragTimer.setSingleShot(true);
307 	connect(&_chooseByDragTimer, SIGNAL(timeout()), this, SLOT(onChooseByDrag()));
308 
309 	setAcceptDrops(true);
310 
311 	_searchTimer.setSingleShot(true);
312 	connect(&_searchTimer, SIGNAL(timeout()), this, SLOT(onSearchMessages()));
313 
314 	_inner->setLoadMoreCallback([=] {
315 		const auto state = _inner->state();
316 		if (state == WidgetState::Filtered
317 			&& (!_inner->waitingForSearch()
318 				|| (_searchInMigrated
319 					&& _searchFull
320 					&& !_searchFullMigrated))) {
321 			onSearchMore();
322 		} else {
323 			const auto folder = _inner->shownFolder();
324 			if (!folder || !folder->chatsList()->loaded()) {
325 				session().api().requestDialogs(folder);
326 			}
327 		}
328 	});
329 	_inner->listBottomReached(
330 	) | rpl::start_with_next([=] {
331 		loadMoreBlockedByDate();
332 	}, lifetime());
333 
334 	_filter->setFocusPolicy(Qt::StrongFocus);
335 	_filter->customUpDown(true);
336 
337 	updateJumpToDateVisibility(true);
338 	updateSearchFromVisibility(true);
339 	setupConnectingWidget();
340 	setupSupportMode();
341 	setupScrollUpButton();
342 
343 	changeOpenedFolder(
344 		controller->openedFolder().current(),
345 		anim::type::instant);
346 
347 	controller->openedFolder().changes(
348 	) | rpl::start_with_next([=](Data::Folder *folder) {
349 		changeOpenedFolder(folder, anim::type::normal);
350 	}, lifetime());
351 }
352 
setGeometryWithTopMoved(const QRect & newGeometry,int topDelta)353 void Widget::setGeometryWithTopMoved(
354 		const QRect &newGeometry,
355 		int topDelta) {
356 	_topDelta = topDelta;
357 	bool willBeResized = (size() != newGeometry.size());
358 	if (geometry() != newGeometry) {
359 		auto weak = Ui::MakeWeak(this);
360 		setGeometry(newGeometry);
361 		if (!weak) {
362 			return;
363 		}
364 	}
365 	if (!willBeResized) {
366 		resizeEvent(nullptr);
367 	}
368 	_topDelta = 0;
369 }
370 
setupScrollUpButton()371 void Widget::setupScrollUpButton() {
372 	_scrollToTop->setClickedCallback([=] {
373 		if (_scrollToAnimation.animating()) {
374 			return;
375 		}
376 		scrollToTop();
377 	});
378 	base::install_event_filter(_scrollToTop, [=](not_null<QEvent*> event) {
379 		if (event->type() != QEvent::Wheel) {
380 			return base::EventFilterResult::Continue;
381 		}
382 		return _scroll->viewportEvent(event)
383 			? base::EventFilterResult::Cancel
384 			: base::EventFilterResult::Continue;
385 	});
386 	updateScrollUpVisibility();
387 }
388 
updateScrollUpVisibility()389 void Widget::updateScrollUpVisibility() {
390 	if (_scrollToAnimation.animating()) {
391 		return;
392 	}
393 
394 	startScrollUpButtonAnimation(
395 		(_scroll->scrollTop() > st::historyToDownShownAfter)
396 		&& (_scroll->scrollTop() < _scroll->scrollTopMax()));
397 }
398 
startScrollUpButtonAnimation(bool shown)399 void Widget::startScrollUpButtonAnimation(bool shown) {
400 	const auto smallColumn = (width() < st::columnMinimalWidthLeft);
401 	shown &= !smallColumn;
402 	if (_scrollToTopIsShown == shown) {
403 		return;
404 	}
405 	_scrollToTopIsShown = shown;
406 	_scrollToTopShown.start(
407 		[=] { updateScrollUpPosition(); },
408 		_scrollToTopIsShown ? 0. : 1.,
409 		_scrollToTopIsShown ? 1. : 0.,
410 		smallColumn ? 0 : st::historyToDownDuration);
411 }
412 
updateScrollUpPosition()413 void Widget::updateScrollUpPosition() {
414 	// _scrollToTop is a child widget of _scroll, not me.
415 	auto top = anim::interpolate(
416 		0,
417 		_scrollToTop->height() + st::connectingMargin.top(),
418 		_scrollToTopShown.value(_scrollToTopIsShown ? 1. : 0.));
419 	_scrollToTop->moveToRight(
420 		st::historyToDownPosition.x(),
421 		_scroll->height() - top);
422 	const auto shouldBeHidden =
423 		!_scrollToTopIsShown && !_scrollToTopShown.animating();
424 	if (shouldBeHidden != _scrollToTop->isHidden()) {
425 		_scrollToTop->setVisible(!shouldBeHidden);
426 	}
427 }
428 
setupConnectingWidget()429 void Widget::setupConnectingWidget() {
430 	_connecting = std::make_unique<Window::ConnectionState>(
431 		this,
432 		&session().account(),
433 		controller()->adaptive().oneColumnValue());
434 }
435 
setupSupportMode()436 void Widget::setupSupportMode() {
437 	if (!session().supportMode()) {
438 		return;
439 	}
440 
441 	fullSearchRefreshOn(session().settings().supportAllSearchResultsValue(
442 	) | rpl::to_empty);
443 }
444 
setupMainMenuToggle()445 void Widget::setupMainMenuToggle() {
446 	_mainMenuToggle->setClickedCallback([=] { showMainMenu(); });
447 
448 	rpl::single(
449 		rpl::empty_value()
450 	) | rpl::then(
451 		controller()->filtersMenuChanged()
452 	) | rpl::start_with_next([=] {
453 		const auto filtersHidden = !controller()->filtersWidth();
454 		_mainMenuToggle->setVisible(filtersHidden);
455 		_searchForNarrowFilters->setVisible(!filtersHidden);
456 		updateControlsGeometry();
457 	}, lifetime());
458 
459 	Window::OtherAccountsUnreadState(
460 	) | rpl::start_with_next([=](const Window::OthersUnreadState &state) {
461 		const auto icon = !state.count
462 			? nullptr
463 			: !state.allMuted
464 			? &st::dialogsMenuToggleUnread
465 			: &st::dialogsMenuToggleUnreadMuted;
466 		_mainMenuToggle->setIconOverride(icon, icon);
467 	}, _mainMenuToggle->lifetime());
468 }
469 
fullSearchRefreshOn(rpl::producer<> events)470 void Widget::fullSearchRefreshOn(rpl::producer<> events) {
471 	std::move(
472 		events
473 	) | rpl::filter([=] {
474 		return !_searchQuery.isEmpty();
475 	}) | rpl::start_with_next([=] {
476 		_searchTimer.stop();
477 		_searchCache.clear();
478 		_singleMessageSearch.clear();
479 		for (const auto &[requestId, query] : base::take(_searchQueries)) {
480 			session().api().request(requestId).cancel();
481 		}
482 		_searchQuery = QString();
483 		_scroll->scrollToY(0);
484 		cancelSearchRequest();
485 		onSearchMessages();
486 	}, lifetime());
487 }
488 
updateControlsVisibility(bool fast)489 void Widget::updateControlsVisibility(bool fast) {
490 	updateLoadMoreChatsVisibility();
491 	_scroll->show();
492 	if (_forwardCancel) {
493 		_forwardCancel->show();
494 	}
495 	if (_openedFolder && _filter->hasFocus()) {
496 		setFocus();
497 	}
498 	if (_updateTelegram) {
499 		_updateTelegram->show();
500 	}
501 	_searchControls->setVisible(!_openedFolder);
502 	if (_openedFolder) {
503 		_folderTopBar->show();
504 	} else {
505 		if (hasFocus()) {
506 			_filter->setFocus();
507 			_filter->finishAnimations();
508 		}
509 		updateLockUnlockVisibility();
510 		updateJumpToDateVisibility(fast);
511 		updateSearchFromVisibility(fast);
512 	}
513 	_connecting->setForceHidden(false);
514 }
515 
changeOpenedFolder(Data::Folder * folder,anim::type animated)516 void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) {
517 	_a_show.stop();
518 
519 	if (isHidden()) {
520 		animated = anim::type::instant;
521 	}
522 	if (animated == anim::type::normal) {
523 		_showDirection = folder
524 			? Window::SlideDirection::FromRight
525 			: Window::SlideDirection::FromLeft;
526 		_showAnimationType = ShowAnimation::Internal;
527 		_connecting->setForceHidden(true);
528 		_cacheUnder = grabForFolderSlideAnimation();
529 	}
530 	_openedFolder = folder;
531 	refreshFolderTopBar();
532 	updateControlsVisibility(true);
533 	_inner->changeOpenedFolder(folder);
534 	if (animated == anim::type::normal) {
535 		_connecting->setForceHidden(true);
536 		_cacheOver = grabForFolderSlideAnimation();
537 		_connecting->setForceHidden(false);
538 		startSlideAnimation();
539 	}
540 }
541 
refreshFolderTopBar()542 void Widget::refreshFolderTopBar() {
543 	if (_openedFolder) {
544 		if (!_folderTopBar) {
545 			_folderTopBar.create(this, controller());
546 			updateControlsGeometry();
547 		}
548 		_folderTopBar->setActiveChat(
549 			HistoryView::TopBarWidget::ActiveChat{
550 				.key = _openedFolder,
551 				.section = Dialogs::EntryState::Section::ChatsList,
552 			},
553 			nullptr);
554 	} else {
555 		_folderTopBar.destroy();
556 	}
557 }
558 
grabForFolderSlideAnimation()559 QPixmap Widget::grabForFolderSlideAnimation() {
560 	const auto hidden = _scrollToTop->isHidden();
561 	if (!hidden) {
562 		_scrollToTop->hide();
563 	}
564 
565 	const auto top = _forwardCancel ? _forwardCancel->height() : 0;
566 	const auto rect = QRect(
567 		0,
568 		top,
569 		width(),
570 		(_updateTelegram ? _updateTelegram->y() : height()) - top);
571 	auto result = Ui::GrabWidget(this, rect);
572 
573 	if (!hidden) {
574 		_scrollToTop->show();
575 	}
576 	return result;
577 }
578 
checkUpdateStatus()579 void Widget::checkUpdateStatus() {
580 	Expects(!Core::UpdaterDisabled());
581 
582 	using Checker = Core::UpdateChecker;
583 	if (Checker().state() == Checker::State::Ready) {
584 		if (_updateTelegram) return;
585 		_updateTelegram.create(
586 			this,
587 			tr::lng_update_telegram(tr::now),
588 			st::dialogsUpdateButton,
589 			st::dialogsInstallUpdate,
590 			st::dialogsInstallUpdateOver);
591 		_updateTelegram->show();
592 		_updateTelegram->setClickedCallback([] {
593 			Core::checkReadyUpdate();
594 			App::restart();
595 		});
596 	} else {
597 		if (!_updateTelegram) return;
598 		_updateTelegram.destroy();
599 	}
600 	updateControlsGeometry();
601 }
602 
setInnerFocus()603 void Widget::setInnerFocus() {
604 	if (_openedFolder) {
605 		setFocus();
606 	} else {
607 		_filter->setFocus();
608 	}
609 }
610 
jumpToTop()611 void Widget::jumpToTop() {
612 	if (session().supportMode()) {
613 		return;
614 	}
615 	if ((_filter->getLastText().trimmed().isEmpty() && !_searchInChat)) {
616 		_scrollToAnimation.stop();
617 		_scroll->scrollToY(0);
618 	}
619 }
620 
scrollToTop()621 void Widget::scrollToTop() {
622 	_scrollToAnimation.stop();
623 	auto scrollTop = _scroll->scrollTop();
624 	const auto scrollTo = 0;
625 	const auto maxAnimatedDelta = _scroll->height();
626 	if (scrollTo + maxAnimatedDelta < scrollTop) {
627 		scrollTop = scrollTo + maxAnimatedDelta;
628 		_scroll->scrollToY(scrollTop);
629 	}
630 
631 	startScrollUpButtonAnimation(false);
632 
633 	const auto scroll = [=] {
634 		_scroll->scrollToY(qRound(_scrollToAnimation.value(scrollTo)));
635 	};
636 
637 	_scrollToAnimation.start(
638 		scroll,
639 		scrollTop,
640 		scrollTo,
641 		st::slideDuration,
642 		anim::sineInOut);
643 }
644 
startWidthAnimation()645 void Widget::startWidthAnimation() {
646 	if (!_widthAnimationCache.isNull()) {
647 		return;
648 	}
649 	auto scrollGeometry = _scroll->geometry();
650 	auto grabGeometry = QRect(
651 		scrollGeometry.x(),
652 		scrollGeometry.y(),
653 		st::columnMinimalWidthLeft,
654 		scrollGeometry.height());
655 	_scroll->setGeometry(grabGeometry);
656 	Ui::SendPendingMoveResizeEvents(_scroll);
657 	auto image = QImage(
658 		grabGeometry.size() * cIntRetinaFactor(),
659 		QImage::Format_ARGB32_Premultiplied);
660 	image.setDevicePixelRatio(cRetinaFactor());
661 	image.fill(Qt::transparent);
662 	{
663 		QPainter p(&image);
664 		Ui::RenderWidget(p, _scroll);
665 	}
666 	_widthAnimationCache = Ui::PixmapFromImage(std::move(image));
667 	_scroll->setGeometry(scrollGeometry);
668 	_scroll->hide();
669 }
670 
stopWidthAnimation()671 void Widget::stopWidthAnimation() {
672 	_widthAnimationCache = QPixmap();
673 	if (!_a_show.animating()) {
674 		_scroll->show();
675 	}
676 	update();
677 }
678 
showFast()679 void Widget::showFast() {
680 	if (isHidden()) {
681 		_inner->clearSelection();
682 	}
683 	show();
684 	updateForwardBar();
685 }
686 
showAnimated(Window::SlideDirection direction,const Window::SectionSlideParams & params)687 void Widget::showAnimated(Window::SlideDirection direction, const Window::SectionSlideParams &params) {
688 	_showDirection = direction;
689 	_showAnimationType = ShowAnimation::External;
690 
691 	_a_show.stop();
692 
693 	_cacheUnder = params.oldContentCache;
694 	showFast();
695 	_cacheOver = controller()->content()->grabForShowAnimation(params);
696 
697 	if (_updateTelegram) {
698 		_updateTelegram->hide();
699 	}
700 	_connecting->setForceHidden(true);
701 	startSlideAnimation();
702 }
703 
startSlideAnimation()704 void Widget::startSlideAnimation() {
705 	_scroll->hide();
706 	if (_forwardCancel) {
707 		_forwardCancel->hide();
708 	}
709 	_searchControls->hide();
710 	if (_folderTopBar) {
711 		_folderTopBar->hide();
712 	}
713 
714 	if (_showDirection == Window::SlideDirection::FromLeft) {
715 		std::swap(_cacheUnder, _cacheOver);
716 	}
717 	_a_show.start([=] { animationCallback(); }, 0., 1., st::slideDuration, Window::SlideAnimation::transition());
718 }
719 
floatPlayerHandleWheelEvent(QEvent * e)720 bool Widget::floatPlayerHandleWheelEvent(QEvent *e) {
721 	return _scroll->viewportEvent(e);
722 }
723 
floatPlayerAvailableRect()724 QRect Widget::floatPlayerAvailableRect() {
725 	return mapToGlobal(_scroll->geometry());
726 }
727 
animationCallback()728 void Widget::animationCallback() {
729 	update();
730 	if (!_a_show.animating()) {
731 		_cacheUnder = _cacheOver = QPixmap();
732 
733 		updateControlsVisibility(true);
734 
735 		if (!_filter->hasFocus()) {
736 			controller()->widget()->setInnerFocus();
737 		}
738 	}
739 }
740 
escape()741 void Widget::escape() {
742 	if (controller()->openedFolder().current()) {
743 		controller()->closeFolder();
744 	} else if (!onCancelSearch()) {
745 		if (controller()->activeChatEntryCurrent().key) {
746 			cancelled();
747 		} else if (controller()->activeChatsFilterCurrent()) {
748 			controller()->setActiveChatsFilter(FilterId(0));
749 		}
750 	} else if (!_searchInChat && !controller()->selectingPeer()) {
751 		if (controller()->activeChatEntryCurrent().key) {
752 			cancelled();
753 		}
754 	}
755 }
756 
refreshLoadMoreButton(bool mayBlock,bool isBlocked)757 void Widget::refreshLoadMoreButton(bool mayBlock, bool isBlocked) {
758 	if (!mayBlock) {
759 		if (_loadMoreChats) {
760 			_loadMoreChats.destroy();
761 			updateControlsGeometry();
762 		}
763 		return;
764 	}
765 	if (!_loadMoreChats) {
766 		_loadMoreChats.create(
767 			this,
768 			"Load more",
769 			st::dialogsLoadMoreButton,
770 			st::dialogsLoadMore,
771 			st::dialogsLoadMore);
772 		_loadMoreChats->show();
773 		_loadMoreChats->addClickHandler([=] {
774 			loadMoreBlockedByDate();
775 		});
776 		updateControlsGeometry();
777 	}
778 	const auto loading = !isBlocked;
779 	_loadMoreChats->setDisabled(loading);
780 	_loadMoreChats->setText(loading ? "Loading..." : "Load more");
781 }
782 
loadMoreBlockedByDate()783 void Widget::loadMoreBlockedByDate() {
784 	if (!_loadMoreChats
785 		|| _loadMoreChats->isDisabled()
786 		|| _loadMoreChats->isHidden()) {
787 		return;
788 	}
789 	session().api().requestMoreBlockedByDateDialogs();
790 }
791 
onDraggingScrollDelta(int delta)792 void Widget::onDraggingScrollDelta(int delta) {
793 	_draggingScrollDelta = _scroll ? delta : 0;
794 	if (_draggingScrollDelta) {
795 		if (!_draggingScrollTimer) {
796 			_draggingScrollTimer.create(this);
797 			_draggingScrollTimer->setSingleShot(false);
798 			connect(_draggingScrollTimer, SIGNAL(timeout()), this, SLOT(onDraggingScrollTimer()));
799 		}
800 		_draggingScrollTimer->start(15);
801 	} else {
802 		_draggingScrollTimer.destroy();
803 	}
804 }
805 
onDraggingScrollTimer()806 void Widget::onDraggingScrollTimer() {
807 	const auto delta = (_draggingScrollDelta > 0)
808 		? qMin(_draggingScrollDelta * 3 / 20 + 1, Ui::kMaxScrollSpeed)
809 		: qMax(_draggingScrollDelta * 3 / 20 - 1, -Ui::kMaxScrollSpeed);
810 	_scroll->scrollToY(_scroll->scrollTop() + delta);
811 }
812 
onSearchMessages(bool searchCache)813 bool Widget::onSearchMessages(bool searchCache) {
814 	auto result = false;
815 	auto q = _filter->getLastText().trimmed();
816 	if (q.isEmpty() && !_searchFromAuthor) {
817 		cancelSearchRequest();
818 		_api.request(base::take(_peerSearchRequest)).cancel();
819 		return true;
820 	}
821 	if (searchCache) {
822 		const auto success = _singleMessageSearch.lookup(q, [=] {
823 			onNeedSearchMessages();
824 		});
825 		if (!success) {
826 			return false;
827 		}
828 		const auto i = _searchCache.find(q);
829 		if (i != _searchCache.end()) {
830 			_searchQuery = q;
831 			_searchQueryFrom = _searchFromAuthor;
832 			_searchNextRate = 0;
833 			_searchFull = _searchFullMigrated = false;
834 			cancelSearchRequest();
835 			searchReceived(
836 				_searchInChat
837 					? SearchRequestType::PeerFromStart
838 					: SearchRequestType::FromStart,
839 				i->second,
840 				0);
841 			result = true;
842 		}
843 	} else if (_searchQuery != q || _searchQueryFrom != _searchFromAuthor) {
844 		_searchQuery = q;
845 		_searchQueryFrom = _searchFromAuthor;
846 		_searchNextRate = 0;
847 		_searchFull = _searchFullMigrated = false;
848 		cancelSearchRequest();
849 		if (const auto peer = _searchInChat.peer()) {
850 			auto &histories = session().data().histories();
851 			const auto type = Data::Histories::RequestType::History;
852 			const auto history = session().data().history(peer);
853 			_searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) {
854 				const auto type = SearchRequestType::PeerFromStart;
855 				const auto flags = _searchQueryFrom
856 					? MTP_flags(MTPmessages_Search::Flag::f_from_id)
857 					: MTP_flags(0);
858 				_searchRequest = session().api().request(MTPmessages_Search(
859 					flags,
860 					peer->input,
861 					MTP_string(_searchQuery),
862 					(_searchQueryFrom
863 						? _searchQueryFrom->input
864 						: MTP_inputPeerEmpty()),
865 					MTPint(), // top_msg_id
866 					MTP_inputMessagesFilterEmpty(),
867 					MTP_int(0), // min_date
868 					MTP_int(0), // max_date
869 					MTP_int(0), // offset_id
870 					MTP_int(0), // add_offset
871 					MTP_int(SearchPerPage),
872 					MTP_int(0), // max_id
873 					MTP_int(0), // min_id
874 					MTP_long(0) // hash
875 				)).done([=](const MTPmessages_Messages &result) {
876 					_searchInHistoryRequest = 0;
877 					searchReceived(type, result, _searchRequest);
878 					finish();
879 				}).fail([=](const MTP::Error &error) {
880 					_searchInHistoryRequest = 0;
881 					searchFailed(type, error, _searchRequest);
882 					finish();
883 				}).send();
884 				_searchQueries.emplace(_searchRequest, _searchQuery);
885 				return _searchRequest;
886 			});
887 		} else {
888 			const auto type = SearchRequestType::FromStart;
889 			const auto flags = session().settings().skipArchiveInSearch()
890 				? MTPmessages_SearchGlobal::Flag::f_folder_id
891 				: MTPmessages_SearchGlobal::Flag(0);
892 			const auto folderId = 0;
893 			_searchRequest = session().api().request(MTPmessages_SearchGlobal(
894 				MTP_flags(flags),
895 				MTP_int(folderId),
896 				MTP_string(_searchQuery),
897 				MTP_inputMessagesFilterEmpty(),
898 				MTP_int(0), // min_date
899 				MTP_int(0), // max_date
900 				MTP_int(0),
901 				MTP_inputPeerEmpty(),
902 				MTP_int(0),
903 				MTP_int(SearchPerPage)
904 			)).done([=](const MTPmessages_Messages &result) {
905 				searchReceived(type, result, _searchRequest);
906 			}).fail([=](const MTP::Error &error) {
907 				searchFailed(type, error, _searchRequest);
908 			}).send();
909 			_searchQueries.emplace(_searchRequest, _searchQuery);
910 		}
911 	}
912 	const auto query = Api::ConvertPeerSearchQuery(q);
913 	if (searchForPeersRequired(query)) {
914 		if (searchCache) {
915 			auto i = _peerSearchCache.find(query);
916 			if (i != _peerSearchCache.end()) {
917 				_peerSearchQuery = query;
918 				_peerSearchRequest = 0;
919 				peerSearchReceived(i->second, 0);
920 				result = true;
921 			}
922 		} else if (_peerSearchQuery != query) {
923 			_peerSearchQuery = query;
924 			_peerSearchFull = false;
925 			_peerSearchRequest = _api.request(MTPcontacts_Search(
926 				MTP_string(_peerSearchQuery),
927 				MTP_int(SearchPeopleLimit)
928 			)).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) {
929 				peerSearchReceived(result, requestId);
930 			}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
931 				peopleFailed(error, requestId);
932 			}).send();
933 			_peerSearchQueries.emplace(_peerSearchRequest, _peerSearchQuery);
934 		}
935 	} else {
936 		_peerSearchQuery = query;
937 		_peerSearchFull = true;
938 		peerSearchReceived(
939 			MTP_contacts_found(
940 				MTP_vector<MTPPeer>(0),
941 				MTP_vector<MTPPeer>(0),
942 				MTP_vector<MTPChat>(0),
943 				MTP_vector<MTPUser>(0)),
944 			0);
945 	}
946 	return result;
947 }
948 
searchForPeersRequired(const QString & query) const949 bool Widget::searchForPeersRequired(const QString &query) const {
950 	if (_searchInChat || query.isEmpty()) {
951 		return false;
952 	}
953 	return (query[0] != '#');
954 }
955 
onNeedSearchMessages()956 void Widget::onNeedSearchMessages() {
957 	if (!onSearchMessages(true)) {
958 		_searchTimer.start(AutoSearchTimeout);
959 	}
960 }
961 
onChooseByDrag()962 void Widget::onChooseByDrag() {
963 	_inner->chooseRow();
964 }
965 
showMainMenu()966 void Widget::showMainMenu() {
967 	controller()->widget()->showMainMenu();
968 }
969 
searchMessages(const QString & query,Key inChat)970 void Widget::searchMessages(
971 		const QString &query,
972 		Key inChat) {
973 	auto inChatChanged = [&] {
974 		if (inChat == _searchInChat) {
975 			return false;
976 		} else if (const auto inPeer = inChat.peer()) {
977 			if (inPeer->migrateTo() == _searchInChat.peer()) {
978 				return false;
979 			}
980 		}
981 		return true;
982 	}();
983 	if ((_filter->getLastText() != query) || inChatChanged) {
984 		if (inChat) {
985 			onCancelSearch();
986 			setSearchInChat(inChat);
987 		}
988 		_filter->setText(query);
989 		_filter->updatePlaceholder();
990 		applyFilterUpdate(true);
991 		_searchTimer.stop();
992 		onSearchMessages();
993 
994 		session().local().saveRecentSearchHashtags(query);
995 	}
996 }
997 
onSearchMore()998 void Widget::onSearchMore() {
999 	if (_searchRequest || _searchInHistoryRequest) {
1000 		return;
1001 	}
1002 	if (!_searchFull) {
1003 		auto offsetPeer = _inner->lastSearchPeer();
1004 		auto offsetId = _inner->lastSearchId();
1005 		if (const auto peer = _searchInChat.peer()) {
1006 			auto &histories = session().data().histories();
1007 			const auto type = Data::Histories::RequestType::History;
1008 			const auto history = session().data().history(peer);
1009 			_searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) {
1010 				const auto type = offsetId
1011 					? SearchRequestType::PeerFromOffset
1012 					: SearchRequestType::PeerFromStart;
1013 				auto flags = _searchQueryFrom
1014 					? MTP_flags(MTPmessages_Search::Flag::f_from_id)
1015 					: MTP_flags(0);
1016 				_searchRequest = session().api().request(MTPmessages_Search(
1017 					flags,
1018 					peer->input,
1019 					MTP_string(_searchQuery),
1020 					(_searchQueryFrom
1021 						? _searchQueryFrom->input
1022 						: MTP_inputPeerEmpty()),
1023 					MTPint(), // top_msg_id
1024 					MTP_inputMessagesFilterEmpty(),
1025 					MTP_int(0), // min_date
1026 					MTP_int(0), // max_date
1027 					MTP_int(offsetId),
1028 					MTP_int(0), // add_offset
1029 					MTP_int(SearchPerPage),
1030 					MTP_int(0), // max_id
1031 					MTP_int(0), // min_id
1032 					MTP_long(0) // hash
1033 				)).done([=](const MTPmessages_Messages &result) {
1034 					searchReceived(type, result, _searchRequest);
1035 					_searchInHistoryRequest = 0;
1036 					finish();
1037 				}).fail([=](const MTP::Error &error) {
1038 					searchFailed(type, error, _searchRequest);
1039 					_searchInHistoryRequest = 0;
1040 					finish();
1041 				}).send();
1042 				if (!offsetId) {
1043 					_searchQueries.emplace(_searchRequest, _searchQuery);
1044 				}
1045 				return _searchRequest;
1046 			});
1047 		} else {
1048 			const auto type = offsetId
1049 				? SearchRequestType::FromOffset
1050 				: SearchRequestType::FromStart;
1051 			const auto flags = session().settings().skipArchiveInSearch()
1052 				? MTPmessages_SearchGlobal::Flag::f_folder_id
1053 				: MTPmessages_SearchGlobal::Flag(0);
1054 			const auto folderId = 0;
1055 			_searchRequest = session().api().request(MTPmessages_SearchGlobal(
1056 				MTP_flags(flags),
1057 				MTP_int(folderId),
1058 				MTP_string(_searchQuery),
1059 				MTP_inputMessagesFilterEmpty(),
1060 				MTP_int(0), // min_date
1061 				MTP_int(0), // max_date
1062 				MTP_int(_searchNextRate),
1063 				offsetPeer
1064 					? offsetPeer->input
1065 					: MTP_inputPeerEmpty(),
1066 				MTP_int(offsetId),
1067 				MTP_int(SearchPerPage)
1068 			)).done([=](const MTPmessages_Messages &result) {
1069 				searchReceived(type, result, _searchRequest);
1070 			}).fail([=](const MTP::Error &error) {
1071 				searchFailed(type, error, _searchRequest);
1072 			}).send();
1073 			if (!offsetId) {
1074 				_searchQueries.emplace(_searchRequest, _searchQuery);
1075 			}
1076 		}
1077 	} else if (_searchInMigrated && !_searchFullMigrated) {
1078 		auto offsetMigratedId = _inner->lastSearchMigratedId();
1079 		auto &histories = session().data().histories();
1080 		const auto type = Data::Histories::RequestType::History;
1081 		const auto history = _searchInMigrated;
1082 		_searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) {
1083 			const auto type = offsetMigratedId
1084 				? SearchRequestType::MigratedFromOffset
1085 				: SearchRequestType::MigratedFromStart;
1086 			const auto flags = _searchQueryFrom
1087 				? MTP_flags(MTPmessages_Search::Flag::f_from_id)
1088 				: MTP_flags(0);
1089 			_searchRequest = session().api().request(MTPmessages_Search(
1090 				flags,
1091 				_searchInMigrated->peer->input,
1092 				MTP_string(_searchQuery),
1093 				(_searchQueryFrom
1094 					? _searchQueryFrom->input
1095 					: MTP_inputPeerEmpty()),
1096 				MTPint(), // top_msg_id
1097 				MTP_inputMessagesFilterEmpty(),
1098 				MTP_int(0), // min_date
1099 				MTP_int(0), // max_date
1100 				MTP_int(offsetMigratedId),
1101 				MTP_int(0), // add_offset
1102 				MTP_int(SearchPerPage),
1103 				MTP_int(0), // max_id
1104 				MTP_int(0), // min_id
1105 				MTP_long(0) // hash
1106 			)).done([=](const MTPmessages_Messages &result) {
1107 				searchReceived(type, result, _searchRequest);
1108 				_searchInHistoryRequest = 0;
1109 				finish();
1110 			}).fail([=](const MTP::Error &error) {
1111 				searchFailed(type, error, _searchRequest);
1112 				_searchInHistoryRequest = 0;
1113 				finish();
1114 			}).send();
1115 			return _searchRequest;
1116 		});
1117 	}
1118 }
1119 
searchReceived(SearchRequestType type,const MTPmessages_Messages & result,mtpRequestId requestId)1120 void Widget::searchReceived(
1121 		SearchRequestType type,
1122 		const MTPmessages_Messages &result,
1123 		mtpRequestId requestId) {
1124 	const auto state = _inner->state();
1125 	if (state == WidgetState::Filtered) {
1126 		if (type == SearchRequestType::FromStart || type == SearchRequestType::PeerFromStart) {
1127 			auto i = _searchQueries.find(requestId);
1128 			if (i != _searchQueries.end()) {
1129 				_searchCache[i->second] = result;
1130 				_searchQueries.erase(i);
1131 			}
1132 		}
1133 	}
1134 	const auto inject = (type == SearchRequestType::FromStart
1135 		|| type == SearchRequestType::PeerFromStart)
1136 		? *_singleMessageSearch.lookup(_searchQuery)
1137 		: nullptr;
1138 
1139 	if (_searchRequest != requestId) {
1140 		return;
1141 	}
1142 	switch (result.type()) {
1143 	case mtpc_messages_messages: {
1144 		auto &d = result.c_messages_messages();
1145 		if (_searchRequest != 0) {
1146 			// Don't apply cached data!
1147 			session().data().processUsers(d.vusers());
1148 			session().data().processChats(d.vchats());
1149 		}
1150 		auto &msgs = d.vmessages().v;
1151 		_inner->searchReceived(msgs, inject, type, msgs.size());
1152 		if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) {
1153 			_searchFullMigrated = true;
1154 		} else {
1155 			_searchFull = true;
1156 		}
1157 	} break;
1158 
1159 	case mtpc_messages_messagesSlice: {
1160 		auto &d = result.c_messages_messagesSlice();
1161 		if (_searchRequest != 0) {
1162 			// Don't apply cached data!
1163 			session().data().processUsers(d.vusers());
1164 			session().data().processChats(d.vchats());
1165 		}
1166 		auto &msgs = d.vmessages().v;
1167 		const auto someAdded = _inner->searchReceived(msgs, inject, type, d.vcount().v);
1168 		const auto nextRate = d.vnext_rate();
1169 		const auto rateUpdated = nextRate && (nextRate->v != _searchNextRate);
1170 		const auto finished = (type == SearchRequestType::FromStart || type == SearchRequestType::FromOffset)
1171 			? !rateUpdated
1172 			: !someAdded;
1173 		if (rateUpdated) {
1174 			_searchNextRate = nextRate->v;
1175 		}
1176 		if (finished) {
1177 			if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) {
1178 				_searchFullMigrated = true;
1179 			} else {
1180 				_searchFull = true;
1181 			}
1182 		}
1183 	} break;
1184 
1185 	case mtpc_messages_channelMessages: {
1186 		auto &d = result.c_messages_channelMessages();
1187 		if (const auto peer = _searchInChat.peer()) {
1188 			if (const auto channel = peer->asChannel()) {
1189 				channel->ptsReceived(d.vpts().v);
1190 			} else {
1191 				LOG(("API Error: "
1192 					"received messages.channelMessages when no channel "
1193 					"was passed! (Widget::searchReceived)"));
1194 			}
1195 		} else {
1196 			LOG(("API Error: "
1197 				"received messages.channelMessages when no channel "
1198 				"was passed! (Widget::searchReceived)"));
1199 		}
1200 		if (_searchRequest != 0) {
1201 			// Don't apply cached data!
1202 			session().data().processUsers(d.vusers());
1203 			session().data().processChats(d.vchats());
1204 		}
1205 		auto &msgs = d.vmessages().v;
1206 		if (!_inner->searchReceived(msgs, inject, type, d.vcount().v)) {
1207 			if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) {
1208 				_searchFullMigrated = true;
1209 			} else {
1210 				_searchFull = true;
1211 			}
1212 		}
1213 	} break;
1214 
1215 	case mtpc_messages_messagesNotModified: {
1216 		LOG(("API Error: received messages.messagesNotModified! (Widget::searchReceived)"));
1217 		if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) {
1218 			_searchFullMigrated = true;
1219 		} else {
1220 			_searchFull = true;
1221 		}
1222 	} break;
1223 	}
1224 
1225 	_searchRequest = 0;
1226 	onListScroll();
1227 	update();
1228 }
1229 
peerSearchReceived(const MTPcontacts_Found & result,mtpRequestId requestId)1230 void Widget::peerSearchReceived(
1231 		const MTPcontacts_Found &result,
1232 		mtpRequestId requestId) {
1233 	const auto state = _inner->state();
1234 	auto q = _peerSearchQuery;
1235 	if (state == WidgetState::Filtered) {
1236 		auto i = _peerSearchQueries.find(requestId);
1237 		if (i != _peerSearchQueries.end()) {
1238 			_peerSearchCache[i->second] = result;
1239 			_peerSearchQueries.erase(i);
1240 		}
1241 	}
1242 	if (_peerSearchRequest == requestId) {
1243 		switch (result.type()) {
1244 		case mtpc_contacts_found: {
1245 			auto &d = result.c_contacts_found();
1246 			session().data().processUsers(d.vusers());
1247 			session().data().processChats(d.vchats());
1248 			_inner->peerSearchReceived(q, d.vmy_results().v, d.vresults().v);
1249 		} break;
1250 		}
1251 
1252 		_peerSearchRequest = 0;
1253 		onListScroll();
1254 	}
1255 }
1256 
searchFailed(SearchRequestType type,const MTP::Error & error,mtpRequestId requestId)1257 void Widget::searchFailed(
1258 		SearchRequestType type,
1259 		const MTP::Error &error,
1260 		mtpRequestId requestId) {
1261 	if (error.type() == qstr("SEARCH_QUERY_EMPTY")) {
1262 		searchReceived(
1263 			type,
1264 			MTP_messages_messages(
1265 				MTP_vector<MTPMessage>(),
1266 				MTP_vector<MTPChat>(),
1267 				MTP_vector<MTPUser>()),
1268 			requestId);
1269 	} else if (_searchRequest == requestId) {
1270 		_searchRequest = 0;
1271 		if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) {
1272 			_searchFullMigrated = true;
1273 		} else {
1274 			_searchFull = true;
1275 		}
1276 	}
1277 }
1278 
peopleFailed(const MTP::Error & error,mtpRequestId requestId)1279 void Widget::peopleFailed(const MTP::Error &error, mtpRequestId requestId) {
1280 	if (_peerSearchRequest == requestId) {
1281 		_peerSearchRequest = 0;
1282 		_peerSearchFull = true;
1283 	}
1284 }
1285 
dragEnterEvent(QDragEnterEvent * e)1286 void Widget::dragEnterEvent(QDragEnterEvent *e) {
1287 	using namespace Storage;
1288 
1289 	if (controller()->selectingPeer()) {
1290 		return;
1291 	}
1292 
1293 	const auto data = e->mimeData();
1294 	_dragInScroll = false;
1295 	_dragForward = controller()->adaptive().isOneColumn()
1296 		? false
1297 		: data->hasFormat(qsl("application/x-td-forward"));
1298 	if (_dragForward) {
1299 		e->setDropAction(Qt::CopyAction);
1300 		e->accept();
1301 		updateDragInScroll(_scroll->geometry().contains(e->pos()));
1302 	} else if (ComputeMimeDataState(data) != MimeDataState::None) {
1303 		e->setDropAction(Qt::CopyAction);
1304 		e->accept();
1305 	}
1306 	_chooseByDragTimer.stop();
1307 }
1308 
dragMoveEvent(QDragMoveEvent * e)1309 void Widget::dragMoveEvent(QDragMoveEvent *e) {
1310 	if (_scroll->geometry().contains(e->pos())) {
1311 		if (_dragForward) {
1312 			updateDragInScroll(true);
1313 		} else {
1314 			_chooseByDragTimer.start(ChoosePeerByDragTimeout);
1315 		}
1316 		if (_inner->updateFromParentDrag(mapToGlobal(e->pos()))) {
1317 			e->setDropAction(Qt::CopyAction);
1318 		} else {
1319 			e->setDropAction(Qt::IgnoreAction);
1320 		}
1321 	} else {
1322 		if (_dragForward) updateDragInScroll(false);
1323 		_inner->dragLeft();
1324 		e->setDropAction(Qt::IgnoreAction);
1325 	}
1326 	e->accept();
1327 }
1328 
dragLeaveEvent(QDragLeaveEvent * e)1329 void Widget::dragLeaveEvent(QDragLeaveEvent *e) {
1330 	if (_dragForward) {
1331 		updateDragInScroll(false);
1332 	} else {
1333 		_chooseByDragTimer.stop();
1334 	}
1335 	_inner->dragLeft();
1336 	e->accept();
1337 }
1338 
updateDragInScroll(bool inScroll)1339 void Widget::updateDragInScroll(bool inScroll) {
1340 	if (_dragInScroll != inScroll) {
1341 		_dragInScroll = inScroll;
1342 		if (_dragInScroll) {
1343 			controller()->content()->showForwardLayer({});
1344 		} else {
1345 			controller()->content()->dialogsCancelled();
1346 		}
1347 	}
1348 }
1349 
dropEvent(QDropEvent * e)1350 void Widget::dropEvent(QDropEvent *e) {
1351 	_chooseByDragTimer.stop();
1352 	if (_scroll->geometry().contains(e->pos())) {
1353 		if (auto peer = _inner->updateFromParentDrag(mapToGlobal(e->pos()))) {
1354 			e->acceptProposedAction();
1355 			controller()->content()->onFilesOrForwardDrop(
1356 				peer->id,
1357 				e->mimeData());
1358 			controller()->widget()->raise();
1359 			controller()->widget()->activateWindow();
1360 		}
1361 	}
1362 }
1363 
onListScroll()1364 void Widget::onListScroll() {
1365 	const auto scrollTop = _scroll->scrollTop();
1366 	_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
1367 	updateScrollUpVisibility();
1368 
1369 	// Fix button rendering glitch, Qt bug with WA_OpaquePaintEvent widgets.
1370 	_scrollToTop->update();
1371 }
1372 
applyFilterUpdate(bool force)1373 void Widget::applyFilterUpdate(bool force) {
1374 	if (_a_show.animating() && !force) {
1375 		return;
1376 	}
1377 
1378 	auto filterText = _filter->getLastText();
1379 	_inner->applyFilterUpdate(filterText, force);
1380 	if (filterText.isEmpty() && !_searchFromAuthor) {
1381 		clearSearchCache();
1382 	}
1383 	_cancelSearch->toggle(!filterText.isEmpty(), anim::type::normal);
1384 	updateLoadMoreChatsVisibility();
1385 	updateJumpToDateVisibility();
1386 
1387 	if (filterText.isEmpty()) {
1388 		_peerSearchCache.clear();
1389 		for (const auto &[requestId, query] : base::take(_peerSearchQueries)) {
1390 			_api.request(requestId).cancel();
1391 		}
1392 		_peerSearchQuery = QString();
1393 	}
1394 
1395 	if (_chooseFromUser->toggled() || _searchFromAuthor) {
1396 		auto switchToChooseFrom = SwitchToChooseFromQuery();
1397 		if (_lastFilterText != switchToChooseFrom
1398 			&& switchToChooseFrom.startsWith(_lastFilterText)
1399 			&& filterText == switchToChooseFrom) {
1400 			showSearchFrom();
1401 		}
1402 	}
1403 	_lastFilterText = filterText;
1404 }
1405 
searchInChat(Key chat)1406 void Widget::searchInChat(Key chat) {
1407 	onCancelSearch();
1408 	setSearchInChat(chat);
1409 	applyFilterUpdate(true);
1410 }
1411 
setSearchInChat(Key chat,PeerData * from)1412 void Widget::setSearchInChat(Key chat, PeerData *from) {
1413 	if (chat.folder()) {
1414 		chat = Key();
1415 	}
1416 	_searchInMigrated = nullptr;
1417 	if (const auto peer = chat.peer()) {
1418 		if (const auto migrateTo = peer->migrateTo()) {
1419 			return setSearchInChat(peer->owner().history(migrateTo), from);
1420 		} else if (const auto migrateFrom = peer->migrateFrom()) {
1421 			_searchInMigrated = peer->owner().history(migrateFrom);
1422 		}
1423 	}
1424 	const auto searchInPeerUpdated = (_searchInChat != chat);
1425 	if (searchInPeerUpdated) {
1426 		_searchInChat = chat;
1427 		from = nullptr;
1428 		controller()->searchInChat = _searchInChat;
1429 		updateJumpToDateVisibility();
1430 	} else if (!_searchInChat) {
1431 		from = nullptr;
1432 	}
1433 	if (_searchFromAuthor != from || searchInPeerUpdated) {
1434 		_searchFromAuthor = from;
1435 		updateSearchFromVisibility();
1436 		clearSearchCache();
1437 	}
1438 	_inner->searchInChat(_searchInChat, _searchFromAuthor);
1439 	if (_searchFromAuthor && _lastFilterText == SwitchToChooseFromQuery()) {
1440 		onCancelSearch();
1441 	}
1442 	_filter->setFocus();
1443 }
1444 
clearSearchCache()1445 void Widget::clearSearchCache() {
1446 	_searchCache.clear();
1447 	_singleMessageSearch.clear();
1448 	for (const auto &[requestId, query] : base::take(_searchQueries)) {
1449 		session().api().request(requestId).cancel();
1450 	}
1451 	_searchQuery = QString();
1452 	_searchQueryFrom = nullptr;
1453 	cancelSearchRequest();
1454 }
1455 
showJumpToDate()1456 void Widget::showJumpToDate() {
1457 	if (_searchInChat) {
1458 		controller()->showJumpToDate(_searchInChat, QDate());
1459 	}
1460 }
1461 
showSearchFrom()1462 void Widget::showSearchFrom() {
1463 	if (const auto peer = _searchInChat.peer()) {
1464 		const auto chat = _searchInChat;
1465 		ShowSearchFromBox(
1466 			peer,
1467 			crl::guard(this, [=](not_null<PeerData*> from) {
1468 				Ui::hideLayer();
1469 				setSearchInChat(chat, from);
1470 				applyFilterUpdate(true);
1471 			}),
1472 			crl::guard(this, [=] { _filter->setFocus(); }));
1473 	}
1474 }
1475 
onFilterCursorMoved(int from,int to)1476 void Widget::onFilterCursorMoved(int from, int to) {
1477 	if (to < 0) to = _filter->cursorPosition();
1478 	QString t = _filter->getLastText();
1479 	QStringView r;
1480 	for (int start = to; start > 0;) {
1481 		--start;
1482 		if (t.size() <= start) break;
1483 		if (t.at(start) == '#') {
1484 			r = base::StringViewMid(t, start, to - start);
1485 			break;
1486 		}
1487 		if (!t.at(start).isLetterOrNumber() && t.at(start) != '_') break;
1488 	}
1489 	_inner->onHashtagFilterUpdate(r);
1490 }
1491 
onCompleteHashtag(QString tag)1492 void Widget::onCompleteHashtag(QString tag) {
1493 	QString t = _filter->getLastText(), r;
1494 	int cur = _filter->cursorPosition();
1495 	for (int start = cur; start > 0;) {
1496 		--start;
1497 		if (t.size() <= start) break;
1498 		if (t.at(start) == '#') {
1499 			if (cur == start + 1
1500 				|| base::StringViewMid(t, start + 1, cur - start - 1)
1501 					== base::StringViewMid(tag, 0, cur - start - 1)) {
1502 				for (; cur < t.size() && cur - start - 1 < tag.size(); ++cur) {
1503 					if (t.at(cur) != tag.at(cur - start - 1)) break;
1504 				}
1505 				if (cur - start - 1 == tag.size() && cur < t.size() && t.at(cur) == ' ') ++cur;
1506 				r = t.mid(0, start + 1) + tag + ' ' + t.mid(cur);
1507 				_filter->setText(r);
1508 				_filter->setCursorPosition(start + 1 + tag.size() + 1);
1509 				applyFilterUpdate(true);
1510 				return;
1511 			}
1512 			break;
1513 		}
1514 		if (!t.at(start).isLetterOrNumber() && t.at(start) != '_') break;
1515 	}
1516 	_filter->setText(t.mid(0, cur) + '#' + tag + ' ' + t.mid(cur));
1517 	_filter->setCursorPosition(cur + 1 + tag.size() + 1);
1518 	applyFilterUpdate(true);
1519 }
1520 
resizeEvent(QResizeEvent * e)1521 void Widget::resizeEvent(QResizeEvent *e) {
1522 	updateControlsGeometry();
1523 }
1524 
updateLockUnlockVisibility()1525 void Widget::updateLockUnlockVisibility() {
1526 	if (_a_show.animating()) {
1527 		return;
1528 	}
1529 	const auto hidden = !session().domain().local().hasLocalPasscode();
1530 	if (_lockUnlock->isHidden() != hidden) {
1531 		_lockUnlock->setVisible(!hidden);
1532 		updateControlsGeometry();
1533 	}
1534 }
1535 
updateLoadMoreChatsVisibility()1536 void Widget::updateLoadMoreChatsVisibility() {
1537 	if (_a_show.animating() || !_loadMoreChats) {
1538 		return;
1539 	}
1540 	const auto hidden = (_openedFolder != nullptr)
1541 		|| !_filter->getLastText().isEmpty();
1542 	if (_loadMoreChats->isHidden() != hidden) {
1543 		_loadMoreChats->setVisible(!hidden);
1544 		updateControlsGeometry();
1545 	}
1546 }
1547 
updateJumpToDateVisibility(bool fast)1548 void Widget::updateJumpToDateVisibility(bool fast) {
1549 	if (_a_show.animating()) return;
1550 
1551 	_jumpToDate->toggle(
1552 		(_searchInChat && _filter->getLastText().isEmpty()),
1553 		fast ? anim::type::instant : anim::type::normal);
1554 }
1555 
updateSearchFromVisibility(bool fast)1556 void Widget::updateSearchFromVisibility(bool fast) {
1557 	auto visible = [&] {
1558 		if (const auto peer = _searchInChat.peer()) {
1559 			if (peer->isChat() || peer->isMegagroup()) {
1560 				return !_searchFromAuthor;
1561 			}
1562 		}
1563 		return false;
1564 	}();
1565 	auto changed = (visible == !_chooseFromUser->toggled());
1566 	_chooseFromUser->toggle(
1567 		visible,
1568 		fast ? anim::type::instant : anim::type::normal);
1569 	if (changed) {
1570 		auto margins = st::dialogsFilter.textMrg;
1571 		if (visible) {
1572 			margins.setRight(margins.right() + _chooseFromUser->width());
1573 		}
1574 		_filter->setTextMrg(margins);
1575 	}
1576 }
1577 
updateControlsGeometry()1578 void Widget::updateControlsGeometry() {
1579 	auto filterAreaTop = 0;
1580 	if (_forwardCancel) {
1581 		_forwardCancel->moveToLeft(0, filterAreaTop);
1582 		filterAreaTop += st::dialogsForwardHeight;
1583 	}
1584 	auto smallLayoutWidth = (st::dialogsPadding.x() + st::dialogsPhotoSize + st::dialogsPadding.x());
1585 	auto smallLayoutRatio = (width() < st::columnMinimalWidthLeft) ? (st::columnMinimalWidthLeft - width()) / float64(st::columnMinimalWidthLeft - smallLayoutWidth) : 0.;
1586 	auto filterLeft = (controller()->filtersWidth() ? st::dialogsFilterSkip : st::dialogsFilterPadding.x() + _mainMenuToggle->width()) + st::dialogsFilterPadding.x();
1587 	auto filterRight = (session().domain().local().hasLocalPasscode() ? (st::dialogsFilterPadding.x() + _lockUnlock->width()) : st::dialogsFilterSkip) + st::dialogsFilterPadding.x();
1588 	auto filterWidth = qMax(width(), st::columnMinimalWidthLeft) - filterLeft - filterRight;
1589 	auto filterAreaHeight = st::topBarHeight;
1590 	_searchControls->setGeometry(0, filterAreaTop, width(), filterAreaHeight);
1591 	if (_folderTopBar) {
1592 		_folderTopBar->setGeometry(_searchControls->geometry());
1593 	}
1594 
1595 	auto filterTop = (filterAreaHeight - _filter->height()) / 2;
1596 	filterLeft = anim::interpolate(filterLeft, smallLayoutWidth, smallLayoutRatio);
1597 	_filter->setGeometryToLeft(filterLeft, filterTop, filterWidth, _filter->height());
1598 	auto mainMenuLeft = anim::interpolate(st::dialogsFilterPadding.x(), (smallLayoutWidth - _mainMenuToggle->width()) / 2, smallLayoutRatio);
1599 	_mainMenuToggle->moveToLeft(mainMenuLeft, st::dialogsFilterPadding.y());
1600 	const auto searchLeft = anim::interpolate(
1601 		-_searchForNarrowFilters->width(),
1602 		(smallLayoutWidth - _searchForNarrowFilters->width()) / 2,
1603 		smallLayoutRatio);
1604 	_searchForNarrowFilters->moveToLeft(searchLeft, st::dialogsFilterPadding.y());
1605 
1606 	auto right = filterLeft + filterWidth;
1607 	_lockUnlock->moveToLeft(right + st::dialogsFilterPadding.x(), st::dialogsFilterPadding.y());
1608 	_cancelSearch->moveToLeft(right - _cancelSearch->width(), _filter->y());
1609 	right -= _jumpToDate->width(); _jumpToDate->moveToLeft(right, _filter->y());
1610 	right -= _chooseFromUser->width(); _chooseFromUser->moveToLeft(right, _filter->y());
1611 
1612 	auto scrollTop = filterAreaTop + filterAreaHeight;
1613 	auto newScrollTop = _scroll->scrollTop() + _topDelta;
1614 	auto scrollHeight = height() - scrollTop;
1615 	const auto putBottomButton = [&](object_ptr<BottomButton> &button) {
1616 		if (button && !button->isHidden()) {
1617 			const auto buttonHeight = button->height();
1618 			scrollHeight -= buttonHeight;
1619 			button->setGeometry(
1620 				0,
1621 				scrollTop + scrollHeight,
1622 				width(),
1623 				buttonHeight);
1624 		}
1625 	};
1626 	putBottomButton(_updateTelegram);
1627 	putBottomButton(_loadMoreChats);
1628 	auto wasScrollHeight = _scroll->height();
1629 	_scroll->setGeometry(0, scrollTop, width(), scrollHeight);
1630 	_inner->resize(width(), _inner->height());
1631 	if (scrollHeight != wasScrollHeight) {
1632 		controller()->floatPlayerAreaUpdated();
1633 	}
1634 	if (_topDelta) {
1635 		_scroll->scrollToY(newScrollTop);
1636 	} else {
1637 		onListScroll();
1638 	}
1639 	if (_scrollToTopIsShown) {
1640 		updateScrollUpPosition();
1641 	}
1642 }
1643 
closeForwardBarRequests() const1644 rpl::producer<> Widget::closeForwardBarRequests() const {
1645 	return _closeForwardBarRequests.events();
1646 }
1647 
updateForwardBar()1648 void Widget::updateForwardBar() {
1649 	auto selecting = controller()->selectingPeer();
1650 	auto oneColumnSelecting = (controller()->adaptive().isOneColumn()
1651 		&& selecting);
1652 	if (!oneColumnSelecting == !_forwardCancel) {
1653 		return;
1654 	}
1655 	if (oneColumnSelecting) {
1656 		_forwardCancel.create(this, st::dialogsForwardCancel);
1657 		_forwardCancel->setClickedCallback([=] {
1658 			_closeForwardBarRequests.fire({});
1659 		});
1660 		if (!_a_show.animating()) _forwardCancel->show();
1661 	} else {
1662 		_forwardCancel.destroyDelayed();
1663 	}
1664 	updateControlsGeometry();
1665 	update();
1666 }
1667 
keyPressEvent(QKeyEvent * e)1668 void Widget::keyPressEvent(QKeyEvent *e) {
1669 	if (e->key() == Qt::Key_Escape) {
1670 		if (_openedFolder) {
1671 			controller()->closeFolder();
1672 		} else {
1673 			e->ignore();
1674 		}
1675 	} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
1676 		if (!_inner->chooseRow()) {
1677 			const auto state = _inner->state();
1678 			if (state == WidgetState::Default
1679 				|| (state == WidgetState::Filtered
1680 					&& (!_inner->waitingForSearch() ||  _inner->hasFilteredResults()))) {
1681 				_inner->selectSkip(1);
1682 				_inner->chooseRow();
1683 			} else {
1684 				onSearchMessages();
1685 			}
1686 		}
1687 	} else if (e->key() == Qt::Key_Down) {
1688 		_inner->selectSkip(1);
1689 	} else if (e->key() == Qt::Key_Up) {
1690 		_inner->selectSkip(-1);
1691 	} else if (e->key() == Qt::Key_PageDown) {
1692 		_inner->selectSkipPage(_scroll->height(), 1);
1693 	} else if (e->key() == Qt::Key_PageUp) {
1694 		_inner->selectSkipPage(_scroll->height(), -1);
1695 	} else {
1696 		e->ignore();
1697 	}
1698 }
1699 
paintEvent(QPaintEvent * e)1700 void Widget::paintEvent(QPaintEvent *e) {
1701 	if (controller()->widget()->contentOverlapped(this, e)) {
1702 		return;
1703 	}
1704 
1705 	Painter p(this);
1706 	QRect r(e->rect());
1707 	if (r != rect()) {
1708 		p.setClipRect(r);
1709 	}
1710 	if (_a_show.animating()) {
1711 		const auto progress = _a_show.value(1.);
1712 		const auto top = (_showAnimationType == ShowAnimation::Internal)
1713 			? (_forwardCancel ? _forwardCancel->height() : 0)
1714 			: 0;
1715 		const auto shift = std::min(st::slideShift, width() / 2);
1716 		const auto retina = cIntRetinaFactor();
1717 		const auto fromLeft = (_showDirection == Window::SlideDirection::FromLeft);
1718 		const auto coordUnder = fromLeft ? anim::interpolate(-shift, 0, progress) : anim::interpolate(0, -shift, progress);
1719 		const auto coordOver = fromLeft ? anim::interpolate(0, width(), progress) : anim::interpolate(width(), 0, progress);
1720 		const auto shadow = fromLeft ? (1. - progress) : progress;
1721 		if (coordOver > 0) {
1722 			p.drawPixmap(QRect(0, top, coordOver, _cacheUnder.height() / retina), _cacheUnder, QRect(-coordUnder * retina, 0, coordOver * retina, _cacheUnder.height()));
1723 			p.setOpacity(shadow);
1724 			p.fillRect(0, top, coordOver, _cacheUnder.height() / retina, st::slideFadeOutBg);
1725 			p.setOpacity(1);
1726 		}
1727 		p.drawPixmap(QRect(coordOver, top, _cacheOver.width() / retina, _cacheOver.height() / retina), _cacheOver, QRect(0, 0, _cacheOver.width(), _cacheOver.height()));
1728 		p.setOpacity(shadow);
1729 		st::slideShadow.fill(p, QRect(coordOver - st::slideShadow.width(), top, st::slideShadow.width(), _cacheOver.height() / retina));
1730 		return;
1731 	}
1732 	auto aboveTop = 0;
1733 	if (_forwardCancel) {
1734 		p.fillRect(0, aboveTop, width(), st::dialogsForwardHeight, st::dialogsForwardBg);
1735 		p.setPen(st::dialogsForwardFg);
1736 		p.setFont(st::dialogsForwardFont);
1737 		p.drawTextLeft(st::dialogsForwardTextLeft, st::dialogsForwardTextTop, width(), tr::lng_forward_choose(tr::now));
1738 		aboveTop += st::dialogsForwardHeight;
1739 	}
1740 	auto above = QRect(0, aboveTop, width(), _scroll->y() - aboveTop);
1741 	if (above.intersects(r)) {
1742 		p.fillRect(above.intersected(r), st::dialogsBg);
1743 	}
1744 
1745 	auto belowTop = _scroll->y() + qMin(_scroll->height(), _inner->height());
1746 	if (!_widthAnimationCache.isNull()) {
1747 		p.drawPixmapLeft(0, _scroll->y(), width(), _widthAnimationCache);
1748 		belowTop = _scroll->y() + (_widthAnimationCache.height() / cIntRetinaFactor());
1749 	}
1750 
1751 	auto below = QRect(0, belowTop, width(), height() - belowTop);
1752 	if (below.intersects(r)) {
1753 		p.fillRect(below.intersected(r), st::dialogsBg);
1754 	}
1755 }
1756 
scrollToEntry(const RowDescriptor & entry)1757 void Widget::scrollToEntry(const RowDescriptor &entry) {
1758 	_inner->scrollToEntry(entry);
1759 }
1760 
cancelSearchRequest()1761 void Widget::cancelSearchRequest() {
1762 	session().api().request(base::take(_searchRequest)).cancel();
1763 	session().data().histories().cancelRequest(
1764 		base::take(_searchInHistoryRequest));
1765 }
1766 
onCancelSearch()1767 bool Widget::onCancelSearch() {
1768 	bool clearing = !_filter->getLastText().isEmpty();
1769 	cancelSearchRequest();
1770 	if (_searchInChat && !clearing) {
1771 		if (controller()->adaptive().isOneColumn()) {
1772 			if (const auto peer = _searchInChat.peer()) {
1773 				Ui::showPeerHistory(peer, ShowAtUnreadMsgId);
1774 			} else {
1775 				Unexpected("Empty key in onCancelSearch().");
1776 			}
1777 		}
1778 		setSearchInChat(Key());
1779 		clearing = true;
1780 	}
1781 	_inner->clearFilter();
1782 	_filter->clear();
1783 	_filter->updatePlaceholder();
1784 	applyFilterUpdate();
1785 	return clearing;
1786 }
1787 
onCancelSearchInChat()1788 void Widget::onCancelSearchInChat() {
1789 	cancelSearchRequest();
1790 	const auto isOneColumn = controller()->adaptive().isOneColumn();
1791 	if (_searchInChat) {
1792 		if (isOneColumn
1793 			&& !controller()->selectingPeer()
1794 			&& _filter->getLastText().trimmed().isEmpty()) {
1795 			if (const auto peer = _searchInChat.peer()) {
1796 				Ui::showPeerHistory(peer, ShowAtUnreadMsgId);
1797 			} else {
1798 				Unexpected("Empty key in onCancelSearchInPeer().");
1799 			}
1800 		}
1801 		setSearchInChat(Key());
1802 	}
1803 	applyFilterUpdate(true);
1804 	if (!isOneColumn && !controller()->selectingPeer()) {
1805 		cancelled();
1806 	}
1807 }
1808 
onDialogMoved(int movedFrom,int movedTo)1809 void Widget::onDialogMoved(int movedFrom, int movedTo) {
1810 	int32 st = _scroll->scrollTop();
1811 	if (st > movedTo && st < movedFrom) {
1812 		_scroll->scrollToY(st + st::dialogsRowHeight);
1813 	}
1814 }
1815 
~Widget()1816 Widget::~Widget() {
1817 	cancelSearchRequest();
1818 }
1819 
1820 } // namespace Dialogs
1821