1534 lines
		
	
	
	
		
			52 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1534 lines
		
	
	
	
		
			52 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
This file is part of Telegram Desktop,
 | 
						|
the official desktop version of Telegram messaging app, see https://telegram.org
 | 
						|
 | 
						|
Telegram Desktop is free software: you can redistribute it and/or modify
 | 
						|
it under the terms of the GNU General Public License as published by
 | 
						|
the Free Software Foundation, either version 3 of the License, or
 | 
						|
(at your option) any later version.
 | 
						|
 | 
						|
It is distributed in the hope that it will be useful,
 | 
						|
but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | 
						|
GNU General Public License for more details.
 | 
						|
 | 
						|
In addition, as a special exception, the copyright holders give permission
 | 
						|
to link the code of portions of this program with the OpenSSL library.
 | 
						|
 | 
						|
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
 | 
						|
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 | 
						|
*/
 | 
						|
#include "history/history_admin_log_inner.h"
 | 
						|
 | 
						|
#include "styles/style_history.h"
 | 
						|
#include "history/history_media_types.h"
 | 
						|
#include "history/history_message.h"
 | 
						|
#include "history/history_service_layout.h"
 | 
						|
#include "history/history_admin_log_section.h"
 | 
						|
#include "history/history_admin_log_filter.h"
 | 
						|
#include "chat_helpers/message_field.h"
 | 
						|
#include "mainwindow.h"
 | 
						|
#include "mainwidget.h"
 | 
						|
#include "messenger.h"
 | 
						|
#include "apiwrap.h"
 | 
						|
#include "window/window_controller.h"
 | 
						|
#include "auth_session.h"
 | 
						|
#include "ui/widgets/popup_menu.h"
 | 
						|
#include "core/file_utilities.h"
 | 
						|
#include "core/tl_help.h"
 | 
						|
#include "base/overload.h"
 | 
						|
#include "lang/lang_keys.h"
 | 
						|
#include "boxes/edit_participant_box.h"
 | 
						|
 | 
						|
namespace AdminLog {
 | 
						|
namespace {
 | 
						|
 | 
						|
// If we require to support more admins we'll have to rewrite this anyway.
 | 
						|
constexpr auto kMaxChannelAdmins = 200;
 | 
						|
constexpr auto kScrollDateHideTimeout = 1000;
 | 
						|
constexpr auto kEventsFirstPage = 20;
 | 
						|
constexpr auto kEventsPerPage = 50;
 | 
						|
 | 
						|
} // namespace
 | 
						|
 | 
						|
template <InnerWidget::EnumItemsDirection direction, typename Method>
 | 
						|
void InnerWidget::enumerateItems(Method method) {
 | 
						|
	constexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom);
 | 
						|
 | 
						|
	// No displayed messages in this history.
 | 
						|
	if (_items.empty()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	if (_visibleBottom <= _itemsTop || _itemsTop + _itemsHeight <= _visibleTop) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	auto begin = std::rbegin(_items), end = std::rend(_items);
 | 
						|
	auto from = TopToBottom ? std::lower_bound(begin, end, _visibleTop, [this](auto &elem, int top) {
 | 
						|
		return this->itemTop(elem) + elem->height() <= top;
 | 
						|
	}) : std::upper_bound(begin, end, _visibleBottom, [this](int bottom, auto &elem) {
 | 
						|
		return this->itemTop(elem) + elem->height() >= bottom;
 | 
						|
	});
 | 
						|
	auto wasEnd = (from == end);
 | 
						|
	if (wasEnd) {
 | 
						|
		--from;
 | 
						|
	}
 | 
						|
	if (TopToBottom) {
 | 
						|
		Assert(itemTop(from->get()) + from->get()->height() > _visibleTop);
 | 
						|
	} else {
 | 
						|
		Assert(itemTop(from->get()) < _visibleBottom);
 | 
						|
	}
 | 
						|
 | 
						|
	while (true) {
 | 
						|
		auto item = from->get();
 | 
						|
		auto itemtop = itemTop(item);
 | 
						|
		auto itembottom = itemtop + item->height();
 | 
						|
 | 
						|
		// Binary search should've skipped all the items that are above / below the visible area.
 | 
						|
		if (TopToBottom) {
 | 
						|
			Assert(itembottom > _visibleTop);
 | 
						|
		} else {
 | 
						|
			Assert(itemtop < _visibleBottom);
 | 
						|
		}
 | 
						|
 | 
						|
		if (!method(item, itemtop, itembottom)) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		// Skip all the items that are below / above the visible area.
 | 
						|
		if (TopToBottom) {
 | 
						|
			if (itembottom >= _visibleBottom) {
 | 
						|
				return;
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			if (itemtop <= _visibleTop) {
 | 
						|
				return;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if (TopToBottom) {
 | 
						|
			if (++from == end) {
 | 
						|
				break;
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			if (from == begin) {
 | 
						|
				break;
 | 
						|
			}
 | 
						|
			--from;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
template <typename Method>
 | 
						|
void InnerWidget::enumerateUserpics(Method method) {
 | 
						|
	// Find and remember the top of an attached messages pack
 | 
						|
	// -1 means we didn't find an attached to next message yet.
 | 
						|
	int lowestAttachedItemTop = -1;
 | 
						|
 | 
						|
	auto userpicCallback = [this, &lowestAttachedItemTop, &method](HistoryItem *item, int itemtop, int itembottom) {
 | 
						|
		// Skip all service messages.
 | 
						|
		auto message = item->toHistoryMessage();
 | 
						|
		if (!message) return true;
 | 
						|
 | 
						|
		if (lowestAttachedItemTop < 0 && message->isAttachedToNext()) {
 | 
						|
			lowestAttachedItemTop = itemtop + message->marginTop();
 | 
						|
		}
 | 
						|
 | 
						|
		// Call method on a userpic for all messages that have it and for those who are not showing it
 | 
						|
		// because of their attachment to the next message if they are bottom-most visible.
 | 
						|
		if (message->displayFromPhoto() || (message->hasFromPhoto() && itembottom >= _visibleBottom)) {
 | 
						|
			if (lowestAttachedItemTop < 0) {
 | 
						|
				lowestAttachedItemTop = itemtop + message->marginTop();
 | 
						|
			}
 | 
						|
			// Attach userpic to the bottom of the visible area with the same margin as the last message.
 | 
						|
			auto userpicMinBottomSkip = st::historyPaddingBottom + st::msgMargin.bottom();
 | 
						|
			auto userpicBottom = qMin(itembottom - message->marginBottom(), _visibleBottom - userpicMinBottomSkip);
 | 
						|
 | 
						|
			// Do not let the userpic go above the attached messages pack top line.
 | 
						|
			userpicBottom = qMax(userpicBottom, lowestAttachedItemTop + st::msgPhotoSize);
 | 
						|
 | 
						|
			// Call the template callback function that was passed
 | 
						|
			// and return if it finished everything it needed.
 | 
						|
			if (!method(message, userpicBottom - st::msgPhotoSize)) {
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// Forget the found top of the pack, search for the next one from scratch.
 | 
						|
		if (!message->isAttachedToNext()) {
 | 
						|
			lowestAttachedItemTop = -1;
 | 
						|
		}
 | 
						|
 | 
						|
		return true;
 | 
						|
	};
 | 
						|
 | 
						|
	enumerateItems<EnumItemsDirection::TopToBottom>(userpicCallback);
 | 
						|
}
 | 
						|
 | 
						|
template <typename Method>
 | 
						|
void InnerWidget::enumerateDates(Method method) {
 | 
						|
	// Find and remember the bottom of an single-day messages pack
 | 
						|
	// -1 means we didn't find a same-day with previous message yet.
 | 
						|
	auto lowestInOneDayItemBottom = -1;
 | 
						|
 | 
						|
	auto dateCallback = [this, &lowestInOneDayItemBottom, &method](HistoryItem *item, int itemtop, int itembottom) {
 | 
						|
		if (lowestInOneDayItemBottom < 0 && item->isInOneDayWithPrevious()) {
 | 
						|
			lowestInOneDayItemBottom = itembottom - item->marginBottom();
 | 
						|
		}
 | 
						|
 | 
						|
		// Call method on a date for all messages that have it and for those who are not showing it
 | 
						|
		// because they are in a one day together with the previous message if they are top-most visible.
 | 
						|
		if (item->displayDate() || (!item->isEmpty() && itemtop <= _visibleTop)) {
 | 
						|
			if (lowestInOneDayItemBottom < 0) {
 | 
						|
				lowestInOneDayItemBottom = itembottom - item->marginBottom();
 | 
						|
			}
 | 
						|
			// Attach date to the top of the visible area with the same margin as it has in service message.
 | 
						|
			auto dateTop = qMax(itemtop, _visibleTop) + st::msgServiceMargin.top();
 | 
						|
 | 
						|
			// Do not let the date go below the single-day messages pack bottom line.
 | 
						|
			auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
 | 
						|
			dateTop = qMin(dateTop, lowestInOneDayItemBottom - dateHeight);
 | 
						|
 | 
						|
			// Call the template callback function that was passed
 | 
						|
			// and return if it finished everything it needed.
 | 
						|
			if (!method(item, itemtop, dateTop)) {
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// Forget the found bottom of the pack, search for the next one from scratch.
 | 
						|
		if (!item->isInOneDayWithPrevious()) {
 | 
						|
			lowestInOneDayItemBottom = -1;
 | 
						|
		}
 | 
						|
 | 
						|
		return true;
 | 
						|
	};
 | 
						|
 | 
						|
	enumerateItems<EnumItemsDirection::BottomToTop>(dateCallback);
 | 
						|
}
 | 
						|
 | 
						|
InnerWidget::InnerWidget(
 | 
						|
	QWidget *parent,
 | 
						|
	not_null<Window::Controller*> controller,
 | 
						|
	not_null<ChannelData*> channel)
 | 
						|
: RpWidget(parent)
 | 
						|
, _controller(controller)
 | 
						|
, _channel(channel)
 | 
						|
, _history(App::history(channel))
 | 
						|
, _scrollDateCheck([this] { scrollDateCheck(); })
 | 
						|
, _emptyText(st::historyAdminLogEmptyWidth - st::historyAdminLogEmptyPadding.left() - st::historyAdminLogEmptyPadding.left()) {
 | 
						|
	setMouseTracking(true);
 | 
						|
	_scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); });
 | 
						|
	Auth().data().itemRepaintRequest()
 | 
						|
		| rpl::start_with_next([this](auto item) {
 | 
						|
			if (item->isLogEntry() && _history == item->history()) {
 | 
						|
				repaintItem(item);
 | 
						|
			}
 | 
						|
		}, lifetime());
 | 
						|
	subscribe(Auth().data().pendingHistoryResize(), [this] { handlePendingHistoryResize(); });
 | 
						|
	subscribe(Auth().data().queryItemVisibility(), [this](const AuthSessionData::ItemVisibilityQuery &query) {
 | 
						|
		if (_history != query.item->history() || !query.item->isLogEntry() || !isVisible()) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
		auto top = itemTop(query.item);
 | 
						|
		if (top >= 0 && top + query.item->height() > _visibleTop && top < _visibleBottom) {
 | 
						|
			*query.isVisible = true;
 | 
						|
		}
 | 
						|
	});
 | 
						|
	updateEmptyText();
 | 
						|
 | 
						|
	requestAdmins();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::visibleTopBottomUpdated(
 | 
						|
		int visibleTop,
 | 
						|
		int visibleBottom) {
 | 
						|
	auto scrolledUp = (visibleTop < _visibleTop);
 | 
						|
	_visibleTop = visibleTop;
 | 
						|
	_visibleBottom = visibleBottom;
 | 
						|
 | 
						|
	updateVisibleTopItem();
 | 
						|
	checkPreloadMore();
 | 
						|
	if (scrolledUp) {
 | 
						|
		_scrollDateCheck.call();
 | 
						|
	} else {
 | 
						|
		scrollDateHideByTimer();
 | 
						|
	}
 | 
						|
	_controller->floatPlayerAreaUpdated().notify(true);
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::updateVisibleTopItem() {
 | 
						|
	if (_visibleBottom == height()) {
 | 
						|
		_visibleTopItem = nullptr;
 | 
						|
	} else {
 | 
						|
		auto begin = std::rbegin(_items), end = std::rend(_items);
 | 
						|
		auto from = std::lower_bound(begin, end, _visibleTop, [this](auto &&elem, int top) {
 | 
						|
			return this->itemTop(elem) + elem->height() <= top;
 | 
						|
		});
 | 
						|
		if (from != end) {
 | 
						|
			_visibleTopItem = *from;
 | 
						|
			_visibleTopFromItem = _visibleTop - _visibleTopItem->y();
 | 
						|
		} else {
 | 
						|
			_visibleTopItem = nullptr;
 | 
						|
			_visibleTopFromItem = _visibleTop;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
bool InnerWidget::displayScrollDate() const {
 | 
						|
	return (_visibleTop <= height() - 2 * (_visibleBottom - _visibleTop));
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::scrollDateCheck() {
 | 
						|
	if (!_visibleTopItem) {
 | 
						|
		_scrollDateLastItem = nullptr;
 | 
						|
		_scrollDateLastItemTop = 0;
 | 
						|
		scrollDateHide();
 | 
						|
	} else if (_visibleTopItem != _scrollDateLastItem || _visibleTopFromItem != _scrollDateLastItemTop) {
 | 
						|
		// Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem).
 | 
						|
		if (_scrollDateLastItem && !_scrollDateShown) {
 | 
						|
			toggleScrollDateShown();
 | 
						|
		}
 | 
						|
		_scrollDateLastItem = _visibleTopItem;
 | 
						|
		_scrollDateLastItemTop = _visibleTopFromItem;
 | 
						|
		_scrollDateHideTimer.callOnce(kScrollDateHideTimeout);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::scrollDateHideByTimer() {
 | 
						|
	_scrollDateHideTimer.cancel();
 | 
						|
	scrollDateHide();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::scrollDateHide() {
 | 
						|
	if (_scrollDateShown) {
 | 
						|
		toggleScrollDateShown();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::toggleScrollDateShown() {
 | 
						|
	_scrollDateShown = !_scrollDateShown;
 | 
						|
	auto from = _scrollDateShown ? 0. : 1.;
 | 
						|
	auto to = _scrollDateShown ? 1. : 0.;
 | 
						|
	_scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration);
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::repaintScrollDateCallback() {
 | 
						|
	auto updateTop = _visibleTop;
 | 
						|
	auto updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();
 | 
						|
	update(0, updateTop, width(), updateHeight);
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::checkPreloadMore() {
 | 
						|
	if (_visibleTop + PreloadHeightsCount * (_visibleBottom - _visibleTop) > height()) {
 | 
						|
		preloadMore(Direction::Down);
 | 
						|
	}
 | 
						|
	if (_visibleTop < PreloadHeightsCount * (_visibleBottom - _visibleTop)) {
 | 
						|
		preloadMore(Direction::Up);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::applyFilter(FilterValue &&value) {
 | 
						|
	if (_filter != value) {
 | 
						|
		_filter = value;
 | 
						|
		clearAndRequestLog();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::applySearch(const QString &query) {
 | 
						|
	auto clearQuery = query.trimmed();
 | 
						|
	if (_searchQuery != query) {
 | 
						|
		_searchQuery = query;
 | 
						|
		clearAndRequestLog();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::requestAdmins() {
 | 
						|
	auto participantsHash = 0;
 | 
						|
	request(MTPchannels_GetParticipants(
 | 
						|
		_channel->inputChannel,
 | 
						|
		MTP_channelParticipantsAdmins(),
 | 
						|
		MTP_int(0),
 | 
						|
		MTP_int(kMaxChannelAdmins),
 | 
						|
		MTP_int(participantsHash)
 | 
						|
	)).done([this](const MTPchannels_ChannelParticipants &result) {
 | 
						|
		auto readCanEdit = base::overload([](const MTPDchannelParticipantAdmin &v) {
 | 
						|
			return v.is_can_edit();
 | 
						|
		}, [](auto &&) {
 | 
						|
			return false;
 | 
						|
		});
 | 
						|
		Auth().api().parseChannelParticipants(result, [&](
 | 
						|
				int fullCount,
 | 
						|
				const QVector<MTPChannelParticipant> &list) {
 | 
						|
			auto filtered = (
 | 
						|
				list
 | 
						|
			) | ranges::view::transform([&](const MTPChannelParticipant &p) {
 | 
						|
				return std::make_pair(
 | 
						|
					TLHelp::ReadChannelParticipantUserId(p),
 | 
						|
					TLHelp::VisitChannelParticipant(p, readCanEdit));
 | 
						|
			}) | ranges::view::transform([&](auto &&pair) {
 | 
						|
				return std::make_pair(
 | 
						|
					App::userLoaded(pair.first),
 | 
						|
					pair.second);
 | 
						|
			}) | ranges::view::filter([&](auto &&pair) {
 | 
						|
				return (pair.first != nullptr);
 | 
						|
			});
 | 
						|
 | 
						|
			for (auto [user, canEdit] : filtered) {
 | 
						|
				_admins.push_back(user);
 | 
						|
				if (canEdit) {
 | 
						|
					_adminsCanEdit.push_back(user);
 | 
						|
				}
 | 
						|
			}
 | 
						|
		});
 | 
						|
		if (_admins.empty()) {
 | 
						|
			_admins.push_back(App::self());
 | 
						|
		}
 | 
						|
		if (_showFilterCallback) {
 | 
						|
			showFilter(std::move(_showFilterCallback));
 | 
						|
		}
 | 
						|
	}).send();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::showFilter(base::lambda<void(FilterValue &&filter)> callback) {
 | 
						|
	if (_admins.empty()) {
 | 
						|
		_showFilterCallback = std::move(callback);
 | 
						|
	} else {
 | 
						|
		Ui::show(Box<FilterBox>(_channel, _admins, _filter, std::move(callback)));
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::clearAndRequestLog() {
 | 
						|
	request(base::take(_preloadUpRequestId)).cancel();
 | 
						|
	request(base::take(_preloadDownRequestId)).cancel();
 | 
						|
	_filterChanged = true;
 | 
						|
	_upLoaded = false;
 | 
						|
	_downLoaded = true;
 | 
						|
	updateMinMaxIds();
 | 
						|
	preloadMore(Direction::Up);
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::updateEmptyText() {
 | 
						|
	auto options = _defaultOptions;
 | 
						|
	options.flags |= TextParseMarkdown;
 | 
						|
	auto hasSearch = !_searchQuery.isEmpty();
 | 
						|
	auto hasFilter = (_filter.flags != 0) || !_filter.allUsers;
 | 
						|
	auto text = TextWithEntities { lang((hasSearch || hasFilter) ? lng_admin_log_no_results_title : lng_admin_log_no_events_title) };
 | 
						|
	text.entities.append(EntityInText(EntityInTextBold, 0, text.text.size()));
 | 
						|
	auto description = hasSearch
 | 
						|
		? lng_admin_log_no_results_search_text(lt_query, TextUtilities::Clean(_searchQuery))
 | 
						|
		: lang(hasFilter ? lng_admin_log_no_results_text : lng_admin_log_no_events_text);
 | 
						|
	text.text.append(qstr("\n\n") + description);
 | 
						|
	_emptyText.setMarkedText(st::defaultTextStyle, text, options);
 | 
						|
}
 | 
						|
 | 
						|
QString InnerWidget::tooltipText() const {
 | 
						|
	if (_mouseCursorState == HistoryInDateCursorState && _mouseAction == MouseAction::None) {
 | 
						|
		if (auto item = App::hoveredItem()) {
 | 
						|
			auto dateText = item->date.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat));
 | 
						|
			return dateText;
 | 
						|
		}
 | 
						|
	} else if (_mouseCursorState == HistoryInForwardedCursorState && _mouseAction == MouseAction::None) {
 | 
						|
		if (auto item = App::hoveredItem()) {
 | 
						|
			if (auto forwarded = item->Get<HistoryMessageForwarded>()) {
 | 
						|
				return forwarded->_text.originalText(AllTextSelection, ExpandLinksNone);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	} else if (auto lnk = ClickHandler::getActive()) {
 | 
						|
		return lnk->tooltip();
 | 
						|
	}
 | 
						|
	return QString();
 | 
						|
}
 | 
						|
 | 
						|
QPoint InnerWidget::tooltipPos() const {
 | 
						|
	return _mousePosition;
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::saveState(not_null<SectionMemento*> memento) {
 | 
						|
	memento->setFilter(std::move(_filter));
 | 
						|
	memento->setAdmins(std::move(_admins));
 | 
						|
	memento->setAdminsCanEdit(std::move(_adminsCanEdit));
 | 
						|
	memento->setSearchQuery(std::move(_searchQuery));
 | 
						|
	if (!_filterChanged) {
 | 
						|
		memento->setItems(std::move(_items), std::move(_itemsByIds), _upLoaded, _downLoaded);
 | 
						|
		memento->setIdManager(std::move(_idManager));
 | 
						|
	}
 | 
						|
	_upLoaded = _downLoaded = true; // Don't load or handle anything anymore.
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::restoreState(not_null<SectionMemento*> memento) {
 | 
						|
	_items = memento->takeItems();
 | 
						|
	_itemsByIds = memento->takeItemsByIds();
 | 
						|
	_idManager = memento->takeIdManager();
 | 
						|
	_admins = memento->takeAdmins();
 | 
						|
	_adminsCanEdit = memento->takeAdminsCanEdit();
 | 
						|
	_filter = memento->takeFilter();
 | 
						|
	_searchQuery = memento->takeSearchQuery();
 | 
						|
	_upLoaded = memento->upLoaded();
 | 
						|
	_downLoaded = memento->downLoaded();
 | 
						|
	_filterChanged = false;
 | 
						|
	updateMinMaxIds();
 | 
						|
	updateSize();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::preloadMore(Direction direction) {
 | 
						|
	auto &requestId = (direction == Direction::Up) ? _preloadUpRequestId : _preloadDownRequestId;
 | 
						|
	auto &loadedFlag = (direction == Direction::Up) ? _upLoaded : _downLoaded;
 | 
						|
	if (requestId != 0 || loadedFlag) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	auto flags = MTPchannels_GetAdminLog::Flags(0);
 | 
						|
	auto filter = MTP_channelAdminLogEventsFilter(MTP_flags(_filter.flags));
 | 
						|
	if (_filter.flags != 0) {
 | 
						|
		flags |= MTPchannels_GetAdminLog::Flag::f_events_filter;
 | 
						|
	}
 | 
						|
	auto admins = QVector<MTPInputUser>(0);
 | 
						|
	if (!_filter.allUsers) {
 | 
						|
		if (!_filter.admins.empty()) {
 | 
						|
			admins.reserve(_filter.admins.size());
 | 
						|
			for (auto &admin : _filter.admins) {
 | 
						|
				admins.push_back(admin->inputUser);
 | 
						|
			}
 | 
						|
		}
 | 
						|
		flags |= MTPchannels_GetAdminLog::Flag::f_admins;
 | 
						|
	}
 | 
						|
	auto maxId = (direction == Direction::Up) ? _minId : 0;
 | 
						|
	auto minId = (direction == Direction::Up) ? 0 : _maxId;
 | 
						|
	auto perPage = _items.empty() ? kEventsFirstPage : kEventsPerPage;
 | 
						|
	requestId = request(MTPchannels_GetAdminLog(MTP_flags(flags), _channel->inputChannel, MTP_string(_searchQuery), filter, MTP_vector<MTPInputUser>(admins), MTP_long(maxId), MTP_long(minId), MTP_int(perPage))).done([this, &requestId, &loadedFlag, direction](const MTPchannels_AdminLogResults &result) {
 | 
						|
		Expects(result.type() == mtpc_channels_adminLogResults);
 | 
						|
		requestId = 0;
 | 
						|
 | 
						|
		auto &results = result.c_channels_adminLogResults();
 | 
						|
		App::feedUsers(results.vusers);
 | 
						|
		App::feedChats(results.vchats);
 | 
						|
		if (!loadedFlag) {
 | 
						|
			addEvents(direction, results.vevents.v);
 | 
						|
		}
 | 
						|
	}).fail([this, &requestId, &loadedFlag](const RPCError &error) {
 | 
						|
		requestId = 0;
 | 
						|
		loadedFlag = true;
 | 
						|
		update();
 | 
						|
	}).send();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::addEvents(Direction direction, const QVector<MTPChannelAdminLogEvent> &events) {
 | 
						|
	if (_filterChanged) {
 | 
						|
		clearAfterFilterChange();
 | 
						|
	}
 | 
						|
 | 
						|
	auto up = (direction == Direction::Up);
 | 
						|
	if (events.empty()) {
 | 
						|
		(up ? _upLoaded : _downLoaded) = true;
 | 
						|
		update();
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	// When loading items up we just add them to the back of the _items vector.
 | 
						|
	// When loading items down we add them to a new vector and copy _items after them.
 | 
						|
	auto newItemsForDownDirection = std::vector<HistoryItemOwned>();
 | 
						|
	auto oldItemsCount = _items.size();
 | 
						|
	auto &addToItems = (direction == Direction::Up) ? _items : newItemsForDownDirection;
 | 
						|
	addToItems.reserve(oldItemsCount + events.size() * 2);
 | 
						|
	for_const (auto &event, events) {
 | 
						|
		Assert(event.type() == mtpc_channelAdminLogEvent);
 | 
						|
		auto &data = event.c_channelAdminLogEvent();
 | 
						|
		if (_itemsByIds.find(data.vid.v) != _itemsByIds.cend()) {
 | 
						|
			continue;
 | 
						|
		}
 | 
						|
 | 
						|
		auto count = 0;
 | 
						|
		GenerateItems(_history, _idManager, data, [this, id = data.vid.v, &addToItems, &count](HistoryItemOwned item) {
 | 
						|
			_itemsByIds.emplace(id, item.get());
 | 
						|
			addToItems.push_back(std::move(item));
 | 
						|
			++count;
 | 
						|
		});
 | 
						|
		if (count > 1) {
 | 
						|
			// Reverse the inner order of the added messages, because we load events
 | 
						|
			// from bottom to top but inside one event they go from top to bottom.
 | 
						|
			auto full = addToItems.size();
 | 
						|
			auto from = full - count;
 | 
						|
			for (auto i = 0, toReverse = count / 2; i != toReverse; ++i) {
 | 
						|
				std::swap(addToItems[from + i], addToItems[full - i - 1]);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	auto newItemsCount = _items.size() + ((direction == Direction::Up) ? 0 : newItemsForDownDirection.size());
 | 
						|
	if (newItemsCount != oldItemsCount) {
 | 
						|
		if (direction == Direction::Down) {
 | 
						|
			for (auto &item : _items) {
 | 
						|
				newItemsForDownDirection.push_back(std::move(item));
 | 
						|
			}
 | 
						|
			_items = std::move(newItemsForDownDirection);
 | 
						|
		}
 | 
						|
		updateMinMaxIds();
 | 
						|
		itemsAdded(direction, newItemsCount - oldItemsCount);
 | 
						|
	}
 | 
						|
	update();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::updateMinMaxIds() {
 | 
						|
	if (_itemsByIds.empty() || _filterChanged) {
 | 
						|
		_maxId = _minId = 0;
 | 
						|
	} else {
 | 
						|
		_maxId = (--_itemsByIds.end())->first;
 | 
						|
		_minId = _itemsByIds.begin()->first;
 | 
						|
		if (_minId == 1) {
 | 
						|
			_upLoaded = true;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::itemsAdded(Direction direction, int addedCount) {
 | 
						|
	Expects(addedCount >= 0);
 | 
						|
	auto checkFrom = (direction == Direction::Up) ? (_items.size() - addedCount) : 1; // Should be ": 0", but zero is skipped anyway.
 | 
						|
	auto checkTo = (direction == Direction::Up) ? (_items.size() + 1) : (addedCount + 1);
 | 
						|
	for (auto i = checkFrom; i != checkTo; ++i) {
 | 
						|
		if (i > 0) {
 | 
						|
			auto item = _items[i - 1].get();
 | 
						|
			if (i < _items.size()) {
 | 
						|
				auto previous = _items[i].get();
 | 
						|
				item->setLogEntryDisplayDate(item->date.date() != previous->date.date());
 | 
						|
				auto attachToPrevious = item->computeIsAttachToPrevious(previous);
 | 
						|
				item->setLogEntryAttachToPrevious(attachToPrevious);
 | 
						|
				previous->setLogEntryAttachToNext(attachToPrevious);
 | 
						|
			} else {
 | 
						|
				item->setLogEntryDisplayDate(true);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	updateSize();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::updateSize() {
 | 
						|
	TWidget::resizeToWidth(width());
 | 
						|
	restoreScrollPosition();
 | 
						|
	updateVisibleTopItem();
 | 
						|
	checkPreloadMore();
 | 
						|
}
 | 
						|
 | 
						|
int InnerWidget::resizeGetHeight(int newWidth) {
 | 
						|
	update();
 | 
						|
 | 
						|
	auto newHeight = 0;
 | 
						|
	for (auto &item : base::reversed(_items)) {
 | 
						|
		item->setY(newHeight);
 | 
						|
		newHeight += item->resizeGetHeight(newWidth);
 | 
						|
	}
 | 
						|
	_itemsHeight = newHeight;
 | 
						|
	_itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom) ? (_minHeight - _itemsHeight - st::historyPaddingBottom) : 0;
 | 
						|
	return _itemsTop + _itemsHeight + st::historyPaddingBottom;
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::restoreScrollPosition() {
 | 
						|
	auto newVisibleTop = _visibleTopItem ? (itemTop(_visibleTopItem) + _visibleTopFromItem) : ScrollMax;
 | 
						|
	scrollToSignal.notify(newVisibleTop, true);
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::paintEvent(QPaintEvent *e) {
 | 
						|
	if (Ui::skipPaintEvent(this, e)) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	Painter p(this);
 | 
						|
 | 
						|
	auto ms = getms();
 | 
						|
	auto clip = e->rect();
 | 
						|
 | 
						|
	if (_items.empty() && _upLoaded && _downLoaded) {
 | 
						|
		paintEmpty(p);
 | 
						|
	} else {
 | 
						|
		auto begin = std::rbegin(_items), end = std::rend(_items);
 | 
						|
		auto from = std::lower_bound(begin, end, clip.top(), [this](auto &elem, int top) {
 | 
						|
			return this->itemTop(elem) + elem->height() <= top;
 | 
						|
		});
 | 
						|
		auto to = std::lower_bound(begin, end, clip.top() + clip.height(), [this](auto &elem, int bottom) {
 | 
						|
			return this->itemTop(elem) < bottom;
 | 
						|
		});
 | 
						|
		if (from != end) {
 | 
						|
			auto top = itemTop(from->get());
 | 
						|
			p.translate(0, top);
 | 
						|
			for (auto i = from; i != to; ++i) {
 | 
						|
				auto selection = (*i == _selectedItem) ? _selectedText : TextSelection();
 | 
						|
				(*i)->draw(p, clip.translated(0, -top), selection, ms);
 | 
						|
				auto height = (*i)->height();
 | 
						|
				top += height;
 | 
						|
				p.translate(0, height);
 | 
						|
			}
 | 
						|
			p.translate(0, -top);
 | 
						|
 | 
						|
			enumerateUserpics([&p, &clip](not_null<HistoryMessage*> message, int userpicTop) {
 | 
						|
				// stop the enumeration if the userpic is below the painted rect
 | 
						|
				if (userpicTop >= clip.top() + clip.height()) {
 | 
						|
					return false;
 | 
						|
				}
 | 
						|
 | 
						|
				// paint the userpic if it intersects the painted rect
 | 
						|
				if (userpicTop + st::msgPhotoSize > clip.top()) {
 | 
						|
					message->from()->paintUserpicLeft(p, st::historyPhotoLeft, userpicTop, message->width(), st::msgPhotoSize);
 | 
						|
				}
 | 
						|
				return true;
 | 
						|
			});
 | 
						|
 | 
						|
			auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
 | 
						|
			auto scrollDateOpacity = _scrollDateOpacity.current(ms, _scrollDateShown ? 1. : 0.);
 | 
						|
			enumerateDates([&p, &clip, scrollDateOpacity, dateHeight/*, lastDate, showFloatingBefore*/](not_null<HistoryItem*> item, int itemtop, int dateTop) {
 | 
						|
				// stop the enumeration if the date is above the painted rect
 | 
						|
				if (dateTop + dateHeight <= clip.top()) {
 | 
						|
					return false;
 | 
						|
				}
 | 
						|
 | 
						|
				bool displayDate = item->displayDate();
 | 
						|
				bool dateInPlace = displayDate;
 | 
						|
				if (dateInPlace) {
 | 
						|
					int correctDateTop = itemtop + st::msgServiceMargin.top();
 | 
						|
					dateInPlace = (dateTop < correctDateTop + dateHeight);
 | 
						|
				}
 | 
						|
				//bool noFloatingDate = (item->date.date() == lastDate && displayDate);
 | 
						|
				//if (noFloatingDate) {
 | 
						|
				//	if (itemtop < showFloatingBefore) {
 | 
						|
				//		noFloatingDate = false;
 | 
						|
				//	}
 | 
						|
				//}
 | 
						|
 | 
						|
				// paint the date if it intersects the painted rect
 | 
						|
				if (dateTop < clip.top() + clip.height()) {
 | 
						|
					auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;
 | 
						|
					if (opacity > 0.) {
 | 
						|
						p.setOpacity(opacity);
 | 
						|
						int dateY = /*noFloatingDate ? itemtop :*/ (dateTop - st::msgServiceMargin.top());
 | 
						|
						int width = item->width();
 | 
						|
						if (auto date = item->Get<HistoryMessageDate>()) {
 | 
						|
							date->paint(p, dateY, width);
 | 
						|
						} else {
 | 
						|
							HistoryLayout::ServiceMessagePainter::paintDate(p, item->date, dateY, width);
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
				return true;
 | 
						|
			});
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::clearAfterFilterChange() {
 | 
						|
	_visibleTopItem = nullptr;
 | 
						|
	_visibleTopFromItem = 0;
 | 
						|
	_scrollDateLastItem = nullptr;
 | 
						|
	_scrollDateLastItemTop = 0;
 | 
						|
	_mouseActionItem = nullptr;
 | 
						|
	_selectedItem = nullptr;
 | 
						|
	_selectedText = TextSelection();
 | 
						|
	_filterChanged = false;
 | 
						|
	_items.clear();
 | 
						|
	_itemsByIds.clear();
 | 
						|
	_idManager = LocalIdManager();
 | 
						|
	updateEmptyText();
 | 
						|
	updateSize();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::paintEmpty(Painter &p) {
 | 
						|
	style::font font(st::msgServiceFont);
 | 
						|
	auto rectWidth = st::historyAdminLogEmptyWidth;
 | 
						|
	auto innerWidth = rectWidth - st::historyAdminLogEmptyPadding.left() - st::historyAdminLogEmptyPadding.right();
 | 
						|
	auto rectHeight = st::historyAdminLogEmptyPadding.top() + _emptyText.countHeight(innerWidth) + st::historyAdminLogEmptyPadding.bottom();
 | 
						|
	auto rect = QRect((width() - rectWidth) / 2, (height() - rectHeight) / 3, rectWidth, rectHeight);
 | 
						|
	HistoryLayout::ServiceMessagePainter::paintBubble(p, rect.x(), rect.y(), rect.width(), rect.height());
 | 
						|
 | 
						|
	p.setPen(st::msgServiceFg);
 | 
						|
	_emptyText.draw(p, rect.x() + st::historyAdminLogEmptyPadding.left(), rect.y() + st::historyAdminLogEmptyPadding.top(), innerWidth, style::al_top);
 | 
						|
}
 | 
						|
 | 
						|
TextWithEntities InnerWidget::getSelectedText() const {
 | 
						|
	return _selectedItem ? _selectedItem->selectedText(_selectedText) : TextWithEntities();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::keyPressEvent(QKeyEvent *e) {
 | 
						|
	if (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) {
 | 
						|
		cancelledSignal.notify(true);
 | 
						|
	} else if (e == QKeySequence::Copy && _selectedItem != nullptr) {
 | 
						|
		copySelectedText();
 | 
						|
#ifdef Q_OS_MAC
 | 
						|
	} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
 | 
						|
		setToClipboard(getSelectedText(), QClipboard::FindBuffer);
 | 
						|
#endif // Q_OS_MAC
 | 
						|
	} else {
 | 
						|
		e->ignore();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::mouseDoubleClickEvent(QMouseEvent *e) {
 | 
						|
	mouseActionStart(e->globalPos(), e->button());
 | 
						|
	if (((_mouseAction == MouseAction::Selecting && _selectedItem != nullptr) || (_mouseAction == MouseAction::None)) && _mouseSelectType == TextSelectType::Letters && _mouseActionItem) {
 | 
						|
		HistoryStateRequest request;
 | 
						|
		request.flags |= Text::StateRequest::Flag::LookupSymbol;
 | 
						|
		auto dragState = _mouseActionItem->getState(_dragStartPosition, request);
 | 
						|
		if (dragState.cursor == HistoryInTextCursorState) {
 | 
						|
			_mouseTextSymbol = dragState.symbol;
 | 
						|
			_mouseSelectType = TextSelectType::Words;
 | 
						|
			if (_mouseAction == MouseAction::None) {
 | 
						|
				_mouseAction = MouseAction::Selecting;
 | 
						|
				auto selection = TextSelection { dragState.symbol, dragState.symbol };
 | 
						|
				repaintItem(std::exchange(_selectedItem, _mouseActionItem));
 | 
						|
				_selectedText = selection;
 | 
						|
			}
 | 
						|
			mouseMoveEvent(e);
 | 
						|
 | 
						|
			_trippleClickPoint = e->globalPos();
 | 
						|
			_trippleClickTimer.callOnce(QApplication::doubleClickInterval());
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::contextMenuEvent(QContextMenuEvent *e) {
 | 
						|
	showContextMenu(e);
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 | 
						|
	if (_menu) {
 | 
						|
		_menu->deleteLater();
 | 
						|
		_menu = 0;
 | 
						|
	}
 | 
						|
	if (e->reason() == QContextMenuEvent::Mouse) {
 | 
						|
		mouseActionUpdate(e->globalPos());
 | 
						|
	}
 | 
						|
 | 
						|
	// -1 - has selection, but no over, 0 - no selection, 1 - over text
 | 
						|
	auto isUponSelected = 0;
 | 
						|
	auto hasSelected = 0;
 | 
						|
	if (_selectedItem) {
 | 
						|
		isUponSelected = -1;
 | 
						|
 | 
						|
		auto selFrom = _selectedText.from;
 | 
						|
		auto selTo = _selectedText.to;
 | 
						|
		hasSelected = (selTo > selFrom) ? 1 : 0;
 | 
						|
		if (App::mousedItem() && App::mousedItem() == App::hoveredItem()) {
 | 
						|
			auto mousePos = mapPointToItem(mapFromGlobal(_mousePosition), App::mousedItem());
 | 
						|
			HistoryStateRequest request;
 | 
						|
			request.flags |= Text::StateRequest::Flag::LookupSymbol;
 | 
						|
			auto dragState = App::mousedItem()->getState(mousePos, request);
 | 
						|
			if (dragState.cursor == HistoryInTextCursorState && dragState.symbol >= selFrom && dragState.symbol < selTo) {
 | 
						|
				isUponSelected = 1;
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (showFromTouch && hasSelected && isUponSelected < hasSelected) {
 | 
						|
		isUponSelected = hasSelected;
 | 
						|
	}
 | 
						|
 | 
						|
	_menu = new Ui::PopupMenu(nullptr);
 | 
						|
 | 
						|
	_contextMenuLink = ClickHandler::getActive();
 | 
						|
	auto item = App::hoveredItem() ? App::hoveredItem() : App::hoveredLinkItem();
 | 
						|
	auto lnkPhoto = dynamic_cast<PhotoClickHandler*>(_contextMenuLink.data());
 | 
						|
	auto lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLink.data());
 | 
						|
	auto lnkPeer = dynamic_cast<PeerClickHandler*>(_contextMenuLink.data());
 | 
						|
	auto lnkIsVideo = lnkDocument ? lnkDocument->document()->isVideo() : false;
 | 
						|
	auto lnkIsAudio = lnkDocument ? (lnkDocument->document()->voice() != nullptr) : false;
 | 
						|
	auto lnkIsSong = lnkDocument ? (lnkDocument->document()->song() != nullptr) : false;
 | 
						|
	if (lnkPhoto || lnkDocument) {
 | 
						|
		if (isUponSelected > 0) {
 | 
						|
			_menu->addAction(lang(lng_context_copy_selected), [this] { copySelectedText(); })->setEnabled(true);
 | 
						|
		}
 | 
						|
		if (lnkPhoto) {
 | 
						|
			_menu->addAction(lang(lng_context_save_image), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, photo = lnkPhoto->photo()] {
 | 
						|
				savePhotoToFile(photo);
 | 
						|
			}))->setEnabled(true);
 | 
						|
			_menu->addAction(lang(lng_context_copy_image), [this, photo = lnkPhoto->photo()] {
 | 
						|
				copyContextImage(photo);
 | 
						|
			})->setEnabled(true);
 | 
						|
		} else {
 | 
						|
			auto document = lnkDocument->document();
 | 
						|
			if (document->loading()) {
 | 
						|
				_menu->addAction(lang(lng_context_cancel_download), [this] { cancelContextDownload(); })->setEnabled(true);
 | 
						|
			} else {
 | 
						|
				if (document->loaded() && document->isGifv()) {
 | 
						|
					if (!cAutoPlayGif()) {
 | 
						|
						_menu->addAction(lang(lng_context_open_gif), [this] { openContextGif(); })->setEnabled(true);
 | 
						|
					}
 | 
						|
				}
 | 
						|
				if (!document->filepath(DocumentData::FilePathResolveChecked).isEmpty()) {
 | 
						|
					_menu->addAction(lang((cPlatform() == dbipMac || cPlatform() == dbipMacOld) ? lng_context_show_in_finder : lng_context_show_in_folder), [this] { showContextInFolder(); })->setEnabled(true);
 | 
						|
				}
 | 
						|
				_menu->addAction(lang(lnkIsVideo ? lng_context_save_video : (lnkIsAudio ? lng_context_save_audio : (lnkIsSong ? lng_context_save_audio_file : lng_context_save_file))), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
 | 
						|
					saveDocumentToFile(document);
 | 
						|
				}))->setEnabled(true);
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if (App::hoveredLinkItem()) {
 | 
						|
			App::contextItem(App::hoveredLinkItem());
 | 
						|
		}
 | 
						|
	} else if (lnkPeer) { // suggest to block
 | 
						|
		if (auto user = lnkPeer->peer()->asUser()) {
 | 
						|
			suggestRestrictUser(user);
 | 
						|
		}
 | 
						|
	} else { // maybe cursor on some text history item?
 | 
						|
		bool canDelete = item && item->canDelete() && (item->id > 0 || !item->serviceMsg());
 | 
						|
		bool canForward = item && item->canForward();
 | 
						|
 | 
						|
		auto msg = dynamic_cast<HistoryMessage*>(item);
 | 
						|
		if (isUponSelected > 0) {
 | 
						|
			_menu->addAction(lang(lng_context_copy_selected), [this] { copySelectedText(); })->setEnabled(true);
 | 
						|
		} else {
 | 
						|
			if (item && !isUponSelected) {
 | 
						|
				auto mediaHasTextForCopy = false;
 | 
						|
				if (auto media = (msg ? msg->getMedia() : nullptr)) {
 | 
						|
					mediaHasTextForCopy = media->hasTextForCopy();
 | 
						|
					if (media->type() == MediaTypeWebPage && static_cast<HistoryWebPage*>(media)->attach()) {
 | 
						|
						media = static_cast<HistoryWebPage*>(media)->attach();
 | 
						|
					}
 | 
						|
					if (media->type() == MediaTypeSticker) {
 | 
						|
						if (auto document = media->getDocument()) {
 | 
						|
							if (document->sticker() && document->sticker()->set.type() != mtpc_inputStickerSetEmpty) {
 | 
						|
								_menu->addAction(lang(document->sticker()->setInstalled() ? lng_context_pack_info : lng_context_pack_add), [this] { showStickerPackInfo(); });
 | 
						|
							}
 | 
						|
							_menu->addAction(lang(lng_context_save_image), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
 | 
						|
								saveDocumentToFile(document);
 | 
						|
							}))->setEnabled(true);
 | 
						|
						}
 | 
						|
					} else if (media->type() == MediaTypeGif && !_contextMenuLink) {
 | 
						|
						if (auto document = media->getDocument()) {
 | 
						|
							if (document->loading()) {
 | 
						|
								_menu->addAction(lang(lng_context_cancel_download), [this] { cancelContextDownload(); })->setEnabled(true);
 | 
						|
							} else {
 | 
						|
								if (document->isGifv()) {
 | 
						|
									if (!cAutoPlayGif()) {
 | 
						|
										_menu->addAction(lang(lng_context_open_gif), [this] { openContextGif(); })->setEnabled(true);
 | 
						|
									}
 | 
						|
								}
 | 
						|
								if (!document->filepath(DocumentData::FilePathResolveChecked).isEmpty()) {
 | 
						|
									_menu->addAction(lang((cPlatform() == dbipMac || cPlatform() == dbipMacOld) ? lng_context_show_in_finder : lng_context_show_in_folder), [this] { showContextInFolder(); })->setEnabled(true);
 | 
						|
								}
 | 
						|
								_menu->addAction(lang(lng_context_save_file), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
 | 
						|
									saveDocumentToFile(document);
 | 
						|
								}))->setEnabled(true);
 | 
						|
							}
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
				if (msg && !_contextMenuLink && (!msg->emptyText() || mediaHasTextForCopy)) {
 | 
						|
					_menu->addAction(lang(lng_context_copy_text), [this] { copyContextText(); })->setEnabled(true);
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		auto linkCopyToClipboardText = _contextMenuLink ? _contextMenuLink->copyToClipboardContextItemText() : QString();
 | 
						|
		if (!linkCopyToClipboardText.isEmpty()) {
 | 
						|
			_menu->addAction(linkCopyToClipboardText, [this] { copyContextUrl(); })->setEnabled(true);
 | 
						|
		}
 | 
						|
		App::contextItem(item);
 | 
						|
	}
 | 
						|
 | 
						|
	if (_menu->actions().isEmpty()) {
 | 
						|
		delete base::take(_menu);
 | 
						|
	} else {
 | 
						|
		connect(_menu, &QObject::destroyed, this, [this](QObject *object) {
 | 
						|
			if (_menu == object) {
 | 
						|
				_menu = nullptr;
 | 
						|
			}
 | 
						|
		});
 | 
						|
		_menu->popup(e->globalPos());
 | 
						|
		e->accept();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::savePhotoToFile(PhotoData *photo) {
 | 
						|
	if (!photo || !photo->date || !photo->loaded()) return;
 | 
						|
 | 
						|
	auto filter = qsl("JPEG Image (*.jpg);;") + FileDialog::AllFilesFilter();
 | 
						|
	FileDialog::GetWritePath(lang(lng_save_photo), filter, filedialogDefaultName(qsl("photo"), qsl(".jpg")), base::lambda_guarded(this, [this, photo](const QString &result) {
 | 
						|
		if (!result.isEmpty()) {
 | 
						|
			photo->full->pix().toImage().save(result, "JPG");
 | 
						|
		}
 | 
						|
	}));
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::saveDocumentToFile(DocumentData *document) {
 | 
						|
	DocumentSaveClickHandler::doSave(document, true);
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::copyContextImage(PhotoData *photo) {
 | 
						|
	if (!photo || !photo->date || !photo->loaded()) return;
 | 
						|
 | 
						|
	QApplication::clipboard()->setPixmap(photo->full->pix());
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::copySelectedText() {
 | 
						|
	setToClipboard(getSelectedText());
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::copyContextUrl() {
 | 
						|
	if (_contextMenuLink) {
 | 
						|
		_contextMenuLink->copyToClipboard();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::showStickerPackInfo() {
 | 
						|
	if (!App::contextItem()) return;
 | 
						|
 | 
						|
	if (auto media = App::contextItem()->getMedia()) {
 | 
						|
		if (auto doc = media->getDocument()) {
 | 
						|
			if (auto sticker = doc->sticker()) {
 | 
						|
				if (sticker->set.type() != mtpc_inputStickerSetEmpty) {
 | 
						|
					App::main()->stickersBox(sticker->set);
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::cancelContextDownload() {
 | 
						|
	if (auto lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLink.data())) {
 | 
						|
		lnkDocument->document()->cancel();
 | 
						|
	} else if (auto item = App::contextItem()) {
 | 
						|
		if (auto media = item->getMedia()) {
 | 
						|
			if (auto doc = media->getDocument()) {
 | 
						|
				doc->cancel();
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::showContextInFolder() {
 | 
						|
	QString filepath;
 | 
						|
	if (auto lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLink.data())) {
 | 
						|
		filepath = lnkDocument->document()->filepath(DocumentData::FilePathResolveChecked);
 | 
						|
	} else if (auto item = App::contextItem()) {
 | 
						|
		if (auto media = item->getMedia()) {
 | 
						|
			if (auto doc = media->getDocument()) {
 | 
						|
				filepath = doc->filepath(DocumentData::FilePathResolveChecked);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (!filepath.isEmpty()) {
 | 
						|
		File::ShowInFolder(filepath);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::openContextGif() {
 | 
						|
	if (auto item = App::contextItem()) {
 | 
						|
		if (auto media = item->getMedia()) {
 | 
						|
			if (auto document = media->getDocument()) {
 | 
						|
				Messenger::Instance().showDocument(document, item);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::copyContextText() {
 | 
						|
	auto item = App::contextItem();
 | 
						|
	if (!item || (item->getMedia() && item->getMedia()->type() == MediaTypeSticker)) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	setToClipboard(item->selectedText(FullSelection));
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::setToClipboard(const TextWithEntities &forClipboard, QClipboard::Mode mode) {
 | 
						|
	if (auto data = MimeDataFromTextWithEntities(forClipboard)) {
 | 
						|
		QApplication::clipboard()->setMimeData(data.release(), mode);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::suggestRestrictUser(not_null<UserData*> user) {
 | 
						|
	Expects(_menu != nullptr);
 | 
						|
	if (!_channel->isMegagroup() || !_channel->canBanMembers() || _admins.empty()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	if (base::contains(_admins, user)) {
 | 
						|
		if (!base::contains(_adminsCanEdit, user)) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	_menu->addAction(lang(lng_context_restrict_user), [this, user] {
 | 
						|
		auto editRestrictions = [user, this](bool hasAdminRights, const MTPChannelBannedRights ¤tRights) {
 | 
						|
			auto weak = QPointer<InnerWidget>(this);
 | 
						|
			auto weakBox = std::make_shared<QPointer<EditRestrictedBox>>();
 | 
						|
			auto box = Box<EditRestrictedBox>(_channel, user, hasAdminRights, currentRights);
 | 
						|
			box->setSaveCallback([user, weak, weakBox](const MTPChannelBannedRights &oldRights, const MTPChannelBannedRights &newRights) {
 | 
						|
				if (weak) {
 | 
						|
					weak->restrictUser(user, oldRights, newRights);
 | 
						|
				}
 | 
						|
				if (*weakBox) {
 | 
						|
					(*weakBox)->closeBox();
 | 
						|
				}
 | 
						|
			});
 | 
						|
			*weakBox = Ui::show(
 | 
						|
				std::move(box),
 | 
						|
				LayerOption::KeepOther);
 | 
						|
		};
 | 
						|
		if (base::contains(_admins, user)) {
 | 
						|
			editRestrictions(true, MTP_channelBannedRights(MTP_flags(0), MTP_int(0)));
 | 
						|
		} else {
 | 
						|
			request(MTPchannels_GetParticipant(_channel->inputChannel, user->inputUser)).done([this, editRestrictions](const MTPchannels_ChannelParticipant &result) {
 | 
						|
				Expects(result.type() == mtpc_channels_channelParticipant);
 | 
						|
 | 
						|
				auto &participant = result.c_channels_channelParticipant();
 | 
						|
				App::feedUsers(participant.vusers);
 | 
						|
				auto type = participant.vparticipant.type();
 | 
						|
				if (type == mtpc_channelParticipantBanned) {
 | 
						|
					auto &banned = participant.vparticipant.c_channelParticipantBanned();
 | 
						|
					editRestrictions(false, banned.vbanned_rights);
 | 
						|
				} else {
 | 
						|
					auto hasAdminRights = (type == mtpc_channelParticipantAdmin)
 | 
						|
						|| (type == mtpc_channelParticipantCreator);
 | 
						|
					auto bannedRights = MTP_channelBannedRights(
 | 
						|
						MTP_flags(0),
 | 
						|
						MTP_int(0));
 | 
						|
					editRestrictions(hasAdminRights, bannedRights);
 | 
						|
				}
 | 
						|
			}).fail([this, editRestrictions](const RPCError &error) {
 | 
						|
				auto bannedRights = MTP_channelBannedRights(
 | 
						|
					MTP_flags(0),
 | 
						|
					MTP_int(0));
 | 
						|
				editRestrictions(false, bannedRights);
 | 
						|
			}).send();
 | 
						|
		}
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::restrictUser(not_null<UserData*> user, const MTPChannelBannedRights &oldRights, const MTPChannelBannedRights &newRights) {
 | 
						|
	auto weak = QPointer<InnerWidget>(this);
 | 
						|
	MTP::send(MTPchannels_EditBanned(_channel->inputChannel, user->inputUser, newRights), rpcDone([megagroup = _channel.get(), user, weak, oldRights, newRights](const MTPUpdates &result) {
 | 
						|
		Auth().api().applyUpdates(result);
 | 
						|
		megagroup->applyEditBanned(user, oldRights, newRights);
 | 
						|
		if (weak) {
 | 
						|
			weak->restrictUserDone(user, newRights);
 | 
						|
		}
 | 
						|
	}));
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::restrictUserDone(not_null<UserData*> user, const MTPChannelBannedRights &rights) {
 | 
						|
	Expects(rights.type() == mtpc_channelBannedRights);
 | 
						|
	if (rights.c_channelBannedRights().vflags.v) {
 | 
						|
		_admins.erase(std::remove(_admins.begin(), _admins.end(), user), _admins.end());
 | 
						|
		_adminsCanEdit.erase(std::remove(_adminsCanEdit.begin(), _adminsCanEdit.end(), user), _adminsCanEdit.end());
 | 
						|
	}
 | 
						|
	_downLoaded = false;
 | 
						|
	checkPreloadMore();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::mousePressEvent(QMouseEvent *e) {
 | 
						|
	if (_menu) {
 | 
						|
		e->accept();
 | 
						|
		return; // ignore mouse press, that was hiding context menu
 | 
						|
	}
 | 
						|
	mouseActionStart(e->globalPos(), e->button());
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::mouseMoveEvent(QMouseEvent *e) {
 | 
						|
	auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
 | 
						|
	if (!buttonsPressed && _mouseAction != MouseAction::None) {
 | 
						|
		mouseReleaseEvent(e);
 | 
						|
	}
 | 
						|
	mouseActionUpdate(e->globalPos());
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::mouseReleaseEvent(QMouseEvent *e) {
 | 
						|
	mouseActionFinish(e->globalPos(), e->button());
 | 
						|
	if (!rect().contains(e->pos())) {
 | 
						|
		leaveEvent(e);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::enterEventHook(QEvent *e) {
 | 
						|
	mouseActionUpdate(QCursor::pos());
 | 
						|
	return TWidget::enterEventHook(e);
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::leaveEventHook(QEvent *e) {
 | 
						|
	if (auto item = App::hoveredItem()) {
 | 
						|
		repaintItem(item);
 | 
						|
		App::hoveredItem(nullptr);
 | 
						|
	}
 | 
						|
	ClickHandler::clearActive();
 | 
						|
	Ui::Tooltip::Hide();
 | 
						|
	if (!ClickHandler::getPressed() && _cursor != style::cur_default) {
 | 
						|
		_cursor = style::cur_default;
 | 
						|
		setCursor(_cursor);
 | 
						|
	}
 | 
						|
	return TWidget::leaveEventHook(e);
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) {
 | 
						|
	mouseActionUpdate(screenPos);
 | 
						|
	if (button != Qt::LeftButton) return;
 | 
						|
 | 
						|
	ClickHandler::pressed();
 | 
						|
	if (App::pressedItem() != App::hoveredItem()) {
 | 
						|
		repaintItem(App::pressedItem());
 | 
						|
		App::pressedItem(App::hoveredItem());
 | 
						|
		repaintItem(App::pressedItem());
 | 
						|
	}
 | 
						|
 | 
						|
	_mouseAction = MouseAction::None;
 | 
						|
	_mouseActionItem = App::mousedItem();
 | 
						|
	_dragStartPosition = mapPointToItem(mapFromGlobal(screenPos), _mouseActionItem);
 | 
						|
	_pressWasInactive = _controller->window()->wasInactivePress();
 | 
						|
	if (_pressWasInactive) _controller->window()->setInactivePress(false);
 | 
						|
 | 
						|
	if (ClickHandler::getPressed()) {
 | 
						|
		_mouseAction = MouseAction::PrepareDrag;
 | 
						|
	}
 | 
						|
	if (_mouseAction == MouseAction::None && _mouseActionItem) {
 | 
						|
		HistoryTextState dragState;
 | 
						|
		if (_trippleClickTimer.isActive() && (screenPos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) {
 | 
						|
			HistoryStateRequest request;
 | 
						|
			request.flags = Text::StateRequest::Flag::LookupSymbol;
 | 
						|
			dragState = _mouseActionItem->getState(_dragStartPosition, request);
 | 
						|
			if (dragState.cursor == HistoryInTextCursorState) {
 | 
						|
				auto selection = TextSelection { dragState.symbol, dragState.symbol };
 | 
						|
				repaintItem(std::exchange(_selectedItem, _mouseActionItem));
 | 
						|
				_selectedText = selection;
 | 
						|
				_mouseTextSymbol = dragState.symbol;
 | 
						|
				_mouseAction = MouseAction::Selecting;
 | 
						|
				_mouseSelectType = TextSelectType::Paragraphs;
 | 
						|
				mouseActionUpdate(_mousePosition);
 | 
						|
				_trippleClickTimer.callOnce(QApplication::doubleClickInterval());
 | 
						|
			}
 | 
						|
		} else if (App::pressedItem()) {
 | 
						|
			HistoryStateRequest request;
 | 
						|
			request.flags = Text::StateRequest::Flag::LookupSymbol;
 | 
						|
			dragState = _mouseActionItem->getState(_dragStartPosition, request);
 | 
						|
		}
 | 
						|
		if (_mouseSelectType != TextSelectType::Paragraphs) {
 | 
						|
			if (App::pressedItem()) {
 | 
						|
				_mouseTextSymbol = dragState.symbol;
 | 
						|
				auto uponSelected = (dragState.cursor == HistoryInTextCursorState);
 | 
						|
				if (uponSelected) {
 | 
						|
					if (!_selectedItem || _selectedItem != _mouseActionItem) {
 | 
						|
						uponSelected = false;
 | 
						|
					} else if (_mouseTextSymbol < _selectedText.from || _mouseTextSymbol >= _selectedText.to) {
 | 
						|
						uponSelected = false;
 | 
						|
					}
 | 
						|
				}
 | 
						|
				if (uponSelected) {
 | 
						|
					_mouseAction = MouseAction::PrepareDrag; // start text drag
 | 
						|
				} else if (!_pressWasInactive) {
 | 
						|
					if (dragState.afterSymbol) ++_mouseTextSymbol;
 | 
						|
					auto selection = TextSelection { _mouseTextSymbol, _mouseTextSymbol };
 | 
						|
					repaintItem(std::exchange(_selectedItem, _mouseActionItem));
 | 
						|
					_selectedText = selection;
 | 
						|
					_mouseAction = MouseAction::Selecting;
 | 
						|
					repaintItem(_mouseActionItem);
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if (!_mouseActionItem) {
 | 
						|
		_mouseAction = MouseAction::None;
 | 
						|
	} else if (_mouseAction == MouseAction::None) {
 | 
						|
		_mouseActionItem = nullptr;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::mouseActionUpdate(const QPoint &screenPos) {
 | 
						|
	_mousePosition = screenPos;
 | 
						|
	updateSelected();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::mouseActionCancel() {
 | 
						|
	_mouseActionItem = nullptr;
 | 
						|
	_mouseAction = MouseAction::None;
 | 
						|
	_dragStartPosition = QPoint(0, 0);
 | 
						|
	_wasSelectedText = false;
 | 
						|
	//_widget->noSelectingScroll(); // TODO
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button) {
 | 
						|
	mouseActionUpdate(screenPos);
 | 
						|
 | 
						|
	ClickHandlerPtr activated = ClickHandler::unpressed();
 | 
						|
	if (_mouseAction == MouseAction::Dragging) {
 | 
						|
		activated.clear();
 | 
						|
	}
 | 
						|
	if (App::pressedItem()) {
 | 
						|
		repaintItem(App::pressedItem());
 | 
						|
		App::pressedItem(nullptr);
 | 
						|
	}
 | 
						|
 | 
						|
	_wasSelectedText = false;
 | 
						|
 | 
						|
	if (activated) {
 | 
						|
		mouseActionCancel();
 | 
						|
		App::activateClickHandler(activated, button);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	if (_mouseAction == MouseAction::PrepareDrag && !_pressWasInactive && button != Qt::RightButton) {
 | 
						|
		repaintItem(base::take(_selectedItem));
 | 
						|
	} else if (_mouseAction == MouseAction::Selecting) {
 | 
						|
		if (_selectedItem && !_pressWasInactive) {
 | 
						|
			if (_selectedText.from == _selectedText.to) {
 | 
						|
				_selectedItem = nullptr;
 | 
						|
				App::wnd()->setInnerFocus();
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	_mouseAction = MouseAction::None;
 | 
						|
	_mouseActionItem = nullptr;
 | 
						|
	_mouseSelectType = TextSelectType::Letters;
 | 
						|
	//_widget->noSelectingScroll(); // TODO
 | 
						|
 | 
						|
#if defined Q_OS_LINUX32 || defined Q_OS_LINUX64
 | 
						|
	if (_selectedItem && _selectedText.from != _selectedText.to) {
 | 
						|
		setToClipboard(_selectedItem->selectedText(_selectedText), QClipboard::Selection);
 | 
						|
	}
 | 
						|
#endif // Q_OS_LINUX32 || Q_OS_LINUX64
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::updateSelected() {
 | 
						|
	auto mousePosition = mapFromGlobal(_mousePosition);
 | 
						|
	auto point = QPoint(snap(mousePosition.x(), 0, width()), snap(mousePosition.y(), _visibleTop, _visibleBottom));
 | 
						|
 | 
						|
	auto itemPoint = QPoint();
 | 
						|
	auto begin = std::rbegin(_items), end = std::rend(_items);
 | 
						|
	auto from = (point.y() >= _itemsTop && point.y() < _itemsTop + _itemsHeight)
 | 
						|
		? std::lower_bound(begin, end, point.y(), [this](auto &elem, int top) {
 | 
						|
			return this->itemTop(elem) + elem->height() <= top;
 | 
						|
		})
 | 
						|
		: end;
 | 
						|
	auto item = (from != end) ? from->get() : nullptr;
 | 
						|
	if (item) {
 | 
						|
		App::mousedItem(item);
 | 
						|
		itemPoint = mapPointToItem(point, item);
 | 
						|
		if (item->hasPoint(itemPoint)) {
 | 
						|
			if (App::hoveredItem() != item) {
 | 
						|
				repaintItem(App::hoveredItem());
 | 
						|
				App::hoveredItem(item);
 | 
						|
				repaintItem(App::hoveredItem());
 | 
						|
			}
 | 
						|
		} else if (App::hoveredItem()) {
 | 
						|
			repaintItem(App::hoveredItem());
 | 
						|
			App::hoveredItem(nullptr);
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	HistoryTextState dragState;
 | 
						|
	ClickHandlerHost *lnkhost = nullptr;
 | 
						|
	auto selectingText = (item == _mouseActionItem && item == App::hoveredItem() && _selectedItem);
 | 
						|
	if (item) {
 | 
						|
		if (item != _mouseActionItem || (itemPoint - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
 | 
						|
			if (_mouseAction == MouseAction::PrepareDrag) {
 | 
						|
				_mouseAction = MouseAction::Dragging;
 | 
						|
				InvokeQueued(this, [this] { performDrag(); });
 | 
						|
			}
 | 
						|
		}
 | 
						|
		HistoryStateRequest request;
 | 
						|
		if (_mouseAction == MouseAction::Selecting) {
 | 
						|
			request.flags |= Text::StateRequest::Flag::LookupSymbol;
 | 
						|
		} else {
 | 
						|
			selectingText = false;
 | 
						|
		}
 | 
						|
		dragState = item->getState(itemPoint, request);
 | 
						|
		lnkhost = item;
 | 
						|
		if (!dragState.link && itemPoint.x() >= st::historyPhotoLeft && itemPoint.x() < st::historyPhotoLeft + st::msgPhotoSize) {
 | 
						|
			if (auto message = item->toHistoryMessage()) {
 | 
						|
				if (message->hasFromPhoto()) {
 | 
						|
					enumerateUserpics([&dragState, &lnkhost, &point](not_null<HistoryMessage*> message, int userpicTop) -> bool {
 | 
						|
						// stop enumeration if the userpic is below our point
 | 
						|
						if (userpicTop > point.y()) {
 | 
						|
							return false;
 | 
						|
						}
 | 
						|
 | 
						|
						// stop enumeration if we've found a userpic under the cursor
 | 
						|
						if (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) {
 | 
						|
							dragState.link = message->from()->openLink();
 | 
						|
							lnkhost = message;
 | 
						|
							return false;
 | 
						|
						}
 | 
						|
						return true;
 | 
						|
					});
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost);
 | 
						|
	if (lnkChanged || dragState.cursor != _mouseCursorState) {
 | 
						|
		Ui::Tooltip::Hide();
 | 
						|
	}
 | 
						|
	if (dragState.link || dragState.cursor == HistoryInDateCursorState || dragState.cursor == HistoryInForwardedCursorState) {
 | 
						|
		Ui::Tooltip::Show(1000, this);
 | 
						|
	}
 | 
						|
 | 
						|
	auto cursor = style::cur_default;
 | 
						|
	if (_mouseAction == MouseAction::None) {
 | 
						|
		_mouseCursorState = dragState.cursor;
 | 
						|
		if (dragState.link) {
 | 
						|
			cursor = style::cur_pointer;
 | 
						|
		} else if (_mouseCursorState == HistoryInTextCursorState) {
 | 
						|
			cursor = style::cur_text;
 | 
						|
		} else if (_mouseCursorState == HistoryInDateCursorState) {
 | 
						|
//			cursor = style::cur_cross;
 | 
						|
		}
 | 
						|
	} else if (item) {
 | 
						|
		if (_mouseAction == MouseAction::Selecting) {
 | 
						|
			if (selectingText) {
 | 
						|
				auto second = dragState.symbol;
 | 
						|
				if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {
 | 
						|
					++second;
 | 
						|
				}
 | 
						|
				auto selection = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) };
 | 
						|
				if (_mouseSelectType != TextSelectType::Letters) {
 | 
						|
					selection = _mouseActionItem->adjustSelection(selection, _mouseSelectType);
 | 
						|
				}
 | 
						|
				if (_selectedText != selection) {
 | 
						|
					_selectedText = selection;
 | 
						|
					repaintItem(_mouseActionItem);
 | 
						|
				}
 | 
						|
				if (!_wasSelectedText && (selection.from != selection.to)) {
 | 
						|
					_wasSelectedText = true;
 | 
						|
					setFocus();
 | 
						|
				}
 | 
						|
			}
 | 
						|
		} else if (_mouseAction == MouseAction::Dragging) {
 | 
						|
		}
 | 
						|
 | 
						|
		if (ClickHandler::getPressed()) {
 | 
						|
			cursor = style::cur_pointer;
 | 
						|
		} else if (_mouseAction == MouseAction::Selecting && _selectedItem) {
 | 
						|
			cursor = style::cur_text;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Voice message seek support.
 | 
						|
	if (auto pressedItem = App::pressedLinkItem()) {
 | 
						|
		if (!pressedItem->detached()) {
 | 
						|
			if (pressedItem->history() == _history) {
 | 
						|
				auto adjustedPoint = mapPointToItem(point, pressedItem);
 | 
						|
				pressedItem->updatePressed(adjustedPoint);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	//if (_mouseAction == MouseAction::Selecting) {
 | 
						|
	//	_widget->checkSelectingScroll(mousePos);
 | 
						|
	//} else {
 | 
						|
	//	_widget->noSelectingScroll();
 | 
						|
	//} // TODO
 | 
						|
 | 
						|
	if (_mouseAction == MouseAction::None && (lnkChanged || cursor != _cursor)) {
 | 
						|
		setCursor(_cursor = cursor);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::performDrag() {
 | 
						|
	if (_mouseAction != MouseAction::Dragging) return;
 | 
						|
 | 
						|
	auto uponSelected = false;
 | 
						|
	//if (_mouseActionItem) {
 | 
						|
	//	if (!_selected.isEmpty() && _selected.cbegin().value() == FullSelection) {
 | 
						|
	//		uponSelected = _selected.contains(_mouseActionItem);
 | 
						|
	//	} else {
 | 
						|
	//		HistoryStateRequest request;
 | 
						|
	//		request.flags |= Text::StateRequest::Flag::LookupSymbol;
 | 
						|
	//		auto dragState = _mouseActionItem->getState(_dragStartPosition.x(), _dragStartPosition.y(), request);
 | 
						|
	//		uponSelected = (dragState.cursor == HistoryInTextCursorState);
 | 
						|
	//		if (uponSelected) {
 | 
						|
	//			if (_selected.isEmpty() ||
 | 
						|
	//				_selected.cbegin().value() == FullSelection ||
 | 
						|
	//				_selected.cbegin().key() != _mouseActionItem
 | 
						|
	//				) {
 | 
						|
	//				uponSelected = false;
 | 
						|
	//			} else {
 | 
						|
	//				uint16 selFrom = _selected.cbegin().value().from, selTo = _selected.cbegin().value().to;
 | 
						|
	//				if (dragState.symbol < selFrom || dragState.symbol >= selTo) {
 | 
						|
	//					uponSelected = false;
 | 
						|
	//				}
 | 
						|
	//			}
 | 
						|
	//		}
 | 
						|
	//	}
 | 
						|
	//}
 | 
						|
	//auto pressedHandler = ClickHandler::getPressed();
 | 
						|
 | 
						|
	//if (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.data())) {
 | 
						|
	//	return;
 | 
						|
	//}
 | 
						|
 | 
						|
	//TextWithEntities sel;
 | 
						|
	//QList<QUrl> urls;
 | 
						|
	//if (uponSelected) {
 | 
						|
	//	sel = getSelectedText();
 | 
						|
	//} else if (pressedHandler) {
 | 
						|
	//	sel = { pressedHandler->dragText(), EntitiesInText() };
 | 
						|
	//	//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
 | 
						|
	//	//	urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
 | 
						|
	//	//}
 | 
						|
	//}
 | 
						|
	//if (auto mimeData = mimeDataFromTextWithEntities(sel)) {
 | 
						|
	//	updateDragSelection(0, 0, false);
 | 
						|
	//	_widget->noSelectingScroll();
 | 
						|
 | 
						|
	//	if (!urls.isEmpty()) mimeData->setUrls(urls);
 | 
						|
	//	if (uponSelected && !Adaptive::OneColumn()) {
 | 
						|
	//		auto selectedState = getSelectionState();
 | 
						|
	//		if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
 | 
						|
	//			mimeData->setData(qsl("application/x-td-forward-selected"), "1");
 | 
						|
	//		}
 | 
						|
	//	}
 | 
						|
	//	_controller->window()->launchDrag(std::move(mimeData));
 | 
						|
	//	return;
 | 
						|
	//} else {
 | 
						|
	//	auto forwardMimeType = QString();
 | 
						|
	//	auto pressedMedia = static_cast<HistoryMedia*>(nullptr);
 | 
						|
	//	if (auto pressedItem = App::pressedItem()) {
 | 
						|
	//		pressedMedia = pressedItem->getMedia();
 | 
						|
	//		if (_mouseCursorState == HistoryInDateCursorState || (pressedMedia && pressedMedia->dragItem())) {
 | 
						|
	//			forwardMimeType = qsl("application/x-td-forward-pressed");
 | 
						|
	//		}
 | 
						|
	//	}
 | 
						|
	//	if (auto pressedLnkItem = App::pressedLinkItem()) {
 | 
						|
	//		if ((pressedMedia = pressedLnkItem->getMedia())) {
 | 
						|
	//			if (forwardMimeType.isEmpty() && pressedMedia->dragItemByHandler(pressedHandler)) {
 | 
						|
	//				forwardMimeType = qsl("application/x-td-forward-pressed-link");
 | 
						|
	//			}
 | 
						|
	//		}
 | 
						|
	//	}
 | 
						|
	//	if (!forwardMimeType.isEmpty()) {
 | 
						|
	//		auto mimeData = std::make_unique<QMimeData>();
 | 
						|
	//		mimeData->setData(forwardMimeType, "1");
 | 
						|
	//		if (auto document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) {
 | 
						|
	//			auto filepath = document->filepath(DocumentData::FilePathResolveChecked);
 | 
						|
	//			if (!filepath.isEmpty()) {
 | 
						|
	//				QList<QUrl> urls;
 | 
						|
	//				urls.push_back(QUrl::fromLocalFile(filepath));
 | 
						|
	//				mimeData->setUrls(urls);
 | 
						|
	//			}
 | 
						|
	//		}
 | 
						|
 | 
						|
	//		// This call enters event loop and can destroy any QObject.
 | 
						|
	//		_controller->window()->launchDrag(std::move(mimeData));
 | 
						|
	//		return;
 | 
						|
	//	}
 | 
						|
	//} // TODO
 | 
						|
}
 | 
						|
 | 
						|
int InnerWidget::itemTop(not_null<const HistoryItem*> item) const {
 | 
						|
	return _itemsTop + item->y();
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::repaintItem(const HistoryItem *item) {
 | 
						|
	if (!item) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	update(0, itemTop(item), width(), item->height());
 | 
						|
}
 | 
						|
 | 
						|
QPoint InnerWidget::mapPointToItem(QPoint point, const HistoryItem *item) const {
 | 
						|
	if (!item) {
 | 
						|
		return QPoint();
 | 
						|
	}
 | 
						|
	return point - QPoint(0, itemTop(item));
 | 
						|
}
 | 
						|
 | 
						|
void InnerWidget::handlePendingHistoryResize() {
 | 
						|
	if (_history->hasPendingResizedItems()) {
 | 
						|
		_history->resizeGetHeight(width());
 | 
						|
		updateSize();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
InnerWidget::~InnerWidget() = default;
 | 
						|
 | 
						|
} // namespace AdminLog
 |