From cc8195209bb9c9942f42b32b7626a43e2aa8c2a8 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:42:21 +0100 Subject: [PATCH] feat: add toolbar position setting and update email viewer layout - Introduced a new setting for toolbar position in appearance settings, allowing users to choose between 'top' and 'below-subject'. - Updated the EmailViewer component to conditionally render the toolbar based on the selected position. - Enhanced the UI for the toolbar with action buttons for replying, archiving, and deleting emails. - Added translations for the new toolbar position setting in multiple languages. --- components/email/email-viewer.tsx | 761 ++++++++++++-------- components/settings/appearance-settings.tsx | 16 +- locales/de/common.json | 6 + locales/en/common.json | 6 + locales/es/common.json | 6 + locales/fr/common.json | 6 + locales/it/common.json | 6 + locales/ja/common.json | 6 + locales/nl/common.json | 6 + locales/pt/common.json | 6 + stores/settings-store.ts | 8 + 11 files changed, 524 insertions(+), 309 deletions(-) diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 3ea28d0b..162f2443 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -20,7 +20,6 @@ import { ChevronUp, ChevronLeft, Download, - Paperclip, Mail, Clock, Loader2, @@ -417,6 +416,7 @@ export function EmailViewer({ const addTrustedSender = useSettingsStore((state) => state.addTrustedSender); const isSenderTrusted = useSettingsStore((state) => state.isSenderTrusted); const emailKeywords = useSettingsStore((state) => state.emailKeywords); + const toolbarPosition = useSettingsStore((state) => state.toolbarPosition); // Detect if current mailbox is Junk folder const isInJunkFolder = currentMailboxRole === 'junk'; @@ -899,15 +899,229 @@ export function EmailViewer({ )} - {/* Subject Bar - sticky on mobile/tablet for quick actions */} + {/* === TOOLBAR (top position) === */} + {toolbarPosition === 'top' && ( +
+
+
+ {/* Left: Back + Reply actions */} +
+ {isTablet && !tabletListVisible && onBack && ( + + )} + + + +
+ + {/* Right: Organize actions */} +
+ {isLoading && ( +
+ +
+ )} + + + {(onMarkAsSpam || onUndoSpam) && ( + + )} + + +
+ + {/* Tag Picker */} +
+ +
+ {colorOptions.map((option) => ( + + ))} + {currentColor && ( + <> +
+ + + )} +
+
+ + + + {/* More menu */} +
+ +
+ + {onShowShortcuts && ( + + )} +
+
+
+
+
+
+ )} + + {/* === SUBJECT BLOCK === */}
- {/* Tablet Back Button - show when list is hidden */} - {isTablet && !tabletListVisible && onBack && ( + {/* Back button (for below-subject mode on tablet) */} + {toolbarPosition === 'below-subject' && isTablet && !tabletListVisible && onBack && ( + )} + {/* Color tag dot */} + {currentColor && (() => { + const kw = emailKeywords.find(k => k.id === currentColor); + const dotClass = kw ? KEYWORD_PALETTE[kw.color]?.dot : null; + return dotClass ? ( + + ) : null; + })()} +
+
{new Date(email.receivedAt).toLocaleString('en-US', { @@ -934,12 +1171,6 @@ export function EmailViewer({ minute: '2-digit' })} - {email.hasAttachment && ( - - - {t('attachments')} - - )} {isImportant && ( {t('important')} @@ -947,246 +1178,211 @@ export function EmailViewer({ )}
+
+
+
- {/* Quick Actions */} -
- {/* Loading indicator */} - {isLoading && ( -
- - {t('loading')} -
- )} - {/* Primary Reply Button */} - - - {/* Reply Options Dropdown - hidden on mobile/tablet */} -
+ {/* === TOOLBAR (below-subject position) === */} + {toolbarPosition === 'below-subject' && ( +
+
+
+ {/* Left: Reply actions */} +
+ + -
- - -
-
- - - - {/* Spam/Not Spam Button - Desktop only, contextual based on folder */} - {(onMarkAsSpam || onUndoSpam) && ( + {/* Right: Organize actions */} +
+ {isLoading && ( +
+ +
+ )} - )} - - - - -
- - {/* Tag Picker - hidden on mobile/tablet */} -
- - - {/* Tag dropdown on hover */} -
- {colorOptions.map((option) => ( - - ))} - {currentColor && ( - <> -
- - - )} -
-
- - {/* More Actions Dropdown */} -
+ + )} + + )} + -
+ +
+ + {/* Tag Picker */} +
- - {onShowShortcuts && ( - - )} - {/* Separator */} -
- {/* Spam action - contextual */} - {(onMarkAsSpam || onUndoSpam) && ( - +
+ {colorOptions.map((option) => ( + + ))} + {currentColor && ( + <> +
+ + + )} +
+
+ + + + {/* More Actions */} +
+ +
+ - )} + {onShowShortcuts && ( + + )} +
-
+ )} - {/* Sender Info - Desktop only (hidden on mobile/tablet, they see it in scrollable content) */} + {/* === SENDER INFO (Desktop) === */}
+ {/* === ATTACHMENTS (integrated into header) === */} + {email.attachments && email.attachments.length > 0 && ( +
+
+ {email.attachments.map((attachment, i) => { + const FileIcon = getFileIcon(attachment.name, attachment.type); + return ( + + ); + })} +
+
+ )} + {/* Email Content Area */}
{/* Mobile/Tablet Sender Info - scrolls with content */} @@ -1649,89 +1879,6 @@ export function EmailViewer({
- {/* Inline Attachments */} - {email.attachments && email.attachments.length > 0 && ( -
- {/* Image attachments as thumbnails */} - {email.attachments.filter(a => - a.type?.startsWith('image/') || - ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(a.name?.split('.').pop()?.toLowerCase() || '') - ).length > 0 && ( -
-
- {email.attachments - .filter(a => - a.type?.startsWith('image/') || - ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(a.name?.split('.').pop()?.toLowerCase() || '') - ) - .map((attachment, i) => ( -
{ - if (attachment.blobId && onDownloadAttachment) { - onDownloadAttachment(attachment.blobId, attachment.name || 'download', attachment.type); - } - }} - > -
-
- -
-
-
- -
-
- {attachment.name} -
-
- ))} -
-
- )} - - {/* Non-image attachments in a compact list */} - {email.attachments.filter(a => - !a.type?.startsWith('image/') && - !['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(a.name?.split('.').pop()?.toLowerCase() || '') - ).length > 0 && ( -
- - {email.attachments - .filter(a => - !a.type?.startsWith('image/') && - !['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(a.name?.split('.').pop()?.toLowerCase() || '') - ) - .map((attachment, i) => { - const FileIcon = getFileIcon(attachment.name, attachment.type); - return ( - - ); - })} -
- )} -
- )} - {/* Email Body */}
{emailContent.isHtml ? ( diff --git a/components/settings/appearance-settings.tsx b/components/settings/appearance-settings.tsx index be32c17d..7363e9f6 100644 --- a/components/settings/appearance-settings.tsx +++ b/components/settings/appearance-settings.tsx @@ -2,14 +2,14 @@ import { useTranslations } from 'next-intl'; import { useThemeStore } from '@/stores/theme-store'; -import { useSettingsStore } from '@/stores/settings-store'; +import { useSettingsStore, type ToolbarPosition } from '@/stores/settings-store'; import { LanguageSwitcher } from '@/components/ui/language-switcher'; import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section'; export function AppearanceSettings() { const t = useTranslations('settings.appearance'); const { theme, setTheme } = useThemeStore(); - const { fontSize, listDensity, animationsEnabled, updateSetting } = useSettingsStore(); + const { fontSize, listDensity, animationsEnabled, toolbarPosition, updateSetting } = useSettingsStore(); return ( @@ -59,6 +59,18 @@ export function AppearanceSettings() { /> + {/* Toolbar Position */} + + updateSetting('toolbarPosition', value as ToolbarPosition)} + options={[ + { value: 'top', label: t('toolbar_position.top') }, + { value: 'below-subject', label: t('toolbar_position.below_subject') }, + ]} + /> + + {/* Animations */} @@ -91,6 +92,9 @@ interface SettingsState { calendarNotificationsEnabled: boolean; calendarNotificationSound: boolean; + // Layout + toolbarPosition: ToolbarPosition; + // Experimental senderFavicons: boolean; @@ -166,6 +170,9 @@ const DEFAULT_SETTINGS = { calendarNotificationsEnabled: true, calendarNotificationSound: true, + // Layout + toolbarPosition: 'top' as ToolbarPosition, + // Experimental senderFavicons: true, @@ -232,6 +239,7 @@ export const useSettingsStore = create()( sessionTimeout: state.sessionTimeout, calendarNotificationsEnabled: state.calendarNotificationsEnabled, calendarNotificationSound: state.calendarNotificationSound, + toolbarPosition: state.toolbarPosition, senderFavicons: state.senderFavicons, folderIcons: state.folderIcons, emailKeywords: state.emailKeywords,