"use client"; import { useTranslations } from "next-intl"; import { Mailbox } from "@/lib/jmap/types"; import { getMailboxPath } from "@/lib/utils"; import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuHeader, } from "@/components/ui/context-menu"; import { CheckCheck, ChevronRight, MailOpen, Mails, MoreHorizontal, Trash2, FolderPlus, Pencil, FolderX, RefreshCw, Upload, Users, EyeOff, Bell, BellOff, RotateCcw, } from "@/components/icons"; import { FolderRulesSubMenu } from "@/components/email/rules-menu"; interface Position { x: number; y: number; } export type MailboxContextTarget = | { kind: "mailbox"; mailbox: Mailbox; hasChildren: boolean } | { kind: "folders-section"; accountId?: string }; const PATH_SEPARATOR = " › "; const MAX_PATH_LENGTH = 40; const MAX_SEGMENT_LENGTH = 16; function truncateSegment(name: string): string { return name.length > MAX_SEGMENT_LENGTH ? `${name.slice(0, MAX_SEGMENT_LENGTH - 1)}…` : name; } function renderPathSegments(segments: string[]): React.ReactNode { return ( {segments.map((seg, i) => ( {i > 0 && } {seg === "…" ? : {seg}} ))} ); } function renderShortenedPath(fullPath: string): React.ReactNode { const segments = fullPath.split(PATH_SEPARATOR); if (fullPath.length <= MAX_PATH_LENGTH) { return renderPathSegments(segments); } if (segments.length <= 2) { return renderPathSegments(segments.map(truncateSegment)); } return renderPathSegments([ truncateSegment(segments[0]), "…", truncateSegment(segments[segments.length - 1]), ]); } /** * Per-account and per-folder notification mutes (#1169). An `accountId` of * undefined is the login's own account, as on the folders-section target. */ export interface MailboxNotificationActions { isMailboxMuted: (mailbox: Mailbox) => boolean; isAccountMuted: (accountId?: string) => boolean; /** True when some folder of the account has its own setting. */ hasFolderSettings: (accountId?: string) => boolean; setMailboxMuted: (mailbox: Mailbox, muted: boolean) => void; setAccountMuted: (accountId: string | undefined, muted: boolean) => void; resetFolderSettings: (accountId?: string) => void; } interface MailboxContextMenuProps { target: MailboxContextTarget | null; position: Position; isOpen: boolean; onClose: () => void; menuRef: React.RefObject; mailboxes: Mailbox[]; onMarkFolderRead?: (mailboxId: string) => void; onMarkFolderTreeRead?: (mailboxId: string) => void; onMarkAllFoldersRead?: () => void; onEmptyFolder?: (mailboxId: string) => void; onCreateSubfolder?: (parentId: string) => void; onCreateFolder?: (accountId?: string) => void; onRenameFolder?: (mailboxId: string) => void; onDeleteFolder?: (mailboxId: string) => void; /** Share the folder with other principals (mail:share); omitted when the server lacks it. */ onShareFolder?: (mailboxId: string) => void; onImportEmail?: (mailboxId: string) => void; onRefresh?: () => void; /** Hide a shared account's mail folders from the sidebar (#1228). */ onHideSharedAccount?: (accountId: string) => void; /** Omitted when muting has no effect (no emailPush filter on the server). */ notifications?: MailboxNotificationActions; } export function MailboxContextMenu({ target, position, isOpen, onClose, menuRef, mailboxes, onMarkFolderRead, onMarkFolderTreeRead, onMarkAllFoldersRead, onEmptyFolder, onCreateSubfolder, onCreateFolder, onRenameFolder, onDeleteFolder, onShareFolder, onImportEmail, onRefresh, onHideSharedAccount, notifications, }: MailboxContextMenuProps) { const t = useTranslations("mailbox_context_menu"); const handleAction = (action: () => void) => { action(); onClose(); }; if (!target) return null; if (target.kind === "folders-section") { return ( {!target.accountId && ( <> handleAction(onMarkAllFoldersRead!)} disabled={!onMarkAllFoldersRead} /> )} handleAction(() => onCreateFolder?.(target.accountId))} disabled={!onCreateFolder} testId="mailbox-new-folder" /> handleAction(onRefresh!)} disabled={!onRefresh} /> {notifications && (() => { const accountMuted = notifications.isAccountMuted(target.accountId); return ( <> handleAction(() => notifications.setAccountMuted(target.accountId, !accountMuted))} testId="mailbox-mute-account" /> {notifications.hasFolderSettings(target.accountId) && ( handleAction(() => notifications.resetFolderSettings(target.accountId))} testId="mailbox-reset-folder-mutes" /> )} ); })()} {target.accountId && onHideSharedAccount && ( <> handleAction(() => onHideSharedAccount(target.accountId!))} testId="mailbox-hide-shared-account" /> )} ); } const mailbox = target.mailbox; const isTrashOrJunk = mailbox.role === "trash" || mailbox.role === "junk"; const isSystem = !!mailbox.role && ["inbox", "sent", "drafts", "trash", "junk", "archive"].includes(mailbox.role); const canRename = mailbox.myRights?.mayRename !== false && !isSystem; const canDelete = mailbox.myRights?.mayDelete !== false && !isSystem; const canCreateChild = mailbox.myRights?.mayCreateChild !== false; const canSetSeen = mailbox.myRights?.maySetSeen !== false; const canRemoveItems = mailbox.myRights?.mayRemoveItems !== false; const canAddItems = mailbox.myRights?.mayAddItems !== false; // Sharing needs the mail:share extension (handler present) and, on a folder // someone else shared with us, their permission to re-share it. const canShare = !!onShareFolder && (!mailbox.isShared || mailbox.myRights?.mayShare === true); const fullPath = getMailboxPath(mailbox, mailboxes); return ( {renderShortenedPath(fullPath)} handleAction(() => onMarkFolderRead?.(mailbox.id))} disabled={!onMarkFolderRead || !canSetSeen} /> {target.hasChildren && ( handleAction(() => onMarkFolderTreeRead?.(mailbox.id))} disabled={!onMarkFolderTreeRead || !canSetSeen} /> )} handleAction(() => onCreateSubfolder?.(mailbox.id))} disabled={!onCreateSubfolder || !canCreateChild} testId="mailbox-new-subfolder" /> handleAction(() => onRenameFolder?.(mailbox.id))} disabled={!onRenameFolder || !canRename} testId="mailbox-rename" /> {onShareFolder && ( handleAction(() => onShareFolder(mailbox.id))} disabled={!canShare} testId="mailbox-share" /> )} {notifications && (() => { const muted = notifications.isMailboxMuted(mailbox); return ( handleAction(() => notifications.setMailboxMuted(mailbox, !muted))} testId="mailbox-mute" /> ); })()} handleAction(() => onImportEmail?.(mailbox.id))} disabled={!onImportEmail || !canAddItems} /> handleAction(() => onEmptyFolder?.(mailbox.id))} disabled={!onEmptyFolder || mailbox.totalEmails === 0 || !canRemoveItems} destructive /> handleAction(() => onDeleteFolder?.(mailbox.id))} disabled={!onDeleteFolder || !canDelete} destructive testId="mailbox-delete" /> handleAction(onRefresh!)} disabled={!onRefresh} /> ); }