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')}
+ setMoreMenuOpen(false)} className="h-9 w-9">
+
+
+
+
+
{ onArchive?.(); setMoreMenuOpen(false); }}
+ className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
+ >
+
+ {t('archive')}
+
+ {(onMarkAsSpam || onUndoSpam) && (
+
{ (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); }}
+ className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
+ >
+ {isInJunkFolder ? (
+
+ ) : (
+
+ )}
+ {isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}
+
+ )}
+ {/* 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 ? (
+
{ onMoveToMailbox(node.id); setMoreMenuOpen(false); }}
+ className="w-full px-4 py-2.5 min-h-[44px] text-sm text-left hover:bg-muted flex items-center gap-3"
+ style={{ paddingLeft: `${1 + depth * 1}rem` }}
+ >
+
+ {node.name}
+
+ ) : (
+
+
+ {node.name}
+
+ )}
+ {node.children.length > 0 && renderMobileNodes(node.children, depth + 1)}
+
+ );
+ });
+ };
+ return renderMobileNodes(moveTree);
+ })()}
+
+ >
+ )}
+ {/* Tags */}
+ {colorOptions.length > 0 && (
+ <>
+
+
{t('tag')}
+ {colorOptions.map((option) => (
+
{ if (email) onSetColorTag?.(email.id, option.value); setMoreMenuOpen(false); }}
+ className={cn(
+ "w-full px-4 py-2.5 min-h-[44px] text-sm text-left hover:bg-muted flex items-center gap-3",
+ currentColor === option.value && "bg-accent font-medium"
+ )}
+ >
+
+ {option.name}
+ {currentColor === option.value && }
+
+ ))}
+ {currentColor && (
+
{ if (email) onSetColorTag?.(email.id, null); setMoreMenuOpen(false); }}
+ className="w-full px-4 py-2.5 min-h-[44px] text-sm text-left hover:bg-muted flex items-center gap-3 text-muted-foreground"
+ >
+
+ {t('remove_color')}
+
+ )}
+
+ >
+ )}
+
{ handlePrint(); setMoreMenuOpen(false); }}
+ className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
+ >
+
+ {t('print')}
+
+
{ setShowSourceModal(true); setMoreMenuOpen(false); }}
+ className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
+ >
+
+ {t('view_source')}
+
+ {onShowShortcuts && (
+
{ onShowShortcuts(); setMoreMenuOpen(false); }}
+ className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
+ >
+
+ {t('keyboard_shortcuts')}
+
+ )}
+
+
+ )}
{/* 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')}
- {t('reply_all')}
+ {t('reply_all')}
- {t('forward')}
+ {t('forward')}
@@ -1134,11 +1314,13 @@ export function EmailViewer({
)}
- {/* Archive - hidden on mobile, available in More menu */}
+ {/* Archive - hidden on mobile, overflows to More menu */}
@@ -1155,12 +1337,14 @@ export function EmailViewer({
{t('delete')}
- {/* Spam - hidden on mobile, available in More menu */}
+ {/* Spam - hidden on mobile, overflows to More menu */}
{(onMarkAsSpam || onUndoSpam) && (
)}
- {/* Move to folder - hidden on mobile, available in More menu */}
+ {/* Move to folder - hidden on mobile, overflows to More menu */}
{moveTree.length > 0 && onMoveToMailbox && (
-
+
{ setMoveMenuOpen(!moveMenuOpen); setMoreMenuOpen(false); setTagMenuOpen(false); }}
className="h-8 rounded hover:bg-muted flex items-center gap-1.5 px-2"
@@ -1237,10 +1421,10 @@ export function EmailViewer({
{isStarred ? t('tooltips.unstar') : t('tooltips.star')}
-
-
- {/* Tag Picker - click-based, hidden on mobile (available in More menu) */}
-
+ {/* Tag Picker + Divider - hidden on mobile, overflows to More menu */}
+
+
+
{ setTagMenuOpen(!tagMenuOpen); setMoreMenuOpen(false); setMoveMenuOpen(false); }}
className={cn(
@@ -1296,12 +1480,15 @@ export function EmailViewer({
)}
+
- {/* Print - hidden on mobile, available in More menu */}
+ {/* Print - hidden on mobile, overflows to More menu */}
@@ -1321,12 +1508,12 @@ export function EmailViewer({
{t('more_actions')}
- {moreMenuOpen && (
+ {moreMenuOpen && !isMobile && (
- {/* Mobile-only actions */}
+ {/* Overflow actions - shown when hidden from toolbar or on mobile */}
{ onArchive?.(); setMoreMenuOpen(false); }}
- className="w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2 sm:hidden"
+ className={cn("w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 5 ? "" : "sm:hidden")}
>
{t('archive')}
@@ -1334,7 +1521,7 @@ export function EmailViewer({
{(onMarkAsSpam || onUndoSpam) && (
{ (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); }}
- className="w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2 sm:hidden"
+ className={cn("w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 4 ? "" : "sm:hidden")}
>
{isInJunkFolder ? (
@@ -1344,9 +1531,9 @@ export function EmailViewer({
{isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}
)}
- {/* 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({
)}
{ handlePrint(); setMoreMenuOpen(false); }}
- className="w-full px-3 py-2.5 sm:py-2 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
+ className={cn("w-full px-3 py-2.5 sm:py-2 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 1 ? "" : "sm:hidden")}
>
{t('print')}
@@ -1519,38 +1706,38 @@ export function EmailViewer({
{toolbarPosition === 'below-subject' && (
-
+
{/* Left: Reply actions */}
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')}
- {t('reply_all')}
+ {t('reply_all')}
- {t('forward')}
+ {t('forward')}
@@ -1561,23 +1748,27 @@ export function EmailViewer({
)}
- {/* Archive - hidden on mobile, available in More menu */}
+ {/* Archive - hidden on mobile, overflows to More menu */}
{t('archive')}
- {/* Spam - hidden on mobile, available in More menu */}
+ {/* Spam - hidden on mobile, overflows to More menu */}
{(onMarkAsSpam || onUndoSpam) && (
)}
- {/* Move to folder - hidden on mobile, available in More menu */}
+ {/* Move to folder - hidden on mobile, overflows to More menu */}
{moveTree.length > 0 && onMoveToMailbox && (
-
+
{ setMoveMenuOpen(!moveMenuOpen); setMoreMenuOpen(false); setTagMenuOpen(false); }}
className="h-8 rounded hover:bg-muted flex items-center gap-1.5 px-2"
@@ -1664,10 +1855,10 @@ export function EmailViewer({
{isStarred ? t('tooltips.unstar') : t('tooltips.star')}
-
-
- {/* Tag Picker - click-based, hidden on mobile (available in More menu) */}
-
+ {/* Tag Picker + Divider - hidden on mobile, overflows to More menu */}
+
+
+
{ setTagMenuOpen(!tagMenuOpen); setMoreMenuOpen(false); setMoveMenuOpen(false); }}
className={cn(
@@ -1723,12 +1914,15 @@ export function EmailViewer({
)}
+
- {/* Print - hidden on mobile, available in More menu */}
+ {/* Print - hidden on mobile, overflows to More menu */}
@@ -1748,12 +1942,12 @@ export function EmailViewer({
{t('more_actions')}
- {moreMenuOpen && (
+ {moreMenuOpen && !isMobile && (
- {/* Mobile-only actions */}
+ {/* Overflow actions - shown when hidden from toolbar or on mobile */}
{ onArchive?.(); setMoreMenuOpen(false); }}
- className="w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2 sm:hidden"
+ className={cn("w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 5 ? "" : "sm:hidden")}
>
{t('archive')}
@@ -1761,7 +1955,7 @@ export function EmailViewer({
{(onMarkAsSpam || onUndoSpam) && (
{ (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); }}
- className="w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2 sm:hidden"
+ className={cn("w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 4 ? "" : "sm:hidden")}
>
{isInJunkFolder ? (
@@ -1771,9 +1965,9 @@ export function EmailViewer({
{isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}
)}
- {/* 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({
)}
{ handlePrint(); setMoreMenuOpen(false); }}
- className="w-full px-3 py-2.5 sm:py-2 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
+ className={cn("w-full px-3 py-2.5 sm:py-2 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 1 ? "" : "sm:hidden")}
>
{t('print')}
@@ -2403,7 +2597,7 @@ export function EmailViewer({
)}
{/* Email Content Area */}
-
+
{/* Mobile/Tablet Sender Info - scrolls with content */}
@@ -2446,12 +2640,12 @@ export function EmailViewer({
}}
/>
)}
- ·
+ ·
>
)}
{email.to && email.to.length > 0 && (
<>
- → {t('recipient_to_prefix')}
+ → {t('recipient_to_prefix')}
{renderClickableRecipients(email.to, currentUserEmail, t, handleViewContactSidebar)}
>
)}
@@ -2693,6 +2887,62 @@ export function EmailViewer({
)}
+ {/* Mobile bottom action bar */}
+ {isMobile && (
+
+
+
+
+ {t('previous')}
+
+ onReply?.()}
+ className="flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px] text-muted-foreground active:text-foreground transition-colors duration-150"
+ aria-label={t('tooltips.reply')}
+ >
+
+ {t('reply')}
+
+
+
+ {t('reply_all')}
+
+
+
+ {t('forward')}
+
+
+
+ {t('next')}
+
+
+
+ )}
+
{/* Contact Detail Sidebar - desktop only */}
{contactSidebarEmail && !isMobileDevice && (