import { useEffect, useState, useCallback, useRef } from "react"; import { api, formatBytes, type Account } from "./api.js"; import { AccountStatsModal } from "./AccountStatsModal.js"; import { useToast } from "./useToast.js"; const PAGE_SIZES = [25, 50, 100, 250]; export function AccountsPanel({ appviewUrl, searchFor, }: { appviewUrl: string; // search request from another panel (e.g. clicking a handle in stats) searchFor?: { q: string } | null; }) { const [accounts, setAccounts] = useState([]); const [total, setTotal] = useState(0); const [flaggedTotal, setFlaggedTotal] = useState(0); // DM notifications deep-link here with ?q= to pull up the flagged account const [query, setQuery] = useState( () => new URLSearchParams(window.location.search).get("q") ?? "", ); const [hideTakendown, setHideTakendown] = useState(true); const [sortStorage, setSortStorage] = useState(false); const [hideEmails, setHideEmails] = useState( () => localStorage.getItem("hideEmails") !== "0", ); const [pageSize, setPageSize] = useState(() => { const saved = Number(localStorage.getItem("accountsPageSize")); return PAGE_SIZES.includes(saved) ? saved : 25; }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const panelRef = useRef(null); useEffect(() => { if (!searchFor) return; setQuery(searchFor.q); panelRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); }, [searchFor]); const [busyDid, setBusyDid] = useState(null); const [confirm, setConfirm] = useState<{ did: string; action: "status" | "reset" } | null>(null); const [menuDid, setMenuDid] = useState(null); const [passwordResult, setPasswordResult] = useState<{ handle: string; password: string } | null>( null, ); const [statsDid, setStatsDid] = useState(null); const [filtersOpen, setFiltersOpen] = useState(false); useEffect(() => { if (!filtersOpen) return; const close = (e: MouseEvent | KeyboardEvent) => { if (e instanceof KeyboardEvent && e.key !== "Escape") return; setFiltersOpen(false); }; document.addEventListener("click", close); document.addEventListener("keydown", close); return () => { document.removeEventListener("click", close); document.removeEventListener("keydown", close); }; }, [filtersOpen]); const [toast, showToast] = useToast(); const copy = (text: string, what: string, e: React.MouseEvent) => { const at = { clientX: e.clientX, clientY: e.clientY }; setMenuDid(null); navigator.clipboard .writeText(text) .then(() => showToast(`${what} copied`, at)) .catch(() => showToast("copy failed", at)); }; useEffect(() => { if (!menuDid) return; const close = (e: MouseEvent | KeyboardEvent) => { if (e instanceof KeyboardEvent && e.key !== "Escape") return; setMenuDid(null); setConfirm(null); }; document.addEventListener("click", close); document.addEventListener("keydown", close); return () => { document.removeEventListener("click", close); document.removeEventListener("keydown", close); }; }, [menuDid]); useEffect(() => { if (!passwordResult) return; const close = (e: KeyboardEvent) => { if (e.key === "Escape") setPasswordResult(null); }; document.addEventListener("keydown", close); return () => document.removeEventListener("keydown", close); }, [passwordResult]); const refresh = useCallback(() => { setLoading(true); api .accounts({ q: query.trim() || undefined, limit: pageSize, hideTakendown, sort: sortStorage ? "storage" : undefined, }) .then((r) => { setAccounts(r.accounts); setTotal(r.total); setFlaggedTotal(r.flaggedTotal); setError(null); }) .catch((e) => setError(e.message)) .finally(() => setLoading(false)); }, [query, hideTakendown, pageSize, sortStorage]); useEffect(() => { const id = setTimeout(refresh, query.trim() ? 300 : 0); return () => clearTimeout(id); }, [refresh, query]); const loadMore = () => { setLoading(true); api .accounts({ q: query.trim() || undefined, offset: accounts.length, limit: pageSize, hideTakendown, sort: sortStorage ? "storage" : undefined, }) .then((r) => { setAccounts((prev) => [...prev, ...r.accounts]); setTotal(r.total); }) .catch((e) => setError(e.message)) .finally(() => setLoading(false)); }; const runAction = async (did: string, action: "takedown" | "enable" | "resetPassword") => { setBusyDid(did); setConfirm(null); try { if (action === "resetPassword") { const { password } = await api.resetPassword(did); const handle = accounts.find((a) => a.did === did)?.handle ?? did; setPasswordResult({ handle, password }); } else { await api[action](did); } refresh(); } catch (e) { setError((e as Error).message); } finally { setBusyDid(null); } }; const tableHead = ( Handle DID Status Indexed ); const renderRow = (a: Account) => ( {a.avatar ? ( ) : ( {(a.labels?.length ?? 0) > 0 &&
{a.labels!.join(", ")}
}
{a.did}
{a.email && !hideEmails &&
{a.email}
} {a.status ?? "active"} {new Date(a.indexedAt).toLocaleDateString()} {formatBytes(a.storageBytes)}
e.stopPropagation()}> {menuDid === a.did && (
{confirm?.did === a.did ? ( <> ) : ( <> {a.email && ( )}
)}
)}
); return ( <>

Accounts ({total - flaggedTotal} {query.trim() ? " matching" : ""} {flaggedTotal > 0 ? ` + ${flaggedTotal} flagged` : ""})

e.stopPropagation()}> {filtersOpen && (
)}
setQuery(e.target.value)} /> {query && ( )}
{error &&
{error}
} {!loading && accounts.length === 0 && !error && (

{query.trim() ? `no accounts match “${query.trim()}”` : "no accounts to show"}

)} {tableHead} {accounts.map(renderRow)}
{total > accounts.length ? ( ) : ( )}
{toast} {statsDid && ( setStatsDid(null)} /> )} {passwordResult && (
setPasswordResult(null)}>
e.stopPropagation()} >

password reset for @{passwordResult.handle}

{passwordResult.password}

this password won’t be shown again — copy it now.

)}
); }