From 7f6cf32cdddeb037a92d8cdd2c514455c57caf40 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 29 Mar 2016 20:17:00 +0300
Subject: [PATCH] ITextLink moved to ClickHandler, TextLinkPtr >
 ClickHandlerPtr.

Global methods textlnkOver/Down/DrawOver were replaced by
static members of ClickHandler, now global state consists
of the handler pointer + host pointer, who declares callbacks
for the active and pressed handler changed events.

This will allow to use ClickHandler from different hosts
simultaneously (like HistoryItem / BotDescription / BotKeyboard).

Not yet tested.
---
 Telegram/Resources/lang.strings              |   4 +
 Telegram/SourceFiles/app.cpp                 |   4 +-
 Telegram/SourceFiles/audio.cpp               |   2 +-
 Telegram/SourceFiles/boxes/confirmbox.cpp    |  46 +-
 Telegram/SourceFiles/boxes/confirmbox.h      |   7 +-
 Telegram/SourceFiles/dropdown.cpp            | 159 ++++---
 Telegram/SourceFiles/dropdown.h              |   1 -
 Telegram/SourceFiles/facades.cpp             |  56 ++-
 Telegram/SourceFiles/facades.h               |   8 +-
 Telegram/SourceFiles/gui/flatlabel.cpp       |  43 +-
 Telegram/SourceFiles/gui/flatlabel.h         |   9 +-
 Telegram/SourceFiles/gui/images.cpp          |  12 +-
 Telegram/SourceFiles/gui/text.cpp            | 241 ++++++-----
 Telegram/SourceFiles/gui/text.h              | 363 ++++++++++------
 Telegram/SourceFiles/history.cpp             | 342 ++++++++-------
 Telegram/SourceFiles/history.h               | 347 ++++++++++------
 Telegram/SourceFiles/historywidget.cpp       | 415 +++++++++----------
 Telegram/SourceFiles/historywidget.h         |  55 ++-
 Telegram/SourceFiles/intro/introphone.cpp    |  13 +-
 Telegram/SourceFiles/layout.cpp              | 197 ++++-----
 Telegram/SourceFiles/layout.h                | 196 +++++----
 Telegram/SourceFiles/mainwidget.cpp          |  15 +-
 Telegram/SourceFiles/mainwidget.h            |   4 +-
 Telegram/SourceFiles/mediaview.cpp           |  61 ++-
 Telegram/SourceFiles/mediaview.h             |  11 +-
 Telegram/SourceFiles/mtproto/scheme.tl       |   5 +
 Telegram/SourceFiles/mtproto/scheme_auto.cpp |  63 +++
 Telegram/SourceFiles/mtproto/scheme_auto.h   | 224 ++++++++++
 Telegram/SourceFiles/overviewwidget.cpp      | 184 +++-----
 Telegram/SourceFiles/overviewwidget.h        |   8 +-
 Telegram/SourceFiles/playerwidget.cpp        |   2 +-
 Telegram/SourceFiles/profilewidget.cpp       |  68 +--
 Telegram/SourceFiles/profilewidget.h         |  10 +-
 Telegram/SourceFiles/settingswidget.cpp      |  14 +-
 Telegram/SourceFiles/settingswidget.h        |   2 +-
 Telegram/SourceFiles/structs.cpp             |  57 +--
 Telegram/SourceFiles/structs.h               | 149 +++----
 Telegram/SourceFiles/types.h                 |   5 +
 Telegram/SourceFiles/window.cpp              |  15 +-
 Telegram/SourceFiles/window.h                |   5 +-
 40 files changed, 1976 insertions(+), 1446 deletions(-)

diff --git a/Telegram/Resources/lang.strings b/Telegram/Resources/lang.strings
index c5b317fdd..36b8f38c0 100644
--- a/Telegram/Resources/lang.strings
+++ b/Telegram/Resources/lang.strings
@@ -601,6 +601,10 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 "lng_forwarded_signed" = "{channel} ({user})";
 "lng_in_reply_to" = "In reply to";
 
+"lng_bot_share_location_unavailable" = "Sorry, the location sharing is currently unavailable in Telegram Desktop.";
+"lng_bot_share_phone" = "Share phone?";
+"lng_bot_share_phone_confirm" = "Share";
+
 "lng_attach_failed" = "Failed";
 "lng_attach_file" = "File";
 "lng_attach_photo" = "Photo";
diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp
index 1edee0a39..1713a564e 100644
--- a/Telegram/SourceFiles/app.cpp
+++ b/Telegram/SourceFiles/app.cpp
@@ -2040,8 +2040,8 @@ namespace App {
 	}
 
 	void clearHistories() {
-		textlnkOver(TextLinkPtr());
-		textlnkDown(TextLinkPtr());
+		ClickHandler::clearActive();
+		ClickHandler::unpressed();
 
 		histories().clear();
 
diff --git a/Telegram/SourceFiles/audio.cpp b/Telegram/SourceFiles/audio.cpp
index 29205c7af..49a536ed1 100644
--- a/Telegram/SourceFiles/audio.cpp
+++ b/Telegram/SourceFiles/audio.cpp
@@ -506,7 +506,7 @@ void AudioPlayer::play(const SongMsgId &song, int64 position) {
 		if (current->file.isEmpty() && current->data.isEmpty()) {
 			setStoppedState(current);
 			if (!song.song->loading()) {
-				DocumentOpenLink::doOpen(song.song);
+				DocumentOpenClickHandler::doOpen(song.song);
 			}
 		} else {
 			current->state = fadedStart ? AudioPlayerStarting : AudioPlayerPlaying;
diff --git a/Telegram/SourceFiles/boxes/confirmbox.cpp b/Telegram/SourceFiles/boxes/confirmbox.cpp
index 3e379aae0..53e02bb5e 100644
--- a/Telegram/SourceFiles/boxes/confirmbox.cpp
+++ b/Telegram/SourceFiles/boxes/confirmbox.cpp
@@ -83,33 +83,30 @@ void ConfirmBox::mouseMoveEvent(QMouseEvent *e) {
 void ConfirmBox::mousePressEvent(QMouseEvent *e) {
 	_lastMousePos = e->globalPos();
 	updateHover();
-	if (textlnkOver()) {
-		textlnkDown(textlnkOver());
-		update();
-	}
+	ClickHandler::pressed();
 	return LayeredWidget::mousePressEvent(e);
 }
 
 void ConfirmBox::mouseReleaseEvent(QMouseEvent *e) {
 	_lastMousePos = e->globalPos();
 	updateHover();
-	if (textlnkOver() && textlnkOver() == textlnkDown()) {
+	if (ClickHandlerPtr activated = ClickHandler::unpressed()) {
 		Ui::hideLayer();
-		textlnkOver()->onClick(e->button());
+		App::activateClickHandler(activated, e->button());
 	}
-	textlnkDown(TextLinkPtr());
 }
 
 void ConfirmBox::leaveEvent(QEvent *e) {
-	if (_myLink) {
-		if (textlnkOver() == _myLink) {
-			textlnkOver(TextLinkPtr());
-			update();
-		}
-		_myLink = TextLinkPtr();
-		setCursor(style::cur_default);
-		update();
-	}
+	ClickHandler::clearActive(this);
+}
+
+void ConfirmBox::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
+	setCursor(active ? style::cur_pointer : style::cur_default);
+	update();
+}
+
+void ConfirmBox::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
+	update();
 }
 
 void ConfirmBox::updateLink() {
@@ -119,17 +116,12 @@ void ConfirmBox::updateLink() {
 
 void ConfirmBox::updateHover() {
 	QPoint m(mapFromGlobal(_lastMousePos));
-	bool wasMy = (_myLink == textlnkOver());
+
 	textstyleSet(&st::boxTextStyle);
-	_myLink = _text.linkLeft(m.x() - st::boxPadding.left(), m.y() - st::boxPadding.top(), _textWidth, width(), (_text.maxWidth() < width()) ? style::al_center : style::al_left);
+	ClickHandlerPtr handler = _text.linkLeft(m.x() - st::boxPadding.left(), m.y() - st::boxPadding.top(), _textWidth, width(), (_text.maxWidth() < width()) ? style::al_center : style::al_left);
 	textstyleRestore();
-	if (_myLink != textlnkOver()) {
-		if (wasMy || _myLink || rect().contains(m)) {
-			textlnkOver(_myLink);
-		}
-		setCursor(_myLink ? style::cur_pointer : style::cur_default);
-		update();
-	}
+
+	ClickHandler::setActive(handler, this);
 }
 
 void ConfirmBox::closePressed() {
@@ -182,9 +174,9 @@ ConfirmLinkBox::ConfirmLinkBox(const QString &url) : ConfirmBox(lang(lng_open_th
 void ConfirmLinkBox::onOpenLink() {
 	Ui::hideLayer();
 	if (reMailStart().match(_url).hasMatch()) {
-		EmailLink(_url).onClick(Qt::LeftButton);
+		EmailClickHandler::doOpen(_url);
 	} else {
-		TextLink(_url).onClick(Qt::LeftButton);
+		UrlClickHandler::doOpen(_url);
 	}
 }
 
diff --git a/Telegram/SourceFiles/boxes/confirmbox.h b/Telegram/SourceFiles/boxes/confirmbox.h
index 05b8febf3..0931b238f 100644
--- a/Telegram/SourceFiles/boxes/confirmbox.h
+++ b/Telegram/SourceFiles/boxes/confirmbox.h
@@ -23,7 +23,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 #include "abstractbox.h"
 
 class InformBox;
-class ConfirmBox : public AbstractBox {
+class ConfirmBox : public AbstractBox, public ClickHandlerHost {
 	Q_OBJECT
 
 public:
@@ -38,6 +38,10 @@ public:
 	void leaveEvent(QEvent *e);
 	void updateLink();
 
+	// ClickHandlerHost interface
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active);
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed);
+
 public slots:
 
 	void onCancel();
@@ -69,7 +73,6 @@ private:
 	void updateHover();
 
 	QPoint _lastMousePos;
-	TextLinkPtr _myLink;
 
 	BoxButton _confirm, _cancel;
 };
diff --git a/Telegram/SourceFiles/dropdown.cpp b/Telegram/SourceFiles/dropdown.cpp
index ee475348b..23b138a26 100644
--- a/Telegram/SourceFiles/dropdown.cpp
+++ b/Telegram/SourceFiles/dropdown.cpp
@@ -1413,8 +1413,7 @@ void StickerPanInner::mousePressEvent(QMouseEvent *e) {
 	updateSelected();
 
 	_pressedSel = _selected;
-	textlnkDown(textlnkOver());
-	_linkDown = _linkOver;
+	ClickHandler::pressed();
 	_previewTimer.start(QApplication::startDragTime());
 }
 
@@ -1422,10 +1421,9 @@ void StickerPanInner::mouseReleaseEvent(QMouseEvent *e) {
 	_previewTimer.stop();
 
 	int32 pressed = _pressedSel;
-	TextLinkPtr down(_linkDown);
 	_pressedSel = -1;
-	_linkDown.reset();
-	textlnkDown(TextLinkPtr());
+
+	ClickHandlerPtr activated = ClickHandler::unpressed();
 
 	_lastMousePos = e->globalPos();
 	updateSelected();
@@ -1435,71 +1433,71 @@ void StickerPanInner::mouseReleaseEvent(QMouseEvent *e) {
 		return;
 	}
 
-	if (_selected < 0 || _selected != pressed || _linkOver != down) return;
+	if (_selected < 0 || _selected != pressed || (_showingInlineItems && !activated)) return;
 	if (_showingInlineItems) {
-		int32 row = _selected / MatrixRowShift, col = _selected % MatrixRowShift;
-		if (row < _inlineRows.size() && col < _inlineRows.at(row).items.size()) {
-			if (down) {
-				if (down->type() == qstr("SendInlineItemLink") && e->button() == Qt::LeftButton) {
-					LayoutInlineItem *item = _inlineRows.at(row).items.at(col);
-					PhotoData *photo = item->photo();
-					DocumentData *doc = item->document();
-					InlineResult *result = item->result();
-					if (doc) {
-						if (doc->loaded()) {
-							emit selected(doc);
-						} else if (doc->loading()) {
-							doc->cancel();
-						} else {
-							DocumentOpenLink::doOpen(doc, ActionOnLoadNone);
-						}
-					} else if (photo) {
-						if (photo->medium->loaded() || photo->thumb->loaded()) {
-							emit selected(photo);
-						} else if (!photo->medium->loading()) {
-							photo->thumb->loadEvenCancelled();
-							photo->medium->loadEvenCancelled();
-						}
-					} else if (result) {
-						if (result->type == qstr("gif")) {
-							if (result->doc) {
-								if (result->doc->loaded()) {
-									emit selected(result, _inlineBot);
-								} else if (result->doc->loading()) {
-									result->doc->cancel();
-								} else {
-									DocumentOpenLink::doOpen(result->doc, ActionOnLoadNone);
-								}
-							} else if (result->loaded()) {
-								emit selected(result, _inlineBot);
-							} else if (result->loading()) {
-								result->cancelFile();
-								Ui::repaintInlineItem(item);
-							} else {
-								result->saveFile(QString(), LoadFromCloudOrLocal, false);
-								Ui::repaintInlineItem(item);
-							}
-						} else if (result->type == qstr("photo")) {
-							if (result->photo) {
-								if (result->photo->medium->loaded() || result->photo->thumb->loaded()) {
-									emit selected(result, _inlineBot);
-								} else if (!result->photo->medium->loading()) {
-									result->photo->thumb->loadEvenCancelled();
-									result->photo->medium->loadEvenCancelled();
-								}
-							} else if (result->thumb->loaded()) {
-								emit selected(result, _inlineBot);
-							} else if (!result->thumb->loading()) {
-								result->thumb->loadEvenCancelled();
-								Ui::repaintInlineItem(item);
-							}
-						} else {
-							emit selected(result, _inlineBot);
-						}
+		if (!dynamic_cast<SendInlineItemClickHandler*>(activated.data())) {
+			App::activateClickHandler(activated, e->button());
+			return;
+		}
+		int row = _selected / MatrixRowShift, col = _selected % MatrixRowShift;
+		if (row >= _inlineRows.size() || col >= _inlineRows.at(row).items.size()) {
+			return;
+		}
+
+		LayoutInlineItem *item = _inlineRows.at(row).items.at(col);
+		PhotoData *photo = item->photo();
+		DocumentData *doc = item->document();
+		InlineResult *result = item->result();
+		if (doc) {
+			if (doc->loaded()) {
+				emit selected(doc);
+			} else if (doc->loading()) {
+				doc->cancel();
+			} else {
+				DocumentOpenClickHandler::doOpen(doc, ActionOnLoadNone);
+			}
+		} else if (photo) {
+			if (photo->medium->loaded() || photo->thumb->loaded()) {
+				emit selected(photo);
+			} else if (!photo->medium->loading()) {
+				photo->thumb->loadEvenCancelled();
+				photo->medium->loadEvenCancelled();
+			}
+		} else if (result) {
+			if (result->type == qstr("gif")) {
+				if (result->doc) {
+					if (result->doc->loaded()) {
+						emit selected(result, _inlineBot);
+					} else if (result->doc->loading()) {
+						result->doc->cancel();
+					} else {
+						DocumentOpenClickHandler::doOpen(result->doc, ActionOnLoadNone);
 					}
+				} else if (result->loaded()) {
+					emit selected(result, _inlineBot);
+				} else if (result->loading()) {
+					result->cancelFile();
+					Ui::repaintInlineItem(item);
 				} else {
-					down->onClick(e->button());
+					result->saveFile(QString(), LoadFromCloudOrLocal, false);
+					Ui::repaintInlineItem(item);
 				}
+			} else if (result->type == qstr("photo")) {
+				if (result->photo) {
+					if (result->photo->medium->loaded() || result->photo->thumb->loaded()) {
+						emit selected(result, _inlineBot);
+					} else if (!result->photo->medium->loading()) {
+						result->photo->thumb->loadEvenCancelled();
+						result->photo->medium->loadEvenCancelled();
+					}
+				} else if (result->thumb->loaded()) {
+					emit selected(result, _inlineBot);
+				} else if (!result->thumb->loading()) {
+					result->thumb->loadEvenCancelled();
+					Ui::repaintInlineItem(item);
+				}
+			} else {
+				emit selected(result, _inlineBot);
 			}
 		}
 		return;
@@ -1578,11 +1576,7 @@ void StickerPanInner::clearSelection(bool fast) {
 			if (_selected >= 0) {
 				int32 srow = _selected / MatrixRowShift, scol = _selected % MatrixRowShift;
 				t_assert(srow >= 0 && srow < _inlineRows.size() && scol >= 0 && scol < _inlineRows.at(srow).items.size());
-				if (_linkOver) {
-					_inlineRows.at(srow).items.at(scol)->linkOut(_linkOver);
-					_linkOver = TextLinkPtr();
-					textlnkOver(_linkOver);
-				}
+				ClickHandler::clearActive(_inlineRows.at(srow).items.at(scol));
 				setCursor(style::cur_default);
 			}
 			_selected = _pressedSel = -1;
@@ -2197,7 +2191,8 @@ void StickerPanInner::updateSelected() {
 	if (_showingInlineItems) {
 		int sx = (rtl() ? width() - p.x() : p.x()) - st::inlineResultsLeft, sy = p.y() - st::emojiPanHeader;
 		int32 row = -1, col = -1, sel = -1;
-		TextLinkPtr lnk;
+		ClickHandlerPtr lnk;
+		ClickHandlerHost *lnkhost = nullptr;
 		HistoryCursorState cursor = HistoryDefaultCursorState;
 		if (sy >= 0) {
 			row = 0;
@@ -2221,6 +2216,7 @@ void StickerPanInner::updateSelected() {
 			if (col < inlineItems.size()) {
 				sel = row * MatrixRowShift + col;
 				inlineItems.at(col)->getState(lnk, cursor, sx, sy);
+				lnkhost = inlineItems.at(col);
 			} else {
 				row = col = -1;
 			}
@@ -2246,23 +2242,8 @@ void StickerPanInner::updateSelected() {
 				}
 			}
 		}
-		if (_linkOver != lnk) {
-			if (_linkOver && srow >= 0 && scol >= 0) {
-				t_assert(srow >= 0 && srow < _inlineRows.size() && scol >= 0 && scol < _inlineRows.at(srow).items.size());
-				_inlineRows.at(srow).items.at(scol)->linkOut(_linkOver);
-				Ui::repaintInlineItem(_inlineRows.at(srow).items.at(scol));
-			}
-			if ((_linkOver && !lnk) || (!_linkOver && lnk)) {
-				setCursor(lnk ? style::cur_pointer : style::cur_default);
-			}
-			_linkOver = lnk;
-			textlnkOver(lnk);
-			if (_linkOver && row >= 0 && col >= 0) {
-				t_assert(row >= 0 && row < _inlineRows.size() && col >= 0 && col < _inlineRows.at(row).items.size());
-				_inlineRows.at(row).items.at(col)->linkOver(_linkOver);
-				Ui::repaintInlineItem(_inlineRows.at(row).items.at(col));
-			}
-
+		if (ClickHandler::setActive(lnk, lnkhost)) {
+			setCursor(lnk ? style::cur_pointer : style::cur_default);
 		}
 		return;
 	}
diff --git a/Telegram/SourceFiles/dropdown.h b/Telegram/SourceFiles/dropdown.h
index f4d18ae30..45d0939d1 100644
--- a/Telegram/SourceFiles/dropdown.h
+++ b/Telegram/SourceFiles/dropdown.h
@@ -469,7 +469,6 @@ private:
 	int32 validateExistingInlineRows(const InlineResults &results);
 	int32 _selected, _pressedSel;
 	QPoint _lastMousePos;
-	TextLinkPtr _linkOver, _linkDown;
 
 	LinkButton _settings;
 
diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp
index 9e955b272..3200ad4fa 100644
--- a/Telegram/SourceFiles/facades.cpp
+++ b/Telegram/SourceFiles/facades.cpp
@@ -24,16 +24,22 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 #include "mainwidget.h"
 #include "application.h"
 
+#include "boxes/confirmbox.h"
+
 #include "layerwidget.h"
 #include "lang.h"
 
-Q_DECLARE_METATYPE(TextLinkPtr);
+Q_DECLARE_METATYPE(ClickHandlerPtr);
 Q_DECLARE_METATYPE(Qt::MouseButton);
 
 namespace App {
 
-	void sendBotCommand(const QString &cmd, MsgId replyTo) {
-		if (MainWidget *m = main()) m->sendBotCommand(cmd, replyTo);
+	void sendBotCommand(PeerData *peer, const QString &cmd, MsgId replyTo) {
+		if (MainWidget *m = main()) m->sendBotCommand(peer, cmd, replyTo);
+	}
+
+	void sendBotCallback(PeerData *peer, const QString &cmd, MsgId replyTo) {
+		if (MainWidget *m = main()) m->sendBotCallback(peer, cmd, replyTo);
 	}
 
 	bool insertBotCommand(const QString &cmd, bool specialGif) {
@@ -41,13 +47,36 @@ namespace App {
 		return false;
 	}
 
-	void activateBotCommand(const HistoryMessageReplyMarkup::Button &button, MsgId replyTo) {
-		QString cmd(button.text);
-		App::sendBotCommand(cmd, replyTo);
+	void activateBotCommand(PeerData *peer, const HistoryMessageReplyMarkup::Button &button, MsgId replyTo) {
+		switch (button.type) {
+		case HistoryMessageReplyMarkup::Button::Default: {
+			// copy string before passing it to the sending method
+			// the original button can be destroyed inside
+			sendBotCommand(peer, QString(button.text), replyTo);
+		} break;
+
+		case HistoryMessageReplyMarkup::Button::Callback: {
+			sendBotCallback(peer, QString(button.text), replyTo);
+		} break;
+
+		case HistoryMessageReplyMarkup::Button::Url: {
+			HiddenUrlClickHandler(button.url).onClick(Qt::LeftButton);
+		} break;
+
+		case HistoryMessageReplyMarkup::Button::RequestLocation: {
+			Ui::showLayer(new InformBox(lang(lng_bot_share_location_unavailable)));
+		} break;
+
+		case HistoryMessageReplyMarkup::Button::RequestPhone: {
+			ConfirmBox *box = new ConfirmBox(lang(lng_bot_share_phone), lang(lng_bot_share_phone_confirm));
+			box->connect(box, SIGNAL(confirmed()), App::main(), SLOT(onShareBotLocation()));
+			Ui::showLayer(box);
+		} break;
+		}
 	}
 
 	void searchByHashtag(const QString &tag, PeerData *inPeer) {
-		if (MainWidget *m = main()) m->searchMessages(tag + ' ', (inPeer && inPeer->isChannel()) ? inPeer : 0);
+		if (MainWidget *m = main()) m->searchMessages(tag + ' ', (inPeer && inPeer->isChannel() && !inPeer->isMegagroup()) ? inPeer : 0);
 	}
 
 	void openPeerByName(const QString &username, MsgId msgId, const QString &startToken) {
@@ -83,11 +112,11 @@ namespace App {
 		}
 	}
 
-	void activateTextLink(TextLinkPtr link, Qt::MouseButton button) {
+	void activateClickHandler(ClickHandlerPtr handler, Qt::MouseButton button) {
 		if (Window *w = wnd()) {
-			qRegisterMetaType<TextLinkPtr>();
+			qRegisterMetaType<ClickHandlerPtr>();
 			qRegisterMetaType<Qt::MouseButton>();
-			QMetaObject::invokeMethod(w, "app_activateTextLink", Qt::QueuedConnection, Q_ARG(TextLinkPtr, link), Q_ARG(Qt::MouseButton, button));
+			QMetaObject::invokeMethod(w, "app_activateClickHandler", Qt::QueuedConnection, Q_ARG(ClickHandlerPtr, handler), Q_ARG(Qt::MouseButton, button));
 		}
 	}
 
@@ -165,6 +194,13 @@ namespace Ui {
 		}
 	}
 
+	PeerData *getPeerForMouseAction() {
+		if (Window *w = App::wnd()) {
+			return w->ui_getPeerForMouseAction();
+		}
+		return nullptr;
+	}
+
 	bool hideWindowNoQuit() {
 		if (!App::quitting()) {
 			if (Window *w = App::wnd()) {
diff --git a/Telegram/SourceFiles/facades.h b/Telegram/SourceFiles/facades.h
index 462299987..207e3a693 100644
--- a/Telegram/SourceFiles/facades.h
+++ b/Telegram/SourceFiles/facades.h
@@ -24,9 +24,10 @@ class LayeredWidget;
 
 namespace App {
 
-	void sendBotCommand(const QString &cmd, MsgId replyTo = 0);
+	void sendBotCommand(PeerData *peer, const QString &cmd, MsgId replyTo = 0);
+	void sendBotCallback(PeerData *peer, const QString &cmd, MsgId replyTo);
 	bool insertBotCommand(const QString &cmd, bool specialGif = false);
-	void activateBotCommand(const HistoryMessageReplyMarkup::Button &button, MsgId replyTo = 0);
+	void activateBotCommand(PeerData *peer, const HistoryMessageReplyMarkup::Button &button, MsgId replyTo = 0);
 	void searchByHashtag(const QString &tag, PeerData *inPeer);
 	void openPeerByName(const QString &username, MsgId msgId = ShowAtUnreadMsgId, const QString &startToken = QString());
 	void joinGroupByHash(const QString &hash);
@@ -36,7 +37,7 @@ namespace App {
 	void removeDialog(History *history);
 	void showSettings();
 
-	void activateTextLink(TextLinkPtr link, Qt::MouseButton button);
+	void activateClickHandler(ClickHandlerPtr handler, Qt::MouseButton button);
 
 };
 
@@ -73,6 +74,7 @@ namespace Ui {
 	inline void showChatsListAsync() {
 		showPeerHistoryAsync(PeerId(0), 0);
 	}
+	PeerData *getPeerForMouseAction();
 
 	bool hideWindowNoQuit();
 
diff --git a/Telegram/SourceFiles/gui/flatlabel.cpp b/Telegram/SourceFiles/gui/flatlabel.cpp
index 8255ff1c2..9e3676b3b 100644
--- a/Telegram/SourceFiles/gui/flatlabel.cpp
+++ b/Telegram/SourceFiles/gui/flatlabel.cpp
@@ -60,7 +60,7 @@ void FlatLabel::resizeToWidth(int32 width) {
 	resize(w, h);
 }
 
-void FlatLabel::setLink(uint16 lnkIndex, const TextLinkPtr &lnk) {
+void FlatLabel::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) {
 	_text.setLink(lnkIndex, lnk);
 }
 
@@ -72,30 +72,28 @@ void FlatLabel::mouseMoveEvent(QMouseEvent *e) {
 void FlatLabel::mousePressEvent(QMouseEvent *e) {
 	_lastMousePos = e->globalPos();
 	updateHover();
-	if (textlnkOver()) {
-		textlnkDown(textlnkOver());
-		update();
-	}
+	ClickHandler::pressed();
 }
 
 void FlatLabel::mouseReleaseEvent(QMouseEvent *e) {
 	_lastMousePos = e->globalPos();
 	updateHover();
-	if (textlnkOver() && textlnkOver() == textlnkDown()) {
-		textlnkOver()->onClick(e->button());
+	if (ClickHandlerPtr activated = ClickHandler::unpressed()) {
+		App::activateClickHandler(activated, e->button());
 	}
-	textlnkDown(TextLinkPtr());
 }
 
 void FlatLabel::leaveEvent(QEvent *e) {
-	if (_myLink) {
-		if (textlnkOver() == _myLink) {
-			textlnkOver(TextLinkPtr());
-			update();
-		}
-		_myLink = TextLinkPtr();
-		setCursor(style::cur_default);
-	}
+	ClickHandler::clearActive(this);
+}
+
+void FlatLabel::clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) {
+	setCursor(active ? style::cur_pointer : style::cur_default);
+	update();
+}
+
+void FlatLabel::clickHandlerPressedChanged(const ClickHandlerPtr &action, bool active) {
+	update();
 }
 
 void FlatLabel::updateLink() {
@@ -105,17 +103,12 @@ void FlatLabel::updateLink() {
 
 void FlatLabel::updateHover() {
 	QPoint m(mapFromGlobal(_lastMousePos));
-	bool wasMy = (_myLink == textlnkOver());
+
 	textstyleSet(&_tst);
-	_myLink = _text.link(m.x(), m.y(), width(), _st.align);
+	ClickHandlerPtr handler = _text.link(m.x(), m.y(), width(), _st.align);
 	textstyleRestore();
-	if (_myLink != textlnkOver()) {
-		if (wasMy || _myLink || rect().contains(m)) {
-			textlnkOver(_myLink);
-		}
-		setCursor(_myLink ? style::cur_pointer : style::cur_default);
-		update();
-	}
+
+	ClickHandler::setActive(handler, this);
 }
 
 void FlatLabel::setOpacity(float64 o) {
diff --git a/Telegram/SourceFiles/gui/flatlabel.h b/Telegram/SourceFiles/gui/flatlabel.h
index b06e3bff5..4c3a60571 100644
--- a/Telegram/SourceFiles/gui/flatlabel.h
+++ b/Telegram/SourceFiles/gui/flatlabel.h
@@ -22,7 +22,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 
 #include "style.h"
 
-class FlatLabel : public TWidget {
+class FlatLabel : public TWidget, public ClickHandlerHost {
 	Q_OBJECT
 
 public:
@@ -42,7 +42,11 @@ public:
 
 	void resizeToWidth(int32 width);
 
-	void setLink(uint16 lnkIndex, const TextLinkPtr &lnk);
+	void setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk);
+
+	// ClickHandlerHost interface
+	void clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) override;
+	void clickHandlerPressedChanged(const ClickHandlerPtr &action, bool pressed) override;
 
 private:
 
@@ -54,6 +58,5 @@ private:
 	float64 _opacity;
 
 	QPoint _lastMousePos;
-	TextLinkPtr _myLink;
 
 };
diff --git a/Telegram/SourceFiles/gui/images.cpp b/Telegram/SourceFiles/gui/images.cpp
index de9dbeee6..296409424 100644
--- a/Telegram/SourceFiles/gui/images.cpp
+++ b/Telegram/SourceFiles/gui/images.cpp
@@ -1045,7 +1045,7 @@ FileLocation::FileLocation(StorageFileType type, const QString &name) : type(typ
 			qint64 s = f.size();
 			if (s > INT_MAX) {
 				fname = QString();
-				_bookmark.reset(0);
+				_bookmark.clear();
 				size = 0;
 				type = StorageFileUnknown;
 			} else {
@@ -1054,7 +1054,7 @@ FileLocation::FileLocation(StorageFileType type, const QString &name) : type(typ
 			}
 		} else {
 			fname = QString();
-			_bookmark.reset(0);
+			_bookmark.clear();
 			size = 0;
 			type = StorageFileUnknown;
 		}
@@ -1066,7 +1066,7 @@ bool FileLocation::check() const {
 
 	ReadAccessEnabler enabler(_bookmark);
 	if (enabler.failed()) {
-		const_cast<FileLocation*>(this)->_bookmark.reset(0);
+		const_cast<FileLocation*>(this)->_bookmark.clear();
 	}
 
 	QFileInfo f(name());
@@ -1087,11 +1087,7 @@ QByteArray FileLocation::bookmark() const {
 }
 
 void FileLocation::setBookmark(const QByteArray &bm) {
-	if (bm.isEmpty()) {
-		_bookmark.reset(0);
-	} else {
-		_bookmark.reset(new PsFileBookmark(bm));
-	}
+	_bookmark.reset(bm.isEmpty() ? nullptr : new PsFileBookmark(bm));
 }
 
 bool FileLocation::accessEnable() const {
diff --git a/Telegram/SourceFiles/gui/text.cpp b/Telegram/SourceFiles/gui/text.cpp
index f350a1f3e..63c363e30 100644
--- a/Telegram/SourceFiles/gui/text.cpp
+++ b/Telegram/SourceFiles/gui/text.cpp
@@ -44,8 +44,6 @@ namespace {
 
 	const style::textStyle *_textStyle = 0;
 
-	TextLinkPtr _overLnk, _downLnk, _zeroLnk;
-
 	void _initDefault() {
 		_textStyle = &st::defaultTextStyle;
 	}
@@ -59,6 +57,49 @@ namespace {
 	}
 }
 
+ClickHandlerHost::~ClickHandlerHost() {
+	ClickHandler::hostDestroyed(this);
+}
+
+ClickHandlerPtr *ClickHandler::_active = nullptr;
+ClickHandlerPtr *ClickHandler::_pressed = nullptr;
+ClickHandlerHost *ClickHandler::_activeHost = nullptr;
+ClickHandlerHost *ClickHandler::_pressedHost = nullptr;
+
+bool ClickHandler::setActive(const ClickHandlerPtr &p, ClickHandlerHost *host) {
+	if ((_active && (*_active == p)) || (!_active && !p)) {
+		return false;
+	}
+
+	// emit clickHandlerActiveChanged only when there is no
+	// other pressed click handler currently, if there is
+	// this method will be called when it is unpressed
+	if (_active && *_active) {
+		bool emitClickHandlerActiveChanged = (!_pressed || !*_pressed || *_pressed == *_active);
+		ClickHandlerPtr wasactive = *_active;
+		(*_active).clear();
+		if (_activeHost) {
+			if (emitClickHandlerActiveChanged) {
+				_activeHost->clickHandlerActiveChanged(wasactive, false);
+			}
+			_activeHost = nullptr;
+		}
+	}
+	if (p) {
+		if (!_active) {
+			_active = new ClickHandlerPtr(); // won't be deleted
+		}
+		*_active = p;
+		if ((_activeHost = host)) {
+			bool emitClickHandlerActiveChanged = (!_pressed || !*_pressed || *_pressed == *_active);
+			if (emitClickHandlerActiveChanged) {
+				_activeHost->clickHandlerActiveChanged(*_active, true);
+			}
+		}
+	}
+	return true;
+}
+
 const QRegularExpression &reDomain() {
 	return _reDomain;
 }
@@ -87,26 +128,6 @@ void textstyleSet(const style::textStyle *style) {
 	_textStyle = style ? style : &st::defaultTextStyle;
 }
 
-void textlnkOver(const TextLinkPtr &lnk) {
-	_overLnk = lnk;
-}
-
-const TextLinkPtr &textlnkOver() {
-	return _overLnk;
-}
-
-void textlnkDown(const TextLinkPtr &lnk) {
-	_downLnk = lnk;
-}
-
-const TextLinkPtr &textlnkDown() {
-	return _downLnk;
-}
-
-bool textlnkDrawOver(const TextLinkPtr &lnk) {
-	return (_overLnk == lnk) && (!_downLnk || _downLnk == lnk);
-}
-
 QString textOneLine(const QString &text, bool trim, bool rich) {
 	QString result(text);
 	const QChar *s = text.unicode(), *ch = s, *e = text.unicode() + text.size();
@@ -766,31 +787,31 @@ public:
 				if (_t->_links.size() < lnkIndex) {
 					_t->_links.resize(lnkIndex);
 					const TextLinkData &data(links[lnkIndex - maxLnkIndex - 1]);
-					TextLinkPtr lnk;
+					ClickHandlerPtr lnk;
 					if (data.fullDisplayed < -4) { // hidden link
-						lnk = TextLinkPtr(new CustomTextLink(data.url));
+						lnk.reset(new HiddenUrlClickHandler(data.url));
 					} else if (data.fullDisplayed < -3) { // bot command
-						lnk = TextLinkPtr(new BotCommandLink(data.url));
+						lnk.reset(new BotCommandClickHandler(data.url));
 					} else if (data.fullDisplayed < -2) { // mention
 						if (options.flags & TextTwitterMentions) {
-							lnk = TextLinkPtr(new TextLink(qsl("https://twitter.com/") + data.url.mid(1), true));
+							lnk.reset(new UrlClickHandler(qsl("https://twitter.com/") + data.url.mid(1), true));
 						} else if (options.flags & TextInstagramMentions) {
-							lnk = TextLinkPtr(new TextLink(qsl("https://instagram.com/") + data.url.mid(1) + '/', true));
+							lnk.reset(new UrlClickHandler(qsl("https://instagram.com/") + data.url.mid(1) + '/', true));
 						} else {
-							lnk = TextLinkPtr(new MentionLink(data.url));
+							lnk.reset(new MentionClickHandler(data.url));
 						}
 					} else if (data.fullDisplayed < -1) { // hashtag
 						if (options.flags & TextTwitterMentions) {
-							lnk = TextLinkPtr(new TextLink(qsl("https://twitter.com/hashtag/") + data.url.mid(1) + qsl("?src=hash"), true));
+							lnk.reset(new UrlClickHandler(qsl("https://twitter.com/hashtag/") + data.url.mid(1) + qsl("?src=hash"), true));
 						} else if (options.flags & TextInstagramMentions) {
-							lnk = TextLinkPtr(new TextLink(qsl("https://instagram.com/explore/tags/") + data.url.mid(1) + '/', true));
+							lnk.reset(new UrlClickHandler(qsl("https://instagram.com/explore/tags/") + data.url.mid(1) + '/', true));
 						} else {
-							lnk = TextLinkPtr(new HashtagLink(data.url));
+							lnk.reset(new HashtagClickHandler(data.url));
 						}
 					} else if (data.fullDisplayed < 0) { // email
-						lnk = TextLinkPtr(new EmailLink(data.url));
+						lnk.reset(new EmailClickHandler(data.url));
 					} else {
-						lnk = TextLinkPtr(new TextLink(data.url, data.fullDisplayed > 0));
+						lnk.reset(new UrlClickHandler(data.url, data.fullDisplayed > 0));
 					}
 					_t->setLink(lnkIndex, lnk);
 				}
@@ -937,78 +958,98 @@ namespace {
 	}
 }
 
-void TextLink::onClick(Qt::MouseButton button) const {
-	if (button == Qt::LeftButton || button == Qt::MiddleButton) {
-		PopupTooltip::Hide();
+QString UrlClickHandler::copyToClipboardContextItem() const {
+	return lang(lng_context_copy_link);
+}
 
-		QString url = TextLink::encoded();
-		QRegularExpressionMatch telegramMeUser = QRegularExpression(qsl("^https?://telegram\\.me/([a-zA-Z0-9\\.\\_]+)(/?\\?|/?$|/(\\d+)/?(?:\\?|$))"), QRegularExpression::CaseInsensitiveOption).match(url);
-		QRegularExpressionMatch telegramMeGroup = QRegularExpression(qsl("^https?://telegram\\.me/joinchat/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"), QRegularExpression::CaseInsensitiveOption).match(url);
-		QRegularExpressionMatch telegramMeStickers = QRegularExpression(qsl("^https?://telegram\\.me/addstickers/([a-zA-Z0-9\\.\\_]+)(\\?|$)"), QRegularExpression::CaseInsensitiveOption).match(url);
-		QRegularExpressionMatch telegramMeShareUrl = QRegularExpression(qsl("^https?://telegram\\.me/share/url\\?(.+)$"), QRegularExpression::CaseInsensitiveOption).match(url);
-		if (telegramMeGroup.hasMatch()) {
-			url = qsl("tg://join?invite=") + myUrlEncode(telegramMeGroup.captured(1));
-		} else if (telegramMeStickers.hasMatch()) {
-			url = qsl("tg://addstickers?set=") + myUrlEncode(telegramMeStickers.captured(1));
-		} else if (telegramMeShareUrl.hasMatch()) {
-			url = qsl("tg://msg_url?") + telegramMeShareUrl.captured(1);
-		} else if (telegramMeUser.hasMatch()) {
-			QString params = url.mid(telegramMeUser.captured(0).size()), postParam;
-			if (QRegularExpression(qsl("^/\\d+/?(?:\\?|$)")).match(telegramMeUser.captured(2)).hasMatch()) {
-				postParam = qsl("&post=") + telegramMeUser.captured(3);
-			}
-			url = qsl("tg://resolve/?domain=") + myUrlEncode(telegramMeUser.captured(1)) + postParam + (params.isEmpty() ? QString() : '&' + params);
-		}
+void UrlClickHandler::doOpen(QString url) {
+	PopupTooltip::Hide();
 
-		if (QRegularExpression(qsl("^tg://[a-zA-Z0-9]+"), QRegularExpression::CaseInsensitiveOption).match(url).hasMatch()) {
-			App::openLocalUrl(url);
-		} else {
-			QDesktopServices::openUrl(url);
+	QRegularExpressionMatch telegramMeUser = QRegularExpression(qsl("^https?://telegram\\.me/([a-zA-Z0-9\\.\\_]+)(/?\\?|/?$|/(\\d+)/?(?:\\?|$))"), QRegularExpression::CaseInsensitiveOption).match(url);
+	QRegularExpressionMatch telegramMeGroup = QRegularExpression(qsl("^https?://telegram\\.me/joinchat/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"), QRegularExpression::CaseInsensitiveOption).match(url);
+	QRegularExpressionMatch telegramMeStickers = QRegularExpression(qsl("^https?://telegram\\.me/addstickers/([a-zA-Z0-9\\.\\_]+)(\\?|$)"), QRegularExpression::CaseInsensitiveOption).match(url);
+	QRegularExpressionMatch telegramMeShareUrl = QRegularExpression(qsl("^https?://telegram\\.me/share/url\\?(.+)$"), QRegularExpression::CaseInsensitiveOption).match(url);
+	if (telegramMeGroup.hasMatch()) {
+		url = qsl("tg://join?invite=") + myUrlEncode(telegramMeGroup.captured(1));
+	} else if (telegramMeStickers.hasMatch()) {
+		url = qsl("tg://addstickers?set=") + myUrlEncode(telegramMeStickers.captured(1));
+	} else if (telegramMeShareUrl.hasMatch()) {
+		url = qsl("tg://msg_url?") + telegramMeShareUrl.captured(1);
+	} else if (telegramMeUser.hasMatch()) {
+		QString params = url.mid(telegramMeUser.captured(0).size()), postParam;
+		if (QRegularExpression(qsl("^/\\d+/?(?:\\?|$)")).match(telegramMeUser.captured(2)).hasMatch()) {
+			postParam = qsl("&post=") + telegramMeUser.captured(3);
 		}
+		url = qsl("tg://resolve/?domain=") + myUrlEncode(telegramMeUser.captured(1)) + postParam + (params.isEmpty() ? QString() : '&' + params);
+	}
+
+	if (QRegularExpression(qsl("^tg://[a-zA-Z0-9]+"), QRegularExpression::CaseInsensitiveOption).match(url).hasMatch()) {
+		App::openLocalUrl(url);
+	} else {
+		QDesktopServices::openUrl(url);
 	}
 }
 
-void EmailLink::onClick(Qt::MouseButton button) const {
-	if (button == Qt::LeftButton || button == Qt::MiddleButton) {
-		PopupTooltip::Hide();
-		QUrl url(qstr("mailto:") + _email);
-		if (!QDesktopServices::openUrl(url)) {
-			psOpenFile(url.toString(QUrl::FullyEncoded), true);
-		}
+QString EmailClickHandler::copyToClipboardContextItem() const {
+	return lang(lng_context_copy_email);
+}
+
+void EmailClickHandler::doOpen(QString email) {
+	PopupTooltip::Hide();
+
+	QUrl url(qstr("mailto:") + email);
+	if (!QDesktopServices::openUrl(url)) {
+		psOpenFile(url.toString(QUrl::FullyEncoded), true);
 	}
 }
 
-void CustomTextLink::onClick(Qt::MouseButton button) const {
-	Ui::showLayer(new ConfirmLinkBox(text()));
+void HiddenUrlClickHandler::onClick(Qt::MouseButton button) const {
+	Ui::showLayer(new ConfirmLinkBox(url()));
 }
 
-void LocationLink::onClick(Qt::MouseButton button) const {
+QString LocationClickHandler::copyToClipboardContextItem() const {
+	return lang(lng_context_copy_link);
+}
+
+void LocationClickHandler::onClick(Qt::MouseButton button) const {
 	if (!psLaunchMaps(_coords)) {
 		QDesktopServices::openUrl(_text);
 	}
 }
 
-void LocationLink::setup() {
+void LocationClickHandler::setup() {
 	QString latlon(qsl("%1,%2").arg(_coords.lat).arg(_coords.lon));
 	_text = qsl("https://maps.google.com/maps?q=") + latlon + qsl("&ll=") + latlon + qsl("&z=16");
 }
 
-void MentionLink::onClick(Qt::MouseButton button) const {
+QString MentionClickHandler::copyToClipboardContextItem() const {
+	return lang(lng_context_copy_mention);
+}
+
+void MentionClickHandler::onClick(Qt::MouseButton button) const {
 	if (button == Qt::LeftButton || button == Qt::MiddleButton) {
 		App::openPeerByName(_tag.mid(1), ShowAtProfileMsgId);
 	}
 }
 
-void HashtagLink::onClick(Qt::MouseButton button) const {
+QString HashtagClickHandler::copyToClipboardContextItem() const {
+	return lang(lng_context_copy_hashtag);
+}
+
+void HashtagClickHandler::onClick(Qt::MouseButton button) const {
 	if (button == Qt::LeftButton || button == Qt::MiddleButton) {
-		App::searchByHashtag(_tag, App::mousedItem() ? App::mousedItem()->history()->peer : 0);
+		App::searchByHashtag(_tag, Ui::getPeerForMouseAction());
 	}
 }
 
-void BotCommandLink::onClick(Qt::MouseButton button) const {
+void BotCommandClickHandler::onClick(Qt::MouseButton button) const {
 	if (button == Qt::LeftButton || button == Qt::MiddleButton) {
-//		App::insertBotCommand(_cmd);
-		App::sendBotCommand(_cmd);
+		if (PeerData *peer = Ui::getPeerForMouseAction()) {
+			Ui::showPeerHistory(peer, ShowAtTheEndMsgId);
+			App::sendBotCommand(peer, _cmd);
+		} else {
+			App::insertBotCommand(_cmd);
+		}
 	}
 }
 
@@ -1291,18 +1332,19 @@ public:
 		draw(left, top, w, align, yFrom, yTo);
 	}
 
-	const TextLinkPtr &link(int32 x, int32 y, int32 w, style::align align) {
+	const ClickHandlerPtr &link(int32 x, int32 y, int32 w, style::align align) {
+		static const ClickHandlerPtr *zero = new ClickHandlerPtr(); // won't be deleted
 		_lnkX = x;
 		_lnkY = y;
-		_lnkResult = &_zeroLnk;
+		_lnkResult = zero;
 		if (!_t->isNull() && _lnkX >= 0 && _lnkX < w && _lnkY >= 0) {
 			draw(0, 0, w, align, _lnkY, _lnkY + 1);
 		}
 		return *_lnkResult;
 	}
 
-	void getState(TextLinkPtr &lnk, bool &inText, int32 x, int32 y, int32 w, style::align align, bool breakEverywhere) {
-		lnk = TextLinkPtr();
+	void getState(ClickHandlerPtr &lnk, bool &inText, int32 x, int32 y, int32 w, style::align align, bool breakEverywhere) {
+		lnk.clear();
 		inText = false;
 
 		if (!_t->isNull() && x >= 0 && x < w && y >= 0) {
@@ -1335,11 +1377,8 @@ public:
 			return block->color()->p;
 		}
 		if (block->lnkIndex()) {
-			const TextLinkPtr &l(_t->_links.at(block->lnkIndex() - 1));
-			if (l == _overLnk) {
-				if (l == _downLnk) {
-					return _textStyle->linkFgDown->p;
-				}
+			if (ClickHandler::showAsPressed(_t->_links.at(block->lnkIndex() - 1))) {
+				return _textStyle->linkFgDown->p;
 			}
 			return _textStyle->linkFg->p;
 		}
@@ -1900,15 +1939,14 @@ public:
 			newFont = applyFlags(flags, _t->_font);
 		}
 		if (block->lnkIndex()) {
-			const TextLinkPtr &l(_t->_links.at(block->lnkIndex() - 1));
-			if (l == _overLnk) {
-				if (l == _downLnk || !_downLnk) {
-					if (_t->_font != _textStyle->linkFlagsOver) newFont = _textStyle->linkFlagsOver;
-				} else {
-					if (_t->_font != _textStyle->linkFlags) newFont = _textStyle->linkFlags;
+			if (ClickHandler::showAsActive(_t->_links.at(block->lnkIndex() - 1))) {
+				if (_t->_font != _textStyle->linkFlagsOver) {
+					newFont = _textStyle->linkFlagsOver;
 				}
 			} else {
-				if (_t->_font != _textStyle->linkFlags) newFont = _textStyle->linkFlags;
+				if (_t->_font != _textStyle->linkFlags) {
+					newFont = _textStyle->linkFlags;
+				}
 			}
 		}
 		if (newFont != _f) {
@@ -2502,7 +2540,7 @@ private:
 	// link and symbol resolve
 	QFixed _lnkX;
 	int32 _lnkY;
-	const TextLinkPtr *_lnkResult;
+	const ClickHandlerPtr *_lnkResult;
 	bool *_inTextFlag;
 	uint16 *_getSymbol;
 	bool *_getSymbolAfter, *_getSymbolUpon;
@@ -2784,7 +2822,7 @@ void Text::setRichText(style::font font, const QString &text, TextParseOptions o
 	setText(font, parsed, options);
 }
 
-void Text::setLink(uint16 lnkIndex, const TextLinkPtr &lnk) {
+void Text::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) {
 	if (!lnkIndex || lnkIndex > _links.size()) return;
 	_links[lnkIndex - 1] = lnk;
 }
@@ -3048,12 +3086,12 @@ void Text::drawElided(QPainter &painter, int32 left, int32 top, int32 w, int32 l
 	p.drawElided(left, top, w, align, lines, yFrom, yTo, removeFromEnd, breakEverywhere);
 }
 
-const TextLinkPtr &Text::link(int32 x, int32 y, int32 width, style::align align) const {
+const ClickHandlerPtr &Text::link(int32 x, int32 y, int32 width, style::align align) const {
 	TextPainter p(0, this);
 	return p.link(x, y, width, align);
 }
 
-void Text::getState(TextLinkPtr &lnk, bool &inText, int32 x, int32 y, int32 width, style::align align, bool breakEverywhere) const {
+void Text::getState(ClickHandlerPtr &lnk, bool &inText, int32 x, int32 y, int32 width, style::align align, bool breakEverywhere) const {
 	TextPainter p(0, this);
 	p.getState(lnk, inText, x, y, width, align, breakEverywhere);
 }
@@ -3102,7 +3140,7 @@ uint32 Text::adjustSelection(uint16 from, uint16 to, TextSelectType selectType)
 }
 
 QString Text::original(uint16 selectedFrom, uint16 selectedTo, ExpandLinksMode mode) const {
-	QString result;
+	QString result, emptyurl;
 	result.reserve(_text.size());
 
 	int32 lnkFrom = 0, lnkIndex = 0;
@@ -3111,14 +3149,14 @@ QString Text::original(uint16 selectedFrom, uint16 selectedTo, ExpandLinksMode m
 		int32 blockFrom = (i == e) ? _text.size() : (*i)->from();
 		if (blockLnkIndex != lnkIndex) {
 			if (lnkIndex) { // write link
-				const TextLinkPtr &lnk(_links.at(lnkIndex - 1));
-				const QString &url(lnk ? lnk->text() : QString());
+				const ClickHandlerPtr &lnk(_links.at(lnkIndex - 1));
+				const QString &url = (mode == ExpandLinksNone || !lnk) ? emptyurl : lnk->text();
 
 				int32 rangeFrom = qMax(int32(selectedFrom), lnkFrom), rangeTo = qMin(blockFrom, int32(selectedTo));
 
 				if (rangeTo > rangeFrom) {
 					QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom);
-					if (url.isEmpty() || mode == ExpandLinksNone || lnkFrom != rangeFrom || blockFrom != rangeTo) {
+					if (url.isEmpty() || lnkFrom != rangeFrom || blockFrom != rangeTo) {
 						result += r;
 					} else {
 						QUrl u(url);
@@ -3155,6 +3193,7 @@ QString Text::original(uint16 selectedFrom, uint16 selectedTo, ExpandLinksMode m
 
 EntitiesInText Text::originalEntities() const {
 	EntitiesInText result;
+	QString emptyurl;
 
 	int32 originalLength = 0, lnkStart = 0, italicStart = 0, boldStart = 0, codeStart = 0, preStart = 0;
 	int32 lnkFrom = 0, lnkIndex = 0, flags = 0;
@@ -3187,8 +3226,8 @@ EntitiesInText Text::originalEntities() const {
 		}
 		if (blockLnkIndex != lnkIndex) {
 			if (lnkIndex) { // write link
-				const TextLinkPtr &lnk(_links.at(lnkIndex - 1));
-				const QString &url(lnk ? lnk->text() : QString());
+				const ClickHandlerPtr &lnk(_links.at(lnkIndex - 1));
+				const QString &url(lnk ? lnk->text() : emptyurl);
 
 				int32 rangeFrom = lnkFrom, rangeTo = blockFrom;
 				if (rangeTo > rangeFrom) {
diff --git a/Telegram/SourceFiles/gui/text.h b/Telegram/SourceFiles/gui/text.h
index 7cb1e23a0..5d2307385 100644
--- a/Telegram/SourceFiles/gui/text.h
+++ b/Telegram/SourceFiles/gui/text.h
@@ -309,69 +309,199 @@ private:
 	friend class TextPainter;
 };
 
-class ITextLink {
-public:
+class ClickHandler;
+using ClickHandlerPtr = QSharedPointer<ClickHandler>;
 
-	virtual void onClick(Qt::MouseButton) const = 0;
-	virtual const QString &text() const {
-		static const QString _tmp;
-		return _tmp;
+class ClickHandlerHost {
+protected:
+
+	virtual void clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) {
 	}
-	virtual const QString &readable() const {
-		static const QString _tmp;
-		return _tmp;
-	}
-	virtual bool fullDisplayed() const {
-		return true;
-	}
-	virtual void setFullDisplayed(bool full) {
-	}
-	virtual QString encoded() const {
-		return QString();
-	}
-	virtual const QLatin1String &type() const = 0;
-	virtual ~ITextLink() {
+	virtual void clickHandlerPressedChanged(const ClickHandlerPtr &action, bool pressed) {
 	}
+	virtual ~ClickHandlerHost() = 0;
+	friend class ClickHandler;
 
 };
 
-#define TEXT_LINK_CLASS(ClassName) public: \
-const QLatin1String &type() const { \
-	static const QLatin1String _type(qstr(#ClassName)); \
-	return _type; \
-}
-
-typedef QSharedPointer<ITextLink> TextLinkPtr;
-
-class TextLink : public ITextLink {
-	TEXT_LINK_CLASS(TextLink)
-
+class ClickHandler {
 public:
 
-	TextLink(const QString &url, bool fullDisplayed = true) : _url(url), _fullDisplayed(fullDisplayed) {
-		QUrl u(_url), good(u.isValid() ? u.toEncoded() : QString());
-		_readable = good.isValid() ? good.toDisplayString() : _url;
+	virtual void onClick(Qt::MouseButton) const = 0;
+
+	virtual QString tooltip() const {
+		return QString();
+	}
+	virtual void copyToClipboard() const {
+	}
+	virtual QString copyToClipboardContextItem() const {
+		return QString();
+	}
+	virtual QString text() const {
+		return QString();
+	}
+	virtual QString dragText() const {
+		return text();
 	}
 
-	const QString &text() const {
-		return _url;
+	virtual ~ClickHandler() {
 	}
 
-	void onClick(Qt::MouseButton button) const;
+	// this method should be called on mouse over a click handler
+	// it returns true if something was changed or false otherwise
+	static bool setActive(const ClickHandlerPtr &p, ClickHandlerHost *host = nullptr);
 
-	const QString &readable() const {
-		return _readable;
+	// this method should be called when mouse leaves the host
+	// it returns true if something was changed or false otherwise
+	static bool clearActive(ClickHandlerHost *host = nullptr) {
+		if (host && _activeHost != host) {
+			return false;
+		}
+		return setActive(ClickHandlerPtr(), host);
 	}
 
-	bool fullDisplayed() const {
-		return _fullDisplayed;
+	// this method should be called on mouse pressed
+	static void pressed() {
+		unpressed();
+		if (!_active || !*_active) {
+			return;
+		}
+		if (!_pressed) {
+			_pressed = new ClickHandlerPtr(); // won't be deleted
+		}
+		*_pressed = *_active;
+		if ((_pressedHost = _activeHost)) {
+			_pressedHost->clickHandlerPressedChanged(*_pressed, true);
+		}
+	}
+
+	// this method should be called on mouse released
+	// the activated click handler is returned
+	static ClickHandlerPtr unpressed() {
+		if (_pressed && *_pressed) {
+			bool activated = (_active && *_active == *_pressed);
+			ClickHandlerPtr waspressed = *_pressed;
+			(*_pressed).clear();
+			if (_pressedHost) {
+				_pressedHost->clickHandlerPressedChanged(waspressed, false);
+				_pressedHost = nullptr;
+			}
+
+			if (activated) {
+				return *_active;
+			} else if (_active && *_active && _activeHost) {
+				// emit clickHandlerActiveChanged for current active
+				// click handler, which we didn't emit while we has
+				// a pressed click handler
+				_activeHost->clickHandlerActiveChanged(*_active, true);
+			}
+		}
+		return ClickHandlerPtr();
+	}
+
+	static ClickHandlerPtr getActive() {
+		return _active ? *_active : ClickHandlerPtr();
+	}
+	static ClickHandlerPtr getPressed() {
+		return _pressed ? *_pressed : ClickHandlerPtr();
+	}
+
+	static bool showAsActive(const ClickHandlerPtr &p) {
+		if (!p || !_active || p != *_active) {
+			return false;
+		}
+		return !_pressed || !*_pressed || (p == *_pressed);
+	}
+	static bool showAsPressed(const ClickHandlerPtr &p) {
+		if (!p || !_active || p != *_active) {
+			return false;
+		}
+		return _pressed && (p == *_pressed);
+	}
+	static void hostDestroyed(ClickHandlerHost *host) {
+		if (_activeHost == host) {
+			_activeHost = nullptr;
+		} else if (_pressedHost == host) {
+			_pressedHost = nullptr;
+		}
+	}
+
+private:
+
+	static ClickHandlerPtr *_active;
+	static ClickHandlerPtr *_pressed;
+	static ClickHandlerHost *_activeHost;
+	static ClickHandlerHost *_pressedHost;
+
+};
+
+class LeftButtonClickHandler : public ClickHandler {
+public:
+	void onClick(Qt::MouseButton button) const override final {
+		if (button != Qt::LeftButton) return;
+		onClickImpl();
+	}
+
+protected:
+	virtual void onClickImpl() const = 0;
+
+};
+
+class TextClickHandler : public ClickHandler {
+public:
+
+	TextClickHandler(bool fullDisplayed = true) : _fullDisplayed(fullDisplayed) {
+	}
+
+	void copyToClipboard() const override {
+		QString u = url();
+		if (!u.isEmpty()) {
+			QApplication::clipboard()->setText(u);
+		}
+	}
+
+	QString tooltip() const override {
+		return _fullDisplayed ? QString() : readable();
 	}
 
 	void setFullDisplayed(bool full) {
 		_fullDisplayed = full;
 	}
 
-	QString encoded() const {
+protected:
+	virtual QString url() const = 0;
+	virtual QString readable() const {
+		return url();
+	}
+
+	bool _fullDisplayed;
+
+};
+
+class UrlClickHandler : public TextClickHandler {
+public:
+	UrlClickHandler(const QString &url, bool fullDisplayed = true) : TextClickHandler(fullDisplayed), _url(url) {
+		QUrl u(_url), good(u.isValid() ? u.toEncoded() : QString());
+		_readable = good.isValid() ? good.toDisplayString() : _url;
+	}
+	QString copyToClipboardContextItem() const override;
+
+	QString text() const override {
+		return _url;
+	}
+	QString dragText() const override {
+		return url();
+	}
+
+	static void doOpen(QString url);
+	void onClick(Qt::MouseButton button) const override {
+		if (button == Qt::LeftButton || button == Qt::MiddleButton) {
+			doOpen(url());
+		}
+	}
+
+protected:
+	QString url() const override {
 		QUrl u(_url), good(u.isValid() ? u.toEncoded() : QString());
 		QString result(good.isValid() ? QString::fromUtf8(good.toEncoded()) : _url);
 
@@ -380,46 +510,47 @@ public:
 		}
 		return result;
 	}
+	QString readable() const override {
+		return _readable;
+	}
 
 private:
-
 	QString _url, _readable;
-	bool _fullDisplayed;
+
+};
+typedef QSharedPointer<TextClickHandler> TextClickHandlerPtr;
+
+class HiddenUrlClickHandler : public UrlClickHandler {
+public:
+	HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) {
+	}
+	void onClick(Qt::MouseButton button) const override;
 
 };
 
-class CustomTextLink : public TextLink {
+class EmailClickHandler : public TextClickHandler {
 public:
-
-	CustomTextLink(const QString &url) : TextLink(url, false) {
+	EmailClickHandler(const QString &email) : _email(email) {
 	}
-	void onClick(Qt::MouseButton button) const;
-};
+	QString copyToClipboardContextItem() const override;
 
-class EmailLink : public ITextLink {
-	TEXT_LINK_CLASS(EmailLink)
-
-public:
-
-	EmailLink(const QString &email) : _email(email) {
-	}
-
-	const QString &text() const {
+	QString text() const override {
 		return _email;
 	}
 
-	void onClick(Qt::MouseButton button) const;
-
-	const QString &readable() const {
-		return _email;
+	static void doOpen(QString email);
+	void onClick(Qt::MouseButton button) const override {
+		if (button == Qt::LeftButton || button == Qt::MiddleButton) {
+			doOpen(_email);
+		}
 	}
 
-	QString encoded() const {
+protected:
+	QString url() const override {
 		return _email;
 	}
 
 private:
-
 	QString _email;
 
 };
@@ -441,26 +572,20 @@ inline uint qHash(const LocationCoords &t, uint seed = 0) {
 	return qHash(QtPrivate::QHashCombine().operator()(qHash(t.lat), t.lon), seed);
 }
 
-class LocationLink : public ITextLink {
-	TEXT_LINK_CLASS(LocationLink)
-
+class LocationClickHandler : public TextClickHandler {
 public:
-
-	LocationLink(const LocationCoords &coords) : _coords(coords) {
+	LocationClickHandler(const LocationCoords &coords) : _coords(coords) {
 		setup();
 	}
+	QString copyToClipboardContextItem() const override;
 
-	const QString &text() const {
+	QString text() const override {
 		return _text;
 	}
+	void onClick(Qt::MouseButton button) const override;
 
-	void onClick(Qt::MouseButton button) const;
-
-	const QString &readable() const {
-		return _text;
-	}
-
-	QString encoded() const {
+protected:
+	QString url() const override {
 		return _text;
 	}
 
@@ -472,86 +597,63 @@ private:
 
 };
 
-class MentionLink : public ITextLink {
-	TEXT_LINK_CLASS(MentionLink)
-
+class MentionClickHandler : public TextClickHandler {
 public:
-
-	MentionLink(const QString &tag) : _tag(tag) {
+	MentionClickHandler(const QString &tag) : _tag(tag) {
 	}
+	QString copyToClipboardContextItem() const override;
 
-	const QString &text() const {
+	QString text() const override {
 		return _tag;
 	}
+	void onClick(Qt::MouseButton button) const override;
 
-	void onClick(Qt::MouseButton button) const;
-
-	const QString &readable() const {
-		return _tag;
-	}
-
-	QString encoded() const {
+protected:
+	QString url() const override {
 		return _tag;
 	}
 
 private:
-
 	QString _tag;
 
 };
 
-class HashtagLink : public ITextLink {
-	TEXT_LINK_CLASS(HashtagLink)
-
+class HashtagClickHandler : public TextClickHandler {
 public:
-
-	HashtagLink(const QString &tag) : _tag(tag) {
+	HashtagClickHandler(const QString &tag) : _tag(tag) {
 	}
+	QString copyToClipboardContextItem() const override;
 
-	const QString &text() const {
+	QString text() const override {
 		return _tag;
 	}
+	void onClick(Qt::MouseButton button) const override;
 
-	void onClick(Qt::MouseButton button) const;
-
-	const QString &readable() const {
-		return _tag;
-	}
-
-	QString encoded() const {
+protected:
+	QString url() const override {
 		return _tag;
 	}
 
 private:
-
 	QString _tag;
 
 };
 
-class BotCommandLink : public ITextLink {
-	TEXT_LINK_CLASS(BotCommandLink)
-
+class BotCommandClickHandler : public TextClickHandler {
 public:
-
-	BotCommandLink(const QString &cmd) : _cmd(cmd) {
+	BotCommandClickHandler(const QString &cmd) : _cmd(cmd) {
 	}
-
-	const QString &text() const {
+	QString text() const override {
 		return _cmd;
 	}
+	void onClick(Qt::MouseButton button) const override;
 
-	void onClick(Qt::MouseButton button) const;
-
-	const QString &readable() const {
-		return _cmd;
-	}
-
-	QString encoded() const {
+protected:
+	QString url() const override {
 		return _cmd;
 	}
 
 private:
-
 	QString _cmd;
 
 };
@@ -608,7 +710,7 @@ public:
 	void setRichText(style::font font, const QString &text, TextParseOptions options = _defaultOptions, const TextCustomTagsMap &custom = TextCustomTagsMap());
 	void setMarkedText(style::font font, const QString &text, const EntitiesInText &entities, const TextParseOptions &options = _defaultOptions);
 
-	void setLink(uint16 lnkIndex, const TextLinkPtr &lnk);
+	void setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk);
 	bool hasLinks() const;
 
 	bool hasSkipBlock() const {
@@ -641,12 +743,12 @@ public:
 		drawElided(p, rtl() ? right : (outerw - right - width), top, width, lines, align, yFrom, yTo, removeFromEnd, breakEverywhere);
 	}
 
-	const TextLinkPtr &link(int32 x, int32 y, int32 width, style::align align = style::al_left) const;
-	const TextLinkPtr &linkLeft(int32 x, int32 y, int32 width, int32 outerw, style::align align = style::al_left) const {
+	const ClickHandlerPtr &link(int32 x, int32 y, int32 width, style::align align = style::al_left) const;
+	const ClickHandlerPtr &linkLeft(int32 x, int32 y, int32 width, int32 outerw, style::align align = style::al_left) const {
 		return link(rtl() ? (outerw - x - width) : x, y, width, align);
 	}
-	void getState(TextLinkPtr &lnk, bool &inText, int32 x, int32 y, int32 width, style::align align = style::al_left, bool breakEverywhere = false) const;
-	void getStateLeft(TextLinkPtr &lnk, bool &inText, int32 x, int32 y, int32 width, int32 outerw, style::align align = style::al_left, bool breakEverywhere = false) const {
+	void getState(ClickHandlerPtr &lnk, bool &inText, int32 x, int32 y, int32 width, style::align align = style::al_left, bool breakEverywhere = false) const;
+	void getStateLeft(ClickHandlerPtr &lnk, bool &inText, int32 x, int32 y, int32 width, int32 outerw, style::align align = style::al_left, bool breakEverywhere = false) const {
 		return getState(lnk, inText, rtl() ? (outerw - x - width) : x, y, width, align, breakEverywhere);
 	}
 	void getSymbol(uint16 &symbol, bool &after, bool &upon, int32 x, int32 y, int32 width, style::align align = style::al_left) const;
@@ -710,7 +812,7 @@ private:
 	typedef QVector<ITextBlock*> TextBlocks;
 	TextBlocks _blocks;
 
-	typedef QVector<TextLinkPtr> TextLinks;
+	typedef QVector<ClickHandlerPtr> TextLinks;
 	TextLinks _links;
 
 	Qt::LayoutDirection _startDir;
@@ -737,15 +839,6 @@ inline void textstyleRestore() {
 	textstyleSet(0);
 }
 
-// textlnk
-void textlnkOver(const TextLinkPtr &lnk);
-const TextLinkPtr &textlnkOver();
-
-void textlnkDown(const TextLinkPtr &lnk);
-const TextLinkPtr &textlnkDown();
-
-bool textlnkDrawOver(const TextLinkPtr &lnk);
-
 // textcmd
 QString textcmdSkipBlock(ushort w, ushort h);
 QString textcmdStartLink(ushort lnkIndex);
diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp
index b123023be..7d853db86 100644
--- a/Telegram/SourceFiles/history.cpp
+++ b/Telegram/SourceFiles/history.cpp
@@ -1288,7 +1288,7 @@ HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction,
 			entities.push_front(EntityInText(EntityInTextItalic, 0, text.size()));
 			result = HistoryMessage::create(this, m.vid.v, m.vflags.v, m.vreply_to_msg_id.v, m.vvia_bot_id.v, date(m.vdate), m.vfrom_id.v, text, entities);
 		} else if (badMedia) {
-			result = HistoryService::create(this, m.vid.v, date(m.vdate), lang(lng_message_empty), m.vflags.v, nullptr, m.has_from_id() ? m.vfrom_id.v : 0);
+			result = HistoryService::create(this, m.vid.v, date(m.vdate), lang(lng_message_empty), m.vflags.v, m.has_from_id() ? m.vfrom_id.v : 0);
 		} else {
 			result = HistoryMessage::create(this, m);
 		}
@@ -1454,8 +1454,8 @@ HistoryItem *History::createItemPhoto(MsgId id, MTPDmessage::Flags flags, int32
 	return HistoryMessage::create(this, id, flags, replyTo, viaBotId, date, from, photo, caption);
 }
 
-HistoryItem *History::addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags, HistoryMedia *media, bool newMsg) {
-	return addNewItem(HistoryService::create(this, msgId, date, text, flags, media), newMsg);
+HistoryItem *History::addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags, bool newMsg) {
+	return addNewItem(HistoryService::create(this, msgId, date, text, flags), newMsg);
 }
 
 HistoryItem *History::addNewMessage(const MTPMessage &msg, NewMessageType type) {
@@ -2705,6 +2705,19 @@ void HistoryBlock::removeItem(HistoryItem *item) {
 	}
 }
 
+void ReplyMarkupClickHandler::onClickImpl() const {
+	if (HistoryItem *item = App::histItemById(_msgId)) {
+		if (auto *markup = item->Get<HistoryMessageReplyMarkup>()) {
+			if (_row < markup->rows.size()) {
+				const HistoryMessageReplyMarkup::ButtonRow &row(markup->rows.at(_row));
+				if (_col < row.size()) {
+					App::activateBotCommand(item->history()->peer, row.at(_col), _msgId.msg);
+				}
+			}
+		}
+	}
+}
+
 ReplyKeyboard::ReplyKeyboard(const HistoryItem *item, StylePtr &&s)
 : _item(item)
 , _a_selected(animation(this, &ReplyKeyboard::step_selected))
@@ -2718,7 +2731,7 @@ ReplyKeyboard::ReplyKeyboard(const HistoryItem *item, StylePtr &&s)
 			for (int j = 0; j != s; ++j) {
 				Button &button(newRow[j]);
 				QString str = row.at(j).text;
-				button.link.reset(new TextLink(qsl("https://telegram.org")));
+				button.link.reset(new ReplyMarkupClickHandler(item->fullId(), i, j));
 				button.text.setText(_st->textFont(), textOneLine(str), _textPlainOptions);
 				button.characters = str.isEmpty() ? 1 : str.size();
 			}
@@ -2793,17 +2806,15 @@ void ReplyKeyboard::paint(Painter &p, const QRect &clip) const {
 
 			if (rtl()) rect.moveLeft(_width - rect.left() - rect.width());
 
-			bool down = (textlnkDown() == button.link);
-			float64 howMuchOver = button.howMuchOver;
-			_st->paintButton(p, rect, button.text, down, howMuchOver);
+			_st->paintButton(p, rect, button.text, ClickHandler::showAsPressed(button.link), button.howMuchOver);
 		}
 	}
 }
 
-void ReplyKeyboard::getState(TextLinkPtr &lnk, int x, int y) const {
+void ReplyKeyboard::getState(ClickHandlerPtr &lnk, int x, int y) const {
 	t_assert(_width > 0);
 
-	lnk.reset();
+	lnk.clear();
 	for_const(const ButtonRow &row, _rows) {
 		for_const(const Button &button, row) {
 			QRect rect(button.rect);
@@ -2818,7 +2829,7 @@ void ReplyKeyboard::getState(TextLinkPtr &lnk, int x, int y) const {
 	}
 }
 
-void ReplyKeyboard::linkOver(const TextLinkPtr &lnk) {
+void ReplyKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
 	/*if (newSel != _sel) {
 		if (newSel < 0) {
 			setCursor(style::cur_default);
@@ -2845,7 +2856,7 @@ void ReplyKeyboard::linkOver(const TextLinkPtr &lnk) {
 	}*/
 }
 
-void ReplyKeyboard::linkOut(const TextLinkPtr &lnk) {
+void ReplyKeyboard::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
 
 }
 
@@ -2876,8 +2887,8 @@ void ReplyKeyboard::clearSelection() {
 	_a_selected.stop();
 }
 
-void ReplyKeyboard::Style::paintButton(Painter &p, const QRect &rect, const Text &text, bool down, float64 howMuchOver) const {
-	paintButtonBg(p, rect, down, howMuchOver);
+void ReplyKeyboard::Style::paintButton(Painter &p, const QRect &rect, const Text &text, bool pressed, float64 howMuchOver) const {
+	paintButtonBg(p, rect, pressed, howMuchOver);
 
 	int tx = rect.x(), tw = rect.width();
 	if (tw > st::botKbFont->elidew + _st->padding * 2) {
@@ -2887,7 +2898,7 @@ void ReplyKeyboard::Style::paintButton(Painter &p, const QRect &rect, const Text
 		tx += (tw - st::botKbFont->elidew) / 2;
 		tw = st::botKbFont->elidew;
 	}
-	int textTop = rect.y() + (down ? _st->downTextTop : _st->textTop);
+	int textTop = rect.y() + (pressed ? _st->downTextTop : _st->textTop);
 	text.drawElided(p, tx, textTop + ((rect.height() - _st->height) / 2), tw, 1, style::al_top);
 }
 
@@ -3006,6 +3017,17 @@ void HistoryMessageDate::paint(Painter &p, int y, int w) const {
 	p.drawText(left + st::msgServicePadding.left(), y + st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->ascent, _text);
 }
 
+void HistoryMediaPtr::reset(HistoryItem *host, HistoryMedia *p) {
+	if (_p) {
+		_p->detachFromItem(host);
+		delete _p;
+	}
+	_p = p;
+	if (_p) {
+		_p->attachToItem(host);
+	}
+}
+
 HistoryItem::HistoryItem(History *history, MsgId msgId, MTPDmessage::Flags flags, QDateTime msgDate, int32 from) : HistoryElem()
 , y(0)
 , id(msgId)
@@ -3020,6 +3042,26 @@ void HistoryItem::finishCreate() {
 	App::historyRegItem(this);
 }
 
+void HistoryItem::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
+	if (auto *markup = Get<HistoryMessageReplyMarkup>()) {
+		if (markup->inlineKeyboard) {
+			markup->inlineKeyboard->clickHandlerActiveChanged(p, active);
+		}
+	}
+	App::hoveredLinkItem(active ? this : nullptr);
+	Ui::repaintHistoryItem(this);
+}
+
+void HistoryItem::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
+	if (auto *markup = Get<HistoryMessageReplyMarkup>()) {
+		if (markup->inlineKeyboard) {
+			markup->inlineKeyboard->clickHandlerPressedChanged(p, pressed);
+		}
+	}
+	App::pressedLinkItem(pressed ? this : nullptr);
+	Ui::repaintHistoryItem(this);
+}
+
 void HistoryItem::destroy() {
 	bool wasAtBottom = history()->loadedAtBottom();
 	_history->removeNotification(this);
@@ -3310,25 +3352,27 @@ HistoryFileMedia::HistoryFileMedia() : HistoryMedia()
 , _animation(0) {
 }
 
-void HistoryFileMedia::linkOver(HistoryItem *parent, const TextLinkPtr &lnk) {
-	if ((lnk == _savel || lnk == _cancell) && !dataLoaded()) {
-		ensureAnimation(parent);
-		_animation->a_thumbOver.start(1);
-		_animation->_a_thumbOver.start();
+void HistoryFileMedia::clickHandlerActiveChanged(HistoryItem *parent, const ClickHandlerPtr &p, bool active) {
+	if (p == _savel || p == _cancell) {
+		if (active && !dataLoaded()) {
+			ensureAnimation(parent);
+			_animation->a_thumbOver.start(1);
+			_animation->_a_thumbOver.start();
+		} else if (!active && _animation) {
+			_animation->a_thumbOver.start(0);
+			_animation->_a_thumbOver.start();
+		}
 	}
 }
 
-void HistoryFileMedia::linkOut(HistoryItem *parent, const TextLinkPtr &lnk) {
-	if (_animation && (lnk == _savel || lnk == _cancell)) {
-		_animation->a_thumbOver.start(0);
-		_animation->_a_thumbOver.start();
-	}
+void HistoryFileMedia::clickHandlerPressedChanged(HistoryItem *parent, const ClickHandlerPtr &p, bool pressed) {
+	Ui::repaintHistoryItem(parent);
 }
 
-void HistoryFileMedia::setLinks(ITextLink *openl, ITextLink *savel, ITextLink *cancell) {
-	_openl.reset(openl);
-	_savel.reset(savel);
-	_cancell.reset(cancell);
+void HistoryFileMedia::setLinks(ClickHandlerPtr &&openl, ClickHandlerPtr &&savel, ClickHandlerPtr &&cancell) {
+	_openl = std_::move(openl);
+	_savel = std_::move(savel);
+	_cancell = std_::move(cancell);
 }
 
 void HistoryFileMedia::setStatusSize(int32 newSize, int32 fullSize, int32 duration, qint64 realDuration) const {
@@ -3394,10 +3438,8 @@ HistoryFileMedia::~HistoryFileMedia() {
 
 HistoryPhoto::HistoryPhoto(PhotoData *photo, const QString &caption, const HistoryItem *parent) : HistoryFileMedia()
 , _data(photo)
-, _pixw(1)
-, _pixh(1)
 , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) {
-	setLinks(new PhotoLink(_data), new PhotoSaveLink(_data), new PhotoCancelLink(_data));
+	setLinks(MakeShared<PhotoOpenClickHandler>(_data), MakeShared<PhotoSaveClickHandler>(_data), MakeShared<PhotoCancelClickHandler>(_data));
 
 	if (!caption.isEmpty()) {
 		_caption.setText(st::msgFont, caption + parent->skipBlock(), itemTextNoMonoOptions(parent));
@@ -3406,10 +3448,8 @@ HistoryPhoto::HistoryPhoto(PhotoData *photo, const QString &caption, const Histo
 }
 
 HistoryPhoto::HistoryPhoto(PeerData *chat, const MTPDphoto &photo, int32 width) : HistoryFileMedia()
-, _data(App::feedPhoto(photo))
-, _pixw(1)
-, _pixh(1) {
-	setLinks(new PhotoLink(_data, chat), new PhotoSaveLink(_data, chat), new PhotoCancelLink(_data));
+, _data(App::feedPhoto(photo)) {
+	setLinks(MakeShared<PhotoOpenClickHandler>(_data, chat), MakeShared<PhotoSaveClickHandler>(_data, chat), MakeShared<PhotoCancelClickHandler>(_data, chat));
 
 	_width = width;
 	init();
@@ -3420,7 +3460,7 @@ HistoryPhoto::HistoryPhoto(const HistoryPhoto &other) : HistoryFileMedia()
 , _pixw(other._pixw)
 , _pixh(other._pixh)
 , _caption(other._caption) {
-	setLinks(new PhotoLink(_data), new PhotoSaveLink(_data), new PhotoCancelLink(_data));
+	setLinks(MakeShared<PhotoOpenClickHandler>(_data), MakeShared<PhotoSaveClickHandler>(_data), MakeShared<PhotoCancelClickHandler>(_data));
 
 	init();
 }
@@ -3568,7 +3608,7 @@ void HistoryPhoto::draw(Painter &p, const HistoryItem *parent, const QRect &r, b
 			p.setOpacity((st::msgDateImgBg->c.alphaF() * (1 - over)) + (st::msgDateImgBgOver->c.alphaF() * over));
 			p.setBrush(st::black);
 		} else {
-			bool over = textlnkDrawOver(_data->loading() ? _cancell : _savel);
+			bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
 			p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg);
 		}
 
@@ -3612,7 +3652,7 @@ void HistoryPhoto::draw(Painter &p, const HistoryItem *parent, const QRect &r, b
 	}
 }
 
-void HistoryPhoto::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
+void HistoryPhoto::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
 	if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return;
 	int32 skipx = 0, skipy = 0, width = _width, height = _height;
 	bool bubble = parent->hasBubble();
@@ -3734,7 +3774,7 @@ HistoryVideo::HistoryVideo(DocumentData *document, const QString &caption, const
 		_caption.setText(st::msgFont, caption + parent->skipBlock(), itemTextNoMonoOptions(parent));
 	}
 
-	setLinks(new DocumentOpenLink(_data), new DocumentSaveLink(_data), new DocumentCancelLink(_data));
+	setDocumentLinks(_data);
 
 	setStatusSize(FileStatusSizeReady);
 
@@ -3745,7 +3785,7 @@ HistoryVideo::HistoryVideo(const HistoryVideo &other) : HistoryFileMedia()
 , _data(other._data)
 , _thumbw(other._thumbw)
 , _caption(other._caption) {
-	setLinks(new DocumentOpenLink(_data), new DocumentSaveLink(_data), new DocumentCancelLink(_data));
+	setDocumentLinks(_data);
 
 	setStatusSize(other._statusSize);
 }
@@ -3871,7 +3911,7 @@ void HistoryVideo::draw(Painter &p, const HistoryItem *parent, const QRect &r, b
 		p.setOpacity((st::msgDateImgBg->c.alphaF() * (1 - over)) + (st::msgDateImgBgOver->c.alphaF() * over));
 		p.setBrush(st::black);
 	} else {
-		bool over = textlnkDrawOver(_data->loading() ? _cancell : _savel);
+		bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
 		p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg);
 	}
 
@@ -3917,7 +3957,7 @@ void HistoryVideo::draw(Painter &p, const HistoryItem *parent, const QRect &r, b
 	}
 }
 
-void HistoryVideo::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
+void HistoryVideo::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
 	if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return;
 
 	bool loaded = _data->loaded();
@@ -4035,7 +4075,7 @@ HistoryDocument::HistoryDocument(DocumentData *document, const QString &caption,
 		named->_namew = st::semiboldFont->width(named->_name);
 	}
 
-	setLinks(new DocumentOpenLink(_data), _data->voice() ? (ITextLink*)(new VoiceSaveLink(_data)) : new DocumentSaveLink(_data), new DocumentCancelLink(_data));
+	setDocumentLinks(_data);
 
 	setStatusSize(FileStatusSizeReady);
 
@@ -4060,7 +4100,7 @@ HistoryDocument::HistoryDocument(const HistoryDocument &other) : HistoryFileMedi
 		}
 	}
 
-	setLinks(new DocumentOpenLink(_data), _data->voice() ? (ITextLink*)(new VoiceSaveLink(_data)) : new DocumentSaveLink(_data), new DocumentCancelLink(_data));
+	setDocumentLinks(_data);
 
 	setStatusSize(other._statusSize);
 
@@ -4084,8 +4124,8 @@ void HistoryDocument::createComponents(bool caption) {
 	}
 	UpdateComponents(mask);
 	if (auto *thumbed = Get<HistoryDocumentThumbed>()) {
-		thumbed->_linksavel.reset(new DocumentSaveLink(_data));
-		thumbed->_linkcancell.reset(new DocumentCancelLink(_data));
+		thumbed->_linksavel.reset(new DocumentSaveClickHandler(_data));
+		thumbed->_linkcancell.reset(new DocumentCancelClickHandler(_data));
 	}
 }
 
@@ -4206,7 +4246,7 @@ void HistoryDocument::draw(Painter &p, const HistoryItem *parent, const QRect &r
 				p.setOpacity((st::msgDateImgBg->c.alphaF() * (1 - over)) + (st::msgDateImgBgOver->c.alphaF() * over));
 				p.setBrush(st::black);
 			} else {
-				bool over = textlnkDrawOver(_data->loading() ? _cancell : _savel);
+				bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
 				p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg);
 			}
 			p.setOpacity(radialOpacity * p.opacity());
@@ -4233,8 +4273,8 @@ void HistoryDocument::draw(Painter &p, const HistoryItem *parent, const QRect &r
 		}
 
 		if (_data->status != FileUploadFailed) {
-			const TextLinkPtr &lnk((_data->loading() || _data->status == FileUploading) ? thumbed->_linkcancell : thumbed->_linksavel);
-			bool over = textlnkDrawOver(lnk);
+			const ClickHandlerPtr &lnk((_data->loading() || _data->status == FileUploading) ? thumbed->_linkcancell : thumbed->_linksavel);
+			bool over = ClickHandler::showAsActive(lnk);
 			p.setFont(over ? st::semiboldFont->underline() : st::semiboldFont);
 			p.setPen(outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg));
 			p.drawTextLeft(nameleft, linktop, _width, thumbed->_link, thumbed->_linkw);
@@ -4254,7 +4294,7 @@ void HistoryDocument::draw(Painter &p, const HistoryItem *parent, const QRect &r
 			float64 over = _animation->a_thumbOver.current();
 			p.setBrush(style::interpolate(outbg ? st::msgFileOutBg : st::msgFileInBg, outbg ? st::msgFileOutBgOver : st::msgFileInBgOver, over));
 		} else {
-			bool over = textlnkDrawOver(_data->loading() ? _cancell : _savel);
+			bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
 			p.setBrush(outbg ? (over ? st::msgFileOutBgOver : st::msgFileOutBg) : (over ? st::msgFileInBgOver : st::msgFileInBg));
 		}
 
@@ -4380,7 +4420,7 @@ void HistoryDocument::draw(Painter &p, const HistoryItem *parent, const QRect &r
 	}
 }
 
-void HistoryDocument::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
+void HistoryDocument::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
 	if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return;
 
 	bool out = parent->out(), isPost = parent->isPost(), outbg = out && !isPost;
@@ -4614,7 +4654,7 @@ HistoryGif::HistoryGif(DocumentData *document, const QString &caption, const His
 , _thumbh(1)
 , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right())
 , _gif(nullptr) {
-	setLinks(new GifOpenLink(_data), new GifOpenLink(_data), new DocumentCancelLink(_data));
+	setDocumentLinks(_data, true);
 
 	setStatusSize(FileStatusSizeReady);
 
@@ -4632,7 +4672,7 @@ HistoryGif::HistoryGif(const HistoryGif &other) : HistoryFileMedia()
 , _thumbh(other._thumbh)
 , _caption(other._caption)
 , _gif(nullptr) {
-	setLinks(new GifOpenLink(_data), new GifOpenLink(_data), new DocumentCancelLink(_data));
+	setDocumentLinks(_data, true);
 
 	setStatusSize(other._statusSize);
 }
@@ -4812,7 +4852,7 @@ void HistoryGif::draw(Painter &p, const HistoryItem *parent, const QRect &r, boo
 			p.setOpacity((st::msgDateImgBg->c.alphaF() * (1 - over)) + (st::msgDateImgBgOver->c.alphaF() * over));
 			p.setBrush(st::black);
 		} else {
-			bool over = textlnkDrawOver(_data->loading() ? _cancell : _savel);
+			bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
 			p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg);
 		}
 		p.setOpacity(radialOpacity * p.opacity());
@@ -4861,7 +4901,7 @@ void HistoryGif::draw(Painter &p, const HistoryItem *parent, const QRect &r, boo
 	}
 }
 
-void HistoryGif::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
+void HistoryGif::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
 	if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return;
 	int32 skipx = 0, skipy = 0, width = _width, height = _height;
 	bool bubble = parent->hasBubble();
@@ -5087,7 +5127,7 @@ void HistorySticker::draw(Painter &p, const HistoryItem *parent, const QRect &r,
 	}
 }
 
-void HistorySticker::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
+void HistorySticker::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
 	if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return;
 
 	bool out = parent->out(), isPost = parent->isPost(), outbg = out && !isPost;
@@ -5144,22 +5184,18 @@ void HistorySticker::updateFrom(const MTPMessageMedia &media, HistoryItem *paren
 	}
 }
 
-void SendMessageLink::onClick(Qt::MouseButton button) const {
-	if (button == Qt::LeftButton) {
-		Ui::showPeerHistory(peer()->id, ShowAtUnreadMsgId);
-	}
+void SendMessageClickHandler::onClickImpl() const {
+	Ui::showPeerHistory(peer()->id, ShowAtUnreadMsgId);
 }
 
-void AddContactLink::onClick(Qt::MouseButton button) const {
-	if (button == Qt::LeftButton) {
-		if (HistoryItem *item = App::histItemById(peerToChannel(peer()), msgid())) {
-			if (HistoryMedia *media = item->getMedia()) {
-				if (media->type() == MediaTypeContact) {
-					QString fname = static_cast<HistoryContact*>(media)->fname();
-					QString lname = static_cast<HistoryContact*>(media)->lname();
-					QString phone = static_cast<HistoryContact*>(media)->phone();
-					Ui::showLayer(new AddContactBox(fname, lname, phone));
-				}
+void AddContactClickHandler::onClickImpl() const {
+	if (HistoryItem *item = App::histItemById(peerToChannel(peer()), msgid())) {
+		if (HistoryMedia *media = item->getMedia()) {
+			if (media->type() == MediaTypeContact) {
+				QString fname = static_cast<HistoryContact*>(media)->fname();
+				QString lname = static_cast<HistoryContact*>(media)->lname();
+				QString phone = static_cast<HistoryContact*>(media)->phone();
+				Ui::showLayer(new AddContactBox(fname, lname, phone));
 			}
 		}
 	}
@@ -5186,10 +5222,10 @@ void HistoryContact::initDimensions(const HistoryItem *parent) {
 		_contact->loadUserpic();
 	}
 	if (_contact && _contact->contact > 0) {
-		_linkl.reset(new SendMessageLink(_contact));
+		_linkl.reset(new SendMessageClickHandler(_contact));
 		_link = lang(lng_profile_send_message).toUpper();
 	} else if (_userId) {
-		_linkl.reset(new AddContactLink(parent->history()->peer->id, parent->id));
+		_linkl.reset(new AddContactClickHandler(parent->history()->peer->id, parent->id));
 		_link = lang(lng_profile_add_contact).toUpper();
 	}
 	_linkw = _link.isEmpty() ? 0 : st::semiboldFont->width(_link);
@@ -5244,7 +5280,7 @@ void HistoryContact::draw(Painter &p, const HistoryItem *parent, const QRect &r,
 			App::roundRect(p, rthumb, textstyleCurrent()->selectOverlay, SelectedOverlayCorners);
 		}
 
-		bool over = textlnkDrawOver(_linkl);
+		bool over = ClickHandler::showAsActive(_linkl);
 		p.setFont(over ? st::semiboldFont->underline() : st::semiboldFont);
 		p.setPen(outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg));
 		p.drawTextLeft(nameleft, linktop, width, _link, _linkw);
@@ -5269,7 +5305,7 @@ void HistoryContact::draw(Painter &p, const HistoryItem *parent, const QRect &r,
 	p.drawTextLeft(nameleft, statustop, width, _phone);
 }
 
-void HistoryContact::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
+void HistoryContact::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
 	bool out = parent->out(), isPost = parent->isPost(), outbg = out && !isPost;
 
 	int32 nameleft = 0, nametop = 0, nameright = 0, statustop = 0, linktop = 0;
@@ -5282,7 +5318,7 @@ void HistoryContact::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32
 		}
 	}
 	if (x >= 0 && y >= 0 && x < _width && y < _height && _contact) {
-		lnk = _contact->lnk;
+		lnk = _contact->openLink();
 		return;
 	}
 }
@@ -5378,7 +5414,7 @@ void HistoryWebPage::initDimensions(const HistoryItem *parent) {
 	}
 	if (!_lineHeight) _lineHeight = qMax(st::webPageTitleFont->height, st::webPageDescriptionFont->height);
 
-	if (!_openl && !_data->url.isEmpty()) _openl = TextLinkPtr(new TextLink(_data->url));
+	if (!_openl && !_data->url.isEmpty()) _openl.reset(new UrlClickHandler(_data->url, true));
 
 	// init layout
 	QString title(_data->title.isEmpty() ? _data->author : _data->title);
@@ -5698,7 +5734,7 @@ void HistoryWebPage::draw(Painter &p, const HistoryItem *parent, const QRect &r,
 	}
 }
 
-void HistoryWebPage::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
+void HistoryWebPage::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
 	if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return;
 	int32 skipx = 0, skipy = 0, width = _width, height = _height;
 
@@ -5753,15 +5789,15 @@ void HistoryWebPage::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32
 	}
 }
 
-void HistoryWebPage::linkOver(HistoryItem *parent, const TextLinkPtr &lnk) {
+void HistoryWebPage::clickHandlerActiveChanged(HistoryItem *parent, const ClickHandlerPtr &p, bool active) {
 	if (_attach) {
-		_attach->linkOver(parent, lnk);
+		_attach->clickHandlerActiveChanged(parent, p, active);
 	}
 }
 
-void HistoryWebPage::linkOut(HistoryItem *parent, const TextLinkPtr &lnk) {
+void HistoryWebPage::clickHandlerPressedChanged(HistoryItem *parent, const ClickHandlerPtr &p, bool pressed) {
 	if (_attach) {
-		_attach->linkOut(parent, lnk);
+		_attach->clickHandlerPressedChanged(parent, p, pressed);
 	}
 }
 
@@ -5994,7 +6030,7 @@ _description(st::msgMinWidth) {
 		_description.setText(st::webPageDescriptionFont, textClean(description), _webpageDescriptionOptions);
 	}
 
-	_link.reset(new LocationLink(coords));
+	_link.reset(new LocationClickHandler(coords));
 	_data = App::location(coords);
 }
 
@@ -6134,7 +6170,7 @@ void HistoryLocation::draw(Painter &p, const HistoryItem *parent, const QRect &r
 	}
 }
 
-void HistoryLocation::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
+void HistoryLocation::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const {
 	if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return;
 	int32 skipx = 0, skipy = 0, width = _width, height = _height;
 	bool bubble = parent->hasBubble();
@@ -6192,14 +6228,14 @@ int32 HistoryLocation::fullHeight() const {
 	return st::locationSize.height();
 }
 
-void ViaInlineBotLink::onClick(Qt::MouseButton button) const {
+void ViaInlineBotClickHandler::onClickImpl() const {
 	App::insertBotCommand('@' + _bot->username);
 }
 
 void HistoryMessageVia::create(int32 userId) {
 	_bot = App::user(peerFromUser(userId));
 	_maxWidth = st::msgServiceNameFont->width(lng_inline_bot_via(lt_inline_bot, '@' + _bot->username));
-	_lnk.reset(new ViaInlineBotLink(_bot));
+	_lnk.reset(new ViaInlineBotClickHandler(_bot));
 }
 
 void HistoryMessageVia::resize(int32 availw) const {
@@ -6254,7 +6290,7 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const {
 	textstyleSet(&st::inFwdTextStyle);
 	_text.setText(st::msgServiceNameFont, text, opts);
 	textstyleRestore();
-	_text.setLink(1, (_originalId && _authorOriginal->isChannel()) ? TextLinkPtr(new MessageLink(_authorOriginal->id, _originalId)) : _authorOriginal->lnk);
+	_text.setLink(1, (_originalId && _authorOriginal->isChannel()) ? ClickHandlerPtr(new GoToMessageClickHandler(_authorOriginal->id, _originalId)) : _authorOriginal->openLink());
 	if (via) {
 		_text.setLink(2, via->_lnk);
 	}
@@ -6278,7 +6314,7 @@ bool HistoryMessageReply::updateData(HistoryMessage *holder, bool force) {
 
 		updateName();
 
-		replyToLnk = TextLinkPtr(new MessageLink(replyToMsg->history()->peer->id, replyToMsg->id));
+		replyToLnk.reset(new GoToMessageClickHandler(replyToMsg->history()->peer->id, replyToMsg->id));
 		if (!replyToMsg->Has<HistoryMessageForwarded>()) {
 			if (UserData *bot = replyToMsg->viaBot()) {
 				_replyToVia.reset(new HistoryMessageVia());
@@ -6457,8 +6493,7 @@ HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags fl
 	createComponents(config);
 
 	if (HistoryMedia *mediaOriginal = fwd->getMedia()) {
-		_media = mediaOriginal->clone();
-		_media->attachToItem(this);
+		_media.reset(this, mediaOriginal->clone());
 	}
 	setText(fwd->originalText(), fwd->originalEntities());
 }
@@ -6482,8 +6517,7 @@ HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags
 : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) {
 	createComponentsHelper(flags, replyTo, viaBotId);
 
-	_media = new HistoryPhoto(photo, caption, this);
-	_media->attachToItem(this);
+	_media.reset(this, new HistoryPhoto(photo, caption, this));
 	setText(QString(), EntitiesInText());
 }
 
@@ -6584,26 +6618,26 @@ void HistoryMessage::initMedia(const MTPMessageMedia *media, QString &currentTex
 	switch (media ? media->type() : mtpc_messageMediaEmpty) {
 	case mtpc_messageMediaContact: {
 		const MTPDmessageMediaContact &d(media->c_messageMediaContact());
-		_media = new HistoryContact(d.vuser_id.v, qs(d.vfirst_name), qs(d.vlast_name), qs(d.vphone_number));
+		_media.reset(this, new HistoryContact(d.vuser_id.v, qs(d.vfirst_name), qs(d.vlast_name), qs(d.vphone_number)));
 	} break;
 	case mtpc_messageMediaGeo: {
 		const MTPGeoPoint &point(media->c_messageMediaGeo().vgeo);
 		if (point.type() == mtpc_geoPoint) {
 			const MTPDgeoPoint &d(point.c_geoPoint());
-			_media = new HistoryLocation(LocationCoords(d.vlat.v, d.vlong.v));
+			_media.reset(this, new HistoryLocation(LocationCoords(d.vlat.v, d.vlong.v)));
 		}
 	} break;
 	case mtpc_messageMediaVenue: {
 		const MTPDmessageMediaVenue &d(media->c_messageMediaVenue());
 		if (d.vgeo.type() == mtpc_geoPoint) {
 			const MTPDgeoPoint &g(d.vgeo.c_geoPoint());
-			_media = new HistoryLocation(LocationCoords(g.vlat.v, g.vlong.v), qs(d.vtitle), qs(d.vaddress));
+			_media.reset(this, new HistoryLocation(LocationCoords(g.vlat.v, g.vlong.v), qs(d.vtitle), qs(d.vaddress)));
 		}
 	} break;
 	case mtpc_messageMediaPhoto: {
 		const MTPDmessageMediaPhoto &photo(media->c_messageMediaPhoto());
 		if (photo.vphoto.type() == mtpc_photo) {
-			_media = new HistoryPhoto(App::feedPhoto(photo.vphoto.c_photo()), qs(photo.vcaption), this);
+			_media.reset(this, new HistoryPhoto(App::feedPhoto(photo.vphoto.c_photo()), qs(photo.vcaption), this));
 		}
 	} break;
 	case mtpc_messageMediaDocument: {
@@ -6617,28 +6651,26 @@ void HistoryMessage::initMedia(const MTPMessageMedia *media, QString &currentTex
 		switch (d.type()) {
 		case mtpc_webPageEmpty: break;
 		case mtpc_webPagePending: {
-			_media = new HistoryWebPage(App::feedWebPage(d.c_webPagePending()));
+			_media.reset(this, new HistoryWebPage(App::feedWebPage(d.c_webPagePending())));
 		} break;
 		case mtpc_webPage: {
-			_media = new HistoryWebPage(App::feedWebPage(d.c_webPage()));
+			_media.reset(this, new HistoryWebPage(App::feedWebPage(d.c_webPage())));
 		} break;
 		}
 	} break;
 	};
-	if (_media) _media->attachToItem(this);
 }
 
 void HistoryMessage::initMediaFromDocument(DocumentData *doc, const QString &caption) {
 	if (doc->sticker()) {
-		_media = new HistorySticker(doc);
+		_media.reset(this, new HistorySticker(doc));
 	} else if (doc->isAnimation()) {
-		_media = new HistoryGif(doc, caption, this);
+		_media.reset(this, new HistoryGif(doc, caption, this));
 	} else if (doc->isVideo()) {
-		_media = new HistoryVideo(doc, caption, this);
+		_media.reset(this, new HistoryVideo(doc, caption, this));
 	} else {
-		_media = new HistoryDocument(doc, caption, this);
+		_media.reset(this, new HistoryDocument(doc, caption, this));
 	}
-	_media->attachToItem(this);
 }
 
 int32 HistoryMessage::plainMaxWidth() const {
@@ -6756,7 +6788,7 @@ int32 HistoryMessage::addToOverview(AddToOverviewMethod method) {
 	if (!indexInOverview()) return 0;
 
 	int32 result = 0;
-	if (HistoryMedia *media = getMedia(true)) {
+	if (HistoryMedia *media = getMedia()) {
 		MediaOverviewType type = mediaToOverviewType(media);
 		if (type != OverviewCount) {
 			if (history()->addToOverview(type, id, method)) {
@@ -6773,7 +6805,7 @@ int32 HistoryMessage::addToOverview(AddToOverviewMethod method) {
 }
 
 void HistoryMessage::eraseFromOverview() {
-	if (HistoryMedia *media = getMedia(true)) {
+	if (HistoryMedia *media = getMedia()) {
 		MediaOverviewType type = mediaToOverviewType(media);
 		if (type != OverviewCount) {
 			history()->eraseFromOverview(type, id);
@@ -6817,8 +6849,8 @@ QString HistoryMessage::inDialogsText() const {
 	return emptyText() ? (_media ? _media->inDialogsText() : QString()) : _text.original(0, 0xFFFF, Text::ExpandLinksNone);
 }
 
-HistoryMedia *HistoryMessage::getMedia(bool inOverview) const {
-	return _media;
+HistoryMedia *HistoryMessage::getMedia() const {
+	return _media.data();
 }
 
 void HistoryMessage::setMedia(const MTPMessageMedia *media) {
@@ -6827,10 +6859,7 @@ void HistoryMessage::setMedia(const MTPMessageMedia *media) {
 	bool mediaWasDisplayed = false;
 	if (_media) {
 		mediaWasDisplayed = _media->isDisplayed();
-
-		_media->detachFromItem(this);
-		delete _media;
-		_media = nullptr;
+		_media.clear(this);
 	}
 	QString t;
 	initMedia(media, t);
@@ -7271,8 +7300,8 @@ bool HistoryMessage::pointInTime(int32 right, int32 bottom, int32 x, int32 y, In
 	return QRect(dateX, dateY, HistoryMessage::timeWidth(), st::msgDateFont->height).contains(x, y);
 }
 
-void HistoryMessage::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const {
-	lnk = TextLinkPtr();
+void HistoryMessage::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const {
+	lnk.clear();
 	state = HistoryDefaultCursorState;
 
 	int left = 0, width = 0, height = _height;
@@ -7300,7 +7329,7 @@ void HistoryMessage::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32
 		if (displayFromName()) {
 			if (y >= trect.top() && y < trect.top() + st::msgNameFont->height) {
 				if (x >= trect.left() && x < trect.left() + trect.width() && x < trect.left() + author()->nameText.maxWidth()) {
-					lnk = author()->lnk;
+					lnk = author()->openLink();
 					return;
 				}
 				if (via && !fwd && x >= trect.left() + author()->nameText.maxWidth() + st::msgServiceFont->spacew && x < trect.left() + author()->nameText.maxWidth() + st::msgServiceFont->spacew + via->_width) {
@@ -7345,7 +7374,6 @@ void HistoryMessage::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32
 
 		bool inDate = false;
 
-		TextLinkPtr medialnk;
 		if (_media && _media->isDisplayed()) {
 			if (!_media->customInfoLayout()) {
 				inDate = HistoryMessage::pointInTime(r.x() + r.width(), r.y() + r.height(), x, y, InfoDisplayDefault);
@@ -7458,17 +7486,14 @@ bool HistoryMessage::hasFromPhoto() const {
 }
 
 HistoryMessage::~HistoryMessage() {
-	if (_media) {
-		_media->detachFromItem(this);
-		deleteAndMark(_media);
-	}
+	_media.clear(this);
 	if (auto *reply = Get<HistoryMessageReply>()) {
 		reply->clearData(this);
 	}
 }
 
 void HistoryService::setMessageByAction(const MTPmessageAction &action) {
-	QList<TextLinkPtr> links;
+	QList<ClickHandlerPtr> links;
 	LangString text = lang(lng_message_empty);
 	QString from = textcmdLink(1, _from->name);
 
@@ -7488,7 +7513,7 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
 			if (u == _from) {
 				text = lng_action_user_joined(lt_from, from);
 			} else {
-				links.push_back(TextLinkPtr(new PeerLink(u)));
+				links.push_back(MakeShared<PeerOpenClickHandler>(u));
 				text = lng_action_add_user(lt_from, from, lt_user, textcmdLink(2, u->name));
 			}
 		} else if (v.isEmpty()) {
@@ -7504,7 +7529,7 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
 				} else {
 					text = lng_action_add_users_and_last(lt_accumulated, text, lt_user, linkText);
 				}
-				links.push_back(TextLinkPtr(new PeerLink(u)));
+				links.push_back(MakeShared<PeerOpenClickHandler>(u));
 			}
 			text = lng_action_add_users_many(lt_from, from, lt_users, text);
 		}
@@ -7517,13 +7542,13 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
 
 	case mtpc_messageActionChatJoinedByLink: {
 		const MTPDmessageActionChatJoinedByLink &d(action.c_messageActionChatJoinedByLink());
-		if (true || peerFromUser(d.vinviter_id) == _from->id) {
+		//if (true || peerFromUser(d.vinviter_id) == _from->id) {
 			text = lng_action_user_joined_by_link(lt_from, from);
 		//} else {
-			//UserData *u = App::user(App::peerFromUser(d.vinviter_id));
-			//second = TextLinkPtr(new PeerLink(u));
-			//text = lng_action_user_joined_by_link_from(lt_from, from, lt_inviter, textcmdLink(2, u->name));
-		}
+		//	UserData *u = App::user(App::peerFromUser(d.vinviter_id));
+		//	links.push_back(MakeShared<PeerOpenClickHandler>(u));
+		//	text = lng_action_user_joined_by_link_from(lt_from, from, lt_inviter, textcmdLink(2, u->name));
+		//}
 		if (_from->isSelf() && history()->peer->isMegagroup()) {
 			history()->peer->asChannel()->mgInfo->joinedMessageFound = true;
 		}
@@ -7553,7 +7578,7 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
 			text = lng_action_user_left(lt_from, from);
 		} else {
 			UserData *u = App::user(peerFromUser(d.vuser_id));
-			links.push_back(TextLinkPtr(new PeerLink(u)));
+			links.push_back(MakeShared<PeerOpenClickHandler>(u));
 			text = lng_action_kick_user(lt_from, from, lt_user, textcmdLink(2, u->name));
 		}
 	} break;
@@ -7561,8 +7586,7 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
 	case mtpc_messageActionChatEditPhoto: {
 		const MTPDmessageActionChatEditPhoto &d(action.c_messageActionChatEditPhoto());
 		if (d.vphoto.type() == mtpc_photo) {
-			_media = new HistoryPhoto(history()->peer, d.vphoto.c_photo(), st::msgServicePhotoWidth);
-			_media->attachToItem(this);
+			_media.reset(this, new HistoryPhoto(history()->peer, d.vphoto.c_photo(), st::msgServicePhotoWidth));
 		}
 		text = isPost() ? lang(lng_action_changed_photo_channel) : lng_action_changed_photo(lt_from, from);
 	} break;
@@ -7608,7 +7632,7 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
 	_text.setText(st::msgServiceFont, text, _historySrvOptions);
 	textstyleRestore();
 	if (!from.isEmpty()) {
-		_text.setLink(1, TextLinkPtr(new PeerLink(_from)));
+		_text.setLink(1, MakeShared<PeerOpenClickHandler>(_from));
 	}
 	for (int32 i = 0, l = links.size(); i < l; ++i) {
 		_text.setLink(i + 2, links.at(i));
@@ -7626,7 +7650,7 @@ bool HistoryService::updatePinned(bool force) {
 	}
 
 	if (!pinned->lnk) {
-		pinned->lnk = TextLinkPtr(new MessageLink(history()->peer->id, pinned->msgId));
+		pinned->lnk.reset(new GoToMessageClickHandler(history()->peer->id, pinned->msgId));
 	}
 	bool gotDependencyItem = false;
 	if (!pinned->msg) {
@@ -7663,7 +7687,7 @@ bool HistoryService::updatePinnedText(const QString *pfrom, QString *ptext) {
 		from = textcmdLink(1, _from->name);
 	}
 
-	TextLinkPtr second;
+	ClickHandlerPtr second;
 	auto *pinned = Get<HistoryServicePinned>();
 	if (pinned && pinned->msg) {
 		HistoryMedia *media = pinned->msg->getMedia();
@@ -7711,7 +7735,7 @@ bool HistoryService::updatePinnedText(const QString *pfrom, QString *ptext) {
 		*ptext = text;
 	} else {
 		setServiceText(text);
-		_text.setLink(1, TextLinkPtr(new PeerLink(_from)));
+		_text.setLink(1, MakeShared<PeerOpenClickHandler>(_from));
 		if (second) {
 			_text.setLink(2, second);
 		}
@@ -7738,10 +7762,9 @@ HistoryService::HistoryService(History *history, const MTPDmessageService &msg)
 	setMessageByAction(msg.vaction);
 }
 
-HistoryService::HistoryService(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags, HistoryMedia *media, int32 from) :
-	HistoryItem(history, msgId, flags, date, from)
-, _text(st::msgServiceFont, msg, _historySrvOptions, st::dlgMinWidth)
-, _media(media) {
+HistoryService::HistoryService(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags, int32 from) :
+	HistoryItem(history, msgId, flags, date, from) {
+	_text.setText(st::msgServiceFont, msg, _historySrvOptions);
 }
 
 void HistoryService::initDimensions() {
@@ -7902,8 +7925,8 @@ bool HistoryService::hasPoint(int32 x, int32 y) const {
 	return QRect(left, st::msgServiceMargin.top(), width, height).contains(x, y);
 }
 
-void HistoryService::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const {
-	lnk = TextLinkPtr();
+void HistoryService::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const {
+	lnk.clear();
 	state = HistoryDefaultCursorState;
 
 	int left = 0, width = 0, height = _height - st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); // two small margins
@@ -7979,8 +8002,8 @@ QString HistoryService::notificationText() const {
     return msg;
 }
 
-HistoryMedia *HistoryService::getMedia(bool inOverview) const {
-	return inOverview ? 0 : _media;
+HistoryMedia *HistoryService::getMedia() const {
+	return _media.data();
 }
 
 HistoryService::~HistoryService() {
@@ -7989,10 +8012,7 @@ HistoryService::~HistoryService() {
 			App::historyUnregDependency(this, pinned->msg);
 		}
 	}
-	if (_media) {
-		_media->detachFromItem(this);
-		deleteAndMark(_media);
-	}
+	_media.clear(this);
 }
 
 HistoryGroup::HistoryGroup(History *history, const MTPDmessageGroup &group, const QDateTime &date)
@@ -8000,7 +8020,7 @@ HistoryGroup::HistoryGroup(History *history, const MTPDmessageGroup &group, cons
 	, _minId(group.vmin_id.v)
 	, _maxId(group.vmax_id.v)
 	, _count(group.vcount.v)
-	, _lnk(new CommentsLink(this)) {
+	, _lnk(new CommentsClickHandler(this)) {
 }
 
 HistoryGroup::HistoryGroup(History *history, HistoryItem *newItem, const QDateTime &date)
@@ -8008,11 +8028,11 @@ HistoryGroup::HistoryGroup(History *history, HistoryItem *newItem, const QDateTi
 	, _minId(newItem->id - 1)
 	, _maxId(newItem->id + 1)
 	, _count(1)
-	, _lnk(new CommentsLink(this)) {
+	, _lnk(new CommentsClickHandler(this)) {
 }
 
-void HistoryGroup::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const {
-	lnk = TextLinkPtr();
+void HistoryGroup::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const {
+	lnk.clear();
 	state = HistoryDefaultCursorState;
 
 	int32 left = 0, width = 0, height = _height - st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); // two small margins
@@ -8081,8 +8101,8 @@ HistoryCollapse::HistoryCollapse(History *history, MsgId wasMinId, const QDateTi
 void HistoryCollapse::draw(Painter &p, const QRect &r, uint32 selection, uint64 ms) const {
 }
 
-void HistoryCollapse::getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const {
-	lnk = TextLinkPtr();
+void HistoryCollapse::getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const {
+	lnk.clear();
 	state = HistoryDefaultCursorState;
 }
 
@@ -8093,7 +8113,23 @@ HistoryJoined::HistoryJoined(History *history, const QDateTime &inviteDate, User
 		_text.setText(st::msgServiceFont, lang(history->isMegagroup() ? lng_action_you_joined_group : lng_action_you_joined), _historySrvOptions);
 	} else {
 		_text.setText(st::msgServiceFont, history->isMegagroup() ? lng_action_add_you_group(lt_from, textcmdLink(1, inviter->name)) : lng_action_add_you(lt_from, textcmdLink(1, inviter->name)), _historySrvOptions);
-		_text.setLink(1, TextLinkPtr(new PeerLink(inviter)));
+		_text.setLink(1, MakeShared<PeerOpenClickHandler>(inviter));
 	}
 	textstyleRestore();
 }
+
+void GoToMessageClickHandler::onClickImpl() const {
+	if (App::main()) {
+		HistoryItem *current = App::mousedItem();
+		if (current && current->history()->peer->id == peer()) {
+			App::main()->pushReplyReturn(current);
+		}
+		Ui::showPeerHistory(peer(), msgid());
+	}
+}
+
+void CommentsClickHandler::onClickImpl() const {
+	if (App::main() && peerIsChannel(peer())) {
+		Ui::showPeerHistory(peer(), msgid());
+	}
+}
diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h
index 17b643604..080fe8392 100644
--- a/Telegram/SourceFiles/history.h
+++ b/Telegram/SourceFiles/history.h
@@ -242,7 +242,7 @@ public:
 
 	virtual ~History();
 
-	HistoryItem *addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags = 0, HistoryMedia *media = 0, bool newMsg = true);
+	HistoryItem *addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags = 0, bool newMsg = true);
 	HistoryItem *addNewMessage(const MTPMessage &msg, NewMessageType type);
 	HistoryItem *addToHistory(const MTPMessage &msg);
 	HistoryItem *addNewForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *item);
@@ -973,7 +973,7 @@ struct HistoryMessageVia : public BaseComponent<HistoryMessageVia> {
 	mutable QString _text;
 	mutable int _width = 0;
 	mutable int _maxWidth = 0;
-	TextLinkPtr _lnk;
+	ClickHandlerPtr _lnk;
 };
 
 struct HistoryMessageViews : public BaseComponent<HistoryMessageViews> {
@@ -1037,13 +1037,13 @@ struct HistoryMessageReply : public BaseComponent<HistoryMessageReply> {
 	int replyToWidth() const {
 		return _maxReplyWidth;
 	}
-	TextLinkPtr replyToLink() const {
+	ClickHandlerPtr replyToLink() const {
 		return replyToLnk;
 	}
 
 	MsgId replyToMsgId = 0;
 	HistoryItem *replyToMsg = nullptr;
-	TextLinkPtr replyToLnk;
+	ClickHandlerPtr replyToLnk;
 	mutable Text replyToName, replyToText;
 	mutable int replyToVersion = 0;
 	mutable int _maxReplyWidth = 0;
@@ -1052,7 +1052,21 @@ struct HistoryMessageReply : public BaseComponent<HistoryMessageReply> {
 };
 Q_DECLARE_OPERATORS_FOR_FLAGS(HistoryMessageReply::PaintFlags);
 
-class ReplyKeyboard final {
+class ReplyMarkupClickHandler : public LeftButtonClickHandler {
+public:
+	ReplyMarkupClickHandler(const FullMsgId &msgId, int row, int col) : _msgId(msgId), _row(row), _col(col) {
+	}
+
+protected:
+	void onClickImpl() const override;
+
+private:
+	FullMsgId _msgId;
+	int _row, _col;
+
+};
+
+class ReplyKeyboard {
 public:
 	class Style {
 	public:
@@ -1062,7 +1076,7 @@ public:
 		virtual void startPaint(Painter &p) const = 0;
 		virtual style::font textFont() const = 0;
 
-		void paintButton(Painter &p, const QRect &rect, const Text &text, bool down, float64 howMuchOver) const;
+		void paintButton(Painter &p, const QRect &rect, const Text &text, bool pressed, float64 howMuchOver) const;
 
 		int buttonSkip() const {
 			return _st->margin;
@@ -1077,7 +1091,7 @@ public:
 		virtual void repaint(const HistoryItem *item) const = 0;
 
 	protected:
-		virtual void paintButtonBg(Painter &p, const QRect &rect, bool down, float64 howMuchOver) const = 0;
+		virtual void paintButtonBg(Painter &p, const QRect &rect, bool pressed, float64 howMuchOver) const = 0;
 
 	private:
 		const style::botKeyboardButton *_st;
@@ -1095,10 +1109,11 @@ public:
 	int naturalHeight() const;
 
 	void paint(Painter &p, const QRect &clip) const;
-	void getState(TextLinkPtr &lnk, int x, int y) const;
+	void getState(ClickHandlerPtr &lnk, int x, int y) const;
+
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active);
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed);
 
-	void linkOver(const TextLinkPtr &lnk);
-	void linkOut(const TextLinkPtr &lnk);
 	void clearSelection();
 
 private:
@@ -1111,7 +1126,7 @@ private:
 		int characters = 0;
 		float64 howMuchOver = 0.;
 		bool full = true;
-		TextLinkPtr link;
+		ClickHandlerPtr link;
 	};
 	using ButtonRow = QVector<Button>;
 	using ButtonRows = QVector<ButtonRow>;
@@ -1195,8 +1210,45 @@ struct HistoryMessageUnreadBar : public BaseComponent<HistoryMessageUnreadBar> {
 	bool _freezed = false;
 };
 
+// HistoryMedia has a special owning smart pointer
+// which regs/unregs this media to the holding HistoryItem
 class HistoryMedia;
-class HistoryItem : public HistoryElem, public Composer {
+class HistoryMediaPtr {
+public:
+	HistoryMediaPtr() = default;
+	HistoryMediaPtr(const HistoryMediaPtr &other) = delete;
+	HistoryMediaPtr &operator=(const HistoryMediaPtr &other) = delete;
+	HistoryMedia *data() const {
+		return _p;
+	}
+	void reset(HistoryItem *host, HistoryMedia *p = nullptr);
+	bool isNull() const {
+		return data() == nullptr;
+	}
+
+	void clear(HistoryItem *host) {
+		reset(host);
+	}
+	HistoryMedia *operator->() const {
+		return data();
+	}
+	HistoryMedia &operator*() const {
+		t_assert(!isNull());
+		return *data();
+	}
+	explicit operator bool() const {
+		return !isNull();
+	}
+	~HistoryMediaPtr() {
+		t_assert(isNull());
+	}
+
+private:
+	HistoryMedia *_p;
+
+};
+
+class HistoryItem : public HistoryElem, public Composer, public ClickHandlerHost {
 public:
 
 	HistoryItem(const HistoryItem &) = delete;
@@ -1357,8 +1409,8 @@ public:
 	virtual bool hasPoint(int32 x, int32 y) const {
 		return false;
 	}
-	virtual void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const {
-		lnk = TextLinkPtr();
+	virtual void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const {
+		lnk.clear();
 		state = HistoryDefaultCursorState;
 	}
 	virtual void getSymbol(uint16 &symbol, bool &after, bool &upon, int32 x, int32 y) const { // from text
@@ -1369,20 +1421,11 @@ public:
 	virtual uint32 adjustSelection(uint16 from, uint16 to, TextSelectType type) const {
 		return (from << 16) | to;
 	}
-	virtual void linkOver(const TextLinkPtr &lnk) {
-		if (auto *markup = Get<HistoryMessageReplyMarkup>()) {
-			if (markup->inlineKeyboard) {
-				markup->inlineKeyboard->linkOver(lnk);
-			}
-		}
-	}
-	virtual void linkOut(const TextLinkPtr &lnk) {
-		if (auto *markup = Get<HistoryMessageReplyMarkup>()) {
-			if (markup->inlineKeyboard) {
-				markup->inlineKeyboard->linkOut(lnk);
-			}
-		}
-	}
+
+	// ClickHandlerHost interface
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
+
 	virtual HistoryItemType type() const {
 		return HistoryItemMsg;
 	}
@@ -1463,8 +1506,8 @@ public:
 		return FullMsgId(channelId(), id);
 	}
 
-	virtual HistoryMedia *getMedia(bool inOverview = false) const {
-		return 0;
+	virtual HistoryMedia *getMedia() const {
+		return nullptr;
 	}
 	virtual void setText(const QString &text, const EntitiesInText &links) {
 	}
@@ -1478,13 +1521,13 @@ public:
 		return false;
 	}
 
-	virtual int32 infoWidth() const {
+	virtual int infoWidth() const {
 		return 0;
 	}
-	virtual int32 timeLeft() const {
+	virtual int timeLeft() const {
 		return 0;
 	}
-	virtual int32 timeWidth() const {
+	virtual int timeWidth() const {
 		return 0;
 	}
 	virtual bool pointInTime(int32 right, int32 bottom, int32 x, int32 y, InfoDisplayType type) const {
@@ -1502,10 +1545,10 @@ public:
 	}
 
 	virtual HistoryMessage *toHistoryMessage() { // dynamic_cast optimize
-		return 0;
+		return nullptr;
 	}
 	virtual const HistoryMessage *toHistoryMessage() const { // dynamic_cast optimize
-		return 0;
+		return nullptr;
 	}
 	MsgId replyToId() const {
 		if (auto *reply = Get<HistoryMessageReply>()) {
@@ -1659,6 +1702,11 @@ protected:
 		return const_cast<ReplyKeyboard*>(static_cast<const HistoryItem*>(this)->inlineReplyKeyboard());
 	}
 
+	Text _text = { int(st::msgMinWidth) };
+	int32 _textWidth, _textHeight;
+
+	HistoryMediaPtr _media;
+
 };
 
 // make all the constructors in HistoryItem children protected
@@ -1676,15 +1724,12 @@ public:
 	}
 };
 
-class MessageLink : public ITextLink {
-	TEXT_LINK_CLASS(MessageLink)
-
+class MessageClickHandler : public LeftButtonClickHandler {
 public:
-	MessageLink(PeerId peer, MsgId msgid) : _peer(peer), _msgid(msgid) {
+	MessageClickHandler(PeerId peer, MsgId msgid) : _peer(peer), _msgid(msgid) {
 	}
-	MessageLink(HistoryItem *item) : _peer(item->history()->peer->id), _msgid(item->id) {
+	MessageClickHandler(HistoryItem *item) : _peer(item->history()->peer->id), _msgid(item->id) {
 	}
-	void onClick(Qt::MouseButton button) const;
 	PeerId peer() const {
 		return _peer;
 	}
@@ -1695,18 +1740,21 @@ public:
 private:
 	PeerId _peer;
 	MsgId _msgid;
+
 };
 
-class CommentsLink : public ITextLink {
-	TEXT_LINK_CLASS(CommentsLink)
-
+class GoToMessageClickHandler : public MessageClickHandler {
 public:
-	CommentsLink(HistoryItem *item) : _item(item) {
-	}
-	void onClick(Qt::MouseButton button) const;
+	using MessageClickHandler::MessageClickHandler;
+protected:
+	void onClickImpl() const override;
+};
 
-private:
-	HistoryItem *_item;
+class CommentsClickHandler : public MessageClickHandler {
+public:
+	using MessageClickHandler::MessageClickHandler;
+protected:
+	void onClickImpl() const override;
 };
 
 class RadialAnimation {
@@ -1766,11 +1814,23 @@ public:
 		return _height;
 	}
 	virtual void draw(Painter &p, const HistoryItem *parent, const QRect &r, bool selected, uint64 ms) const = 0;
-	virtual void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const = 0;
+	virtual void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const = 0;
 
-	virtual void linkOver(HistoryItem *parent, const TextLinkPtr &lnk) {
+	// if we are in selecting items mode perhaps we want to
+	// toggle selection instead of activating the pressed link
+	virtual bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const = 0;
+
+	// if we press and drag on this media should we drag the item
+	virtual bool dragItem() const {
+		return false;
 	}
-	virtual void linkOut(HistoryItem *parent, const TextLinkPtr &lnk) {
+
+	// if we press and drag this link should we drag the item
+	virtual bool dragItemByHandler(const ClickHandlerPtr &p) const = 0;
+
+	virtual void clickHandlerActiveChanged(HistoryItem *parent, const ClickHandlerPtr &p, bool active) {
+	}
+	virtual void clickHandlerPressedChanged(HistoryItem *parent, const ClickHandlerPtr &p, bool pressed) {
 	}
 
 	virtual bool uploading() const {
@@ -1860,15 +1920,38 @@ public:
 
 	HistoryFileMedia();
 
-	void linkOver(HistoryItem *parent, const TextLinkPtr &lnk);
-	void linkOut(HistoryItem *parent, const TextLinkPtr &lnk);
+	bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
+		return p == _openl || p == _savel || p == _cancell;
+	}
+	bool dragItemByHandler(const ClickHandlerPtr &p) const override {
+		return p == _openl || p == _savel || p == _cancell;
+	}
+
+	void clickHandlerActiveChanged(HistoryItem *parent, const ClickHandlerPtr &p, bool active) override;
+	void clickHandlerPressedChanged(HistoryItem *parent, const ClickHandlerPtr &p, bool pressed) override;
 
 	~HistoryFileMedia();
 
 protected:
 
-	TextLinkPtr _openl, _savel, _cancell;
-	void setLinks(ITextLink *openl, ITextLink *savel, ITextLink *cancell);
+	ClickHandlerPtr _openl, _savel, _cancell;
+	void setLinks(ClickHandlerPtr &&openl, ClickHandlerPtr &&savel, ClickHandlerPtr &&cancell);
+	void setDocumentLinks(DocumentData *document, bool inlinegif = false) {
+		ClickHandlerPtr open, save;
+		if (inlinegif) {
+			open.reset(new GifOpenClickHandler(document));
+		} else {
+			open.reset(new DocumentOpenClickHandler(document));
+		}
+		if (inlinegif) {
+			save.reset(new GifOpenClickHandler(document));
+		} else if (document->voice()) {
+			save.reset(new DocumentOpenClickHandler(document));
+		} else {
+			save.reset(new DocumentSaveClickHandler(document));
+		}
+		setLinks(std_::move(open), std_::move(save), MakeShared<DocumentCancelClickHandler>(document));
+	}
 
 	// >= 0 will contain download / upload string, _statusSize = loaded bytes
 	// < 0 will contain played string, _statusSize = -(seconds + 1) played
@@ -1940,7 +2023,7 @@ public:
 	int32 resize(int32 width, const HistoryItem *parent) override;
 
 	void draw(Painter &p, const HistoryItem *parent, const QRect &r, bool selected, uint64 ms) const override;
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
 
 	const QString inDialogsText() const override;
 	const QString inHistoryText() const override;
@@ -1986,7 +2069,8 @@ protected:
 
 private:
 	PhotoData *_data;
-	int16 _pixw, _pixh;
+	int16 _pixw = 1;
+	int16 _pixh = 1;
 	Text _caption;
 
 };
@@ -2007,7 +2091,7 @@ public:
 	int32 resize(int32 width, const HistoryItem *parent) override;
 
 	void draw(Painter &p, const HistoryItem *parent, const QRect &r, bool selected, uint64 ms) const override;
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
 
 	const QString inDialogsText() const override;
 	const QString inHistoryText() const override;
@@ -2064,7 +2148,7 @@ private:
 };
 
 struct HistoryDocumentThumbed : public BaseComponent<HistoryDocumentThumbed> {
-	TextLinkPtr _linksavel, _linkcancell;
+	ClickHandlerPtr _linksavel, _linkcancell;
 	int _thumbw = 0;
 
 	mutable int _linkw = 0;
@@ -2114,7 +2198,7 @@ public:
 	int32 resize(int32 width, const HistoryItem *parent) override;
 
 	void draw(Painter &p, const HistoryItem *parent, const QRect &r, bool selected, uint64 ms) const override;
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
 
 	const QString inDialogsText() const override;
 	const QString inHistoryText() const override;
@@ -2197,7 +2281,7 @@ public:
 	int32 resize(int32 width, const HistoryItem *parent) override;
 
 	void draw(Painter &p, const HistoryItem *parent, const QRect &r, bool selected, uint64 ms) const override;
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
 
 	const QString inDialogsText() const override;
 	const QString inHistoryText() const override;
@@ -2282,7 +2366,17 @@ public:
 	int32 resize(int32 width, const HistoryItem *parent) override;
 
 	void draw(Painter &p, const HistoryItem *parent, const QRect &r, bool selected, uint64 ms) const override;
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
+
+	bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
+		return true;
+	}
+	bool dragItem() const override {
+		return true;
+	}
+	bool dragItemByHandler(const ClickHandlerPtr &p) const override {
+		return true;
+	}
 
 	const QString inDialogsText() const override;
 	const QString inHistoryText() const override;
@@ -2311,24 +2405,18 @@ private:
 
 };
 
-class SendMessageLink : public PeerLink {
-	TEXT_LINK_CLASS(SendMessageLink)
-
+class SendMessageClickHandler : public PeerClickHandler {
 public:
-	SendMessageLink(PeerData *peer) : PeerLink(peer) {
-	}
-	void onClick(Qt::MouseButton button) const;
-
+	using PeerClickHandler::PeerClickHandler;
+protected:
+	void onClickImpl() const override;
 };
 
-class AddContactLink : public MessageLink {
-	TEXT_LINK_CLASS(AddContactLink)
-
+class AddContactClickHandler : public MessageClickHandler {
 public:
-	AddContactLink(PeerId peer, MsgId msgid) : MessageLink(peer, msgid) {
-	}
-	void onClick(Qt::MouseButton button) const;
-
+	using MessageClickHandler::MessageClickHandler;
+protected:
+	void onClickImpl() const override;
 };
 
 class HistoryContact : public HistoryMedia {
@@ -2345,7 +2433,14 @@ public:
 	void initDimensions(const HistoryItem *parent) override;
 
 	void draw(Painter &p, const HistoryItem *parent, const QRect &r, bool selected, uint64 ms) const override;
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
+
+	bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
+		return true;
+	}
+	bool dragItemByHandler(const ClickHandlerPtr &p) const override {
+		return true;
+	}
 
 	const QString inDialogsText() const override;
 	const QString inHistoryText() const override;
@@ -2381,7 +2476,7 @@ private:
 	QString _fname, _lname, _phone;
 	Text _name;
 
-	TextLinkPtr _linkl;
+	ClickHandlerPtr _linkl;
 	int32 _linkw;
 	QString _link;
 };
@@ -2402,13 +2497,20 @@ public:
 	int32 resize(int32 width, const HistoryItem *parent) override;
 
 	void draw(Painter &p, const HistoryItem *parent, const QRect &r, bool selected, uint64 ms) const override;
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const override;
+
+	bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
+		return _attach && _attach->toggleSelectionByHandlerClick(p);
+	}
+	bool dragItemByHandler(const ClickHandlerPtr &p) const override {
+		return _attach && _attach->dragItemByHandler(p);
+	}
 
 	const QString inDialogsText() const override;
 	const QString inHistoryText() const override;
 
-	void linkOver(HistoryItem *parent, const TextLinkPtr &lnk) override;
-	void linkOut(HistoryItem *parent, const TextLinkPtr &lnk) override;
+	void clickHandlerActiveChanged(HistoryItem *parent, const ClickHandlerPtr &p, bool active) override;
+	void clickHandlerPressedChanged(HistoryItem *parent, const ClickHandlerPtr &p, bool pressed) override;
 
 	bool isDisplayed() const override {
 		return !_data->pendingTill;
@@ -2453,7 +2555,7 @@ public:
 
 private:
 	WebPageData *_data;
-	TextLinkPtr _openl;
+	ClickHandlerPtr _openl;
 	HistoryMedia *_attach;
 
 	bool _asArticle;
@@ -2526,7 +2628,14 @@ public:
 	int32 resize(int32 width, const HistoryItem *parent);
 
 	void draw(Painter &p, const HistoryItem *parent, const QRect &r, bool selected, uint64 ms) const;
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y, const HistoryItem *parent) const;
+
+	bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
+		return p == _link;
+	}
+	bool dragItemByHandler(const ClickHandlerPtr &p) const override {
+		return p == _link;
+	}
 
 	const QString inDialogsText() const;
 	const QString inHistoryText() const;
@@ -2545,20 +2654,20 @@ public:
 private:
 	LocationData *_data;
 	Text _title, _description;
-	TextLinkPtr _link;
+	ClickHandlerPtr _link;
 
 	int32 fullWidth() const;
 	int32 fullHeight() const;
 
 };
 
-class ViaInlineBotLink : public ITextLink {
-	TEXT_LINK_CLASS(ViaInlineBotLink)
-
+class ViaInlineBotClickHandler : public LeftButtonClickHandler {
 public:
-	ViaInlineBotLink(UserData *bot) : _bot(bot) {
+	ViaInlineBotClickHandler(UserData *bot) : _bot(bot) {
 	}
-	void onClick(Qt::MouseButton button) const;
+
+protected:
+	void onClickImpl() const override;
 
 private:
 	UserData *_bot;
@@ -2623,19 +2732,21 @@ public:
 	bool hasPoint(int32 x, int32 y) const override;
 	bool pointInTime(int32 right, int32 bottom, int32 x, int32 y, InfoDisplayType type) const override;
 
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const override;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const override;
 
 	void getSymbol(uint16 &symbol, bool &after, bool &upon, int32 x, int32 y) const override;
 	uint32 adjustSelection(uint16 from, uint16 to, TextSelectType type) const override {
 		return _text.adjustSelection(from, to, type);
 	}
-	void linkOver(const TextLinkPtr &lnk) override {
-		if (_media) _media->linkOver(this, lnk);
-		HistoryItem::linkOver(lnk);
+
+	// ClickHandlerHost interface
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override {
+		if (_media) _media->clickHandlerActiveChanged(this, p, active);
+		HistoryItem::clickHandlerActiveChanged(p, active);
 	}
-	void linkOut(const TextLinkPtr &lnk) override {
-		if (_media) _media->linkOut(this, lnk);
-		HistoryItem::linkOut(lnk);
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override {
+		if (_media) _media->clickHandlerActiveChanged(this, p, pressed);
+		HistoryItem::clickHandlerPressedChanged(p, pressed);
 	}
 
 	void drawInDialog(Painter &p, const QRect &r, bool act, const HistoryItem *&cacheFor, Text &cache) const override;
@@ -2655,7 +2766,7 @@ public:
 
 	QString selectedText(uint32 selection) const override;
 	QString inDialogsText() const override;
-	HistoryMedia *getMedia(bool inOverview = false) const override;
+	HistoryMedia *getMedia() const override;
 	void setMedia(const MTPMessageMedia *media);
 	void setText(const QString &text, const EntitiesInText &entities) override;
 	QString originalText() const override;
@@ -2743,12 +2854,6 @@ protected:
 	// this method draws "via @bot" if it is not painted in forwarded info or in from name
 	void paintViaBotIdInfo(Painter &p, QRect &trect, bool selected) const;
 
-	Text _text = { int(st::msgMinWidth) };
-
-	int _textWidth = 0;
-	int _textHeight = 0;
-
-	HistoryMedia *_media = nullptr;
 	QString _timeText;
 	int _timeWidth = 0;
 
@@ -2812,7 +2917,7 @@ inline MTPDmessage::Flags newForwardedFlags(PeerData *p, int32 from, HistoryMess
 struct HistoryServicePinned : public BaseComponent<HistoryServicePinned> {
 	MsgId msgId = 0;
 	HistoryItem *msg = nullptr;
-	TextLinkPtr lnk;
+	ClickHandlerPtr lnk;
 };
 
 class HistoryService : public HistoryItem, private HistoryItemInstantiated<HistoryService> {
@@ -2821,8 +2926,8 @@ public:
 	static HistoryService *create(History *history, const MTPDmessageService &msg) {
 		return _create(history, msg);
 	}
-	static HistoryService *create(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags = 0, HistoryMedia *media = 0, int32 from = 0) {
-		return _create(history, msgId, date, msg, flags, media, from);
+	static HistoryService *create(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags = 0, int32 from = 0) {
+		return _create(history, msgId, date, msg, flags, from);
 	}
 
 	bool updateDependencyItem() override {
@@ -2845,19 +2950,19 @@ public:
 
 	void draw(Painter &p, const QRect &r, uint32 selection, uint64 ms) const override;
 	bool hasPoint(int32 x, int32 y) const override;
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const override;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const override;
 	void getSymbol(uint16 &symbol, bool &after, bool &upon, int32 x, int32 y) const override;
 	uint32 adjustSelection(uint16 from, uint16 to, TextSelectType type) const override {
 		return _text.adjustSelection(from, to, type);
 	}
 
-	void linkOver(const TextLinkPtr &lnk) override {
-		if (_media) _media->linkOver(this, lnk);
-		HistoryItem::linkOver(lnk);
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override {
+		if (_media) _media->clickHandlerActiveChanged(this, p, active);
+		HistoryItem::clickHandlerActiveChanged(p, active);
 	}
-	void linkOut(const TextLinkPtr &lnk) override {
-		if (_media) _media->linkOut(this, lnk);
-		HistoryItem::linkOut(lnk);
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override {
+		if (_media) _media->clickHandlerPressedChanged(this, p, pressed);
+		HistoryItem::clickHandlerPressedChanged(p, pressed);
 	}
 
 	void drawInDialog(Painter &p, const QRect &r, bool act, const HistoryItem *&cacheFor, Text &cache) const override;
@@ -2873,7 +2978,7 @@ public:
 	QString inDialogsText() const override;
 	QString inReplyText() const override;
 
-	HistoryMedia *getMedia(bool inOverview = false) const override;
+	HistoryMedia *getMedia() const override;
 
 	void setServiceText(const QString &text);
 
@@ -2882,7 +2987,7 @@ public:
 protected:
 
 	HistoryService(History *history, const MTPDmessageService &msg);
-	HistoryService(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags = 0, HistoryMedia *media = 0, int32 from = 0);
+	HistoryService(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags = 0, int32 from = 0);
 	friend class HistoryItemInstantiated<HistoryService>;
 
 	void initDimensions() override;
@@ -2892,10 +2997,6 @@ protected:
 	bool updatePinned(bool force = false);
 	bool updatePinnedText(const QString *pfrom = nullptr, QString *ptext = nullptr);
 
-	Text _text = { int(st::msgMinWidth) };
-	HistoryMedia *_media = nullptr;
-
-	int32 _textWidth, _textHeight;
 };
 
 class HistoryGroup : public HistoryService, private HistoryItemInstantiated<HistoryGroup> {
@@ -2908,7 +3009,7 @@ public:
 		return _create(history, newItem, date);
 	}
 
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const;
 	void getSymbol(uint16 &symbol, bool &after, bool &upon, int32 x, int32 y) const {
 		symbol = 0xFFFF;
 		after = false;
@@ -2948,7 +3049,7 @@ private:
 	MsgId _minId, _maxId;
 	int32 _count;
 
-	TextLinkPtr _lnk;
+	ClickHandlerPtr _lnk;
 
 	void updateText();
 
@@ -2962,7 +3063,7 @@ public:
 	}
 
 	void draw(Painter &p, const QRect &r, uint32 selection, uint64 ms) const;
-	void getState(TextLinkPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const;
+	void getState(ClickHandlerPtr &lnk, HistoryCursorState &state, int32 x, int32 y) const;
 	void getSymbol(uint16 &symbol, bool &after, bool &upon, int32 x, int32 y) const {
 		symbol = 0xFFFF;
 		after = false;
diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp
index f1f2764be..ce29895ef 100644
--- a/Telegram/SourceFiles/historywidget.cpp
+++ b/Telegram/SourceFiles/historywidget.cpp
@@ -42,7 +42,6 @@ HistoryInner::HistoryInner(HistoryWidget *historyWidget, ScrollArea *scroll, His
 , _peer(history->peer)
 , _migrated(history->peer->migrateFrom() ? App::history(history->peer->migrateFrom()->id) : nullptr)
 , _history(history)
-, _botInfo(history->peer->isUser() ? history->peer->asUser()->botInfo : nullptr)
 , _widget(historyWidget)
 , _scroll(scroll) {
 	connect(App::wnd(), SIGNAL(imageLoaded()), this, SLOT(update()));
@@ -55,9 +54,7 @@ HistoryInner::HistoryInner(HistoryWidget *historyWidget, ScrollArea *scroll, His
 
 	_trippleClickTimer.setSingleShot(true);
 
-	if (_botInfo && !_botInfo->inited && App::api()) {
-		App::api()->requestFullPeer(_peer);
-	}
+	notifyIsBotChanged();
 
 	setMouseTracking(true);
 }
@@ -205,16 +202,16 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
 	}
 	uint64 ms = getms();
 
-	if (!_firstLoading && _botInfo && !_botInfo->text.isEmpty() && _botDescHeight > 0) {
-		if (r.y() < _botDescRect.y() + _botDescRect.height() && r.y() + r.height() > _botDescRect.y()) {
+	if (!_firstLoading && _botAbout && !_botAbout->info->text.isEmpty() && _botAbout->height > 0) {
+		if (r.y() < _botAbout->rect.y() + _botAbout->rect.height() && r.y() + r.height() > _botAbout->rect.y()) {
 			textstyleSet(&st::inTextStyle);
-			App::roundRect(p, _botDescRect, st::msgInBg, MessageInCorners, &st::msgInShadow);
+			App::roundRect(p, _botAbout->rect, st::msgInBg, MessageInCorners, &st::msgInShadow);
 
-			p.setFont(st::msgNameFont->f);
-			p.setPen(st::black->p);
-			p.drawText(_botDescRect.left() + st::msgPadding.left(), _botDescRect.top() + st::msgPadding.top() + st::msgNameFont->ascent, lang(lng_bot_description));
+			p.setFont(st::msgNameFont);
+			p.setPen(st::black);
+			p.drawText(_botAbout->rect.left() + st::msgPadding.left(), _botAbout->rect.top() + st::msgPadding.top() + st::msgNameFont->ascent, lang(lng_bot_description));
 
-			_botInfo->text.draw(p, _botDescRect.left() + st::msgPadding.left(), _botDescRect.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip, _botDescWidth);
+			_botAbout->info->text.draw(p, _botAbout->rect.left() + st::msgPadding.left(), _botAbout->rect.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip, _botAbout->width);
 
 			textstyleRestore();
 		}
@@ -519,7 +516,7 @@ void HistoryInner::touchEvent(QTouchEvent *e) {
 }
 
 void HistoryInner::mouseMoveEvent(QMouseEvent *e) {
-	if (!(e->buttons() & (Qt::LeftButton | Qt::MiddleButton)) && (textlnkDown() || _dragAction != NoDrag)) {
+	if (!(e->buttons() & (Qt::LeftButton | Qt::MiddleButton)) && _dragAction != NoDrag) {
 		mouseReleaseEvent(e);
 	}
 	dragActionUpdate(e->globalPos());
@@ -556,25 +553,20 @@ void HistoryInner::dragActionStart(const QPoint &screenPos, Qt::MouseButton butt
 	dragActionUpdate(screenPos);
 	if (button != Qt::LeftButton) return;
 
+	ClickHandler::pressed();
 	if (App::pressedItem() != App::hoveredItem()) {
 		repaintItem(App::pressedItem());
 		App::pressedItem(App::hoveredItem());
 		repaintItem(App::pressedItem());
 	}
-	if (textlnkDown() != textlnkOver()) {
-		repaintItem(App::pressedLinkItem());
-		textlnkDown(textlnkOver());
-		App::pressedLinkItem(App::hoveredLinkItem());
-		repaintItem(App::pressedLinkItem());
-		repaintItem(App::pressedItem());
-	}
 
 	_dragAction = NoDrag;
 	_dragItem = App::mousedItem();
 	_dragStartPos = mapMouseToItem(mapFromGlobal(screenPos), _dragItem);
 	_dragWasInactive = App::wnd()->inactivePress();
 	if (_dragWasInactive) App::wnd()->inactivePress(false);
-	if (textlnkDown()) {
+
+	if (ClickHandler::getPressed()) {
 		_dragAction = PrepareDrag;
 	} else if (!_selected.isEmpty()) {
 		if (_selected.cbegin().value() == FullSelection) {
@@ -694,15 +686,16 @@ void HistoryInner::onDragExec() {
 			}
 		}
 	}
+	ClickHandlerPtr pressedHandler = ClickHandler::getPressed();
 	QString sel;
 	QList<QUrl> urls;
 	if (uponSelected) {
 		sel = getSelectedText();
-	} else if (textlnkDown()) {
-		sel = textlnkDown()->encoded();
-		if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
-//			urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
-		}
+	} else if (pressedHandler) {
+		sel = pressedHandler->dragText();
+		//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
+		//	urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
+		//}
 	}
 	if (!sel.isEmpty()) {
 		updateDragSelection(0, 0, false);
@@ -721,26 +714,28 @@ void HistoryInner::onDragExec() {
 		if (App::main()) App::main()->updateAfterDrag();
 		return;
 	} else {
-		HistoryItem *pressedLnkItem = App::pressedLinkItem(), *pressedItem = App::pressedItem();
-		QLatin1String lnkType = (textlnkDown() && pressedLnkItem) ? textlnkDown()->type() : qstr("");
-		bool lnkPhoto = (lnkType == qstr("PhotoLink")),
-			lnkVideo = (lnkType == qstr("VideoOpenLink")),
-			lnkAudio = (lnkType == qstr("AudioOpenLink")),
-			lnkDocument = (lnkType == qstr("DocumentOpenLink") || lnkType == qstr("GifOpenLink")),
-			lnkContact = (lnkType == qstr("PeerLink") && dynamic_cast<HistoryContact*>(pressedLnkItem->getMedia())),
-			dragSticker = dynamic_cast<HistorySticker*>(pressedItem ? pressedItem->getMedia() : 0),
-			dragByDate = (_dragCursorState == HistoryInDateCursorState);
-		if (lnkPhoto || lnkVideo || lnkAudio || lnkDocument || lnkContact || dragSticker || dragByDate) {
+		QString forwardMimeType;
+		HistoryMedia *pressedMedia = nullptr;
+		if (HistoryItem *pressedItem = App::pressedItem()) {
+			pressedMedia = pressedItem->getMedia();
+			if (_dragCursorState == HistoryInDateCursorState || (pressedMedia && pressedMedia->dragItem())) {
+				forwardMimeType = qsl("application/x-td-forward-pressed");
+			}
+		}
+		if (HistoryItem *pressedLnkItem = App::pressedLinkItem()) {
+			if ((pressedMedia = pressedLnkItem->getMedia())) {
+				if (forwardMimeType.isEmpty() && pressedMedia->dragItemByHandler(pressedHandler)) {
+					forwardMimeType = qsl("application/x-td-forward-pressed-link");
+				}
+			}
+		}
+		if (!forwardMimeType.isEmpty()) {
 			QDrag *drag = new QDrag(App::wnd());
 			QMimeData *mimeData = new QMimeData;
 
-			if (lnkPhoto || lnkVideo || lnkAudio || lnkDocument || lnkContact) {
-				mimeData->setData(qsl("application/x-td-forward-pressed-link"), "1");
-			} else {
-				mimeData->setData(qsl("application/x-td-forward-pressed"), "1");
-			}
-			if (lnkDocument) {
-				QString filepath = static_cast<DocumentOpenLink*>(textlnkDown().data())->document()->filepath(DocumentData::FilePathResolveChecked);
+			mimeData->setData(forwardMimeType, "1");
+			if (DocumentData *document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) {
+				QString filepath = document->filepath(DocumentData::FilePathResolveChecked);
 				if (!filepath.isEmpty()) {
 					QList<QUrl> urls;
 					urls.push_back(QUrl::fromLocalFile(filepath));
@@ -776,47 +771,32 @@ void HistoryInner::itemRemoved(HistoryItem *item) {
 }
 
 void HistoryInner::dragActionFinish(const QPoint &screenPos, Qt::MouseButton button) {
-	TextLinkPtr needClick;
-
 	dragActionUpdate(screenPos);
 
-	if (textlnkOver()) {
-		if (textlnkDown() == textlnkOver() && _dragAction != Dragging) {
-			needClick = textlnkDown();
-
-			QLatin1String lnkType = needClick->type();
-			bool lnkPhoto = (lnkType == qstr("PhotoLink")),
-				lnkVideo = (lnkType == qstr("VideoOpenLink")),
-				lnkAudio = (lnkType == qstr("AudioOpenLink")),
-				lnkDocument = (lnkType == qstr("DocumentOpenLink") || lnkType == qstr("GifOpenLink")),
-				lnkContact = (lnkType == qstr("PeerLink") && dynamic_cast<HistoryContact*>(App::pressedLinkItem() ? App::pressedLinkItem()->getMedia() : 0));
-			if (_dragAction == PrepareDrag && !_dragWasInactive && !_selected.isEmpty() && _selected.cbegin().value() == FullSelection && button != Qt::RightButton) {
-				if (lnkPhoto || lnkVideo || lnkAudio || lnkDocument || lnkContact) {
-					needClick = TextLinkPtr();
+	ClickHandlerPtr activated = ClickHandler::unpressed();
+	if (_dragAction == Dragging) {
+		activated.clear();
+	} else if (HistoryItem *pressed = App::pressedLinkItem()) {
+		// if we are in selecting items mode perhaps we want to
+		// toggle selection instead of activating the pressed link
+		if (_dragAction == PrepareDrag && !_dragWasInactive && !_selected.isEmpty() && _selected.cbegin().value() == FullSelection && button != Qt::RightButton) {
+			if (HistoryMedia *media = pressed->getMedia()) {
+				if (media->toggleSelectionByHandlerClick(activated)) {
+					activated.clear();
 				}
 			}
 		}
 	}
-	if (textlnkDown()) {
-		repaintItem(App::pressedLinkItem());
-		textlnkDown(TextLinkPtr());
-		App::pressedLinkItem(0);
-		if (!textlnkOver() && _cursor != style::cur_default) {
-			_cursor = style::cur_default;
-			setCursor(_cursor);
-		}
-	}
 	if (App::pressedItem()) {
 		repaintItem(App::pressedItem());
-		App::pressedItem(0);
+		App::pressedItem(nullptr);
 	}
 
 	_wasSelectedText = false;
 
-	if (needClick) {
-		DEBUG_LOG(("Will click link: %1 (%2) %3").arg(needClick->text()).arg(needClick->readable()).arg(needClick->encoded()));
+	if (activated) {
 		dragActionCancel();
-		App::activateTextLink(needClick, button);
+		App::activateClickHandler(activated, button);
 		return;
 	}
 	if (_dragAction == PrepareSelect && !_dragWasInactive && !_selected.isEmpty() && _selected.cbegin().value() == FullSelection) {
@@ -943,10 +923,10 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 
 	_menu = new PopupMenu();
 
-	_contextMenuLnk = textlnkOver();
+	_contextMenuLnk = ClickHandler::getActive();
 	HistoryItem *item = App::hoveredItem() ? App::hoveredItem() : App::hoveredLinkItem();
-	PhotoLink *lnkPhoto = dynamic_cast<PhotoLink*>(_contextMenuLnk.data());
-    DocumentLink *lnkDocument = dynamic_cast<DocumentLink*>(_contextMenuLnk.data());
+	PhotoClickHandler *lnkPhoto = dynamic_cast<PhotoClickHandler*>(_contextMenuLnk.data());
+    DocumentClickHandler *lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLnk.data());
 	bool lnkIsVideo = lnkDocument ? lnkDocument->document()->isVideo() : false;
 	bool lnkIsAudio = lnkDocument ? (lnkDocument->document()->voice() != nullptr) : false;
 	bool lnkIsSong = lnkDocument ? (lnkDocument->document()->song() != nullptr) : false;
@@ -1073,16 +1053,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 			}
 		}
 
-		QLatin1String linktype = _contextMenuLnk ? _contextMenuLnk->type() : qstr("");
-		if (linktype == qstr("TextLink") || linktype == qstr("LocationLink")) {
-			_menu->addAction(lang(lng_context_copy_link), this, SLOT(copyContextUrl()))->setEnabled(true);
-		} else if (linktype == qstr("EmailLink")) {
-			_menu->addAction(lang(lng_context_copy_email), this, SLOT(copyContextUrl()))->setEnabled(true);
-		} else if (linktype == qstr("MentionLink")) {
-			_menu->addAction(lang(lng_context_copy_mention), this, SLOT(copyContextUrl()))->setEnabled(true);
-		} else if (linktype == qstr("HashtagLink")) {
-			_menu->addAction(lang(lng_context_copy_hashtag), this, SLOT(copyContextUrl()))->setEnabled(true);
-		} else {
+		QString copyToClipboardContextItem = _contextMenuLnk ? _contextMenuLnk->copyToClipboardContextItem() : QString();
+		if (!copyToClipboardContextItem.isEmpty()) {
+			_menu->addAction(copyToClipboardContextItem, this, SLOT(copyContextUrl()))->setEnabled(true);
 		}
 		if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) {
 			_menu->addAction(lang(lng_context_copy_post_link), _widget, SLOT(onCopyPostLink()));
@@ -1139,14 +1112,13 @@ void HistoryInner::copySelectedText() {
 }
 
 void HistoryInner::copyContextUrl() {
-	QString enc = _contextMenuLnk->encoded();
-	if (!enc.isEmpty()) {
-		QApplication::clipboard()->setText(enc);
+	if (_contextMenuLnk) {
+		_contextMenuLnk->copyToClipboard();
 	}
 }
 
 void HistoryInner::saveContextImage() {
-    PhotoLink *lnk = dynamic_cast<PhotoLink*>(_contextMenuLnk.data());
+    PhotoClickHandler *lnk = dynamic_cast<PhotoClickHandler*>(_contextMenuLnk.data());
 	if (!lnk) return;
 
 	PhotoData *photo = lnk->photo();
@@ -1161,7 +1133,7 @@ void HistoryInner::saveContextImage() {
 }
 
 void HistoryInner::copyContextImage() {
-    PhotoLink *lnk = dynamic_cast<PhotoLink*>(_contextMenuLnk.data());
+	PhotoClickHandler *lnk = dynamic_cast<PhotoClickHandler*>(_contextMenuLnk.data());
 	if (!lnk) return;
 
 	PhotoData *photo = lnk->photo();
@@ -1171,7 +1143,7 @@ void HistoryInner::copyContextImage() {
 }
 
 void HistoryInner::cancelContextDownload() {
-	if (DocumentLink *lnkDocument = dynamic_cast<DocumentLink*>(_contextMenuLnk.data())) {
+	if (DocumentClickHandler *lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLnk.data())) {
 		lnkDocument->document()->cancel();
 	} else if (HistoryItem *item = App::contextItem()) {
 		if (HistoryMedia *media = item->getMedia()) {
@@ -1184,7 +1156,7 @@ void HistoryInner::cancelContextDownload() {
 
 void HistoryInner::showContextInFolder() {
 	QString filepath;
-	if (DocumentLink *lnkDocument = dynamic_cast<DocumentLink*>(_contextMenuLnk.data())) {
+	if (DocumentClickHandler *lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLnk.data())) {
 		filepath = lnkDocument->document()->filepath(DocumentData::FilePathResolveChecked);
 	} else if (HistoryItem *item = App::contextItem()) {
 		if (HistoryMedia *media = item->getMedia()) {
@@ -1199,12 +1171,12 @@ void HistoryInner::showContextInFolder() {
 }
 
 void HistoryInner::saveContextFile() {
-	if (DocumentLink *lnkDocument = dynamic_cast<DocumentLink*>(_contextMenuLnk.data())) {
-		DocumentSaveLink::doSave(lnkDocument->document(), true);
+	if (DocumentClickHandler *lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLnk.data())) {
+		DocumentSaveClickHandler::doSave(lnkDocument->document(), true);
 	} else if (HistoryItem *item = App::contextItem()) {
 		if (HistoryMedia *media = item->getMedia()) {
 			if (DocumentData *doc = media->getDocument()) {
-				DocumentSaveLink::doSave(doc, true);
+				DocumentSaveClickHandler::doSave(doc, true);
 			}
 		}
 	}
@@ -1298,8 +1270,8 @@ void HistoryInner::recountHeight() {
 
 	int ph = _scroll->height(), minadd = 0;
 	int wasYSkip = ph - historyHeight() - st::historyPadding;
-	if (_botInfo && !_botInfo->text.isEmpty()) {
-		minadd = st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botDescHeight;
+	if (_botAbout && !_botAbout->info->text.isEmpty()) {
+		minadd = st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botAbout->height;
 	}
 	if (wasYSkip < minadd) wasYSkip = minadd;
 
@@ -1325,33 +1297,33 @@ void HistoryInner::recountHeight() {
 	}
 
 	updateBotInfo(false);
-	if (_botInfo && !_botInfo->text.isEmpty()) {
+	if (_botAbout && !_botAbout->info->text.isEmpty()) {
 		int32 tw = _scroll->width() - st::msgMargin.left() - st::msgMargin.right();
 		if (tw > st::msgMaxWidth) tw = st::msgMaxWidth;
 		tw -= st::msgPadding.left() + st::msgPadding.right();
-		int32 mw = qMax(_botInfo->text.maxWidth(), st::msgNameFont->width(lang(lng_bot_description)));
+		int32 mw = qMax(_botAbout->info->text.maxWidth(), st::msgNameFont->width(lang(lng_bot_description)));
 		if (tw > mw) tw = mw;
 
-		_botDescWidth = tw;
-		_botDescHeight = _botInfo->text.countHeight(_botDescWidth);
+		_botAbout->width = tw;
+		_botAbout->height = _botAbout->info->text.countHeight(_botAbout->width);
 
-		int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botDescHeight + st::msgPadding.bottom() + st::msgMargin.bottom();
+		int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
 		int32 descMaxWidth = _scroll->width();
 		if (Adaptive::Wide()) {
 			descMaxWidth = qMin(descMaxWidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
 		}
-		int32 descAtX = (descMaxWidth - _botDescWidth) / 2 - st::msgPadding.left();
+		int32 descAtX = (descMaxWidth - _botAbout->width) / 2 - st::msgPadding.left();
 		int32 descAtY = qMin(_historyOffset - descH, qMax(0, (_scroll->height() - descH) / 2)) + st::msgMargin.top();
 
-		_botDescRect = QRect(descAtX, descAtY, _botDescWidth + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
-	} else {
-		_botDescWidth = _botDescHeight = 0;
-		_botDescRect = QRect();
+		_botAbout->rect = QRect(descAtX, descAtY, _botAbout->width + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
+	} else if (_botAbout) {
+		_botAbout->width = _botAbout->height = 0;
+		_botAbout->rect = QRect();
 	}
 
 	int32 newYSkip = ph - historyHeight() - st::historyPadding;
-	if (_botInfo && !_botInfo->text.isEmpty()) {
-		minadd = st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botDescHeight;
+	if (_botAbout && !_botAbout->info->text.isEmpty()) {
+		minadd = st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botAbout->height;
 	}
 	if (newYSkip < minadd) newYSkip = minadd;
 
@@ -1365,38 +1337,38 @@ void HistoryInner::recountHeight() {
 }
 
 void HistoryInner::updateBotInfo(bool recount) {
-	int32 newh = 0;
-	if (_botInfo && !_botInfo->description.isEmpty()) {
-		if (_botInfo->text.isEmpty()) {
-			_botInfo->text.setText(st::msgFont, _botInfo->description, _historyBotNoMonoOptions);
+	int newh = 0;
+	if (_botAbout && !_botAbout->info->description.isEmpty()) {
+		if (_botAbout->info->text.isEmpty()) {
+			_botAbout->info->text.setText(st::msgFont, _botAbout->info->description, _historyBotNoMonoOptions);
 			if (recount) {
 				int32 tw = _scroll->width() - st::msgMargin.left() - st::msgMargin.right();
 				if (tw > st::msgMaxWidth) tw = st::msgMaxWidth;
 				tw -= st::msgPadding.left() + st::msgPadding.right();
-				int32 mw = qMax(_botInfo->text.maxWidth(), st::msgNameFont->width(lang(lng_bot_description)));
+				int32 mw = qMax(_botAbout->info->text.maxWidth(), st::msgNameFont->width(lang(lng_bot_description)));
 				if (tw > mw) tw = mw;
 
-				_botDescWidth = tw;
-				newh = _botInfo->text.countHeight(_botDescWidth);
+				_botAbout->width = tw;
+				newh = _botAbout->info->text.countHeight(_botAbout->width);
 			}
 		} else if (recount) {
-			newh = _botDescHeight;
+			newh = _botAbout->height;
 		}
 	}
-	if (recount) {
-		if (_botDescHeight != newh) {
-			_botDescHeight = newh;
+	if (recount && _botAbout) {
+		if (_botAbout->height != newh) {
+			_botAbout->height = newh;
 			updateSize();
 		}
-		if (_botDescHeight > 0) {
-			int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botDescHeight + st::msgPadding.bottom() + st::msgMargin.bottom();
-			int32 descAtX = (_scroll->width() - _botDescWidth) / 2 - st::msgPadding.left();
+		if (_botAbout->height > 0) {
+			int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
+			int32 descAtX = (_scroll->width() - _botAbout->width) / 2 - st::msgPadding.left();
 			int32 descAtY = qMin(_historyOffset - descH, (_scroll->height() - descH) / 2) + st::msgMargin.top();
 
-			_botDescRect = QRect(descAtX, descAtY, _botDescWidth + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
+			_botAbout->rect = QRect(descAtX, descAtY, _botAbout->width + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
 		} else {
-			_botDescWidth = 0;
-			_botDescRect = QRect();
+			_botAbout->width = 0;
+			_botAbout->rect = QRect();
 		}
 	}
 }
@@ -1480,21 +1452,21 @@ void HistoryInner::visibleAreaUpdated(int top, int bottom) {
 void HistoryInner::updateSize() {
 	int32 ph = _scroll->height(), minadd = 0;
 	int32 newYSkip = ph - historyHeight() - st::historyPadding;
-	if (_botInfo && !_botInfo->text.isEmpty()) {
-		minadd = st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botDescHeight;
+	if (_botAbout && !_botAbout->info->text.isEmpty()) {
+		minadd = st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botAbout->height;
 	}
 	if (newYSkip < minadd) newYSkip = minadd;
 
-	if (_botDescHeight > 0) {
-		int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botDescHeight + st::msgPadding.bottom() + st::msgMargin.bottom();
+	if (_botAbout && _botAbout->height > 0) {
+		int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
 		int32 descMaxWidth = _scroll->width();
 		if (Adaptive::Wide()) {
 			descMaxWidth = qMin(descMaxWidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
 		}
-		int32 descAtX = (descMaxWidth - _botDescWidth) / 2 - st::msgPadding.left();
+		int32 descAtX = (descMaxWidth - _botAbout->width) / 2 - st::msgPadding.left();
 		int32 descAtY = qMin(newYSkip - descH, qMax(0, (_scroll->height() - descH) / 2)) + st::msgMargin.top();
 
-		_botDescRect = QRect(descAtX, descAtY, _botDescWidth + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
+		_botAbout->rect = QRect(descAtX, descAtY, _botAbout->width + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
 	}
 
 	int32 yAdded = newYSkip - _historyOffset;
@@ -1517,19 +1489,12 @@ void HistoryInner::enterEvent(QEvent *e) {
 void HistoryInner::leaveEvent(QEvent *e) {
 	if (HistoryItem *item = App::hoveredItem()) {
 		repaintItem(item);
-		App::hoveredItem(0);
+		App::hoveredItem(nullptr);
 	}
-	if (textlnkOver()) {
-		if (HistoryItem *item = App::hoveredLinkItem()) {
-			item->linkOut(textlnkOver());
-			repaintItem(item);
-			App::hoveredLinkItem(0);
-		}
-		textlnkOver(TextLinkPtr());
-		if (!textlnkDown() && _cursor != style::cur_default) {
-			_cursor = style::cur_default;
-			setCursor(_cursor);
-		}
+	ClickHandler::clearActive();
+	if (!ClickHandler::getPressed() && _cursor != style::cur_default) {
+		_cursor = style::cur_default;
+		setCursor(_cursor);
 	}
 	return QWidget::leaveEvent(e);
 }
@@ -1708,24 +1673,22 @@ void HistoryInner::onUpdateSelected() {
 		dragActionCancel();
 	}
 
-	Qt::CursorShape cur = style::cur_default;
+	ClickHandlerPtr lnk;
+	ClickHandlerHost *lnkhost = nullptr;
 	HistoryCursorState cursorState = HistoryDefaultCursorState;
-	bool lnkChanged = false, lnkInDesc = false;
-
-	TextLinkPtr lnk;
 	if (point.y() < _historyOffset) {
-		if (_botInfo && !_botInfo->text.isEmpty() && _botDescHeight > 0) {
+		if (_botAbout && !_botAbout->info->text.isEmpty() && _botAbout->height > 0) {
 			bool inText = false;
-			_botInfo->text.getState(lnk, inText, point.x() - _botDescRect.left() - st::msgPadding.left(), point.y() - _botDescRect.top() - st::msgPadding.top() - st::botDescSkip - st::msgNameFont->height, _botDescWidth);
+			_botAbout->info->text.getState(lnk, inText, point.x() - _botAbout->rect.left() - st::msgPadding.left(), point.y() - _botAbout->rect.top() - st::msgPadding.top() - st::botDescSkip - st::msgNameFont->height, _botAbout->width);
+			lnkhost = _botAbout.data();
 			cursorState = inText ? HistoryInTextCursorState : HistoryDefaultCursorState;
-			lnkInDesc = true;
 		}
 	} else if (item) {
 		item->getState(lnk, cursorState, m.x(), m.y());
 		if (!lnk && m.x() >= st::msgMargin.left() && m.x() < st::msgMargin.left() + st::msgPhotoSize) {
 			if (HistoryMessage *msg = item->toHistoryMessage()) {
 				if (msg->hasFromPhoto()) {
-					enumerateUserpics([&lnk, msg, &point](HistoryMessage *message, int userpicTop) -> bool {
+					enumerateUserpics([&lnk, &lnkhost, msg, &point](HistoryMessage *message, int userpicTop) -> bool {
 						// stop enumeration if the userpic is above our point
 						if (userpicTop + st::msgPhotoSize <= point.y()) {
 							return false;
@@ -1733,7 +1696,8 @@ void HistoryInner::onUpdateSelected() {
 
 						// stop enumeration if we've found a userpic under the cursor
 						if (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) {
-							lnk = message->from()->lnk;
+							lnk = message->from()->openLink();
+							lnkhost = msg;
 							return false;
 						}
 						return true;
@@ -1742,35 +1706,15 @@ void HistoryInner::onUpdateSelected() {
 			}
 		}
 	}
-	if (lnk != textlnkOver()) {
-		lnkChanged = true;
-		if (textlnkOver()) {
-			if (HistoryItem *item = App::hoveredLinkItem()) {
-				item->linkOut(textlnkOver());
-				repaintItem(item);
-			} else {
-				update(_botDescRect);
-			}
-		}
-		textlnkOver(lnk);
-		PopupTooltip::Hide();
-		App::hoveredLinkItem((lnk && !lnkInDesc) ? item : nullptr);
-		if (textlnkOver()) {
-			if (HistoryItem *item = App::hoveredLinkItem()) {
-				item->linkOver(textlnkOver());
-				repaintItem(item);
-			} else {
-				update(_botDescRect);
-			}
-		}
-	}
-	if (cursorState != _dragCursorState) {
+	bool lnkChanged = ClickHandler::setActive(lnk, lnkhost);
+	if (lnkChanged || cursorState != _dragCursorState) {
 		PopupTooltip::Hide();
 	}
 	if (lnk || cursorState == HistoryInDateCursorState || cursorState == HistoryInForwardedCursorState) {
 		PopupTooltip::Show(1000, this);
 	}
 
+	Qt::CursorShape cur = style::cur_default;
 	if (_dragAction == NoDrag) {
 		_dragCursorState = cursorState;
 		if (lnk) {
@@ -1789,7 +1733,6 @@ void HistoryInner::onUpdateSelected() {
 				_dragAction = Selecting;
 			}
 		}
-		cur = textlnkDown() ? style::cur_pointer : style::cur_default;
 		if (_dragAction == Selecting) {
 			bool canSelectMany = (_history != 0);
 			if (item == _dragItem && item == App::hoveredItem() && !_selected.isEmpty() && _selected.cbegin().value() != FullSelection) {
@@ -1846,7 +1789,7 @@ void HistoryInner::onUpdateSelected() {
 		} else if (_dragAction == Dragging) {
 		}
 
-		if (textlnkDown()) {
+		if (ClickHandler::getPressed()) {
 			cur = style::cur_pointer;
 		} else if (_dragAction == Selecting && !_selected.isEmpty() && _selected.cbegin().value() != FullSelection) {
 			if (!_dragSelFrom || !_dragSelTo) {
@@ -1886,6 +1829,14 @@ void HistoryInner::updateDragSelection(HistoryItem *dragSelFrom, HistoryItem *dr
 	update();
 }
 
+void HistoryInner::BotAbout::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
+	_parent->update(rect);
+}
+
+void HistoryInner::BotAbout::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
+	_parent->update(rect);
+}
+
 int HistoryInner::historyHeight() const {
 	int result = 0;
 	if (!_history || _history->isEmpty()) {
@@ -1932,9 +1883,18 @@ int HistoryInner::itemTop(const HistoryItem *item) const { // -1 if should not b
 }
 
 void HistoryInner::notifyIsBotChanged() {
-	_botInfo = (_history && _history->peer->isUser()) ? _history->peer->asUser()->botInfo : 0;
-	if (_botInfo && !_botInfo->inited && App::api()) {
-		App::api()->requestFullPeer(_peer);
+	BotInfo *newinfo = (_history && _history->peer->isUser()) ? _history->peer->asUser()->botInfo : nullptr;
+	if ((!newinfo && !_botAbout) || (newinfo && _botAbout && _botAbout->info == newinfo)) {
+		return;
+	}
+
+	if (newinfo) {
+		_botAbout.reset(new BotAbout(this, newinfo));
+		if (newinfo && !newinfo->inited && App::api()) {
+			App::api()->requestFullPeer(_peer);
+		}
+	} else {
+		_botAbout.clear();
 	}
 }
 
@@ -2019,10 +1979,7 @@ void HistoryInner::applyDragSelection(SelectedItems *toItems) const {
 }
 
 QString HistoryInner::tooltipText() const {
-	TextLinkPtr lnk = textlnkOver();
-	if (lnk && !lnk->fullDisplayed()) {
-		return lnk->readable();
-	} else if (_dragCursorState == HistoryInDateCursorState && _dragAction == NoDrag) {
+	if (_dragCursorState == HistoryInDateCursorState && _dragAction == NoDrag) {
 		if (App::hoveredItem()) {
 			return App::hoveredItem()->date.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat));
 		}
@@ -2032,6 +1989,8 @@ QString HistoryInner::tooltipText() const {
 				return fwd->_text.original(0, 0xFFFF, Text::ExpandLinksNone);
 			}
 		}
+	} else if (ClickHandlerPtr lnk = ClickHandler::getActive()) {
+		return lnk->tooltip();
 	}
 	return QString();
 }
@@ -2210,6 +2169,8 @@ void BotKeyboard::Style::paintButtonBg(Painter &p, const QRect &rect, bool down,
 }
 
 void BotKeyboard::resizeEvent(QResizeEvent *e) {
+	if (!_impl) return;
+
 	updateStyle();
 
 	_height = _impl->naturalHeight() + 2 * _st->margin;
@@ -2225,14 +2186,8 @@ void BotKeyboard::resizeEvent(QResizeEvent *e) {
 void BotKeyboard::mousePressEvent(QMouseEvent *e) {
 	_lastMousePos = e->globalPos();
 	updateSelected();
-	if (textlnkDown() != textlnkOver()) {
-		Ui::repaintHistoryItem(App::pressedLinkItem());
-		textlnkDown(textlnkOver());
-		App::hoveredLinkItem(nullptr);
-		App::pressedLinkItem(App::hoveredLinkItem());
-		Ui::repaintHistoryItem(App::pressedLinkItem());
-	}
-	update();
+
+	ClickHandler::pressed();
 }
 
 void BotKeyboard::mouseMoveEvent(QMouseEvent *e) {
@@ -2241,13 +2196,11 @@ void BotKeyboard::mouseMoveEvent(QMouseEvent *e) {
 }
 
 void BotKeyboard::mouseReleaseEvent(QMouseEvent *e) {
-	TextLinkPtr down(textlnkDown());
-	textlnkDown(TextLinkPtr());
-
 	_lastMousePos = e->globalPos();
 	updateSelected();
-	if (down && textlnkOver() == down) {
-		down->onClick(e->button());
+
+	if (ClickHandlerPtr activated = ClickHandler::unpressed()) {
+		App::activateClickHandler(activated, e->button());
 	}
 }
 
@@ -2256,6 +2209,12 @@ void BotKeyboard::leaveEvent(QEvent *e) {
 	updateSelected();
 }
 
+void BotKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
+}
+
+void BotKeyboard::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
+}
+
 bool BotKeyboard::updateMarkup(HistoryItem *to) {
 	if (to && to->definesReplyKeyboard()) {
 		if (_wasForMsgId == FullMsgId(to->channelId(), to->id)) return false;
@@ -2284,7 +2243,7 @@ bool BotKeyboard::updateMarkup(HistoryItem *to) {
 		_maximizeSize = _singleUse = _forceReply = false;
 		_wasForMsgId = FullMsgId();
 		clearSelection();
-		_impl.reset();
+		_impl.clear();
 		return true;
 	}
 	return false;
@@ -2335,9 +2294,8 @@ QPoint BotKeyboard::tooltipPos() const {
 }
 
 QString BotKeyboard::tooltipText() const {
-	TextLinkPtr lnk = textlnkOver();
-	if (lnk && !lnk->fullDisplayed()) {
-		return lnk->readable();
+	if (ClickHandlerPtr lnk = ClickHandler::getActive()) {
+		return lnk->tooltip();
 	}
 	return QString();
 }
@@ -2345,29 +2303,16 @@ QString BotKeyboard::tooltipText() const {
 void BotKeyboard::updateSelected() {
 	PopupTooltip::Show(1000, this);
 
-	if (textlnkDown() || !_impl) return;
+	if (!_impl) return;
 
 	QPoint p(mapFromGlobal(_lastMousePos));
 	int x = rtl() ? st::botKbScroll.width : _st->margin;
 
-	TextLinkPtr lnk;
+	ClickHandlerPtr lnk;
 	_impl->getState(lnk, p.x() - x, p.y() - _st->margin);
-	if (lnk != textlnkOver()) {
-		if (textlnkOver()) {
-			if (HistoryItem *item = App::hoveredLinkItem()) {
-				item->linkOut(textlnkOver());
-				Ui::repaintHistoryItem(item);
-			} else {
-				App::main()->update();// update(_botDescRect);
-				_impl->linkOut(textlnkOver());
-			}
-		}
-		textlnkOver(lnk);
-		_impl->linkOver(lnk);
+	if (ClickHandler::setActive(lnk, this)) {
 		PopupTooltip::Hide();
-		App::hoveredLinkItem(nullptr);
 		setCursor(lnk ? style::cur_pointer : style::cur_default);
-		update();
 	}
 }
 
@@ -2865,7 +2810,7 @@ void HistoryWidget::onStickersUpdated() {
 
 void HistoryWidget::onMentionHashtagOrBotCommandInsert(QString str) {
 	if (str.at(0) == '/') { // bot command
-		App::sendBotCommand(str);
+		App::sendBotCommand(_peer, str);
 		setFieldText(_field.getLastText().mid(_field.textCursor().position()));
 	} else {
 		_field.onMentionHashtagOrBotCommandInsert(str);
@@ -4729,7 +4674,7 @@ void HistoryWidget::onBotStart() {
 
 	QString token = _peer->asUser()->botInfo->startToken;
 	if (token.isEmpty()) {
-		sendBotCommand(qsl("/start"), 0);
+		sendBotCommand(_peer, qsl("/start"), 0);
 	} else {
 		uint64 randomId = rand_value<uint64>();
 		MTP::send(MTPmessages_StartBot(_peer->asUser()->inputUser, MTP_inputPeerEmpty(), MTP_long(randomId), MTP_string(token)), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::addParticipantFail, _peer->asUser()));
@@ -5158,8 +5103,8 @@ void HistoryWidget::stopRecording(bool send) {
 	_a_record.start();
 }
 
-void HistoryWidget::sendBotCommand(const QString &cmd, MsgId replyTo) { // replyTo != 0 from ReplyKeyboardMarkup, == 0 from cmd links
-	if (!_history) return;
+void HistoryWidget::sendBotCommand(PeerData *peer, const QString &cmd, MsgId replyTo) { // replyTo != 0 from ReplyKeyboardMarkup, == 0 from cmd links
+	if (!_peer || _peer != peer) return;
 
 	bool lastKeyboardUsed = (_keyboard.forMsgId() == FullMsgId(_channel, _history->lastKeyboardId)) && (_keyboard.forMsgId() == FullMsgId(_channel, replyTo));
 
@@ -5184,6 +5129,32 @@ void HistoryWidget::sendBotCommand(const QString &cmd, MsgId replyTo) { // reply
 	_field.setFocus();
 }
 
+void HistoryWidget::sendBotCallback(PeerData *peer, const QString &cmd, MsgId replyTo) {
+	if (!_peer || _peer != peer) return;
+
+	bool lastKeyboardUsed = (_keyboard.forMsgId() == FullMsgId(_channel, _history->lastKeyboardId)) && (_keyboard.forMsgId() == FullMsgId(_channel, replyTo));
+
+	MTP::send(MTPmessages_GetBotCallbackAnswer(_peer->input, MTP_int(replyTo), MTP_string(cmd)), rpcDone(&HistoryWidget::botCallbackDone), rpcFail(&HistoryWidget::botCallbackFail));
+
+	if (replyTo) {
+		cancelReply();
+		if (_keyboard.singleUse() && _keyboard.hasMarkup() && lastKeyboardUsed) {
+			if (_kbShown) onKbToggle(false);
+			_history->lastKeyboardUsed = true;
+		}
+	}
+}
+
+void HistoryWidget::botCallbackDone(const MTPmessages_BotCallbackAnswer &answer) {
+
+}
+
+bool HistoryWidget::botCallbackFail(const RPCError &error) {
+	if (mtpIsFlood(error)) return false;
+
+	return true;
+}
+
 bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) {
 	if (!_history) return false;
 
@@ -6169,6 +6140,10 @@ bool HistoryWidget::ui_isInlineItemBeingChosen() {
 	return _emojiPan.ui_isInlineItemBeingChosen();
 }
 
+PeerData *HistoryWidget::ui_getPeerForMouseAction() {
+	return _peer;
+}
+
 void HistoryWidget::notify_historyItemLayoutChanged(const HistoryItem *item) {
 	if (_peer && _list && (item == App::mousedItem() || item == App::hoveredItem() || item == App::hoveredLinkItem())) {
 		_list->onUpdateSelected();
diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h
index 94e168cb4..626320a46 100644
--- a/Telegram/SourceFiles/historywidget.h
+++ b/Telegram/SourceFiles/historywidget.h
@@ -101,9 +101,9 @@ public:
 	void notifyIsBotChanged();
 	void notifyMigrateUpdated();
 
-	// AbstractTooltipShower
-	virtual QString tooltipText() const;
-	virtual QPoint tooltipPos() const;
+	// AbstractTooltipShower interface
+	QString tooltipText() const override;
+	QPoint tooltipPos() const override;
 
 	~HistoryInner();
 
@@ -149,10 +149,24 @@ private:
 	// or at least we don't need to display first _history date (just skip it by height)
 	int _historySkipHeight = 0;
 
-	BotInfo *_botInfo = nullptr;
-	int _botDescWidth = 0;
-	int _botDescHeight = 0;
-	QRect _botDescRect;
+	class BotAbout : public ClickHandlerHost {
+	public:
+		BotAbout(HistoryInner *parent, BotInfo *info) : _parent(parent), info(info) {
+		}
+		BotInfo *info = nullptr;
+		int width = 0;
+		int height = 0;
+		QRect rect;
+
+		// ClickHandlerHost interface
+		void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
+		void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
+
+	private:
+		HistoryInner *_parent;
+
+	};
+	UniquePointer<BotAbout> _botAbout;
 
 	HistoryWidget *_widget = nullptr;
 	ScrollArea *_scroll = nullptr;
@@ -187,7 +201,7 @@ private:
 	QPoint _trippleClickPoint;
 	QTimer _trippleClickTimer;
 
-	TextLinkPtr _contextMenuLnk;
+	ClickHandlerPtr _contextMenuLnk;
 
 	HistoryItem *_dragSelFrom = nullptr;
 	HistoryItem *_dragSelTo = nullptr;
@@ -285,7 +299,7 @@ private:
 
 };
 
-class BotKeyboard : public TWidget, public AbstractTooltipShower {
+class BotKeyboard : public TWidget, public AbstractTooltipShower, public ClickHandlerHost {
 	Q_OBJECT
 
 public:
@@ -313,9 +327,13 @@ public:
 		return _wasForMsgId;
 	}
 
-	// AbstractTooltipShower
-	virtual QString tooltipText() const;
-	virtual QPoint tooltipPos() const;
+	// AbstractTooltipShower interface
+	QString tooltipText() const override;
+	QPoint tooltipPos() const override;
+
+	// ClickHandlerHost interface
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active);
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed);
 
 public slots:
 
@@ -439,9 +457,9 @@ public:
 	void mouseReleaseEvent(QMouseEvent *e);
 	void leaveEvent(QEvent *e);
 
-	// AbstractTooltipShower
-	virtual QString tooltipText() const;
-	virtual QPoint tooltipPos() const;
+	// AbstractTooltipShower interface
+	QString tooltipText() const override;
+	QPoint tooltipPos() const override;
 
 };
 
@@ -582,7 +600,8 @@ public:
 
 	void onListEscapePressed();
 
-	void sendBotCommand(const QString &cmd, MsgId replyTo);
+	void sendBotCommand(PeerData *peer, const QString &cmd, MsgId replyTo);
+	void sendBotCallback(PeerData *peer, const QString &cmd, MsgId replyTo);
 	bool insertBotCommand(const QString &cmd, bool specialGif);
 
 	bool eventFilter(QObject *obj, QEvent *e) override;
@@ -629,6 +648,7 @@ public:
 	void ui_repaintInlineItem(const LayoutInlineItem *gif);
 	bool ui_isInlineItemVisible(const LayoutInlineItem *layout);
 	bool ui_isInlineItemBeingChosen();
+	PeerData *ui_getPeerForMouseAction();
 
 	void notify_historyItemLayoutChanged(const HistoryItem *item);
 	void notify_botCommandsChanged(UserData *user);
@@ -836,6 +856,9 @@ private:
 	void addMessagesToFront(PeerData *peer, const QVector<MTPMessage> &messages, const QVector<MTPMessageGroup> *collapsed);
 	void addMessagesToBack(PeerData *peer, const QVector<MTPMessage> &messages, const QVector<MTPMessageGroup> *collapsed);
 
+	void botCallbackDone(const MTPmessages_BotCallbackAnswer &answer);
+	bool botCallbackFail(const RPCError &error);
+
 	enum ScrollChangeType {
 		ScrollChangeNone,
 		ScrollChangeAdd,
diff --git a/Telegram/SourceFiles/intro/introphone.cpp b/Telegram/SourceFiles/intro/introphone.cpp
index f915ab3a7..bc4d8f691 100644
--- a/Telegram/SourceFiles/intro/introphone.cpp
+++ b/Telegram/SourceFiles/intro/introphone.cpp
@@ -28,20 +28,19 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 #include "intro/introcode.h"
 
 namespace {
-	class SignUpLink : public ITextLink {
-		TEXT_LINK_CLASS(SignUpLink)
-
+	class SignUpClickHandler : public LeftButtonClickHandler {
 	public:
-
-		SignUpLink(IntroPhone *widget) : _widget(widget) {
+		SignUpClickHandler(IntroPhone *widget) : _widget(widget) {
 		}
 
-		void onClick(Qt::MouseButton) const {
+	protected:
+		void onClickImpl() const override {
 			_widget->toSignUp();
 		}
 
 	private:
 		IntroPhone *_widget;
+
 	};
 }
 
@@ -71,7 +70,7 @@ IntroPhone::IntroPhone(IntroWidget *parent) : IntroStep(parent)
 	connect(intro(), SIGNAL(countryChanged()), this, SLOT(countryChanged()));
 	connect(&checkRequest, SIGNAL(timeout()), this, SLOT(onCheckRequest()));
 
-	_signup.setLink(1, TextLinkPtr(new SignUpLink(this)));
+	_signup.setLink(1, MakeShared<SignUpClickHandler>(this));
 	_signup.hide();
 
 	_signupCache = myGrab(&_signup);
diff --git a/Telegram/SourceFiles/layout.cpp b/Telegram/SourceFiles/layout.cpp
index 794d81de5..b1f3d572d 100644
--- a/Telegram/SourceFiles/layout.cpp
+++ b/Telegram/SourceFiles/layout.cpp
@@ -217,24 +217,32 @@ RoundCorners documentCorners(int32 colorIndex) {
 	return RoundCorners(DocBlueCorners + (colorIndex & 3));
 }
 
-void LayoutRadialProgressItem::linkOver(const TextLinkPtr &lnk) {
-	if (lnk == _openl || lnk == _savel || lnk == _cancell) {
-		a_iconOver.start(1);
-		_a_iconOver.start();
-	}
+void LayoutMediaItem::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
+	App::hoveredLinkItem(active ? _parent : nullptr);
+	Ui::repaintHistoryItem(_parent);
 }
 
-void LayoutRadialProgressItem::linkOut(const TextLinkPtr &lnk) {
-	if (lnk == _openl || lnk == _savel || lnk == _cancell) {
-		a_iconOver.start(0);
-		_a_iconOver.start();
-	}
+void LayoutMediaItem::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
+	App::pressedLinkItem(pressed ? _parent : nullptr);
+	Ui::repaintHistoryItem(_parent);
 }
 
-void LayoutRadialProgressItem::setLinks(ITextLink *openl, ITextLink *savel, ITextLink *cancell) {
-	_openl.reset(openl);
-	_savel.reset(savel);
-	_cancell.reset(cancell);
+void LayoutRadialProgressItem::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
+	if (p == _openl || p == _savel || p == _cancell) {
+		a_iconOver.start(active ? 1 : 0);
+		_a_iconOver.start();
+	}
+	LayoutMediaItem::clickHandlerActiveChanged(p, active);
+}
+
+void LayoutRadialProgressItem::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
+	LayoutMediaItem::clickHandlerPressedChanged(p, pressed);
+}
+
+void LayoutRadialProgressItem::setLinks(ClickHandlerPtr &&openl, ClickHandlerPtr &&savel, ClickHandlerPtr &&cancell) {
+	_openl = std_::move(openl);
+	_savel = std_::move(savel);
+	_cancell = std_::move(cancell);
 }
 
 void LayoutRadialProgressItem::step_iconOver(float64 ms, bool timer) {
@@ -314,7 +322,7 @@ void LayoutOverviewDate::paint(Painter &p, const QRect &clip, uint32 selection,
 
 LayoutOverviewPhoto::LayoutOverviewPhoto(PhotoData *photo, HistoryItem *parent) : LayoutMediaItem(parent)
 , _data(photo)
-, _link(new PhotoLink(photo))
+, _link(new PhotoOpenClickHandler(photo))
 , _goodLoaded(false) {
 
 }
@@ -380,7 +388,7 @@ void LayoutOverviewPhoto::paint(Painter &p, const QRect &clip, uint32 selection,
 	}
 }
 
-void LayoutOverviewPhoto::getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
+void LayoutOverviewPhoto::getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
 	if (hasPoint(x, y)) {
 		link = _link;
 	}
@@ -390,7 +398,7 @@ LayoutOverviewVideo::LayoutOverviewVideo(DocumentData *video, HistoryItem *paren
 , _data(video)
 , _duration(formatDurationText(_data->duration()))
 , _thumbLoaded(false) {
-	setLinks(new DocumentOpenLink(_data), new DocumentSaveLink(_data), new DocumentCancelLink(_data));
+	setDocumentLinks(_data);
 }
 
 void LayoutOverviewVideo::initDimensions() {
@@ -485,7 +493,7 @@ void LayoutOverviewVideo::paint(Painter &p, const QRect &clip, uint32 selection,
 			p.setOpacity((st::msgDateImgBg->c.alphaF() * (1 - over)) + (st::msgDateImgBgOver->c.alphaF() * over));
 			p.setBrush(st::black);
 		} else {
-			bool over = textlnkDrawOver(loaded ? _openl : (_data->loading() ? _cancell : _savel));
+			bool over = ClickHandler::showAsActive(loaded ? _openl : (_data->loading() ? _cancell : _savel));
 			p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg);
 		}
 
@@ -517,7 +525,7 @@ void LayoutOverviewVideo::paint(Painter &p, const QRect &clip, uint32 selection,
 	}
 }
 
-void LayoutOverviewVideo::getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
+void LayoutOverviewVideo::getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
 	bool loaded = _data->loaded();
 
 	if (hasPoint(x, y)) {
@@ -552,17 +560,18 @@ void LayoutOverviewVideo::updateStatusText() const {
 
 LayoutOverviewVoice::LayoutOverviewVoice(DocumentData *voice, HistoryItem *parent) : LayoutAbstractFileItem(parent)
 , _data(voice)
-, _namel(new DocumentOpenLink(_data)) {
+, _namel(new DocumentOpenClickHandler(_data)) {
 	AddComponents(OverviewItemInfo::Bit());
 
 	t_assert(_data->voice() != 0);
 
-	setLinks(new DocumentOpenLink(_data), new DocumentOpenLink(_data), new DocumentCancelLink(_data));
+	setDocumentLinks(_data);
+
 	updateName();
 	QString d = textcmdLink(1, textRichPrepare(langDateTime(date(_data->date))));
 	TextParseOptions opts = { TextParseRichText, 0, 0, Qt::LayoutDirectionAuto };
 	_details.setText(st::normalFont, lng_date_and_duration(lt_date, d, lt_duration, formatDurationText(_data->voice()->duration)), opts);
-	_details.setLink(1, TextLinkPtr(new MessageLink(parent)));
+	_details.setLink(1, MakeShared<GoToMessageClickHandler>(parent));
 }
 
 void LayoutOverviewVoice::initDimensions() {
@@ -610,7 +619,7 @@ void LayoutOverviewVoice::paint(Painter &p, const QRect &clip, uint32 selection,
 			float64 over = a_iconOver.current();
 			p.setBrush(style::interpolate(st::msgFileInBg, st::msgFileInBgOver, over));
 		} else {
-			bool over = textlnkDrawOver(loaded ? _openl : (_data->loading() ? _cancell : _openl));
+			bool over = ClickHandler::showAsActive(loaded ? _openl : (_data->loading() ? _cancell : _openl));
 			p.setBrush(over ? st::msgFileInBgOver : st::msgFileInBg);
 		}
 
@@ -669,7 +678,7 @@ void LayoutOverviewVoice::paint(Painter &p, const QRect &clip, uint32 selection,
 	}
 }
 
-void LayoutOverviewVoice::getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
+void LayoutOverviewVoice::getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
 	bool loaded = _data->loaded();
 
 	bool showPause = updateStatusText();
@@ -746,8 +755,8 @@ bool LayoutOverviewVoice::updateStatusText() const {
 
 LayoutOverviewDocument::LayoutOverviewDocument(DocumentData *document, HistoryItem *parent) : LayoutAbstractFileItem(parent)
 , _data(document)
-, _msgl(new MessageLink(parent))
-, _namel(new DocumentOpenLink(_data))
+, _msgl(new GoToMessageClickHandler(parent))
+, _namel(new DocumentOpenClickHandler(_data))
 , _thumbForLoaded(false)
 , _name(documentName(_data))
 , _date(langDateTime(date(_data->date)))
@@ -756,7 +765,7 @@ LayoutOverviewDocument::LayoutOverviewDocument(DocumentData *document, HistoryIt
 , _colorIndex(documentColorIndex(_data, _ext)) {
 	AddComponents(OverviewItemInfo::Bit());
 
-	setLinks(new DocumentOpenLink(_data), new DocumentSaveLink(_data), new DocumentCancelLink(_data));
+	setDocumentLinks(_data);
 
 	setStatusSize(FileStatusSizeReady, _data->size, _data->song() ? _data->song()->duration : -1, 0);
 
@@ -826,7 +835,7 @@ void LayoutOverviewDocument::paint(Painter &p, const QRect &clip, uint32 selecti
 				float64 over = a_iconOver.current();
 				p.setBrush(style::interpolate(st::msgFileInBg, st::msgFileInBgOver, over));
 			} else {
-				bool over = textlnkDrawOver(loaded ? _openl : (_data->loading() ? _cancell : _openl));
+				bool over = ClickHandler::showAsActive(loaded ? _openl : (_data->loading() ? _cancell : _openl));
 				p.setBrush(over ? st::msgFileInBgOver : st::msgFileInBg);
 			}
 
@@ -908,7 +917,7 @@ void LayoutOverviewDocument::paint(Painter &p, const QRect &clip, uint32 selecti
 							p.setBrush(style::interpolate(documentDarkColor(_colorIndex), documentOverColor(_colorIndex), over));
 						}
 					} else {
-						bool over = textlnkDrawOver(_data->loading() ? _cancell : _savel);
+						bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
 						p.setBrush(over ? (wthumb ? st::msgDateImgBgOver : documentOverColor(_colorIndex)) : (wthumb ? st::msgDateImgBg : documentDarkColor(_colorIndex)));
 					}
 					p.setOpacity(radialOpacity * p.opacity());
@@ -959,13 +968,13 @@ void LayoutOverviewDocument::paint(Painter &p, const QRect &clip, uint32 selecti
 		p.drawTextLeft(nameleft, statustop, _width, _statusText);
 	}
 	if (datetop >= 0 && clip.intersects(rtlrect(nameleft, datetop, _datew, st::normalFont->height, _width))) {
-		p.setFont(textlnkDrawOver(_msgl) ? st::normalFont->underline() : st::normalFont);
+		p.setFont(ClickHandler::showAsActive(_msgl) ? st::normalFont->underline() : st::normalFont);
 		p.setPen(st::mediaInFg);
 		p.drawTextLeft(nameleft, datetop, _width, _date, _datew);
 	}
 }
 
-void LayoutOverviewDocument::getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
+void LayoutOverviewDocument::getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
 	bool loaded = _data->loaded() || Local::willStickerImageLoad(mediaKey(DocumentFileLocation, _data->dc, _data->id));
 
 	bool showPause = updateStatusText();
@@ -1062,14 +1071,14 @@ bool LayoutOverviewDocument::updateStatusText() const {
 }
 
 namespace {
-	ITextLink *linkFromUrl(const QString &url) {
+	TextClickHandlerPtr clickHandlerFromUrl(const QString &url) {
 		int32 at = url.indexOf('@'), slash = url.indexOf('/');
 		if ((at > 0) && (slash < 0 || slash > at)) {
-			return new EmailLink(url);
+			return MakeShared<EmailClickHandler>(url);
 		}
-		return new TextLink(url);
+		return MakeShared<UrlClickHandler>(url);
 	}
-}
+} // namespace
 
 LayoutOverviewLink::LayoutOverviewLink(HistoryMedia *media, HistoryItem *parent) : LayoutMediaItem(parent) {
 	AddComponents(OverviewItemInfo::Bit());
@@ -1109,20 +1118,20 @@ LayoutOverviewLink::LayoutOverviewLink(HistoryMedia *media, HistoryItem *parent)
 	_page = (media && media->type() == MediaTypeWebPage) ? static_cast<HistoryWebPage*>(media)->webpage() : 0;
 	if (_page) {
 		if (_page->doc) {
-			_photol = TextLinkPtr(new DocumentOpenLink(_page->doc));
+			_photol.reset(new DocumentOpenClickHandler(_page->doc));
 		} else if (_page->photo) {
 			if (_page->type == WebPageProfile || _page->type == WebPageVideo) {
-				_photol = TextLinkPtr(linkFromUrl(_page->url));
+				_photol = clickHandlerFromUrl(_page->url);
 			} else if (_page->type == WebPagePhoto || _page->siteName == qstr("Twitter") || _page->siteName == qstr("Facebook")) {
-				_photol = TextLinkPtr(new PhotoLink(_page->photo));
+				_photol.reset(new PhotoOpenClickHandler(_page->photo));
 			} else {
-				_photol = TextLinkPtr(linkFromUrl(_page->url));
+				_photol = clickHandlerFromUrl(_page->url);
 			}
 		} else {
-			_photol = TextLinkPtr(linkFromUrl(_page->url));
+			_photol = clickHandlerFromUrl(_page->url);
 		}
 	} else if (!_links.isEmpty()) {
-		_photol = TextLinkPtr(linkFromUrl(_links.at(0).lnk->text()));
+		_photol = clickHandlerFromUrl(_links.front().lnk->text());
 	}
 	if (from >= till && _page) {
 		text = _page->description;
@@ -1277,7 +1286,7 @@ void LayoutOverviewLink::paint(Painter &p, const QRect &clip, uint32 selection,
 	p.setPen(st::btnYesColor);
 	for (int32 i = 0, l = _links.size(); i < l; ++i) {
 		if (clip.intersects(rtlrect(left, top, qMin(w, _links.at(i).width), st::normalFont->height, _width))) {
-			p.setFont(textlnkDrawOver(_links.at(i).lnk) ? st::normalFont->underline() : st::normalFont);
+			p.setFont(ClickHandler::showAsActive(_links.at(i).lnk) ? st::normalFont->underline() : st::normalFont);
 			p.drawTextLeft(left, top, _width, (w < _links.at(i).width) ? st::normalFont->elided(_links.at(i).text, w) : _links.at(i).text);
 		}
 		top += st::normalFont->height;
@@ -1291,7 +1300,7 @@ void LayoutOverviewLink::paint(Painter &p, const QRect &clip, uint32 selection,
 	}
 }
 
-void LayoutOverviewLink::getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
+void LayoutOverviewLink::getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
 	int32 left = st::dlgPhotoSize + st::dlgPhotoPadding, top = st::linksMargin.top() + st::linksBorder, w = _width - left;
 	if (rtlrect(0, top, st::dlgPhotoSize, st::dlgPhotoSize, _width).contains(x, y)) {
 		link = _photol;
@@ -1323,7 +1332,7 @@ void LayoutOverviewLink::getState(TextLinkPtr &link, HistoryCursorState &cursor,
 LayoutOverviewLink::Link::Link(const QString &url, const QString &text)
 : text(text)
 , width(st::normalFont->width(text))
-, lnk(linkFromUrl(url)) {
+, lnk(clickHandlerFromUrl(url)) {
 }
 
 LayoutInlineItem::LayoutInlineItem(InlineResult *result, DocumentData *doc, PhotoData *photo) : LayoutItem()
@@ -1378,8 +1387,8 @@ void LayoutInlineItem::update() {
 LayoutInlineGif::LayoutInlineGif(InlineResult *result, DocumentData *doc, bool saved) : LayoutInlineItem(result, doc, 0)
 , _state(0)
 , _gif(0)
-, _send(new SendInlineItemLink())
-, _delete((doc && saved) ? new DeleteSavedGifLink(doc) : 0)
+, _send(new SendInlineItemClickHandler())
+, _delete((doc && saved) ? new DeleteSavedGifClickHandler(doc) : nullptr)
 , _animation(0) {
 }
 
@@ -1402,9 +1411,7 @@ void LayoutInlineGif::setPosition(int32 position) {
 	}
 }
 
-void DeleteSavedGifLink::onClick(Qt::MouseButton button) const {
-	if (button != Qt::LeftButton) return;
-
+void DeleteSavedGifClickHandler::onClickImpl() const {
 	int32 index = cSavedGifs().indexOf(_data);
 	if (index >= 0) {
 		cRefSavedGifs().remove(index);
@@ -1490,7 +1497,7 @@ void LayoutInlineGif::paint(Painter &p, const QRect &clip, uint32 selection, con
 	}
 }
 
-void LayoutInlineGif::getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
+void LayoutInlineGif::getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
 	if (x >= 0 && x < _width && y >= 0 && y < st::inlineMediaHeight) {
 		if (_delete && (rtl() ? _width - x : x) >= _width - st::stickerPanDelete.pxWidth() && y < st::stickerPanDelete.pxHeight()) {
 			link = _delete;
@@ -1500,45 +1507,43 @@ void LayoutInlineGif::getState(TextLinkPtr &link, HistoryCursorState &cursor, in
 	}
 }
 
-void LayoutInlineGif::linkOver(const TextLinkPtr &link) {
-	if (_delete && link == _delete) {
-		if (!(_state & StateDeleteOver)) {
-			EnsureAnimation(_a_deleteOver, 0, func(this, &LayoutInlineGif::update));
-			_state |= StateDeleteOver;
-			_a_deleteOver.start(1, st::stickersRowDuration);
-		}
-	}
-	if ((_delete && link == _delete) || link == _send) {
-		if (!content_loaded()) {
-			ensureAnimation();
-			if (!(_state & StateOver)) {
-				EnsureAnimation(_animation->_a_over, 0, func(this, &LayoutInlineGif::update));
-				_animation->_a_over.start(1, st::stickersRowDuration);
+void LayoutInlineGif::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
+	if (!p) return;
+
+	if (_delete && p == _delete) {
+		bool wasactive = (_state & StateDeleteOver);
+		if (active != wasactive) {
+			float64 from = active ? 0 : 1, to = active ? 1 : 0;
+			EnsureAnimation(_a_deleteOver, from, func(this, &LayoutInlineGif::update));
+			_a_deleteOver.start(to, st::stickersRowDuration);
+			if (active) {
+				_state |= StateDeleteOver;
+			} else {
+				_state &= ~StateDeleteOver;
 			}
 		}
-		_state |= StateOver;
 	}
+	if (p == _delete || p == _send) {
+		bool wasactive = (_state & StateOver);
+		if (active != wasactive) {
+			if (!content_loaded()) {
+				ensureAnimation();
+				float64 from = active ? 0 : 1, to = active ? 1 : 0;
+				EnsureAnimation(_animation->_a_over, from, func(this, &LayoutInlineGif::update));
+				_animation->_a_over.start(to, st::stickersRowDuration);
+			}
+			if (active) {
+				_state |= StateOver;
+			} else {
+				_state &= ~StateOver;
+			}
+		}
+	}
+	LayoutInlineItem::clickHandlerActiveChanged(p, active);
 }
 
-void LayoutInlineGif::linkOut(const TextLinkPtr &link) {
-	if (_delete && link == _delete) {
-		if (_state & StateDeleteOver) {
-			update();
-			EnsureAnimation(_a_deleteOver, 1, func(this, &LayoutInlineItem::update));
-			_state &= ~StateDeleteOver;
-			_a_deleteOver.start(0, st::stickersRowDuration);
-		}
-	}
-	if ((_delete && link == _delete) || link == _send) {
-		if (!content_loaded()) {
-			ensureAnimation();
-			if (_state & StateOver) {
-				EnsureAnimation(_animation->_a_over, 1, func(this, &LayoutInlineItem::update));
-				_animation->_a_over.start(0, st::stickersRowDuration);
-			}
-		}
-		_state &= ~StateOver;
-	}
+void LayoutInlineGif::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
+	LayoutInlineItem::clickHandlerPressedChanged(p, pressed);
 }
 
 QSize LayoutInlineGif::countFrameSize() const {
@@ -1728,7 +1733,7 @@ QByteArray LayoutInlineGif::content_data() const {
 }
 
 LayoutInlinePhoto::LayoutInlinePhoto(InlineResult *result, PhotoData *photo) : LayoutInlineItem(result, 0, photo)
-, _send(new SendInlineItemLink())
+, _send(new SendInlineItemClickHandler())
 , _thumbLoaded(false) {
 }
 
@@ -1759,7 +1764,7 @@ void LayoutInlinePhoto::paint(Painter &p, const QRect &clip, uint32 selection, c
 	}
 }
 
-void LayoutInlinePhoto::getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
+void LayoutInlinePhoto::getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
 	if (x >= 0 && x < _width && y >= 0 && y < st::inlineMediaHeight) {
 		link = _send;
 	}
@@ -1853,10 +1858,12 @@ void LayoutInlinePhoto::content_forget() {
 }
 
 LayoutInlineWebVideo::LayoutInlineWebVideo(InlineResult *result) : LayoutInlineItem(result, 0, 0)
-, _send(new SendInlineItemLink())
-, _link(result->content_url.isEmpty() ? 0 : linkFromUrl(result->content_url))
+, _send(new SendInlineItemClickHandler())
 , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip)
 , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) {
+	if (!result->content_url.isEmpty()) {
+		_link = clickHandlerFromUrl(result->content_url);
+	}
 	if (_result->duration) {
 		_duration = formatDurationText(_result->duration);
 	}
@@ -1911,7 +1918,7 @@ void LayoutInlineWebVideo::paint(Painter &p, const QRect &clip, uint32 selection
 	}
 }
 
-void LayoutInlineWebVideo::getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
+void LayoutInlineWebVideo::getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
 	if (x >= 0 && x < st::inlineThumbSize && y >= st::inlineRowMargin && y < st::inlineRowMargin + st::inlineThumbSize) {
 		link = _link;
 		return;
@@ -1945,12 +1952,16 @@ void LayoutInlineWebVideo::prepareThumb(int32 width, int32 height) const {
 }
 
 LayoutInlineArticle::LayoutInlineArticle(InlineResult *result, bool withThumb) : LayoutInlineItem(result, 0, 0)
-, _send(new SendInlineItemLink())
-, _url(result->url.isEmpty() ? 0 : linkFromUrl(result->url))
-, _link(result->content_url.isEmpty() ? 0 : linkFromUrl(result->content_url))
+, _send(new SendInlineItemClickHandler())
 , _withThumb(withThumb)
 , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip)
 , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) {
+	if (!result->url.isEmpty()) {
+		_url = clickHandlerFromUrl(result->url);
+	}
+	if (!result->content_url.isEmpty()) {
+		_link = clickHandlerFromUrl(result->content_url);
+	}
 	QVector<QStringRef> parts = _result->url.splitRef('/');
 	if (!parts.isEmpty()) {
 		QStringRef domain = parts.at(0);
@@ -2046,7 +2057,7 @@ void LayoutInlineArticle::paint(Painter &p, const QRect &clip, uint32 selection,
 	}
 }
 
-void LayoutInlineArticle::getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
+void LayoutInlineArticle::getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
 	int32 left = _withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : 0;
 	if (x >= 0 && x < left - st::inlineThumbSkip && y >= st::inlineRowMargin && y < st::inlineRowMargin + st::inlineThumbSize) {
 		link = _link;
diff --git a/Telegram/SourceFiles/layout.h b/Telegram/SourceFiles/layout.h
index 88d3cc4dd..31ddea8cb 100644
--- a/Telegram/SourceFiles/layout.h
+++ b/Telegram/SourceFiles/layout.h
@@ -92,16 +92,16 @@ public:
 	bool selecting;
 
 	virtual const OverviewPaintContext *toOverviewPaintContext() const {
-		return 0;
+		return nullptr;
 	}
 	virtual const InlinePaintContext *toInlinePaintContext() const {
-		return 0;
+		return nullptr;
 	}
 
 };
 
 class LayoutMediaItem;
-class LayoutItem : public Composer {
+class LayoutItem : public Composer, public ClickHandlerHost {
 public:
 	LayoutItem() {
 	}
@@ -121,8 +121,8 @@ public:
 	}
 
 	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const = 0;
-	virtual void getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
-		link = TextLinkPtr();
+	virtual void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const {
+		link.clear();
 		cursor = HistoryDefaultCursorState;
 	}
 	virtual void getSymbol(uint16 &symbol, bool &after, bool &upon, int32 x, int32 y) const { // from text
@@ -130,10 +130,6 @@ public:
 		symbol = upon ? 0xFFFF : 0;
 		after = false;
 	}
-	virtual void linkOver(const TextLinkPtr &lnk) {
-	}
-	virtual void linkOut(const TextLinkPtr &lnk) {
-	}
 
 	int32 width() const {
 		return _width;
@@ -150,17 +146,17 @@ public:
 	}
 
 	virtual LayoutMediaItem *toLayoutMediaItem() {
-		return 0;
+		return nullptr;
 	}
 	virtual const LayoutMediaItem *toLayoutMediaItem() const {
-		return 0;
+		return nullptr;
 	}
 
 	virtual HistoryItem *getItem() const {
-		return 0;
+		return nullptr;
 	}
 	virtual DocumentData *getDocument() const {
-		return 0;
+		return nullptr;
 	}
 	MsgId msgId() const {
 		const HistoryItem *item = getItem();
@@ -180,16 +176,19 @@ public:
 	LayoutMediaItem(HistoryItem *parent) : _parent(parent) {
 	}
 
-	virtual LayoutMediaItem *toLayoutMediaItem() {
+	LayoutMediaItem *toLayoutMediaItem() override {
 		return this;
 	}
-	virtual const LayoutMediaItem *toLayoutMediaItem() const {
+	const LayoutMediaItem *toLayoutMediaItem() const override {
 		return this;
 	}
-	virtual HistoryItem *getItem() const {
+	HistoryItem *getItem() const override {
 		return _parent;
 	}
 
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool active) override;
+
 protected:
 	HistoryItem *_parent;
 
@@ -203,14 +202,23 @@ public:
 		, _a_iconOver(animation(this, &LayoutRadialProgressItem::step_iconOver)) {
 	}
 
-	void linkOver(const TextLinkPtr &lnk);
-	void linkOut(const TextLinkPtr &lnk);
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool active) override;
 
 	~LayoutRadialProgressItem();
 
 protected:
-	TextLinkPtr _openl, _savel, _cancell;
-	void setLinks(ITextLink *openl, ITextLink *savel, ITextLink *cancell);
+	ClickHandlerPtr _openl, _savel, _cancell;
+	void setLinks(ClickHandlerPtr &&openl, ClickHandlerPtr &&savel, ClickHandlerPtr &&cancell);
+	void setDocumentLinks(DocumentData *document) {
+		ClickHandlerPtr save;
+		if (document->voice()) {
+			save.reset(new DocumentOpenClickHandler(document));
+		} else {
+			save.reset(new DocumentSaveClickHandler(document));
+		}
+		setLinks(MakeShared<DocumentOpenClickHandler>(document), std_::move(save), MakeShared<DocumentCancelClickHandler>(document));
+	}
 
 	void step_iconOver(float64 ms, bool timer);
 	void step_radial(uint64 ms, bool timer);
@@ -279,8 +287,8 @@ class LayoutOverviewDate : public LayoutItem {
 public:
 	LayoutOverviewDate(const QDate &date, bool month);
 
-	virtual void initDimensions();
-	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const;
+	void initDimensions() override;
+	void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const override;
 
 private:
 	QDate _date;
@@ -292,14 +300,14 @@ class LayoutOverviewPhoto : public LayoutMediaItem {
 public:
 	LayoutOverviewPhoto(PhotoData *photo, HistoryItem *parent);
 
-	virtual void initDimensions();
-	virtual int32 resizeGetHeight(int32 width);
-	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const;
-	virtual void getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const;
+	void initDimensions() override;
+	int32 resizeGetHeight(int32 width) override;
+	void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const override;
+	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const override;
 
 private:
 	PhotoData *_data;
-	TextLinkPtr _link;
+	ClickHandlerPtr _link;
 
 	mutable QPixmap _pix;
 	mutable bool _goodLoaded;
@@ -310,22 +318,22 @@ class LayoutOverviewVideo : public LayoutAbstractFileItem {
 public:
 	LayoutOverviewVideo(DocumentData *video, HistoryItem *parent);
 
-	virtual void initDimensions();
-	virtual int32 resizeGetHeight(int32 width);
-	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const;
-	virtual void getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const;
+	void initDimensions() override;
+	int32 resizeGetHeight(int32 width) override;
+	void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const override;
+	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const override;
 
 protected:
-	virtual float64 dataProgress() const {
+	float64 dataProgress() const override {
 		return _data->progress();
 	}
-	virtual bool dataFinished() const {
+	bool dataFinished() const override {
 		return !_data->loading();
 	}
-	virtual bool dataLoaded() const {
+	bool dataLoaded() const override {
 		return _data->loaded();
 	}
-	virtual bool iconAnimated() const {
+	bool iconAnimated() const override {
 		return true;
 	}
 
@@ -344,27 +352,27 @@ class LayoutOverviewVoice : public LayoutAbstractFileItem {
 public:
 	LayoutOverviewVoice(DocumentData *voice, HistoryItem *parent);
 
-	virtual void initDimensions();
-	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const;
-	virtual void getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const;
+	void initDimensions() override;
+	void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const override;
+	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const override;
 
 protected:
-	virtual float64 dataProgress() const {
+	float64 dataProgress() const override {
 		return _data->progress();
 	}
-	virtual bool dataFinished() const {
+	bool dataFinished() const override {
 		return !_data->loading();
 	}
-	virtual bool dataLoaded() const {
+	bool dataLoaded() const override {
 		return _data->loaded();
 	}
-	virtual bool iconAnimated() const {
+	bool iconAnimated() const override {
 		return true;
 	}
 
 private:
 	DocumentData *_data;
-	TextLinkPtr _namel;
+	ClickHandlerPtr _namel;
 
 	mutable Text _name, _details;
 	mutable int32 _nameVersion;
@@ -378,31 +386,31 @@ class LayoutOverviewDocument : public LayoutAbstractFileItem {
 public:
 	LayoutOverviewDocument(DocumentData *document, HistoryItem *parent);
 
-	virtual void initDimensions();
-	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const;
-	virtual void getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const;
+	void initDimensions() override;
+	void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const override;
+	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const override;
 
 	virtual DocumentData *getDocument() const {
 		return _data;
 	}
 
 protected:
-	virtual float64 dataProgress() const {
+	float64 dataProgress() const override {
 		return _data->progress();
 	}
-	virtual bool dataFinished() const {
+	bool dataFinished() const override {
 		return !_data->loading();
 	}
-	virtual bool dataLoaded() const {
+	bool dataLoaded() const override {
 		return _data->loaded();
 	}
-	virtual bool iconAnimated() const {
+	bool iconAnimated() const override {
 		return _data->song() || !_data->loaded() || (_radial && _radial->animating());
 	}
 
 private:
 	DocumentData *_data;
-	TextLinkPtr _msgl, _namel;
+	ClickHandlerPtr _msgl, _namel;
 
 	mutable bool _thumbForLoaded;
 	mutable QPixmap _thumb;
@@ -422,13 +430,13 @@ class LayoutOverviewLink : public LayoutMediaItem {
 public:
 	LayoutOverviewLink(HistoryMedia *media, HistoryItem *parent);
 
-	virtual void initDimensions();
-	virtual int32 resizeGetHeight(int32 width);
-	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const;
-	virtual void getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const;
+	void initDimensions() override;
+	int32 resizeGetHeight(int32 width) override;
+	void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const override;
+	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const override;
 
 private:
-	TextLinkPtr _photol;
+	ClickHandlerPtr _photol;
 
 	QString _title, _letter;
 	int _titlew = 0;
@@ -443,7 +451,7 @@ private:
 		Link(const QString &url, const QString &text);
 		QString text;
 		int32 width;
-		TextLinkPtr lnk;
+		TextClickHandlerPtr lnk;
 	};
 	QVector<Link> _links;
 
@@ -456,7 +464,7 @@ public:
 		, paused(paused)
 		, lastRow(lastRow) {
 	}
-	virtual const InlinePaintContext *toInlinePaintContext() const {
+	const InlinePaintContext *toInlinePaintContext() const override {
 		return this;
 	}
 	bool paused, lastRow;
@@ -481,6 +489,14 @@ public:
 
 	void update();
 
+	// ClickHandlerHost interface
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override {
+		update();
+	}
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override {
+		update();
+	}
+
 protected:
 	InlineResult *_result;
 	DocumentData *_doc;
@@ -490,22 +506,20 @@ protected:
 
 };
 
-class SendInlineItemLink : public ITextLink {
-	TEXT_LINK_CLASS(SendInlineItemLink)
-
+// this type used as a flag, we dynamic_cast<> to it
+class SendInlineItemClickHandler : public ClickHandler {
 public:
-	virtual void onClick(Qt::MouseButton) const {
+	void onClick(Qt::MouseButton) const override {
 	}
-
 };
 
-class DeleteSavedGifLink : public ITextLink {
-	TEXT_LINK_CLASS(DeleteSavedGifLink)
-
+class DeleteSavedGifClickHandler : public LeftButtonClickHandler {
 public:
-	DeleteSavedGifLink(DocumentData *data) : _data(data) {
+	DeleteSavedGifClickHandler(DocumentData *data) : _data(data) {
 	}
-	virtual void onClick(Qt::MouseButton) const;
+
+protected:
+	void onClickImpl() const override;
 
 private:
 	DocumentData  *_data;
@@ -516,17 +530,19 @@ class LayoutInlineGif : public LayoutInlineItem {
 public:
 	LayoutInlineGif(InlineResult *result, DocumentData *doc, bool saved);
 
-	virtual void setPosition(int32 position);
-	virtual void initDimensions();
+	void setPosition(int32 position) override;
+	void initDimensions() override;
 
-	virtual bool fullLine() const {
+	bool fullLine() const override {
 		return false;
 	}
 
-	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const;
-	virtual void getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const;
-	virtual void linkOver(const TextLinkPtr &lnk);
-	virtual void linkOut(const TextLinkPtr &lnk);
+	void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const override;
+	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const override;
+
+	// ClickHandlerHost interface
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
 
 	~LayoutInlineGif();
 
@@ -551,7 +567,7 @@ private:
 	int32 _state;
 
 	ClipReader *_gif;
-	TextLinkPtr _send, _delete;
+	ClickHandlerPtr _send, _delete;
 	bool gif() const {
 		return (!_gif || _gif == BadClipReader) ? false : true;
 	}
@@ -582,14 +598,14 @@ class LayoutInlinePhoto : public LayoutInlineItem {
 public:
 	LayoutInlinePhoto(InlineResult *result, PhotoData *photo);
 
-	virtual void initDimensions();
+	void initDimensions() override;
 
-	virtual bool fullLine() const {
+	bool fullLine() const override {
 		return false;
 	}
 
-	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const;
-	virtual void getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const;
+	void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const override;
+	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const override;
 
 private:
 	QSize countFrameSize() const;
@@ -599,7 +615,7 @@ private:
 	bool content_loaded() const;
 	void content_forget();
 
-	TextLinkPtr _send;
+	ClickHandlerPtr _send;
 
 	mutable QPixmap _thumb;
 	mutable bool _thumbLoaded;
@@ -611,14 +627,14 @@ class LayoutInlineWebVideo : public LayoutInlineItem {
 public:
 	LayoutInlineWebVideo(InlineResult *result);
 
-	virtual void initDimensions();
+	void initDimensions() override;
 
-	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const;
-	virtual void getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const;
+	void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const override;
+	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const override;
 
 private:
 
-	TextLinkPtr _send, _link;
+	ClickHandlerPtr _send, _link;
 
 	mutable QPixmap _thumb;
 	Text _title, _description;
@@ -633,15 +649,15 @@ class LayoutInlineArticle : public LayoutInlineItem {
 public:
 	LayoutInlineArticle(InlineResult *result, bool withThumb);
 
-	virtual void initDimensions();
-	virtual int32 resizeGetHeight(int32 width);
+	void initDimensions() override;
+	int32 resizeGetHeight(int32 width) override;
 
-	virtual void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const;
-	virtual void getState(TextLinkPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const;
+	void paint(Painter &p, const QRect &clip, uint32 selection, const PaintContext *context) const override;
+	void getState(ClickHandlerPtr &link, HistoryCursorState &cursor, int32 x, int32 y) const override;
 
 private:
 
-	TextLinkPtr _send, _url, _link;
+	ClickHandlerPtr _send, _url, _link;
 
 	bool _withThumb;
 	mutable QPixmap _thumb;
diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp
index 2f2805d1e..9f4f3e0a5 100644
--- a/Telegram/SourceFiles/mainwidget.cpp
+++ b/Telegram/SourceFiles/mainwidget.cpp
@@ -1470,8 +1470,12 @@ void MainWidget::stopAnimActive() {
 	history.stopAnimActive();
 }
 
-void MainWidget::sendBotCommand(const QString &cmd, MsgId replyTo) {
-	history.sendBotCommand(cmd, replyTo);
+void MainWidget::sendBotCommand(PeerData *peer, const QString &cmd, MsgId replyTo) {
+	history.sendBotCommand(peer, cmd, replyTo);
+}
+
+void MainWidget::sendBotCallback(PeerData *peer, const QString &cmd, MsgId replyTo) {
+	history.sendBotCallback(peer, cmd, replyTo);
 }
 
 bool MainWidget::insertBotCommand(const QString &cmd, bool specialGif) {
@@ -2352,6 +2356,13 @@ void MainWidget::ui_showPeerHistory(quint64 peerId, qint32 showAtMsgId, bool bac
 	App::wnd()->getTitle()->updateBackButton();
 }
 
+PeerData *MainWidget::ui_getPeerForMouseAction() {
+	if (profile) {
+		return profile->ui_getPeerForMouseAction();
+	}
+	return history.ui_getPeerForMouseAction();
+}
+
 void MainWidget::peerBefore(const PeerData *inPeer, MsgId inMsg, PeerData *&outPeer, MsgId &outMsg) {
 	if (selectingPeer()) {
 		outPeer = 0;
diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h
index 67c2398f0..ae20d3ca3 100644
--- a/Telegram/SourceFiles/mainwidget.h
+++ b/Telegram/SourceFiles/mainwidget.h
@@ -343,7 +343,8 @@ public:
 	uint64 animActiveTimeStart(const HistoryItem *msg) const;
 	void stopAnimActive();
 
-	void sendBotCommand(const QString &cmd, MsgId msgId);
+	void sendBotCommand(PeerData *peer, const QString &cmd, MsgId replyTo);
+	void sendBotCallback(PeerData *peer, const QString &cmd, MsgId replyTo);
 	bool insertBotCommand(const QString &cmd, bool specialGif);
 
 	void searchMessages(const QString &query, PeerData *inPeer);
@@ -438,6 +439,7 @@ public:
 	bool ui_isInlineItemVisible(const LayoutInlineItem *layout);
 	bool ui_isInlineItemBeingChosen();
 	void ui_showPeerHistory(quint64 peer, qint32 msgId, bool back);
+	PeerData *ui_getPeerForMouseAction();
 
 	void notify_botCommandsChanged(UserData *bot);
 	void notify_inlineBotRequesting(bool requesting);
diff --git a/Telegram/SourceFiles/mediaview.cpp b/Telegram/SourceFiles/mediaview.cpp
index 853603304..5cedff00c 100644
--- a/Telegram/SourceFiles/mediaview.cpp
+++ b/Telegram/SourceFiles/mediaview.cpp
@@ -28,12 +28,10 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 #include "gui/filedialog.h"
 
 namespace {
-	class SaveMsgLink : public ITextLink {
-		TEXT_LINK_CLASS(SaveMsgLink)
-
+	class SaveMsgClickHandler : public ClickHandler {
 	public:
 
-		SaveMsgLink(MediaView *view) : _view(view) {
+		SaveMsgClickHandler(MediaView *view) : _view(view) {
 		}
 
 		void onClick(Qt::MouseButton button) const {
@@ -123,7 +121,7 @@ MediaView::MediaView() : TWidget(App::wnd())
 	custom.insert(QChar('c'), qMakePair(textcmdStartLink(1), textcmdStopLink()));
 	_saveMsgText.setRichText(st::medviewSaveMsgFont, lang(lng_mediaview_saved), _textDlgOptions, custom);
 	_saveMsg = QRect(0, 0, _saveMsgText.maxWidth() + st::medviewSaveMsgPadding.left() + st::medviewSaveMsgPadding.right(), st::medviewSaveMsgFont->height + st::medviewSaveMsgPadding.top() + st::medviewSaveMsgPadding.bottom());
-	_saveMsgText.setLink(1, TextLinkPtr(new SaveMsgLink(this)));
+	_saveMsgText.setLink(1, MakeShared<SaveMsgClickHandler>(this));
 
 	_transparentBrush = QBrush(App::sprite().copy(st::mvTransparentBrush));
 
@@ -478,6 +476,15 @@ MediaView::~MediaView() {
 	deleteAndMark(_menu);
 }
 
+void MediaView::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
+	setCursor((active || ClickHandler::getPressed()) ? style::cur_pointer : style::cur_default);
+	update(QRegion(_saveMsg) + _captionRect);
+}
+void MediaView::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
+	setCursor((pressed || ClickHandler::getActive()) ? style::cur_pointer : style::cur_default);
+	update(QRegion(_saveMsg) + _captionRect);
+}
+
 void MediaView::showSaveMsgFile() {
 	psShowInFolder(_saveMsgFilename);
 }
@@ -565,7 +572,7 @@ void MediaView::onSaveAs() {
 			if (_doc->data().isEmpty()) location.accessDisable();
 		} else {
 			if (!fileShown()) {
-				DocumentSaveLink::doSave(_doc, true);
+				DocumentSaveClickHandler::doSave(_doc, true);
 				updateControls();
 			} else {
 				_saveVisible = false;
@@ -594,7 +601,7 @@ void MediaView::onDocClick() {
 	if (_doc->loading()) {
 		onSaveCancel();
 	} else {
-		DocumentOpenLink::doOpen(_doc, ActionOnLoadNone);
+		DocumentOpenClickHandler::doOpen(_doc, ActionOnLoadNone);
 		if (_doc->loading() && !_docRadial.animating()) {
 			_docRadial.start(_doc->progress());
 		}
@@ -624,6 +631,10 @@ void MediaView::clipCallback(ClipReaderNotification notification) {
 	}
 }
 
+PeerData *MediaView::ui_getPeerForMouseAction() {
+	return _history ? _history->peer : nullptr;
+}
+
 void MediaView::onDownload() {
 	if (cAskDownloadPath()) {
 		return onSaveAs();
@@ -649,7 +660,7 @@ void MediaView::onDownload() {
 			location.accessDisable();
 		} else {
 			if (!fileShown()) {
-				DocumentSaveLink::doSave(_doc);
+				DocumentSaveClickHandler::doSave(_doc);
 				updateControls();
 			} else {
 				_saveVisible = false;
@@ -1631,13 +1642,11 @@ void MediaView::mousePressEvent(QMouseEvent *e) {
 	updateOver(e->pos());
 	if (_menu || !_receiveMouse) return;
 
-	if (textlnkDown() != textlnkOver()) {
-		textlnkDown(textlnkOver());
-	}
+	ClickHandler::pressed();
 
 	if (e->button() == Qt::LeftButton) {
 		_down = OverNone;
-		if (!textlnkDown()) {
+		if (!ClickHandler::getPressed()) {
 			if (_over == OverLeftNav && _index >= 0) {
 				moveToNext(-1);
 				_lastAction = e->pos();
@@ -1766,13 +1775,16 @@ bool MediaView::updateOverState(OverState newState) {
 }
 
 void MediaView::updateOver(QPoint pos) {
-	TextLinkPtr lnk;
+	ClickHandlerPtr lnk;
+	ClickHandlerHost *lnkhost = nullptr;
 	bool inText;
 
 	if (_saveMsgStarted && _saveMsg.contains(pos)) {
         _saveMsgText.getState(lnk, inText, pos.x() - _saveMsg.x() - st::medviewSaveMsgPadding.left(), pos.y() - _saveMsg.y() - st::medviewSaveMsgPadding.top(), _saveMsg.width() - st::medviewSaveMsgPadding.left() - st::medviewSaveMsgPadding.right());
+		lnkhost = this;
 	} else if (_captionRect.contains(pos)) {
 		_caption.getState(lnk, inText, pos.x() - _captionRect.x(), pos.y() - _captionRect.y(), _captionRect.width());
+		lnkhost = this;
 	}
 
 	// retina
@@ -1783,11 +1795,7 @@ void MediaView::updateOver(QPoint pos) {
 		pos.setY(pos.y() - 1);
 	}
 
-	if (lnk != textlnkOver()) {
-		textlnkOver(lnk);
-		setCursor((textlnkOver() || textlnkDown()) ? style::cur_pointer : style::cur_default);
-		update(QRegion(_saveMsg) + _captionRect);
-	}
+	ClickHandler::setActive(lnk, lnkhost);
 
 	if (_pressed || _dragging) return;
 
@@ -1816,21 +1824,12 @@ void MediaView::updateOver(QPoint pos) {
 
 void MediaView::mouseReleaseEvent(QMouseEvent *e) {
 	updateOver(e->pos());
-	TextLinkPtr lnk = textlnkDown();
-	textlnkDown(TextLinkPtr());
-	if (lnk && textlnkOver() == lnk) {
-		if (reHashtag().match(lnk->encoded()).hasMatch() && _history && _history->isChannel() && !_history->isMegagroup()) {
-			App::wnd()->hideMediaview();
-			App::searchByHashtag(lnk->encoded(), _history->peer);
-		} else {
-			if (reBotCommand().match(lnk->encoded()).hasMatch() && _history) {
-				App::wnd()->hideMediaview();
-				Ui::showPeerHistory(_history, ShowAtTheEndMsgId);
-			}
-			lnk->onClick(e->button());
-		}
+
+	if (ClickHandlerPtr activated = ClickHandler::unpressed()) {
+		App::activateClickHandler(activated, e->button());
 		return;
 	}
+
 	if (_over == OverName && _down == OverName) {
 		if (App::wnd() && _from) {
 			close();
diff --git a/Telegram/SourceFiles/mediaview.h b/Telegram/SourceFiles/mediaview.h
index 1736bcf10..51f585eff 100644
--- a/Telegram/SourceFiles/mediaview.h
+++ b/Telegram/SourceFiles/mediaview.h
@@ -22,7 +22,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 
 #include "dropdown.h"
 
-class MediaView : public TWidget, public RPCSender {
+class MediaView : public TWidget, public RPCSender, public ClickHandlerHost {
 	Q_OBJECT
 
 public:
@@ -30,7 +30,7 @@ public:
 	MediaView();
 
 	void paintEvent(QPaintEvent *e);
-	
+
 	void keyPressEvent(QKeyEvent *e);
 	void mousePressEvent(QMouseEvent *e);
 	void mouseMoveEvent(QMouseEvent *e);
@@ -72,9 +72,14 @@ public:
 	void onDocClick();
 
 	void clipCallback(ClipReaderNotification notification);
+	PeerData *ui_getPeerForMouseAction();
 
 	~MediaView();
 
+	// ClickHandlerHost interface
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
+
 public slots:
 
 	void onHideControls(bool force = false);
@@ -161,7 +166,7 @@ private:
 	History *_migrated, *_history; // if conversation photos or files overview
 	PeerData *_peer;
 	UserData *_user; // if user profile photos overview
-	
+
 	PeerData *_from;
 	Text _fromName;
 
diff --git a/Telegram/SourceFiles/mtproto/scheme.tl b/Telegram/SourceFiles/mtproto/scheme.tl
index 120ad0b7f..204de380b 100644
--- a/Telegram/SourceFiles/mtproto/scheme.tl
+++ b/Telegram/SourceFiles/mtproto/scheme.tl
@@ -387,6 +387,7 @@ updateBotInlineQuery#c01eea08 query_id:long user_id:int query:string offset:stri
 updateBotInlineSend#f69e113 user_id:int query:string id:string = Update;
 updateEditChannelMessage#1b3f4df7 message:Message pts:int pts_count:int = Update;
 updateChannelPinnedMessage#98592475 channel_id:int id:int = Update;
+updateBotCallbackQuery#5024c2b0 query_id:long user_id:int peer:Peer msg_id:int text:string = Update;
 
 updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
 
@@ -659,6 +660,8 @@ auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType;
 auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType;
 auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType;
 
+messages.botCallbackAnswer#b4868d29 message:string = messages.BotCallbackAnswer;
+
 ---functions---
 
 invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@@ -783,6 +786,8 @@ messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool;
 messages.getInlineBotResults#9324600d bot:InputUser query:string offset:string = messages.BotResults;
 messages.setInlineBotResults#3f23ec12 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector<InputBotInlineResult> cache_time:int next_offset:flags.2?string = Bool;
 messages.sendInlineBotResult#b16e06fe flags:# broadcast:flags.4?true silent:flags.5?true background:flags.6?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string = Updates;
+messages.getBotCallbackAnswer#d3157edf peer:InputPeer msg_id:int text:string = messages.BotCallbackAnswer;
+messages.setBotCallbackAnswer#a13a9254 query_id:long message:string = Bool;
 
 updates.getState#edd4882a = updates.State;
 updates.getDifference#a041495 pts:int date:int qts:int = updates.Difference;
diff --git a/Telegram/SourceFiles/mtproto/scheme_auto.cpp b/Telegram/SourceFiles/mtproto/scheme_auto.cpp
index 1fe0a87a4..40e427612 100644
--- a/Telegram/SourceFiles/mtproto/scheme_auto.cpp
+++ b/Telegram/SourceFiles/mtproto/scheme_auto.cpp
@@ -2946,6 +2946,23 @@ void _serialize_updateChannelPinnedMessage(MTPStringLogger &to, int32 stage, int
 	}
 }
 
+void _serialize_updateBotCallbackQuery(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) {
+	if (stage) {
+		to.add(",\n").addSpaces(lev);
+	} else {
+		to.add("{ updateBotCallbackQuery");
+		to.add("\n").addSpaces(lev);
+	}
+	switch (stage) {
+	case 0: to.add("  query_id: "); ++stages.back(); types.push_back(mtpc_long+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	case 1: to.add("  user_id: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	case 2: to.add("  peer: "); ++stages.back(); types.push_back(0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	case 3: to.add("  msg_id: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	case 4: to.add("  text: "); ++stages.back(); types.push_back(mtpc_string+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break;
+	}
+}
+
 void _serialize_updates_state(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) {
 	if (stage) {
 		to.add(",\n").addSpaces(lev);
@@ -5339,6 +5356,19 @@ void _serialize_auth_sentCodeTypeFlashCall(MTPStringLogger &to, int32 stage, int
 	}
 }
 
+void _serialize_messages_botCallbackAnswer(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) {
+	if (stage) {
+		to.add(",\n").addSpaces(lev);
+	} else {
+		to.add("{ messages_botCallbackAnswer");
+		to.add("\n").addSpaces(lev);
+	}
+	switch (stage) {
+	case 0: to.add("  message: "); ++stages.back(); types.push_back(mtpc_string+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break;
+	}
+}
+
 void _serialize_req_pq(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) {
 	if (stage) {
 		to.add(",\n").addSpaces(lev);
@@ -5887,6 +5917,20 @@ void _serialize_messages_setInlineBotResults(MTPStringLogger &to, int32 stage, i
 	}
 }
 
+void _serialize_messages_setBotCallbackAnswer(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) {
+	if (stage) {
+		to.add(",\n").addSpaces(lev);
+	} else {
+		to.add("{ messages_setBotCallbackAnswer");
+		to.add("\n").addSpaces(lev);
+	}
+	switch (stage) {
+	case 0: to.add("  query_id: "); ++stages.back(); types.push_back(mtpc_long+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	case 1: to.add("  message: "); ++stages.back(); types.push_back(mtpc_string+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break;
+	}
+}
+
 void _serialize_upload_saveFilePart(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) {
 	if (stage) {
 		to.add(",\n").addSpaces(lev);
@@ -7526,6 +7570,21 @@ void _serialize_messages_getInlineBotResults(MTPStringLogger &to, int32 stage, i
 	}
 }
 
+void _serialize_messages_getBotCallbackAnswer(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) {
+	if (stage) {
+		to.add(",\n").addSpaces(lev);
+	} else {
+		to.add("{ messages_getBotCallbackAnswer");
+		to.add("\n").addSpaces(lev);
+	}
+	switch (stage) {
+	case 0: to.add("  peer: "); ++stages.back(); types.push_back(0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	case 1: to.add("  msg_id: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	case 2: to.add("  text: "); ++stages.back(); types.push_back(mtpc_string+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break;
+	default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break;
+	}
+}
+
 void _serialize_updates_getState(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) {
 	to.add("{ updates_getState }"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back();
 }
@@ -8034,6 +8093,7 @@ namespace {
 		_serializers.insert(mtpc_updateBotInlineSend, _serialize_updateBotInlineSend);
 		_serializers.insert(mtpc_updateEditChannelMessage, _serialize_updateEditChannelMessage);
 		_serializers.insert(mtpc_updateChannelPinnedMessage, _serialize_updateChannelPinnedMessage);
+		_serializers.insert(mtpc_updateBotCallbackQuery, _serialize_updateBotCallbackQuery);
 		_serializers.insert(mtpc_updates_state, _serialize_updates_state);
 		_serializers.insert(mtpc_updates_differenceEmpty, _serialize_updates_differenceEmpty);
 		_serializers.insert(mtpc_updates_difference, _serialize_updates_difference);
@@ -8223,6 +8283,7 @@ namespace {
 		_serializers.insert(mtpc_auth_sentCodeTypeSms, _serialize_auth_sentCodeTypeSms);
 		_serializers.insert(mtpc_auth_sentCodeTypeCall, _serialize_auth_sentCodeTypeCall);
 		_serializers.insert(mtpc_auth_sentCodeTypeFlashCall, _serialize_auth_sentCodeTypeFlashCall);
+		_serializers.insert(mtpc_messages_botCallbackAnswer, _serialize_messages_botCallbackAnswer);
 
 		_serializers.insert(mtpc_req_pq, _serialize_req_pq);
 		_serializers.insert(mtpc_req_DH_params, _serialize_req_DH_params);
@@ -8265,6 +8326,7 @@ namespace {
 		_serializers.insert(mtpc_messages_reorderStickerSets, _serialize_messages_reorderStickerSets);
 		_serializers.insert(mtpc_messages_saveGif, _serialize_messages_saveGif);
 		_serializers.insert(mtpc_messages_setInlineBotResults, _serialize_messages_setInlineBotResults);
+		_serializers.insert(mtpc_messages_setBotCallbackAnswer, _serialize_messages_setBotCallbackAnswer);
 		_serializers.insert(mtpc_upload_saveFilePart, _serialize_upload_saveFilePart);
 		_serializers.insert(mtpc_upload_saveBigFilePart, _serialize_upload_saveBigFilePart);
 		_serializers.insert(mtpc_help_saveAppLog, _serialize_help_saveAppLog);
@@ -8381,6 +8443,7 @@ namespace {
 		_serializers.insert(mtpc_messages_searchGifs, _serialize_messages_searchGifs);
 		_serializers.insert(mtpc_messages_getSavedGifs, _serialize_messages_getSavedGifs);
 		_serializers.insert(mtpc_messages_getInlineBotResults, _serialize_messages_getInlineBotResults);
+		_serializers.insert(mtpc_messages_getBotCallbackAnswer, _serialize_messages_getBotCallbackAnswer);
 		_serializers.insert(mtpc_updates_getState, _serialize_updates_getState);
 		_serializers.insert(mtpc_updates_getDifference, _serialize_updates_getDifference);
 		_serializers.insert(mtpc_updates_getChannelDifference, _serialize_updates_getChannelDifference);
diff --git a/Telegram/SourceFiles/mtproto/scheme_auto.h b/Telegram/SourceFiles/mtproto/scheme_auto.h
index e945eba47..203ae4768 100644
--- a/Telegram/SourceFiles/mtproto/scheme_auto.h
+++ b/Telegram/SourceFiles/mtproto/scheme_auto.h
@@ -281,6 +281,7 @@ enum {
 	mtpc_updateBotInlineSend = 0xf69e113,
 	mtpc_updateEditChannelMessage = 0x1b3f4df7,
 	mtpc_updateChannelPinnedMessage = 0x98592475,
+	mtpc_updateBotCallbackQuery = 0x5024c2b0,
 	mtpc_updates_state = 0xa56c2a3e,
 	mtpc_updates_differenceEmpty = 0x5d75a138,
 	mtpc_updates_difference = 0xf49ca0,
@@ -470,6 +471,7 @@ enum {
 	mtpc_auth_sentCodeTypeSms = 0xc000bba2,
 	mtpc_auth_sentCodeTypeCall = 0x5353e5a7,
 	mtpc_auth_sentCodeTypeFlashCall = 0xab03c6d9,
+	mtpc_messages_botCallbackAnswer = 0xb4868d29,
 	mtpc_invokeAfterMsg = 0xcb9f372d,
 	mtpc_invokeAfterMsgs = 0x3dc4b4f0,
 	mtpc_initConnection = 0x69796de9,
@@ -587,6 +589,8 @@ enum {
 	mtpc_messages_getInlineBotResults = 0x9324600d,
 	mtpc_messages_setInlineBotResults = 0x3f23ec12,
 	mtpc_messages_sendInlineBotResult = 0xb16e06fe,
+	mtpc_messages_getBotCallbackAnswer = 0xd3157edf,
+	mtpc_messages_setBotCallbackAnswer = 0xa13a9254,
 	mtpc_updates_getState = 0xedd4882a,
 	mtpc_updates_getDifference = 0xa041495,
 	mtpc_updates_getChannelDifference = 0xbb32d7c0,
@@ -976,6 +980,7 @@ class MTPDupdateBotInlineQuery;
 class MTPDupdateBotInlineSend;
 class MTPDupdateEditChannelMessage;
 class MTPDupdateChannelPinnedMessage;
+class MTPDupdateBotCallbackQuery;
 
 class MTPupdates_state;
 class MTPDupdates_state;
@@ -1280,6 +1285,9 @@ class MTPDauth_sentCodeTypeSms;
 class MTPDauth_sentCodeTypeCall;
 class MTPDauth_sentCodeTypeFlashCall;
 
+class MTPmessages_botCallbackAnswer;
+class MTPDmessages_botCallbackAnswer;
+
 
 // Boxed types definitions
 typedef MTPBoxed<MTPresPQ> MTPResPQ;
@@ -1446,6 +1454,7 @@ typedef MTPBoxed<MTPmessageFwdHeader> MTPMessageFwdHeader;
 typedef MTPBoxed<MTPchannels_messageEditData> MTPchannels_MessageEditData;
 typedef MTPBoxed<MTPauth_codeType> MTPauth_CodeType;
 typedef MTPBoxed<MTPauth_sentCodeType> MTPauth_SentCodeType;
+typedef MTPBoxed<MTPmessages_botCallbackAnswer> MTPmessages_BotCallbackAnswer;
 
 // Type classes definitions
 
@@ -5410,6 +5419,18 @@ public:
 		return *(const MTPDupdateChannelPinnedMessage*)data;
 	}
 
+	MTPDupdateBotCallbackQuery &_updateBotCallbackQuery() {
+		if (!data) throw mtpErrorUninitialized();
+		if (_type != mtpc_updateBotCallbackQuery) throw mtpErrorWrongTypeId(_type, mtpc_updateBotCallbackQuery);
+		split();
+		return *(MTPDupdateBotCallbackQuery*)data;
+	}
+	const MTPDupdateBotCallbackQuery &c_updateBotCallbackQuery() const {
+		if (!data) throw mtpErrorUninitialized();
+		if (_type != mtpc_updateBotCallbackQuery) throw mtpErrorWrongTypeId(_type, mtpc_updateBotCallbackQuery);
+		return *(const MTPDupdateBotCallbackQuery*)data;
+	}
+
 	uint32 innerLength() const;
 	mtpTypeId type() const;
 	void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons);
@@ -5462,6 +5483,7 @@ private:
 	explicit MTPupdate(MTPDupdateBotInlineSend *_data);
 	explicit MTPupdate(MTPDupdateEditChannelMessage *_data);
 	explicit MTPupdate(MTPDupdateChannelPinnedMessage *_data);
+	explicit MTPupdate(MTPDupdateBotCallbackQuery *_data);
 
 	friend class MTP::internal::TypeCreator;
 
@@ -9016,6 +9038,37 @@ private:
 };
 typedef MTPBoxed<MTPauth_sentCodeType> MTPauth_SentCodeType;
 
+class MTPmessages_botCallbackAnswer : private mtpDataOwner {
+public:
+	MTPmessages_botCallbackAnswer();
+	MTPmessages_botCallbackAnswer(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_messages_botCallbackAnswer) : mtpDataOwner(0) {
+		read(from, end, cons);
+	}
+
+	MTPDmessages_botCallbackAnswer &_messages_botCallbackAnswer() {
+		if (!data) throw mtpErrorUninitialized();
+		split();
+		return *(MTPDmessages_botCallbackAnswer*)data;
+	}
+	const MTPDmessages_botCallbackAnswer &c_messages_botCallbackAnswer() const {
+		if (!data) throw mtpErrorUninitialized();
+		return *(const MTPDmessages_botCallbackAnswer*)data;
+	}
+
+	uint32 innerLength() const;
+	mtpTypeId type() const;
+	void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_messages_botCallbackAnswer);
+	void write(mtpBuffer &to) const;
+
+	typedef void ResponseType;
+
+private:
+	explicit MTPmessages_botCallbackAnswer(MTPDmessages_botCallbackAnswer *_data);
+
+	friend class MTP::internal::TypeCreator;
+};
+typedef MTPBoxed<MTPmessages_botCallbackAnswer> MTPmessages_BotCallbackAnswer;
+
 // Type constructors with data
 
 class MTPDresPQ : public mtpDataImpl<MTPDresPQ> {
@@ -11375,6 +11428,20 @@ public:
 	MTPint vid;
 };
 
+class MTPDupdateBotCallbackQuery : public mtpDataImpl<MTPDupdateBotCallbackQuery> {
+public:
+	MTPDupdateBotCallbackQuery() {
+	}
+	MTPDupdateBotCallbackQuery(const MTPlong &_query_id, MTPint _user_id, const MTPPeer &_peer, MTPint _msg_id, const MTPstring &_text) : vquery_id(_query_id), vuser_id(_user_id), vpeer(_peer), vmsg_id(_msg_id), vtext(_text) {
+	}
+
+	MTPlong vquery_id;
+	MTPint vuser_id;
+	MTPPeer vpeer;
+	MTPint vmsg_id;
+	MTPstring vtext;
+};
+
 class MTPDupdates_state : public mtpDataImpl<MTPDupdates_state> {
 public:
 	MTPDupdates_state() {
@@ -13438,6 +13505,16 @@ public:
 	MTPstring vpattern;
 };
 
+class MTPDmessages_botCallbackAnswer : public mtpDataImpl<MTPDmessages_botCallbackAnswer> {
+public:
+	MTPDmessages_botCallbackAnswer() {
+	}
+	MTPDmessages_botCallbackAnswer(const MTPstring &_message) : vmessage(_message) {
+	}
+
+	MTPstring vmessage;
+};
+
 // RPC methods
 
 class MTPreq_pq { // RPC method 'req_pq'
@@ -18871,6 +18948,93 @@ public:
 	}
 };
 
+class MTPmessages_getBotCallbackAnswer { // RPC method 'messages.getBotCallbackAnswer'
+public:
+	MTPInputPeer vpeer;
+	MTPint vmsg_id;
+	MTPstring vtext;
+
+	MTPmessages_getBotCallbackAnswer() {
+	}
+	MTPmessages_getBotCallbackAnswer(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_messages_getBotCallbackAnswer) {
+		read(from, end, cons);
+	}
+	MTPmessages_getBotCallbackAnswer(const MTPInputPeer &_peer, MTPint _msg_id, const MTPstring &_text) : vpeer(_peer), vmsg_id(_msg_id), vtext(_text) {
+	}
+
+	uint32 innerLength() const {
+		return vpeer.innerLength() + vmsg_id.innerLength() + vtext.innerLength();
+	}
+	mtpTypeId type() const {
+		return mtpc_messages_getBotCallbackAnswer;
+	}
+	void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_messages_getBotCallbackAnswer) {
+		vpeer.read(from, end);
+		vmsg_id.read(from, end);
+		vtext.read(from, end);
+	}
+	void write(mtpBuffer &to) const {
+		vpeer.write(to);
+		vmsg_id.write(to);
+		vtext.write(to);
+	}
+
+	typedef MTPmessages_BotCallbackAnswer ResponseType;
+};
+class MTPmessages_GetBotCallbackAnswer : public MTPBoxed<MTPmessages_getBotCallbackAnswer> {
+public:
+	MTPmessages_GetBotCallbackAnswer() {
+	}
+	MTPmessages_GetBotCallbackAnswer(const MTPmessages_getBotCallbackAnswer &v) : MTPBoxed<MTPmessages_getBotCallbackAnswer>(v) {
+	}
+	MTPmessages_GetBotCallbackAnswer(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = 0) : MTPBoxed<MTPmessages_getBotCallbackAnswer>(from, end, cons) {
+	}
+	MTPmessages_GetBotCallbackAnswer(const MTPInputPeer &_peer, MTPint _msg_id, const MTPstring &_text) : MTPBoxed<MTPmessages_getBotCallbackAnswer>(MTPmessages_getBotCallbackAnswer(_peer, _msg_id, _text)) {
+	}
+};
+
+class MTPmessages_setBotCallbackAnswer { // RPC method 'messages.setBotCallbackAnswer'
+public:
+	MTPlong vquery_id;
+	MTPstring vmessage;
+
+	MTPmessages_setBotCallbackAnswer() {
+	}
+	MTPmessages_setBotCallbackAnswer(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_messages_setBotCallbackAnswer) {
+		read(from, end, cons);
+	}
+	MTPmessages_setBotCallbackAnswer(const MTPlong &_query_id, const MTPstring &_message) : vquery_id(_query_id), vmessage(_message) {
+	}
+
+	uint32 innerLength() const {
+		return vquery_id.innerLength() + vmessage.innerLength();
+	}
+	mtpTypeId type() const {
+		return mtpc_messages_setBotCallbackAnswer;
+	}
+	void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_messages_setBotCallbackAnswer) {
+		vquery_id.read(from, end);
+		vmessage.read(from, end);
+	}
+	void write(mtpBuffer &to) const {
+		vquery_id.write(to);
+		vmessage.write(to);
+	}
+
+	typedef MTPBool ResponseType;
+};
+class MTPmessages_SetBotCallbackAnswer : public MTPBoxed<MTPmessages_setBotCallbackAnswer> {
+public:
+	MTPmessages_SetBotCallbackAnswer() {
+	}
+	MTPmessages_SetBotCallbackAnswer(const MTPmessages_setBotCallbackAnswer &v) : MTPBoxed<MTPmessages_setBotCallbackAnswer>(v) {
+	}
+	MTPmessages_SetBotCallbackAnswer(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = 0) : MTPBoxed<MTPmessages_setBotCallbackAnswer>(from, end, cons) {
+	}
+	MTPmessages_SetBotCallbackAnswer(const MTPlong &_query_id, const MTPstring &_message) : MTPBoxed<MTPmessages_setBotCallbackAnswer>(MTPmessages_setBotCallbackAnswer(_query_id, _message)) {
+	}
+};
+
 class MTPupdates_getState { // RPC method 'updates.getState'
 public:
 	MTPupdates_getState() {
@@ -21694,6 +21858,9 @@ public:
 	inline static MTPupdate new_updateChannelPinnedMessage(MTPint _channel_id, MTPint _id) {
 		return MTPupdate(new MTPDupdateChannelPinnedMessage(_channel_id, _id));
 	}
+	inline static MTPupdate new_updateBotCallbackQuery(const MTPlong &_query_id, MTPint _user_id, const MTPPeer &_peer, MTPint _msg_id, const MTPstring &_text) {
+		return MTPupdate(new MTPDupdateBotCallbackQuery(_query_id, _user_id, _peer, _msg_id, _text));
+	}
 	inline static MTPupdates_state new_updates_state(MTPint _pts, MTPint _qts, MTPint _date, MTPint _seq, MTPint _unread_count) {
 		return MTPupdates_state(new MTPDupdates_state(_pts, _qts, _date, _seq, _unread_count));
 	}
@@ -22261,6 +22428,9 @@ public:
 	inline static MTPauth_sentCodeType new_auth_sentCodeTypeFlashCall(const MTPstring &_pattern) {
 		return MTPauth_sentCodeType(new MTPDauth_sentCodeTypeFlashCall(_pattern));
 	}
+	inline static MTPmessages_botCallbackAnswer new_messages_botCallbackAnswer(const MTPstring &_message) {
+		return MTPmessages_botCallbackAnswer(new MTPDmessages_botCallbackAnswer(_message));
+	}
 	};
 
 } // namespace internal
@@ -27159,6 +27329,10 @@ inline uint32 MTPupdate::innerLength() const {
 			const MTPDupdateChannelPinnedMessage &v(c_updateChannelPinnedMessage());
 			return v.vchannel_id.innerLength() + v.vid.innerLength();
 		}
+		case mtpc_updateBotCallbackQuery: {
+			const MTPDupdateBotCallbackQuery &v(c_updateBotCallbackQuery());
+			return v.vquery_id.innerLength() + v.vuser_id.innerLength() + v.vpeer.innerLength() + v.vmsg_id.innerLength() + v.vtext.innerLength();
+		}
 	}
 	return 0;
 }
@@ -27458,6 +27632,15 @@ inline void MTPupdate::read(const mtpPrime *&from, const mtpPrime *end, mtpTypeI
 			v.vchannel_id.read(from, end);
 			v.vid.read(from, end);
 		} break;
+		case mtpc_updateBotCallbackQuery: _type = cons; {
+			if (!data) setData(new MTPDupdateBotCallbackQuery());
+			MTPDupdateBotCallbackQuery &v(_updateBotCallbackQuery());
+			v.vquery_id.read(from, end);
+			v.vuser_id.read(from, end);
+			v.vpeer.read(from, end);
+			v.vmsg_id.read(from, end);
+			v.vtext.read(from, end);
+		} break;
 		default: throw mtpErrorUnexpected(cons, "MTPupdate");
 	}
 }
@@ -27707,6 +27890,14 @@ inline void MTPupdate::write(mtpBuffer &to) const {
 			v.vchannel_id.write(to);
 			v.vid.write(to);
 		} break;
+		case mtpc_updateBotCallbackQuery: {
+			const MTPDupdateBotCallbackQuery &v(c_updateBotCallbackQuery());
+			v.vquery_id.write(to);
+			v.vuser_id.write(to);
+			v.vpeer.write(to);
+			v.vmsg_id.write(to);
+			v.vtext.write(to);
+		} break;
 	}
 }
 inline MTPupdate::MTPupdate(mtpTypeId type) : mtpDataOwner(0), _type(type) {
@@ -27756,6 +27947,7 @@ inline MTPupdate::MTPupdate(mtpTypeId type) : mtpDataOwner(0), _type(type) {
 		case mtpc_updateBotInlineSend: setData(new MTPDupdateBotInlineSend()); break;
 		case mtpc_updateEditChannelMessage: setData(new MTPDupdateEditChannelMessage()); break;
 		case mtpc_updateChannelPinnedMessage: setData(new MTPDupdateChannelPinnedMessage()); break;
+		case mtpc_updateBotCallbackQuery: setData(new MTPDupdateBotCallbackQuery()); break;
 		default: throw mtpErrorBadTypeId(type, "MTPupdate");
 	}
 }
@@ -27845,6 +28037,8 @@ inline MTPupdate::MTPupdate(MTPDupdateEditChannelMessage *_data) : mtpDataOwner(
 }
 inline MTPupdate::MTPupdate(MTPDupdateChannelPinnedMessage *_data) : mtpDataOwner(_data), _type(mtpc_updateChannelPinnedMessage) {
 }
+inline MTPupdate::MTPupdate(MTPDupdateBotCallbackQuery *_data) : mtpDataOwner(_data), _type(mtpc_updateBotCallbackQuery) {
+}
 inline MTPupdate MTP_updateNewMessage(const MTPMessage &_message, MTPint _pts, MTPint _pts_count) {
 	return MTP::internal::TypeCreator::new_updateNewMessage(_message, _pts, _pts_count);
 }
@@ -27981,6 +28175,9 @@ inline MTPupdate MTP_updateEditChannelMessage(const MTPMessage &_message, MTPint
 inline MTPupdate MTP_updateChannelPinnedMessage(MTPint _channel_id, MTPint _id) {
 	return MTP::internal::TypeCreator::new_updateChannelPinnedMessage(_channel_id, _id);
 }
+inline MTPupdate MTP_updateBotCallbackQuery(const MTPlong &_query_id, MTPint _user_id, const MTPPeer &_peer, MTPint _msg_id, const MTPstring &_text) {
+	return MTP::internal::TypeCreator::new_updateBotCallbackQuery(_query_id, _user_id, _peer, _msg_id, _text);
+}
 
 inline MTPupdates_state::MTPupdates_state() : mtpDataOwner(new MTPDupdates_state()) {
 }
@@ -32871,6 +33068,33 @@ inline MTPauth_sentCodeType MTP_auth_sentCodeTypeCall(MTPint _length) {
 inline MTPauth_sentCodeType MTP_auth_sentCodeTypeFlashCall(const MTPstring &_pattern) {
 	return MTP::internal::TypeCreator::new_auth_sentCodeTypeFlashCall(_pattern);
 }
+
+inline MTPmessages_botCallbackAnswer::MTPmessages_botCallbackAnswer() : mtpDataOwner(new MTPDmessages_botCallbackAnswer()) {
+}
+
+inline uint32 MTPmessages_botCallbackAnswer::innerLength() const {
+	const MTPDmessages_botCallbackAnswer &v(c_messages_botCallbackAnswer());
+	return v.vmessage.innerLength();
+}
+inline mtpTypeId MTPmessages_botCallbackAnswer::type() const {
+	return mtpc_messages_botCallbackAnswer;
+}
+inline void MTPmessages_botCallbackAnswer::read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons) {
+	if (cons != mtpc_messages_botCallbackAnswer) throw mtpErrorUnexpected(cons, "MTPmessages_botCallbackAnswer");
+
+	if (!data) setData(new MTPDmessages_botCallbackAnswer());
+	MTPDmessages_botCallbackAnswer &v(_messages_botCallbackAnswer());
+	v.vmessage.read(from, end);
+}
+inline void MTPmessages_botCallbackAnswer::write(mtpBuffer &to) const {
+	const MTPDmessages_botCallbackAnswer &v(c_messages_botCallbackAnswer());
+	v.vmessage.write(to);
+}
+inline MTPmessages_botCallbackAnswer::MTPmessages_botCallbackAnswer(MTPDmessages_botCallbackAnswer *_data) : mtpDataOwner(_data) {
+}
+inline MTPmessages_botCallbackAnswer MTP_messages_botCallbackAnswer(const MTPstring &_message) {
+	return MTP::internal::TypeCreator::new_messages_botCallbackAnswer(_message);
+}
 inline MTPDmessage::Flags mtpCastFlags(MTPDmessageService::Flags flags) { return MTPDmessage::Flags(QFlag(flags)); }
 inline MTPDmessage::Flags mtpCastFlags(MTPflags<MTPDmessageService::Flags> flags) { return mtpCastFlags(flags.v); }
 inline MTPDmessage::Flags mtpCastFlags(MTPDupdateShortMessage::Flags flags) { return MTPDmessage::Flags(QFlag(flags)); }
diff --git a/Telegram/SourceFiles/overviewwidget.cpp b/Telegram/SourceFiles/overviewwidget.cpp
index df371894b..57aa800e6 100644
--- a/Telegram/SourceFiles/overviewwidget.cpp
+++ b/Telegram/SourceFiles/overviewwidget.cpp
@@ -474,12 +474,7 @@ void OverviewInner::dragActionStart(const QPoint &screenPos, Qt::MouseButton but
 	dragActionUpdate(screenPos);
 	if (button != Qt::LeftButton) return;
 
-	if (textlnkDown() != textlnkOver()) {
-		repaintItem(App::pressedLinkItem());
-		textlnkDown(textlnkOver());
-		App::pressedLinkItem(App::hoveredLinkItem());
-		repaintItem(App::pressedLinkItem());
-	}
+	ClickHandler::pressed();
 
 	_dragAction = NoDrag;
 	_dragItem = _mousedItem;
@@ -487,11 +482,11 @@ void OverviewInner::dragActionStart(const QPoint &screenPos, Qt::MouseButton but
 	_dragStartPos = mapMouseToItem(mapFromGlobal(screenPos), _dragItem, _dragItemIndex);
 	_dragWasInactive = App::wnd()->inactivePress();
 	if (_dragWasInactive) App::wnd()->inactivePress(false);
-	if (textlnkDown() && _selected.isEmpty()) {
+	if (ClickHandler::getPressed() && _selected.isEmpty()) {
 		_dragAction = PrepareDrag;
 	} else if (!_selected.isEmpty()) {
 		if (_selected.cbegin().value() == FullSelection) {
-			if (_selected.constFind(_dragItem) != _selected.cend() && textlnkDown()) {
+			if (_selected.constFind(_dragItem) != _selected.cend() && ClickHandler::getPressed()) {
 				_dragAction = PrepareDrag; // start items drag
 			} else {
 				_dragAction = PrepareSelect; // start items select
@@ -499,27 +494,8 @@ void OverviewInner::dragActionStart(const QPoint &screenPos, Qt::MouseButton but
 		}
 	}
 	if (_dragAction == NoDrag && _dragItem) {
-		bool afterDragSymbol = false , uponSymbol = false;
-		uint16 symbol = 0;
 		if (!_dragWasInactive) {
-			if (textlnkDown()) {
-				_dragSymbol = symbol;
-				uint32 selStatus = (_dragSymbol << 16) | _dragSymbol;
-				if (selStatus != FullSelection && (_selected.isEmpty() || _selected.cbegin().value() != FullSelection)) {
-					if (!_selected.isEmpty()) {
-						repaintItem(_selected.cbegin().key(), -1);
-						_selected.clear();
-					}
-					_selected.insert(_dragItem, selStatus);
-					_dragAction = Selecting;
-					repaintItem(_dragItem, _dragItemIndex);
-					_overview->updateTopBarSelection();
-				} else {
-					_dragAction = PrepareSelect;
-				}
-			} else {
-				_dragAction = PrepareSelect; // start items select
-			}
+			_dragAction = PrepareSelect;
 		}
 	}
 
@@ -541,31 +517,22 @@ void OverviewInner::dragActionCancel() {
 }
 
 void OverviewInner::dragActionFinish(const QPoint &screenPos, Qt::MouseButton button) {
-	TextLinkPtr needClick;
-
 	dragActionUpdate(screenPos);
 
-	if (textlnkOver()) {
-		if (textlnkDown() == textlnkOver() && _dragAction != Dragging && !_selMode) {
-			needClick = textlnkDown();
-		}
+	ClickHandlerPtr activated = ClickHandler::unpressed();
+	if (_dragAction == Dragging || _selMode) {
+		activated.clear();
 	}
-	if (textlnkDown()) {
-		repaintItem(App::pressedLinkItem());
-		textlnkDown(TextLinkPtr());
-		App::pressedLinkItem(0);
-		if (!textlnkOver() && _cursor != style::cur_default) {
-			_cursor = style::cur_default;
-			setCursor(_cursor);
-		}
+	if (!ClickHandler::getActive() && _cursor != style::cur_default) {
+		_cursor = style::cur_default;
+		setCursor(_cursor);
 	}
-	if (needClick) {
-		DEBUG_LOG(("Will click link: %1 (%2) %3").arg(needClick->text()).arg(needClick->readable()).arg(needClick->encoded()));
+	if (activated) {
 		dragActionCancel();
-		App::activateTextLink(needClick, button);
+		App::activateClickHandler(activated, button);
 		return;
 	}
-	if (_dragAction == PrepareSelect && !needClick && !_dragWasInactive && !_selected.isEmpty() && _selected.cbegin().value() == FullSelection) {
+	if (_dragAction == PrepareSelect && !_dragWasInactive && !_selected.isEmpty() && _selected.cbegin().value() == FullSelection) {
 		SelectedItems::iterator i = _selected.find(_dragItem);
 		if (i == _selected.cend() && itemMsgId(_dragItem) > 0) {
 			if (_selected.size() < MaxSelectedItems) {
@@ -578,7 +545,7 @@ void OverviewInner::dragActionFinish(const QPoint &screenPos, Qt::MouseButton bu
 			_selected.erase(i);
 		}
 		repaintItem(_dragItem, _dragItemIndex);
-	} else if (_dragAction == PrepareDrag && !needClick && !_dragWasInactive && button != Qt::RightButton) {
+	} else if (_dragAction == PrepareDrag && !_dragWasInactive && button != Qt::RightButton) {
 		SelectedItems::iterator i = _selected.find(_dragItem);
 		if (i != _selected.cend() && i.value() == FullSelection) {
 			_selected.erase(i);
@@ -619,16 +586,17 @@ void OverviewInner::onDragExec() {
 			uponSelected = false;
 		}
 	}
+	ClickHandlerPtr pressedHandler = ClickHandler::getPressed();
 	QString sel;
 	QList<QUrl> urls;
 	bool forwardSelected = false;
 	if (uponSelected) {
 		forwardSelected = !_selected.isEmpty() && _selected.cbegin().value() == FullSelection && !Adaptive::OneColumn();
-	} else if (textlnkDown()) {
-		sel = textlnkDown()->encoded();
-		if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
-//			urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
-		}
+	} else if (pressedHandler) {
+		sel = pressedHandler->dragText();
+		//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
+		//	urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
+		//}
 	}
 	if (!sel.isEmpty() || forwardSelected) {
 		updateDragSelection(0, -1, 0, -1, false);
@@ -647,19 +615,22 @@ void OverviewInner::onDragExec() {
 		if (App::main()) App::main()->updateAfterDrag();
 		return;
 	} else {
-		HistoryItem *pressedLnkItem = App::pressedLinkItem(), *pressedItem = App::pressedItem();
-		QLatin1String lnkType = (textlnkDown() && pressedLnkItem) ? textlnkDown()->type() : qstr("");
-		bool lnkPhoto = (lnkType == qstr("PhotoLink")),
-			lnkVideo = (lnkType == qstr("VideoOpenLink")),
-			lnkAudio = (lnkType == qstr("AudioOpenLink")),
-			lnkDocument = (lnkType == qstr("DocumentOpenLink") || lnkType == qstr("GifOpenLink"));
-		if (lnkPhoto || lnkVideo || lnkAudio || lnkDocument) {
+		QString forwardMimeType;
+		HistoryMedia *pressedMedia = nullptr;
+		if (HistoryItem *pressedLnkItem = App::pressedLinkItem()) {
+			if ((pressedMedia = pressedLnkItem->getMedia())) {
+				if (forwardMimeType.isEmpty() && pressedMedia->dragItemByHandler(pressedHandler)) {
+					forwardMimeType = qsl("application/x-td-forward-pressed-link");
+				}
+			}
+		}
+		if (!forwardMimeType.isEmpty()) {
 			QDrag *drag = new QDrag(App::wnd());
 			QMimeData *mimeData = new QMimeData;
 
 			mimeData->setData(qsl("application/x-td-forward-pressed-link"), "1");
-			if (lnkDocument) {
-				QString filepath = static_cast<DocumentOpenLink*>(textlnkDown().data())->document()->filepath(DocumentData::FilePathResolveChecked);
+			if (DocumentData *document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) {
+				QString filepath = document->filepath(DocumentData::FilePathResolveChecked);
 				if (!filepath.isEmpty()) {
 					QList<QUrl> urls;
 					urls.push_back(QUrl::fromLocalFile(filepath));
@@ -901,7 +872,7 @@ void OverviewInner::paintEvent(QPaintEvent *e) {
 }
 
 void OverviewInner::mouseMoveEvent(QMouseEvent *e) {
-	if (!(e->buttons() & (Qt::LeftButton | Qt::MiddleButton)) && (textlnkDown() || _dragAction != NoDrag)) {
+	if (!(e->buttons() & (Qt::LeftButton | Qt::MiddleButton)) && _dragAction != NoDrag) {
 		mouseReleaseEvent(e);
 	}
 	dragActionUpdate(e->globalPos());
@@ -913,7 +884,8 @@ void OverviewInner::onUpdateSelected() {
 	QPoint mousePos(mapFromGlobal(_dragPos));
 	QPoint m(_overview->clampMousePosition(mousePos));
 
-	TextLinkPtr lnk;
+	ClickHandlerPtr lnk;
+	ClickHandlerHost *lnkhost = nullptr;
 	HistoryItem *item = 0;
 	int32 index = -1;
 	int32 newsel = 0;
@@ -941,6 +913,7 @@ void OverviewInner::onUpdateSelected() {
 				index = i;
 				if (upon) {
 					media->getState(lnk, cursorState, m.x() - col * w - st::overviewPhotoSkip, m.y() - _marginTop - row * vsize - st::overviewPhotoSkip);
+					lnkhost = media;
 				}
 			}
 		}
@@ -976,6 +949,7 @@ void OverviewInner::onUpdateSelected() {
 					item = media->getItem();
 					index = i;
 					media->getState(lnk, cursorState, m.x() - _rowsLeft, m.y() - _marginTop - top);
+					lnkhost = media;
 				}
 				break;
 			}
@@ -989,37 +963,15 @@ void OverviewInner::onUpdateSelected() {
 	m = mapMouseToItem(m, _mousedItem, _mousedItemIndex);
 
 	Qt::CursorShape cur = style::cur_default;
-	bool lnkChanged = false;
-	if (lnk != textlnkOver()) {
-		lnkChanged = true;
-		if (textlnkOver()) {
-			if (HistoryItem *item = App::hoveredLinkItem()) {
-				MsgId itemId = complexMsgId(item);
-				int32 itemIndex = oldMousedItemIndex;
-				fixItemIndex(itemIndex, itemId);
-				if (itemIndex >= 0) {
-					_items.at(itemIndex)->linkOut(textlnkOver());
-					repaintItem(itemId, itemIndex);
-				}
-			}
-		}
-		textlnkOver(lnk);
+	bool lnkChanged = ClickHandler::setActive(lnk, lnkhost);
+	if (lnkChanged) {
 		PopupTooltip::Hide();
-		App::hoveredLinkItem(lnk ? item : 0);
-		if (textlnkOver()) {
-			if (item && index >= 0) {
-				_items.at(index)->linkOver(textlnkOver());
-				repaintItem(complexMsgId(item), index);
-			}
-		}
-	} else {
-		App::mousedItem(item);
 	}
+	App::mousedItem(item);
 	if (_mousedItem != oldMousedItem) {
-		lnkChanged = true;
+		PopupTooltip::Hide();
 		if (oldMousedItem) repaintItem(oldMousedItem, oldMousedItemIndex);
 		if (item) repaintItem(item);
-		PopupTooltip::Hide();
 	}
 	if (_cursorState == HistoryInDateCursorState && cursorState != HistoryInDateCursorState) {
 		PopupTooltip::Hide();
@@ -1050,7 +1002,6 @@ void OverviewInner::onUpdateSelected() {
 				_dragAction = Selecting;
 			}
 		}
-		cur = textlnkDown() ? style::cur_pointer : style::cur_default;
 		if (_dragAction == Selecting) {
 			bool canSelectMany = (_peer != 0);
 			if (_mousedItem == _dragItem && lnk && !_selected.isEmpty() && _selected.cbegin().value() != FullSelection) {
@@ -1123,7 +1074,7 @@ void OverviewInner::onUpdateSelected() {
 		} else if (_dragAction == Dragging) {
 		}
 
-		if (textlnkDown()) {
+		if (ClickHandler::getPressed()) {
 			cur = style::cur_pointer;
 		} else if (_dragAction == Selecting && !_selected.isEmpty() && _selected.cbegin().value() != FullSelection) {
 			if (!_dragSelFrom || !_dragSelTo) {
@@ -1148,13 +1099,12 @@ QPoint OverviewInner::tooltipPos() const {
 }
 
 QString OverviewInner::tooltipText() const {
-	TextLinkPtr lnk = textlnkOver();
-	if (lnk && !lnk->fullDisplayed()) {
-		return lnk->readable();
-	} else if (_cursorState == HistoryInDateCursorState && _dragAction == NoDrag && _mousedItem) {
+	if (_cursorState == HistoryInDateCursorState && _dragAction == NoDrag && _mousedItem) {
 		if (HistoryItem *item = App::histItemById(itemChannel(_mousedItem), itemMsgId(_mousedItem))) {
 			return item->date.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat));
 		}
+	} else if (ClickHandlerPtr lnk = ClickHandler::getActive()) {
+		return lnk->tooltip();
 	}
 	return QString();
 }
@@ -1208,14 +1158,10 @@ void OverviewInner::leaveEvent(QEvent *e) {
 		repaintItem(_selectedMsgId, -1);
 		_selectedMsgId = 0;
 	}
-	if (textlnkOver()) {
-		repaintItem(App::hoveredLinkItem());
-		textlnkOver(TextLinkPtr());
-		App::hoveredLinkItem(0);
-		if (!textlnkDown() && _cursor != style::cur_default) {
-			_cursor = style::cur_default;
-			setCursor(_cursor);
-		}
+	ClickHandler::clearActive();
+	if (!ClickHandler::getPressed() && _cursor != style::cur_default) {
+		_cursor = style::cur_default;
+		setCursor(_cursor);
 	}
 	return QWidget::leaveEvent(e);
 }
@@ -1264,9 +1210,9 @@ void OverviewInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 		isUponSelected = hasSelected;
 	}
 
-	_contextMenuLnk = textlnkOver();
-	PhotoLink *lnkPhoto = dynamic_cast<PhotoLink*>(_contextMenuLnk.data());
-	DocumentLink *lnkDocument = dynamic_cast<DocumentLink*>(_contextMenuLnk.data());
+	_contextMenuLnk = ClickHandler::getActive();
+	PhotoClickHandler *lnkPhoto = dynamic_cast<PhotoClickHandler*>(_contextMenuLnk.data());
+	DocumentClickHandler *lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLnk.data());
 	bool lnkIsVideo = lnkDocument ? lnkDocument->document()->isVideo() : false;
 	bool lnkIsAudio = lnkDocument ? (lnkDocument->document()->voice() != nullptr) : false;
 	bool lnkIsSong = lnkDocument ? (lnkDocument->document()->song() != nullptr) : false;
@@ -1310,16 +1256,9 @@ void OverviewInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 		if (_selectedMsgId) repaintItem(_selectedMsgId, -1);
 	} else if (!ignoreMousedItem && App::mousedItem() && App::mousedItem()->channelId() == itemChannel(_mousedItem) && App::mousedItem()->id == itemMsgId(_mousedItem)) {
 		_menu = new PopupMenu();
-		QLatin1String linktype = _contextMenuLnk ? _contextMenuLnk->type() : qstr("");
-		if (linktype == qstr("TextLink") || linktype == qstr("LocationLink")) {
-			_menu->addAction(lang(lng_context_copy_link), this, SLOT(copyContextUrl()))->setEnabled(true);
-		} else if (linktype == qstr("EmailLink")) {
-			_menu->addAction(lang(lng_context_copy_email), this, SLOT(copyContextUrl()))->setEnabled(true);
-		} else if (linktype == qstr("MentionLink")) {
-			_menu->addAction(lang(lng_context_copy_mention), this, SLOT(copyContextUrl()))->setEnabled(true);
-		} else if (linktype == qstr("HashtagLink")) {
-			_menu->addAction(lang(lng_context_copy_hashtag), this, SLOT(copyContextUrl()))->setEnabled(true);
-		} else {
+		QString copyToClipboardContextItem = _contextMenuLnk ? _contextMenuLnk->copyToClipboardContextItem() : QString();
+		if (!copyToClipboardContextItem.isEmpty()) {
+			_menu->addAction(copyToClipboardContextItem, this, SLOT(copyContextUrl()))->setEnabled(true);
 		}
 		_menu->addAction(lang(lng_context_to_msg), this, SLOT(goToMessage()))->setEnabled(true);
 		if (isUponSelected > 1) {
@@ -1447,9 +1386,8 @@ void OverviewInner::setSelectMode(bool enabled) {
 }
 
 void OverviewInner::copyContextUrl() {
-	QString enc = _contextMenuLnk ? _contextMenuLnk->encoded() : QString();
-	if (!enc.isEmpty()) {
-		QApplication::clipboard()->setText(enc);
+	if (_contextMenuLnk) {
+		_contextMenuLnk->copyToClipboard();
 	}
 }
 
@@ -1494,14 +1432,14 @@ void OverviewInner::selectMessage() {
 }
 
 void OverviewInner::cancelContextDownload() {
-	DocumentLink *lnkDocument = dynamic_cast<DocumentLink*>(_contextMenuLnk.data());
+	DocumentClickHandler *lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLnk.data());
 	if (lnkDocument) {
 		lnkDocument->document()->cancel();
 	}
 }
 
 void OverviewInner::showContextInFolder() {
-	if (DocumentLink *lnkDocument = dynamic_cast<DocumentLink*>(_contextMenuLnk.data())) {
+	if (DocumentClickHandler *lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLnk.data())) {
 		QString filepath = lnkDocument->document()->filepath(DocumentData::FilePathResolveChecked);
 		if (!filepath.isEmpty()) {
 			psShowInFolder(filepath);
@@ -1510,8 +1448,8 @@ void OverviewInner::showContextInFolder() {
 }
 
 void OverviewInner::saveContextFile() {
-	DocumentLink *lnkDocument = dynamic_cast<DocumentLink*>(_contextMenuLnk.data());
-	if (lnkDocument) DocumentSaveLink::doSave(lnkDocument->document(), true);
+	DocumentClickHandler *lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLnk.data());
+	if (lnkDocument) DocumentSaveClickHandler::doSave(lnkDocument->document(), true);
 }
 
 bool OverviewInner::onSearchMessages(bool searchCache) {
diff --git a/Telegram/SourceFiles/overviewwidget.h b/Telegram/SourceFiles/overviewwidget.h
index 2b13596be..6cd7affbb 100644
--- a/Telegram/SourceFiles/overviewwidget.h
+++ b/Telegram/SourceFiles/overviewwidget.h
@@ -76,9 +76,9 @@ public:
 	void clearSelectedItems(bool onlyTextSelection = false);
 	void fillSelectedItems(SelectedItemSet &sel, bool forDelete = true);
 
-	// AbstractTooltipShower
-	virtual QString tooltipText() const;
-	virtual QPoint tooltipPos() const;
+	// AbstractTooltipShower interface
+	QString tooltipText() const override;
+	QPoint tooltipPos() const override;
 
 	~OverviewInner();
 
@@ -217,7 +217,7 @@ private:
 	uint16 _dragSymbol;
 	bool _dragWasInactive;
 
-	TextLinkPtr _contextMenuLnk;
+	ClickHandlerPtr _contextMenuLnk;
 
 	MsgId _dragSelFrom, _dragSelTo;
 	int32 _dragSelFromIndex, _dragSelToIndex;
diff --git a/Telegram/SourceFiles/playerwidget.cpp b/Telegram/SourceFiles/playerwidget.cpp
index eefeb4da0..85bc5c732 100644
--- a/Telegram/SourceFiles/playerwidget.cpp
+++ b/Telegram/SourceFiles/playerwidget.cpp
@@ -325,7 +325,7 @@ void PlayerWidget::preloadNext() {
 		if (HistoryDocument *document = static_cast<HistoryDocument*>(next->getMedia())) {
 			DocumentData *d = document->getDocument();
 			if (!d->loaded(DocumentData::FilePathResolveSaveFromDataSilent)) {
-				DocumentOpenLink::doOpen(d, ActionOnLoadNone);
+				DocumentOpenClickHandler::doOpen(d, ActionOnLoadNone);
 			}
 		}
 	}
diff --git a/Telegram/SourceFiles/profilewidget.cpp b/Telegram/SourceFiles/profilewidget.cpp
index a3036db88..dcfc9ad3e 100644
--- a/Telegram/SourceFiles/profilewidget.cpp
+++ b/Telegram/SourceFiles/profilewidget.cpp
@@ -117,7 +117,7 @@ ProfileInner::ProfileInner(ProfileWidget *profile, ScrollArea *scroll, PeerData
 		_phoneText = App::formatPhone(_peerUser->phone.isEmpty() ? App::phoneFromSharedContact(peerToUser(_peerUser->id)) : _peerUser->phone);
 		PhotoData *userPhoto = (_peerUser->photoId && _peerUser->photoId != UnknownPeerPhotoId) ? App::photo(_peerUser->photoId) : 0;
 		if (userPhoto && userPhoto->date) {
-			_photoLink = TextLinkPtr(new PhotoLink(userPhoto, _peer));
+			_photoLink.reset(new PhotoOpenClickHandler(userPhoto, _peer));
 		}
 		if ((_peerUser->botInfo && !_peerUser->botInfo->inited) || (_peerUser->photoId == UnknownPeerPhotoId) || (_peerUser->photoId && !userPhoto->date) || (_peerUser->blocked == UserBlockUnknown)) {
 			if (App::api()) App::api()->requestFullPeer(_peer);
@@ -125,7 +125,7 @@ ProfileInner::ProfileInner(ProfileWidget *profile, ScrollArea *scroll, PeerData
 	} else if (_peerChat) {
 		PhotoData *chatPhoto = (_peerChat->photoId && _peerChat->photoId != UnknownPeerPhotoId) ? App::photo(_peerChat->photoId) : 0;
 		if (chatPhoto && chatPhoto->date) {
-			_photoLink = TextLinkPtr(new PhotoLink(chatPhoto, _peer));
+			_photoLink.reset(new PhotoOpenClickHandler(chatPhoto, _peer));
 		}
 		if (_peerChat->photoId == UnknownPeerPhotoId) {
 			if (App::api()) App::api()->requestFullPeer(_peer);
@@ -133,7 +133,7 @@ ProfileInner::ProfileInner(ProfileWidget *profile, ScrollArea *scroll, PeerData
 	} else if (_peerChannel) {
 		PhotoData *chatPhoto = (_peerChannel->photoId && _peerChannel->photoId != UnknownPeerPhotoId) ? App::photo(_peerChannel->photoId) : 0;
 		if (chatPhoto && chatPhoto->date) {
-			_photoLink = TextLinkPtr(new PhotoLink(chatPhoto, _peer));
+			_photoLink.reset(new PhotoOpenClickHandler(chatPhoto, _peer));
 		}
 		bool needAdmins = (_peerChannel->isMegagroup() && _peerChannel->amEditor()), adminsOutdated = (_peerChannel->isMegagroup() && (_peerChannel->mgInfo->lastParticipantsStatus & MegagroupInfo::LastParticipantsAdminsOutdated));
 		if (_peerChannel->isMegagroup() && (_peerChannel->mgInfo->lastParticipants.isEmpty() || (needAdmins && adminsOutdated) || _peerChannel->lastParticipantsCountOutdated())) {
@@ -532,9 +532,9 @@ void ProfileInner::onFullPeerUpdated(PeerData *peer) {
 	if (_peerUser) {
 		PhotoData *userPhoto = (_peerUser->photoId && _peerUser->photoId != UnknownPeerPhotoId) ? App::photo(_peerUser->photoId) : 0;
 		if (userPhoto && userPhoto->date) {
-			_photoLink = TextLinkPtr(new PhotoLink(userPhoto, _peer));
+			_photoLink.reset(new PhotoOpenClickHandler(userPhoto, _peer));
 		} else {
-			_photoLink = TextLinkPtr();
+			_photoLink.clear();
 		}
 		if (_peerUser) {
 			if (_peerUser->about.isEmpty()) {
@@ -573,7 +573,7 @@ void ProfileInner::onBotSettings() {
 		QString cmd = _peerUser->botInfo->commands.at(i).command;
 		if (!cmd.compare(qsl("settings"), Qt::CaseInsensitive)) {
 			Ui::showPeerHistory(_peer, ShowAtTheEndMsgId);
-			App::main()->sendBotCommand('/' + cmd, 0);
+			App::sendBotCommand(_peerUser, '/' + cmd);
 			return;
 		}
 	}
@@ -587,7 +587,7 @@ void ProfileInner::onBotHelp() {
 		QString cmd = _peerUser->botInfo->commands.at(i).command;
 		if (!cmd.compare(qsl("help"), Qt::CaseInsensitive)) {
 			Ui::showPeerHistory(_peer, ShowAtTheEndMsgId);
-			App::main()->sendBotCommand('/' + cmd, 0);
+			App::sendBotCommand(_peerUser, '/' + cmd);
 			return;
 		}
 	}
@@ -627,7 +627,11 @@ void ProfileInner::peerUpdated(PeerData *data) {
 			_onlineText = (_peerChannel->count > 0) ? lng_chat_status_members(lt_count, _peerChannel->count) : lang(_peerChannel->isMegagroup() ? lng_group_status : lng_channel_status);
 			updatePinnedMessageVisibility();
 		}
-		_photoLink = (photo && photo->date) ? TextLinkPtr(new PhotoLink(photo, _peer)) : TextLinkPtr();
+		if (photo && photo->date) {
+			_photoLink.reset(new PhotoOpenClickHandler(photo, _peer));
+		} else {
+			_photoLink.clear();
+		}
 		if (_peer->name != _nameCache) {
 			_nameCache = _peer->name;
 			_nameText.setText(st::profileNameFont, _nameCache, _textNameOptions);
@@ -1093,26 +1097,33 @@ void ProfileInner::mouseMoveEvent(QMouseEvent *e) {
 		}
 	}
 	if (!_photoLink && (_peerUser || (_peerChat && !_peerChat->canEdit()) || (_peerChannel && !_amCreator))) {
-		setCursor((_kickOver || _kickDown || textlnkOver()) ? style::cur_pointer : style::cur_default);
+		setCursor((_kickOver || _kickDown || ClickHandler::getActive()) ? style::cur_pointer : style::cur_default);
 	} else {
-		setCursor((_kickOver || _kickDown || _photoOver || textlnkOver()) ? style::cur_pointer : style::cur_default);
+		setCursor((_kickOver || _kickDown || _photoOver || ClickHandler::getActive()) ? style::cur_pointer : style::cur_default);
 	}
 }
 
+void ProfileInner::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
+	update(QRect(_left, _aboutTop, _width, _aboutHeight));
+}
+
+void ProfileInner::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
+	update(QRect(_left, _aboutTop, _width, _aboutHeight));
+}
+
 void ProfileInner::updateSelected() {
 	if (!isVisible()) return;
 
 	QPoint lp = mapFromGlobal(_lastPos);
 
-	TextLinkPtr lnk;
+	ClickHandlerPtr lnk;
+	ClickHandlerHost *lnkhost = nullptr;
 	bool inText = false;
 	if (!_about.isEmpty() && lp.y() >= _aboutTop && lp.y() < _aboutTop + _aboutHeight && lp.x() >= _left && lp.x() < _left + _width) {
 		_about.getState(lnk, inText, lp.x() - _left, lp.y() - _aboutTop, _width);
+		lnkhost = this;
 	}
-	if (textlnkOver() != lnk) {
-		textlnkOver(lnk);
-		update(QRect(_left, _aboutTop, _width, _aboutHeight));
-	}
+	ClickHandler::setActive(lnk, lnkhost);
 
 	int32 participantsTop = 0;
 	if (canDeleteChannel()) {
@@ -1150,6 +1161,9 @@ void ProfileInner::updateSelected() {
 void ProfileInner::mousePressEvent(QMouseEvent *e) {
 	_lastPos = e->globalPos();
 	updateSelected();
+
+	ClickHandler::pressed();
+
 	if (e->button() == Qt::LeftButton) {
 		if (_kickOver) {
 			_kickDown = _kickOver;
@@ -1163,7 +1177,6 @@ void ProfileInner::mousePressEvent(QMouseEvent *e) {
 				onUpdatePhoto();
 			}
 		}
-		textlnkDown(textlnkOver());
 	}
 }
 
@@ -1179,25 +1192,14 @@ void ProfileInner::mouseReleaseEvent(QMouseEvent *e) {
 
 	_kickDown = 0;
 	if (!_photoLink && (_peerUser || (_peerChat && !_peerChat->canEdit()) || (_peerChannel && !_amCreator))) {
-		setCursor((_kickOver || _kickDown || textlnkOver()) ? style::cur_pointer : style::cur_default);
+		setCursor((_kickOver || _kickDown || ClickHandler::getActive()) ? style::cur_pointer : style::cur_default);
 	} else {
-		setCursor((_kickOver || _kickDown || _photoOver || textlnkOver()) ? style::cur_pointer : style::cur_default);
+		setCursor((_kickOver || _kickDown || _photoOver || ClickHandler::getActive()) ? style::cur_pointer : style::cur_default);
 	}
 	update();
 
-	if (textlnkDown()) {
-		TextLinkPtr lnk = textlnkDown();
-		textlnkDown(TextLinkPtr());
-		if (lnk == textlnkOver()) {
-			if (reHashtag().match(lnk->encoded()).hasMatch() && _peerChannel) {
-				App::searchByHashtag(lnk->encoded(), _peerChannel);
-			} else {
-				if (reBotCommand().match(lnk->encoded()).hasMatch()) {
-					Ui::showPeerHistory(_peer, ShowAtTheEndMsgId);
-				}
-				App::activateTextLink(lnk, e->button());
-			}
-		}
+	if (ClickHandlerPtr activated = ClickHandler::unpressed()) {
+		App::activateClickHandler(activated, e->button());
 	}
 }
 
@@ -2056,6 +2058,10 @@ void ProfileWidget::updateAdaptiveLayout() {
 	_sideShadow.setVisible(!Adaptive::OneColumn());
 }
 
+PeerData *ProfileWidget::ui_getPeerForMouseAction() {
+	return _inner.peer();
+}
+
 void ProfileWidget::clear() {
 	if (_inner.peer() && _inner.peer()->isUser() && _inner.peer()->asUser()->botInfo) {
 		_inner.peer()->asUser()->botInfo->startGroupToken = QString();
diff --git a/Telegram/SourceFiles/profilewidget.h b/Telegram/SourceFiles/profilewidget.h
index fe27c90e5..8f3676cdb 100644
--- a/Telegram/SourceFiles/profilewidget.h
+++ b/Telegram/SourceFiles/profilewidget.h
@@ -21,7 +21,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 #pragma once
 
 class ProfileWidget;
-class ProfileInner : public TWidget, public RPCSender {
+class ProfileInner : public TWidget, public RPCSender, public ClickHandlerHost {
 	Q_OBJECT
 
 public:
@@ -66,6 +66,10 @@ public:
 
 	~ProfileInner();
 
+	// ClickHandlerHost interface
+	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
+	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
+
 public slots:
 
 	void peerUpdated(PeerData *data);
@@ -158,7 +162,7 @@ private:
 	Text _nameText;
 	QString _nameCache;
 	QString _phoneText;
-	TextLinkPtr _photoLink;
+	ClickHandlerPtr _photoLink;
 	FlatButton _uploadPhoto, _addParticipant;
 	FlatButton _sendMessage, _shareContact, _inviteToGroup;
 	LinkButton _cancelPhoto, _createInvitationLink, _invitationLink;
@@ -271,6 +275,8 @@ public:
 		RPCSender::rpcClear();
 	}
 
+	PeerData *ui_getPeerForMouseAction();
+
 	void clear();
 	~ProfileWidget();
 
diff --git a/Telegram/SourceFiles/settingswidget.cpp b/Telegram/SourceFiles/settingswidget.cpp
index 6eb623270..538ea1cfc 100644
--- a/Telegram/SourceFiles/settingswidget.cpp
+++ b/Telegram/SourceFiles/settingswidget.cpp
@@ -215,7 +215,9 @@ SettingsInner::SettingsInner(SettingsWidget *parent) : TWidget(parent)
 
 		_nameText.setText(st::setNameFont, _nameCache, _textNameOptions);
 		PhotoData *selfPhoto = (self()->photoId && self()->photoId != UnknownPeerPhotoId) ? App::photo(self()->photoId) : 0;
-		if (selfPhoto && selfPhoto->date) _photoLink = TextLinkPtr(new PhotoLink(selfPhoto, self()));
+		if (selfPhoto && selfPhoto->date) {
+			_photoLink.reset(new PhotoOpenClickHandler(selfPhoto, self()));
+		}
 		App::api()->requestFullPeer(self());
 		onReloadPassword();
 
@@ -354,13 +356,13 @@ void SettingsInner::peerUpdated(PeerData *data) {
 		if (self()->photoId && self()->photoId != UnknownPeerPhotoId) {
 			PhotoData *selfPhoto = App::photo(self()->photoId);
 			if (selfPhoto->date) {
-				_photoLink = TextLinkPtr(new PhotoLink(selfPhoto, self()));
+				_photoLink.reset(new PhotoOpenClickHandler(selfPhoto, self()));
 			} else {
-				_photoLink = TextLinkPtr();
+				_photoLink.clear();
 				App::api()->requestFullPeer(self());
 			}
 		} else {
-			_photoLink = TextLinkPtr();
+			_photoLink.clear();
 		}
 
 		if (_nameCache != self()->name) {
@@ -940,9 +942,9 @@ void SettingsInner::onFullPeerUpdated(PeerData *peer) {
 
 	PhotoData *selfPhoto = (self()->photoId && self()->photoId != UnknownPeerPhotoId) ? App::photo(self()->photoId) : 0;
 	if (selfPhoto && selfPhoto->date) {
-		_photoLink = TextLinkPtr(new PhotoLink(selfPhoto, self()));
+		_photoLink.reset(new PhotoOpenClickHandler(selfPhoto, self()));
 	} else {
-		_photoLink = TextLinkPtr();
+		_photoLink.clear();
 	}
 }
 
diff --git a/Telegram/SourceFiles/settingswidget.h b/Telegram/SourceFiles/settingswidget.h
index 667921ba4..5d0954891 100644
--- a/Telegram/SourceFiles/settingswidget.h
+++ b/Telegram/SourceFiles/settingswidget.h
@@ -206,7 +206,7 @@ private:
 	// profile
 	Text _nameText;
 	QString _nameCache;
-	TextLinkPtr _photoLink;
+	ClickHandlerPtr _photoLink;
 	FlatButton _uploadPhoto;
 	LinkButton _cancelPhoto;
 	bool _nameOver, _photoOver;
diff --git a/Telegram/SourceFiles/structs.cpp b/Telegram/SourceFiles/structs.cpp
index 122380730..a60fce862 100644
--- a/Telegram/SourceFiles/structs.cpp
+++ b/Telegram/SourceFiles/structs.cpp
@@ -99,7 +99,6 @@ NotifySettings globalNotifyAll, globalNotifyUsers, globalNotifyChats;
 NotifySettingsPtr globalNotifyAllPtr = UnknownNotifySettings, globalNotifyUsersPtr = UnknownNotifySettings, globalNotifyChatsPtr = UnknownNotifySettings;
 
 PeerData::PeerData(const PeerId &id) : id(id)
-, lnk(new PeerLink(this))
 , loadedStatus(NotLoaded)
 , colorIndex(peerColorIndex(id))
 , color(peerColor(colorIndex))
@@ -684,24 +683,18 @@ PhotoData::~PhotoData() {
 	deleteAndMark(uploadingData);
 }
 
-void PhotoLink::onClick(Qt::MouseButton button) const {
-	if (button == Qt::LeftButton) {
-		App::wnd()->showPhoto(this, App::hoveredLinkItem() ? App::hoveredLinkItem() : App::contextItem());
-	}
+void PhotoOpenClickHandler::onClickImpl() const {
+	App::wnd()->showPhoto(this, App::hoveredLinkItem() ? App::hoveredLinkItem() : App::contextItem());
 }
 
-void PhotoSaveLink::onClick(Qt::MouseButton button) const {
-	if (button != Qt::LeftButton) return;
-
+void PhotoSaveClickHandler::onClickImpl() const {
 	PhotoData *data = photo();
 	if (!data->date) return;
 
 	data->download();
 }
 
-void PhotoCancelLink::onClick(Qt::MouseButton button) const {
-	if (button != Qt::LeftButton) return;
-
+void PhotoCancelClickHandler::onClickImpl() const {
 	PhotoData *data = photo();
 	if (!data->date) return;
 
@@ -857,7 +850,7 @@ QString documentSaveFilename(const DocumentData *data, bool forceSavingAs = fals
 	return saveFileName(caption, filter, prefix, name, forceSavingAs, dir);
 }
 
-void DocumentOpenLink::doOpen(DocumentData *data, ActionOnLoad action) {
+void DocumentOpenClickHandler::doOpen(DocumentData *data, ActionOnLoad action) {
 	if (!data->date) return;
 
 	HistoryItem *item = App::hoveredLinkItem() ? App::hoveredLinkItem() : (App::contextItem() ? App::contextItem() : 0);
@@ -933,22 +926,15 @@ void DocumentOpenLink::doOpen(DocumentData *data, ActionOnLoad action) {
 	data->save(filename, action, item ? item->fullId() : FullMsgId());
 }
 
-void DocumentOpenLink::onClick(Qt::MouseButton button) const {
-	if (button != Qt::LeftButton) return;
+void DocumentOpenClickHandler::onClickImpl() const {
 	doOpen(document(), document()->voice() ? ActionOnLoadNone : ActionOnLoadOpen);
 }
 
-void VoiceSaveLink::onClick(Qt::MouseButton button) const {
-	if (button != Qt::LeftButton) return;
-	doOpen(document(), ActionOnLoadNone);
-}
-
-void GifOpenLink::onClick(Qt::MouseButton button) const {
-	if (button != Qt::LeftButton) return;
+void GifOpenClickHandler::onClickImpl() const {
 	doOpen(document(), ActionOnLoadPlayInline);
 }
 
-void DocumentSaveLink::doSave(DocumentData *data, bool forceSavingAs) {
+void DocumentSaveClickHandler::doSave(DocumentData *data, bool forceSavingAs) {
 	if (!data->date) return;
 
 	QString filepath = data->filepath(DocumentData::FilePathResolveSaveFromDataSilent, forceSavingAs);
@@ -970,14 +956,11 @@ void DocumentSaveLink::doSave(DocumentData *data, bool forceSavingAs) {
 	}
 }
 
-void DocumentSaveLink::onClick(Qt::MouseButton button) const {
-	if (button != Qt::LeftButton) return;
+void DocumentSaveClickHandler::onClickImpl() const {
 	doSave(document());
 }
 
-void DocumentCancelLink::onClick(Qt::MouseButton button) const {
-	if (button != Qt::LeftButton) return;
-
+void DocumentCancelClickHandler::onClickImpl() const {
 	DocumentData *data = document();
 	if (!data->date) return;
 
@@ -1557,8 +1540,8 @@ InlineResult::~InlineResult() {
 	cancelFile();
 }
 
-void PeerLink::onClick(Qt::MouseButton button) const {
-	if (button == Qt::LeftButton && App::main()) {
+void PeerOpenClickHandler::onClickImpl() const {
+	if (App::main()) {
 		if (peer() && peer()->isChannel() && App::main()->historyPeer() != peer()) {
 			if (!peer()->asChannel()->isPublic() && !peer()->asChannel()->amIn()) {
 				Ui::showLayer(new InformBox(lang((peer()->isMegagroup()) ? lng_group_not_accessible : lng_channel_not_accessible)));
@@ -1571,22 +1554,6 @@ void PeerLink::onClick(Qt::MouseButton button) const {
 	}
 }
 
-void MessageLink::onClick(Qt::MouseButton button) const {
-	if (button == Qt::LeftButton && App::main()) {
-		HistoryItem *current = App::mousedItem();
-		if (current && current->history()->peer->id == peer()) {
-			App::main()->pushReplyReturn(current);
-		}
-		Ui::showPeerHistory(peer(), msgid());
-	}
-}
-
-void CommentsLink::onClick(Qt::MouseButton button) const {
-	if (button == Qt::LeftButton && App::main() && _item->history()->isChannel()) {
-		Ui::showPeerHistoryAtItem(_item);
-	}
-}
-
 MsgId clientMsgId() {
 	static MsgId currentClientMsgId = StartClientMsgId;
 	Q_ASSERT(currentClientMsgId < EndClientMsgId);
diff --git a/Telegram/SourceFiles/structs.h b/Telegram/SourceFiles/structs.h
index 6546a8c03..5a5e7def6 100644
--- a/Telegram/SourceFiles/structs.h
+++ b/Telegram/SourceFiles/structs.h
@@ -213,22 +213,40 @@ inline const QString &emptyUsername() {
 	return empty;
 }
 
+class PeerClickHandler : public LeftButtonClickHandler {
+public:
+	PeerClickHandler(PeerData *peer) : _peer(peer) {
+	}
+	PeerData *peer() const {
+		return _peer;
+	}
+
+private:
+	PeerData *_peer;
+
+};
+
+class PeerOpenClickHandler : public PeerClickHandler {
+public:
+	using PeerClickHandler::PeerClickHandler;
+protected:
+	void onClickImpl() const override;
+};
+
 class UserData;
 class ChatData;
 class ChannelData;
+
 class PeerData {
 protected:
-
 	PeerData(const PeerId &id);
 	PeerData(const PeerData &other) = delete;
 	PeerData &operator=(const PeerData &other) = delete;
 
 public:
-
 	virtual ~PeerData() {
 		if (notify != UnknownNotifySettings && notify != EmptyNotifySettings) {
-			delete notify;
-			notify = UnknownNotifySettings;
+			deleteAndMark(notify);
 		}
 	}
 
@@ -270,8 +288,6 @@ public:
 		return int32(uint32(id & 0xFFFFFFFFULL));
 	}
 
-	TextLinkPtr lnk;
-
 	QString name;
 	Text nameText;
 	typedef QSet<QString> Names;
@@ -315,29 +331,24 @@ public:
 		return QString();
 	}
 
-protected:
+	const ClickHandlerPtr &openLink() {
+		if (!_openLink) {
+			_openLink.reset(new PeerOpenClickHandler(this));
+		}
+		return _openLink;
+	}
 
+protected:
 	ImagePtr _userpic;
 	ImagePtr currentUserpic() const;
+
+private:
+	ClickHandlerPtr _openLink;
+
 };
 
 static const uint64 UserNoAccess = 0xFFFFFFFFFFFFFFFFULL;
 
-class PeerLink : public ITextLink {
-	TEXT_LINK_CLASS(PeerLink)
-
-public:
-	PeerLink(PeerData *peer) : _peer(peer) {
-	}
-	void onClick(Qt::MouseButton button) const;
-	PeerData *peer() const {
-		return _peer;
-	}
-
-private:
-	PeerData *_peer;
-};
-
 class BotCommand {
 public:
 	BotCommand(const QString &command, const QString &description) : command(command), _description(description) {
@@ -885,13 +896,10 @@ private:
 
 };
 
-class PhotoLink : public ITextLink {
-	TEXT_LINK_CLASS(PhotoLink)
-
+class PhotoClickHandler : public LeftButtonClickHandler {
 public:
-	PhotoLink(PhotoData *photo, PeerData *peer = 0) : _photo(photo), _peer(peer) {
+	PhotoClickHandler(PhotoData *photo, PeerData *peer = 0) : _photo(photo), _peer(peer) {
 	}
-	void onClick(Qt::MouseButton button) const;
 	PhotoData *photo() const {
 		return _photo;
 	}
@@ -905,24 +913,25 @@ private:
 
 };
 
-class PhotoSaveLink : public PhotoLink {
-	TEXT_LINK_CLASS(PhotoSaveLink)
-
+class PhotoOpenClickHandler : public PhotoClickHandler {
 public:
-	PhotoSaveLink(PhotoData *photo, PeerData *peer = 0) : PhotoLink(photo, peer) {
-	}
-	void onClick(Qt::MouseButton button) const;
-
+	using PhotoClickHandler::PhotoClickHandler;
+protected:
+	void onClickImpl() const override;
 };
 
-class PhotoCancelLink : public PhotoLink {
-	TEXT_LINK_CLASS(PhotoCancelLink)
-
+class PhotoSaveClickHandler : public PhotoClickHandler {
 public:
-	PhotoCancelLink(PhotoData *photo, PeerData *peer = 0) : PhotoLink(photo, peer) {
-	}
-	void onClick(Qt::MouseButton button) const;
+	using PhotoClickHandler::PhotoClickHandler;
+protected:
+	void onClickImpl() const override;
+};
 
+class PhotoCancelClickHandler : public PhotoClickHandler {
+public:
+	using PhotoClickHandler::PhotoClickHandler;
+protected:
+	void onClickImpl() const override;
 };
 
 enum FileStatus {
@@ -1160,11 +1169,9 @@ inline bool operator!=(const AudioMsgId &a, const AudioMsgId &b) {
 	return !(a == b);
 }
 
-class DocumentLink : public ITextLink {
-	TEXT_LINK_CLASS(DocumentLink)
-
+class DocumentClickHandler : public LeftButtonClickHandler {
 public:
-	DocumentLink(DocumentData *document) : _document(document) {
+	DocumentClickHandler(DocumentData *document) : _document(document) {
 	}
 	DocumentData *document() const {
 		return _document;
@@ -1175,56 +1182,34 @@ private:
 
 };
 
-class DocumentSaveLink : public DocumentLink {
-	TEXT_LINK_CLASS(DocumentSaveLink)
-
+class DocumentSaveClickHandler : public DocumentClickHandler {
 public:
-	DocumentSaveLink(DocumentData *document) : DocumentLink(document) {
-	}
+	using DocumentClickHandler::DocumentClickHandler;
 	static void doSave(DocumentData *document, bool forceSavingAs = false);
-	void onClick(Qt::MouseButton button) const;
-
+protected:
+	void onClickImpl() const override;
 };
 
-class DocumentOpenLink : public DocumentLink {
-	TEXT_LINK_CLASS(DocumentOpenLink)
-
+class DocumentOpenClickHandler : public DocumentClickHandler {
 public:
-	DocumentOpenLink(DocumentData *document) : DocumentLink(document) {
-	}
+	using DocumentClickHandler::DocumentClickHandler;
 	static void doOpen(DocumentData *document, ActionOnLoad action = ActionOnLoadOpen);
-	void onClick(Qt::MouseButton button) const;
-
+protected:
+	void onClickImpl() const override;
 };
 
-class VoiceSaveLink : public DocumentOpenLink {
-	TEXT_LINK_CLASS(VoiceSaveLink)
-
+class GifOpenClickHandler : public DocumentOpenClickHandler {
 public:
-	VoiceSaveLink(DocumentData *document) : DocumentOpenLink(document) {
-	}
-	void onClick(Qt::MouseButton button) const;
-
+	using DocumentOpenClickHandler::DocumentOpenClickHandler;
+protected:
+	void onClickImpl() const override;
 };
 
-class GifOpenLink : public DocumentOpenLink {
-	TEXT_LINK_CLASS(GifOpenLink)
-
+class DocumentCancelClickHandler : public DocumentClickHandler {
 public:
-	GifOpenLink(DocumentData *document) : DocumentOpenLink(document) {
-	}
-	void onClick(Qt::MouseButton button) const;
-
-};
-
-class DocumentCancelLink : public DocumentLink {
-	TEXT_LINK_CLASS(DocumentCancelLink)
-
-public:
-	DocumentCancelLink(DocumentData *document) : DocumentLink(document) {
-	}
-	void onClick(Qt::MouseButton button) const;
-
+	using DocumentClickHandler::DocumentClickHandler;
+protected:
+	void onClickImpl() const override;
 };
 
 enum WebPageType {
diff --git a/Telegram/SourceFiles/types.h b/Telegram/SourceFiles/types.h
index df9fd6d27..a6a776a3e 100644
--- a/Telegram/SourceFiles/types.h
+++ b/Telegram/SourceFiles/types.h
@@ -775,6 +775,11 @@ inline UniquePointer<T> MakeUnique(Args&&... args) {
 	return UniquePointer<T>(new T(std_::forward<Args>(args)...));
 }
 
+template <typename T, class... Args>
+inline QSharedPointer<T> MakeShared(Args&&... args) {
+	return QSharedPointer<T>(new T(std_::forward<Args>(args)...));
+}
+
 template <typename I>
 inline void destroyImplementation(I *&ptr) {
 	if (ptr) {
diff --git a/Telegram/SourceFiles/window.cpp b/Telegram/SourceFiles/window.cpp
index d2ce17a4f..3299275fc 100644
--- a/Telegram/SourceFiles/window.cpp
+++ b/Telegram/SourceFiles/window.cpp
@@ -792,7 +792,7 @@ PasscodeWidget *Window::passcodeWidget() {
 	return _passcode;
 }
 
-void Window::showPhoto(const PhotoLink *lnk, HistoryItem *item) {
+void Window::showPhoto(const PhotoOpenClickHandler *lnk, HistoryItem *item) {
 	return lnk->peer() ? showPhoto(lnk->photo(), lnk->peer()) : showPhoto(lnk->photo(), item);
 }
 
@@ -879,6 +879,15 @@ void Window::ui_hideStickerPreview() {
 	_stickerPreview->hidePreview();
 }
 
+PeerData *Window::ui_getPeerForMouseAction() {
+	if (_mediaView && !_mediaView->isHidden()) {
+		return _mediaView->ui_getPeerForMouseAction();
+	} else if (main) {
+		return main->ui_getPeerForMouseAction();
+	}
+	return nullptr;
+}
+
 void Window::showConnecting(const QString &text, const QString &reconnect) {
 	if (_connecting) {
 		_connecting->set(text, reconnect);
@@ -1736,8 +1745,8 @@ void Window::notifyUpdateAllPhotos() {
 	if (_mediaView && !_mediaView->isHidden()) _mediaView->updateControls();
 }
 
-void Window::app_activateTextLink(TextLinkPtr link, Qt::MouseButton button) {
-	link->onClick(button);
+void Window::app_activateClickHandler(ClickHandlerPtr handler, Qt::MouseButton button) {
+	handler->onClick(button);
 }
 
 void Window::notifyUpdateAll() {
diff --git a/Telegram/SourceFiles/window.h b/Telegram/SourceFiles/window.h
index cecad8cf7..0c7f589c0 100644
--- a/Telegram/SourceFiles/window.h
+++ b/Telegram/SourceFiles/window.h
@@ -180,7 +180,7 @@ public:
 	void hideConnecting();
 	bool connectingVisible() const;
 
-	void showPhoto(const PhotoLink *lnk, HistoryItem *item = 0);
+	void showPhoto(const PhotoOpenClickHandler *lnk, HistoryItem *item = 0);
 	void showPhoto(PhotoData *photo, HistoryItem *item);
 	void showPhoto(PhotoData *photo, PeerData *item);
 	void showDocument(DocumentData *doc, HistoryItem *item);
@@ -241,6 +241,7 @@ public:
 	bool ui_isMediaViewShown();
 	void ui_showStickerPreview(DocumentData *sticker);
 	void ui_hideStickerPreview();
+	PeerData *ui_getPeerForMouseAction();
 
 public slots:
 
@@ -283,7 +284,7 @@ public slots:
 
 	void notifyUpdateAllPhotos();
 
-	void app_activateTextLink(TextLinkPtr link, Qt::MouseButton button);
+	void app_activateClickHandler(ClickHandlerPtr handler, Qt::MouseButton button);
 
 signals: