mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-07-25 15:04:57 +00:00
667 lines
17 KiB
C++
667 lines
17 KiB
C++
/*
|
|
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/spellchecker_common.h"
|
|
|
|
#ifndef TDESKTOP_DISABLE_SPELLCHECK
|
|
|
|
#include "base/platform/base_platform_info.h"
|
|
#include "base/weak_ptr.h"
|
|
#include "base/zlib_help.h"
|
|
#include "data/data_session.h"
|
|
#include "lang/lang_instance.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "main/main_account.h"
|
|
#include "main/main_domain.h"
|
|
#include "main/main_session.h"
|
|
#include "mainwidget.h"
|
|
#include "mtproto/dedicated_file_loader.h"
|
|
#include "spellcheck/platform/platform_spellcheck.h"
|
|
#include "spellcheck/spellcheck_utils.h"
|
|
#include "spellcheck/spellcheck_value.h"
|
|
#include "core/application.h"
|
|
#include "core/core_settings.h"
|
|
|
|
#include <QtCore/QJsonArray>
|
|
#include <QtCore/QJsonDocument>
|
|
#include <QtCore/QJsonObject>
|
|
#include <QtGui/QGuiApplication>
|
|
#include <QtGui/QInputMethod>
|
|
|
|
namespace Spellchecker {
|
|
|
|
namespace {
|
|
|
|
using namespace Storage::CloudBlob;
|
|
|
|
constexpr auto kDictExtensions = { "dic", "aff" };
|
|
|
|
constexpr auto kExceptions = {
|
|
AppFile,
|
|
"\xd0\xa2\xd0\xb5\xd0\xbb\xd0\xb5\xd0\xb3\xd1\x80\xd0\xb0\xd0\xbc"_cs,
|
|
};
|
|
|
|
constexpr auto kLangsForLWC = { QLocale::English, QLocale::Portuguese };
|
|
constexpr auto kDefaultCountries = { QLocale::UnitedStates, QLocale::Brazil };
|
|
|
|
// Language With Country.
|
|
inline auto LWC(QLocale::Language language, QLocale::Country country) {
|
|
if (ranges::contains(kDefaultCountries, country)) {
|
|
return int(language);
|
|
}
|
|
return (language * 1000) + country;
|
|
}
|
|
|
|
inline auto LanguageFromLocale(QLocale loc) {
|
|
const auto locLang = loc.language();
|
|
return (ranges::contains(kLangsForLWC, locLang)
|
|
&& (loc.country() != QLocale::AnyCountry))
|
|
? LWC(locLang, loc.country())
|
|
: int(locLang);
|
|
}
|
|
|
|
constexpr auto kDictionariesManifestChannel = "tdhbcfiles"_cs;
|
|
constexpr auto kDictionariesManifestPostId = 2949;
|
|
|
|
// Runtime-loaded dictionaries manifest. Kept in memory only: fetched from
|
|
// the pinned JSON post the first time something actually needs it
|
|
// (Manage Dictionaries, auto-download). Nothing persists to disk, so an
|
|
// install whose enabled dictionaries are all on disk never hits the net.
|
|
std::vector<Dict> DictionariesList;
|
|
rpl::event_stream<> DictionariesListChanged;
|
|
|
|
void EnsurePath();
|
|
|
|
bool ParseLocation(const QString &text, QString &channel, int &postId) {
|
|
const auto sep = text.indexOf('#');
|
|
if (sep <= 0 || sep == text.size() - 1) {
|
|
return false;
|
|
}
|
|
auto ok = false;
|
|
const auto parsed = text.mid(sep + 1).toInt(&ok);
|
|
if (!ok || parsed <= 0) {
|
|
return false;
|
|
}
|
|
channel = text.left(sep);
|
|
postId = parsed;
|
|
return true;
|
|
}
|
|
|
|
std::vector<Dict> ParseManifest(const QByteArray &bytes) {
|
|
auto result = std::vector<Dict>();
|
|
auto err = QJsonParseError();
|
|
const auto doc = QJsonDocument::fromJson(bytes, &err);
|
|
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
|
|
LOG(("Spellcheck Error: manifest JSON parse failed: %1"
|
|
).arg(err.errorString()));
|
|
return result;
|
|
}
|
|
const auto list = doc.object().value(u"dictionaries"_q).toArray();
|
|
result.reserve(list.size());
|
|
for (const auto &v : list) {
|
|
const auto obj = v.toObject();
|
|
auto d = Dict();
|
|
d.id = obj.value(u"id"_q).toInt();
|
|
d.size = int64(obj.value(u"size"_q).toDouble());
|
|
d.name = obj.value(u"name"_q).toString();
|
|
const auto location = obj.value(u"location"_q).toString();
|
|
if (!d.id
|
|
|| d.name.isEmpty()
|
|
|| !ParseLocation(location, d.channel, d.postId)) {
|
|
continue;
|
|
}
|
|
result.push_back(std::move(d));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
class DictManifestLoader final : public base::has_weak_ptr {
|
|
public:
|
|
explicit DictManifestLoader(base::weak_ptr<Main::Session> session);
|
|
|
|
void start();
|
|
|
|
private:
|
|
void resolved(const MTPInputChannel &channel);
|
|
void received(const MTPmessages_Messages &result);
|
|
void apply(const QByteArray &bytes);
|
|
void finish();
|
|
|
|
MTP::WeakInstance _mtp;
|
|
|
|
};
|
|
|
|
std::shared_ptr<DictManifestLoader> ActiveManifestLoader;
|
|
|
|
DictManifestLoader::DictManifestLoader(base::weak_ptr<Main::Session> session)
|
|
: _mtp(session) {
|
|
}
|
|
|
|
void DictManifestLoader::start() {
|
|
if (!_mtp.valid()) {
|
|
finish();
|
|
return;
|
|
}
|
|
const auto weak = base::make_weak(this);
|
|
MTP::ResolveChannel(&_mtp, kDictionariesManifestChannel.utf16(), [=](
|
|
const MTPInputChannel &channel) {
|
|
if (const auto strong = weak.get()) {
|
|
strong->resolved(channel);
|
|
}
|
|
}, [=] {
|
|
if (const auto strong = weak.get()) {
|
|
strong->finish();
|
|
}
|
|
});
|
|
}
|
|
|
|
void DictManifestLoader::resolved(const MTPInputChannel &channel) {
|
|
const auto weak = base::make_weak(this);
|
|
_mtp.send(
|
|
MTPchannels_GetMessages(
|
|
channel,
|
|
MTP_vector<MTPInputMessage>(1,
|
|
MTP_inputMessageID(
|
|
MTP_int(kDictionariesManifestPostId)))),
|
|
[=](const MTPmessages_Messages &result) {
|
|
if (const auto strong = weak.get()) {
|
|
strong->received(result);
|
|
}
|
|
},
|
|
[=](const MTP::Error &) {
|
|
if (const auto strong = weak.get()) {
|
|
strong->finish();
|
|
}
|
|
});
|
|
}
|
|
|
|
void DictManifestLoader::received(const MTPmessages_Messages &result) {
|
|
const auto message = MTP::GetMessagesElement(result);
|
|
if (!message || message->type() != mtpc_message) {
|
|
LOG(("Spellcheck Error: manifest message not found."));
|
|
finish();
|
|
return;
|
|
}
|
|
apply(message->c_message().vmessage().v);
|
|
finish();
|
|
}
|
|
|
|
void DictManifestLoader::apply(const QByteArray &bytes) {
|
|
auto parsed = ParseManifest(bytes);
|
|
if (parsed.empty()) {
|
|
LOG(("Spellcheck Error: manifest empty or unparseable."));
|
|
return;
|
|
}
|
|
DictionariesList = std::move(parsed);
|
|
DictionariesListChanged.fire({});
|
|
}
|
|
|
|
void DictManifestLoader::finish() {
|
|
crl::on_main([] {
|
|
ActiveManifestLoader.reset();
|
|
});
|
|
}
|
|
|
|
void StartManifestRefresh(not_null<Main::Session*> session) {
|
|
if (ActiveManifestLoader) {
|
|
return;
|
|
}
|
|
ActiveManifestLoader = std::make_shared<DictManifestLoader>(
|
|
base::make_weak(session));
|
|
ActiveManifestLoader->start();
|
|
}
|
|
|
|
// Callers waiting for the first manifest arrival in this session.
|
|
// We don't cache manifest to disk, so each cold start begins empty;
|
|
// queued callbacks fire once when the MTP fetch lands.
|
|
std::vector<Fn<void()>> ManifestPending;
|
|
rpl::lifetime ManifestPendingSubscription;
|
|
|
|
void EnsureManifestThen(
|
|
not_null<Main::Session*> session,
|
|
Fn<void()> callback) {
|
|
if (!DictionariesList.empty()) {
|
|
callback();
|
|
return;
|
|
}
|
|
const auto firstCaller = ManifestPending.empty();
|
|
ManifestPending.push_back(std::move(callback));
|
|
if (firstCaller) {
|
|
DictionariesListChanged.events(
|
|
) | rpl::take(1) | rpl::on_next([] {
|
|
auto fns = std::move(ManifestPending);
|
|
ManifestPending.clear();
|
|
for (auto &fn : fns) {
|
|
fn();
|
|
}
|
|
}, ManifestPendingSubscription);
|
|
}
|
|
StartManifestRefresh(session);
|
|
}
|
|
|
|
inline auto IsSupportedLang(int lang) {
|
|
return ranges::contains(DictionariesList, lang, &Dict::id);
|
|
}
|
|
|
|
void EnsurePath() {
|
|
if (!QDir::current().mkpath(Spellchecker::DictionariesPath())) {
|
|
LOG(("App Error: Could not create dictionaries path."));
|
|
}
|
|
}
|
|
|
|
bool IsGoodPartName(const QString &name) {
|
|
return ranges::any_of(kDictExtensions, [&](const auto &ext) {
|
|
return name.endsWith(ext);
|
|
});
|
|
}
|
|
|
|
using DictLoaderPtr = std::shared_ptr<base::unique_qptr<DictLoader>>;
|
|
|
|
DictLoaderPtr BackgroundLoader;
|
|
rpl::event_stream<int> BackgroundLoaderChanged;
|
|
|
|
void SetBackgroundLoader(DictLoaderPtr loader) {
|
|
BackgroundLoader = std::move(loader);
|
|
}
|
|
|
|
void DownloadDictionaryInBackground(
|
|
not_null<Main::Session*> session,
|
|
int counter,
|
|
std::vector<int> langs) {
|
|
if (counter >= langs.size()) {
|
|
return;
|
|
}
|
|
const auto id = langs[counter];
|
|
counter++;
|
|
const auto destroyer = [=] {
|
|
BackgroundLoader = nullptr;
|
|
BackgroundLoaderChanged.fire(0);
|
|
|
|
if (DictionaryExists(id)) {
|
|
auto dicts = Core::App().settings().dictionariesEnabled();
|
|
if (ranges::contains(dicts, id)) {
|
|
Platform::Spellchecker::UpdateLanguages(dicts);
|
|
} else {
|
|
dicts.push_back(id);
|
|
Core::App().settings().setDictionariesEnabled(
|
|
std::move(dicts));
|
|
Core::App().saveSettingsDelayed();
|
|
}
|
|
}
|
|
|
|
DownloadDictionaryInBackground(session, counter, langs);
|
|
};
|
|
if (DictionaryExists(id)) {
|
|
destroyer();
|
|
return;
|
|
}
|
|
|
|
auto sharedLoader = std::make_shared<base::unique_qptr<DictLoader>>();
|
|
*sharedLoader = base::make_unique_q<DictLoader>(
|
|
QCoreApplication::instance(),
|
|
session,
|
|
id,
|
|
GetDownloadLocation(id),
|
|
DictPathByLangId(id),
|
|
GetDownloadSize(id),
|
|
crl::guard(session, destroyer));
|
|
SetBackgroundLoader(std::move(sharedLoader));
|
|
BackgroundLoaderChanged.fire_copy(id);
|
|
}
|
|
|
|
void AddExceptions() {
|
|
const auto exceptions = ranges::views::all(
|
|
kExceptions
|
|
) | ranges::views::transform([](const auto &word) {
|
|
return word.utf16();
|
|
}) | ranges::views::filter([](const auto &word) {
|
|
return !(Platform::Spellchecker::IsWordInDictionary(word)
|
|
|| Spellchecker::IsWordSkippable(word));
|
|
}) | ranges::to_vector;
|
|
|
|
ranges::for_each(exceptions, Platform::Spellchecker::AddWord);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
DictLoaderPtr GlobalLoader() {
|
|
return BackgroundLoader;
|
|
}
|
|
|
|
rpl::producer<int> GlobalLoaderChanged() {
|
|
return BackgroundLoaderChanged.events();
|
|
}
|
|
|
|
DictLoader::DictLoader(
|
|
QObject *parent,
|
|
not_null<Main::Session*> session,
|
|
int id,
|
|
MTP::DedicatedLoader::Location location,
|
|
const QString &folder,
|
|
int64 size,
|
|
Fn<void()> destroyCallback)
|
|
: BlobLoader(parent, session, id, location, folder, size)
|
|
, _destroyCallback(std::move(destroyCallback)) {
|
|
}
|
|
|
|
void DictLoader::unpack(const QString &path) {
|
|
crl::async([=] {
|
|
const auto success = Spellchecker::UnpackDictionary(path, id());
|
|
if (success) {
|
|
QFile(path).remove();
|
|
destroy();
|
|
return;
|
|
}
|
|
crl::on_main([=] { fail(); });
|
|
});
|
|
}
|
|
|
|
void DictLoader::destroy() {
|
|
Expects(_destroyCallback);
|
|
|
|
crl::on_main(_destroyCallback);
|
|
}
|
|
|
|
void DictLoader::fail() {
|
|
BlobLoader::fail();
|
|
destroy();
|
|
}
|
|
|
|
std::vector<Dict> Dictionaries() {
|
|
return DictionariesList;
|
|
}
|
|
|
|
rpl::producer<> DictionariesChanged() {
|
|
return DictionariesListChanged.events();
|
|
}
|
|
|
|
void RefreshDictionariesManifest(not_null<Main::Session*> session) {
|
|
StartManifestRefresh(session);
|
|
}
|
|
|
|
int64 GetDownloadSize(int id) {
|
|
const auto i = ranges::find(DictionariesList, id, &Dict::id);
|
|
return (i == end(DictionariesList)) ? 0 : i->size;
|
|
}
|
|
|
|
MTP::DedicatedLoader::Location GetDownloadLocation(int id) {
|
|
const auto i = ranges::find(DictionariesList, id, &Dict::id);
|
|
if (i == end(DictionariesList)) {
|
|
return MTP::DedicatedLoader::Location{};
|
|
}
|
|
return MTP::DedicatedLoader::Location{ i->channel, i->postId };
|
|
}
|
|
|
|
QString DictPathByLangId(int langId) {
|
|
EnsurePath();
|
|
return u"%1/%2"_q.arg(
|
|
DictionariesPath(),
|
|
Spellchecker::LocaleFromLangId(langId).name());
|
|
}
|
|
|
|
QString DictionariesPath() {
|
|
return cWorkingDir() + u"tdata/dictionaries"_q;
|
|
}
|
|
|
|
bool UnpackDictionary(const QString &path, int langId) {
|
|
const auto folder = DictPathByLangId(langId);
|
|
if (!UnpackBlob(path, folder, IsGoodPartName)) {
|
|
return false;
|
|
}
|
|
// The Serbian archive ships "sr_Cyrl_RS.{dic,aff}", but
|
|
// QLocale(QLocale::Serbian).name() is "sr_RS" on our Qt, so the
|
|
// Hunspell loader and DictionaryExists miss them. Rename to the
|
|
// expected stem.
|
|
if (langId == int(QLocale::Serbian)) {
|
|
const auto dir = QDir(folder);
|
|
for (const auto &ext : kDictExtensions) {
|
|
const auto from = u"sr_Cyrl_RS.%1"_q.arg(ext);
|
|
const auto to = u"sr_RS.%1"_q.arg(ext);
|
|
if (dir.exists(from) && !dir.exists(to)) {
|
|
QFile::rename(dir.filePath(from), dir.filePath(to));
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool DictionaryExists(int langId) {
|
|
if (!langId) {
|
|
return true;
|
|
}
|
|
const auto folder = DictPathByLangId(langId) + '/';
|
|
return ranges::none_of(kDictExtensions, [&](const auto &ext) {
|
|
const auto name = Spellchecker::LocaleFromLangId(langId).name();
|
|
return !QFile(folder + name + '.' + ext).exists();
|
|
});
|
|
}
|
|
|
|
bool RemoveDictionary(int langId) {
|
|
if (!langId) {
|
|
return true;
|
|
}
|
|
const auto fileName = Spellchecker::LocaleFromLangId(langId).name();
|
|
const auto folder = u"%1/%2/"_q.arg(
|
|
DictionariesPath(),
|
|
fileName);
|
|
return QDir(folder).removeRecursively();
|
|
}
|
|
|
|
bool WriteDefaultDictionary() {
|
|
// This is an unused function.
|
|
const auto en = QLocale::English;
|
|
if (DictionaryExists(en)) {
|
|
return false;
|
|
}
|
|
const auto fileName = QLocale(en).name();
|
|
const auto folder = u"%1/%2/"_q.arg(
|
|
DictionariesPath(),
|
|
fileName);
|
|
QDir(folder).removeRecursively();
|
|
|
|
const auto path = folder + fileName;
|
|
QDir().mkpath(folder);
|
|
auto input = QFile(u":/misc/en_US_dictionary"_q);
|
|
auto output = QFile(path);
|
|
if (input.open(QIODevice::ReadOnly)
|
|
&& output.open(QIODevice::WriteOnly)) {
|
|
output.write(input.readAll());
|
|
const auto result = Spellchecker::UnpackDictionary(path, en);
|
|
output.remove();
|
|
return result;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
rpl::producer<QString> ButtonManageDictsState(
|
|
not_null<Main::Session*> session) {
|
|
if (Platform::Spellchecker::IsSystemSpellchecker()) {
|
|
return rpl::single(QString());
|
|
}
|
|
const auto computeString = [=] {
|
|
if (!Core::App().settings().spellcheckerEnabled()) {
|
|
return QString();
|
|
}
|
|
const auto dicts = Core::App().settings().dictionariesEnabled();
|
|
if (dicts.empty()) {
|
|
return QString();
|
|
}
|
|
const auto filtered = ranges::views::all(
|
|
dicts
|
|
) | ranges::views::filter(
|
|
DictionaryExists
|
|
) | ranges::to_vector;
|
|
|
|
return (filtered.size() < dicts.size())
|
|
? tr::lng_contacts_loading(tr::now)
|
|
: QString::number(filtered.size());
|
|
};
|
|
return rpl::single(
|
|
computeString()
|
|
) | rpl::then(
|
|
rpl::merge(
|
|
Spellchecker::SupportedScriptsChanged(),
|
|
Spellchecker::DictionariesChanged(),
|
|
Core::App().settings().dictionariesEnabledChanges(
|
|
) | rpl::to_empty,
|
|
Core::App().settings().spellcheckerEnabledChanges(
|
|
) | rpl::to_empty
|
|
) | rpl::map(computeString)
|
|
);
|
|
}
|
|
|
|
std::vector<int> DefaultLanguages() {
|
|
std::vector<int> langs;
|
|
|
|
const auto append = [&](const auto loc) {
|
|
const auto l = LanguageFromLocale(loc);
|
|
if (!ranges::contains(langs, l) && IsSupportedLang(l)) {
|
|
langs.push_back(l);
|
|
}
|
|
};
|
|
|
|
const auto method = QGuiApplication::inputMethod();
|
|
langs.reserve(method ? 3 : 2);
|
|
if (method) {
|
|
append(method->locale());
|
|
}
|
|
append(QLocale(Platform::SystemLanguage()));
|
|
append(QLocale(Lang::LanguageIdOrDefault(Lang::Id())));
|
|
|
|
return langs;
|
|
}
|
|
|
|
void Start(not_null<Main::Session*> session) {
|
|
Spellchecker::SetPhrases({ {
|
|
{ &ph::lng_spellchecker_submenu, tr::lng_spellchecker_submenu() },
|
|
{ &ph::lng_spellchecker_add, tr::lng_spellchecker_add() },
|
|
{ &ph::lng_spellchecker_remove, tr::lng_spellchecker_remove() },
|
|
{ &ph::lng_spellchecker_ignore, tr::lng_spellchecker_ignore() },
|
|
} });
|
|
const auto settings = &Core::App().settings();
|
|
auto &lifetime = session->lifetime();
|
|
|
|
const auto onEnabled = [=](auto enabled) {
|
|
Platform::Spellchecker::UpdateLanguages(
|
|
enabled
|
|
? settings->dictionariesEnabled()
|
|
: std::vector<int>());
|
|
};
|
|
|
|
const auto guard = gsl::finally([=] {
|
|
onEnabled(settings->spellcheckerEnabled());
|
|
});
|
|
|
|
if (Platform::Spellchecker::IsSystemSpellchecker()) {
|
|
Spellchecker::SupportedScriptsChanged()
|
|
| rpl::take(1)
|
|
| rpl::on_next(AddExceptions, lifetime);
|
|
|
|
return;
|
|
}
|
|
|
|
Spellchecker::SupportedScriptsChanged(
|
|
) | rpl::on_next(AddExceptions, lifetime);
|
|
|
|
Spellchecker::SetWorkingDirPath(DictionariesPath());
|
|
|
|
settings->dictionariesEnabledChanges(
|
|
) | rpl::on_next([](auto dictionaries) {
|
|
Platform::Spellchecker::UpdateLanguages(dictionaries);
|
|
}, lifetime);
|
|
|
|
settings->spellcheckerEnabledChanges(
|
|
) | rpl::on_next(onEnabled, lifetime);
|
|
|
|
const auto method = QGuiApplication::inputMethod();
|
|
|
|
const auto connectInput = [=] {
|
|
if (!method || !settings->spellcheckerEnabled()) {
|
|
return;
|
|
}
|
|
auto callback = [=] {
|
|
if (BackgroundLoader) {
|
|
return;
|
|
}
|
|
const auto l = LanguageFromLocale(method->locale());
|
|
// Avoid pulling the manifest just because an input-method
|
|
// locale flipped; only fetch if we'd actually need to
|
|
// download something for the new locale.
|
|
if (DictionaryExists(l)) {
|
|
return;
|
|
}
|
|
EnsureManifestThen(session, crl::guard(session, [=] {
|
|
if (!IsSupportedLang(l) || DictionaryExists(l)) {
|
|
return;
|
|
}
|
|
DownloadDictionaryInBackground(session, 0, { l });
|
|
}));
|
|
};
|
|
QObject::connect(
|
|
method,
|
|
&QInputMethod::localeChanged,
|
|
std::move(callback));
|
|
};
|
|
|
|
if (settings->autoDownloadDictionaries()) {
|
|
session->data().contactsLoaded().changes(
|
|
) | rpl::on_next([=](bool loaded) {
|
|
if (!loaded) {
|
|
return;
|
|
}
|
|
const auto enabled = settings->dictionariesEnabled();
|
|
if (!enabled.empty()
|
|
&& ranges::all_of(enabled, &DictionaryExists)) {
|
|
// Every previously-enabled dictionary is already on
|
|
// disk; no manifest fetch, no network traffic.
|
|
return;
|
|
}
|
|
EnsureManifestThen(session, crl::guard(session, [=] {
|
|
DownloadDictionaryInBackground(
|
|
session, 0, DefaultLanguages());
|
|
}));
|
|
}, lifetime);
|
|
|
|
connectInput();
|
|
}
|
|
|
|
const auto disconnect = [=] {
|
|
QObject::disconnect(
|
|
method,
|
|
&QInputMethod::localeChanged,
|
|
nullptr,
|
|
nullptr);
|
|
};
|
|
lifetime.add([=] {
|
|
disconnect();
|
|
for (auto &[index, account] : session->domain().accounts()) {
|
|
if (const auto anotherSession = account->maybeSession()) {
|
|
if (anotherSession->uniqueId() != session->uniqueId()) {
|
|
Spellchecker::Start(anotherSession);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
rpl::combine(
|
|
settings->spellcheckerEnabledValue(),
|
|
settings->autoDownloadDictionariesValue()
|
|
) | rpl::on_next([=](bool spell, bool download) {
|
|
if (spell && download) {
|
|
connectInput();
|
|
return;
|
|
}
|
|
disconnect();
|
|
}, lifetime);
|
|
|
|
}
|
|
|
|
} // namespace Spellchecker
|
|
|
|
#endif // !TDESKTOP_DISABLE_SPELLCHECK
|