From 3d846fcd49c93ee0b09ea60df32c8f9c499f113d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 27 Mar 2017 21:11:51 +0300
Subject: [PATCH] Show inline bot results in a separate widget.

Add a InlineBots::Layout::Widget for inline bot results.
GIF search from EmojiPan is disabled for now.
---
 Telegram/SourceFiles/facades.cpp              |   22 +-
 Telegram/SourceFiles/facades.h                |    5 +-
 Telegram/SourceFiles/historywidget.cpp        |  129 +-
 Telegram/SourceFiles/historywidget.h          |   25 +-
 .../inline_bot_layout_internal.cpp            |   42 +-
 .../inline_bots/inline_bot_layout_internal.h  |   30 +-
 .../inline_bots/inline_bot_layout_item.cpp    |   30 +-
 .../inline_bots/inline_bot_layout_item.h      |   26 +-
 .../inline_bots/inline_bot_result.cpp         |   14 +-
 .../inline_bots/inline_results_widget.cpp     | 1128 +++++++++++++++++
 .../inline_bots/inline_results_widget.h       |  274 ++++
 Telegram/SourceFiles/mainwidget.cpp           |   18 +-
 Telegram/SourceFiles/mainwidget.h             |    5 +-
 Telegram/SourceFiles/settings.h               |    3 -
 Telegram/SourceFiles/stickers/emoji_pan.cpp   |  124 +-
 Telegram/SourceFiles/stickers/emoji_pan.h     |   19 +-
 Telegram/SourceFiles/stickers/stickers.style  |    5 +
 Telegram/SourceFiles/structs.cpp              |    2 +-
 Telegram/gyp/telegram_sources.txt             |    2 +
 19 files changed, 1616 insertions(+), 287 deletions(-)
 create mode 100644 Telegram/SourceFiles/inline_bots/inline_results_widget.cpp
 create mode 100644 Telegram/SourceFiles/inline_bots/inline_results_widget.h

diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp
index 3aef7bc3f..220f5b7ce 100644
--- a/Telegram/SourceFiles/facades.cpp
+++ b/Telegram/SourceFiles/facades.cpp
@@ -54,9 +54,9 @@ void hideSingleUseKeyboard(const HistoryItem *msg) {
 	}
 }
 
-bool insertBotCommand(const QString &cmd, bool specialGif) {
+bool insertBotCommand(const QString &cmd) {
 	if (auto m = main()) {
-		return m->insertBotCommand(cmd, specialGif);
+		return m->insertBotCommand(cmd);
 	}
 	return false;
 }
@@ -264,20 +264,6 @@ void repaintHistoryItem(const HistoryItem *item) {
 	}
 }
 
-void repaintInlineItem(const InlineBots::Layout::ItemBase *layout) {
-	if (!layout) return;
-	if (auto main = App::main()) {
-		main->ui_repaintInlineItem(layout);
-	}
-}
-
-bool isInlineItemVisible(const InlineBots::Layout::ItemBase *layout) {
-	if (auto main = App::main()) {
-		return main->ui_isInlineItemVisible(layout);
-	}
-	return false;
-}
-
 void autoplayMediaInlineAsync(const FullMsgId &msgId) {
 	if (auto main = App::main()) {
 		QMetaObject::invokeMethod(main, "ui_autoplayMediaInlineAsync", Qt::QueuedConnection, Q_ARG(qint32, msgId.channel), Q_ARG(qint32, msgId.msg));
@@ -386,10 +372,6 @@ void historyItemLayoutChanged(const HistoryItem *item) {
 	if (MainWidget *m = App::main()) m->notify_historyItemLayoutChanged(item);
 }
 
-void inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout) {
-	if (MainWidget *m = App::main()) m->notify_inlineItemLayoutChanged(layout);
-}
-
 void historyMuteUpdated(History *history) {
 	if (MainWidget *m = App::main()) m->notify_historyMuteUpdated(history);
 }
diff --git a/Telegram/SourceFiles/facades.h b/Telegram/SourceFiles/facades.h
index 9dfa264ec..b08ceff1b 100644
--- a/Telegram/SourceFiles/facades.h
+++ b/Telegram/SourceFiles/facades.h
@@ -67,7 +67,7 @@ inline base::lambda_once<void()> LambdaDelayedOnce(int duration, PointersAndLamb
 }
 
 void sendBotCommand(PeerData *peer, UserData *bot, const QString &cmd, MsgId replyTo = 0);
-bool insertBotCommand(const QString &cmd, bool specialGif = false);
+bool insertBotCommand(const QString &cmd);
 void activateBotCommand(const HistoryItem *msg, int row, int col);
 void searchByHashtag(const QString &tag, PeerData *inPeer);
 void openPeerByName(const QString &username, MsgId msgId = ShowAtUnreadMsgId, const QString &startToken = QString());
@@ -109,8 +109,6 @@ bool isMediaViewShown();
 bool isInlineItemBeingChosen();
 
 void repaintHistoryItem(const HistoryItem *item);
-void repaintInlineItem(const InlineBots::Layout::ItemBase *layout);
-bool isInlineItemVisible(const InlineBots::Layout::ItemBase *reader);
 void autoplayMediaInlineAsync(const FullMsgId &msgId);
 
 void showPeerProfile(const PeerId &peer);
@@ -180,7 +178,6 @@ void migrateUpdated(PeerData *peer);
 void clipStopperHidden(ClipStopperType type);
 
 void historyItemLayoutChanged(const HistoryItem *item);
-void inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout);
 void historyMuteUpdated(History *history);
 
 // handle pending resize() / paint() on history items
diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp
index 1e46c7ed6..0f15166c5 100644
--- a/Telegram/SourceFiles/historywidget.cpp
+++ b/Telegram/SourceFiles/historywidget.cpp
@@ -63,6 +63,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "auth_session.h"
 #include "window/notifications_manager.h"
 #include "window/window_controller.h"
+#include "inline_bots/inline_results_widget.h"
 
 namespace {
 
@@ -3427,11 +3428,15 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) {
 			_inlineBot = bot;
 			inlineBotChanged();
 		}
-		if (_inlineBot->username == cInlineGifBotUsername() && query.isEmpty()) {
-			_emojiPan->clearInlineBot();
-		} else {
-			_emojiPan->queryInlineBot(_inlineBot, _peer, query);
+		if (!_inlineResults) {
+			_inlineResults.create(this);
+			_inlineResults->setResultSelectedCallback([this](InlineBots::Result *result, UserData *bot) {
+				onInlineResultSend(result, bot);
+			});
+			updateControlsGeometry();
+			orderWidgets();
 		}
+		_inlineResults->queryInlineBot(_inlineBot, _peer, query);
 		if (!_fieldAutocomplete->isHidden()) {
 			_fieldAutocomplete->hideAnimated();
 		}
@@ -3440,6 +3445,20 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) {
 	}
 }
 
+void HistoryWidget::orderWidgets() {
+	_reportSpamPanel->raise();
+	_topShadow->raise();
+	if (_membersDropdown) {
+		_membersDropdown->raise();
+	}
+	if (_inlineResults) {
+		_inlineResults->raise();
+	}
+	_emojiPan->raise();
+	_attachDragDocument->raise();
+	_attachDragPhoto->raise();
+}
+
 void HistoryWidget::updateStickersByEmoji() {
 	int len = 0;
 	if (!_editMsgId) {
@@ -4557,10 +4576,11 @@ void HistoryWidget::updateNotifySettings() {
 }
 
 bool HistoryWidget::contentOverlapped(const QRect &globalRect) {
-	return (_attachDragDocument->overlaps(globalRect) ||
-			_attachDragPhoto->overlaps(globalRect) ||
-			_fieldAutocomplete->overlaps(globalRect) ||
-			_emojiPan->overlaps(globalRect));
+	return (_attachDragDocument->overlaps(globalRect)
+			|| _attachDragPhoto->overlaps(globalRect)
+			|| _fieldAutocomplete->overlaps(globalRect)
+			|| _emojiPan->overlaps(globalRect)
+			|| (_inlineResults && _inlineResults->overlaps(globalRect)));
 }
 
 void HistoryWidget::updateReportSpamStatus() {
@@ -4692,6 +4712,9 @@ void HistoryWidget::updateControlsVisibility() {
 		_botKeyboardHide->hide();
 		_botCommandStart->hide();
 		_emojiPan->hide();
+		if (_inlineResults) {
+			_inlineResults->hide();
+		}
 		if (_pinnedBar) {
 			_pinnedBar->cancel->hide();
 			_pinnedBar->shadow->hide();
@@ -4750,6 +4773,9 @@ void HistoryWidget::updateControlsVisibility() {
 		_botKeyboardHide->hide();
 		_botCommandStart->hide();
 		_emojiPan->hide();
+		if (_inlineResults) {
+			_inlineResults->hide();
+		}
 		if (!_field->isHidden()) {
 			_field->hide();
 			resizeEvent(0);
@@ -4861,6 +4887,9 @@ void HistoryWidget::updateControlsVisibility() {
 		_botKeyboardHide->hide();
 		_botCommandStart->hide();
 		_emojiPan->hide();
+		if (_inlineResults) {
+			_inlineResults->hide();
+		}
 		_kbScroll->hide();
 		if (!_field->isHidden()) {
 			_field->hide();
@@ -5379,6 +5408,9 @@ bool HistoryWidget::saveEditMsgFail(History *history, const RPCError &error, mtp
 void HistoryWidget::hideSelectorControlsAnimated() {
 	_fieldAutocomplete->hideAnimated();
 	_emojiPan->hideAnimated();
+	if (_inlineResults) {
+		_inlineResults->hideAnimated();
+	}
 }
 
 void HistoryWidget::onSend(bool ctrlShiftEnter, MsgId replyTo) {
@@ -5971,7 +6003,7 @@ bool HistoryWidget::botCallbackFail(BotCallbackInfo info, const RPCError &error,
 	return true;
 }
 
-bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) {
+bool HistoryWidget::insertBotCommand(const QString &cmd) {
 	if (!canWriteMessage()) return false;
 
 	bool insertingInlineBot = !cmd.isEmpty() && (cmd.at(0) == '@');
@@ -5989,34 +6021,26 @@ bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) {
 
 	if (!insertingInlineBot) {
 		auto &textWithTags = _field->getTextWithTags();
-		if (specialGif) {
-			if (textWithTags.text.trimmed() == '@' + cInlineGifBotUsername() && textWithTags.text.at(0) == '@') {
-				clearFieldText(TextUpdateEvent::SaveDraft, Ui::FlatTextarea::AddToUndoHistory);
-			}
+		TextWithTags textWithTagsToSet;
+		QRegularExpressionMatch m = QRegularExpression(qsl("^/[A-Za-z_0-9]{0,64}(@[A-Za-z_0-9]{0,32})?(\\s|$)")).match(textWithTags.text);
+		if (m.hasMatch()) {
+			textWithTagsToSet = _field->getTextWithTagsPart(m.capturedLength());
 		} else {
-			TextWithTags textWithTagsToSet;
-			QRegularExpressionMatch m = QRegularExpression(qsl("^/[A-Za-z_0-9]{0,64}(@[A-Za-z_0-9]{0,32})?(\\s|$)")).match(textWithTags.text);
-			if (m.hasMatch()) {
-				textWithTagsToSet = _field->getTextWithTagsPart(m.capturedLength());
-			} else {
-				textWithTagsToSet = textWithTags;
-			}
-			textWithTagsToSet.text = toInsert + textWithTagsToSet.text;
-			for (auto &tag : textWithTagsToSet.tags) {
-				tag.offset += toInsert.size();
-			}
-			_field->setTextWithTags(textWithTagsToSet);
+			textWithTagsToSet = textWithTags;
+		}
+		textWithTagsToSet.text = toInsert + textWithTagsToSet.text;
+		for (auto &tag : textWithTagsToSet.tags) {
+			tag.offset += toInsert.size();
+		}
+		_field->setTextWithTags(textWithTagsToSet);
 
-			QTextCursor cur(_field->textCursor());
-			cur.movePosition(QTextCursor::End);
-			_field->setTextCursor(cur);
-		}
+		QTextCursor cur(_field->textCursor());
+		cur.movePosition(QTextCursor::End);
+		_field->setTextCursor(cur);
 	} else {
-		if (!specialGif || _field->isEmpty()) {
-			setFieldText({ toInsert, TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, Ui::FlatTextarea::AddToUndoHistory);
-			_field->setFocus();
-			return true;
-		}
+		setFieldText({ toInsert, TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, Ui::FlatTextarea::AddToUndoHistory);
+		_field->setFocus();
+		return true;
 	}
 	return false;
 }
@@ -6498,7 +6522,7 @@ void HistoryWidget::moveFieldControls() {
 		_kbScroll->setGeometry(0, bottom, width(), keyboardHeight);
 	}
 
-// _attachToggle --------------------------------------------------------- _emojiPan --------- _fieldBarCancel
+// _attachToggle -------- _inlineResults ---------------------------------- _emojiPan -------- _fieldBarCancel
 // (_attachDocument|_attachPhoto) _field (_silent|_cmdStart|_kbShow) (_kbHide|_attachEmoji) [_broadcast] _send
 // (_botStart|_unblock|_joinChannel|_muteUnmute)
 
@@ -6515,6 +6539,9 @@ void HistoryWidget::moveFieldControls() {
 	_silent->moveToRight(right, buttonsBottom);
 
 	_fieldBarCancel->moveToRight(0, _field->y() - st::historySendPadding - _fieldBarCancel->height());
+	if (_inlineResults) {
+		_inlineResults->moveBottom(_field->y() - st::historySendPadding);
+	}
 	_emojiPan->moveBottom(_field->y() - st::historySendPadding);
 
 	auto fullWidthButtonRect = QRect(0, bottom - _botStart->height(), width(), _botStart->height());
@@ -6546,7 +6573,9 @@ void HistoryWidget::clearInlineBot() {
 		inlineBotChanged();
 		_field->finishPlaceholder();
 	}
-	_emojiPan->clearInlineBot();
+	if (_inlineResults) {
+		_inlineResults->clearInlineBot();
+	}
 	onCheckFieldAutocomplete();
 }
 
@@ -7132,16 +7161,9 @@ void HistoryWidget::onUpdateHistoryItems() {
 	}
 }
 
-void HistoryWidget::ui_repaintInlineItem(const InlineBots::Layout::ItemBase *layout) {
-	_emojiPan->ui_repaintInlineItem(layout);
-}
-
-bool HistoryWidget::ui_isInlineItemVisible(const InlineBots::Layout::ItemBase *layout) {
-	return _emojiPan->ui_isInlineItemVisible(layout);
-}
-
 bool HistoryWidget::ui_isInlineItemBeingChosen() {
-	return _emojiPan->ui_isInlineItemBeingChosen();
+	return _emojiPan->ui_isInlineItemBeingChosen()
+		|| (_inlineResults && _inlineResults->ui_isInlineItemBeingChosen());
 }
 
 PeerData *HistoryWidget::ui_getPeerForMouseAction() {
@@ -7154,10 +7176,6 @@ void HistoryWidget::notify_historyItemLayoutChanged(const HistoryItem *item) {
 	}
 }
 
-void HistoryWidget::notify_inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout) {
-	_emojiPan->notify_inlineItemLayoutChanged(layout);
-}
-
 void HistoryWidget::notify_handlePendingHistoryUpdate() {
 	if (hasPendingResizedItems()) {
 		updateListSize();
@@ -7200,6 +7218,10 @@ void HistoryWidget::updateControlsGeometry() {
 
 	_emojiPan->setMinTop(0);
 	_emojiPan->setMinBottom(_attachEmoji->height());
+	if (_inlineResults) {
+		_inlineResults->setMinTop(0);
+		_inlineResults->setMinBottom(_attachEmoji->height());
+	}
 	if (_membersDropdown) {
 		_membersDropdown->setMaxHeight(countMembersDropdownHeightMax());
 	}
@@ -7776,14 +7798,7 @@ bool HistoryWidget::pinnedMsgVisibilityUpdated() {
 				_pinnedBar->shadow->show();
 			}
 			connect(_pinnedBar->cancel, SIGNAL(clicked()), this, SLOT(onPinnedHide()));
-			_reportSpamPanel->raise();
-			_topShadow->raise();
-			if (_membersDropdown) {
-				_membersDropdown->raise();
-			}
-			_emojiPan->raise();
-			_attachDragDocument->raise();
-			_attachDragPhoto->raise();
+			orderWidgets();
 
 			updatePinnedBar();
 			result = true;
diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h
index c9aaecafe..7bc0c190c 100644
--- a/Telegram/SourceFiles/historywidget.h
+++ b/Telegram/SourceFiles/historywidget.h
@@ -32,6 +32,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 namespace InlineBots {
 namespace Layout {
 class ItemBase;
+class Widget;
 } // namespace Layout
 class Result;
 } // namespace InlineBots
@@ -661,7 +662,7 @@ public:
 
 	void sendBotCommand(PeerData *peer, UserData *bot, const QString &cmd, MsgId replyTo);
 	void hideSingleUseKeyboard(PeerData *peer, MsgId replyTo);
-	bool insertBotCommand(const QString &cmd, bool specialGif);
+	bool insertBotCommand(const QString &cmd);
 
 	bool eventFilter(QObject *obj, QEvent *e) override;
 
@@ -712,13 +713,10 @@ public:
 	void app_sendBotCallback(const HistoryMessageReplyMarkup::Button *button, const HistoryItem *msg, int row, int col);
 
 	void ui_repaintHistoryItem(const HistoryItem *item);
-	void ui_repaintInlineItem(const InlineBots::Layout::ItemBase *gif);
-	bool ui_isInlineItemVisible(const InlineBots::Layout::ItemBase *layout);
 	bool ui_isInlineItemBeingChosen();
 	PeerData *ui_getPeerForMouseAction();
 
 	void notify_historyItemLayoutChanged(const HistoryItem *item);
-	void notify_inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout);
 	void notify_botCommandsChanged(UserData *user);
 	void notify_inlineBotRequesting(bool requesting);
 	void notify_replyMarkupUpdated(const HistoryItem *item);
@@ -848,6 +846,15 @@ private slots:
 	void updateField();
 
 private:
+	struct SendingFilesLists {
+		QList<QUrl> nonLocalUrls;
+		QStringList directories;
+		QStringList emptyFiles;
+		QStringList tooLargeFiles;
+		QStringList filesToSend;
+		bool allFilesForCompress = true;
+	};
+
 	void topBarClick();
 
 	void animationCallback();
@@ -858,14 +865,6 @@ private:
 	void chooseAttach();
 	void historyDownAnimationFinish();
 	void sendButtonClicked();
-	struct SendingFilesLists {
-		QList<QUrl> nonLocalUrls;
-		QStringList directories;
-		QStringList emptyFiles;
-		QStringList tooLargeFiles;
-		QStringList filesToSend;
-		bool allFilesForCompress = true;
-	};
 	SendingFilesLists getSendingFilesLists(const QList<QUrl> &files);
 	SendingFilesLists getSendingFilesLists(const QStringList &files);
 	void getSendingLocalFileInfo(SendingFilesLists &result, const QString &filepath);
@@ -887,6 +886,7 @@ private:
 
 	bool historyHasNotFreezedUnreadBar(History *history) const;
 	bool canWriteMessage() const;
+	void orderWidgets();
 
 	void clearInlineBot();
 	void inlineBotChanged();
@@ -1159,6 +1159,7 @@ private:
 	object_ptr<Ui::InnerDropdown> _membersDropdown = { nullptr };
 	QTimer _membersDropdownShowTimer;
 
+	object_ptr<InlineBots::Layout::Widget> _inlineResults = { nullptr };
 	object_ptr<EmojiPan> _emojiPan;
 	DragState _attachDrag = DragStateNone;
 	object_ptr<DragArea> _attachDragDocument, _attachDragPhoto;
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
index 90ebf7b84..d0316c9da 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
@@ -37,10 +37,10 @@ namespace InlineBots {
 namespace Layout {
 namespace internal {
 
-FileBase::FileBase(Result *result) : ItemBase(result) {
+FileBase::FileBase(gsl::not_null<Context*> context, Result *result) : ItemBase(context, result) {
 }
 
-FileBase::FileBase(DocumentData *document) : ItemBase(document) {
+FileBase::FileBase(gsl::not_null<Context*> context, DocumentData *document) : ItemBase(context, document) {
 }
 
 DocumentData *FileBase::getShownDocument() const {
@@ -94,11 +94,13 @@ ImagePtr FileBase::content_thumb() const {
 	return getResultThumb();
 }
 
-Gif::Gif(Result *result) : FileBase(result) {
+Gif::Gif(gsl::not_null<Context*> context, Result *result) : FileBase(context, result) {
 }
 
-Gif::Gif(DocumentData *document, bool hasDeleteButton) : FileBase(document)
-, _delete(hasDeleteButton ? new DeleteSavedGifClickHandler(document) : nullptr) {
+Gif::Gif(gsl::not_null<Context*> context, DocumentData *document, bool hasDeleteButton) : FileBase(context, document) {
+	if (hasDeleteButton) {
+		_delete = MakeShared<DeleteSavedGifClickHandler>(document);
+	}
 }
 
 void Gif::initDimensions() {
@@ -338,7 +340,7 @@ void Gif::clipCallback(Media::Clip::Notification notification) {
 				int32 height = st::inlineMediaHeight;
 				QSize frame = countFrameSize();
 				_gif->start(frame.width(), frame.height(), _width, height, ImageRoundRadius::None, ImageRoundCorner::None);
-			} else if (_gif->autoPausedGif() && !Ui::isInlineItemVisible(this)) {
+			} else if (_gif->autoPausedGif() && !context()->inlineItemVisible(this)) {
 				_gif.reset();
 				getShownDocument()->forget();
 			}
@@ -355,7 +357,7 @@ void Gif::clipCallback(Media::Clip::Notification notification) {
 	}
 }
 
-Sticker::Sticker(Result *result) : FileBase(result) {
+Sticker::Sticker(gsl::not_null<Context*> context, Result *result) : FileBase(context, result) {
 }
 
 void Sticker::initDimensions() {
@@ -455,7 +457,7 @@ void Sticker::prepareThumb() const {
 	}
 }
 
-Photo::Photo(Result *result) : ItemBase(result) {
+Photo::Photo(gsl::not_null<Context*> context, Result *result) : ItemBase(context, result) {
 }
 
 void Photo::initDimensions() {
@@ -551,7 +553,7 @@ void Photo::prepareThumb(int32 width, int32 height, const QSize &frame) const {
 	}
 }
 
-Video::Video(Result *result) : FileBase(result)
+Video::Video(gsl::not_null<Context*> context, Result *result) : FileBase(context, result)
 , _link(getResultContentUrlHandler())
 , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip)
 , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) {
@@ -668,11 +670,11 @@ void CancelFileClickHandler::onClickImpl() const {
 	_result->cancelFile();
 }
 
-File::File(Result *result) : FileBase(result)
+File::File(gsl::not_null<Context*> context, Result *result) : FileBase(context, result)
 , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::msgFileSize - st::inlineThumbSkip)
 , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::msgFileSize - st::inlineThumbSkip)
-, _open(new OpenFileClickHandler(result))
-, _cancel(new CancelFileClickHandler(result)) {
+, _open(MakeShared<OpenFileClickHandler>(result))
+, _cancel(MakeShared<CancelFileClickHandler>(result)) {
 	updateStatusText();
 	regDocumentItem(getShownDocument(), this);
 }
@@ -789,12 +791,12 @@ File::~File() {
 }
 
 void File::thumbAnimationCallback() {
-	Ui::repaintInlineItem(this);
+	update();
 }
 
 void File::step_radial(TimeMs ms, bool timer) {
 	if (timer) {
-		Ui::repaintInlineItem(this);
+		update();
 	} else {
 		DocumentData *document = getShownDocument();
 		_animation->radial.update(document->progress(), !document->loading() || document->loaded(), ms);
@@ -806,7 +808,7 @@ void File::step_radial(TimeMs ms, bool timer) {
 
 void File::ensureAnimation() const {
 	if (!_animation) {
-		_animation.reset(new AnimationData(animation(const_cast<File*>(this), &File::step_radial)));
+		_animation = std::make_unique<AnimationData>(animation(const_cast<File*>(this), &File::step_radial));
 	}
 }
 
@@ -877,7 +879,7 @@ void File::setStatusSize(int32 newSize, int32 fullSize, int32 duration, qint64 r
 	}
 }
 
-Contact::Contact(Result *result) : ItemBase(result)
+Contact::Contact(gsl::not_null<Context*> context, Result *result) : ItemBase(context, result)
 , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip)
 , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) {
 }
@@ -966,7 +968,7 @@ void Contact::prepareThumb(int width, int height) const {
 	}
 }
 
-Article::Article(Result *result, bool withThumb) : ItemBase(result)
+Article::Article(gsl::not_null<Context*> context, Result *result, bool withThumb) : ItemBase(context, result)
 , _url(getResultUrlHandler())
 , _link(getResultContentUrlHandler())
 , _withThumb(withThumb)
@@ -974,7 +976,7 @@ Article::Article(Result *result, bool withThumb) : ItemBase(result)
 , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) {
 	LocationCoords location;
 	if (!_link && result->getLocationCoords(&location)) {
-		_link.reset(new LocationClickHandler(location));
+		_link = MakeShared<LocationClickHandler>(location);
 	}
 	_thumbLetter = getResultThumbLetter();
 }
@@ -1113,7 +1115,7 @@ void Article::prepareThumb(int width, int height) const {
 	}
 }
 
-Game::Game(Result *result) : ItemBase(result)
+Game::Game(gsl::not_null<Context*> context, Result *result) : ItemBase(context, result)
 , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip)
 , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) {
 	countFrameSize();
@@ -1322,7 +1324,7 @@ void Game::clipCallback(Media::Clip::Notification notification) {
 				getResultDocument()->forget();
 			} else if (_gif->ready() && !_gif->started()) {
 				_gif->start(_frameSize.width(), _frameSize.height(), st::inlineThumbSize, st::inlineThumbSize, ImageRoundRadius::None, ImageRoundCorner::None);
-			} else if (_gif->autoPausedGif() && !Ui::isInlineItemVisible(this)) {
+			} else if (_gif->autoPausedGif() && !context()->inlineItemVisible(this)) {
 				_gif.reset();
 				getResultDocument()->forget();
 			}
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h
index 0f19325dd..7685fdb2e 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h
@@ -30,9 +30,9 @@ namespace internal {
 
 class FileBase : public ItemBase {
 public:
-	FileBase(Result *result);
+	FileBase(gsl::not_null<Context*> context, Result *result);
 	// for saved gif layouts
-	FileBase(DocumentData *doc);
+	FileBase(gsl::not_null<Context*> context, DocumentData *doc);
 
 protected:
 	DocumentData *getShownDocument() const;
@@ -58,8 +58,8 @@ private:
 
 class Gif : public FileBase {
 public:
-	Gif(Result *result);
-	Gif(DocumentData *doc, bool hasDeleteButton);
+	Gif(gsl::not_null<Context*> context, Result *result);
+	Gif(gsl::not_null<Context*> context, DocumentData *doc, bool hasDeleteButton);
 
 	void setPosition(int32 position) override;
 	void initDimensions() override;
@@ -117,9 +117,9 @@ private:
 
 class Photo : public ItemBase {
 public:
-	Photo(Result *result);
+	Photo(gsl::not_null<Context*> context, Result *result);
 	// Not used anywhere currently.
-	//LayoutInlinePhoto(PhotoData *photo);
+	//Photo(gsl::not_null<Context*> context, PhotoData *photo);
 
 	void initDimensions() override;
 
@@ -146,9 +146,9 @@ private:
 
 class Sticker : public FileBase {
 public:
-	Sticker(Result *result);
+	Sticker(gsl::not_null<Context*> context, Result *result);
 	// Not used anywhere currently.
-	//LayoutInlineSticker(DocumentData *document);
+	//Sticker(gsl::not_null<Context*> context, DocumentData *document);
 
 	void initDimensions() override;
 
@@ -167,7 +167,6 @@ public:
 	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
 
 private:
-
 	QSize getThumbSize() const;
 
 	mutable Animation _a_over;
@@ -181,7 +180,7 @@ private:
 
 class Video : public FileBase {
 public:
-	Video(Result *result);
+	Video(gsl::not_null<Context*> context, Result *result);
 
 	void initDimensions() override;
 
@@ -189,7 +188,6 @@ public:
 	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int x, int y) const override;
 
 private:
-
 	ClickHandlerPtr _link;
 
 	mutable QPixmap _thumb;
@@ -229,7 +227,7 @@ private:
 
 class File : public FileBase {
 public:
-	File(Result *result);
+	File(gsl::not_null<Context*> context, Result *result);
 
 	void initDimensions() override;
 
@@ -291,7 +289,7 @@ private:
 
 class Contact : public ItemBase {
 public:
-	Contact(Result *result);
+	Contact(gsl::not_null<Context*> context, Result *result);
 
 	void initDimensions() override;
 	int resizeGetHeight(int width) override;
@@ -300,7 +298,6 @@ public:
 	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int x, int y) const override;
 
 private:
-
 	mutable QPixmap _thumb;
 	Text _title, _description;
 
@@ -310,7 +307,7 @@ private:
 
 class Article : public ItemBase {
 public:
-	Article(Result *result, bool withThumb);
+	Article(gsl::not_null<Context*> context, Result *result, bool withThumb);
 
 	void initDimensions() override;
 	int resizeGetHeight(int width) override;
@@ -319,7 +316,6 @@ public:
 	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int x, int y) const override;
 
 private:
-
 	ClickHandlerPtr _url, _link;
 
 	bool _withThumb;
@@ -334,7 +330,7 @@ private:
 
 class Game : public ItemBase {
 public:
-	Game(Result *result);
+	Game(gsl::not_null<Context*> context, Result *result);
 
 	void setPosition(int32 position) override;
 	void initDimensions() override;
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp
index d1a003c9c..f79ef4e0c 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp
@@ -95,31 +95,37 @@ void ItemBase::preload() const {
 
 void ItemBase::update() {
 	if (_position >= 0) {
-		Ui::repaintInlineItem(this);
+		context()->inlineItemRepaint(this);
 	}
 }
 
-std::unique_ptr<ItemBase> ItemBase::createLayout(Result *result, bool forceThumb) {
+void ItemBase::layoutChanged() {
+	if (_position >= 0) {
+		context()->inlineItemLayoutChanged(this);
+	}
+}
+
+std::unique_ptr<ItemBase> ItemBase::createLayout(gsl::not_null<Context*> context, Result *result, bool forceThumb) {
 	using Type = Result::Type;
 
 	switch (result->_type) {
-	case Type::Photo: return std::make_unique<internal::Photo>(result); break;
+	case Type::Photo: return std::make_unique<internal::Photo>(context, result); break;
 	case Type::Audio:
-	case Type::File: return std::make_unique<internal::File>(result); break;
-	case Type::Video: return std::make_unique<internal::Video>(result); break;
-	case Type::Sticker: return std::make_unique<internal::Sticker>(result); break;
-	case Type::Gif: return std::make_unique<internal::Gif>(result); break;
+	case Type::File: return std::make_unique<internal::File>(context, result); break;
+	case Type::Video: return std::make_unique<internal::Video>(context, result); break;
+	case Type::Sticker: return std::make_unique<internal::Sticker>(context, result); break;
+	case Type::Gif: return std::make_unique<internal::Gif>(context, result); break;
 	case Type::Article:
 	case Type::Geo:
-	case Type::Venue: return std::make_unique<internal::Article>(result, forceThumb); break;
-	case Type::Game: return std::make_unique<internal::Game>(result); break;
-	case Type::Contact: return std::make_unique<internal::Contact>(result); break;
+	case Type::Venue: return std::make_unique<internal::Article>(context, result, forceThumb); break;
+	case Type::Game: return std::make_unique<internal::Game>(context, result); break;
+	case Type::Contact: return std::make_unique<internal::Contact>(context, result); break;
 	}
 	return std::unique_ptr<ItemBase>();
 }
 
-std::unique_ptr<ItemBase> ItemBase::createLayoutGif(DocumentData *document) {
-	return std::make_unique<internal::Gif>(document, true);
+std::unique_ptr<ItemBase> ItemBase::createLayoutGif(gsl::not_null<Context*> context, DocumentData *document) {
+	return std::make_unique<internal::Gif>(context, document, true);
 }
 
 DocumentData *ItemBase::getResultDocument() const {
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h
index c284bac06..810405c93 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h
@@ -47,15 +47,21 @@ public:
 	}
 };
 
+class Context {
+public:
+	virtual void inlineItemLayoutChanged(const ItemBase *layout) = 0;
+	virtual bool inlineItemVisible(const ItemBase *item) = 0;
+	virtual void inlineItemRepaint(const ItemBase *item) = 0;
+};
+
 class ItemBase : public LayoutItemBase {
 public:
-
-	ItemBase(Result *result) : _result(result) {
+	ItemBase(gsl::not_null<Context*> context, Result *result) : _context(context), _result(result) {
 	}
-	ItemBase(DocumentData *doc) : _doc(doc) {
+	ItemBase(gsl::not_null<Context*> context, DocumentData *doc) : _context(context), _doc(doc) {
 	}
 	// Not used anywhere currently.
-	//ItemBase(PhotoData *photo) : _photo(photo) {
+	//ItemBase(gsl::not_null<Context*> context, PhotoData *photo) : _context(context), _photo(photo) {
 	//}
 
 	virtual void paint(Painter &p, const QRect &clip, const PaintContext *context) const = 0;
@@ -82,6 +88,7 @@ public:
 	virtual void preload() const;
 
 	void update();
+	void layoutChanged();
 
 	// ClickHandlerHost interface
 	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override {
@@ -91,8 +98,8 @@ public:
 		update();
 	}
 
-	static std::unique_ptr<ItemBase> createLayout(Result *result, bool forceThumb);
-	static std::unique_ptr<ItemBase> createLayoutGif(DocumentData *document);
+	static std::unique_ptr<ItemBase> createLayout(gsl::not_null<Context*> context, Result *result, bool forceThumb);
+	static std::unique_ptr<ItemBase> createLayoutGif(gsl::not_null<Context*> context, DocumentData *document);
 
 protected:
 	DocumentData *getResultDocument() const;
@@ -105,6 +112,10 @@ protected:
 	ClickHandlerPtr getResultContentUrlHandler() const;
 	QString getResultThumbLetter() const;
 
+	gsl::not_null<Context*> context() const {
+		return _context;
+	}
+
 	Result *_result = nullptr;
 	DocumentData *_doc = nullptr;
 	PhotoData *_photo = nullptr;
@@ -113,6 +124,9 @@ protected:
 
 	int _position = 0; // < 0 means removed from layout
 
+private:
+	gsl::not_null<Context*> _context;
+
 };
 
 using DocumentItems = QMap<DocumentData*, OrderedSet<ItemBase*>>;
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp
index 152229340..91c320be2 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp
@@ -121,13 +121,13 @@ std::unique_ptr<Result> Result::create(uint64 queryId, const MTPBotInlineResult
 		auto &r = message->c_botInlineMessageMediaAuto();
 		if (result->_type == Type::Photo) {
 			result->createPhoto();
-			result->sendData.reset(new internal::SendPhoto(result->_photo, qs(r.vcaption)));
+			result->sendData = std::make_unique<internal::SendPhoto>(result->_photo, qs(r.vcaption));
 		} else if (result->_type == Type::Game) {
 			result->createGame();
-			result->sendData.reset(new internal::SendGame(result->_game));
+			result->sendData = std::make_unique<internal::SendGame>(result->_game);
 		} else {
 			result->createDocument();
-			result->sendData.reset(new internal::SendFile(result->_document, qs(r.vcaption)));
+			result->sendData = std::make_unique<internal::SendFile>(result->_document, qs(r.vcaption));
 		}
 		if (r.has_reply_markup()) {
 			result->_mtpKeyboard = std::make_unique<MTPReplyMarkup>(r.vreply_markup);
@@ -137,7 +137,7 @@ std::unique_ptr<Result> Result::create(uint64 queryId, const MTPBotInlineResult
 	case mtpc_botInlineMessageText: {
 		auto &r = message->c_botInlineMessageText();
 		auto entities = r.has_entities() ? entitiesFromMTP(r.ventities.v) : EntitiesInText();
-		result->sendData.reset(new internal::SendText(qs(r.vmessage), entities, r.is_no_webpage()));
+		result->sendData = std::make_unique<internal::SendText>(qs(r.vmessage), entities, r.is_no_webpage());
 		if (result->_type == Type::Photo) {
 			result->createPhoto();
 		} else if (result->_type == Type::Audio || result->_type == Type::File || result->_type == Type::Video || result->_type == Type::Sticker || result->_type == Type::Gif) {
@@ -151,7 +151,7 @@ std::unique_ptr<Result> Result::create(uint64 queryId, const MTPBotInlineResult
 	case mtpc_botInlineMessageMediaGeo: {
 		auto &r = message->c_botInlineMessageMediaGeo();
 		if (r.vgeo.type() == mtpc_geoPoint) {
-			result->sendData.reset(new internal::SendGeo(r.vgeo.c_geoPoint()));
+			result->sendData = std::make_unique<internal::SendGeo>(r.vgeo.c_geoPoint());
 		} else {
 			badAttachment = true;
 		}
@@ -163,7 +163,7 @@ std::unique_ptr<Result> Result::create(uint64 queryId, const MTPBotInlineResult
 	case mtpc_botInlineMessageMediaVenue: {
 		auto &r = message->c_botInlineMessageMediaVenue();
 		if (r.vgeo.type() == mtpc_geoPoint) {
-			result->sendData.reset(new internal::SendVenue(r.vgeo.c_geoPoint(), qs(r.vvenue_id), qs(r.vprovider), qs(r.vtitle), qs(r.vaddress)));
+			result->sendData = std::make_unique<internal::SendVenue>(r.vgeo.c_geoPoint(), qs(r.vvenue_id), qs(r.vprovider), qs(r.vtitle), qs(r.vaddress));
 		} else {
 			badAttachment = true;
 		}
@@ -174,7 +174,7 @@ std::unique_ptr<Result> Result::create(uint64 queryId, const MTPBotInlineResult
 
 	case mtpc_botInlineMessageMediaContact: {
 		auto &r = message->c_botInlineMessageMediaContact();
-		result->sendData.reset(new internal::SendContact(qs(r.vfirst_name), qs(r.vlast_name), qs(r.vphone_number)));
+		result->sendData = std::make_unique<internal::SendContact>(qs(r.vfirst_name), qs(r.vlast_name), qs(r.vphone_number));
 		if (r.has_reply_markup()) {
 			result->_mtpKeyboard = std::make_unique<MTPReplyMarkup>(r.vreply_markup);
 		}
diff --git a/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp b/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp
new file mode 100644
index 000000000..be7ec30d7
--- /dev/null
+++ b/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp
@@ -0,0 +1,1128 @@
+/*
+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 "inline_bots/inline_results_widget.h"
+
+#include "styles/style_stickers.h"
+#include "ui/widgets/buttons.h"
+#include "ui/widgets/shadow.h"
+#include "ui/effects/ripple_animation.h"
+#include "boxes/confirmbox.h"
+#include "inline_bots/inline_bot_result.h"
+#include "inline_bots/inline_bot_layout_item.h"
+#include "dialogs/dialogs_layout.h"
+#include "historywidget.h"
+#include "storage/localstorage.h"
+#include "lang.h"
+#include "mainwindow.h"
+#include "apiwrap.h"
+#include "mainwidget.h"
+#include "auth_session.h"
+
+namespace InlineBots {
+namespace Layout {
+namespace internal {
+
+Inner::Inner(QWidget *parent) : TWidget(parent) {
+	setMaxHeight(st::emojiPanMaxHeight - st::emojiCategory.height);
+
+	setMouseTracking(true);
+	setAttribute(Qt::WA_OpaquePaintEvent);
+
+	_previewTimer.setSingleShot(true);
+	connect(&_previewTimer, SIGNAL(timeout()), this, SLOT(onPreview()));
+
+	_updateInlineItems.setSingleShot(true);
+	connect(&_updateInlineItems, SIGNAL(timeout()), this, SLOT(onUpdateInlineItems()));
+
+	subscribe(AuthSession::CurrentDownloaderTaskFinished(), [this] {
+		update();
+	});
+}
+
+void Inner::setMaxHeight(int maxHeight) {
+	_maxHeight = maxHeight;
+	resize(st::emojiPanWidth - st::emojiScroll.width - st::buttonRadius, countHeight());
+}
+
+void Inner::setVisibleTopBottom(int visibleTop, int visibleBottom) {
+	_visibleBottom = visibleBottom;
+	if (_visibleTop != visibleTop) {
+		_visibleTop = visibleTop;
+		_lastScrolled = getms();
+	}
+}
+
+int Inner::countHeight(bool plain) {
+	auto result = st::stickerPanPadding;
+	auto minLastH = plain ? 0 : (_maxHeight - st::stickerPanPadding);
+	if (_switchPmButton) {
+		result += _switchPmButton->height() + st::inlineResultsSkip;
+	}
+	for (int i = 0, l = _rows.count(); i < l; ++i) {
+		result += _rows[i].height;
+	}
+	return qMax(minLastH, result) + st::stickerPanPadding;
+}
+
+Inner::~Inner() = default;
+
+void Inner::paintEvent(QPaintEvent *e) {
+	Painter p(this);
+	QRect r = e ? e->rect() : rect();
+	if (r != rect()) {
+		p.setClipRect(r);
+	}
+	p.fillRect(r, st::emojiPanBg);
+
+	paintInlineItems(p, r);
+}
+
+void Inner::paintInlineItems(Painter &p, const QRect &r) {
+	if (_rows.isEmpty() && !_switchPmButton) {
+		p.setFont(st::normalFont);
+		p.setPen(st::noContactsColor);
+		p.drawText(QRect(0, 0, width(), (height() / 3) * 2 + st::normalFont->height), lang(lng_inline_bot_no_results), style::al_center);
+		return;
+	}
+	auto gifPaused = Ui::isLayerShown() || Ui::isMediaViewShown() || _previewShown || !App::wnd()->isActive();
+	InlineBots::Layout::PaintContext context(getms(), false, gifPaused, false);
+
+	auto top = st::stickerPanPadding;
+	if (_switchPmButton) {
+		top += _switchPmButton->height() + st::inlineResultsSkip;
+	}
+
+	auto fromx = rtl() ? (width() - r.x() - r.width()) : r.x();
+	auto tox = rtl() ? (width() - r.x()) : (r.x() + r.width());
+	for (auto row = 0, rows = _rows.size(); row != rows; ++row) {
+		auto &inlineRow = _rows[row];
+		if (top >= r.top() + r.height()) break;
+		if (top + inlineRow.height > r.top()) {
+			auto left = st::inlineResultsLeft - st::buttonRadius;
+			if (row == rows - 1) context.lastRow = true;
+			for (int col = 0, cols = inlineRow.items.size(); col < cols; ++col) {
+				if (left >= tox) break;
+
+				auto item = inlineRow.items.at(col);
+				auto w = item->width();
+				if (left + w > fromx) {
+					p.translate(left, top);
+					item->paint(p, r.translated(-left, -top), &context);
+					p.translate(-left, -top);
+				}
+				left += w;
+				if (item->hasRightSkip()) {
+					left += st::inlineResultsSkip;
+				}
+			}
+		}
+		top += inlineRow.height;
+	}
+}
+
+void Inner::mousePressEvent(QMouseEvent *e) {
+	if (e->button() != Qt::LeftButton) {
+		return;
+	}
+	_lastMousePos = e->globalPos();
+	updateSelected();
+
+	_pressed = _selected;
+	ClickHandler::pressed();
+	_previewTimer.start(QApplication::startDragTime());
+}
+
+void Inner::mouseReleaseEvent(QMouseEvent *e) {
+	_previewTimer.stop();
+
+	auto pressed = std::exchange(_pressed, -1);
+	auto activated = ClickHandler::unpressed();
+
+	if (_previewShown) {
+		_previewShown = false;
+		return;
+	}
+
+	_lastMousePos = e->globalPos();
+	updateSelected();
+
+	if (_selected < 0 || _selected != pressed || !activated) {
+		return;
+	}
+
+	if (dynamic_cast<InlineBots::Layout::SendClickHandler*>(activated.data())) {
+		int row = _selected / MatrixRowShift, column = _selected % MatrixRowShift;
+		selectInlineResult(row, column);
+	} else {
+		App::activateClickHandler(activated, e->button());
+	}
+}
+
+void Inner::selectInlineResult(int row, int column) {
+	if (row >= _rows.size() || column >= _rows.at(row).items.size()) {
+		return;
+	}
+
+	auto item = _rows[row].items[column];
+	if (auto inlineResult = item->getResult()) {
+		if (inlineResult->onChoose(item)) {
+			_resultSelectedCallback(inlineResult, _inlineBot);
+		}
+	}
+}
+
+void Inner::mouseMoveEvent(QMouseEvent *e) {
+	_lastMousePos = e->globalPos();
+	updateSelected();
+}
+
+void Inner::leaveEventHook(QEvent *e) {
+	clearSelection();
+}
+
+void Inner::leaveToChildEvent(QEvent *e, QWidget *child) {
+	clearSelection();
+}
+
+void Inner::enterFromChildEvent(QEvent *e, QWidget *child) {
+	_lastMousePos = QCursor::pos();
+	updateSelected();
+}
+
+void Inner::clearSelection() {
+	if (_selected >= 0) {
+		int srow = _selected / MatrixRowShift, scol = _selected % MatrixRowShift;
+		t_assert(srow >= 0 && srow < _rows.size() && scol >= 0 && scol < _rows.at(srow).items.size());
+		ClickHandler::clearActive(_rows.at(srow).items.at(scol));
+		setCursor(style::cur_default);
+	}
+	_selected = _pressed = -1;
+	update();
+}
+
+void Inner::hideFinish(bool completely) {
+	if (completely) {
+		auto itemForget = [](auto &item) {
+			if (auto document = item->getDocument()) {
+				document->forget();
+			}
+			if (auto photo = item->getPhoto()) {
+				photo->forget();
+			}
+			if (auto result = item->getResult()) {
+				result->forget();
+			}
+		};
+		clearInlineRows(false);
+		for_const (auto &item, _inlineLayouts) {
+			itemForget(item.second);
+		}
+	}
+}
+
+bool Inner::inlineRowsAddItem(Result *result, Row &row, int32 &sumWidth) {
+	auto layout = layoutPrepareInlineResult(result, (_rows.size() * MatrixRowShift) + row.items.size());
+	if (!layout) return false;
+
+	layout->preload();
+	if (inlineRowFinalize(row, sumWidth, layout->isFullLine())) {
+		layout->setPosition(_rows.size() * MatrixRowShift);
+	}
+
+	sumWidth += layout->maxWidth();
+	if (!row.items.isEmpty() && row.items.back()->hasRightSkip()) {
+		sumWidth += st::inlineResultsSkip;
+	}
+
+	row.items.push_back(layout);
+	return true;
+}
+
+bool Inner::inlineRowFinalize(Row &row, int32 &sumWidth, bool force) {
+	if (row.items.isEmpty()) return false;
+
+	auto full = (row.items.size() >= kInlineItemsMaxPerRow);
+	auto big = (sumWidth >= st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft);
+	if (full || big || force) {
+		_rows.push_back(layoutInlineRow(row, (full || big) ? sumWidth : 0));
+		row = Row();
+		row.items.reserve(kInlineItemsMaxPerRow);
+		sumWidth = 0;
+		return true;
+	}
+	return false;
+}
+
+void Inner::inlineBotChanged() {
+	refreshInlineRows(nullptr, nullptr, true);
+}
+
+void Inner::clearInlineRows(bool resultsDeleted) {
+	if (resultsDeleted) {
+		_selected = _pressed = -1;
+	} else {
+		clearSelection();
+		for_const (auto &row, _rows) {
+			for_const (auto &item, row.items) {
+				item->setPosition(-1);
+			}
+		}
+	}
+	_rows.clear();
+}
+
+ItemBase *Inner::layoutPrepareInlineResult(Result *result, int32 position) {
+	auto it = _inlineLayouts.find(result);
+	if (it == _inlineLayouts.cend()) {
+		if (auto layout = ItemBase::createLayout(this, result, _inlineWithThumb)) {
+			it = _inlineLayouts.emplace(result, std::move(layout)).first;
+			it->second->initDimensions();
+		} else {
+			return nullptr;
+		}
+	}
+	if (!it->second->maxWidth()) {
+		return nullptr;
+	}
+
+	it->second->setPosition(position);
+	return it->second.get();
+}
+
+void Inner::deleteUnusedInlineLayouts() {
+	if (_rows.isEmpty()) { // delete all
+		_inlineLayouts.clear();
+	} else {
+		for (auto i = _inlineLayouts.begin(); i != _inlineLayouts.cend();) {
+			if (i->second->position() < 0) {
+				i = _inlineLayouts.erase(i);
+			} else {
+				++i;
+			}
+		}
+	}
+}
+
+Inner::Row &Inner::layoutInlineRow(Row &row, int32 sumWidth) {
+	auto count = int(row.items.size());
+	t_assert(count <= kInlineItemsMaxPerRow);
+
+	// enumerate items in the order of growing maxWidth()
+	// for that sort item indices by maxWidth()
+	int indices[kInlineItemsMaxPerRow];
+	for (auto i = 0; i != count; ++i) {
+		indices[i] = i;
+	}
+	std::sort(indices, indices + count, [&row](int a, int b) -> bool {
+		return row.items.at(a)->maxWidth() < row.items.at(b)->maxWidth();
+	});
+
+	row.height = 0;
+	int availw = width() - (st::inlineResultsLeft - st::buttonRadius);
+	for (int i = 0; i < count; ++i) {
+		int index = indices[i];
+		int w = sumWidth ? (row.items.at(index)->maxWidth() * availw / sumWidth) : row.items.at(index)->maxWidth();
+		int actualw = qMax(w, int(st::inlineResultsMinWidth));
+		row.height = qMax(row.height, row.items.at(index)->resizeGetHeight(actualw));
+		if (sumWidth) {
+			availw -= actualw;
+			sumWidth -= row.items.at(index)->maxWidth();
+			if (index > 0 && row.items.at(index - 1)->hasRightSkip()) {
+				availw -= st::inlineResultsSkip;
+				sumWidth -= st::inlineResultsSkip;
+			}
+		}
+	}
+	return row;
+}
+
+void Inner::preloadImages() {
+	for (auto row = 0, rows = _rows.size(); row != rows; ++row) {
+		for (auto col = 0, cols = _rows[row].items.size(); col != cols; ++col) {
+			_rows[row].items[col]->preload();
+		}
+	}
+}
+
+void Inner::hideInlineRowsPanel() {
+	clearInlineRows(false);
+}
+
+void Inner::clearInlineRowsPanel() {
+	clearInlineRows(false);
+}
+
+void Inner::refreshSwitchPmButton(const CacheEntry *entry) {
+	if (!entry || entry->switchPmText.isEmpty()) {
+		_switchPmButton.destroy();
+		_switchPmStartToken.clear();
+	} else {
+		if (!_switchPmButton) {
+			_switchPmButton.create(this, QString(), st::switchPmButton);
+			_switchPmButton->show();
+			_switchPmButton->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
+			connect(_switchPmButton, SIGNAL(clicked()), this, SLOT(onSwitchPm()));
+		}
+		_switchPmButton->setText(entry->switchPmText); // doesn't perform text.toUpper()
+		_switchPmStartToken = entry->switchPmStartToken;
+		auto buttonTop = st::stickerPanPadding;
+		_switchPmButton->move(st::inlineResultsLeft - st::buttonRadius, buttonTop);
+	}
+	update();
+}
+
+int Inner::refreshInlineRows(UserData *bot, const CacheEntry *entry, bool resultsDeleted) {
+	_inlineBot = bot;
+	refreshSwitchPmButton(entry);
+	auto clearResults = [this, entry]() {
+		if (!entry) {
+			return true;
+		}
+		if (entry->results.empty() && entry->switchPmText.isEmpty()) {
+			return true;
+		}
+		return false;
+	};
+	if (clearResults()) {
+		if (resultsDeleted) {
+			clearInlineRows(true);
+			deleteUnusedInlineLayouts();
+		}
+		emit emptyInlineRows();
+		return 0;
+	}
+
+	clearSelection();
+
+	t_assert(_inlineBot != 0);
+	_inlineBotTitle = lng_inline_bot_results(lt_inline_bot, _inlineBot->username.isEmpty() ? _inlineBot->name : ('@' + _inlineBot->username));
+
+	auto count = int(entry->results.size());
+	auto from = validateExistingInlineRows(entry->results);
+	auto added = 0;
+
+	if (count) {
+		_rows.reserve(count);
+		auto row = Row();
+		row.items.reserve(kInlineItemsMaxPerRow);
+		auto sumWidth = 0;
+		for (auto i = from; i != count; ++i) {
+			if (inlineRowsAddItem(entry->results[i].get(), row, sumWidth)) {
+				++added;
+			}
+		}
+		inlineRowFinalize(row, sumWidth, true);
+	}
+
+	int32 h = countHeight();
+	if (h != height()) resize(width(), h);
+	update();
+
+	_lastMousePos = QCursor::pos();
+	updateSelected();
+
+	return added;
+}
+
+int Inner::validateExistingInlineRows(const Results &results) {
+	int count = results.size(), until = 0, untilrow = 0, untilcol = 0;
+	for (; until < count;) {
+		if (untilrow >= _rows.size() || _rows[untilrow].items[untilcol]->getResult() != results[until].get()) {
+			break;
+		}
+		++until;
+		if (++untilcol == _rows[untilrow].items.size()) {
+			++untilrow;
+			untilcol = 0;
+		}
+	}
+	if (until == count) { // all items are layed out
+		if (untilrow == _rows.size()) { // nothing changed
+			return until;
+		}
+
+		for (int i = untilrow, l = _rows.size(), skip = untilcol; i < l; ++i) {
+			for (int j = 0, s = _rows[i].items.size(); j < s; ++j) {
+				if (skip) {
+					--skip;
+				} else {
+					_rows[i].items[j]->setPosition(-1);
+				}
+			}
+		}
+		if (!untilcol) { // all good rows are filled
+			_rows.resize(untilrow);
+			return until;
+		}
+		_rows.resize(untilrow + 1);
+		_rows[untilrow].items.resize(untilcol);
+		_rows[untilrow] = layoutInlineRow(_rows[untilrow]);
+		return until;
+	}
+	if (untilrow && !untilcol) { // remove last row, maybe it is not full
+		--untilrow;
+		untilcol = _rows[untilrow].items.size();
+	}
+	until -= untilcol;
+
+	for (int i = untilrow, l = _rows.size(); i < l; ++i) {
+		for (int j = 0, s = _rows[i].items.size(); j < s; ++j) {
+			_rows[i].items[j]->setPosition(-1);
+		}
+	}
+	_rows.resize(untilrow);
+
+	if (_rows.isEmpty()) {
+		_inlineWithThumb = false;
+		for (int i = until; i < count; ++i) {
+			if (results.at(i)->hasThumbDisplay()) {
+				_inlineWithThumb = true;
+				break;
+			}
+		}
+	}
+	return until;
+}
+
+void Inner::inlineItemLayoutChanged(const ItemBase *layout) {
+	if (_selected < 0 || !isVisible()) {
+		return;
+	}
+
+	int row = _selected / MatrixRowShift, col = _selected % MatrixRowShift;
+	if (row < _rows.size() && col < _rows.at(row).items.size()) {
+		if (layout == _rows.at(row).items.at(col)) {
+			updateSelected();
+		}
+	}
+}
+
+void Inner::inlineItemRepaint(const ItemBase *layout) {
+	auto ms = getms();
+	if (_lastScrolled + 100 <= ms) {
+		update();
+	} else {
+		_updateInlineItems.start(_lastScrolled + 100 - ms);
+	}
+}
+
+bool Inner::inlineItemVisible(const ItemBase *layout) {
+	int32 position = layout->position();
+	if (position < 0 || !isVisible()) {
+		return false;
+	}
+
+	int row = position / MatrixRowShift, col = position % MatrixRowShift;
+	t_assert((row < _rows.size()) && (col < _rows[row].items.size()));
+
+	auto &inlineItems = _rows[row].items;
+	int top = st::stickerPanPadding;
+	for (int32 i = 0; i < row; ++i) {
+		top += _rows.at(i).height;
+	}
+
+	return (top < _visibleTop + _maxHeight) && (top + _rows[row].items[col]->height() > _visibleTop);
+}
+
+bool Inner::ui_isInlineItemBeingChosen() {
+	return true;
+}
+
+void Inner::updateSelected() {
+	if (_pressed >= 0 && !_previewShown) {
+		return;
+	}
+
+	auto newSelected = -1;
+	auto p = mapFromGlobal(_lastMousePos);
+
+	int sx = (rtl() ? width() - p.x() : p.x()) - (st::inlineResultsLeft - st::buttonRadius);
+	int sy = p.y() - st::stickerPanPadding;
+	if (_switchPmButton) {
+		sy -= _switchPmButton->height() + st::inlineResultsSkip;
+	}
+	int row = -1, col = -1, sel = -1;
+	ClickHandlerPtr lnk;
+	ClickHandlerHost *lnkhost = nullptr;
+	HistoryCursorState cursor = HistoryDefaultCursorState;
+	if (sy >= 0) {
+		row = 0;
+		for (int rows = _rows.size(); row < rows; ++row) {
+			if (sy < _rows.at(row).height) {
+				break;
+			}
+			sy -= _rows.at(row).height;
+		}
+	}
+	if (sx >= 0 && row >= 0 && row < _rows.size()) {
+		auto &inlineItems = _rows[row].items;
+		col = 0;
+		for (int cols = inlineItems.size(); col < cols; ++col) {
+			int width = inlineItems.at(col)->width();
+			if (sx < width) {
+				break;
+			}
+			sx -= width;
+			if (inlineItems.at(col)->hasRightSkip()) {
+				sx -= st::inlineResultsSkip;
+			}
+		}
+		if (col < inlineItems.size()) {
+			sel = row * MatrixRowShift + col;
+			inlineItems.at(col)->getState(lnk, cursor, sx, sy);
+			lnkhost = inlineItems.at(col);
+		} else {
+			row = col = -1;
+		}
+	} else {
+		row = col = -1;
+	}
+	int srow = (_selected >= 0) ? (_selected / MatrixRowShift) : -1;
+	int scol = (_selected >= 0) ? (_selected % MatrixRowShift) : -1;
+	if (_selected != sel) {
+		if (srow >= 0 && scol >= 0) {
+			t_assert(srow >= 0 && srow < _rows.size() && scol >= 0 && scol < _rows.at(srow).items.size());
+			_rows[srow].items[scol]->update();
+		}
+		_selected = sel;
+		if (row >= 0 && col >= 0) {
+			t_assert(row >= 0 && row < _rows.size() && col >= 0 && col < _rows.at(row).items.size());
+			_rows[row].items[col]->update();
+		}
+		if (_previewShown && _selected >= 0 && _pressed != _selected) {
+			_pressed = _selected;
+			if (row >= 0 && col >= 0) {
+				auto layout = _rows.at(row).items.at(col);
+				if (auto previewDocument = layout->getPreviewDocument()) {
+					Ui::showMediaPreview(previewDocument);
+				} else if (auto previewPhoto = layout->getPreviewPhoto()) {
+					Ui::showMediaPreview(previewPhoto);
+				}
+			}
+		}
+	}
+	if (ClickHandler::setActive(lnk, lnkhost)) {
+		setCursor(lnk ? style::cur_pointer : style::cur_default);
+	}
+}
+
+void Inner::onPreview() {
+	if (_pressed < 0) return;
+
+	int row = _pressed / MatrixRowShift, col = _pressed % MatrixRowShift;
+	if (row < _rows.size() && col < _rows.at(row).items.size()) {
+		auto layout = _rows.at(row).items.at(col);
+		if (auto previewDocument = layout->getPreviewDocument()) {
+			Ui::showMediaPreview(previewDocument);
+			_previewShown = true;
+		} else if (auto previewPhoto = layout->getPreviewPhoto()) {
+			Ui::showMediaPreview(previewPhoto);
+			_previewShown = true;
+		}
+	}
+}
+
+void Inner::onUpdateInlineItems() {
+	auto ms = getms();
+	if (_lastScrolled + 100 <= ms) {
+		update();
+	} else {
+		_updateInlineItems.start(_lastScrolled + 100 - ms);
+	}
+}
+
+void Inner::onSwitchPm() {
+	if (_inlineBot && _inlineBot->botInfo) {
+		_inlineBot->botInfo->startToken = _switchPmStartToken;
+		Ui::showPeerHistory(_inlineBot, ShowAndStartBotMsgId);
+	}
+}
+
+} // namespace internal
+
+Widget::Widget(QWidget *parent) : TWidget(parent)
+, _contentMaxHeight(st::emojiPanMaxHeight)
+, _contentHeight(_contentMaxHeight)
+, _scroll(this, st::inlineBotsScroll) {
+	resize(QRect(0, 0, st::emojiPanWidth, _contentHeight).marginsAdded(innerPadding()).size());
+	_width = width();
+	_height = height();
+
+	_scroll->resize(st::emojiPanWidth - st::buttonRadius, _contentHeight);
+
+	_scroll->move(verticalRect().topLeft());
+	_inner = _scroll->setOwnedWidget(object_ptr<internal::Inner>(this));
+
+	_inner->moveToLeft(0, 0, _scroll->width());
+
+	connect(_scroll, SIGNAL(scrolled()), this, SLOT(onScroll()));
+
+	connect(_inner, SIGNAL(emptyInlineRows()), this, SLOT(onEmptyInlineRows()));
+
+	// inline bots
+	_inlineRequestTimer.setSingleShot(true);
+	connect(&_inlineRequestTimer, SIGNAL(timeout()), this, SLOT(onInlineRequest()));
+
+	if (cPlatform() == dbipMac || cPlatform() == dbipMacOld) {
+		connect(App::wnd()->windowHandle(), SIGNAL(activeChanged()), this, SLOT(onWndActiveChanged()));
+	}
+
+	// Inner widget has OpaquePaintEvent attribute so it doesn't repaint on scroll.
+	// But we should force it to repaint so that GIFs will continue to animate without update() calls.
+	// We do that by creating a transparent widget above our _inner.
+	auto forceRepaintOnScroll = object_ptr<TWidget>(this);
+	forceRepaintOnScroll->setGeometry(innerRect().x() + st::buttonRadius, innerRect().y() + st::buttonRadius, st::buttonRadius, st::buttonRadius);
+	forceRepaintOnScroll->setAttribute(Qt::WA_TransparentForMouseEvents);
+	forceRepaintOnScroll->show();
+
+	setMouseTracking(true);
+	setAttribute(Qt::WA_OpaquePaintEvent, false);
+}
+
+void Widget::setMinTop(int minTop) {
+	_minTop = minTop;
+	updateContentHeight();
+}
+
+void Widget::setMinBottom(int minBottom) {
+	_minBottom = minBottom;
+	updateContentHeight();
+}
+
+void Widget::moveBottom(int bottom) {
+	_bottom = bottom;
+	updateContentHeight();
+}
+
+void Widget::updateContentHeight() {
+	auto wantedBottom = countBottom();
+	auto maxContentHeight = wantedBottom - st::emojiPanMargins.top() - st::emojiPanMargins.bottom();
+	auto contentHeight = qMin(_contentMaxHeight, maxContentHeight);
+	auto resultTop = wantedBottom - st::emojiPanMargins.bottom() - contentHeight - st::emojiPanMargins.top();
+	accumulate_max(resultTop, _minTop);
+	auto hs = contentHeight;
+	if (contentHeight == _contentHeight) {
+		move(x(), resultTop);
+		return;
+	}
+
+	auto was = _contentHeight;
+	_contentHeight = contentHeight;
+
+	resize(QRect(0, 0, innerRect().width(), _contentHeight).marginsAdded(innerPadding()).size());
+	_height = height();
+	move(x(), resultTop);
+
+	if (was > _contentHeight) {
+		_scroll->resize(_scroll->width(), _contentHeight);
+		_inner->setMaxHeight(_contentHeight);
+	} else {
+		_inner->setMaxHeight(_contentHeight);
+		_scroll->resize(_scroll->width(), _contentHeight);
+	}
+
+	update();
+}
+
+void Widget::onWndActiveChanged() {
+	if (!App::wnd()->windowHandle()->isActive() && !isHidden()) {
+		leaveEvent(0);
+	}
+}
+
+void Widget::paintEvent(QPaintEvent *e) {
+	Painter p(this);
+
+	auto ms = getms();
+
+	// This call can finish _a_show animation and destroy _showAnimation.
+	auto opacityAnimating = _a_opacity.animating(ms);
+
+	auto showAnimating = _a_show.animating(ms);
+	if (_showAnimation && !showAnimating) {
+		_showAnimation.reset();
+		if (!opacityAnimating) {
+			showChildren();
+		}
+	}
+
+	if (showAnimating) {
+		t_assert(_showAnimation != nullptr);
+		if (auto opacity = _a_opacity.current(_hiding ? 0. : 1.)) {
+			_showAnimation->paintFrame(p, 0, 0, width(), _a_show.current(1.), opacity);
+		}
+	} else if (opacityAnimating) {
+		p.setOpacity(_a_opacity.current(_hiding ? 0. : 1.));
+		p.drawPixmap(0, 0, _cache);
+	} else if (_hiding || isHidden()) {
+		hideFinished();
+	} else {
+		if (!_cache.isNull()) _cache = QPixmap();
+		if (!_inPanelGrab) Ui::Shadow::paint(p, innerRect(), width(), st::emojiPanAnimation.shadow);
+		paintContent(p);
+	}
+}
+
+void Widget::paintContent(Painter &p) {
+	auto inner = innerRect();
+	App::roundRect(p, inner, st::emojiPanBg, ImageRoundRadius::Small, App::RectPart::TopFull | App::RectPart::BottomFull);
+
+	auto horizontal = horizontalRect();
+	auto sidesTop = horizontal.y();
+	auto sidesHeight = horizontal.height();
+	p.fillRect(myrtlrect(inner.x() + inner.width() - st::emojiScroll.width, sidesTop, st::emojiScroll.width, sidesHeight), st::emojiPanBg);
+	p.fillRect(myrtlrect(inner.x(), sidesTop, st::buttonRadius, sidesHeight), st::emojiPanBg);
+}
+
+int Widget::countBottom() const {
+	return _bottom;
+}
+
+void Widget::moveByBottom() {
+	moveToLeft(0, y());
+	updateContentHeight();
+}
+
+void Widget::hideFast() {
+	if (isHidden()) return;
+
+	_hiding = false;
+	_a_opacity.finish();
+	hideFinished();
+}
+
+void Widget::opacityAnimationCallback() {
+	update();
+	if (!_a_opacity.animating()) {
+		if (_hiding) {
+			_hiding = false;
+			hideFinished();
+		} else if (!_a_show.animating()) {
+			showChildren();
+		}
+	}
+}
+
+void Widget::prepareCache() {
+	if (_a_opacity.animating()) return;
+
+	auto showAnimation = base::take(_a_show);
+	auto showAnimationData = base::take(_showAnimation);
+	showChildren();
+	_cache = myGrab(this);
+	_showAnimation = base::take(showAnimationData);
+	_a_show = base::take(showAnimation);
+	if (_a_show.animating()) {
+		hideChildren();
+	}
+}
+
+void Widget::startOpacityAnimation(bool hiding) {
+	_hiding = false;
+	prepareCache();
+	_hiding = hiding;
+	hideChildren();
+	_a_opacity.start([this] { opacityAnimationCallback(); }, _hiding ? 1. : 0., _hiding ? 0. : 1., st::emojiPanDuration);
+}
+
+void Widget::startShowAnimation() {
+	if (!_a_show.animating()) {
+		auto cache = base::take(_cache);
+		auto opacityAnimation = base::take(_a_opacity);
+		showChildren();
+		auto image = grabForPanelAnimation();
+		_a_opacity = base::take(opacityAnimation);
+		_cache = base::take(_cache);
+
+		_showAnimation = std::make_unique<Ui::PanelAnimation>(st::emojiPanAnimation, Ui::PanelAnimation::Origin::BottomLeft);
+		auto inner = rect().marginsRemoved(st::emojiPanMargins);
+		_showAnimation->setFinalImage(std::move(image), QRect(inner.topLeft() * cIntRetinaFactor(), inner.size() * cIntRetinaFactor()));
+		auto corners = App::cornersMask(ImageRoundRadius::Small);
+		_showAnimation->setCornerMasks(QImage(*corners[0]), QImage(*corners[1]), QImage(*corners[2]), QImage(*corners[3]));
+		_showAnimation->start();
+	}
+	hideChildren();
+	_a_show.start([this] { update(); }, 0., 1., st::emojiPanShowDuration);
+}
+
+QImage Widget::grabForPanelAnimation() {
+	myEnsureResized(this);
+	auto result = QImage(size() * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied);
+	result.setDevicePixelRatio(cRetinaFactor());
+	result.fill(Qt::transparent);
+	_inPanelGrab = true;
+	render(&result);
+	_inPanelGrab = false;
+	return result;
+}
+
+void Widget::hideAnimated() {
+	if (isHidden()) return;
+	if (_hiding) return;
+
+	startOpacityAnimation(true);
+}
+
+Widget::~Widget() = default;
+
+void Widget::hideFinished() {
+	hide();
+	_inner->hideFinish(true);
+	_a_show.finish();
+	_showAnimation.reset();
+	_cache = QPixmap();
+	_horizontal = false;
+	_hiding = false;
+
+	_scroll->scrollToY(0);
+
+	Notify::clipStopperHidden(ClipStopperSavedGifsPanel);
+}
+
+void Widget::showAnimated() {
+	showStarted();
+}
+
+void Widget::showStarted() {
+	if (isHidden()) {
+		recountContentMaxHeight();
+		_inner->preloadImages();
+		moveByBottom();
+		show();
+		startShowAnimation();
+	} else if (_hiding) {
+		startOpacityAnimation(false);
+	}
+}
+
+bool Widget::ui_isInlineItemBeingChosen() {
+	if (!isHidden()) {
+		return _inner->ui_isInlineItemBeingChosen();
+	}
+	return false;
+}
+
+void Widget::onScroll() {
+	auto st = _scroll->scrollTop();
+	if (st + _scroll->height() > _scroll->scrollTopMax()) {
+		onInlineRequest();
+	}
+	_inner->setVisibleTopBottom(st, st + _scroll->height());
+}
+
+style::margins Widget::innerPadding() const {
+	return st::emojiPanMargins;
+}
+
+QRect Widget::innerRect() const {
+	return rect().marginsRemoved(innerPadding());
+}
+
+QRect Widget::horizontalRect() const {
+	return innerRect().marginsRemoved(style::margins(0, st::buttonRadius, 0, st::buttonRadius));
+}
+
+QRect Widget::verticalRect() const {
+	return innerRect().marginsRemoved(style::margins(st::buttonRadius, 0, st::buttonRadius, 0));
+}
+
+void Widget::clearInlineBot() {
+	inlineBotChanged();
+}
+
+bool Widget::overlaps(const QRect &globalRect) const {
+	if (isHidden() || !_cache.isNull()) return false;
+
+	auto testRect = QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size());
+	auto inner = rect().marginsRemoved(st::emojiPanMargins);
+	return inner.marginsRemoved(QMargins(st::buttonRadius, 0, st::buttonRadius, 0)).contains(testRect)
+		|| inner.marginsRemoved(QMargins(0, st::buttonRadius, 0, st::buttonRadius)).contains(testRect);
+}
+
+void Widget::inlineBotChanged() {
+	if (!_inlineBot) return;
+
+	if (!isHidden() && !_hiding) {
+		hideAnimated();
+	}
+
+	if (_inlineRequestId) MTP::cancel(_inlineRequestId);
+	_inlineRequestId = 0;
+	_inlineQuery = _inlineNextQuery = _inlineNextOffset = QString();
+	_inlineBot = nullptr;
+	_inlineCache.clear();
+	_inner->inlineBotChanged();
+	_inner->hideInlineRowsPanel();
+
+	Notify::inlineBotRequesting(false);
+}
+
+void Widget::inlineResultsDone(const MTPmessages_BotResults &result) {
+	_inlineRequestId = 0;
+	Notify::inlineBotRequesting(false);
+
+	auto it = _inlineCache.find(_inlineQuery);
+	auto adding = (it != _inlineCache.cend());
+	if (result.type() == mtpc_messages_botResults) {
+		auto &d = result.c_messages_botResults();
+		auto &v = d.vresults.v;
+		auto queryId = d.vquery_id.v;
+
+		if (it == _inlineCache.cend()) {
+			it = _inlineCache.emplace(_inlineQuery, std::make_unique<internal::CacheEntry>()).first;
+		}
+		auto entry = it->second.get();
+		entry->nextOffset = qs(d.vnext_offset);
+		if (d.has_switch_pm() && d.vswitch_pm.type() == mtpc_inlineBotSwitchPM) {
+			auto &switchPm = d.vswitch_pm.c_inlineBotSwitchPM();
+			entry->switchPmText = qs(switchPm.vtext);
+			entry->switchPmStartToken = qs(switchPm.vstart_param);
+		}
+
+		if (auto count = v.size()) {
+			entry->results.reserve(entry->results.size() + count);
+		}
+		auto added = 0;
+		for_const (const auto &res, v) {
+			if (auto result = InlineBots::Result::create(queryId, res)) {
+				++added;
+				entry->results.push_back(std::move(result));
+			}
+		}
+
+		if (!added) {
+			entry->nextOffset = QString();
+		}
+	} else if (adding) {
+		it->second->nextOffset = QString();
+	}
+
+	if (!showInlineRows(!adding)) {
+		it->second->nextOffset = QString();
+	}
+	onScroll();
+}
+
+void Widget::queryInlineBot(UserData *bot, PeerData *peer, QString query) {
+	bool force = false;
+	_inlineQueryPeer = peer;
+	if (bot != _inlineBot) {
+		inlineBotChanged();
+		_inlineBot = bot;
+		force = true;
+		//if (_inlineBot->isBotInlineGeo()) {
+		//	Ui::show(Box<InformBox>(lang(lng_bot_inline_geo_unavailable)));
+		//}
+	}
+	//if (_inlineBot && _inlineBot->isBotInlineGeo()) {
+	//	return;
+	//}
+
+	if (_inlineQuery != query || force) {
+		if (_inlineRequestId) {
+			MTP::cancel(_inlineRequestId);
+			_inlineRequestId = 0;
+			Notify::inlineBotRequesting(false);
+		}
+		if (_inlineCache.find(query) != _inlineCache.cend()) {
+			_inlineRequestTimer.stop();
+			_inlineQuery = _inlineNextQuery = query;
+			showInlineRows(true);
+		} else {
+			_inlineNextQuery = query;
+			_inlineRequestTimer.start(InlineBotRequestDelay);
+		}
+	}
+}
+
+void Widget::onInlineRequest() {
+	if (_inlineRequestId || !_inlineBot || !_inlineQueryPeer) return;
+	_inlineQuery = _inlineNextQuery;
+
+	QString nextOffset;
+	auto it = _inlineCache.find(_inlineQuery);
+	if (it != _inlineCache.cend()) {
+		nextOffset = it->second->nextOffset;
+		if (nextOffset.isEmpty()) return;
+	}
+	Notify::inlineBotRequesting(true);
+	_inlineRequestId = request(MTPmessages_GetInlineBotResults(MTP_flags(0), _inlineBot->inputUser, _inlineQueryPeer->input, MTPInputGeoPoint(), MTP_string(_inlineQuery), MTP_string(nextOffset))).done([this](const MTPmessages_BotResults &result, mtpRequestId requestId) {
+		inlineResultsDone(result);
+	}).fail([this](const RPCError &error) {
+		// show error?
+		Notify::inlineBotRequesting(false);
+		_inlineRequestId = 0;
+	}).handleAllErrors().send();
+}
+
+void Widget::onEmptyInlineRows() {
+	hideAnimated();
+	_inner->clearInlineRowsPanel();
+}
+
+bool Widget::refreshInlineRows(int *added) {
+	auto it = _inlineCache.find(_inlineQuery);
+	const internal::CacheEntry *entry = nullptr;
+	if (it != _inlineCache.cend()) {
+		if (!it->second->results.empty() || !it->second->switchPmText.isEmpty()) {
+			entry = it->second.get();
+		}
+		_inlineNextOffset = it->second->nextOffset;
+	}
+	if (!entry) prepareCache();
+	auto result = _inner->refreshInlineRows(_inlineBot, entry, false);
+	if (added) *added = result;
+	return (entry != nullptr);
+}
+
+int Widget::showInlineRows(bool newResults) {
+	auto added = 0;
+	auto clear = !refreshInlineRows(&added);
+	if (newResults) {
+		_scroll->scrollToY(0);
+	}
+
+	bool hidden = isHidden();
+	if (!hidden && !clear) {
+		recountContentMaxHeight();
+	}
+	if (clear) {
+		if (!hidden) {
+			hideAnimated();
+		} else if (!_hiding) {
+			_cache = QPixmap(); // clear after refreshInlineRows()
+		}
+	} else {
+		if (hidden || _hiding) {
+			showAnimated();
+		}
+	}
+
+	return added;
+}
+
+void Widget::recountContentMaxHeight() {
+	_contentMaxHeight = qMin(_inner->countHeight(true), st::emojiPanMaxHeight);
+	updateContentHeight();
+}
+
+} // namespace Layout
+} // namespace InlineBots
diff --git a/Telegram/SourceFiles/inline_bots/inline_results_widget.h b/Telegram/SourceFiles/inline_bots/inline_results_widget.h
new file mode 100644
index 000000000..8523ac900
--- /dev/null
+++ b/Telegram/SourceFiles/inline_bots/inline_results_widget.h
@@ -0,0 +1,274 @@
+/*
+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
+*/
+#pragma once
+
+#include "ui/twidget.h"
+#include "ui/abstract_button.h"
+#include "ui/effects/panel_animation.h"
+#include "mtproto/sender.h"
+#include "inline_bots/inline_bot_layout_item.h"
+
+namespace Ui {
+class ScrollArea;
+class IconButton;
+class LinkButton;
+class RoundButton;
+class RippleAnimation;
+} // namesapce Ui
+
+namespace InlineBots {
+
+class Result;
+
+namespace Layout {
+
+class ItemBase;
+
+namespace internal {
+
+constexpr int kInlineItemsMaxPerRow = 5;
+
+using Results = std::vector<std::unique_ptr<Result>>;
+
+struct CacheEntry {
+	QString nextOffset;
+	QString switchPmText, switchPmStartToken;
+	Results results;
+};
+
+class Inner : public TWidget, public Context, private base::Subscriber {
+	Q_OBJECT
+
+public:
+	Inner(QWidget *parent);
+
+	void setMaxHeight(int maxHeight);
+
+	void hideFinish(bool completely);
+
+	void clearSelection();
+
+	int refreshInlineRows(UserData *bot, const CacheEntry *results, bool resultsDeleted);
+	void inlineBotChanged();
+	void hideInlineRowsPanel();
+	void clearInlineRowsPanel();
+
+	void setVisibleTopBottom(int visibleTop, int visibleBottom) override;
+	void preloadImages();
+
+	void inlineItemLayoutChanged(const ItemBase *layout) override;
+	void inlineItemRepaint(const ItemBase *layout) override;
+	bool inlineItemVisible(const ItemBase *layout) override;
+	bool ui_isInlineItemBeingChosen();
+
+	int countHeight(bool plain = false);
+
+	void setResultSelectedCallback(base::lambda<void(Result *result, UserData *bot)> callback) {
+		_resultSelectedCallback = std::move(callback);
+	}
+
+	~Inner();
+
+protected:
+	void mousePressEvent(QMouseEvent *e) override;
+	void mouseReleaseEvent(QMouseEvent *e) override;
+	void mouseMoveEvent(QMouseEvent *e) override;
+	void paintEvent(QPaintEvent *e) override;
+	void leaveEventHook(QEvent *e) override;
+	void leaveToChildEvent(QEvent *e, QWidget *child) override;
+	void enterFromChildEvent(QEvent *e, QWidget *child) override;
+
+private slots:
+	void onPreview();
+	void onUpdateInlineItems();
+	void onSwitchPm();
+
+signals:
+	void emptyInlineRows();
+
+private:
+	static constexpr bool kRefreshIconsScrollAnimation = true;
+	static constexpr bool kRefreshIconsNoAnimation = false;
+
+	void updateSelected();
+
+	void paintInlineItems(Painter &p, const QRect &r);
+
+	void refreshSwitchPmButton(const CacheEntry *entry);
+
+	int32 _maxHeight;
+
+	int _visibleTop = 0;
+	int _visibleBottom = 0;
+
+	UserData *_inlineBot;
+	QString _inlineBotTitle;
+	TimeMs _lastScrolled = 0;
+	QTimer _updateInlineItems;
+	bool _inlineWithThumb = false;
+
+	object_ptr<Ui::RoundButton> _switchPmButton = { nullptr };
+	QString _switchPmStartToken;
+
+	struct Row {
+		int height = 0;
+		QVector<ItemBase*> items;
+	};
+	QVector<Row> _rows;
+	void clearInlineRows(bool resultsDeleted);
+
+	std::map<Result*, std::unique_ptr<ItemBase>> _inlineLayouts;
+	ItemBase *layoutPrepareInlineResult(Result *result, int32 position);
+
+	bool inlineRowsAddItem(Result *result, Row &row, int32 &sumWidth);
+	bool inlineRowFinalize(Row &row, int32 &sumWidth, bool force = false);
+
+	Row &layoutInlineRow(Row &row, int32 sumWidth = 0);
+	void deleteUnusedInlineLayouts();
+
+	int validateExistingInlineRows(const Results &results);
+	void selectInlineResult(int row, int column);
+
+	int _selected = -1;
+	int _pressed = -1;
+	QPoint _lastMousePos;
+
+	QTimer _previewTimer;
+	bool _previewShown = false;
+
+	base::lambda<void(Result *result, UserData *bot)> _resultSelectedCallback;
+
+};
+
+} // namespace internal
+
+class Widget : public TWidget, private MTP::Sender {
+	Q_OBJECT
+
+public:
+	Widget(QWidget *parent);
+
+	void setMinTop(int minTop);
+	void setMinBottom(int minBottom);
+	void moveBottom(int bottom);
+
+	void hideFast();
+	bool hiding() const {
+		return _hiding;
+	}
+
+	void queryInlineBot(UserData *bot, PeerData *peer, QString query);
+	void clearInlineBot();
+
+	bool overlaps(const QRect &globalRect) const;
+
+	bool ui_isInlineItemBeingChosen();
+
+	void showAnimated();
+	void hideAnimated();
+
+	void setResultSelectedCallback(base::lambda<void(Result *result, UserData *bot)> callback) {
+		_inner->setResultSelectedCallback(std::move(callback));
+	}
+
+	~Widget();
+
+protected:
+	void paintEvent(QPaintEvent *e) override;
+
+private slots:
+	void onWndActiveChanged();
+
+	void onScroll();
+
+	void onInlineRequest();
+	void onEmptyInlineRows();
+
+private:
+	int countBottom() const;
+	void moveByBottom();
+	void paintContent(Painter &p);
+
+	style::margins innerPadding() const;
+
+	// Rounded rect which has shadow around it.
+	QRect innerRect() const;
+
+	// Inner rect with removed st::buttonRadius from top and bottom.
+	// This one is allowed to be not rounded.
+	QRect horizontalRect() const;
+
+	// Inner rect with removed st::buttonRadius from left and right.
+	// This one is allowed to be not rounded.
+	QRect verticalRect() const;
+
+	QImage grabForPanelAnimation();
+	void startShowAnimation();
+	void startOpacityAnimation(bool hiding);
+	void prepareCache();
+
+	class Container;
+	void opacityAnimationCallback();
+
+	void hideFinished();
+	void showStarted();
+
+	void updateContentHeight();
+
+	void inlineBotChanged();
+	int showInlineRows(bool newResults);
+	void recountContentMaxHeight();
+	bool refreshInlineRows(int *added = nullptr);
+	void inlineResultsDone(const MTPmessages_BotResults &result);
+
+	int _minTop = 0;
+	int _minBottom = 0;
+	int _contentMaxHeight = 0;
+	int _contentHeight = 0;
+	bool _horizontal = false;
+
+	int _width = 0;
+	int _height = 0;
+	int _bottom = 0;
+
+	std::unique_ptr<Ui::PanelAnimation> _showAnimation;
+	Animation _a_show;
+
+	bool _hiding = false;
+	QPixmap _cache;
+	Animation _a_opacity;
+	bool _inPanelGrab = false;
+
+	object_ptr<Ui::ScrollArea> _scroll;
+	QPointer<internal::Inner> _inner;
+
+	std::map<QString, std::unique_ptr<internal::CacheEntry>> _inlineCache;
+	QTimer _inlineRequestTimer;
+
+	UserData *_inlineBot = nullptr;
+	PeerData *_inlineQueryPeer = nullptr;
+	QString _inlineQuery, _inlineNextQuery, _inlineNextOffset;
+	mtpRequestId _inlineRequestId = 0;
+
+};
+
+} // namespace Layout
+} // namespace InlineBots
diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp
index 3ad48209f..1fa4fa648 100644
--- a/Telegram/SourceFiles/mainwidget.cpp
+++ b/Telegram/SourceFiles/mainwidget.cpp
@@ -554,14 +554,6 @@ void MainWidget::ui_repaintHistoryItem(const HistoryItem *item) {
 	if (_overview) _overview->ui_repaintHistoryItem(item);
 }
 
-void MainWidget::ui_repaintInlineItem(const InlineBots::Layout::ItemBase *layout) {
-	_history->ui_repaintInlineItem(layout);
-}
-
-bool MainWidget::ui_isInlineItemVisible(const InlineBots::Layout::ItemBase *layout) {
-	return _history->ui_isInlineItemVisible(layout);
-}
-
 bool MainWidget::ui_isInlineItemBeingChosen() {
 	return _history->ui_isInlineItemBeingChosen();
 }
@@ -571,10 +563,6 @@ void MainWidget::notify_historyItemLayoutChanged(const HistoryItem *item) {
 	if (_overview) _overview->notify_historyItemLayoutChanged(item);
 }
 
-void MainWidget::notify_inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout) {
-	_history->notify_inlineItemLayoutChanged(layout);
-}
-
 void MainWidget::notify_historyMuteUpdated(History *history) {
 	_dialogs->notify_historyMuteUpdated(history);
 }
@@ -1339,8 +1327,8 @@ void MainWidget::app_sendBotCallback(const HistoryMessageReplyMarkup::Button *bu
 	_history->app_sendBotCallback(button, msg, row, col);
 }
 
-bool MainWidget::insertBotCommand(const QString &cmd, bool specialGif) {
-	return _history->insertBotCommand(cmd, specialGif);
+bool MainWidget::insertBotCommand(const QString &cmd) {
+	return _history->insertBotCommand(cmd);
 }
 
 void MainWidget::searchMessages(const QString &query, PeerData *inPeer) {
@@ -1564,7 +1552,7 @@ void MainWidget::handleAudioUpdate(const AudioMsgId &audioId) {
 	}
 	if (auto items = InlineBots::Layout::documentItems()) {
 		for (auto item : items->value(audioId.audio())) {
-			Ui::repaintInlineItem(item);
+			item->update();
 		}
 	}
 }
diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h
index d71c9daef..8c3a89432 100644
--- a/Telegram/SourceFiles/mainwidget.h
+++ b/Telegram/SourceFiles/mainwidget.h
@@ -306,7 +306,7 @@ public:
 
 	void sendBotCommand(PeerData *peer, UserData *bot, const QString &cmd, MsgId replyTo);
 	void hideSingleUseKeyboard(PeerData *peer, MsgId replyTo);
-	bool insertBotCommand(const QString &cmd, bool specialGif);
+	bool insertBotCommand(const QString &cmd);
 
 	void jumpToDate(PeerData *peer, const QDate &date);
 	void searchMessages(const QString &query, PeerData *inPeer);
@@ -383,8 +383,6 @@ public:
 	void app_sendBotCallback(const HistoryMessageReplyMarkup::Button *button, const HistoryItem *msg, int row, int col);
 
 	void ui_repaintHistoryItem(const HistoryItem *item);
-	void ui_repaintInlineItem(const InlineBots::Layout::ItemBase *layout);
-	bool ui_isInlineItemVisible(const InlineBots::Layout::ItemBase *layout);
 	bool ui_isInlineItemBeingChosen();
 	void ui_showPeerHistory(quint64 peer, qint32 msgId, Ui::ShowWay way);
 	PeerData *ui_getPeerForMouseAction();
@@ -399,7 +397,6 @@ public:
 	void notify_migrateUpdated(PeerData *peer);
 	void notify_clipStopperHidden(ClipStopperType type);
 	void notify_historyItemLayoutChanged(const HistoryItem *item);
-	void notify_inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout);
 	void notify_historyMuteUpdated(History *history);
 	void notify_handlePendingHistoryUpdate();
 
diff --git a/Telegram/SourceFiles/settings.h b/Telegram/SourceFiles/settings.h
index 6c131ec35..76a66bafe 100644
--- a/Telegram/SourceFiles/settings.h
+++ b/Telegram/SourceFiles/settings.h
@@ -61,9 +61,6 @@ DeclareSetting(uint64, RealBetaVersion);
 DeclareSetting(QByteArray, BetaPrivateKey);
 
 DeclareSetting(bool, TestMode);
-inline QString cInlineGifBotUsername() {
-	return cTestMode() ? qstr("contextbot") : qstr("gif");
-}
 DeclareSetting(QString, LoggedPhoneNumber);
 DeclareSetting(bool, AutoStart);
 DeclareSetting(bool, StartMinimized);
diff --git a/Telegram/SourceFiles/stickers/emoji_pan.cpp b/Telegram/SourceFiles/stickers/emoji_pan.cpp
index a74be354f..657211850 100644
--- a/Telegram/SourceFiles/stickers/emoji_pan.cpp
+++ b/Telegram/SourceFiles/stickers/emoji_pan.cpp
@@ -1304,10 +1304,6 @@ void StickerPanInner::hideFinish(bool completely) {
 		}
 		clearInstalledLocally();
 	}
-	if (_setGifCommand && _section == Section::Gifs) {
-		App::insertBotCommand(qsl(""), true);
-	}
-	_setGifCommand = false;
 
 	// Reset to the recent stickers section.
 	if (_section == Section::Featured) {
@@ -1420,7 +1416,6 @@ void StickerPanInner::refreshSavedGifs() {
 }
 
 void StickerPanInner::inlineBotChanged() {
-	_setGifCommand = false;
 	refreshInlineRows(nullptr, nullptr, true);
 }
 
@@ -1445,7 +1440,7 @@ void StickerPanInner::clearInlineRows(bool resultsDeleted) {
 InlineItem *StickerPanInner::layoutPrepareSavedGif(DocumentData *doc, int32 position) {
 	auto it = _gifLayouts.find(doc);
 	if (it == _gifLayouts.cend()) {
-		if (auto layout = InlineItem::createLayoutGif(doc)) {
+		if (auto layout = InlineItem::createLayoutGif(this, doc)) {
 			it = _gifLayouts.emplace(doc, std::move(layout)).first;
 			it->second->initDimensions();
 		} else {
@@ -1461,7 +1456,7 @@ InlineItem *StickerPanInner::layoutPrepareSavedGif(DocumentData *doc, int32 posi
 InlineItem *StickerPanInner::layoutPrepareInlineResult(InlineResult *result, int32 position) {
 	auto it = _inlineLayouts.find(result);
 	if (it == _inlineLayouts.cend()) {
-		if (auto layout = InlineItem::createLayout(result, _inlineWithThumb)) {
+		if (auto layout = InlineItem::createLayout(this, result, _inlineWithThumb)) {
 			it = _inlineLayouts.emplace(result, std::move(layout)).first;
 			it->second->initDimensions();
 		} else {
@@ -1632,7 +1627,7 @@ int StickerPanInner::refreshInlineRows(UserData *bot, const InlineCacheEntry *en
 			return true;
 		}
 		if (entry->results.empty() && entry->switchPmText.isEmpty()) {
-			if (!_inlineBot || _inlineBot->username != cInlineGifBotUsername()) {
+			if (!_inlineBot) {
 				return true;
 			}
 		}
@@ -1745,8 +1740,8 @@ int StickerPanInner::validateExistingInlineRows(const InlineResults &results) {
 	return until;
 }
 
-void StickerPanInner::notify_inlineItemLayoutChanged(const InlineItem *layout) {
-	if (_selected < 0 || !showingInlineItems()) {
+void StickerPanInner::inlineItemLayoutChanged(const InlineItem *layout) {
+	if (_selected < 0 || !showingInlineItems() || !isVisible()) {
 		return;
 	}
 
@@ -1758,7 +1753,7 @@ void StickerPanInner::notify_inlineItemLayoutChanged(const InlineItem *layout) {
 	}
 }
 
-void StickerPanInner::ui_repaintInlineItem(const InlineItem *layout) {
+void StickerPanInner::inlineItemRepaint(const InlineItem *layout) {
 	auto ms = getms();
 	if (_lastScrolled + 100 <= ms) {
 		update();
@@ -1767,9 +1762,9 @@ void StickerPanInner::ui_repaintInlineItem(const InlineItem *layout) {
 	}
 }
 
-bool StickerPanInner::ui_isInlineItemVisible(const InlineItem *layout) {
+bool StickerPanInner::inlineItemVisible(const InlineItem *layout) {
 	int32 position = layout->position();
-	if (!showingInlineItems() || position < 0) {
+	if (!showingInlineItems() || position < 0 || !isVisible()) {
 		return false;
 	}
 
@@ -2015,12 +2010,12 @@ void StickerPanInner::updateSelected() {
 		if (_selected != sel) {
 			if (srow >= 0 && scol >= 0) {
 				t_assert(srow >= 0 && srow < _inlineRows.size() && scol >= 0 && scol < _inlineRows.at(srow).items.size());
-				Ui::repaintInlineItem(_inlineRows.at(srow).items.at(scol));
+				_inlineRows[srow].items[scol]->update();
 			}
 			_selected = sel;
 			if (row >= 0 && col >= 0) {
 				t_assert(row >= 0 && row < _inlineRows.size() && col >= 0 && col < _inlineRows.at(row).items.size());
-				Ui::repaintInlineItem(_inlineRows.at(row).items.at(col));
+				_inlineRows[row].items[col]->update();
 			}
 			if (_previewShown && _selected >= 0 && _pressed != _selected) {
 				_pressed = _selected;
@@ -2193,16 +2188,10 @@ void StickerPanInner::showStickerSet(uint64 setId) {
 		refreshSavedGifs();
 		emit scrollToY(0);
 		emit scrollUpdated();
-		showFinish();
 		return;
 	}
 
 	if (showingInlineItems()) {
-		if (_setGifCommand && _section == Section::Gifs) {
-			App::insertBotCommand(qsl(""), true);
-		}
-		_setGifCommand = false;
-
 		cSetShowingSavedGifs(false);
 		emit saveConfigDelayed(kSaveRecentEmojiTimeout);
 		Notify::clipStopperHidden(ClipStopperSavedGifsPanel);
@@ -2259,12 +2248,6 @@ void StickerPanInner::updateShowingSavedGifs() {
 	}
 }
 
-void StickerPanInner::showFinish() {
-	if (_section == Section::Gifs) {
-		_setGifCommand = App::insertBotCommand('@' + cInlineGifBotUsername(), true);
-	}
-}
-
 EmojiPanel::EmojiPanel(QWidget *parent, const QString &text, uint64 setId, bool special, int32 wantedY) : TWidget(parent)
 , _wantedY(wantedY)
 , _setId(setId)
@@ -2388,22 +2371,6 @@ void EmojiSwitchButton::paintEvent(QPaintEvent *e) {
 
 namespace {
 
-FORCE_INLINE uint32 twoImagesOnBgWithAlpha(
-	const anim::Shifted shiftedBg,
-	const uint32 source1Alpha,
-	const uint32 source2Alpha,
-	const uint32 source1,
-	const uint32 source2,
-	const uint32 alpha) {
-	auto source1Pattern = anim::reshifted(anim::shifted(source1) * source1Alpha);
-	auto bg1Alpha = 256 - anim::getAlpha(source1Pattern);
-	auto mixed1Pattern = anim::reshifted(shiftedBg * bg1Alpha) + source1Pattern;
-	auto source2Pattern = anim::reshifted(anim::shifted(source2) * source2Alpha);
-	auto bg2Alpha = 256 - anim::getAlpha(source2Pattern);
-	auto mixed2Pattern = anim::reshifted(mixed1Pattern * bg2Alpha) + source2Pattern;
-	return anim::unshifted(mixed2Pattern * alpha);
-}
-
 FORCE_INLINE uint32 oneImageOnBgWithAlpha(
 	const anim::Shifted shiftedBg,
 	const uint32 sourceAlpha,
@@ -2966,23 +2933,19 @@ bool EmojiPan::inlineResultsShown() const {
 }
 
 int EmojiPan::countBottom() const {
-	return (_origin == Ui::PanelAnimation::Origin::BottomLeft) ? _bottom : (parentWidget()->height() - _minBottom);
+	return (parentWidget()->height() - _minBottom);
 }
 
 void EmojiPan::moveByBottom() {
-	if (inlineResultsShown()) {
-		setOrigin(Ui::PanelAnimation::Origin::BottomLeft);
-		moveToLeft(0, y());
-	} else {
-		setOrigin(Ui::PanelAnimation::Origin::BottomRight);
-		moveToRight(0, y());
-	}
+	moveToRight(0, y());
 	updateContentHeight();
 }
 
 void EmojiPan::enterEventHook(QEvent *e) {
 	_hideTimer.stop();
-	if (_hiding) showAnimated(_origin);
+	if (_hiding) {
+		showAnimated();
+	}
 }
 
 bool EmojiPan::preventAutoHide() const {
@@ -2990,7 +2953,9 @@ bool EmojiPan::preventAutoHide() const {
 }
 
 void EmojiPan::leaveEventHook(QEvent *e) {
-	if (preventAutoHide() || s_inner->inlineResultsShown()) return;
+	if (preventAutoHide()) {
+		return;
+	}
 	auto ms = getms();
 	if (_a_show.animating(ms) || _a_opacity.animating(ms)) {
 		hideAnimated();
@@ -3002,7 +2967,7 @@ void EmojiPan::leaveEventHook(QEvent *e) {
 
 void EmojiPan::otherEnter() {
 	_hideTimer.stop();
-	showAnimated(_origin);
+	showAnimated();
 }
 
 void EmojiPan::otherLeave() {
@@ -3244,7 +3209,7 @@ void EmojiPan::opacityAnimationCallback() {
 }
 
 void EmojiPan::hideByTimerOrLeave() {
-	if (isHidden() || preventAutoHide() || s_inner->inlineResultsShown()) return;
+	if (isHidden() || preventAutoHide()) return;
 
 	hideAnimated();
 }
@@ -3286,7 +3251,7 @@ void EmojiPan::startShowAnimation() {
 		_a_opacity = base::take(opacityAnimation);
 		_cache = base::take(_cache);
 
-		_showAnimation = std::make_unique<Ui::PanelAnimation>(st::emojiPanAnimation, _origin);
+		_showAnimation = std::make_unique<Ui::PanelAnimation>(st::emojiPanAnimation, Ui::PanelAnimation::Origin::BottomRight);
 		auto inner = rect().marginsRemoved(st::emojiPanMargins);
 		_showAnimation->setFinalImage(std::move(image), QRect(inner.topLeft() * cIntRetinaFactor(), inner.size() * cIntRetinaFactor()));
 		auto corners = App::cornersMask(ImageRoundRadius::Small);
@@ -3343,12 +3308,7 @@ void EmojiPan::hideFinished() {
 	Notify::clipStopperHidden(ClipStopperSavedGifsPanel);
 }
 
-void EmojiPan::setOrigin(Ui::PanelAnimation::Origin origin) {
-	_origin = origin;
-}
-
-void EmojiPan::showAnimated(Ui::PanelAnimation::Origin origin) {
-	setOrigin(origin);
+void EmojiPan::showAnimated() {
 	_hideTimer.stop();
 	showStarted();
 }
@@ -3397,7 +3357,7 @@ bool EmojiPan::eventFilter(QObject *obj, QEvent *e) {
 	} else if (e->type() == QEvent::MouseButtonPress && static_cast<QMouseEvent*>(e)->button() == Qt::LeftButton/* && !dynamic_cast<StickerPan*>(obj)*/) {
 		if (isHidden() || _hiding) {
 			_hideTimer.stop();
-			showAnimated(_origin);
+			showAnimated();
 		} else {
 			hideAnimated();
 		}
@@ -3415,26 +3375,7 @@ void EmojiPan::stickersInstalled(uint64 setId) {
 	showAll();
 	s_inner->showStickerSet(setId);
 	updateContentHeight();
-	showAnimated(Ui::PanelAnimation::Origin::BottomRight);
-}
-
-void EmojiPan::notify_inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout) {
-	if (!_emojiShown && !isHidden()) {
-		s_inner->notify_inlineItemLayoutChanged(layout);
-	}
-}
-
-void EmojiPan::ui_repaintInlineItem(const InlineBots::Layout::ItemBase *layout) {
-	if (!_emojiShown && !isHidden()) {
-		s_inner->ui_repaintInlineItem(layout);
-	}
-}
-
-bool EmojiPan::ui_isInlineItemVisible(const InlineBots::Layout::ItemBase *layout) {
-	if (!_emojiShown && !isHidden()) {
-		return s_inner->ui_isInlineItemVisible(layout);
-	}
-	return false;
+	showAnimated();
 }
 
 bool EmojiPan::ui_isInlineItemBeingChosen() {
@@ -3640,9 +3581,6 @@ void EmojiPan::performSwitch() {
 		} else {
 			s_inner->updateShowingSavedGifs();
 		}
-		if (cShowingSavedGifs()) {
-			s_inner->showFinish();
-		}
 		validateSelectedIcon(ValidateIconAnimations::None);
 		updateContentHeight();
 	}
@@ -3745,15 +3683,11 @@ bool EmojiPan::overlaps(const QRect &globalRect) const {
 		|| inner.marginsRemoved(QMargins(0, st::buttonRadius, 0, st::buttonRadius)).contains(testRect);
 }
 
-bool EmojiPan::hideOnNoInlineResults() {
-	return _inlineBot && inlineResultsShown() && (_shownFromInlineQuery || _inlineBot->username != cInlineGifBotUsername());
-}
-
 void EmojiPan::inlineBotChanged() {
 	if (!_inlineBot) return;
 
 	if (!isHidden() && !_hiding) {
-		if (hideOnNoInlineResults() || !rect().contains(mapFromGlobal(QCursor::pos()))) {
+		if (!rect().contains(mapFromGlobal(QCursor::pos()))) {
 			hideAnimated();
 		}
 	}
@@ -3868,7 +3802,7 @@ void EmojiPan::onInlineRequest() {
 }
 
 void EmojiPan::onEmptyInlineRows() {
-	if (_shownFromInlineQuery || hideOnNoInlineResults()) {
+	if (_shownFromInlineQuery) {
 		hideAnimated();
 		s_inner->clearInlineRowsPanel();
 	} else if (!_inlineBot) {
@@ -3906,15 +3840,13 @@ int32 EmojiPan::showInlineRows(bool newResults) {
 		recountContentMaxHeight();
 	}
 	if (clear) {
-		if (!hidden && hideOnNoInlineResults()) {
-			hideAnimated();
-		} else if (!_hiding) {
+		if (!_hiding) {
 			_cache = QPixmap(); // clear after refreshInlineRows()
 		}
 	} else {
 		_hideTimer.stop();
 		if (hidden || _hiding) {
-			showAnimated(_origin);
+			showAnimated();
 		} else if (_emojiShown) {
 			onSwitch();
 		}
diff --git a/Telegram/SourceFiles/stickers/emoji_pan.h b/Telegram/SourceFiles/stickers/emoji_pan.h
index f56f2475c..f5adef051 100644
--- a/Telegram/SourceFiles/stickers/emoji_pan.h
+++ b/Telegram/SourceFiles/stickers/emoji_pan.h
@@ -24,6 +24,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "ui/abstract_button.h"
 #include "ui/effects/panel_animation.h"
 #include "mtproto/sender.h"
+#include "inline_bots/inline_bot_layout_item.h"
 
 namespace InlineBots {
 namespace Layout {
@@ -199,7 +200,7 @@ struct StickerIcon {
 	int pixh = 0;
 };
 
-class StickerPanInner : public TWidget, private base::Subscriber {
+class StickerPanInner : public TWidget, public InlineBots::Layout::Context, private base::Subscriber {
 	Q_OBJECT
 
 public:
@@ -208,7 +209,6 @@ public:
 	void setMaxHeight(int maxHeight);
 
 	void hideFinish(bool completely);
-	void showFinish();
 	void showStickerSet(uint64 setId);
 	void updateShowingSavedGifs();
 
@@ -233,9 +233,9 @@ public:
 
 	uint64 currentSet(int yOffset) const;
 
-	void notify_inlineItemLayoutChanged(const InlineItem *layout);
-	void ui_repaintInlineItem(const InlineItem *layout);
-	bool ui_isInlineItemVisible(const InlineItem *layout);
+	void inlineItemLayoutChanged(const InlineItem *layout) override;
+	void inlineItemRepaint(const InlineItem *layout) override;
+	bool inlineItemVisible(const InlineItem *layout) override;
 	bool ui_isInlineItemBeingChosen();
 
 	bool inlineResultsShown() const {
@@ -353,7 +353,6 @@ private:
 		Stickers,
 	};
 	Section _section = Section::Stickers;
-	bool _setGifCommand = false;
 	UserData *_inlineBot;
 	QString _inlineBotTitle;
 	TimeMs _lastScrolled = 0;
@@ -487,13 +486,9 @@ public:
 
 	bool overlaps(const QRect &globalRect) const;
 
-	void notify_inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout);
-	void ui_repaintInlineItem(const InlineBots::Layout::ItemBase *layout);
-	bool ui_isInlineItemVisible(const InlineBots::Layout::ItemBase *layout);
 	bool ui_isInlineItemBeingChosen();
 
-	void setOrigin(Ui::PanelAnimation::Origin origin);
-	void showAnimated(Ui::PanelAnimation::Origin origin);
+	void showAnimated();
 	void hideAnimated();
 
 	~EmojiPan();
@@ -614,7 +609,6 @@ private:
 	int _height = 0;
 	int _bottom = 0;
 
-	Ui::PanelAnimation::Origin _origin = Ui::PanelAnimation::Origin::BottomRight;
 	std::unique_ptr<Ui::PanelAnimation> _showAnimation;
 	Animation _a_show;
 
@@ -675,7 +669,6 @@ private:
 
 	void inlineBotChanged();
 	int32 showInlineRows(bool newResults);
-	bool hideOnNoInlineResults();
 	void recountContentMaxHeight();
 	bool refreshInlineRows(int32 *added = 0);
 	UserData *_inlineBot = nullptr;
diff --git a/Telegram/SourceFiles/stickers/stickers.style b/Telegram/SourceFiles/stickers/stickers.style
index bdcc5fbf9..07d546379 100644
--- a/Telegram/SourceFiles/stickers/stickers.style
+++ b/Telegram/SourceFiles/stickers/stickers.style
@@ -206,3 +206,8 @@ hashtagClose: IconButton {
 
 stickersToastMaxWidth: 340px;
 stickersToastPadding: margins(16px, 13px, 16px, 12px);
+
+inlineBotsScroll: ScrollArea(defaultSolidScroll) {
+	deltat: stickerPanPadding;
+	deltab: stickerPanPadding;
+}
diff --git a/Telegram/SourceFiles/structs.cpp b/Telegram/SourceFiles/structs.cpp
index 79e5f923b..f6c0dfb67 100644
--- a/Telegram/SourceFiles/structs.cpp
+++ b/Telegram/SourceFiles/structs.cpp
@@ -1641,7 +1641,7 @@ void DocumentData::notifyLayoutChanged() const {
 
 	if (auto items = InlineBots::Layout::documentItems()) {
 		for (auto item : items->value(const_cast<DocumentData*>(this))) {
-			Notify::inlineItemLayoutChanged(item);
+			item->layoutChanged();
 		}
 	}
 }
diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt
index 876cdcb68..6acac682f 100644
--- a/Telegram/gyp/telegram_sources.txt
+++ b/Telegram/gyp/telegram_sources.txt
@@ -125,6 +125,8 @@
 <(src_loc)/inline_bots/inline_bot_result.h
 <(src_loc)/inline_bots/inline_bot_send_data.cpp
 <(src_loc)/inline_bots/inline_bot_send_data.h
+<(src_loc)/inline_bots/inline_results_widget.cpp
+<(src_loc)/inline_bots/inline_results_widget.h
 <(src_loc)/intro/introwidget.cpp
 <(src_loc)/intro/introwidget.h
 <(src_loc)/intro/introcode.cpp