mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
660 lines
21 KiB
TypeScript
660 lines
21 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { useTranslations } from "next-intl";
|
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
import { useEmailStore, removalFolder, canRemoveFromFolder, emailLinkSource } from "@/stores/email-store";
|
|
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 { PluginActions, usePluginActions } from "@/components/plugins/plugin-actions";
|
|
import {
|
|
Reply,
|
|
ReplyAll,
|
|
Forward,
|
|
Mail,
|
|
MailOpen,
|
|
Star,
|
|
Pin,
|
|
PinOff,
|
|
Trash2,
|
|
Archive,
|
|
FolderInput,
|
|
FolderX,
|
|
Tag,
|
|
Inbox,
|
|
Send,
|
|
File,
|
|
Folder,
|
|
ShieldAlert,
|
|
ShieldCheck,
|
|
EditIcon,
|
|
CalendarClock,
|
|
Paperclip,
|
|
Link as LinkIcon,
|
|
MessagesSquare,
|
|
Copy,
|
|
Search,
|
|
} from "@/components/icons";
|
|
import { buildMailPath } from "@/lib/deep-links";
|
|
import { useCopyLink } from "@/hooks/use-copy-link";
|
|
import { buildMailboxTree, MailboxNode } from "@/lib/utils";
|
|
import { buildMoveTargets, resolveMoveOwnerAccountId } from "@/lib/move-targets";
|
|
import { localizeMailboxName } from "@/lib/mailbox-label";
|
|
import { getEmailTagIds } from "@/lib/thread-utils";
|
|
import { TagPicker } from "./tag-picker";
|
|
import { RulesContextSubMenu } from "./rules-menu";
|
|
import { useArchiveActionEnabled } from "@/lib/archive-gate";
|
|
|
|
interface Position {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
/** Another connected account the "Copy to" entry can copy into. */
|
|
export interface CopyTargetAccount {
|
|
/** The login (AccountEntry.id). */
|
|
accountId: string;
|
|
label: string;
|
|
/** That account's own folders, as its client returns them. */
|
|
mailboxes: Mailbox[];
|
|
}
|
|
|
|
interface EmailContextMenuProps {
|
|
email: Email;
|
|
position: Position;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
menuRef: React.RefObject<HTMLDivElement | null>;
|
|
mailboxes: Mailbox[];
|
|
selectedMailbox: string;
|
|
currentMailboxRole?: string;
|
|
isMultiSelect?: boolean;
|
|
selectedCount?: number;
|
|
/**
|
|
* The messages whose senders the Rules entry makes rules for: the whole
|
|
* selection, or for a single row the message to take the sender from.
|
|
* Without it, the menu's own message.
|
|
*/
|
|
ruleEmails?: Email[];
|
|
// Single email actions
|
|
onReply?: () => void;
|
|
onReplyAll?: () => void;
|
|
onForward?: () => void;
|
|
onForwardAsAttachment?: () => void;
|
|
/** Mountain View: search for the mail this sender has sent. */
|
|
onFindFromSender?: () => void;
|
|
onMarkAsRead?: (read: boolean) => void;
|
|
onToggleStar?: () => void;
|
|
onTogglePinned?: () => void;
|
|
onDelete?: () => void;
|
|
onArchive?: () => void;
|
|
onSetTag?: (tagId: string | null) => void;
|
|
onMoveToMailbox?: (mailboxId: string) => void;
|
|
/**
|
|
* Take the message out of the open folder only (#615). Offered when the
|
|
* message is also in another folder and the list shows a single folder.
|
|
*/
|
|
onRemoveFromFolder?: () => void;
|
|
onMarkAsSpam?: () => void;
|
|
onUndoSpam?: () => void;
|
|
onEditDraft?: () => void;
|
|
onCancelScheduledForEdit?: () => void;
|
|
onRescheduleScheduled?: () => void;
|
|
// Batch actions
|
|
onBatchMarkAsRead?: (read: boolean) => void;
|
|
/** Tags every selected message carries, and those only some of them do. */
|
|
batchTagIds?: string[];
|
|
batchPartialTagIds?: string[];
|
|
/** Takes the tag off when the whole selection has it, else puts it on all. */
|
|
onBatchToggleTag?: (tagId: string) => void;
|
|
onBatchDelete?: () => void;
|
|
onBatchArchive?: () => void;
|
|
onBatchMoveToMailbox?: (mailboxId: string) => void;
|
|
/** The selected messages, for the batch "Remove from this folder". */
|
|
batchEmails?: Email[];
|
|
onBatchRemoveFromFolder?: () => void;
|
|
onBatchMarkAsSpam?: () => void;
|
|
onBatchUndoSpam?: () => void;
|
|
/**
|
|
* Other connected accounts to offer under "Copy to". Copies the selection
|
|
* (or the single message) and keeps the originals. Hidden when empty.
|
|
*/
|
|
copyTargets?: CopyTargetAccount[];
|
|
onCopyToAccount?: (accountId: string, mailboxId: string) => void;
|
|
onBatchForwardAsAttachment?: () => 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;
|
|
}
|
|
};
|
|
|
|
export function EmailContextMenu({
|
|
email,
|
|
position,
|
|
isOpen,
|
|
onClose,
|
|
menuRef,
|
|
mailboxes,
|
|
selectedMailbox,
|
|
currentMailboxRole,
|
|
isMultiSelect = false,
|
|
selectedCount = 1,
|
|
ruleEmails,
|
|
onReply,
|
|
onReplyAll,
|
|
onForward,
|
|
onForwardAsAttachment,
|
|
onFindFromSender,
|
|
onMarkAsRead,
|
|
onToggleStar,
|
|
onTogglePinned,
|
|
onDelete,
|
|
onArchive,
|
|
onSetTag,
|
|
onMoveToMailbox,
|
|
onRemoveFromFolder,
|
|
onMarkAsSpam,
|
|
onUndoSpam,
|
|
onBatchMarkAsRead,
|
|
batchTagIds,
|
|
batchPartialTagIds,
|
|
onBatchToggleTag,
|
|
onBatchDelete,
|
|
onBatchArchive,
|
|
onBatchMoveToMailbox,
|
|
batchEmails,
|
|
onBatchRemoveFromFolder,
|
|
onBatchMarkAsSpam,
|
|
onBatchUndoSpam,
|
|
onBatchForwardAsAttachment,
|
|
onEditDraft,
|
|
onCancelScheduledForEdit,
|
|
onRescheduleScheduled,
|
|
copyTargets,
|
|
onCopyToAccount,
|
|
}: EmailContextMenuProps) {
|
|
const t = useTranslations("context_menu");
|
|
const archiveActionEnabled = useArchiveActionEnabled();
|
|
const mountainView = useSettingsStore((state) => state.interfaceLayout === "mountain-view");
|
|
const tSidebar = useTranslations("sidebar");
|
|
const tEmailViewer = useTranslations("email_viewer");
|
|
const tDeepLink = useTranslations("deep_link");
|
|
const copyLink = useCopyLink();
|
|
const isUnread = !email.keywords?.$seen;
|
|
const isStarred = email.keywords?.$flagged;
|
|
const isPinned = email.keywords?.['$pinned'] === true;
|
|
const isDraft = email.keywords?.['$draft'] === true;
|
|
const currentTagIds = getEmailTagIds(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';
|
|
// "Remove from this folder" needs one open folder and messages that are
|
|
// also in another one; for a selection, every selected message must be.
|
|
const folderToLeave = useEmailStore(removalFolder);
|
|
const canRemove = showBatchActions
|
|
? !!onBatchRemoveFromFolder && !!batchEmails?.length && batchEmails.every((e) => canRemoveFromFolder(e, folderToLeave))
|
|
: !!onRemoveFromFolder && canRemoveFromFolder(email, folderToLeave);
|
|
|
|
// Move-to submenu: the message's own account first (#1149)
|
|
const { tree: moveTree, targetIds: moveTargetIds } = buildMoveTargets(mailboxes, {
|
|
currentMailboxId: selectedMailbox,
|
|
ownerAccountId: resolveMoveOwnerAccountId(email, mailboxes, selectedMailbox),
|
|
});
|
|
|
|
// Filter tree to only include branches that contain valid targets
|
|
const filterTree = (nodes: MailboxNode[], targetIds: Set<string>): MailboxNode[] => {
|
|
return nodes.reduce<MailboxNode[]>((acc, node) => {
|
|
const filteredChildren = filterTree(node.children, targetIds);
|
|
if (targetIds.has(node.id) || filteredChildren.length > 0) {
|
|
acc.push({ ...node, children: filteredChildren });
|
|
}
|
|
return acc;
|
|
}, []);
|
|
};
|
|
|
|
// Folders of the other connected accounts a copy can land in.
|
|
const copyTrees = (copyTargets ?? [])
|
|
.map((target) => {
|
|
const targetIds = new Set(
|
|
target.mailboxes
|
|
.filter((m) => !m.isShared && m.role !== "drafts" && m.myRights?.mayAddItems)
|
|
.map((m) => m.id)
|
|
);
|
|
return { ...target, targetIds, tree: filterTree(buildMailboxTree(target.mailboxes), targetIds) };
|
|
})
|
|
.filter((target) => target.tree.length > 0);
|
|
|
|
const renderMailboxNodes = (
|
|
nodes: MailboxNode[],
|
|
targetIds: Set<string>,
|
|
testIdPrefix: string,
|
|
onPick: (mailboxId: string) => void,
|
|
): React.ReactNode =>
|
|
nodes.map((node) => {
|
|
const Icon = getMailboxIcon(node.role);
|
|
const nodeLabel = localizeMailboxName(node.role, node.name, (k) => tSidebar(`mailboxes.${k}`));
|
|
return (
|
|
<div key={node.id}>
|
|
{targetIds.has(node.id) ? (
|
|
<ContextMenuItem
|
|
icon={Icon}
|
|
label={nodeLabel}
|
|
testId={`${testIdPrefix}${node.id}`}
|
|
onClick={() => handleAction(() => onPick(node.id))}
|
|
/>
|
|
) : (
|
|
<div className="px-3 py-1.5 text-sm flex items-center gap-2 text-muted-foreground">
|
|
<Icon className="w-4 h-4 flex-shrink-0" />
|
|
<span>{nodeLabel}</span>
|
|
</div>
|
|
)}
|
|
{node.children.length > 0 && (
|
|
<div className="ps-4">
|
|
{renderMailboxNodes(node.children, targetIds, testIdPrefix, onPick)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
const handleAction = (action: () => void) => {
|
|
action();
|
|
onClose();
|
|
};
|
|
|
|
// Stable, so the Rules entry does not re-resolve its account every render.
|
|
const ruleSenderEmails = useMemo(() => ruleEmails ?? [email], [ruleEmails, email]);
|
|
|
|
// The row's plugin buttons (message-row-actions). On phones the long press
|
|
// opens this menu, so it is where those buttons live there.
|
|
const hasRowPluginActions = usePluginActions("message-row-actions", { mailboxRole: currentMailboxRole ?? null }).length > 0;
|
|
const rowPluginContext = useMemo(() => ({ emailId: email.id }), [email.id]);
|
|
const rowPluginBlock = !showBatchActions && hasRowPluginActions ? (
|
|
<>
|
|
<ContextMenuSeparator />
|
|
<PluginActions
|
|
surface="message-row-actions"
|
|
variant="menu"
|
|
context={rowPluginContext}
|
|
mailboxRole={currentMailboxRole ?? null}
|
|
onRan={onClose}
|
|
/>
|
|
</>
|
|
) : null;
|
|
|
|
// The menu's entries, composed below in the default layout's order or,
|
|
// under Mountain View, in Gmail's.
|
|
const replyBlock = (
|
|
<>
|
|
{/* Single email actions - Reply, Reply All, Forward */}
|
|
{!isScheduled && !showBatchActions && (
|
|
<>
|
|
<ContextMenuItem
|
|
icon={Reply}
|
|
label={t("reply")}
|
|
onClick={() => handleAction(onReply!)}
|
|
disabled={!onReply}
|
|
/>
|
|
<ContextMenuItem
|
|
icon={ReplyAll}
|
|
label={t("reply_all")}
|
|
onClick={() => handleAction(onReplyAll!)}
|
|
disabled={!onReplyAll}
|
|
/>
|
|
<ContextMenuItem
|
|
icon={Forward}
|
|
label={t("forward")}
|
|
onClick={() => handleAction(onForward!)}
|
|
disabled={!onForward}
|
|
/>
|
|
<ContextMenuItem
|
|
icon={Paperclip}
|
|
label={tEmailViewer("forward_as_attachment")}
|
|
onClick={() => handleAction(onForwardAsAttachment!)}
|
|
disabled={!onForwardAsAttachment || !email.blobId}
|
|
/>
|
|
<ContextMenuSeparator />
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
const permalinkBlock = (
|
|
<>
|
|
{/* Permalinks (#733). The conversation entry only appears when the
|
|
message actually belongs to a thread worth linking to. */}
|
|
{!showBatchActions && (
|
|
<>
|
|
<ContextMenuItem
|
|
icon={LinkIcon}
|
|
label={tDeepLink("copy_message")}
|
|
onClick={() => handleAction(() => {
|
|
void copyLink(buildMailPath({ mailboxId: null, emailId: email.id, threadId: null, ...emailLinkSource(email) }));
|
|
})}
|
|
/>
|
|
{email.threadId && (
|
|
<ContextMenuItem
|
|
icon={MessagesSquare}
|
|
label={tDeepLink("copy_conversation")}
|
|
onClick={() => handleAction(() => {
|
|
void copyLink(buildMailPath({ mailboxId: null, emailId: null, threadId: email.threadId!, ...emailLinkSource(email) }));
|
|
})}
|
|
/>
|
|
)}
|
|
<ContextMenuSeparator />
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
const archiveItem = archiveActionEnabled && (
|
|
<>
|
|
{/* Archive */}
|
|
<ContextMenuItem
|
|
icon={Archive}
|
|
label={t("archive")}
|
|
onClick={() =>
|
|
handleAction(showBatchActions ? onBatchArchive! : onArchive!)
|
|
}
|
|
disabled={showBatchActions ? !onBatchArchive : !onArchive}
|
|
/>
|
|
</>
|
|
);
|
|
const deleteItem = (
|
|
<>
|
|
{/* Delete */}
|
|
<ContextMenuItem
|
|
icon={Trash2}
|
|
label={t("delete")}
|
|
testId="ctx-delete"
|
|
onClick={() =>
|
|
handleAction(showBatchActions ? onBatchDelete! : onDelete!)
|
|
}
|
|
disabled={showBatchActions ? !onBatchDelete : !onDelete}
|
|
destructive={!mountainView}
|
|
/>
|
|
</>
|
|
);
|
|
const readItem = (
|
|
<>
|
|
{/* Mark as read/unread */}
|
|
<ContextMenuItem
|
|
icon={isUnread ? MailOpen : Mail}
|
|
label={isUnread ? t("mark_read") : t("mark_unread")}
|
|
testId={isUnread ? "ctx-mark-read" : "ctx-mark-unread"}
|
|
onClick={() =>
|
|
handleAction(() =>
|
|
showBatchActions
|
|
? onBatchMarkAsRead?.(isUnread)
|
|
: onMarkAsRead?.(isUnread)
|
|
)
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
const moveItem = (
|
|
<>
|
|
{/* Move to submenu */}
|
|
{moveTree.length > 0 && (
|
|
<ContextMenuSubMenu icon={FolderInput} label={t("move_to")} testId="ctx-move-to">
|
|
{renderMailboxNodes(moveTree, moveTargetIds, "move-to:", (mailboxId) =>
|
|
showBatchActions
|
|
? onBatchMoveToMailbox?.(mailboxId)
|
|
: onMoveToMailbox?.(mailboxId)
|
|
)}
|
|
</ContextMenuSubMenu>
|
|
)}
|
|
{canRemove && (
|
|
<ContextMenuItem
|
|
icon={FolderX}
|
|
label={t("remove_from_folder")}
|
|
testId="ctx-remove-from-folder"
|
|
onClick={() => handleAction(showBatchActions ? onBatchRemoveFromFolder! : onRemoveFromFolder!)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
const copyItem = (
|
|
<>
|
|
{/* Copy to another connected account, keeping the originals */}
|
|
{onCopyToAccount && copyTrees.length > 0 && (
|
|
<ContextMenuSubMenu icon={Copy} label={t("copy_to_account")} testId="ctx-copy-to">
|
|
{copyTrees.map((target) => (
|
|
<div key={target.accountId}>
|
|
<ContextMenuHeader>{target.label}</ContextMenuHeader>
|
|
{renderMailboxNodes(target.tree, target.targetIds, `copy-to:${target.accountId}:`, (mailboxId) =>
|
|
onCopyToAccount(target.accountId, mailboxId)
|
|
)}
|
|
</div>
|
|
))}
|
|
</ContextMenuSubMenu>
|
|
)}
|
|
</>
|
|
);
|
|
const rulesItem = (
|
|
<>
|
|
{/* Rules: filter rules made from the sender(s), saved as Sieve */}
|
|
<RulesContextSubMenu anchor={email} senderEmails={ruleSenderEmails} onClose={onClose} />
|
|
</>
|
|
);
|
|
const starItem = (
|
|
<>
|
|
{/* Star/Unstar - only for single email */}
|
|
{!showBatchActions && (
|
|
<ContextMenuItem
|
|
icon={Star}
|
|
label={isStarred ? t("unstar") : t("star")}
|
|
onClick={() => handleAction(onToggleStar!)}
|
|
disabled={!onToggleStar}
|
|
testId={isStarred ? "ctx-unstar" : "ctx-star"}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
const pinItem = (
|
|
<>
|
|
{/* Pin/Unpin - only for single email; pinned mails float to the top of the list */}
|
|
{!showBatchActions && onTogglePinned && (
|
|
<ContextMenuItem
|
|
icon={isPinned ? PinOff : Pin}
|
|
label={isPinned ? t("unpin") : t("pin")}
|
|
onClick={() => handleAction(onTogglePinned)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
const tagItem = (
|
|
<>
|
|
{/* Set tag submenu - the row's own tags, or the whole selection's (#1077) */}
|
|
{(!showBatchActions || onBatchToggleTag) && (
|
|
<ContextMenuSubMenu icon={Tag} label={t("tag")} testId="ctx-tag">
|
|
<div className="w-56 max-w-[18rem]">
|
|
{showBatchActions ? (
|
|
<TagPicker
|
|
selectedIds={batchTagIds ?? []}
|
|
partialIds={batchPartialTagIds}
|
|
onToggle={(tagId) => onBatchToggleTag?.(tagId)}
|
|
/>
|
|
) : (
|
|
<TagPicker
|
|
selectedIds={currentTagIds}
|
|
onToggle={(tagId) => onSetTag?.(tagId)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</ContextMenuSubMenu>
|
|
)}
|
|
</>
|
|
);
|
|
const spamBlock = (
|
|
<>
|
|
{/* Spam - contextual based on folder; pointless on own outgoing mail */}
|
|
{spamApplicable && (
|
|
<>
|
|
<ContextMenuSeparator />
|
|
|
|
<ContextMenuItem
|
|
icon={isInJunkFolder ? ShieldCheck : ShieldAlert}
|
|
label={isInJunkFolder ? t("not_spam") : t("mark_as_spam")}
|
|
testId={isInJunkFolder ? "ctx-not-spam" : "ctx-spam"}
|
|
onClick={() =>
|
|
handleAction(
|
|
showBatchActions
|
|
? (isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!)
|
|
: (isInJunkFolder ? onUndoSpam! : onMarkAsSpam!)
|
|
)
|
|
}
|
|
disabled={showBatchActions ? (isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) : (isInJunkFolder ? !onUndoSpam : !onMarkAsSpam)}
|
|
destructive={!isInJunkFolder && !mountainView}
|
|
/>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
const sender = email.from?.[0];
|
|
const findBlock = !showBatchActions && onFindFromSender && sender?.email ? (
|
|
<>
|
|
<ContextMenuSeparator />
|
|
<ContextMenuItem
|
|
icon={Search}
|
|
label={t("find_emails_from", { name: sender.name || sender.email })}
|
|
testId="ctx-find-from"
|
|
onClick={() => handleAction(onFindFromSender)}
|
|
/>
|
|
</>
|
|
) : null;
|
|
|
|
return (
|
|
<ContextMenu
|
|
ref={menuRef}
|
|
isOpen={isOpen}
|
|
position={position}
|
|
onClose={onClose}
|
|
>
|
|
{/* Batch header */}
|
|
{showBatchActions && (
|
|
<ContextMenuHeader>
|
|
{t("items_selected", { count: selectedCount })}
|
|
</ContextMenuHeader>
|
|
)}
|
|
|
|
{isScheduled && !showBatchActions && canCancelScheduled && (
|
|
<>
|
|
<ContextMenuItem
|
|
icon={CalendarClock}
|
|
label={t("reschedule_send")}
|
|
onClick={() => handleAction(onRescheduleScheduled!)}
|
|
disabled={!onRescheduleScheduled}
|
|
/>
|
|
<ContextMenuItem
|
|
icon={EditIcon}
|
|
label={email.isSmimeScheduled ? t("cancel_and_compose_again") : t("cancel_and_edit")}
|
|
onClick={() => handleAction(onCancelScheduledForEdit!)}
|
|
disabled={!onCancelScheduledForEdit}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{canCancelScheduled && <ContextMenuSeparator />}
|
|
|
|
{!isScheduled && (
|
|
<>
|
|
|
|
{/* Batch forward - multi-select sibling of the single-message item below.
|
|
First action in batch mode so compose-style actions stay above the
|
|
archive/delete group, mirroring the single menu's ordering. */}
|
|
{showBatchActions && (
|
|
<>
|
|
<ContextMenuItem
|
|
icon={Paperclip}
|
|
label={t("forward_as_attachments")}
|
|
testId="ctx-batch-forward-as-attachment"
|
|
onClick={() => handleAction(onBatchForwardAsAttachment!)}
|
|
disabled={!onBatchForwardAsAttachment}
|
|
/>
|
|
<ContextMenuSeparator />
|
|
</>
|
|
)}
|
|
|
|
{/* Edit Draft - only for single draft emails */}
|
|
{!isScheduled && !showBatchActions && isDraft && onEditDraft && (
|
|
<>
|
|
<ContextMenuItem
|
|
icon={EditIcon}
|
|
label={t("edit_draft")}
|
|
onClick={() => handleAction(onEditDraft)}
|
|
/>
|
|
<ContextMenuSeparator />
|
|
</>
|
|
)}
|
|
|
|
{mountainView ? (
|
|
<>
|
|
{replyBlock}
|
|
{archiveItem}
|
|
{deleteItem}
|
|
{readItem}
|
|
<ContextMenuSeparator />
|
|
{moveItem}
|
|
{tagItem}
|
|
{rulesItem}
|
|
{copyItem}
|
|
<ContextMenuSeparator />
|
|
{starItem}
|
|
{pinItem}
|
|
{spamBlock}
|
|
{findBlock}
|
|
<ContextMenuSeparator />
|
|
{permalinkBlock}
|
|
{rowPluginBlock}
|
|
</>
|
|
) : (
|
|
<>
|
|
{replyBlock}
|
|
{permalinkBlock}
|
|
{archiveItem}
|
|
{deleteItem}
|
|
<ContextMenuSeparator />
|
|
{moveItem}
|
|
{copyItem}
|
|
{rulesItem}
|
|
{starItem}
|
|
{pinItem}
|
|
{tagItem}
|
|
{spamBlock}
|
|
<ContextMenuSeparator />
|
|
{readItem}
|
|
{rowPluginBlock}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<PluginSlot name="context-menu-email" />
|
|
</ContextMenu>
|
|
);
|
|
}
|