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