From c08cd6d62a128caf6509d13445829c767f2ff791 Mon Sep 17 00:00:00 2001 From: Echo Date: Mon, 17 Nov 2025 16:34:18 +0100 Subject: [PATCH] Emoji: Fix path resolution for emoji worker (#36897) --- .../mastodon/features/emoji/index.ts | 32 ++++++-- .../mastodon/features/emoji/loader.ts | 82 ++++++++++--------- .../mastodon/features/emoji/render.test.ts | 2 +- .../mastodon/features/emoji/worker.ts | 25 ++++-- app/javascript/mastodon/main.tsx | 2 +- 5 files changed, 83 insertions(+), 60 deletions(-) diff --git a/app/javascript/mastodon/features/emoji/index.ts b/app/javascript/mastodon/features/emoji/index.ts index 11ee26aac2..4b0f79133c 100644 --- a/app/javascript/mastodon/features/emoji/index.ts +++ b/app/javascript/mastodon/features/emoji/index.ts @@ -1,6 +1,7 @@ import { initialState } from '@/mastodon/initial_state'; import { toSupportedLocale } from './locale'; +import type { LocaleOrCustom } from './types'; import { emojiLogger } from './utils'; // eslint-disable-next-line import/default -- Importing via worker loader. import EmojiWorker from './worker?worker&inline'; @@ -24,19 +25,17 @@ export function initializeEmoji() { } if (worker) { - // Assign worker to const to make TS happy inside the event listener. - const thisWorker = worker; const timeoutId = setTimeout(() => { log('worker is not ready after timeout'); worker = null; void fallbackLoad(); }, WORKER_TIMEOUT); - thisWorker.addEventListener('message', (event: MessageEvent) => { + worker.addEventListener('message', (event: MessageEvent) => { const { data: message } = event; if (message === 'ready') { log('worker ready, loading data'); clearTimeout(timeoutId); - thisWorker.postMessage('custom'); + messageWorker('custom'); void loadEmojiLocale(userLocale); // Load English locale as well, because people are still used to // using it from before we supported other locales. @@ -55,20 +54,35 @@ export function initializeEmoji() { async function fallbackLoad() { log('falling back to main thread for loading'); const { importCustomEmojiData } = await import('./loader'); - await importCustomEmojiData(); + const emojis = await importCustomEmojiData(); + if (emojis) { + log('loaded %d custom emojis', emojis.length); + } await loadEmojiLocale(userLocale); if (userLocale !== 'en') { await loadEmojiLocale('en'); } } -export async function loadEmojiLocale(localeString: string) { +async function loadEmojiLocale(localeString: string) { const locale = toSupportedLocale(localeString); + const { importEmojiData, localeToPath } = await import('./loader'); if (worker) { - worker.postMessage(locale); + const path = await localeToPath(locale); + log('asking worker to load locale %s from %s', locale, path); + messageWorker(locale, path); } else { - const { importEmojiData } = await import('./loader'); - await importEmojiData(locale); + const emojis = await importEmojiData(locale); + if (emojis) { + log('loaded %d emojis to locale %s', emojis.length, locale); + } + } +} + +function messageWorker(locale: LocaleOrCustom, path?: string) { + if (!worker) { + return; } + worker.postMessage({ locale, path }); } diff --git a/app/javascript/mastodon/features/emoji/loader.ts b/app/javascript/mastodon/features/emoji/loader.ts index 330c5e6a2a..7251559d6b 100644 --- a/app/javascript/mastodon/features/emoji/loader.ts +++ b/app/javascript/mastodon/features/emoji/loader.ts @@ -8,44 +8,64 @@ import { putLatestEtag, } from './database'; import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; -import type { CustomEmojiData, LocaleOrCustom } from './types'; -import { emojiLogger } from './utils'; +import type { CustomEmojiData } from './types'; -const log = emojiLogger('loader'); - -export async function importEmojiData(localeString: string) { +export async function importEmojiData(localeString: string, path?: string) { const locale = toSupportedLocale(localeString); - const emojis = await fetchAndCheckEtag(locale); + + // Validate the provided path. + if (path && !/^[/a-z]*\/packs\/assets\/compact-\w+\.json$/.test(path)) { + throw new Error('Invalid path for emoji data'); + } else { + // Otherwise get the path if not provided. + path ??= await localeToPath(locale); + } + + const emojis = await fetchAndCheckEtag(locale, path); if (!emojis) { return; } const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis); - log('loaded %d for %s locale', flattenedEmojis.length, locale); await putEmojiData(flattenedEmojis, locale); + return flattenedEmojis; } export async function importCustomEmojiData() { - const emojis = await fetchAndCheckEtag('custom'); + const emojis = await fetchAndCheckEtag( + 'custom', + '/api/v1/custom_emojis', + ); if (!emojis) { return; } - log('loaded %d custom emojis', emojis.length); await putCustomEmojiData(emojis); + return emojis; +} + +const modules = import.meta.glob( + '../../../../../node_modules/emojibase-data/**/compact.json', + { + query: '?url', + import: 'default', + }, +); + +export function localeToPath(locale: Locale) { + const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`; + if (!modules[key] || typeof modules[key] !== 'function') { + throw new Error(`Unsupported locale: ${locale}`); + } + return modules[key](); } -async function fetchAndCheckEtag( - localeOrCustom: LocaleOrCustom, +export async function fetchAndCheckEtag( + localeString: string, + path: string, ): Promise { - const locale = toSupportedLocaleOrCustom(localeOrCustom); + const locale = toSupportedLocaleOrCustom(localeString); // Use location.origin as this script may be loaded from a CDN domain. - const url = new URL(location.origin); - if (locale === 'custom') { - url.pathname = '/api/v1/custom_emojis'; - } else { - const modulePath = await localeToPath(locale); - url.pathname = modulePath; - } + const url = new URL(path, location.origin); const oldEtag = await loadLatestEtag(locale); const response = await fetch(url, { @@ -60,38 +80,20 @@ async function fetchAndCheckEtag( } if (!response.ok) { throw new Error( - `Failed to fetch emoji data for ${localeOrCustom}: ${response.statusText}`, + `Failed to fetch emoji data for ${locale}: ${response.statusText}`, ); } const data = (await response.json()) as ResultType; if (!Array.isArray(data)) { - throw new Error( - `Unexpected data format for ${localeOrCustom}: expected an array`, - ); + throw new Error(`Unexpected data format for ${locale}: expected an array`); } // Store the ETag for future requests const etag = response.headers.get('ETag'); if (etag) { - await putLatestEtag(etag, localeOrCustom); + await putLatestEtag(etag, localeString); } return data; } - -const modules = import.meta.glob( - '../../../../../node_modules/emojibase-data/**/compact.json', - { - query: '?url', - import: 'default', - }, -); - -function localeToPath(locale: Locale) { - const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`; - if (!modules[key] || typeof modules[key] !== 'function') { - throw new Error(`Unsupported locale: ${locale}`); - } - return modules[key](); -} diff --git a/app/javascript/mastodon/features/emoji/render.test.ts b/app/javascript/mastodon/features/emoji/render.test.ts index 05dbc388c4..3c96cbfb55 100644 --- a/app/javascript/mastodon/features/emoji/render.test.ts +++ b/app/javascript/mastodon/features/emoji/render.test.ts @@ -162,7 +162,7 @@ describe('loadEmojiDataToState', () => { const dbCall = vi .spyOn(db, 'loadEmojiByHexcode') .mockRejectedValue(new db.LocaleNotLoadedError('en')); - vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce(); + vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce(undefined); const consoleCall = vi .spyOn(console, 'warn') .mockImplementationOnce(() => null); diff --git a/app/javascript/mastodon/features/emoji/worker.ts b/app/javascript/mastodon/features/emoji/worker.ts index 6fb7d36e93..5360484d77 100644 --- a/app/javascript/mastodon/features/emoji/worker.ts +++ b/app/javascript/mastodon/features/emoji/worker.ts @@ -1,18 +1,25 @@ -import { importEmojiData, importCustomEmojiData } from './loader'; +import { importCustomEmojiData, importEmojiData } from './loader'; addEventListener('message', handleMessage); self.postMessage('ready'); // After the worker is ready, notify the main thread -function handleMessage(event: MessageEvent) { - const { data: locale } = event; - void loadData(locale); +function handleMessage(event: MessageEvent<{ locale: string; path?: string }>) { + const { + data: { locale, path }, + } = event; + void loadData(locale, path); } -async function loadData(locale: string) { - if (locale !== 'custom') { - await importEmojiData(locale); +async function loadData(locale: string, path?: string) { + let importCount: number | undefined; + if (locale === 'custom') { + importCount = (await importCustomEmojiData())?.length; + } else if (path) { + importCount = (await importEmojiData(locale, path))?.length; } else { - await importCustomEmojiData(); + throw new Error('Path is required for loading locale emoji data'); + } + if (importCount) { + self.postMessage(`loaded ${importCount} emojis into ${locale}`); } - self.postMessage(`loaded ${locale}`); } diff --git a/app/javascript/mastodon/main.tsx b/app/javascript/mastodon/main.tsx index f89baf66cd..249baf65fc 100644 --- a/app/javascript/mastodon/main.tsx +++ b/app/javascript/mastodon/main.tsx @@ -9,7 +9,6 @@ import { me, reduceMotion } from 'mastodon/initial_state'; import ready from 'mastodon/ready'; import { store } from 'mastodon/store'; -import { initializeEmoji } from './features/emoji'; import { isProduction, isDevelopment } from './utils/environment'; function main() { @@ -30,6 +29,7 @@ function main() { }); } + const { initializeEmoji } = await import('./features/emoji/index'); initializeEmoji(); const root = createRoot(mountNode);