import { useEffect, useState } from "react"; import { Trash2 } from "lucide-react"; import { Dialog } from "@/ui/dialog"; import { useContacts } from "@/store/contacts"; import { useMail } from "@/store/mail"; import { useCalendar } from "@/store/calendar"; import { client } from "@/jmap/client"; import { toast } from "@/ui/toast"; import type { Id, Principal } from "@/jmap/types"; type Kind = "Mailbox" | "Calendar" | "AddressBook"; const RIGHTS: Record> = { Mailbox: [ { key: "mayReadItems", label: "Read" }, { key: "mayAddItems", label: "Add" }, { key: "mayRemoveItems", label: "Remove" }, { key: "maySetSeen", label: "Mark read" }, { key: "maySetKeywords", label: "Flag" }, { key: "mayCreateChild", label: "Create subfolders" }, { key: "mayRename", label: "Rename" }, { key: "mayDelete", label: "Delete" }, { key: "maySubmit", label: "Send" }, ], Calendar: [ { key: "mayReadFreeBusy", label: "See free/busy" }, { key: "mayReadItems", label: "Read events" }, { key: "mayWriteAll", label: "Edit all" }, { key: "mayWriteOwn", label: "Edit own" }, { key: "mayUpdatePrivate", label: "Private props" }, { key: "mayRSVP", label: "RSVP" }, { key: "mayShare", label: "Share" }, { key: "mayDelete", label: "Delete" }, ], AddressBook: [ { key: "mayRead", label: "Read" }, { key: "mayWrite", label: "Write" }, { key: "mayShare", label: "Share" }, { key: "mayDelete", label: "Delete" }, ], }; const PRESETS: Record = { Mailbox: { reader: ["mayReadItems"], editor: ["mayReadItems", "mayAddItems", "mayRemoveItems", "maySetSeen", "maySetKeywords", "mayCreateChild"] }, Calendar: { reader: ["mayReadFreeBusy", "mayReadItems"], editor: ["mayReadFreeBusy", "mayReadItems", "mayWriteAll", "mayRSVP"] }, AddressBook: { reader: ["mayRead"], editor: ["mayRead", "mayWrite"] }, }; /** Share a mailbox / calendar / address book with other principals (JMAP Sharing, RFC 9670). */ export function ShareDialog({ kind, id, name, shareWith, onClose }: { kind: Kind; id: Id; name: string; shareWith: Record | null; onClose: () => void }) { const principals = useContacts((s) => s.principals); const loadPrincipals = useContacts((s) => s.loadPrincipals); const [rights, setRights] = useState>>(() => ({ ...((shareWith ?? {}) as Record>) })); const [pick, setPick] = useState(""); const [busy, setBusy] = useState(false); useEffect(() => { void loadPrincipals(); }, [loadPrincipals]); const available = principals.filter((p) => !rights[p.id]); const add = (p: Principal, preset: "reader" | "editor") => { const r: Record = {}; for (const k of PRESETS[kind][preset]) r[k] = true; setRights({ ...rights, [p.id]: r }); setPick(""); }; const save = async () => { setBusy(true); try { const accountId = kind === "Mailbox" ? useMail.getState().accountId : kind === "Calendar" ? useCalendar.getState().accountId : useContacts.getState().accountId; const res = await client.call<{ notUpdated?: Record }>(`${kind}/set`, { accountId, update: { [id]: { shareWith: Object.keys(rights).length ? rights : null } } }); const err = res.notUpdated?.[id]; if (err) throw new Error(err.description ?? err.type); toast.success("Sharing updated"); if (kind === "Mailbox") void useMail.getState().loadMailboxes(); if (kind === "Calendar") void useCalendar.getState().loadCalendars(); if (kind === "AddressBook") void useContacts.getState().loadBooks(); onClose(); } catch (err) { toast.error((err as Error).message); } finally { setBusy(false); } }; return ( }> {!principals.length ? (

No other users found in the directory, or sharing is not enabled on this server.

) : ( <>
{Object.entries(rights).map(([pid, r]) => { const p = principals.find((x) => x.id === pid); return (

{p?.name ?? pid}{p?.email ? · {p.email} : null}

{RIGHTS[kind].map((rt) => ( ))}
); })} {!Object.keys(rights).length &&

Not shared with anyone yet.

} )}
); }