From b1dad2e2543c151b15d46e83e5979f4cfcac7ca7 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Wed, 14 May 2025 13:16:50 +0100 Subject: [PATCH] feat: implement keyboard shortcuts for mail categories and improve selection modes --- .../context/command-palette-context.tsx | 4 + apps/mail/components/mail/mail-display.tsx | 24 ++++++ apps/mail/components/mail/mail-list.tsx | 73 +++++++++++++++---- apps/mail/components/mail/mail.tsx | 10 ++- apps/mail/config/navigation.ts | 12 +-- apps/mail/config/shortcuts.ts | 56 ++++++++++++-- apps/mail/hooks/use-hot-key.ts | 34 +++++++++ apps/mail/lib/hotkeys/mail-list-hotkeys.tsx | 28 +++++++ apps/mail/lib/hotkeys/navigation-hotkeys.tsx | 2 +- apps/mail/locales/en.json | 3 +- 10 files changed, 216 insertions(+), 30 deletions(-) diff --git a/apps/mail/components/context/command-palette-context.tsx b/apps/mail/components/context/command-palette-context.tsx index 28379e398..58f21d35a 100644 --- a/apps/mail/components/context/command-palette-context.tsx +++ b/apps/mail/components/context/command-palette-context.tsx @@ -37,6 +37,7 @@ type CommandItem = { onClick?: () => unknown; shortcut?: string; isBackButton?: boolean; + disabled?: boolean; }; const CommandPaletteContext = React.createContext(null); @@ -86,6 +87,7 @@ export function CommandPalette({ children }: { children: React.ReactNode }) { mailCommands.push({ title: 'common.commandPalette.commands.composeMessage', icon: Pencil2, + shortcut: 'c', onClick: () => { setIsComposeOpen('true'); }, @@ -96,12 +98,14 @@ export function CommandPalette({ children }: { children: React.ReactNode }) { section?.sections.forEach((group) => { group.items.forEach((navItem) => { + if (navItem.disabled) return; const item: CommandItem = { title: navItem.title, icon: navItem.icon, url: navItem.url, shortcut: navItem.shortcut, isBackButton: navItem.isBackButton, + disabled: navItem.disabled, }; if (sectionKey === 'mail') { diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index 82e9d44fa..4f4f9e63f 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -821,6 +821,14 @@ const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => { Reply + + r + diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index f63ea7278..f28883894 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -611,11 +611,7 @@ const Thread = memo( {latestMessage.to.map((e) => e.email).join(', ')}

) : ( -

+

{highlightText(latestMessage.subject, searchValue.highlight)}

)} @@ -773,15 +769,27 @@ export const MailList = memo(({ isCompact }: MailListProps) => { const isKeyPressed = useKeyState(); const getSelectMode = useCallback((): MailSelectMode => { + // Check for Alt key using both 'Alt' and 'AltLeft'/'AltRight' for better browser compatibility + const isAltPressed = isKeyPressed('Alt') || isKeyPressed('AltLeft') || isKeyPressed('AltRight'); + + // Check for Shift key using both 'Shift' and 'ShiftLeft'/'ShiftRight' + const isShiftPressed = + isKeyPressed('Shift') || isKeyPressed('ShiftLeft') || isKeyPressed('ShiftRight'); + if (isKeyPressed('Control') || isKeyPressed('Meta')) { return 'mass'; } - if (isKeyPressed('Shift')) { - return 'range'; - } - if (isKeyPressed('Alt') && isKeyPressed('Shift')) { + + // Check for Alt+Shift combination first (higher priority) + if (isAltPressed && isShiftPressed) { + console.log('Select All Below mode activated'); // Debug log return 'selectAllBelow'; } + + if (isShiftPressed) { + return 'range'; + } + return 'single'; }, [isKeyPressed]); @@ -791,26 +799,63 @@ export const MailList = memo(({ isCompact }: MailListProps) => { const handleSelectMail = useCallback( (message: ParsedMessage) => { const itemId = message.threadId ?? message.id; - switch (getSelectMode()) { + const currentMode = getSelectMode(); + console.log('Selection mode:', currentMode, 'for item:', itemId); + + switch (currentMode) { case 'mass': { const newSelected = mail.bulkSelected.includes(itemId) ? mail.bulkSelected.filter((id) => id !== itemId) : [...mail.bulkSelected, itemId]; + console.log('Mass selection mode - selected items:', newSelected.length); return setMail({ ...mail, bulkSelected: newSelected }); } + case 'selectAllBelow': { + // Find the index of the clicked item + const clickedIndex = items.findIndex((item) => item.id === itemId); + console.log( + 'SelectAllBelow - clicked index:', + clickedIndex, + 'total items:', + items.length, + ); + + // Select all items from the clicked one to the end of the list + if (clickedIndex !== -1) { + const itemsBelow = items.slice(clickedIndex); + const idsBelow = itemsBelow.map((item) => item.id); + console.log('Selecting all items below - count:', idsBelow.length); + return setMail({ ...mail, bulkSelected: idsBelow }); + } + console.log('Item not found in list, selecting just this item'); + return setMail({ ...mail, bulkSelected: [itemId] }); + } + case 'range': { + // For future implementation of range selection + console.log('Range selection mode - not fully implemented'); + return setMail({ ...mail, bulkSelected: [itemId] }); + } + default: { + console.log('Single selection mode'); + return setMail({ ...mail, bulkSelected: [itemId] }); + } } - setMail({ ...mail, bulkSelected: [message.threadId ?? message.id] }); }, - [mail, setMail, getSelectMode], + [mail, setMail, getSelectMode, items], ); const [, setFocusedIndex] = useAtom(focusedIndexAtom); const handleMailClick = useCallback( (message: ParsedMessage) => () => { - if (getSelectMode() !== 'single') { + // Log the current selection mode for debugging + const mode = getSelectMode(); + console.log('Mail click with mode:', mode); + + if (mode !== 'single') { return handleSelectMail(message); } + handleMouseEnter(message.id); const messageThreadId = message.threadId ?? message.id; @@ -822,7 +867,7 @@ export const MailList = memo(({ isCompact }: MailListProps) => { void setDraftId(null); void setActiveReplyId(null); }, - [mail, items, setFocusedIndex], + [mail, items, setFocusedIndex, getSelectMode, handleSelectMail], ); const isFiltering = searchValue.value.trim().length > 0; diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx index 819a87985..84d8d4634 100644 --- a/apps/mail/components/mail/mail.tsx +++ b/apps/mail/components/mail/mail.tsx @@ -810,7 +810,15 @@ function CategorySelect({ isMultiSelectMode }: { isMultiSelectMode: boolean }) { {!isSelected && ( - {cat.name} + {cat.name} + + {idx ? idx + 1 : ''} + )} diff --git a/apps/mail/config/navigation.ts b/apps/mail/config/navigation.ts index e6bbf8afc..281bc599b 100644 --- a/apps/mail/config/navigation.ts +++ b/apps/mail/config/navigation.ts @@ -58,21 +58,21 @@ export const navigationConfig: Record = { title: 'navigation.sidebar.inbox', url: '/mail/inbox', icon: Inbox, - shortcut: "g + i", + shortcut: 'g + i', }, { id: 'drafts', title: 'navigation.sidebar.drafts', url: '/mail/draft', icon: Folder, - shortcut: "g + d", + shortcut: 'g + d', }, { id: 'sent', title: 'navigation.sidebar.sent', url: '/mail/sent', icon: Plane2, - shortcut: "g + t", + shortcut: 'g + t', }, ], }, @@ -84,21 +84,19 @@ export const navigationConfig: Record = { title: 'navigation.sidebar.archive', url: '/mail/archive', icon: Archive, - shortcut: "g + a", + shortcut: 'g + a', }, { id: 'spam', title: 'navigation.sidebar.spam', url: '/mail/spam', icon: ExclamationCircle, - shortcut: "g + s", }, { id: 'trash', title: 'navigation.sidebar.bin', url: '/mail/bin', icon: Bin, - shortcut: "g + b", }, ], }, @@ -150,6 +148,7 @@ export const navigationConfig: Record = { title: 'navigation.settings.general', url: '/settings/general', icon: SettingsGear, + shortcut: 'g + s', }, { title: 'navigation.settings.connections', @@ -181,6 +180,7 @@ export const navigationConfig: Record = { title: 'navigation.settings.shortcuts', url: '/settings/shortcuts', icon: Tabs, + shortcut: '?', }, // { // title: 'navigation.settings.signatures', diff --git a/apps/mail/config/shortcuts.ts b/apps/mail/config/shortcuts.ts index 1bb58590e..5269caec9 100644 --- a/apps/mail/config/shortcuts.ts +++ b/apps/mail/config/shortcuts.ts @@ -79,13 +79,13 @@ const navigation: Shortcut[] = [ description: 'Go to sent mail', scope: 'navigation', }, - // { - // keys: ['g', 's'], - // action: 'goToSpam', - // type: 'combination', - // description: 'Go to spam', - // scope: 'navigation', - // }, + { + keys: ['g', 's'], + action: 'goToSettings', + type: 'combination', + description: 'Go to general settings', + scope: 'navigation', + }, { keys: ['g', 'a'], action: 'goToArchive', @@ -228,6 +228,48 @@ const mailListShortcuts: Shortcut[] = [ // description: 'Scroll up', // scope: 'mail-list', // }, + { + keys: ['1'], + action: 'showImportant', + type: 'single', + description: 'Show important', + scope: 'mail-list', + }, + { + keys: ['2'], + action: 'showAllMail', + type: 'single', + description: 'Show all mail', + scope: 'mail-list', + }, + { + keys: ['3'], + action: 'showPersonal', + type: 'single', + description: 'Show personal', + scope: 'mail-list', + }, + { + keys: ['4'], + action: 'showUpdates', + type: 'single', + description: 'Show updates', + scope: 'mail-list', + }, + { + keys: ['5'], + action: 'showPromotions', + type: 'single', + description: 'Show promotions', + scope: 'mail-list', + }, + { + keys: ['6'], + action: 'showUnread', + type: 'single', + description: 'Show unread', + scope: 'mail-list', + }, ]; const composeShortcuts: Shortcut[] = [ diff --git a/apps/mail/hooks/use-hot-key.ts b/apps/mail/hooks/use-hot-key.ts index c9f83f48c..94d5d464d 100644 --- a/apps/mail/hooks/use-hot-key.ts +++ b/apps/mail/hooks/use-hot-key.ts @@ -7,17 +7,51 @@ let listenersInit = false; function initKeyListeners() { if (typeof window !== 'undefined' && !listenersInit) { window.addEventListener('keydown', (e) => { + // Store the key state keyStates.set(e.key, true); + + // Also store specific states for modifier keys + if (e.altKey) { + keyStates.set('Alt', true); + keyStates.set('AltLeft', true); + keyStates.set('AltRight', true); + } + + if (e.shiftKey) { + keyStates.set('Shift', true); + keyStates.set('ShiftLeft', true); + keyStates.set('ShiftRight', true); + } + + console.log('Key down:', e.key, 'Alt:', e.altKey, 'Shift:', e.shiftKey); }); window.addEventListener('keyup', (e) => { + // Clear the key state keyStates.set(e.key, false); + + // Also clear specific states for modifier keys + if (e.key === 'Alt' || e.key === 'AltLeft' || e.key === 'AltRight') { + keyStates.set('Alt', false); + keyStates.set('AltLeft', false); + keyStates.set('AltRight', false); + } + + if (e.key === 'Shift' || e.key === 'ShiftLeft' || e.key === 'ShiftRight') { + keyStates.set('Shift', false); + keyStates.set('ShiftLeft', false); + keyStates.set('ShiftRight', false); + } + + console.log('Key up:', e.key); }); window.addEventListener('blur', () => { + // Clear all key states when window loses focus keyStates.forEach((_, key) => { keyStates.set(key, false); }); + console.log('Window blur - all keys reset'); }); listenersInit = true; diff --git a/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx b/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx index dd2b78c3e..c3dae4c75 100644 --- a/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx +++ b/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx @@ -7,8 +7,10 @@ import { useMail } from '@/components/mail/use-mail'; import { useTRPC } from '@/providers/query-provider'; import { useShortcuts } from './use-hotkey-utils'; import { useThreads } from '@/hooks/use-threads'; +import { usePathname } from 'next/navigation'; import { useStats } from '@/hooks/use-stats'; import { useTranslations } from 'next-intl'; +import { useQueryState } from 'nuqs'; import { toast } from 'sonner'; export function MailListHotkeys() { @@ -20,6 +22,8 @@ export function MailListHotkeys() { const hoveredEmailId = useRef(null); const trpc = useTRPC(); const queryClient = useQueryClient(); + const [, setCategory] = useQueryState('category'); + const pathname = usePathname(); const invalidateCount = () => queryClient.invalidateQueries({ queryKey: trpc.mail.count.queryKey() }); const { mutateAsync: bulkArchive } = useMutation(trpc.mail.bulkArchive.mutationOptions()); @@ -175,6 +179,12 @@ export function MailListHotkeys() { // } // }, []); + const switchMailListCategory = (category: string | null) => { + if (pathname?.includes('/mail/inbox')) { + setCategory(category); + } + }; + const handlers = { markAsRead, markAsUnread, @@ -184,6 +194,24 @@ export function MailListHotkeys() { // muteThread, // scrollDown, // scrollUp, + showImportant: () => { + switchMailListCategory(null); + }, + showAllMail: () => { + switchMailListCategory('All Mail'); + }, + showPersonal: () => { + switchMailListCategory('Personal'); + }, + showUpdates: () => { + switchMailListCategory('Updates'); + }, + showPromotions: () => { + switchMailListCategory('Promotions'); + }, + showUnread: () => { + switchMailListCategory('Unread'); + }, }; const mailListShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope); diff --git a/apps/mail/lib/hotkeys/navigation-hotkeys.tsx b/apps/mail/lib/hotkeys/navigation-hotkeys.tsx index e0ae51f14..aa74dbcd9 100644 --- a/apps/mail/lib/hotkeys/navigation-hotkeys.tsx +++ b/apps/mail/lib/hotkeys/navigation-hotkeys.tsx @@ -14,7 +14,7 @@ export function NavigationHotkeys() { sentMail: () => router.push('/mail/sent'), goToArchive: () => router.push('/mail/archive'), goToBin: () => router.push('/mail/bin'), - goToSpam: () => router.push('/mail/spam'), + goToSettings: () => router.push('/settings'), }; const globalShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope); diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json index cf738fbf8..9c1102a25 100644 --- a/apps/mail/locales/en.json +++ b/apps/mail/locales/en.json @@ -445,7 +445,8 @@ "goToArchive": "Go to Archive", "goToBin": "Go to Bin", "scrollDown": "Scroll down in mail list", - "scrollUp": "Scroll up in mail list" + "scrollUp": "Scroll up in mail list", + "goToSettings": "Go to Settings" } }, "labels": {