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 "info/media/info_media_list_widget.h"
9 
10 #include "info/info_controller.h"
11 #include "overview/overview_layout.h"
12 #include "layout/layout_mosaic.h"
13 #include "layout/layout_selection.h"
14 #include "data/data_media_types.h"
15 #include "data/data_photo.h"
16 #include "data/data_document.h"
17 #include "data/data_session.h"
18 #include "data/data_file_click_handler.h"
19 #include "data/data_file_origin.h"
20 #include "history/history_item.h"
21 #include "history/history.h"
22 #include "history/view/history_view_cursor_state.h"
23 #include "history/view/history_view_service_message.h"
24 #include "window/window_session_controller.h"
25 #include "window/window_peer_menu.h"
26 #include "ui/widgets/popup_menu.h"
27 #include "ui/controls/delete_message_context_action.h"
28 #include "ui/chat/chat_style.h"
29 #include "ui/cached_round_corners.h"
30 #include "ui/ui_utility.h"
31 #include "ui/inactive_press.h"
32 #include "lang/lang_keys.h"
33 #include "main/main_session.h"
34 #include "mainwidget.h"
35 #include "mainwindow.h"
36 #include "styles/style_overview.h"
37 #include "styles/style_info.h"
38 #include "base/platform/base_platform_info.h"
39 #include "base/weak_ptr.h"
40 #include "media/player/media_player_instance.h"
41 #include "boxes/delete_messages_box.h"
42 #include "boxes/peer_list_controllers.h"
43 #include "core/file_utilities.h"
44 #include "facades.h"
45 
46 #include <QtWidgets/QApplication>
47 #include <QtGui/QClipboard>
48 
49 namespace Info {
50 namespace Media {
51 namespace {
52 
53 constexpr auto kFloatingHeaderAlpha = 0.9;
54 constexpr auto kPreloadedScreensCount = 4;
55 constexpr auto kPreloadIfLessThanScreens = 2;
56 constexpr auto kPreloadedScreensCountFull
57 	= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
58 constexpr auto kMediaCountForSearch = 10;
59 
GetUniversalId(FullMsgId itemId)60 UniversalMsgId GetUniversalId(FullMsgId itemId) {
61 	return (itemId.channel != 0)
62 		? UniversalMsgId(itemId.msg)
63 		: UniversalMsgId(itemId.msg - ServerMaxMsgId);
64 }
65 
GetUniversalId(not_null<const HistoryItem * > item)66 UniversalMsgId GetUniversalId(not_null<const HistoryItem*> item) {
67 	return GetUniversalId(item->fullId());
68 }
69 
GetUniversalId(not_null<const BaseLayout * > layout)70 UniversalMsgId GetUniversalId(not_null<const BaseLayout*> layout) {
71 	return GetUniversalId(layout->getItem()->fullId());
72 }
73 
HasFloatingHeader(Type type)74 bool HasFloatingHeader(Type type) {
75 	switch (type) {
76 	case Type::Photo:
77 	case Type::GIF:
78 	case Type::Video:
79 	case Type::RoundFile:
80 	case Type::RoundVoiceFile:
81 	case Type::MusicFile:
82 		return false;
83 	case Type::File:
84 	case Type::Link:
85 		return true;
86 	}
87 	Unexpected("Type in HasFloatingHeader()");
88 }
89 
90 } // namespace
91 
92 struct ListWidget::Context {
93 	Overview::Layout::PaintContext layoutContext;
94 	not_null<SelectedMap*> selected;
95 	not_null<SelectedMap*> dragSelected;
96 	DragSelectAction dragSelectAction;
97 };
98 
99 struct ListWidget::DateBadge {
100 	DateBadge(Type type, Fn<void()> checkCallback, Fn<void()> hideCallback);
101 
102 	SingleQueuedInvokation check;
103 	base::Timer hideTimer;
104 	Ui::Animations::Simple opacity;
105 	Ui::CornersPixmaps corners;
106 	bool goodType = false;
107 	bool shown = false;
108 	QString text;
109 	int textWidth = 0;
110 	QRect rect;
111 };
112 
113 class ListWidget::Section {
114 public:
Section(Type type)115 	Section(Type type)
116 	: _type(type)
117 	, _hasFloatingHeader(HasFloatingHeader(type))
118 	, _mosaic(st::emojiPanWidth - st::inlineResultsLeft) {
119 	}
120 
121 	bool addItem(not_null<BaseLayout*> item);
122 	void finishSection();
123 
empty() const124 	bool empty() const {
125 		return _items.empty();
126 	}
127 
minId() const128 	UniversalMsgId minId() const {
129 		Expects(!empty());
130 
131 		return _items.back().first;
132 	}
maxId() const133 	UniversalMsgId maxId() const {
134 		Expects(!empty());
135 
136 		return _items.front().first;
137 	}
138 
setTop(int top)139 	void setTop(int top) {
140 		_top = top;
141 	}
top() const142 	int top() const {
143 		return _top;
144 	}
145 	void resizeToWidth(int newWidth);
height() const146 	int height() const {
147 		return _height;
148 	}
149 
bottom() const150 	int bottom() const {
151 		return top() + height();
152 	}
153 
154 	bool removeItem(UniversalMsgId universalId);
155 	FoundItem findItemNearId(UniversalMsgId universalId) const;
156 	FoundItem findItemDetails(not_null<BaseLayout*> item) const;
157 	FoundItem findItemByPoint(QPoint point) const;
158 
159 	void paint(
160 		Painter &p,
161 		const Context &context,
162 		QRect clip,
163 		int outerWidth) const;
164 
165 	void paintFloatingHeader(Painter &p, int visibleTop, int outerWidth);
166 
167 	static int MinItemHeight(Type type, int width);
168 
169 private:
170 	using Items = base::flat_map<
171 		UniversalMsgId,
172 		not_null<BaseLayout*>,
173 		std::greater<>>;
174 	int headerHeight() const;
175 	void appendItem(not_null<BaseLayout*> item);
176 	void setHeader(not_null<BaseLayout*> item);
177 	bool belongsHere(not_null<BaseLayout*> item) const;
178 	Items::iterator findItemAfterTop(int top);
179 	Items::const_iterator findItemAfterTop(int top) const;
180 	Items::const_iterator findItemAfterBottom(
181 		Items::const_iterator from,
182 		int bottom) const;
183 	QRect findItemRect(not_null<const BaseLayout*> item) const;
184 	FoundItem completeResult(
185 		not_null<BaseLayout*> item,
186 		bool exact) const;
187 	TextSelection itemSelection(
188 		not_null<const BaseLayout*> item,
189 		const Context &context) const;
190 
191 	int recountHeight();
192 	void refreshHeight();
193 
194 	Type _type = Type::Photo;
195 	bool _hasFloatingHeader = false;
196 	Ui::Text::String _header;
197 	Items _items;
198 	int _itemsLeft = 0;
199 	int _itemsTop = 0;
200 	int _itemWidth = 0;
201 	int _itemsInRow = 1;
202 	mutable int _rowsCount = 0;
203 	int _top = 0;
204 	int _height = 0;
205 
206 	Mosaic::Layout::MosaicLayout<BaseLayout> _mosaic;
207 
208 };
209 
IsAfter(const MouseState & a,const MouseState & b)210 bool ListWidget::IsAfter(
211 		const MouseState &a,
212 		const MouseState &b) {
213 	if (a.itemId != b.itemId) {
214 		return (a.itemId < b.itemId);
215 	}
216 	auto xAfter = a.cursor.x() - b.cursor.x();
217 	auto yAfter = a.cursor.y() - b.cursor.y();
218 	return (xAfter + yAfter >= 0);
219 }
220 
SkipSelectFromItem(const MouseState & state)221 bool ListWidget::SkipSelectFromItem(const MouseState &state) {
222 	if (state.cursor.y() >= state.size.height()
223 		|| state.cursor.x() >= state.size.width()) {
224 		return true;
225 	}
226 	return false;
227 }
228 
SkipSelectTillItem(const MouseState & state)229 bool ListWidget::SkipSelectTillItem(const MouseState &state) {
230 	if (state.cursor.x() < 0 || state.cursor.y() < 0) {
231 		return true;
232 	}
233 	return false;
234 }
235 
CachedItem(std::unique_ptr<BaseLayout> item)236 ListWidget::CachedItem::CachedItem(std::unique_ptr<BaseLayout> item)
237 : item(std::move(item)) {
238 }
239 
240 ListWidget::CachedItem::CachedItem(CachedItem &&other) = default;
241 
242 ListWidget::CachedItem &ListWidget::CachedItem::operator=(
243 	CachedItem && other) = default;
244 
245 ListWidget::CachedItem::~CachedItem() = default;
246 
DateBadge(Type type,Fn<void ()> checkCallback,Fn<void ()> hideCallback)247 ListWidget::DateBadge::DateBadge(
248 	Type type,
249 	Fn<void()> checkCallback,
250 	Fn<void()> hideCallback)
251 : check(std::move(checkCallback))
252 , hideTimer(std::move(hideCallback))
253 , goodType(type == Type::Photo
254 	|| type == Type::Video
255 	|| type == Type::GIF) {
256 }
257 
addItem(not_null<BaseLayout * > item)258 bool ListWidget::Section::addItem(not_null<BaseLayout*> item) {
259 	if (_items.empty() || belongsHere(item)) {
260 		if (_items.empty()) setHeader(item);
261 		appendItem(item);
262 		return true;
263 	}
264 	return false;
265 }
266 
finishSection()267 void ListWidget::Section::finishSection() {
268 	if (_type == Type::GIF) {
269 		_mosaic.setOffset(st::infoMediaSkip, headerHeight());
270 		_mosaic.setRightSkip(st::infoMediaSkip);
271 		const auto items = ranges::views::values(_items) | ranges::to_vector;
272 		_mosaic.addItems(items);
273 	}
274 }
275 
setHeader(not_null<BaseLayout * > item)276 void ListWidget::Section::setHeader(not_null<BaseLayout*> item) {
277 	auto text = [&] {
278 		auto date = item->dateTime().date();
279 		switch (_type) {
280 		case Type::Photo:
281 		case Type::GIF:
282 		case Type::Video:
283 		case Type::RoundFile:
284 		case Type::RoundVoiceFile:
285 		case Type::File:
286 			return langMonthFull(date);
287 
288 		case Type::Link:
289 			return langDayOfMonthFull(date);
290 
291 		case Type::MusicFile:
292 			return QString();
293 		}
294 		Unexpected("Type in ListWidget::Section::setHeader()");
295 	}();
296 	_header.setText(st::infoMediaHeaderStyle, text);
297 }
298 
belongsHere(not_null<BaseLayout * > item) const299 bool ListWidget::Section::belongsHere(
300 		not_null<BaseLayout*> item) const {
301 	Expects(!_items.empty());
302 
303 	auto date = item->dateTime().date();
304 	auto myDate = _items.back().second->dateTime().date();
305 
306 	switch (_type) {
307 	case Type::Photo:
308 	case Type::GIF:
309 	case Type::Video:
310 	case Type::RoundFile:
311 	case Type::RoundVoiceFile:
312 	case Type::File:
313 		return date.year() == myDate.year()
314 			&& date.month() == myDate.month();
315 
316 	case Type::Link:
317 		return date.year() == myDate.year()
318 			&& date.month() == myDate.month()
319 			&& date.day() == myDate.day();
320 
321 	case Type::MusicFile:
322 		return true;
323 	}
324 	Unexpected("Type in ListWidget::Section::belongsHere()");
325 }
326 
appendItem(not_null<BaseLayout * > item)327 void ListWidget::Section::appendItem(not_null<BaseLayout*> item) {
328 	_items.emplace(GetUniversalId(item), item);
329 }
330 
removeItem(UniversalMsgId universalId)331 bool ListWidget::Section::removeItem(UniversalMsgId universalId) {
332 	if (auto it = _items.find(universalId); it != _items.end()) {
333 		it = _items.erase(it);
334 		refreshHeight();
335 		return true;
336 	}
337 	return false;
338 }
339 
findItemRect(not_null<const BaseLayout * > item) const340 QRect ListWidget::Section::findItemRect(
341 		not_null<const BaseLayout*> item) const {
342 	auto position = item->position();
343 	if (!_mosaic.empty()) {
344 		return _mosaic.findRect(position);
345 	}
346 	auto top = position / _itemsInRow;
347 	auto indexInRow = position % _itemsInRow;
348 	auto left = _itemsLeft
349 		+ indexInRow * (_itemWidth + st::infoMediaSkip);
350 	return QRect(left, top, _itemWidth, item->height());
351 }
352 
completeResult(not_null<BaseLayout * > item,bool exact) const353 auto ListWidget::Section::completeResult(
354 		not_null<BaseLayout*> item,
355 		bool exact) const -> FoundItem {
356 	return { item, findItemRect(item), exact };
357 }
358 
findItemByPoint(QPoint point) const359 auto ListWidget::Section::findItemByPoint(
360 		QPoint point) const -> FoundItem {
361 	Expects(!_items.empty());
362 	if (!_mosaic.empty()) {
363 		const auto found = _mosaic.findByPoint(point);
364 		Assert(found.index != -1);
365 		const auto item = _mosaic.itemAt(found.index);
366 		const auto rect = findItemRect(item);
367 		return { item, rect, found.exact };
368 	}
369 	auto itemIt = findItemAfterTop(point.y());
370 	if (itemIt == _items.end()) {
371 		--itemIt;
372 	}
373 	auto item = itemIt->second;
374 	auto rect = findItemRect(item);
375 	if (point.y() >= rect.top()) {
376 		auto shift = floorclamp(
377 			point.x(),
378 			(_itemWidth + st::infoMediaSkip),
379 			0,
380 			_itemsInRow);
381 		while (shift-- && itemIt != _items.end()) {
382 			++itemIt;
383 		}
384 		if (itemIt == _items.end()) {
385 			--itemIt;
386 		}
387 		item = itemIt->second;
388 		rect = findItemRect(item);
389 	}
390 	return { item, rect, rect.contains(point) };
391 }
392 
findItemNearId(UniversalMsgId universalId) const393 auto ListWidget::Section::findItemNearId(UniversalMsgId universalId) const
394 -> FoundItem {
395 	Expects(!_items.empty());
396 
397 	auto itemIt = ranges::lower_bound(
398 		_items,
399 		universalId,
400 		std::greater<>(),
401 		[](const auto &item) -> UniversalMsgId { return item.first; });
402 	if (itemIt == _items.end()) {
403 		--itemIt;
404 	}
405 	auto item = itemIt->second;
406 	auto exact = (GetUniversalId(item) == universalId);
407 	return { item, findItemRect(item), exact };
408 }
409 
findItemDetails(not_null<BaseLayout * > item) const410 auto ListWidget::Section::findItemDetails(not_null<BaseLayout*> item) const
411 -> FoundItem {
412 	return { item, findItemRect(item), true };
413 }
414 
findItemAfterTop(int top)415 auto ListWidget::Section::findItemAfterTop(
416 		int top) -> Items::iterator {
417 	Expects(_mosaic.empty());
418 	return ranges::lower_bound(
419 		_items,
420 		top,
421 		std::less_equal<>(),
422 		[this](const auto &item) {
423 			auto itemTop = item.second->position() / _itemsInRow;
424 			return itemTop + item.second->height();
425 		});
426 }
427 
findItemAfterTop(int top) const428 auto ListWidget::Section::findItemAfterTop(
429 		int top) const -> Items::const_iterator {
430 	Expects(_mosaic.empty());
431 	return ranges::lower_bound(
432 		_items,
433 		top,
434 		std::less_equal<>(),
435 		[this](const auto &item) {
436 			auto itemTop = item.second->position() / _itemsInRow;
437 			return itemTop + item.second->height();
438 		});
439 }
440 
findItemAfterBottom(Items::const_iterator from,int bottom) const441 auto ListWidget::Section::findItemAfterBottom(
442 		Items::const_iterator from,
443 		int bottom) const -> Items::const_iterator {
444 	Expects(_mosaic.empty());
445 	return ranges::lower_bound(
446 		from,
447 		_items.end(),
448 		bottom,
449 		std::less<>(),
450 		[this](const auto &item) {
451 			auto itemTop = item.second->position() / _itemsInRow;
452 			return itemTop;
453 		});
454 }
455 
paint(Painter & p,const Context & context,QRect clip,int outerWidth) const456 void ListWidget::Section::paint(
457 		Painter &p,
458 		const Context &context,
459 		QRect clip,
460 		int outerWidth) const {
461 	auto header = headerHeight();
462 	if (QRect(0, 0, outerWidth, header).intersects(clip)) {
463 		p.setPen(st::infoMediaHeaderFg);
464 		_header.drawLeftElided(
465 			p,
466 			st::infoMediaHeaderPosition.x(),
467 			st::infoMediaHeaderPosition.y(),
468 			outerWidth - 2 * st::infoMediaHeaderPosition.x(),
469 			outerWidth);
470 	}
471 	auto localContext = context.layoutContext;
472 	localContext.isAfterDate = (header > 0);
473 
474 	if (!_mosaic.empty()) {
475 		auto paintItem = [&](not_null<BaseLayout*> item, QPoint point) {
476 			p.translate(point.x(), point.y());
477 			item->paint(
478 				p,
479 				clip.translated(-point),
480 				itemSelection(item, context),
481 				&localContext);
482 			p.translate(-point.x(), -point.y());
483 		};
484 		_mosaic.paint(std::move(paintItem), clip);
485 		return;
486 	}
487 
488 	auto fromIt = findItemAfterTop(clip.y());
489 	auto tillIt = findItemAfterBottom(
490 		fromIt,
491 		clip.y() + clip.height());
492 	for (auto it = fromIt; it != tillIt; ++it) {
493 		auto item = it->second;
494 		auto rect = findItemRect(item);
495 		localContext.isAfterDate = (header > 0)
496 			&& (rect.y() <= header + _itemsTop);
497 		if (rect.intersects(clip)) {
498 			p.translate(rect.topLeft());
499 			item->paint(
500 				p,
501 				clip.translated(-rect.topLeft()),
502 				itemSelection(item, context),
503 				&localContext);
504 			p.translate(-rect.topLeft());
505 		}
506 	}
507 }
508 
paintFloatingHeader(Painter & p,int visibleTop,int outerWidth)509 void ListWidget::Section::paintFloatingHeader(
510 		Painter &p,
511 		int visibleTop,
512 		int outerWidth) {
513 	if (!_hasFloatingHeader) {
514 		return;
515 	}
516 	const auto headerTop = st::infoMediaHeaderPosition.y() / 2;
517 	if (visibleTop <= (_top + headerTop)) {
518 		return;
519 	}
520 	const auto header = headerHeight();
521 	const auto headerLeft = st::infoMediaHeaderPosition.x();
522 	const auto floatingTop = std::min(
523 		visibleTop,
524 		bottom() - header + headerTop);
525 	p.save();
526 	p.resetTransform();
527 	p.setOpacity(kFloatingHeaderAlpha);
528 	p.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg);
529 	p.setOpacity(1.0);
530 	p.setPen(st::infoMediaHeaderFg);
531 	_header.drawLeftElided(
532 		p,
533 		headerLeft,
534 		floatingTop + headerTop,
535 		outerWidth - 2 * headerLeft,
536 		outerWidth);
537 	p.restore();
538 }
539 
itemSelection(not_null<const BaseLayout * > item,const Context & context) const540 TextSelection ListWidget::Section::itemSelection(
541 		not_null<const BaseLayout*> item,
542 		const Context &context) const {
543 	auto universalId = GetUniversalId(item);
544 	auto dragSelectAction = context.dragSelectAction;
545 	if (dragSelectAction != DragSelectAction::None) {
546 		auto i = context.dragSelected->find(universalId);
547 		if (i != context.dragSelected->end()) {
548 			return (dragSelectAction == DragSelectAction::Selecting)
549 				? FullSelection
550 				: TextSelection();
551 		}
552 	}
553 	auto i = context.selected->find(universalId);
554 	return (i == context.selected->cend())
555 		? TextSelection()
556 		: i->second.text;
557 }
558 
headerHeight() const559 int ListWidget::Section::headerHeight() const {
560 	return _header.isEmpty() ? 0 : st::infoMediaHeaderHeight;
561 }
562 
resizeToWidth(int newWidth)563 void ListWidget::Section::resizeToWidth(int newWidth) {
564 	auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2;
565 	if (newWidth < minWidth) {
566 		return;
567 	}
568 
569 	auto resizeOneColumn = [&](int itemsLeft, int itemWidth) {
570 		_itemsLeft = itemsLeft;
571 		_itemsTop = 0;
572 		_itemsInRow = 1;
573 		_itemWidth = itemWidth;
574 		for (auto &item : _items) {
575 			item.second->resizeGetHeight(_itemWidth);
576 		}
577 	};
578 	switch (_type) {
579 	case Type::Photo:
580 	case Type::Video:
581 	case Type::RoundFile: {
582 		_itemsLeft = st::infoMediaSkip;
583 		_itemsTop = st::infoMediaSkip;
584 		_itemsInRow = (newWidth - _itemsLeft)
585 			/ (st::infoMediaMinGridSize + st::infoMediaSkip);
586 		_itemWidth = ((newWidth - _itemsLeft) / _itemsInRow)
587 			- st::infoMediaSkip;
588 		for (auto &item : _items) {
589 			item.second->resizeGetHeight(_itemWidth);
590 		}
591 	} break;
592 
593 	case Type::GIF: {
594 		_mosaic.setFullWidth(newWidth - st::infoMediaSkip);
595 	} break;
596 
597 	case Type::RoundVoiceFile:
598 	case Type::MusicFile:
599 		resizeOneColumn(0, newWidth);
600 		break;
601 	case Type::File:
602 	case Type::Link: {
603 		auto itemsLeft = st::infoMediaHeaderPosition.x();
604 		auto itemWidth = newWidth - 2 * itemsLeft;
605 		resizeOneColumn(itemsLeft, itemWidth);
606 	} break;
607 	}
608 
609 	refreshHeight();
610 }
611 
MinItemHeight(Type type,int width)612 int ListWidget::Section::MinItemHeight(Type type, int width) {
613 	auto &songSt = st::overviewFileLayout;
614 	switch (type) {
615 	case Type::Photo:
616 	case Type::GIF:
617 	case Type::Video:
618 	case Type::RoundFile: {
619 		auto itemsLeft = st::infoMediaSkip;
620 		auto itemsInRow = (width - itemsLeft)
621 			/ (st::infoMediaMinGridSize + st::infoMediaSkip);
622 		return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow;
623 	} break;
624 
625 	case Type::RoundVoiceFile:
626 		return songSt.songPadding.top() + songSt.songThumbSize + songSt.songPadding.bottom() + st::lineWidth;
627 	case Type::File:
628 		return songSt.filePadding.top() + songSt.fileThumbSize + songSt.filePadding.bottom() + st::lineWidth;
629 	case Type::MusicFile:
630 		return songSt.songPadding.top() + songSt.songThumbSize + songSt.songPadding.bottom();
631 	case Type::Link:
632 		return st::linksPhotoSize + st::linksMargin.top() + st::linksMargin.bottom() + st::linksBorder;
633 	}
634 	Unexpected("Type in ListWidget::Section::MinItemHeight()");
635 }
636 
recountHeight()637 int ListWidget::Section::recountHeight() {
638 	auto result = headerHeight();
639 
640 	switch (_type) {
641 	case Type::Photo:
642 	case Type::Video:
643 	case Type::RoundFile: {
644 		auto itemHeight = _itemWidth + st::infoMediaSkip;
645 		auto index = 0;
646 		result += _itemsTop;
647 		for (auto &item : _items) {
648 			item.second->setPosition(_itemsInRow * result + index);
649 			if (++index == _itemsInRow) {
650 				result += itemHeight;
651 				index = 0;
652 			}
653 		}
654 		if (_items.size() % _itemsInRow) {
655 			_rowsCount = int(_items.size()) / _itemsInRow + 1;
656 			result += itemHeight;
657 		} else {
658 			_rowsCount = int(_items.size()) / _itemsInRow;
659 		}
660 	} break;
661 
662 	case Type::GIF: {
663 		return _mosaic.countDesiredHeight(0) + result;
664 	} break;
665 
666 	case Type::RoundVoiceFile:
667 	case Type::File:
668 	case Type::MusicFile:
669 	case Type::Link:
670 		for (auto &item : _items) {
671 			item.second->setPosition(result);
672 			result += item.second->height();
673 		}
674 		_rowsCount = _items.size();
675 		break;
676 	}
677 
678 	return result;
679 }
680 
refreshHeight()681 void ListWidget::Section::refreshHeight() {
682 	_height = recountHeight();
683 }
684 
ListWidget(QWidget * parent,not_null<AbstractController * > controller)685 ListWidget::ListWidget(
686 	QWidget *parent,
687 	not_null<AbstractController*> controller)
688 : RpWidget(parent)
689 , _controller(controller)
690 , _peer(_controller->key().peer())
691 , _migrated(_controller->migrated())
692 , _type(_controller->section().mediaType())
693 , _slice(sliceKey(_universalAroundId))
694 , _dateBadge(std::make_unique<DateBadge>(
695 		_type,
696 		[=] { scrollDateCheck(); },
__anon86c7e6610a02null697 		[=] { scrollDateHide(); })) {
698 	setMouseTracking(true);
699 	start();
700 }
701 
session() const702 Main::Session &ListWidget::session() const {
703 	return _controller->session();
704 }
705 
start()706 void ListWidget::start() {
707 	_controller->setSearchEnabledByContent(false);
708 	style::PaletteChanged(
709 	) | rpl::start_with_next([=] {
710 		invalidatePaletteCache();
711 	}, lifetime());
712 
713 	session().downloaderTaskFinished(
714 	) | rpl::start_with_next([=] {
715 		update();
716 	}, lifetime());
717 
718 	session().data().itemLayoutChanged(
719 	) | rpl::start_with_next([this](auto item) {
720 		itemLayoutChanged(item);
721 	}, lifetime());
722 
723 	session().data().itemRemoved(
724 	) | rpl::start_with_next([this](auto item) {
725 		itemRemoved(item);
726 	}, lifetime());
727 
728 	session().data().itemRepaintRequest(
729 	) | rpl::start_with_next([this](auto item) {
730 		repaintItem(item);
731 	}, lifetime());
732 
733 	_controller->mediaSourceQueryValue(
734 	) | rpl::start_with_next([this]{
735 		restart();
736 	}, lifetime());
737 }
738 
scrollToRequests() const739 rpl::producer<int> ListWidget::scrollToRequests() const {
740 	return _scrollToRequests.events();
741 }
742 
selectedListValue() const743 rpl::producer<SelectedItems> ListWidget::selectedListValue() const {
744 	return _selectedListStream.events_starting_with(
745 		collectSelectedItems());
746 }
747 
getCurrentSongGeometry()748 QRect ListWidget::getCurrentSongGeometry() {
749 	const auto type = AudioMsgId::Type::Song;
750 	const auto current = ::Media::Player::instance()->current(type);
751 	const auto fullMsgId = current.contextId();
752 	if (fullMsgId && isPossiblyMyId(fullMsgId)) {
753 		if (const auto item = findItemById(GetUniversalId(fullMsgId))) {
754 			return item->geometry;
755 		}
756 	}
757 	return QRect(0, 0, width(), 0);
758 }
759 
restart()760 void ListWidget::restart() {
761 	mouseActionCancel();
762 
763 	_overLayout = nullptr;
764 	_sections.clear();
765 	_layouts.clear();
766 	_heavyLayouts.clear();
767 
768 	_universalAroundId = kDefaultAroundId;
769 	_idsLimit = kMinimalIdsLimit;
770 	_slice = SparseIdsMergedSlice(sliceKey(_universalAroundId));
771 
772 	refreshViewer();
773 }
774 
itemRemoved(not_null<const HistoryItem * > item)775 void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
776 	if (!isMyItem(item)) {
777 		return;
778 	}
779 	auto id = GetUniversalId(item);
780 
781 	auto needHeightRefresh = false;
782 	auto sectionIt = findSectionByItem(id);
783 	if (sectionIt != _sections.end()) {
784 		if (sectionIt->removeItem(id)) {
785 			if (sectionIt->empty()) {
786 				_sections.erase(sectionIt);
787 			}
788 			needHeightRefresh = true;
789 		}
790 	}
791 
792 	if (isItemLayout(item, _overLayout)) {
793 		_overLayout = nullptr;
794 	}
795 
796 	if (const auto i = _layouts.find(id); i != _layouts.end()) {
797 		_heavyLayouts.remove(i->second.item.get());
798 		_layouts.erase(i);
799 	}
800 	_dragSelected.remove(id);
801 
802 	if (const auto i = _selected.find(id); i != _selected.cend()) {
803 		removeItemSelection(i);
804 	}
805 
806 	if (needHeightRefresh) {
807 		refreshHeight();
808 	}
809 	mouseActionUpdate(_mousePosition);
810 }
811 
computeFullId(UniversalMsgId universalId) const812 FullMsgId ListWidget::computeFullId(
813 		UniversalMsgId universalId) const {
814 	Expects(universalId != 0);
815 
816 	return (universalId > 0)
817 		? FullMsgId(peerToChannel(_peer->id), universalId)
818 		: FullMsgId(NoChannel, ServerMaxMsgId + universalId);
819 }
820 
collectSelectedItems() const821 auto ListWidget::collectSelectedItems() const -> SelectedItems {
822 	auto convert = [&](
823 			UniversalMsgId universalId,
824 			const SelectionData &selection) {
825 		auto result = SelectedItem(computeFullId(universalId));
826 		result.canDelete = selection.canDelete;
827 		result.canForward = selection.canForward;
828 		return result;
829 	};
830 	auto transformation = [&](const auto &item) {
831 		return convert(item.first, item.second);
832 	};
833 	auto items = SelectedItems(_type);
834 	if (hasSelectedItems()) {
835 		items.list.reserve(_selected.size());
836 		std::transform(
837 			_selected.begin(),
838 			_selected.end(),
839 			std::back_inserter(items.list),
840 			transformation);
841 	}
842 	return items;
843 }
844 
collectSelectedIds() const845 MessageIdsList ListWidget::collectSelectedIds() const {
846 	const auto selected = collectSelectedItems();
847 	return ranges::views::all(
848 		selected.list
849 	) | ranges::views::transform([](const SelectedItem &item) {
850 		return item.msgId;
851 	}) | ranges::to_vector;
852 }
853 
pushSelectedItems()854 void ListWidget::pushSelectedItems() {
855 	_selectedListStream.fire(collectSelectedItems());
856 }
857 
hasSelected() const858 bool ListWidget::hasSelected() const {
859 	return !_selected.empty();
860 }
861 
isSelectedItem(const SelectedMap::const_iterator & i) const862 bool ListWidget::isSelectedItem(
863 		const SelectedMap::const_iterator &i) const {
864 	return (i != _selected.end())
865 		&& (i->second.text == FullSelection);
866 }
867 
removeItemSelection(const SelectedMap::const_iterator & i)868 void ListWidget::removeItemSelection(
869 		const SelectedMap::const_iterator &i) {
870 	Expects(i != _selected.cend());
871 	_selected.erase(i);
872 	if (_selected.empty()) {
873 		update();
874 	}
875 	pushSelectedItems();
876 }
877 
hasSelectedText() const878 bool ListWidget::hasSelectedText() const {
879 	return hasSelected()
880 		&& !hasSelectedItems();
881 }
882 
hasSelectedItems() const883 bool ListWidget::hasSelectedItems() const {
884 	return isSelectedItem(_selected.cbegin());
885 }
886 
itemLayoutChanged(not_null<const HistoryItem * > item)887 void ListWidget::itemLayoutChanged(
888 		not_null<const HistoryItem*> item) {
889 	if (isItemLayout(item, _overLayout)) {
890 		mouseActionUpdate();
891 	}
892 }
893 
repaintItem(const HistoryItem * item)894 void ListWidget::repaintItem(const HistoryItem *item) {
895 	if (item && isMyItem(item)) {
896 		repaintItem(GetUniversalId(item));
897 	}
898 }
899 
repaintItem(UniversalMsgId universalId)900 void ListWidget::repaintItem(UniversalMsgId universalId) {
901 	if (auto item = findItemById(universalId)) {
902 		repaintItem(item->geometry);
903 	}
904 }
905 
repaintItem(const BaseLayout * item)906 void ListWidget::repaintItem(const BaseLayout *item) {
907 	if (item) {
908 		repaintItem(GetUniversalId(item));
909 	}
910 }
911 
repaintItem(not_null<const BaseLayout * > item)912 void ListWidget::repaintItem(not_null<const BaseLayout*> item) {
913 	repaintItem(GetUniversalId(item));
914 }
915 
repaintItem(QRect itemGeometry)916 void ListWidget::repaintItem(QRect itemGeometry) {
917 	rtlupdate(itemGeometry);
918 }
919 
isMyItem(not_null<const HistoryItem * > item) const920 bool ListWidget::isMyItem(not_null<const HistoryItem*> item) const {
921 	auto peer = item->history()->peer;
922 	return (_peer == peer) || (_migrated == peer);
923 }
924 
isPossiblyMyId(FullMsgId fullId) const925 bool ListWidget::isPossiblyMyId(FullMsgId fullId) const {
926 	return fullId.channel
927 		? (_peer->isChannel() && peerToChannel(_peer->id) == fullId.channel)
928 		: (!_peer->isChannel() || _migrated);
929 }
930 
isItemLayout(not_null<const HistoryItem * > item,BaseLayout * layout) const931 bool ListWidget::isItemLayout(
932 		not_null<const HistoryItem*> item,
933 		BaseLayout *layout) const {
934 	return layout && (layout->getItem() == item);
935 }
936 
invalidatePaletteCache()937 void ListWidget::invalidatePaletteCache() {
938 	for (auto &layout : _layouts) {
939 		layout.second.item->invalidateCache();
940 	}
941 }
942 
registerHeavyItem(not_null<const BaseLayout * > item)943 void ListWidget::registerHeavyItem(not_null<const BaseLayout*> item) {
944 	if (!_heavyLayouts.contains(item)) {
945 		_heavyLayouts.emplace(item);
946 		_heavyLayoutsInvalidated = true;
947 	}
948 }
949 
unregisterHeavyItem(not_null<const BaseLayout * > item)950 void ListWidget::unregisterHeavyItem(not_null<const BaseLayout*> item) {
951 	const auto i = _heavyLayouts.find(item);
952 	if (i != _heavyLayouts.end()) {
953 		_heavyLayouts.erase(i);
954 		_heavyLayoutsInvalidated = true;
955 	}
956 }
957 
itemVisible(not_null<const BaseLayout * > item)958 bool ListWidget::itemVisible(not_null<const BaseLayout*> item) {
959 	if (const auto &found = findItemById(GetUniversalId(item))) {
960 		const auto geometry = found->geometry;
961 		return (geometry.top() < _visibleBottom)
962 			&& (geometry.top() + geometry.height() > _visibleTop);
963 	}
964 	return true;
965 }
966 
openPhoto(not_null<PhotoData * > photo,FullMsgId id)967 void ListWidget::openPhoto(not_null<PhotoData*> photo, FullMsgId id) {
968 	_controller->parentController()->openPhoto(photo, id);
969 }
970 
openDocument(not_null<DocumentData * > document,FullMsgId id,bool showInMediaView)971 void ListWidget::openDocument(
972 		not_null<DocumentData*> document,
973 		FullMsgId id,
974 		bool showInMediaView) {
975 	_controller->parentController()->openDocument(
976 		document,
977 		id,
978 		showInMediaView);
979 }
980 
sliceKey(UniversalMsgId universalId) const981 SparseIdsMergedSlice::Key ListWidget::sliceKey(
982 		UniversalMsgId universalId) const {
983 	using Key = SparseIdsMergedSlice::Key;
984 	if (_migrated) {
985 		return Key(_peer->id, _migrated->id, universalId);
986 	}
987 	if (universalId < 0) {
988 		// Convert back to plain id for non-migrated histories.
989 		universalId = universalId + ServerMaxMsgId;
990 	}
991 	return Key(_peer->id, 0, universalId);
992 }
993 
refreshViewer()994 void ListWidget::refreshViewer() {
995 	_viewerLifetime.destroy();
996 	const auto idForViewer = sliceKey(_universalAroundId).universalId;
997 	_controller->mediaSource(
998 		idForViewer,
999 		_idsLimit,
1000 		_idsLimit
1001 	) | rpl::start_with_next([=](SparseIdsMergedSlice &&slice) {
1002 		if (!slice.fullCount()) {
1003 			// Don't display anything while full count is unknown.
1004 			return;
1005 		}
1006 		_slice = std::move(slice);
1007 		if (auto nearest = _slice.nearest(idForViewer)) {
1008 			_universalAroundId = GetUniversalId(*nearest);
1009 		}
1010 		refreshRows();
1011 	}, _viewerLifetime);
1012 }
1013 
getLayout(UniversalMsgId universalId)1014 BaseLayout *ListWidget::getLayout(UniversalMsgId universalId) {
1015 	auto it = _layouts.find(universalId);
1016 	if (it == _layouts.end()) {
1017 		if (auto layout = createLayout(universalId, _type)) {
1018 			layout->initDimensions();
1019 			it = _layouts.emplace(
1020 				universalId,
1021 				std::move(layout)).first;
1022 		} else {
1023 			return nullptr;
1024 		}
1025 	}
1026 	it->second.stale = false;
1027 	return it->second.item.get();
1028 }
1029 
getExistingLayout(UniversalMsgId universalId) const1030 BaseLayout *ListWidget::getExistingLayout(
1031 		UniversalMsgId universalId) const {
1032 	auto it = _layouts.find(universalId);
1033 	return (it != _layouts.end())
1034 		? it->second.item.get()
1035 		: nullptr;
1036 }
1037 
createLayout(UniversalMsgId universalId,Type type)1038 std::unique_ptr<BaseLayout> ListWidget::createLayout(
1039 		UniversalMsgId universalId,
1040 		Type type) {
1041 	auto item = session().data().message(computeFullId(universalId));
1042 	if (!item) {
1043 		return nullptr;
1044 	}
1045 	auto getPhoto = [&]() -> PhotoData* {
1046 		if (const auto media = item->media()) {
1047 			return media->photo();
1048 		}
1049 		return nullptr;
1050 	};
1051 	auto getFile = [&]() -> DocumentData* {
1052 		if (auto media = item->media()) {
1053 			return media->document();
1054 		}
1055 		return nullptr;
1056 	};
1057 
1058 	auto &songSt = st::overviewFileLayout;
1059 	using namespace Overview::Layout;
1060 	switch (type) {
1061 	case Type::Photo:
1062 		if (const auto photo = getPhoto()) {
1063 			return std::make_unique<Photo>(this, item, photo);
1064 		}
1065 		return nullptr;
1066 	case Type::GIF:
1067 		if (const auto file = getFile()) {
1068 			return std::make_unique<Gif>(this, item, file);
1069 		}
1070 		return nullptr;
1071 	case Type::Video:
1072 		if (const auto file = getFile()) {
1073 			return std::make_unique<Video>(this, item, file);
1074 		}
1075 		return nullptr;
1076 	case Type::File:
1077 		if (const auto file = getFile()) {
1078 			return std::make_unique<Document>(this, item, file, songSt);
1079 		}
1080 		return nullptr;
1081 	case Type::MusicFile:
1082 		if (const auto file = getFile()) {
1083 			return std::make_unique<Document>(this, item, file, songSt);
1084 		}
1085 		return nullptr;
1086 	case Type::RoundVoiceFile:
1087 		if (const auto file = getFile()) {
1088 			return std::make_unique<Voice>(this, item, file, songSt);
1089 		}
1090 		return nullptr;
1091 	case Type::Link:
1092 		return std::make_unique<Link>(this, item, item->media());
1093 	case Type::RoundFile:
1094 		return nullptr;
1095 	}
1096 	Unexpected("Type in ListWidget::createLayout()");
1097 }
1098 
refreshRows()1099 void ListWidget::refreshRows() {
1100 	saveScrollState();
1101 
1102 	markLayoutsStale();
1103 
1104 
1105 	_sections.clear();
1106 	auto section = Section(_type);
1107 	auto count = _slice.size();
1108 	for (auto i = count; i != 0;) {
1109 		auto universalId = GetUniversalId(_slice[--i]);
1110 		if (auto layout = getLayout(universalId)) {
1111 			if (!section.addItem(layout)) {
1112 				section.finishSection();
1113 				_sections.push_back(std::move(section));
1114 				section = Section(_type);
1115 				section.addItem(layout);
1116 			}
1117 		}
1118 	}
1119 	if (!section.empty()) {
1120 		section.finishSection();
1121 		_sections.push_back(std::move(section));
1122 	}
1123 
1124 	if (auto count = _slice.fullCount()) {
1125 		if (*count > kMediaCountForSearch) {
1126 			_controller->setSearchEnabledByContent(true);
1127 		}
1128 	}
1129 
1130 	clearStaleLayouts();
1131 
1132 	resizeToWidth(width());
1133 	restoreScrollState();
1134 	mouseActionUpdate();
1135 }
1136 
markLayoutsStale()1137 void ListWidget::markLayoutsStale() {
1138 	for (auto &layout : _layouts) {
1139 		layout.second.stale = true;
1140 	}
1141 }
1142 
preventAutoHide() const1143 bool ListWidget::preventAutoHide() const {
1144 	return (_contextMenu != nullptr) || (_actionBoxWeak != nullptr);
1145 }
1146 
saveState(not_null<Memento * > memento)1147 void ListWidget::saveState(not_null<Memento*> memento) {
1148 	if (_universalAroundId != kDefaultAroundId) {
1149 		auto state = countScrollState();
1150 		if (state.item) {
1151 			memento->setAroundId(computeFullId(_universalAroundId));
1152 			memento->setIdsLimit(_idsLimit);
1153 			memento->setScrollTopItem(computeFullId(state.item));
1154 			memento->setScrollTopShift(state.shift);
1155 		}
1156 	}
1157 }
1158 
restoreState(not_null<Memento * > memento)1159 void ListWidget::restoreState(not_null<Memento*> memento) {
1160 	if (auto limit = memento->idsLimit()) {
1161 		auto wasAroundId = memento->aroundId();
1162 		if (isPossiblyMyId(wasAroundId)) {
1163 			_idsLimit = limit;
1164 			_universalAroundId = GetUniversalId(wasAroundId);
1165 			_scrollTopState.item = GetUniversalId(memento->scrollTopItem());
1166 			_scrollTopState.shift = memento->scrollTopShift();
1167 			refreshViewer();
1168 		}
1169 	}
1170 }
1171 
resizeGetHeight(int newWidth)1172 int ListWidget::resizeGetHeight(int newWidth) {
1173 	if (newWidth > 0) {
1174 		for (auto &section : _sections) {
1175 			section.resizeToWidth(newWidth);
1176 		}
1177 	}
1178 	return recountHeight();
1179 }
1180 
findItemByPoint(QPoint point) const1181 auto ListWidget::findItemByPoint(QPoint point) const -> FoundItem {
1182 	Expects(!_sections.empty());
1183 	auto sectionIt = findSectionAfterTop(point.y());
1184 	if (sectionIt == _sections.end()) {
1185 		--sectionIt;
1186 	}
1187 	auto shift = QPoint(0, sectionIt->top());
1188 	return foundItemInSection(
1189 		sectionIt->findItemByPoint(point - shift),
1190 		*sectionIt);
1191 }
1192 
findItemById(UniversalMsgId universalId)1193 auto ListWidget::findItemById(
1194 		UniversalMsgId universalId) -> std::optional<FoundItem> {
1195 	auto sectionIt = findSectionByItem(universalId);
1196 	if (sectionIt != _sections.end()) {
1197 		auto item = sectionIt->findItemNearId(universalId);
1198 		if (item.exact) {
1199 			return foundItemInSection(item, *sectionIt);
1200 		}
1201 	}
1202 	return std::nullopt;
1203 }
1204 
findItemDetails(not_null<BaseLayout * > item)1205 auto ListWidget::findItemDetails(not_null<BaseLayout*> item)
1206 -> FoundItem {
1207 	const auto sectionIt = findSectionByItem(GetUniversalId(item));
1208 	Assert(sectionIt != _sections.end());
1209 	return foundItemInSection(sectionIt->findItemDetails(item), *sectionIt);
1210 }
1211 
foundItemInSection(const FoundItem & item,const Section & section) const1212 auto ListWidget::foundItemInSection(
1213 	const FoundItem &item,
1214 	const Section &section) const
1215 -> FoundItem {
1216 	return {
1217 		item.layout,
1218 		item.geometry.translated(0, section.top()),
1219 		item.exact };
1220 }
1221 
visibleTopBottomUpdated(int visibleTop,int visibleBottom)1222 void ListWidget::visibleTopBottomUpdated(
1223 		int visibleTop,
1224 		int visibleBottom) {
1225 	_visibleTop = visibleTop;
1226 	_visibleBottom = visibleBottom;
1227 
1228 	checkMoveToOtherViewer();
1229 	clearHeavyItems();
1230 
1231 	if (_dateBadge->goodType) {
1232 		updateDateBadgeFor(_visibleTop);
1233 		if (!_visibleTop) {
1234 			if (_dateBadge->shown) {
1235 				scrollDateHide();
1236 			} else {
1237 				update(_dateBadge->rect);
1238 			}
1239 		} else {
1240 			_dateBadge->check.call();
1241 		}
1242 	}
1243 }
1244 
updateDateBadgeFor(int top)1245 void ListWidget::updateDateBadgeFor(int top) {
1246 	if (_sections.empty()) {
1247 		return;
1248 	}
1249 	const auto layout = findItemByPoint({ st::infoMediaSkip, top }).layout;
1250 	const auto rectHeight = st::msgServiceMargin.top()
1251 		+ st::msgServicePadding.top()
1252 		+ st::msgServiceFont->height
1253 		+ st::msgServicePadding.bottom();
1254 
1255 	_dateBadge->text = ItemDateText(layout->getItem(), false);
1256 	_dateBadge->textWidth = st::msgServiceFont->width(_dateBadge->text);
1257 	_dateBadge->rect = QRect(0, top, width(), rectHeight);
1258 }
1259 
scrollDateCheck()1260 void ListWidget::scrollDateCheck() {
1261 	if (!_dateBadge->shown) {
1262 		toggleScrollDateShown();
1263 	}
1264 	_dateBadge->hideTimer.callOnce(st::infoScrollDateHideTimeout);
1265 }
1266 
scrollDateHide()1267 void ListWidget::scrollDateHide() {
1268 	if (_dateBadge->shown) {
1269 		toggleScrollDateShown();
1270 	}
1271 }
1272 
toggleScrollDateShown()1273 void ListWidget::toggleScrollDateShown() {
1274 	_dateBadge->shown = !_dateBadge->shown;
1275 	_dateBadge->opacity.start(
1276 		[=] { update(_dateBadge->rect); },
1277 		_dateBadge->shown ? 0. : 1.,
1278 		_dateBadge->shown ? 1. : 0.,
1279 		st::infoDateFadeDuration);
1280 }
1281 
checkMoveToOtherViewer()1282 void ListWidget::checkMoveToOtherViewer() {
1283 	auto visibleHeight = (_visibleBottom - _visibleTop);
1284 	if (width() <= 0
1285 		|| visibleHeight <= 0
1286 		|| _sections.empty()
1287 		|| _scrollTopState.item) {
1288 		return;
1289 	}
1290 
1291 	auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop });
1292 	auto bottomItem = findItemByPoint({ st::infoMediaSkip, _visibleBottom });
1293 
1294 	auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
1295 	auto minItemHeight = Section::MinItemHeight(_type, width());
1296 	auto preloadedCount = preloadedHeight / minItemHeight;
1297 	auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
1298 	auto preloadIdsLimit = preloadIdsLimitMin
1299 		+ (visibleHeight / minItemHeight);
1300 
1301 	auto preloadBefore = kPreloadIfLessThanScreens * visibleHeight;
1302 	auto after = _slice.skippedAfter();
1303 	auto preloadTop = (_visibleTop < preloadBefore);
1304 	auto topLoaded = after && (*after == 0);
1305 	auto before = _slice.skippedBefore();
1306 	auto preloadBottom = (height() - _visibleBottom < preloadBefore);
1307 	auto bottomLoaded = before && (*before == 0);
1308 
1309 	auto minScreenDelta = kPreloadedScreensCount
1310 		- kPreloadIfLessThanScreens;
1311 	auto minUniversalIdDelta = (minScreenDelta * visibleHeight)
1312 		/ minItemHeight;
1313 	auto preloadAroundItem = [&](const FoundItem &item) {
1314 		auto preloadRequired = false;
1315 		auto universalId = GetUniversalId(item.layout);
1316 		if (!preloadRequired) {
1317 			preloadRequired = (_idsLimit < preloadIdsLimitMin);
1318 		}
1319 		if (!preloadRequired) {
1320 			auto delta = _slice.distance(
1321 				sliceKey(_universalAroundId),
1322 				sliceKey(universalId));
1323 			Assert(delta != std::nullopt);
1324 			preloadRequired = (qAbs(*delta) >= minUniversalIdDelta);
1325 		}
1326 		if (preloadRequired) {
1327 			_idsLimit = preloadIdsLimit;
1328 			_universalAroundId = universalId;
1329 			refreshViewer();
1330 		}
1331 	};
1332 
1333 	if (preloadTop && !topLoaded) {
1334 		preloadAroundItem(topItem);
1335 	} else if (preloadBottom && !bottomLoaded) {
1336 		preloadAroundItem(bottomItem);
1337 	}
1338 }
1339 
clearHeavyItems()1340 void ListWidget::clearHeavyItems() {
1341 	const auto visibleHeight = _visibleBottom - _visibleTop;
1342 	if (!visibleHeight) {
1343 		return;
1344 	}
1345 	_heavyLayoutsInvalidated = false;
1346 	const auto above = _visibleTop - visibleHeight;
1347 	const auto below = _visibleBottom + visibleHeight;
1348 	for (auto i = _heavyLayouts.begin(); i != _heavyLayouts.end();) {
1349 		const auto item = const_cast<BaseLayout*>(i->get());
1350 		const auto rect = findItemDetails(item).geometry;
1351 		if (rect.top() + rect.height() <= above || rect.top() >= below) {
1352 			i = _heavyLayouts.erase(i);
1353 			item->clearHeavyPart();
1354 			if (_heavyLayoutsInvalidated) {
1355 				break;
1356 			}
1357 		} else {
1358 			++i;
1359 		}
1360 	}
1361 	if (_heavyLayoutsInvalidated) {
1362 		clearHeavyItems();
1363 	}
1364 }
1365 
countScrollState() const1366 auto ListWidget::countScrollState() const -> ScrollTopState {
1367 	if (_sections.empty()) {
1368 		return { 0, 0 };
1369 	}
1370 	auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop });
1371 	return {
1372 		GetUniversalId(topItem.layout),
1373 		_visibleTop - topItem.geometry.y()
1374 	};
1375 }
1376 
saveScrollState()1377 void ListWidget::saveScrollState() {
1378 	if (!_scrollTopState.item) {
1379 		_scrollTopState = countScrollState();
1380 	}
1381 }
1382 
restoreScrollState()1383 void ListWidget::restoreScrollState() {
1384 	if (_sections.empty() || !_scrollTopState.item) {
1385 		return;
1386 	}
1387 	auto sectionIt = findSectionByItem(_scrollTopState.item);
1388 	if (sectionIt == _sections.end()) {
1389 		--sectionIt;
1390 	}
1391 	auto item = foundItemInSection(
1392 		sectionIt->findItemNearId(_scrollTopState.item),
1393 		*sectionIt);
1394 	auto newVisibleTop = item.geometry.y() + _scrollTopState.shift;
1395 	if (_visibleTop != newVisibleTop) {
1396 		_scrollToRequests.fire_copy(newVisibleTop);
1397 	}
1398 	_scrollTopState = ScrollTopState();
1399 }
1400 
padding() const1401 QMargins ListWidget::padding() const {
1402 	return st::infoMediaMargin;
1403 }
1404 
paintEvent(QPaintEvent * e)1405 void ListWidget::paintEvent(QPaintEvent *e) {
1406 	Painter p(this);
1407 
1408 	auto outerWidth = width();
1409 	auto clip = e->rect();
1410 	auto ms = crl::now();
1411 	auto fromSectionIt = findSectionAfterTop(clip.y());
1412 	auto tillSectionIt = findSectionAfterBottom(
1413 		fromSectionIt,
1414 		clip.y() + clip.height());
1415 	auto context = Context {
1416 		Overview::Layout::PaintContext(ms, hasSelectedItems()),
1417 		&_selected,
1418 		&_dragSelected,
1419 		_dragSelectAction
1420 	};
1421 	for (auto it = fromSectionIt; it != tillSectionIt; ++it) {
1422 		auto top = it->top();
1423 		p.translate(0, top);
1424 		it->paint(p, context, clip.translated(0, -top), outerWidth);
1425 		p.translate(0, -top);
1426 	}
1427 	if (fromSectionIt != _sections.end()) {
1428 		fromSectionIt->paintFloatingHeader(p, _visibleTop, outerWidth);
1429 	}
1430 
1431 	if (_dateBadge->goodType && clip.intersects(_dateBadge->rect)) {
1432 		const auto scrollDateOpacity =
1433 			_dateBadge->opacity.value(_dateBadge->shown ? 1. : 0.);
1434 		if (scrollDateOpacity > 0.) {
1435 			p.setOpacity(scrollDateOpacity);
1436 			if (_dateBadge->corners.p[0].isNull()) {
1437 				_dateBadge->corners = Ui::PrepareCornerPixmaps(
1438 					Ui::HistoryServiceMsgRadius(),
1439 					st::roundedBg,
1440 					nullptr);
1441 			}
1442 			HistoryView::ServiceMessagePainter::PaintDate(
1443 				p,
1444 				st::roundedBg,
1445 				_dateBadge->corners,
1446 				st::roundedFg,
1447 				_dateBadge->text,
1448 				_dateBadge->textWidth,
1449 				_visibleTop,
1450 				outerWidth,
1451 				false);
1452 		}
1453 	}
1454 }
1455 
mousePressEvent(QMouseEvent * e)1456 void ListWidget::mousePressEvent(QMouseEvent *e) {
1457 	if (_contextMenu) {
1458 		e->accept();
1459 		return; // ignore mouse press, that was hiding context menu
1460 	}
1461 	mouseActionStart(e->globalPos(), e->button());
1462 }
1463 
mouseMoveEvent(QMouseEvent * e)1464 void ListWidget::mouseMoveEvent(QMouseEvent *e) {
1465 	auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
1466 	if (!buttonsPressed && _mouseAction != MouseAction::None) {
1467 		mouseReleaseEvent(e);
1468 	}
1469 	mouseActionUpdate(e->globalPos());
1470 }
1471 
mouseReleaseEvent(QMouseEvent * e)1472 void ListWidget::mouseReleaseEvent(QMouseEvent *e) {
1473 	mouseActionFinish(e->globalPos(), e->button());
1474 	if (!rect().contains(e->pos())) {
1475 		leaveEvent(e);
1476 	}
1477 }
1478 
mouseDoubleClickEvent(QMouseEvent * e)1479 void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {
1480 	mouseActionStart(e->globalPos(), e->button());
1481 	trySwitchToWordSelection();
1482 }
1483 
showContextMenu(QContextMenuEvent * e,ContextMenuSource source)1484 void ListWidget::showContextMenu(
1485 		QContextMenuEvent *e,
1486 		ContextMenuSource source) {
1487 	if (_contextMenu) {
1488 		_contextMenu = nullptr;
1489 		repaintItem(_contextUniversalId);
1490 	}
1491 	if (e->reason() == QContextMenuEvent::Mouse) {
1492 		mouseActionUpdate(e->globalPos());
1493 	}
1494 
1495 	auto item = session().data().message(computeFullId(_overState.itemId));
1496 	if (!item || !_overState.inside) {
1497 		return;
1498 	}
1499 	auto universalId = _contextUniversalId = _overState.itemId;
1500 
1501 	enum class SelectionState {
1502 		NoSelectedItems,
1503 		NotOverSelectedItems,
1504 		OverSelectedItems,
1505 		NotOverSelectedText,
1506 		OverSelectedText,
1507 	};
1508 	auto overSelected = SelectionState::NoSelectedItems;
1509 	if (source == ContextMenuSource::Touch) {
1510 		if (hasSelectedItems()) {
1511 			overSelected = SelectionState::OverSelectedItems;
1512 		} else if (hasSelectedText()) {
1513 			overSelected = SelectionState::OverSelectedItems;
1514 		}
1515 	} else if (hasSelectedText()) {
1516 		// #TODO text selection
1517 	} else if (hasSelectedItems()) {
1518 		auto it = _selected.find(_overState.itemId);
1519 		if (isSelectedItem(it) && _overState.inside) {
1520 			overSelected = SelectionState::OverSelectedItems;
1521 		} else {
1522 			overSelected = SelectionState::NotOverSelectedItems;
1523 		}
1524 	}
1525 
1526 	auto canDeleteAll = [&] {
1527 		return ranges::none_of(_selected, [](auto &&item) {
1528 			return !item.second.canDelete;
1529 		});
1530 	};
1531 	auto canForwardAll = [&] {
1532 		return ranges::none_of(_selected, [](auto &&item) {
1533 			return !item.second.canForward;
1534 		});
1535 	};
1536 
1537 	auto link = ClickHandler::getActive();
1538 
1539 	const auto itemFullId = item->fullId();
1540 	const auto owner = &session().data();
1541 	_contextMenu = base::make_unique_q<Ui::PopupMenu>(this);
1542 	_contextMenu->addAction(
1543 		tr::lng_context_to_msg(tr::now),
1544 		[=] {
1545 			if (const auto item = owner->message(itemFullId)) {
1546 				_controller->parentController()->showPeerHistoryAtItem(item);
1547 			}
1548 		});
1549 
1550 	auto photoLink = dynamic_cast<PhotoClickHandler*>(link.get());
1551 	auto fileLink = dynamic_cast<DocumentClickHandler*>(link.get());
1552 	if (photoLink || fileLink) {
1553 		auto [isVideo, isVoice, isAudio] = [&] {
1554 			if (fileLink) {
1555 				auto document = fileLink->document();
1556 				return std::make_tuple(
1557 					document->isVideoFile(),
1558 					document->isVoiceMessage(),
1559 					document->isAudioFile()
1560 				);
1561 			}
1562 			return std::make_tuple(false, false, false);
1563 		}();
1564 
1565 		if (photoLink) {
1566 		} else {
1567 			if (auto document = fileLink->document()) {
1568 				if (document->loading()) {
1569 					_contextMenu->addAction(
1570 						tr::lng_context_cancel_download(tr::now),
1571 						[document] {
1572 							document->cancel();
1573 						});
1574 				} else {
1575 					auto filepath = document->filepath(true);
1576 					if (!filepath.isEmpty()) {
1577 						auto handler = App::LambdaDelayed(
1578 							st::defaultDropdownMenu.menu.ripple.hideDuration,
1579 							this,
1580 							[filepath] {
1581 								File::ShowInFolder(filepath);
1582 							});
1583 						_contextMenu->addAction(
1584 							(Platform::IsMac()
1585 								? tr::lng_context_show_in_finder(tr::now)
1586 								: tr::lng_context_show_in_folder(tr::now)),
1587 							std::move(handler));
1588 					}
1589 					auto handler = App::LambdaDelayed(
1590 						st::defaultDropdownMenu.menu.ripple.hideDuration,
1591 						this,
1592 						[=] {
1593 							DocumentSaveClickHandler::Save(
1594 								itemFullId,
1595 								document,
1596 								DocumentSaveClickHandler::Mode::ToNewFile);
1597 						});
1598 					_contextMenu->addAction(
1599 						(isVideo
1600 							? tr::lng_context_save_video(tr::now)
1601 							: isVoice
1602 							? tr::lng_context_save_audio(tr::now)
1603 							: isAudio
1604 							? tr::lng_context_save_audio_file(tr::now)
1605 							: tr::lng_context_save_file(tr::now)),
1606 						std::move(handler));
1607 				}
1608 			}
1609 		}
1610 	} else if (link) {
1611 		const auto actionText = link->copyToClipboardContextItemText();
1612 		if (!actionText.isEmpty()) {
1613 			_contextMenu->addAction(
1614 				actionText,
1615 				[text = link->copyToClipboardText()] {
1616 					QGuiApplication::clipboard()->setText(text);
1617 				});
1618 		}
1619 	}
1620 	if (overSelected == SelectionState::OverSelectedItems) {
1621 		if (canForwardAll()) {
1622 			_contextMenu->addAction(
1623 				tr::lng_context_forward_selected(tr::now),
1624 				crl::guard(this, [this] {
1625 					forwardSelected();
1626 				}));
1627 		}
1628 		if (canDeleteAll()) {
1629 			_contextMenu->addAction(
1630 				tr::lng_context_delete_selected(tr::now),
1631 				crl::guard(this, [this] {
1632 					deleteSelected();
1633 				}));
1634 		}
1635 		_contextMenu->addAction(
1636 			tr::lng_context_clear_selection(tr::now),
1637 			crl::guard(this, [this] {
1638 				clearSelected();
1639 			}));
1640 	} else {
1641 		if (overSelected != SelectionState::NotOverSelectedItems) {
1642 			if (item->allowsForward()) {
1643 				_contextMenu->addAction(
1644 					tr::lng_context_forward_msg(tr::now),
1645 					crl::guard(this, [this, universalId] {
1646 						forwardItem(universalId);
1647 					}));
1648 			}
1649 			if (item->canDelete()) {
1650 				_contextMenu->addAction(Ui::DeleteMessageContextAction(
1651 					_contextMenu->menu(),
1652 					[=] { deleteItem(universalId); },
1653 					item->ttlDestroyAt(),
1654 					[=] { _contextMenu = nullptr; }));
1655 			}
1656 		}
1657 		_contextMenu->addAction(
1658 			tr::lng_context_select_msg(tr::now),
1659 			crl::guard(this, [this, universalId] {
1660 				if (hasSelectedText()) {
1661 					clearSelected();
1662 				} else if (_selected.size() == MaxSelectedItems) {
1663 					return;
1664 				} else if (_selected.empty()) {
1665 					update();
1666 				}
1667 				applyItemSelection(universalId, FullSelection);
1668 			}));
1669 	}
1670 
1671 	_contextMenu->setDestroyedCallback(crl::guard(
1672 		this,
1673 		[this, universalId] {
1674 			mouseActionUpdate(QCursor::pos());
1675 			repaintItem(universalId);
1676 			_checkForHide.fire({});
1677 		}));
1678 	_contextMenu->popup(e->globalPos());
1679 	e->accept();
1680 }
1681 
contextMenuEvent(QContextMenuEvent * e)1682 void ListWidget::contextMenuEvent(QContextMenuEvent *e) {
1683 	showContextMenu(
1684 		e,
1685 		(e->reason() == QContextMenuEvent::Mouse)
1686 			? ContextMenuSource::Mouse
1687 			: ContextMenuSource::Other);
1688 }
1689 
forwardSelected()1690 void ListWidget::forwardSelected() {
1691 	if (auto items = collectSelectedIds(); !items.empty()) {
1692 		forwardItems(std::move(items));
1693 	}
1694 }
1695 
forwardItem(UniversalMsgId universalId)1696 void ListWidget::forwardItem(UniversalMsgId universalId) {
1697 	if (const auto item = session().data().message(computeFullId(universalId))) {
1698 		forwardItems({ 1, item->fullId() });
1699 	}
1700 }
1701 
forwardItems(MessageIdsList && items)1702 void ListWidget::forwardItems(MessageIdsList &&items) {
1703 	auto callback = [weak = Ui::MakeWeak(this)] {
1704 		if (const auto strong = weak.data()) {
1705 			strong->clearSelected();
1706 		}
1707 	};
1708 	setActionBoxWeak(Window::ShowForwardMessagesBox(
1709 		_controller,
1710 		std::move(items),
1711 		std::move(callback)));
1712 }
1713 
deleteSelected()1714 void ListWidget::deleteSelected() {
1715 	if (const auto box = deleteItems(collectSelectedIds())) {
1716 		const auto weak = Ui::MakeWeak(this);
1717 		box->setDeleteConfirmedCallback([=]{
1718 			if (const auto strong = weak.data()) {
1719 				strong->clearSelected();
1720 			}
1721 		});
1722 	}
1723 }
1724 
deleteItem(UniversalMsgId universalId)1725 void ListWidget::deleteItem(UniversalMsgId universalId) {
1726 	if (const auto item = session().data().message(computeFullId(universalId))) {
1727 		deleteItems({ 1, item->fullId() });
1728 	}
1729 }
1730 
deleteItems(MessageIdsList && items)1731 DeleteMessagesBox *ListWidget::deleteItems(MessageIdsList &&items) {
1732 	if (!items.empty()) {
1733 		const auto box = Ui::show(
1734 			Box<DeleteMessagesBox>(
1735 				&_controller->session(),
1736 				std::move(items))).data();
1737 		setActionBoxWeak(box);
1738 		return box;
1739 	}
1740 	return nullptr;
1741 }
1742 
setActionBoxWeak(QPointer<Ui::RpWidget> box)1743 void ListWidget::setActionBoxWeak(QPointer<Ui::RpWidget> box) {
1744 	if ((_actionBoxWeak = box)) {
1745 		_actionBoxWeakLifetime = _actionBoxWeak->alive(
1746 		) | rpl::start_with_done([weak = Ui::MakeWeak(this)]{
1747 			if (weak) {
1748 				weak->_checkForHide.fire({});
1749 			}
1750 		});
1751 	}
1752 }
1753 
trySwitchToWordSelection()1754 void ListWidget::trySwitchToWordSelection() {
1755 	auto selectingSome = (_mouseAction == MouseAction::Selecting)
1756 		&& hasSelectedText();
1757 	auto willSelectSome = (_mouseAction == MouseAction::None)
1758 		&& !hasSelectedItems();
1759 	auto checkSwitchToWordSelection = _overLayout
1760 		&& (_mouseSelectType == TextSelectType::Letters)
1761 		&& (selectingSome || willSelectSome);
1762 	if (checkSwitchToWordSelection) {
1763 		switchToWordSelection();
1764 	}
1765 }
1766 
switchToWordSelection()1767 void ListWidget::switchToWordSelection() {
1768 	Expects(_overLayout != nullptr);
1769 
1770 	StateRequest request;
1771 	request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
1772 	auto dragState = _overLayout->getState(_pressState.cursor, request);
1773 	if (dragState.cursor != CursorState::Text) {
1774 		return;
1775 	}
1776 	_mouseTextSymbol = dragState.symbol;
1777 	_mouseSelectType = TextSelectType::Words;
1778 	if (_mouseAction == MouseAction::None) {
1779 		_mouseAction = MouseAction::Selecting;
1780 		clearSelected();
1781 		auto selStatus = TextSelection {
1782 			dragState.symbol,
1783 			dragState.symbol
1784 		};
1785 		applyItemSelection(_overState.itemId, selStatus);
1786 	}
1787 	mouseActionUpdate();
1788 
1789 	_trippleClickPoint = _mousePosition;
1790 	_trippleClickStartTime = crl::now();
1791 }
1792 
applyItemSelection(UniversalMsgId universalId,TextSelection selection)1793 void ListWidget::applyItemSelection(
1794 		UniversalMsgId universalId,
1795 		TextSelection selection) {
1796 	if (changeItemSelection(
1797 			_selected,
1798 			universalId,
1799 			selection)) {
1800 		repaintItem(universalId);
1801 		pushSelectedItems();
1802 	}
1803 }
1804 
toggleItemSelection(UniversalMsgId universalId)1805 void ListWidget::toggleItemSelection(UniversalMsgId universalId) {
1806 	auto it = _selected.find(universalId);
1807 	if (it == _selected.cend()) {
1808 		applyItemSelection(universalId, FullSelection);
1809 	} else {
1810 		removeItemSelection(it);
1811 	}
1812 }
1813 
changeItemSelection(SelectedMap & selected,UniversalMsgId universalId,TextSelection selection) const1814 bool ListWidget::changeItemSelection(
1815 		SelectedMap &selected,
1816 		UniversalMsgId universalId,
1817 		TextSelection selection) const {
1818 	auto changeExisting = [&](auto it) {
1819 		if (it == selected.cend()) {
1820 			return false;
1821 		} else if (it->second.text != selection) {
1822 			it->second.text = selection;
1823 			return true;
1824 		}
1825 		return false;
1826 	};
1827 	if (selected.size() < MaxSelectedItems) {
1828 		auto [iterator, ok] = selected.try_emplace(
1829 			universalId,
1830 			selection);
1831 		if (ok) {
1832 			auto item = session().data().message(computeFullId(universalId));
1833 			if (!item) {
1834 				selected.erase(iterator);
1835 				return false;
1836 			}
1837 			iterator->second.canDelete = item->canDelete();
1838 			iterator->second.canForward = item->allowsForward();
1839 			return true;
1840 		}
1841 		return changeExisting(iterator);
1842 	}
1843 	return changeExisting(selected.find(universalId));
1844 }
1845 
isItemUnderPressSelected() const1846 bool ListWidget::isItemUnderPressSelected() const {
1847 	return itemUnderPressSelection() != _selected.end();
1848 }
1849 
itemUnderPressSelection()1850 auto ListWidget::itemUnderPressSelection() -> SelectedMap::iterator {
1851 	return (_pressState.itemId && _pressState.inside)
1852 		? _selected.find(_pressState.itemId)
1853 		: _selected.end();
1854 }
1855 
itemUnderPressSelection() const1856 auto ListWidget::itemUnderPressSelection() const
1857 -> SelectedMap::const_iterator {
1858 	return (_pressState.itemId && _pressState.inside)
1859 		? _selected.find(_pressState.itemId)
1860 		: _selected.end();
1861 }
1862 
requiredToStartDragging(not_null<BaseLayout * > layout) const1863 bool ListWidget::requiredToStartDragging(
1864 		not_null<BaseLayout*> layout) const {
1865 	if (_mouseCursorState == CursorState::Date) {
1866 		return true;
1867 	}
1868 //	return dynamic_cast<Sticker*>(layout->getMedia());
1869 	return false;
1870 }
1871 
isPressInSelectedText(TextState state) const1872 bool ListWidget::isPressInSelectedText(TextState state) const {
1873 	if (state.cursor != CursorState::Text) {
1874 		return false;
1875 	}
1876 	if (!hasSelectedText()
1877 		|| !isItemUnderPressSelected()) {
1878 		return false;
1879 	}
1880 	auto pressedSelection = itemUnderPressSelection();
1881 	auto from = pressedSelection->second.text.from;
1882 	auto to = pressedSelection->second.text.to;
1883 	return (state.symbol >= from && state.symbol < to);
1884 }
1885 
clearSelected()1886 void ListWidget::clearSelected() {
1887 	if (_selected.empty()) {
1888 		return;
1889 	}
1890 	if (hasSelectedText()) {
1891 		repaintItem(_selected.begin()->first);
1892 		_selected.clear();
1893 	} else {
1894 		_selected.clear();
1895 		pushSelectedItems();
1896 		update();
1897 	}
1898 }
1899 
validateTrippleClickStartTime()1900 void ListWidget::validateTrippleClickStartTime() {
1901 	if (_trippleClickStartTime) {
1902 		auto elapsed = (crl::now() - _trippleClickStartTime);
1903 		if (elapsed >= QApplication::doubleClickInterval()) {
1904 			_trippleClickStartTime = 0;
1905 		}
1906 	}
1907 }
1908 
enterEventHook(QEnterEvent * e)1909 void ListWidget::enterEventHook(QEnterEvent *e) {
1910 	mouseActionUpdate(QCursor::pos());
1911 	return RpWidget::enterEventHook(e);
1912 }
1913 
leaveEventHook(QEvent * e)1914 void ListWidget::leaveEventHook(QEvent *e) {
1915 	if (const auto item = _overLayout) {
1916 		if (_overState.inside) {
1917 			repaintItem(item);
1918 			_overState.inside = false;
1919 		}
1920 	}
1921 	ClickHandler::clearActive();
1922 	if (!ClickHandler::getPressed() && _cursor != style::cur_default) {
1923 		_cursor = style::cur_default;
1924 		setCursor(_cursor);
1925 	}
1926 	return RpWidget::leaveEventHook(e);
1927 }
1928 
clampMousePosition(QPoint position) const1929 QPoint ListWidget::clampMousePosition(QPoint position) const {
1930 	return {
1931 		std::clamp(position.x(), 0, qMax(0, width() - 1)),
1932 		std::clamp(position.y(), _visibleTop, _visibleBottom - 1)
1933 	};
1934 }
1935 
mouseActionUpdate(const QPoint & globalPosition)1936 void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
1937 	if (_sections.empty() || _visibleBottom <= _visibleTop) {
1938 		return;
1939 	}
1940 
1941 	_mousePosition = globalPosition;
1942 
1943 	auto local = mapFromGlobal(_mousePosition);
1944 	auto point = clampMousePosition(local);
1945 	auto [layout, geometry, inside] = findItemByPoint(point);
1946 	auto state = MouseState{
1947 		GetUniversalId(layout),
1948 		geometry.size(),
1949 		point - geometry.topLeft(),
1950 		inside
1951 	};
1952 	if (_overLayout != layout) {
1953 		repaintItem(_overLayout);
1954 		_overLayout = layout;
1955 		repaintItem(geometry);
1956 	}
1957 	_overState = state;
1958 
1959 	TextState dragState;
1960 	ClickHandlerHost *lnkhost = nullptr;
1961 	auto inTextSelection = _overState.inside
1962 		&& (_overState.itemId == _pressState.itemId)
1963 		&& hasSelectedText();
1964 	if (_overLayout) {
1965 		auto cursorDeltaLength = [&] {
1966 			auto cursorDelta = (_overState.cursor - _pressState.cursor);
1967 			return cursorDelta.manhattanLength();
1968 		};
1969 		auto dragStartLength = [] {
1970 			return QApplication::startDragDistance();
1971 		};
1972 		if (_overState.itemId != _pressState.itemId
1973 			|| cursorDeltaLength() >= dragStartLength()) {
1974 			if (_mouseAction == MouseAction::PrepareDrag) {
1975 				_mouseAction = MouseAction::Dragging;
1976 				InvokeQueued(this, [this] { performDrag(); });
1977 			} else if (_mouseAction == MouseAction::PrepareSelect) {
1978 				_mouseAction = MouseAction::Selecting;
1979 			}
1980 		}
1981 		StateRequest request;
1982 		if (_mouseAction == MouseAction::Selecting) {
1983 			request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
1984 		} else {
1985 			inTextSelection = false;
1986 		}
1987 		dragState = _overLayout->getState(_overState.cursor, request);
1988 		lnkhost = _overLayout;
1989 	}
1990 	ClickHandler::setActive(dragState.link, lnkhost);
1991 
1992 	if (_mouseAction == MouseAction::None) {
1993 		_mouseCursorState = dragState.cursor;
1994 		auto cursor = computeMouseCursor();
1995 		if (_cursor != cursor) {
1996 			setCursor(_cursor = cursor);
1997 		}
1998 	} else if (_mouseAction == MouseAction::Selecting) {
1999 		if (inTextSelection) {
2000 			auto second = dragState.symbol;
2001 			if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {
2002 				++second;
2003 			}
2004 			auto selState = TextSelection {
2005 				qMin(second, _mouseTextSymbol),
2006 				qMax(second, _mouseTextSymbol)
2007 			};
2008 			if (_mouseSelectType != TextSelectType::Letters) {
2009 				selState = _overLayout->adjustSelection(selState, _mouseSelectType);
2010 			}
2011 			applyItemSelection(_overState.itemId, selState);
2012 			auto hasSelection = (selState == FullSelection)
2013 				|| (selState.from != selState.to);
2014 			if (!_wasSelectedText && hasSelection) {
2015 				_wasSelectedText = true;
2016 				setFocus();
2017 			}
2018 			clearDragSelection();
2019 		} else if (_pressState.itemId) {
2020 			updateDragSelection();
2021 		}
2022 	} else if (_mouseAction == MouseAction::Dragging) {
2023 	}
2024 
2025 	// #TODO scroll by drag
2026 	//if (_mouseAction == MouseAction::Selecting) {
2027 	//	_widget->checkSelectingScroll(mousePos);
2028 	//} else {
2029 	//	clearDragSelection();
2030 	//	_widget->noSelectingScroll();
2031 	//}
2032 }
2033 
computeMouseCursor() const2034 style::cursor ListWidget::computeMouseCursor() const {
2035 	if (ClickHandler::getPressed() || ClickHandler::getActive()) {
2036 		return style::cur_pointer;
2037 	} else if (!hasSelectedItems()
2038 		&& (_mouseCursorState == CursorState::Text)) {
2039 		return style::cur_text;
2040 	}
2041 	return style::cur_default;
2042 }
2043 
updateDragSelection()2044 void ListWidget::updateDragSelection() {
2045 	auto fromState = _pressState;
2046 	auto tillState = _overState;
2047 	auto swapStates = IsAfter(fromState, tillState);
2048 	if (swapStates) {
2049 		std::swap(fromState, tillState);
2050 	}
2051 	if (!fromState.itemId || !tillState.itemId) {
2052 		clearDragSelection();
2053 		return;
2054 	}
2055 	auto fromId = SkipSelectFromItem(fromState)
2056 		? (fromState.itemId - 1)
2057 		: fromState.itemId;
2058 	auto tillId = SkipSelectTillItem(tillState)
2059 		? tillState.itemId
2060 		: (tillState.itemId - 1);
2061 	for (auto i = _dragSelected.begin(); i != _dragSelected.end();) {
2062 		auto itemId = i->first;
2063 		if (itemId > fromId || itemId <= tillId) {
2064 			i = _dragSelected.erase(i);
2065 		} else {
2066 			++i;
2067 		}
2068 	}
2069 	for (auto &layoutItem : _layouts) {
2070 		auto &&universalId = layoutItem.first;
2071 		if (universalId <= fromId && universalId > tillId) {
2072 			changeItemSelection(
2073 				_dragSelected,
2074 				universalId,
2075 				FullSelection);
2076 		}
2077 	}
2078 	_dragSelectAction = [&] {
2079 		if (_dragSelected.empty()) {
2080 			return DragSelectAction::None;
2081 		}
2082 		auto &[firstDragItem, data] = swapStates
2083 			? _dragSelected.front()
2084 			: _dragSelected.back();
2085 		if (isSelectedItem(_selected.find(firstDragItem))) {
2086 			return DragSelectAction::Deselecting;
2087 		} else {
2088 			return DragSelectAction::Selecting;
2089 		}
2090 	}();
2091 	if (!_wasSelectedText
2092 		&& !_dragSelected.empty()
2093 		&& _dragSelectAction == DragSelectAction::Selecting) {
2094 		_wasSelectedText = true;
2095 		setFocus();
2096 	}
2097 	update();
2098 }
2099 
clearDragSelection()2100 void ListWidget::clearDragSelection() {
2101 	_dragSelectAction = DragSelectAction::None;
2102 	if (!_dragSelected.empty()) {
2103 		_dragSelected.clear();
2104 		update();
2105 	}
2106 }
2107 
mouseActionStart(const QPoint & globalPosition,Qt::MouseButton button)2108 void ListWidget::mouseActionStart(
2109 		const QPoint &globalPosition,
2110 		Qt::MouseButton button) {
2111 	mouseActionUpdate(globalPosition);
2112 	if (button != Qt::LeftButton) {
2113 		return;
2114 	}
2115 
2116 	ClickHandler::pressed();
2117 	if (_pressState != _overState) {
2118 		if (_pressState.itemId != _overState.itemId) {
2119 			repaintItem(_pressState.itemId);
2120 		}
2121 		_pressState = _overState;
2122 		repaintItem(_overLayout);
2123 	}
2124 	auto pressLayout = _overLayout;
2125 
2126 	_mouseAction = MouseAction::None;
2127 	_pressWasInactive = Ui::WasInactivePress(
2128 		_controller->parentController()->widget());
2129 	if (_pressWasInactive) {
2130 		Ui::MarkInactivePress(
2131 			_controller->parentController()->widget(),
2132 			false);
2133 	}
2134 
2135 	if (ClickHandler::getPressed() && !hasSelected()) {
2136 		_mouseAction = MouseAction::PrepareDrag;
2137 	} else if (hasSelectedItems()) {
2138 		if (isItemUnderPressSelected() && ClickHandler::getPressed()) {
2139 			// In shared media overview drag only by click handlers.
2140 			_mouseAction = MouseAction::PrepareDrag; // start items drag
2141 		} else if (!_pressWasInactive) {
2142 			_mouseAction = MouseAction::PrepareSelect; // start items select
2143 		}
2144 	}
2145 	if (_mouseAction == MouseAction::None && pressLayout) {
2146 		validateTrippleClickStartTime();
2147 		TextState dragState;
2148 		auto startDistance = (globalPosition - _trippleClickPoint).manhattanLength();
2149 		auto validStartPoint = startDistance < QApplication::startDragDistance();
2150 		if (_trippleClickStartTime != 0 && validStartPoint) {
2151 			StateRequest request;
2152 			request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
2153 			dragState = pressLayout->getState(_pressState.cursor, request);
2154 			if (dragState.cursor == CursorState::Text) {
2155 				TextSelection selStatus = { dragState.symbol, dragState.symbol };
2156 				if (selStatus != FullSelection && !hasSelectedItems()) {
2157 					clearSelected();
2158 					applyItemSelection(_pressState.itemId, selStatus);
2159 					_mouseTextSymbol = dragState.symbol;
2160 					_mouseAction = MouseAction::Selecting;
2161 					_mouseSelectType = TextSelectType::Paragraphs;
2162 					mouseActionUpdate(_mousePosition);
2163 					_trippleClickStartTime = crl::now();
2164 				}
2165 			}
2166 		} else {
2167 			StateRequest request;
2168 			request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
2169 			dragState = pressLayout->getState(_pressState.cursor, request);
2170 		}
2171 		if (_mouseSelectType != TextSelectType::Paragraphs) {
2172 			if (_pressState.inside) {
2173 				_mouseTextSymbol = dragState.symbol;
2174 				if (isPressInSelectedText(dragState)) {
2175 					_mouseAction = MouseAction::PrepareDrag; // start text drag
2176 				} else if (!_pressWasInactive) {
2177 					if (requiredToStartDragging(pressLayout)) {
2178 						_mouseAction = MouseAction::PrepareDrag;
2179 					} else {
2180 						if (dragState.afterSymbol) ++_mouseTextSymbol;
2181 						TextSelection selStatus = { _mouseTextSymbol, _mouseTextSymbol };
2182 						if (selStatus != FullSelection && !hasSelectedItems()) {
2183 							clearSelected();
2184 							applyItemSelection(_pressState.itemId, selStatus);
2185 							_mouseAction = MouseAction::Selecting;
2186 							repaintItem(pressLayout);
2187 						} else {
2188 							_mouseAction = MouseAction::PrepareSelect;
2189 						}
2190 					}
2191 				}
2192 			} else if (!_pressWasInactive) {
2193 				_mouseAction = MouseAction::PrepareSelect; // start items select
2194 			}
2195 		}
2196 	}
2197 
2198 	if (!pressLayout) {
2199 		_mouseAction = MouseAction::None;
2200 	} else if (_mouseAction == MouseAction::None) {
2201 		mouseActionCancel();
2202 	}
2203 }
2204 
mouseActionCancel()2205 void ListWidget::mouseActionCancel() {
2206 	_pressState = MouseState();
2207 	_mouseAction = MouseAction::None;
2208 	clearDragSelection();
2209 	_wasSelectedText = false;
2210 //	_widget->noSelectingScroll(); // #TODO scroll by drag
2211 }
2212 
performDrag()2213 void ListWidget::performDrag() {
2214 	if (_mouseAction != MouseAction::Dragging) return;
2215 
2216 	auto uponSelected = false;
2217 	if (_pressState.itemId && _pressState.inside) {
2218 		if (hasSelectedItems()) {
2219 			uponSelected = isItemUnderPressSelected();
2220 		} else if (auto pressLayout = getExistingLayout(
2221 				_pressState.itemId)) {
2222 			StateRequest request;
2223 			request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
2224 			auto dragState = pressLayout->getState(
2225 				_pressState.cursor,
2226 				request);
2227 			uponSelected = isPressInSelectedText(dragState);
2228 		}
2229 	}
2230 	auto pressedHandler = ClickHandler::getPressed();
2231 
2232 	if (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.get())) {
2233 		return;
2234 	}
2235 
2236 	TextWithEntities sel;
2237 	//QList<QUrl> urls;
2238 	if (uponSelected) {
2239 //		sel = getSelectedText();
2240 	} else if (pressedHandler) {
2241 		sel = { pressedHandler->dragText(), EntitiesInText() };
2242 		//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
2243 		//	urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
2244 		//}
2245 	}
2246 	//if (auto mimeData = MimeDataFromText(sel)) {
2247 	//	clearDragSelection();
2248 	//	_widget->noSelectingScroll();
2249 
2250 	//	if (!urls.isEmpty()) mimeData->setUrls(urls);
2251 	//	if (uponSelected && !Adaptive::OneColumn()) {
2252 	//		auto selectedState = getSelectionState();
2253 	//		if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
2254 	//			session().data().setMimeForwardIds(collectSelectedIds());
2255 	//			mimeData->setData(qsl("application/x-td-forward"), "1");
2256 	//		}
2257 	//	}
2258 	//	_controller->parentController()->window()->launchDrag(std::move(mimeData));
2259 	//	return;
2260 	//} else {
2261 	//	auto forwardMimeType = QString();
2262 	//	auto pressedMedia = static_cast<HistoryView::Media*>(nullptr);
2263 	//	if (auto pressedItem = _pressState.layout) {
2264 	//		pressedMedia = pressedItem->getMedia();
2265 	//		if (_mouseCursorState == CursorState::Date || (pressedMedia && pressedMedia->dragItem())) {
2266 	//			session().data().setMimeForwardIds(session().data().itemOrItsGroup(pressedItem));
2267 	//			forwardMimeType = qsl("application/x-td-forward");
2268 	//		}
2269 	//	}
2270 	//	if (auto pressedLnkItem = App::pressedLinkItem()) {
2271 	//		if ((pressedMedia = pressedLnkItem->getMedia())) {
2272 	//			if (forwardMimeType.isEmpty() && pressedMedia->dragItemByHandler(pressedHandler)) {
2273 	//				session().data().setMimeForwardIds({ 1, pressedLnkItem->fullId() });
2274 	//				forwardMimeType = qsl("application/x-td-forward");
2275 	//			}
2276 	//		}
2277 	//	}
2278 	//	if (!forwardMimeType.isEmpty()) {
2279 	//		auto mimeData = std::make_unique<QMimeData>();
2280 	//		mimeData->setData(forwardMimeType, "1");
2281 	//		if (auto document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) {
2282 	//			auto filepath = document->filepath(true);
2283 	//			if (!filepath.isEmpty()) {
2284 	//				QList<QUrl> urls;
2285 	//				urls.push_back(QUrl::fromLocalFile(filepath));
2286 	//				mimeData->setUrls(urls);
2287 	//			}
2288 	//		}
2289 
2290 	//		// This call enters event loop and can destroy any QObject.
2291 	//		_controller->parentController()->window()->launchDrag(std::move(mimeData));
2292 	//		return;
2293 	//	}
2294 	//}
2295 }
2296 
mouseActionFinish(const QPoint & globalPosition,Qt::MouseButton button)2297 void ListWidget::mouseActionFinish(
2298 		const QPoint &globalPosition,
2299 		Qt::MouseButton button) {
2300 	mouseActionUpdate(globalPosition);
2301 
2302 	auto pressState = base::take(_pressState);
2303 	repaintItem(pressState.itemId);
2304 
2305 	auto simpleSelectionChange = pressState.itemId
2306 		&& pressState.inside
2307 		&& !_pressWasInactive
2308 		&& (button != Qt::RightButton)
2309 		&& (_mouseAction == MouseAction::PrepareDrag
2310 			|| _mouseAction == MouseAction::PrepareSelect);
2311 	auto needSelectionToggle = simpleSelectionChange
2312 		&& hasSelectedItems();
2313 	auto needSelectionClear = simpleSelectionChange
2314 		&& hasSelectedText();
2315 
2316 	auto activated = ClickHandler::unpressed();
2317 	if (_mouseAction == MouseAction::Dragging
2318 		|| _mouseAction == MouseAction::Selecting) {
2319 		activated = nullptr;
2320 	} else if (needSelectionToggle) {
2321 		activated = nullptr;
2322 	}
2323 
2324 	_wasSelectedText = false;
2325 	if (activated) {
2326 		mouseActionCancel();
2327 		const auto found = findItemById(pressState.itemId);
2328 		const auto fullId = found
2329 			? found->layout->getItem()->fullId()
2330 			: FullMsgId();
2331 		ActivateClickHandler(window(), activated, {
2332 			button,
2333 			QVariant::fromValue(ClickHandlerContext{
2334 				.itemId = fullId,
2335 				.sessionWindow = base::make_weak(
2336 					_controller->parentController().get()),
2337 			})
2338 		});
2339 		return;
2340 	}
2341 
2342 	if (needSelectionToggle) {
2343 		toggleItemSelection(pressState.itemId);
2344 	} else if (needSelectionClear) {
2345 		clearSelected();
2346 	} else if (_mouseAction == MouseAction::Selecting) {
2347 		if (!_dragSelected.empty()) {
2348 			applyDragSelection();
2349 		} else if (!_selected.empty() && !_pressWasInactive) {
2350 			auto selection = _selected.cbegin()->second;
2351 			if (selection.text != FullSelection
2352 				&& selection.text.from == selection.text.to) {
2353 				clearSelected();
2354 				//_controller->parentController()->window()->setInnerFocus(); // #TODO focus
2355 			}
2356 		}
2357 	}
2358 	_mouseAction = MouseAction::None;
2359 	_mouseSelectType = TextSelectType::Letters;
2360 	//_widget->noSelectingScroll(); // #TODO scroll by drag
2361 	//_widget->updateTopBarSelection();
2362 
2363 	//if (QGuiApplication::clipboard()->supportsSelection() && hasSelectedText()) { // #TODO linux clipboard
2364 	//	TextUtilities::SetClipboardText(_selected.cbegin()->first->selectedText(_selected.cbegin()->second), QClipboard::Selection);
2365 	//}
2366 }
2367 
applyDragSelection()2368 void ListWidget::applyDragSelection() {
2369 	applyDragSelection(_selected);
2370 	clearDragSelection();
2371 	pushSelectedItems();
2372 }
2373 
applyDragSelection(SelectedMap & applyTo) const2374 void ListWidget::applyDragSelection(SelectedMap &applyTo) const {
2375 	if (_dragSelectAction == DragSelectAction::Selecting) {
2376 		for (auto &[universalId,data] : _dragSelected) {
2377 			changeItemSelection(applyTo, universalId, FullSelection);
2378 		}
2379 	} else if (_dragSelectAction == DragSelectAction::Deselecting) {
2380 		for (auto &[universalId,data] : _dragSelected) {
2381 			applyTo.remove(universalId);
2382 		}
2383 	}
2384 }
2385 
refreshHeight()2386 void ListWidget::refreshHeight() {
2387 	resize(width(), recountHeight());
2388 }
2389 
recountHeight()2390 int ListWidget::recountHeight() {
2391 	if (_sections.empty()) {
2392 		if (auto count = _slice.fullCount()) {
2393 			if (*count == 0) {
2394 				return 0;
2395 			}
2396 		}
2397 	}
2398 	auto cachedPadding = padding();
2399 	auto result = cachedPadding.top();
2400 	for (auto &section : _sections) {
2401 		section.setTop(result);
2402 		result += section.height();
2403 	}
2404 	return result + cachedPadding.bottom();
2405 }
2406 
mouseActionUpdate()2407 void ListWidget::mouseActionUpdate() {
2408 	mouseActionUpdate(_mousePosition);
2409 }
2410 
clearStaleLayouts()2411 void ListWidget::clearStaleLayouts() {
2412 	for (auto i = _layouts.begin(); i != _layouts.end();) {
2413 		if (i->second.stale) {
2414 			if (i->second.item.get() == _overLayout) {
2415 				_overLayout = nullptr;
2416 			}
2417 			_heavyLayouts.erase(i->second.item.get());
2418 			i = _layouts.erase(i);
2419 		} else {
2420 			++i;
2421 		}
2422 	}
2423 }
2424 
findSectionByItem(UniversalMsgId universalId)2425 auto ListWidget::findSectionByItem(
2426 		UniversalMsgId universalId) -> std::vector<Section>::iterator {
2427 	return ranges::lower_bound(
2428 		_sections,
2429 		universalId,
2430 		std::greater<>(),
2431 		[](const Section &section) { return section.minId(); });
2432 }
2433 
findSectionAfterTop(int top)2434 auto ListWidget::findSectionAfterTop(
2435 		int top) -> std::vector<Section>::iterator {
2436 	return ranges::lower_bound(
2437 		_sections,
2438 		top,
2439 		std::less_equal<>(),
2440 		[](const Section &section) { return section.bottom(); });
2441 }
2442 
findSectionAfterTop(int top) const2443 auto ListWidget::findSectionAfterTop(
2444 		int top) const -> std::vector<Section>::const_iterator {
2445 	return ranges::lower_bound(
2446 		_sections,
2447 		top,
2448 		std::less_equal<>(),
2449 		[](const Section &section) { return section.bottom(); });
2450 }
2451 
findSectionAfterBottom(std::vector<Section>::const_iterator from,int bottom) const2452 auto ListWidget::findSectionAfterBottom(
2453 		std::vector<Section>::const_iterator from,
2454 		int bottom) const -> std::vector<Section>::const_iterator {
2455 	return ranges::lower_bound(
2456 		from,
2457 		_sections.end(),
2458 		bottom,
2459 		std::less<>(),
2460 		[](const Section &section) { return section.top(); });
2461 }
2462 
~ListWidget()2463 ListWidget::~ListWidget() {
2464 	if (_contextMenu) {
2465 		// We don't want it to be called after ListWidget is destroyed.
2466 		_contextMenu->setDestroyedCallback(nullptr);
2467 	}
2468 }
2469 
2470 } // namespace Media
2471 } // namespace Info
2472