import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useAppSelector } from '@/flavours/glitch/store'; import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment'; import { toSupportedLocale } from './locale'; import { determineEmojiMode } from './mode'; import { cleanExtraEmojis } from './normalize'; import { emojifyElement, emojifyText } from './render'; import type { CustomEmojiMapArg, EmojiAppState } from './types'; import { stringHasAnyEmoji } from './utils'; interface UseEmojifyOptions { text: string; extraEmojis?: CustomEmojiMapArg; deep?: boolean; } export function useEmojify({ text, extraEmojis, deep = true, }: UseEmojifyOptions) { const [emojifiedText, setEmojifiedText] = useState(null); const appState = useEmojiAppState(); const extra = useMemo(() => cleanExtraEmojis(extraEmojis), [extraEmojis]); const emojify = useCallback( async (input: string) => { let result: string | null = null; if (deep) { const wrapper = document.createElement('div'); wrapper.innerHTML = input; if (await emojifyElement(wrapper, appState, extra ?? {})) { result = wrapper.innerHTML; } } else { result = await emojifyText(text, appState, extra ?? {}); } if (result) { setEmojifiedText(result); } else { setEmojifiedText(input); } }, [appState, deep, extra, text], ); useLayoutEffect(() => { if (isModernEmojiEnabled() && !!text.trim() && stringHasAnyEmoji(text)) { void emojify(text); } else { // If no emoji or we don't want to render, fall back. setEmojifiedText(text); } }, [emojify, text]); return emojifiedText; } export function useEmojiAppState(): EmojiAppState { const locale = useAppSelector((state) => toSupportedLocale(state.meta.get('locale') as string), ); const mode = useAppSelector((state) => determineEmojiMode(state.meta.get('emoji_style') as string), ); return { currentLocale: locale, locales: [locale], mode, darkTheme: document.body.classList.contains('theme-default'), }; }