import { EMOJI_PICKER_DATA_SOURCE } from '@/lib/emoji-picker-data-source' import { cn } from '@/lib/utils' import { preloadEmojiPickerModule } from '@/lib/emoji-picker-preload' import { DEFAULT_LIKE_REACTION_CONTENT, DEFAULT_LIKE_REACTION_DISPLAY_EMOJI, DEFAULT_SUGGESTED_EMOJIS } from '@/lib/like-reaction-emojis' import { recordEmojiUsed } from '@/lib/recently-used-emojis' import { useNostr } from '@/providers/NostrProvider' import { useTheme } from '@/providers/ThemeProvider' import customEmojiService from '@/services/custom-emoji.service' import { TEmoji } from '@/types' import { Plus } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' export { DEFAULT_SUGGESTED_EMOJIS as EMOJI_PICKER_REACTIONS } from '@/lib/like-reaction-emojis' export default function EmojiPicker({ onEmojiClick, reactionsDefaultOpen, reactions, layout = 'popover' }: { onEmojiClick: (emoji: string | TEmoji | undefined, event: Event) => void reactionsDefaultOpen?: boolean reactions?: string[] /** `drawer` fills the mobile sheet; `popover` uses a fixed height for dropdowns. */ layout?: 'drawer' | 'popover' }) { const inDrawer = layout === 'drawer' const { themeSetting } = useTheme() const { pubkey } = useNostr() const [mode, setMode] = useState<'reactions' | 'full'>( reactionsDefaultOpen ? 'reactions' : 'full' ) const [customEmojiTick, setCustomEmojiTick] = useState(0) const [pickerReady, setPickerReady] = useState(false) const containerRef = useRef(null) const pickerRef = useRef<(HTMLElement & { customEmoji: unknown[] }) | null>(null) useEffect(() => customEmojiService.subscribeIndexUpdate(() => setCustomEmojiTick((t) => t + 1)), []) const customEmojis = useMemo( () => customEmojiService.getAllCustomEmojisForPicker(pubkey ?? null), [pubkey, customEmojiTick] ) const ownEmojis = useMemo( () => (pubkey ? customEmojiService.getOwnCustomEmojis(pubkey) : []), [pubkey, customEmojiTick] ) useEffect(() => { if (mode !== 'full') return let cancelled = false setPickerReady(false) preloadEmojiPickerModule().then(({ Picker }) => { if (cancelled || !containerRef.current) return const picker = new Picker({ dataSource: EMOJI_PICKER_DATA_SOURCE, customEmoji: customEmojis }) as HTMLElement & { customEmoji: unknown[] } pickerRef.current = picker if (themeSetting === 'dark') { picker.className = 'dark' } else if (themeSetting === 'light') { picker.className = 'light' } picker.style.width = '100%' picker.style.minWidth = '280px' picker.style.maxWidth = '350px' if (inDrawer) { picker.style.height = '100%' picker.style.minHeight = '0' } else { picker.style.height = 'min(350px, 50dvh)' picker.style.minHeight = '280px' } picker.style.setProperty('--num-columns', '8') const handleClick = (e: Event) => { const detail = (e as CustomEvent).detail as { unicode?: string emoji?: { custom?: boolean unicode?: string name?: string shortcodes?: string[] url?: string } } let result: string | TEmoji | undefined /** * emoji-picker-element only puts `unicode` on the event detail when `skinTonedUnicode` is truthy * (see getDetailForClickEvent in picker.js). Native picks often expose the sequence on `detail.emoji.unicode` * instead, so we must fall back — otherwise `insertEmoji` receives undefined and “most emojis don’t work”. */ const top = typeof detail.unicode === 'string' && detail.unicode.length > 0 ? detail.unicode : undefined const nested = typeof detail.emoji?.unicode === 'string' && detail.emoji.unicode.length > 0 ? detail.emoji.unicode : undefined const nativeUnicode = top ?? nested if (nativeUnicode) { result = nativeUnicode } else { const em = detail.emoji // Custom entries: `url` (+ shortcodes / name); avoid treating native `unicode` as custom. if (em?.url && !em.unicode) { const shortcode = em.shortcodes?.[0] ?? em.name if (shortcode) { result = { shortcode, url: em.url } } } else if (em?.custom && em.shortcodes?.[0] && em.url) { result = { shortcode: em.shortcodes[0], url: em.url } } } if (result !== undefined) recordEmojiUsed(result) onEmojiClick(result, e) } picker.addEventListener('emoji-click', handleClick) containerRef.current.appendChild(picker) if (!cancelled) setPickerReady(true) }) return () => { cancelled = true setPickerReady(false) if (pickerRef.current) { pickerRef.current.remove() pickerRef.current = null } } }, [mode, inDrawer]) useEffect(() => { if (pickerRef.current) { pickerRef.current.customEmoji = customEmojis } }, [customEmojis]) useEffect(() => { if (!pickerRef.current) return if (themeSetting === 'dark') { pickerRef.current.className = 'dark' } else if (themeSetting === 'light') { pickerRef.current.className = 'light' } else { pickerRef.current.className = '' } }, [themeSetting]) const reactionsList = reactions ?? [...DEFAULT_SUGGESTED_EMOJIS] const ownEmojisRow = ownEmojis.length > 0 ? (
{ownEmojis.map((emoji) => ( ))}
) : null if (mode === 'reactions') { return (
{ownEmojisRow}
{reactionsList.map((emoji) => ( ))}
) } return (
{ownEmojisRow}
{!pickerReady ? (
) : null}
) }