Add localized emoji search

This commit is contained in:
Fedor Indutny 2024-03-21 09:35:54 -07:00 committed by GitHub
parent ce0fb22041
commit e90553b3b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 878 additions and 97 deletions

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

View File

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

View 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=="
}
}

View File

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

View File

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

View File

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

View File

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

View 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);
}, []);
}

View File

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

View 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);
});

View File

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

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

View File

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

View File

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

View File

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

View File

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