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 "history/view/history_view_list_widget.h"
9 
10 #include "base/unixtime.h"
11 #include "history/history_message.h"
12 #include "history/history_item_components.h"
13 #include "history/history_item_text.h"
14 #include "history/view/media/history_view_media.h"
15 #include "history/view/media/history_view_sticker.h"
16 #include "history/view/history_view_context_menu.h"
17 #include "history/view/history_view_element.h"
18 #include "history/view/history_view_message.h"
19 #include "history/view/history_view_service_message.h"
20 #include "history/view/history_view_cursor_state.h"
21 #include "chat_helpers/message_field.h"
22 #include "mainwindow.h"
23 #include "mainwidget.h"
24 #include "core/click_handler_types.h"
25 #include "apiwrap.h"
26 #include "layout/layout_selection.h"
27 #include "window/window_adaptive.h"
28 #include "window/window_session_controller.h"
29 #include "window/window_peer_menu.h"
30 #include "main/main_session.h"
31 #include "ui/widgets/popup_menu.h"
32 #include "ui/toast/toast.h"
33 #include "ui/inactive_press.h"
34 #include "ui/effects/path_shift_gradient.h"
35 #include "ui/chat/chat_theme.h"
36 #include "ui/chat/chat_style.h"
37 #include "lang/lang_keys.h"
38 #include "boxes/delete_messages_box.h"
39 #include "boxes/peers/edit_participant_box.h"
40 #include "data/data_session.h"
41 #include "data/data_folder.h"
42 #include "data/data_media_types.h"
43 #include "data/data_document.h"
44 #include "data/data_peer.h"
45 #include "data/data_user.h"
46 #include "data/data_chat.h"
47 #include "data/data_channel.h"
48 #include "data/data_file_click_handler.h"
49 #include "facades.h"
50 #include "styles/style_chat.h"
51 
52 #include <QtWidgets/QApplication>
53 #include <QtCore/QMimeData>
54 
55 namespace HistoryView {
56 namespace {
57 
58 constexpr auto kPreloadedScreensCount = 4;
59 constexpr auto kPreloadIfLessThanScreens = 2;
60 constexpr auto kPreloadedScreensCountFull
61 	= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
62 constexpr auto kClearUserpicsAfter = 50;
63 
64 } // namespace
65 
MouseState()66 ListWidget::MouseState::MouseState() : pointState(PointState::Outside) {
67 }
68 
MouseState(FullMsgId itemId,int height,QPoint point,PointState pointState)69 ListWidget::MouseState::MouseState(
70 	FullMsgId itemId,
71 	int height,
72 	QPoint point,
73 	PointState pointState)
74 : itemId(itemId)
75 , height(height)
76 , point(point)
77 , pointState(pointState) {
78 }
79 
80 const crl::time ListWidget::kItemRevealDuration = crl::time(150);
81 
82 template <ListWidget::EnumItemsDirection direction, typename Method>
enumerateItems(Method method)83 void ListWidget::enumerateItems(Method method) {
84 	constexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom);
85 
86 	// No displayed messages in this history.
87 	if (_items.empty()) {
88 		return;
89 	}
90 	if (_visibleBottom <= _itemsTop || _itemsTop + _itemsHeight <= _visibleTop) {
91 		return;
92 	}
93 
94 	const auto beginning = begin(_items);
95 	const auto ending = end(_items);
96 	auto from = TopToBottom
97 		? std::lower_bound(
98 			beginning,
99 			ending,
100 			_visibleTop,
101 			[this](auto &elem, int top) {
102 				return this->itemTop(elem) + elem->height() <= top;
103 			})
104 		: std::upper_bound(
105 			beginning,
106 			ending,
107 			_visibleBottom,
108 			[this](int bottom, auto &elem) {
109 				return this->itemTop(elem) + elem->height() >= bottom;
110 			});
111 	auto wasEnd = (from == ending);
112 	if (wasEnd) {
113 		--from;
114 	}
115 	if (TopToBottom) {
116 		Assert(itemTop(from->get()) + from->get()->height() > _visibleTop);
117 	} else {
118 		Assert(itemTop(from->get()) < _visibleBottom);
119 	}
120 
121 	while (true) {
122 		auto view = from->get();
123 		auto itemtop = itemTop(view);
124 		auto itembottom = itemtop + view->height();
125 
126 		// Binary search should've skipped all the items that are above / below the visible area.
127 		if (TopToBottom) {
128 			Assert(itembottom > _visibleTop);
129 		} else {
130 			Assert(itemtop < _visibleBottom);
131 		}
132 
133 		if (!method(view, itemtop, itembottom)) {
134 			return;
135 		}
136 
137 		// Skip all the items that are below / above the visible area.
138 		if (TopToBottom) {
139 			if (itembottom >= _visibleBottom) {
140 				return;
141 			}
142 		} else {
143 			if (itemtop <= _visibleTop) {
144 				return;
145 			}
146 		}
147 
148 		if (TopToBottom) {
149 			if (++from == ending) {
150 				break;
151 			}
152 		} else {
153 			if (from == beginning) {
154 				break;
155 			}
156 			--from;
157 		}
158 	}
159 }
160 
161 template <typename Method>
enumerateUserpics(Method method)162 void ListWidget::enumerateUserpics(Method method) {
163 	// Find and remember the top of an attached messages pack
164 	// -1 means we didn't find an attached to next message yet.
165 	int lowestAttachedItemTop = -1;
166 
167 	auto userpicCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {
168 		// Skip all service messages.
169 		if (view->data()->isService()) {
170 			return true;
171 		}
172 
173 		if (lowestAttachedItemTop < 0 && view->isAttachedToNext()) {
174 			lowestAttachedItemTop = itemtop + view->marginTop();
175 		}
176 
177 		// Call method on a userpic for all messages that have it and for those who are not showing it
178 		// because of their attachment to the next message if they are bottom-most visible.
179 		if (view->displayFromPhoto() || (view->hasFromPhoto() && itembottom >= _visibleBottom)) {
180 			if (lowestAttachedItemTop < 0) {
181 				lowestAttachedItemTop = itemtop + view->marginTop();
182 			}
183 			// Attach userpic to the bottom of the visible area with the same margin as the last message.
184 			auto userpicMinBottomSkip = st::historyPaddingBottom + st::msgMargin.bottom();
185 			auto userpicBottom = qMin(itembottom - view->marginBottom(), _visibleBottom - userpicMinBottomSkip);
186 
187 			// Do not let the userpic go above the attached messages pack top line.
188 			userpicBottom = qMax(userpicBottom, lowestAttachedItemTop + st::msgPhotoSize);
189 
190 			// Call the template callback function that was passed
191 			// and return if it finished everything it needed.
192 			if (!method(view, userpicBottom - st::msgPhotoSize)) {
193 				return false;
194 			}
195 		}
196 
197 		// Forget the found top of the pack, search for the next one from scratch.
198 		if (!view->isAttachedToNext()) {
199 			lowestAttachedItemTop = -1;
200 		}
201 
202 		return true;
203 	};
204 
205 	enumerateItems<EnumItemsDirection::TopToBottom>(userpicCallback);
206 }
207 
208 template <typename Method>
enumerateDates(Method method)209 void ListWidget::enumerateDates(Method method) {
210 	// Find and remember the bottom of an single-day messages pack
211 	// -1 means we didn't find a same-day with previous message yet.
212 	auto lowestInOneDayItemBottom = -1;
213 
214 	auto dateCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {
215 		const auto item = view->data();
216 		if (lowestInOneDayItemBottom < 0 && view->isInOneDayWithPrevious()) {
217 			lowestInOneDayItemBottom = itembottom - view->marginBottom();
218 		}
219 
220 		// Call method on a date for all messages that have it and for those who are not showing it
221 		// because they are in a one day together with the previous message if they are top-most visible.
222 		if (view->displayDate() || (!item->isEmpty() && itemtop <= _visibleTop)) {
223 			if (lowestInOneDayItemBottom < 0) {
224 				lowestInOneDayItemBottom = itembottom - view->marginBottom();
225 			}
226 			// Attach date to the top of the visible area with the same margin as it has in service message.
227 			auto dateTop = qMax(itemtop, _visibleTop) + st::msgServiceMargin.top();
228 
229 			// Do not let the date go below the single-day messages pack bottom line.
230 			auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
231 			dateTop = qMin(dateTop, lowestInOneDayItemBottom - dateHeight);
232 
233 			// Call the template callback function that was passed
234 			// and return if it finished everything it needed.
235 			if (!method(view, itemtop, dateTop)) {
236 				return false;
237 			}
238 		}
239 
240 		// Forget the found bottom of the pack, search for the next one from scratch.
241 		if (!view->isInOneDayWithPrevious()) {
242 			lowestInOneDayItemBottom = -1;
243 		}
244 
245 		return true;
246 	};
247 
248 	enumerateItems<EnumItemsDirection::BottomToTop>(dateCallback);
249 }
250 
ListWidget(QWidget * parent,not_null<Window::SessionController * > controller,not_null<ListDelegate * > delegate)251 ListWidget::ListWidget(
252 	QWidget *parent,
253 	not_null<Window::SessionController*> controller,
254 	not_null<ListDelegate*> delegate)
255 : RpWidget(parent)
256 , _delegate(delegate)
257 , _controller(controller)
258 , _context(_delegate->listContext())
259 , _itemAverageHeight(itemMinimalHeight())
260 , _pathGradient(
261 	MakePathShiftGradient(
262 		controller->chatStyle(),
263 		[=] { update(); }))
__anon8ca1cd430702null264 , _scrollDateCheck([this] { scrollDateCheck(); })
__anon8ca1cd430802null265 , _applyUpdatedScrollState([this] { applyUpdatedScrollState(); })
266 , _selectEnabled(_delegate->listAllowsMultiSelect())
__anon8ca1cd430902null267 , _highlightTimer([this] { updateHighlightedMessage(); }) {
268 	setMouseTracking(true);
__anon8ca1cd430a02null269 	_scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); });
270 	session().data().viewRepaintRequest(
__anon8ca1cd430b02(auto view) 271 	) | rpl::start_with_next([this](auto view) {
272 		if (view->delegate() == this) {
273 			repaintItem(view);
274 		}
275 	}, lifetime());
276 	session().data().viewResizeRequest(
__anon8ca1cd430c02(auto view) 277 	) | rpl::start_with_next([this](auto view) {
278 		if (view->delegate() == this) {
279 			resizeItem(view);
280 		}
281 	}, lifetime());
282 	session().data().itemViewRefreshRequest(
__anon8ca1cd430d02(auto item) 283 	) | rpl::start_with_next([this](auto item) {
284 		if (const auto view = viewForItem(item)) {
285 			refreshItem(view);
286 		}
287 	}, lifetime());
288 	session().data().viewLayoutChanged(
__anon8ca1cd430e02(auto view) 289 	) | rpl::start_with_next([this](auto view) {
290 		if (view->delegate() == this) {
291 			if (view->isUnderCursor()) {
292 				mouseActionUpdate();
293 			}
294 		}
295 	}, lifetime());
296 	session().data().animationPlayInlineRequest(
__anon8ca1cd430f02(auto item) 297 	) | rpl::start_with_next([this](auto item) {
298 		if (const auto view = viewForItem(item)) {
299 			if (const auto media = view->media()) {
300 				media->playAnimation();
301 			}
302 		}
303 	}, lifetime());
304 
305 	session().downloaderTaskFinished(
__anon8ca1cd431002null306 	) | rpl::start_with_next([=] {
307 		update();
308 	}, lifetime());
309 
310 	session().data().itemRemoved(
__anon8ca1cd431102(not_null<const HistoryItem*> item) 311 	) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
312 		itemRemoved(item);
313 	}, lifetime());
314 
__anon8ca1cd431202(const Data::Session::ItemVisibilityQuery &query) 315 	subscribe(session().data().queryItemVisibility(), [this](const Data::Session::ItemVisibilityQuery &query) {
316 		if (const auto view = viewForItem(query.item)) {
317 			const auto top = itemTop(view);
318 			if (top >= 0
319 				&& top + view->height() > _visibleTop
320 				&& top < _visibleBottom) {
321 				*query.isVisible = true;
322 			}
323 		}
324 	});
325 
326 	controller->adaptive().chatWideValue(
__anon8ca1cd431302(bool wide) 327 	) | rpl::start_with_next([=](bool wide) {
328 		_isChatWide = wide;
329 	}, lifetime());
330 
331 	_selectScroll.scrolls(
__anon8ca1cd431402(int d) 332 	) | rpl::start_with_next([=](int d) {
333 		delegate->listScrollTo(_visibleTop + d);
334 	}, lifetime());
335 }
336 
session() const337 Main::Session &ListWidget::session() const {
338 	return _controller->session();
339 }
340 
controller() const341 not_null<Window::SessionController*> ListWidget::controller() const {
342 	return _controller;
343 }
344 
delegate() const345 not_null<ListDelegate*> ListWidget::delegate() const {
346 	return _delegate;
347 }
348 
refreshViewer()349 void ListWidget::refreshViewer() {
350 	_viewerLifetime.destroy();
351 	_delegate->listSource(
352 		_aroundPosition,
353 		_idsLimit,
354 		_idsLimit
355 	) | rpl::start_with_next([=](Data::MessagesSlice &&slice) {
356 		std::swap(_slice, slice);
357 		refreshRows(slice);
358 	}, _viewerLifetime);
359 }
360 
refreshRows(const Data::MessagesSlice & old)361 void ListWidget::refreshRows(const Data::MessagesSlice &old) {
362 	saveScrollState();
363 
364 	const auto addedToEndFrom = (old.skippedAfter == 0
365 		&& (_slice.skippedAfter == 0)
366 		&& !old.ids.empty())
367 		? ranges::find(_slice.ids, old.ids.back())
368 		: end(_slice.ids);
369 	const auto addedToEndCount = std::max(
370 		int(end(_slice.ids) - addedToEndFrom),
371 		1
372 	) - 1;
373 
374 	_items.clear();
375 	_items.reserve(_slice.ids.size());
376 	auto nearestIndex = -1;
377 	for (const auto &fullId : _slice.ids) {
378 		if (const auto item = session().data().message(fullId)) {
379 			if (_slice.nearestToAround == fullId) {
380 				nearestIndex = int(_items.size());
381 			}
382 			_items.push_back(enforceViewForItem(item));
383 		}
384 	}
385 	for (auto e = end(_items), i = e - addedToEndCount; i != e; ++i) {
386 		_itemRevealPending.emplace(*i);
387 	}
388 	updateAroundPositionFromNearest(nearestIndex);
389 
390 	updateItemsGeometry();
391 	checkUnreadBarCreation();
392 	restoreScrollState();
393 	if (!_itemsRevealHeight) {
394 		mouseActionUpdate(QCursor::pos());
395 	}
396 	if (_emptyInfo) {
397 		_emptyInfo->setVisible(isEmpty());
398 	}
399 	_delegate->listContentRefreshed();
400 }
401 
scrollTopForPosition(Data::MessagePosition position) const402 std::optional<int> ListWidget::scrollTopForPosition(
403 		Data::MessagePosition position) const {
404 	if (position == Data::MaxMessagePosition) {
405 		if (loadedAtBottom()) {
406 			return height();
407 		}
408 		return std::nullopt;
409 	} else if (_items.empty()
410 		|| isBelowPosition(position)
411 		|| isAbovePosition(position)) {
412 		return std::nullopt;
413 	}
414 	const auto index = findNearestItem(position);
415 	const auto view = _items[index];
416 	return scrollTopForView(view);
417 }
418 
scrollTopForView(not_null<Element * > view) const419 std::optional<int> ListWidget::scrollTopForView(
420 		not_null<Element*> view) const {
421 	if (view->isHiddenByGroup()) {
422 		if (const auto group = session().data().groups().find(view->data())) {
423 			if (const auto leader = viewForItem(group->items.front())) {
424 				if (!leader->isHiddenByGroup()) {
425 					return scrollTopForView(leader);
426 				}
427 			}
428 		}
429 	}
430 	const auto top = view->y();
431 	const auto height = view->height();
432 	const auto available = _visibleBottom - _visibleTop;
433 	return top - std::max((available - height) / 2, 0);
434 }
435 
scrollTo(int scrollTop,Data::MessagePosition attachPosition,int delta,AnimatedScroll type)436 void ListWidget::scrollTo(
437 		int scrollTop,
438 		Data::MessagePosition attachPosition,
439 		int delta,
440 		AnimatedScroll type) {
441 	_scrollToAnimation.stop();
442 	if (!delta || _items.empty() || type == AnimatedScroll::None) {
443 		_delegate->listScrollTo(scrollTop);
444 		return;
445 	}
446 	const auto transition = (type == AnimatedScroll::Full)
447 		? anim::sineInOut
448 		: anim::easeOutCubic;
449 	if (delta > 0 && scrollTop == height() - (_visibleBottom - _visibleTop)) {
450 		// Animated scroll to bottom.
451 		_scrollToAnimation.start(
452 			[=] { scrollToAnimationCallback(FullMsgId(), 0); },
453 			-delta,
454 			0,
455 			st::slideDuration,
456 			transition);
457 		return;
458 	}
459 	const auto index = findNearestItem(attachPosition);
460 	Assert(index >= 0 && index < int(_items.size()));
461 	const auto attachTo = _items[index];
462 	const auto attachToId = attachTo->data()->fullId();
463 	const auto initial = scrollTop - delta;
464 	_delegate->listScrollTo(initial);
465 
466 	const auto attachToTop = itemTop(attachTo);
467 	const auto relativeStart = initial - attachToTop;
468 	const auto relativeFinish = scrollTop - attachToTop;
469 	_scrollToAnimation.start(
470 		[=] { scrollToAnimationCallback(attachToId, relativeFinish); },
471 		relativeStart,
472 		relativeFinish,
473 		st::slideDuration,
474 		transition);
475 }
476 
animatedScrolling() const477 bool ListWidget::animatedScrolling() const {
478 	return _scrollToAnimation.animating();
479 }
480 
scrollToAnimationCallback(FullMsgId attachToId,int relativeTo)481 void ListWidget::scrollToAnimationCallback(
482 		FullMsgId attachToId,
483 		int relativeTo) {
484 	if (!attachToId) {
485 		// Animated scroll to bottom.
486 		const auto current = int(base::SafeRound(
487 			_scrollToAnimation.value(0)));
488 		_delegate->listScrollTo(height()
489 			- (_visibleBottom - _visibleTop)
490 			+ current);
491 		return;
492 	}
493 	const auto attachTo = session().data().message(attachToId);
494 	const auto attachToView = viewForItem(attachTo);
495 	if (!attachToView) {
496 		_scrollToAnimation.stop();
497 	} else {
498 		const auto current = int(base::SafeRound(_scrollToAnimation.value(
499 			relativeTo)));
500 		_delegate->listScrollTo(itemTop(attachToView) + current);
501 	}
502 }
503 
isAbovePosition(Data::MessagePosition position) const504 bool ListWidget::isAbovePosition(Data::MessagePosition position) const {
505 	if (_items.empty() || loadedAtBottom()) {
506 		return false;
507 	}
508 	return _items.back()->data()->position() < position;
509 }
510 
isBelowPosition(Data::MessagePosition position) const511 bool ListWidget::isBelowPosition(Data::MessagePosition position) const {
512 	if (_items.empty() || loadedAtTop()) {
513 		return false;
514 	}
515 	return _items.front()->data()->position() > position;
516 }
517 
highlightMessage(FullMsgId itemId)518 void ListWidget::highlightMessage(FullMsgId itemId) {
519 	if (const auto item = session().data().message(itemId)) {
520 		if (const auto view = viewForItem(item)) {
521 			_highlightStart = crl::now();
522 			_highlightedMessageId = itemId;
523 			_highlightTimer.callEach(AnimationTimerDelta);
524 
525 			repaintHighlightedItem(view);
526 		}
527 	}
528 }
529 
showAroundPosition(Data::MessagePosition position,Fn<bool ()> overrideInitialScroll)530 void ListWidget::showAroundPosition(
531 		Data::MessagePosition position,
532 		Fn<bool()> overrideInitialScroll) {
533 	_aroundPosition = position;
534 	_aroundIndex = -1;
535 	_overrideInitialScroll = std::move(overrideInitialScroll);
536 	refreshViewer();
537 }
538 
repaintHighlightedItem(not_null<const Element * > view)539 void ListWidget::repaintHighlightedItem(not_null<const Element*> view) {
540 	if (view->isHiddenByGroup()) {
541 		if (const auto group = session().data().groups().find(view->data())) {
542 			if (const auto leader = viewForItem(group->items.front())) {
543 				if (!leader->isHiddenByGroup()) {
544 					repaintItem(leader);
545 					return;
546 				}
547 			}
548 		}
549 	}
550 	repaintItem(view);
551 }
552 
updateHighlightedMessage()553 void ListWidget::updateHighlightedMessage() {
554 	if (const auto item = session().data().message(_highlightedMessageId)) {
555 		if (const auto view = viewForItem(item)) {
556 			repaintHighlightedItem(view);
557 			auto duration = st::activeFadeInDuration + st::activeFadeOutDuration;
558 			if (crl::now() - _highlightStart <= duration) {
559 				return;
560 			}
561 		}
562 	}
563 	_highlightTimer.cancel();
564 	_highlightedMessageId = FullMsgId();
565 }
566 
clearHighlightedMessage()567 void ListWidget::clearHighlightedMessage() {
568 	_highlightedMessageId = FullMsgId();
569 	updateHighlightedMessage();
570 }
571 
checkUnreadBarCreation()572 void ListWidget::checkUnreadBarCreation() {
573 	if (!_bar.element) {
574 		if (auto data = _delegate->listMessagesBar(_items); data.bar.element) {
575 			_bar = std::move(data.bar);
576 			_barText = std::move(data.text);
577 			if (!_bar.hidden) {
578 				_bar.element->createUnreadBar(_barText.value());
579 				const auto i = ranges::find(_items, not_null{ _bar.element });
580 				Assert(i != end(_items));
581 				refreshAttachmentsAtIndex(i - begin(_items));
582 			}
583 		}
584 	}
585 }
586 
saveScrollState()587 void ListWidget::saveScrollState() {
588 	if (!_scrollTopState.item) {
589 		_scrollTopState = countScrollState();
590 	}
591 }
592 
restoreScrollState()593 void ListWidget::restoreScrollState() {
594 	if (_items.empty()) {
595 		return;
596 	} else if (_overrideInitialScroll
597 		&& base::take(_overrideInitialScroll)()) {
598 		_scrollTopState = ScrollTopState();
599 		_scrollInited = true;
600 		return;
601 	}
602 	if (!_scrollTopState.item) {
603 		if (!_bar.element || _bar.hidden || !_bar.focus || _scrollInited) {
604 			return;
605 		}
606 		_scrollInited = true;
607 		_scrollTopState.item = _bar.element->data()->position();
608 		_scrollTopState.shift = st::lineWidth + st::historyUnreadBarMargin;
609 	}
610 	const auto index = findNearestItem(_scrollTopState.item);
611 	if (index >= 0) {
612 		const auto view = _items[index];
613 		auto newVisibleTop = itemTop(view) + _scrollTopState.shift;
614 		if (_visibleTop != newVisibleTop) {
615 			_delegate->listScrollTo(newVisibleTop);
616 		}
617 	}
618 	_scrollTopState = ScrollTopState();
619 }
620 
viewForItem(FullMsgId itemId) const621 Element *ListWidget::viewForItem(FullMsgId itemId) const {
622 	if (const auto item = session().data().message(itemId)) {
623 		return viewForItem(item);
624 	}
625 	return nullptr;
626 }
627 
viewForItem(const HistoryItem * item) const628 Element *ListWidget::viewForItem(const HistoryItem *item) const {
629 	if (item) {
630 		if (const auto i = _views.find(item); i != _views.end()) {
631 			return i->second.get();
632 		}
633 	}
634 	return nullptr;
635 }
636 
enforceViewForItem(not_null<HistoryItem * > item)637 not_null<Element*> ListWidget::enforceViewForItem(
638 		not_null<HistoryItem*> item) {
639 	if (const auto view = viewForItem(item)) {
640 		return view;
641 	}
642 	const auto [i, ok] = _views.emplace(
643 		item,
644 		item->createView(this));
645 	return i->second.get();
646 }
647 
updateAroundPositionFromNearest(int nearestIndex)648 void ListWidget::updateAroundPositionFromNearest(int nearestIndex) {
649 	if (nearestIndex < 0) {
650 		_aroundIndex = -1;
651 		return;
652 	}
653 	const auto isGoodIndex = [&](int index) {
654 		Expects(index >= 0 && index < _items.size());
655 
656 		return _delegate->listIsGoodForAroundPosition(_items[index]);
657 	};
658 	_aroundIndex = [&] {
659 		for (auto index = nearestIndex; index < _items.size(); ++index) {
660 			if (isGoodIndex(index)) {
661 				return index;
662 			}
663 		}
664 		for (auto index = nearestIndex; index != 0;) {
665 			if (isGoodIndex(--index)) {
666 				return index;
667 			}
668 		}
669 		return -1;
670 	}();
671 	if (_aroundIndex < 0) {
672 		return;
673 	}
674 	const auto newPosition = _items[_aroundIndex]->data()->position();
675 	if (_aroundPosition != newPosition) {
676 		_aroundPosition = newPosition;
677 		crl::on_main(this, [=] { refreshViewer(); });
678 	}
679 }
680 
viewByPosition(Data::MessagePosition position) const681 Element *ListWidget::viewByPosition(Data::MessagePosition position) const {
682 	const auto index = findNearestItem(position);
683 	return (index < 0 || _items[index]->data()->position() != position)
684 		? nullptr
685 		: _items[index].get();
686 }
687 
findNearestItem(Data::MessagePosition position) const688 int ListWidget::findNearestItem(Data::MessagePosition position) const {
689 	if (_items.empty()) {
690 		return -1;
691 	}
692 	const auto after = ranges::find_if(
693 		_items,
694 		[&](not_null<Element*> view) {
695 			return (view->data()->position() >= position);
696 		});
697 	return (after == end(_items))
698 		? int(_items.size() - 1)
699 		: int(after - begin(_items));
700 }
701 
collectVisibleItems() const702 HistoryItemsList ListWidget::collectVisibleItems() const {
703 	auto result = HistoryItemsList();
704 	const auto from = std::lower_bound(
705 		begin(_items),
706 		end(_items),
707 		_visibleTop,
708 		[this](auto &elem, int top) {
709 			return this->itemTop(elem) + elem->height() <= top;
710 		});
711 	const auto to = std::lower_bound(
712 		begin(_items),
713 		end(_items),
714 		_visibleBottom,
715 		[this](auto &elem, int bottom) {
716 			return this->itemTop(elem) < bottom;
717 		});
718 	result.reserve(to - from);
719 	for (auto i = from; i != to; ++i) {
720 		result.push_back((*i)->data());
721 	}
722 	return result;
723 }
724 
visibleTopBottomUpdated(int visibleTop,int visibleBottom)725 void ListWidget::visibleTopBottomUpdated(
726 		int visibleTop,
727 		int visibleBottom) {
728 	if (!(visibleTop < visibleBottom)) {
729 		return;
730 	}
731 
732 	const auto initializing = !(_visibleTop < _visibleBottom);
733 	const auto scrolledUp = (visibleTop < _visibleTop);
734 	_visibleTop = visibleTop;
735 	_visibleBottom = visibleBottom;
736 
737 	// Unload userpics.
738 	if (_userpics.size() > kClearUserpicsAfter) {
739 		_userpicsCache = std::move(_userpics);
740 	}
741 
742 	if (initializing) {
743 		checkUnreadBarCreation();
744 	}
745 	updateVisibleTopItem();
746 	if (scrolledUp) {
747 		_scrollDateCheck.call();
748 	} else {
749 		scrollDateHideByTimer();
750 	}
751 	_controller->floatPlayerAreaUpdated();
752 	_applyUpdatedScrollState.call();
753 }
754 
applyUpdatedScrollState()755 void ListWidget::applyUpdatedScrollState() {
756 	checkMoveToOtherViewer();
757 	_delegate->listVisibleItemsChanged(collectVisibleItems());
758 }
759 
updateVisibleTopItem()760 void ListWidget::updateVisibleTopItem() {
761 	if (_visibleBottom == height()) {
762 		_visibleTopItem = nullptr;
763 	} else if (_items.empty()) {
764 		_visibleTopItem = nullptr;
765 		_visibleTopFromItem = _visibleTop;
766 	} else {
767 		_visibleTopItem = findItemByY(_visibleTop);
768 		_visibleTopFromItem = _visibleTop - itemTop(_visibleTopItem);
769 	}
770 }
771 
displayScrollDate() const772 bool ListWidget::displayScrollDate() const {
773 	return (_visibleTop <= height() - 2 * (_visibleBottom - _visibleTop));
774 }
775 
scrollDateCheck()776 void ListWidget::scrollDateCheck() {
777 	if (!_visibleTopItem) {
778 		_scrollDateLastItem = nullptr;
779 		_scrollDateLastItemTop = 0;
780 		scrollDateHide();
781 	} else if (_visibleTopItem != _scrollDateLastItem || _visibleTopFromItem != _scrollDateLastItemTop) {
782 		// Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem).
783 		if (_scrollDateLastItem && !_scrollDateShown) {
784 			toggleScrollDateShown();
785 		}
786 		_scrollDateLastItem = _visibleTopItem;
787 		_scrollDateLastItemTop = _visibleTopFromItem;
788 		_scrollDateHideTimer.callOnce(st::historyScrollDateHideTimeout);
789 	}
790 }
791 
scrollDateHideByTimer()792 void ListWidget::scrollDateHideByTimer() {
793 	_scrollDateHideTimer.cancel();
794 	if (!_scrollDateLink || ClickHandler::getPressed() != _scrollDateLink) {
795 		scrollDateHide();
796 	}
797 }
798 
scrollDateHide()799 void ListWidget::scrollDateHide() {
800 	if (_scrollDateShown) {
801 		toggleScrollDateShown();
802 	}
803 }
804 
keepScrollDateForNow()805 void ListWidget::keepScrollDateForNow() {
806 	if (!_scrollDateShown
807 		&& _scrollDateLastItem
808 		&& _scrollDateOpacity.animating()) {
809 		toggleScrollDateShown();
810 	}
811 	_scrollDateHideTimer.callOnce(st::historyScrollDateHideTimeout);
812 }
813 
toggleScrollDateShown()814 void ListWidget::toggleScrollDateShown() {
815 	_scrollDateShown = !_scrollDateShown;
816 	auto from = _scrollDateShown ? 0. : 1.;
817 	auto to = _scrollDateShown ? 1. : 0.;
818 	_scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration);
819 }
820 
repaintScrollDateCallback()821 void ListWidget::repaintScrollDateCallback() {
822 	auto updateTop = _visibleTop;
823 	auto updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();
824 	update(0, updateTop, width(), updateHeight);
825 }
826 
collectSelectedItems() const827 auto ListWidget::collectSelectedItems() const -> SelectedItems {
828 	auto transformation = [&](const auto &item) {
829 		const auto [itemId, selection] = item;
830 		auto result = SelectedItem(itemId);
831 		result.canDelete = selection.canDelete;
832 		result.canForward = selection.canForward;
833 		result.canSendNow = selection.canSendNow;
834 		return result;
835 	};
836 	auto items = SelectedItems();
837 	if (hasSelectedItems()) {
838 		items.reserve(_selected.size());
839 		std::transform(
840 			_selected.begin(),
841 			_selected.end(),
842 			std::back_inserter(items),
843 			transformation);
844 	}
845 	return items;
846 }
847 
collectSelectedIds() const848 MessageIdsList ListWidget::collectSelectedIds() const {
849 	const auto selected = collectSelectedItems();
850 	return ranges::views::all(
851 		selected
852 	) | ranges::views::transform([](const SelectedItem &item) {
853 		return item.msgId;
854 	}) | ranges::to_vector;
855 }
856 
pushSelectedItems()857 void ListWidget::pushSelectedItems() {
858 	_delegate->listSelectionChanged(collectSelectedItems());
859 }
860 
removeItemSelection(const SelectedMap::const_iterator & i)861 void ListWidget::removeItemSelection(
862 		const SelectedMap::const_iterator &i) {
863 	Expects(i != _selected.cend());
864 
865 	_selected.erase(i);
866 	if (_selected.empty()) {
867 		update();
868 	}
869 	pushSelectedItems();
870 }
871 
hasSelectedText() const872 bool ListWidget::hasSelectedText() const {
873 	return (_selectedTextItem != nullptr) && !hasSelectedItems();
874 }
875 
hasSelectedItems() const876 bool ListWidget::hasSelectedItems() const {
877 	return !_selected.empty();
878 }
879 
overSelectedItems() const880 bool ListWidget::overSelectedItems() const {
881 	if (_overState.pointState == PointState::GroupPart) {
882 		return _overItemExact
883 			&& _selected.contains(_overItemExact->fullId());
884 	} else if (_overState.pointState == PointState::Inside) {
885 		return _overElement
886 			&& isSelectedAsGroup(_selected, _overElement->data());
887 	}
888 	return false;
889 }
890 
isSelectedGroup(const SelectedMap & applyTo,not_null<const Data::Group * > group) const891 bool ListWidget::isSelectedGroup(
892 		const SelectedMap &applyTo,
893 		not_null<const Data::Group*> group) const {
894 	for (const auto &other : group->items) {
895 		if (!applyTo.contains(other->fullId())) {
896 			return false;
897 		}
898 	}
899 	return true;
900 }
901 
isSelectedAsGroup(const SelectedMap & applyTo,not_null<HistoryItem * > item) const902 bool ListWidget::isSelectedAsGroup(
903 		const SelectedMap &applyTo,
904 		not_null<HistoryItem*> item) const {
905 	if (const auto group = session().data().groups().find(item)) {
906 		return isSelectedGroup(applyTo, group);
907 	}
908 	return applyTo.contains(item->fullId());
909 }
910 
isGoodForSelection(SelectedMap & applyTo,not_null<HistoryItem * > item,int & totalCount) const911 bool ListWidget::isGoodForSelection(
912 		SelectedMap &applyTo,
913 		not_null<HistoryItem*> item,
914 		int &totalCount) const {
915 	if (!_delegate->listIsItemGoodForSelection(item)) {
916 		return false;
917 	} else if (!applyTo.contains(item->fullId())) {
918 		++totalCount;
919 	}
920 	return (totalCount <= MaxSelectedItems);
921 }
922 
addToSelection(SelectedMap & applyTo,not_null<HistoryItem * > item) const923 bool ListWidget::addToSelection(
924 		SelectedMap &applyTo,
925 		not_null<HistoryItem*> item) const {
926 	const auto itemId = item->fullId();
927 	auto [iterator, ok] = applyTo.try_emplace(
928 		itemId,
929 		SelectionData());
930 	if (!ok) {
931 		return false;
932 	}
933 	iterator->second.canDelete = item->canDelete();
934 	iterator->second.canForward = item->allowsForward();
935 	iterator->second.canSendNow = item->allowsSendNow();
936 	return true;
937 }
938 
removeFromSelection(SelectedMap & applyTo,FullMsgId itemId) const939 bool ListWidget::removeFromSelection(
940 		SelectedMap &applyTo,
941 		FullMsgId itemId) const {
942 	return applyTo.remove(itemId);
943 }
944 
changeSelection(SelectedMap & applyTo,not_null<HistoryItem * > item,SelectAction action) const945 void ListWidget::changeSelection(
946 		SelectedMap &applyTo,
947 		not_null<HistoryItem*> item,
948 		SelectAction action) const {
949 	const auto itemId = item->fullId();
950 	if (action == SelectAction::Invert) {
951 		action = applyTo.contains(itemId)
952 			? SelectAction::Deselect
953 			: SelectAction::Select;
954 	}
955 	if (action == SelectAction::Select) {
956 		auto already = int(applyTo.size());
957 		if (isGoodForSelection(applyTo, item, already)) {
958 			addToSelection(applyTo, item);
959 		}
960 	} else {
961 		removeFromSelection(applyTo, itemId);
962 	}
963 }
964 
changeSelectionAsGroup(SelectedMap & applyTo,not_null<HistoryItem * > item,SelectAction action) const965 void ListWidget::changeSelectionAsGroup(
966 		SelectedMap &applyTo,
967 		not_null<HistoryItem*> item,
968 		SelectAction action) const {
969 	const auto group = session().data().groups().find(item);
970 	if (!group) {
971 		return changeSelection(applyTo, item, action);
972 	}
973 	if (action == SelectAction::Invert) {
974 		action = isSelectedAsGroup(applyTo, item)
975 			? SelectAction::Deselect
976 			: SelectAction::Select;
977 	}
978 	auto already = int(applyTo.size());
979 	const auto canSelect = [&] {
980 		for (const auto &other : group->items) {
981 			if (!isGoodForSelection(applyTo, other, already)) {
982 				return false;
983 			}
984 		}
985 		return true;
986 	}();
987 	if (action == SelectAction::Select && canSelect) {
988 		for (const auto &other : group->items) {
989 			addToSelection(applyTo, other);
990 		}
991 	} else {
992 		for (const auto &other : group->items) {
993 			removeFromSelection(applyTo, other->fullId());
994 		}
995 	}
996 }
997 
isItemUnderPressSelected() const998 bool ListWidget::isItemUnderPressSelected() const {
999 	return itemUnderPressSelection() != _selected.end();
1000 }
1001 
itemUnderPressSelection()1002 auto ListWidget::itemUnderPressSelection() -> SelectedMap::iterator {
1003 	return (_pressState.itemId
1004 		&& _pressState.pointState != PointState::Outside)
1005 		? _selected.find(_pressState.itemId)
1006 		: _selected.end();
1007 }
1008 
isInsideSelection(not_null<const Element * > view,not_null<HistoryItem * > exactItem,const MouseState & state) const1009 bool ListWidget::isInsideSelection(
1010 		not_null<const Element*> view,
1011 		not_null<HistoryItem*> exactItem,
1012 		const MouseState &state) const {
1013 	if (!_selected.empty()) {
1014 		if (state.pointState == PointState::GroupPart) {
1015 			return _selected.contains(exactItem->fullId());
1016 		} else {
1017 			return isSelectedAsGroup(_selected, view->data());
1018 		}
1019 	} else if (_selectedTextItem
1020 		&& _selectedTextItem == view->data()
1021 		&& state.pointState != PointState::Outside) {
1022 		StateRequest stateRequest;
1023 		stateRequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
1024 		const auto dragState = view->textState(
1025 			state.point,
1026 			stateRequest);
1027 		if (dragState.cursor == CursorState::Text
1028 			&& base::in_range(
1029 				dragState.symbol,
1030 				_selectedTextRange.from,
1031 				_selectedTextRange.to)) {
1032 			return true;
1033 		}
1034 	}
1035 	return false;
1036 }
1037 
itemUnderPressSelection() const1038 auto ListWidget::itemUnderPressSelection() const
1039 -> SelectedMap::const_iterator {
1040 	return (_pressState.itemId
1041 		&& _pressState.pointState != PointState::Outside)
1042 		? _selected.find(_pressState.itemId)
1043 		: _selected.end();
1044 }
1045 
requiredToStartDragging(not_null<Element * > view) const1046 bool ListWidget::requiredToStartDragging(
1047 		not_null<Element*> view) const {
1048 	if (_mouseCursorState == CursorState::Date) {
1049 		return true;
1050 	} else if (const auto media = view->media()) {
1051 		if (media->dragItem()) {
1052 			return true;
1053 		}
1054 	}
1055 	return false;
1056 }
1057 
isPressInSelectedText(TextState state) const1058 bool ListWidget::isPressInSelectedText(TextState state) const {
1059 	if (state.cursor != CursorState::Text) {
1060 		return false;
1061 	}
1062 	if (!hasSelectedText()
1063 		|| !_selectedTextItem
1064 		|| _selectedTextItem->fullId() != _pressState.itemId) {
1065 		return false;
1066 	}
1067 	auto from = _selectedTextRange.from;
1068 	auto to = _selectedTextRange.to;
1069 	return (state.symbol >= from && state.symbol < to);
1070 }
1071 
cancelSelection()1072 void ListWidget::cancelSelection() {
1073 	clearSelected();
1074 	clearTextSelection();
1075 }
1076 
selectItem(not_null<HistoryItem * > item)1077 void ListWidget::selectItem(not_null<HistoryItem*> item) {
1078 	if (const auto view = viewForItem(item)) {
1079 		clearTextSelection();
1080 		changeSelection(
1081 			_selected,
1082 			item,
1083 			SelectAction::Select);
1084 		pushSelectedItems();
1085 	}
1086 }
1087 
selectItemAsGroup(not_null<HistoryItem * > item)1088 void ListWidget::selectItemAsGroup(not_null<HistoryItem*> item) {
1089 	if (const auto view = viewForItem(item)) {
1090 		clearTextSelection();
1091 		changeSelectionAsGroup(
1092 			_selected,
1093 			item,
1094 			SelectAction::Select);
1095 		pushSelectedItems();
1096 		update();
1097 	}
1098 }
1099 
clearSelected()1100 void ListWidget::clearSelected() {
1101 	if (_selected.empty()) {
1102 		return;
1103 	}
1104 	if (hasSelectedText()) {
1105 		repaintItem(_selected.begin()->first);
1106 		_selected.clear();
1107 	} else {
1108 		_selected.clear();
1109 		pushSelectedItems();
1110 		update();
1111 	}
1112 }
1113 
clearTextSelection()1114 void ListWidget::clearTextSelection() {
1115 	if (_selectedTextItem) {
1116 		if (const auto view = viewForItem(_selectedTextItem)) {
1117 			repaintItem(view);
1118 		}
1119 		_selectedTextItem = nullptr;
1120 		_selectedTextRange = TextSelection();
1121 		_selectedText = TextForMimeData();
1122 	}
1123 }
1124 
setTextSelection(not_null<Element * > view,TextSelection selection)1125 void ListWidget::setTextSelection(
1126 		not_null<Element*> view,
1127 		TextSelection selection) {
1128 	clearSelected();
1129 	const auto item = view->data();
1130 	if (_selectedTextItem != item) {
1131 		clearTextSelection();
1132 		_selectedTextItem = view->data();
1133 	}
1134 	_selectedTextRange = selection;
1135 	_selectedText = (selection.from != selection.to)
1136 		? view->selectedText(selection)
1137 		: TextForMimeData();
1138 	repaintItem(view);
1139 	if (!_wasSelectedText && !_selectedText.empty()) {
1140 		_wasSelectedText = true;
1141 		setFocus();
1142 	}
1143 }
1144 
loadedAtTopKnown() const1145 bool ListWidget::loadedAtTopKnown() const {
1146 	return !!_slice.skippedBefore;
1147 }
1148 
loadedAtTop() const1149 bool ListWidget::loadedAtTop() const {
1150 	return _slice.skippedBefore && (*_slice.skippedBefore == 0);
1151 }
1152 
loadedAtBottomKnown() const1153 bool ListWidget::loadedAtBottomKnown() const {
1154 	return !!_slice.skippedAfter;
1155 }
1156 
loadedAtBottom() const1157 bool ListWidget::loadedAtBottom() const {
1158 	return _slice.skippedAfter && (*_slice.skippedAfter == 0);
1159 }
1160 
isEmpty() const1161 bool ListWidget::isEmpty() const {
1162 	return loadedAtTop()
1163 		&& loadedAtBottom()
1164 		&& (_itemsHeight + _itemsRevealHeight == 0);
1165 }
1166 
itemMinimalHeight() const1167 int ListWidget::itemMinimalHeight() const {
1168 	return st::msgMarginTopAttached
1169 		+ st::msgPhotoSize
1170 		+ st::msgMargin.bottom();
1171 }
1172 
checkMoveToOtherViewer()1173 void ListWidget::checkMoveToOtherViewer() {
1174 	auto visibleHeight = (_visibleBottom - _visibleTop);
1175 	if (width() <= 0
1176 		|| visibleHeight <= 0
1177 		|| _items.empty()
1178 		|| _aroundIndex < 0
1179 		|| _scrollTopState.item) {
1180 		return;
1181 	}
1182 
1183 	auto topItemIndex = findItemIndexByY(_visibleTop);
1184 	auto bottomItemIndex = findItemIndexByY(_visibleBottom);
1185 	auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
1186 	auto preloadedCount = preloadedHeight / _itemAverageHeight;
1187 	auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
1188 	auto preloadIdsLimit = preloadIdsLimitMin
1189 		+ (visibleHeight / _itemAverageHeight);
1190 
1191 	auto preloadBefore = kPreloadIfLessThanScreens * visibleHeight;
1192 	auto before = _slice.skippedBefore;
1193 	auto preloadTop = (_visibleTop < preloadBefore);
1194 	auto topLoaded = before && (*before == 0);
1195 	auto after = _slice.skippedAfter;
1196 	auto preloadBottom = (height() - _visibleBottom < preloadBefore);
1197 	auto bottomLoaded = after && (*after == 0);
1198 
1199 	auto minScreenDelta = kPreloadedScreensCount
1200 		- kPreloadIfLessThanScreens;
1201 	auto minUniversalIdDelta = (minScreenDelta * visibleHeight)
1202 		/ _itemAverageHeight;
1203 	const auto preloadAroundMessage = [&](int index) {
1204 		Expects(index >= 0 && index < _items.size());
1205 
1206 		auto preloadRequired = false;
1207 		auto itemPosition = _items[index]->data()->position();
1208 
1209 		if (!preloadRequired) {
1210 			preloadRequired = (_idsLimit < preloadIdsLimitMin);
1211 		}
1212 		if (!preloadRequired) {
1213 			Assert(_aroundIndex >= 0);
1214 			auto delta = std::abs(index - _aroundIndex);
1215 			preloadRequired = (delta >= minUniversalIdDelta);
1216 		}
1217 		if (preloadRequired) {
1218 			_idsLimit = preloadIdsLimit;
1219 			_aroundPosition = itemPosition;
1220 			_aroundIndex = index;
1221 			refreshViewer();
1222 		}
1223 	};
1224 
1225 	const auto findGoodAbove = [&](int index) {
1226 		Expects(index >= 0 && index < _items.size());
1227 
1228 		for (; index != _items.size(); ++index) {
1229 			if (_delegate->listIsGoodForAroundPosition(_items[index])) {
1230 				return index;
1231 			}
1232 		}
1233 		return -1;
1234 	};
1235 	const auto findGoodBelow = [&](int index) {
1236 		Expects(index >= 0 && index < _items.size());
1237 
1238 		for (++index; index != 0;) {
1239 			if (_delegate->listIsGoodForAroundPosition(_items[--index])) {
1240 				return index;
1241 			}
1242 		}
1243 		return -1;
1244 	};
1245 	if (preloadTop && !topLoaded) {
1246 		const auto goodAboveIndex = findGoodAbove(topItemIndex);
1247 		const auto goodIndex = (goodAboveIndex >= 0)
1248 			? goodAboveIndex
1249 			: findGoodBelow(topItemIndex);
1250 		if (goodIndex >= 0) {
1251 			preloadAroundMessage(goodIndex);
1252 		}
1253 	} else if (preloadBottom && !bottomLoaded) {
1254 		const auto goodBelowIndex = findGoodBelow(bottomItemIndex);
1255 		const auto goodIndex = (goodBelowIndex >= 0)
1256 			? goodBelowIndex
1257 			: findGoodAbove(bottomItemIndex);
1258 		if (goodIndex >= 0) {
1259 			preloadAroundMessage(goodIndex);
1260 		}
1261 	}
1262 }
1263 
tooltipText() const1264 QString ListWidget::tooltipText() const {
1265 	const auto item = (_overElement && _mouseAction == MouseAction::None)
1266 		? _overElement->data().get()
1267 		: nullptr;
1268 	if (_mouseCursorState == CursorState::Date && item) {
1269 		return HistoryView::DateTooltipText(_overElement);
1270 	} else if (_mouseCursorState == CursorState::Forwarded && item) {
1271 		if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
1272 			return forwarded->text.toString();
1273 		}
1274 	} else if (const auto link = ClickHandler::getActive()) {
1275 		return link->tooltip();
1276 	}
1277 	return QString();
1278 }
1279 
tooltipPos() const1280 QPoint ListWidget::tooltipPos() const {
1281 	return _mousePosition;
1282 }
1283 
tooltipWindowActive() const1284 bool ListWidget::tooltipWindowActive() const {
1285 	return Ui::AppInFocus() && Ui::InFocusChain(window());
1286 }
1287 
elementContext()1288 Context ListWidget::elementContext() {
1289 	return _delegate->listContext();
1290 }
1291 
elementCreate(not_null<HistoryMessage * > message,Element * replacing)1292 std::unique_ptr<Element> ListWidget::elementCreate(
1293 		not_null<HistoryMessage*> message,
1294 		Element *replacing) {
1295 	return std::make_unique<Message>(this, message, replacing);
1296 }
1297 
elementCreate(not_null<HistoryService * > message,Element * replacing)1298 std::unique_ptr<Element> ListWidget::elementCreate(
1299 		not_null<HistoryService*> message,
1300 		Element *replacing) {
1301 	return std::make_unique<Service>(this, message, replacing);
1302 }
1303 
elementUnderCursor(not_null<const HistoryView::Element * > view)1304 bool ListWidget::elementUnderCursor(
1305 		not_null<const HistoryView::Element*> view) {
1306 	return (_overElement == view);
1307 }
1308 
elementHighlightTime(not_null<const HistoryItem * > item)1309 crl::time ListWidget::elementHighlightTime(
1310 		not_null<const HistoryItem*> item) {
1311 	if (item->fullId() == _highlightedMessageId) {
1312 		if (_highlightTimer.isActive()) {
1313 			return crl::now() - _highlightStart;
1314 		}
1315 	}
1316 	return crl::time(0);
1317 }
1318 
elementInSelectionMode()1319 bool ListWidget::elementInSelectionMode() {
1320 	return hasSelectedItems() || !_dragSelected.empty();
1321 }
1322 
elementIntersectsRange(not_null<const Element * > view,int from,int till)1323 bool ListWidget::elementIntersectsRange(
1324 		not_null<const Element*> view,
1325 		int from,
1326 		int till) {
1327 	Expects(view->delegate() == this);
1328 
1329 	const auto top = itemTop(view);
1330 	const auto bottom = top + view->height();
1331 	return (top < till && bottom > from);
1332 }
1333 
elementStartStickerLoop(not_null<const Element * > view)1334 void ListWidget::elementStartStickerLoop(not_null<const Element*> view) {
1335 }
1336 
elementShowPollResults(not_null<PollData * > poll,FullMsgId context)1337 void ListWidget::elementShowPollResults(
1338 		not_null<PollData*> poll,
1339 		FullMsgId context) {
1340 	_controller->showPollResults(poll, context);
1341 }
1342 
elementOpenPhoto(not_null<PhotoData * > photo,FullMsgId context)1343 void ListWidget::elementOpenPhoto(
1344 		not_null<PhotoData*> photo,
1345 		FullMsgId context) {
1346 	_controller->openPhoto(photo, context);
1347 }
1348 
elementOpenDocument(not_null<DocumentData * > document,FullMsgId context,bool showInMediaView)1349 void ListWidget::elementOpenDocument(
1350 		not_null<DocumentData*> document,
1351 		FullMsgId context,
1352 		bool showInMediaView) {
1353 	_controller->openDocument(document, context, showInMediaView);
1354 }
1355 
elementCancelUpload(const FullMsgId & context)1356 void ListWidget::elementCancelUpload(const FullMsgId &context) {
1357 	if (const auto item = session().data().message(context)) {
1358 		_controller->cancelUploadLayer(item);
1359 	}
1360 }
1361 
elementShowTooltip(const TextWithEntities & text,Fn<void ()> hiddenCallback)1362 void ListWidget::elementShowTooltip(
1363 	const TextWithEntities &text,
1364 	Fn<void()> hiddenCallback) {
1365 }
1366 
elementIsGifPaused()1367 bool ListWidget::elementIsGifPaused() {
1368 	return _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any);
1369 }
1370 
elementHideReply(not_null<const Element * > view)1371 bool ListWidget::elementHideReply(not_null<const Element*> view) {
1372 	return _delegate->listElementHideReply(view);
1373 }
1374 
elementShownUnread(not_null<const Element * > view)1375 bool ListWidget::elementShownUnread(not_null<const Element*> view) {
1376 	return _delegate->listElementShownUnread(view);
1377 }
1378 
elementSendBotCommand(const QString & command,const FullMsgId & context)1379 void ListWidget::elementSendBotCommand(
1380 		const QString &command,
1381 		const FullMsgId &context) {
1382 	_delegate->listSendBotCommand(command, context);
1383 }
1384 
elementHandleViaClick(not_null<UserData * > bot)1385 void ListWidget::elementHandleViaClick(not_null<UserData*> bot) {
1386 	_delegate->listHandleViaClick(bot);
1387 }
1388 
elementIsChatWide()1389 bool ListWidget::elementIsChatWide() {
1390 	return _isChatWide;
1391 }
1392 
elementPathShiftGradient()1393 not_null<Ui::PathShiftGradient*> ListWidget::elementPathShiftGradient() {
1394 	return _pathGradient.get();
1395 }
1396 
elementReplyTo(const FullMsgId & to)1397 void ListWidget::elementReplyTo(const FullMsgId &to) {
1398 	replyToMessageRequestNotify(to);
1399 }
1400 
elementStartInteraction(not_null<const Element * > view)1401 void ListWidget::elementStartInteraction(not_null<const Element*> view) {
1402 }
1403 
saveState(not_null<ListMemento * > memento)1404 void ListWidget::saveState(not_null<ListMemento*> memento) {
1405 	memento->setAroundPosition(_aroundPosition);
1406 	auto state = countScrollState();
1407 	if (state.item) {
1408 		memento->setIdsLimit(_idsLimit);
1409 		memento->setScrollTopState(state);
1410 	}
1411 }
1412 
restoreState(not_null<ListMemento * > memento)1413 void ListWidget::restoreState(not_null<ListMemento*> memento) {
1414 	_aroundPosition = memento->aroundPosition();
1415 	_aroundIndex = -1;
1416 	if (const auto limit = memento->idsLimit()) {
1417 		_idsLimit = limit;
1418 	}
1419 	_scrollTopState = memento->scrollTopState();
1420 	refreshViewer();
1421 }
1422 
updateItemsGeometry()1423 void ListWidget::updateItemsGeometry() {
1424 	const auto count = int(_items.size());
1425 	const auto first = [&] {
1426 		for (auto i = 0; i != count; ++i) {
1427 			const auto view = _items[i].get();
1428 			if (view->isHidden()) {
1429 				view->setDisplayDate(false);
1430 			} else {
1431 				view->setDisplayDate(true);
1432 				view->setAttachToPrevious(false);
1433 				return i;
1434 			}
1435 		}
1436 		return count;
1437 	}();
1438 	refreshAttachmentsFromTill(first, count);
1439 }
1440 
updateSize()1441 void ListWidget::updateSize() {
1442 	resizeToWidth(width(), _minHeight);
1443 	updateVisibleTopItem();
1444 }
1445 
resizeToWidth(int newWidth,int minHeight)1446 void ListWidget::resizeToWidth(int newWidth, int minHeight) {
1447 	_minHeight = minHeight;
1448 	TWidget::resizeToWidth(newWidth);
1449 	restoreScrollPosition();
1450 }
1451 
startItemRevealAnimations()1452 void ListWidget::startItemRevealAnimations() {
1453 	for (const auto &view : base::take(_itemRevealPending)) {
1454 		if (const auto height = view->height()) {
1455 			if (!_itemRevealAnimations.contains(view)) {
1456 				auto &animation = _itemRevealAnimations[view];
1457 				animation.startHeight = height;
1458 				_itemsRevealHeight += height;
1459 				animation.animation.start(
1460 					[=] { revealItemsCallback(); },
1461 					0.,
1462 					1.,
1463 					kItemRevealDuration,
1464 					anim::easeOutCirc);
1465 				if (view->data()->out()) {
1466 					_delegate->listChatTheme()->rotateComplexGradientBackground();
1467 				}
1468 			}
1469 		}
1470 	}
1471 }
1472 
revealItemsCallback()1473 void ListWidget::revealItemsCallback() {
1474 	auto revealHeight = 0;
1475 	for (auto i = begin(_itemRevealAnimations)
1476 		; i != end(_itemRevealAnimations);) {
1477 		if (!i->second.animation.animating()) {
1478 			i = _itemRevealAnimations.erase(i);
1479 		} else {
1480 			revealHeight += anim::interpolate(
1481 				i->second.startHeight,
1482 				0,
1483 				i->second.animation.value(1.));
1484 			++i;
1485 		}
1486 	}
1487 	if (_itemsRevealHeight != revealHeight) {
1488 		updateVisibleTopItem();
1489 		if (_visibleTopItem) {
1490 			// We're not at the bottom.
1491 			revealHeight = 0;
1492 			_itemRevealAnimations.clear();
1493 		}
1494 		const auto old = std::exchange(_itemsRevealHeight, revealHeight);
1495 		const auto delta = old - _itemsRevealHeight;
1496 		_itemsHeight += delta;
1497 		_itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom)
1498 			? (_minHeight - _itemsHeight - st::historyPaddingBottom)
1499 			: 0;
1500 		const auto wasHeight = height();
1501 		const auto nowHeight = _itemsTop
1502 			+ _itemsHeight
1503 			+ st::historyPaddingBottom;
1504 		if (wasHeight != nowHeight) {
1505 			resize(width(), nowHeight);
1506 		}
1507 		update();
1508 		restoreScrollPosition();
1509 		updateVisibleTopItem();
1510 
1511 		if (!_itemsRevealHeight) {
1512 			mouseActionUpdate(QCursor::pos());
1513 		}
1514 	}
1515 }
1516 
resizeGetHeight(int newWidth)1517 int ListWidget::resizeGetHeight(int newWidth) {
1518 	update();
1519 
1520 	const auto resizeAllItems = (_itemsWidth != newWidth);
1521 	auto newHeight = 0;
1522 	for (auto &view : _items) {
1523 		view->setY(newHeight);
1524 		if (view->pendingResize() || resizeAllItems) {
1525 			newHeight += view->resizeGetHeight(newWidth);
1526 		} else {
1527 			newHeight += view->height();
1528 		}
1529 	}
1530 	if (newHeight > 0) {
1531 		_itemAverageHeight = std::max(
1532 			itemMinimalHeight(),
1533 			newHeight / int(_items.size()));
1534 	}
1535 	startItemRevealAnimations();
1536 	_itemsWidth = newWidth;
1537 	_itemsHeight = newHeight - _itemsRevealHeight;
1538 	_itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom)
1539 		? (_minHeight - _itemsHeight - st::historyPaddingBottom)
1540 		: 0;
1541 	return _itemsTop + _itemsHeight + st::historyPaddingBottom;
1542 }
1543 
restoreScrollPosition()1544 void ListWidget::restoreScrollPosition() {
1545 	auto newVisibleTop = _visibleTopItem
1546 		? (itemTop(_visibleTopItem) + _visibleTopFromItem)
1547 		: ScrollMax;
1548 	_delegate->listScrollTo(newVisibleTop);
1549 }
1550 
computeRenderSelection(not_null<const SelectedMap * > selected,not_null<const Element * > view) const1551 TextSelection ListWidget::computeRenderSelection(
1552 		not_null<const SelectedMap*> selected,
1553 		not_null<const Element*> view) const {
1554 	const auto itemSelection = [&](not_null<HistoryItem*> item) {
1555 		auto i = selected->find(item->fullId());
1556 		if (i != selected->end()) {
1557 			return FullSelection;
1558 		}
1559 		return TextSelection();
1560 	};
1561 	const auto item = view->data();
1562 	if (const auto group = session().data().groups().find(item)) {
1563 		if (group->items.front() != item) {
1564 			return TextSelection();
1565 		}
1566 		auto result = TextSelection();
1567 		auto allFullSelected = true;
1568 		const auto count = int(group->items.size());
1569 		for (auto i = 0; i != count; ++i) {
1570 			if (itemSelection(group->items[i]) == FullSelection) {
1571 				result = AddGroupItemSelection(result, i);
1572 			} else {
1573 				allFullSelected = false;
1574 			}
1575 		}
1576 		if (allFullSelected) {
1577 			return FullSelection;
1578 		}
1579 		const auto leaderSelection = itemSelection(item);
1580 		if (leaderSelection != FullSelection
1581 			&& leaderSelection != TextSelection()) {
1582 			return leaderSelection;
1583 		}
1584 		return result;
1585 	}
1586 	return itemSelection(item);
1587 }
1588 
itemRenderSelection(not_null<const Element * > view) const1589 TextSelection ListWidget::itemRenderSelection(
1590 		not_null<const Element*> view) const {
1591 	if (!_dragSelected.empty()) {
1592 		const auto i = _dragSelected.find(view->data()->fullId());
1593 		if (i != _dragSelected.end()) {
1594 			return (_dragSelectAction == DragSelectAction::Selecting)
1595 				? FullSelection
1596 				: TextSelection();
1597 		}
1598 	}
1599 	if (!_selected.empty() || !_dragSelected.empty()) {
1600 		return computeRenderSelection(&_selected, view);
1601 	} else if (view->data() == _selectedTextItem) {
1602 		return _selectedTextRange;
1603 	}
1604 	return TextSelection();
1605 }
1606 
paintEvent(QPaintEvent * e)1607 void ListWidget::paintEvent(QPaintEvent *e) {
1608 	if (Ui::skipPaintEvent(this, e)) {
1609 		return;
1610 	}
1611 
1612 	const auto guard = gsl::finally([&] {
1613 		_userpicsCache.clear();
1614 	});
1615 
1616 	Painter p(this);
1617 
1618 	_pathGradient->startFrame(
1619 		0,
1620 		width(),
1621 		std::min(st::msgMaxWidth / 2, width() / 2));
1622 
1623 	auto clip = e->rect();
1624 
1625 	auto from = std::lower_bound(begin(_items), end(_items), clip.top(), [this](auto &elem, int top) {
1626 		return this->itemTop(elem) + elem->height() <= top;
1627 	});
1628 	auto to = std::lower_bound(begin(_items), end(_items), clip.top() + clip.height(), [this](auto &elem, int bottom) {
1629 		return this->itemTop(elem) < bottom;
1630 	});
1631 
1632 	if (from != end(_items)) {
1633 		auto top = itemTop(from->get());
1634 		auto context = controller()->preparePaintContext({
1635 			.theme = _delegate->listChatTheme(),
1636 			.visibleAreaTop = _visibleTop,
1637 			.visibleAreaTopGlobal = mapToGlobal(QPoint(0, _visibleTop)).y(),
1638 			.visibleAreaWidth = width(),
1639 			.clip = clip,
1640 		}).translated(0, -top);
1641 		p.translate(0, top);
1642 		for (auto i = from; i != to; ++i) {
1643 			const auto view = *i;
1644 			context.outbg = view->hasOutLayout();
1645 			context.selection = itemRenderSelection(view);
1646 			view->draw(p, context);
1647 			const auto height = view->height();
1648 			top += height;
1649 			context.viewport.translate(0, -height);
1650 			context.clip.translate(0, -height);
1651 			p.translate(0, height);
1652 		}
1653 		p.translate(0, -top);
1654 
1655 		enumerateUserpics([&](not_null<Element*> view, int userpicTop) {
1656 			// stop the enumeration if the userpic is below the painted rect
1657 			if (userpicTop >= clip.top() + clip.height()) {
1658 				return false;
1659 			}
1660 
1661 			// paint the userpic if it intersects the painted rect
1662 			if (userpicTop + st::msgPhotoSize > clip.top()) {
1663 				if (const auto from = view->data()->displayFrom()) {
1664 					from->paintUserpicLeft(
1665 						p,
1666 						_userpics[from],
1667 						st::historyPhotoLeft,
1668 						userpicTop,
1669 						view->width(),
1670 						st::msgPhotoSize);
1671 				} else if (const auto info = view->data()->hiddenForwardedInfo()) {
1672 					info->userpic.paint(
1673 						p,
1674 						st::historyPhotoLeft,
1675 						userpicTop,
1676 						view->width(),
1677 						st::msgPhotoSize);
1678 				} else {
1679 					Unexpected("Corrupt forwarded information in message.");
1680 				}
1681 			}
1682 			return true;
1683 		});
1684 
1685 		auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
1686 		auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);
1687 		enumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) {
1688 			// stop the enumeration if the date is above the painted rect
1689 			if (dateTop + dateHeight <= clip.top()) {
1690 				return false;
1691 			}
1692 
1693 			const auto displayDate = view->displayDate();
1694 			auto dateInPlace = displayDate;
1695 			if (dateInPlace) {
1696 				const auto correctDateTop = itemtop + st::msgServiceMargin.top();
1697 				dateInPlace = (dateTop < correctDateTop + dateHeight);
1698 			}
1699 			//bool noFloatingDate = (item->date.date() == lastDate && displayDate);
1700 			//if (noFloatingDate) {
1701 			//	if (itemtop < showFloatingBefore) {
1702 			//		noFloatingDate = false;
1703 			//	}
1704 			//}
1705 
1706 			// paint the date if it intersects the painted rect
1707 			if (dateTop < clip.top() + clip.height()) {
1708 				auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;
1709 				if (opacity > 0.) {
1710 					p.setOpacity(opacity);
1711 					int dateY = /*noFloatingDate ? itemtop :*/ (dateTop - st::msgServiceMargin.top());
1712 					int width = view->width();
1713 					if (const auto date = view->Get<HistoryView::DateBadge>()) {
1714 						date->paint(p, context.st, dateY, width, _isChatWide);
1715 					} else {
1716 						ServiceMessagePainter::PaintDate(
1717 							p,
1718 							context.st,
1719 							ItemDateText(
1720 								view->data(),
1721 								IsItemScheduledUntilOnline(view->data())),
1722 							dateY,
1723 							width,
1724 							_isChatWide);
1725 					}
1726 				}
1727 			}
1728 			return true;
1729 		});
1730 	}
1731 }
1732 
applyDragSelection()1733 void ListWidget::applyDragSelection() {
1734 	applyDragSelection(_selected);
1735 	clearDragSelection();
1736 	pushSelectedItems();
1737 }
1738 
applyDragSelection(SelectedMap & applyTo) const1739 void ListWidget::applyDragSelection(SelectedMap &applyTo) const {
1740 	if (_dragSelectAction == DragSelectAction::Selecting) {
1741 		for (const auto &itemId : _dragSelected) {
1742 			if (applyTo.size() >= MaxSelectedItems) {
1743 				break;
1744 			} else if (!applyTo.contains(itemId)) {
1745 				if (const auto item = session().data().message(itemId)) {
1746 					addToSelection(applyTo, item);
1747 				}
1748 			}
1749 		}
1750 	} else if (_dragSelectAction == DragSelectAction::Deselecting) {
1751 		for (const auto &itemId : _dragSelected) {
1752 			removeFromSelection(applyTo, itemId);
1753 		}
1754 	}
1755 }
1756 
getSelectedText() const1757 TextForMimeData ListWidget::getSelectedText() const {
1758 	auto selected = _selected;
1759 
1760 	if (_mouseAction == MouseAction::Selecting && !_dragSelected.empty()) {
1761 		applyDragSelection(selected);
1762 	}
1763 
1764 	if (selected.empty()) {
1765 		if (const auto view = viewForItem(_selectedTextItem)) {
1766 			return view->selectedText(_selectedTextRange);
1767 		}
1768 		return _selectedText;
1769 	}
1770 
1771 	const auto timeFormat = QString(", [%1 %2]\n")
1772 		.arg(cDateFormat())
1773 		.arg(cTimeFormat());
1774 	auto groups = base::flat_set<not_null<const Data::Group*>>();
1775 	auto fullSize = 0;
1776 	auto texts = std::vector<std::pair<
1777 		not_null<HistoryItem*>,
1778 		TextForMimeData>>();
1779 	texts.reserve(selected.size());
1780 
1781 	const auto wrapItem = [&](
1782 			not_null<HistoryItem*> item,
1783 			TextForMimeData &&unwrapped) {
1784 		auto time = ItemDateTime(item).toString(timeFormat);
1785 		auto part = TextForMimeData();
1786 		auto size = item->author()->name.size()
1787 			+ time.size()
1788 			+ unwrapped.expanded.size();
1789 		part.reserve(size);
1790 		part.append(item->author()->name).append(time);
1791 		part.append(std::move(unwrapped));
1792 		texts.emplace_back(std::move(item), std::move(part));
1793 		fullSize += size;
1794 	};
1795 	const auto addItem = [&](not_null<HistoryItem*> item) {
1796 		wrapItem(item, HistoryItemText(item));
1797 	};
1798 	const auto addGroup = [&](not_null<const Data::Group*> group) {
1799 		Expects(!group->items.empty());
1800 
1801 		wrapItem(group->items.back(), HistoryGroupText(group));
1802 	};
1803 
1804 	for (const auto &[itemId, data] : selected) {
1805 		if (const auto item = session().data().message(itemId)) {
1806 			if (const auto group = session().data().groups().find(item)) {
1807 				if (groups.contains(group)) {
1808 					continue;
1809 				}
1810 				if (isSelectedGroup(selected, group)) {
1811 					groups.emplace(group);
1812 					addGroup(group);
1813 				} else {
1814 					addItem(item);
1815 				}
1816 			} else {
1817 				addItem(item);
1818 			}
1819 		}
1820 	}
1821 	ranges::sort(texts, [&](
1822 			const std::pair<not_null<HistoryItem*>, TextForMimeData> &a,
1823 			const std::pair<not_null<HistoryItem*>, TextForMimeData> &b) {
1824 		return _delegate->listIsLessInOrder(a.first, b.first);
1825 	});
1826 
1827 	auto result = TextForMimeData();
1828 	auto sep = qstr("\n\n");
1829 	result.reserve(fullSize + (texts.size() - 1) * sep.size());
1830 	for (auto i = begin(texts), e = end(texts); i != e;) {
1831 		result.append(std::move(i->second));
1832 		if (++i != e) {
1833 			result.append(sep);
1834 		}
1835 	}
1836 	return result;
1837 }
1838 
getSelectedIds() const1839 MessageIdsList ListWidget::getSelectedIds() const {
1840 	return collectSelectedIds();
1841 }
1842 
getSelectedItems() const1843 SelectedItems ListWidget::getSelectedItems() const {
1844 	return collectSelectedItems();
1845 }
1846 
findItemIndexByY(int y) const1847 int ListWidget::findItemIndexByY(int y) const {
1848 	Expects(!_items.empty());
1849 
1850 	if (y < _itemsTop) {
1851 		return 0;
1852 	}
1853 	auto i = std::lower_bound(
1854 		begin(_items),
1855 		end(_items),
1856 		y,
1857 		[this](auto &elem, int top) {
1858 		return this->itemTop(elem) + elem->height() <= top;
1859 	});
1860 	return std::min(int(i - begin(_items)), int(_items.size() - 1));
1861 }
1862 
findItemByY(int y) const1863 not_null<Element*> ListWidget::findItemByY(int y) const {
1864 	return _items[findItemIndexByY(y)];
1865 }
1866 
strictFindItemByY(int y) const1867 Element *ListWidget::strictFindItemByY(int y) const {
1868 	if (_items.empty()) {
1869 		return nullptr;
1870 	}
1871 	return (y >= _itemsTop && y < _itemsTop + _itemsHeight)
1872 		? findItemByY(y).get()
1873 		: nullptr;
1874 }
1875 
countScrollState() const1876 auto ListWidget::countScrollState() const -> ScrollTopState {
1877 	if (_items.empty() || _visibleBottom == height()) {
1878 		return { Data::MessagePosition(), 0 };
1879 	}
1880 	auto topItem = findItemByY(_visibleTop);
1881 	return {
1882 		topItem->data()->position(),
1883 		_visibleTop - itemTop(topItem)
1884 	};
1885 }
1886 
keyPressEvent(QKeyEvent * e)1887 void ListWidget::keyPressEvent(QKeyEvent *e) {
1888 	if (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) {
1889 		if (hasSelectedText() || hasSelectedItems()) {
1890 			cancelSelection();
1891 		} else {
1892 			_delegate->listCancelRequest();
1893 		}
1894 	} else if (e == QKeySequence::Copy
1895 		&& (hasSelectedText() || hasSelectedItems())) {
1896 		TextUtilities::SetClipboardText(getSelectedText());
1897 #ifdef Q_OS_MAC
1898 	} else if (e->key() == Qt::Key_E
1899 		&& e->modifiers().testFlag(Qt::ControlModifier)) {
1900 		TextUtilities::SetClipboardText(getSelectedText(), QClipboard::FindBuffer);
1901 #endif // Q_OS_MAC
1902 	} else if (e == QKeySequence::Delete) {
1903 		_delegate->listDeleteRequest();
1904 	} else {
1905 		e->ignore();
1906 	}
1907 }
1908 
mouseDoubleClickEvent(QMouseEvent * e)1909 void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {
1910 	mouseActionStart(e->globalPos(), e->button());
1911 	trySwitchToWordSelection();
1912 	if (!ClickHandler::getActive()
1913 		&& !ClickHandler::getPressed()
1914 		&& (_mouseCursorState == CursorState::None
1915 			|| _mouseCursorState == CursorState::Date)
1916 		&& _selected.empty()
1917 		&& _overElement
1918 		&& _overElement->data()->isRegular()) {
1919 		mouseActionCancel();
1920 		replyToMessageRequestNotify(_overElement->data()->fullId());
1921 	}
1922 }
1923 
trySwitchToWordSelection()1924 void ListWidget::trySwitchToWordSelection() {
1925 	auto selectingSome = (_mouseAction == MouseAction::Selecting)
1926 		&& hasSelectedText();
1927 	auto willSelectSome = (_mouseAction == MouseAction::None)
1928 		&& !hasSelectedItems();
1929 	auto checkSwitchToWordSelection = _overElement
1930 		&& (_mouseSelectType == TextSelectType::Letters)
1931 		&& (selectingSome || willSelectSome);
1932 	if (checkSwitchToWordSelection) {
1933 		switchToWordSelection();
1934 	}
1935 }
1936 
switchToWordSelection()1937 void ListWidget::switchToWordSelection() {
1938 	Expects(_overElement != nullptr);
1939 
1940 	StateRequest request;
1941 	request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
1942 	auto dragState = _overElement->textState(_pressState.point, request);
1943 	if (dragState.cursor != CursorState::Text) {
1944 		return;
1945 	}
1946 	_mouseTextSymbol = dragState.symbol;
1947 	_mouseSelectType = TextSelectType::Words;
1948 	if (_mouseAction == MouseAction::None) {
1949 		_mouseAction = MouseAction::Selecting;
1950 		setTextSelection(_overElement, TextSelection(
1951 			dragState.symbol,
1952 			dragState.symbol
1953 		));
1954 	}
1955 	mouseActionUpdate();
1956 
1957 	_trippleClickPoint = _mousePosition;
1958 	_trippleClickStartTime = crl::now();
1959 }
1960 
validateTrippleClickStartTime()1961 void ListWidget::validateTrippleClickStartTime() {
1962 	if (_trippleClickStartTime) {
1963 		const auto elapsed = (crl::now() - _trippleClickStartTime);
1964 		if (elapsed >= QApplication::doubleClickInterval()) {
1965 			_trippleClickStartTime = 0;
1966 		}
1967 	}
1968 }
1969 
contextMenuEvent(QContextMenuEvent * e)1970 void ListWidget::contextMenuEvent(QContextMenuEvent *e) {
1971 	showContextMenu(e);
1972 }
1973 
showContextMenu(QContextMenuEvent * e,bool showFromTouch)1974 void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
1975 	if (e->reason() == QContextMenuEvent::Mouse) {
1976 		mouseActionUpdate(e->globalPos());
1977 	}
1978 
1979 	auto request = ContextMenuRequest(_controller);
1980 
1981 	request.link = ClickHandler::getActive();
1982 	request.view = _overElement;
1983 	request.item = _overItemExact
1984 		? _overItemExact
1985 		: _overElement
1986 		? _overElement->data().get()
1987 		: nullptr;
1988 	request.pointState = _overState.pointState;
1989 	request.selectedText = _selectedText;
1990 	request.selectedItems = collectSelectedItems();
1991 	request.overSelection = showFromTouch
1992 		|| (_overElement && isInsideSelection(
1993 			_overElement,
1994 			_overItemExact ? _overItemExact : _overElement->data().get(),
1995 			_overState));
1996 
1997 	_menu = FillContextMenu(this, request);
1998 	if (_menu && !_menu->empty()) {
1999 		_menu->popup(e->globalPos());
2000 		e->accept();
2001 	} else if (_menu) {
2002 		_menu = nullptr;
2003 	}
2004 }
2005 
mousePressEvent(QMouseEvent * e)2006 void ListWidget::mousePressEvent(QMouseEvent *e) {
2007 	if (_menu) {
2008 		e->accept();
2009 		return; // ignore mouse press, that was hiding context menu
2010 	}
2011 	mouseActionStart(e->globalPos(), e->button());
2012 }
2013 
mouseMoveEvent(QMouseEvent * e)2014 void ListWidget::mouseMoveEvent(QMouseEvent *e) {
2015 	static auto lastGlobalPosition = e->globalPos();
2016 	auto reallyMoved = (lastGlobalPosition != e->globalPos());
2017 	auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
2018 	if (!buttonsPressed && _mouseAction != MouseAction::None) {
2019 		mouseReleaseEvent(e);
2020 	}
2021 	if (reallyMoved) {
2022 		lastGlobalPosition = e->globalPos();
2023 		if (!buttonsPressed
2024 			|| (_scrollDateLink
2025 				&& ClickHandler::getPressed() == _scrollDateLink)) {
2026 			keepScrollDateForNow();
2027 		}
2028 	}
2029 	mouseActionUpdate(e->globalPos());
2030 }
2031 
mouseReleaseEvent(QMouseEvent * e)2032 void ListWidget::mouseReleaseEvent(QMouseEvent *e) {
2033 	mouseActionFinish(e->globalPos(), e->button());
2034 	if (!rect().contains(e->pos())) {
2035 		leaveEvent(e);
2036 	}
2037 }
2038 
enterEventHook(QEnterEvent * e)2039 void ListWidget::enterEventHook(QEnterEvent *e) {
2040 	mouseActionUpdate(QCursor::pos());
2041 	return TWidget::enterEventHook(e);
2042 }
2043 
leaveEventHook(QEvent * e)2044 void ListWidget::leaveEventHook(QEvent *e) {
2045 	if (const auto view = _overElement) {
2046 		if (_overState.pointState != PointState::Outside) {
2047 			repaintItem(view);
2048 			_overState.pointState = PointState::Outside;
2049 		}
2050 	}
2051 	ClickHandler::clearActive();
2052 	Ui::Tooltip::Hide();
2053 	if (!ClickHandler::getPressed() && _cursor != style::cur_default) {
2054 		_cursor = style::cur_default;
2055 		setCursor(_cursor);
2056 	}
2057 	return TWidget::leaveEventHook(e);
2058 }
2059 
updateDragSelection()2060 void ListWidget::updateDragSelection() {
2061 	if (!_overState.itemId || !_pressState.itemId) {
2062 		clearDragSelection();
2063 		return;
2064 	} else if (_items.empty() || !_overElement || !_selectEnabled) {
2065 		return;
2066 	}
2067 	const auto pressItem = session().data().message(_pressState.itemId);
2068 	if (!pressItem) {
2069 		return;
2070 	}
2071 
2072 	const auto overView = _overElement;
2073 	const auto pressView = viewForItem(pressItem);
2074 	const auto selectingUp = _delegate->listIsLessInOrder(
2075 		overView->data(),
2076 		pressItem);
2077 	if (selectingUp != _dragSelectDirectionUp) {
2078 		_dragSelectDirectionUp = selectingUp;
2079 		_dragSelectAction = DragSelectAction::None;
2080 	}
2081 	const auto fromView = selectingUp ? overView : pressView;
2082 	const auto tillView = selectingUp ? pressView : overView;
2083 	const auto fromState = selectingUp ? _overState : _pressState;
2084 	const auto tillState = selectingUp ? _pressState : _overState;
2085 	updateDragSelection(fromView, fromState, tillView, tillState);
2086 }
2087 
updateDragSelection(const Element * fromView,const MouseState & fromState,const Element * tillView,const MouseState & tillState)2088 void ListWidget::updateDragSelection(
2089 		const Element *fromView,
2090 		const MouseState &fromState,
2091 		const Element *tillView,
2092 		const MouseState &tillState) {
2093 	Expects(fromView != nullptr || tillView != nullptr);
2094 
2095 	const auto delta = QApplication::startDragDistance();
2096 
2097 	const auto includeFrom = [&] (
2098 			not_null<const Element*> view,
2099 			const MouseState &state) {
2100 		const auto bottom = view->height() - view->marginBottom();
2101 		return (state.point.y() < bottom - delta);
2102 	};
2103 	const auto includeTill = [&] (
2104 			not_null<const Element*> view,
2105 			const MouseState &state) {
2106 		const auto top = view->marginTop();
2107 		return (state.point.y() >= top + delta);
2108 	};
2109 	const auto includeSingleItem = [&] (
2110 			not_null<const Element*> view,
2111 			const MouseState &state1,
2112 			const MouseState &state2) {
2113 		const auto top = view->marginTop();
2114 		const auto bottom = view->height() - view->marginBottom();
2115 		const auto y1 = std::min(state1.point.y(), state2.point.y());
2116 		const auto y2 = std::max(state1.point.y(), state2.point.y());
2117 		return (y1 < bottom - delta && y2 >= top + delta)
2118 			? (y2 - y1 >= delta)
2119 			: false;
2120 	};
2121 
2122 	const auto from = [&] {
2123 		const auto result = fromView ? ranges::find(
2124 			_items,
2125 			fromView,
2126 			[](auto view) { return view.get(); }) : end(_items);
2127 		return (result == end(_items))
2128 			? begin(_items)
2129 			: (fromView == tillView || includeFrom(fromView, fromState))
2130 			? result
2131 			: (result + 1);
2132 	}();
2133 	const auto till = [&] {
2134 		if (fromView == tillView) {
2135 			return (from == end(_items))
2136 				? from
2137 				: includeSingleItem(fromView, fromState, tillState)
2138 				? (from + 1)
2139 				: from;
2140 		}
2141 		const auto result = tillView ? ranges::find(
2142 			_items,
2143 			tillView,
2144 			[](auto view) { return view.get(); }) : end(_items);
2145 		return (result == end(_items))
2146 			? end(_items)
2147 			: includeTill(tillView, tillState)
2148 			? (result + 1)
2149 			: result;
2150 	}();
2151 	if (from < till) {
2152 		updateDragSelection(from, till);
2153 	} else {
2154 		clearDragSelection();
2155 	}
2156 }
2157 
updateDragSelection(std::vector<not_null<Element * >>::const_iterator from,std::vector<not_null<Element * >>::const_iterator till)2158 void ListWidget::updateDragSelection(
2159 		std::vector<not_null<Element*>>::const_iterator from,
2160 		std::vector<not_null<Element*>>::const_iterator till) {
2161 	Expects(from < till);
2162 
2163 	const auto &groups = session().data().groups();
2164 	const auto changeItem = [&](not_null<HistoryItem*> item, bool add) {
2165 		const auto itemId = item->fullId();
2166 		if (add) {
2167 			_dragSelected.emplace(itemId);
2168 		} else {
2169 			_dragSelected.remove(itemId);
2170 		}
2171 	};
2172 	const auto changeGroup = [&](not_null<HistoryItem*> item, bool add) {
2173 		if (const auto group = groups.find(item)) {
2174 			for (const auto &item : group->items) {
2175 				if (!_delegate->listIsItemGoodForSelection(item)) {
2176 					return;
2177 				}
2178 			}
2179 			for (const auto &item : group->items) {
2180 				changeItem(item, add);
2181 			}
2182 		} else if (_delegate->listIsItemGoodForSelection(item)) {
2183 			changeItem(item, add);
2184 		}
2185 	};
2186 	const auto changeView = [&](not_null<Element*> view, bool add) {
2187 		if (!view->isHidden()) {
2188 			changeGroup(view->data(), add);
2189 		}
2190 	};
2191 	for (auto i = begin(_items); i != from; ++i) {
2192 		changeView(*i, false);
2193 	}
2194 	for (auto i = from; i != till; ++i) {
2195 		changeView(*i, true);
2196 	}
2197 	for (auto i = till; i != end(_items); ++i) {
2198 		changeView(*i, false);
2199 	}
2200 
2201 	ensureDragSelectAction(from, till);
2202 	update();
2203 }
2204 
ensureDragSelectAction(std::vector<not_null<Element * >>::const_iterator from,std::vector<not_null<Element * >>::const_iterator till)2205 void ListWidget::ensureDragSelectAction(
2206 		std::vector<not_null<Element*>>::const_iterator from,
2207 		std::vector<not_null<Element*>>::const_iterator till) {
2208 	if (_dragSelectAction != DragSelectAction::None) {
2209 		return;
2210 	}
2211 	const auto start = _dragSelectDirectionUp ? (till - 1) : from;
2212 	const auto startId = (*start)->data()->fullId();
2213 	_dragSelectAction = _selected.contains(startId)
2214 		? DragSelectAction::Deselecting
2215 		: DragSelectAction::Selecting;
2216 	if (!_wasSelectedText
2217 		&& !_dragSelected.empty()
2218 		&& _dragSelectAction == DragSelectAction::Selecting) {
2219 		_wasSelectedText = true;
2220 		setFocus();
2221 	}
2222 }
2223 
clearDragSelection()2224 void ListWidget::clearDragSelection() {
2225 	_dragSelectAction = DragSelectAction::None;
2226 	if (!_dragSelected.empty()) {
2227 		_dragSelected.clear();
2228 		update();
2229 	}
2230 }
2231 
mouseActionStart(const QPoint & globalPosition,Qt::MouseButton button)2232 void ListWidget::mouseActionStart(
2233 		const QPoint &globalPosition,
2234 		Qt::MouseButton button) {
2235 	mouseActionUpdate(globalPosition);
2236 	if (button != Qt::LeftButton) {
2237 		return;
2238 	}
2239 
2240 	ClickHandler::pressed();
2241 	if (_pressState != _overState) {
2242 		if (_pressState.itemId != _overState.itemId) {
2243 			repaintItem(_pressState.itemId);
2244 		}
2245 		_pressState = _overState;
2246 		repaintItem(_overState.itemId);
2247 	}
2248 	_pressItemExact = _overItemExact;
2249 	const auto pressElement = _overElement;
2250 
2251 	_mouseAction = MouseAction::None;
2252 	_pressWasInactive = Ui::WasInactivePress(_controller->widget());
2253 	if (_pressWasInactive) {
2254 		Ui::MarkInactivePress(_controller->widget(), false);
2255 	}
2256 
2257 	if (ClickHandler::getPressed()) {
2258 		_mouseAction = MouseAction::PrepareDrag;
2259 	} else if (hasSelectedItems()) {
2260 		if (overSelectedItems()) {
2261 			_mouseAction = MouseAction::PrepareDrag;
2262 		} else if (!_pressWasInactive) {
2263 			_mouseAction = MouseAction::PrepareSelect;
2264 		}
2265 	}
2266 	if (_mouseAction == MouseAction::None && pressElement) {
2267 		validateTrippleClickStartTime();
2268 		TextState dragState;
2269 		auto startDistance = (globalPosition - _trippleClickPoint).manhattanLength();
2270 		auto validStartPoint = startDistance < QApplication::startDragDistance();
2271 		if (_trippleClickStartTime != 0 && validStartPoint) {
2272 			StateRequest request;
2273 			request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
2274 			dragState = pressElement->textState(_pressState.point, request);
2275 			if (dragState.cursor == CursorState::Text) {
2276 				setTextSelection(pressElement, TextSelection(
2277 					dragState.symbol,
2278 					dragState.symbol
2279 				));
2280 				_mouseTextSymbol = dragState.symbol;
2281 				_mouseAction = MouseAction::Selecting;
2282 				_mouseSelectType = TextSelectType::Paragraphs;
2283 				mouseActionUpdate();
2284 				_trippleClickStartTime = crl::now();
2285 			}
2286 		} else if (pressElement) {
2287 			StateRequest request;
2288 			request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
2289 			dragState = pressElement->textState(_pressState.point, request);
2290 		}
2291 		if (_mouseSelectType != TextSelectType::Paragraphs) {
2292 			_mouseTextSymbol = dragState.symbol;
2293 			if (isPressInSelectedText(dragState)) {
2294 				_mouseAction = MouseAction::PrepareDrag; // start text drag
2295 			} else if (!_pressWasInactive) {
2296 				if (requiredToStartDragging(pressElement)
2297 					&& _pressState.pointState != PointState::Outside) {
2298 					_mouseAction = MouseAction::PrepareDrag;
2299 				} else {
2300 					if (dragState.afterSymbol) ++_mouseTextSymbol;
2301 					if (!hasSelectedItems()
2302 						&& _overState.pointState != PointState::Outside) {
2303 						setTextSelection(pressElement, TextSelection(
2304 							_mouseTextSymbol,
2305 							_mouseTextSymbol));
2306 						_mouseAction = MouseAction::Selecting;
2307 					} else {
2308 						_mouseAction = MouseAction::PrepareSelect;
2309 					}
2310 				}
2311 			}
2312 		}
2313 	}
2314 	if (!pressElement) {
2315 		_mouseAction = MouseAction::None;
2316 	} else if (_mouseAction == MouseAction::None) {
2317 		mouseActionCancel();
2318 	}
2319 }
2320 
mouseActionUpdate(const QPoint & globalPosition)2321 void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
2322 	_mousePosition = globalPosition;
2323 	mouseActionUpdate();
2324 }
2325 
mouseActionCancel()2326 void ListWidget::mouseActionCancel() {
2327 	_pressState = MouseState();
2328 	_pressItemExact = nullptr;
2329 	_mouseAction = MouseAction::None;
2330 	clearDragSelection();
2331 	_wasSelectedText = false;
2332 	_selectScroll.cancel();
2333 }
2334 
mouseActionFinish(const QPoint & globalPosition,Qt::MouseButton button)2335 void ListWidget::mouseActionFinish(
2336 		const QPoint &globalPosition,
2337 		Qt::MouseButton button) {
2338 	mouseActionUpdate(globalPosition);
2339 
2340 	auto pressState = base::take(_pressState);
2341 	base::take(_pressItemExact);
2342 	repaintItem(pressState.itemId);
2343 
2344 	const auto toggleByHandler = [&](const ClickHandlerPtr &handler) {
2345 		// If we are in selecting items mode perhaps we want to
2346 		// toggle selection instead of activating the pressed link.
2347 		return _overElement
2348 			&& _overElement->toggleSelectionByHandlerClick(handler);
2349 	};
2350 
2351 	auto activated = ClickHandler::unpressed();
2352 
2353 	auto simpleSelectionChange = pressState.itemId
2354 		&& !_pressWasInactive
2355 		&& (button != Qt::RightButton)
2356 		&& (_mouseAction == MouseAction::PrepareSelect
2357 			|| _mouseAction == MouseAction::PrepareDrag);
2358 	auto needItemSelectionToggle = simpleSelectionChange
2359 		&& (!activated || toggleByHandler(activated))
2360 		&& hasSelectedItems();
2361 	auto needTextSelectionClear = simpleSelectionChange
2362 		&& hasSelectedText();
2363 
2364 	_wasSelectedText = false;
2365 
2366 	if (_mouseAction == MouseAction::Dragging
2367 		|| _mouseAction == MouseAction::Selecting
2368 		|| needItemSelectionToggle) {
2369 		activated = nullptr;
2370 	} else if (activated) {
2371 		mouseActionCancel();
2372 		ActivateClickHandler(window(), activated, {
2373 			button,
2374 			QVariant::fromValue(ClickHandlerContext{
2375 				.itemId = pressState.itemId,
2376 				.elementDelegate = [weak = Ui::MakeWeak(this)] {
2377 					return weak
2378 						? (ElementDelegate*)weak
2379 						: nullptr;
2380 				},
2381 				.sessionWindow = base::make_weak(_controller.get()),
2382 			})
2383 		});
2384 		return;
2385 	}
2386 	if (needItemSelectionToggle) {
2387 		if (const auto item = session().data().message(pressState.itemId)) {
2388 			clearTextSelection();
2389 			if (pressState.pointState == PointState::GroupPart) {
2390 				changeSelection(
2391 					_selected,
2392 					_overItemExact ? _overItemExact : item,
2393 					SelectAction::Invert);
2394 			} else {
2395 				changeSelectionAsGroup(
2396 					_selected,
2397 					item,
2398 					SelectAction::Invert);
2399 			}
2400 			pushSelectedItems();
2401 		}
2402 	} else if (needTextSelectionClear) {
2403 		clearTextSelection();
2404 	} else if (_mouseAction == MouseAction::Selecting) {
2405 		if (!_dragSelected.empty()) {
2406 			applyDragSelection();
2407 		} else if (_selectedTextItem && !_pressWasInactive) {
2408 			if (_selectedTextRange.from == _selectedTextRange.to) {
2409 				clearTextSelection();
2410 				_controller->widget()->setInnerFocus();
2411 			}
2412 		}
2413 	}
2414 	_mouseAction = MouseAction::None;
2415 	_mouseSelectType = TextSelectType::Letters;
2416 	_selectScroll.cancel();
2417 
2418 	if (QGuiApplication::clipboard()->supportsSelection()
2419 		&& _selectedTextItem
2420 		&& _selectedTextRange.from != _selectedTextRange.to) {
2421 		if (const auto view = viewForItem(_selectedTextItem)) {
2422 			TextUtilities::SetClipboardText(
2423 				view->selectedText(_selectedTextRange),
2424 				QClipboard::Selection);
2425 		}
2426 	}
2427 }
2428 
mouseActionUpdate()2429 void ListWidget::mouseActionUpdate() {
2430 	auto mousePosition = mapFromGlobal(_mousePosition);
2431 	auto point = QPoint(
2432 		std::clamp(mousePosition.x(), 0, width()),
2433 		std::clamp(mousePosition.y(), _visibleTop, _visibleBottom));
2434 
2435 	const auto view = strictFindItemByY(point.y());
2436 	const auto item = view ? view->data().get() : nullptr;
2437 	const auto itemPoint = mapPointToItem(point, view);
2438 	_overState = MouseState(
2439 		item ? item->fullId() : FullMsgId(),
2440 		view ? view->height() : 0,
2441 		itemPoint,
2442 		view ? view->pointState(itemPoint) : PointState::Outside);
2443 	if (_overElement != view) {
2444 		repaintItem(_overElement);
2445 		_overElement = view;
2446 		repaintItem(_overElement);
2447 	}
2448 
2449 	TextState dragState;
2450 	ClickHandlerHost *lnkhost = nullptr;
2451 	auto inTextSelection = (_overState.pointState != PointState::Outside)
2452 		&& (_overState.itemId == _pressState.itemId)
2453 		&& hasSelectedText();
2454 	if (view) {
2455 		auto cursorDeltaLength = [&] {
2456 			auto cursorDelta = (_overState.point - _pressState.point);
2457 			return cursorDelta.manhattanLength();
2458 		};
2459 		auto dragStartLength = [] {
2460 			return QApplication::startDragDistance();
2461 		};
2462 		if (_overState.itemId != _pressState.itemId
2463 			|| cursorDeltaLength() >= dragStartLength()) {
2464 			if (_mouseAction == MouseAction::PrepareDrag) {
2465 				_mouseAction = MouseAction::Dragging;
2466 				InvokeQueued(this, [this] { performDrag(); });
2467 			} else if (_mouseAction == MouseAction::PrepareSelect) {
2468 				_mouseAction = MouseAction::Selecting;
2469 			}
2470 		}
2471 		StateRequest request;
2472 		if (_mouseAction == MouseAction::Selecting) {
2473 			request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
2474 		} else {
2475 			inTextSelection = false;
2476 		}
2477 
2478 		const auto dateHeight = st::msgServicePadding.bottom()
2479 			+ st::msgServiceFont->height
2480 			+ st::msgServicePadding.top();
2481 		const auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);
2482 		enumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) {
2483 			// stop enumeration if the date is above our point
2484 			if (dateTop + dateHeight <= point.y()) {
2485 				return false;
2486 			}
2487 
2488 			const auto displayDate = view->displayDate();
2489 			auto dateInPlace = displayDate;
2490 			if (dateInPlace) {
2491 				const auto correctDateTop = itemtop + st::msgServiceMargin.top();
2492 				dateInPlace = (dateTop < correctDateTop + dateHeight);
2493 			}
2494 
2495 			// stop enumeration if we've found a date under the cursor
2496 			if (dateTop <= point.y()) {
2497 				auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;
2498 				if (opacity > 0.) {
2499 					auto dateWidth = 0;
2500 					if (const auto date = view->Get<HistoryView::DateBadge>()) {
2501 						dateWidth = date->width;
2502 					} else {
2503 						dateWidth = st::msgServiceFont->width(langDayOfMonthFull(view->dateTime().date()));
2504 					}
2505 					dateWidth += st::msgServicePadding.left() + st::msgServicePadding.right();
2506 					auto dateLeft = st::msgServiceMargin.left();
2507 					auto maxwidth = view->width();
2508 					if (_isChatWide) {
2509 						maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
2510 					}
2511 					auto widthForDate = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left();
2512 
2513 					dateLeft += (widthForDate - dateWidth) / 2;
2514 
2515 					if (point.x() >= dateLeft && point.x() < dateLeft + dateWidth) {
2516 						_scrollDateLink = _delegate->listDateLink(view);
2517 						dragState = TextState(
2518 							nullptr,
2519 							_scrollDateLink);
2520 						_overItemExact = session().data().message(dragState.itemId);
2521 						lnkhost = view;
2522 					}
2523 				}
2524 				return false;
2525 			}
2526 			return true;
2527 		});
2528 		if (!dragState.link) {
2529 			dragState = view->textState(itemPoint, request);
2530 			_overItemExact = session().data().message(dragState.itemId);
2531 			lnkhost = view;
2532 			if (!dragState.link
2533 				&& itemPoint.x() >= st::historyPhotoLeft
2534 				&& itemPoint.x() < st::historyPhotoLeft + st::msgPhotoSize) {
2535 				if (view->hasFromPhoto()) {
2536 					enumerateUserpics([&](not_null<Element*> view, int userpicTop) {
2537 						// stop enumeration if the userpic is below our point
2538 						if (userpicTop > point.y()) {
2539 							return false;
2540 						}
2541 
2542 						// stop enumeration if we've found a userpic under the cursor
2543 						if (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) {
2544 							dragState = TextState(nullptr, view->fromPhotoLink());
2545 							_overItemExact = nullptr;
2546 							lnkhost = view;
2547 							return false;
2548 						}
2549 						return true;
2550 					});
2551 				}
2552 			}
2553 		}
2554 	}
2555 	auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost);
2556 	if (lnkChanged || dragState.cursor != _mouseCursorState) {
2557 		Ui::Tooltip::Hide();
2558 	}
2559 	if (dragState.link
2560 		|| dragState.cursor == CursorState::Date
2561 		|| dragState.cursor == CursorState::Forwarded) {
2562 		Ui::Tooltip::Show(1000, this);
2563 	}
2564 
2565 	if (_mouseAction == MouseAction::None) {
2566 		_mouseCursorState = dragState.cursor;
2567 		auto cursor = computeMouseCursor();
2568 		if (_cursor != cursor) {
2569 			setCursor((_cursor = cursor));
2570 		}
2571 	} else if (view) {
2572 		if (_mouseAction == MouseAction::Selecting) {
2573 			if (inTextSelection) {
2574 				auto second = dragState.symbol;
2575 				if (dragState.afterSymbol
2576 					&& _mouseSelectType == TextSelectType::Letters) {
2577 					++second;
2578 				}
2579 				auto selection = TextSelection(
2580 					qMin(second, _mouseTextSymbol),
2581 					qMax(second, _mouseTextSymbol)
2582 				);
2583 				if (_mouseSelectType != TextSelectType::Letters) {
2584 					selection = view->adjustSelection(
2585 						selection,
2586 						_mouseSelectType);
2587 				}
2588 				setTextSelection(view, selection);
2589 				clearDragSelection();
2590 			} else if (_pressState.itemId) {
2591 				updateDragSelection();
2592 			}
2593 		} else if (_mouseAction == MouseAction::Dragging) {
2594 		}
2595 	}
2596 
2597 	// Voice message seek support.
2598 	if (_pressState.pointState != PointState::Outside
2599 		&& ClickHandler::getPressed()) {
2600 		if (const auto item = session().data().message(_pressState.itemId)) {
2601 			if (const auto view = viewForItem(item)) {
2602 				auto adjustedPoint = mapPointToItem(point, view);
2603 				view->updatePressed(adjustedPoint);
2604 			}
2605 		}
2606 	}
2607 
2608 	if (_mouseAction == MouseAction::Selecting) {
2609 		_selectScroll.checkDeltaScroll(
2610 			mousePosition,
2611 			_visibleTop,
2612 			_visibleBottom);
2613 	} else {
2614 		_selectScroll.cancel();
2615 	}
2616 }
2617 
computeMouseCursor() const2618 style::cursor ListWidget::computeMouseCursor() const {
2619 	if (ClickHandler::getPressed() || ClickHandler::getActive()) {
2620 		return style::cur_pointer;
2621 	} else if (!hasSelectedItems()
2622 		&& (_mouseCursorState == CursorState::Text)) {
2623 		return style::cur_text;
2624 	}
2625 	return style::cur_default;
2626 }
2627 
prepareDrag()2628 std::unique_ptr<QMimeData> ListWidget::prepareDrag() {
2629 	if (_mouseAction != MouseAction::Dragging) {
2630 		return nullptr;
2631 	}
2632 	auto pressedHandler = ClickHandler::getPressed();
2633 	if (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.get())) {
2634 		return nullptr;
2635 	}
2636 
2637 	const auto pressedItem = session().data().message(_pressState.itemId);
2638 	const auto pressedView = viewForItem(pressedItem);
2639 	const auto uponSelected = pressedView && isInsideSelection(
2640 		pressedView,
2641 		_pressItemExact ? _pressItemExact : pressedItem,
2642 		_pressState);
2643 
2644 	auto urls = QList<QUrl>();
2645 	const auto selectedText = [&] {
2646 		if (uponSelected) {
2647 			return getSelectedText();
2648 		} else if (pressedHandler) {
2649 			//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
2650 			//	urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
2651 			//}
2652 			return TextForMimeData::Simple(pressedHandler->dragText());
2653 		}
2654 		return TextForMimeData();
2655 	}();
2656 	if (auto mimeData = TextUtilities::MimeDataFromText(selectedText)) {
2657 		clearDragSelection();
2658 		_selectScroll.cancel();
2659 
2660 		if (!urls.isEmpty()) {
2661 			mimeData->setUrls(urls);
2662 		}
2663 		if (uponSelected && !_controller->adaptive().isOneColumn()) {
2664 			const auto canForwardAll = [&] {
2665 				for (const auto &[itemId, data] : _selected) {
2666 					if (!data.canForward) {
2667 						return false;
2668 					}
2669 				}
2670 				return true;
2671 			}();
2672 			auto items = canForwardAll
2673 				? collectSelectedIds()
2674 				: MessageIdsList();
2675 			if (!items.empty()) {
2676 				session().data().setMimeForwardIds(std::move(items));
2677 				mimeData->setData(qsl("application/x-td-forward"), "1");
2678 			}
2679 		}
2680 		return mimeData;
2681 	} else if (pressedView) {
2682 		auto forwardIds = MessageIdsList();
2683 		const auto exactItem = _pressItemExact
2684 			? _pressItemExact
2685 			: pressedItem;
2686 		if (_mouseCursorState == CursorState::Date) {
2687 			if (_overElement->data()->allowsForward()) {
2688 				forwardIds = session().data().itemOrItsGroup(
2689 					_overElement->data());
2690 			}
2691 		} else if (_pressState.pointState == PointState::GroupPart) {
2692 			if (exactItem->allowsForward()) {
2693 				forwardIds = MessageIdsList(1, exactItem->fullId());
2694 			}
2695 		} else if (const auto media = pressedView->media()) {
2696 			if (pressedView->data()->allowsForward()
2697 				&& (media->dragItemByHandler(pressedHandler)
2698 					|| media->dragItem())) {
2699 				forwardIds = MessageIdsList(1, exactItem->fullId());
2700 			}
2701 		}
2702 		if (forwardIds.empty()) {
2703 			return nullptr;
2704 		}
2705 		session().data().setMimeForwardIds(std::move(forwardIds));
2706 		auto result = std::make_unique<QMimeData>();
2707 		result->setData(qsl("application/x-td-forward"), "1");
2708 		if (const auto media = pressedView->media()) {
2709 			if (const auto document = media->getDocument()) {
2710 				const auto filepath = document->filepath(true);
2711 				if (!filepath.isEmpty()) {
2712 					QList<QUrl> urls;
2713 					urls.push_back(QUrl::fromLocalFile(filepath));
2714 					result->setUrls(urls);
2715 				}
2716 			}
2717 		}
2718 		return result;
2719 	}
2720 	return nullptr;
2721 }
2722 
performDrag()2723 void ListWidget::performDrag() {
2724 	if (auto mimeData = prepareDrag()) {
2725 		// This call enters event loop and can destroy any QObject.
2726 		_controller->widget()->launchDrag(
2727 			std::move(mimeData),
2728 			crl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); }));;
2729 	}
2730 }
2731 
itemTop(not_null<const Element * > view) const2732 int ListWidget::itemTop(not_null<const Element*> view) const {
2733 	return _itemsTop + view->y();
2734 }
2735 
repaintItem(const Element * view)2736 void ListWidget::repaintItem(const Element *view) {
2737 	if (!view) {
2738 		return;
2739 	}
2740 	const auto top = itemTop(view);
2741 	const auto range = view->verticalRepaintRange();
2742 	update(0, top + range.top, width(), range.height);
2743 }
2744 
repaintItem(FullMsgId itemId)2745 void ListWidget::repaintItem(FullMsgId itemId) {
2746 	if (const auto view = viewForItem(itemId)) {
2747 		repaintItem(view);
2748 	}
2749 }
2750 
resizeItem(not_null<Element * > view)2751 void ListWidget::resizeItem(not_null<Element*> view) {
2752 	const auto index = ranges::find(_items, view) - begin(_items);
2753 	if (index < int(_items.size())) {
2754 		refreshAttachmentsAtIndex(index);
2755 	}
2756 }
2757 
refreshAttachmentsAtIndex(int index)2758 void ListWidget::refreshAttachmentsAtIndex(int index) {
2759 	Expects(index >= 0 && index < _items.size());
2760 
2761 	const auto from = [&] {
2762 		if (index > 0) {
2763 			for (auto i = index - 1; i != 0; --i) {
2764 				if (!_items[i]->isHidden()) {
2765 					return i;
2766 				}
2767 			}
2768 		}
2769 		return index;
2770 	}();
2771 	const auto till = [&] {
2772 		const auto count = int(_items.size());
2773 		for (auto i = index + 1; i != count; ++i) {
2774 			if (!_items[i]->isHidden()) {
2775 				return i + 1;
2776 			}
2777 		}
2778 		return index + 1;
2779 	}();
2780 	refreshAttachmentsFromTill(from, till);
2781 }
2782 
refreshAttachmentsFromTill(int from,int till)2783 void ListWidget::refreshAttachmentsFromTill(int from, int till) {
2784 	Expects(from >= 0 && from <= till && till <= int(_items.size()));
2785 
2786 	if (from == till) {
2787 		updateSize();
2788 		return;
2789 	}
2790 	auto view = _items[from].get();
2791 	for (auto i = from + 1; i != till; ++i) {
2792 		const auto next = _items[i].get();
2793 		if (next->isHidden()) {
2794 			next->setDisplayDate(false);
2795 		} else {
2796 			const auto viewDate = view->dateTime();
2797 			const auto nextDate = next->dateTime();
2798 			next->setDisplayDate(nextDate.date() != viewDate.date());
2799 			auto attached = next->computeIsAttachToPrevious(view);
2800 			next->setAttachToPrevious(attached);
2801 			view->setAttachToNext(attached);
2802 			view = next;
2803 		}
2804 	}
2805 	if (till == int(_items.size())) {
2806 		_items.back()->setAttachToNext(false);
2807 	}
2808 	updateSize();
2809 }
2810 
refreshItem(not_null<const Element * > view)2811 void ListWidget::refreshItem(not_null<const Element*> view) {
2812 	const auto i = ranges::find(_items, view);
2813 	const auto index = i - begin(_items);
2814 	if (index < int(_items.size())) {
2815 		const auto item = view->data();
2816 		const auto was = [&]() -> std::unique_ptr<Element> {
2817 			if (const auto i = _views.find(item); i != end(_views)) {
2818 				auto result = std::move(i->second);
2819 				_views.erase(i);
2820 				return result;
2821 			}
2822 			return nullptr;
2823 		}();
2824 		const auto [i, ok] = _views.emplace(
2825 			item,
2826 			item->createView(this));
2827 		const auto now = i->second.get();
2828 		_items[index] = now;
2829 
2830 		viewReplaced(view, i->second.get());
2831 
2832 		refreshAttachmentsAtIndex(index);
2833 	}
2834 }
2835 
viewReplaced(not_null<const Element * > was,Element * now)2836 void ListWidget::viewReplaced(not_null<const Element*> was, Element *now) {
2837 	if (_visibleTopItem == was) _visibleTopItem = now;
2838 	if (_scrollDateLastItem == was) _scrollDateLastItem = now;
2839 	if (_overElement == was) _overElement = now;
2840 	if (_bar.element == was.get()) {
2841 		const auto bar = _bar.element->Get<UnreadBar>();
2842 		_bar.element = now;
2843 		if (now && bar) {
2844 			_bar.element->createUnreadBar(_barText.value());
2845 		}
2846 	}
2847 	const auto i = _itemRevealPending.find(was);
2848 	if (i != end(_itemRevealPending)) {
2849 		_itemRevealPending.erase(i);
2850 		if (now) {
2851 			_itemRevealPending.emplace(now);
2852 		}
2853 	}
2854 	const auto j = _itemRevealAnimations.find(was);
2855 	if (j != end(_itemRevealAnimations)) {
2856 		auto data = std::move(j->second);
2857 		_itemRevealAnimations.erase(j);
2858 		if (now) {
2859 			_itemRevealAnimations.emplace(now, std::move(data));
2860 		} else {
2861 			revealItemsCallback();
2862 		}
2863 	}
2864 }
2865 
itemRemoved(not_null<const HistoryItem * > item)2866 void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
2867 	if (_selectedTextItem == item) {
2868 		clearTextSelection();
2869 	}
2870 	if (_overItemExact == item) {
2871 		_overItemExact = nullptr;
2872 	}
2873 	if (_pressItemExact == item) {
2874 		_pressItemExact = nullptr;
2875 	}
2876 	const auto i = _views.find(item);
2877 	if (i == end(_views)) {
2878 		return;
2879 	}
2880 	const auto view = i->second.get();
2881 	_items.erase(
2882 		ranges::remove(_items, view, [](auto view) { return view.get(); }),
2883 		end(_items));
2884 	viewReplaced(view, nullptr);
2885 	_views.erase(i);
2886 
2887 	updateItemsGeometry();
2888 }
2889 
mapPointToItem(QPoint point,const Element * view) const2890 QPoint ListWidget::mapPointToItem(
2891 		QPoint point,
2892 		const Element *view) const {
2893 	if (!view) {
2894 		return QPoint();
2895 	}
2896 	return point - QPoint(0, itemTop(view));
2897 }
2898 
editMessageRequested() const2899 rpl::producer<FullMsgId> ListWidget::editMessageRequested() const {
2900 	return _requestedToEditMessage.events();
2901 }
2902 
editMessageRequestNotify(FullMsgId item) const2903 void ListWidget::editMessageRequestNotify(FullMsgId item) const {
2904 	_requestedToEditMessage.fire(std::move(item));
2905 }
2906 
lastMessageEditRequestNotify() const2907 bool ListWidget::lastMessageEditRequestNotify() const {
2908 	const auto now = base::unixtime::now();
2909 	auto proj = [&](not_null<Element*> view) {
2910 		return view->data()->allowsEdit(now);
2911 	};
2912 	const auto &list = ranges::views::reverse(_items);
2913 	const auto it = ranges::find_if(list, std::move(proj));
2914 	if (it == end(list)) {
2915 		return false;
2916 	} else {
2917 		const auto item =
2918 			session().data().groups().findItemToEdit((*it)->data()).get();
2919 		editMessageRequestNotify(item->fullId());
2920 		return true;
2921 	}
2922 }
2923 
replyToMessageRequested() const2924 rpl::producer<FullMsgId> ListWidget::replyToMessageRequested() const {
2925 	return _requestedToReplyToMessage.events();
2926 }
2927 
replyToMessageRequestNotify(FullMsgId item)2928 void ListWidget::replyToMessageRequestNotify(FullMsgId item) {
2929 	_requestedToReplyToMessage.fire(std::move(item));
2930 }
2931 
readMessageRequested() const2932 rpl::producer<FullMsgId> ListWidget::readMessageRequested() const {
2933 	return _requestedToReadMessage.events();
2934 }
2935 
showMessageRequested() const2936 rpl::producer<FullMsgId> ListWidget::showMessageRequested() const {
2937 	return _requestedToShowMessage.events();
2938 }
2939 
replyNextMessage(FullMsgId fullId,bool next)2940 void ListWidget::replyNextMessage(FullMsgId fullId, bool next) {
2941 	const auto reply = [&](Element *view) {
2942 		if (view) {
2943 			const auto newFullId = view->data()->fullId();
2944 			replyToMessageRequestNotify(newFullId);
2945 			_requestedToShowMessage.fire_copy(newFullId);
2946 		} else {
2947 			replyToMessageRequestNotify(FullMsgId());
2948 			clearHighlightedMessage();
2949 		}
2950 	};
2951 	const auto replyFirst = [&] {
2952 		reply(next ? nullptr : _items.back().get());
2953 	};
2954 	if (!fullId) {
2955 		replyFirst();
2956 		return;
2957 	}
2958 
2959 	auto proj = [&](not_null<Element*> view) {
2960 		return view->data()->fullId() == fullId;
2961 	};
2962 	const auto &list = ranges::views::reverse(_items);
2963 	const auto it = ranges::find_if(list, std::move(proj));
2964 	if (it == end(list)) {
2965 		replyFirst();
2966 		return;
2967 	} else {
2968 		const auto nextIt = it + (next ? -1 : 1);
2969 		if (nextIt == end(list)) {
2970 			return;
2971 		} else if (next && (it == begin(list))) {
2972 			reply(nullptr);
2973 		} else {
2974 			reply(nextIt->get());
2975 		}
2976 	}
2977 }
2978 
setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> && w)2979 void ListWidget::setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w) {
2980 	_emptyInfo = std::move(w);
2981 }
2982 
2983 ListWidget::~ListWidget() = default;
2984 
ConfirmDeleteSelectedItems(not_null<ListWidget * > widget)2985 void ConfirmDeleteSelectedItems(not_null<ListWidget*> widget) {
2986 	const auto items = widget->getSelectedItems();
2987 	if (items.empty()) {
2988 		return;
2989 	}
2990 	for (const auto &item : items) {
2991 		if (!item.canDelete) {
2992 			return;
2993 		}
2994 	}
2995 	const auto weak = Ui::MakeWeak(widget);
2996 	auto box = Box<DeleteMessagesBox>(
2997 		&widget->controller()->session(),
2998 		widget->getSelectedIds());
2999 	box->setDeleteConfirmedCallback([=] {
3000 		if (const auto strong = weak.data()) {
3001 			strong->cancelSelection();
3002 		}
3003 	});
3004 	widget->controller()->show(std::move(box));
3005 }
3006 
ConfirmForwardSelectedItems(not_null<ListWidget * > widget)3007 void ConfirmForwardSelectedItems(not_null<ListWidget*> widget) {
3008 	const auto items = widget->getSelectedItems();
3009 	if (items.empty()) {
3010 		return;
3011 	}
3012 	for (const auto &item : items) {
3013 		if (!item.canForward) {
3014 			return;
3015 		}
3016 	}
3017 	auto ids = widget->getSelectedIds();
3018 	const auto weak = Ui::MakeWeak(widget);
3019 	Window::ShowForwardMessagesBox(widget->controller(), std::move(ids), [=] {
3020 		if (const auto strong = weak.data()) {
3021 			strong->cancelSelection();
3022 		}
3023 	});
3024 }
3025 
ConfirmSendNowSelectedItems(not_null<ListWidget * > widget)3026 void ConfirmSendNowSelectedItems(not_null<ListWidget*> widget) {
3027 	const auto items = widget->getSelectedItems();
3028 	if (items.empty()) {
3029 		return;
3030 	}
3031 	const auto navigation = widget->controller();
3032 	const auto history = [&]() -> History* {
3033 		auto result = (History*)nullptr;
3034 		auto &data = navigation->session().data();
3035 		for (const auto &item : items) {
3036 			if (!item.canSendNow) {
3037 				return nullptr;
3038 			}
3039 			const auto message = data.message(item.msgId);
3040 			if (message) {
3041 				result = message->history();
3042 			}
3043 		}
3044 		return result;
3045 	}();
3046 	if (!history) {
3047 		return;
3048 	}
3049 	const auto clearSelection = [weak = Ui::MakeWeak(widget)] {
3050 		if (const auto strong = weak.data()) {
3051 			strong->cancelSelection();
3052 		}
3053 	};
3054 	Window::ShowSendNowMessagesBox(
3055 		navigation,
3056 		history,
3057 		widget->getSelectedIds(),
3058 		clearSelection);
3059 }
3060 
3061 } // namespace HistoryView
3062