diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 225026fb..bb09912a 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -808,6 +808,17 @@ export default function Home() { setActiveView("list"); }; + // Navigate to next/previous email in the list + const selectedEmailIndex = selectedEmail ? emails.findIndex(e => e.id === selectedEmail.id) : -1; + + const handleNavigateNext = selectedEmailIndex >= 0 && selectedEmailIndex < emails.length - 1 + ? () => handleEmailSelect(emails[selectedEmailIndex + 1]) + : undefined; + + const handleNavigatePrev = selectedEmailIndex > 0 + ? () => handleEmailSelect(emails[selectedEmailIndex - 1]) + : undefined; + // Handle opening conversation view on mobile const handleOpenConversation = async (thread: ThreadGroup) => { if (!client) return; @@ -1403,6 +1414,8 @@ export default function Home() { setTabletListVisible(true); selectEmail(null); }} + onNavigateNext={handleNavigateNext} + onNavigatePrev={handleNavigatePrev} onShowShortcuts={() => setShowShortcutsModal(true)} currentUserEmail={client?.["username"]} currentUserName={client?.["username"]?.split("@")[0]} diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 411b8e9c..da9a52b1 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -19,6 +19,7 @@ import { ChevronDown, ChevronUp, ChevronLeft, + ChevronRight, Download, Mail, Clock, @@ -89,6 +90,8 @@ interface EmailViewerProps { onUndoSpam?: () => void; onMoveToMailbox?: (mailboxId: string) => void; onBack?: () => void; + onNavigateNext?: () => void; + onNavigatePrev?: () => void; onShowShortcuts?: () => void; currentUserEmail?: string; currentUserName?: string; @@ -339,7 +342,7 @@ function ContactSidebarPanel({
{o.name} {o.units && o.units.length > 0 && ( - — {o.units.map(u => u.name).join(", ")} + — {o.units.map(u => u.name).join(", ")} )}
))} @@ -410,6 +413,8 @@ export function EmailViewer({ onUndoSpam, onMoveToMailbox, onBack, + onNavigateNext, + onNavigatePrev, onShowShortcuts, currentUserEmail, currentUserName, @@ -438,7 +443,7 @@ export function EmailViewer({ })); // Tablet list visibility - const { isTablet } = useDeviceDetection(); + const { isTablet, isMobile } = useDeviceDetection(); const { tabletListVisible } = useUIStore(); const { identities, client } = useAuthStore(); const resolvedTheme = useThemeStore((state) => state.resolvedTheme); @@ -456,6 +461,8 @@ export function EmailViewer({ const moreMenuRef = useRef(null); const tagMenuRef = useRef(null); const moveMenuRef = useRef(null); + const toolbarRef = useRef(null); + const [overflowCount, setOverflowCount] = useState(0); const currentColor = getCurrentColor(email?.keywords); // Build mailbox tree for move-to dropdown @@ -522,6 +529,41 @@ export function EmailViewer({ setMoveMenuOpen(false); }, [email?.id]); + // Dynamically detect which toolbar items overflow and should move to the More menu + useEffect(() => { + const el = toolbarRef.current; + if (!el) return; + const calculate = () => { + const items = Array.from(el.querySelectorAll('[data-overflow-item]')); + if (items.length === 0) return; + // Sort descending by priority so highest number (least important) is first + items.sort((a, b) => + Number(b.dataset.overflowPriority || 0) - Number(a.dataset.overflowPriority || 0) + ); + // Show all items to measure their natural widths + items.forEach(item => { item.style.display = ''; }); + const containerWidth = el.clientWidth; + const leftGroup = el.firstElementChild as HTMLElement; + const rightGroup = el.lastElementChild as HTMLElement; + const mainGap = parseFloat(getComputedStyle(el).gap) || 0; + // Iteratively hide items until content fits + let count = 0; + const isOverflowing = () => + leftGroup.scrollWidth + rightGroup.scrollWidth + mainGap > containerWidth + 1; + for (const item of items) { + if (!isOverflowing()) break; + // Skip items already hidden by CSS (e.g., on mobile) + if (item.offsetWidth === 0) continue; + item.style.display = 'none'; + count++; + } + setOverflowCount(prev => prev === count ? prev : count); + }; + const observer = new ResizeObserver(calculate); + observer.observe(el); + return () => observer.disconnect(); + }, [toolbarPosition]); + // Contact sidebar state const [contactSidebarEmail, setContactSidebarEmail] = useState(null); const contacts = useContactStore((s) => s.contacts); @@ -1063,6 +1105,144 @@ export function EmailViewer({ key={email.id} className={cn("flex-1 flex flex-row h-full bg-background overflow-hidden animate-in fade-in duration-300 relative", className)} > + {/* Mobile More menu sidebar overlay */} + {isMobile && moreMenuOpen && ( +
setMoreMenuOpen(false)} + /> + )} + {isMobile && ( +
+
+ {t('more_actions')} + +
+
+ + {(onMarkAsSpam || onUndoSpam) && ( + + )} + {/* Move to folder */} + {moveTree.length > 0 && onMoveToMailbox && ( + <> +
+
{t('move_to')}
+ {(() => { + const renderMobileNodes = (nodes: MailboxNode[], depth = 0) => { + return nodes.map((node) => { + const Icon = getMoveMailboxIcon(node.role); + const isTarget = moveTargetIds.has(node.id); + return ( +
+ {isTarget ? ( + + ) : ( +
+ + {node.name} +
+ )} + {node.children.length > 0 && renderMobileNodes(node.children, depth + 1)} +
+ ); + }); + }; + return renderMobileNodes(moveTree); + })()} +
+ + )} + {/* Tags */} + {colorOptions.length > 0 && ( + <> +
+
{t('tag')}
+ {colorOptions.map((option) => ( + + ))} + {currentColor && ( + + )} +
+ + )} + + + {onShowShortcuts && ( + + )} +
+
+ )} {/* Main email content */}
{/* Loading overlay when fetching new email */} @@ -1081,7 +1261,7 @@ export function EmailViewer({ "max-lg:sticky max-lg:top-0 max-lg:z-10" )}>
-
+
{/* Left: Back + Reply actions */}
{isTablet && !tabletListVisible && onBack && ( @@ -1099,31 +1279,31 @@ export function EmailViewer({ variant="ghost" size="sm" onClick={() => onReply?.()} - className="flex-col items-center gap-0.5 h-auto py-1.5 px-2 sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0" + className="hidden sm:flex sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0" title={t('tooltips.reply')} > - {t('reply')} + {t('reply')}
@@ -1134,11 +1314,13 @@ export function EmailViewer({
)} - {/* Archive - hidden on mobile, available in More menu */} + {/* Archive - hidden on mobile, overflows to More menu */} - {/* Spam - hidden on mobile, available in More menu */} + {/* Spam - hidden on mobile, overflows to More menu */} {(onMarkAsSpam || onUndoSpam) && ( -
- - {/* Tag Picker - click-based, hidden on mobile (available in More menu) */} -
+ {/* Tag Picker + Divider - hidden on mobile, overflows to More menu */} +
+
+
)}
+
- {/* Print - hidden on mobile, available in More menu */} + {/* Print - hidden on mobile, overflows to More menu */} - {moreMenuOpen && ( + {moreMenuOpen && !isMobile && (
- {/* Mobile-only actions */} + {/* Overflow actions - shown when hidden from toolbar or on mobile */} )} - {/* Move to folder submenu on mobile */} + {/* Move to folder submenu */} {moveTree.length > 0 && onMoveToMailbox && ( -
+
= 3 ? "" : "sm:hidden")}>
{t('move_to')}
{(() => { @@ -1383,9 +1570,9 @@ export function EmailViewer({ })()}
- )} {/* Tag submenu on mobile */} + )} {/* Tag submenu */} {colorOptions.length > 0 && ( -
+
= 2 ? "" : "sm:hidden")}>
{t('tag')}
{colorOptions.map((option) => ( @@ -1416,7 +1603,7 @@ export function EmailViewer({ )}
@@ -1561,23 +1748,27 @@ export function EmailViewer({
)} - {/* Archive - hidden on mobile, available in More menu */} + {/* Archive - hidden on mobile, overflows to More menu */} - {/* Spam - hidden on mobile, available in More menu */} + {/* Spam - hidden on mobile, overflows to More menu */} {(onMarkAsSpam || onUndoSpam) && ( -
- - {/* Tag Picker - click-based, hidden on mobile (available in More menu) */} -
+ {/* Tag Picker + Divider - hidden on mobile, overflows to More menu */} +
+
+
)}
+
- {/* Print - hidden on mobile, available in More menu */} + {/* Print - hidden on mobile, overflows to More menu */} - {moreMenuOpen && ( + {moreMenuOpen && !isMobile && (
- {/* Mobile-only actions */} + {/* Overflow actions - shown when hidden from toolbar or on mobile */} )} - {/* Move to folder submenu on mobile */} + {/* Move to folder submenu */} {moveTree.length > 0 && onMoveToMailbox && ( -
+
= 3 ? "" : "sm:hidden")}>
{t('move_to')}
{(() => { @@ -1810,9 +2004,9 @@ export function EmailViewer({ })()}
- )} {/* Tag submenu on mobile */} + )} {/* Tag submenu */} {colorOptions.length > 0 && ( -
+
= 2 ? "" : "sm:hidden")}>
{t('tag')}
{colorOptions.map((option) => ( @@ -1843,7 +2037,7 @@ export function EmailViewer({ )} + + + + +
+ + )} + {/* Contact Detail Sidebar - desktop only */} {contactSidebarEmail && !isMobileDevice && (