import { useEffect, useMemo, useRef, useState, type RefObject } from 'react'; import { useTranslation } from 'react-i18next'; import { fileContentUrl } from '@/api/files'; import { useGuildEmojis } from '@/components/chat/useGuildEmojis'; import { POPULAR_EMOJIS, QUICK_REACTIONS } from '@/lib/chat'; import { customEmojiShortcode } from '@/lib/customEmoji'; interface EmojiPickerProps { open: boolean; onClose: () => void; onPick: (emoji: string) => void; /** Якорь: клик по нему не считается кликом «вне пикера». */ anchorRef?: RefObject; /** Куда раскрывается панель: под якорем или над ним. */ placement?: 'top' | 'bottom'; /** Сервер, чьи кастомные эмодзи показываем: в беседе их нет. */ guildId?: string | null; className?: string; } /** * Выпадающий пикер эмодзи: ряд быстрых реакций, эмодзи сервера и сетка * популярных. Пикер общий для ленты (реакции) и композера (вставка в текст): * `onPick` получает либо юникод-эмодзи, либо токен `<:имя:file_id>`. * * Эмодзи сервера ищутся по имени; список пуст, если сервера нет (личная * беседа) или на нём ещё не загрузили ни одного эмодзи. * * Закрывается по Escape и клику вне панели; фокус остаётся в пределах * tab-порядка страницы. */ export function EmojiPicker({ open, onClose, onPick, anchorRef, placement = 'bottom', guildId = null, className = '', }: EmojiPickerProps) { const { t } = useTranslation(); const panelRef = useRef(null); const [query, setQuery] = useState(''); const serverEmojis = useGuildEmojis(guildId); const onCloseRef = useRef(onClose); useEffect(() => { onCloseRef.current = onClose; }, [onClose]); // Поиск сбрасываем при каждом открытии: иначе список «залипал» бы от прошлого раза. useEffect(() => { if (open) { setQuery(''); } }, [open]); const found = useMemo(() => { const needle = query.trim().toLowerCase(); if (needle === '') { return serverEmojis; } return serverEmojis.filter((emoji) => emoji.name.toLowerCase().includes(needle)); }, [serverEmojis, query]); useEffect(() => { if (!open) { return; } const onPointerDown = (event: Event): void => { const target = event.target; if (!(target instanceof Node)) { return; } if (panelRef.current?.contains(target) === true) { return; } if (anchorRef?.current?.contains(target) === true) { return; } onCloseRef.current(); }; const onKeyDown = (event: KeyboardEvent): void => { if (event.key === 'Escape') { onCloseRef.current(); } }; document.addEventListener('mousedown', onPointerDown); document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('mousedown', onPointerDown); document.removeEventListener('keydown', onKeyDown); }; }, [open, anchorRef]); if (!open) { return null; } return (
{QUICK_REACTIONS.map((emoji) => ( ))}
{serverEmojis.length === 0 ? null : (
setQuery(event.target.value)} className="w-full rounded-[var(--radius-sm)] border border-border/60 bg-surface-1 px-2 py-1 text-xs text-fg placeholder:text-fg-muted/70" /> {found.length === 0 ? (

{t('chat.emoji.notFound')}

) : (
    {found.map((emoji) => (
  • ))}
)}
)}
{POPULAR_EMOJIS.map((emoji) => ( ))}
); }