Add localized emoji search
This commit is contained in:
parent
ce0fb22041
commit
e90553b3b3
17 changed files with 878 additions and 97 deletions
188
app/OptionalResourceService.ts
Normal file
188
app/OptionalResourceService.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { join, dirname } from 'node:path';
|
||||
import { mkdir, readFile, readdir, writeFile, unlink } from 'node:fs/promises';
|
||||
import { createHash, timingSafeEqual } from 'node:crypto';
|
||||
import { ipcMain } from 'electron';
|
||||
import LRU from 'lru-cache';
|
||||
import got from 'got';
|
||||
import PQueue from 'p-queue';
|
||||
|
||||
import type {
|
||||
OptionalResourceType,
|
||||
OptionalResourcesDictType,
|
||||
} from '../ts/types/OptionalResource';
|
||||
import { OptionalResourcesDictSchema } from '../ts/types/OptionalResource';
|
||||
import * as log from '../ts/logging/log';
|
||||
import { getGotOptions } from '../ts/updater/got';
|
||||
import { drop } from '../ts/util/drop';
|
||||
|
||||
const RESOURCES_DICT_PATH = join(
|
||||
__dirname,
|
||||
'..',
|
||||
'build',
|
||||
'optional-resources.json'
|
||||
);
|
||||
|
||||
const MAX_CACHE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
export class OptionalResourceService {
|
||||
private maybeDeclaration: OptionalResourcesDictType | undefined;
|
||||
|
||||
private readonly cache = new LRU<string, Buffer>({
|
||||
max: MAX_CACHE_SIZE,
|
||||
|
||||
length: buf => buf.length,
|
||||
});
|
||||
|
||||
private readonly fileQueues = new Map<string, PQueue>();
|
||||
|
||||
constructor(private readonly resourcesDir: string) {
|
||||
ipcMain.handle('OptionalResourceService:getData', (_event, name) =>
|
||||
this.getData(name)
|
||||
);
|
||||
|
||||
drop(this.lazyInit());
|
||||
}
|
||||
|
||||
public static create(resourcesDir: string): OptionalResourceService {
|
||||
return new OptionalResourceService(resourcesDir);
|
||||
}
|
||||
|
||||
public async getData(name: string): Promise<Buffer | undefined> {
|
||||
await this.lazyInit();
|
||||
|
||||
const decl = this.declaration[name];
|
||||
if (!decl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const inMemory = this.cache.get(name);
|
||||
if (inMemory) {
|
||||
return inMemory;
|
||||
}
|
||||
|
||||
const filePath = join(this.resourcesDir, name);
|
||||
return this.queueFileWork(filePath, async () => {
|
||||
try {
|
||||
const onDisk = await readFile(filePath);
|
||||
const digest = createHash('sha512').update(onDisk).digest();
|
||||
|
||||
// Same digest and size
|
||||
if (
|
||||
timingSafeEqual(digest, Buffer.from(decl.digest, 'base64')) &&
|
||||
onDisk.length === decl.size
|
||||
) {
|
||||
log.warn(`OptionalResourceService: loaded ${name} from disk`);
|
||||
this.cache.set(name, onDisk);
|
||||
return onDisk;
|
||||
}
|
||||
|
||||
log.warn(`OptionalResourceService: ${name} is no longer valid on disk`);
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// We get here if file doesn't exist or if its digest/size is different
|
||||
try {
|
||||
await unlink(filePath);
|
||||
} catch {
|
||||
// Just do our best effort and move forward
|
||||
}
|
||||
|
||||
return this.fetch(name, decl, filePath);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Private
|
||||
//
|
||||
|
||||
private async lazyInit(): Promise<void> {
|
||||
if (this.maybeDeclaration !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const json = JSON.parse(await readFile(RESOURCES_DICT_PATH, 'utf8'));
|
||||
this.maybeDeclaration = OptionalResourcesDictSchema.parse(json);
|
||||
|
||||
// Clean unknown resources
|
||||
let subPaths: Array<string>;
|
||||
try {
|
||||
subPaths = await readdir(this.resourcesDir);
|
||||
} catch (error) {
|
||||
// Directory wasn't created yet
|
||||
if (error.code === 'ENOENT') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
subPaths.map(async subPath => {
|
||||
if (this.declaration[subPath]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = join(this.resourcesDir, subPath);
|
||||
|
||||
try {
|
||||
await unlink(fullPath);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`OptionalResourceService: failed to cleanup ${subPath}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private get declaration(): OptionalResourcesDictType {
|
||||
if (this.maybeDeclaration === undefined) {
|
||||
throw new Error('optional-resources.json not loaded yet');
|
||||
}
|
||||
return this.maybeDeclaration;
|
||||
}
|
||||
|
||||
private async queueFileWork<R>(
|
||||
filePath: string,
|
||||
body: () => Promise<R>
|
||||
): Promise<R> {
|
||||
let queue = this.fileQueues.get(filePath);
|
||||
if (!queue) {
|
||||
queue = new PQueue({ concurrency: 1 });
|
||||
this.fileQueues.set(filePath, queue);
|
||||
}
|
||||
try {
|
||||
return await queue.add(body);
|
||||
} finally {
|
||||
if (queue.size === 0) {
|
||||
this.fileQueues.delete(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async fetch(
|
||||
name: string,
|
||||
decl: OptionalResourceType,
|
||||
destPath: string
|
||||
): Promise<Buffer> {
|
||||
const result = await got(decl.url, getGotOptions()).buffer();
|
||||
|
||||
this.cache.set(name, result);
|
||||
|
||||
try {
|
||||
await mkdir(dirname(destPath), { recursive: true });
|
||||
await writeFile(destPath, result);
|
||||
} catch (error) {
|
||||
log.error('OptionalResourceService: failed to save file', error);
|
||||
// Still return the data that we just fetched
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -79,6 +79,7 @@ import { updateDefaultSession } from './updateDefaultSession';
|
|||
import { PreventDisplaySleepService } from './PreventDisplaySleepService';
|
||||
import { SystemTrayService, focusAndForceToTop } from './SystemTrayService';
|
||||
import { SystemTraySettingCache } from './SystemTraySettingCache';
|
||||
import { OptionalResourceService } from './OptionalResourceService';
|
||||
import {
|
||||
SystemTraySetting,
|
||||
shouldMinimizeToSystemTray,
|
||||
|
@ -1759,6 +1760,8 @@ app.on('ready', async () => {
|
|||
// Write buffered information into newly created logger.
|
||||
consoleLogger.writeBufferInto(logger);
|
||||
|
||||
OptionalResourceService.create(join(userDataPath, 'optionalResources'));
|
||||
|
||||
sqlInitPromise = initializeSQL(userDataPath);
|
||||
|
||||
if (!resolvedTranslationsLocale) {
|
||||
|
|
337
build/optional-resources.json
Normal file
337
build/optional-resources.json
Normal file
|
@ -0,0 +1,337 @@
|
|||
{
|
||||
"emoji-index-ar.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ar.json",
|
||||
"size": 448278,
|
||||
"digest": "lgdLVdv4hSGfVTEvJbk733xYk8ZJvH+yi47peAJsytl7NWjm2WJN/d6Z3Aoxe1kLYiup7ugtoX3MIzeR3JZc0A=="
|
||||
},
|
||||
"emoji-index-en.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/en.json",
|
||||
"size": 383831,
|
||||
"digest": "jIu4ARhWJ8rP/suFEgB3T50nXbECt78CNXrHcUWAtfUiDLLLIUKn+52p3NfygmqCdxa5TRyDF5dFRbnGrWocIg=="
|
||||
},
|
||||
"emoji-index-hu.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/hu.json",
|
||||
"size": 399572,
|
||||
"digest": "PUvDX27TJtruOTFhz5m1ztnpTXpMJBYnAezl47gMIdwaKJSMwuS94pYMn3u1VgDRaC2DZpuLL19NFqsNWBgAYA=="
|
||||
},
|
||||
"emoji-index-sw.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/sw.json",
|
||||
"size": 399076,
|
||||
"digest": "mZmsfYh+bmxUPbut2wAojZjJZlzzFptkNMoe20PyV4znpUiDHRT/DLcLJ+EFdoJU39wlXkVWUxq3lbTUb7EUjQ=="
|
||||
},
|
||||
"emoji-index-cs.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/cs.json",
|
||||
"size": 403468,
|
||||
"digest": "ao9kU4RKppGTmtg37J+JxW7luexkQtvTcp6WhpjItNPqnNzWF2l8hrD24uN0ahFbBccrQ6cmEaaST9wkuZ2HZA=="
|
||||
},
|
||||
"emoji-index-hr-HR.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/hr.json",
|
||||
"size": 399715,
|
||||
"digest": "x3XmsTKpaORoJOx0GcHs4gDOeKpq6djGYpzlG8GPvhxmU5PDzC++eK2Nu7HU78+tYQoATolIhvZ7NGOA3WRWKw=="
|
||||
},
|
||||
"emoji-index-lv-LV.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/lv.json",
|
||||
"size": 410046,
|
||||
"digest": "YnWNX+uDOde5CpDi0BcjQHknyQMRCjCbr0Wx9GVzRYj4G8JWWQXcQ1pzFFBjdS27L5dI+Vxj43HGHp7BoJC2SA=="
|
||||
},
|
||||
"emoji-index-pt-PT.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/pt.json",
|
||||
"size": 405039,
|
||||
"digest": "ZSp9Nev4nv+001uSQbXCbyw2TV9B96D2+aFdDUvgnOF31enZjPPPyRhdea4ax3Eyo177e5TNv6A43TfwPXOBXA=="
|
||||
},
|
||||
"emoji-index-de.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/de.json",
|
||||
"size": 399152,
|
||||
"digest": "yuycBAbdswH5gC+CwXL1V+NEIAHwMWG4hsQsPgRlqdG6fz6byEFE/HPdVtrGzHMA3VL25lNFGVhv1021iMWG5A=="
|
||||
},
|
||||
"emoji-index-id.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/in.json",
|
||||
"size": 398444,
|
||||
"digest": "4CibIEL6Ya9TGdh8Qp33v6yopn05x8xD+Eks/QGL3i7l6wm3fqMkHyO9xeZGA4rJHpuDZlgRxu3Q/JJg8Nbs2A=="
|
||||
},
|
||||
"emoji-index-fa-IR.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/fa.json",
|
||||
"size": 448610,
|
||||
"digest": "35MG1yAF3pfPCArKCo4OCje18mRXNGO37F2KtCrQjnEYfhKyhadrhU0GsODE/RVsjTklBNLDHqL9uaFL4bAM2w=="
|
||||
},
|
||||
"emoji-index-ur.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ur.json",
|
||||
"size": 446079,
|
||||
"digest": "XCWNxs9DG2UV12f8CwDlXjPsU7GJgSkQdXtDXPvewGPTZjYhhOkGtkf5jJ+Bveu+InZj744OuTOehEnu558qyA=="
|
||||
},
|
||||
"emoji-index-fr.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/fr.json",
|
||||
"size": 405258,
|
||||
"digest": "3ca5YvikLc/uVSjhZt/xZDFdY0dAqW4GyYPvOvFhP1ZQYW0s+rJkyxVMheV6O5Jp/J6C7I/3pNh1ITevnyfWQQ=="
|
||||
},
|
||||
"emoji-index-gl-ES.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/gl.json",
|
||||
"size": 401820,
|
||||
"digest": "VeaMoig6TeRFp2jACd4ZvfNu0q3j8fn/Daugw0/orzkf2bM/FXNpglyI1dOljKytcViK8NuFfjR1yrgRX2shFA=="
|
||||
},
|
||||
"emoji-index-da.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/da.json",
|
||||
"size": 389097,
|
||||
"digest": "B2Dz/yCA/5p43bqtqZy8kfDMMU6wL0Bo5fXvJnkK6c3UcVILB8DhkJSTp3pJoGpJUBNwumNH9Xot9a56e7k5Gw=="
|
||||
},
|
||||
"emoji-index-bs-BA.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/bs.json",
|
||||
"size": 394827,
|
||||
"digest": "TyQ6Evk7BHZzHoPBhzw8P6V8yrdE2ZCF8T6WQ7yneHlhAqLdo6wVK/tc4jTlK/6cOsDEbxUqLBPhYlRD36w8Kw=="
|
||||
},
|
||||
"emoji-index-nb.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/nb.json",
|
||||
"size": 388450,
|
||||
"digest": "0s2sCsddq0YzP8coq0L/l2Nd0YToOnEMfdmgZezhxteTdJAepXCJGpNSBNScrYm2RfEaLxr80i5vImb1XYvlDA=="
|
||||
},
|
||||
"emoji-index-tl-PH.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/tl.json",
|
||||
"size": 400801,
|
||||
"digest": "Riypkbj6yk9f5Sxbzca9AKqRLbL2+fLRPV3x4+UUCV78VR4aCRDRxHcp2mxrDCautxMDItzKd1Rk9DWMywWTGw=="
|
||||
},
|
||||
"emoji-index-tr.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/tr.json",
|
||||
"size": 398976,
|
||||
"digest": "QGVU51dVQjDteek7nxPC3NvULJvzZRh7Wi/jxunXMZywK13reZCii3M79W/zbYnjV3r6J7F1uoMOjjSYO00TlQ=="
|
||||
},
|
||||
"emoji-index-sv.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/sv.json",
|
||||
"size": 393132,
|
||||
"digest": "/r//en2/db6FARxA5OYEGaZrIO0gzlSa8DtbXPnhKC+xwjMoAqrKIYU+BZn8Dvs+GlUzcvEkKQGy2BHFcLZQ6A=="
|
||||
},
|
||||
"emoji-index-kk-KZ.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/kk.json",
|
||||
"size": 473398,
|
||||
"digest": "0VAbJgweXv94jHQppDAxw45sN0oLdTNaUDuZ88Zvls99lCjCsqUH5JjBvi/uuZ+Jec9u2zRI/BnrPO3+c2wzqw=="
|
||||
},
|
||||
"emoji-index-sr.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/sr.json",
|
||||
"size": 476243,
|
||||
"digest": "OPckwWyTruHoqD1Ef7QSXkwJtwe0aoGHhJFq6GwV7jnki2uGiZXLAHuGZYlRQRu96t/UOwbBwr7Fwgna5zazFw=="
|
||||
},
|
||||
"emoji-index-bg-BG.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/bg.json",
|
||||
"size": 480607,
|
||||
"digest": "0gExbSWvTh5B43iQzFZHhevuBpY8rhXXuvF64lG2oGLzcEMMkA5MdCKIpovn+s2Wuo+CpLMuRCPQsbpsLu1VPg=="
|
||||
},
|
||||
"emoji-index-ms.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ms.json",
|
||||
"size": 399653,
|
||||
"digest": "8zzR5xbPQlGvN24mQvfGrpO+bW63kcIiezqcJuiz6xVp7vuJRHZRfiMnguz0HOXC5xUL890torTlM1r9ebOK3A=="
|
||||
},
|
||||
"emoji-index-zh-CN.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/zh_CN.json",
|
||||
"size": 387896,
|
||||
"digest": "Gzm37NKMn7QPx2+1INSapW+0ttmFYLEpDAYev+Hjgccvhpf3p+WlYDamT3cZpWn1BuKwsSTaVqWCUuJ45DnWwA=="
|
||||
},
|
||||
"emoji-index-ca.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ca.json",
|
||||
"size": 397974,
|
||||
"digest": "FhSmIge+dElK99HcNCjwJlRCGm/u4q2tbYgMq827Sol3cPDZigxltgTzXrjTaKxRWVDDzSNITWYQunt/bc4PHQ=="
|
||||
},
|
||||
"emoji-index-he.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/iw.json",
|
||||
"size": 455478,
|
||||
"digest": "4XPBr8X8Ip0aI67e1R1xjgrK9JJaTnoUC3zpxf84DaqDfpiMUrdOpkV3YYToAE8/k8BS9lQKzx4ZF5FNsK8Slw=="
|
||||
},
|
||||
"emoji-index-el.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/el.json",
|
||||
"size": 492200,
|
||||
"digest": "yilYdYAQTUmzRoGjDEDyuUzLuZ0aGVb2n/zMC8qNMyzcLit3uSd5TLuAnO821LUiwcpINdt3K5CTKhtCZCZTlw=="
|
||||
},
|
||||
"emoji-index-uk-UA.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/uk.json",
|
||||
"size": 493305,
|
||||
"digest": "VeDAEaMY/AIC1IqY+UvwFqAZizwk22vGbTrlXOxg5NN8qXrRr5z0E+BDaqIsbnBttMHKzuw+GA/DTlCJG3+s7w=="
|
||||
},
|
||||
"emoji-index-gu-IN.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/gu.json",
|
||||
"size": 529403,
|
||||
"digest": "Z+AsVURhpUvFLPP2sXNjwWF0qIz0SoMUnK42DTG2+ogx+h39GXMW3l4UBxlMabXaE347GUZ6hFerXbXBA68ZPA=="
|
||||
},
|
||||
"emoji-index-vi.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/vi.json",
|
||||
"size": 435169,
|
||||
"digest": "UjS9LG5BmtTvfhMLkyn79IzgjcX+kKeSjVI2u3hka3YjBGhVg1uUERPE3cVeZg1V4QkVZvq6EwvfP5QBU6MJwg=="
|
||||
},
|
||||
"emoji-index-my.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/my.json",
|
||||
"size": 590529,
|
||||
"digest": "cVlHU6KoDnmj+XImT39p6Hkbk9jADUmP0Ro5fsTNnC1D66wYw+FXEys/Ox75J7+nUy507jmFBzNVOqXmCzYUDw=="
|
||||
},
|
||||
"emoji-index-te-IN.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/te.json",
|
||||
"size": 573374,
|
||||
"digest": "8z4stnh1595Xxq+IDEIIHkCQjujyOmKiFe0Aj6iPHGGTZ5SEtigWsO8mKyOGd2u9ZQnxRIuUa0P73fQwXfvviQ=="
|
||||
},
|
||||
"emoji-index-af-ZA.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/af.json",
|
||||
"size": 387066,
|
||||
"digest": "Ic5nCozImF9ozH1FqjWAJj77dBvh4VimNod5Z6sulYT/RzSIgaPgwiD3/KJqiFf0c63BwPK+xPc/78zmIZrSGQ=="
|
||||
},
|
||||
"emoji-index-ky-KG.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ky.json",
|
||||
"size": 470385,
|
||||
"digest": "O2R5Im8B9Q8HExPH6r4Tk2lWekiZNOyHnCXnKjtPDbFiTKtcyNW7qaIFvRdiEhgCSW789a59nQsIF+PjGF96uw=="
|
||||
},
|
||||
"emoji-index-az-AZ.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/az.json",
|
||||
"size": 401639,
|
||||
"digest": "FoijICivz2Ewr6dN3Dds2WCVkpy7+QI/BdxqkIEebm0KdEpFXwth7I1Nnm0bzTIx5BT6wsN4QMNyaQTNS2oFxA=="
|
||||
},
|
||||
"emoji-index-kn-IN.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/kn.json",
|
||||
"size": 565842,
|
||||
"digest": "esUkUfZcTTU4ZdzdXcGTcD5YyfM4vDASWWxklZVihggWgph+rZFP6u9TjNgv57+RkWcxkvp8B3gOP6yMj5rnBA=="
|
||||
},
|
||||
"emoji-index-ml-IN.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ml.json",
|
||||
"size": 590912,
|
||||
"digest": "GNipi1zDRLaB2pzLVQVK7X4FBfo8c81qIRNB1c1DwRYEjQRJvb3AgsEc4tTDqZftueYq5Ed32i2sTHz0y4ahHA=="
|
||||
},
|
||||
"emoji-index-sk-SK.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/sk.json",
|
||||
"size": 402538,
|
||||
"digest": "Q/xFVaWSnPiN/NZVo7a2ddElKpawF+LG7yuSlvYQOpM7lByv0LxFxs1nIQ7xIo+QeYZRvjQ0l9o9zaTlVgtR1w=="
|
||||
},
|
||||
"emoji-index-eu.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/eu.json",
|
||||
"size": 405751,
|
||||
"digest": "ZchYHNd0QlyHHI57NjfaPlidsl9JqaJaH//4v3SxfyYxykgZSdfYgA040sV/na/muye4Aovdw1Qv3Et5wk85GA=="
|
||||
},
|
||||
"emoji-index-sl-SI.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/sl.json",
|
||||
"size": 398593,
|
||||
"digest": "SSqzn1EGtK35mOuRevmrhkC2ehgW5hV0JO8JWDdAS9m2rAYWrDi8LBI8G80qQR/BSSCParKp/YbSx/8cqhIkhA=="
|
||||
},
|
||||
"emoji-index-ko.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ko.json",
|
||||
"size": 406752,
|
||||
"digest": "tbwJJ+1Hae32a+rceoX9V/OY7gOP+Ygx7Zl5b5Ev8ojuTaPHPTG8gPjD3sFoQqIYJOYAM6JeTgSJprh8gao1hA=="
|
||||
},
|
||||
"emoji-index-zh-Hant.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/zh_TW.json",
|
||||
"size": 383697,
|
||||
"digest": "4X/PPP2yjZvMuy4uxTy/9DJF8afqsH4H8Xnv1tyUb5XYAioukcRsTvQT2XT46bVnHc6QAOdKgX0L6lnqkHsZ1A=="
|
||||
},
|
||||
"emoji-index-mk-MK.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/mk.json",
|
||||
"size": 480649,
|
||||
"digest": "7oYHTZVcmJTxc9JIBKbRNAtjbR7Ddw89K+WNj1NdLPIT3J42iujrul6tFZcal2qV5hVeadoqKeOYfHAfJTFGaQ=="
|
||||
},
|
||||
"emoji-index-es.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/es.json",
|
||||
"size": 403651,
|
||||
"digest": "QHGbkfvuNrN472l0sYEknLOMseqTuCfCw/WsotcUPecnWY8WC8AKzjhR1OgsyGFXqkuhVCkrn0MrYYfcVEfP/w=="
|
||||
},
|
||||
"emoji-index-th.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/th.json",
|
||||
"size": 547303,
|
||||
"digest": "BC79X04SP17K94EVHFhgK8WPJCW9cYO7AwjR4iD7LFI+kuOeCFJAwWK+nIXBgSjfeAy5sP3XhlP1M+QGsjwoTA=="
|
||||
},
|
||||
"emoji-index-fi.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/fi.json",
|
||||
"size": 400663,
|
||||
"digest": "L5D1Wp9J9pu+jZDhzpKh2p+zbyQzRdVEc8OuEuJpAxIiXL1KKu2+5PU05giBiICi7BtMa4kfWVHkko8Vy3svow=="
|
||||
},
|
||||
"emoji-index-ug.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ug.json",
|
||||
"size": 460146,
|
||||
"digest": "6PDZlF9jGYItoZzxjjfcmfAwrNDaPGtI/urFESQauCmTKbrtcqCjfie3MIB0S8L5rulJBIS4t5u+5BLVLNOSwg=="
|
||||
},
|
||||
"emoji-index-sq-AL.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/sq.json",
|
||||
"size": 405020,
|
||||
"digest": "byryE8y1c9jDhn25qobC6yxSOd5YpKmHBw8PPCqUL/Yb1BpuMKt5lbfh/WWgD3LI9UUkYiyBXiyOcj/X4V2jpg=="
|
||||
},
|
||||
"emoji-index-pa-IN.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/pa.json",
|
||||
"size": 513322,
|
||||
"digest": "lYu8oSdrHSSEek20xRprpu3pXVZPR92TN0B8DahXHYgrYrk7dU5qFfL57xnhOw+dTFmq6XoqVKOx3bPdUzyXPQ=="
|
||||
},
|
||||
"emoji-index-ja.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ja.json",
|
||||
"size": 423808,
|
||||
"digest": "XRQDifFGd5epZaT/2iHQYV0pFF+0W0e+cIuZ2b3xOzgv5JVknXopDU/SVspPiiomET86QwtOEPDJSlRUvBN3tg=="
|
||||
},
|
||||
"emoji-index-ga-IE.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ga.json",
|
||||
"size": 404268,
|
||||
"digest": "5jTb+2WZEFi1kksKFZ/krRoyD1/VGsMVVJqRJoRe7MCkjdAmr1fFuCRcf0Kujxu2WDpWpB2bRijRuBpv9LXfgA=="
|
||||
},
|
||||
"emoji-index-ro-RO.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ro.json",
|
||||
"size": 403632,
|
||||
"digest": "EGVjdUCizJ/qgUZ01fiMHl9AdLtfN9qmqoZdmLXsqlouzZ1jlUP+1lTIYC9j2RpR9VWH3b+//Y8CxdaqaSeH4w=="
|
||||
},
|
||||
"emoji-index-it.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/it.json",
|
||||
"size": 403536,
|
||||
"digest": "BTb24WzM17Xi0yiT2rjH30G0VyXU+s68qb09z2cV20qByclBo1Nba2ftwcPGhTkg8XAdky7EcrdGSQKFgtXgJw=="
|
||||
},
|
||||
"emoji-index-hi-IN.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/hi.json",
|
||||
"size": 538940,
|
||||
"digest": "8gvvXYgs8FzukH+tkdn1VPu2xt+ooWneoYqT+WmAQkq43C4aNxv9hko1jgq9kD+Ms+0aqu0Pl/4qfUNZos1QUw=="
|
||||
},
|
||||
"emoji-index-km-KH.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/km.json",
|
||||
"size": 559762,
|
||||
"digest": "tg/UYo0154HpfsZfcESEPWHigECsHW9ekzEUjipdU/voo12zpBczwZSCAYU9DawPQV7IeaDF3ZpJgtocHvrELQ=="
|
||||
},
|
||||
"emoji-index-ka-GE.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ka.json",
|
||||
"size": 580052,
|
||||
"digest": "d9H+bKUoUHUwP1EstnTGWxHgOL8RXgTj9u/CpB644Qj0DqippPuTLvqAeRO0gheaGZ0Uv4m6TVEE09NwwyjNTQ=="
|
||||
},
|
||||
"emoji-index-nl.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/nl.json",
|
||||
"size": 392291,
|
||||
"digest": "kaFKhMK+kWbsq+NFhexL8G5uPOEi2ATVVbZ7Q8925yOlHlZ6KNNHZtR2BDhVirbErXflbs7Fh+b4qWWtL5lnrg=="
|
||||
},
|
||||
"emoji-index-et-EE.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/et.json",
|
||||
"size": 393026,
|
||||
"digest": "3lBTCEdqw4vTuk/KaD6QDxtXpzc7zKs7ZiobcxS5ZISjs8iC3cuPDz77n/E2zEVwluv0mWTPOLV0DNWBG8s7Jg=="
|
||||
},
|
||||
"emoji-index-zh-HK.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/zh_HK.json",
|
||||
"size": 383653,
|
||||
"digest": "Mnt9AjeQfbU0Czae8QhtnV4naPGWTDy7EniLrv397gTpZWqX5n396NLA09n3ZgMLv0AmZd3+nCgZdHlaVZMQ4w=="
|
||||
},
|
||||
"emoji-index-pl.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/pl.json",
|
||||
"size": 405256,
|
||||
"digest": "q2CxPVCrGtKIVVHlls4GRGUsxTzHe6cwG6XdIr3Iu55F4bYlrdw6TrBOWY3D/6k1XCtTqbGtOIyWhUJHdomViA=="
|
||||
},
|
||||
"emoji-index-ru.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ru.json",
|
||||
"size": 489997,
|
||||
"digest": "I4BbmcE7dYVpAda7zH1qaErrBppZ3cq1Nw58SYB9q4sjdZ8xPPRSfBUoGZlc4YXQwvpPJ75pEok49jD9O7xN6Q=="
|
||||
},
|
||||
"emoji-index-pt-BR.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/pt_BR.json",
|
||||
"size": 405896,
|
||||
"digest": "X20ND4rjd0wG5IyqqtDwynGhZxQ8i6JAd0BPjsOLM/GkqW2HLCPJRdwYC+TDtuJa0cn9YmVsadu+ty0vLgojaA=="
|
||||
},
|
||||
"emoji-index-ta-IN.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/ta.json",
|
||||
"size": 588808,
|
||||
"digest": "ifOYhbzJRh9sOyDv4333AlwrL1UD+Z7pE/Z92rjXb9IoGz65UxXm+D893moJ/ceJtOpjGfH+T84xOEkn19frBA=="
|
||||
},
|
||||
"emoji-index-bn-BD.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/bn.json",
|
||||
"size": 537619,
|
||||
"digest": "3Hnk3I4RTFy0xbJjQC8QBWlj5gCt9nEKGTOQVxu1H9WfQxkBs4sAJYVjwqD9TT6rZPIX0uW3a7JGXpT5Dv7V+A=="
|
||||
},
|
||||
"emoji-index-mr-IN.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/mr.json",
|
||||
"size": 529450,
|
||||
"digest": "yNLiRFp70o6JfErdbDUM5odixVOvqM3spmD//80Q0gwGQfBjy0MuVA2p8BHjoPQJ1kxZIVpCToLexaSTkM/Ufw=="
|
||||
},
|
||||
"emoji-index-lt-LT.json": {
|
||||
"url": "https://updates2.signal.org/static/android/emoji/search/13/lt.json",
|
||||
"size": 417252,
|
||||
"digest": "IUKfMAIywuj6frUBYec1uqW6fjtmqpPYKahYNCzTC8fw15LgGN+rjhHIn7pBppsnOdiKPlGTpLmKboML0Ide0w=="
|
||||
}
|
||||
}
|
|
@ -23,9 +23,10 @@
|
|||
"build-release": "yarn run build",
|
||||
"sign-release": "node ts/updater/generateSignature.js",
|
||||
"notarize": "echo 'No longer necessary'",
|
||||
"get-strings": "ts-node ts/scripts/get-strings.ts && ts-node ts/scripts/gen-nsis-script.ts && ts-node ts/scripts/gen-locales-config.ts && run-p get-strings:locales get-strings:countries mark-unusued-strings-deleted",
|
||||
"get-strings": "ts-node ts/scripts/get-strings.ts && ts-node ts/scripts/gen-nsis-script.ts && ts-node ts/scripts/gen-locales-config.ts && run-p get-strings:locales get-strings:countries get-strings:emoji mark-unusued-strings-deleted",
|
||||
"get-strings:locales": "ts-node ./ts/scripts/build-localized-display-names.ts locales ts/scripts/locale-data/locale-display-names.csv build/locale-display-names.json",
|
||||
"get-strings:countries": "ts-node ./ts/scripts/build-localized-display-names.ts countries ts/scripts/locale-data/country-display-names.csv build/country-display-names.json",
|
||||
"get-strings:emoji": "ts-node ./ts/scripts/get-emoji-locales.ts",
|
||||
"push-strings": "node ts/scripts/remove-strings.js && node ts/scripts/push-strings.js",
|
||||
"mark-unusued-strings-deleted": "ts-node ./ts/scripts/mark-unused-strings-deleted.ts",
|
||||
"get-expire-time": "node ts/scripts/get-expire-time.js",
|
||||
|
@ -516,6 +517,7 @@
|
|||
"build/locale-display-names.json",
|
||||
"build/country-display-names.json",
|
||||
"build/dns-fallback.json",
|
||||
"build/optional-resources.json",
|
||||
"node_modules/**",
|
||||
"!node_modules/underscore/**",
|
||||
"!node_modules/emoji-datasource/emoji_pretty.json",
|
||||
|
|
|
@ -52,6 +52,7 @@ import { isNotNil } from '../util/isNotNil';
|
|||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import { useRefMerger } from '../hooks/useRefMerger';
|
||||
import { useEmojiSearch } from '../hooks/useEmojiSearch';
|
||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||
import type { DraftEditMessageType } from '../model-types.d';
|
||||
|
@ -688,6 +689,8 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
const callbacksRef = React.useRef(unstaleCallbacks);
|
||||
callbacksRef.current = unstaleCallbacks;
|
||||
|
||||
const search = useEmojiSearch(i18n.getLocale());
|
||||
|
||||
const reactQuill = React.useMemo(
|
||||
() => {
|
||||
const delta = generateDelta(draftText || '', draftBodyRanges || []);
|
||||
|
@ -739,6 +742,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
onPickEmoji: (emoji: EmojiPickDataType) =>
|
||||
callbacksRef.current.onPickEmoji(emoji),
|
||||
skinTone,
|
||||
search,
|
||||
},
|
||||
autoSubstituteAsciiEmojis: {
|
||||
skinTone,
|
||||
|
|
|
@ -21,10 +21,11 @@ import {
|
|||
import FocusTrap from 'focus-trap-react';
|
||||
|
||||
import { Emoji } from './Emoji';
|
||||
import { dataByCategory, search } from './lib';
|
||||
import { dataByCategory } from './lib';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { isSingleGrapheme } from '../../util/grapheme';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { useEmojiSearch } from '../../hooks/useEmojiSearch';
|
||||
|
||||
export type EmojiPickDataType = {
|
||||
skinTone?: number;
|
||||
|
@ -108,6 +109,8 @@ export const EmojiPicker = React.memo(
|
|||
const [scrollToRow, setScrollToRow] = React.useState(0);
|
||||
const [selectedTone, setSelectedTone] = React.useState(skinTone);
|
||||
|
||||
const search = useEmojiSearch(i18n.getLocale());
|
||||
|
||||
const handleToggleSearch = React.useCallback(
|
||||
(
|
||||
e:
|
||||
|
@ -261,10 +264,7 @@ export const EmojiPicker = React.memo(
|
|||
|
||||
const emojiGrid = React.useMemo(() => {
|
||||
if (searchText) {
|
||||
return chunk(
|
||||
search(searchText).map(e => e.short_name),
|
||||
COL_COUNT
|
||||
);
|
||||
return chunk(search(searchText), COL_COUNT);
|
||||
}
|
||||
|
||||
const chunks = flatMap(renderableCategories, cat =>
|
||||
|
@ -275,7 +275,7 @@ export const EmojiPicker = React.memo(
|
|||
);
|
||||
|
||||
return [...chunk(firstRecent, COL_COUNT), ...chunks];
|
||||
}, [firstRecent, renderableCategories, searchText]);
|
||||
}, [firstRecent, renderableCategories, searchText, search]);
|
||||
|
||||
const rowCount = emojiGrid.length;
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import { getOwn } from '../../util/getOwn';
|
|||
import * as log from '../../logging/log';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
import { drop } from '../../util/drop';
|
||||
import type { LocaleEmojiType } from '../../types/emoji';
|
||||
|
||||
export const skinTones = ['1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'];
|
||||
|
||||
|
@ -218,34 +219,69 @@ export function getImagePath(
|
|||
return makeImagePath(emojiData.image);
|
||||
}
|
||||
|
||||
const fuse = new Fuse(data, {
|
||||
shouldSort: true,
|
||||
threshold: 0.2,
|
||||
minMatchCharLength: 1,
|
||||
keys: ['short_name', 'short_names'],
|
||||
});
|
||||
export type SearchFnType = (query: string, count?: number) => Array<string>;
|
||||
|
||||
const fuseExactPrefix = new Fuse(data, {
|
||||
shouldSort: true,
|
||||
threshold: 0, // effectively a prefix search
|
||||
minMatchCharLength: 2,
|
||||
keys: ['short_name', 'short_names'],
|
||||
});
|
||||
export type SearchEmojiListType = ReadonlyArray<
|
||||
Pick<LocaleEmojiType, 'shortName' | 'rank' | 'tags'>
|
||||
>;
|
||||
|
||||
export function search(query: string, count = 0): Array<EmojiData> {
|
||||
// when we only have 2 characters, do an exact prefix match
|
||||
// to avoid matching on emoticon, like :-P
|
||||
const fuseIndex = query.length === 2 ? fuseExactPrefix : fuse;
|
||||
type CachedSearchFnType = Readonly<{
|
||||
localeEmoji: SearchEmojiListType;
|
||||
fn: SearchFnType;
|
||||
}>;
|
||||
|
||||
const results = fuseIndex
|
||||
.search(query.substr(0, 32))
|
||||
.map(result => result.item);
|
||||
let cachedSearchFn: CachedSearchFnType | undefined;
|
||||
|
||||
if (count) {
|
||||
return take(results, count);
|
||||
export function createSearch(localeEmoji: SearchEmojiListType): SearchFnType {
|
||||
if (cachedSearchFn && cachedSearchFn.localeEmoji === localeEmoji) {
|
||||
return cachedSearchFn.fn;
|
||||
}
|
||||
|
||||
return results;
|
||||
const fuse = new Fuse(localeEmoji, {
|
||||
shouldSort: true,
|
||||
threshold: 0.2,
|
||||
minMatchCharLength: 1,
|
||||
keys: ['shortName', 'tags'],
|
||||
includeScore: true,
|
||||
});
|
||||
|
||||
const fuseExactPrefix = new Fuse(localeEmoji, {
|
||||
shouldSort: true,
|
||||
threshold: 0, // effectively a prefix search
|
||||
minMatchCharLength: 2,
|
||||
keys: ['shortName', 'tags'],
|
||||
includeScore: true,
|
||||
});
|
||||
|
||||
const fn = (query: string, count = 0): Array<string> => {
|
||||
// when we only have 2 characters, do an exact prefix match
|
||||
// to avoid matching on emoticon, like :-P
|
||||
const fuseIndex = query.length === 2 ? fuseExactPrefix : fuse;
|
||||
|
||||
const rawResults = fuseIndex.search(query.substr(0, 32));
|
||||
|
||||
const rankedResults = rawResults.map(entry => {
|
||||
const rank = entry.item.rank || 1e9;
|
||||
|
||||
return {
|
||||
score: (entry.score ?? 0) + rank / localeEmoji.length,
|
||||
item: entry.item,
|
||||
};
|
||||
});
|
||||
|
||||
const results = rankedResults
|
||||
.sort((a, b) => a.score - b.score)
|
||||
.map(result => result.item.shortName);
|
||||
|
||||
if (count) {
|
||||
return take(results, count);
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
cachedSearchFn = { localeEmoji, fn };
|
||||
return fn;
|
||||
}
|
||||
|
||||
const shortNames = new Set([
|
||||
|
|
63
ts/hooks/useEmojiSearch.ts
Normal file
63
ts/hooks/useEmojiSearch.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import data from 'emoji-datasource';
|
||||
|
||||
import { createSearch } from '../components/emoji/lib';
|
||||
import type { SearchEmojiListType } from '../components/emoji/lib';
|
||||
import { drop } from '../util/drop';
|
||||
import * as log from '../logging/log';
|
||||
|
||||
const uninitialized: SearchEmojiListType = data.map(
|
||||
({ short_name: shortName, short_names: shortNames }) => {
|
||||
return {
|
||||
shortName,
|
||||
rank: 0,
|
||||
tags: shortNames,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const defaultSearch = createSearch(uninitialized);
|
||||
|
||||
export function useEmojiSearch(
|
||||
locale: string
|
||||
): ReturnType<typeof createSearch> {
|
||||
const searchRef = useRef(defaultSearch);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
async function run() {
|
||||
let result: SearchEmojiListType | undefined;
|
||||
try {
|
||||
result = await window.SignalContext.getLocalizedEmojiList(locale);
|
||||
} catch (error) {
|
||||
log.error(`Failed to get localized emoji list for ${locale}`, error);
|
||||
}
|
||||
|
||||
// Fallback
|
||||
if (result === undefined) {
|
||||
try {
|
||||
result = await window.SignalContext.getLocalizedEmojiList('en');
|
||||
} catch (error) {
|
||||
log.error('Failed to get fallback localized emoji list');
|
||||
}
|
||||
}
|
||||
|
||||
if (!canceled && result !== undefined) {
|
||||
searchRef.current = createSearch(result);
|
||||
}
|
||||
}
|
||||
drop(run());
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [locale]);
|
||||
|
||||
return useCallback((...args) => {
|
||||
return searchRef.current?.(...args);
|
||||
}, []);
|
||||
}
|
|
@ -10,13 +10,8 @@ import { Popper } from 'react-popper';
|
|||
import classNames from 'classnames';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { VirtualElement } from '@popperjs/core';
|
||||
import type { EmojiData } from '../../components/emoji/lib';
|
||||
import {
|
||||
search,
|
||||
convertShortName,
|
||||
isShortName,
|
||||
convertShortNameToData,
|
||||
} from '../../components/emoji/lib';
|
||||
import { convertShortName, isShortName } from '../../components/emoji/lib';
|
||||
import type { SearchFnType } from '../../components/emoji/lib';
|
||||
import { Emoji } from '../../components/emoji/Emoji';
|
||||
import type { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
|
||||
import { getBlotTextPartitions, matchBlotTextPartitions } from '../util';
|
||||
|
@ -29,10 +24,11 @@ type EmojiPickerOptions = {
|
|||
onPickEmoji: (emoji: EmojiPickDataType) => void;
|
||||
setEmojiPickerElement: (element: JSX.Element | null) => void;
|
||||
skinTone: number;
|
||||
search: SearchFnType;
|
||||
};
|
||||
|
||||
export class EmojiCompletion {
|
||||
results: Array<EmojiData>;
|
||||
results: Array<string>;
|
||||
|
||||
index: number;
|
||||
|
||||
|
@ -132,8 +128,8 @@ export class EmojiCompletion {
|
|||
const [leftTokenTextMatch, rightTokenTextMatch] = matchBlotTextPartitions(
|
||||
blot,
|
||||
index,
|
||||
/(?<=^|\s):([-+0-9a-zA-Z_]*)(:?)$/,
|
||||
/^([-+0-9a-zA-Z_]*):/
|
||||
/(?<=^|\s):([-+0-9\p{Alpha}_]*)(:?)$/iu,
|
||||
/^([-+0-9\p{Alpha}_]*):/iu
|
||||
);
|
||||
|
||||
if (leftTokenTextMatch) {
|
||||
|
@ -141,25 +137,17 @@ export class EmojiCompletion {
|
|||
|
||||
if (isSelfClosing || justPressedColon) {
|
||||
if (isShortName(leftTokenText)) {
|
||||
const emojiData = convertShortNameToData(
|
||||
leftTokenText,
|
||||
this.options.skinTone
|
||||
);
|
||||
|
||||
const numberOfColons = isSelfClosing ? 2 : 1;
|
||||
|
||||
if (emojiData) {
|
||||
this.insertEmoji(
|
||||
emojiData,
|
||||
range.index - leftTokenText.length - numberOfColons,
|
||||
leftTokenText.length + numberOfColons
|
||||
);
|
||||
return INTERCEPT;
|
||||
}
|
||||
} else {
|
||||
this.reset();
|
||||
return PASS_THROUGH;
|
||||
this.insertEmoji(
|
||||
leftTokenText,
|
||||
range.index - leftTokenText.length - numberOfColons,
|
||||
leftTokenText.length + numberOfColons
|
||||
);
|
||||
return INTERCEPT;
|
||||
}
|
||||
this.reset();
|
||||
return PASS_THROUGH;
|
||||
}
|
||||
|
||||
if (rightTokenTextMatch) {
|
||||
|
@ -167,19 +155,12 @@ export class EmojiCompletion {
|
|||
const tokenText = leftTokenText + rightTokenText;
|
||||
|
||||
if (isShortName(tokenText)) {
|
||||
const emojiData = convertShortNameToData(
|
||||
this.insertEmoji(
|
||||
tokenText,
|
||||
this.options.skinTone
|
||||
range.index - leftTokenText.length - 1,
|
||||
tokenText.length + 2
|
||||
);
|
||||
|
||||
if (emojiData) {
|
||||
this.insertEmoji(
|
||||
emojiData,
|
||||
range.index - leftTokenText.length - 1,
|
||||
tokenText.length + 2
|
||||
);
|
||||
return INTERCEPT;
|
||||
}
|
||||
return INTERCEPT;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,7 +169,7 @@ export class EmojiCompletion {
|
|||
return PASS_THROUGH;
|
||||
}
|
||||
|
||||
const showEmojiResults = search(leftTokenText, 10);
|
||||
const showEmojiResults = this.options.search(leftTokenText, 10);
|
||||
|
||||
if (showEmojiResults.length > 0) {
|
||||
this.results = showEmojiResults;
|
||||
|
@ -223,7 +204,7 @@ export class EmojiCompletion {
|
|||
const emoji = this.results[this.index];
|
||||
const [leafText] = this.getCurrentLeafTextPartitions();
|
||||
|
||||
const tokenTextMatch = /:([-+0-9a-z_]*)(:?)$/.exec(leafText);
|
||||
const tokenTextMatch = /:([-+0-9\p{Alpha}_]*)(:?)$/iu.exec(leafText);
|
||||
|
||||
if (tokenTextMatch == null) {
|
||||
return;
|
||||
|
@ -240,12 +221,12 @@ export class EmojiCompletion {
|
|||
}
|
||||
|
||||
insertEmoji(
|
||||
emojiData: EmojiData,
|
||||
shortName: string,
|
||||
index: number,
|
||||
range: number,
|
||||
withTrailingSpace = false
|
||||
): void {
|
||||
const emoji = convertShortName(emojiData.short_name, this.options.skinTone);
|
||||
const emoji = convertShortName(shortName, this.options.skinTone);
|
||||
|
||||
const delta = new Delta()
|
||||
.retain(index)
|
||||
|
@ -265,7 +246,7 @@ export class EmojiCompletion {
|
|||
}
|
||||
|
||||
this.options.onPickEmoji({
|
||||
shortName: emojiData.short_name,
|
||||
shortName,
|
||||
skinTone: this.options.skinTone,
|
||||
});
|
||||
|
||||
|
@ -344,17 +325,15 @@ export class EmojiCompletion {
|
|||
role="listbox"
|
||||
aria-expanded
|
||||
aria-activedescendant={`emoji-result--${
|
||||
emojiResults.length
|
||||
? emojiResults[emojiResultsIndex].short_name
|
||||
: ''
|
||||
emojiResults.length ? emojiResults[emojiResultsIndex] : ''
|
||||
}`}
|
||||
tabIndex={0}
|
||||
>
|
||||
{emojiResults.map((emoji, index) => (
|
||||
<button
|
||||
type="button"
|
||||
key={emoji.short_name}
|
||||
id={`emoji-result--${emoji.short_name}`}
|
||||
key={emoji}
|
||||
id={`emoji-result--${emoji}`}
|
||||
role="option button"
|
||||
aria-selected={emojiResultsIndex === index}
|
||||
onClick={() => {
|
||||
|
@ -369,12 +348,12 @@ export class EmojiCompletion {
|
|||
)}
|
||||
>
|
||||
<Emoji
|
||||
shortName={emoji.short_name}
|
||||
shortName={emoji}
|
||||
size={16}
|
||||
skinTone={this.options.skinTone}
|
||||
/>
|
||||
<div className="module-composition-input__suggestions__row__short-name">
|
||||
:{emoji.short_name}:
|
||||
:{emoji}:
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
|
100
ts/scripts/get-emoji-locales.ts
Normal file
100
ts/scripts/get-emoji-locales.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { writeFile, readFile } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { join } from 'node:path';
|
||||
import z from 'zod';
|
||||
import prettier from 'prettier';
|
||||
|
||||
import type { OptionalResourceType } from '../types/OptionalResource';
|
||||
import { OptionalResourcesDictSchema } from '../types/OptionalResource';
|
||||
|
||||
const MANIFEST_URL =
|
||||
'https://updates.signal.org/dynamic/android/emoji/search/manifest.json';
|
||||
|
||||
const ManifestSchema = z.object({
|
||||
version: z.number(),
|
||||
languages: z.string().array(),
|
||||
languageToSmartlingLocale: z.record(z.string(), z.string()),
|
||||
});
|
||||
|
||||
async function fetchJSON(url: string): Promise<unknown> {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch ${url}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const manifest = ManifestSchema.parse(await fetchJSON(MANIFEST_URL));
|
||||
|
||||
// eslint-disable-next-line dot-notation
|
||||
manifest.languageToSmartlingLocale['zh_TW'] = 'zh-Hant';
|
||||
// eslint-disable-next-line dot-notation
|
||||
manifest.languageToSmartlingLocale['sr'] = 'sr';
|
||||
|
||||
const extraResources = new Map<string, OptionalResourceType>();
|
||||
|
||||
await Promise.all(
|
||||
manifest.languages.map(async language => {
|
||||
const langUrl =
|
||||
'https://updates.signal.org/static/android/' +
|
||||
`emoji/search/${manifest.version}/${language}.json`;
|
||||
|
||||
const res = await fetch(langUrl);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch ${langUrl}`);
|
||||
}
|
||||
|
||||
const data = Buffer.from(await res.arrayBuffer());
|
||||
|
||||
const digest = createHash('sha512').update(data).digest('base64');
|
||||
|
||||
let locale = manifest.languageToSmartlingLocale[language] ?? language;
|
||||
locale = locale.replace(/_/g, '-');
|
||||
|
||||
const pinnedUrl =
|
||||
'https://updates2.signal.org/static/android/' +
|
||||
`emoji/search/${manifest.version}/${language}.json`;
|
||||
|
||||
extraResources.set(locale, {
|
||||
url: pinnedUrl,
|
||||
size: data.length,
|
||||
digest,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const resourcesPath = join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'build',
|
||||
'optional-resources.json'
|
||||
);
|
||||
const resources = OptionalResourcesDictSchema.parse(
|
||||
JSON.parse(await readFile(resourcesPath, 'utf8'))
|
||||
);
|
||||
|
||||
for (const [locale, resource] of extraResources) {
|
||||
resources[`emoji-index-${locale}.json`] = resource;
|
||||
}
|
||||
|
||||
const prettierConfig = await prettier.resolveConfig(
|
||||
join(__dirname, '..', '..', 'build')
|
||||
);
|
||||
|
||||
const output = prettier.format(JSON.stringify(resources, null, 2), {
|
||||
...prettierConfig,
|
||||
filepath: resourcesPath,
|
||||
});
|
||||
await writeFile(resourcesPath, output);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
|
@ -5,7 +5,7 @@ import { assert } from 'chai';
|
|||
import sinon from 'sinon';
|
||||
|
||||
import { EmojiCompletion } from '../../../quill/emoji/completion';
|
||||
import type { EmojiData } from '../../../components/emoji/lib';
|
||||
import { createSearch } from '../../../components/emoji/lib';
|
||||
|
||||
describe('emojiCompletion', () => {
|
||||
let emojiCompletion: EmojiCompletion;
|
||||
|
@ -27,6 +27,10 @@ describe('emojiCompletion', () => {
|
|||
onPickEmoji: sinon.stub(),
|
||||
setEmojiPickerElement: sinon.stub(),
|
||||
skinTone: 0,
|
||||
search: createSearch([
|
||||
{ shortName: 'smile', tags: [], rank: 0 },
|
||||
{ shortName: 'smile_cat', tags: [], rank: 0 },
|
||||
]),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -52,13 +56,12 @@ describe('emojiCompletion', () => {
|
|||
|
||||
describe('onTextChange', () => {
|
||||
let insertEmojiStub: sinon.SinonStub<
|
||||
[EmojiData, number, number, (boolean | undefined)?],
|
||||
[string, number, number, (boolean | undefined)?],
|
||||
void
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
emojiCompletion.results = [{ short_name: 'joy' } as any];
|
||||
emojiCompletion.results = ['joy'];
|
||||
emojiCompletion.index = 5;
|
||||
insertEmojiStub = sinon
|
||||
.stub(emojiCompletion, 'insertEmoji')
|
||||
|
@ -165,7 +168,7 @@ describe('emojiCompletion', () => {
|
|||
});
|
||||
|
||||
it('stores the results and renders', () => {
|
||||
assert.equal(emojiCompletion.results.length, 10);
|
||||
assert.equal(emojiCompletion.results.length, 2);
|
||||
assert.equal((emojiCompletion.render as sinon.SinonStub).called, true);
|
||||
});
|
||||
});
|
||||
|
@ -193,7 +196,7 @@ describe('emojiCompletion', () => {
|
|||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [emoji, index, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile');
|
||||
assert.equal(emoji, 'smile');
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, 7);
|
||||
});
|
||||
|
@ -222,7 +225,7 @@ describe('emojiCompletion', () => {
|
|||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [emoji, index, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile');
|
||||
assert.equal(emoji, 'smile');
|
||||
assert.equal(index, 7);
|
||||
assert.equal(range, 7);
|
||||
});
|
||||
|
@ -282,7 +285,7 @@ describe('emojiCompletion', () => {
|
|||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [emoji, index, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile');
|
||||
assert.equal(emoji, 'smile');
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, validEmoji.length);
|
||||
});
|
||||
|
@ -331,7 +334,7 @@ describe('emojiCompletion', () => {
|
|||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [emoji, index, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile');
|
||||
assert.equal(emoji, 'smile');
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, 6);
|
||||
});
|
||||
|
@ -345,17 +348,12 @@ describe('emojiCompletion', () => {
|
|||
|
||||
describe('completeEmoji', () => {
|
||||
let insertEmojiStub: sinon.SinonStub<
|
||||
[EmojiData, number, number, (boolean | undefined)?],
|
||||
[string, number, number, (boolean | undefined)?],
|
||||
void
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
emojiCompletion.results = [
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ short_name: 'smile' } as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ short_name: 'smile_cat' } as any,
|
||||
];
|
||||
emojiCompletion.results = ['smile', 'smile_cat'];
|
||||
emojiCompletion.index = 1;
|
||||
insertEmojiStub = sinon.stub(emojiCompletion, 'insertEmoji');
|
||||
});
|
||||
|
@ -381,7 +379,7 @@ describe('emojiCompletion', () => {
|
|||
it('inserts the currently selected emoji at the current cursor position', () => {
|
||||
const [emoji, insertIndex, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile_cat');
|
||||
assert.equal(emoji, 'smile_cat');
|
||||
assert.equal(insertIndex, 0);
|
||||
assert.equal(range, text.length);
|
||||
});
|
||||
|
|
21
ts/types/OptionalResource.ts
Normal file
21
ts/types/OptionalResource.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import z from 'zod';
|
||||
|
||||
export const OptionalResourceSchema = z.object({
|
||||
digest: z.string(),
|
||||
url: z.string(),
|
||||
size: z.number(),
|
||||
});
|
||||
|
||||
export type OptionalResourceType = z.infer<typeof OptionalResourceSchema>;
|
||||
|
||||
export const OptionalResourcesDictSchema = z.record(
|
||||
z.string(),
|
||||
OptionalResourceSchema
|
||||
);
|
||||
|
||||
export type OptionalResourcesDictType = z.infer<
|
||||
typeof OptionalResourcesDictSchema
|
||||
>;
|
17
ts/types/emoji.ts
Normal file
17
ts/types/emoji.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import z from 'zod';
|
||||
|
||||
export const LocaleEmojiSchema = z.object({
|
||||
emoji: z.string(),
|
||||
shortName: z.string(),
|
||||
tags: z.string().array(),
|
||||
rank: z.number(),
|
||||
});
|
||||
|
||||
export type LocaleEmojiType = z.infer<typeof LocaleEmojiSchema>;
|
||||
|
||||
export const LocaleEmojiListSchema = LocaleEmojiSchema.array();
|
||||
|
||||
export type LocaleEmojiListType = z.infer<typeof LocaleEmojiListSchema>;
|
|
@ -3691,6 +3691,14 @@
|
|||
"updated": "2022-06-14T22:04:43.988Z",
|
||||
"reasonDetail": "Handling outside click"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/hooks/useEmojiSearch.ts",
|
||||
"line": " const searchRef = useRef(defaultSearch);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-03-16T18:34:38.165Z",
|
||||
"reasonDetail": "Quill requires an immutable reference to the search function"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/hooks/useIntersectionObserver.ts",
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
createCachedIntl as createCachedIntlMain,
|
||||
setupI18n as setupI18nMain,
|
||||
} from './setupI18nMain';
|
||||
import type { SetupI18nOptionsType } from './setupI18nMain';
|
||||
import { strictAssert } from './assert';
|
||||
|
||||
export { isLocaleMessageType } from './setupI18nMain';
|
||||
|
@ -30,7 +31,8 @@ export function createCachedIntl(
|
|||
|
||||
export function setupI18n(
|
||||
locale: string,
|
||||
messages: LocaleMessagesType
|
||||
messages: LocaleMessagesType,
|
||||
options: Omit<SetupI18nOptionsType, 'renderEmojify'> = {}
|
||||
): LocalizerType {
|
||||
return setupI18nMain(locale, messages, { renderEmojify });
|
||||
return setupI18nMain(locale, messages, { ...options, renderEmojify });
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { initialize as initializeLogging } from '../logging/set_up_renderer_logg
|
|||
import { MinimalSignalContext } from './minimalContext';
|
||||
import type { LocaleDirection } from '../../app/locale';
|
||||
import type { HourCyclePreference } from '../types/I18N';
|
||||
import type { LocaleEmojiListType } from '../types/emoji';
|
||||
|
||||
strictAssert(Boolean(window.SignalContext), 'context must be defined');
|
||||
|
||||
|
@ -48,6 +49,9 @@ export type MinimalSignalContextType = {
|
|||
getResolvedMessagesLocale: () => string;
|
||||
getPreferredSystemLocales: () => Array<string>;
|
||||
getLocaleOverride: () => string | null;
|
||||
getLocalizedEmojiList: (
|
||||
locale: string
|
||||
) => Promise<LocaleEmojiListType | undefined>;
|
||||
getMainWindowStats: () => Promise<MainWindowStatsType>;
|
||||
getMenuOptions: () => Promise<MenuOptionsType>;
|
||||
getNodeVersion: () => string;
|
||||
|
|
|
@ -5,6 +5,8 @@ import type { MenuItemConstructorOptions } from 'electron';
|
|||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import type { MenuOptionsType } from '../types/menu';
|
||||
import type { LocaleEmojiListType } from '../types/emoji';
|
||||
import { LocaleEmojiListSchema } from '../types/emoji';
|
||||
import type { MainWindowStatsType, MinimalSignalContextType } from './context';
|
||||
import { activeWindowService } from '../context/activeWindowService';
|
||||
import { config } from '../context/config';
|
||||
|
@ -18,6 +20,8 @@ import {
|
|||
} from '../context/localeMessages';
|
||||
import { waitForSettingsChange } from '../context/waitForSettingsChange';
|
||||
|
||||
const emojiListCache = new Map<string, LocaleEmojiListType>();
|
||||
|
||||
export const MinimalSignalContext: MinimalSignalContextType = {
|
||||
activeWindowService,
|
||||
config,
|
||||
|
@ -40,6 +44,21 @@ export const MinimalSignalContext: MinimalSignalContextType = {
|
|||
async getMenuOptions(): Promise<MenuOptionsType> {
|
||||
return ipcRenderer.invoke('getMenuOptions');
|
||||
},
|
||||
async getLocalizedEmojiList(locale: string) {
|
||||
const cached = emojiListCache.get(locale);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const buf = await ipcRenderer.invoke(
|
||||
'OptionalResourceService:getData',
|
||||
`emoji-index-${locale}.json`
|
||||
);
|
||||
const json = JSON.parse(Buffer.from(buf).toString());
|
||||
const result = LocaleEmojiListSchema.parse(json);
|
||||
emojiListCache.set(locale, result);
|
||||
return result;
|
||||
},
|
||||
getI18nAvailableLocales: () => config.availableLocales,
|
||||
getI18nLocale: () => config.resolvedTranslationsLocale,
|
||||
getI18nLocaleMessages: () => localeMessages,
|
||||
|
|
Loading…
Add table
Reference in a new issue