import { useMemo, useState } from "react"; import { Eye, EyeOff, Folder, Pencil, Plus, Share2, Trash2, Inbox } from "lucide-react"; import { useMail } from "@/store/mail"; import { confirmDialog, promptDialog } from "@/ui/dialog"; import { toast } from "@/ui/toast"; import { formatSize } from "@/lib/format"; import { ShareDialog } from "./ShareDialog"; import type { Mailbox, MailboxRole } from "@/jmap/types"; import { plural, t } from "@/lib/i18n"; import { mailboxDisplayPath } from "@/lib/mailbox/mailboxName"; /* * Roles a folder can be given here. * * These are the three that ihasmail's own behavior depends on and that * Stalwart will let move. Inbox, Junk and Trash are absent on purpose: 0.16.20 * refuses them outright -- "You are not allowed to change the role of Inbox, * Junk or Trash folders" -- so offering them would only produce an error. * * `label` rather than a bare string so the catalog sees them: they are * translated where they render. */ const SETTABLE_ROLES: { value: Exclude; label: string }[] = [ { value: "archive", label: "Archive" }, { value: "drafts", label: "Drafts" }, { value: "sent", label: "Sent" }, ]; /** Roles the server keeps to itself, shown but not offered. */ const FIXED_ROLES = new Set(["inbox", "junk", "trash"]); export function FoldersSettings() { const mailboxes = useMail((s) => s.mailboxes); const mailboxPath = useMail((s) => s.mailboxPath); const [share, setShare] = useState(null); const list = useMemo(() => Object.values(mailboxes).map((m) => ({ m, path: mailboxPath(m.id) })).sort((a, b) => a.path.localeCompare(b.path)), [mailboxes, mailboxPath]); const quotas = useMail((s) => s.quotas); const q = quotas.find((x) => x.resourceType === "octets"); /* * A role belongs to exactly one folder -- Stalwart answers "A mailbox with * role 'archive' already exists" -- so a role another folder holds is left * out of the list rather than offered and refused. Clearing it there frees it * here, which is two steps and no surprises. */ const taken = useMemo(() => { const by = new Map(); for (const m of Object.values(mailboxes)) if (m.role) by.set(m.role, m.id); return by; }, [mailboxes]); const setRole = async (m: Mailbox, role: MailboxRole) => { try { await useMail.getState().updateMailbox(m.id, { role }); // Deliberately not naming the role: the value is the protocol's word // ("archive"), and dropping an untranslated English token into nine // languages reads worse than saying nothing about it. The select already // shows what it now is. toast.success(t("Folder role updated")); } catch (err) { toast.error((err as Error).message); } }; const create = async () => { const name = await promptDialog({ title: t("New folder"), placeholder: t("Folder name (use / for subfolders, e.g. Work/Invoices)") }); if (!name?.trim()) return; try { const parts = name.split("/").map((p) => p.trim()).filter(Boolean); let parentId: string | null = null; for (const part of parts) { const existing = Object.values(useMail.getState().mailboxes).find((m) => (m.parentId ?? null) === parentId && m.name.toLowerCase() === part.toLowerCase()); parentId = existing ? existing.id : await useMail.getState().createMailbox(part, parentId); } toast.success(t("Folder created")); } catch (err) { toast.error((err as Error).message); } }; return (

{t("Folders")}

{`${t("Create, rename and hide folders.")} ${q && q.hardLimit ? t("Storage: {used} of {total} used.", { used: formatSize(q.used), total: formatSize(q.hardLimit) }) : ""}`}

{list.map(({ m, path }) => ( ))}
{t("Folder")}{t("Role")}{t("Messages")}{t("Unread")}
{m.role === "inbox" ? : }{mailboxDisplayPath(m, mailboxes)}{!m.isSubscribed && {t("hidden")}}
{m.totalEmails.toLocaleString()} {m.unreadEmails.toLocaleString()}
{Object.keys(m.shareWith ?? {}).length > 0 && }
{share && setShare(null)} />}
); }