| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759 |
- /*
- This file is part of Telegram Desktop,
- the official desktop application for the Telegram messaging service.
- For license and copyright information please follow this link:
- https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
- */
- #include "chat_helpers/emoji_keywords.h"
- #include "emoji_suggestions_helper.h"
- #include "lang/lang_instance.h"
- #include "lang/lang_cloud_manager.h"
- #include "lang/lang_keys.h"
- #include "core/application.h"
- #include "base/platform/base_platform_info.h"
- #include "ui/emoji_config.h"
- #include "main/main_domain.h"
- #include "main/main_session.h"
- #include "apiwrap.h"
- #include "core/application.h"
- #include "core/core_settings.h"
- #include <QtGui/QGuiApplication>
- namespace ChatHelpers {
- namespace {
- constexpr auto kRefreshEach = 60 * 60 * crl::time(1000); // 1 hour.
- constexpr auto kKeepNotUsedLangPacksCount = 4;
- constexpr auto kKeepNotUsedInputLanguagesCount = 4;
- using namespace Ui::Emoji;
- using Result = EmojiKeywords::Result;
- struct LangPackEmoji {
- EmojiPtr emoji = nullptr;
- QString text;
- };
- struct LangPackData {
- int version = 0;
- int maxKeyLength = 0;
- std::map<QString, std::vector<LangPackEmoji>> emoji;
- };
- [[nodiscard]] bool MustAddPostfix(const QString &text) {
- if (text.size() != 1) {
- return false;
- }
- const auto code = text[0].unicode();
- return (code == 0x2122U) || (code == 0xA9U) || (code == 0xAEU);
- }
- [[nodiscard]] bool SkipExactKeyword(
- const QString &language,
- const QString &word) {
- if ((word.size() == 1) && !word[0].isLetter()) {
- return true;
- } else if (word == u"10"_q) {
- return true;
- } else if (language != u"en"_q) {
- return false;
- } else if ((word.size() == 1)
- && (word[0] != '$')
- && (word[0].unicode() != 8364)) { // Euro.
- return true;
- } else if ((word.size() == 2)
- && (word != u"us"_q)
- && (word != u"uk"_q)
- && (word != u"hi"_q)
- && (word != u"ok"_q)) {
- return true;
- }
- return false;
- }
- [[nodiscard]] EmojiPtr FindExact(const QString &text) {
- auto length = 0;
- const auto result = Find(text, &length);
- return (length < text.size()) ? nullptr : result;
- }
- void CreateCacheFilePath() {
- QDir().mkpath(internal::CacheFileFolder() + u"/keywords"_q);
- }
- [[nodiscard]] QString CacheFilePath(QString id) {
- static const auto BadSymbols = QRegularExpression("[^a-zA-Z0-9_\\.\\-]");
- id.replace(BadSymbols, QString());
- if (id.isEmpty()) {
- return QString();
- }
- return internal::CacheFileFolder() + u"/keywords/"_q + id;
- }
- [[nodiscard]] LangPackData ReadLocalCache(const QString &id) {
- auto file = QFile(CacheFilePath(id));
- if (!file.open(QIODevice::ReadOnly)) {
- return {};
- }
- auto result = LangPackData();
- auto stream = QDataStream(&file);
- stream.setVersion(QDataStream::Qt_5_1);
- auto version = qint32();
- auto count = qint32();
- stream
- >> version
- >> count;
- if (version < 0 || count < 0 || stream.status() != QDataStream::Ok) {
- return {};
- }
- for (auto i = 0; i != count; ++i) {
- auto key = QString();
- auto size = qint32();
- stream
- >> key
- >> size;
- if (size < 0 || stream.status() != QDataStream::Ok) {
- return {};
- }
- auto &list = result.emoji[key];
- for (auto j = 0; j != size; ++j) {
- auto text = QString();
- stream >> text;
- if (stream.status() != QDataStream::Ok) {
- return {};
- }
- const auto emoji = MustAddPostfix(text)
- ? (text + QChar(Ui::Emoji::kPostfix))
- : text;
- const auto entry = LangPackEmoji{ FindExact(emoji), text };
- if (!entry.emoji) {
- return {};
- }
- list.push_back(entry);
- }
- result.maxKeyLength = std::max(result.maxKeyLength, int(key.size()));
- }
- result.version = version;
- return result;
- }
- void WriteLocalCache(const QString &id, const LangPackData &data) {
- if (!data.version && data.emoji.empty()) {
- return;
- }
- CreateCacheFilePath();
- auto file = QFile(CacheFilePath(id));
- if (!file.open(QIODevice::WriteOnly)) {
- return;
- }
- auto stream = QDataStream(&file);
- stream.setVersion(QDataStream::Qt_5_1);
- stream
- << qint32(data.version)
- << qint32(data.emoji.size());
- for (const auto &[key, list] : data.emoji) {
- stream
- << key
- << qint32(list.size());
- for (const auto &emoji : list) {
- stream << emoji.text;
- }
- }
- }
- [[nodiscard]] QString NormalizeQuery(const QString &query) {
- return query.toLower();
- }
- [[nodiscard]] QString NormalizeKey(const QString &key) {
- return key.toLower().trimmed();
- }
- void AppendFoundEmoji(
- std::vector<Result> &result,
- const QString &label,
- const std::vector<LangPackEmoji> &list) {
- // It is important that the 'result' won't relocate while inserting.
- result.reserve(result.size() + list.size());
- const auto alreadyBegin = begin(result);
- const auto alreadyEnd = alreadyBegin + result.size();
- auto &&add = ranges::views::all(
- list
- ) | ranges::views::filter([&](const LangPackEmoji &entry) {
- const auto i = ranges::find(
- alreadyBegin,
- alreadyEnd,
- entry.emoji,
- &Result::emoji);
- return (i == alreadyEnd);
- }) | ranges::views::transform([&](const LangPackEmoji &entry) {
- return Result{ entry.emoji, label, entry.text };
- });
- result.insert(end(result), add.begin(), add.end());
- }
- void AppendLegacySuggestions(
- std::vector<Result> &result,
- const QString &query) {
- const auto badSuggestionChar = [](QChar ch) {
- return (ch < 'a' || ch > 'z')
- && (ch < 'A' || ch > 'Z')
- && (ch < '0' || ch > '9')
- && (ch != '_')
- && (ch != '-')
- && (ch != '+');
- };
- if (ranges::any_of(query, badSuggestionChar)) {
- return;
- }
- const auto suggestions = GetSuggestions(QStringToUTF16(query));
- // It is important that the 'result' won't relocate while inserting.
- result.reserve(result.size() + suggestions.size());
- const auto alreadyBegin = begin(result);
- const auto alreadyEnd = alreadyBegin + result.size();
- auto &&add = ranges::views::all(
- suggestions
- ) | ranges::views::transform([](const Suggestion &suggestion) {
- return Result{
- Find(QStringFromUTF16(suggestion.emoji())),
- QStringFromUTF16(suggestion.label()),
- QStringFromUTF16(suggestion.replacement())
- };
- }) | ranges::views::filter([&](const Result &entry) {
- const auto i = entry.emoji
- ? ranges::find(
- alreadyBegin,
- alreadyEnd,
- entry.emoji,
- &Result::emoji)
- : alreadyEnd;
- return (entry.emoji != nullptr)
- && (i == alreadyEnd);
- });
- result.insert(end(result), add.begin(), add.end());
- }
- void ApplyDifference(
- LangPackData &data,
- const QVector<MTPEmojiKeyword> &keywords,
- int version) {
- data.version = version;
- for (const auto &keyword : keywords) {
- keyword.match([&](const MTPDemojiKeyword &keyword) {
- const auto word = NormalizeKey(qs(keyword.vkeyword()));
- if (word.isEmpty()) {
- return;
- }
- auto &list = data.emoji[word];
- auto &&emoji = ranges::views::all(
- keyword.vemoticons().v
- ) | ranges::views::transform([](const MTPstring &string) {
- const auto text = qs(string);
- const auto emoji = MustAddPostfix(text)
- ? (text + QChar(Ui::Emoji::kPostfix))
- : text;
- return LangPackEmoji{ FindExact(emoji), text };
- }) | ranges::views::filter([&](const LangPackEmoji &entry) {
- if (!entry.emoji) {
- LOG(("API Warning: emoji %1 is not supported, word: %2."
- ).arg(
- entry.text,
- word));
- }
- return (entry.emoji != nullptr);
- });
- list.insert(end(list), emoji.begin(), emoji.end());
- }, [&](const MTPDemojiKeywordDeleted &keyword) {
- const auto word = NormalizeKey(qs(keyword.vkeyword()));
- if (word.isEmpty()) {
- return;
- }
- const auto i = data.emoji.find(word);
- if (i == end(data.emoji)) {
- return;
- }
- auto &list = i->second;
- for (const auto &emoji : keyword.vemoticons().v) {
- list.erase(
- ranges::remove(list, qs(emoji), &LangPackEmoji::text),
- end(list));
- }
- if (list.empty()) {
- data.emoji.erase(i);
- }
- });
- }
- if (data.emoji.empty()) {
- data.maxKeyLength = 0;
- } else {
- auto &&lengths = ranges::views::all(
- data.emoji
- ) | ranges::views::transform([](auto &&pair) {
- return pair.first.size();
- });
- data.maxKeyLength = *ranges::max_element(lengths);
- }
- }
- } // namespace
- class EmojiKeywords::LangPack final {
- public:
- using Delegate = details::EmojiKeywordsLangPackDelegate;
- LangPack(not_null<Delegate*> delegate, const QString &id);
- LangPack(const LangPack &other) = delete;
- LangPack &operator=(const LangPack &other) = delete;
- ~LangPack();
- [[nodiscard]] QString id() const;
- void refresh();
- void apiChanged();
- [[nodiscard]] std::vector<Result> query(
- const QString &normalized,
- bool exact) const;
- [[nodiscard]] int maxQueryLength() const;
- private:
- enum class State {
- ReadingCache,
- PendingRequest,
- Requested,
- Refreshed,
- };
- void readLocalCache();
- void applyDifference(const MTPEmojiKeywordsDifference &result);
- void applyData(LangPackData &&data);
- not_null<Delegate*> _delegate;
- QString _id;
- State _state = State::ReadingCache;
- LangPackData _data;
- crl::time _lastRefreshTime = 0;
- mtpRequestId _requestId = 0;
- base::binary_guard _guard;
- };
- EmojiKeywords::LangPack::LangPack(
- not_null<Delegate*> delegate,
- const QString &id)
- : _delegate(delegate)
- , _id(id) {
- readLocalCache();
- }
- EmojiKeywords::LangPack::~LangPack() {
- if (_requestId) {
- if (const auto api = _delegate->api()) {
- api->request(_requestId).cancel();
- }
- }
- }
- void EmojiKeywords::LangPack::readLocalCache() {
- const auto id = _id;
- auto callback = crl::guard(_guard.make_guard(), [=](
- LangPackData &&result) {
- applyData(std::move(result));
- refresh();
- });
- crl::async([id, callback = std::move(callback)]() mutable {
- crl::on_main([
- callback = std::move(callback),
- result = ReadLocalCache(id)
- ]() mutable {
- callback(std::move(result));
- });
- });
- }
- QString EmojiKeywords::LangPack::id() const {
- return _id;
- }
- void EmojiKeywords::LangPack::refresh() {
- if (_state != State::Refreshed) {
- return;
- } else if (_lastRefreshTime > 0
- && crl::now() - _lastRefreshTime < kRefreshEach) {
- return;
- }
- const auto api = _delegate->api();
- if (!api) {
- _state = State::PendingRequest;
- return;
- }
- _state = State::Requested;
- const auto send = [&](auto &&request) {
- return api->request(
- std::move(request)
- ).done([=](const MTPEmojiKeywordsDifference &result) {
- _requestId = 0;
- _lastRefreshTime = crl::now();
- applyDifference(result);
- }).fail([=] {
- _requestId = 0;
- _lastRefreshTime = crl::now();
- }).send();
- };
- _requestId = (_data.version > 0)
- ? send(MTPmessages_GetEmojiKeywordsDifference(
- MTP_string(_id),
- MTP_int(_data.version)))
- : send(MTPmessages_GetEmojiKeywords(
- MTP_string(_id)));
- }
- void EmojiKeywords::LangPack::applyDifference(
- const MTPEmojiKeywordsDifference &result) {
- result.match([&](const MTPDemojiKeywordsDifference &data) {
- const auto code = qs(data.vlang_code());
- const auto version = data.vversion().v;
- const auto &keywords = data.vkeywords().v;
- if (code != _id) {
- LOG(("API Error: Bad lang_code for emoji keywords %1 -> %2").arg(
- _id,
- code));
- _data.version = 0;
- _state = State::Refreshed;
- return;
- } else if (keywords.isEmpty() && _data.version >= version) {
- _state = State::Refreshed;
- return;
- }
- const auto id = _id;
- auto copy = _data;
- auto callback = crl::guard(_guard.make_guard(), [=](
- LangPackData &&result) {
- applyData(std::move(result));
- });
- crl::async([=,
- copy = std::move(copy),
- callback = std::move(callback)]() mutable {
- ApplyDifference(copy, keywords, version);
- WriteLocalCache(id, copy);
- crl::on_main([
- result = std::move(copy),
- callback = std::move(callback)
- ]() mutable {
- callback(std::move(result));
- });
- });
- });
- }
- void EmojiKeywords::LangPack::applyData(LangPackData &&data) {
- _data = std::move(data);
- _state = State::Refreshed;
- _delegate->langPackRefreshed();
- }
- void EmojiKeywords::LangPack::apiChanged() {
- if (_state == State::Requested && !_delegate->api()) {
- _requestId = 0;
- } else if (_state != State::PendingRequest) {
- return;
- }
- _state = State::Refreshed;
- refresh();
- }
- std::vector<Result> EmojiKeywords::LangPack::query(
- const QString &normalized,
- bool exact) const {
- if (normalized.size() > _data.maxKeyLength
- || _data.emoji.empty()
- || (exact && SkipExactKeyword(_id, normalized))) {
- return {};
- }
- const auto from = _data.emoji.lower_bound(normalized);
- auto &&chosen = ranges::make_subrange(
- from,
- end(_data.emoji)
- ) | ranges::views::take_while([&](const auto &pair) {
- const auto &key = pair.first;
- return exact ? (key == normalized) : key.startsWith(normalized);
- });
- auto result = std::vector<Result>();
- for (const auto &[key, list] : chosen) {
- AppendFoundEmoji(result, key, list);
- }
- return result;
- }
- int EmojiKeywords::LangPack::maxQueryLength() const {
- return _data.maxKeyLength;
- }
- EmojiKeywords::EmojiKeywords() {
- crl::on_main(&_guard, [=] {
- handleSessionChanges();
- });
- }
- EmojiKeywords::~EmojiKeywords() = default;
- not_null<details::EmojiKeywordsLangPackDelegate*> EmojiKeywords::delegate() {
- return static_cast<details::EmojiKeywordsLangPackDelegate*>(this);
- }
- ApiWrap *EmojiKeywords::api() {
- return _api;
- }
- void EmojiKeywords::langPackRefreshed() {
- _refreshed.fire({});
- }
- void EmojiKeywords::handleSessionChanges() {
- Core::App().domain().activeSessionValue( // #TODO multi someSessionValue
- ) | rpl::map([](Main::Session *session) {
- return session ? &session->api() : nullptr;
- }) | rpl::start_with_next([=](ApiWrap *api) {
- apiChanged(api);
- }, _lifetime);
- }
- void EmojiKeywords::apiChanged(ApiWrap *api) {
- _api = api;
- if (_api) {
- crl::on_main(&_api->session(), crl::guard(&_guard, [=] {
- Lang::CurrentCloudManager().firstLanguageSuggestion(
- ) | rpl::filter([=] {
- // Refresh with the suggested language if we already were asked.
- return !_data.empty();
- }) | rpl::start_with_next([=] {
- refresh();
- }, _suggestedChangeLifetime);
- }));
- } else {
- _langsRequestId = 0;
- _suggestedChangeLifetime.destroy();
- }
- for (const auto &[language, item] : _data) {
- item->apiChanged();
- }
- }
- void EmojiKeywords::refresh() {
- auto list = languages();
- if (_localList != list) {
- _localList = std::move(list);
- refreshRemoteList();
- } else {
- refreshFromRemoteList();
- }
- }
- std::vector<QString> EmojiKeywords::languages() {
- if (!_api) {
- return {};
- }
- refreshInputLanguages();
- auto result = std::vector<QString>();
- const auto yield = [&](const QString &language) {
- result.push_back(language);
- };
- const auto yieldList = [&](const QStringList &list) {
- result.insert(end(result), list.begin(), list.end());
- };
- yield(Lang::Id());
- yield(Lang::DefaultLanguageId());
- yield(Lang::CurrentCloudManager().suggestedLanguage());
- yield(Platform::SystemLanguage());
- yieldList(QLocale::system().uiLanguages());
- for (const auto &list : _inputLanguages) {
- yieldList(list);
- }
- ranges::sort(result);
- return result;
- }
- void EmojiKeywords::refreshInputLanguages() {
- const auto method = QGuiApplication::inputMethod();
- if (!method) {
- return;
- }
- const auto list = method->locale().uiLanguages();
- const auto i = ranges::find(_inputLanguages, list);
- if (i != end(_inputLanguages)) {
- std::rotate(i, i + 1, end(_inputLanguages));
- } else {
- if (_inputLanguages.size() >= kKeepNotUsedInputLanguagesCount) {
- _inputLanguages.pop_front();
- }
- _inputLanguages.push_back(list);
- }
- }
- rpl::producer<> EmojiKeywords::refreshed() const {
- return _refreshed.events();
- }
- std::vector<Result> EmojiKeywords::query(
- const QString &query,
- bool exact) const {
- const auto normalized = NormalizeQuery(query);
- if (normalized.isEmpty()) {
- return {};
- }
- auto result = std::vector<Result>();
- for (const auto &[language, item] : _data) {
- const auto list = item->query(normalized, exact);
- // It is important that the 'result' won't relocate while inserting.
- result.reserve(result.size() + list.size());
- const auto alreadyBegin = begin(result);
- const auto alreadyEnd = alreadyBegin + result.size();
- auto &&add = ranges::views::all(
- list
- ) | ranges::views::filter([&](Result entry) {
- // In each item->query() result the list has no duplicates.
- // So we need to check only for duplicates between queries.
- const auto i = ranges::find(
- alreadyBegin,
- alreadyEnd,
- entry.emoji,
- &Result::emoji);
- return (i == alreadyEnd);
- });
- result.insert(end(result), add.begin(), add.end());
- }
- if (!exact) {
- AppendLegacySuggestions(result, query);
- }
- return result;
- }
- std::vector<Result> EmojiKeywords::queryMine(
- const QString &query,
- bool exact) const {
- return ApplyVariants(PrioritizeRecent(this->query(query, exact)));
- }
- std::vector<Result> EmojiKeywords::PrioritizeRecent(
- std::vector<Result> list) {
- using Entry = Result;
- auto lastRecent = begin(list);
- const auto &recent = Core::App().settings().recentEmoji();
- for (const auto &item : recent) {
- const auto emoji = std::get_if<EmojiPtr>(&item.id.data);
- if (!emoji) {
- continue;
- }
- const auto original = (*emoji)->original()
- ? (*emoji)->original()
- : (*emoji);
- const auto it = ranges::find(list, original, [](const Entry &entry) {
- return entry.emoji;
- });
- if (it > lastRecent && it != end(list)) {
- std::rotate(lastRecent, it, it + 1);
- ++lastRecent;
- }
- }
- return list;
- }
- std::vector<Result> EmojiKeywords::ApplyVariants(std::vector<Result> list) {
- auto &settings = Core::App().settings();
- for (auto &item : list) {
- item.emoji = settings.lookupEmojiVariant(item.emoji);
- }
- return list;
- }
- int EmojiKeywords::maxQueryLength() const {
- if (_data.empty()) {
- return 0;
- }
- auto &&lengths = _data | ranges::views::transform([](const auto &pair) {
- return pair.second->maxQueryLength();
- });
- return *ranges::max_element(lengths);
- }
- void EmojiKeywords::refreshRemoteList() {
- if (!_api) {
- _localList.clear();
- setRemoteList({});
- return;
- }
- _api->request(base::take(_langsRequestId)).cancel();
- auto languages = QVector<MTPstring>();
- for (const auto &id : _localList) {
- languages.push_back(MTP_string(id));
- }
- _langsRequestId = _api->request(MTPmessages_GetEmojiKeywordsLanguages(
- MTP_vector<MTPstring>(languages)
- )).done([=](const MTPVector<MTPEmojiLanguage> &result) {
- setRemoteList(ranges::views::all(
- result.v
- ) | ranges::views::transform([](const MTPEmojiLanguage &language) {
- return language.match([&](const MTPDemojiLanguage &language) {
- return qs(language.vlang_code());
- });
- }) | ranges::to_vector);
- _langsRequestId = 0;
- }).fail([=] {
- _langsRequestId = 0;
- }).send();
- }
- void EmojiKeywords::setRemoteList(std::vector<QString> &&list) {
- if (_remoteList == list) {
- return;
- }
- _remoteList = std::move(list);
- for (auto i = begin(_data); i != end(_data);) {
- if (ranges::find(_remoteList, i->first) != end(_remoteList)) {
- ++i;
- } else {
- if (_notUsedData.size() >= kKeepNotUsedLangPacksCount) {
- _notUsedData.pop_front();
- }
- _notUsedData.push_back(std::move(i->second));
- i = _data.erase(i);
- }
- }
- refreshFromRemoteList();
- }
- void EmojiKeywords::refreshFromRemoteList() {
- for (const auto &id : _remoteList) {
- if (const auto i = _data.find(id); i != end(_data)) {
- i->second->refresh();
- continue;
- }
- const auto i = ranges::find(
- _notUsedData,
- id,
- [](const std::unique_ptr<LangPack> &p) { return p->id(); });
- if (i != end(_notUsedData)) {
- _data.emplace(id, std::move(*i));
- _notUsedData.erase(i);
- } else {
- _data.emplace(
- id,
- std::make_unique<LangPack>(delegate(), id));
- }
- }
- }
- } // namespace ChatHelpers
|