/** * AddMembersDialog * * Search-based dialog for adding profiles to a specific list. * Uses NIP-50 profile search and allows keyboard navigation. */ import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { Search, Loader2, Plus, UserPlus, Check } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { useSearchProfiles } from '@/hooks/useSearchProfiles'; import { useUserLists } from '@/hooks/useUserLists'; import { toast } from '@/hooks/useToast'; import { genUserName } from '@/lib/genUserName'; import type { SearchProfile } from '@/hooks/useSearchProfiles'; interface AddMembersDialogProps { open: boolean; onOpenChange: (open: boolean) => void; listId: string; listPubkeys: string[]; } export function AddMembersDialog({ open, onOpenChange, listId, listPubkeys }: AddMembersDialogProps) { const [query, setQuery] = useState(''); const [selectedIdx, setSelectedIdx] = useState(0); const [addingPubkeys, setAddingPubkeys] = useState>(new Set()); const [addedPubkeys, setAddedPubkeys] = useState>(new Set()); const inputRef = useRef(null); const listRef = useRef(null); const { data: searchResults, isLoading, isFetching } = useSearchProfiles(query); const { addToList } = useUserLists(); // Existing member set for filtering const existingMembers = useMemo(() => new Set(listPubkeys), [listPubkeys]); // Filter out profiles already in the list const filteredResults = useMemo(() => { if (!searchResults) return []; return searchResults.filter((p) => !existingMembers.has(p.pubkey)); }, [searchResults, existingMembers]); // Reset state when dialog opens/closes useEffect(() => { if (open) { setQuery(''); setSelectedIdx(0); setAddingPubkeys(new Set()); setAddedPubkeys(new Set()); setTimeout(() => inputRef.current?.focus(), 100); } }, [open]); // Reset selection when results change useEffect(() => { setSelectedIdx(0); }, [filteredResults.length]); const handleAdd = useCallback(async (profile: SearchProfile) => { if (addingPubkeys.has(profile.pubkey) || addedPubkeys.has(profile.pubkey)) return; setAddingPubkeys((prev) => new Set(prev).add(profile.pubkey)); try { await addToList.mutateAsync({ listId, pubkey: profile.pubkey }); setAddedPubkeys((prev) => new Set(prev).add(profile.pubkey)); const name = profile.metadata.name || profile.metadata.display_name || genUserName(profile.pubkey); toast({ title: `Added ${name} to list` }); } catch { toast({ title: 'Failed to add member', variant: 'destructive' }); } finally { setAddingPubkeys((prev) => { const next = new Set(prev); next.delete(profile.pubkey); return next; }); } }, [addToList, listId, addingPubkeys, addedPubkeys]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIdx((prev) => Math.min(prev + 1, filteredResults.length - 1)); scrollSelectedIntoView(selectedIdx + 1); } else if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIdx((prev) => Math.max(prev - 1, 0)); scrollSelectedIntoView(selectedIdx - 1); } else if (e.key === 'Enter' && filteredResults[selectedIdx]) { e.preventDefault(); handleAdd(filteredResults[selectedIdx]); } }; const scrollSelectedIntoView = (idx: number) => { const container = listRef.current; if (!container) return; const item = container.children[idx] as HTMLElement | undefined; item?.scrollIntoView({ block: 'nearest' }); }; return (
Add Members
setQuery(e.target.value)} onKeyDown={handleKeyDown} placeholder="Search by name or NIP-05…" className="pl-9 pr-8" /> {isFetching && ( )}
{!query.trim() ? (
Search for people to add to this list.
) : isLoading && !searchResults ? (
) : filteredResults.length === 0 ? (
{searchResults && searchResults.length > 0 ? 'All matching users are already in this list.' : 'No profiles found.'}
) : ( filteredResults.map((profile, idx) => { const name = profile.metadata.name || profile.metadata.display_name || genUserName(profile.pubkey); const isAdding = addingPubkeys.has(profile.pubkey); const isAdded = addedPubkeys.has(profile.pubkey); const isSelected = idx === selectedIdx; return (
handleAdd(profile)} onMouseEnter={() => setSelectedIdx(idx)} > {name[0]?.toUpperCase()}
{name}
{profile.metadata.nip05 && (
{profile.metadata.nip05}
)}
); }) )}
); }