feat: add right-click context menu to mail folders sidebar
This commit is contained in:
@@ -14,6 +14,7 @@ import { useAccountStore } from "@/stores/account-store";
|
||||
import type { UnifiedAccountClient } from "@/lib/unified-mailbox";
|
||||
import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal";
|
||||
import { useEmailStore } from "@/stores/email-store";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { useContactStore } from "@/stores/contact-store";
|
||||
@@ -162,6 +163,11 @@ export default function Home() {
|
||||
fetchUnifiedEmails: fetchUnifiedEmailsAction,
|
||||
refreshUnifiedCounts,
|
||||
exitUnifiedView,
|
||||
emptyMailbox,
|
||||
markMailboxAsRead,
|
||||
createMailbox,
|
||||
renameMailbox,
|
||||
deleteMailbox,
|
||||
} = useEmailStore();
|
||||
|
||||
const enableUnifiedMailbox = useSettingsStore((s) => s.enableUnifiedMailbox);
|
||||
@@ -1097,6 +1103,181 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
const tCtxMenu = t;
|
||||
|
||||
const handleMarkFolderRead = async (mailboxId: string) => {
|
||||
if (!client) return;
|
||||
try {
|
||||
const count = await markMailboxAsRead(client, mailboxId);
|
||||
await fetchMailboxes(client);
|
||||
if (selectedMailbox === mailboxId) await fetchEmails(client, mailboxId);
|
||||
if (count > 0) {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_marked_read_count', { count }));
|
||||
} else {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_already_read'));
|
||||
}
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_mark_read'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkFolderTreeRead = async (mailboxId: string) => {
|
||||
if (!client) return;
|
||||
const collectIds = (rootId: string): string[] => {
|
||||
const ids: string[] = [rootId];
|
||||
const stack = [rootId];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()!;
|
||||
for (const mb of mailboxes) {
|
||||
if (mb.parentId === current) {
|
||||
ids.push(mb.id);
|
||||
stack.push(mb.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
|
||||
try {
|
||||
const ids = collectIds(mailboxId);
|
||||
let total = 0;
|
||||
for (const id of ids) {
|
||||
total += await markMailboxAsRead(client, id);
|
||||
}
|
||||
await fetchMailboxes(client);
|
||||
if (selectedMailbox && ids.includes(selectedMailbox)) await fetchEmails(client, selectedMailbox);
|
||||
if (total > 0) {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_marked_read_count', { count: total }));
|
||||
} else {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_already_read'));
|
||||
}
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_mark_read'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkAllFoldersRead = async () => {
|
||||
if (!client) return;
|
||||
|
||||
const confirmed = await confirmDialog({
|
||||
title: tCtxMenu('mailbox_context_menu.mark_all_confirm_title'),
|
||||
message: tCtxMenu('mailbox_context_menu.mark_all_confirm_message'),
|
||||
confirmText: tCtxMenu('mailbox_context_menu.mark_all_folders_read'),
|
||||
variant: "default",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const total = await client.markAllAsRead();
|
||||
await fetchMailboxes(client);
|
||||
if (selectedMailbox) await fetchEmails(client, selectedMailbox);
|
||||
if (total > 0) {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_marked_read_count', { count: total }));
|
||||
} else {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_already_read'));
|
||||
}
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_mark_read'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmptyFolderFromContextMenu = async (mailboxId: string) => {
|
||||
if (!client) return;
|
||||
const mailbox = mailboxes.find(mb => mb.id === mailboxId);
|
||||
if (!mailbox) return;
|
||||
|
||||
const confirmed = await confirmDialog({
|
||||
title: tCtxMenu('email_list.empty_folder.confirm_title'),
|
||||
message: tCtxMenu('email_list.empty_folder.confirm_message'),
|
||||
confirmText: tCtxMenu('email_list.empty_folder.confirm_button'),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await emptyMailbox(client, mailboxId);
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_emptied'));
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_empty'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateSubfolderFromContextMenu = async (parentId: string) => {
|
||||
if (!client) return;
|
||||
const name = window.prompt(tCtxMenu('mailbox_context_menu.prompt_new_subfolder'));
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
await createMailbox(client, name.trim(), parentId);
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_folder_created'));
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_create'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateFolderFromContextMenu = async () => {
|
||||
if (!client) return;
|
||||
const name = window.prompt(tCtxMenu('mailbox_context_menu.prompt_new_folder'));
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
await createMailbox(client, name.trim());
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_folder_created'));
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_create'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameFolderFromContextMenu = async (mailboxId: string) => {
|
||||
if (!client) return;
|
||||
const mailbox = mailboxes.find(mb => mb.id === mailboxId);
|
||||
if (!mailbox) return;
|
||||
const name = window.prompt(tCtxMenu('mailbox_context_menu.prompt_rename'), mailbox.name);
|
||||
if (!name || !name.trim() || name.trim() === mailbox.name) return;
|
||||
try {
|
||||
await renameMailbox(client, mailboxId, name.trim());
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_folder_renamed'));
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_rename'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFolderFromContextMenu = async (mailboxId: string) => {
|
||||
if (!client) return;
|
||||
const mailbox = mailboxes.find(mb => mb.id === mailboxId);
|
||||
if (!mailbox) return;
|
||||
|
||||
const confirmed = await confirmDialog({
|
||||
title: tCtxMenu('mailbox_context_menu.delete_confirm_title'),
|
||||
message: tCtxMenu('mailbox_context_menu.delete_confirm_message', { name: mailbox.name }),
|
||||
confirmText: tCtxMenu('mailbox_context_menu.delete_folder'),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await deleteMailbox(client, mailboxId);
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_folder_deleted'));
|
||||
} catch (err: unknown) {
|
||||
const jmapType = (err as Error & { jmapType?: string })?.jmapType;
|
||||
if (jmapType === 'mailboxHasChild') {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_delete_has_children'));
|
||||
} else if (jmapType === 'mailboxHasEmail') {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_delete_has_email'));
|
||||
} else {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_delete'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshMailboxes = async () => {
|
||||
if (!client) return;
|
||||
try {
|
||||
await fetchMailboxes(client);
|
||||
if (selectedMailbox) await fetchEmails(client, selectedMailbox);
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = logout;
|
||||
|
||||
const handleSearch = async (query: string) => {
|
||||
@@ -1454,6 +1635,15 @@ export default function Home() {
|
||||
onMailboxSelect={handleMailboxSelect}
|
||||
onTagSelect={handleTagSelect}
|
||||
onUnreadFilterClick={handleUnreadFilterClick}
|
||||
onMarkFolderRead={handleMarkFolderRead}
|
||||
onMarkFolderTreeRead={handleMarkFolderTreeRead}
|
||||
onMarkAllFoldersRead={handleMarkAllFoldersRead}
|
||||
onEmptyFolder={handleEmptyFolderFromContextMenu}
|
||||
onCreateSubfolder={handleCreateSubfolderFromContextMenu}
|
||||
onCreateFolder={handleCreateFolderFromContextMenu}
|
||||
onRenameFolder={handleRenameFolderFromContextMenu}
|
||||
onDeleteFolder={handleDeleteFolderFromContextMenu}
|
||||
onRefreshMailboxes={handleRefreshMailboxes}
|
||||
onCompose={() => {
|
||||
setComposerMode('compose');
|
||||
setShowComposer(true);
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Mailbox } from "@/lib/jmap/types";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuHeader,
|
||||
} from "@/components/ui/context-menu";
|
||||
import {
|
||||
CheckCheck,
|
||||
MailOpen,
|
||||
Mails,
|
||||
Trash2,
|
||||
FolderPlus,
|
||||
Pencil,
|
||||
FolderX,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
|
||||
interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export type MailboxContextTarget =
|
||||
| { kind: "mailbox"; mailbox: Mailbox; hasChildren: boolean }
|
||||
| { kind: "folders-section" };
|
||||
|
||||
interface MailboxContextMenuProps {
|
||||
target: MailboxContextTarget | null;
|
||||
position: Position;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
menuRef: React.RefObject<HTMLDivElement | null>;
|
||||
onMarkFolderRead?: (mailboxId: string) => void;
|
||||
onMarkFolderTreeRead?: (mailboxId: string) => void;
|
||||
onMarkAllFoldersRead?: () => void;
|
||||
onEmptyFolder?: (mailboxId: string) => void;
|
||||
onCreateSubfolder?: (parentId: string) => void;
|
||||
onCreateFolder?: () => void;
|
||||
onRenameFolder?: (mailboxId: string) => void;
|
||||
onDeleteFolder?: (mailboxId: string) => void;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
export function MailboxContextMenu({
|
||||
target,
|
||||
position,
|
||||
isOpen,
|
||||
onClose,
|
||||
menuRef,
|
||||
onMarkFolderRead,
|
||||
onMarkFolderTreeRead,
|
||||
onMarkAllFoldersRead,
|
||||
onEmptyFolder,
|
||||
onCreateSubfolder,
|
||||
onCreateFolder,
|
||||
onRenameFolder,
|
||||
onDeleteFolder,
|
||||
onRefresh,
|
||||
}: MailboxContextMenuProps) {
|
||||
const t = useTranslations("mailbox_context_menu");
|
||||
|
||||
const handleAction = (action: () => void) => {
|
||||
action();
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!target) return null;
|
||||
|
||||
if (target.kind === "folders-section") {
|
||||
return (
|
||||
<ContextMenu ref={menuRef} isOpen={isOpen} position={position} onClose={onClose}>
|
||||
<ContextMenuItem
|
||||
icon={CheckCheck}
|
||||
label={t("mark_all_folders_read")}
|
||||
onClick={() => handleAction(onMarkAllFoldersRead!)}
|
||||
disabled={!onMarkAllFoldersRead}
|
||||
/>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem
|
||||
icon={FolderPlus}
|
||||
label={t("new_folder")}
|
||||
onClick={() => handleAction(onCreateFolder!)}
|
||||
disabled={!onCreateFolder}
|
||||
/>
|
||||
<ContextMenuItem
|
||||
icon={RefreshCw}
|
||||
label={t("refresh")}
|
||||
onClick={() => handleAction(onRefresh!)}
|
||||
disabled={!onRefresh}
|
||||
/>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<ContextMenu ref={menuRef} isOpen={isOpen} position={position} onClose={onClose}>
|
||||
<ContextMenuHeader>{mailbox.name}</ContextMenuHeader>
|
||||
|
||||
<ContextMenuItem
|
||||
icon={MailOpen}
|
||||
label={t("mark_folder_read")}
|
||||
onClick={() => handleAction(() => onMarkFolderRead?.(mailbox.id))}
|
||||
disabled={!onMarkFolderRead || !canSetSeen}
|
||||
/>
|
||||
{target.hasChildren && (
|
||||
<ContextMenuItem
|
||||
icon={Mails}
|
||||
label={t("mark_folder_tree_read")}
|
||||
onClick={() => handleAction(() => onMarkFolderTreeRead?.(mailbox.id))}
|
||||
disabled={!onMarkFolderTreeRead || !canSetSeen}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
<ContextMenuItem
|
||||
icon={FolderPlus}
|
||||
label={t("new_subfolder")}
|
||||
onClick={() => handleAction(() => onCreateSubfolder?.(mailbox.id))}
|
||||
disabled={!onCreateSubfolder || !canCreateChild}
|
||||
/>
|
||||
<ContextMenuItem
|
||||
icon={Pencil}
|
||||
label={t("rename")}
|
||||
onClick={() => handleAction(() => onRenameFolder?.(mailbox.id))}
|
||||
disabled={!onRenameFolder || !canRename}
|
||||
/>
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
<ContextMenuItem
|
||||
icon={FolderX}
|
||||
label={isTrashOrJunk ? t("empty_folder") : t("empty_folder_generic")}
|
||||
onClick={() => handleAction(() => onEmptyFolder?.(mailbox.id))}
|
||||
disabled={!onEmptyFolder || mailbox.totalEmails === 0 || !canRemoveItems}
|
||||
destructive
|
||||
/>
|
||||
<ContextMenuItem
|
||||
icon={Trash2}
|
||||
label={t("delete_folder")}
|
||||
onClick={() => handleAction(() => onDeleteFolder?.(mailbox.id))}
|
||||
disabled={!onDeleteFolder || !canDelete}
|
||||
destructive
|
||||
/>
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
<ContextMenuItem
|
||||
icon={RefreshCw}
|
||||
label={t("refresh")}
|
||||
onClick={() => handleAction(onRefresh!)}
|
||||
disabled={!onRefresh}
|
||||
/>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
} from "lucide-react";
|
||||
import { cn, buildMailboxTree, MailboxNode } from "@/lib/utils";
|
||||
import { Mailbox } from "@/lib/jmap/types";
|
||||
import { useContextMenu } from "@/hooks/use-context-menu";
|
||||
import { MailboxContextMenu, type MailboxContextTarget } from "./mailbox-context-menu";
|
||||
import { useAccountStore } from '@/stores/account-store';
|
||||
import { UNIFIED_MAILBOX_IDS } from '@/lib/jmap/types';
|
||||
import type { UnifiedMailboxRole } from '@/lib/jmap/types';
|
||||
@@ -56,6 +58,15 @@ interface SidebarProps {
|
||||
onCompose?: () => void;
|
||||
onSidebarClose?: () => void;
|
||||
onUnreadFilterClick?: (mailboxId: string) => void;
|
||||
onMarkFolderRead?: (mailboxId: string) => void;
|
||||
onMarkFolderTreeRead?: (mailboxId: string) => void;
|
||||
onMarkAllFoldersRead?: () => void;
|
||||
onEmptyFolder?: (mailboxId: string) => void;
|
||||
onCreateSubfolder?: (parentId: string) => void;
|
||||
onCreateFolder?: () => void;
|
||||
onRenameFolder?: (mailboxId: string) => void;
|
||||
onDeleteFolder?: (mailboxId: string) => void;
|
||||
onRefreshMailboxes?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -187,6 +198,7 @@ interface SidebarRowProps {
|
||||
dropHandlers?: Record<string, unknown>;
|
||||
isValidDropTarget?: boolean;
|
||||
isInvalidDropTarget?: boolean;
|
||||
onContextMenu?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
function SidebarRow({
|
||||
@@ -206,6 +218,7 @@ function SidebarRow({
|
||||
dropHandlers,
|
||||
isValidDropTarget,
|
||||
isInvalidDropTarget,
|
||||
onContextMenu,
|
||||
}: SidebarRowProps) {
|
||||
const t = useTranslations('sidebar');
|
||||
const leftPad = isCollapsed ? 0 : ROW_PX_BASE + depth * INDENT_STEP;
|
||||
@@ -213,6 +226,7 @@ function SidebarRow({
|
||||
return (
|
||||
<div
|
||||
{...(dropHandlers || {})}
|
||||
onContextMenu={onContextMenu}
|
||||
style={{ paddingBlock: 'var(--density-sidebar-py)' }}
|
||||
className={cn(
|
||||
"group w-full flex items-center max-lg:min-h-[44px] text-sm transition-colors duration-150",
|
||||
@@ -365,6 +379,7 @@ function MailboxTreeItem({
|
||||
isCollapsed,
|
||||
onUnreadFilterClick,
|
||||
colorful,
|
||||
onContextMenu,
|
||||
}: {
|
||||
node: MailboxNode;
|
||||
selectedMailbox: string;
|
||||
@@ -374,6 +389,7 @@ function MailboxTreeItem({
|
||||
isCollapsed: boolean;
|
||||
onUnreadFilterClick?: (mailboxId: string) => void;
|
||||
colorful: boolean;
|
||||
onContextMenu?: (e: React.MouseEvent, node: MailboxNode) => void;
|
||||
}) {
|
||||
const tNotifications = useTranslations('notifications');
|
||||
const hasChildren = node.children.length > 0;
|
||||
@@ -423,6 +439,7 @@ function MailboxTreeItem({
|
||||
dropHandlers={globalDragging ? (dropHandlers as Record<string, unknown>) : undefined}
|
||||
isValidDropTarget={isValidDropTarget}
|
||||
isInvalidDropTarget={isInvalidDropTarget}
|
||||
onContextMenu={onContextMenu && !isVirtualNode ? (e) => onContextMenu(e, node) : undefined}
|
||||
/>
|
||||
|
||||
{hasChildren && isExpanded && !isCollapsed && node.children.map((child) => (
|
||||
@@ -436,6 +453,7 @@ function MailboxTreeItem({
|
||||
isCollapsed={isCollapsed}
|
||||
onUnreadFilterClick={onUnreadFilterClick}
|
||||
colorful={colorful}
|
||||
onContextMenu={onContextMenu}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@@ -610,6 +628,15 @@ export function Sidebar({
|
||||
onCompose: _onCompose,
|
||||
onSidebarClose,
|
||||
onUnreadFilterClick,
|
||||
onMarkFolderRead,
|
||||
onMarkFolderTreeRead,
|
||||
onMarkAllFoldersRead,
|
||||
onEmptyFolder,
|
||||
onCreateSubfolder,
|
||||
onCreateFolder,
|
||||
onRenameFolder,
|
||||
onDeleteFolder,
|
||||
onRefreshMailboxes,
|
||||
className,
|
||||
}: SidebarProps) {
|
||||
const router = useRouter();
|
||||
@@ -790,6 +817,23 @@ export function Sidebar({
|
||||
router.push('/settings');
|
||||
};
|
||||
|
||||
const {
|
||||
contextMenu: mailboxContextMenu,
|
||||
openContextMenu: openMailboxContextMenu,
|
||||
closeContextMenu: closeMailboxContextMenu,
|
||||
menuRef: mailboxMenuRef,
|
||||
} = useContextMenu<MailboxContextTarget>();
|
||||
|
||||
const handleMailboxContextMenu = (e: React.MouseEvent, node: MailboxNode) => {
|
||||
const mailbox = mailboxes.find(mb => mb.id === node.id);
|
||||
if (!mailbox) return;
|
||||
openMailboxContextMenu(e, { kind: "mailbox", mailbox, hasChildren: node.children.length > 0 });
|
||||
};
|
||||
|
||||
const handleFoldersHeaderContextMenu = (e: React.MouseEvent) => {
|
||||
openMailboxContextMenu(e, { kind: "folders-section" });
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -866,7 +910,7 @@ export function Sidebar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div onContextMenu={handleFoldersHeaderContextMenu}>
|
||||
<SidebarSectionHeader
|
||||
label={t("folders")}
|
||||
expanded={foldersExpanded}
|
||||
@@ -894,6 +938,7 @@ export function Sidebar({
|
||||
isCollapsed={isCollapsed}
|
||||
onUnreadFilterClick={onUnreadFilterClick}
|
||||
colorful={colorfulSidebarIcons}
|
||||
onContextMenu={handleMailboxContextMenu}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -934,6 +979,7 @@ export function Sidebar({
|
||||
isCollapsed={isCollapsed}
|
||||
onUnreadFilterClick={onUnreadFilterClick}
|
||||
colorful={colorfulSidebarIcons}
|
||||
onContextMenu={handleMailboxContextMenu}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -975,6 +1021,23 @@ export function Sidebar({
|
||||
|
||||
{!isCollapsed && <PluginSlot name="sidebar-widget" className="border-t border-border" />}
|
||||
</div>
|
||||
|
||||
<MailboxContextMenu
|
||||
target={mailboxContextMenu.data}
|
||||
position={mailboxContextMenu.position}
|
||||
isOpen={mailboxContextMenu.isOpen}
|
||||
onClose={closeMailboxContextMenu}
|
||||
menuRef={mailboxMenuRef}
|
||||
onMarkFolderRead={onMarkFolderRead}
|
||||
onMarkFolderTreeRead={onMarkFolderTreeRead}
|
||||
onMarkAllFoldersRead={onMarkAllFoldersRead}
|
||||
onEmptyFolder={onEmptyFolder}
|
||||
onCreateSubfolder={onCreateSubfolder}
|
||||
onCreateFolder={onCreateFolder}
|
||||
onRenameFolder={onRenameFolder}
|
||||
onDeleteFolder={onDeleteFolder}
|
||||
onRefresh={onRefreshMailboxes}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -312,6 +312,33 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
return removed;
|
||||
}
|
||||
|
||||
async markMailboxAsRead(mailboxId: string): Promise<number> {
|
||||
let count = 0;
|
||||
for (const email of this.data.emails) {
|
||||
if (email.mailboxIds[mailboxId] && email.keywords.$seen !== true) {
|
||||
email.keywords.$seen = true;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
this.recalcMailboxCounts();
|
||||
return count;
|
||||
}
|
||||
|
||||
async markAllAsRead(excludeMailboxIds: string[] = []): Promise<number> {
|
||||
const excluded = new Set(excludeMailboxIds);
|
||||
let count = 0;
|
||||
for (const email of this.data.emails) {
|
||||
if (email.keywords.$seen === true) continue;
|
||||
const mbIds = Object.keys(email.mailboxIds);
|
||||
const onlyInExcluded = mbIds.length > 0 && mbIds.every(id => excluded.has(id));
|
||||
if (onlyInExcluded) continue;
|
||||
email.keywords.$seen = true;
|
||||
count++;
|
||||
}
|
||||
this.recalcMailboxCounts();
|
||||
return count;
|
||||
}
|
||||
|
||||
async markAsSpam(emailId: string): Promise<void> {
|
||||
const email = this.data.emails.find(e => e.id === emailId);
|
||||
const junkMb = this.data.mailboxes.find(m => m.role === 'junk');
|
||||
|
||||
@@ -94,6 +94,8 @@ export interface IJMAPClient {
|
||||
): Promise<void>;
|
||||
moveEmail(emailId: string, toMailboxId: string, accountId?: string): Promise<void>;
|
||||
emptyMailbox(mailboxId: string): Promise<number>;
|
||||
markMailboxAsRead(mailboxId: string, accountId?: string): Promise<number>;
|
||||
markAllAsRead(excludeMailboxIds?: string[], accountId?: string): Promise<number>;
|
||||
markAsSpam(emailId: string, accountId?: string): Promise<void>;
|
||||
undoSpam(emailId: string, originalMailboxId: string, accountId?: string): Promise<void>;
|
||||
|
||||
|
||||
@@ -1361,6 +1361,99 @@ export class JMAPClient implements IJMAPClient {
|
||||
return totalDestroyed;
|
||||
}
|
||||
|
||||
async markMailboxAsRead(mailboxId: string, accountId?: string): Promise<number> {
|
||||
const targetAccountId = accountId || this.accountId;
|
||||
let totalMarked = 0;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const queryResponse = await this.request([
|
||||
["Email/query", {
|
||||
accountId: targetAccountId,
|
||||
filter: {
|
||||
operator: "AND",
|
||||
conditions: [
|
||||
{ inMailbox: mailboxId },
|
||||
{ notKeyword: "$seen" },
|
||||
],
|
||||
},
|
||||
limit: 500,
|
||||
}, "0"],
|
||||
]);
|
||||
|
||||
const ids: string[] = queryResponse.methodResponses?.[0]?.[1]?.ids || [];
|
||||
if (ids.length === 0) break;
|
||||
|
||||
const updates = Object.fromEntries(
|
||||
ids.map((id) => [id, { "keywords/$seen": true }])
|
||||
);
|
||||
|
||||
await this.request([
|
||||
["Email/set", { accountId: targetAccountId, update: updates }, "0"],
|
||||
]);
|
||||
|
||||
totalMarked += ids.length;
|
||||
hasMore = ids.length === 500;
|
||||
}
|
||||
|
||||
return totalMarked;
|
||||
}
|
||||
|
||||
async markAllAsRead(excludeMailboxIds: string[] = [], accountId?: string): Promise<number> {
|
||||
const targetAccountId = accountId || this.accountId;
|
||||
const excludeSet = new Set(excludeMailboxIds);
|
||||
let totalMarked = 0;
|
||||
let hasMore = true;
|
||||
let position = 0;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await this.request([
|
||||
["Email/query", {
|
||||
accountId: targetAccountId,
|
||||
filter: { notKeyword: "$seen" },
|
||||
limit: 500,
|
||||
position,
|
||||
}, "0"],
|
||||
["Email/get", {
|
||||
accountId: targetAccountId,
|
||||
"#ids": { resultOf: "0", name: "Email/query", path: "/ids" },
|
||||
properties: ["id", "mailboxIds"],
|
||||
}, "1"],
|
||||
]);
|
||||
|
||||
const queryResult = response.methodResponses?.[0]?.[1];
|
||||
const getResult = response.methodResponses?.[1]?.[1];
|
||||
const ids: string[] = queryResult?.ids || [];
|
||||
const emails: Array<{ id: string; mailboxIds?: Record<string, boolean> }> = getResult?.list || [];
|
||||
|
||||
if (ids.length === 0) break;
|
||||
|
||||
const targetIds = excludeSet.size === 0
|
||||
? ids
|
||||
: emails
|
||||
.filter(e => {
|
||||
const mbIds = e.mailboxIds ? Object.keys(e.mailboxIds) : [];
|
||||
return mbIds.some(id => !excludeSet.has(id));
|
||||
})
|
||||
.map(e => e.id);
|
||||
|
||||
if (targetIds.length > 0) {
|
||||
const updates = Object.fromEntries(
|
||||
targetIds.map((id) => [id, { "keywords/$seen": true }])
|
||||
);
|
||||
await this.request([
|
||||
["Email/set", { accountId: targetAccountId, update: updates }, "0"],
|
||||
]);
|
||||
totalMarked += targetIds.length;
|
||||
}
|
||||
|
||||
hasMore = ids.length === 500;
|
||||
position += ids.length;
|
||||
}
|
||||
|
||||
return totalMarked;
|
||||
}
|
||||
|
||||
async markAsSpam(emailId: string, accountId?: string): Promise<void> {
|
||||
const targetAccountId = accountId || this.accountId;
|
||||
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count} E-Mails ausgewählt",
|
||||
"edit_draft": "Entwurf bearbeiten"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Ordner als gelesen markieren",
|
||||
"mark_folder_tree_read": "Ordner & Unterordner als gelesen markieren",
|
||||
"mark_all_folders_read": "Alle Ordner als gelesen markieren",
|
||||
"new_subfolder": "Neuer Unterordner...",
|
||||
"new_folder": "Neuer Ordner...",
|
||||
"rename": "Umbenennen...",
|
||||
"empty_folder": "Ordner leeren",
|
||||
"empty_folder_generic": "Ordner leeren",
|
||||
"delete_folder": "Ordner löschen",
|
||||
"refresh": "Aktualisieren",
|
||||
"mark_all_confirm_title": "Alle Ordner als gelesen markieren",
|
||||
"mark_all_confirm_message": "Jede ungelesene Nachricht in deinem persönlichen Konto als gelesen markieren?",
|
||||
"delete_confirm_title": "Ordner löschen",
|
||||
"delete_confirm_message": "Den Ordner \"{name}\" dauerhaft löschen? Dies kann nicht rückgängig gemacht werden.",
|
||||
"prompt_new_subfolder": "Name des neuen Unterordners:",
|
||||
"prompt_new_folder": "Name des neuen Ordners:",
|
||||
"prompt_rename": "Ordner umbenennen in:",
|
||||
"toast_marked_read": "Ordner als gelesen markiert",
|
||||
"toast_marked_read_count": "{count, plural, one {1 Nachricht} other {# Nachrichten}} als gelesen markiert",
|
||||
"toast_already_read": "Keine ungelesenen Nachrichten",
|
||||
"toast_marked_all_read": "Alle Ordner als gelesen markiert",
|
||||
"toast_emptied": "Ordner geleert",
|
||||
"toast_folder_created": "Ordner erstellt",
|
||||
"toast_folder_renamed": "Ordner umbenannt",
|
||||
"toast_folder_deleted": "Ordner gelöscht",
|
||||
"toast_error_mark_read": "Konnte nicht als gelesen markiert werden",
|
||||
"toast_error_empty": "Ordner konnte nicht geleert werden",
|
||||
"toast_error_create": "Ordner konnte nicht erstellt werden",
|
||||
"toast_error_rename": "Ordner konnte nicht umbenannt werden",
|
||||
"toast_error_delete": "Ordner konnte nicht gelöscht werden",
|
||||
"toast_error_delete_has_children": "Ordner enthält Unterordner. Entferne diese zuerst.",
|
||||
"toast_error_delete_has_email": "Ordner ist nicht leer. Leere ihn zuerst."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Tastaturkürzel",
|
||||
"tip": "Drücken Sie ? jederzeit, um diese Hilfe anzuzeigen",
|
||||
|
||||
@@ -1594,6 +1594,40 @@
|
||||
"items_selected": "{count} emails selected",
|
||||
"edit_draft": "Edit Draft"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"tip": "Press ? anytime to show this help",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count} correos seleccionados",
|
||||
"edit_draft": "Editar borrador"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Atajos de Teclado",
|
||||
"tip": "Presione ? en cualquier momento para mostrar esta ayuda",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count} emails sélectionnés",
|
||||
"edit_draft": "Modifier le brouillon"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Raccourcis clavier",
|
||||
"tip": "Appuyez sur ? à tout moment pour afficher cette aide",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count} messaggi selezionati",
|
||||
"edit_draft": "Modifica bozza"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Scorciatoie da tastiera",
|
||||
"tip": "Premi ? in qualsiasi momento per mostrare questo aiuto",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count}件のメールを選択",
|
||||
"edit_draft": "下書きを編集"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "キーボードショートカット",
|
||||
"tip": "? キーを押すといつでもこのヘルプを表示できます",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count}개의 메일 선택됨",
|
||||
"edit_draft": "임시보관 메일 수정"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "단축키",
|
||||
"tip": "언제든 ? 키를 누르면 이 도움말을 볼 수 있어요",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count} vēstules atlasītas",
|
||||
"edit_draft": "Rediģēt melnrakstu"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Īsinājumtaustiņi",
|
||||
"tip": "Nospiediet ? jebkurā laikā, lai skatītu palīdzību",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count} e-mails geselecteerd",
|
||||
"edit_draft": "Concept bewerken"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Sneltoetsen",
|
||||
"tip": "Druk op ? om deze hulp te tonen",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count} zaznaczonych wiadomości",
|
||||
"edit_draft": "Edytuj szkic"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Skróty klawiszowe",
|
||||
"tip": "Naciśnij ? w dowolnym momencie, aby wyświetlić tę pomoc",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count} e-mails selecionados",
|
||||
"edit_draft": "Editar rascunho"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Atalhos de Teclado",
|
||||
"tip": "Pressione ? a qualquer momento para mostrar esta ajuda",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "{count} писем выбрано",
|
||||
"edit_draft": "Редактировать черновик"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Сочетания клавиш",
|
||||
"tip": "Нажмите ? в любое время для отображения справки",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "Вибрано електронних листів: {count}",
|
||||
"edit_draft": "Редагувати чернетку"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Комбінації клавіш",
|
||||
"tip": "Натисніть ? у будь-який час, щоб показати цю допомогу",
|
||||
|
||||
@@ -1590,6 +1590,40 @@
|
||||
"items_selected": "已选择 {count} 封邮件",
|
||||
"edit_draft": "编辑草稿"
|
||||
},
|
||||
"mailbox_context_menu": {
|
||||
"mark_folder_read": "Mark folder as read",
|
||||
"mark_folder_tree_read": "Mark folder & subfolders as read",
|
||||
"mark_all_folders_read": "Mark all folders as read",
|
||||
"new_subfolder": "New subfolder...",
|
||||
"new_folder": "New folder...",
|
||||
"rename": "Rename...",
|
||||
"empty_folder": "Empty folder",
|
||||
"empty_folder_generic": "Empty folder",
|
||||
"delete_folder": "Delete folder",
|
||||
"refresh": "Refresh",
|
||||
"mark_all_confirm_title": "Mark all folders as read",
|
||||
"mark_all_confirm_message": "Mark every unread message in your personal account as read?",
|
||||
"delete_confirm_title": "Delete folder",
|
||||
"delete_confirm_message": "Permanently delete the folder \"{name}\"? This action cannot be undone.",
|
||||
"prompt_new_subfolder": "New subfolder name:",
|
||||
"prompt_new_folder": "New folder name:",
|
||||
"prompt_rename": "Rename folder to:",
|
||||
"toast_marked_read": "Folder marked as read",
|
||||
"toast_marked_read_count": "Marked {count, plural, one {1 message} other {# messages}} as read",
|
||||
"toast_already_read": "No unread messages",
|
||||
"toast_marked_all_read": "All folders marked as read",
|
||||
"toast_emptied": "Folder emptied",
|
||||
"toast_folder_created": "Folder created",
|
||||
"toast_folder_renamed": "Folder renamed",
|
||||
"toast_folder_deleted": "Folder deleted",
|
||||
"toast_error_mark_read": "Failed to mark as read",
|
||||
"toast_error_empty": "Failed to empty folder",
|
||||
"toast_error_create": "Failed to create folder",
|
||||
"toast_error_rename": "Failed to rename folder",
|
||||
"toast_error_delete": "Failed to delete folder",
|
||||
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
|
||||
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "键盘快捷键",
|
||||
"tip": "按?随时显示此帮助",
|
||||
|
||||
@@ -118,6 +118,7 @@ interface EmailStore {
|
||||
deleteMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
||||
setMailboxRole: (client: IJMAPClient, mailboxId: string, role: string | null) => Promise<void>;
|
||||
emptyMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
||||
markMailboxAsRead: (client: IJMAPClient, mailboxId: string) => Promise<number>;
|
||||
|
||||
// Unified mailbox operations
|
||||
fetchUnifiedEmails: (accounts: UnifiedAccountClient[], role: UnifiedMailboxRole) => Promise<void>;
|
||||
@@ -1750,6 +1751,39 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
markMailboxAsRead: async (client, mailboxId) => {
|
||||
try {
|
||||
const mailbox = get().mailboxes.find(mb => mb.id === mailboxId);
|
||||
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||
const jmapMailboxId = mailbox?.originalId || mailboxId;
|
||||
|
||||
const count = await client.markMailboxAsRead(jmapMailboxId, accountId);
|
||||
|
||||
// Update local state: mark all emails currently visible in this mailbox as read,
|
||||
// and zero-out the mailbox unread counter.
|
||||
set((state) => ({
|
||||
emails: state.emails.map(e =>
|
||||
e.mailboxIds && e.mailboxIds[mailboxId]
|
||||
? { ...e, keywords: { ...e.keywords, $seen: true } }
|
||||
: e
|
||||
),
|
||||
selectedEmail: state.selectedEmail && state.selectedEmail.mailboxIds?.[mailboxId]
|
||||
? { ...state.selectedEmail, keywords: { ...state.selectedEmail.keywords, $seen: true } }
|
||||
: state.selectedEmail,
|
||||
mailboxes: state.mailboxes.map(mb =>
|
||||
mb.id === mailboxId
|
||||
? { ...mb, unreadEmails: 0, unreadThreads: 0 }
|
||||
: mb
|
||||
),
|
||||
}));
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
set({ error: error instanceof Error ? error.message : 'Failed to mark folder as read' });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Unified mailbox operations
|
||||
fetchUnifiedEmails: async (accounts, role) => {
|
||||
set({
|
||||
|
||||
Reference in New Issue
Block a user