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