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