diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp
index 7dddb8582..aebf8efb0 100644
--- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp
+++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp
@@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Calls {
 namespace {
 
-ushort Data[] = {
+const ushort Data[] = {
 0xd83d, 0xde09, 0xd83d, 0xde0d, 0xd83d, 0xde1b, 0xd83d, 0xde2d, 0xd83d, 0xde31, 0xd83d, 0xde21,
 0xd83d, 0xde0e, 0xd83d, 0xde34, 0xd83d, 0xde35, 0xd83d, 0xde08, 0xd83d, 0xde2c, 0xd83d, 0xde07,
 0xd83d, 0xde0f, 0xd83d, 0xdc6e, 0xd83d, 0xdc77, 0xd83d, 0xdc82, 0xd83d, 0xdc76, 0xd83d, 0xdc68,
@@ -69,7 +69,7 @@ ushort Data[] = {
 0x0030, 0x20e3, 0xd83d, 0xdd1f, 0x2757, 0x2753, 0x2665, 0x2666, 0xd83d, 0xdcaf, 0xd83d, 0xdd17,
 0xd83d, 0xdd31, 0xd83d, 0xdd34, 0xd83d, 0xdd35, 0xd83d, 0xdd36, 0xd83d, 0xdd37 };
 
-ushort Offsets[] = {
+const ushort Offsets[] = {
 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22,
 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46,
 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70,
@@ -119,7 +119,9 @@ std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
 	for (auto index = 0; index != EmojiCount; ++index) {
 		auto offset = Offsets[index];
 		auto size = Offsets[index + 1] - offset;
-		auto string = QString::fromRawData(reinterpret_cast<QChar*>(Data + offset), size);
+		auto string = QString::fromRawData(
+			reinterpret_cast<const QChar*>(Data + offset),
+			size);
 		auto emoji = Ui::Emoji::Find(string);
 		Assert(emoji != nullptr);
 	}
@@ -131,7 +133,9 @@ std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
 			auto index = value % EmojiCount;
 			auto offset = Offsets[index];
 			auto size = Offsets[index + 1] - offset;
-			auto string = QString::fromRawData(reinterpret_cast<QChar*>(Data + offset), size);
+			auto string = QString::fromRawData(
+				reinterpret_cast<const QChar*>(Data + offset),
+				size);
 			auto emoji = Ui::Emoji::Find(string);
 			Assert(emoji != nullptr);
 			result.push_back(emoji);
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_helper.h b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_helper.h
index f7df9bdeb..0b4b55796 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_helper.h
+++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_helper.h
@@ -14,11 +14,15 @@ namespace Ui {
 namespace Emoji {
 
 inline utf16string QStringToUTF16(const QString &string) {
-	return utf16string(reinterpret_cast<const utf16char*>(string.constData()), string.size());
+	return utf16string(
+		reinterpret_cast<const utf16char*>(string.constData()),
+		string.size());
 }
 
 inline QString QStringFromUTF16(utf16string string) {
-	return QString::fromRawData(reinterpret_cast<const QChar*>(string.data()), string.size());
+	return QString::fromRawData(
+		reinterpret_cast<const QChar*>(string.data()),
+		string.size());
 }
 
 constexpr auto kSuggestionMaxLength = internal::kReplacementMaxLength;
diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp
index d0d76938a..a2268935a 100644
--- a/Telegram/SourceFiles/chat_helpers/message_field.cpp
+++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp
@@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/qthelp_regex.h"
 #include "styles/style_history.h"
 #include "window/window_controller.h"
+#include "emoji_suggestions_data.h"
+#include "chat_helpers/emoji_suggestions_helper.h"
 #include "mainwindow.h"
 #include "auth_session.h"
 
@@ -115,6 +117,22 @@ MessageField::MessageField(QWidget *parent, not_null<Window::Controller*> contro
 	setMaxHeight(st::historyComposeFieldMaxHeight);
 
 	setTagMimeProcessor(std::make_unique<FieldTagMimeProcessor>());
+
+	addInstantReplace("--", QString(1, QChar(8212)));
+	addInstantReplace("<<", QString(1, QChar(171)));
+	addInstantReplace(">>", QString(1, QChar(187)));
+	const auto &replacements = Ui::Emoji::internal::GetAllReplacements();
+	for (const auto &one : replacements) {
+		const auto with = Ui::Emoji::QStringFromUTF16(one.emoji);
+		const auto what = Ui::Emoji::QStringFromUTF16(one.replacement);
+		addInstantReplace(what, with);
+	}
+	const auto &pairs = Ui::Emoji::internal::GetReplacementPairs();
+	for (const auto &[what, index] : pairs) {
+		const auto emoji = Ui::Emoji::internal::ByIndex(index);
+		Assert(emoji != nullptr);
+		addInstantReplace(what, emoji->text());
+	}
 }
 
 bool MessageField::hasSendText() const {
diff --git a/Telegram/SourceFiles/codegen/emoji/data.cpp b/Telegram/SourceFiles/codegen/emoji/data.cpp
index a880621a3..e5407b58e 100644
--- a/Telegram/SourceFiles/codegen/emoji/data.cpp
+++ b/Telegram/SourceFiles/codegen/emoji/data.cpp
@@ -57,7 +57,7 @@ Replace Replaces[] = {
 	{ { 0xD83DDE22U }, ":'(" },
 	{ { 0xD83DDE2DU }, ":_(" },
 	{ { 0xD83DDE29U }, ":((" },
-	{ { 0xD83DDE28U }, ":o" },
+//	{ { 0xD83DDE28U }, ":o" }, // Conflicts with typing :ok...
 	{ { 0xD83DDE10U }, ":|" },
 	{ { 0xD83DDE0CU }, "3-)" },
 	{ { 0xD83DDE20U }, ">(" },
diff --git a/Telegram/SourceFiles/codegen/emoji/generator.cpp b/Telegram/SourceFiles/codegen/emoji/generator.cpp
index a6b9e960e..610bbe3c7 100644
--- a/Telegram/SourceFiles/codegen/emoji/generator.cpp
+++ b/Telegram/SourceFiles/codegen/emoji/generator.cpp
@@ -335,6 +335,10 @@ EmojiPtr FindReplace(const QChar *start, const QChar *end, int *outLength) {\n\
 	return index ? &Items[index - 1] : nullptr;\n\
 }\n\
 \n\
+const std::vector<std::pair<QString, int>> GetReplacementPairs() {\n\
+	return ReplacementPairs;\n\
+}\n\
+\n\
 EmojiPtr Find(const QChar *start, const QChar *end, int *outLength) {\n\
 	auto index = FindIndex(start, end, outLength);\n\
 	return index ? &Items[index - 1] : nullptr;\n\
@@ -389,6 +393,7 @@ inline bool IsReplaceEdge(const QChar *ch) {\n\
 //	return false;\n\
 }\n\
 \n\
+const std::vector<std::pair<QString, int>> GetReplacementPairs();\n\
 EmojiPtr FindReplace(const QChar *ch, const QChar *end, int *outLength = nullptr);\n\
 \n";
 	header->popNamespace().stream() << "\
@@ -591,6 +596,14 @@ EmojiPack GetSection(Section section) {\n\
 bool Generator::writeFindReplace() {
 	source_->stream() << "\
 \n\
+const std::vector<std::pair<QString, int>> ReplacementPairs = {\n";
+	for (const auto &[what, index] : data_.replaces) {
+		source_->stream() << "\
+	{ qsl(\"" << what << "\"), " << index << " },\n";
+	}
+	source_->stream() << "\
+};\n\
+\n\
 int FindReplaceIndex(const QChar *start, const QChar *end, int *outLength) {\n\
 	auto ch = start;\n\
 \n";
@@ -783,6 +796,7 @@ struct Replacement {\n\
 constexpr auto kReplacementMaxLength = " << maxLength << ";\n\
 \n\
 void InitReplacements();\n\
+const std::vector<Replacement> &GetAllReplacements();\n\
 const std::vector<const Replacement*> *GetReplacements(utf16char first);\n\
 utf16string GetReplacementEmoji(utf16string replacement);\n\
 \n";
@@ -923,6 +937,10 @@ const std::vector<const Replacement*> *GetReplacements(utf16char first) {\n\
 	return (it == ReplacementsMap.cend()) ? nullptr : &it->second;\n\
 }\n\
 \n\
+const std::vector<Replacement> &GetAllReplacements() {\n\
+	return Replacements;\n\
+}\n\
+\n\
 utf16string GetReplacementEmoji(utf16string replacement) {\n\
 	auto code = internal::countChecksum(replacement.data(), replacement.size() * sizeof(utf16char));\n\
 	auto it = ReplacementsHash.find(code);\n\
diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp
index f4f65d53e..17f425f71 100644
--- a/Telegram/SourceFiles/ui/text/text_entity.cpp
+++ b/Telegram/SourceFiles/ui/text/text_entity.cpp
@@ -2153,47 +2153,6 @@ void MovePartAndGoForward(TextWithEntities &result, int &to, int &from, int coun
 	from += count;
 }
 
-void ReplaceStringWithChar(const QLatin1String &from, QChar to, TextWithEntities &result, bool checkSpace = false) {
-	Expects(from.size() > 1);
-	auto len = from.size(), s = result.text.size(), offset = 0, length = 0;
-	auto i = result.entities.begin(), e = result.entities.end();
-	for (auto start = result.text.data(); offset < s;) {
-		auto nextOffset = result.text.indexOf(from, offset);
-		if (nextOffset < 0) {
-			MovePartAndGoForward(result, length, offset, s - offset);
-			break;
-		}
-
-		if (checkSpace) {
-			bool spaceBefore = (nextOffset > 0) && (start + nextOffset - 1)->isSpace();
-			bool spaceAfter = (nextOffset + len < s) && (start + nextOffset + len)->isSpace();
-			if (!spaceBefore && !spaceAfter) {
-				MovePartAndGoForward(result, length, offset, nextOffset - offset + len + 1);
-				continue;
-			}
-		}
-
-		auto skip = false;
-		for (; i != e; ++i) { // find and check next finishing entity
-			if (i->offset() + i->length() > nextOffset) {
-				skip = (i->offset() < nextOffset + len);
-				break;
-			}
-		}
-		if (skip) {
-			MovePartAndGoForward(result, length, offset, nextOffset - offset + len);
-			continue;
-		}
-
-		MovePartAndGoForward(result, length, offset, nextOffset - offset);
-
-		*(start + length) = to;
-		++length;
-		offset += len;
-	}
-	if (length < s) result.text.resize(length);
-}
-
 void PrepareForSending(TextWithEntities &result, int32 flags) {
 	ApplyServerCleaning(result);
 
@@ -2201,14 +2160,6 @@ void PrepareForSending(TextWithEntities &result, int32 flags) {
 		ParseEntities(result, flags);
 	}
 
-	ReplaceStringWithChar(qstr("--"), QChar(8212), result, true);
-	ReplaceStringWithChar(qstr("<<"), QChar(171), result);
-	ReplaceStringWithChar(qstr(">>"), QChar(187), result);
-
-	if (cReplaceEmojis()) {
-		Ui::Emoji::ReplaceInText(result);
-	}
-
 	Trim(result);
 }
 
diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp
index 42813eb9d..cb0370423 100644
--- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp
+++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp
@@ -19,6 +19,13 @@ namespace Ui {
 namespace {
 
 constexpr auto kMaxUsernameLength = 32;
+constexpr auto kInstantReplaceRandomId = QTextFormat::UserProperty;
+constexpr auto kInstantReplaceWhatId = QTextFormat::UserProperty + 1;
+constexpr auto kInstantReplaceWithId = QTextFormat::UserProperty + 2;
+const auto kObjectReplacementCh = QChar(QChar::ObjectReplacementCharacter);
+const auto kObjectReplacement = QString::fromRawData(
+	&kObjectReplacementCh,
+	1);
 
 template <typename InputClass>
 class InputStyle : public QCommonStyle {
@@ -61,6 +68,28 @@ private:
 template <typename InputClass>
 InputStyle<InputClass> *InputStyle<InputClass>::_instance = nullptr;
 
+template <typename Iterator>
+QString AccumulateText(Iterator begin, Iterator end) {
+	auto result = QString();
+	result.reserve(end - begin);
+	for (auto i = end; i != begin;) {
+		result.push_back(*--i);
+	}
+	return result;
+}
+
+QTextImageFormat PrepareEmojiFormat(EmojiPtr emoji, const style::font &f) {
+	const auto factor = cIntRetinaFactor();
+	const auto width = Ui::Emoji::Size() + st::emojiPadding * factor * 2;
+	const auto height = f->height * factor;
+	auto result = QTextImageFormat();
+	result.setWidth(width / factor);
+	result.setHeight(height / factor);
+	result.setName(emoji->toUrl());
+	result.setVerticalAlignment(QTextCharFormat::AlignBaseline);
+	return result;
+}
+
 } // namespace
 
 QByteArray FlatTextarea::serializeTagsList(const TagList &tags) {
@@ -122,6 +151,7 @@ FlatTextarea::FlatTextarea(QWidget *parent, const style::FlatTextarea &st, base:
 , _placeholderVisible(!v.length())
 , _lastTextWithTags { v, tags }
 , _st(st) {
+	_defaultCharFormat = textCursor().charFormat();
 
 	setCursor(style::cur_text);
 	setAcceptRichText(false);
@@ -170,6 +200,17 @@ FlatTextarea::FlatTextarea(QWidget *parent, const style::FlatTextarea &st, base:
 	}
 }
 
+void FlatTextarea::addInstantReplace(
+		const QString &what,
+		const QString &with) {
+	auto node = &_reverseInstantReplaces;
+	for (const auto ch : base::reversed(what)) {
+		node = &node->tail.emplace(ch, InstantReplaceNode()).first->second;
+	}
+	node->text = with;
+	accumulate_max(_instantReplaceMaxLength, int(what.size()));
+}
+
 void FlatTextarea::updatePalette() {
 	auto p = palette();
 	p.setColor(QPalette::Text, _st.textColor->c);
@@ -472,7 +513,7 @@ QString FlatTextarea::getMentionHashtagBotCommandPart(bool &start) const {
 		int32 p = fr.position(), e = (p + fr.length());
 		if (p >= pos || e < pos) continue;
 
-		QTextCharFormat f = fr.charFormat();
+		const auto f = fr.charFormat();
 		if (f.isImageFormat()) continue;
 
 		bool mentionInCommand = false;
@@ -559,11 +600,7 @@ void FlatTextarea::insertTag(const QString &text, QString tagId) {
 		break;
 	}
 	if (tagId.isEmpty()) {
-		QTextCharFormat format = cursor.charFormat();
-		format.setAnchor(false);
-		format.setAnchorName(QString());
-		format.clearForeground();
-		cursor.insertText(text + ' ', format);
+		cursor.insertText(text + ' ', _defaultCharFormat);
 	} else {
 		_insertedTags.clear();
 		_insertedTags.push_back({ 0, text.size(), tagId });
@@ -597,15 +634,18 @@ void FlatTextarea::getSingleEmojiFragment(QString &text, QTextFragment &fragment
 				continue;
 			}
 
-			QTextCharFormat f = fr.charFormat();
-			QString t(fr.text());
+			const auto f = fr.charFormat();
+			auto t = fr.text();
 			if (p < start) {
 				t = t.mid(start - p, end - start);
 			} else if (e > end) {
 				t = t.mid(0, end - p);
 			}
-			if (f.isImageFormat() && !t.isEmpty() && t.at(0).unicode() == QChar::ObjectReplacementCharacter) {
-				auto imageName = static_cast<QTextImageFormat*>(&f)->name();
+			if (f.isImageFormat()
+				&& !t.isEmpty()
+				&& t[0] == kObjectReplacementCh) {
+				const auto imageName = static_cast<const QTextImageFormat*>(
+					&f)->name();
 				if (Ui::Emoji::FromUrl(imageName)) {
 					fragment = fr;
 					text = t;
@@ -743,9 +783,9 @@ QString FlatTextarea::getTextPart(int start, int end, TagList *outTagsList, bool
 				tagAccumulator.feed(fragment.charFormat().anchorName(), result.size());
 			}
 
-			QTextCharFormat f = fragment.charFormat();
+			const auto f = fragment.charFormat();
 			QString emojiText;
-			QString t(fragment.text());
+			auto t = fragment.text();
 			if (!full) {
 				if (p < start) {
 					t = t.mid(start - p, end - start);
@@ -767,8 +807,8 @@ QString FlatTextarea::getTextPart(int start, int end, TagList *outTagsList, bool
 				} break;
 				case QChar::ObjectReplacementCharacter: {
 					if (emojiText.isEmpty() && f.isImageFormat()) {
-						auto imageName = static_cast<QTextImageFormat*>(&f)->name();
-						if (auto emoji = Ui::Emoji::FromUrl(imageName)) {
+						const auto imageName = static_cast<const QTextImageFormat*>(&f)->name();
+						if (const auto emoji = Ui::Emoji::FromUrl(imageName)) {
 							emojiText = emoji->text();
 						}
 					}
@@ -929,20 +969,13 @@ void FlatTextarea::insertFromMimeData(const QMimeData *source) {
 }
 
 void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) {
-	QTextImageFormat imageFormat;
-	auto ew = Ui::Emoji::Size() + st::emojiPadding * cIntRetinaFactor() * 2;
-	auto eh = _st.font->height * cIntRetinaFactor();
-	imageFormat.setWidth(ew / cIntRetinaFactor());
-	imageFormat.setHeight(eh / cIntRetinaFactor());
-	imageFormat.setName(emoji->toUrl());
-	imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline);
+	auto format = PrepareEmojiFormat(emoji, _st.font);
 	if (c.charFormat().isAnchor()) {
-		imageFormat.setAnchor(true);
-		imageFormat.setAnchorName(c.charFormat().anchorName());
-		imageFormat.setForeground(st::defaultTextPalette.linkFg);
+		format.setAnchor(true);
+		format.setAnchorName(c.charFormat().anchorName());
+		format.setForeground(st::defaultTextPalette.linkFg);
 	}
-	static QString objectReplacement(QChar::ObjectReplacementCharacter);
-	c.insertText(objectReplacement, imageFormat);
+	c.insertText(kObjectReplacement, format);
 }
 
 QVariant FlatTextarea::loadResource(int type, const QUrl &name) {
@@ -1054,6 +1087,7 @@ struct FormattingAction {
 		InsertEmoji,
 		TildeFont,
 		RemoveTag,
+		ClearInstantReplace,
 	};
 	Type type = Type::Invalid;
 	EmojiPtr emoji = nullptr;
@@ -1104,18 +1138,33 @@ void FlatTextarea::processFormatting(int insertPosition, int insertEnd) {
 					break;
 				}
 
-				auto charFormat = fragment.charFormat();
+				auto format = fragment.charFormat();
 				if (tildeFormatting) {
-					isTildeFragment = (charFormat.fontFamily() == tildeFixedFont);
+					isTildeFragment = (format.fontFamily() == tildeFixedFont);
 				}
 
 				auto fragmentText = fragment.text();
 				auto *textStart = fragmentText.constData();
 				auto *textEnd = textStart + fragmentText.size();
 
+				const auto with = format.property(kInstantReplaceWithId);
+				if (with.isValid()) {
+					const auto string = with.toString();
+					if (fragmentText != string) {
+						action.type = ActionType::ClearInstantReplace;
+						action.intervalStart = fragmentPosition
+							+ (fragmentText.startsWith(string)
+								? string.size()
+								: 0);
+						action.intervalEnd = fragmentPosition
+							+ fragmentText.size();
+						break;
+					}
+				}
+
 				if (!startTagFound) {
 					startTagFound = true;
-					auto tagName = charFormat.anchorName();
+					auto tagName = format.anchorName();
 					if (!tagName.isEmpty()) {
 						breakTagOnNotLetter = wasInsertTillTheEndOfTag(block, fragmentIt, insertEnd);
 					}
@@ -1193,6 +1242,8 @@ void FlatTextarea::processFormatting(int insertPosition, int insertEnd) {
 				format.setFontFamily(action.isTilde ? tildeFixedFont : tildeRegularFont);
 				c.mergeCharFormat(format);
 				insertPosition = action.intervalEnd;
+			} else if (action.type == ActionType::ClearInstantReplace) {
+				c.setCharFormat(_defaultCharFormat);
 			}
 		} else {
 			break;
@@ -1372,6 +1423,10 @@ void FlatTextarea::keyPressEvent(QKeyEvent *e) {
 		start.movePosition(QTextCursor::StartOfLine);
 		tc.setPosition(start.position(), QTextCursor::KeepAnchor);
 		tc.removeSelectedText();
+	} else if (e->key() == Qt::Key_Backspace
+		&& e->modifiers() == 0
+		&& revertInstantReplace()) {
+		e->accept();
 	} else if (enter && enterSubmit) {
 		emit submitted(ctrl && shift);
 	} else if (e->key() == Qt::Key_Escape) {
@@ -1394,39 +1449,174 @@ void FlatTextarea::keyPressEvent(QKeyEvent *e) {
 		}
 #endif // Q_OS_MAC
 	} else {
-		QTextCursor tc(textCursor());
+		const auto text = e->text();
+		const auto key = e->key();
+		auto cursor = textCursor();
 		if (enter && ctrl) {
 			e->setModifiers(e->modifiers() & ~Qt::ControlModifier);
 		}
 		bool spaceOrReturn = false;
-		QString t(e->text());
-		if (!t.isEmpty() && t.size() < 3) {
-			if (t.at(0) == '\n' || t.at(0) == '\r' || t.at(0).isSpace() || t.at(0) == QChar::LineSeparator) {
+		if (!text.isEmpty() && text.size() < 3) {
+			const auto ch = text[0];
+			if (ch == '\n'
+				|| ch == '\r'
+				|| ch.isSpace()
+				|| ch == QChar::LineSeparator) {
 				spaceOrReturn = true;
 			}
 		}
 		QTextEdit::keyPressEvent(e);
-		if (tc == textCursor()) {
+		if (cursor == textCursor()) {
 			bool check = false;
-			if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) {
-				tc.movePosition(QTextCursor::Start, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor);
+			if (key == Qt::Key_PageUp || key == Qt::Key_Up) {
+				cursor.movePosition(
+					QTextCursor::Start,
+					(e->modifiers().testFlag(Qt::ShiftModifier)
+						? QTextCursor::KeepAnchor
+						: QTextCursor::MoveAnchor));
 				check = true;
-			} else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_Down) {
-				tc.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor);
+			} else if (key == Qt::Key_PageDown || key == Qt::Key_Down) {
+				cursor.movePosition(
+					QTextCursor::End,
+					(e->modifiers().testFlag(Qt::ShiftModifier)
+						? QTextCursor::KeepAnchor
+						: QTextCursor::MoveAnchor));
 				check = true;
 			}
 			if (check) {
-				if (tc == textCursor()) {
+				if (cursor == textCursor()) {
 					e->ignore();
 				} else {
-					setTextCursor(tc);
+					setTextCursor(cursor);
 				}
 			}
 		}
-		if (spaceOrReturn) emit spacedReturnedPasted();
+		processInstantReplaces(text);
+		if (spaceOrReturn) {
+			emit spacedReturnedPasted();
+		}
 	}
 }
 
+void FlatTextarea::processInstantReplaces(const QString &text) {
+	if (text.size() != 1 || !_instantReplaceMaxLength) {
+		return;
+	}
+	const auto it = _reverseInstantReplaces.tail.find(text[0]);
+	if (it == end(_reverseInstantReplaces.tail)) {
+		return;
+	}
+	const auto position = textCursor().position();
+	auto tags = QVector<TextWithTags::Tag>();
+	const auto typed = getTextPart(
+		std::max(position - _instantReplaceMaxLength, 0),
+		position - 1,
+		&tags);
+	auto node = &it->second;
+	auto i = typed.size();
+	do {
+		if (!node->text.isEmpty()) {
+			applyInstantReplace(typed.mid(i) + text, node->text);
+			return;
+		} else if (!i) {
+			return;
+		}
+		const auto it = node->tail.find(typed[--i]);
+		if (it == end(node->tail)) {
+			return;
+		}
+		node = &it->second;
+	} while (true);
+}
+
+void FlatTextarea::applyInstantReplace(
+		const QString &what,
+		const QString &with) {
+	const auto length = int(what.size());
+	const auto cursor = textCursor();
+	const auto position = cursor.position();
+	if (cursor.anchor() != position) {
+		return;
+	} else if (position < length) {
+		return;
+	}
+	auto tags = QVector<TextWithTags::Tag>();
+	const auto original = getTextPart(position - length, position, &tags);
+	if (what.compare(original, Qt::CaseInsensitive) != 0) {
+		return;
+	}
+
+	auto format = [&]() -> QTextCharFormat {
+		auto emojiLength = 0;
+		const auto emoji = Ui::Emoji::Find(with, &emojiLength);
+		if (!emoji || with.size() != emojiLength) {
+			return cursor.charFormat();
+		}
+		const auto use = [&] {
+			if (!emoji->hasVariants()) {
+				return emoji;
+			}
+			const auto nonColored = emoji->nonColoredId();
+			const auto it = cEmojiVariants().constFind(nonColored);
+			return (it != cEmojiVariants().cend())
+				? emoji->variant(it.value())
+				: emoji;
+		}();
+		return PrepareEmojiFormat(use, _st.font);
+	}();
+	const auto replacement = format.isImageFormat()
+		? kObjectReplacement
+		: with;
+	format.setProperty(kInstantReplaceWhatId, original);
+	format.setProperty(kInstantReplaceWithId, replacement);
+	format.setProperty(kInstantReplaceRandomId, rand_value<uint32>());
+	auto replaceCursor = cursor;
+	replaceCursor.setPosition(position - length);
+	replaceCursor.setPosition(position, QTextCursor::KeepAnchor);
+	replaceCursor.insertText(
+		replacement,
+		format);
+}
+
+bool FlatTextarea::revertInstantReplace() {
+	const auto cursor = textCursor();
+	const auto position = cursor.position();
+	if (position <= 0 || cursor.anchor() != position) {
+		return false;
+	}
+	const auto inside = position - 1;
+	const auto block = document()->findBlock(inside);
+	if (block == document()->end()) {
+		return false;
+	}
+	for (auto i = block.begin(); !i.atEnd(); ++i) {
+		const auto fragment = i.fragment();
+		const auto fragmentStart = fragment.position();
+		const auto fragmentEnd = fragmentStart + fragment.length();
+		if (fragmentEnd <= inside) {
+			continue;
+		} else if (fragmentStart > inside || fragmentEnd != position) {
+			return false;
+		}
+		const auto format = fragment.charFormat();
+		const auto with = format.property(kInstantReplaceWithId);
+		if (!with.isValid()) {
+			return false;
+		}
+		const auto string = with.toString();
+		if (fragment.text() != string) {
+			return false;
+		}
+		auto replaceCursor = cursor;
+		replaceCursor.setPosition(fragmentStart);
+		replaceCursor.setPosition(fragmentEnd, QTextCursor::KeepAnchor);
+		const auto what = format.property(kInstantReplaceWhatId).toString();
+		replaceCursor.insertText(what, _defaultCharFormat);
+		return true;
+	}
+	return false;
+}
+
 void FlatTextarea::resizeEvent(QResizeEvent *e) {
 	refreshPlaceholder();
 	QTextEdit::resizeEvent(e);
@@ -2135,16 +2325,8 @@ bool InputArea::isRedoAvailable() const {
 }
 
 void InputArea::insertEmoji(EmojiPtr emoji, QTextCursor c) {
-	QTextImageFormat imageFormat;
-	auto ew = Ui::Emoji::Size() + st::emojiPadding * cIntRetinaFactor() * 2;
-	auto eh = _st.font->height * cIntRetinaFactor();
-	imageFormat.setWidth(ew / cIntRetinaFactor());
-	imageFormat.setHeight(eh / cIntRetinaFactor());
-	imageFormat.setName(emoji->toUrl());
-	imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline);
-
-	static QString objectReplacement(QChar::ObjectReplacementCharacter);
-	c.insertText(objectReplacement, imageFormat);
+	const auto format = PrepareEmojiFormat(emoji, _st.font);
+	c.insertText(kObjectReplacement, format);
 }
 
 QVariant InputArea::Inner::loadResource(int type, const QUrl &name) {
@@ -2905,15 +3087,8 @@ bool InputField::isRedoAvailable() const {
 }
 
 void InputField::insertEmoji(EmojiPtr emoji, QTextCursor c) {
-	QTextImageFormat imageFormat;
-	auto ew = Ui::Emoji::Size() + st::emojiPadding * cIntRetinaFactor() * 2, eh = _st.font->height * cIntRetinaFactor();
-	imageFormat.setWidth(ew / cIntRetinaFactor());
-	imageFormat.setHeight(eh / cIntRetinaFactor());
-	imageFormat.setName(emoji->toUrl());
-	imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline);
-
-	static QString objectReplacement(QChar::ObjectReplacementCharacter);
-	c.insertText(objectReplacement, imageFormat);
+	const auto format = PrepareEmojiFormat(emoji, _st.font);
+	c.insertText(kObjectReplacement, format);
 }
 
 QVariant InputField::Inner::loadResource(int type, const QUrl &name) {
diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.h b/Telegram/SourceFiles/ui/widgets/input_fields.h
index c3d6bb3fa..b31f476b4 100644
--- a/Telegram/SourceFiles/ui/widgets/input_fields.h
+++ b/Telegram/SourceFiles/ui/widgets/input_fields.h
@@ -32,6 +32,8 @@ public:
 	void setMinHeight(int minHeight);
 	void setMaxHeight(int maxHeight);
 
+	void addInstantReplace(const QString &what, const QString &with);
+
 	void setPlaceholder(base::lambda<QString()> placeholderFactory, int afterSymbols = 0);
 	void updatePlaceholder();
 	void finishPlaceholder();
@@ -142,6 +144,10 @@ protected:
 	void checkContentHeight();
 
 private:
+	struct InstantReplaceNode {
+		QString text;
+		std::map<QChar, InstantReplaceNode> tail;
+	};
 	void updatePalette();
 	void refreshPlaceholder();
 
@@ -160,6 +166,10 @@ private:
 	// Rule 4 applies only if we inserted chars not in the middle of a tag (but at the end).
 	void processFormatting(int changedPosition, int changedEnd);
 
+	void processInstantReplaces(const QString &text);
+	void applyInstantReplace(const QString &what, const QString &with);
+	bool revertInstantReplace();
+
 	bool heightAutoupdated();
 
 	int placeholderSkipWidth() const;
@@ -215,6 +225,12 @@ private:
 	friend bool operator!=(const LinkRange &a, const LinkRange &b);
 	using LinkRanges = QVector<LinkRange>;
 	LinkRanges _links;
+
+	QTextCharFormat _defaultCharFormat;
+
+	int _instantReplaceMaxLength = 0;
+	InstantReplaceNode _reverseInstantReplaces;
+
 };
 
 inline bool operator==(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) {