2440 lines
		
	
	
	
		
			85 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			2440 lines
		
	
	
	
		
			85 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_inner_widget.h"
 | 
						|
 | 
						|
#include "styles/style_history.h"
 | 
						|
#include "core/file_utilities.h"
 | 
						|
#include "history/history_message.h"
 | 
						|
#include "history/history_service_layout.h"
 | 
						|
#include "history/history_media_types.h"
 | 
						|
#include "ui/widgets/popup_menu.h"
 | 
						|
#include "window/window_controller.h"
 | 
						|
#include "chat_helpers/message_field.h"
 | 
						|
#include "history/history_widget.h"
 | 
						|
#include "mainwindow.h"
 | 
						|
#include "mainwidget.h"
 | 
						|
#include "auth_session.h"
 | 
						|
#include "apiwrap.h"
 | 
						|
#include "lang/lang_keys.h"
 | 
						|
 | 
						|
namespace {
 | 
						|
 | 
						|
constexpr auto kScrollDateHideTimeout = 1000;
 | 
						|
 | 
						|
class DateClickHandler : public ClickHandler {
 | 
						|
public:
 | 
						|
	DateClickHandler(PeerData *peer, QDate date) : _peer(peer), _date(date) {
 | 
						|
	}
 | 
						|
 | 
						|
	void setDate(QDate date) {
 | 
						|
		_date = date;
 | 
						|
	}
 | 
						|
 | 
						|
	void onClick(Qt::MouseButton) const override {
 | 
						|
		App::main()->showJumpToDate(_peer, _date);
 | 
						|
	}
 | 
						|
 | 
						|
private:
 | 
						|
	PeerData *_peer = nullptr;
 | 
						|
	QDate _date;
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
// Helper binary search for an item in a list that is not completely
 | 
						|
// above the given top of the visible area or below the given bottom of the visible area
 | 
						|
// is applied once for blocks list in a history and once for items list in the found block.
 | 
						|
template <bool TopToBottom, typename T>
 | 
						|
int BinarySearchBlocksOrItems(const T &list, int edge) {
 | 
						|
	// static_cast to work around GCC bug #78693
 | 
						|
	auto start = 0, end = static_cast<int>(list.size());
 | 
						|
	while (end - start > 1) {
 | 
						|
		auto middle = (start + end) / 2;
 | 
						|
		auto top = list[middle]->y();
 | 
						|
		auto chooseLeft = (TopToBottom ? (top <= edge) : (top < edge));
 | 
						|
		if (chooseLeft) {
 | 
						|
			start = middle;
 | 
						|
		} else {
 | 
						|
			end = middle;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return start;
 | 
						|
}
 | 
						|
 | 
						|
} // namespace
 | 
						|
 | 
						|
// flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html
 | 
						|
 | 
						|
HistoryInner::HistoryInner(HistoryWidget *historyWidget, gsl::not_null<Window::Controller*> controller, Ui::ScrollArea *scroll, History *history) : TWidget(nullptr)
 | 
						|
, _controller(controller)
 | 
						|
, _peer(history->peer)
 | 
						|
, _migrated(history->peer->migrateFrom() ? App::history(history->peer->migrateFrom()->id) : nullptr)
 | 
						|
, _history(history)
 | 
						|
, _widget(historyWidget)
 | 
						|
, _scroll(scroll)
 | 
						|
, _scrollDateCheck([this] { onScrollDateCheck(); }) {
 | 
						|
	_touchSelectTimer.setSingleShot(true);
 | 
						|
	connect(&_touchSelectTimer, SIGNAL(timeout()), this, SLOT(onTouchSelect()));
 | 
						|
 | 
						|
	setAttribute(Qt::WA_AcceptTouchEvents);
 | 
						|
	connect(&_touchScrollTimer, SIGNAL(timeout()), this, SLOT(onTouchScrollTimer()));
 | 
						|
 | 
						|
	_trippleClickTimer.setSingleShot(true);
 | 
						|
 | 
						|
	connect(&_scrollDateHideTimer, SIGNAL(timeout()), this, SLOT(onScrollDateHideByTimer()));
 | 
						|
 | 
						|
	notifyIsBotChanged();
 | 
						|
 | 
						|
	setMouseTracking(true);
 | 
						|
	subscribe(Global::RefItemRemoved(), [this](HistoryItem *item) {
 | 
						|
		itemRemoved(item);
 | 
						|
	});
 | 
						|
	subscribe(_controller->gifPauseLevelChanged(), [this] {
 | 
						|
		if (!_controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any)) {
 | 
						|
			update();
 | 
						|
		}
 | 
						|
	});
 | 
						|
	subscribe(_controller->window()->dragFinished(), [this] {
 | 
						|
		mouseActionUpdate(QCursor::pos());
 | 
						|
	});
 | 
						|
	subscribe(AuthSession::Current().data().historyCleared(), [this](gsl::not_null<History*> history) {
 | 
						|
		if (_history == history) {
 | 
						|
			mouseActionCancel();
 | 
						|
		}
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::messagesReceived(PeerData *peer, const QVector<MTPMessage> &messages) {
 | 
						|
	if (_history && _history->peer == peer) {
 | 
						|
		_history->addOlderSlice(messages);
 | 
						|
	} else if (_migrated && _migrated->peer == peer) {
 | 
						|
		bool newLoaded = (_migrated && _migrated->isEmpty() && !_history->isEmpty());
 | 
						|
		_migrated->addOlderSlice(messages);
 | 
						|
		if (newLoaded) {
 | 
						|
			_migrated->addNewerSlice(QVector<MTPMessage>());
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::messagesReceivedDown(PeerData *peer, const QVector<MTPMessage> &messages) {
 | 
						|
	if (_history && _history->peer == peer) {
 | 
						|
		bool oldLoaded = (_migrated && _history->isEmpty() && !_migrated->isEmpty());
 | 
						|
		_history->addNewerSlice(messages);
 | 
						|
		if (oldLoaded) {
 | 
						|
			_history->addOlderSlice(QVector<MTPMessage>());
 | 
						|
		}
 | 
						|
	} else if (_migrated && _migrated->peer == peer) {
 | 
						|
		_migrated->addNewerSlice(messages);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::repaintItem(const HistoryItem *item) {
 | 
						|
	if (!item || item->detached() || !_history) return;
 | 
						|
	int32 msgy = itemTop(item);
 | 
						|
	if (msgy >= 0) {
 | 
						|
		update(0, msgy, width(), item->height());
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
template <bool TopToBottom, typename Method>
 | 
						|
void HistoryInner::enumerateItemsInHistory(History *history, int historytop, Method method) {
 | 
						|
	// No displayed messages in this history.
 | 
						|
	if (historytop < 0 || history->isEmpty()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	if (_visibleAreaBottom <= historytop || historytop + history->height <= _visibleAreaTop) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	auto searchEdge = TopToBottom ? _visibleAreaTop : _visibleAreaBottom;
 | 
						|
 | 
						|
	// Binary search for blockIndex of the first block that is not completely below the visible area.
 | 
						|
	auto blockIndex = BinarySearchBlocksOrItems<TopToBottom>(history->blocks, searchEdge - historytop);
 | 
						|
 | 
						|
	// Binary search for itemIndex of the first item that is not completely below the visible area.
 | 
						|
	auto block = history->blocks.at(blockIndex);
 | 
						|
	auto blocktop = historytop + block->y();
 | 
						|
	auto blockbottom = blocktop + block->height();
 | 
						|
	auto itemIndex = BinarySearchBlocksOrItems<TopToBottom>(block->items, searchEdge - blocktop);
 | 
						|
 | 
						|
	while (true) {
 | 
						|
		while (true) {
 | 
						|
			auto item = block->items.at(itemIndex);
 | 
						|
			auto itemtop = blocktop + item->y();
 | 
						|
			auto itembottom = itemtop + item->height();
 | 
						|
 | 
						|
			// Binary search should've skipped all the items that are above / below the visible area.
 | 
						|
			if (TopToBottom) {
 | 
						|
				t_assert(itembottom > _visibleAreaTop);
 | 
						|
			} else {
 | 
						|
				t_assert(itemtop < _visibleAreaBottom);
 | 
						|
			}
 | 
						|
 | 
						|
			if (!method(item, itemtop, itembottom)) {
 | 
						|
				return;
 | 
						|
			}
 | 
						|
 | 
						|
			// Skip all the items that are below / above the visible area.
 | 
						|
			if (TopToBottom) {
 | 
						|
				if (itembottom >= _visibleAreaBottom) {
 | 
						|
					return;
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				if (itemtop <= _visibleAreaTop) {
 | 
						|
					return;
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			if (TopToBottom) {
 | 
						|
				if (++itemIndex >= block->items.size()) {
 | 
						|
					break;
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				if (--itemIndex < 0) {
 | 
						|
					break;
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// Skip all the rest blocks that are below / above the visible area.
 | 
						|
		if (TopToBottom) {
 | 
						|
			if (blockbottom >= _visibleAreaBottom) {
 | 
						|
				return;
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			if (blocktop <= _visibleAreaTop) {
 | 
						|
				return;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if (TopToBottom) {
 | 
						|
			if (++blockIndex >= history->blocks.size()) {
 | 
						|
				return;
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			if (--blockIndex < 0) {
 | 
						|
				return;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		block = history->blocks[blockIndex];
 | 
						|
		blocktop = historytop + block->y();
 | 
						|
		blockbottom = blocktop + block->height();
 | 
						|
		if (TopToBottom) {
 | 
						|
			itemIndex = 0;
 | 
						|
		} else {
 | 
						|
			itemIndex = block->items.size() - 1;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
template <typename Method>
 | 
						|
void HistoryInner::enumerateUserpics(Method method) {
 | 
						|
	if ((!_history || !_history->canHaveFromPhotos()) && (!_migrated || !_migrated->canHaveFromPhotos())) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	// 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](gsl::not_null<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 >= _visibleAreaBottom)) {
 | 
						|
			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(), _visibleAreaBottom - 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 HistoryInner::enumerateDates(Method method) {
 | 
						|
	auto drawtop = historyDrawTop();
 | 
						|
 | 
						|
	// 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, drawtop](gsl::not_null<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 <= _visibleAreaTop)) {
 | 
						|
			// skip the date of history migrate item if it will be in migrated
 | 
						|
			if (itemtop < drawtop && item->history() == _history) {
 | 
						|
				if (itemtop > _visibleAreaTop) {
 | 
						|
					// Previous item (from the _migrated history) is drawing date now.
 | 
						|
					return false;
 | 
						|
				} else if (item == _history->blocks.front()->items.front() && item->isGroupMigrate()
 | 
						|
					&& _migrated->blocks.back()->items.back()->isGroupMigrate()) {
 | 
						|
					// This item is completely invisible and should be completely ignored.
 | 
						|
					return false;
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			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.
 | 
						|
			int dateTop = qMax(itemtop, _visibleAreaTop) + st::msgServiceMargin.top();
 | 
						|
 | 
						|
			// Do not let the date go below the single-day messages pack bottom line.
 | 
						|
			int 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);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::paintEvent(QPaintEvent *e) {
 | 
						|
	if (Ui::skipPaintEvent(this, e)) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	if (hasPendingResizedItems()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	Painter p(this);
 | 
						|
	auto clip = e->rect();
 | 
						|
	auto ms = getms();
 | 
						|
 | 
						|
	bool historyDisplayedEmpty = (_history->isDisplayedEmpty() && (!_migrated || _migrated->isDisplayedEmpty()));
 | 
						|
	bool noHistoryDisplayed = _firstLoading || historyDisplayedEmpty;
 | 
						|
	if (!_firstLoading && _botAbout && !_botAbout->info->text.isEmpty() && _botAbout->height > 0) {
 | 
						|
		if (clip.y() < _botAbout->rect.y() + _botAbout->rect.height() && clip.y() + clip.height() > _botAbout->rect.y()) {
 | 
						|
			p.setTextPalette(st::inTextPalette);
 | 
						|
			App::roundRect(p, _botAbout->rect, st::msgInBg, MessageInCorners, &st::msgInShadow);
 | 
						|
 | 
						|
			p.setFont(st::msgNameFont);
 | 
						|
			p.setPen(st::dialogsNameFg);
 | 
						|
			p.drawText(_botAbout->rect.left() + st::msgPadding.left(), _botAbout->rect.top() + st::msgPadding.top() + st::msgNameFont->ascent, lang(lng_bot_description));
 | 
						|
 | 
						|
			p.setPen(st::historyTextInFg);
 | 
						|
			_botAbout->info->text.draw(p, _botAbout->rect.left() + st::msgPadding.left(), _botAbout->rect.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip, _botAbout->width);
 | 
						|
 | 
						|
			p.restoreTextPalette();
 | 
						|
		}
 | 
						|
	} else if (noHistoryDisplayed) {
 | 
						|
		HistoryLayout::paintEmpty(p, width(), height());
 | 
						|
	}
 | 
						|
	if (!noHistoryDisplayed) {
 | 
						|
		adjustCurrent(clip.top());
 | 
						|
 | 
						|
		auto selEnd = _selected.cend();
 | 
						|
		auto hasSel = !_selected.isEmpty();
 | 
						|
 | 
						|
		auto drawToY = clip.y() + clip.height();
 | 
						|
 | 
						|
		auto selfromy = itemTop(_dragSelFrom);
 | 
						|
		auto seltoy = itemTop(_dragSelTo);
 | 
						|
		if (selfromy < 0 || seltoy < 0) {
 | 
						|
			selfromy = seltoy = -1;
 | 
						|
		} else {
 | 
						|
			seltoy += _dragSelTo->height();
 | 
						|
		}
 | 
						|
 | 
						|
		auto mtop = migratedTop();
 | 
						|
		auto htop = historyTop();
 | 
						|
		auto hdrawtop = historyDrawTop();
 | 
						|
		if (mtop >= 0) {
 | 
						|
			auto iBlock = (_curHistory == _migrated ? _curBlock : (_migrated->blocks.size() - 1));
 | 
						|
			auto block = _migrated->blocks[iBlock];
 | 
						|
			auto iItem = (_curHistory == _migrated ? _curItem : (block->items.size() - 1));
 | 
						|
			auto item = block->items[iItem];
 | 
						|
 | 
						|
			auto y = mtop + block->y() + item->y();
 | 
						|
			p.save();
 | 
						|
			p.translate(0, y);
 | 
						|
			if (clip.y() < y + item->height()) while (y < drawToY) {
 | 
						|
				TextSelection sel;
 | 
						|
				if (y >= selfromy && y < seltoy) {
 | 
						|
					if (_dragSelecting && !item->serviceMsg() && item->id > 0) {
 | 
						|
						sel = FullSelection;
 | 
						|
					}
 | 
						|
				} else if (hasSel) {
 | 
						|
					auto i = _selected.constFind(item);
 | 
						|
					if (i != selEnd) {
 | 
						|
						sel = i.value();
 | 
						|
					}
 | 
						|
				}
 | 
						|
				item->draw(p, clip.translated(0, -y), sel, ms);
 | 
						|
 | 
						|
				if (item->hasViews()) {
 | 
						|
					App::main()->scheduleViewIncrement(item);
 | 
						|
				}
 | 
						|
 | 
						|
				int32 h = item->height();
 | 
						|
				p.translate(0, h);
 | 
						|
				y += h;
 | 
						|
 | 
						|
				++iItem;
 | 
						|
				if (iItem == block->items.size()) {
 | 
						|
					iItem = 0;
 | 
						|
					++iBlock;
 | 
						|
					if (iBlock == _migrated->blocks.size()) {
 | 
						|
						break;
 | 
						|
					}
 | 
						|
					block = _migrated->blocks[iBlock];
 | 
						|
				}
 | 
						|
				item = block->items[iItem];
 | 
						|
			}
 | 
						|
			p.restore();
 | 
						|
		}
 | 
						|
		if (htop >= 0) {
 | 
						|
			auto iBlock = (_curHistory == _history ? _curBlock : 0);
 | 
						|
			auto block = _history->blocks[iBlock];
 | 
						|
			auto iItem = (_curHistory == _history ? _curItem : 0);
 | 
						|
			auto item = block->items[iItem];
 | 
						|
 | 
						|
			auto historyRect = clip.intersected(QRect(0, hdrawtop, width(), clip.top() + clip.height()));
 | 
						|
			auto y = htop + block->y() + item->y();
 | 
						|
			p.save();
 | 
						|
			p.translate(0, y);
 | 
						|
			while (y < drawToY) {
 | 
						|
				auto h = item->height();
 | 
						|
				if (historyRect.y() < y + h && hdrawtop < y + h) {
 | 
						|
					TextSelection sel;
 | 
						|
					if (y >= selfromy && y < seltoy) {
 | 
						|
						if (_dragSelecting && !item->serviceMsg() && item->id > 0) {
 | 
						|
							sel = FullSelection;
 | 
						|
						}
 | 
						|
					} else if (hasSel) {
 | 
						|
						auto i = _selected.constFind(item);
 | 
						|
						if (i != selEnd) {
 | 
						|
							sel = i.value();
 | 
						|
						}
 | 
						|
					}
 | 
						|
					item->draw(p, historyRect.translated(0, -y), sel, ms);
 | 
						|
 | 
						|
					if (item->hasViews()) {
 | 
						|
						App::main()->scheduleViewIncrement(item);
 | 
						|
					}
 | 
						|
				}
 | 
						|
				p.translate(0, h);
 | 
						|
				y += h;
 | 
						|
 | 
						|
				++iItem;
 | 
						|
				if (iItem == block->items.size()) {
 | 
						|
					iItem = 0;
 | 
						|
					++iBlock;
 | 
						|
					if (iBlock == _history->blocks.size()) {
 | 
						|
						break;
 | 
						|
					}
 | 
						|
					block = _history->blocks[iBlock];
 | 
						|
				}
 | 
						|
				item = block->items[iItem];
 | 
						|
			}
 | 
						|
			p.restore();
 | 
						|
		}
 | 
						|
 | 
						|
		if (mtop >= 0 || htop >= 0) {
 | 
						|
			enumerateUserpics([&p, &clip](gsl::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->history()->width, st::msgPhotoSize);
 | 
						|
				}
 | 
						|
				return true;
 | 
						|
			});
 | 
						|
 | 
						|
			int dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
 | 
						|
			//QDate lastDate;
 | 
						|
			//if (!_history->isEmpty()) {
 | 
						|
			//	lastDate = _history->blocks.back()->items.back()->date.date();
 | 
						|
			//}
 | 
						|
 | 
						|
			//// if item top is before this value always show date as a floating date
 | 
						|
			//int showFloatingBefore = height() - 2 * (_visibleAreaBottom - _visibleAreaTop) - dateHeight;
 | 
						|
 | 
						|
			auto scrollDateOpacity = _scrollDateOpacity.current(ms, _scrollDateShown ? 1. : 0.);
 | 
						|
			enumerateDates([&p, &clip, scrollDateOpacity, dateHeight/*, lastDate, showFloatingBefore*/](gsl::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->history()->width;
 | 
						|
						if (auto date = item->Get<HistoryMessageDate>()) {
 | 
						|
							date->paint(p, dateY, width);
 | 
						|
						} else {
 | 
						|
							HistoryLayout::ServiceMessagePainter::paintDate(p, item->date, dateY, width);
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
				return true;
 | 
						|
			});
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
bool HistoryInner::event(QEvent *e) {
 | 
						|
	if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) {
 | 
						|
		QTouchEvent *ev = static_cast<QTouchEvent*>(e);
 | 
						|
		if (ev->device()->type() == QTouchDevice::TouchScreen) {
 | 
						|
			touchEvent(ev);
 | 
						|
			return true;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return QWidget::event(e);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::onTouchScrollTimer() {
 | 
						|
	auto nowTime = getms();
 | 
						|
	if (_touchScrollState == Ui::TouchScrollState::Acceleration && _touchWaitingAcceleration && (nowTime - _touchAccelerationTime) > 40) {
 | 
						|
		_touchScrollState = Ui::TouchScrollState::Manual;
 | 
						|
		touchResetSpeed();
 | 
						|
	} else if (_touchScrollState == Ui::TouchScrollState::Auto || _touchScrollState == Ui::TouchScrollState::Acceleration) {
 | 
						|
		int32 elapsed = int32(nowTime - _touchTime);
 | 
						|
		QPoint delta = _touchSpeed * elapsed / 1000;
 | 
						|
		bool hasScrolled = _widget->touchScroll(delta);
 | 
						|
 | 
						|
		if (_touchSpeed.isNull() || !hasScrolled) {
 | 
						|
			_touchScrollState = Ui::TouchScrollState::Manual;
 | 
						|
			_touchScroll = false;
 | 
						|
			_touchScrollTimer.stop();
 | 
						|
		} else {
 | 
						|
			_touchTime = nowTime;
 | 
						|
		}
 | 
						|
		touchDeaccelerate(elapsed);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::touchUpdateSpeed() {
 | 
						|
	const auto nowTime = getms();
 | 
						|
	if (_touchPrevPosValid) {
 | 
						|
		const int elapsed = nowTime - _touchSpeedTime;
 | 
						|
		if (elapsed) {
 | 
						|
			const QPoint newPixelDiff = (_touchPos - _touchPrevPos);
 | 
						|
			const QPoint pixelsPerSecond = newPixelDiff * (1000 / elapsed);
 | 
						|
 | 
						|
			// fingers are inacurates, we ignore small changes to avoid stopping the autoscroll because
 | 
						|
			// of a small horizontal offset when scrolling vertically
 | 
						|
			const int newSpeedY = (qAbs(pixelsPerSecond.y()) > FingerAccuracyThreshold) ? pixelsPerSecond.y() : 0;
 | 
						|
			const int newSpeedX = (qAbs(pixelsPerSecond.x()) > FingerAccuracyThreshold) ? pixelsPerSecond.x() : 0;
 | 
						|
			if (_touchScrollState == Ui::TouchScrollState::Auto) {
 | 
						|
				const int oldSpeedY = _touchSpeed.y();
 | 
						|
				const int oldSpeedX = _touchSpeed.x();
 | 
						|
				if ((oldSpeedY <= 0 && newSpeedY <= 0) || ((oldSpeedY >= 0 && newSpeedY >= 0)
 | 
						|
					&& (oldSpeedX <= 0 && newSpeedX <= 0)) || (oldSpeedX >= 0 && newSpeedX >= 0)) {
 | 
						|
					_touchSpeed.setY(snap((oldSpeedY + (newSpeedY / 4)), -MaxScrollAccelerated, +MaxScrollAccelerated));
 | 
						|
					_touchSpeed.setX(snap((oldSpeedX + (newSpeedX / 4)), -MaxScrollAccelerated, +MaxScrollAccelerated));
 | 
						|
				} else {
 | 
						|
					_touchSpeed = QPoint();
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				// we average the speed to avoid strange effects with the last delta
 | 
						|
				if (!_touchSpeed.isNull()) {
 | 
						|
					_touchSpeed.setX(snap((_touchSpeed.x() / 4) + (newSpeedX * 3 / 4), -MaxScrollFlick, +MaxScrollFlick));
 | 
						|
					_touchSpeed.setY(snap((_touchSpeed.y() / 4) + (newSpeedY * 3 / 4), -MaxScrollFlick, +MaxScrollFlick));
 | 
						|
				} else {
 | 
						|
					_touchSpeed = QPoint(newSpeedX, newSpeedY);
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		_touchPrevPosValid = true;
 | 
						|
	}
 | 
						|
	_touchSpeedTime = nowTime;
 | 
						|
	_touchPrevPos = _touchPos;
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::touchResetSpeed() {
 | 
						|
	_touchSpeed = QPoint();
 | 
						|
	_touchPrevPosValid = false;
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::touchDeaccelerate(int32 elapsed) {
 | 
						|
	int32 x = _touchSpeed.x();
 | 
						|
	int32 y = _touchSpeed.y();
 | 
						|
	_touchSpeed.setX((x == 0) ? x : (x > 0) ? qMax(0, x - elapsed) : qMin(0, x + elapsed));
 | 
						|
	_touchSpeed.setY((y == 0) ? y : (y > 0) ? qMax(0, y - elapsed) : qMin(0, y + elapsed));
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::touchEvent(QTouchEvent *e) {
 | 
						|
	const Qt::TouchPointStates &states(e->touchPointStates());
 | 
						|
	if (e->type() == QEvent::TouchCancel) { // cancel
 | 
						|
		if (!_touchInProgress) return;
 | 
						|
		_touchInProgress = false;
 | 
						|
		_touchSelectTimer.stop();
 | 
						|
		_touchScroll = _touchSelect = false;
 | 
						|
		_touchScrollState = Ui::TouchScrollState::Manual;
 | 
						|
		mouseActionCancel();
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	if (!e->touchPoints().isEmpty()) {
 | 
						|
		_touchPrevPos = _touchPos;
 | 
						|
		_touchPos = e->touchPoints().cbegin()->screenPos().toPoint();
 | 
						|
	}
 | 
						|
 | 
						|
	switch (e->type()) {
 | 
						|
	case QEvent::TouchBegin:
 | 
						|
	if (_menu) {
 | 
						|
		e->accept();
 | 
						|
		return; // ignore mouse press, that was hiding context menu
 | 
						|
	}
 | 
						|
	if (_touchInProgress) return;
 | 
						|
	if (e->touchPoints().isEmpty()) return;
 | 
						|
 | 
						|
	_touchInProgress = true;
 | 
						|
	if (_touchScrollState == Ui::TouchScrollState::Auto) {
 | 
						|
		_touchScrollState = Ui::TouchScrollState::Acceleration;
 | 
						|
		_touchWaitingAcceleration = true;
 | 
						|
		_touchAccelerationTime = getms();
 | 
						|
		touchUpdateSpeed();
 | 
						|
		_touchStart = _touchPos;
 | 
						|
	} else {
 | 
						|
		_touchScroll = false;
 | 
						|
		_touchSelectTimer.start(QApplication::startDragTime());
 | 
						|
	}
 | 
						|
	_touchSelect = false;
 | 
						|
	_touchStart = _touchPrevPos = _touchPos;
 | 
						|
	break;
 | 
						|
 | 
						|
	case QEvent::TouchUpdate:
 | 
						|
	if (!_touchInProgress) return;
 | 
						|
	if (_touchSelect) {
 | 
						|
		mouseActionUpdate(_touchPos);
 | 
						|
	} else if (!_touchScroll && (_touchPos - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
 | 
						|
		_touchSelectTimer.stop();
 | 
						|
		_touchScroll = true;
 | 
						|
		touchUpdateSpeed();
 | 
						|
	}
 | 
						|
	if (_touchScroll) {
 | 
						|
		if (_touchScrollState == Ui::TouchScrollState::Manual) {
 | 
						|
			touchScrollUpdated(_touchPos);
 | 
						|
		} else if (_touchScrollState == Ui::TouchScrollState::Acceleration) {
 | 
						|
			touchUpdateSpeed();
 | 
						|
			_touchAccelerationTime = getms();
 | 
						|
			if (_touchSpeed.isNull()) {
 | 
						|
				_touchScrollState = Ui::TouchScrollState::Manual;
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	break;
 | 
						|
 | 
						|
	case QEvent::TouchEnd:
 | 
						|
	if (!_touchInProgress) return;
 | 
						|
	_touchInProgress = false;
 | 
						|
	if (_touchSelect) {
 | 
						|
		mouseActionFinish(_touchPos, Qt::RightButton);
 | 
						|
		QContextMenuEvent contextMenu(QContextMenuEvent::Mouse, mapFromGlobal(_touchPos), _touchPos);
 | 
						|
		showContextMenu(&contextMenu, true);
 | 
						|
		_touchScroll = false;
 | 
						|
	} else if (_touchScroll) {
 | 
						|
		if (_touchScrollState == Ui::TouchScrollState::Manual) {
 | 
						|
			_touchScrollState = Ui::TouchScrollState::Auto;
 | 
						|
			_touchPrevPosValid = false;
 | 
						|
			_touchScrollTimer.start(15);
 | 
						|
			_touchTime = getms();
 | 
						|
		} else if (_touchScrollState == Ui::TouchScrollState::Auto) {
 | 
						|
			_touchScrollState = Ui::TouchScrollState::Manual;
 | 
						|
			_touchScroll = false;
 | 
						|
			touchResetSpeed();
 | 
						|
		} else if (_touchScrollState == Ui::TouchScrollState::Acceleration) {
 | 
						|
			_touchScrollState = Ui::TouchScrollState::Auto;
 | 
						|
			_touchWaitingAcceleration = false;
 | 
						|
			_touchPrevPosValid = false;
 | 
						|
		}
 | 
						|
	} else { // One short tap is like left mouse click.
 | 
						|
		mouseActionStart(_touchPos, Qt::LeftButton);
 | 
						|
		mouseActionFinish(_touchPos, Qt::LeftButton);
 | 
						|
	}
 | 
						|
	_touchSelectTimer.stop();
 | 
						|
	_touchSelect = false;
 | 
						|
	break;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::mouseMoveEvent(QMouseEvent *e) {
 | 
						|
	static auto lastGlobalPosition = e->globalPos();
 | 
						|
	auto reallyMoved = (lastGlobalPosition != e->globalPos());
 | 
						|
	auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
 | 
						|
	if (!buttonsPressed && _mouseAction != MouseAction::None) {
 | 
						|
		mouseReleaseEvent(e);
 | 
						|
	}
 | 
						|
	if (reallyMoved) {
 | 
						|
		lastGlobalPosition = e->globalPos();
 | 
						|
		if (!buttonsPressed || (_scrollDateLink && ClickHandler::getPressed() == _scrollDateLink)) {
 | 
						|
			keepScrollDateForNow();
 | 
						|
		}
 | 
						|
	}
 | 
						|
	mouseActionUpdate(e->globalPos());
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::mouseActionUpdate(const QPoint &screenPos) {
 | 
						|
	_mousePosition = screenPos;
 | 
						|
	onUpdateSelected();
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::touchScrollUpdated(const QPoint &screenPos) {
 | 
						|
	_touchPos = screenPos;
 | 
						|
	_widget->touchScroll(_touchPos - _touchPrevPos);
 | 
						|
	touchUpdateSpeed();
 | 
						|
}
 | 
						|
 | 
						|
QPoint HistoryInner::mapPointToItem(QPoint p, HistoryItem *item) {
 | 
						|
	int32 msgy = itemTop(item);
 | 
						|
	if (msgy < 0) return QPoint(0, 0);
 | 
						|
 | 
						|
	p.setY(p.y() - msgy);
 | 
						|
	return p;
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::mousePressEvent(QMouseEvent *e) {
 | 
						|
	if (_menu) {
 | 
						|
		e->accept();
 | 
						|
		return; // ignore mouse press, that was hiding context menu
 | 
						|
	}
 | 
						|
	mouseActionStart(e->globalPos(), e->button());
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::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;
 | 
						|
	} else if (!_selected.isEmpty()) {
 | 
						|
		if (_selected.cbegin().value() == FullSelection) {
 | 
						|
			if (_selected.constFind(_mouseActionItem) != _selected.cend() && App::hoveredItem()) {
 | 
						|
				_mouseAction = MouseAction::PrepareDrag; // start items drag
 | 
						|
			} else if (!_pressWasInactive) {
 | 
						|
				_mouseAction = MouseAction::PrepareSelect; // start items select
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	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) {
 | 
						|
				TextSelection selStatus = { dragState.symbol, dragState.symbol };
 | 
						|
				if (selStatus != FullSelection && (_selected.isEmpty() || _selected.cbegin().value() != FullSelection)) {
 | 
						|
					if (!_selected.isEmpty()) {
 | 
						|
						repaintItem(_selected.cbegin().key());
 | 
						|
						_selected.clear();
 | 
						|
					}
 | 
						|
					_selected.insert(_mouseActionItem, selStatus);
 | 
						|
					_mouseTextSymbol = dragState.symbol;
 | 
						|
					_mouseAction = MouseAction::Selecting;
 | 
						|
					_mouseSelectType = TextSelectType::Paragraphs;
 | 
						|
					mouseActionUpdate(_mousePosition);
 | 
						|
					_trippleClickTimer.start(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;
 | 
						|
				bool 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 (_mouseTextSymbol < selFrom || _mouseTextSymbol >= selTo) {
 | 
						|
							uponSelected = false;
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
				if (uponSelected) {
 | 
						|
					_mouseAction = MouseAction::PrepareDrag; // start text drag
 | 
						|
				} else if (!_pressWasInactive) {
 | 
						|
					if (dynamic_cast<HistorySticker*>(App::pressedItem()->getMedia()) || _mouseCursorState == HistoryInDateCursorState) {
 | 
						|
						_mouseAction = MouseAction::PrepareDrag; // start sticker drag or by-date drag
 | 
						|
					} else {
 | 
						|
						if (dragState.afterSymbol) ++_mouseTextSymbol;
 | 
						|
						TextSelection selStatus = { _mouseTextSymbol, _mouseTextSymbol };
 | 
						|
						if (selStatus != FullSelection && (_selected.isEmpty() || _selected.cbegin().value() != FullSelection)) {
 | 
						|
							if (!_selected.isEmpty()) {
 | 
						|
								repaintItem(_selected.cbegin().key());
 | 
						|
								_selected.clear();
 | 
						|
							}
 | 
						|
							_selected.insert(_mouseActionItem, selStatus);
 | 
						|
							_mouseAction = MouseAction::Selecting;
 | 
						|
							repaintItem(_mouseActionItem);
 | 
						|
						} else {
 | 
						|
							_mouseAction = MouseAction::PrepareSelect;
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
			} else if (!_pressWasInactive) {
 | 
						|
				_mouseAction = MouseAction::PrepareSelect; // start items select
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if (!_mouseActionItem) {
 | 
						|
		_mouseAction = MouseAction::None;
 | 
						|
	} else if (_mouseAction == MouseAction::None) {
 | 
						|
		_mouseActionItem = nullptr;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::mouseActionCancel() {
 | 
						|
	_mouseActionItem = nullptr;
 | 
						|
	_mouseAction = MouseAction::None;
 | 
						|
	_dragStartPosition = QPoint(0, 0);
 | 
						|
	_dragSelFrom = _dragSelTo = nullptr;
 | 
						|
	_wasSelectedText = false;
 | 
						|
	_widget->noSelectingScroll();
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::performDrag() {
 | 
						|
	if (_mouseAction != MouseAction::Dragging) return;
 | 
						|
 | 
						|
	bool 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, 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;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::itemRemoved(HistoryItem *item) {
 | 
						|
	if (_history != item->history() && _migrated != item->history()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	if (!App::main()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	auto i = _selected.find(item);
 | 
						|
	if (i != _selected.cend()) {
 | 
						|
		_selected.erase(i);
 | 
						|
		_widget->updateTopBarSelection();
 | 
						|
	}
 | 
						|
 | 
						|
	if (_mouseActionItem == item) {
 | 
						|
		mouseActionCancel();
 | 
						|
	}
 | 
						|
 | 
						|
	if (_dragSelFrom == item || _dragSelTo == item) {
 | 
						|
		_dragSelFrom = 0;
 | 
						|
		_dragSelTo = 0;
 | 
						|
		update();
 | 
						|
	}
 | 
						|
	onUpdateSelected();
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button) {
 | 
						|
	mouseActionUpdate(screenPos);
 | 
						|
 | 
						|
	ClickHandlerPtr activated = ClickHandler::unpressed();
 | 
						|
	if (_mouseAction == MouseAction::Dragging) {
 | 
						|
		activated.clear();
 | 
						|
	} else if (auto pressed = App::pressedLinkItem()) {
 | 
						|
		// if we are in selecting items mode perhaps we want to
 | 
						|
		// toggle selection instead of activating the pressed link
 | 
						|
		if (_mouseAction == MouseAction::PrepareDrag && !_pressWasInactive && !_selected.isEmpty() && _selected.cbegin().value() == FullSelection && button != Qt::RightButton) {
 | 
						|
			if (auto media = pressed->getMedia()) {
 | 
						|
				if (media->toggleSelectionByHandlerClick(activated)) {
 | 
						|
					activated.clear();
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (App::pressedItem()) {
 | 
						|
		repaintItem(App::pressedItem());
 | 
						|
		App::pressedItem(nullptr);
 | 
						|
	}
 | 
						|
 | 
						|
	_wasSelectedText = false;
 | 
						|
 | 
						|
	if (activated) {
 | 
						|
		mouseActionCancel();
 | 
						|
		App::activateClickHandler(activated, button);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	if (_mouseAction == MouseAction::PrepareSelect && !_pressWasInactive && !_selected.isEmpty() && _selected.cbegin().value() == FullSelection) {
 | 
						|
		SelectedItems::iterator i = _selected.find(_mouseActionItem);
 | 
						|
		if (i == _selected.cend() && !_mouseActionItem->serviceMsg() && _mouseActionItem->id > 0) {
 | 
						|
			if (_selected.size() < MaxSelectedItems) {
 | 
						|
				if (!_selected.isEmpty() && _selected.cbegin().value() != FullSelection) {
 | 
						|
					_selected.clear();
 | 
						|
				}
 | 
						|
				_selected.insert(_mouseActionItem, FullSelection);
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			_selected.erase(i);
 | 
						|
		}
 | 
						|
		repaintItem(_mouseActionItem);
 | 
						|
	} else if (_mouseAction == MouseAction::PrepareDrag && !_pressWasInactive && button != Qt::RightButton) {
 | 
						|
		SelectedItems::iterator i = _selected.find(_mouseActionItem);
 | 
						|
		if (i != _selected.cend() && i.value() == FullSelection) {
 | 
						|
			_selected.erase(i);
 | 
						|
			repaintItem(_mouseActionItem);
 | 
						|
		} else if (i == _selected.cend() && !_mouseActionItem->serviceMsg() && _mouseActionItem->id > 0 && !_selected.isEmpty() && _selected.cbegin().value() == FullSelection) {
 | 
						|
			if (_selected.size() < MaxSelectedItems) {
 | 
						|
				_selected.insert(_mouseActionItem, FullSelection);
 | 
						|
				repaintItem(_mouseActionItem);
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			_selected.clear();
 | 
						|
			update();
 | 
						|
		}
 | 
						|
	} else if (_mouseAction == MouseAction::Selecting) {
 | 
						|
		if (_dragSelFrom && _dragSelTo) {
 | 
						|
			applyDragSelection();
 | 
						|
			_dragSelFrom = _dragSelTo = 0;
 | 
						|
		} else if (!_selected.isEmpty() && !_pressWasInactive) {
 | 
						|
			auto sel = _selected.cbegin().value();
 | 
						|
			if (sel != FullSelection && sel.from == sel.to) {
 | 
						|
				_selected.clear();
 | 
						|
				App::wnd()->setInnerFocus();
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	_mouseAction = MouseAction::None;
 | 
						|
	_mouseActionItem = nullptr;
 | 
						|
	_mouseSelectType = TextSelectType::Letters;
 | 
						|
	_widget->noSelectingScroll();
 | 
						|
	_widget->updateTopBarSelection();
 | 
						|
 | 
						|
#if defined Q_OS_LINUX32 || defined Q_OS_LINUX64
 | 
						|
	if (!_selected.isEmpty() && _selected.cbegin().value() != FullSelection) {
 | 
						|
		setToClipboard(_selected.cbegin().key()->selectedText(_selected.cbegin().value()), QClipboard::Selection);
 | 
						|
	}
 | 
						|
#endif // Q_OS_LINUX32 || Q_OS_LINUX64
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::mouseReleaseEvent(QMouseEvent *e) {
 | 
						|
	mouseActionFinish(e->globalPos(), e->button());
 | 
						|
	if (!rect().contains(e->pos())) {
 | 
						|
		leaveEvent(e);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) {
 | 
						|
	if (!_history) return;
 | 
						|
 | 
						|
	mouseActionStart(e->globalPos(), e->button());
 | 
						|
	if (((_mouseAction == MouseAction::Selecting && !_selected.isEmpty() && _selected.cbegin().value() != FullSelection) || (_mouseAction == MouseAction::None && (_selected.isEmpty() || _selected.cbegin().value() != FullSelection))) && _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;
 | 
						|
				TextSelection selStatus = { dragState.symbol, dragState.symbol };
 | 
						|
				if (!_selected.isEmpty()) {
 | 
						|
					repaintItem(_selected.cbegin().key());
 | 
						|
					_selected.clear();
 | 
						|
				}
 | 
						|
				_selected.insert(_mouseActionItem, selStatus);
 | 
						|
			}
 | 
						|
			mouseMoveEvent(e);
 | 
						|
 | 
						|
			_trippleClickPoint = e->globalPos();
 | 
						|
			_trippleClickTimer.start(QApplication::doubleClickInterval());
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::contextMenuEvent(QContextMenuEvent *e) {
 | 
						|
	showContextMenu(e);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 | 
						|
	if (_menu) {
 | 
						|
		_menu->deleteLater();
 | 
						|
		_menu = nullptr;
 | 
						|
	}
 | 
						|
	if (e->reason() == QContextMenuEvent::Mouse) {
 | 
						|
		mouseActionUpdate(e->globalPos());
 | 
						|
	}
 | 
						|
 | 
						|
	auto selectedState = getSelectionState();
 | 
						|
	auto canSendMessages = _widget->canSendMessages(_peer);
 | 
						|
 | 
						|
	// -2 - has full selected items, but not over, -1 - has selection, but no over, 0 - no selection, 1 - over text, 2 - over full selected items
 | 
						|
	auto isUponSelected = 0;
 | 
						|
	auto hasSelected = 0;;
 | 
						|
	if (!_selected.isEmpty()) {
 | 
						|
		isUponSelected = -1;
 | 
						|
		if (_selected.cbegin().value() == FullSelection) {
 | 
						|
			hasSelected = 2;
 | 
						|
			if (App::hoveredItem() && _selected.constFind(App::hoveredItem()) != _selected.cend()) {
 | 
						|
				isUponSelected = 2;
 | 
						|
			} else {
 | 
						|
				isUponSelected = -2;
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			uint16 selFrom = _selected.cbegin().value().from, selTo = _selected.cbegin().value().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();
 | 
						|
	HistoryItem *item = App::hoveredItem() ? App::hoveredItem() : App::hoveredLinkItem();
 | 
						|
	PhotoClickHandler *lnkPhoto = dynamic_cast<PhotoClickHandler*>(_contextMenuLink.data());
 | 
						|
	DocumentClickHandler *lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLink.data());
 | 
						|
	bool lnkIsVideo = lnkDocument ? lnkDocument->document()->isVideo() : false;
 | 
						|
	bool lnkIsAudio = lnkDocument ? (lnkDocument->document()->voice() != nullptr) : false;
 | 
						|
	bool lnkIsSong = lnkDocument ? (lnkDocument->document()->song() != nullptr) : false;
 | 
						|
	if (lnkPhoto || lnkDocument) {
 | 
						|
		if (isUponSelected > 0) {
 | 
						|
			_menu->addAction(lang((isUponSelected > 1) ? lng_context_copy_selected_items : lng_context_copy_selected), this, SLOT(copySelectedText()))->setEnabled(true);
 | 
						|
		}
 | 
						|
		if (item && item->id > 0 && isUponSelected != 2 && isUponSelected != -2) {
 | 
						|
			if (canSendMessages) {
 | 
						|
				_menu->addAction(lang(lng_context_reply_msg), _widget, SLOT(onReplyToMessage()));
 | 
						|
			}
 | 
						|
			if (item->canEdit(::date(unixtime()))) {
 | 
						|
				_menu->addAction(lang(lng_context_edit_msg), _widget, SLOT(onEditMessage()));
 | 
						|
			}
 | 
						|
			if (item->canPin()) {
 | 
						|
				bool ispinned = (item->history()->peer->asChannel()->mgInfo->pinnedMsgId == item->id);
 | 
						|
				_menu->addAction(lang(ispinned ? lng_context_unpin_msg : lng_context_pin_msg), _widget, ispinned ? SLOT(onUnpinMessage()) : SLOT(onPinMessage()));
 | 
						|
			}
 | 
						|
		}
 | 
						|
		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, SLOT(cancelContextDownload()))->setEnabled(true);
 | 
						|
			} else {
 | 
						|
				if (document->loaded() && document->isGifv()) {
 | 
						|
					if (!cAutoPlayGif()) {
 | 
						|
						_menu->addAction(lang(lng_context_open_gif), this, SLOT(openContextGif()))->setEnabled(true);
 | 
						|
					}
 | 
						|
					_menu->addAction(lang(lng_context_save_gif), this, SLOT(saveContextGif()))->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, SLOT(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 (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) {
 | 
						|
			_menu->addAction(lang(lng_context_copy_post_link), _widget, SLOT(onCopyPostLink()));
 | 
						|
		}
 | 
						|
		if (isUponSelected > 1) {
 | 
						|
			if (selectedState.count > 0 && selectedState.canForwardCount == selectedState.count) {
 | 
						|
				_menu->addAction(lang(lng_context_forward_selected), _widget, SLOT(onForwardSelected()));
 | 
						|
			}
 | 
						|
			if (selectedState.count > 0 && selectedState.canDeleteCount == selectedState.count) {
 | 
						|
				_menu->addAction(lang(lng_context_delete_selected), base::lambda_guarded(this, [this] {
 | 
						|
					_widget->confirmDeleteSelectedItems();
 | 
						|
				}));
 | 
						|
			}
 | 
						|
			_menu->addAction(lang(lng_context_clear_selection), _widget, SLOT(onClearSelected()));
 | 
						|
		} else if (App::hoveredLinkItem()) {
 | 
						|
			if (isUponSelected != -2) {
 | 
						|
				if (App::hoveredLinkItem()->canForward()) {
 | 
						|
					_menu->addAction(lang(lng_context_forward_msg), _widget, SLOT(forwardMessage()))->setEnabled(true);
 | 
						|
				}
 | 
						|
				if (App::hoveredLinkItem()->canDelete()) {
 | 
						|
					_menu->addAction(lang(lng_context_delete_msg), base::lambda_guarded(this, [this] {
 | 
						|
						_widget->confirmDeleteContextItem();
 | 
						|
					}));
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if (App::hoveredLinkItem()->id > 0 && !App::hoveredLinkItem()->serviceMsg()) {
 | 
						|
				_menu->addAction(lang(lng_context_select_msg), _widget, SLOT(selectMessage()))->setEnabled(true);
 | 
						|
			}
 | 
						|
			App::contextItem(App::hoveredLinkItem());
 | 
						|
		}
 | 
						|
	} 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((isUponSelected > 1) ? lng_context_copy_selected_items : lng_context_copy_selected), this, SLOT(copySelectedText()))->setEnabled(true);
 | 
						|
			if (item && item->id > 0 && isUponSelected != 2) {
 | 
						|
				if (canSendMessages) {
 | 
						|
					_menu->addAction(lang(lng_context_reply_msg), _widget, SLOT(onReplyToMessage()));
 | 
						|
				}
 | 
						|
				if (item->canEdit(::date(unixtime()))) {
 | 
						|
					_menu->addAction(lang(lng_context_edit_msg), _widget, SLOT(onEditMessage()));
 | 
						|
				}
 | 
						|
				if (item->canPin()) {
 | 
						|
					bool ispinned = (item->history()->peer->asChannel()->mgInfo->pinnedMsgId == item->id);
 | 
						|
					_menu->addAction(lang(ispinned ? lng_context_unpin_msg : lng_context_pin_msg), _widget, ispinned ? SLOT(onUnpinMessage()) : SLOT(onPinMessage()));
 | 
						|
				}
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			if (item && item->id > 0 && isUponSelected != -2) {
 | 
						|
				if (canSendMessages) {
 | 
						|
					_menu->addAction(lang(lng_context_reply_msg), _widget, SLOT(onReplyToMessage()));
 | 
						|
				}
 | 
						|
				if (item->canEdit(::date(unixtime()))) {
 | 
						|
					_menu->addAction(lang(lng_context_edit_msg), _widget, SLOT(onEditMessage()));
 | 
						|
				}
 | 
						|
				if (item->canPin()) {
 | 
						|
					bool ispinned = (item->history()->peer->asChannel()->mgInfo->pinnedMsgId == item->id);
 | 
						|
					_menu->addAction(lang(ispinned ? lng_context_unpin_msg : lng_context_pin_msg), _widget, ispinned ? SLOT(onUnpinMessage()) : SLOT(onPinMessage()));
 | 
						|
				}
 | 
						|
			}
 | 
						|
			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, SLOT(cancelContextDownload()))->setEnabled(true);
 | 
						|
							} else {
 | 
						|
								if (document->isGifv()) {
 | 
						|
									if (!cAutoPlayGif()) {
 | 
						|
										_menu->addAction(lang(lng_context_open_gif), this, SLOT(openContextGif()))->setEnabled(true);
 | 
						|
									}
 | 
						|
									_menu->addAction(lang(lng_context_save_gif), this, SLOT(saveContextGif()))->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, SLOT(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, SLOT(copyContextText()))->setEnabled(true);
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		auto linkCopyToClipboardText = _contextMenuLink ? _contextMenuLink->copyToClipboardContextItemText() : QString();
 | 
						|
		if (!linkCopyToClipboardText.isEmpty()) {
 | 
						|
			_menu->addAction(linkCopyToClipboardText, this, SLOT(copyContextUrl()))->setEnabled(true);
 | 
						|
		}
 | 
						|
		if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) {
 | 
						|
			_menu->addAction(lang(lng_context_copy_post_link), _widget, SLOT(onCopyPostLink()));
 | 
						|
		}
 | 
						|
		if (isUponSelected > 1) {
 | 
						|
			if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
 | 
						|
				_menu->addAction(lang(lng_context_forward_selected), _widget, SLOT(onForwardSelected()));
 | 
						|
			}
 | 
						|
			if (selectedState.count > 0 && selectedState.count == selectedState.canDeleteCount) {
 | 
						|
				_menu->addAction(lang(lng_context_delete_selected), base::lambda_guarded(this, [this] {
 | 
						|
					_widget->confirmDeleteSelectedItems();
 | 
						|
				}));
 | 
						|
			}
 | 
						|
			_menu->addAction(lang(lng_context_clear_selection), _widget, SLOT(onClearSelected()));
 | 
						|
		} else if (item && ((isUponSelected != -2 && (canForward || canDelete)) || item->id > 0)) {
 | 
						|
			if (isUponSelected != -2) {
 | 
						|
				if (canForward) {
 | 
						|
					_menu->addAction(lang(lng_context_forward_msg), _widget, SLOT(forwardMessage()))->setEnabled(true);
 | 
						|
				}
 | 
						|
 | 
						|
				if (canDelete) {
 | 
						|
					_menu->addAction(lang((msg && msg->uploading()) ? lng_context_cancel_upload : lng_context_delete_msg), base::lambda_guarded(this, [this] {
 | 
						|
						_widget->confirmDeleteContextItem();
 | 
						|
					}));
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if (item->id > 0 && !item->serviceMsg()) {
 | 
						|
				_menu->addAction(lang(lng_context_select_msg), _widget, SLOT(selectMessage()))->setEnabled(true);
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			if (App::mousedItem() && !App::mousedItem()->serviceMsg() && App::mousedItem()->id > 0) {
 | 
						|
				_menu->addAction(lang(lng_context_select_msg), _widget, SLOT(selectMessage()))->setEnabled(true);
 | 
						|
				item = App::mousedItem();
 | 
						|
			}
 | 
						|
		}
 | 
						|
		App::contextItem(item);
 | 
						|
	}
 | 
						|
 | 
						|
	if (_menu->actions().isEmpty()) {
 | 
						|
		delete _menu;
 | 
						|
		_menu = 0;
 | 
						|
	} else {
 | 
						|
		connect(_menu, SIGNAL(destroyed(QObject*)), this, SLOT(onMenuDestroy(QObject*)));
 | 
						|
		_menu->popup(e->globalPos());
 | 
						|
		e->accept();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::onMenuDestroy(QObject *obj) {
 | 
						|
	if (_menu == obj) {
 | 
						|
		_menu = nullptr;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::copySelectedText() {
 | 
						|
	setToClipboard(getSelectedText());
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::copyContextUrl() {
 | 
						|
	if (_contextMenuLink) {
 | 
						|
		_contextMenuLink->copyToClipboard();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::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 HistoryInner::copyContextImage(PhotoData *photo) {
 | 
						|
	if (!photo || !photo->date || !photo->loaded()) return;
 | 
						|
 | 
						|
	QApplication::clipboard()->setPixmap(photo->full->pix());
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::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 HistoryInner::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 HistoryInner::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 HistoryInner::saveDocumentToFile(DocumentData *document) {
 | 
						|
	DocumentSaveClickHandler::doSave(document, true);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::openContextGif() {
 | 
						|
	if (auto item = App::contextItem()) {
 | 
						|
		if (auto media = item->getMedia()) {
 | 
						|
			if (auto document = media->getDocument()) {
 | 
						|
				_controller->window()->showDocument(document, item);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::saveContextGif() {
 | 
						|
	if (auto item = App::contextItem()) {
 | 
						|
		if (auto media = item->getMedia()) {
 | 
						|
			if (auto document = media->getDocument()) {
 | 
						|
				_widget->saveGif(document);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::copyContextText() {
 | 
						|
	auto item = App::contextItem();
 | 
						|
	if (!item || (item->getMedia() && item->getMedia()->type() == MediaTypeSticker)) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	setToClipboard(item->selectedText(FullSelection));
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::setToClipboard(const TextWithEntities &forClipboard, QClipboard::Mode mode) {
 | 
						|
	if (auto data = MimeDataFromTextWithEntities(forClipboard)) {
 | 
						|
		QApplication::clipboard()->setMimeData(data.release(), mode);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::resizeEvent(QResizeEvent *e) {
 | 
						|
	onUpdateSelected();
 | 
						|
}
 | 
						|
 | 
						|
TextWithEntities HistoryInner::getSelectedText() const {
 | 
						|
	SelectedItems sel = _selected;
 | 
						|
 | 
						|
	if (_mouseAction == MouseAction::Selecting && _dragSelFrom && _dragSelTo) {
 | 
						|
		applyDragSelection(&sel);
 | 
						|
	}
 | 
						|
 | 
						|
	if (sel.isEmpty()) {
 | 
						|
		return TextWithEntities();
 | 
						|
	}
 | 
						|
	if (sel.cbegin().value() != FullSelection) {
 | 
						|
		return sel.cbegin().key()->selectedText(sel.cbegin().value());
 | 
						|
	}
 | 
						|
 | 
						|
	int fullSize = 0;
 | 
						|
	QString timeFormat(qsl(", [dd.MM.yy hh:mm]\n"));
 | 
						|
	QMap<int, TextWithEntities> texts;
 | 
						|
	for (auto i = sel.cbegin(), e = sel.cend(); i != e; ++i) {
 | 
						|
		HistoryItem *item = i.key();
 | 
						|
		if (item->detached()) continue;
 | 
						|
 | 
						|
		QString time = item->date.toString(timeFormat);
 | 
						|
		TextWithEntities part, unwrapped = item->selectedText(FullSelection);
 | 
						|
		int size = item->author()->name.size() + time.size() + unwrapped.text.size();
 | 
						|
		part.text.reserve(size);
 | 
						|
 | 
						|
		int y = itemTop(item);
 | 
						|
		if (y >= 0) {
 | 
						|
			part.text.append(item->author()->name).append(time);
 | 
						|
			TextUtilities::Append(part, std::move(unwrapped));
 | 
						|
			texts.insert(y, part);
 | 
						|
			fullSize += size;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	TextWithEntities result;
 | 
						|
	auto sep = qsl("\n\n");
 | 
						|
	result.text.reserve(fullSize + (texts.size() - 1) * sep.size());
 | 
						|
	for (auto i = texts.begin(), e = texts.end(); i != e; ++i) {
 | 
						|
		TextUtilities::Append(result, std::move(i.value()));
 | 
						|
		if (i + 1 != e) {
 | 
						|
			result.text.append(sep);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::keyPressEvent(QKeyEvent *e) {
 | 
						|
	if (e->key() == Qt::Key_Escape) {
 | 
						|
		_widget->onListEscapePressed();
 | 
						|
	} else if (e == QKeySequence::Copy && !_selected.isEmpty()) {
 | 
						|
		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 if (e == QKeySequence::Delete) {
 | 
						|
		auto selectedState = getSelectionState();
 | 
						|
		if (selectedState.count > 0 && selectedState.canDeleteCount == selectedState.count) {
 | 
						|
			_widget->confirmDeleteSelectedItems();
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		e->ignore();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::recountHeight() {
 | 
						|
	int visibleHeight = _scroll->height();
 | 
						|
	int oldHistoryPaddingTop = qMax(visibleHeight - historyHeight() - st::historyPaddingBottom, 0);
 | 
						|
	if (_botAbout && !_botAbout->info->text.isEmpty()) {
 | 
						|
		accumulate_max(oldHistoryPaddingTop, st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botAbout->height);
 | 
						|
	}
 | 
						|
 | 
						|
	_history->resizeGetHeight(_scroll->width());
 | 
						|
	if (_migrated) {
 | 
						|
		_migrated->resizeGetHeight(_scroll->width());
 | 
						|
	}
 | 
						|
 | 
						|
	// with migrated history we perhaps do not need to display first _history message
 | 
						|
	// (if last _migrated message and first _history message are both isGroupMigrate)
 | 
						|
	// or at least we don't need to display first _history date (just skip it by height)
 | 
						|
	_historySkipHeight = 0;
 | 
						|
	if (_migrated) {
 | 
						|
		if (!_migrated->isEmpty() && !_history->isEmpty() && _migrated->loadedAtBottom() && _history->loadedAtTop()) {
 | 
						|
			if (_migrated->blocks.back()->items.back()->date.date() == _history->blocks.front()->items.front()->date.date()) {
 | 
						|
				if (_migrated->blocks.back()->items.back()->isGroupMigrate() && _history->blocks.front()->items.front()->isGroupMigrate()) {
 | 
						|
					_historySkipHeight += _history->blocks.front()->items.front()->height();
 | 
						|
				} else {
 | 
						|
					_historySkipHeight += _history->blocks.front()->items.front()->displayedDateHeight();
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	updateBotInfo(false);
 | 
						|
	if (_botAbout && !_botAbout->info->text.isEmpty()) {
 | 
						|
		int32 tw = _scroll->width() - st::msgMargin.left() - st::msgMargin.right();
 | 
						|
		if (tw > st::msgMaxWidth) tw = st::msgMaxWidth;
 | 
						|
		tw -= st::msgPadding.left() + st::msgPadding.right();
 | 
						|
		int32 mw = qMax(_botAbout->info->text.maxWidth(), st::msgNameFont->width(lang(lng_bot_description)));
 | 
						|
		if (tw > mw) tw = mw;
 | 
						|
 | 
						|
		_botAbout->width = tw;
 | 
						|
		_botAbout->height = _botAbout->info->text.countHeight(_botAbout->width);
 | 
						|
 | 
						|
		int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
 | 
						|
		int32 descMaxWidth = _scroll->width();
 | 
						|
		if (Adaptive::ChatWide()) {
 | 
						|
			descMaxWidth = qMin(descMaxWidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
 | 
						|
		}
 | 
						|
		int32 descAtX = (descMaxWidth - _botAbout->width) / 2 - st::msgPadding.left();
 | 
						|
		int32 descAtY = qMin(_historyPaddingTop - descH, qMax(0, (_scroll->height() - descH) / 2)) + st::msgMargin.top();
 | 
						|
 | 
						|
		_botAbout->rect = QRect(descAtX, descAtY, _botAbout->width + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
 | 
						|
	} else if (_botAbout) {
 | 
						|
		_botAbout->width = _botAbout->height = 0;
 | 
						|
		_botAbout->rect = QRect();
 | 
						|
	}
 | 
						|
 | 
						|
	int newHistoryPaddingTop = qMax(visibleHeight - historyHeight() - st::historyPaddingBottom, 0);
 | 
						|
	if (_botAbout && !_botAbout->info->text.isEmpty()) {
 | 
						|
		accumulate_max(newHistoryPaddingTop, st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botAbout->height);
 | 
						|
	}
 | 
						|
 | 
						|
	auto historyPaddingTopDelta = (newHistoryPaddingTop - oldHistoryPaddingTop);
 | 
						|
	if (historyPaddingTopDelta != 0) {
 | 
						|
		if (_history->scrollTopItem) {
 | 
						|
			_history->scrollTopOffset += historyPaddingTopDelta;
 | 
						|
		} else if (_migrated && _migrated->scrollTopItem) {
 | 
						|
			_migrated->scrollTopOffset += historyPaddingTopDelta;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::updateBotInfo(bool recount) {
 | 
						|
	int newh = 0;
 | 
						|
	if (_botAbout && !_botAbout->info->description.isEmpty()) {
 | 
						|
		if (_botAbout->info->text.isEmpty()) {
 | 
						|
			_botAbout->info->text.setText(st::messageTextStyle, _botAbout->info->description, _historyBotNoMonoOptions);
 | 
						|
			if (recount) {
 | 
						|
				int32 tw = _scroll->width() - st::msgMargin.left() - st::msgMargin.right();
 | 
						|
				if (tw > st::msgMaxWidth) tw = st::msgMaxWidth;
 | 
						|
				tw -= st::msgPadding.left() + st::msgPadding.right();
 | 
						|
				int32 mw = qMax(_botAbout->info->text.maxWidth(), st::msgNameFont->width(lang(lng_bot_description)));
 | 
						|
				if (tw > mw) tw = mw;
 | 
						|
 | 
						|
				_botAbout->width = tw;
 | 
						|
				newh = _botAbout->info->text.countHeight(_botAbout->width);
 | 
						|
			}
 | 
						|
		} else if (recount) {
 | 
						|
			newh = _botAbout->height;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (recount && _botAbout) {
 | 
						|
		if (_botAbout->height != newh) {
 | 
						|
			_botAbout->height = newh;
 | 
						|
			updateSize();
 | 
						|
		}
 | 
						|
		if (_botAbout->height > 0) {
 | 
						|
			int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
 | 
						|
			int32 descAtX = (_scroll->width() - _botAbout->width) / 2 - st::msgPadding.left();
 | 
						|
			int32 descAtY = qMin(_historyPaddingTop - descH, (_scroll->height() - descH) / 2) + st::msgMargin.top();
 | 
						|
 | 
						|
			_botAbout->rect = QRect(descAtX, descAtY, _botAbout->width + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
 | 
						|
		} else {
 | 
						|
			_botAbout->width = 0;
 | 
						|
			_botAbout->rect = QRect();
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
bool HistoryInner::wasSelectedText() const {
 | 
						|
	return _wasSelectedText;
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::setFirstLoading(bool loading) {
 | 
						|
	_firstLoading = loading;
 | 
						|
	update();
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::visibleAreaUpdated(int top, int bottom) {
 | 
						|
	auto scrolledUp = (top < _visibleAreaTop);
 | 
						|
	_visibleAreaTop = top;
 | 
						|
	_visibleAreaBottom = bottom;
 | 
						|
 | 
						|
	// if history has pending resize events we should not update scrollTopItem
 | 
						|
	if (hasPendingResizedItems()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	if (bottom >= _historyPaddingTop + historyHeight() + st::historyPaddingBottom) {
 | 
						|
		_history->forgetScrollState();
 | 
						|
		if (_migrated) {
 | 
						|
			_migrated->forgetScrollState();
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		int htop = historyTop(), mtop = migratedTop();
 | 
						|
		if ((htop >= 0 && top >= htop) || mtop < 0) {
 | 
						|
			_history->countScrollState(top - htop);
 | 
						|
			if (_migrated) {
 | 
						|
				_migrated->forgetScrollState();
 | 
						|
			}
 | 
						|
		} else if (mtop >= 0 && top >= mtop) {
 | 
						|
			_history->forgetScrollState();
 | 
						|
			_migrated->countScrollState(top - mtop);
 | 
						|
		} else {
 | 
						|
			_history->countScrollState(top - htop);
 | 
						|
			if (_migrated) {
 | 
						|
				_migrated->forgetScrollState();
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (scrolledUp) {
 | 
						|
		_scrollDateCheck.call();
 | 
						|
	} else {
 | 
						|
		onScrollDateHideByTimer();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
bool HistoryInner::displayScrollDate() const {
 | 
						|
	return (_visibleAreaTop <= height() - 2 * (_visibleAreaBottom - _visibleAreaTop));
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::onScrollDateCheck() {
 | 
						|
	if (!_history) return;
 | 
						|
 | 
						|
	auto newScrollDateItem = _history->scrollTopItem ? _history->scrollTopItem : (_migrated ? _migrated->scrollTopItem : nullptr);
 | 
						|
	auto newScrollDateItemTop = _history->scrollTopItem ? _history->scrollTopOffset : (_migrated ? _migrated->scrollTopOffset : 0);
 | 
						|
	//if (newScrollDateItem && !displayScrollDate()) {
 | 
						|
	//	if (!_history->isEmpty() && newScrollDateItem->date.date() == _history->blocks.back()->items.back()->date.date()) {
 | 
						|
	//		newScrollDateItem = nullptr;
 | 
						|
	//	}
 | 
						|
	//}
 | 
						|
	if (!newScrollDateItem) {
 | 
						|
		_scrollDateLastItem = nullptr;
 | 
						|
		_scrollDateLastItemTop = 0;
 | 
						|
		scrollDateHide();
 | 
						|
	} else if (newScrollDateItem != _scrollDateLastItem || newScrollDateItemTop != _scrollDateLastItemTop) {
 | 
						|
		// Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem).
 | 
						|
		if (_scrollDateLastItem && !_scrollDateShown) {
 | 
						|
			toggleScrollDateShown();
 | 
						|
		}
 | 
						|
		_scrollDateLastItem = newScrollDateItem;
 | 
						|
		_scrollDateLastItemTop = newScrollDateItemTop;
 | 
						|
		_scrollDateHideTimer.start(kScrollDateHideTimeout);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::onScrollDateHideByTimer() {
 | 
						|
	_scrollDateHideTimer.stop();
 | 
						|
	if (!_scrollDateLink || ClickHandler::getPressed() != _scrollDateLink) {
 | 
						|
		scrollDateHide();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::scrollDateHide() {
 | 
						|
	if (_scrollDateShown) {
 | 
						|
		toggleScrollDateShown();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::keepScrollDateForNow() {
 | 
						|
	if (!_scrollDateShown && _scrollDateLastItem && _scrollDateOpacity.animating()) {
 | 
						|
		toggleScrollDateShown();
 | 
						|
	}
 | 
						|
	_scrollDateHideTimer.start(kScrollDateHideTimeout);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::toggleScrollDateShown() {
 | 
						|
	_scrollDateShown = !_scrollDateShown;
 | 
						|
	auto from = _scrollDateShown ? 0. : 1.;
 | 
						|
	auto to = _scrollDateShown ? 1. : 0.;
 | 
						|
	_scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::repaintScrollDateCallback() {
 | 
						|
	int updateTop = _visibleAreaTop;
 | 
						|
	int updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();
 | 
						|
	update(0, updateTop, width(), updateHeight);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::updateSize() {
 | 
						|
	int visibleHeight = _scroll->height();
 | 
						|
	int newHistoryPaddingTop = qMax(visibleHeight - historyHeight() - st::historyPaddingBottom, 0);
 | 
						|
	if (_botAbout && !_botAbout->info->text.isEmpty()) {
 | 
						|
		accumulate_max(newHistoryPaddingTop, st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botAbout->height);
 | 
						|
	}
 | 
						|
 | 
						|
	if (_botAbout && _botAbout->height > 0) {
 | 
						|
		int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
 | 
						|
		int32 descMaxWidth = _scroll->width();
 | 
						|
		if (Adaptive::ChatWide()) {
 | 
						|
			descMaxWidth = qMin(descMaxWidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
 | 
						|
		}
 | 
						|
		int32 descAtX = (descMaxWidth - _botAbout->width) / 2 - st::msgPadding.left();
 | 
						|
		int32 descAtY = qMin(newHistoryPaddingTop - descH, qMax(0, (_scroll->height() - descH) / 2)) + st::msgMargin.top();
 | 
						|
 | 
						|
		_botAbout->rect = QRect(descAtX, descAtY, _botAbout->width + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
 | 
						|
	}
 | 
						|
 | 
						|
	_historyPaddingTop = newHistoryPaddingTop;
 | 
						|
 | 
						|
	int newHeight = _historyPaddingTop + historyHeight() + st::historyPaddingBottom;
 | 
						|
	if (width() != _scroll->width() || height() != newHeight) {
 | 
						|
		resize(_scroll->width(), newHeight);
 | 
						|
 | 
						|
		mouseActionUpdate(QCursor::pos());
 | 
						|
	} else {
 | 
						|
		update();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::enterEventHook(QEvent *e) {
 | 
						|
	mouseActionUpdate(QCursor::pos());
 | 
						|
	return TWidget::enterEventHook(e);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::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);
 | 
						|
}
 | 
						|
 | 
						|
HistoryInner::~HistoryInner() {
 | 
						|
	delete _menu;
 | 
						|
	_mouseAction = MouseAction::None;
 | 
						|
}
 | 
						|
 | 
						|
bool HistoryInner::focusNextPrevChild(bool next) {
 | 
						|
	if (_selected.isEmpty()) {
 | 
						|
		return TWidget::focusNextPrevChild(next);
 | 
						|
	} else {
 | 
						|
		clearSelectedItems();
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::adjustCurrent(int32 y) const {
 | 
						|
	int32 htop = historyTop(), hdrawtop = historyDrawTop(), mtop = migratedTop();
 | 
						|
	_curHistory = 0;
 | 
						|
	if (mtop >= 0) {
 | 
						|
		adjustCurrent(y - mtop, _migrated);
 | 
						|
	}
 | 
						|
	if (htop >= 0 && hdrawtop >= 0 && (mtop < 0 || y >= hdrawtop)) {
 | 
						|
		adjustCurrent(y - htop, _history);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::adjustCurrent(int32 y, History *history) const {
 | 
						|
	t_assert(!history->isEmpty());
 | 
						|
	_curHistory = history;
 | 
						|
	if (_curBlock >= history->blocks.size()) {
 | 
						|
		_curBlock = history->blocks.size() - 1;
 | 
						|
		_curItem = 0;
 | 
						|
	}
 | 
						|
	while (history->blocks[_curBlock]->y() > y && _curBlock > 0) {
 | 
						|
		--_curBlock;
 | 
						|
		_curItem = 0;
 | 
						|
	}
 | 
						|
	while (history->blocks[_curBlock]->y() + history->blocks[_curBlock]->height() <= y && _curBlock + 1 < history->blocks.size()) {
 | 
						|
		++_curBlock;
 | 
						|
		_curItem = 0;
 | 
						|
	}
 | 
						|
	auto block = history->blocks[_curBlock];
 | 
						|
	if (_curItem >= block->items.size()) {
 | 
						|
		_curItem = block->items.size() - 1;
 | 
						|
	}
 | 
						|
	auto by = block->y();
 | 
						|
	while (block->items[_curItem]->y() + by > y && _curItem > 0) {
 | 
						|
		--_curItem;
 | 
						|
	}
 | 
						|
	while (block->items[_curItem]->y() + block->items[_curItem]->height() + by <= y && _curItem + 1 < block->items.size()) {
 | 
						|
		++_curItem;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
HistoryItem *HistoryInner::prevItem(HistoryItem *item) {
 | 
						|
	if (!item || item->detached()) return nullptr;
 | 
						|
 | 
						|
	HistoryBlock *block = item->block();
 | 
						|
	int blockIndex = block->indexInHistory(), itemIndex = item->indexInBlock();
 | 
						|
	if (itemIndex > 0) {
 | 
						|
		return block->items.at(itemIndex - 1);
 | 
						|
	}
 | 
						|
	if (blockIndex > 0) {
 | 
						|
		return item->history()->blocks.at(blockIndex - 1)->items.back();
 | 
						|
	}
 | 
						|
	if (item->history() == _history && _migrated && _history->loadedAtTop() && !_migrated->isEmpty() && _migrated->loadedAtBottom()) {
 | 
						|
		return _migrated->blocks.back()->items.back();
 | 
						|
	}
 | 
						|
	return nullptr;
 | 
						|
}
 | 
						|
 | 
						|
HistoryItem *HistoryInner::nextItem(HistoryItem *item) {
 | 
						|
	if (!item || item->detached()) return nullptr;
 | 
						|
 | 
						|
	HistoryBlock *block = item->block();
 | 
						|
	int blockIndex = block->indexInHistory(), itemIndex = item->indexInBlock();
 | 
						|
	if (itemIndex + 1 < block->items.size()) {
 | 
						|
		return block->items.at(itemIndex + 1);
 | 
						|
	}
 | 
						|
	if (blockIndex + 1 < item->history()->blocks.size()) {
 | 
						|
		return item->history()->blocks.at(blockIndex + 1)->items.front();
 | 
						|
	}
 | 
						|
	if (item->history() == _migrated && _history && _migrated->loadedAtBottom() && _history->loadedAtTop() && !_history->isEmpty()) {
 | 
						|
		return _history->blocks.front()->items.front();
 | 
						|
	}
 | 
						|
	return nullptr;
 | 
						|
}
 | 
						|
 | 
						|
bool HistoryInner::canCopySelected() const {
 | 
						|
	return !_selected.isEmpty();
 | 
						|
}
 | 
						|
 | 
						|
bool HistoryInner::canDeleteSelected() const {
 | 
						|
	auto selectedState = getSelectionState();
 | 
						|
	return (selectedState.count > 0) && (selectedState.count == selectedState.canDeleteCount);
 | 
						|
}
 | 
						|
 | 
						|
Window::TopBarWidget::SelectedState HistoryInner::getSelectionState() const {
 | 
						|
	auto result = Window::TopBarWidget::SelectedState {};
 | 
						|
	for (auto i = _selected.cbegin(), e = _selected.cend(); i != e; ++i) {
 | 
						|
		if (i.value() == FullSelection) {
 | 
						|
			++result.count;
 | 
						|
			if (i.key()->canDelete()) {
 | 
						|
				++result.canDeleteCount;
 | 
						|
			}
 | 
						|
			if (i.key()->canForward()) {
 | 
						|
				++result.canForwardCount;
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			result.textSelected = true;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::clearSelectedItems(bool onlyTextSelection) {
 | 
						|
	if (!_selected.isEmpty() && (!onlyTextSelection || _selected.cbegin().value() != FullSelection)) {
 | 
						|
		_selected.clear();
 | 
						|
		_widget->updateTopBarSelection();
 | 
						|
		_widget->update();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
SelectedItemSet HistoryInner::getSelectedItems() const {
 | 
						|
	auto result = SelectedItemSet();
 | 
						|
	if (_selected.isEmpty() || _selected.cbegin().value() != FullSelection) {
 | 
						|
		return result;
 | 
						|
	}
 | 
						|
 | 
						|
	for (auto i = _selected.cbegin(), e = _selected.cend(); i != e; ++i) {
 | 
						|
		auto item = i.key();
 | 
						|
		if (item && item->toHistoryMessage() && item->id > 0) {
 | 
						|
			if (item->history() == _migrated) {
 | 
						|
				result.insert(item->id - ServerMaxMsgId, item);
 | 
						|
			} else {
 | 
						|
				result.insert(item->id, item);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::selectItem(HistoryItem *item) {
 | 
						|
	if (!_selected.isEmpty() && _selected.cbegin().value() != FullSelection) {
 | 
						|
		_selected.clear();
 | 
						|
	} else if (_selected.size() == MaxSelectedItems && _selected.constFind(item) == _selected.cend()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	_selected.insert(item, FullSelection);
 | 
						|
	_widget->updateTopBarSelection();
 | 
						|
	_widget->update();
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::onTouchSelect() {
 | 
						|
	_touchSelect = true;
 | 
						|
	mouseActionStart(_touchPos, Qt::LeftButton);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::onUpdateSelected() {
 | 
						|
	if (!_history || hasPendingResizedItems()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	auto mousePos = mapFromGlobal(_mousePosition);
 | 
						|
	auto point = _widget->clampMousePosition(mousePos);
 | 
						|
 | 
						|
	HistoryBlock *block = 0;
 | 
						|
	HistoryItem *item = 0;
 | 
						|
	QPoint m;
 | 
						|
 | 
						|
	adjustCurrent(point.y());
 | 
						|
	if (_curHistory && !_curHistory->isEmpty()) {
 | 
						|
		block = _curHistory->blocks[_curBlock];
 | 
						|
		item = block->items[_curItem];
 | 
						|
 | 
						|
		App::mousedItem(item);
 | 
						|
		m = mapPointToItem(point, item);
 | 
						|
		if (item->hasPoint(m)) {
 | 
						|
			if (App::hoveredItem() != item) {
 | 
						|
				repaintItem(App::hoveredItem());
 | 
						|
				App::hoveredItem(item);
 | 
						|
				repaintItem(App::hoveredItem());
 | 
						|
			}
 | 
						|
		} else if (App::hoveredItem()) {
 | 
						|
			repaintItem(App::hoveredItem());
 | 
						|
			App::hoveredItem(0);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (_mouseActionItem && _mouseActionItem->detached()) {
 | 
						|
		mouseActionCancel();
 | 
						|
	}
 | 
						|
 | 
						|
	HistoryTextState dragState;
 | 
						|
	ClickHandlerHost *lnkhost = nullptr;
 | 
						|
	bool selectingText = (item == _mouseActionItem && item == App::hoveredItem() && !_selected.isEmpty() && _selected.cbegin().value() != FullSelection);
 | 
						|
	if (point.y() < _historyPaddingTop) {
 | 
						|
		if (_botAbout && !_botAbout->info->text.isEmpty() && _botAbout->height > 0) {
 | 
						|
			dragState = _botAbout->info->text.getState(point - _botAbout->rect.topLeft() - QPoint(st::msgPadding.left(), st::msgPadding.top() + st::botDescSkip + st::msgNameFont->height), _botAbout->width);
 | 
						|
			lnkhost = _botAbout.get();
 | 
						|
		}
 | 
						|
	} else if (item) {
 | 
						|
		if (item != _mouseActionItem || (m - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
 | 
						|
			if (_mouseAction == MouseAction::PrepareDrag) {
 | 
						|
				_mouseAction = MouseAction::Dragging;
 | 
						|
				InvokeQueued(this, [this] { performDrag(); });
 | 
						|
			} else if (_mouseAction == MouseAction::PrepareSelect) {
 | 
						|
				_mouseAction = MouseAction::Selecting;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
 | 
						|
		auto scrollDateOpacity = _scrollDateOpacity.current(_scrollDateShown ? 1. : 0.);
 | 
						|
		enumerateDates([this, &dragState, &lnkhost, &point, scrollDateOpacity, dateHeight/*, lastDate, showFloatingBefore*/](gsl::not_null<HistoryItem*> item, int itemtop, int dateTop) {
 | 
						|
			// stop enumeration if the date is above our point
 | 
						|
			if (dateTop + dateHeight <= point.y()) {
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
 | 
						|
			bool displayDate = item->displayDate();
 | 
						|
			bool dateInPlace = displayDate;
 | 
						|
			if (dateInPlace) {
 | 
						|
				int correctDateTop = itemtop + st::msgServiceMargin.top();
 | 
						|
				dateInPlace = (dateTop < correctDateTop + dateHeight);
 | 
						|
			}
 | 
						|
 | 
						|
			// stop enumeration if we've found a date under the cursor
 | 
						|
			if (dateTop <= point.y()) {
 | 
						|
				auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;
 | 
						|
				if (opacity > 0.) {
 | 
						|
					auto dateWidth = 0;
 | 
						|
					if (auto date = item->Get<HistoryMessageDate>()) {
 | 
						|
						dateWidth = date->_width;
 | 
						|
					} else {
 | 
						|
						dateWidth = st::msgServiceFont->width(langDayOfMonthFull(item->date.date()));
 | 
						|
					}
 | 
						|
					dateWidth += st::msgServicePadding.left() + st::msgServicePadding.right();
 | 
						|
					auto dateLeft = st::msgServiceMargin.left();
 | 
						|
					auto maxwidth = item->history()->width;
 | 
						|
					if (Adaptive::ChatWide()) {
 | 
						|
						maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
 | 
						|
					}
 | 
						|
					auto widthForDate = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left();
 | 
						|
 | 
						|
					dateLeft += (widthForDate - dateWidth) / 2;
 | 
						|
 | 
						|
					if (point.x() >= dateLeft && point.x() < dateLeft + dateWidth) {
 | 
						|
						if (!_scrollDateLink) {
 | 
						|
							_scrollDateLink = MakeShared<DateClickHandler>(item->history()->peer, item->date.date());
 | 
						|
						} else {
 | 
						|
							static_cast<DateClickHandler*>(_scrollDateLink.data())->setDate(item->date.date());
 | 
						|
						}
 | 
						|
						dragState.link = _scrollDateLink;
 | 
						|
						lnkhost = item;
 | 
						|
					}
 | 
						|
				}
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
			return true;
 | 
						|
		});
 | 
						|
		if (!dragState.link) {
 | 
						|
			HistoryStateRequest request;
 | 
						|
			if (_mouseAction == MouseAction::Selecting) {
 | 
						|
				request.flags |= Text::StateRequest::Flag::LookupSymbol;
 | 
						|
			} else {
 | 
						|
				selectingText = false;
 | 
						|
			}
 | 
						|
			dragState = item->getState(m, request);
 | 
						|
			lnkhost = item;
 | 
						|
			if (!dragState.link && m.x() >= st::historyPhotoLeft && m.x() < st::historyPhotoLeft + st::msgPhotoSize) {
 | 
						|
				if (auto msg = item->toHistoryMessage()) {
 | 
						|
					if (msg->hasFromPhoto()) {
 | 
						|
						enumerateUserpics([&dragState, &lnkhost, &point](gsl::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);
 | 
						|
	}
 | 
						|
 | 
						|
	Qt::CursorShape cur = style::cur_default;
 | 
						|
	if (_mouseAction == MouseAction::None) {
 | 
						|
		_mouseCursorState = dragState.cursor;
 | 
						|
		if (dragState.link) {
 | 
						|
			cur = style::cur_pointer;
 | 
						|
		} else if (_mouseCursorState == HistoryInTextCursorState && (_selected.isEmpty() || _selected.cbegin().value() != FullSelection)) {
 | 
						|
			cur = style::cur_text;
 | 
						|
		} else if (_mouseCursorState == HistoryInDateCursorState) {
 | 
						|
			//			cur = style::cur_cross;
 | 
						|
		}
 | 
						|
	} else if (item) {
 | 
						|
		if (_mouseAction == MouseAction::Selecting) {
 | 
						|
			auto canSelectMany = (_history != nullptr);
 | 
						|
			if (selectingText) {
 | 
						|
				uint16 second = dragState.symbol;
 | 
						|
				if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {
 | 
						|
					++second;
 | 
						|
				}
 | 
						|
				auto selState = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) };
 | 
						|
				if (_mouseSelectType != TextSelectType::Letters) {
 | 
						|
					selState = _mouseActionItem->adjustSelection(selState, _mouseSelectType);
 | 
						|
				}
 | 
						|
				if (_selected[_mouseActionItem] != selState) {
 | 
						|
					_selected[_mouseActionItem] = selState;
 | 
						|
					repaintItem(_mouseActionItem);
 | 
						|
				}
 | 
						|
				if (!_wasSelectedText && (selState == FullSelection || selState.from != selState.to)) {
 | 
						|
					_wasSelectedText = true;
 | 
						|
					setFocus();
 | 
						|
				}
 | 
						|
				updateDragSelection(0, 0, false);
 | 
						|
			} else if (canSelectMany) {
 | 
						|
				auto selectingDown = (itemTop(_mouseActionItem) < itemTop(item)) || (_mouseActionItem == item && _dragStartPosition.y() < m.y());
 | 
						|
				auto dragSelFrom = _mouseActionItem, dragSelTo = item;
 | 
						|
				if (!dragSelFrom->hasPoint(_dragStartPosition)) { // maybe exclude dragSelFrom
 | 
						|
					if (selectingDown) {
 | 
						|
						if (_dragStartPosition.y() >= dragSelFrom->height() - dragSelFrom->marginBottom() || ((item == dragSelFrom) && (m.y() < _dragStartPosition.y() + QApplication::startDragDistance() || m.y() < dragSelFrom->marginTop()))) {
 | 
						|
							dragSelFrom = (dragSelFrom == dragSelTo) ? 0 : nextItem(dragSelFrom);
 | 
						|
						}
 | 
						|
					} else {
 | 
						|
						if (_dragStartPosition.y() < dragSelFrom->marginTop() || ((item == dragSelFrom) && (m.y() >= _dragStartPosition.y() - QApplication::startDragDistance() || m.y() >= dragSelFrom->height() - dragSelFrom->marginBottom()))) {
 | 
						|
							dragSelFrom = (dragSelFrom == dragSelTo) ? 0 : prevItem(dragSelFrom);
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
				if (_mouseActionItem != item) { // maybe exclude dragSelTo
 | 
						|
					if (selectingDown) {
 | 
						|
						if (m.y() < dragSelTo->marginTop()) {
 | 
						|
							dragSelTo = (dragSelFrom == dragSelTo) ? 0 : prevItem(dragSelTo);
 | 
						|
						}
 | 
						|
					} else {
 | 
						|
						if (m.y() >= dragSelTo->height() - dragSelTo->marginBottom()) {
 | 
						|
							dragSelTo = (dragSelFrom == dragSelTo) ? 0 : nextItem(dragSelTo);
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
				auto dragSelecting = false;
 | 
						|
				auto dragFirstAffected = dragSelFrom;
 | 
						|
				while (dragFirstAffected && (dragFirstAffected->id < 0 || dragFirstAffected->serviceMsg())) {
 | 
						|
					dragFirstAffected = (dragFirstAffected == dragSelTo) ? 0 : (selectingDown ? nextItem(dragFirstAffected) : prevItem(dragFirstAffected));
 | 
						|
				}
 | 
						|
				if (dragFirstAffected) {
 | 
						|
					auto i = _selected.constFind(dragFirstAffected);
 | 
						|
					dragSelecting = (i == _selected.cend() || i.value() != FullSelection);
 | 
						|
				}
 | 
						|
				updateDragSelection(dragSelFrom, dragSelTo, dragSelecting);
 | 
						|
			}
 | 
						|
		} else if (_mouseAction == MouseAction::Dragging) {
 | 
						|
		}
 | 
						|
 | 
						|
		if (ClickHandler::getPressed()) {
 | 
						|
			cur = style::cur_pointer;
 | 
						|
		} else if (_mouseAction == MouseAction::Selecting && !_selected.isEmpty() && _selected.cbegin().value() != FullSelection) {
 | 
						|
			if (!_dragSelFrom || !_dragSelTo) {
 | 
						|
				cur = style::cur_text;
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Voice message seek support.
 | 
						|
	if (auto pressedItem = App::pressedLinkItem()) {
 | 
						|
		if (!pressedItem->detached()) {
 | 
						|
			if (pressedItem->history() == _history || pressedItem->history() == _migrated) {
 | 
						|
				auto adjustedPoint = mapPointToItem(point, pressedItem);
 | 
						|
				pressedItem->updatePressed(adjustedPoint);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if (_mouseAction == MouseAction::Selecting) {
 | 
						|
		_widget->checkSelectingScroll(mousePos);
 | 
						|
	} else {
 | 
						|
		updateDragSelection(0, 0, false);
 | 
						|
		_widget->noSelectingScroll();
 | 
						|
	}
 | 
						|
 | 
						|
	if (_mouseAction == MouseAction::None && (lnkChanged || cur != _cursor)) {
 | 
						|
		setCursor(_cursor = cur);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::updateDragSelection(HistoryItem *dragSelFrom, HistoryItem *dragSelTo, bool dragSelecting, bool force) {
 | 
						|
	if (_dragSelFrom != dragSelFrom || _dragSelTo != dragSelTo || _dragSelecting != dragSelecting) {
 | 
						|
		_dragSelFrom = dragSelFrom;
 | 
						|
		_dragSelTo = dragSelTo;
 | 
						|
		int32 fromy = itemTop(_dragSelFrom), toy = itemTop(_dragSelTo);
 | 
						|
		if (fromy >= 0 && toy >= 0 && fromy > toy) {
 | 
						|
			qSwap(_dragSelFrom, _dragSelTo);
 | 
						|
		}
 | 
						|
		_dragSelecting = dragSelecting;
 | 
						|
		if (!_wasSelectedText && _dragSelFrom && _dragSelTo && _dragSelecting) {
 | 
						|
			_wasSelectedText = true;
 | 
						|
			setFocus();
 | 
						|
		}
 | 
						|
		force = true;
 | 
						|
	}
 | 
						|
	if (!force) return;
 | 
						|
 | 
						|
	update();
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::BotAbout::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
 | 
						|
	_parent->update(rect);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::BotAbout::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
 | 
						|
	_parent->update(rect);
 | 
						|
}
 | 
						|
 | 
						|
int HistoryInner::historyHeight() const {
 | 
						|
	int result = 0;
 | 
						|
	if (!_history || _history->isEmpty()) {
 | 
						|
		result += _migrated ? _migrated->height : 0;
 | 
						|
	} else {
 | 
						|
		result += _history->height - _historySkipHeight + (_migrated ? _migrated->height : 0);
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
int HistoryInner::historyScrollTop() const {
 | 
						|
	auto htop = historyTop();
 | 
						|
	auto mtop = migratedTop();
 | 
						|
	if (htop >= 0 && _history->scrollTopItem) {
 | 
						|
		t_assert(!_history->scrollTopItem->detached());
 | 
						|
		return htop + _history->scrollTopItem->block()->y() + _history->scrollTopItem->y() + _history->scrollTopOffset;
 | 
						|
	}
 | 
						|
	if (mtop >= 0 && _migrated->scrollTopItem) {
 | 
						|
		t_assert(!_migrated->scrollTopItem->detached());
 | 
						|
		return mtop + _migrated->scrollTopItem->block()->y() + _migrated->scrollTopItem->y() + _migrated->scrollTopOffset;
 | 
						|
	}
 | 
						|
	return ScrollMax;
 | 
						|
}
 | 
						|
 | 
						|
int HistoryInner::migratedTop() const {
 | 
						|
	return (_migrated && !_migrated->isEmpty()) ? _historyPaddingTop : -1;
 | 
						|
}
 | 
						|
 | 
						|
int HistoryInner::historyTop() const {
 | 
						|
	int mig = migratedTop();
 | 
						|
	return (_history && !_history->isEmpty()) ? (mig >= 0 ? (mig + _migrated->height - _historySkipHeight) : _historyPaddingTop) : -1;
 | 
						|
}
 | 
						|
 | 
						|
int HistoryInner::historyDrawTop() const {
 | 
						|
	auto top = historyTop();
 | 
						|
	return (top >= 0) ? (top + _historySkipHeight) : -1;
 | 
						|
}
 | 
						|
 | 
						|
int HistoryInner::itemTop(const HistoryItem *item) const { // -1 if should not be visible, -2 if bad history()
 | 
						|
	if (!item) return -2;
 | 
						|
	if (item->detached()) return -1;
 | 
						|
 | 
						|
	auto top = (item->history() == _history) ? historyTop() : (item->history() == _migrated ? migratedTop() : -2);
 | 
						|
	return (top < 0) ? top : (top + item->y() + item->block()->y());
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::notifyIsBotChanged() {
 | 
						|
	BotInfo *newinfo = (_history && _history->peer->isUser()) ? _history->peer->asUser()->botInfo.get() : nullptr;
 | 
						|
	if ((!newinfo && !_botAbout) || (newinfo && _botAbout && _botAbout->info == newinfo)) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	if (newinfo) {
 | 
						|
		_botAbout.reset(new BotAbout(this, newinfo));
 | 
						|
		if (newinfo && !newinfo->inited) {
 | 
						|
			AuthSession::Current().api().requestFullPeer(_peer);
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		_botAbout = nullptr;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::notifyMigrateUpdated() {
 | 
						|
	_migrated = _peer->migrateFrom() ? App::history(_peer->migrateFrom()->id) : 0;
 | 
						|
}
 | 
						|
 | 
						|
int HistoryInner::moveScrollFollowingInlineKeyboard(const HistoryItem *item, int oldKeyboardTop, int newKeyboardTop) {
 | 
						|
	if (item == App::mousedItem()) {
 | 
						|
		int top = itemTop(item);
 | 
						|
		if (top >= oldKeyboardTop) {
 | 
						|
			return newKeyboardTop - oldKeyboardTop;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return 0;
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::applyDragSelection() {
 | 
						|
	applyDragSelection(&_selected);
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::addSelectionRange(SelectedItems *toItems, int32 fromblock, int32 fromitem, int32 toblock, int32 toitem, History *h) const {
 | 
						|
	if (fromblock >= 0 && fromitem >= 0 && toblock >= 0 && toitem >= 0) {
 | 
						|
		for (; fromblock <= toblock; ++fromblock) {
 | 
						|
			HistoryBlock *block = h->blocks[fromblock];
 | 
						|
			for (int32 cnt = (fromblock < toblock) ? block->items.size() : (toitem + 1); fromitem < cnt; ++fromitem) {
 | 
						|
				HistoryItem *item = block->items[fromitem];
 | 
						|
				SelectedItems::iterator i = toItems->find(item);
 | 
						|
				if (item->id > 0 && !item->serviceMsg()) {
 | 
						|
					if (i == toItems->cend()) {
 | 
						|
						if (toItems->size() >= MaxSelectedItems) break;
 | 
						|
						toItems->insert(item, FullSelection);
 | 
						|
					} else if (i.value() != FullSelection) {
 | 
						|
						*i = FullSelection;
 | 
						|
					}
 | 
						|
				} else {
 | 
						|
					if (i != toItems->cend()) {
 | 
						|
						toItems->erase(i);
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if (toItems->size() >= MaxSelectedItems) break;
 | 
						|
			fromitem = 0;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::applyDragSelection(SelectedItems *toItems) const {
 | 
						|
	int32 selfromy = itemTop(_dragSelFrom), seltoy = itemTop(_dragSelTo);
 | 
						|
	if (selfromy < 0 || seltoy < 0) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	seltoy += _dragSelTo->height();
 | 
						|
 | 
						|
	if (!toItems->isEmpty() && toItems->cbegin().value() != FullSelection) {
 | 
						|
		toItems->clear();
 | 
						|
	}
 | 
						|
	if (_dragSelecting) {
 | 
						|
		int32 fromblock = _dragSelFrom->block()->indexInHistory(), fromitem = _dragSelFrom->indexInBlock();
 | 
						|
		int32 toblock = _dragSelTo->block()->indexInHistory(), toitem = _dragSelTo->indexInBlock();
 | 
						|
		if (_migrated) {
 | 
						|
			if (_dragSelFrom->history() == _migrated) {
 | 
						|
				if (_dragSelTo->history() == _migrated) {
 | 
						|
					addSelectionRange(toItems, fromblock, fromitem, toblock, toitem, _migrated);
 | 
						|
					toblock = -1;
 | 
						|
					toitem = -1;
 | 
						|
				} else {
 | 
						|
					addSelectionRange(toItems, fromblock, fromitem, _migrated->blocks.size() - 1, _migrated->blocks.back()->items.size() - 1, _migrated);
 | 
						|
				}
 | 
						|
				fromblock = 0;
 | 
						|
				fromitem = 0;
 | 
						|
			} else if (_dragSelTo->history() == _migrated) { // wtf
 | 
						|
				toblock = -1;
 | 
						|
				toitem = -1;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		addSelectionRange(toItems, fromblock, fromitem, toblock, toitem, _history);
 | 
						|
	} else {
 | 
						|
		for (SelectedItems::iterator i = toItems->begin(); i != toItems->cend();) {
 | 
						|
			int32 iy = itemTop(i.key());
 | 
						|
			if (iy < 0) {
 | 
						|
				if (iy < -1) i = toItems->erase(i);
 | 
						|
				continue;
 | 
						|
			}
 | 
						|
			if (iy >= selfromy && iy < seltoy) {
 | 
						|
				i = toItems->erase(i);
 | 
						|
			} else {
 | 
						|
				++i;
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
QString HistoryInner::tooltipText() const {
 | 
						|
	if (_mouseCursorState == HistoryInDateCursorState && _mouseAction == MouseAction::None) {
 | 
						|
		if (App::hoveredItem()) {
 | 
						|
			auto dateText = App::hoveredItem()->date.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat));
 | 
						|
			if (auto edited = App::hoveredItem()->Get<HistoryMessageEdited>()) {
 | 
						|
				dateText += '\n' + lng_edited_date(lt_date, edited->_editDate.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat)));
 | 
						|
			}
 | 
						|
			if (auto forwarded = App::hoveredItem()->Get<HistoryMessageForwarded>()) {
 | 
						|
				dateText += '\n' + lng_forwarded_date(lt_date, forwarded->_originalDate.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat)));
 | 
						|
			}
 | 
						|
			return dateText;
 | 
						|
		}
 | 
						|
	} else if (_mouseCursorState == HistoryInForwardedCursorState && _mouseAction == MouseAction::None) {
 | 
						|
		if (App::hoveredItem()) {
 | 
						|
			if (auto forwarded = App::hoveredItem()->Get<HistoryMessageForwarded>()) {
 | 
						|
				return forwarded->_text.originalText(AllTextSelection, ExpandLinksNone);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	} else if (auto lnk = ClickHandler::getActive()) {
 | 
						|
		return lnk->tooltip();
 | 
						|
	}
 | 
						|
	return QString();
 | 
						|
}
 | 
						|
 | 
						|
QPoint HistoryInner::tooltipPos() const {
 | 
						|
	return _mousePosition;
 | 
						|
}
 | 
						|
 | 
						|
void HistoryInner::onParentGeometryChanged() {
 | 
						|
	auto mousePos = QCursor::pos();
 | 
						|
	auto mouseOver = _widget->rect().contains(_widget->mapFromGlobal(mousePos));
 | 
						|
	auto needToUpdate = (_mouseAction != MouseAction::None || _touchScroll || mouseOver);
 | 
						|
	if (needToUpdate) {
 | 
						|
		mouseActionUpdate(mousePos);
 | 
						|
	}
 | 
						|
}
 |