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 "boxes/peer_list_box.h"
9 
10 #include "main/main_session.h"
11 #include "mainwidget.h"
12 #include "ui/widgets/multi_select.h"
13 #include "ui/widgets/labels.h"
14 #include "ui/widgets/scroll_area.h"
15 #include "ui/widgets/popup_menu.h"
16 #include "ui/effects/round_checkbox.h"
17 #include "ui/effects/ripple_animation.h"
18 #include "ui/empty_userpic.h"
19 #include "ui/wrap/slide_wrap.h"
20 #include "ui/text/text_options.h"
21 #include "lang/lang_keys.h"
22 #include "storage/file_download.h"
23 #include "data/data_peer_values.h"
24 #include "data/data_chat.h"
25 #include "data/data_session.h"
26 #include "data/data_changes.h"
27 #include "base/unixtime.h"
28 #include "styles/style_layers.h"
29 #include "styles/style_boxes.h"
30 #include "styles/style_dialogs.h"
31 #include "styles/style_widgets.h"
32 
33 #include <rpl/range.h>
34 
PaintUserpicCallback(not_null<PeerData * > peer,bool respectSavedMessagesChat)35 PaintRoundImageCallback PaintUserpicCallback(
36 		not_null<PeerData*> peer,
37 		bool respectSavedMessagesChat) {
38 	if (respectSavedMessagesChat) {
39 		if (peer->isSelf()) {
40 			return [](Painter &p, int x, int y, int outerWidth, int size) {
41 				Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
42 			};
43 		} else if (peer->isRepliesChat()) {
44 			return [](Painter &p, int x, int y, int outerWidth, int size) {
45 				Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
46 			};
47 		}
48 	}
49 	auto userpic = std::shared_ptr<Data::CloudImageView>();
50 	return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
51 		peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
52 	};
53 }
54 
PeerListBox(QWidget *,std::unique_ptr<PeerListController> controller,Fn<void (not_null<PeerListBox * >)> init)55 PeerListBox::PeerListBox(
56 	QWidget*,
57 	std::unique_ptr<PeerListController> controller,
58 	Fn<void(not_null<PeerListBox*>)> init)
59 : _controller(std::move(controller))
60 , _init(std::move(init)) {
61 	Expects(_controller != nullptr);
62 }
63 
createMultiSelect()64 void PeerListBox::createMultiSelect() {
65 	Expects(_select == nullptr);
66 
67 	auto entity = object_ptr<Ui::MultiSelect>(
68 		this,
69 		(_controller->selectSt()
70 			? *_controller->selectSt()
71 			: st::defaultMultiSelect),
72 		tr::lng_participant_filter());
73 	_select.create(this, std::move(entity));
74 	_select->heightValue(
75 	) | rpl::start_with_next(
76 		[this] { updateScrollSkips(); },
77 		lifetime());
78 	_select->entity()->setSubmittedCallback([=](Qt::KeyboardModifiers) {
79 		content()->submitted();
80 	});
81 	_select->entity()->setQueryChangedCallback([=](const QString &query) {
82 		searchQueryChanged(query);
83 	});
84 	_select->entity()->setItemRemovedCallback([=](uint64 itemId) {
85 		if (_controller->handleDeselectForeignRow(itemId)) {
86 			return;
87 		}
88 		if (const auto peer = _controller->session().data().peerLoaded(PeerId(itemId))) {
89 			if (const auto row = peerListFindRow(itemId)) {
90 				content()->changeCheckState(row, false, anim::type::normal);
91 				update();
92 			}
93 			_controller->itemDeselectedHook(peer);
94 		}
95 	});
96 	_select->resizeToWidth(_controller->contentWidth());
97 	_select->moveToLeft(0, 0);
98 }
99 
getTopScrollSkip() const100 int PeerListBox::getTopScrollSkip() const {
101 	auto result = 0;
102 	if (_select && !_select->isHidden()) {
103 		result += _select->height();
104 	}
105 	return result;
106 }
107 
updateScrollSkips()108 void PeerListBox::updateScrollSkips() {
109 	// If we show / hide the search field scroll top is fixed.
110 	// If we resize search field by bubbles scroll bottom is fixed.
111 	setInnerTopSkip(getTopScrollSkip(), _scrollBottomFixed);
112 	if (!_select->animating()) {
113 		_scrollBottomFixed = true;
114 	}
115 }
116 
prepare()117 void PeerListBox::prepare() {
118 	setContent(setInnerWidget(
119 		object_ptr<PeerListContent>(
120 			this,
121 			_controller.get()),
122 		st::boxScroll));
123 	content()->resizeToWidth(_controller->contentWidth());
124 
125 	_controller->setDelegate(this);
126 
127 	_controller->boxHeightValue(
128 	) | rpl::start_with_next([=](int height) {
129 		setDimensions(_controller->contentWidth(), height);
130 	}, lifetime());
131 
132 	if (_select) {
133 		_select->finishAnimating();
134 		Ui::SendPendingMoveResizeEvents(_select);
135 		_scrollBottomFixed = true;
136 		onScrollToY(0);
137 	}
138 
139 	content()->scrollToRequests(
140 	) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
141 		onScrollToY(request.ymin, request.ymax);
142 	}, lifetime());
143 
144 	if (_init) {
145 		_init(this);
146 	}
147 }
148 
keyPressEvent(QKeyEvent * e)149 void PeerListBox::keyPressEvent(QKeyEvent *e) {
150 	if (e->key() == Qt::Key_Down) {
151 		content()->selectSkip(1);
152 	} else if (e->key() == Qt::Key_Up) {
153 		content()->selectSkip(-1);
154 	} else if (e->key() == Qt::Key_PageDown) {
155 		content()->selectSkipPage(height(), 1);
156 	} else if (e->key() == Qt::Key_PageUp) {
157 		content()->selectSkipPage(height(), -1);
158 	} else if (e->key() == Qt::Key_Escape && _select && !_select->entity()->getQuery().isEmpty()) {
159 		_select->entity()->clearQuery();
160 	} else {
161 		BoxContent::keyPressEvent(e);
162 	}
163 }
164 
searchQueryChanged(const QString & query)165 void PeerListBox::searchQueryChanged(const QString &query) {
166 	onScrollToY(0);
167 	content()->searchQueryChanged(query);
168 }
169 
resizeEvent(QResizeEvent * e)170 void PeerListBox::resizeEvent(QResizeEvent *e) {
171 	BoxContent::resizeEvent(e);
172 
173 	if (_select) {
174 		_select->resizeToWidth(width());
175 		_select->moveToLeft(0, 0);
176 
177 		updateScrollSkips();
178 	}
179 
180 	content()->resizeToWidth(width());
181 }
182 
paintEvent(QPaintEvent * e)183 void PeerListBox::paintEvent(QPaintEvent *e) {
184 	Painter p(this);
185 
186 	const auto &bg = (_controller->listSt()
187 		? *_controller->listSt()
188 		: st::peerListBox).bg;
189 	for (const auto &rect : e->region()) {
190 		p.fillRect(rect, bg);
191 	}
192 }
193 
setInnerFocus()194 void PeerListBox::setInnerFocus() {
195 	if (!_select || !_select->toggled()) {
196 		content()->setFocus();
197 	} else {
198 		_select->entity()->setInnerFocus();
199 	}
200 }
201 
peerListSetRowChecked(not_null<PeerListRow * > row,bool checked)202 void PeerListBox::peerListSetRowChecked(
203 		not_null<PeerListRow*> row,
204 		bool checked) {
205 	if (checked) {
206 		addSelectItem(row, anim::type::normal);
207 		PeerListContentDelegate::peerListSetRowChecked(row, checked);
208 		peerListUpdateRow(row);
209 
210 		// This call deletes row from _searchRows.
211 		_select->entity()->clearQuery();
212 	} else {
213 		// The itemRemovedCallback will call changeCheckState() here.
214 		_select->entity()->removeItem(row->id());
215 		peerListUpdateRow(row);
216 	}
217 }
218 
peerListSetForeignRowChecked(not_null<PeerListRow * > row,bool checked,anim::type animated)219 void PeerListBox::peerListSetForeignRowChecked(
220 		not_null<PeerListRow*> row,
221 		bool checked,
222 		anim::type animated) {
223 	if (checked) {
224 		addSelectItem(row, animated);
225 
226 		// This call deletes row from _searchRows.
227 		_select->entity()->clearQuery();
228 	} else {
229 		// The itemRemovedCallback will call changeCheckState() here.
230 		_select->entity()->removeItem(row->id());
231 	}
232 }
233 
peerListScrollToTop()234 void PeerListBox::peerListScrollToTop() {
235 	onScrollToY(0);
236 }
237 
peerListSetSearchMode(PeerListSearchMode mode)238 void PeerListBox::peerListSetSearchMode(PeerListSearchMode mode) {
239 	PeerListContentDelegate::peerListSetSearchMode(mode);
240 
241 	auto selectVisible = (mode != PeerListSearchMode::Disabled);
242 	if (selectVisible && !_select) {
243 		createMultiSelect();
244 		_select->toggle(!selectVisible, anim::type::instant);
245 	}
246 	if (_select) {
247 		_select->toggle(selectVisible, anim::type::normal);
248 		_scrollBottomFixed = false;
249 		setInnerFocus();
250 	}
251 }
252 
PeerListController(std::unique_ptr<PeerListSearchController> searchController)253 PeerListController::PeerListController(std::unique_ptr<PeerListSearchController> searchController) : _searchController(std::move(searchController)) {
254 	if (_searchController) {
255 		_searchController->setDelegate(this);
256 	}
257 }
258 
computeListSt() const259 const style::PeerList &PeerListController::computeListSt() const {
260 	return _listSt ? *_listSt : st::peerListBox;
261 }
262 
computeSelectSt() const263 const style::MultiSelect &PeerListController::computeSelectSt() const {
264 	return _selectSt ? *_selectSt : st::defaultMultiSelect;
265 }
266 
hasComplexSearch() const267 bool PeerListController::hasComplexSearch() const {
268 	return (_searchController != nullptr);
269 }
270 
search(const QString & query)271 void PeerListController::search(const QString &query) {
272 	Expects(hasComplexSearch());
273 
274 	_searchController->searchQuery(query);
275 }
276 
peerListSearchAddRow(not_null<PeerData * > peer)277 void PeerListController::peerListSearchAddRow(not_null<PeerData*> peer) {
278 	if (auto row = delegate()->peerListFindRow(peer->id.value)) {
279 		Assert(row->id() == row->peer()->id.value);
280 		delegate()->peerListAppendFoundRow(row);
281 	} else if (auto row = createSearchRow(peer)) {
282 		Assert(row->id() == row->peer()->id.value);
283 		delegate()->peerListAppendSearchRow(std::move(row));
284 	}
285 }
286 
peerListSearchRefreshRows()287 void PeerListController::peerListSearchRefreshRows() {
288 	delegate()->peerListRefreshRows();
289 }
290 
onlineCountValue() const291 rpl::producer<int> PeerListController::onlineCountValue() const {
292 	return rpl::single(0);
293 }
294 
setDescriptionText(const QString & text)295 void PeerListController::setDescriptionText(const QString &text) {
296 	if (text.isEmpty()) {
297 		setDescription(nullptr);
298 	} else {
299 		setDescription(object_ptr<Ui::FlatLabel>(nullptr, text, computeListSt().about));
300 	}
301 }
302 
setSearchLoadingText(const QString & text)303 void PeerListController::setSearchLoadingText(const QString &text) {
304 	if (text.isEmpty()) {
305 		setSearchLoading(nullptr);
306 	} else {
307 		setSearchLoading(object_ptr<Ui::FlatLabel>(nullptr, text, st::membersAbout));
308 	}
309 }
310 
setSearchNoResultsText(const QString & text)311 void PeerListController::setSearchNoResultsText(const QString &text) {
312 	if (text.isEmpty()) {
313 		setSearchNoResults(nullptr);
314 	} else {
315 		setSearchNoResults(object_ptr<Ui::FlatLabel>(nullptr, text, st::membersAbout));
316 	}
317 }
318 
rowContextMenu(QWidget * parent,not_null<PeerListRow * > row)319 base::unique_qptr<Ui::PopupMenu> PeerListController::rowContextMenu(
320 		QWidget *parent,
321 		not_null<PeerListRow*> row) {
322 	return nullptr;
323 }
324 
saveState() const325 std::unique_ptr<PeerListState> PeerListController::saveState() const {
326 	return delegate()->peerListSaveState();
327 }
328 
restoreState(std::unique_ptr<PeerListState> state)329 void PeerListController::restoreState(
330 		std::unique_ptr<PeerListState> state) {
331 	delegate()->peerListRestoreState(std::move(state));
332 }
333 
contentWidth() const334 int PeerListController::contentWidth() const {
335 	return st::boxWideWidth;
336 }
337 
boxHeightValue() const338 rpl::producer<int> PeerListController::boxHeightValue() const {
339 	return rpl::single(st::boxMaxListHeight);
340 }
341 
descriptionTopSkipMin() const342 int PeerListController::descriptionTopSkipMin() const {
343 	return computeListSt().item.height;
344 }
345 
addSelectItem(not_null<PeerData * > peer,anim::type animated)346 void PeerListBox::addSelectItem(
347 		not_null<PeerData*> peer,
348 		anim::type animated) {
349 	const auto respect = _controller->respectSavedMessagesChat();
350 	const auto text = (respect && peer->isSelf())
351 		? tr::lng_saved_short(tr::now)
352 		: (respect && peer->isRepliesChat())
353 		? tr::lng_replies_messages(tr::now)
354 		: peer->shortName();
355 	addSelectItem(
356 		peer->id.value,
357 		text,
358 		PaintUserpicCallback(peer, respect),
359 		animated);
360 }
361 
addSelectItem(not_null<PeerListRow * > row,anim::type animated)362 void PeerListBox::addSelectItem(
363 		not_null<PeerListRow*> row,
364 		anim::type animated) {
365 	addSelectItem(
366 		row->id(),
367 		row->generateShortName(),
368 		row->generatePaintUserpicCallback(),
369 		animated);
370 }
371 
addSelectItem(uint64 itemId,const QString & text,Ui::MultiSelect::PaintRoundImage paintUserpic,anim::type animated)372 void PeerListBox::addSelectItem(
373 		uint64 itemId,
374 		const QString &text,
375 		Ui::MultiSelect::PaintRoundImage paintUserpic,
376 		anim::type animated) {
377 	if (!_select) {
378 		createMultiSelect();
379 		_select->hide(anim::type::instant);
380 	}
381 	const auto &activeBg = (_controller->selectSt()
382 		? *_controller->selectSt()
383 		: st::defaultMultiSelect).item.textActiveBg;
384 	if (animated == anim::type::instant) {
385 		_select->entity()->addItemInBunch(
386 			itemId,
387 			text,
388 			activeBg,
389 			std::move(paintUserpic));
390 	} else {
391 		_select->entity()->addItem(
392 			itemId,
393 			text,
394 			activeBg,
395 			std::move(paintUserpic));
396 	}
397 }
398 
peerListFinishSelectedRowsBunch()399 void PeerListBox::peerListFinishSelectedRowsBunch() {
400 	Expects(_select != nullptr);
401 
402 	_select->entity()->finishItemsBunch();
403 }
404 
peerListIsRowChecked(not_null<PeerListRow * > row)405 bool PeerListBox::peerListIsRowChecked(not_null<PeerListRow*> row) {
406 	return _select ? _select->entity()->hasItem(row->id()) : false;
407 }
408 
peerListSelectedRowsCount()409 int PeerListBox::peerListSelectedRowsCount() {
410 	return _select ? _select->entity()->getItemsCount() : 0;
411 }
412 
collectSelectedRows()413 auto PeerListBox::collectSelectedRows()
414 -> std::vector<not_null<PeerData*>> {
415 	auto result = std::vector<not_null<PeerData*>>();
416 	auto items = _select
417 		? _select->entity()->getItems()
418 		: QVector<uint64>();
419 	if (!items.empty()) {
420 		result.reserve(items.size());
421 		for (const auto itemId : items) {
422 			if (!_controller->isForeignRow(itemId)) {
423 				result.push_back(_controller->session().data().peer(PeerId(itemId)));
424 			}
425 		}
426 	}
427 	return result;
428 }
429 
PeerListRow(not_null<PeerData * > peer)430 PeerListRow::PeerListRow(not_null<PeerData*> peer)
431 : PeerListRow(peer, peer->id.value) {
432 }
433 
PeerListRow(not_null<PeerData * > peer,PeerListRowId id)434 PeerListRow::PeerListRow(not_null<PeerData*> peer, PeerListRowId id)
435 : _id(id)
436 , _peer(peer)
437 , _hidden(false)
438 , _initialized(false)
439 , _isSearchResult(false)
440 , _isSavedMessagesChat(false)
441 , _isRepliesMessagesChat(false) {
442 }
443 
PeerListRow(PeerListRowId id)444 PeerListRow::PeerListRow(PeerListRowId id)
445 : _id(id)
446 , _hidden(false)
447 , _initialized(false)
448 , _isSearchResult(false)
449 , _isSavedMessagesChat(false)
450 , _isRepliesMessagesChat(false) {
451 }
452 
453 PeerListRow::~PeerListRow() = default;
454 
checked() const455 bool PeerListRow::checked() const {
456 	return _checkbox && _checkbox->checked();
457 }
458 
setCustomStatus(const QString & status,bool active)459 void PeerListRow::setCustomStatus(const QString &status, bool active) {
460 	setStatusText(status);
461 	_statusType = active ? StatusType::CustomActive : StatusType::Custom;
462 	_statusValidTill = 0;
463 }
464 
clearCustomStatus()465 void PeerListRow::clearCustomStatus() {
466 	_statusType = StatusType::Online;
467 	refreshStatus();
468 }
469 
refreshStatus()470 void PeerListRow::refreshStatus() {
471 	if (!_initialized
472 		|| special()
473 		|| _statusType == StatusType::Custom
474 		|| _statusType == StatusType::CustomActive) {
475 		return;
476 	}
477 	_statusType = StatusType::LastSeen;
478 	_statusValidTill = 0;
479 	if (auto user = peer()->asUser()) {
480 		if (_isSavedMessagesChat) {
481 			setStatusText(tr::lng_saved_forward_here(tr::now));
482 		} else {
483 			auto time = base::unixtime::now();
484 			setStatusText(Data::OnlineText(user, time));
485 			if (Data::OnlineTextActive(user, time)) {
486 				_statusType = StatusType::Online;
487 			}
488 			_statusValidTill = crl::now()
489 				+ Data::OnlineChangeTimeout(user, time);
490 		}
491 	} else if (auto chat = peer()->asChat()) {
492 		if (!chat->amIn()) {
493 			setStatusText(tr::lng_chat_status_unaccessible(tr::now));
494 		} else if (chat->count > 0) {
495 			setStatusText(tr::lng_chat_status_members(tr::now, lt_count_decimal, chat->count));
496 		} else {
497 			setStatusText(tr::lng_group_status(tr::now));
498 		}
499 	} else if (peer()->isMegagroup()) {
500 		setStatusText(tr::lng_group_status(tr::now));
501 	} else if (peer()->isChannel()) {
502 		setStatusText(tr::lng_channel_status(tr::now));
503 	}
504 }
505 
refreshStatusTime() const506 crl::time PeerListRow::refreshStatusTime() const {
507 	return _statusValidTill;
508 }
509 
refreshName(const style::PeerListItem & st)510 void PeerListRow::refreshName(const style::PeerListItem &st) {
511 	if (!_initialized) {
512 		return;
513 	}
514 	const auto text = _isSavedMessagesChat
515 		? tr::lng_saved_messages(tr::now)
516 		: _isRepliesMessagesChat
517 		? tr::lng_replies_messages(tr::now)
518 		: generateName();
519 	_name.setText(st.nameStyle, text, Ui::NameTextOptions());
520 }
521 
elementsCount() const522 int PeerListRow::elementsCount() const {
523 	return 1;
524 }
525 
elementGeometry(int element,int outerWidth) const526 QRect PeerListRow::elementGeometry(int element, int outerWidth) const {
527 	if (element != 1) {
528 		return QRect();
529 	}
530 	const auto size = rightActionSize();
531 	if (size.isEmpty()) {
532 		return QRect();
533 	}
534 	const auto margins = rightActionMargins();
535 	const auto right = margins.right();
536 	const auto top = margins.top();
537 	const auto left = outerWidth - right - size.width();
538 	return QRect(QPoint(left, top), size);
539 }
540 
elementDisabled(int element) const541 bool PeerListRow::elementDisabled(int element) const {
542 	return (element == 1) && rightActionDisabled();
543 }
544 
elementOnlySelect(int element) const545 bool PeerListRow::elementOnlySelect(int element) const {
546 	return false;
547 }
548 
elementAddRipple(int element,QPoint point,Fn<void ()> updateCallback)549 void PeerListRow::elementAddRipple(
550 		int element,
551 		QPoint point,
552 		Fn<void()> updateCallback) {
553 	if (element == 1) {
554 		rightActionAddRipple(point, std::move(updateCallback));
555 	}
556 }
557 
elementsStopLastRipple()558 void PeerListRow::elementsStopLastRipple() {
559 	rightActionStopLastRipple();
560 }
561 
elementsPaint(Painter & p,int outerWidth,bool selected,int selectedElement)562 void PeerListRow::elementsPaint(
563 		Painter &p,
564 		int outerWidth,
565 		bool selected,
566 		int selectedElement) {
567 	const auto geometry = elementGeometry(1, outerWidth);
568 	if (!geometry.isEmpty()) {
569 		rightActionPaint(
570 			p,
571 			geometry.x(),
572 			geometry.y(),
573 			outerWidth,
574 			selected,
575 			(selectedElement == 1));
576 	}
577 }
578 
generateName()579 QString PeerListRow::generateName() {
580 	return peer()->name;
581 }
582 
generateShortName()583 QString PeerListRow::generateShortName() {
584 	return _isSavedMessagesChat
585 		? tr::lng_saved_short(tr::now)
586 		: _isRepliesMessagesChat
587 		? tr::lng_replies_messages(tr::now)
588 		: peer()->shortName();
589 }
590 
ensureUserpicView()591 std::shared_ptr<Data::CloudImageView> &PeerListRow::ensureUserpicView() {
592 	if (!_userpic) {
593 		_userpic = peer()->createUserpicView();
594 	}
595 	return _userpic;
596 }
597 
generatePaintUserpicCallback()598 PaintRoundImageCallback PeerListRow::generatePaintUserpicCallback() {
599 	const auto saved = _isSavedMessagesChat;
600 	const auto replies = _isRepliesMessagesChat;
601 	const auto peer = this->peer();
602 	auto userpic = saved ? nullptr : ensureUserpicView();
603 	return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
604 		if (saved) {
605 			Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
606 		} else if (replies) {
607 			Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
608 		} else {
609 			peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
610 		}
611 	};
612 }
613 
invalidatePixmapsCache()614 void PeerListRow::invalidatePixmapsCache() {
615 	if (_checkbox) {
616 		_checkbox->invalidateCache();
617 	}
618 }
619 
nameIconWidth() const620 int PeerListRow::nameIconWidth() const {
621 	return (special() || !_peer->isVerified())
622 		? 0
623 		: st::dialogsVerifiedIcon.width();
624 }
625 
paintNameIcon(Painter & p,int x,int y,int outerWidth,bool selected)626 void PeerListRow::paintNameIcon(
627 		Painter &p,
628 		int x,
629 		int y,
630 		int outerWidth,
631 		bool selected) {
632 	st::dialogsVerifiedIcon.paint(p, x, y, outerWidth);
633 }
634 
paintStatusText(Painter & p,const style::PeerListItem & st,int x,int y,int availableWidth,int outerWidth,bool selected)635 void PeerListRow::paintStatusText(
636 		Painter &p,
637 		const style::PeerListItem &st,
638 		int x,
639 		int y,
640 		int availableWidth,
641 		int outerWidth,
642 		bool selected) {
643 	auto statusHasOnlineColor = (_statusType == PeerListRow::StatusType::Online)
644 		|| (_statusType == PeerListRow::StatusType::CustomActive);
645 	p.setFont(st::contactsStatusFont);
646 	p.setPen(statusHasOnlineColor ? st.statusFgActive : (selected ? st.statusFgOver : st.statusFg));
647 	_status.drawLeftElided(p, x, y, availableWidth, outerWidth);
648 }
649 
650 template <typename MaskGenerator, typename UpdateCallback>
addRipple(const style::PeerListItem & st,MaskGenerator && maskGenerator,QPoint point,UpdateCallback && updateCallback)651 void PeerListRow::addRipple(const style::PeerListItem &st, MaskGenerator &&maskGenerator, QPoint point, UpdateCallback &&updateCallback) {
652 	if (!_ripple) {
653 		auto mask = maskGenerator();
654 		if (mask.isNull()) {
655 			return;
656 		}
657 		_ripple = std::make_unique<Ui::RippleAnimation>(st.button.ripple, std::move(mask), std::forward<UpdateCallback>(updateCallback));
658 	}
659 	_ripple->add(point);
660 }
661 
stopLastRipple()662 void PeerListRow::stopLastRipple() {
663 	if (_ripple) {
664 		_ripple->lastStop();
665 	}
666 }
667 
paintRipple(Painter & p,int x,int y,int outerWidth)668 void PeerListRow::paintRipple(Painter &p, int x, int y, int outerWidth) {
669 	if (_ripple) {
670 		_ripple->paint(p, x, y, outerWidth);
671 		if (_ripple->empty()) {
672 			_ripple.reset();
673 		}
674 	}
675 }
676 
paintUserpic(Painter & p,const style::PeerListItem & st,int x,int y,int outerWidth)677 void PeerListRow::paintUserpic(
678 		Painter &p,
679 		const style::PeerListItem &st,
680 		int x,
681 		int y,
682 		int outerWidth) {
683 	if (_disabledState == State::DisabledChecked) {
684 		paintDisabledCheckUserpic(p, st, x, y, outerWidth);
685 	} else if (_checkbox) {
686 		_checkbox->paint(p, x, y, outerWidth);
687 	} else if (const auto callback = generatePaintUserpicCallback()) {
688 		callback(p, x, y, outerWidth, st.photoSize);
689 	}
690 }
691 
692 // Emulates Ui::RoundImageCheckbox::paint() in a checked state.
paintDisabledCheckUserpic(Painter & p,const style::PeerListItem & st,int x,int y,int outerWidth) const693 void PeerListRow::paintDisabledCheckUserpic(
694 		Painter &p,
695 		const style::PeerListItem &st,
696 		int x,
697 		int y,
698 		int outerWidth) const {
699 	auto userpicRadius = st.checkbox.imageSmallRadius;
700 	auto userpicShift = st.checkbox.imageRadius - userpicRadius;
701 	auto userpicDiameter = st.checkbox.imageRadius * 2;
702 	auto userpicLeft = x + userpicShift;
703 	auto userpicTop = y + userpicShift;
704 	auto userpicEllipse = style::rtlrect(x, y, userpicDiameter, userpicDiameter, outerWidth);
705 	auto userpicBorderPen = st.disabledCheckFg->p;
706 	userpicBorderPen.setWidth(st.checkbox.selectWidth);
707 
708 	auto iconDiameter = st.checkbox.check.size;
709 	auto iconLeft = x + userpicDiameter + st.checkbox.selectWidth - iconDiameter;
710 	auto iconTop = y + userpicDiameter + st.checkbox.selectWidth - iconDiameter;
711 	auto iconEllipse = style::rtlrect(iconLeft, iconTop, iconDiameter, iconDiameter, outerWidth);
712 	auto iconBorderPen = st.checkbox.check.border->p;
713 	iconBorderPen.setWidth(st.checkbox.selectWidth);
714 
715 	if (_isSavedMessagesChat) {
716 		Ui::EmptyUserpic::PaintSavedMessages(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2);
717 	} else if (_isRepliesMessagesChat) {
718 		Ui::EmptyUserpic::PaintRepliesMessages(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2);
719 	} else {
720 		peer()->paintUserpicLeft(p, _userpic, userpicLeft, userpicTop, outerWidth, userpicRadius * 2);
721 	}
722 
723 	{
724 		PainterHighQualityEnabler hq(p);
725 
726 		p.setPen(userpicBorderPen);
727 		p.setBrush(Qt::NoBrush);
728 		p.drawEllipse(userpicEllipse);
729 
730 		p.setPen(iconBorderPen);
731 		p.setBrush(st.disabledCheckFg);
732 		p.drawEllipse(iconEllipse);
733 	}
734 
735 	st.checkbox.check.check.paint(p, iconEllipse.topLeft(), outerWidth);
736 }
737 
setStatusText(const QString & text)738 void PeerListRow::setStatusText(const QString &text) {
739 	_status.setText(st::defaultTextStyle, text, Ui::NameTextOptions());
740 }
741 
checkedRatio()742 float64 PeerListRow::checkedRatio() {
743 	return _checkbox ? _checkbox->checkedAnimationRatio() : 0.;
744 }
745 
lazyInitialize(const style::PeerListItem & st)746 void PeerListRow::lazyInitialize(const style::PeerListItem &st) {
747 	if (_initialized) {
748 		return;
749 	}
750 	_initialized = true;
751 	refreshName(st);
752 	refreshStatus();
753 }
754 
createCheckbox(const style::RoundImageCheckbox & st,Fn<void ()> updateCallback)755 void PeerListRow::createCheckbox(
756 		const style::RoundImageCheckbox &st,
757 		Fn<void()> updateCallback) {
758 	_checkbox = std::make_unique<Ui::RoundImageCheckbox>(
759 		st,
760 		std::move(updateCallback),
761 		generatePaintUserpicCallback());
762 }
763 
setCheckedInternal(bool checked,anim::type animated)764 void PeerListRow::setCheckedInternal(bool checked, anim::type animated) {
765 	Expects(_checkbox != nullptr);
766 
767 	_checkbox->setChecked(checked, animated);
768 }
769 
finishCheckedAnimation()770 void PeerListRow::finishCheckedAnimation() {
771 	_checkbox->setChecked(_checkbox->checked(), anim::type::instant);
772 }
773 
PeerListContent(QWidget * parent,not_null<PeerListController * > controller)774 PeerListContent::PeerListContent(
775 	QWidget *parent,
776 	not_null<PeerListController*> controller)
777 : RpWidget(parent)
778 , _st(controller->computeListSt())
779 , _controller(controller)
780 , _rowHeight(_st.item.height) {
781 	_controller->session().downloaderTaskFinished(
782 	) | rpl::start_with_next([=] {
783 		update();
784 	}, lifetime());
785 
786 	using UpdateFlag = Data::PeerUpdate::Flag;
787 	_controller->session().changes().peerUpdates(
788 		UpdateFlag::Name | UpdateFlag::Photo
789 	) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
790 		if (update.flags & UpdateFlag::Name) {
791 			handleNameChanged(update.peer);
792 		}
793 		if (update.flags & UpdateFlag::Photo) {
794 			this->update();
795 		}
796 	}, lifetime());
797 
798 	style::PaletteChanged(
799 	) | rpl::start_with_next([=] {
800 		invalidatePixmapsCache();
801 	}, lifetime());
802 
803 	_repaintByStatus.setCallback([this] { update(); });
804 }
805 
setMode(Mode mode)806 void PeerListContent::setMode(Mode mode) {
807 	if (mode == Mode::Default && _mode == Mode::Default) {
808 		return;
809 	}
810 	_mode = mode;
811 	switch (_mode) {
812 	case Mode::Default:
813 		_rowHeight = _st.item.height;
814 		break;
815 	case Mode::Custom:
816 		_rowHeight = _controller->customRowHeight();
817 		break;
818 	}
819 	const auto wasMouseSelection = _mouseSelection;
820 	const auto wasLastMousePosition = _lastMousePosition;
821 	_contextMenu = nullptr;
822 	if (wasMouseSelection) {
823 		setSelected(Selected());
824 	}
825 	setPressed(Selected());
826 	refreshRows();
827 	if (wasMouseSelection && wasLastMousePosition) {
828 		selectByMouse(*wasLastMousePosition);
829 	}
830 }
831 
appendRow(std::unique_ptr<PeerListRow> row)832 void PeerListContent::appendRow(std::unique_ptr<PeerListRow> row) {
833 	Expects(row != nullptr);
834 
835 	if (_rowsById.find(row->id()) == _rowsById.cend()) {
836 		row->setAbsoluteIndex(_rows.size());
837 		addRowEntry(row.get());
838 		if (!_hiddenRows.empty()) {
839 			Assert(!row->hidden());
840 			_filterResults.push_back(row.get());
841 		}
842 		_rows.push_back(std::move(row));
843 	}
844 }
845 
appendSearchRow(std::unique_ptr<PeerListRow> row)846 void PeerListContent::appendSearchRow(std::unique_ptr<PeerListRow> row) {
847 	Expects(row != nullptr);
848 	Expects(showingSearch());
849 
850 	if (_rowsById.find(row->id()) == _rowsById.cend()) {
851 		row->setAbsoluteIndex(_searchRows.size());
852 		row->setIsSearchResult(true);
853 		addRowEntry(row.get());
854 		_filterResults.push_back(row.get());
855 		_searchRows.push_back(std::move(row));
856 	}
857 }
858 
appendFoundRow(not_null<PeerListRow * > row)859 void PeerListContent::appendFoundRow(not_null<PeerListRow*> row) {
860 	Expects(showingSearch());
861 
862 	auto index = findRowIndex(row);
863 	if (index.value < 0) {
864 		_filterResults.push_back(row);
865 	}
866 }
867 
changeCheckState(not_null<PeerListRow * > row,bool checked,anim::type animated)868 void PeerListContent::changeCheckState(
869 		not_null<PeerListRow*> row,
870 		bool checked,
871 		anim::type animated) {
872 	row->setChecked(
873 		checked,
874 		_st.item.checkbox,
875 		animated,
876 		[=] { updateRow(row); });
877 }
878 
setRowHidden(not_null<PeerListRow * > row,bool hidden)879 void PeerListContent::setRowHidden(not_null<PeerListRow*> row, bool hidden) {
880 	Expects(!row->isSearchResult());
881 
882 	row->setHidden(hidden);
883 	if (hidden) {
884 		_hiddenRows.emplace(row);
885 	} else {
886 		_hiddenRows.remove(row);
887 	}
888 }
889 
addRowEntry(not_null<PeerListRow * > row)890 void PeerListContent::addRowEntry(not_null<PeerListRow*> row) {
891 	if (_controller->respectSavedMessagesChat() && !row->special()) {
892 		if (row->peer()->isSelf()) {
893 			row->setIsSavedMessagesChat(true);
894 		} else if (row->peer()->isRepliesChat()) {
895 			row->setIsRepliesMessagesChat(true);
896 		}
897 	}
898 	_rowsById.emplace(row->id(), row);
899 	if (!row->special()) {
900 		_rowsByPeer[row->peer()].push_back(row);
901 	}
902 	if (addingToSearchIndex()) {
903 		addToSearchIndex(row);
904 	}
905 	if (_controller->isRowSelected(row)) {
906 		Assert(row->special() || row->id() == row->peer()->id.value);
907 		changeCheckState(row, true, anim::type::instant);
908 	}
909 }
910 
invalidatePixmapsCache()911 void PeerListContent::invalidatePixmapsCache() {
912 	auto invalidate = [](auto &&row) { row->invalidatePixmapsCache(); };
913 	ranges::for_each(_rows, invalidate);
914 	ranges::for_each(_searchRows, invalidate);
915 }
916 
addingToSearchIndex() const917 bool PeerListContent::addingToSearchIndex() const {
918 	// If we started indexing already, we continue.
919 	return (_searchMode != PeerListSearchMode::Disabled) || !_searchIndex.empty();
920 }
921 
addToSearchIndex(not_null<PeerListRow * > row)922 void PeerListContent::addToSearchIndex(not_null<PeerListRow*> row) {
923 	if (row->isSearchResult() || row->special()) {
924 		return;
925 	}
926 
927 	removeFromSearchIndex(row);
928 	row->setNameFirstLetters(row->peer()->nameFirstLetters());
929 	for (auto ch : row->nameFirstLetters()) {
930 		_searchIndex[ch].push_back(row);
931 	}
932 }
933 
removeFromSearchIndex(not_null<PeerListRow * > row)934 void PeerListContent::removeFromSearchIndex(not_null<PeerListRow*> row) {
935 	const auto &nameFirstLetters = row->nameFirstLetters();
936 	if (!nameFirstLetters.empty()) {
937 		for (auto ch : row->nameFirstLetters()) {
938 			auto it = _searchIndex.find(ch);
939 			if (it != _searchIndex.cend()) {
940 				auto &entry = it->second;
941 				entry.erase(ranges::remove(entry, row), end(entry));
942 				if (entry.empty()) {
943 					_searchIndex.erase(it);
944 				}
945 			}
946 		}
947 		row->setNameFirstLetters({});
948 	}
949 }
950 
prependRow(std::unique_ptr<PeerListRow> row)951 void PeerListContent::prependRow(std::unique_ptr<PeerListRow> row) {
952 	Expects(row != nullptr);
953 
954 	if (_rowsById.find(row->id()) == _rowsById.cend()) {
955 		addRowEntry(row.get());
956 		if (!_hiddenRows.empty()) {
957 			Assert(!row->hidden());
958 			_filterResults.insert(_filterResults.begin(), row.get());
959 		}
960 		_rows.insert(_rows.begin(), std::move(row));
961 		refreshIndices();
962 	}
963 }
964 
prependRowFromSearchResult(not_null<PeerListRow * > row)965 void PeerListContent::prependRowFromSearchResult(not_null<PeerListRow*> row) {
966 	if (!row->isSearchResult()) {
967 		return;
968 	}
969 	Assert(_rowsById.find(row->id()) != _rowsById.cend());
970 	auto index = row->absoluteIndex();
971 	Assert(index >= 0 && index < _searchRows.size());
972 	Assert(_searchRows[index].get() == row);
973 
974 	row->setIsSearchResult(false);
975 	if (!_hiddenRows.empty()) {
976 		Assert(!row->hidden());
977 		_filterResults.insert(_filterResults.begin(), row);
978 	}
979 	_rows.insert(_rows.begin(), std::move(_searchRows[index]));
980 	refreshIndices();
981 	removeRowAtIndex(_searchRows, index);
982 
983 	if (addingToSearchIndex()) {
984 		addToSearchIndex(row);
985 	}
986 }
987 
refreshIndices()988 void PeerListContent::refreshIndices() {
989 	auto index = 0;
990 	for (auto &row : _rows) {
991 		row->setAbsoluteIndex(index++);
992 	}
993 }
994 
removeRowAtIndex(std::vector<std::unique_ptr<PeerListRow>> & from,int index)995 void PeerListContent::removeRowAtIndex(
996 		std::vector<std::unique_ptr<PeerListRow>> &from,
997 		int index) {
998 	from.erase(from.begin() + index);
999 	for (auto i = index, count = int(from.size()); i != count; ++i) {
1000 		from[i]->setAbsoluteIndex(i);
1001 	}
1002 }
1003 
findRow(PeerListRowId id)1004 PeerListRow *PeerListContent::findRow(PeerListRowId id) {
1005 	auto it = _rowsById.find(id);
1006 	return (it == _rowsById.cend()) ? nullptr : it->second.get();
1007 }
1008 
removeRow(not_null<PeerListRow * > row)1009 void PeerListContent::removeRow(not_null<PeerListRow*> row) {
1010 	auto index = row->absoluteIndex();
1011 	auto isSearchResult = row->isSearchResult();
1012 	auto &eraseFrom = isSearchResult ? _searchRows : _rows;
1013 
1014 	Assert(index >= 0 && index < eraseFrom.size());
1015 	Assert(eraseFrom[index].get() == row);
1016 
1017 	auto pressedData = saveSelectedData(_pressed);
1018 	auto contextedData = saveSelectedData(_contexted);
1019 	setSelected(Selected());
1020 	setPressed(Selected());
1021 	setContexted(Selected());
1022 
1023 	_rowsById.erase(row->id());
1024 	if (!row->special()) {
1025 		auto &byPeer = _rowsByPeer[row->peer()];
1026 		byPeer.erase(ranges::remove(byPeer, row), end(byPeer));
1027 	}
1028 	removeFromSearchIndex(row);
1029 	_filterResults.erase(
1030 		ranges::remove(_filterResults, row),
1031 		end(_filterResults));
1032 	_hiddenRows.remove(row);
1033 	removeRowAtIndex(eraseFrom, index);
1034 
1035 	restoreSelection();
1036 	setPressed(restoreSelectedData(pressedData));
1037 	setContexted(restoreSelectedData(contextedData));
1038 }
1039 
clearAllContent()1040 void PeerListContent::clearAllContent() {
1041 	setSelected(Selected());
1042 	setPressed(Selected());
1043 	setContexted(Selected());
1044 	_mouseSelection = false;
1045 	_lastMousePosition = std::nullopt;
1046 	_rowsById.clear();
1047 	_rowsByPeer.clear();
1048 	_filterResults.clear();
1049 	_searchIndex.clear();
1050 	_rows.clear();
1051 	_searchRows.clear();
1052 	_searchQuery
1053 		= _normalizedSearchQuery
1054 		= _mentionHighlight
1055 		= QString();
1056 }
1057 
convertRowToSearchResult(not_null<PeerListRow * > row)1058 void PeerListContent::convertRowToSearchResult(not_null<PeerListRow*> row) {
1059 	if (row->isSearchResult()) {
1060 		return;
1061 	} else if (!showingSearch() || !_controller->hasComplexSearch()) {
1062 		return removeRow(row);
1063 	}
1064 	auto index = row->absoluteIndex();
1065 	Assert(index >= 0 && index < _rows.size());
1066 	Assert(_rows[index].get() == row);
1067 
1068 	removeFromSearchIndex(row);
1069 	row->setIsSearchResult(true);
1070 	row->setHidden(false);
1071 	row->setAbsoluteIndex(_searchRows.size());
1072 	_hiddenRows.remove(row);
1073 	_searchRows.push_back(std::move(_rows[index]));
1074 	removeRowAtIndex(_rows, index);
1075 }
1076 
fullRowsCount() const1077 int PeerListContent::fullRowsCount() const {
1078 	return _rows.size();
1079 }
1080 
rowAt(int index) const1081 not_null<PeerListRow*> PeerListContent::rowAt(int index) const {
1082 	Expects(index >= 0 && index < _rows.size());
1083 
1084 	return _rows[index].get();
1085 }
1086 
setDescription(object_ptr<Ui::FlatLabel> description)1087 void PeerListContent::setDescription(object_ptr<Ui::FlatLabel> description) {
1088 	_description = std::move(description);
1089 	if (_description) {
1090 		_description->setParent(this);
1091 	}
1092 }
1093 
setSearchLoading(object_ptr<Ui::FlatLabel> loading)1094 void PeerListContent::setSearchLoading(object_ptr<Ui::FlatLabel> loading) {
1095 	_searchLoading = std::move(loading);
1096 	if (_searchLoading) {
1097 		_searchLoading->setParent(this);
1098 	}
1099 }
1100 
setSearchNoResults(object_ptr<Ui::FlatLabel> noResults)1101 void PeerListContent::setSearchNoResults(object_ptr<Ui::FlatLabel> noResults) {
1102 	_searchNoResults = std::move(noResults);
1103 	if (_searchNoResults) {
1104 		_searchNoResults->setParent(this);
1105 	}
1106 }
1107 
setAboveWidget(object_ptr<TWidget> widget)1108 void PeerListContent::setAboveWidget(object_ptr<TWidget> widget) {
1109 	_aboveWidget = std::move(widget);
1110 	if (_aboveWidget) {
1111 		_aboveWidget->setParent(this);
1112 	}
1113 }
1114 
setAboveSearchWidget(object_ptr<TWidget> widget)1115 void PeerListContent::setAboveSearchWidget(object_ptr<TWidget> widget) {
1116 	_aboveSearchWidget = std::move(widget);
1117 	if (_aboveSearchWidget) {
1118 		_aboveSearchWidget->setParent(this);
1119 	}
1120 }
1121 
setHideEmpty(bool hide)1122 void PeerListContent::setHideEmpty(bool hide) {
1123 	_hideEmpty = hide;
1124 	resizeToWidth(width());
1125 }
1126 
setBelowWidget(object_ptr<TWidget> widget)1127 void PeerListContent::setBelowWidget(object_ptr<TWidget> widget) {
1128 	_belowWidget = std::move(widget);
1129 	if (_belowWidget) {
1130 		_belowWidget->setParent(this);
1131 	}
1132 }
1133 
labelHeight() const1134 int PeerListContent::labelHeight() const {
1135 	if (_hideEmpty && !shownRowsCount()) {
1136 		return 0;
1137 	}
1138 	auto computeLabelHeight = [](auto &label) {
1139 		if (!label) {
1140 			return 0;
1141 		}
1142 		return st::membersAboutLimitPadding.top() + label->height() + st::membersAboutLimitPadding.bottom();
1143 	};
1144 	if (showingSearch()) {
1145 		if (!_filterResults.empty()) {
1146 			return 0;
1147 		}
1148 		if (_controller->isSearchLoading()) {
1149 			return computeLabelHeight(_searchLoading);
1150 		}
1151 		return computeLabelHeight(_searchNoResults);
1152 	}
1153 	return computeLabelHeight(_description);
1154 }
1155 
refreshRows()1156 void PeerListContent::refreshRows() {
1157 	if (!_hiddenRows.empty()) {
1158 		_filterResults.clear();
1159 		for (const auto &row : _rows) {
1160 			if (!row->hidden()) {
1161 				_filterResults.push_back(row.get());
1162 			}
1163 		}
1164 	}
1165 	resizeToWidth(width());
1166 	if (_visibleBottom > 0) {
1167 		checkScrollForPreload();
1168 	}
1169 	if (_mouseSelection) {
1170 		selectByMouse(QCursor::pos());
1171 	}
1172 	update();
1173 }
1174 
setSearchMode(PeerListSearchMode mode)1175 void PeerListContent::setSearchMode(PeerListSearchMode mode) {
1176 	if (_searchMode != mode) {
1177 		if (!addingToSearchIndex()) {
1178 			for (const auto &row : _rows) {
1179 				addToSearchIndex(row.get());
1180 			}
1181 		}
1182 		_searchMode = mode;
1183 		if (_controller->hasComplexSearch()) {
1184 			if (!_searchLoading) {
1185 				setSearchLoading(object_ptr<Ui::FlatLabel>(
1186 					this,
1187 					tr::lng_contacts_loading(tr::now),
1188 					st::membersAbout));
1189 			}
1190 		} else {
1191 			clearSearchRows();
1192 		}
1193 	}
1194 }
1195 
clearSearchRows()1196 void PeerListContent::clearSearchRows() {
1197 	while (!_searchRows.empty()) {
1198 		removeRow(_searchRows.back().get());
1199 	}
1200 }
1201 
paintEvent(QPaintEvent * e)1202 void PeerListContent::paintEvent(QPaintEvent *e) {
1203 	Painter p(this);
1204 
1205 	const auto clip = e->rect();
1206 	if (_mode != Mode::Custom) {
1207 		p.fillRect(clip, _st.item.button.textBg);
1208 	}
1209 
1210 	const auto repaintByStatusAfter = _repaintByStatus.remainingTime();
1211 	auto repaintAfterMin = repaintByStatusAfter;
1212 
1213 	const auto rowsTopCached = rowsTop();
1214 	const auto now = crl::now();
1215 	const auto yFrom = clip.y() - rowsTopCached;
1216 	const auto yTo = clip.y() + clip.height() - rowsTopCached;
1217 	p.translate(0, rowsTopCached);
1218 	const auto count = shownRowsCount();
1219 	if (count > 0) {
1220 		const auto from = floorclamp(yFrom, _rowHeight, 0, count);
1221 		const auto to = ceilclamp(yTo, _rowHeight, 0, count);
1222 		p.translate(0, from * _rowHeight);
1223 		for (auto index = from; index != to; ++index) {
1224 			const auto repaintAfter = paintRow(p, now, RowIndex(index));
1225 			if (repaintAfter > 0
1226 				&& (repaintAfterMin < 0
1227 					|| repaintAfterMin > repaintAfter)) {
1228 				repaintAfterMin = repaintAfter;
1229 			}
1230 			p.translate(0, _rowHeight);
1231 		}
1232 	}
1233 	if (repaintAfterMin != repaintByStatusAfter) {
1234 		Assert(repaintAfterMin >= 0);
1235 		_repaintByStatus.callOnce(repaintAfterMin);
1236 	}
1237 }
1238 
resizeGetHeight(int newWidth)1239 int PeerListContent::resizeGetHeight(int newWidth) {
1240 	const auto rowsCount = shownRowsCount();
1241 	const auto hideAll = !rowsCount && _hideEmpty;
1242 	_aboveHeight = 0;
1243 	if (_aboveWidget) {
1244 		_aboveWidget->resizeToWidth(newWidth);
1245 		_aboveWidget->moveToLeft(0, 0, newWidth);
1246 		if (hideAll || showingSearch()) {
1247 			_aboveWidget->hide();
1248 		} else {
1249 			_aboveWidget->show();
1250 			_aboveHeight = _aboveWidget->height();
1251 		}
1252 	}
1253 	if (_aboveSearchWidget) {
1254 		_aboveSearchWidget->resizeToWidth(newWidth);
1255 		_aboveSearchWidget->moveToLeft(0, 0, newWidth);
1256 		if (hideAll || !showingSearch()) {
1257 			_aboveSearchWidget->hide();
1258 		} else {
1259 			_aboveSearchWidget->show();
1260 			_aboveHeight = _aboveSearchWidget->height();
1261 		}
1262 	}
1263 	const auto labelTop = rowsTop()
1264 		+ std::max(
1265 			shownRowsCount() * _rowHeight,
1266 			_controller->descriptionTopSkipMin());
1267 	const auto labelWidth = newWidth - 2 * st::contactsPadding.left();
1268 	if (_description) {
1269 		_description->resizeToWidth(labelWidth);
1270 		_description->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);
1271 		_description->setVisible(!hideAll && !showingSearch());
1272 	}
1273 	if (_searchNoResults) {
1274 		_searchNoResults->resizeToWidth(labelWidth);
1275 		_searchNoResults->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);
1276 		_searchNoResults->setVisible(!hideAll && showingSearch() && _filterResults.empty() && !_controller->isSearchLoading());
1277 	}
1278 	if (_searchLoading) {
1279 		_searchLoading->resizeToWidth(labelWidth);
1280 		_searchLoading->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);
1281 		_searchLoading->setVisible(!hideAll && showingSearch() && _filterResults.empty() && _controller->isSearchLoading());
1282 	}
1283 	const auto label = labelHeight();
1284 	const auto belowTop = (label > 0 || rowsCount > 0)
1285 		? (labelTop + label + _st.padding.bottom())
1286 		: _aboveHeight;
1287 	_belowHeight = 0;
1288 	if (_belowWidget) {
1289 		_belowWidget->resizeToWidth(newWidth);
1290 		_belowWidget->moveToLeft(0, belowTop, newWidth);
1291 		if (hideAll || showingSearch()) {
1292 			_belowWidget->hide();
1293 		} else {
1294 			_belowWidget->show();
1295 			_belowHeight = _belowWidget->height();
1296 		}
1297 	}
1298 	return belowTop + _belowHeight;
1299 }
1300 
enterEventHook(QEnterEvent * e)1301 void PeerListContent::enterEventHook(QEnterEvent *e) {
1302 	setMouseTracking(true);
1303 }
1304 
leaveEventHook(QEvent * e)1305 void PeerListContent::leaveEventHook(QEvent *e) {
1306 	setMouseTracking(false);
1307 	mouseLeftGeometry();
1308 }
1309 
mouseMoveEvent(QMouseEvent * e)1310 void PeerListContent::mouseMoveEvent(QMouseEvent *e) {
1311 	handleMouseMove(e->globalPos());
1312 }
1313 
handleMouseMove(QPoint globalPosition)1314 void PeerListContent::handleMouseMove(QPoint globalPosition) {
1315 	if (!_lastMousePosition) {
1316 		_lastMousePosition = globalPosition;
1317 		return;
1318 	} else if (!_mouseSelection
1319 		&& *_lastMousePosition == globalPosition) {
1320 		return;
1321 	}
1322 	selectByMouse(globalPosition);
1323 }
1324 
mousePressEvent(QMouseEvent * e)1325 void PeerListContent::mousePressEvent(QMouseEvent *e) {
1326 	_pressButton = e->button();
1327 	selectByMouse(e->globalPos());
1328 	setPressed(_selected);
1329 	if (auto row = getRow(_selected.index)) {
1330 		auto updateCallback = [this, row, hint = _selected.index] {
1331 			updateRow(row, hint);
1332 		};
1333 		if (_selected.element) {
1334 			const auto elementRect = getElementRect(
1335 				row,
1336 				_selected.index,
1337 				_selected.element);
1338 			if (!elementRect.isEmpty()) {
1339 				row->elementAddRipple(
1340 					_selected.element,
1341 					mapFromGlobal(QCursor::pos()) - elementRect.topLeft(),
1342 					std::move(updateCallback));
1343 			}
1344 		} else {
1345 			auto point = mapFromGlobal(QCursor::pos()) - QPoint(0, getRowTop(_selected.index));
1346 			if (_mode == Mode::Custom) {
1347 				row->addRipple(_st.item, _controller->customRowRippleMaskGenerator(), point, std::move(updateCallback));
1348 			} else {
1349 				const auto maskGenerator = [&] {
1350 					return Ui::RippleAnimation::rectMask(
1351 						QSize(width(), _rowHeight));
1352 				};
1353 				row->addRipple(_st.item, maskGenerator, point, std::move(updateCallback));
1354 			}
1355 		}
1356 	}
1357 	if (anim::Disabled() && !_selected.element) {
1358 		mousePressReleased(e->button());
1359 	}
1360 }
1361 
mouseReleaseEvent(QMouseEvent * e)1362 void PeerListContent::mouseReleaseEvent(QMouseEvent *e) {
1363 	mousePressReleased(e->button());
1364 }
1365 
mousePressReleased(Qt::MouseButton button)1366 void PeerListContent::mousePressReleased(Qt::MouseButton button) {
1367 	updateRow(_pressed.index);
1368 	updateRow(_selected.index);
1369 
1370 	auto pressed = _pressed;
1371 	setPressed(Selected());
1372 	if (button == Qt::LeftButton && pressed == _selected) {
1373 		if (auto row = getRow(pressed.index)) {
1374 			if (pressed.element) {
1375 				_controller->rowElementClicked(row, pressed.element);
1376 			} else {
1377 				_controller->rowClicked(row);
1378 			}
1379 		}
1380 	}
1381 }
1382 
showRowMenu(not_null<PeerListRow * > row,bool highlightRow,Fn<void (not_null<Ui::PopupMenu * >)> destroyed)1383 void PeerListContent::showRowMenu(
1384 		not_null<PeerListRow*> row,
1385 		bool highlightRow,
1386 		Fn<void(not_null<Ui::PopupMenu*>)> destroyed) {
1387 	const auto index = findRowIndex(row);
1388 	showRowMenu(
1389 		index,
1390 		row,
1391 		QCursor::pos(),
1392 		highlightRow,
1393 		std::move(destroyed));
1394 }
1395 
showRowMenu(RowIndex index,PeerListRow * row,QPoint globalPos,bool highlightRow,Fn<void (not_null<Ui::PopupMenu * >)> destroyed)1396 bool PeerListContent::showRowMenu(
1397 		RowIndex index,
1398 		PeerListRow *row,
1399 		QPoint globalPos,
1400 		bool highlightRow,
1401 		Fn<void(not_null<Ui::PopupMenu*>)> destroyed) {
1402 	if (_contextMenu) {
1403 		_contextMenu->setDestroyedCallback(nullptr);
1404 		_contextMenu = nullptr;
1405 	}
1406 	setContexted(Selected());
1407 	if (_pressButton != Qt::LeftButton) {
1408 		mousePressReleased(_pressButton);
1409 	}
1410 
1411 	if (highlightRow) {
1412 		row = getRow(index);
1413 	}
1414 	if (!row) {
1415 		return false;
1416 	}
1417 
1418 	_contextMenu = _controller->rowContextMenu(this, row);
1419 	const auto raw = _contextMenu.get();
1420 	if (!raw) {
1421 		return false;
1422 	}
1423 
1424 	if (highlightRow) {
1425 		setContexted({ index, false });
1426 	}
1427 	raw->setDestroyedCallback(crl::guard(
1428 		this,
1429 		[=] {
1430 			if (highlightRow) {
1431 				setContexted(Selected());
1432 			}
1433 			handleMouseMove(QCursor::pos());
1434 			if (destroyed) {
1435 				destroyed(raw);
1436 			}
1437 		}));
1438 	raw->popup(globalPos);
1439 	return true;
1440 }
1441 
contextMenuEvent(QContextMenuEvent * e)1442 void PeerListContent::contextMenuEvent(QContextMenuEvent *e) {
1443 	if (e->reason() == QContextMenuEvent::Mouse) {
1444 		handleMouseMove(e->globalPos());
1445 	}
1446 	if (showRowMenu(_selected.index, nullptr, e->globalPos(), true)) {
1447 		e->accept();
1448 	}
1449 }
1450 
setPressed(Selected pressed)1451 void PeerListContent::setPressed(Selected pressed) {
1452 	if (_pressed == pressed) {
1453 		return;
1454 	} else if (const auto row = getRow(_pressed.index)) {
1455 		row->stopLastRipple();
1456 		row->elementsStopLastRipple();
1457 	}
1458 	_pressed = pressed;
1459 }
1460 
paintRow(Painter & p,crl::time now,RowIndex index)1461 crl::time PeerListContent::paintRow(
1462 		Painter &p,
1463 		crl::time now,
1464 		RowIndex index) {
1465 	const auto row = getRow(index);
1466 	Assert(row != nullptr);
1467 
1468 	row->lazyInitialize(_st.item);
1469 	const auto outerWidth = width();
1470 
1471 	auto refreshStatusAt = row->refreshStatusTime();
1472 	if (refreshStatusAt > 0 && now >= refreshStatusAt) {
1473 		row->refreshStatus();
1474 		refreshStatusAt = row->refreshStatusTime();
1475 	}
1476 	const auto refreshStatusIn = (refreshStatusAt > 0)
1477 		? std::max(refreshStatusAt - now, crl::time(1))
1478 		: 0;
1479 
1480 	const auto peer = row->special() ? nullptr : row->peer().get();
1481 	const auto active = (_contexted.index.value >= 0)
1482 		? _contexted
1483 		: (_pressed.index.value >= 0)
1484 		? _pressed
1485 		: _selected;
1486 	const auto selected = (active.index == index)
1487 		&& (!active.element || !row->elementOnlySelect(active.element));
1488 
1489 	if (_mode == Mode::Custom) {
1490 		_controller->customRowPaint(p, now, row, selected);
1491 		return refreshStatusIn;
1492 	}
1493 
1494 	const auto &bg = selected
1495 		? _st.item.button.textBgOver
1496 		: _st.item.button.textBg;
1497 	p.fillRect(0, 0, outerWidth, _rowHeight, bg);
1498 	row->paintRipple(p, 0, 0, outerWidth);
1499 	row->paintUserpic(
1500 		p,
1501 		_st.item,
1502 		_st.item.photoPosition.x(),
1503 		_st.item.photoPosition.y(),
1504 		outerWidth);
1505 
1506 	p.setPen(st::contactsNameFg);
1507 
1508 	auto skipRight = _st.item.photoPosition.x();
1509 	auto rightActionSize = row->rightActionSize();
1510 	auto rightActionMargins = rightActionSize.isEmpty()
1511 		? QMargins()
1512 		: row->rightActionMargins();
1513 	auto &name = row->name();
1514 	auto namex = _st.item.namePosition.x();
1515 	auto namew = outerWidth - namex - skipRight;
1516 	if (!rightActionSize.isEmpty()) {
1517 		namew -= rightActionMargins.left()
1518 			+ rightActionSize.width()
1519 			+ rightActionMargins.right()
1520 			- skipRight;
1521 	}
1522 	auto statusw = namew;
1523 	if (auto iconWidth = row->nameIconWidth()) {
1524 		namew -= iconWidth;
1525 		row->paintNameIcon(
1526 			p,
1527 			namex + qMin(name.maxWidth(), namew),
1528 			_st.item.namePosition.y(),
1529 			width(),
1530 			selected);
1531 	}
1532 	auto nameCheckedRatio = row->disabled() ? 0. : row->checkedRatio();
1533 	p.setPen(anim::pen(_st.item.nameFg, _st.item.nameFgChecked, nameCheckedRatio));
1534 	name.drawLeftElided(p, namex, _st.item.namePosition.y(), namew, width());
1535 
1536 	p.setFont(st::contactsStatusFont);
1537 	if (row->isSearchResult()
1538 		&& !_mentionHighlight.isEmpty()
1539 		&& peer
1540 		&& peer->userName().startsWith(
1541 			_mentionHighlight,
1542 			Qt::CaseInsensitive)) {
1543 		const auto username = peer->userName();
1544 		const auto availableWidth = statusw;
1545 		auto highlightedPart = '@' + username.mid(0, _mentionHighlight.size());
1546 		auto grayedPart = username.mid(_mentionHighlight.size());
1547 		const auto highlightedWidth = st::contactsStatusFont->width(highlightedPart);
1548 		if (highlightedWidth >= availableWidth || grayedPart.isEmpty()) {
1549 			if (highlightedWidth > availableWidth) {
1550 				highlightedPart = st::contactsStatusFont->elided(highlightedPart, availableWidth);
1551 			}
1552 			p.setPen(_st.item.statusFgActive);
1553 			p.drawTextLeft(_st.item.statusPosition.x(), _st.item.statusPosition.y(), width(), highlightedPart);
1554 		} else {
1555 			grayedPart = st::contactsStatusFont->elided(grayedPart, availableWidth - highlightedWidth);
1556 			p.setPen(_st.item.statusFgActive);
1557 			p.drawTextLeft(_st.item.statusPosition.x(), _st.item.statusPosition.y(), width(), highlightedPart);
1558 			p.setPen(selected ? _st.item.statusFgOver : _st.item.statusFg);
1559 			p.drawTextLeft(_st.item.statusPosition.x() + highlightedWidth, _st.item.statusPosition.y(), width(), grayedPart);
1560 		}
1561 	} else {
1562 		row->paintStatusText(p, _st.item, _st.item.statusPosition.x(), _st.item.statusPosition.y(), statusw, width(), selected);
1563 	}
1564 
1565 	row->elementsPaint(
1566 		p,
1567 		width(),
1568 		selected,
1569 		(active.index == index) ? active.element : 0);
1570 
1571 	return refreshStatusIn;
1572 }
1573 
selectSkip(int direction)1574 PeerListContent::SkipResult PeerListContent::selectSkip(int direction) {
1575 	if (hasPressed()) {
1576 		return { _selected.index.value, _selected.index.value };
1577 	}
1578 	_mouseSelection = false;
1579 	_lastMousePosition = std::nullopt;
1580 
1581 	auto newSelectedIndex = _selected.index.value + direction;
1582 
1583 	auto result = SkipResult();
1584 	result.shouldMoveTo = newSelectedIndex;
1585 
1586 	auto rowsCount = shownRowsCount();
1587 	auto index = 0;
1588 	auto firstEnabled = -1, lastEnabled = -1;
1589 	enumerateShownRows([&firstEnabled, &lastEnabled, &index](not_null<PeerListRow*> row) {
1590 		if (!row->disabled()) {
1591 			if (firstEnabled < 0) {
1592 				firstEnabled = index;
1593 			}
1594 			lastEnabled = index;
1595 		}
1596 		++index;
1597 		return true;
1598 	});
1599 	if (firstEnabled < 0) {
1600 		firstEnabled = rowsCount;
1601 		lastEnabled = firstEnabled - 1;
1602 	}
1603 
1604 	Assert(lastEnabled < rowsCount);
1605 	Assert(firstEnabled - 1 <= lastEnabled);
1606 
1607 	// Always pass through the first enabled item when changing from / to none selected.
1608 	if ((_selected.index.value > firstEnabled && newSelectedIndex < firstEnabled)
1609 		|| (_selected.index.value < firstEnabled && newSelectedIndex > firstEnabled)) {
1610 		newSelectedIndex = firstEnabled;
1611 	}
1612 
1613 	// Snap the index.
1614 	newSelectedIndex = std::clamp(
1615 		newSelectedIndex,
1616 		firstEnabled - 1,
1617 		lastEnabled);
1618 
1619 	// Skip the disabled rows.
1620 	if (newSelectedIndex < firstEnabled) {
1621 		newSelectedIndex = -1;
1622 	} else if (newSelectedIndex > lastEnabled) {
1623 		newSelectedIndex = lastEnabled;
1624 	} else if (getRow(RowIndex(newSelectedIndex))->disabled()) {
1625 		auto delta = (direction > 0) ? 1 : -1;
1626 		for (newSelectedIndex += delta; ; newSelectedIndex += delta) {
1627 			// We must find an enabled row, firstEnabled <= us <= lastEnabled.
1628 			Assert(newSelectedIndex >= 0 && newSelectedIndex < rowsCount);
1629 			if (!getRow(RowIndex(newSelectedIndex))->disabled()) {
1630 				break;
1631 			}
1632 		}
1633 	}
1634 
1635 	_selected.index.value = newSelectedIndex;
1636 	_selected.element = 0;
1637 	if (newSelectedIndex >= 0) {
1638 		auto top = (newSelectedIndex > 0) ? getRowTop(RowIndex(newSelectedIndex)) : 0;
1639 		auto bottom = (newSelectedIndex + 1 < rowsCount) ? getRowTop(RowIndex(newSelectedIndex + 1)) : height();
1640 		_scrollToRequests.fire({ top, bottom });
1641 	}
1642 
1643 	update();
1644 
1645 	_selectedIndex = _selected.index.value;
1646 	result.reallyMovedTo = _selected.index.value;
1647 	return result;
1648 }
1649 
selectSkipPage(int height,int direction)1650 void PeerListContent::selectSkipPage(int height, int direction) {
1651 	auto rowsToSkip = height / _rowHeight;
1652 	if (!rowsToSkip) {
1653 		return;
1654 	}
1655 	selectSkip(rowsToSkip * direction);
1656 }
1657 
selectedIndexValue() const1658 rpl::producer<int> PeerListContent::selectedIndexValue() const {
1659 	return _selectedIndex.value();
1660 }
1661 
hasSelection() const1662 bool PeerListContent::hasSelection() const {
1663 	return _selected.index.value >= 0;
1664 }
1665 
hasPressed() const1666 bool PeerListContent::hasPressed() const {
1667 	return _pressed.index.value >= 0;
1668 }
1669 
clearSelection()1670 void PeerListContent::clearSelection() {
1671 	setSelected(Selected());
1672 }
1673 
mouseLeftGeometry()1674 void PeerListContent::mouseLeftGeometry() {
1675 	if (_mouseSelection) {
1676 		setSelected(Selected());
1677 		_mouseSelection = false;
1678 		_lastMousePosition = std::nullopt;
1679 	}
1680 }
1681 
loadProfilePhotos()1682 void PeerListContent::loadProfilePhotos() {
1683 	if (_visibleTop >= _visibleBottom) return;
1684 
1685 	auto yFrom = _visibleTop;
1686 	auto yTo = _visibleBottom + (_visibleBottom - _visibleTop) * PreloadHeightsCount;
1687 
1688 	if (yTo < 0) return;
1689 	if (yFrom < 0) yFrom = 0;
1690 
1691 	auto rowsCount = shownRowsCount();
1692 	if (rowsCount > 0) {
1693 		auto from = yFrom / _rowHeight;
1694 		if (from < 0) from = 0;
1695 		if (from < rowsCount) {
1696 			auto to = (yTo / _rowHeight) + 1;
1697 			if (to > rowsCount) to = rowsCount;
1698 
1699 			for (auto index = from; index != to; ++index) {
1700 				const auto row = getRow(RowIndex(index));
1701 				if (!row->special()) {
1702 					row->peer()->loadUserpic();
1703 				}
1704 			}
1705 		}
1706 	}
1707 }
1708 
checkScrollForPreload()1709 void PeerListContent::checkScrollForPreload() {
1710 	if (_visibleBottom + PreloadHeightsCount * (_visibleBottom - _visibleTop) >= height()) {
1711 		_controller->loadMoreRows();
1712 	}
1713 }
1714 
searchQueryChanged(QString query)1715 void PeerListContent::searchQueryChanged(QString query) {
1716 	const auto searchWordsList = TextUtilities::PrepareSearchWords(query);
1717 	const auto normalizedQuery = searchWordsList.join(' ');
1718 	if (_normalizedSearchQuery != normalizedQuery) {
1719 		setSearchQuery(query, normalizedQuery);
1720 		if (_controller->searchInLocal() && !searchWordsList.isEmpty()) {
1721 			Assert(_hiddenRows.empty());
1722 
1723 			auto minimalList = (const std::vector<not_null<PeerListRow*>>*)nullptr;
1724 			for (const auto &searchWord : searchWordsList) {
1725 				auto searchWordStart = searchWord[0].toLower();
1726 				auto it = _searchIndex.find(searchWordStart);
1727 				if (it == _searchIndex.cend()) {
1728 					// Some word can't be found in any row.
1729 					minimalList = nullptr;
1730 					break;
1731 				} else if (!minimalList || minimalList->size() > it->second.size()) {
1732 					minimalList = &it->second;
1733 				}
1734 			}
1735 			if (minimalList) {
1736 				auto searchWordInNames = [](
1737 						not_null<PeerData*> peer,
1738 						const QString &searchWord) {
1739 					for (auto &nameWord : peer->nameWords()) {
1740 						if (nameWord.startsWith(searchWord)) {
1741 							return true;
1742 						}
1743 					}
1744 					return false;
1745 				};
1746 				auto allSearchWordsInNames = [&](
1747 						not_null<PeerData*> peer) {
1748 					for (const auto &searchWord : searchWordsList) {
1749 						if (!searchWordInNames(peer, searchWord)) {
1750 							return false;
1751 						}
1752 					}
1753 					return true;
1754 				};
1755 
1756 				_filterResults.reserve(minimalList->size());
1757 				for (const auto &row : *minimalList) {
1758 					if (!row->special() && allSearchWordsInNames(row->peer())) {
1759 						_filterResults.push_back(row);
1760 					}
1761 				}
1762 			}
1763 		}
1764 		if (_controller->hasComplexSearch()) {
1765 			_controller->search(_searchQuery);
1766 		}
1767 		refreshRows();
1768 	}
1769 }
1770 
saveState() const1771 std::unique_ptr<PeerListState> PeerListContent::saveState() const {
1772 	Expects(_hiddenRows.empty());
1773 
1774 	auto result = std::make_unique<PeerListState>();
1775 	result->controllerState
1776 		= std::make_unique<PeerListController::SavedStateBase>();
1777 	result->list.reserve(_rows.size());
1778 	for (const auto &row : _rows) {
1779 		result->list.push_back(row->peer());
1780 	}
1781 	result->filterResults.reserve(_filterResults.size());
1782 	for (const auto &row : _filterResults) {
1783 		result->filterResults.push_back(row->peer());
1784 	}
1785 	result->searchQuery = _searchQuery;
1786 	return result;
1787 }
1788 
restoreState(std::unique_ptr<PeerListState> state)1789 void PeerListContent::restoreState(
1790 		std::unique_ptr<PeerListState> state) {
1791 	if (!state || !state->controllerState) {
1792 		return;
1793 	}
1794 
1795 	clearAllContent();
1796 
1797 	for (auto peer : state->list) {
1798 		if (auto row = _controller->createRestoredRow(peer)) {
1799 			appendRow(std::move(row));
1800 		}
1801 	}
1802 	auto query = state->searchQuery;
1803 	auto searchWords = TextUtilities::PrepareSearchWords(query);
1804 	setSearchQuery(query, searchWords.join(' '));
1805 	for (auto peer : state->filterResults) {
1806 		if (auto existingRow = findRow(peer->id.value)) {
1807 			_filterResults.push_back(existingRow);
1808 		} else if (auto row = _controller->createSearchRow(peer)) {
1809 			appendSearchRow(std::move(row));
1810 		}
1811 	}
1812 	refreshRows();
1813 }
1814 
setSearchQuery(const QString & query,const QString & normalizedQuery)1815 void PeerListContent::setSearchQuery(
1816 		const QString &query,
1817 		const QString &normalizedQuery) {
1818 	setSelected(Selected());
1819 	setPressed(Selected());
1820 	setContexted(Selected());
1821 	_mouseSelection = false;
1822 	_lastMousePosition = std::nullopt;
1823 	_searchQuery = query;
1824 	_normalizedSearchQuery = normalizedQuery;
1825 	_mentionHighlight = _searchQuery.startsWith('@')
1826 		? _searchQuery.mid(1)
1827 		: _searchQuery;
1828 	_filterResults.clear();
1829 	clearSearchRows();
1830 }
1831 
submitted()1832 bool PeerListContent::submitted() {
1833 	if (const auto row = getRow(_selected.index)) {
1834 		_controller->rowClicked(row);
1835 		return true;
1836 	} else if (showingSearch()) {
1837 		if (const auto row = getRow(RowIndex(0))) {
1838 			_controller->rowClicked(row);
1839 			return true;
1840 		}
1841 	}
1842 	return false;
1843 }
1844 
visibleTopBottomUpdated(int visibleTop,int visibleBottom)1845 void PeerListContent::visibleTopBottomUpdated(
1846 		int visibleTop,
1847 		int visibleBottom) {
1848 	_visibleTop = visibleTop;
1849 	_visibleBottom = visibleBottom;
1850 	loadProfilePhotos();
1851 	checkScrollForPreload();
1852 }
1853 
setSelected(Selected selected)1854 void PeerListContent::setSelected(Selected selected) {
1855 	updateRow(_selected.index);
1856 	if (_selected == selected) {
1857 		return;
1858 	}
1859 	_selected = selected;
1860 	updateRow(_selected.index);
1861 	setCursor(_selected.element ? style::cur_pointer : style::cur_default);
1862 
1863 	_selectedIndex = _selected.index.value;
1864 }
1865 
setContexted(Selected contexted)1866 void PeerListContent::setContexted(Selected contexted) {
1867 	updateRow(_contexted.index);
1868 	if (_contexted != contexted) {
1869 		_contexted = contexted;
1870 		updateRow(_contexted.index);
1871 	}
1872 }
1873 
restoreSelection()1874 void PeerListContent::restoreSelection() {
1875 	if (_mouseSelection) {
1876 		selectByMouse(QCursor::pos());
1877 	}
1878 }
1879 
saveSelectedData(Selected from)1880 auto PeerListContent::saveSelectedData(Selected from)
1881 -> SelectedSaved {
1882 	if (auto row = getRow(from.index)) {
1883 		return { row->id(), from };
1884 	}
1885 	return { PeerListRowId(0), from };
1886 }
1887 
restoreSelectedData(SelectedSaved from)1888 auto PeerListContent::restoreSelectedData(SelectedSaved from)
1889 -> Selected {
1890 	auto result = from.old;
1891 	if (auto row = findRow(from.id)) {
1892 		result.index = findRowIndex(row, result.index);
1893 	} else {
1894 		result.index.value = -1;
1895 	}
1896 	return result;
1897 }
1898 
selectByMouse(QPoint globalPosition)1899 void PeerListContent::selectByMouse(QPoint globalPosition) {
1900 	_mouseSelection = true;
1901 	_lastMousePosition = globalPosition;
1902 	const auto point = mapFromGlobal(globalPosition);
1903 	const auto customMode = (_mode == Mode::Custom);
1904 	auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(globalPosition));
1905 	auto selected = Selected();
1906 	auto rowsPointY = point.y() - rowsTop();
1907 	selected.index.value = (in
1908 		&& rowsPointY >= 0
1909 		&& rowsPointY < shownRowsCount() * _rowHeight)
1910 		? (rowsPointY / _rowHeight)
1911 		: -1;
1912 	if (selected.index.value >= 0) {
1913 		const auto row = getRow(selected.index);
1914 		if (row->disabled()
1915 			|| (customMode
1916 				&& !_controller->customRowSelectionPoint(
1917 					row,
1918 					point.x(),
1919 					rowsPointY - (selected.index.value * _rowHeight)))) {
1920 			selected = Selected();
1921 		} else if (!customMode) {
1922 			for (auto i = 0, count = row->elementsCount(); i != count; ++i) {
1923 				const auto rect = getElementRect(row, selected.index, i + 1);
1924 				if (rect.contains(point)) {
1925 					selected.element = i + 1;
1926 					break;
1927 				}
1928 			}
1929 		}
1930 	}
1931 	setSelected(selected);
1932 }
1933 
getElementRect(not_null<PeerListRow * > row,RowIndex index,int element) const1934 QRect PeerListContent::getElementRect(
1935 		not_null<PeerListRow*> row,
1936 		RowIndex index,
1937 		int element) const {
1938 	if (row->elementDisabled(element)) {
1939 		return QRect();
1940 	}
1941 	const auto geometry = row->elementGeometry(element, width());
1942 	if (geometry.isEmpty()) {
1943 		return QRect();
1944 	}
1945 	return geometry.translated(0, getRowTop(index));
1946 }
1947 
rowsTop() const1948 int PeerListContent::rowsTop() const {
1949 	return _aboveHeight + _st.padding.top();
1950 }
1951 
getRowTop(RowIndex index) const1952 int PeerListContent::getRowTop(RowIndex index) const {
1953 	if (index.value >= 0) {
1954 		return rowsTop() + index.value * _rowHeight;
1955 	}
1956 	return -1;
1957 }
1958 
updateRow(not_null<PeerListRow * > row,RowIndex hint)1959 void PeerListContent::updateRow(not_null<PeerListRow*> row, RowIndex hint) {
1960 	updateRow(findRowIndex(row, hint));
1961 }
1962 
updateRow(RowIndex index)1963 void PeerListContent::updateRow(RowIndex index) {
1964 	if (index.value < 0) {
1965 		return;
1966 	}
1967 	if (const auto row = getRow(index); row && row->disabled()) {
1968 		if (index == _selected.index) {
1969 			setSelected(Selected());
1970 		}
1971 		if (index == _pressed.index) {
1972 			setPressed(Selected());
1973 		}
1974 		if (index == _contexted.index) {
1975 			setContexted(Selected());
1976 		}
1977 	}
1978 	update(0, getRowTop(index), width(), _rowHeight);
1979 }
1980 
1981 template <typename Callback>
enumerateShownRows(Callback callback)1982 bool PeerListContent::enumerateShownRows(Callback callback) {
1983 	return enumerateShownRows(0, shownRowsCount(), std::move(callback));
1984 }
1985 
1986 template <typename Callback>
enumerateShownRows(int from,int to,Callback callback)1987 bool PeerListContent::enumerateShownRows(int from, int to, Callback callback) {
1988 	Assert(0 <= from);
1989 	Assert(from <= to);
1990 	if (showingSearch()) {
1991 		Assert(to <= _filterResults.size());
1992 		for (auto i = from; i != to; ++i) {
1993 			if (!callback(_filterResults[i])) {
1994 				return false;
1995 			}
1996 		}
1997 	} else {
1998 		Assert(to <= _rows.size());
1999 		for (auto i = from; i != to; ++i) {
2000 			if (!callback(_rows[i].get())) {
2001 				return false;
2002 			}
2003 		}
2004 	}
2005 	return true;
2006 }
2007 
getRow(RowIndex index)2008 PeerListRow *PeerListContent::getRow(RowIndex index) {
2009 	if (index.value >= 0) {
2010 		if (showingSearch()) {
2011 			if (index.value < _filterResults.size()) {
2012 				return _filterResults[index.value];
2013 			}
2014 		} else if (index.value < _rows.size()) {
2015 			return _rows[index.value].get();
2016 		}
2017 	}
2018 	return nullptr;
2019 }
2020 
findRowIndex(not_null<PeerListRow * > row,RowIndex hint)2021 PeerListContent::RowIndex PeerListContent::findRowIndex(
2022 		not_null<PeerListRow*> row,
2023 		RowIndex hint) {
2024 	if (!showingSearch()) {
2025 		Assert(!row->isSearchResult());
2026 		return RowIndex(row->absoluteIndex());
2027 	}
2028 
2029 	auto result = hint;
2030 	if (getRow(result) == row) {
2031 		return result;
2032 	}
2033 
2034 	auto count = shownRowsCount();
2035 	for (result.value = 0; result.value != count; ++result.value) {
2036 		if (getRow(result) == row) {
2037 			return result;
2038 		}
2039 	}
2040 	result.value = -1;
2041 	return result;
2042 }
2043 
handleNameChanged(not_null<PeerData * > peer)2044 void PeerListContent::handleNameChanged(not_null<PeerData*> peer) {
2045 	auto byPeer = _rowsByPeer.find(peer);
2046 	if (byPeer != _rowsByPeer.cend()) {
2047 		for (auto row : byPeer->second) {
2048 			if (addingToSearchIndex()) {
2049 				addToSearchIndex(row);
2050 			}
2051 			row->refreshName(_st.item);
2052 			updateRow(row);
2053 		}
2054 	}
2055 }
2056 
~PeerListContent()2057 PeerListContent::~PeerListContent() {
2058 	if (_contextMenu) {
2059 		_contextMenu->setDestroyedCallback(nullptr);
2060 	}
2061 }
2062 
peerListShowRowMenu(not_null<PeerListRow * > row,bool highlightRow,Fn<void (not_null<Ui::PopupMenu * >)> destroyed)2063 void PeerListContentDelegate::peerListShowRowMenu(
2064 		not_null<PeerListRow*> row,
2065 		bool highlightRow,
2066 		Fn<void(not_null<Ui::PopupMenu *>)> destroyed) {
2067 	_content->showRowMenu(row, highlightRow, std::move(destroyed));
2068 }
2069