import { useEffect, useMemo, useState } from "react"; import { useLocation } from "wouter"; import { ChevronLeft, ChevronRight, List, Plus, Search } from "lucide-react"; import { can } from "@/lib/adminAccess"; import { describeDirectoryError, listDomains, type DirectoryDomain } from "@/lib/adminDirectory"; import { getLists, queryLists, type DirectoryList } from "@/lib/adminLists"; import { plural, t } from "@/lib/i18n"; import { Empty, Spinner } from "@/ui/misc"; import { useSession } from "@/store/session"; import { STALWART_CAP } from "@/jmap/client"; import { usePermissions } from "./usePermissions"; import type { DirectoryContext } from "./directoryContext"; import { ListSheet } from "./ListSheet"; const PAGE_SIZE = 50; /** * Mailing lists: an address that passes mail on to others. * * The same shape as Accounts and Groups -- search, fifty to a page, a panel -- * with the number of recipients where they have a role or members. */ export function ListsAdmin({ selectedId }: { selectedId?: string }) { const [, navigate] = useLocation(); const perms = usePermissions(); const session = useSession((s) => s.session); const [text, setText] = useState(""); const [query, setQuery] = useState(""); const [position, setPosition] = useState(0); const [page, setPage] = useState<{ lists: DirectoryList[]; total: number } | null>(null); const [error, setError] = useState(null); const [reload, setReload] = useState(0); const [serverDomains, setServerDomains] = useState(null); const [loose, setLoose] = useState(null); useEffect(() => { const id = window.setTimeout(() => { setQuery(text); setPosition(0); }, 250); return () => window.clearTimeout(id); }, [text]); useEffect(() => { let cancelled = false; setError(null); void (async () => { try { const q = await queryLists({ text: query, position, limit: PAGE_SIZE }); const lists = await getLists(q.ids); if (!cancelled) setPage({ lists, total: q.total }); } catch (err) { if (!cancelled) { setPage({ lists: [], total: 0 }); setError(describeDirectoryError(err, "list")); } } })(); return () => { cancelled = true; }; }, [query, position, reload]); useEffect(() => { if (can(perms, "Domain", "Query") && can(perms, "Domain", "Get")) void listDomains().then(setServerDomains, () => setServerDomains(null)); }, [perms, reload]); useEffect(() => { if (!selectedId || selectedId === "new" || page?.lists.some((l) => l.id === selectedId)) { setLoose(null); return; } let cancelled = false; void getLists([selectedId]).then( ([l]) => { if (!cancelled) setLoose(l ?? null); }, () => { if (!cancelled) setLoose(null); }, ); return () => { cancelled = true; }; }, [selectedId, page]); const ctx: DirectoryContext = useMemo(() => { const seen = new Map(); for (const l of page?.lists ?? []) { const domain = l.emailAddress?.split("@")[1]; if (domain && !seen.has(l.domainId)) seen.set(l.domainId, { id: l.domainId, name: domain }); } const ownId = session?.primaryAccounts?.[STALWART_CAP]; return { domains: (serverDomains ?? [...seen.values()]).slice().sort((x, y) => x.name.localeCompare(y.name)), roles: null, groups: new Map(), self: { ids: new Set(ownId ? [ownId] : []), address: (session?.username ?? "").toLowerCase() }, }; }, [page, serverDomains, session]); const selected = selectedId && selectedId !== "new" ? (page?.lists.find((l) => l.id === selectedId) ?? loose) : null; const close = () => navigate("/admin/lists"); const changed = () => setReload((n) => n + 1); return (

{t("Mailing lists")}

{t("Addresses that pass mail on to everyone on them.")}

{can(perms, "MailingList", "Create") && ( )}
{error &&

{error}

} {page === null ? ( ) : page.lists.length === 0 ? ( !error && ( } title={query ? t("No mailing lists match") : t("No mailing lists yet")}> {query ? t("Nothing on your domains matches “{query}”.", { query }) : undefined} ) ) : ( <>
{page.lists.map((l) => ( navigate(`/admin/lists/${l.id}`)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); navigate(`/admin/lists/${l.id}`); } }} aria-label={t("Open {address}", { address: l.emailAddress ?? l.name })} > ))}
{t("Mailing list")} {t("Recipients")} {t("Other addresses")}
{l.description || l.name}
{l.emailAddress}
{Object.keys(l.recipients ?? {}).length} {Object.values(l.aliases ?? {}).map((a) => a.name).join(", ") || "—"}
{page.total <= PAGE_SIZE && position === 0 ? (

{plural(page.total, { one: "{n} mailing list", other: "{n} mailing lists" })}

) : (
{t("{from}–{to} of {total}", { from: position + 1, to: position + page.lists.length, total: page.total })}
)} )} {(selectedId === "new" || selected) && ( { changed(); navigate(`/admin/lists/${id}`); }} onDeleted={() => { changed(); close(); }} /> )}
); }