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/admin_log/history_admin_log_inner.h"
9
10 #include "history/history.h"
11 #include "history/view/media/history_view_media.h"
12 #include "history/view/media/history_view_web_page.h"
13 #include "history/history_message.h"
14 #include "history/history_item_components.h"
15 #include "history/history_item_text.h"
16 #include "history/admin_log/history_admin_log_section.h"
17 #include "history/admin_log/history_admin_log_filter.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 "boxes/sticker_set_box.h"
23 #include "base/platform/base_platform_info.h"
24 #include "base/unixtime.h"
25 #include "mainwindow.h"
26 #include "mainwidget.h"
27 #include "core/application.h"
28 #include "apiwrap.h"
29 #include "api/api_attached_stickers.h"
30 #include "window/window_session_controller.h"
31 #include "main/main_session.h"
32 #include "main/main_session_settings.h"
33 #include "ui/chat/chat_theme.h"
34 #include "ui/chat/chat_style.h"
35 #include "ui/widgets/popup_menu.h"
36 #include "ui/image/image.h"
37 #include "ui/text/text_utilities.h"
38 #include "ui/inactive_press.h"
39 #include "ui/effects/path_shift_gradient.h"
40 #include "core/click_handler_types.h"
41 #include "core/file_utilities.h"
42 #include "lang/lang_keys.h"
43 #include "boxes/peers/edit_participant_box.h"
44 #include "boxes/peers/edit_participants_box.h"
45 #include "data/data_session.h"
46 #include "data/data_photo.h"
47 #include "data/data_photo_media.h"
48 #include "data/data_document.h"
49 #include "data/data_media_types.h"
50 #include "data/data_file_click_handler.h"
51 #include "data/data_file_origin.h"
52 #include "data/data_cloud_file.h"
53 #include "data/data_channel.h"
54 #include "data/data_user.h"
55 #include "facades.h"
56 #include "app.h"
57 #include "styles/style_chat.h"
58
59 #include <QtWidgets/QApplication>
60 #include <QtGui/QClipboard>
61
62 namespace AdminLog {
63 namespace {
64
65 // If we require to support more admins we'll have to rewrite this anyway.
66 constexpr auto kMaxChannelAdmins = 200;
67 constexpr auto kScrollDateHideTimeout = 1000;
68 constexpr auto kEventsFirstPage = 20;
69 constexpr auto kEventsPerPage = 50;
70 constexpr auto kClearUserpicsAfter = 50;
71
72 } // namespace
73
74 template <InnerWidget::EnumItemsDirection direction, typename Method>
enumerateItems(Method method)75 void InnerWidget::enumerateItems(Method method) {
76 constexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom);
77
78 // No displayed messages in this history.
79 if (_items.empty()) {
80 return;
81 }
82 if (_visibleBottom <= _itemsTop || _itemsTop + _itemsHeight <= _visibleTop) {
83 return;
84 }
85
86 auto begin = std::rbegin(_items), end = std::rend(_items);
87 auto from = TopToBottom ? std::lower_bound(begin, end, _visibleTop, [this](auto &elem, int top) {
88 return this->itemTop(elem) + elem->height() <= top;
89 }) : std::upper_bound(begin, end, _visibleBottom, [this](int bottom, auto &elem) {
90 return this->itemTop(elem) + elem->height() >= bottom;
91 });
92 auto wasEnd = (from == end);
93 if (wasEnd) {
94 --from;
95 }
96 if (TopToBottom) {
97 Assert(itemTop(from->get()) + from->get()->height() > _visibleTop);
98 } else {
99 Assert(itemTop(from->get()) < _visibleBottom);
100 }
101
102 while (true) {
103 auto item = from->get();
104 auto itemtop = itemTop(item);
105 auto itembottom = itemtop + item->height();
106
107 // Binary search should've skipped all the items that are above / below the visible area.
108 if (TopToBottom) {
109 Assert(itembottom > _visibleTop);
110 } else {
111 Assert(itemtop < _visibleBottom);
112 }
113
114 if (!method(item, itemtop, itembottom)) {
115 return;
116 }
117
118 // Skip all the items that are below / above the visible area.
119 if (TopToBottom) {
120 if (itembottom >= _visibleBottom) {
121 return;
122 }
123 } else {
124 if (itemtop <= _visibleTop) {
125 return;
126 }
127 }
128
129 if (TopToBottom) {
130 if (++from == end) {
131 break;
132 }
133 } else {
134 if (from == begin) {
135 break;
136 }
137 --from;
138 }
139 }
140 }
141
142 template <typename Method>
enumerateUserpics(Method method)143 void InnerWidget::enumerateUserpics(Method method) {
144 // Find and remember the top of an attached messages pack
145 // -1 means we didn't find an attached to next message yet.
146 int lowestAttachedItemTop = -1;
147
148 auto userpicCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {
149 // Skip all service messages.
150 if (view->data()->isService()) {
151 return true;
152 }
153
154 if (lowestAttachedItemTop < 0 && view->isAttachedToNext()) {
155 lowestAttachedItemTop = itemtop + view->marginTop();
156 }
157
158 // Call method on a userpic for all messages that have it and for those who are not showing it
159 // because of their attachment to the next message if they are bottom-most visible.
160 if (view->displayFromPhoto() || (view->hasFromPhoto() && itembottom >= _visibleBottom)) {
161 if (lowestAttachedItemTop < 0) {
162 lowestAttachedItemTop = itemtop + view->marginTop();
163 }
164 // Attach userpic to the bottom of the visible area with the same margin as the last message.
165 auto userpicMinBottomSkip = st::historyPaddingBottom + st::msgMargin.bottom();
166 auto userpicBottom = qMin(itembottom - view->marginBottom(), _visibleBottom - userpicMinBottomSkip);
167
168 // Do not let the userpic go above the attached messages pack top line.
169 userpicBottom = qMax(userpicBottom, lowestAttachedItemTop + st::msgPhotoSize);
170
171 // Call the template callback function that was passed
172 // and return if it finished everything it needed.
173 if (!method(view, userpicBottom - st::msgPhotoSize)) {
174 return false;
175 }
176 }
177
178 // Forget the found top of the pack, search for the next one from scratch.
179 if (!view->isAttachedToNext()) {
180 lowestAttachedItemTop = -1;
181 }
182
183 return true;
184 };
185
186 enumerateItems<EnumItemsDirection::TopToBottom>(userpicCallback);
187 }
188
189 template <typename Method>
enumerateDates(Method method)190 void InnerWidget::enumerateDates(Method method) {
191 // Find and remember the bottom of an single-day messages pack
192 // -1 means we didn't find a same-day with previous message yet.
193 auto lowestInOneDayItemBottom = -1;
194
195 auto dateCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {
196 const auto item = view->data();
197 if (lowestInOneDayItemBottom < 0 && view->isInOneDayWithPrevious()) {
198 lowestInOneDayItemBottom = itembottom - view->marginBottom();
199 }
200
201 // Call method on a date for all messages that have it and for those who are not showing it
202 // because they are in a one day together with the previous message if they are top-most visible.
203 if (view->displayDate() || (!item->isEmpty() && itemtop <= _visibleTop)) {
204 if (lowestInOneDayItemBottom < 0) {
205 lowestInOneDayItemBottom = itembottom - view->marginBottom();
206 }
207 // Attach date to the top of the visible area with the same margin as it has in service message.
208 auto dateTop = qMax(itemtop, _visibleTop) + st::msgServiceMargin.top();
209
210 // Do not let the date go below the single-day messages pack bottom line.
211 auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
212 dateTop = qMin(dateTop, lowestInOneDayItemBottom - dateHeight);
213
214 // Call the template callback function that was passed
215 // and return if it finished everything it needed.
216 if (!method(view, itemtop, dateTop)) {
217 return false;
218 }
219 }
220
221 // Forget the found bottom of the pack, search for the next one from scratch.
222 if (!view->isInOneDayWithPrevious()) {
223 lowestInOneDayItemBottom = -1;
224 }
225
226 return true;
227 };
228
229 enumerateItems<EnumItemsDirection::BottomToTop>(dateCallback);
230 }
231
InnerWidget(QWidget * parent,not_null<Window::SessionController * > controller,not_null<ChannelData * > channel)232 InnerWidget::InnerWidget(
233 QWidget *parent,
234 not_null<Window::SessionController*> controller,
235 not_null<ChannelData*> channel)
236 : RpWidget(parent)
237 , _controller(controller)
238 , _channel(channel)
239 , _history(channel->owner().history(channel))
240 , _api(&_channel->session().mtp())
241 , _pathGradient(
242 HistoryView::MakePathShiftGradient(
243 controller->chatStyle(),
244 [=] { update(); }))
__anon4173873e0702null245 , _scrollDateCheck([=] { scrollDateCheck(); })
246 , _emptyText(
247 st::historyAdminLogEmptyWidth
248 - st::historyAdminLogEmptyPadding.left()
249 - st::historyAdminLogEmptyPadding.left()) {
250 Window::ChatThemeValueFromPeer(
251 controller,
252 channel
__anon4173873e0802(std::shared_ptr<Ui::ChatTheme> &&theme) 253 ) | rpl::start_with_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {
254 _theme = std::move(theme);
255 controller->setChatStyleTheme(_theme);
256 }, lifetime());
257
258 setMouseTracking(true);
__anon4173873e0902null259 _scrollDateHideTimer.setCallback([=] { scrollDateHideByTimer(); });
260 session().data().viewRepaintRequest(
__anon4173873e0a02(auto view) 261 ) | rpl::start_with_next([=](auto view) {
262 if (view->delegate() == this) {
263 repaintItem(view);
264 }
265 }, lifetime());
266 session().data().viewResizeRequest(
__anon4173873e0b02(auto view) 267 ) | rpl::start_with_next([=](auto view) {
268 if (view->delegate() == this) {
269 resizeItem(view);
270 }
271 }, lifetime());
272 session().data().itemViewRefreshRequest(
__anon4173873e0c02(auto item) 273 ) | rpl::start_with_next([=](auto item) {
274 if (const auto view = viewForItem(item)) {
275 refreshItem(view);
276 }
277 }, lifetime());
278 session().data().viewLayoutChanged(
__anon4173873e0d02(auto view) 279 ) | rpl::start_with_next([=](auto view) {
280 if (view->delegate() == this) {
281 if (view->isUnderCursor()) {
282 updateSelected();
283 }
284 }
285 }, lifetime());
286 session().data().animationPlayInlineRequest(
__anon4173873e0e02(auto item) 287 ) | rpl::start_with_next([=](auto item) {
288 if (const auto view = viewForItem(item)) {
289 if (const auto media = view->media()) {
290 media->playAnimation();
291 }
292 }
293 }, lifetime());
294 subscribe(session().data().queryItemVisibility(), [=](
__anon4173873e0f02( const Data::Session::ItemVisibilityQuery &query) 295 const Data::Session::ItemVisibilityQuery &query) {
296 if (_history != query.item->history()
297 || !query.item->isAdminLogEntry()
298 || !isVisible()) {
299 return;
300 }
301 if (const auto view = viewForItem(query.item)) {
302 auto top = itemTop(view);
303 if (top >= 0 && top + view->height() > _visibleTop && top < _visibleBottom) {
304 *query.isVisible = true;
305 }
306 }
307 });
308 updateEmptyText();
309
310 requestAdmins();
311 }
312
session() const313 Main::Session &InnerWidget::session() const {
314 return _controller->session();
315 }
316
showSearchSignal() const317 rpl::producer<> InnerWidget::showSearchSignal() const {
318 return _showSearchSignal.events();
319 }
320
scrollToSignal() const321 rpl::producer<int> InnerWidget::scrollToSignal() const {
322 return _scrollToSignal.events();
323 }
324
cancelSignal() const325 rpl::producer<> InnerWidget::cancelSignal() const {
326 return _cancelSignal.events();
327 }
328
visibleTopBottomUpdated(int visibleTop,int visibleBottom)329 void InnerWidget::visibleTopBottomUpdated(
330 int visibleTop,
331 int visibleBottom) {
332 auto scrolledUp = (visibleTop < _visibleTop);
333 _visibleTop = visibleTop;
334 _visibleBottom = visibleBottom;
335
336 // Unload userpics.
337 if (_userpics.size() > kClearUserpicsAfter) {
338 _userpicsCache = std::move(_userpics);
339 }
340
341 updateVisibleTopItem();
342 checkPreloadMore();
343 if (scrolledUp) {
344 _scrollDateCheck.call();
345 } else {
346 scrollDateHideByTimer();
347 }
348 _controller->floatPlayerAreaUpdated();
349 }
350
updateVisibleTopItem()351 void InnerWidget::updateVisibleTopItem() {
352 if (_visibleBottom == height()) {
353 _visibleTopItem = nullptr;
354 } else {
355 auto begin = std::rbegin(_items), end = std::rend(_items);
356 auto from = std::lower_bound(begin, end, _visibleTop, [this](auto &&elem, int top) {
357 return this->itemTop(elem) + elem->height() <= top;
358 });
359 if (from != end) {
360 _visibleTopItem = *from;
361 _visibleTopFromItem = _visibleTop - _visibleTopItem->y();
362 } else {
363 _visibleTopItem = nullptr;
364 _visibleTopFromItem = _visibleTop;
365 }
366 }
367 }
368
displayScrollDate() const369 bool InnerWidget::displayScrollDate() const {
370 return (_visibleTop <= height() - 2 * (_visibleBottom - _visibleTop));
371 }
372
scrollDateCheck()373 void InnerWidget::scrollDateCheck() {
374 if (!_visibleTopItem) {
375 _scrollDateLastItem = nullptr;
376 _scrollDateLastItemTop = 0;
377 scrollDateHide();
378 } else if (_visibleTopItem != _scrollDateLastItem || _visibleTopFromItem != _scrollDateLastItemTop) {
379 // Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem).
380 if (_scrollDateLastItem && !_scrollDateShown) {
381 toggleScrollDateShown();
382 }
383 _scrollDateLastItem = _visibleTopItem;
384 _scrollDateLastItemTop = _visibleTopFromItem;
385 _scrollDateHideTimer.callOnce(kScrollDateHideTimeout);
386 }
387 }
388
scrollDateHideByTimer()389 void InnerWidget::scrollDateHideByTimer() {
390 _scrollDateHideTimer.cancel();
391 scrollDateHide();
392 }
393
scrollDateHide()394 void InnerWidget::scrollDateHide() {
395 if (_scrollDateShown) {
396 toggleScrollDateShown();
397 }
398 }
399
toggleScrollDateShown()400 void InnerWidget::toggleScrollDateShown() {
401 _scrollDateShown = !_scrollDateShown;
402 auto from = _scrollDateShown ? 0. : 1.;
403 auto to = _scrollDateShown ? 1. : 0.;
404 _scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration);
405 }
406
repaintScrollDateCallback()407 void InnerWidget::repaintScrollDateCallback() {
408 auto updateTop = _visibleTop;
409 auto updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();
410 update(0, updateTop, width(), updateHeight);
411 }
412
checkPreloadMore()413 void InnerWidget::checkPreloadMore() {
414 if (_visibleTop + PreloadHeightsCount * (_visibleBottom - _visibleTop) > height()) {
415 preloadMore(Direction::Down);
416 }
417 if (_visibleTop < PreloadHeightsCount * (_visibleBottom - _visibleTop)) {
418 preloadMore(Direction::Up);
419 }
420 }
421
applyFilter(FilterValue && value)422 void InnerWidget::applyFilter(FilterValue &&value) {
423 if (_filter != value) {
424 _filter = value;
425 clearAndRequestLog();
426 }
427 }
428
applySearch(const QString & query)429 void InnerWidget::applySearch(const QString &query) {
430 auto clearQuery = query.trimmed();
431 if (_searchQuery != query) {
432 _searchQuery = query;
433 clearAndRequestLog();
434 }
435 }
436
requestAdmins()437 void InnerWidget::requestAdmins() {
438 const auto offset = 0;
439 const auto participantsHash = uint64(0);
440 _api.request(MTPchannels_GetParticipants(
441 _channel->inputChannel,
442 MTP_channelParticipantsAdmins(),
443 MTP_int(offset),
444 MTP_int(kMaxChannelAdmins),
445 MTP_long(participantsHash)
446 )).done([=](const MTPchannels_ChannelParticipants &result) {
447 session().api().parseChannelParticipants(_channel, result, [&](
448 int availableCount,
449 const QVector<MTPChannelParticipant> &list) {
450 auto filtered = (
451 list
452 ) | ranges::views::transform([&](const MTPChannelParticipant &p) {
453 const auto participantId = p.match([](
454 const MTPDchannelParticipantBanned &data) {
455 return peerFromMTP(data.vpeer());
456 }, [](const MTPDchannelParticipantLeft &data) {
457 return peerFromMTP(data.vpeer());
458 }, [](const auto &data) {
459 return peerFromUser(data.vuser_id());
460 });
461 const auto canEdit = p.match([](
462 const MTPDchannelParticipantAdmin &data) {
463 return data.is_can_edit();
464 }, [](const auto &) {
465 return false;
466 });
467 return std::make_pair(participantId, canEdit);
468 }) | ranges::views::transform([&](auto &&pair) {
469 return std::make_pair(
470 (peerIsUser(pair.first)
471 ? session().data().userLoaded(
472 peerToUser(pair.first))
473 : nullptr),
474 pair.second);
475 }) | ranges::views::filter([&](auto &&pair) {
476 return (pair.first != nullptr);
477 });
478
479 for (auto [user, canEdit] : filtered) {
480 _admins.emplace_back(user);
481 if (canEdit) {
482 _adminsCanEdit.emplace_back(user);
483 }
484 }
485 });
486 if (_admins.empty()) {
487 _admins.push_back(session().user());
488 }
489 if (_showFilterCallback) {
490 showFilter(std::move(_showFilterCallback));
491 }
492 }).send();
493 }
494
showFilter(Fn<void (FilterValue && filter)> callback)495 void InnerWidget::showFilter(Fn<void(FilterValue &&filter)> callback) {
496 if (_admins.empty()) {
497 _showFilterCallback = std::move(callback);
498 } else {
499 _controller->show(
500 Box<FilterBox>(_channel, _admins, _filter, std::move(callback)));
501 }
502 }
503
clearAndRequestLog()504 void InnerWidget::clearAndRequestLog() {
505 _api.request(base::take(_preloadUpRequestId)).cancel();
506 _api.request(base::take(_preloadDownRequestId)).cancel();
507 _filterChanged = true;
508 _upLoaded = false;
509 _downLoaded = true;
510 updateMinMaxIds();
511 preloadMore(Direction::Up);
512 }
513
updateEmptyText()514 void InnerWidget::updateEmptyText() {
515 auto options = _defaultOptions;
516 options.flags |= TextParseMarkdown;
517 auto hasSearch = !_searchQuery.isEmpty();
518 auto hasFilter = (_filter.flags != 0) || !_filter.allUsers;
519 auto text = Ui::Text::Semibold((hasSearch || hasFilter)
520 ? tr::lng_admin_log_no_results_title(tr::now)
521 : tr::lng_admin_log_no_events_title(tr::now));
522 auto description = hasSearch
523 ? tr::lng_admin_log_no_results_search_text(
524 tr::now,
525 lt_query,
526 TextUtilities::Clean(_searchQuery))
527 : hasFilter
528 ? tr::lng_admin_log_no_results_text(tr::now)
529 : _channel->isMegagroup()
530 ? tr::lng_admin_log_no_events_text(tr::now)
531 : tr::lng_admin_log_no_events_text_channel(tr::now);
532 text.text.append(qstr("\n\n") + description);
533 _emptyText.setMarkedText(st::defaultTextStyle, text, options);
534 }
535
tooltipText() const536 QString InnerWidget::tooltipText() const {
537 if (_mouseCursorState == CursorState::Date
538 && _mouseAction == MouseAction::None) {
539 if (const auto view = App::hoveredItem()) {
540 const auto format = QLocale::system().dateTimeFormat(
541 QLocale::LongFormat);
542 auto dateText = HistoryView::DateTooltipText(view);
543
544 const auto sentIt = _itemDates.find(view->data());
545 if (sentIt != end(_itemDates)) {
546 dateText += '\n' + tr::lng_sent_date(
547 tr::now,
548 lt_date,
549 base::unixtime::parse(sentIt->second).toString(format));
550 }
551 return dateText;
552 }
553 } else if (_mouseCursorState == CursorState::Forwarded
554 && _mouseAction == MouseAction::None) {
555 if (const auto view = App::hoveredItem()) {
556 if (const auto forwarded = view->data()->Get<HistoryMessageForwarded>()) {
557 return forwarded->text.toString();
558 }
559 }
560 } else if (const auto lnk = ClickHandler::getActive()) {
561 return lnk->tooltip();
562 }
563 return QString();
564 }
565
tooltipPos() const566 QPoint InnerWidget::tooltipPos() const {
567 return _mousePosition;
568 }
569
tooltipWindowActive() const570 bool InnerWidget::tooltipWindowActive() const {
571 return Ui::AppInFocus() && Ui::InFocusChain(window());
572 }
573
elementContext()574 HistoryView::Context InnerWidget::elementContext() {
575 return HistoryView::Context::AdminLog;
576 }
577
elementCreate(not_null<HistoryMessage * > message,Element * replacing)578 std::unique_ptr<HistoryView::Element> InnerWidget::elementCreate(
579 not_null<HistoryMessage*> message,
580 Element *replacing) {
581 return std::make_unique<HistoryView::Message>(this, message, replacing);
582 }
583
elementCreate(not_null<HistoryService * > message,Element * replacing)584 std::unique_ptr<HistoryView::Element> InnerWidget::elementCreate(
585 not_null<HistoryService*> message,
586 Element *replacing) {
587 return std::make_unique<HistoryView::Service>(this, message, replacing);
588 }
589
elementUnderCursor(not_null<const HistoryView::Element * > view)590 bool InnerWidget::elementUnderCursor(
591 not_null<const HistoryView::Element*> view) {
592 return (App::hoveredItem() == view);
593 }
594
elementHighlightTime(not_null<const HistoryItem * > item)595 crl::time InnerWidget::elementHighlightTime(
596 not_null<const HistoryItem*> item) {
597 return crl::time(0);
598 }
599
elementInSelectionMode()600 bool InnerWidget::elementInSelectionMode() {
601 return false;
602 }
603
elementIntersectsRange(not_null<const Element * > view,int from,int till)604 bool InnerWidget::elementIntersectsRange(
605 not_null<const Element*> view,
606 int from,
607 int till) {
608 Expects(view->delegate() == this);
609
610 const auto top = itemTop(view);
611 const auto bottom = top + view->height();
612 return (top < till && bottom > from);
613 }
614
elementStartStickerLoop(not_null<const Element * > view)615 void InnerWidget::elementStartStickerLoop(not_null<const Element*> view) {
616 }
617
elementShowPollResults(not_null<PollData * > poll,FullMsgId context)618 void InnerWidget::elementShowPollResults(
619 not_null<PollData*> poll,
620 FullMsgId context) {
621 }
622
elementOpenPhoto(not_null<PhotoData * > photo,FullMsgId context)623 void InnerWidget::elementOpenPhoto(
624 not_null<PhotoData*> photo,
625 FullMsgId context) {
626 _controller->openPhoto(photo, context);
627 }
628
elementOpenDocument(not_null<DocumentData * > document,FullMsgId context,bool showInMediaView)629 void InnerWidget::elementOpenDocument(
630 not_null<DocumentData*> document,
631 FullMsgId context,
632 bool showInMediaView) {
633 _controller->openDocument(document, context, showInMediaView);
634 }
635
elementCancelUpload(const FullMsgId & context)636 void InnerWidget::elementCancelUpload(const FullMsgId &context) {
637 if (const auto item = session().data().message(context)) {
638 _controller->cancelUploadLayer(item);
639 }
640 }
641
elementShowTooltip(const TextWithEntities & text,Fn<void ()> hiddenCallback)642 void InnerWidget::elementShowTooltip(
643 const TextWithEntities &text,
644 Fn<void()> hiddenCallback) {
645 }
646
elementIsGifPaused()647 bool InnerWidget::elementIsGifPaused() {
648 return _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any);
649 }
650
elementHideReply(not_null<const Element * > view)651 bool InnerWidget::elementHideReply(not_null<const Element*> view) {
652 return true;
653 }
654
elementShownUnread(not_null<const Element * > view)655 bool InnerWidget::elementShownUnread(not_null<const Element*> view) {
656 return view->data()->unread();
657 }
658
elementSendBotCommand(const QString & command,const FullMsgId & context)659 void InnerWidget::elementSendBotCommand(
660 const QString &command,
661 const FullMsgId &context) {
662 }
663
elementHandleViaClick(not_null<UserData * > bot)664 void InnerWidget::elementHandleViaClick(not_null<UserData*> bot) {
665 }
666
elementIsChatWide()667 bool InnerWidget::elementIsChatWide() {
668 return _controller->adaptive().isChatWide();
669 }
670
elementPathShiftGradient()671 not_null<Ui::PathShiftGradient*> InnerWidget::elementPathShiftGradient() {
672 return _pathGradient.get();
673 }
674
elementReplyTo(const FullMsgId & to)675 void InnerWidget::elementReplyTo(const FullMsgId &to) {
676 }
677
elementStartInteraction(not_null<const Element * > view)678 void InnerWidget::elementStartInteraction(not_null<const Element*> view) {
679 }
680
saveState(not_null<SectionMemento * > memento)681 void InnerWidget::saveState(not_null<SectionMemento*> memento) {
682 memento->setFilter(std::move(_filter));
683 memento->setAdmins(std::move(_admins));
684 memento->setAdminsCanEdit(std::move(_adminsCanEdit));
685 memento->setSearchQuery(std::move(_searchQuery));
686 if (!_filterChanged) {
687 for (auto &item : _items) {
688 item.clearView();
689 }
690 memento->setItems(
691 base::take(_items),
692 base::take(_eventIds),
693 _upLoaded,
694 _downLoaded);
695 base::take(_itemsByData);
696 }
697 _upLoaded = _downLoaded = true; // Don't load or handle anything anymore.
698 }
699
restoreState(not_null<SectionMemento * > memento)700 void InnerWidget::restoreState(not_null<SectionMemento*> memento) {
701 _items = memento->takeItems();
702 for (auto &item : _items) {
703 item.refreshView(this);
704 _itemsByData.emplace(item->data(), item.get());
705 }
706 _eventIds = memento->takeEventIds();
707 _admins = memento->takeAdmins();
708 _adminsCanEdit = memento->takeAdminsCanEdit();
709 _filter = memento->takeFilter();
710 _searchQuery = memento->takeSearchQuery();
711 _upLoaded = memento->upLoaded();
712 _downLoaded = memento->downLoaded();
713 _filterChanged = false;
714 updateMinMaxIds();
715 updateSize();
716 }
717
preloadMore(Direction direction)718 void InnerWidget::preloadMore(Direction direction) {
719 auto &requestId = (direction == Direction::Up) ? _preloadUpRequestId : _preloadDownRequestId;
720 auto &loadedFlag = (direction == Direction::Up) ? _upLoaded : _downLoaded;
721 if (requestId != 0 || loadedFlag) {
722 return;
723 }
724
725 auto flags = MTPchannels_GetAdminLog::Flags(0);
726 auto filter = MTP_channelAdminLogEventsFilter(MTP_flags(_filter.flags));
727 if (_filter.flags != 0) {
728 flags |= MTPchannels_GetAdminLog::Flag::f_events_filter;
729 }
730 auto admins = QVector<MTPInputUser>(0);
731 if (!_filter.allUsers) {
732 if (!_filter.admins.empty()) {
733 admins.reserve(_filter.admins.size());
734 for (auto &admin : _filter.admins) {
735 admins.push_back(admin->inputUser);
736 }
737 }
738 flags |= MTPchannels_GetAdminLog::Flag::f_admins;
739 }
740 auto maxId = (direction == Direction::Up) ? _minId : 0;
741 auto minId = (direction == Direction::Up) ? 0 : _maxId;
742 auto perPage = _items.empty() ? kEventsFirstPage : kEventsPerPage;
743 requestId = _api.request(MTPchannels_GetAdminLog(
744 MTP_flags(flags),
745 _channel->inputChannel,
746 MTP_string(_searchQuery),
747 filter,
748 MTP_vector<MTPInputUser>(admins),
749 MTP_long(maxId),
750 MTP_long(minId),
751 MTP_int(perPage)
752 )).done([=, &requestId, &loadedFlag](const MTPchannels_AdminLogResults &result) {
753 Expects(result.type() == mtpc_channels_adminLogResults);
754
755 requestId = 0;
756
757 auto &results = result.c_channels_adminLogResults();
758 _channel->owner().processUsers(results.vusers());
759 _channel->owner().processChats(results.vchats());
760 if (!loadedFlag) {
761 addEvents(direction, results.vevents().v);
762 }
763 }).fail([this, &requestId, &loadedFlag](const MTP::Error &error) {
764 requestId = 0;
765 loadedFlag = true;
766 update();
767 }).send();
768 }
769
addEvents(Direction direction,const QVector<MTPChannelAdminLogEvent> & events)770 void InnerWidget::addEvents(Direction direction, const QVector<MTPChannelAdminLogEvent> &events) {
771 if (_filterChanged) {
772 clearAfterFilterChange();
773 }
774
775 auto up = (direction == Direction::Up);
776 if (events.empty()) {
777 (up ? _upLoaded : _downLoaded) = true;
778 update();
779 return;
780 }
781
782 // When loading items up we just add them to the back of the _items vector.
783 // When loading items down we add them to a new vector and copy _items after them.
784 auto newItemsForDownDirection = std::vector<OwnedItem>();
785 auto oldItemsCount = _items.size();
786 auto &addToItems = (direction == Direction::Up)
787 ? _items
788 : newItemsForDownDirection;
789 addToItems.reserve(oldItemsCount + events.size() * 2);
790 for (const auto &event : events) {
791 event.match([&](const MTPDchannelAdminLogEvent &data) {
792 const auto id = data.vid().v;
793 if (_eventIds.find(id) != _eventIds.end()) {
794 return;
795 }
796
797 auto count = 0;
798 const auto addOne = [&](OwnedItem item, TimeId sentDate) {
799 if (sentDate) {
800 _itemDates.emplace(item->data(), sentDate);
801 }
802 _eventIds.emplace(id);
803 _itemsByData.emplace(item->data(), item.get());
804 addToItems.push_back(std::move(item));
805 ++count;
806 };
807 GenerateItems(
808 this,
809 _history,
810 data,
811 addOne);
812 if (count > 1) {
813 // Reverse the inner order of the added messages, because we load events
814 // from bottom to top but inside one event they go from top to bottom.
815 auto full = addToItems.size();
816 auto from = full - count;
817 for (auto i = 0, toReverse = count / 2; i != toReverse; ++i) {
818 std::swap(addToItems[from + i], addToItems[full - i - 1]);
819 }
820 }
821 });
822 }
823 auto newItemsCount = _items.size() + ((direction == Direction::Up) ? 0 : newItemsForDownDirection.size());
824 if (newItemsCount != oldItemsCount) {
825 if (direction == Direction::Down) {
826 for (auto &item : _items) {
827 newItemsForDownDirection.push_back(std::move(item));
828 }
829 _items = std::move(newItemsForDownDirection);
830 }
831 updateMinMaxIds();
832 itemsAdded(direction, newItemsCount - oldItemsCount);
833 }
834 update();
835 }
836
updateMinMaxIds()837 void InnerWidget::updateMinMaxIds() {
838 if (_eventIds.empty() || _filterChanged) {
839 _maxId = _minId = 0;
840 } else {
841 _maxId = *_eventIds.rbegin();
842 _minId = *_eventIds.begin();
843 if (_minId == 1) {
844 _upLoaded = true;
845 }
846 }
847 }
848
itemsAdded(Direction direction,int addedCount)849 void InnerWidget::itemsAdded(Direction direction, int addedCount) {
850 Expects(addedCount >= 0);
851 auto checkFrom = (direction == Direction::Up)
852 ? (_items.size() - addedCount)
853 : 1; // Should be ": 0", but zero is skipped anyway.
854 auto checkTo = (direction == Direction::Up) ? (_items.size() + 1) : (addedCount + 1);
855 for (auto i = checkFrom; i != checkTo; ++i) {
856 if (i > 0) {
857 const auto view = _items[i - 1].get();
858 if (i < _items.size()) {
859 const auto previous = _items[i].get();
860 view->setDisplayDate(view->dateTime().date() != previous->dateTime().date());
861 const auto attach = view->computeIsAttachToPrevious(previous);
862 view->setAttachToPrevious(attach);
863 previous->setAttachToNext(attach);
864 } else {
865 view->setDisplayDate(true);
866 }
867 }
868 }
869 updateSize();
870 }
871
updateSize()872 void InnerWidget::updateSize() {
873 TWidget::resizeToWidth(width());
874 restoreScrollPosition();
875 updateVisibleTopItem();
876 checkPreloadMore();
877 }
878
resizeGetHeight(int newWidth)879 int InnerWidget::resizeGetHeight(int newWidth) {
880 update();
881
882 const auto resizeAllItems = (_itemsWidth != newWidth);
883 auto newHeight = 0;
884 for (const auto &item : ranges::views::reverse(_items)) {
885 item->setY(newHeight);
886 if (item->pendingResize() || resizeAllItems) {
887 newHeight += item->resizeGetHeight(newWidth);
888 } else {
889 newHeight += item->height();
890 }
891 }
892 _itemsWidth = newWidth;
893 _itemsHeight = newHeight;
894 _itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom) ? (_minHeight - _itemsHeight - st::historyPaddingBottom) : 0;
895 return _itemsTop + _itemsHeight + st::historyPaddingBottom;
896 }
897
restoreScrollPosition()898 void InnerWidget::restoreScrollPosition() {
899 const auto newVisibleTop = _visibleTopItem
900 ? (itemTop(_visibleTopItem) + _visibleTopFromItem)
901 : ScrollMax;
902 _scrollToSignal.fire_copy(newVisibleTop);
903 }
904
paintEvent(QPaintEvent * e)905 void InnerWidget::paintEvent(QPaintEvent *e) {
906 if (Ui::skipPaintEvent(this, e)) {
907 return;
908 }
909
910 const auto guard = gsl::finally([&] {
911 _userpicsCache.clear();
912 });
913
914 Painter p(this);
915
916 auto clip = e->rect();
917 auto context = _controller->preparePaintContext({
918 .theme = _theme.get(),
919 .visibleAreaTop = _visibleTop,
920 .visibleAreaTopGlobal = mapToGlobal(QPoint(0, _visibleTop)).y(),
921 .visibleAreaWidth = width(),
922 .clip = clip,
923 });
924 if (_items.empty() && _upLoaded && _downLoaded) {
925 paintEmpty(p, context.st);
926 } else {
927 _pathGradient->startFrame(
928 0,
929 width(),
930 std::min(st::msgMaxWidth / 2, width() / 2));
931
932 auto begin = std::rbegin(_items), end = std::rend(_items);
933 auto from = std::lower_bound(begin, end, clip.top(), [this](auto &elem, int top) {
934 return this->itemTop(elem) + elem->height() <= top;
935 });
936 auto to = std::lower_bound(begin, end, clip.top() + clip.height(), [this](auto &elem, int bottom) {
937 return this->itemTop(elem) < bottom;
938 });
939 if (from != end) {
940 auto top = itemTop(from->get());
941 context.translate(0, -top);
942 p.translate(0, top);
943 for (auto i = from; i != to; ++i) {
944 const auto view = i->get();
945 context.outbg = view->hasOutLayout();
946 context.selection = (view == _selectedItem)
947 ? _selectedText
948 : TextSelection();
949 view->draw(p, context);
950
951 const auto height = view->height();
952 top += height;
953 context.viewport.translate(0, -height);
954 context.clip.translate(0, -height);
955 p.translate(0, height);
956 }
957 p.translate(0, -top);
958
959 enumerateUserpics([&](not_null<Element*> view, int userpicTop) {
960 // stop the enumeration if the userpic is below the painted rect
961 if (userpicTop >= clip.top() + clip.height()) {
962 return false;
963 }
964
965 // paint the userpic if it intersects the painted rect
966 if (userpicTop + st::msgPhotoSize > clip.top()) {
967 const auto from = view->data()->from();
968 from->paintUserpicLeft(
969 p,
970 _userpics[from],
971 st::historyPhotoLeft,
972 userpicTop,
973 view->width(),
974 st::msgPhotoSize);
975 }
976 return true;
977 });
978
979 auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
980 auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);
981 enumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) {
982 // stop the enumeration if the date is above the painted rect
983 if (dateTop + dateHeight <= clip.top()) {
984 return false;
985 }
986
987 const auto displayDate = view->displayDate();
988 auto dateInPlace = displayDate;
989 if (dateInPlace) {
990 const auto correctDateTop = itemtop + st::msgServiceMargin.top();
991 dateInPlace = (dateTop < correctDateTop + dateHeight);
992 }
993 //bool noFloatingDate = (item->date.date() == lastDate && displayDate);
994 //if (noFloatingDate) {
995 // if (itemtop < showFloatingBefore) {
996 // noFloatingDate = false;
997 // }
998 //}
999
1000 // paint the date if it intersects the painted rect
1001 if (dateTop < clip.top() + clip.height()) {
1002 auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;
1003 if (opacity > 0.) {
1004 p.setOpacity(opacity);
1005 const auto dateY = /*noFloatingDate ? itemtop :*/ (dateTop - st::msgServiceMargin.top());
1006 const auto width = view->width();
1007 const auto chatWide =
1008 _controller->adaptive().isChatWide();
1009 if (const auto date = view->Get<HistoryView::DateBadge>()) {
1010 date->paint(p, context.st, dateY, width, chatWide);
1011 } else {
1012 HistoryView::ServiceMessagePainter::PaintDate(
1013 p,
1014 context.st,
1015 view->dateTime(),
1016 dateY,
1017 width,
1018 chatWide);
1019 }
1020 }
1021 }
1022 return true;
1023 });
1024 }
1025 }
1026 }
1027
clearAfterFilterChange()1028 void InnerWidget::clearAfterFilterChange() {
1029 _visibleTopItem = nullptr;
1030 _visibleTopFromItem = 0;
1031 _scrollDateLastItem = nullptr;
1032 _scrollDateLastItemTop = 0;
1033 _mouseActionItem = nullptr;
1034 _selectedItem = nullptr;
1035 _selectedText = TextSelection();
1036 _filterChanged = false;
1037 _items.clear();
1038 _eventIds.clear();
1039 _itemsByData.clear();
1040 updateEmptyText();
1041 updateSize();
1042 }
1043
viewForItem(const HistoryItem * item)1044 auto InnerWidget::viewForItem(const HistoryItem *item) -> Element* {
1045 if (item) {
1046 const auto i = _itemsByData.find(item);
1047 if (i != _itemsByData.end()) {
1048 return i->second;
1049 }
1050 }
1051 return nullptr;
1052 }
1053
paintEmpty(Painter & p,not_null<const Ui::ChatStyle * > st)1054 void InnerWidget::paintEmpty(Painter &p, not_null<const Ui::ChatStyle*> st) {
1055 auto rectWidth = st::historyAdminLogEmptyWidth;
1056 auto innerWidth = rectWidth - st::historyAdminLogEmptyPadding.left() - st::historyAdminLogEmptyPadding.right();
1057 auto rectHeight = st::historyAdminLogEmptyPadding.top() + _emptyText.countHeight(innerWidth) + st::historyAdminLogEmptyPadding.bottom();
1058 auto rect = QRect((width() - rectWidth) / 2, (height() - rectHeight) / 3, rectWidth, rectHeight);
1059 HistoryView::ServiceMessagePainter::PaintBubble(p, st, rect);
1060
1061 p.setPen(st->msgServiceFg());
1062 _emptyText.draw(p, rect.x() + st::historyAdminLogEmptyPadding.left(), rect.y() + st::historyAdminLogEmptyPadding.top(), innerWidth, style::al_top);
1063 }
1064
getSelectedText() const1065 TextForMimeData InnerWidget::getSelectedText() const {
1066 return _selectedItem
1067 ? _selectedItem->selectedText(_selectedText)
1068 : TextForMimeData();
1069 }
1070
keyPressEvent(QKeyEvent * e)1071 void InnerWidget::keyPressEvent(QKeyEvent *e) {
1072 if (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) {
1073 _cancelSignal.fire({});
1074 } else if (e == QKeySequence::Copy && _selectedItem != nullptr) {
1075 copySelectedText();
1076 #ifdef Q_OS_MAC
1077 } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
1078 TextUtilities::SetClipboardText(getSelectedText(), QClipboard::FindBuffer);
1079 #endif // Q_OS_MAC
1080 } else {
1081 e->ignore();
1082 }
1083 }
1084
mouseDoubleClickEvent(QMouseEvent * e)1085 void InnerWidget::mouseDoubleClickEvent(QMouseEvent *e) {
1086 mouseActionStart(e->globalPos(), e->button());
1087 if (((_mouseAction == MouseAction::Selecting && _selectedItem != nullptr) || (_mouseAction == MouseAction::None)) && _mouseSelectType == TextSelectType::Letters && _mouseActionItem) {
1088 StateRequest request;
1089 request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
1090 auto dragState = _mouseActionItem->textState(_dragStartPosition, request);
1091 if (dragState.cursor == CursorState::Text) {
1092 _mouseTextSymbol = dragState.symbol;
1093 _mouseSelectType = TextSelectType::Words;
1094 if (_mouseAction == MouseAction::None) {
1095 _mouseAction = MouseAction::Selecting;
1096 auto selection = TextSelection { dragState.symbol, dragState.symbol };
1097 repaintItem(std::exchange(_selectedItem, _mouseActionItem));
1098 _selectedText = selection;
1099 }
1100 mouseMoveEvent(e);
1101
1102 _trippleClickPoint = e->globalPos();
1103 _trippleClickTimer.callOnce(QApplication::doubleClickInterval());
1104 }
1105 }
1106 }
1107
contextMenuEvent(QContextMenuEvent * e)1108 void InnerWidget::contextMenuEvent(QContextMenuEvent *e) {
1109 showContextMenu(e);
1110 }
1111
showContextMenu(QContextMenuEvent * e,bool showFromTouch)1112 void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
1113 if (e->reason() == QContextMenuEvent::Mouse) {
1114 mouseActionUpdate(e->globalPos());
1115 }
1116
1117 // -1 - has selection, but no over, 0 - no selection, 1 - over text
1118 auto isUponSelected = 0;
1119 auto hasSelected = 0;
1120 if (_selectedItem) {
1121 isUponSelected = -1;
1122
1123 auto selFrom = _selectedText.from;
1124 auto selTo = _selectedText.to;
1125 hasSelected = (selTo > selFrom) ? 1 : 0;
1126 if (App::mousedItem() && App::mousedItem() == App::hoveredItem()) {
1127 auto mousePos = mapPointToItem(
1128 mapFromGlobal(_mousePosition),
1129 App::mousedItem());
1130 StateRequest request;
1131 request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
1132 auto dragState = App::mousedItem()->textState(mousePos, request);
1133 if (dragState.cursor == CursorState::Text
1134 && base::in_range(dragState.symbol, selFrom, selTo)) {
1135 isUponSelected = 1;
1136 }
1137 }
1138 }
1139 if (showFromTouch && hasSelected && isUponSelected < hasSelected) {
1140 isUponSelected = hasSelected;
1141 }
1142
1143 _menu = base::make_unique_q<Ui::PopupMenu>(this);
1144
1145 const auto link = ClickHandler::getActive();
1146 auto view = App::hoveredItem()
1147 ? App::hoveredItem()
1148 : App::hoveredLinkItem();
1149 auto lnkPhoto = dynamic_cast<PhotoClickHandler*>(link.get());
1150 auto lnkDocument = dynamic_cast<DocumentClickHandler*>(link.get());
1151 auto lnkPeer = dynamic_cast<PeerClickHandler*>(link.get());
1152 auto lnkIsVideo = lnkDocument ? lnkDocument->document()->isVideoFile() : false;
1153 auto lnkIsVoice = lnkDocument ? lnkDocument->document()->isVoiceMessage() : false;
1154 auto lnkIsAudio = lnkDocument ? lnkDocument->document()->isAudioFile() : false;
1155 if (lnkPhoto || lnkDocument) {
1156 if (isUponSelected > 0) {
1157 _menu->addAction(tr::lng_context_copy_selected(tr::now), [=] {
1158 copySelectedText();
1159 });
1160 }
1161 if (lnkPhoto) {
1162 const auto photo = lnkPhoto->photo();
1163 const auto media = photo->activeMediaView();
1164 if (!photo->isNull() && media && media->loaded()) {
1165 _menu->addAction(tr::lng_context_save_image(tr::now), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] {
1166 savePhotoToFile(photo);
1167 }));
1168 _menu->addAction(tr::lng_context_copy_image(tr::now), [=] {
1169 copyContextImage(photo);
1170 });
1171 }
1172 if (photo->hasAttachedStickers()) {
1173 const auto controller = _controller;
1174 auto callback = [=] {
1175 auto &attached = session().api().attachedStickers();
1176 attached.requestAttachedStickerSets(controller, photo);
1177 };
1178 _menu->addAction(
1179 tr::lng_context_attached_stickers(tr::now),
1180 std::move(callback));
1181 }
1182 } else {
1183 auto document = lnkDocument->document();
1184 if (document->loading()) {
1185 _menu->addAction(tr::lng_context_cancel_download(tr::now), [=] {
1186 cancelContextDownload(document);
1187 });
1188 } else {
1189 const auto itemId = view
1190 ? view->data()->fullId()
1191 : FullMsgId();
1192 if (const auto item = document->session().data().message(itemId)) {
1193 const auto notAutoplayedGif = [&] {
1194 return document->isGifv()
1195 && !Data::AutoDownload::ShouldAutoPlay(
1196 document->session().settings().autoDownload(),
1197 item->history()->peer,
1198 document);
1199 }();
1200 if (notAutoplayedGif) {
1201 _menu->addAction(tr::lng_context_open_gif(tr::now), [=] {
1202 openContextGif(itemId);
1203 });
1204 }
1205 }
1206 if (!document->filepath(true).isEmpty()) {
1207 _menu->addAction(Platform::IsMac() ? tr::lng_context_show_in_finder(tr::now) : tr::lng_context_show_in_folder(tr::now), [=] {
1208 showContextInFolder(document);
1209 });
1210 }
1211 _menu->addAction(lnkIsVideo ? tr::lng_context_save_video(tr::now) : (lnkIsVoice ? tr::lng_context_save_audio(tr::now) : (lnkIsAudio ? tr::lng_context_save_audio_file(tr::now) : tr::lng_context_save_file(tr::now))), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
1212 saveDocumentToFile(document);
1213 }));
1214 if (document->hasAttachedStickers()) {
1215 const auto controller = _controller;
1216 auto callback = [=, doc = document] {
1217 auto &attached = session().api().attachedStickers();
1218 attached.requestAttachedStickerSets(controller, doc);
1219 };
1220 _menu->addAction(
1221 tr::lng_context_attached_stickers(tr::now),
1222 std::move(callback));
1223 }
1224 }
1225 }
1226 } else if (lnkPeer) { // suggest to block
1227 if (auto user = lnkPeer->peer()->asUser()) {
1228 suggestRestrictUser(user);
1229 }
1230 } else { // maybe cursor on some text history item?
1231 const auto item = view ? view->data().get() : nullptr;
1232 const auto itemId = item ? item->fullId() : FullMsgId();
1233
1234 auto msg = dynamic_cast<HistoryMessage*>(item);
1235 if (isUponSelected > 0) {
1236 _menu->addAction(tr::lng_context_copy_selected(tr::now), [this] { copySelectedText(); });
1237 } else {
1238 if (item && !isUponSelected) {
1239 const auto media = view->media();
1240 const auto mediaHasTextForCopy = media && media->hasTextForCopy();
1241 if (const auto document = media ? media->getDocument() : nullptr) {
1242 if (document->sticker()) {
1243 _menu->addAction(tr::lng_context_save_image(tr::now), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
1244 saveDocumentToFile(document);
1245 }));
1246 }
1247 }
1248 if (msg
1249 && !link
1250 && (view->hasVisibleText()
1251 || mediaHasTextForCopy
1252 || item->Has<HistoryMessageLogEntryOriginal>())) {
1253 _menu->addAction(tr::lng_context_copy_text(tr::now), [=] {
1254 copyContextText(itemId);
1255 });
1256 }
1257 }
1258 }
1259
1260 const auto actionText = link
1261 ? link->copyToClipboardContextItemText()
1262 : QString();
1263 if (!actionText.isEmpty()) {
1264 _menu->addAction(
1265 actionText,
1266 [text = link->copyToClipboardText()] {
1267 QGuiApplication::clipboard()->setText(text);
1268 });
1269 }
1270 }
1271
1272 if (_menu->empty()) {
1273 _menu = nullptr;
1274 } else {
1275 _menu->popup(e->globalPos());
1276 e->accept();
1277 }
1278 }
1279
savePhotoToFile(not_null<PhotoData * > photo)1280 void InnerWidget::savePhotoToFile(not_null<PhotoData*> photo) {
1281 const auto media = photo->activeMediaView();
1282 if (photo->isNull() || !media || !media->loaded()) {
1283 return;
1284 }
1285
1286 const auto image = media->image(Data::PhotoSize::Large)->original();
1287 auto filter = qsl("JPEG Image (*.jpg);;") + FileDialog::AllFilesFilter();
1288 FileDialog::GetWritePath(
1289 this,
1290 tr::lng_save_photo(tr::now),
1291 filter,
1292 filedialogDefaultName(qsl("photo"), qsl(".jpg")),
1293 crl::guard(this, [=](const QString &result) {
1294 if (!result.isEmpty()) {
1295 image.save(result, "JPG");
1296 }
1297 }));
1298 }
1299
saveDocumentToFile(not_null<DocumentData * > document)1300 void InnerWidget::saveDocumentToFile(not_null<DocumentData*> document) {
1301 DocumentSaveClickHandler::Save(
1302 Data::FileOrigin(),
1303 document,
1304 DocumentSaveClickHandler::Mode::ToNewFile);
1305 }
1306
copyContextImage(not_null<PhotoData * > photo)1307 void InnerWidget::copyContextImage(not_null<PhotoData*> photo) {
1308 const auto media = photo->activeMediaView();
1309 if (photo->isNull() || !media || !media->loaded()) {
1310 return;
1311 }
1312
1313 const auto image = media->image(Data::PhotoSize::Large)->original();
1314 QGuiApplication::clipboard()->setImage(image);
1315 }
1316
copySelectedText()1317 void InnerWidget::copySelectedText() {
1318 TextUtilities::SetClipboardText(getSelectedText());
1319 }
1320
showStickerPackInfo(not_null<DocumentData * > document)1321 void InnerWidget::showStickerPackInfo(not_null<DocumentData*> document) {
1322 StickerSetBox::Show(_controller, document);
1323 }
1324
cancelContextDownload(not_null<DocumentData * > document)1325 void InnerWidget::cancelContextDownload(not_null<DocumentData*> document) {
1326 document->cancel();
1327 }
1328
showContextInFolder(not_null<DocumentData * > document)1329 void InnerWidget::showContextInFolder(not_null<DocumentData*> document) {
1330 const auto filepath = document->filepath(true);
1331 if (!filepath.isEmpty()) {
1332 File::ShowInFolder(filepath);
1333 }
1334 }
1335
openContextGif(FullMsgId itemId)1336 void InnerWidget::openContextGif(FullMsgId itemId) {
1337 if (const auto item = session().data().message(itemId)) {
1338 if (const auto media = item->media()) {
1339 if (const auto document = media->document()) {
1340 _controller->openDocument(document, itemId, true);
1341 }
1342 }
1343 }
1344 }
1345
copyContextText(FullMsgId itemId)1346 void InnerWidget::copyContextText(FullMsgId itemId) {
1347 if (const auto item = session().data().message(itemId)) {
1348 TextUtilities::SetClipboardText(HistoryItemText(item));
1349 }
1350 }
1351
suggestRestrictUser(not_null<UserData * > user)1352 void InnerWidget::suggestRestrictUser(not_null<UserData*> user) {
1353 Expects(_menu != nullptr);
1354
1355 if (!_channel->isMegagroup() || !_channel->canBanMembers() || _admins.empty()) {
1356 return;
1357 }
1358 if (base::contains(_admins, user)) {
1359 if (!base::contains(_adminsCanEdit, user)) {
1360 return;
1361 }
1362 }
1363 _menu->addAction(tr::lng_context_restrict_user(tr::now), [=] {
1364 auto editRestrictions = [=](bool hasAdminRights, ChatRestrictionsInfo currentRights) {
1365 auto weak = QPointer<InnerWidget>(this);
1366 auto weakBox = std::make_shared<QPointer<EditRestrictedBox>>();
1367 auto box = Box<EditRestrictedBox>(_channel, user, hasAdminRights, currentRights);
1368 box->setSaveCallback([=](
1369 ChatRestrictionsInfo oldRights,
1370 ChatRestrictionsInfo newRights) {
1371 if (weak) {
1372 weak->restrictUser(user, oldRights, newRights);
1373 }
1374 if (*weakBox) {
1375 (*weakBox)->closeBox();
1376 }
1377 });
1378 *weakBox = QPointer<EditRestrictedBox>(box.data());
1379 _controller->show(
1380 std::move(box),
1381 Ui::LayerOption::KeepOther);
1382 };
1383 if (base::contains(_admins, user)) {
1384 editRestrictions(true, ChatRestrictionsInfo());
1385 } else {
1386 _api.request(MTPchannels_GetParticipant(
1387 _channel->inputChannel,
1388 user->input
1389 )).done([=](const MTPchannels_ChannelParticipant &result) {
1390 Expects(result.type() == mtpc_channels_channelParticipant);
1391
1392 auto &participant = result.c_channels_channelParticipant();
1393 _channel->owner().processUsers(participant.vusers());
1394 auto type = participant.vparticipant().type();
1395 if (type == mtpc_channelParticipantBanned) {
1396 auto &banned = participant.vparticipant().c_channelParticipantBanned();
1397 editRestrictions(
1398 false,
1399 ChatRestrictionsInfo(banned.vbanned_rights()));
1400 } else {
1401 auto hasAdminRights = (type == mtpc_channelParticipantAdmin)
1402 || (type == mtpc_channelParticipantCreator);
1403 editRestrictions(hasAdminRights, ChatRestrictionsInfo());
1404 }
1405 }).fail([=](const MTP::Error &error) {
1406 editRestrictions(false, ChatRestrictionsInfo());
1407 }).send();
1408 }
1409 });
1410 }
1411
restrictUser(not_null<UserData * > user,ChatRestrictionsInfo oldRights,ChatRestrictionsInfo newRights)1412 void InnerWidget::restrictUser(
1413 not_null<UserData*> user,
1414 ChatRestrictionsInfo oldRights,
1415 ChatRestrictionsInfo newRights) {
1416 const auto done = [=](ChatRestrictionsInfo newRights) {
1417 restrictUserDone(user, newRights);
1418 };
1419 const auto callback = SaveRestrictedCallback(
1420 _channel,
1421 user,
1422 crl::guard(this, done),
1423 nullptr);
1424 callback(oldRights, newRights);
1425 }
1426
restrictUserDone(not_null<UserData * > user,ChatRestrictionsInfo rights)1427 void InnerWidget::restrictUserDone(
1428 not_null<UserData*> user,
1429 ChatRestrictionsInfo rights) {
1430 if (rights.flags) {
1431 _admins.erase(
1432 std::remove(_admins.begin(), _admins.end(), user),
1433 _admins.end());
1434 _adminsCanEdit.erase(
1435 std::remove(_adminsCanEdit.begin(), _adminsCanEdit.end(), user),
1436 _adminsCanEdit.end());
1437 }
1438 _downLoaded = false;
1439 checkPreloadMore();
1440 }
1441
mousePressEvent(QMouseEvent * e)1442 void InnerWidget::mousePressEvent(QMouseEvent *e) {
1443 if (_menu) {
1444 e->accept();
1445 return; // ignore mouse press, that was hiding context menu
1446 }
1447 mouseActionStart(e->globalPos(), e->button());
1448 }
1449
mouseMoveEvent(QMouseEvent * e)1450 void InnerWidget::mouseMoveEvent(QMouseEvent *e) {
1451 auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
1452 if (!buttonsPressed && _mouseAction != MouseAction::None) {
1453 mouseReleaseEvent(e);
1454 }
1455 mouseActionUpdate(e->globalPos());
1456 }
1457
mouseReleaseEvent(QMouseEvent * e)1458 void InnerWidget::mouseReleaseEvent(QMouseEvent *e) {
1459 mouseActionFinish(e->globalPos(), e->button());
1460 if (!rect().contains(e->pos())) {
1461 leaveEvent(e);
1462 }
1463 }
1464
enterEventHook(QEnterEvent * e)1465 void InnerWidget::enterEventHook(QEnterEvent *e) {
1466 mouseActionUpdate(QCursor::pos());
1467 return TWidget::enterEventHook(e);
1468 }
1469
leaveEventHook(QEvent * e)1470 void InnerWidget::leaveEventHook(QEvent *e) {
1471 if (const auto view = App::hoveredItem()) {
1472 repaintItem(view);
1473 App::hoveredItem(nullptr);
1474 }
1475 ClickHandler::clearActive();
1476 Ui::Tooltip::Hide();
1477 if (!ClickHandler::getPressed() && _cursor != style::cur_default) {
1478 _cursor = style::cur_default;
1479 setCursor(_cursor);
1480 }
1481 return TWidget::leaveEventHook(e);
1482 }
1483
mouseActionStart(const QPoint & screenPos,Qt::MouseButton button)1484 void InnerWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) {
1485 mouseActionUpdate(screenPos);
1486 if (button != Qt::LeftButton) return;
1487
1488 ClickHandler::pressed();
1489 if (App::pressedItem() != App::hoveredItem()) {
1490 repaintItem(App::pressedItem());
1491 App::pressedItem(App::hoveredItem());
1492 repaintItem(App::pressedItem());
1493 }
1494
1495 _mouseAction = MouseAction::None;
1496 _mouseActionItem = App::mousedItem();
1497 _dragStartPosition = mapPointToItem(
1498 mapFromGlobal(screenPos),
1499 _mouseActionItem);
1500 _pressWasInactive = Ui::WasInactivePress(_controller->widget());
1501 if (_pressWasInactive) {
1502 Ui::MarkInactivePress(_controller->widget(), false);
1503 }
1504
1505 if (ClickHandler::getPressed()) {
1506 _mouseAction = MouseAction::PrepareDrag;
1507 }
1508 if (_mouseAction == MouseAction::None && _mouseActionItem) {
1509 TextState dragState;
1510 if (_trippleClickTimer.isActive() && (screenPos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) {
1511 StateRequest request;
1512 request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
1513 dragState = _mouseActionItem->textState(_dragStartPosition, request);
1514 if (dragState.cursor == CursorState::Text) {
1515 auto selection = TextSelection { dragState.symbol, dragState.symbol };
1516 repaintItem(std::exchange(_selectedItem, _mouseActionItem));
1517 _selectedText = selection;
1518 _mouseTextSymbol = dragState.symbol;
1519 _mouseAction = MouseAction::Selecting;
1520 _mouseSelectType = TextSelectType::Paragraphs;
1521 mouseActionUpdate(_mousePosition);
1522 _trippleClickTimer.callOnce(QApplication::doubleClickInterval());
1523 }
1524 } else if (App::pressedItem()) {
1525 StateRequest request;
1526 request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
1527 dragState = _mouseActionItem->textState(_dragStartPosition, request);
1528 }
1529 if (_mouseSelectType != TextSelectType::Paragraphs) {
1530 if (App::pressedItem()) {
1531 _mouseTextSymbol = dragState.symbol;
1532 auto uponSelected = (dragState.cursor == CursorState::Text);
1533 if (uponSelected) {
1534 if (!_selectedItem || _selectedItem != _mouseActionItem) {
1535 uponSelected = false;
1536 } else if (_mouseTextSymbol < _selectedText.from || _mouseTextSymbol >= _selectedText.to) {
1537 uponSelected = false;
1538 }
1539 }
1540 if (uponSelected) {
1541 _mouseAction = MouseAction::PrepareDrag; // start text drag
1542 } else if (!_pressWasInactive) {
1543 if (dragState.afterSymbol) ++_mouseTextSymbol;
1544 auto selection = TextSelection { _mouseTextSymbol, _mouseTextSymbol };
1545 repaintItem(std::exchange(_selectedItem, _mouseActionItem));
1546 _selectedText = selection;
1547 _mouseAction = MouseAction::Selecting;
1548 repaintItem(_mouseActionItem);
1549 }
1550 }
1551 }
1552 }
1553
1554 if (!_mouseActionItem) {
1555 _mouseAction = MouseAction::None;
1556 } else if (_mouseAction == MouseAction::None) {
1557 _mouseActionItem = nullptr;
1558 }
1559 }
1560
mouseActionUpdate(const QPoint & screenPos)1561 void InnerWidget::mouseActionUpdate(const QPoint &screenPos) {
1562 _mousePosition = screenPos;
1563 updateSelected();
1564 }
1565
mouseActionCancel()1566 void InnerWidget::mouseActionCancel() {
1567 _mouseActionItem = nullptr;
1568 _mouseAction = MouseAction::None;
1569 _dragStartPosition = QPoint(0, 0);
1570 _wasSelectedText = false;
1571 //_widget->noSelectingScroll(); // TODO
1572 }
1573
mouseActionFinish(const QPoint & screenPos,Qt::MouseButton button)1574 void InnerWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button) {
1575 mouseActionUpdate(screenPos);
1576
1577 auto activated = ClickHandler::unpressed();
1578 if (_mouseAction == MouseAction::Dragging) {
1579 activated = nullptr;
1580 }
1581 if (const auto view = App::pressedItem()) {
1582 repaintItem(view);
1583 App::pressedItem(nullptr);
1584 }
1585
1586 _wasSelectedText = false;
1587
1588 if (activated) {
1589 mouseActionCancel();
1590 ActivateClickHandler(window(), activated, {
1591 button,
1592 QVariant::fromValue(ClickHandlerContext{
1593 .elementDelegate = [weak = Ui::MakeWeak(this)] {
1594 return weak
1595 ? (ElementDelegate*)weak
1596 : nullptr;
1597 },
1598 .sessionWindow = base::make_weak(_controller.get()),
1599 })
1600 });
1601 return;
1602 }
1603 if (_mouseAction == MouseAction::PrepareDrag && !_pressWasInactive && button != Qt::RightButton) {
1604 repaintItem(base::take(_selectedItem));
1605 } else if (_mouseAction == MouseAction::Selecting) {
1606 if (_selectedItem && !_pressWasInactive) {
1607 if (_selectedText.from == _selectedText.to) {
1608 _selectedItem = nullptr;
1609 _controller->widget()->setInnerFocus();
1610 }
1611 }
1612 }
1613 _mouseAction = MouseAction::None;
1614 _mouseActionItem = nullptr;
1615 _mouseSelectType = TextSelectType::Letters;
1616 //_widget->noSelectingScroll(); // TODO
1617
1618 if (QGuiApplication::clipboard()->supportsSelection()
1619 && _selectedItem
1620 && _selectedText.from != _selectedText.to) {
1621 TextUtilities::SetClipboardText(
1622 _selectedItem->selectedText(_selectedText),
1623 QClipboard::Selection);
1624 }
1625 }
1626
updateSelected()1627 void InnerWidget::updateSelected() {
1628 auto mousePosition = mapFromGlobal(_mousePosition);
1629 auto point = QPoint(
1630 std::clamp(mousePosition.x(), 0, width()),
1631 std::clamp(mousePosition.y(), _visibleTop, _visibleBottom));
1632
1633 auto itemPoint = QPoint();
1634 auto begin = std::rbegin(_items), end = std::rend(_items);
1635 auto from = (point.y() >= _itemsTop && point.y() < _itemsTop + _itemsHeight)
1636 ? std::lower_bound(begin, end, point.y(), [this](auto &elem, int top) {
1637 return this->itemTop(elem) + elem->height() <= top;
1638 })
1639 : end;
1640 const auto view = (from != end) ? from->get() : nullptr;
1641 const auto item = view ? view->data().get() : nullptr;
1642 if (item) {
1643 App::mousedItem(view);
1644 itemPoint = mapPointToItem(point, view);
1645 if (view->pointState(itemPoint) != PointState::Outside) {
1646 if (App::hoveredItem() != view) {
1647 repaintItem(App::hoveredItem());
1648 App::hoveredItem(view);
1649 repaintItem(view);
1650 }
1651 } else if (const auto view = App::hoveredItem()) {
1652 repaintItem(view);
1653 App::hoveredItem(nullptr);
1654 }
1655 }
1656
1657 TextState dragState;
1658 ClickHandlerHost *lnkhost = nullptr;
1659 auto selectingText = _selectedItem
1660 && (view == _mouseActionItem)
1661 && (view == App::hoveredItem());
1662 if (view) {
1663 if (view != _mouseActionItem || (itemPoint - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
1664 if (_mouseAction == MouseAction::PrepareDrag) {
1665 _mouseAction = MouseAction::Dragging;
1666 InvokeQueued(this, [this] { performDrag(); });
1667 }
1668 }
1669 StateRequest request;
1670 if (_mouseAction == MouseAction::Selecting) {
1671 request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
1672 } else {
1673 selectingText = false;
1674 }
1675 dragState = view->textState(itemPoint, request);
1676 lnkhost = view;
1677 if (!dragState.link && itemPoint.x() >= st::historyPhotoLeft && itemPoint.x() < st::historyPhotoLeft + st::msgPhotoSize) {
1678 if (!item->isService() && view->hasFromPhoto()) {
1679 enumerateUserpics([&](not_null<Element*> view, int userpicTop) {
1680 // stop enumeration if the userpic is below our point
1681 if (userpicTop > point.y()) {
1682 return false;
1683 }
1684
1685 // stop enumeration if we've found a userpic under the cursor
1686 if (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) {
1687 dragState.link = view->data()->from()->openLink();
1688 lnkhost = view;
1689 return false;
1690 }
1691 return true;
1692 });
1693 }
1694 }
1695 }
1696 auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost);
1697 if (lnkChanged || dragState.cursor != _mouseCursorState) {
1698 Ui::Tooltip::Hide();
1699 }
1700 if (dragState.link
1701 || dragState.cursor == CursorState::Date
1702 || dragState.cursor == CursorState::Forwarded) {
1703 Ui::Tooltip::Show(1000, this);
1704 }
1705
1706 auto cursor = style::cur_default;
1707 if (_mouseAction == MouseAction::None) {
1708 _mouseCursorState = dragState.cursor;
1709 if (dragState.link) {
1710 cursor = style::cur_pointer;
1711 } else if (_mouseCursorState == CursorState::Text) {
1712 cursor = style::cur_text;
1713 } else if (_mouseCursorState == CursorState::Date) {
1714 // cursor = style::cur_cross;
1715 }
1716 } else if (item) {
1717 if (_mouseAction == MouseAction::Selecting) {
1718 if (selectingText) {
1719 auto second = dragState.symbol;
1720 if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {
1721 ++second;
1722 }
1723 auto selection = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) };
1724 if (_mouseSelectType != TextSelectType::Letters) {
1725 selection = _mouseActionItem->adjustSelection(
1726 selection,
1727 _mouseSelectType);
1728 }
1729 if (_selectedText != selection) {
1730 _selectedText = selection;
1731 repaintItem(_mouseActionItem);
1732 }
1733 if (!_wasSelectedText && (selection.from != selection.to)) {
1734 _wasSelectedText = true;
1735 setFocus();
1736 }
1737 }
1738 } else if (_mouseAction == MouseAction::Dragging) {
1739 }
1740
1741 if (ClickHandler::getPressed()) {
1742 cursor = style::cur_pointer;
1743 } else if (_mouseAction == MouseAction::Selecting && _selectedItem) {
1744 cursor = style::cur_text;
1745 }
1746 }
1747
1748 // Voice message seek support.
1749 if (const auto pressedView = App::pressedLinkItem()) {
1750 const auto adjustedPoint = mapPointToItem(point, pressedView);
1751 pressedView->updatePressed(adjustedPoint);
1752 }
1753
1754 //if (_mouseAction == MouseAction::Selecting) {
1755 // _widget->checkSelectingScroll(mousePos);
1756 //} else {
1757 // _widget->noSelectingScroll();
1758 //} // TODO
1759
1760 if (_mouseAction == MouseAction::None && (lnkChanged || cursor != _cursor)) {
1761 setCursor(_cursor = cursor);
1762 }
1763 }
1764
performDrag()1765 void InnerWidget::performDrag() {
1766 if (_mouseAction != MouseAction::Dragging) return;
1767
1768 //auto uponSelected = false;
1769 //if (_mouseActionItem) {
1770 // if (!_selected.isEmpty() && _selected.cbegin().value() == FullSelection) {
1771 // uponSelected = _selected.contains(_mouseActionItem);
1772 // } else {
1773 // StateRequest request;
1774 // request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
1775 // auto dragState = _mouseActionItem->textState(_dragStartPosition.x(), _dragStartPosition.y(), request);
1776 // uponSelected = (dragState.cursor == CursorState::Text);
1777 // if (uponSelected) {
1778 // if (_selected.isEmpty() ||
1779 // _selected.cbegin().value() == FullSelection ||
1780 // _selected.cbegin().key() != _mouseActionItem
1781 // ) {
1782 // uponSelected = false;
1783 // } else {
1784 // uint16 selFrom = _selected.cbegin().value().from, selTo = _selected.cbegin().value().to;
1785 // if (dragState.symbol < selFrom || dragState.symbol >= selTo) {
1786 // uponSelected = false;
1787 // }
1788 // }
1789 // }
1790 // }
1791 //}
1792 //auto pressedHandler = ClickHandler::getPressed();
1793
1794 //if (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.data())) {
1795 // return;
1796 //}
1797
1798 //TextWithEntities sel;
1799 //QList<QUrl> urls;
1800 //if (uponSelected) {
1801 // sel = getSelectedText();
1802 //} else if (pressedHandler) {
1803 // sel = { pressedHandler->dragText(), EntitiesInText() };
1804 // //if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
1805 // // urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
1806 // //}
1807 //}
1808 //if (auto mimeData = mimeDataFromTextWithEntities(sel)) {
1809 // updateDragSelection(0, 0, false);
1810 // _widget->noSelectingScroll();
1811
1812 // if (!urls.isEmpty()) mimeData->setUrls(urls);
1813 // if (uponSelected && !Adaptive::OneColumn()) {
1814 // auto selectedState = getSelectionState();
1815 // if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
1816 // session().data().setMimeForwardIds(getSelectedItems());
1817 // mimeData->setData(qsl("application/x-td-forward"), "1");
1818 // }
1819 // }
1820 // _controller->window()->launchDrag(std::move(mimeData));
1821 // return;
1822 //} else {
1823 // auto forwardMimeType = QString();
1824 // auto pressedMedia = static_cast<HistoryView::Media*>(nullptr);
1825 // if (auto pressedItem = App::pressedItem()) {
1826 // pressedMedia = pressedItem->media();
1827 // if (_mouseCursorState == CursorState::Date
1828 // || (pressedMedia && pressedMedia->dragItem())) {
1829 // forwardMimeType = qsl("application/x-td-forward");
1830 // session().data().setMimeForwardIds(
1831 // session().data().itemOrItsGroup(pressedItem->data()));
1832 // }
1833 // }
1834 // if (auto pressedLnkItem = App::pressedLinkItem()) {
1835 // if ((pressedMedia = pressedLnkItem->media())) {
1836 // if (forwardMimeType.isEmpty()
1837 // && pressedMedia->dragItemByHandler(pressedHandler)) {
1838 // forwardMimeType = qsl("application/x-td-forward");
1839 // session().data().setMimeForwardIds(
1840 // { 1, pressedLnkItem->fullId() });
1841 // }
1842 // }
1843 // }
1844 // if (!forwardMimeType.isEmpty()) {
1845 // auto mimeData = std::make_unique<QMimeData>();
1846 // mimeData->setData(forwardMimeType, "1");
1847 // if (auto document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) {
1848 // auto filepath = document->filepath(true);
1849 // if (!filepath.isEmpty()) {
1850 // QList<QUrl> urls;
1851 // urls.push_back(QUrl::fromLocalFile(filepath));
1852 // mimeData->setUrls(urls);
1853 // }
1854 // }
1855
1856 // // This call enters event loop and can destroy any QObject.
1857 // _controller->window()->launchDrag(std::move(mimeData));
1858 // return;
1859 // }
1860 //} // TODO
1861 }
1862
itemTop(not_null<const Element * > view) const1863 int InnerWidget::itemTop(not_null<const Element*> view) const {
1864 return _itemsTop + view->y();
1865 }
1866
repaintItem(const Element * view)1867 void InnerWidget::repaintItem(const Element *view) {
1868 if (!view) {
1869 return;
1870 }
1871 const auto top = itemTop(view);
1872 const auto range = view->verticalRepaintRange();
1873 update(0, top + range.top, width(), range.height);
1874 }
1875
resizeItem(not_null<Element * > view)1876 void InnerWidget::resizeItem(not_null<Element*> view) {
1877 updateSize();
1878 }
1879
refreshItem(not_null<const Element * > view)1880 void InnerWidget::refreshItem(not_null<const Element*> view) {
1881 // No need to refresh views in admin log.
1882 }
1883
mapPointToItem(QPoint point,const Element * view) const1884 QPoint InnerWidget::mapPointToItem(QPoint point, const Element *view) const {
1885 if (!view) {
1886 return QPoint();
1887 }
1888 return point - QPoint(0, itemTop(view));
1889 }
1890
1891 InnerWidget::~InnerWidget() = default;
1892
1893 } // namespace AdminLog
1894