From df2aeb0c582c689a24d183b00c3bb157cdd43611 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 24 Jul 2017 19:18:39 +0300
Subject: [PATCH] Provide emoji suggestions as a service.

---
 .../chat_helpers/emoji_suggestions.cpp        | 302 ------------
 .../chat_helpers/emoji_suggestions.h          |  35 --
 .../chat_helpers/emoji_suggestions_widget.cpp |  15 +-
 .../chat_helpers/emoji_suggestions_widget.h   |   1 -
 Telegram/SourceFiles/codegen/emoji/data.cpp   |  16 +-
 .../SourceFiles/codegen/emoji/generator.cpp   | 394 ++++++++++------
 .../SourceFiles/codegen/emoji/generator.h     |  12 +-
 .../SourceFiles/codegen/emoji/replaces.cpp    |  25 +-
 .../emoji_suggestions/emoji_suggestions.cpp   | 432 ++++++++++++++++++
 .../emoji_suggestions/emoji_suggestions.h     | 106 +++++
 Telegram/gyp/Telegram.gyp                     |   2 +
 Telegram/gyp/codegen_rules.gypi               |   2 +
 Telegram/gyp/telegram_sources.txt             |   4 +-
 13 files changed, 839 insertions(+), 507 deletions(-)
 delete mode 100644 Telegram/SourceFiles/chat_helpers/emoji_suggestions.cpp
 delete mode 100644 Telegram/SourceFiles/chat_helpers/emoji_suggestions.h
 create mode 100644 Telegram/ThirdParty/emoji_suggestions/emoji_suggestions.cpp
 create mode 100644 Telegram/ThirdParty/emoji_suggestions/emoji_suggestions.h

diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions.cpp
deleted file mode 100644
index 9918f2336..000000000
--- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions.cpp
+++ /dev/null
@@ -1,302 +0,0 @@
-/*
-This file is part of Telegram Desktop,
-the official desktop version of Telegram messaging app, see https://telegram.org
-
-Telegram Desktop is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-It is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-In addition, as a special exception, the copyright holders give permission
-to link the code of portions of this program with the OpenSSL library.
-
-Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
-Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
-*/
-#include "chat_helpers/emoji_suggestions.h"
-
-namespace Ui {
-namespace Emoji {
-namespace {
-
-class Completer {
-public:
-	Completer(const QString &query);
-
-	QVector<Suggestion> resolve();
-
-private:
-	struct Result {
-		gsl::not_null<const Replacement*> replacement;
-		int wordsUsed;
-	};
-
-	static QString NormalizeQuery(const QString &query);
-	void addResult(gsl::not_null<const Replacement*> replacement);
-	bool isDuplicateOfLastResult(gsl::not_null<const Replacement*> replacement) const;
-	bool isBetterThanLastResult(gsl::not_null<const Replacement*> replacement) const;
-	void processInitialList();
-	void filterInitialList();
-	void initWordsTracking();
-	bool matchQueryForCurrentItem();
-	bool matchQueryTailStartingFrom(int position);
-	gsl::span<const QString> findWordsStartingWith(QChar ch);
-	int findEqualCharsCount(int position, const QString *word);
-	QVector<Suggestion> prepareResult();
-
-	std::vector<Result> _result;
-
-	const QString _query;
-	const QChar *_queryBegin = nullptr;
-	int _querySize = 0;
-
-	const std::vector<gsl::not_null<const Replacement*>> *_initialList = nullptr;
-
-	gsl::span<const QString> _currentItemWords;
-	int _currentItemWordsUsedCount = 0;
-
-	class UsedWordGuard {
-	public:
-		UsedWordGuard(QVector<bool> &map, int index);
-		UsedWordGuard(const UsedWordGuard &other) = delete;
-		UsedWordGuard(UsedWordGuard &&other);
-		UsedWordGuard &operator=(const UsedWordGuard &other) = delete;
-		UsedWordGuard &operator=(UsedWordGuard &&other) = delete;
-		explicit operator bool() const;
-		~UsedWordGuard();
-
-	private:
-		QVector<bool> &_map;
-		int _index = 0;
-		bool _guarded = false;
-
-	};
-	QVector<bool> _currentItemWordsUsedMap;
-
-};
-
-Completer::UsedWordGuard::UsedWordGuard(QVector<bool> &map, int index) : _map(map), _index(index) {
-	Expects(_map.size() > _index);
-	if (!_map[_index]) {
-		_guarded = _map[_index] = true;
-	}
-}
-
-Completer::UsedWordGuard::UsedWordGuard(UsedWordGuard &&other) : _map(other._map), _index(other._index), _guarded(base::take(other._guarded)) {
-}
-
-Completer::UsedWordGuard::operator bool() const {
-	return _guarded;
-}
-
-Completer::UsedWordGuard::~UsedWordGuard() {
-	if (_guarded) {
-		_map[_index] = false;
-	}
-}
-
-Completer::Completer(const QString &query) : _query(NormalizeQuery(query)) {
-}
-
-// Remove all non-letters-or-numbers.
-// Leave '-' and '+' only if they're followed by a number or
-// at the end of the query (so it is possibly followed by a number).
-QString Completer::NormalizeQuery(const QString &query) {
-	auto result = query;
-	auto copyFrom = query.constData();
-	auto e = copyFrom + query.size();
-	auto copyTo = (QChar*)nullptr;
-	for (auto i = query.constData(); i != e; ++i) {
-		if (i->isLetterOrNumber()) {
-			continue;
-		} else if (*i == '-' || *i == '+') {
-			if (i + 1 == e || (i + 1)->isNumber()) {
-				continue;
-			}
-		}
-		if (i > copyFrom) {
-			if (!copyTo) copyTo = result.data();
-			memcpy(copyTo, copyFrom, (i - copyFrom) * sizeof(QChar));
-			copyTo += (i - copyFrom);
-		}
-		copyFrom = i + 1;
-	}
-	if (copyFrom == query.constData()) {
-		return query;
-	} else if (e > copyFrom) {
-		if (!copyTo) copyTo = result.data();
-		memcpy(copyTo, copyFrom, (e - copyFrom) * sizeof(QChar));
-		copyTo += (e - copyFrom);
-	}
-	result.chop(result.constData() + result.size() - copyTo);
-	return result;
-}
-
-QVector<Suggestion> Completer::resolve() {
-	_queryBegin = _query.constData();
-	_querySize = _query.size();
-	if (!_querySize) {
-		return QVector<Suggestion>();
-	}
-	_initialList = Ui::Emoji::GetReplacements(*_queryBegin);
-	if (!_initialList) {
-		return QVector<Suggestion>();
-	}
-	_result.reserve(_initialList->size());
-	processInitialList();
-	return prepareResult();
-}
-
-bool Completer::isDuplicateOfLastResult(gsl::not_null<const Replacement*> item) const {
-	if (_result.empty()) {
-		return false;
-	}
-	return (_result.back().replacement->id == item->id);
-}
-
-bool Completer::isBetterThanLastResult(gsl::not_null<const Replacement*> item) const {
-	Expects(!_result.empty());
-	auto &last = _result.back();
-	if (_currentItemWordsUsedCount < last.wordsUsed) {
-		return true;
-	}
-
-	auto firstCharOfQuery = _query[0];
-	auto firstCharAfterColonLast = last.replacement->replacement[1];
-	auto firstCharAfterColonCurrent = item->replacement[1];
-	auto goodLast = (firstCharAfterColonLast == firstCharOfQuery);
-	auto goodCurrent = (firstCharAfterColonCurrent == firstCharOfQuery);
-	return !goodLast && goodCurrent;
-}
-
-void Completer::addResult(gsl::not_null<const Replacement*> item) {
-	if (!isDuplicateOfLastResult(item)) {
-		_result.push_back({ item, _currentItemWordsUsedCount });
-	} else if (isBetterThanLastResult(item)) {
-		_result.back() = { item, _currentItemWordsUsedCount };
-	}
-}
-
-void Completer::processInitialList() {
-	if (_querySize > 1) {
-		filterInitialList();
-	} else {
-		_currentItemWordsUsedCount = 1;
-		for (auto item : *_initialList) {
-			addResult(item);
-		}
-	}
-}
-
-void Completer::initWordsTracking() {
-	auto maxWordsCount = 0;
-	for (auto item : *_initialList) {
-		accumulate_max(maxWordsCount, item->words.size());
-	}
-	_currentItemWordsUsedMap = QVector<bool>(maxWordsCount, false);
-}
-
-void Completer::filterInitialList() {
-	initWordsTracking();
-	for (auto item : *_initialList) {
-		_currentItemWords = gsl::make_span(item->words);
-		_currentItemWordsUsedCount = 1;
-		if (matchQueryForCurrentItem()) {
-			addResult(item);
-		}
-		_currentItemWordsUsedCount = 0;
-	}
-}
-
-bool Completer::matchQueryForCurrentItem() {
-	Expects(!_currentItemWords.empty());
-	if (_currentItemWords.size() < 2) {
-		return _currentItemWords.data()->startsWith(_query);
-	}
-	return matchQueryTailStartingFrom(0);
-}
-
-bool Completer::matchQueryTailStartingFrom(int position) {
-	auto charsLeftToMatch = (_querySize - position);
-	if (!charsLeftToMatch) {
-		return true;
-	}
-
-	auto firstCharToMatch = *(_queryBegin + position);
-	auto foundWords = findWordsStartingWith(firstCharToMatch);
-
-	for (auto word = foundWords.data(), foundWordsEnd = word + foundWords.size(); word != foundWordsEnd; ++word) {
-		auto wordIndex = word - _currentItemWords.data();
-		if (auto guard = UsedWordGuard(_currentItemWordsUsedMap, wordIndex)) {
-			++_currentItemWordsUsedCount;
-			auto equalCharsCount = findEqualCharsCount(position, word);
-			for (auto check = equalCharsCount; check != 0; --check) {
-				if (matchQueryTailStartingFrom(position + check)) {
-					return true;
-				}
-			}
-			--_currentItemWordsUsedCount;
-		}
-	}
-	return false;
-}
-
-int Completer::findEqualCharsCount(int position, const QString *word) {
-	auto charsLeft = (_querySize - position);
-	auto wordBegin = word->constData();
-	auto wordSize = word->size();
-	auto possibleEqualCharsCount = qMin(charsLeft, wordSize);
-	for (auto equalTill = 1; equalTill != possibleEqualCharsCount; ++equalTill) {
-		auto wordCh = *(wordBegin + equalTill);
-		auto queryCh = *(_queryBegin + position + equalTill);
-		if (wordCh != queryCh) {
-			return equalTill;
-		}
-	}
-	return possibleEqualCharsCount;
-}
-
-QVector<Suggestion> Completer::prepareResult() {
-	auto firstCharOfQuery = _query[0];
-	std::stable_partition(_result.begin(), _result.end(), [firstCharOfQuery](Result &result) {
-		auto firstCharAfterColon = result.replacement->replacement[1];
-		return (firstCharAfterColon == firstCharOfQuery);
-	});
-	std::stable_partition(_result.begin(), _result.end(), [](Result &result) {
-		return (result.wordsUsed < 2);
-	});
-	std::stable_partition(_result.begin(), _result.end(), [](Result &result) {
-		return (result.wordsUsed < 3);
-	});
-
-	auto result = QVector<Suggestion>();
-	result.reserve(_result.size());
-	for (auto &item : _result) {
-		result.push_back({ item.replacement->id, item.replacement->label, item.replacement->replacement });
-	}
-	return result;
-}
-
-gsl::span<const QString> Completer::findWordsStartingWith(QChar ch) {
-	auto begin = std::lower_bound(_currentItemWords.cbegin(), _currentItemWords.cend(), ch, [](const QString &word, QChar ch) {
-		return word[0] < ch;
-	});
-	auto end = std::upper_bound(_currentItemWords.cbegin(), _currentItemWords.cend(), ch, [](QChar ch, const QString &word) {
-		return ch < word[0];
-	});
-	return _currentItemWords.subspan(begin - _currentItemWords.cbegin(), end - begin);
-}
-
-} // namespace
-
-QVector<Suggestion> GetSuggestions(const QString &query) {
-	return Completer(query).resolve();
-}
-
-} // namespace Emoji
-} // namespace Ui
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions.h b/Telegram/SourceFiles/chat_helpers/emoji_suggestions.h
deleted file mode 100644
index c57b4c255..000000000
--- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions.h
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
-This file is part of Telegram Desktop,
-the official desktop version of Telegram messaging app, see https://telegram.org
-
-Telegram Desktop is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-It is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-In addition, as a special exception, the copyright holders give permission
-to link the code of portions of this program with the OpenSSL library.
-
-Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
-Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
-*/
-#pragma once
-
-namespace Ui {
-namespace Emoji {
-
-struct Suggestion {
-	QString id;
-	QString label;
-	QString replacement;
-};
-
-QVector<Suggestion> GetSuggestions(const QString &query);
-
-} // namespace Emoji
-} // namespace Ui
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp
index 47321959c..26e3cd770 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp
+++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp
@@ -20,6 +20,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 */
 #include "chat_helpers/emoji_suggestions_widget.h"
 
+#include "emoji_suggestions.h"
 #include "ui/effects/ripple_animation.h"
 #include "ui/widgets/shadow.h"
 #include "platform/platform_specific.h"
@@ -33,6 +34,14 @@ namespace {
 constexpr auto kRowLimit = 5;
 constexpr auto kLargestReplacementLength = 128;
 
+utf16string QStringToUTF16(const QString &string) {
+	return utf16string(reinterpret_cast<const utf16char*>(string.constData()), string.size());
+}
+
+QString QStringFromUTF16(utf16string string) {
+	return QString::fromRawData(reinterpret_cast<const QChar*>(string.data()), string.size());
+}
+
 } // namespace
 
 class SuggestionsWidget::Row {
@@ -93,9 +102,9 @@ void SuggestionsWidget::showWithQuery(const QString &query) {
 	_query = query;
 	if (!_query.isEmpty()) {
 		rows.reserve(kRowLimit);
-		for (auto &item : GetSuggestions(_query)) {
-			if (auto emoji = Find(item.id)) {
-				rows.emplace_back(emoji, item.label, item.replacement);
+		for (auto &item : GetSuggestions(QStringToUTF16(_query))) {
+			if (auto emoji = Find(QStringFromUTF16(item.emoji()))) {
+				rows.emplace_back(emoji, QStringFromUTF16(item.label()), QStringFromUTF16(item.replacement()));
 				if (rows.size() == kRowLimit) {
 					break;
 				}
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h
index 24500b4f5..23b82c437 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h
+++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h
@@ -20,7 +20,6 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 */
 #pragma once
 
-#include "chat_helpers/emoji_suggestions.h"
 #include "ui/effects/panel_animation.h"
 
 namespace Ui {
diff --git a/Telegram/SourceFiles/codegen/emoji/data.cpp b/Telegram/SourceFiles/codegen/emoji/data.cpp
index d71966d81..07557a0d3 100644
--- a/Telegram/SourceFiles/codegen/emoji/data.cpp
+++ b/Telegram/SourceFiles/codegen/emoji/data.cpp
@@ -49,16 +49,16 @@ Replace Replaces[] = {
 	{ { 0xD83DDE0AU }, ":-)" },
 	{ { 0xD83DDE0DU }, "8-)" },
 	{ { 0x2764U }, "<3" },
-	{ { 0xD83DDC8BU }, ":kiss:" },
-	{ { 0xD83DDE01U }, ":grin:" },
-	{ { 0xD83DDE02U }, ":joy:" },
+//	{ { 0xD83DDC8BU }, ":kiss:" },
+//	{ { 0xD83DDE01U }, ":grin:" },
+//	{ { 0xD83DDE02U }, ":joy:" },
 	{ { 0xD83DDE1AU }, ":-*" },
 	{ { 0xD83DDE06U }, "xD" },
-	{ { 0xD83DDC4DU }, ":like:" },
-	{ { 0xD83DDC4EU }, ":dislike:" },
-	{ { 0x261DU }, ":up:" },
-	{ { 0x270CU }, ":v:" },
-	{ { 0xD83DDC4CU }, ":ok:" },
+//	{ { 0xD83DDC4DU }, ":like:" },
+//	{ { 0xD83DDC4EU }, ":dislike:" },
+//	{ { 0x261DU }, ":up:" },
+//	{ { 0x270CU }, ":v:" },
+//	{ { 0xD83DDC4CU }, ":ok:" },
 	{ { 0xD83DDE0EU }, "B-)" },
 	{ { 0xD83DDE03U }, ":-D" },
 	{ { 0xD83DDE09U }, ";-)" },
diff --git a/Telegram/SourceFiles/codegen/emoji/generator.cpp b/Telegram/SourceFiles/codegen/emoji/generator.cpp
index 6ecaafe3b..55c0f3a1f 100644
--- a/Telegram/SourceFiles/codegen/emoji/generator.cpp
+++ b/Telegram/SourceFiles/codegen/emoji/generator.cpp
@@ -123,6 +123,45 @@ QRect computeSourceRect(const QImage &image) {
 	return result;
 }
 
+uint32 Crc32Table[256];
+class Crc32Initializer {
+public:
+	Crc32Initializer() {
+		uint32 poly = 0x04C11DB7U;
+		for (auto i = 0; i != 256; ++i) {
+			Crc32Table[i] = reflect(i, 8) << 24;
+			for (auto j = 0; j != 8; ++j) {
+				Crc32Table[i] = (Crc32Table[i] << 1) ^ (Crc32Table[i] & (1 << 31) ? poly : 0);
+			}
+			Crc32Table[i] = reflect(Crc32Table[i], 32);
+		}
+	}
+
+private:
+	uint32 reflect(uint32 val, char ch) {
+		uint32 result = 0;
+		for (int i = 1; i < (ch + 1); ++i) {
+			if (val & 1) {
+				result |= 1 << (ch - i);
+			}
+			val >>= 1;
+		}
+		return result;
+	}
+
+};
+
+uint32 countCrc32(const void *data, std::size_t size) {
+	static Crc32Initializer InitTable;
+
+	auto buffer = static_cast<const unsigned char*>(data);
+	auto result = uint32(0xFFFFFFFFU);
+	for (auto i = std::size_t(0); i != size; ++i) {
+		result = (result >> 8) ^ Crc32Table[(result & 0xFFU) ^ buffer[i]];
+	}
+	return (result ^ 0xFFFFFFFFU);
+}
+
 } // namespace
 
 Generator::Generator(const Options &options) : project_(Project)
@@ -142,6 +181,7 @@ Generator::Generator(const Options &options) : project_(Project)
 
 	outputPath_ = dir.absolutePath() + "/emoji";
 	spritePath_ = dir.absolutePath() + "/emoji";
+	suggestionsPath_ = dir.absolutePath() + "/emoji_suggestions_data";
 }
 
 int Generator::generate() {
@@ -161,6 +201,12 @@ int Generator::generate() {
 	if (!writeHeader()) {
 		return -1;
 	}
+	if (!writeSuggestionsSource()) {
+		return -1;
+	}
+	if (!writeSuggestionsHeader()) {
+		return -1;
+	}
 
 	return 0;
 }
@@ -272,6 +318,7 @@ bool Generator::writeImages() {
 bool Generator::writeSource() {
 	source_ = std::make_unique<common::CppFile>(outputPath_ + ".cpp", project_);
 
+	source_->include("emoji_suggestions_data.h").newline();
 	source_->pushNamespace("Ui").pushNamespace("Emoji").pushNamespace();
 	source_->stream() << "\
 \n\
@@ -283,9 +330,6 @@ std::vector<One> Items;\n\
 	if (!writeSections()) {
 		return false;
 	}
-	if (!writeReplacements()) {
-		return false;
-	}
 	if (!writeFindReplace()) {
 		return false;
 	}
@@ -329,9 +373,6 @@ void Init() {\n\
 	if (!writeGetSections()) {
 		return false;
 	}
-	if (!writeGetReplacements()) {
-		return false;
-	}
 
 	return source_->finalize();
 }
@@ -380,15 +421,6 @@ int Index();\n\
 \n\
 int GetSectionCount(Section section);\n\
 EmojiPack GetSection(Section section);\n\
-\n\
-struct Replacement {\n\
-	QString id;\n\
-	QString replacement;\n\
-	QString label;\n\
-	QVector<QString> words;\n\
-};\n\
-\n\
-const std::vector<gsl::not_null<const Replacement*>> *GetReplacements(QChar first);\n\
 \n";
 	return header->finalize();
 }
@@ -447,7 +479,7 @@ struct DataStruct {\n\
 const ushort IdData[] = {";
 	startBinary();
 	if (!enumerateWholeList([this](Id id, int column, int row, bool isPostfixed, bool isVariated, bool isColored, int original) {
-		return writeStringBinary(id);
+		return writeStringBinary(source_.get(), id);
 	})) {
 		return false;
 	}
@@ -490,7 +522,7 @@ const ushort SectionData[] = {";
 	startBinary();
 	for (auto &category : data_.categories) {
 		for (auto index : category) {
-			writeIntBinary(index);
+			writeIntBinary(source_.get(), index);
 		}
 	}
 	source_->stream() << " };\n\
@@ -506,115 +538,6 @@ EmojiPack FillSection(int offset, int size) {\n\
 	return true;
 }
 
-bool Generator::writeReplacements() {
-	QMap<QChar, QVector<int>> byCharIndices;
-	source_->stream() << "\
-struct ReplacementStruct {\n\
-	uchar idSize;\n\
-	uchar replacementSize;\n\
-	uchar wordsCount;\n\
-};\n\
-\n\
-const ushort ReplacementData[] = {";
-	startBinary();
-	for (auto i = 0, size = replaces_.list.size(); i != size; ++i) {
-		auto &replace = replaces_.list[i];
-		if (!writeStringBinary(replace.id)) {
-			return false;
-		}
-		if (!writeStringBinary(replace.replacement)) {
-			return false;
-		}
-		for (auto &word : replace.words) {
-			if (!writeStringBinary(word)) {
-				return false;
-			}
-			auto &index = byCharIndices[word[0]];
-			if (index.isEmpty() || index.back() != i) {
-				index.push_back(i);
-			}
-		}
-	}
-	source_->stream() << " };\n\
-\n\
-const uchar ReplacementWordLengths[] = {";
-	startBinary();
-	for (auto &replace : replaces_.list) {
-		auto wordLengths = QStringList();
-		for (auto &word : replace.words) {
-			writeIntBinary(word.size());
-		}
-	}
-	source_->stream() << " };\n\
-\n\
-const ReplacementStruct ReplacementInitData[] = {\n";
-	for (auto &replace : replaces_.list) {
-		source_->stream() << "\
-	{ uchar(" << replace.id.size() << "), uchar(" << replace.replacement.size() << "), uchar(" << replace.words.size() << ") },\n";
-	}
-	source_->stream() << "};\n\
-\n\
-const ushort ReplacementIndices[] = {";
-	startBinary();
-	for (auto &byCharIndex : byCharIndices) {
-		for (auto index : byCharIndex) {
-			writeIntBinary(index);
-		}
-	}
-	source_->stream() << " };\n\
-\n\
-struct ReplacementIndexStruct {\n\
-	ushort ch;\n\
-	ushort count;\n\
-};\n\
-\n\
-const ReplacementIndexStruct ReplacementIndexData[] = {\n";
-	startBinary();
-	for (auto i = byCharIndices.cbegin(), e = byCharIndices.cend(); i != e; ++i) {
-		source_->stream() << "\
-	{ ushort(" << i.key().unicode() << "), ushort(" << i.value().size() << ") },\n";
-	}
-	source_->stream() << "};\n\
-\n\
-std::vector<Replacement> Replacements;\n\
-std::map<ushort, std::vector<gsl::not_null<const Replacement*>>> ReplacementsMap;\n\
-\n\
-void InitReplacements() {\n\
-	auto data = ReplacementData;\n\
-	auto takeString = [&data](int size) {\n\
-		auto result = QString::fromRawData(reinterpret_cast<const QChar*>(data), size);\n\
-		data += size;\n\
-		return result;\n\
-	};\n\
-	auto wordSize = ReplacementWordLengths;\n\
-\n\
-	Replacements.reserve(base::array_size(ReplacementInitData));\n\
-	for (auto item : ReplacementInitData) {\n\
-		auto id = takeString(item.idSize);\n\
-		auto replacement = takeString(item.replacementSize);\n\
-		auto label = replacement;\n\
-		auto words = QVector<QString>();\n\
-		words.reserve(item.wordsCount);\n\
-		for (auto i = 0; i != item.wordsCount; ++i) {\n\
-			words.push_back(takeString(*wordSize++));\n\
-		}\n\
-		Replacements.push_back({ std::move(id), std::move(replacement), std::move(label), std::move(words) });\n\
-	}\n\
-\n\
-	auto indices = ReplacementIndices;\n\
-	auto items = &Replacements[0];\n\
-	for (auto item : ReplacementIndexData) {\n\
-		auto index = std::vector<gsl::not_null<const Replacement*>>();\n\
-		index.reserve(item.count);\n\
-		for (auto i = 0; i != item.count; ++i) {\n\
-			index.push_back(items + (*indices++));\n\
-		}\n\
-		ReplacementsMap.emplace(item.ch, std::move(index));\n\
-	}\n\
-}\n";
-	return true;
-}
-
 bool Generator::writeGetSections() {
 	constexpr const char *sectionNames[] = {
 		"Section::People",
@@ -833,12 +756,191 @@ bool Generator::writeFindFromDictionary(const std::map<QString, int, std::greate
 	return true;
 }
 
+bool Generator::writeSuggestionsSource() {
+	suggestionsSource_ = std::make_unique<common::CppFile>(suggestionsPath_ + ".cpp", project_);
+	suggestionsSource_->stream() << "\
+#include <map>\n\
+\n";
+	suggestionsSource_->pushNamespace("Ui").pushNamespace("Emoji").pushNamespace("internal").pushNamespace();
+	suggestionsSource_->stream() << "\
+\n";
+	if (!writeReplacements()) {
+		return false;
+	}
+	suggestionsSource_->popNamespace().newline();
+	if (!writeGetReplacements()) {
+		return false;
+	}
+
+	return suggestionsSource_->finalize();
+}
+
+bool Generator::writeSuggestionsHeader() {
+	auto maxLength = 0;
+	for (auto &replace : replaces_.list) {
+		if (maxLength < replace.replacement.size()) {
+			maxLength = replace.replacement.size();
+		}
+	}
+	auto header = std::make_unique<common::CppFile>(suggestionsPath_ + ".h", project_);
+	header->include("emoji_suggestions.h").newline();
+	header->pushNamespace("Ui").pushNamespace("Emoji").pushNamespace("internal");
+	header->stream() << "\
+\n\
+struct Replacement {\n\
+	utf16string emoji;\n\
+	utf16string replacement;\n\
+	std::vector<utf16string> words;\n\
+};\n\
+\n\
+constexpr auto kReplacementMaxLength = " << maxLength << ";\n\
+\n\
+void InitReplacements();\n\
+const std::vector<const Replacement*> *GetReplacements(utf16char first);\n\
+utf16string GetReplacementEmoji(utf16string replacement);\n\
+\n";
+	return header->finalize();
+}
+
+bool Generator::writeReplacements() {
+	QMap<QChar, QVector<int>> byCharIndices;
+	suggestionsSource_->stream() << "\
+struct ReplacementStruct {\n\
+	small emojiSize;\n\
+	small replacementSize;\n\
+	small wordsCount;\n\
+};\n\
+\n\
+const utf16char ReplacementData[] = {";
+	startBinary();
+	for (auto i = 0, size = replaces_.list.size(); i != size; ++i) {
+		auto &replace = replaces_.list[i];
+		if (!writeStringBinary(suggestionsSource_.get(), replace.id)) {
+			return false;
+		}
+		if (!writeStringBinary(suggestionsSource_.get(), replace.replacement)) {
+			return false;
+		}
+		for (auto &word : replace.words) {
+			if (!writeStringBinary(suggestionsSource_.get(), word)) {
+				return false;
+			}
+			auto &index = byCharIndices[word[0]];
+			if (index.isEmpty() || index.back() != i) {
+				index.push_back(i);
+			}
+		}
+	}
+	suggestionsSource_->stream() << " };\n\
+\n\
+const small ReplacementWordLengths[] = {";
+	startBinary();
+	for (auto &replace : replaces_.list) {
+		auto wordLengths = QStringList();
+		for (auto &word : replace.words) {
+			writeIntBinary(suggestionsSource_.get(), word.size());
+		}
+	}
+	suggestionsSource_->stream() << " };\n\
+\n\
+const ReplacementStruct ReplacementInitData[] = {\n";
+	for (auto &replace : replaces_.list) {
+		suggestionsSource_->stream() << "\
+	{ small(" << replace.id.size() << "), small(" << replace.replacement.size() << "), small(" << replace.words.size() << ") },\n";
+	}
+	suggestionsSource_->stream() << "};\n\
+\n\
+const medium ReplacementIndices[] = {";
+	startBinary();
+	for (auto &byCharIndex : byCharIndices) {
+		for (auto index : byCharIndex) {
+			writeIntBinary(suggestionsSource_.get(), index);
+		}
+	}
+	suggestionsSource_->stream() << " };\n\
+\n\
+struct ReplacementIndexStruct {\n\
+	utf16char ch;\n\
+	medium count;\n\
+};\n\
+\n\
+const internal::checksum ReplacementChecksums[] = {\n";
+	startBinary();
+	for (auto &replace : replaces_.list) {
+		writeUintBinary(suggestionsSource_.get(), countCrc32(replace.replacement.constData(), replace.replacement.size() * sizeof(QChar)));
+	}
+	suggestionsSource_->stream() << " };\n\
+\n\
+const ReplacementIndexStruct ReplacementIndexData[] = {\n";
+	startBinary();
+	for (auto i = byCharIndices.cbegin(), e = byCharIndices.cend(); i != e; ++i) {
+		suggestionsSource_->stream() << "\
+	{ utf16char(" << i.key().unicode() << "), medium(" << i.value().size() << ") },\n";
+	}
+	suggestionsSource_->stream() << "};\n\
+\n\
+std::vector<Replacement> Replacements;\n\
+std::map<utf16char, std::vector<const Replacement*>> ReplacementsMap;\n\
+std::map<internal::checksum, const Replacement*> ReplacementsHash;\n\
+\n";
+	return true;
+}
+
 bool Generator::writeGetReplacements() {
-	source_->stream() << "\
-const std::vector<gsl::not_null<const Replacement*>> *GetReplacements(QChar first) {\n\
-	auto it = ReplacementsMap.find(first.unicode());\n\
+	suggestionsSource_->stream() << "\
+void InitReplacements() {\n\
+	if (!Replacements.empty()) {\n\
+		return;\n\
+	}\n\
+	auto data = ReplacementData;\n\
+	auto takeString = [&data](int size) {\n\
+		auto result = utf16string(data, size);\n\
+		data += size;\n\
+		return result;\n\
+	};\n\
+	auto wordSize = ReplacementWordLengths;\n\
+\n\
+	Replacements.reserve(" << replaces_.list.size() << ");\n\
+	for (auto item : ReplacementInitData) {\n\
+		auto emoji = takeString(item.emojiSize);\n\
+		auto replacement = takeString(item.replacementSize);\n\
+		auto words = std::vector<utf16string>();\n\
+		words.reserve(item.wordsCount);\n\
+		for (auto i = 0; i != item.wordsCount; ++i) {\n\
+			words.push_back(takeString(*wordSize++));\n\
+		}\n\
+		Replacements.push_back({ std::move(emoji), std::move(replacement), std::move(words) });\n\
+	}\n\
+\n\
+	auto indices = ReplacementIndices;\n\
+	auto items = &Replacements[0];\n\
+	for (auto item : ReplacementIndexData) {\n\
+		auto index = std::vector<const Replacement*>();\n\
+		index.reserve(item.count);\n\
+		for (auto i = 0; i != item.count; ++i) {\n\
+			index.push_back(items + (*indices++));\n\
+		}\n\
+		ReplacementsMap.emplace(item.ch, std::move(index));\n\
+	}\n\
+\n\
+	for (auto checksum : ReplacementChecksums) {\n\
+		ReplacementsHash.emplace(checksum, items++);\n\
+	}\n\
+}\n\
+\n\
+const std::vector<const Replacement*> *GetReplacements(utf16char first) {\n\
+	if (ReplacementsMap.empty()) {\n\
+		InitReplacements();\n\
+	}\n\
+	auto it = ReplacementsMap.find(first);\n\
 	return (it == ReplacementsMap.cend()) ? nullptr : &it->second;\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\
+	return (it == ReplacementsHash.cend()) ? utf16string() : it->second->emoji;\n\
+}\n\
 \n";
 	return true;
 }
@@ -847,38 +949,52 @@ void Generator::startBinary() {
 	_binaryFullLength = _binaryCount = 0;
 }
 
-bool Generator::writeStringBinary(const QString &string) {
+bool Generator::writeStringBinary(common::CppFile *source, const QString &string) {
 	if (string.size() >= 256) {
 		logDataError() << "Too long string: " << string.toStdString();
 		return false;
 	}
 	for (auto ch : string) {
-		if (_binaryFullLength > 0) source_->stream() << ",";
+		if (_binaryFullLength > 0) source->stream() << ",";
 		if (!_binaryCount++) {
-			source_->stream() << "\n";
+			source->stream() << "\n";
 		} else {
 			if (_binaryCount == 12) {
 				_binaryCount = 0;
 			}
-			source_->stream() << " ";
+			source->stream() << " ";
 		}
-		source_->stream() << "0x" << QString::number(ch.unicode(), 16);
+		source->stream() << "0x" << QString::number(ch.unicode(), 16);
 		++_binaryFullLength;
 	}
 	return true;
 }
 
-void Generator::writeIntBinary(int data) {
-	if (_binaryFullLength > 0) source_->stream() << ",";
+void Generator::writeIntBinary(common::CppFile *source, int data) {
+	if (_binaryFullLength > 0) source->stream() << ",";
 	if (!_binaryCount++) {
-		source_->stream() << "\n";
+		source->stream() << "\n";
 	} else {
 		if (_binaryCount == 12) {
 			_binaryCount = 0;
 		}
-		source_->stream() << " ";
+		source->stream() << " ";
 	}
-	source_->stream() << data;
+	source->stream() << data;
+	++_binaryFullLength;
+}
+
+void Generator::writeUintBinary(common::CppFile *source, uint32 data) {
+	if (_binaryFullLength > 0) source->stream() << ",";
+	if (!_binaryCount++) {
+		source->stream() << "\n";
+	} else {
+		if (_binaryCount == 12) {
+			_binaryCount = 0;
+		}
+		source->stream() << " ";
+	}
+	source->stream() << "0x" << QString::number(data, 16).toUpper() << "U";
 	++_binaryFullLength;
 }
 
diff --git a/Telegram/SourceFiles/codegen/emoji/generator.h b/Telegram/SourceFiles/codegen/emoji/generator.h
index a963855f8..974ad998e 100644
--- a/Telegram/SourceFiles/codegen/emoji/generator.h
+++ b/Telegram/SourceFiles/codegen/emoji/generator.h
@@ -31,6 +31,8 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 namespace codegen {
 namespace emoji {
 
+using uint32 = unsigned int;
+
 class Generator {
 public:
 	Generator(const Options &options);
@@ -47,6 +49,8 @@ private:
 
 	bool writeSource();
 	bool writeHeader();
+	bool writeSuggestionsSource();
+	bool writeSuggestionsHeader();
 
 	template <typename Callback>
 	bool enumerateWholeList(Callback callback);
@@ -60,8 +64,9 @@ private:
 	bool writeFindFromDictionary(const std::map<QString, int, std::greater<QString>> &dictionary, bool skipPostfixes = false);
 	bool writeGetReplacements();
 	void startBinary();
-	bool writeStringBinary(const QString &string);
-	void writeIntBinary(int data);
+	bool writeStringBinary(common::CppFile *source, const QString &string);
+	void writeIntBinary(common::CppFile *source, int data);
+	void writeUintBinary(common::CppFile *source, uint32 data);
 
 	const common::ProjectInfo &project_;
 	int colorsCount_ = 0;
@@ -72,6 +77,9 @@ private:
 	QString spritePath_;
 	std::unique_ptr<common::CppFile> source_;
 	Data data_;
+
+	QString suggestionsPath_;
+	std::unique_ptr<common::CppFile> suggestionsSource_;
 	Replaces replaces_;
 
 	int _binaryFullLength = 0;
diff --git a/Telegram/SourceFiles/codegen/emoji/replaces.cpp b/Telegram/SourceFiles/codegen/emoji/replaces.cpp
index 2b43bb4ef..dedba9de1 100644
--- a/Telegram/SourceFiles/codegen/emoji/replaces.cpp
+++ b/Telegram/SourceFiles/codegen/emoji/replaces.cpp
@@ -281,9 +281,6 @@ QString ConvertEmojiId(const Id &id, const QString &replacement) {
 	if (NotSupported.contains(id)) {
 		return QString();
 	}
-	if (QRegularExpression("_tone").match(replacement).hasMatch()) {
-		int a = 0;
-	}
 	return ConvertMap.value(id, id);
 }
 
@@ -403,15 +400,20 @@ bool CheckAndConvertReplaces(Replaces &replaces, const Data &data) {
 	auto findId = [&data](const Id &id) {
 		return data.map.find(id) != data.map.cend();
 	};
-	auto findAndSort = [findId, &sorted](Id id, const Replace &replace) {
+	auto findAndSort = [findId, &data, &sorted](Id id, const Replace &replace) {
 		if (!findId(id)) {
 			id.replace(QChar(0xFE0F), QString());
 			if (!findId(id)) {
 				return false;
 			}
 		}
-		auto it = sorted.insertMulti(id, replace);
-		it.value().id = id;
+		auto it = data.map.find(id);
+		id = data.list[it->second].id;
+		if (data.list[it->second].postfixed) {
+			id += QChar(kPostfix);
+		}
+		auto inserted = sorted.insertMulti(id, replace);
+		inserted.value().id = id;
 		return true;
 	};
 
@@ -453,17 +455,10 @@ bool CheckAndConvertReplaces(Replaces &replaces, const Data &data) {
 	for (auto &category : data.categories) {
 		for (auto index : category) {
 			auto id = data.list[index].id;
-			auto found = false;
-			for (auto it = sorted.find(id); it != sorted.cend(); sorted.erase(it), it = sorted.find(id)) {
-				found = true;
-				result.list.push_back(it.value());
+			if (data.list[index].postfixed) {
+				id += QChar(kPostfix);
 			}
-			id.replace(QChar(0xFE0F), QString());
 			for (auto it = sorted.find(id); it != sorted.cend(); sorted.erase(it), it = sorted.find(id)) {
-				if (found) {
-					logReplacesError(replaces.filename) << "Strange emoji, found in both ways: " << it->replacement.toStdString();
-					return false;
-				}
 				result.list.push_back(it.value());
 			}
 		}
diff --git a/Telegram/ThirdParty/emoji_suggestions/emoji_suggestions.cpp b/Telegram/ThirdParty/emoji_suggestions/emoji_suggestions.cpp
new file mode 100644
index 000000000..b0b7d4021
--- /dev/null
+++ b/Telegram/ThirdParty/emoji_suggestions/emoji_suggestions.cpp
@@ -0,0 +1,432 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop version of Telegram messaging app, see https://telegram.org
+
+Telegram Desktop is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+It is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+In addition, as a special exception, the copyright holders give permission
+to link the code of portions of this program with the OpenSSL library.
+
+Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
+Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
+*/
+#include "emoji_suggestions.h"
+
+#include <algorithm>
+#include "emoji_suggestions_data.h"
+
+#ifndef Expects
+#include <cassert>
+#define Expects(condition) assert(condition)
+#endif // Expects
+
+namespace Ui {
+namespace Emoji {
+namespace internal {
+namespace {
+
+checksum Crc32Table[256];
+class Crc32Initializer {
+public:
+	Crc32Initializer() {
+		checksum poly = 0x04C11DB7U;
+		for (auto i = 0; i != 256; ++i) {
+			Crc32Table[i] = reflect(i, 8) << 24;
+			for (auto j = 0; j != 8; ++j) {
+				Crc32Table[i] = (Crc32Table[i] << 1) ^ (Crc32Table[i] & (1 << 31) ? poly : 0);
+			}
+			Crc32Table[i] = reflect(Crc32Table[i], 32);
+		}
+	}
+
+private:
+	checksum reflect(checksum val, char ch) {
+		checksum result = 0;
+		for (int i = 1; i < (ch + 1); ++i) {
+			if (val & 1) {
+				result |= 1 << (ch - i);
+			}
+			val >>= 1;
+		}
+		return result;
+	}
+
+};
+
+} // namespace
+
+checksum countChecksum(const void *data, std::size_t size) {
+	static Crc32Initializer InitTable;
+
+	auto buffer = static_cast<const unsigned char*>(data);
+	auto result = checksum(0xFFFFFFFFU);
+	for (auto i = std::size_t(0); i != size; ++i) {
+		result = (result >> 8) ^ Crc32Table[(result & 0xFFU) ^ buffer[i]];
+	}
+	return (result ^ 0xFFFFFFFFU);
+}
+
+} // namespace internal
+
+namespace {
+
+class string_span {
+public:
+	string_span() = default;
+	string_span(const utf16string *data, std::size_t size) : begin_(data), size_(size) {
+	}
+	string_span(const std::vector<utf16string> &data) : begin_(data.data()), size_(data.size()) {
+	}
+	string_span(const string_span &other) = default;
+	string_span &operator=(const string_span &other) = default;
+
+	const utf16string *begin() const {
+		return begin_;
+	}
+	const utf16string *end() const {
+		return begin_ + size_;
+	}
+	std::size_t size() const {
+		return size_;
+	}
+
+	string_span subspan(std::size_t offset, std::size_t size) {
+		return string_span(begin_ + offset, size);
+	}
+
+private:
+	const utf16string *begin_ = nullptr;
+	std::size_t size_ = 0;
+
+};
+
+bool IsNumber(utf16char ch) {
+	return (ch >= '0' && ch <= '9');
+}
+
+bool IsLetterOrNumber(utf16char ch) {
+	return (ch >= 'a' && ch <= 'z') || IsNumber(ch);
+}
+
+using Replacement = internal::Replacement;
+
+class Completer {
+public:
+	Completer(utf16string query);
+
+	std::vector<Suggestion> resolve();
+
+private:
+	struct Result {
+		const Replacement *replacement;
+		int wordsUsed;
+	};
+
+	static std::vector<utf16char> NormalizeQuery(utf16string query);
+	void addResult(const Replacement *replacement);
+	bool isDuplicateOfLastResult(const Replacement *replacement) const;
+	bool isBetterThanLastResult(const Replacement *replacement) const;
+	void processInitialList();
+	void filterInitialList();
+	void initWordsTracking();
+	bool matchQueryForCurrentItem();
+	bool matchQueryTailStartingFrom(int position);
+	string_span findWordsStartingWith(utf16char ch);
+	int findEqualCharsCount(int position, const utf16string *word);
+	std::vector<Suggestion> prepareResult();
+	bool startsWithQuery(utf16string word);
+	bool isExactMatch(utf16string replacement);
+
+	std::vector<Result> _result;
+
+	utf16string _initialQuery;
+	const std::vector<utf16char> _query;
+	const utf16char *_queryBegin = nullptr;
+	int _querySize = 0;
+
+	const std::vector<const Replacement*> *_initialList = nullptr;
+
+	string_span _currentItemWords;
+	int _currentItemWordsUsedCount = 0;
+
+	class UsedWordGuard {
+	public:
+		UsedWordGuard(std::vector<small> &map, int index);
+		UsedWordGuard(const UsedWordGuard &other) = delete;
+		UsedWordGuard(UsedWordGuard &&other);
+		UsedWordGuard &operator=(const UsedWordGuard &other) = delete;
+		UsedWordGuard &operator=(UsedWordGuard &&other) = delete;
+		explicit operator bool() const;
+		~UsedWordGuard();
+
+	private:
+		std::vector<small> &_map;
+		int _index = 0;
+		bool _guarded = false;
+
+	};
+	std::vector<small> _currentItemWordsUsedMap;
+
+};
+
+Completer::UsedWordGuard::UsedWordGuard(std::vector<small> &map, int index) : _map(map), _index(index) {
+	Expects(_map.size() > _index);
+	if (!_map[_index]) {
+		_guarded = _map[_index] = 1;
+	}
+}
+
+Completer::UsedWordGuard::UsedWordGuard(UsedWordGuard &&other) : _map(other._map), _index(other._index), _guarded(other._guarded) {
+	other._guarded = 0;
+}
+
+Completer::UsedWordGuard::operator bool() const {
+	return _guarded;
+}
+
+Completer::UsedWordGuard::~UsedWordGuard() {
+	if (_guarded) {
+		_map[_index] = 0;
+	}
+}
+
+Completer::Completer(utf16string query) : _initialQuery(query), _query(NormalizeQuery(query)) {
+}
+
+// Remove all non-letters-or-numbers.
+// Leave '-' and '+' only if they're followed by a number or
+// at the end of the query (so it is possibly followed by a number).
+std::vector<utf16char> Completer::NormalizeQuery(utf16string query) {
+	auto result = std::vector<utf16char>();
+	result.reserve(query.size());
+	auto copyFrom = query.data();
+	auto e = copyFrom + query.size();
+	auto copyTo = result.data();
+	for (auto i = query.data(); i != e; ++i) {
+		if (IsLetterOrNumber(*i)) {
+			continue;
+		} else if (*i == '-' || *i == '+') {
+			if (i + 1 == e || IsNumber(*(i + 1))) {
+				continue;
+			}
+		}
+		if (i > copyFrom) {
+			result.resize(result.size() + (i - copyFrom));
+			memcpy(copyTo, copyFrom, (i - copyFrom) * sizeof(utf16char));
+			copyTo += (i - copyFrom);
+		}
+		copyFrom = i + 1;
+	}
+	if (e > copyFrom) {
+		result.resize(result.size() + (e - copyFrom));
+		memcpy(copyTo, copyFrom, (e - copyFrom) * sizeof(utf16char));
+		copyTo += (e - copyFrom);
+	}
+	return result;
+}
+
+std::vector<Suggestion> Completer::resolve() {
+	_queryBegin = _query.data();
+	_querySize = _query.size();
+	if (!_querySize) {
+		return std::vector<Suggestion>();
+	}
+	_initialList = Ui::Emoji::internal::GetReplacements(*_queryBegin);
+	if (!_initialList) {
+		return std::vector<Suggestion>();
+	}
+	_result.reserve(_initialList->size());
+	processInitialList();
+	return prepareResult();
+}
+
+bool Completer::isDuplicateOfLastResult(const Replacement *item) const {
+	if (_result.empty()) {
+		return false;
+	}
+	return (_result.back().replacement->emoji == item->emoji);
+}
+
+bool Completer::isBetterThanLastResult(const Replacement *item) const {
+	Expects(!_result.empty());
+	auto &last = _result.back();
+	if (_currentItemWordsUsedCount < last.wordsUsed) {
+		return true;
+	}
+
+	auto firstCharOfQuery = _query[0];
+	auto firstCharAfterColonLast = last.replacement->replacement[1];
+	auto firstCharAfterColonCurrent = item->replacement[1];
+	auto goodLast = (firstCharAfterColonLast == firstCharOfQuery);
+	auto goodCurrent = (firstCharAfterColonCurrent == firstCharOfQuery);
+	return !goodLast && goodCurrent;
+}
+
+void Completer::addResult(const Replacement *item) {
+	if (!isDuplicateOfLastResult(item)) {
+		_result.push_back({ item, _currentItemWordsUsedCount });
+	} else if (isBetterThanLastResult(item)) {
+		_result.back() = { item, _currentItemWordsUsedCount };
+	}
+}
+
+void Completer::processInitialList() {
+	if (_querySize > 1) {
+		filterInitialList();
+	} else {
+		_currentItemWordsUsedCount = 1;
+		for (auto item : *_initialList) {
+			addResult(item);
+		}
+	}
+}
+
+void Completer::initWordsTracking() {
+	auto maxWordsCount = 0;
+	for (auto item : *_initialList) {
+		auto wordsCount = item->words.size();
+		if (maxWordsCount < wordsCount) {
+			maxWordsCount = wordsCount;
+		}
+	}
+	_currentItemWordsUsedMap = std::vector<small>(maxWordsCount, 0);
+}
+
+void Completer::filterInitialList() {
+	initWordsTracking();
+	for (auto item : *_initialList) {
+		_currentItemWords = string_span(item->words);
+		_currentItemWordsUsedCount = 1;
+		if (matchQueryForCurrentItem()) {
+			addResult(item);
+		}
+		_currentItemWordsUsedCount = 0;
+	}
+}
+
+bool Completer::matchQueryForCurrentItem() {
+	Expects(_currentItemWords.size() != 0);
+	if (_currentItemWords.size() < 2) {
+		return startsWithQuery(*_currentItemWords.begin());
+	}
+	return matchQueryTailStartingFrom(0);
+}
+
+bool Completer::startsWithQuery(utf16string word) {
+	if (word.size() < _query.size()) {
+		return false;
+	}
+	for (auto i = std::size_t(0), size = _query.size(); i != size; ++i) {
+		if (word[i] != _query[i]) {
+			return false;
+		}
+	}
+	return true;
+}
+
+bool Completer::isExactMatch(utf16string replacement) {
+	if (replacement.size() != _initialQuery.size() + 1) {
+		return false;
+	}
+	for (auto i = std::size_t(0), size = _initialQuery.size(); i != size; ++i) {
+		if (replacement[i] != _initialQuery[i]) {
+			return false;
+		}
+	}
+	return true;
+}
+
+bool Completer::matchQueryTailStartingFrom(int position) {
+	auto charsLeftToMatch = (_querySize - position);
+	if (!charsLeftToMatch) {
+		return true;
+	}
+
+	auto firstCharToMatch = *(_queryBegin + position);
+	auto foundWords = findWordsStartingWith(firstCharToMatch);
+
+	for (auto word = foundWords.begin(), foundWordsEnd = word + foundWords.size(); word != foundWordsEnd; ++word) {
+		auto wordIndex = word - _currentItemWords.begin();
+		if (auto guard = UsedWordGuard(_currentItemWordsUsedMap, wordIndex)) {
+			++_currentItemWordsUsedCount;
+			auto equalCharsCount = findEqualCharsCount(position, word);
+			for (auto check = equalCharsCount; check != 0; --check) {
+				if (matchQueryTailStartingFrom(position + check)) {
+					return true;
+				}
+			}
+			--_currentItemWordsUsedCount;
+		}
+	}
+	return false;
+}
+
+int Completer::findEqualCharsCount(int position, const utf16string *word) {
+	auto charsLeft = (_querySize - position);
+	auto wordBegin = word->data();
+	auto wordSize = word->size();
+	auto possibleEqualCharsCount = (charsLeft > wordSize ? wordSize : charsLeft);
+	for (auto equalTill = 1; equalTill != possibleEqualCharsCount; ++equalTill) {
+		auto wordCh = *(wordBegin + equalTill);
+		auto queryCh = *(_queryBegin + position + equalTill);
+		if (wordCh != queryCh) {
+			return equalTill;
+		}
+	}
+	return possibleEqualCharsCount;
+}
+
+std::vector<Suggestion> Completer::prepareResult() {
+	auto firstCharOfQuery = _query[0];
+	std::stable_partition(_result.begin(), _result.end(), [firstCharOfQuery](Result &result) {
+		auto firstCharAfterColon = result.replacement->replacement[1];
+		return (firstCharAfterColon == firstCharOfQuery);
+	});
+	std::stable_partition(_result.begin(), _result.end(), [](Result &result) {
+		return (result.wordsUsed < 2);
+	});
+	std::stable_partition(_result.begin(), _result.end(), [](Result &result) {
+		return (result.wordsUsed < 3);
+	});
+	std::stable_partition(_result.begin(), _result.end(), [this](Result &result) {
+		return isExactMatch(result.replacement->replacement);
+	});
+
+	auto result = std::vector<Suggestion>();
+	result.reserve(_result.size());
+	for (auto &item : _result) {
+		result.emplace_back(item.replacement->emoji, item.replacement->replacement, item.replacement->replacement);
+	}
+	return result;
+}
+
+string_span Completer::findWordsStartingWith(utf16char ch) {
+	auto begin = std::lower_bound(_currentItemWords.begin(), _currentItemWords.end(), ch, [](utf16string word, utf16char ch) {
+		return word[0] < ch;
+	});
+	auto end = std::upper_bound(_currentItemWords.begin(), _currentItemWords.end(), ch, [](utf16char ch, utf16string word) {
+		return ch < word[0];
+	});
+	return _currentItemWords.subspan(begin - _currentItemWords.begin(), end - begin);
+}
+
+} // namespace
+
+std::vector<Suggestion> GetSuggestions(utf16string query) {
+	return Completer(query).resolve();
+}
+
+int GetSuggestionMaxLength() {
+	return internal::kReplacementMaxLength;
+}
+
+} // namespace Emoji
+} // namespace Ui
diff --git a/Telegram/ThirdParty/emoji_suggestions/emoji_suggestions.h b/Telegram/ThirdParty/emoji_suggestions/emoji_suggestions.h
new file mode 100644
index 000000000..8a593d1d9
--- /dev/null
+++ b/Telegram/ThirdParty/emoji_suggestions/emoji_suggestions.h
@@ -0,0 +1,106 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop version of Telegram messaging app, see https://telegram.org
+
+Telegram Desktop is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+It is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+In addition, as a special exception, the copyright holders give permission
+to link the code of portions of this program with the OpenSSL library.
+
+Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
+Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
+*/
+#pragma once
+
+#include <vector>
+
+namespace Ui {
+namespace Emoji {
+
+using small = unsigned char;
+using medium = unsigned short;
+using utf16char = unsigned short;
+
+static_assert(sizeof(utf16char) == 2, "Bad UTF-16 character size.");
+
+class utf16string {
+public:
+	utf16string() = default;
+	utf16string(const utf16char *data, std::size_t size) : data_(data), size_(size) {
+	}
+	utf16string(const utf16string &other) = default;
+	utf16string &operator=(const utf16string &other) = default;
+
+	const utf16char *data() const {
+		return data_;
+	}
+	std::size_t size() const {
+		return size_;
+	}
+
+	utf16char operator[](int index) const {
+		return data_[index];
+	}
+
+private:
+	const utf16char *data_ = nullptr;
+	std::size_t size_ = 0;
+
+};
+
+inline bool operator==(utf16string a, utf16string b) {
+	return (a.size() == b.size()) && (!a.size() || !memcmp(a.data(), b.data(), a.size() * sizeof(utf16char)));
+}
+
+namespace internal {
+
+using checksum = unsigned int;
+checksum countChecksum(const void *data, std::size_t size);
+
+utf16string GetReplacementEmoji(utf16string replacement);
+
+} // namespace internal
+
+class Suggestion {
+public:
+	Suggestion() = default;
+	Suggestion(utf16string emoji, utf16string label, utf16string replacement) : emoji_(emoji), label_(label), replacement_(replacement) {
+	}
+	Suggestion(const Suggestion &other) = default;
+	Suggestion &operator=(const Suggestion &other) = default;
+
+	utf16string emoji() const {
+		return emoji_;
+	}
+	utf16string label() const {
+		return label_;
+	}
+	utf16string replacement() const {
+		return replacement_;
+	}
+
+private:
+	utf16string emoji_;
+	utf16string label_;
+	utf16string replacement_;
+
+};
+
+std::vector<Suggestion> GetSuggestions(utf16string query);
+
+inline utf16string GetSuggestionEmoji(utf16string replacement) {
+	return internal::GetReplacementEmoji(replacement);
+}
+
+int GetSuggestionMaxLength();
+
+} // namespace Emoji
+} // namespace Ui
diff --git a/Telegram/gyp/Telegram.gyp b/Telegram/gyp/Telegram.gyp
index 80160d161..9d92b817d 100644
--- a/Telegram/gyp/Telegram.gyp
+++ b/Telegram/gyp/Telegram.gyp
@@ -33,6 +33,7 @@
       'submodules_loc': '../ThirdParty',
       'minizip_loc': '<(submodules_loc)/minizip',
       'sp_media_key_tap_loc': '<(submodules_loc)/SPMediaKeyTap',
+      'emoji_suggestions_loc': '<(submodules_loc)/emoji_suggestions',
       'style_files': [
         '<(res_loc)/colors.palette',
         '<(res_loc)/basic.style',
@@ -101,6 +102,7 @@
       '<(libs_loc)/opus/include',
       '<(minizip_loc)',
       '<(sp_media_key_tap_loc)',
+      '<(emoji_suggestions_loc)',
       '<(submodules_loc)/GSL/include',
       '<(submodules_loc)/variant/include',
     ],
diff --git a/Telegram/gyp/codegen_rules.gypi b/Telegram/gyp/codegen_rules.gypi
index d56283401..b12a1439b 100644
--- a/Telegram/gyp/codegen_rules.gypi
+++ b/Telegram/gyp/codegen_rules.gypi
@@ -136,6 +136,8 @@
     'outputs': [
       '<(SHARED_INTERMEDIATE_DIR)/emoji.cpp',
       '<(SHARED_INTERMEDIATE_DIR)/emoji.h',
+      '<(SHARED_INTERMEDIATE_DIR)/emoji_suggestions_data.cpp',
+      '<(SHARED_INTERMEDIATE_DIR)/emoji_suggestions_data.h',
     ],
     'action': [
       '<(PRODUCT_DIR)/codegen_emoji<(exe_ext)',
diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt
index 00463d6fc..86dbbc666 100644
--- a/Telegram/gyp/telegram_sources.txt
+++ b/Telegram/gyp/telegram_sources.txt
@@ -98,8 +98,6 @@
 <(src_loc)/chat_helpers/bot_keyboard.h
 <(src_loc)/chat_helpers/emoji_list_widget.cpp
 <(src_loc)/chat_helpers/emoji_list_widget.h
-<(src_loc)/chat_helpers/emoji_suggestions.cpp
-<(src_loc)/chat_helpers/emoji_suggestions.h
 <(src_loc)/chat_helpers/emoji_suggestions_widget.cpp
 <(src_loc)/chat_helpers/emoji_suggestions_widget.h
 <(src_loc)/chat_helpers/field_autocomplete.cpp
@@ -581,6 +579,8 @@
 <(src_loc)/stdafx.h
 <(src_loc)/structs.cpp
 <(src_loc)/structs.h
+<(emoji_suggestions_loc)/emoji_suggestions.cpp
+<(emoji_suggestions_loc)/emoji_suggestions.h
 
 platforms: !win
 <(minizip_loc)/crypt.h