From 68f1fabc4b6407cd6dbbb263a39772b9cc6abf74 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:04:29 +0200 Subject: [PATCH] fix: batch shortcuts act on multi-selection when present #228 --- app/[locale]/page.tsx | 80 ++++++++++++++++++++++++++++----- hooks/use-keyboard-shortcuts.ts | 16 ++++--- 2 files changed, 78 insertions(+), 18 deletions(-) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index d3797799..a8ba1100 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -171,6 +171,11 @@ export default function Home() { createMailbox, renameMailbox, deleteMailbox, + batchDelete, + batchArchive, + batchMarkAsRead, + batchMarkAsSpam, + batchUndoSpam, } = useEmailStore(); const enableUnifiedMailbox = useSettingsStore((s) => s.enableUnifiedMailbox); @@ -353,27 +358,77 @@ export default function Home() { onToggleStar: () => { if (selectedEmail) handleToggleStar(); }, - onArchive: () => { - if (selectedEmail) handleArchive(); + onArchive: async () => { + if (selectedEmailIds.size > 0 && client) { + try { + await batchArchive(client); + } catch (error) { + console.error("Failed to batch archive:", error); + } + } else if (selectedEmail) { + handleArchive(); + } }, - onDelete: () => { - if (selectedEmail) handleDelete(); + onDelete: async () => { + if (selectedEmailIds.size > 0 && client) { + const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); + const isInTrash = currentMailbox?.role === 'trash'; + const isInJunk = currentMailbox?.role === 'junk'; + const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk; + const permanent = isInTrash || (isInJunk && permanentlyDeleteJunk); + const confirmed = await confirmDialog({ + title: permanent + ? t('email_list.permanent_delete_confirm_title') + : t('email_list.batch_actions.delete_confirm_title'), + message: permanent + ? t('email_list.permanent_delete_confirm_batch_message', { count: selectedEmailIds.size }) + : t('email_list.batch_actions.delete_confirm_message', { count: selectedEmailIds.size }), + confirmText: permanent + ? t('email_list.permanent_delete') + : t('email_list.batch_actions.delete'), + variant: "destructive", + }); + if (!confirmed) return; + try { + await batchDelete(client, permanent); + } catch (error) { + console.error("Failed to batch delete:", error); + } + } else if (selectedEmail) { + handleDelete(); + } }, onMarkAsUnread: async () => { - if (selectedEmail && client) { + if (!client) return; + if (selectedEmailIds.size > 0) { + await batchMarkAsRead(client, false); + } else if (selectedEmail) { await markAsRead(client, selectedEmail.id, false); } }, onMarkAsRead: async () => { - if (selectedEmail && client) { + if (!client) return; + if (selectedEmailIds.size > 0) { + await batchMarkAsRead(client, true); + } else if (selectedEmail) { await markAsRead(client, selectedEmail.id, true); } }, - onToggleSpam: () => { - if (selectedEmail) { - // Check if we're in junk folder - const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); - const isInJunk = currentMailbox?.role === 'junk'; + onToggleSpam: async () => { + const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); + const isInJunk = currentMailbox?.role === 'junk'; + if (selectedEmailIds.size > 0 && client) { + const ids = Array.from(selectedEmailIds); + try { + if (isInJunk) { + await batchUndoSpam(client, ids); + } else { + await batchMarkAsSpam(client, ids); + } + } catch (error) { + console.error("Failed to batch toggle spam:", error); + } + } else if (selectedEmail) { if (isInJunk) { handleUndoSpam(); } else { @@ -408,13 +463,14 @@ export default function Home() { clearSelection(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - }), [emails, selectedEmail, client, selectedMailbox, isMobile, isTablet]); + }), [emails, selectedEmail, client, selectedMailbox, isMobile, isTablet, selectedEmailIds, mailboxes]); // Initialize keyboard shortcuts useKeyboardShortcuts({ enabled: isAuthenticated && !showComposer, emails, selectedEmailId: selectedEmail?.id, + selectionCount: selectedEmailIds.size, handlers: keyboardHandlers, }); diff --git a/hooks/use-keyboard-shortcuts.ts b/hooks/use-keyboard-shortcuts.ts index 108f0023..0a27942c 100644 --- a/hooks/use-keyboard-shortcuts.ts +++ b/hooks/use-keyboard-shortcuts.ts @@ -39,6 +39,7 @@ export interface UseKeyboardShortcutsOptions { enabled?: boolean; emails: Email[]; selectedEmailId?: string; + selectionCount?: number; handlers: KeyboardShortcutHandlers; } @@ -58,6 +59,7 @@ export function useKeyboardShortcuts({ enabled = true, emails, selectedEmailId, + selectionCount = 0, handlers, }: UseKeyboardShortcutsOptions) { const handlersRef = useRef(handlers); @@ -90,6 +92,8 @@ export function useKeyboardShortcuts({ // Shortcuts that should NOT work with modifiers if (hasModifier) return; + const hasBatchTarget = !!selectedEmailId || selectionCount > 0; + switch (key) { // Navigation case "j": @@ -152,7 +156,7 @@ export function useKeyboardShortcuts({ break; case "e": - if (selectedEmailId) { + if (hasBatchTarget) { event.preventDefault(); h.onArchive?.(); } @@ -161,28 +165,28 @@ export function useKeyboardShortcuts({ case "#": case "delete": case "backspace": - if (selectedEmailId && (key === "#" || key === "delete" || key === "backspace")) { + if (hasBatchTarget) { event.preventDefault(); h.onDelete?.(); } break; case "u": - if (selectedEmailId) { + if (hasBatchTarget) { event.preventDefault(); h.onMarkAsUnread?.(); } break; case "i": - if (selectedEmailId && event.shiftKey) { + if (hasBatchTarget && event.shiftKey) { event.preventDefault(); h.onMarkAsRead?.(); } break; case "!": - if (selectedEmailId) { + if (hasBatchTarget) { event.preventDefault(); h.onToggleSpam?.(); } @@ -220,7 +224,7 @@ export function useKeyboardShortcuts({ break; } }, - [selectedEmailId] + [selectedEmailId, selectionCount] ); useEffect(() => {