From 4b67d8e34fda7dce8150d6db103c0292de530df3 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 12 Jun 2026 17:30:27 +0200 Subject: [PATCH] [Glitch] Emoji: Add back to state Port 963a54664804e60ea8e30795090a0f20de7a48dc to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/actions/importer/emoji.ts | 4 - .../account_edit/modals/fields_modals.tsx | 2 +- .../components/emoji_picker_dropdown.jsx | 5 +- .../glitch/features/emoji/emoji_picker.tsx | 12 +- .../flavours/glitch/features/emoji/index.ts | 61 ++++--- .../flavours/glitch/features/emoji/mode.ts | 2 - .../flavours/glitch/features/emoji/picker.ts | 152 +++++++----------- .../flavours/glitch/features/emoji/render.ts | 2 +- .../flavours/glitch/features/emoji/types.ts | 11 +- .../flavours/glitch/features/emoji/worker.ts | 10 +- .../flavours/glitch/hooks/useCustomEmojis.ts | 53 +++--- .../flavours/glitch/reducers/slices/emojis.ts | 71 ++++++++ .../flavours/glitch/reducers/slices/index.ts | 2 + 13 files changed, 217 insertions(+), 170 deletions(-) create mode 100644 app/javascript/flavours/glitch/reducers/slices/emojis.ts diff --git a/app/javascript/flavours/glitch/actions/importer/emoji.ts b/app/javascript/flavours/glitch/actions/importer/emoji.ts index beccbcebae..8bc6bbbcb3 100644 --- a/app/javascript/flavours/glitch/actions/importer/emoji.ts +++ b/app/javascript/flavours/glitch/actions/importer/emoji.ts @@ -22,10 +22,6 @@ export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) { await clearCache('custom'); await loadCustomEmoji(); - const { reloadCustomEmojis } = - await import('@/flavours/glitch/features/emoji/picker'); - await reloadCustomEmojis(); - log('Custom emojis updated, reloaded cache and picker data.'); } } diff --git a/app/javascript/flavours/glitch/features/account_edit/modals/fields_modals.tsx b/app/javascript/flavours/glitch/features/account_edit/modals/fields_modals.tsx index 702122f0fd..ecc043cef5 100644 --- a/app/javascript/flavours/glitch/features/account_edit/modals/fields_modals.tsx +++ b/app/javascript/flavours/glitch/features/account_edit/modals/fields_modals.tsx @@ -140,7 +140,7 @@ export const EditFieldModal = forwardRef< const customEmojis = useCustomEmojis(); const customEmojiCodes = useMemo( - () => Object.keys(customEmojis ?? {}), + () => Object.keys(customEmojis), [customEmojis], ); const checkField = useCallback( diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx index 9beaabaaac..6d685ba32f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx @@ -323,10 +323,7 @@ class EmojiPickerDropdown extends PureComponent { EmojiPickerAsync().then(EmojiMart => { EmojiPicker = EmojiMart.Picker; Emoji = EmojiMart.Emoji; - - void EmojiMart.loadCustomEmojiData().then(() => { - this.setState({ loading: false }); - }); + this.setState({ loading: false }); }).catch(() => { this.setState({ loading: false, active: false }); }); diff --git a/app/javascript/flavours/glitch/features/emoji/emoji_picker.tsx b/app/javascript/flavours/glitch/features/emoji/emoji_picker.tsx index 39fc548fd6..5a8ddb7ff1 100644 --- a/app/javascript/flavours/glitch/features/emoji/emoji_picker.tsx +++ b/app/javascript/flavours/glitch/features/emoji/emoji_picker.tsx @@ -13,8 +13,6 @@ import { usePickerEmojis } from './picker'; const backgroundImageFnDefault = () => `${assetHost}/emoji/sheet_16_0.png`; -export { fetchCustomEmojiData as loadCustomEmojiData } from './picker'; - export const Picker: FC = ({ set = 'twitter', sheetSize = 32, @@ -24,17 +22,13 @@ export const Picker: FC = ({ ...props }) => { const { mode } = useEmojiAppState(); - const { customCategories, customEmojis } = usePickerEmojis(); - - if (!customEmojis) { - return null; - } + const { categories, emojis } = usePickerEmojis(); return ( ) => { - const { data: message } = event; + tempWorker.addEventListener( + 'message', + (event: MessageEvent) => { + const { data: message } = event; - worker ??= tempWorker; - clearTimeout(timeoutId); + worker ??= tempWorker; + clearTimeout(timeoutId); - if (message !== 'ready') { - workerLog(message); - return; - } + const { type } = message; + if (type === 'log') { + workerLog(message.message); + } else if (type === 'done' && message.storeName === 'custom') { + void loadEmojisToStore(); + } - const debugValue = localStorage.getItem('debug'); - if (debugValue) { - messageWorker({ type: 'debug', debugValue }); - } + if (type !== 'ready') { + return; // Exit for other messages. + } - workerLog('loading data'); - messageWorker(userLocale); - messageWorker('custom'); - messageWorker('shortcodes'); - }); + const debugValue = localStorage.getItem('debug'); + if (debugValue) { + messageWorker({ type: 'debug', debugValue }); + } + + workerLog('loading data'); + messageWorker(userLocale); + messageWorker('custom'); + messageWorker('shortcodes'); + void loadEmojisToStore(); + }, + ); } async function fallbackLoad() { @@ -71,8 +80,8 @@ async function fallbackLoad() { const customEmojis = await importCustomEmojiData(); if (customEmojis && customEmojis.length > 0) { log('loaded %d custom emojis', customEmojis.length); - await reloadCustomEmojis(); } + const shortcodes = await importLegacyShortcodes(); if (shortcodes?.length) { log('loaded %d legacy shortcodes', shortcodes.length); @@ -82,6 +91,7 @@ async function fallbackLoad() { if (emojis) { log('loaded %d emojis to locale %s', emojis.length, userLocale); } + await loadEmojisToStore(); } export async function loadCustomEmoji() { @@ -92,9 +102,9 @@ export async function loadCustomEmoji() { const emojis = await importCustomEmojiData(); if (emojis && emojis.length > 0) { log('loaded %d custom emojis', emojis.length); - await reloadCustomEmojis(); } } + await loadEmojisToStore(); } function messageWorker(data: EmojiWorkerMessage | string) { @@ -110,3 +120,14 @@ function messageWorker(data: EmojiWorkerMessage | string) { worker.postMessage(data); } } + +async function loadEmojisToStore() { + const { store } = await import('@/flavours/glitch/store'); + const { loadCustomEmojis, loadLocale } = + await import('@/flavours/glitch/reducers/slices/emojis'); + + loadLocale(userLocale); + await store.dispatch(loadCustomEmojis()); + + log('loaded emoji data into store'); +} diff --git a/app/javascript/flavours/glitch/features/emoji/mode.ts b/app/javascript/flavours/glitch/features/emoji/mode.ts index 9cd45621cc..769e2859b8 100644 --- a/app/javascript/flavours/glitch/features/emoji/mode.ts +++ b/app/javascript/flavours/glitch/features/emoji/mode.ts @@ -27,7 +27,6 @@ export function useEmojiAppState(): EmojiAppState { return { currentLocale: locale, - locales: [locale], mode, darkTheme: isDarkMode(), assetHost, @@ -64,7 +63,6 @@ export function getEmojiAppState(): EmojiAppState { return { currentLocale, - locales: [currentLocale], mode: determineEmojiMode(emojiStyle), darkTheme: isDarkMode(), assetHost, diff --git a/app/javascript/flavours/glitch/features/emoji/picker.ts b/app/javascript/flavours/glitch/features/emoji/picker.ts index 9e131f7036..f9d3d84cb3 100644 --- a/app/javascript/flavours/glitch/features/emoji/picker.ts +++ b/app/javascript/flavours/glitch/features/emoji/picker.ts @@ -1,83 +1,18 @@ -import { useEffect, useState } from 'react'; - import type { CategoryName, CustomEmoji } from 'emoji-mart'; import { autoPlayGif } from '@/flavours/glitch/initial_state'; +import { + createAppSelector, + useAppSelector, +} from '@/flavours/glitch/store/typed_functions'; import { createLimitedCache } from '@/flavours/glitch/utils/cache'; import { emojiLogger } from './utils'; const log = emojiLogger('picker'); -let customEmojis: CustomEmoji[] | null = null; -let customCategories = [ - 'recent', - 'people', - 'nature', - 'foods', - 'activity', - 'places', - 'objects', - 'symbols', - 'flags', -] as CategoryName[]; - const searchCache = createLimitedCache({ maxSize: 10, log }); -export async function fetchCustomEmojiData() { - if (customEmojis !== null) { - return customEmojis; - } - - const { loadAllCustomEmoji } = await import('./database'); - const emojisRaw = await loadAllCustomEmoji(); - - // If it returns null then custom emojis aren't even loaded yet. - if (emojisRaw === null) { - return []; - } - - // If it's empty, then they are loaded but there aren't any. - if (emojisRaw.length === 0) { - customEmojis = []; - return customEmojis; - } - - const categories = new Set(['custom']); - const emojis = []; - for (const emoji of emojisRaw) { - const name = emoji.shortcode.replaceAll(':', ''); - emojis.push({ - name, - id: name, - custom: true, - short_names: [name], - imageUrl: autoPlayGif ? emoji.url : emoji.static_url, - customCategory: emoji.category, - }); - - if (emoji.category) { - categories.add(`custom-${emoji.category}`); - } - } - - customEmojis = emojis.toSorted((a, b) => { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); - }); - customCategories = customCategories.toSpliced( - 1, - 0, - ...(Array.from(categories).toSorted() as CategoryName[]), - ); - log( - 'loaded %d custom emojis in %d categories', - customEmojis.length, - categories.size, - ); - - return customEmojis; -} - type LegacyEmoji = | { id: string; custom?: false; native: string } | { @@ -85,16 +20,6 @@ type LegacyEmoji = custom: true; }; -export async function reloadCustomEmojis() { - customEmojis = null; - - const { loadEmojisIntoCache } = - await import('@/flavours/glitch/hooks/useCustomEmojis'); - - await Promise.all([fetchCustomEmojiData(), loadEmojisIntoCache()]); - searchCache.clear(); -} - // Replicates the old legacy search function. export async function emojiMartSearch( token: string, @@ -127,19 +52,64 @@ export async function emojiMartSearch( return legacyResults; } -export function usePickerEmojis() { - const [, setLoaded] = useState(customEmojis !== null); +const defaultCategories = [ + 'people', + 'nature', + 'foods', + 'activity', + 'places', + 'objects', + 'symbols', + 'flags', +] as CategoryName[]; - useEffect(() => { - if (customEmojis === null) { - void fetchCustomEmojiData().then(() => { - setLoaded(true); - }); +const selectPickerData = createAppSelector( + [(state) => state.emojis.custom, (state) => state.emojis.customCategories], + (emojis, categories) => { + // Create a map of shortcode to category name. + const categoryMap = new Map(); + for (const category in categories) { + const catEmojis = categories[category]; + if (!catEmojis?.length) { + continue; + } + for (const shortcode of catEmojis) { + categoryMap.set(shortcode, category); + } } - }, []); - return { - customEmojis, - customCategories, - }; + const customEmojis: CustomEmoji[] = []; + for (const shortcode in emojis) { + const emoji = emojis[shortcode]; + if (!emoji) { + continue; + } + + customEmojis.push({ + name: shortcode, + id: shortcode, + custom: true, + short_names: [shortcode], + imageUrl: autoPlayGif ? emoji.url : emoji.static_url, + customCategory: categoryMap.get(shortcode), + } as CustomEmoji); + } + + searchCache.clear(); + log('regenerated the picker data'); + + return { + emojis: customEmojis, + categories: [ + 'recent', + 'custom', + ...Object.keys(categories).toSorted(), + ...defaultCategories, + ] as CategoryName[], + }; + }, +); + +export function usePickerEmojis() { + return useAppSelector(selectPickerData); } diff --git a/app/javascript/flavours/glitch/features/emoji/render.ts b/app/javascript/flavours/glitch/features/emoji/render.ts index affa8e0f16..e5ac5bcc6e 100644 --- a/app/javascript/flavours/glitch/features/emoji/render.ts +++ b/app/javascript/flavours/glitch/features/emoji/render.ts @@ -109,7 +109,7 @@ export async function updateHtmlWithEmoji({ }: { element: Element; locale: string; -} & Omit) { +} & Omit) { if (mode === EMOJI_MODE_NATIVE) { return; } diff --git a/app/javascript/flavours/glitch/features/emoji/types.ts b/app/javascript/flavours/glitch/features/emoji/types.ts index ea27a8f699..a5c408903e 100644 --- a/app/javascript/flavours/glitch/features/emoji/types.ts +++ b/app/javascript/flavours/glitch/features/emoji/types.ts @@ -28,7 +28,6 @@ export type CacheKey = | LocaleWithShortcodes; export interface EmojiAppState { - locales: Locale[]; currentLocale: Locale; mode: EmojiMode; darkTheme: boolean; @@ -82,10 +81,20 @@ export type ExtraCustomEmojiMap = Record< >; export type EmojiWorkerMessage = + | { type: 'ready' } | { type: 'load'; storeName: string; } + | { + type: 'done'; + storeName: string; + importCount: number; + } + | { + type: 'log'; + message: string; + } | { type: 'debug'; debugValue: string; diff --git a/app/javascript/flavours/glitch/features/emoji/worker.ts b/app/javascript/flavours/glitch/features/emoji/worker.ts index d4f002daf6..49b7ce45f1 100644 --- a/app/javascript/flavours/glitch/features/emoji/worker.ts +++ b/app/javascript/flavours/glitch/features/emoji/worker.ts @@ -9,13 +9,13 @@ import { import type { EmojiWorkerMessage } from './types'; addEventListener('message', handleMessage); -self.postMessage('ready'); // After the worker is ready, notify the main thread +self.postMessage({ type: 'ready' } satisfies EmojiWorkerMessage); // After the worker is ready, notify the main thread function handleMessage(event: MessageEvent) { const { data } = event; if (data.type === 'debug') { debug.enable(data.debugValue); - } else { + } else if (data.type === 'load') { void loadData(data.storeName); } } @@ -31,6 +31,10 @@ async function loadData(storeName: string) { } if (importCount) { - self.postMessage(`loaded ${importCount} emojis into ${storeName}`); + self.postMessage({ + type: 'done', + storeName, + importCount, + } satisfies EmojiWorkerMessage); } } diff --git a/app/javascript/flavours/glitch/hooks/useCustomEmojis.ts b/app/javascript/flavours/glitch/hooks/useCustomEmojis.ts index 6eb3c17fbf..26b4ebefab 100644 --- a/app/javascript/flavours/glitch/hooks/useCustomEmojis.ts +++ b/app/javascript/flavours/glitch/hooks/useCustomEmojis.ts @@ -1,40 +1,25 @@ -import { useEffect, useState } from 'react'; +import { createAppSelector, useAppSelector } from '@/flavours/glitch/store'; import type { ExtraCustomEmojiMap } from '../features/emoji/types'; -import { emojiLogger } from '../features/emoji/utils'; -let emojis: ExtraCustomEmojiMap | null = null; - -const log = emojiLogger('useCustomEmojis'); +const selectCustomEmojis = createAppSelector( + [(state) => state.emojis.custom], + (custom) => { + const emojis: ExtraCustomEmojiMap = {}; + for (const shortcode in custom) { + const emoji = custom[shortcode]; + if (!emoji) { + continue; + } + emojis[shortcode] = { + shortcode, + ...emoji, + }; + } + return emojis; + }, +); export function useCustomEmojis() { - const [, setLoaded] = useState(emojis !== null); - useEffect(() => { - if (!emojis) { - void loadEmojisIntoCache().then(() => { - setLoaded(true); - }); - } - }, []); - - return emojis; -} - -export async function loadEmojisIntoCache() { - const { loadAllCustomEmoji } = await import('../features/emoji/database'); - const emojisRaw = await loadAllCustomEmoji(); - if (emojisRaw === null) { - log('Custom emojis not loaded yet'); - return; - } - - emojis = {}; - for (const emoji of emojisRaw) { - emojis[emoji.shortcode] = { - url: emoji.url, - shortcode: emoji.shortcode, - static_url: emoji.static_url, - }; - } - log('Loaded %d custom emojis into cache', Object.keys(emojis).length); + return useAppSelector(selectCustomEmojis); } diff --git a/app/javascript/flavours/glitch/reducers/slices/emojis.ts b/app/javascript/flavours/glitch/reducers/slices/emojis.ts new file mode 100644 index 0000000000..df19f23206 --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/slices/emojis.ts @@ -0,0 +1,71 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +import type { Locale } from 'emojibase'; + +import type { ApiCustomEmojiJSON } from '@/flavours/glitch/api_types/custom_emoji'; +import { toSupportedLocale } from '@/flavours/glitch/features/emoji/locale'; +import { createAsyncThunk } from '@/flavours/glitch/store/typed_functions'; + +interface EmojisState { + custom: Record>; + customCategories: Record; // { name: shortcodes[] } + customLoaded: boolean; + localesLoaded: Locale[]; +} + +const emojisSlice = createSlice({ + name: 'emojis', + initialState: { + custom: {}, + customCategories: {}, + customLoaded: false, + localesLoaded: [], + } as EmojisState, + reducers: { + loadLocale(state, action: PayloadAction) { + const locale = toSupportedLocale(action.payload); + if (!state.localesLoaded.includes(locale)) { + state.localesLoaded.push(locale); + } + }, + }, + extraReducers(builder) { + builder.addAsyncThunk(loadCustomEmojis, { + fulfilled(state, action) { + if (!action.payload?.length) { + return; + } + + for (const emoji of action.payload) { + const { shortcode, category, url, static_url } = emoji; + state.custom[shortcode] = { + url, + static_url, + }; + + if (category) { + state.customCategories[category] ??= []; + if (!state.customCategories[category].includes(shortcode)) { + state.customCategories[category].push(shortcode); + } + } + + state.customLoaded = true; + } + }, + }); + }, +}); + +export const emojis = emojisSlice.reducer; +export const { loadLocale } = emojisSlice.actions; + +export const loadCustomEmojis = createAsyncThunk( + `${emojisSlice.name}/loadCustomEmojis`, + async () => { + const { loadAllCustomEmoji } = + await import('@/flavours/glitch/features/emoji/database'); + return loadAllCustomEmoji(); + }, +); diff --git a/app/javascript/flavours/glitch/reducers/slices/index.ts b/app/javascript/flavours/glitch/reducers/slices/index.ts index 8d4ecd552d..1b94687cc6 100644 --- a/app/javascript/flavours/glitch/reducers/slices/index.ts +++ b/app/javascript/flavours/glitch/reducers/slices/index.ts @@ -1,9 +1,11 @@ import { annualReport } from './annual_report'; import { collections } from './collections'; +import { emojis } from './emojis'; import { profileEdit } from './profile_edit'; export const sliceReducers = { annualReport, collections, + emojis, profileEdit, };