"use client"; /** * Search for people and add them to a group. * * Search runs through the same-origin /api/search/users proxy the search page * uses. It returns handles, which is exactly what the member API takes — no id * plumbing needed. */ import { useEffect, useState } from "react"; import { Check, Search, X } from "lucide-react"; import { toast } from "sonner"; import { useAuth } from "@/components/auth/auth-provider"; import { UserAvatar } from "@/components/ui/user-avatar"; import { Button } from "@/components/ui/button"; import { chat } from "@/lib/chat"; type Candidate = { handle: string; name: string; avatar_url: string | null }; export function AddMembersDialog({ conversationId, existing, onClose, onAdded, }: { conversationId: string; /** Handles already in the group. Filtered out of results entirely. */ existing: string[]; onClose: () => void; onAdded: () => void; }) { const { user } = useAuth(); const [query, setQuery] = useState(""); // Results are stored with the query they belong to, so what's displayed can // be derived: a stale page from a previous query simply stops matching, and // nothing has to be cleared from an effect. const [results, setResults] = useState<{ q: string; rows: Candidate[] }>({ q: "", rows: [], }); const [picked, setPicked] = useState>(new Set()); const [busy, setBusy] = useState(false); const q = query.trim(); const searchable = q.length >= 2; // Current members and the caller are dropped rather than shown greyed out: // this list exists to answer "who can I add?", and anyone already here is // not an answer to that. const alreadyIn = new Set(existing); const visible = results.q === q ? results.rows.filter( (u) => u.handle !== user?.handle && !alreadyIn.has(u.handle), ) : []; useEffect(() => { if (!searchable) return; const timer = setTimeout(() => { fetch(`/api/search/users?q=${encodeURIComponent(q)}&limit=15`) .then((r) => r.json()) .then((data: { results?: Candidate[] }) => setResults({ q, rows: data.results ?? [] }), ) .catch(() => setResults({ q, rows: [] })); }, 300); return () => clearTimeout(timer); }, [q, searchable]); useEffect(() => { const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const toggle = (handle: string) => setPicked((prev) => { const next = new Set(prev); if (next.has(handle)) next.delete(handle); else next.add(handle); return next; }); const submit = async () => { if (picked.size === 0) return; setBusy(true); try { await chat.addMembers(conversationId, [...picked]); toast.success( picked.size === 1 ? "Member added" : `${picked.size} members added`, ); onAdded(); onClose(); } catch { toast.error("Could not add members"); } finally { setBusy(false); } }; return (

Add members

setQuery(e.target.value)} placeholder="Search by name or handle" className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" />
{!searchable ? (

Type at least two characters to search.

) : visible.length === 0 ? (

No one found.

) : (
    {visible.map((u) => (
  • ))}
)}
); }