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 type { UnifiedAccountClient } from "@/lib/unified-mailbox";
|
||||||
import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal";
|
import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal";
|
||||||
import { useEmailStore } from "@/stores/email-store";
|
import { useEmailStore } from "@/stores/email-store";
|
||||||
|
import { toast } from "@/stores/toast-store";
|
||||||
import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
|
import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
|
||||||
import { useSettingsStore } from "@/stores/settings-store";
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
import { useContactStore } from "@/stores/contact-store";
|
import { useContactStore } from "@/stores/contact-store";
|
||||||
@@ -162,6 +163,11 @@ export default function Home() {
|
|||||||
fetchUnifiedEmails: fetchUnifiedEmailsAction,
|
fetchUnifiedEmails: fetchUnifiedEmailsAction,
|
||||||
refreshUnifiedCounts,
|
refreshUnifiedCounts,
|
||||||
exitUnifiedView,
|
exitUnifiedView,
|
||||||
|
emptyMailbox,
|
||||||
|
markMailboxAsRead,
|
||||||
|
createMailbox,
|
||||||
|
renameMailbox,
|
||||||
|
deleteMailbox,
|
||||||
} = useEmailStore();
|
} = useEmailStore();
|
||||||
|
|
||||||
const enableUnifiedMailbox = useSettingsStore((s) => s.enableUnifiedMailbox);
|
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 handleLogout = logout;
|
||||||
|
|
||||||
const handleSearch = async (query: string) => {
|
const handleSearch = async (query: string) => {
|
||||||
@@ -1454,6 +1635,15 @@ export default function Home() {
|
|||||||
onMailboxSelect={handleMailboxSelect}
|
onMailboxSelect={handleMailboxSelect}
|
||||||
onTagSelect={handleTagSelect}
|
onTagSelect={handleTagSelect}
|
||||||
onUnreadFilterClick={handleUnreadFilterClick}
|
onUnreadFilterClick={handleUnreadFilterClick}
|
||||||
|
onMarkFolderRead={handleMarkFolderRead}
|
||||||
|
onMarkFolderTreeRead={handleMarkFolderTreeRead}
|
||||||
|
onMarkAllFoldersRead={handleMarkAllFoldersRead}
|
||||||
|
onEmptyFolder={handleEmptyFolderFromContextMenu}
|
||||||
|
onCreateSubfolder={handleCreateSubfolderFromContextMenu}
|
||||||
|
onCreateFolder={handleCreateFolderFromContextMenu}
|
||||||
|
onRenameFolder={handleRenameFolderFromContextMenu}
|
||||||
|
onDeleteFolder={handleDeleteFolderFromContextMenu}
|
||||||
|
onRefreshMailboxes={handleRefreshMailboxes}
|
||||||
onCompose={() => {
|
onCompose={() => {
|
||||||
setComposerMode('compose');
|
setComposerMode('compose');
|
||||||
setShowComposer(true);
|
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";
|
} from "lucide-react";
|
||||||
import { cn, buildMailboxTree, MailboxNode } from "@/lib/utils";
|
import { cn, buildMailboxTree, MailboxNode } from "@/lib/utils";
|
||||||
import { Mailbox } from "@/lib/jmap/types";
|
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 { useAccountStore } from '@/stores/account-store';
|
||||||
import { UNIFIED_MAILBOX_IDS } from '@/lib/jmap/types';
|
import { UNIFIED_MAILBOX_IDS } from '@/lib/jmap/types';
|
||||||
import type { UnifiedMailboxRole } from '@/lib/jmap/types';
|
import type { UnifiedMailboxRole } from '@/lib/jmap/types';
|
||||||
@@ -56,6 +58,15 @@ interface SidebarProps {
|
|||||||
onCompose?: () => void;
|
onCompose?: () => void;
|
||||||
onSidebarClose?: () => void;
|
onSidebarClose?: () => void;
|
||||||
onUnreadFilterClick?: (mailboxId: string) => 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;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +198,7 @@ interface SidebarRowProps {
|
|||||||
dropHandlers?: Record<string, unknown>;
|
dropHandlers?: Record<string, unknown>;
|
||||||
isValidDropTarget?: boolean;
|
isValidDropTarget?: boolean;
|
||||||
isInvalidDropTarget?: boolean;
|
isInvalidDropTarget?: boolean;
|
||||||
|
onContextMenu?: (e: React.MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidebarRow({
|
function SidebarRow({
|
||||||
@@ -206,6 +218,7 @@ function SidebarRow({
|
|||||||
dropHandlers,
|
dropHandlers,
|
||||||
isValidDropTarget,
|
isValidDropTarget,
|
||||||
isInvalidDropTarget,
|
isInvalidDropTarget,
|
||||||
|
onContextMenu,
|
||||||
}: SidebarRowProps) {
|
}: SidebarRowProps) {
|
||||||
const t = useTranslations('sidebar');
|
const t = useTranslations('sidebar');
|
||||||
const leftPad = isCollapsed ? 0 : ROW_PX_BASE + depth * INDENT_STEP;
|
const leftPad = isCollapsed ? 0 : ROW_PX_BASE + depth * INDENT_STEP;
|
||||||
@@ -213,6 +226,7 @@ function SidebarRow({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...(dropHandlers || {})}
|
{...(dropHandlers || {})}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
style={{ paddingBlock: 'var(--density-sidebar-py)' }}
|
style={{ paddingBlock: 'var(--density-sidebar-py)' }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group w-full flex items-center max-lg:min-h-[44px] text-sm transition-colors duration-150",
|
"group w-full flex items-center max-lg:min-h-[44px] text-sm transition-colors duration-150",
|
||||||
@@ -365,6 +379,7 @@ function MailboxTreeItem({
|
|||||||
isCollapsed,
|
isCollapsed,
|
||||||
onUnreadFilterClick,
|
onUnreadFilterClick,
|
||||||
colorful,
|
colorful,
|
||||||
|
onContextMenu,
|
||||||
}: {
|
}: {
|
||||||
node: MailboxNode;
|
node: MailboxNode;
|
||||||
selectedMailbox: string;
|
selectedMailbox: string;
|
||||||
@@ -374,6 +389,7 @@ function MailboxTreeItem({
|
|||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
onUnreadFilterClick?: (mailboxId: string) => void;
|
onUnreadFilterClick?: (mailboxId: string) => void;
|
||||||
colorful: boolean;
|
colorful: boolean;
|
||||||
|
onContextMenu?: (e: React.MouseEvent, node: MailboxNode) => void;
|
||||||
}) {
|
}) {
|
||||||
const tNotifications = useTranslations('notifications');
|
const tNotifications = useTranslations('notifications');
|
||||||
const hasChildren = node.children.length > 0;
|
const hasChildren = node.children.length > 0;
|
||||||
@@ -423,6 +439,7 @@ function MailboxTreeItem({
|
|||||||
dropHandlers={globalDragging ? (dropHandlers as Record<string, unknown>) : undefined}
|
dropHandlers={globalDragging ? (dropHandlers as Record<string, unknown>) : undefined}
|
||||||
isValidDropTarget={isValidDropTarget}
|
isValidDropTarget={isValidDropTarget}
|
||||||
isInvalidDropTarget={isInvalidDropTarget}
|
isInvalidDropTarget={isInvalidDropTarget}
|
||||||
|
onContextMenu={onContextMenu && !isVirtualNode ? (e) => onContextMenu(e, node) : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasChildren && isExpanded && !isCollapsed && node.children.map((child) => (
|
{hasChildren && isExpanded && !isCollapsed && node.children.map((child) => (
|
||||||
@@ -436,6 +453,7 @@ function MailboxTreeItem({
|
|||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
onUnreadFilterClick={onUnreadFilterClick}
|
onUnreadFilterClick={onUnreadFilterClick}
|
||||||
colorful={colorful}
|
colorful={colorful}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -610,6 +628,15 @@ export function Sidebar({
|
|||||||
onCompose: _onCompose,
|
onCompose: _onCompose,
|
||||||
onSidebarClose,
|
onSidebarClose,
|
||||||
onUnreadFilterClick,
|
onUnreadFilterClick,
|
||||||
|
onMarkFolderRead,
|
||||||
|
onMarkFolderTreeRead,
|
||||||
|
onMarkAllFoldersRead,
|
||||||
|
onEmptyFolder,
|
||||||
|
onCreateSubfolder,
|
||||||
|
onCreateFolder,
|
||||||
|
onRenameFolder,
|
||||||
|
onDeleteFolder,
|
||||||
|
onRefreshMailboxes,
|
||||||
className,
|
className,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -790,6 +817,23 @@ export function Sidebar({
|
|||||||
router.push('/settings');
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -866,7 +910,7 @@ export function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div onContextMenu={handleFoldersHeaderContextMenu}>
|
||||||
<SidebarSectionHeader
|
<SidebarSectionHeader
|
||||||
label={t("folders")}
|
label={t("folders")}
|
||||||
expanded={foldersExpanded}
|
expanded={foldersExpanded}
|
||||||
@@ -894,6 +938,7 @@ export function Sidebar({
|
|||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
onUnreadFilterClick={onUnreadFilterClick}
|
onUnreadFilterClick={onUnreadFilterClick}
|
||||||
colorful={colorfulSidebarIcons}
|
colorful={colorfulSidebarIcons}
|
||||||
|
onContextMenu={handleMailboxContextMenu}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -934,6 +979,7 @@ export function Sidebar({
|
|||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
onUnreadFilterClick={onUnreadFilterClick}
|
onUnreadFilterClick={onUnreadFilterClick}
|
||||||
colorful={colorfulSidebarIcons}
|
colorful={colorfulSidebarIcons}
|
||||||
|
onContextMenu={handleMailboxContextMenu}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -975,6 +1021,23 @@ export function Sidebar({
|
|||||||
|
|
||||||
{!isCollapsed && <PluginSlot name="sidebar-widget" className="border-t border-border" />}
|
{!isCollapsed && <PluginSlot name="sidebar-widget" className="border-t border-border" />}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,6 +312,33 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
return removed;
|
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> {
|
async markAsSpam(emailId: string): Promise<void> {
|
||||||
const email = this.data.emails.find(e => e.id === emailId);
|
const email = this.data.emails.find(e => e.id === emailId);
|
||||||
const junkMb = this.data.mailboxes.find(m => m.role === 'junk');
|
const junkMb = this.data.mailboxes.find(m => m.role === 'junk');
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ export interface IJMAPClient {
|
|||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
moveEmail(emailId: string, toMailboxId: string, accountId?: string): Promise<void>;
|
moveEmail(emailId: string, toMailboxId: string, accountId?: string): Promise<void>;
|
||||||
emptyMailbox(mailboxId: string): Promise<number>;
|
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>;
|
markAsSpam(emailId: string, accountId?: string): Promise<void>;
|
||||||
undoSpam(emailId: string, originalMailboxId: 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;
|
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> {
|
async markAsSpam(emailId: string, accountId?: string): Promise<void> {
|
||||||
const targetAccountId = accountId || this.accountId;
|
const targetAccountId = accountId || this.accountId;
|
||||||
|
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count} E-Mails ausgewählt",
|
"items_selected": "{count} E-Mails ausgewählt",
|
||||||
"edit_draft": "Entwurf bearbeiten"
|
"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": {
|
"shortcuts": {
|
||||||
"title": "Tastaturkürzel",
|
"title": "Tastaturkürzel",
|
||||||
"tip": "Drücken Sie ? jederzeit, um diese Hilfe anzuzeigen",
|
"tip": "Drücken Sie ? jederzeit, um diese Hilfe anzuzeigen",
|
||||||
|
|||||||
@@ -1594,6 +1594,40 @@
|
|||||||
"items_selected": "{count} emails selected",
|
"items_selected": "{count} emails selected",
|
||||||
"edit_draft": "Edit Draft"
|
"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": {
|
"shortcuts": {
|
||||||
"title": "Keyboard Shortcuts",
|
"title": "Keyboard Shortcuts",
|
||||||
"tip": "Press ? anytime to show this help",
|
"tip": "Press ? anytime to show this help",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count} correos seleccionados",
|
"items_selected": "{count} correos seleccionados",
|
||||||
"edit_draft": "Editar borrador"
|
"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": {
|
"shortcuts": {
|
||||||
"title": "Atajos de Teclado",
|
"title": "Atajos de Teclado",
|
||||||
"tip": "Presione ? en cualquier momento para mostrar esta ayuda",
|
"tip": "Presione ? en cualquier momento para mostrar esta ayuda",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count} emails sélectionnés",
|
"items_selected": "{count} emails sélectionnés",
|
||||||
"edit_draft": "Modifier le brouillon"
|
"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": {
|
"shortcuts": {
|
||||||
"title": "Raccourcis clavier",
|
"title": "Raccourcis clavier",
|
||||||
"tip": "Appuyez sur ? à tout moment pour afficher cette aide",
|
"tip": "Appuyez sur ? à tout moment pour afficher cette aide",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count} messaggi selezionati",
|
"items_selected": "{count} messaggi selezionati",
|
||||||
"edit_draft": "Modifica bozza"
|
"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": {
|
"shortcuts": {
|
||||||
"title": "Scorciatoie da tastiera",
|
"title": "Scorciatoie da tastiera",
|
||||||
"tip": "Premi ? in qualsiasi momento per mostrare questo aiuto",
|
"tip": "Premi ? in qualsiasi momento per mostrare questo aiuto",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count}件のメールを選択",
|
"items_selected": "{count}件のメールを選択",
|
||||||
"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": {
|
"shortcuts": {
|
||||||
"title": "キーボードショートカット",
|
"title": "キーボードショートカット",
|
||||||
"tip": "? キーを押すといつでもこのヘルプを表示できます",
|
"tip": "? キーを押すといつでもこのヘルプを表示できます",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count}개의 메일 선택됨",
|
"items_selected": "{count}개의 메일 선택됨",
|
||||||
"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": {
|
"shortcuts": {
|
||||||
"title": "단축키",
|
"title": "단축키",
|
||||||
"tip": "언제든 ? 키를 누르면 이 도움말을 볼 수 있어요",
|
"tip": "언제든 ? 키를 누르면 이 도움말을 볼 수 있어요",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count} vēstules atlasītas",
|
"items_selected": "{count} vēstules atlasītas",
|
||||||
"edit_draft": "Rediģēt melnrakstu"
|
"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": {
|
"shortcuts": {
|
||||||
"title": "Īsinājumtaustiņi",
|
"title": "Īsinājumtaustiņi",
|
||||||
"tip": "Nospiediet ? jebkurā laikā, lai skatītu palīdzību",
|
"tip": "Nospiediet ? jebkurā laikā, lai skatītu palīdzību",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count} e-mails geselecteerd",
|
"items_selected": "{count} e-mails geselecteerd",
|
||||||
"edit_draft": "Concept bewerken"
|
"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": {
|
"shortcuts": {
|
||||||
"title": "Sneltoetsen",
|
"title": "Sneltoetsen",
|
||||||
"tip": "Druk op ? om deze hulp te tonen",
|
"tip": "Druk op ? om deze hulp te tonen",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count} zaznaczonych wiadomości",
|
"items_selected": "{count} zaznaczonych wiadomości",
|
||||||
"edit_draft": "Edytuj szkic"
|
"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": {
|
"shortcuts": {
|
||||||
"title": "Skróty klawiszowe",
|
"title": "Skróty klawiszowe",
|
||||||
"tip": "Naciśnij ? w dowolnym momencie, aby wyświetlić tę pomoc",
|
"tip": "Naciśnij ? w dowolnym momencie, aby wyświetlić tę pomoc",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count} e-mails selecionados",
|
"items_selected": "{count} e-mails selecionados",
|
||||||
"edit_draft": "Editar rascunho"
|
"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": {
|
"shortcuts": {
|
||||||
"title": "Atalhos de Teclado",
|
"title": "Atalhos de Teclado",
|
||||||
"tip": "Pressione ? a qualquer momento para mostrar esta ajuda",
|
"tip": "Pressione ? a qualquer momento para mostrar esta ajuda",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "{count} писем выбрано",
|
"items_selected": "{count} писем выбрано",
|
||||||
"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": {
|
"shortcuts": {
|
||||||
"title": "Сочетания клавиш",
|
"title": "Сочетания клавиш",
|
||||||
"tip": "Нажмите ? в любое время для отображения справки",
|
"tip": "Нажмите ? в любое время для отображения справки",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "Вибрано електронних листів: {count}",
|
"items_selected": "Вибрано електронних листів: {count}",
|
||||||
"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": {
|
"shortcuts": {
|
||||||
"title": "Комбінації клавіш",
|
"title": "Комбінації клавіш",
|
||||||
"tip": "Натисніть ? у будь-який час, щоб показати цю допомогу",
|
"tip": "Натисніть ? у будь-який час, щоб показати цю допомогу",
|
||||||
|
|||||||
@@ -1590,6 +1590,40 @@
|
|||||||
"items_selected": "已选择 {count} 封邮件",
|
"items_selected": "已选择 {count} 封邮件",
|
||||||
"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": {
|
"shortcuts": {
|
||||||
"title": "键盘快捷键",
|
"title": "键盘快捷键",
|
||||||
"tip": "按?随时显示此帮助",
|
"tip": "按?随时显示此帮助",
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ interface EmailStore {
|
|||||||
deleteMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
deleteMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
||||||
setMailboxRole: (client: IJMAPClient, mailboxId: string, role: string | null) => Promise<void>;
|
setMailboxRole: (client: IJMAPClient, mailboxId: string, role: string | null) => Promise<void>;
|
||||||
emptyMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
emptyMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
||||||
|
markMailboxAsRead: (client: IJMAPClient, mailboxId: string) => Promise<number>;
|
||||||
|
|
||||||
// Unified mailbox operations
|
// Unified mailbox operations
|
||||||
fetchUnifiedEmails: (accounts: UnifiedAccountClient[], role: UnifiedMailboxRole) => Promise<void>;
|
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
|
// Unified mailbox operations
|
||||||
fetchUnifiedEmails: async (accounts, role) => {
|
fetchUnifiedEmails: async (accounts, role) => {
|
||||||
set({
|
set({
|
||||||
|
|||||||
Reference in New Issue
Block a user