"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Avatar } from "@/components/ui/avatar"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { Loader2, UserPlus } from "@/components/icons"; import { SettingsSection } from "@/components/settings/settings-section"; import { useAuthStore } from "@/stores/auth-store"; import { toast } from "@/stores/toast-store"; import { useRestriction } from "@/hooks/use-restriction"; import { loadPrincipalDirectory } from "@/lib/jmap/principal-directory"; import type { Principal, PrincipalListing } from "@/lib/jmap/types"; import type { ShareChangeResult, ShareTarget } from "@/lib/jmap/share-batch"; import type { ShareKind } from "@/lib/jmap/share-rights"; import { allowedLevels, buildRevokeChanges, failedChanges, scopeOfSummary, summarizeDelegates, type DelegateSummary, } from "@/lib/delegation/delegation"; import { rememberPrincipals, rememberedPrincipals } from "@/lib/delegation/known-principals"; import { DelegateDialog, type DelegateDialogProps } from "./delegate-dialog"; import { principalDisplay } from "./principal-display"; type DialogState = { mode: "add" } | { mode: "change"; summary: DelegateSummary } | null; /** * Settings > Sharing > Delegates: give someone access to mail, calendars * and contacts in one step, and see and take back who has access to what. * Hidden when the admin hides `delegation`; `delegationRights` limits the * rights offered. */ export function DelegatesSettings() { const t = useTranslations("delegates"); const client = useAuthStore((s) => s.client); const accountKey = useAuthStore((s) => s.activeAccountId) ?? ""; const delegation = useRestriction("delegation"); const rights = useRestriction("delegationRights"); const levels = useMemo(() => allowedLevels(rights.allowedValues), [rights.allowedValues]); const [targets, setTargets] = useState(null); const [failedKinds, setFailedKinds] = useState([]); const [directory, setDirectory] = useState(null); const [remembered, setRemembered] = useState>(() => new Map()); const [dialog, setDialog] = useState(null); const [confirmRemove, setConfirmRemove] = useState(null); const [removingId, setRemovingId] = useState(null); const reload = useCallback(async () => { if (!client?.getShareTargets) return; const result = await client.getShareTargets(); setTargets(result.targets); setFailedKinds(result.failed); }, [client]); useEffect(() => { if (!client || delegation.hidden) return; let cancelled = false; setTargets(null); setDirectory(null); setRemembered(rememberedPrincipals(accountKey)); void reload(); void loadPrincipalDirectory(client).then((listing) => { if (cancelled) return; setDirectory(listing); rememberPrincipals(accountKey, listing.principals); }); return () => { cancelled = true; }; }, [client, accountKey, delegation.hidden, reload]); const ownIds = useMemo(() => (client ? [client.getAccountId()] : []), [client]); const summaries = useMemo(() => (targets ? summarizeDelegates(targets, ownIds) : []), [targets, ownIds]); const principalFor = useCallback((id: string): Principal | null => directory?.principals.find((p) => p.id === id) ?? remembered.get(id) ?? null, [directory, remembered]); if (delegation.hidden || !client) return null; const onApplied: DelegateDialogProps["onApplied"] = (principal, results) => { rememberPrincipals(accountKey, [principal]); setRemembered(rememberedPrincipals(accountKey)); const failed = failedChanges(results); if (failed.length === 0) { toast.success(dialog?.mode === "change" ? t("updated") : t("applied", { name: principalDisplay(principal).name })); } void reload(); }; const remove = async (summary: DelegateSummary) => { if (!targets || !client.applyShareChanges) return; setRemovingId(summary.principalId); try { const results: ShareChangeResult[] = await client.applyShareChanges(buildRevokeChanges(targets, summary.principalId)); const failed = failedChanges(results); if (failed.length === 0) toast.success(t("removed")); else toast.error(t("remove_partial", { items: failed.map((f) => f.change.name).join(", ") })); await reload(); } finally { setRemovingId(null); } }; const nameOf = (id: string) => { const principal = principalFor(id); return principal ? principalDisplay(principal).name : t("unknown_person"); }; return (

{t("who_has_access")}

{failedKinds.length > 0 &&

{t("read_failed")}

} {targets === null ? (

{t("loading")}

) : summaries.length === 0 ? (

{t("nobody")}

) : (
    {summaries.map((summary) => { const principal = principalFor(summary.principalId); const display = principal ? principalDisplay(principal) : { name: t("unknown_person"), detail: summary.principalId }; return (
  • {display.name}
    {display.detail &&
    {display.detail}
    }
    {describeSummary(summary, t)}
  • ); })}
)}
{dialog && targets && ( s.principalId))} initial={dialog.mode === "change" ? { principal: principalFor(dialog.summary.principalId) ?? { id: dialog.summary.principalId, name: t("unknown_person"), type: "individual" }, ...scopeOfSummary(dialog.summary, targets), } : undefined} levels={levels} levelsRestricted={rights.allowedValues !== null && levels.length < 2} onApplied={onApplied} onClose={() => setDialog(null)} /> )} setConfirmRemove(null)} onConfirm={() => { if (confirmRemove) void remove(confirmRemove); }} title={confirmRemove ? t("remove_title", { name: nameOf(confirmRemove.principalId) }) : ""} message={confirmRemove ? t("remove_message", { name: nameOf(confirmRemove.principalId) }) : ""} confirmText={t("remove")} variant="destructive" />
); } type Translate = ReturnType>; /** "All mail folders · 1 calendar · Read", in the order of the dialog. */ export function describeSummary(summary: DelegateSummary, t: Translate): string { const parts: string[] = []; const { mail, calendars, addressBooks, files } = summary; if (mail.shared > 0) { parts.push(mail.shared === mail.total ? t("summary.mail_all") : mail.shared === 1 && mail.inbox ? t("summary.mail_inbox") : t("summary.mail_some", { count: mail.shared })); } if (calendars.shared > 0) parts.push(t("summary.calendars", { count: calendars.shared })); if (addressBooks.shared > 0) parts.push(t("summary.address_books", { count: addressBooks.shared })); if (files > 0) parts.push(t("summary.files", { count: files })); parts.push(t(`level.${summary.level ?? "none"}`)); return parts.join(" · "); }