feat: add right-click context menu to mail folders sidebar
This commit is contained in:
@@ -14,6 +14,7 @@ import { useAccountStore } from "@/stores/account-store";
|
||||
import type { UnifiedAccountClient } from "@/lib/unified-mailbox";
|
||||
import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal";
|
||||
import { useEmailStore } from "@/stores/email-store";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { useContactStore } from "@/stores/contact-store";
|
||||
@@ -162,6 +163,11 @@ export default function Home() {
|
||||
fetchUnifiedEmails: fetchUnifiedEmailsAction,
|
||||
refreshUnifiedCounts,
|
||||
exitUnifiedView,
|
||||
emptyMailbox,
|
||||
markMailboxAsRead,
|
||||
createMailbox,
|
||||
renameMailbox,
|
||||
deleteMailbox,
|
||||
} = useEmailStore();
|
||||
|
||||
const enableUnifiedMailbox = useSettingsStore((s) => s.enableUnifiedMailbox);
|
||||
@@ -1097,6 +1103,181 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
const tCtxMenu = t;
|
||||
|
||||
const handleMarkFolderRead = async (mailboxId: string) => {
|
||||
if (!client) return;
|
||||
try {
|
||||
const count = await markMailboxAsRead(client, mailboxId);
|
||||
await fetchMailboxes(client);
|
||||
if (selectedMailbox === mailboxId) await fetchEmails(client, mailboxId);
|
||||
if (count > 0) {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_marked_read_count', { count }));
|
||||
} else {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_already_read'));
|
||||
}
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_mark_read'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkFolderTreeRead = async (mailboxId: string) => {
|
||||
if (!client) return;
|
||||
const collectIds = (rootId: string): string[] => {
|
||||
const ids: string[] = [rootId];
|
||||
const stack = [rootId];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()!;
|
||||
for (const mb of mailboxes) {
|
||||
if (mb.parentId === current) {
|
||||
ids.push(mb.id);
|
||||
stack.push(mb.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
|
||||
try {
|
||||
const ids = collectIds(mailboxId);
|
||||
let total = 0;
|
||||
for (const id of ids) {
|
||||
total += await markMailboxAsRead(client, id);
|
||||
}
|
||||
await fetchMailboxes(client);
|
||||
if (selectedMailbox && ids.includes(selectedMailbox)) await fetchEmails(client, selectedMailbox);
|
||||
if (total > 0) {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_marked_read_count', { count: total }));
|
||||
} else {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_already_read'));
|
||||
}
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_mark_read'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkAllFoldersRead = async () => {
|
||||
if (!client) return;
|
||||
|
||||
const confirmed = await confirmDialog({
|
||||
title: tCtxMenu('mailbox_context_menu.mark_all_confirm_title'),
|
||||
message: tCtxMenu('mailbox_context_menu.mark_all_confirm_message'),
|
||||
confirmText: tCtxMenu('mailbox_context_menu.mark_all_folders_read'),
|
||||
variant: "default",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const total = await client.markAllAsRead();
|
||||
await fetchMailboxes(client);
|
||||
if (selectedMailbox) await fetchEmails(client, selectedMailbox);
|
||||
if (total > 0) {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_marked_read_count', { count: total }));
|
||||
} else {
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_already_read'));
|
||||
}
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_mark_read'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmptyFolderFromContextMenu = async (mailboxId: string) => {
|
||||
if (!client) return;
|
||||
const mailbox = mailboxes.find(mb => mb.id === mailboxId);
|
||||
if (!mailbox) return;
|
||||
|
||||
const confirmed = await confirmDialog({
|
||||
title: tCtxMenu('email_list.empty_folder.confirm_title'),
|
||||
message: tCtxMenu('email_list.empty_folder.confirm_message'),
|
||||
confirmText: tCtxMenu('email_list.empty_folder.confirm_button'),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await emptyMailbox(client, mailboxId);
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_emptied'));
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_empty'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateSubfolderFromContextMenu = async (parentId: string) => {
|
||||
if (!client) return;
|
||||
const name = window.prompt(tCtxMenu('mailbox_context_menu.prompt_new_subfolder'));
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
await createMailbox(client, name.trim(), parentId);
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_folder_created'));
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_create'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateFolderFromContextMenu = async () => {
|
||||
if (!client) return;
|
||||
const name = window.prompt(tCtxMenu('mailbox_context_menu.prompt_new_folder'));
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
await createMailbox(client, name.trim());
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_folder_created'));
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_create'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameFolderFromContextMenu = async (mailboxId: string) => {
|
||||
if (!client) return;
|
||||
const mailbox = mailboxes.find(mb => mb.id === mailboxId);
|
||||
if (!mailbox) return;
|
||||
const name = window.prompt(tCtxMenu('mailbox_context_menu.prompt_rename'), mailbox.name);
|
||||
if (!name || !name.trim() || name.trim() === mailbox.name) return;
|
||||
try {
|
||||
await renameMailbox(client, mailboxId, name.trim());
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_folder_renamed'));
|
||||
} catch {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_rename'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFolderFromContextMenu = async (mailboxId: string) => {
|
||||
if (!client) return;
|
||||
const mailbox = mailboxes.find(mb => mb.id === mailboxId);
|
||||
if (!mailbox) return;
|
||||
|
||||
const confirmed = await confirmDialog({
|
||||
title: tCtxMenu('mailbox_context_menu.delete_confirm_title'),
|
||||
message: tCtxMenu('mailbox_context_menu.delete_confirm_message', { name: mailbox.name }),
|
||||
confirmText: tCtxMenu('mailbox_context_menu.delete_folder'),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await deleteMailbox(client, mailboxId);
|
||||
toast.success(tCtxMenu('mailbox_context_menu.toast_folder_deleted'));
|
||||
} catch (err: unknown) {
|
||||
const jmapType = (err as Error & { jmapType?: string })?.jmapType;
|
||||
if (jmapType === 'mailboxHasChild') {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_delete_has_children'));
|
||||
} else if (jmapType === 'mailboxHasEmail') {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_delete_has_email'));
|
||||
} else {
|
||||
toast.error(tCtxMenu('mailbox_context_menu.toast_error_delete'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshMailboxes = async () => {
|
||||
if (!client) return;
|
||||
try {
|
||||
await fetchMailboxes(client);
|
||||
if (selectedMailbox) await fetchEmails(client, selectedMailbox);
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = logout;
|
||||
|
||||
const handleSearch = async (query: string) => {
|
||||
@@ -1454,6 +1635,15 @@ export default function Home() {
|
||||
onMailboxSelect={handleMailboxSelect}
|
||||
onTagSelect={handleTagSelect}
|
||||
onUnreadFilterClick={handleUnreadFilterClick}
|
||||
onMarkFolderRead={handleMarkFolderRead}
|
||||
onMarkFolderTreeRead={handleMarkFolderTreeRead}
|
||||
onMarkAllFoldersRead={handleMarkAllFoldersRead}
|
||||
onEmptyFolder={handleEmptyFolderFromContextMenu}
|
||||
onCreateSubfolder={handleCreateSubfolderFromContextMenu}
|
||||
onCreateFolder={handleCreateFolderFromContextMenu}
|
||||
onRenameFolder={handleRenameFolderFromContextMenu}
|
||||
onDeleteFolder={handleDeleteFolderFromContextMenu}
|
||||
onRefreshMailboxes={handleRefreshMailboxes}
|
||||
onCompose={() => {
|
||||
setComposerMode('compose');
|
||||
setShowComposer(true);
|
||||
|
||||
Reference in New Issue
Block a user