import React from 'react'; import { CustomEmoji } from '../../config/customEmojiConfig'; import { useSuggestedEmojisModeConfig } from '../../config/useConfig'; import defaultEmojiData from '../../data/emojis'; import { DataEmoji, DataEmojis, EmojiProperties, EmojiProperties as Keys, } from '../../dataUtils/DataTypes'; import { emojiByUnified } from '../../dataUtils/emojiSelectors'; import { activeVariationFromUnified, unifiedWithoutSkinTone, } from '../../dataUtils/emojiUtils'; import { getSuggested } from '../../dataUtils/suggested'; import { useIsMounted } from '../../hooks/useIsMounted'; import { Categories, EmojiData, SkinTones } from '../../types/exposedTypes'; import { usePickerConfig } from './PickerConfigContext'; import { useUpdateSuggested } from './PickerContext'; export interface PickerDataContextValue { emojiData: EmojiData; allEmojis: DataEmojis; allEmojisByUnified: Record; searchIndex: Record>; customGroups: Record; emojiByUnified: (unified?: string) => DataEmoji | undefined; activeVariationFromUnified: (unified: string) => SkinTones | null; } const PickerDataContext = React.createContext({ emojiData: {} as EmojiData, allEmojis: [], allEmojisByUnified: Object.create(null), searchIndex: Object.create(null), customGroups: Object.create(null), emojiByUnified, activeVariationFromUnified: () => null, }); export function PickerDataProvider({ children, }: { children: React.ReactNode; }) { const { customEmojis, emojiData: genericEmojiData } = usePickerConfig(); const data = React.useMemo(() => { const emojiData = genericEmojiData || (defaultEmojiData as EmojiData); // Clone to avoid mutation of shared source const newData: EmojiData = JSON.parse(JSON.stringify(emojiData)); // Group names and emoji ids are user-controlled strings: use // null-prototype dictionaries so names like `__proto__` behave as // ordinary data instead of resolving Object.prototype members. const customGroups: Record = Object.create(null); if (customEmojis && customEmojis.length > 0) { for (const emoji of customEmojis) { if (!emoji.group) { continue; } customGroups[emoji.group] = customGroups[emoji.group] ?? []; customGroups[emoji.group].push(customToRegularEmoji(emoji)); } newData.emojis[Categories.CUSTOM] = customEmojis .filter(emoji => !emoji.group) .map(customToRegularEmoji); } const emojis = newData.emojis || {}; const allEmojis: DataEmojis = Object.values(emojis) .concat(Object.values(customGroups)) .flat(); const allEmojisByUnified: Record = Object.create(null); const searchIndex: Record> = Object.create(null); allEmojis.forEach((emoji) => { const unified = emoji[Keys.unified]; allEmojisByUnified[unified] = emoji; if (emoji[Keys.variations]) { emoji[Keys.variations]?.forEach((variation) => { allEmojisByUnified[variation] = emoji; }); } // Index for search // Re-implement indexEmoji logic here to be local const joinedNameString = (emoji[Keys.name] || []) .join('') .toLowerCase() .split(''); joinedNameString.forEach((char: string) => { searchIndex[char] = searchIndex[char] ?? Object.create(null); searchIndex[char][unified] = emoji; }); }); return { emojiData: newData, allEmojis, allEmojisByUnified, searchIndex, customGroups, }; }, [genericEmojiData, customEmojis]); const emojiByUnified = React.useCallback( (unified?: string): DataEmoji | undefined => { if (!unified) return undefined; const result = data.allEmojisByUnified[unified] ?? data.allEmojisByUnified[unifiedWithoutSkinTone(unified)]; return result; }, [data.allEmojisByUnified], ); return ( {children} ); } export function usePickerDataContext() { return React.useContext(PickerDataContext); } export function useGetEmojisByCategory() { const { emojiData, emojiByUnified, customGroups } = usePickerDataContext(); const suggestedEmojisModeConfig = useSuggestedEmojisModeConfig(); const [suggestedUpdated] = useUpdateSuggested(); // Suggestions come from localStorage, which doesn't exist during SSR. // Read them only after mount so the first client render matches the server. const isMounted = useIsMounted(); const suggested = React.useMemo(() => { if (!isMounted) { return [] as DataEmojis; } const suggested = getSuggested(suggestedEmojisModeConfig) ?? []; return suggested .map((s) => { const emoji = emojiByUnified(s.unified); if (!emoji) return undefined; return { ...emoji, [Keys.unified]: s.unified, }; }) .filter(Boolean) as DataEmojis; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMounted, suggestedUpdated, suggestedEmojisModeConfig, emojiByUnified]); return function getEmojisByCategory( category: Categories, group?: string, ): DataEmojis { if (category === Categories.SUGGESTED) { return suggested; } if (category === Categories.CUSTOM && group) { return customGroups[group] ?? []; } return emojiData.emojis?.[category] ?? []; }; } function customToRegularEmoji(emoji: CustomEmoji): DataEmoji { return { [EmojiProperties.name]: emoji.names.map((name: string) => name.toLowerCase(), ), [EmojiProperties.unified]: emoji.id.toLowerCase(), [EmojiProperties.added_in]: '0', [EmojiProperties.imgUrl]: emoji.imgUrl, }; }