import type { ConversationSummary } from '@chat-app/shared/chat'; import { useEffect, useState } from 'react'; import { Avatar } from './Avatar'; interface Props { members: ConversationSummary['members']; query: string; excludeUserId: string | undefined; onSelect: (username: string) => void; onClose: () => void; } // Dropdown shown above the composer when the user has typed `@` followed // by the start of a member name. Keyboard-first — arrow keys move through, // enter/tab commits, escape cancels. export function MentionAutocomplete({ members, query, excludeUserId, onSelect, onClose, }: Props) { const q = query.toLowerCase(); const matches = members .filter((m) => m.userId !== excludeUserId) .filter((m) => { if (!q) return true; const name = (m.profile?.displayName ?? '').toLowerCase(); const handle = (m.profile?.username ?? '').toLowerCase(); return name.includes(q) || handle.includes(q); }) .slice(0, 8); const [active, setActive] = useState(0); useEffect(() => { setActive(0); }, [query]); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (matches.length === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); setActive((i) => (i + 1) % matches.length); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((i) => (i - 1 + matches.length) % matches.length); } else if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); const pick = matches[active]; if (pick?.profile?.username) onSelect(pick.profile.username); } else if (e.key === 'Escape') { e.preventDefault(); onClose(); } }; window.addEventListener('keydown', onKey, true); return () => { window.removeEventListener('keydown', onKey, true); }; }, [matches, active, onSelect, onClose]); if (matches.length === 0) return null; return (
{matches.map((m, idx) => { const name = m.profile?.displayName ?? m.profile?.username ?? '?'; const handle = m.profile?.username ?? ''; const isActive = idx === active; return ( ); })}
); }