From 3cd123778ec4e15cbacbd75c1e9894117b0c9514 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:19:51 +0100 Subject: [PATCH] feat: add keyboard shortcuts button and show/hide toolbar labels option - Implemented a keyboard shortcuts button in the navigation rail. - Added a setting to toggle the visibility of toolbar labels in appearance settings. - Updated the email context menu to streamline actions for archiving, deleting, and marking emails as read/unread. - Enhanced the email viewer with improved spam handling and read state toggling. - Updated localization files to include new strings for keyboard shortcuts and toolbar labels. --- app/[locale]/page.tsx | 1 + components/email/email-context-menu.tsx | 100 +++-- components/email/email-viewer.tsx | 402 ++++++++++++-------- components/layout/navigation-rail.tsx | 14 +- components/settings/appearance-settings.tsx | 10 +- locales/en/common.json | 5 + stores/settings-store.ts | 2 + 7 files changed, 329 insertions(+), 205 deletions(-) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index fc614c89..99fc6bc7 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -941,6 +941,7 @@ export default function Home() { quota={quota} isPushConnected={isPushConnected} onLogout={handleLogout} + onShowShortcuts={() => setShowShortcutsModal(true)} /> )} diff --git a/components/email/email-context-menu.tsx b/components/email/email-context-menu.tsx index 058be0fb..ecf449f2 100644 --- a/components/email/email-context-menu.tsx +++ b/components/email/email-context-menu.tsx @@ -213,28 +213,24 @@ export function EmailContextMenu({ )} - {/* Mark as read/unread */} + {/* Archive */} - handleAction(() => - showBatchActions - ? onBatchMarkAsRead?.(isUnread) - : onMarkAsRead?.(isUnread) - ) - } + icon={Archive} + label={t("archive")} + onClick={() => handleAction(onArchive!)} + disabled={!onArchive} /> - {/* Star/Unstar - only for single email */} - {!showBatchActions && ( - handleAction(onToggleStar!)} - disabled={!onToggleStar} - /> - )} + {/* Delete */} + + handleAction(showBatchActions ? onBatchDelete! : onDelete!) + } + disabled={showBatchActions ? !onBatchDelete : !onDelete} + destructive + /> @@ -280,32 +276,15 @@ export function EmailContextMenu({ )} - {/* Archive */} - handleAction(onArchive!)} - disabled={!onArchive} - /> - - - - {/* Spam - contextual based on folder */} - - handleAction( - showBatchActions - ? (isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!) - : (isInJunkFolder ? onUndoSpam! : onMarkAsSpam!) - ) - } - disabled={showBatchActions ? (isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) : (isInJunkFolder ? !onUndoSpam : !onMarkAsSpam)} - destructive={!isInJunkFolder} - /> - - + {/* Star/Unstar - only for single email */} + {!showBatchActions && ( + handleAction(onToggleStar!)} + disabled={!onToggleStar} + /> + )} {/* Set tag submenu - only for single email */} {!showBatchActions && ( @@ -342,15 +321,34 @@ export function EmailContextMenu({ - {/* Delete */} + {/* Spam - contextual based on folder */} - handleAction(showBatchActions ? onBatchDelete! : onDelete!) + handleAction( + showBatchActions + ? (isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!) + : (isInJunkFolder ? onUndoSpam! : onMarkAsSpam!) + ) + } + disabled={showBatchActions ? (isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) : (isInJunkFolder ? !onUndoSpam : !onMarkAsSpam)} + destructive={!isInJunkFolder} + /> + + + + {/* Mark as read/unread */} + + handleAction(() => + showBatchActions + ? onBatchMarkAsRead?.(isUnread) + : onMarkAsRead?.(isUnread) + ) } - disabled={showBatchActions ? !onBatchDelete : !onDelete} - destructive /> ); diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index c296c0d0..8605fdd2 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -23,6 +23,7 @@ import { ChevronRight, Download, Mail, + MailOpen, Clock, Loader2, Printer, @@ -487,6 +488,7 @@ export function EmailViewer({ const isSenderTrusted = useSettingsStore((state) => state.isSenderTrusted); const emailKeywords = useSettingsStore((state) => state.emailKeywords); const toolbarPosition = useSettingsStore((state) => state.toolbarPosition); + const showToolbarLabels = useSettingsStore((state) => state.showToolbarLabels); // Detect if current mailbox is Junk folder const isInJunkFolder = currentMailboxRole === 'junk'; @@ -1205,6 +1207,7 @@ export function EmailViewer({ const sender = email.from?.[0]; const isStarred = email.keywords?.$flagged; + const isUnread = !email.keywords?.$seen; const isImportant = email.keywords?.["$important"]; return ( @@ -1240,19 +1243,6 @@ export function EmailViewer({ {t('archive')} - {(onMarkAsSpam || onUndoSpam) && ( - - )} {/* Move to folder */} {moveTree.length > 0 && onMoveToMailbox && ( <> @@ -1324,6 +1314,28 @@ export function EmailViewer({
)} + {/* Spam */} + {(onMarkAsSpam || onUndoSpam) && ( + + )} + {/* Toggle read state */} +
- {/* Right: Organize actions */} + {/* Right: Organize actions — order: archive, delete, move, star, tag, spam, read state, print */}
{isLoading && (
)} - {/* Archive - hidden on mobile, overflows to More menu */} + {/* Archive */} + {/* Delete */} - {/* Spam - hidden on mobile, overflows to More menu */} - {(onMarkAsSpam || onUndoSpam) && ( - - )} - {/* Move to folder - hidden on mobile, overflows to More menu */} + {/* Move to folder — promoted to Button, same design as archive/delete */} {moveTree.length > 0 && onMoveToMailbox && ( -
- + + {showToolbarLabels && {t('move_to')}} + {moveMenuOpen && (
{(() => { @@ -1514,11 +1508,12 @@ export function EmailViewer({ )}
)} + {/* Star/Flag toggle */} - {/* Tag Picker + Divider - hidden on mobile, overflows to More menu */} -
+ {/* Tag Picker — hidden on mobile, overflows to More menu */} +
- {/* Print - hidden on mobile, overflows to More menu */} + {/* Spam — hidden on mobile, overflows to More menu */} + {(onMarkAsSpam || onUndoSpam) && ( + + )} + + {/* Toggle read state — hidden on mobile, overflows to More menu */} + + + {/* Print — hidden on mobile, overflows to More menu */} - {/* More menu - click-based */} + {/* View source — hidden on mobile, overflows to More menu */} + + + {/* More menu — click-based */}
- {(onMarkAsSpam || onUndoSpam) && ( - - )} - {/* Move to folder submenu */} + {/* Overflow: move to folder */} {moveTree.length > 0 && onMoveToMailbox && ( -
= 3 ? "" : "sm:hidden")}> +
= 6 ? "" : "sm:hidden")}>
{t('move_to')}
{(() => { @@ -1677,9 +1707,10 @@ export function EmailViewer({ })()}
- )} {/* Tag submenu */} + )} + {/* Overflow: tag submenu */} {colorOptions.length > 0 && ( -
= 2 ? "" : "sm:hidden")}> +
= 5 ? "" : "sm:hidden")}>
{t('tag')}
{colorOptions.map((option) => ( @@ -1708,16 +1739,40 @@ export function EmailViewer({
)} + {/* Overflow: spam */} + {(onMarkAsSpam || onUndoSpam) && ( + + )} + {/* Overflow: toggle read */} + + {/* Overflow: print */} + {/* Overflow: view source */}
- {/* Right: Organize actions */} + {/* Right: Organize actions — order: archive, delete, move, star, tag, spam, read state, print, view source */}
{isLoading && (
)} - {/* Archive - hidden on mobile, overflows to More menu */} + {/* Archive */} - {/* Spam - hidden on mobile, overflows to More menu */} - {(onMarkAsSpam || onUndoSpam) && ( - - )} - {/* Move to folder - hidden on mobile, overflows to More menu */} + {/* Delete */} + + {/* Move to folder — promoted to Button, same design as archive/delete */} {moveTree.length > 0 && onMoveToMailbox && ( -
- + + {showToolbarLabels && {t('move_to')}} + {moveMenuOpen && (
{(() => { @@ -1938,21 +1985,12 @@ export function EmailViewer({ )}
)} - + {/* Star/Flag toggle */} - {/* Tag Picker + Divider - hidden on mobile, overflows to More menu */} -
+ {/* Tag Picker — hidden on mobile, overflows to More menu */} +
- {/* Print - hidden on mobile, overflows to More menu */} + {/* Spam — hidden on mobile, overflows to More menu */} + {(onMarkAsSpam || onUndoSpam) && ( + + )} + + {/* Toggle read state — hidden on mobile, overflows to More menu */} + + + {/* Print — hidden on mobile, overflows to More menu */} - {/* More menu - click-based */} + {/* View source — hidden on mobile, overflows to More menu */} + + + {/* More menu — click-based */}
- {(onMarkAsSpam || onUndoSpam) && ( - - )} - {/* Move to folder submenu */} + {/* Overflow: move to folder */} {moveTree.length > 0 && onMoveToMailbox && ( -
= 3 ? "" : "sm:hidden")}> +
= 6 ? "" : "sm:hidden")}>
{t('move_to')}
{(() => { @@ -2111,9 +2184,10 @@ export function EmailViewer({ })()}
- )} {/* Tag submenu */} + )} + {/* Overflow: tag submenu */} {colorOptions.length > 0 && ( -
= 2 ? "" : "sm:hidden")}> +
= 5 ? "" : "sm:hidden")}>
{t('tag')}
{colorOptions.map((option) => ( @@ -2142,16 +2216,40 @@ export function EmailViewer({
)} + {/* Overflow: spam */} + {(onMarkAsSpam || onUndoSpam) && ( + + )} + {/* Overflow: toggle read */} + + {/* Overflow: print */} + {/* Overflow: view source */} + )} + {isPushConnected != null && ( @@ -126,6 +126,14 @@ export function AppearanceSettings() { /> + {/* Toolbar Labels */} + + updateSetting('showToolbarLabels', checked)} + /> + + {/* Animations */}