From 4788e8a91a8424b7a3ff3f415650cd5f116b8846 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:26:05 +0200 Subject: [PATCH] feat: add right-click context menu to mail folders sidebar --- app/[locale]/page.tsx | 190 +++++++++++++++++++++ components/layout/mailbox-context-menu.tsx | 171 +++++++++++++++++++ components/layout/sidebar.tsx | 65 ++++++- lib/demo/demo-client.ts | 27 +++ lib/jmap/client-interface.ts | 2 + lib/jmap/client.ts | 93 ++++++++++ locales/de/common.json | 34 ++++ locales/en/common.json | 34 ++++ locales/es/common.json | 34 ++++ locales/fr/common.json | 34 ++++ locales/it/common.json | 34 ++++ locales/ja/common.json | 34 ++++ locales/ko/common.json | 34 ++++ locales/lv/common.json | 34 ++++ locales/nl/common.json | 34 ++++ locales/pl/common.json | 34 ++++ locales/pt/common.json | 34 ++++ locales/ru/common.json | 34 ++++ locales/uk/common.json | 34 ++++ locales/zh/common.json | 34 ++++ stores/email-store.ts | 34 ++++ 21 files changed, 1057 insertions(+), 1 deletion(-) create mode 100644 components/layout/mailbox-context-menu.tsx diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 060e4603..90ddd3f2 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -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); diff --git a/components/layout/mailbox-context-menu.tsx b/components/layout/mailbox-context-menu.tsx new file mode 100644 index 00000000..b6f48768 --- /dev/null +++ b/components/layout/mailbox-context-menu.tsx @@ -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; + 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 ( + + handleAction(onMarkAllFoldersRead!)} + disabled={!onMarkAllFoldersRead} + /> + + handleAction(onCreateFolder!)} + disabled={!onCreateFolder} + /> + handleAction(onRefresh!)} + disabled={!onRefresh} + /> + + ); + } + + 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 ( + + {mailbox.name} + + handleAction(() => onMarkFolderRead?.(mailbox.id))} + disabled={!onMarkFolderRead || !canSetSeen} + /> + {target.hasChildren && ( + handleAction(() => onMarkFolderTreeRead?.(mailbox.id))} + disabled={!onMarkFolderTreeRead || !canSetSeen} + /> + )} + + + + handleAction(() => onCreateSubfolder?.(mailbox.id))} + disabled={!onCreateSubfolder || !canCreateChild} + /> + handleAction(() => onRenameFolder?.(mailbox.id))} + disabled={!onRenameFolder || !canRename} + /> + + + + handleAction(() => onEmptyFolder?.(mailbox.id))} + disabled={!onEmptyFolder || mailbox.totalEmails === 0 || !canRemoveItems} + destructive + /> + handleAction(() => onDeleteFolder?.(mailbox.id))} + disabled={!onDeleteFolder || !canDelete} + destructive + /> + + + + handleAction(onRefresh!)} + disabled={!onRefresh} + /> + + ); +} diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 73d34b20..a8fd95b6 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -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; 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 (
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) : 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(); + + 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 (
)} -
+
)) )} @@ -934,6 +979,7 @@ export function Sidebar({ isCollapsed={isCollapsed} onUnreadFilterClick={onUnreadFilterClick} colorful={colorfulSidebarIcons} + onContextMenu={handleMailboxContextMenu} /> ))}
@@ -975,6 +1021,23 @@ export function Sidebar({ {!isCollapsed && }
+ +
); } diff --git a/lib/demo/demo-client.ts b/lib/demo/demo-client.ts index b312952a..83f36370 100644 --- a/lib/demo/demo-client.ts +++ b/lib/demo/demo-client.ts @@ -312,6 +312,33 @@ export class DemoJMAPClient implements IJMAPClient { return removed; } + async markMailboxAsRead(mailboxId: string): Promise { + 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 { + 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 { const email = this.data.emails.find(e => e.id === emailId); const junkMb = this.data.mailboxes.find(m => m.role === 'junk'); diff --git a/lib/jmap/client-interface.ts b/lib/jmap/client-interface.ts index d09767bc..a05b19b7 100644 --- a/lib/jmap/client-interface.ts +++ b/lib/jmap/client-interface.ts @@ -94,6 +94,8 @@ export interface IJMAPClient { ): Promise; moveEmail(emailId: string, toMailboxId: string, accountId?: string): Promise; emptyMailbox(mailboxId: string): Promise; + markMailboxAsRead(mailboxId: string, accountId?: string): Promise; + markAllAsRead(excludeMailboxIds?: string[], accountId?: string): Promise; markAsSpam(emailId: string, accountId?: string): Promise; undoSpam(emailId: string, originalMailboxId: string, accountId?: string): Promise; diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 5dfd713f..9ad44504 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -1361,6 +1361,99 @@ export class JMAPClient implements IJMAPClient { return totalDestroyed; } + async markMailboxAsRead(mailboxId: string, accountId?: string): Promise { + 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 { + 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 }> = 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 { const targetAccountId = accountId || this.accountId; diff --git a/locales/de/common.json b/locales/de/common.json index 9c97c162..129016cb 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -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", diff --git a/locales/en/common.json b/locales/en/common.json index 8df8f072..47777a38 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -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", diff --git a/locales/es/common.json b/locales/es/common.json index 6d7d02b1..b9f6d301 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -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", diff --git a/locales/fr/common.json b/locales/fr/common.json index 0fe2adf0..83c8c8cc 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -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", diff --git a/locales/it/common.json b/locales/it/common.json index 802c95b2..89f84974 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -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", diff --git a/locales/ja/common.json b/locales/ja/common.json index db6a9bb7..bb24ed51 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -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": "? キーを押すといつでもこのヘルプを表示できます", diff --git a/locales/ko/common.json b/locales/ko/common.json index 813a50e5..5f14da62 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -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": "언제든 ? 키를 누르면 이 도움말을 볼 수 있어요", diff --git a/locales/lv/common.json b/locales/lv/common.json index c3c7fd68..cad72013 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -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", diff --git a/locales/nl/common.json b/locales/nl/common.json index b8a90183..223d59a7 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -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", diff --git a/locales/pl/common.json b/locales/pl/common.json index 71604296..e1a644ac 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -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", diff --git a/locales/pt/common.json b/locales/pt/common.json index 24d0a444..2a784b84 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -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", diff --git a/locales/ru/common.json b/locales/ru/common.json index 23dd13c2..036917a8 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -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": "Нажмите ? в любое время для отображения справки", diff --git a/locales/uk/common.json b/locales/uk/common.json index 893bdfc2..3c25d6b5 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -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": "Натисніть ? у будь-який час, щоб показати цю допомогу", diff --git a/locales/zh/common.json b/locales/zh/common.json index 02ca92c9..474e8ef4 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -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": "按?随时显示此帮助", diff --git a/stores/email-store.ts b/stores/email-store.ts index c4aed6a6..197273c7 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -118,6 +118,7 @@ interface EmailStore { deleteMailbox: (client: IJMAPClient, mailboxId: string) => Promise; setMailboxRole: (client: IJMAPClient, mailboxId: string, role: string | null) => Promise; emptyMailbox: (client: IJMAPClient, mailboxId: string) => Promise; + markMailboxAsRead: (client: IJMAPClient, mailboxId: string) => Promise; // Unified mailbox operations fetchUnifiedEmails: (accounts: UnifiedAccountClient[], role: UnifiedMailboxRole) => Promise; @@ -1750,6 +1751,39 @@ export const useEmailStore = create((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({