Provide emoji suggestions as a service.

This commit is contained in:
John Preston 2017-07-24 19:18:39 +03:00
parent 2dec9c46a7
commit df2aeb0c58
13 changed files with 839 additions and 507 deletions

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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 {

View file

@ -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 }, ";-)" },

View file

@ -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;
}

View file

@ -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;

View file

@ -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());
}
}

View file

@ -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

View file

@ -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

View file

@ -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',
],

View file

@ -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)',

View file

@ -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