"use client"; import { useTranslations } from "next-intl"; import { Email, Mailbox } from "@/lib/jmap/types"; import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu, ContextMenuHeader, } from "@/components/ui/context-menu"; import { PluginSlot } from "@/components/plugins/plugin-slot"; import { Reply, ReplyAll, Forward, Mail, MailOpen, Star, Pin, PinOff, Trash2, Archive, FolderInput, Tag, X, Check, Inbox, Send, File, Folder, ShieldAlert, ShieldCheck, EditIcon, CalendarClock, XCircle, } from "lucide-react"; import { cn, buildMailboxTree, MailboxNode } from "@/lib/utils"; import { localizeMailboxName } from "@/lib/mailbox-label"; import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store"; interface Position { x: number; y: number; } interface EmailContextMenuProps { email: Email; position: Position; isOpen: boolean; onClose: () => void; menuRef: React.RefObject; mailboxes: Mailbox[]; selectedMailbox: string; currentMailboxRole?: string; isMultiSelect?: boolean; selectedCount?: number; // Single email actions onReply?: () => void; onReplyAll?: () => void; onForward?: () => void; onMarkAsRead?: (read: boolean) => void; onToggleStar?: () => void; onTogglePinned?: () => void; onDelete?: () => void; onArchive?: () => void; onSetColorTag?: (color: string | null) => void; onMoveToMailbox?: (mailboxId: string) => void; onMarkAsSpam?: () => void; onUndoSpam?: () => void; onEditDraft?: () => void; onCancelScheduled?: () => void; onCancelScheduledForEdit?: () => void; onRescheduleScheduled?: () => void; // Batch actions onBatchMarkAsRead?: (read: boolean) => void; onBatchDelete?: () => void; onBatchArchive?: () => void; onBatchMoveToMailbox?: (mailboxId: string) => void; onBatchMarkAsSpam?: () => void; onBatchUndoSpam?: () => void; } // Get mailbox icon based on role const getMailboxIcon = (role?: string) => { switch (role) { case "inbox": return Inbox; case "sent": return Send; case "drafts": return File; case "trash": return Trash2; case "archive": return Archive; default: return Folder; } }; // Get all active label/color tag IDs from email keywords const getCurrentColors = (keywords: Record | undefined): string[] => { if (!keywords) return []; const tags: string[] = []; for (const key of Object.keys(keywords)) { if ((key.startsWith("$label:") || key.startsWith("$color:")) && keywords[key] === true) { tags.push( key.startsWith("$label:") ? key.slice("$label:".length) : key.slice("$color:".length) ); } } return tags; }; export function EmailContextMenu({ email, position, isOpen, onClose, menuRef, mailboxes, selectedMailbox, currentMailboxRole, isMultiSelect = false, selectedCount = 1, onReply, onReplyAll, onForward, onMarkAsRead, onToggleStar, onTogglePinned, onDelete, onArchive, onSetColorTag, onMoveToMailbox, onMarkAsSpam, onUndoSpam, onBatchMarkAsRead, onBatchDelete, onBatchArchive, onBatchMoveToMailbox, onBatchMarkAsSpam, onBatchUndoSpam, onEditDraft, onCancelScheduled, onCancelScheduledForEdit, onRescheduleScheduled, }: EmailContextMenuProps) { const t = useTranslations("context_menu"); const tSidebar = useTranslations("sidebar"); const _tColor = useTranslations("email_viewer.color_tag"); const emailKeywords = useSettingsStore((state) => state.emailKeywords); const isUnread = !email.keywords?.$seen; const isStarred = email.keywords?.$flagged; const isPinned = email.keywords?.['$pinned'] === true; const isDraft = email.keywords?.['$draft'] === true; const currentColors = getCurrentColors(email.keywords); const showBatchActions = isMultiSelect && selectedCount > 1; const isInJunkFolder = currentMailboxRole === 'junk'; // Marking your own outgoing mail as spam makes no sense - hide the action // in Sent, Drafts and Scheduled. const spamApplicable = !['sent', 'drafts', 'scheduled'].includes(currentMailboxRole || ''); const isScheduled = email.isScheduled === true; const canCancelScheduled = isScheduled && email.scheduledUndoStatus === 'pending'; // Build color options from keyword definitions in settings const colorOptions = emailKeywords.map((kw) => ({ name: kw.label, value: kw.id, color: KEYWORD_PALETTE[kw.color]?.dot || "bg-gray-500", })); // Build mailbox tree for move-to submenu with proper hierarchy const moveTargetIds = new Set( mailboxes .filter( (m) => m.id !== selectedMailbox && m.role !== "drafts" && !m.id.startsWith("shared-") && m.myRights?.mayAddItems ) .map((m) => m.id) ); const mailboxTree = buildMailboxTree(mailboxes); // Filter tree to only include branches that contain valid move targets const filterTree = (nodes: MailboxNode[]): MailboxNode[] => { return nodes.reduce((acc, node) => { const filteredChildren = filterTree(node.children); if (moveTargetIds.has(node.id) || filteredChildren.length > 0) { acc.push({ ...node, children: filteredChildren }); } return acc; }, []); }; const moveTree = filterTree(mailboxTree); const handleAction = (action: () => void) => { action(); onClose(); }; return ( {/* Batch header */} {showBatchActions && ( {t("items_selected", { count: selectedCount })} )} {isScheduled && !showBatchActions && canCancelScheduled && ( <> handleAction(onRescheduleScheduled!)} disabled={!onRescheduleScheduled} /> handleAction(onCancelScheduled!)} disabled={!onCancelScheduled} /> handleAction(onCancelScheduledForEdit!)} disabled={!onCancelScheduledForEdit} /> )} {canCancelScheduled && } {!isScheduled && ( <> {/* Edit Draft - only for single draft emails */} {!isScheduled && !showBatchActions && isDraft && onEditDraft && ( <> handleAction(onEditDraft)} /> )} {/* Single email actions - Reply, Reply All, Forward */} {!isScheduled && !showBatchActions && ( <> handleAction(onReply!)} disabled={!onReply} /> handleAction(onReplyAll!)} disabled={!onReplyAll} /> handleAction(onForward!)} disabled={!onForward} /> )} {/* Archive */} handleAction(showBatchActions ? onBatchArchive! : onArchive!) } disabled={showBatchActions ? !onBatchArchive : !onArchive} /> {/* Delete */} handleAction(showBatchActions ? onBatchDelete! : onDelete!) } disabled={showBatchActions ? !onBatchDelete : !onDelete} destructive /> {/* Move to submenu */} {moveTree.length > 0 && ( {(() => { const renderNodes = (nodes: MailboxNode[]) => { return nodes.map((node) => { const Icon = getMailboxIcon(node.role); const isTarget = moveTargetIds.has(node.id); const nodeLabel = localizeMailboxName(node.role, node.name, (k) => tSidebar(`mailboxes.${k}`)); return (
{isTarget ? ( handleAction(() => showBatchActions ? onBatchMoveToMailbox?.(node.id) : onMoveToMailbox?.(node.id) ) } /> ) : (
{nodeLabel}
)} {node.children.length > 0 && (
{renderNodes(node.children)}
)}
); }); }; return renderNodes(moveTree); })()}
)} {/* Star/Unstar - only for single email */} {!showBatchActions && ( handleAction(onToggleStar!)} disabled={!onToggleStar} /> )} {/* Pin/Unpin - only for single email; pinned mails float to the top of the list */} {!showBatchActions && onTogglePinned && ( handleAction(onTogglePinned)} /> )} {/* Set tag submenu - only for single email */} {!showBatchActions && ( {colorOptions.map((option) => { const isActive = currentColors.includes(option.value); return ( ); })} {currentColors.length > 0 && ( <> handleAction(() => onSetColorTag?.(null))} /> )} )} {/* Spam - contextual based on folder; pointless on own outgoing mail */} {spamApplicable && ( <> handleAction( showBatchActions ? (isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!) : (isInJunkFolder ? onUndoSpam! : onMarkAsSpam!) ) } disabled={showBatchActions ? (isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) : (isInJunkFolder ? !onUndoSpam : !onMarkAsSpam)} destructive={!isInJunkFolder} /> )} {/* Mark as read/unread */} handleAction(() => showBatchActions ? onBatchMarkAsRead?.(isUnread) : onMarkAsRead?.(isUnread) ) } /> )}
); }