From df8d04e233e4c1c05aa11a4a469310143e505083 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Fri, 24 Apr 2026 19:31:38 +0200
Subject: [PATCH] fix: restore broken viewer toolbar actions and improve mobile
menu #220
---
app/[locale]/page.tsx | 18 +-
components/email/email-viewer.tsx | 274 +++++++++++++++++-------------
2 files changed, 160 insertions(+), 132 deletions(-)
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index 197a65a2..060e4603 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -1295,15 +1295,11 @@ export default function Home() {
};
// Handle back navigation from viewer on mobile.
- // Delegate to the browser history stack so this button is equivalent to
- // the OS back button / mouse back button - popstate then restores the
- // previous snapshot via handleNavRestore. The viewer is only reachable
- // from a state that pushed history, so back() always lands on an app entry.
+ // Reset to list state directly. We can't just call window.history.back()
+ // because the nav hook pushes a new entry for every email the user opens,
+ // so history.back() would pop to the previous email rather than the list.
+ // The OS / hardware back button is still wired through popstate → handleNavRestore.
const handleMobileBack = () => {
- if (typeof window !== 'undefined') {
- window.history.back();
- return;
- }
if (conversationThread) {
setConversationThread(null);
setConversationEmails([]);
@@ -1934,12 +1930,12 @@ export default function Home() {
onReply={handleReply}
onReplyAll={handleReplyAll}
onForward={handleForward}
- onDelete={handleDelete}
+ onDelete={() => handleDelete()}
onArchive={() => handleArchive()}
onToggleStar={handleToggleStar}
onSetColorTag={handleSetColorTag}
- onMarkAsSpam={handleMarkAsSpam}
- onUndoSpam={handleUndoSpam}
+ onMarkAsSpam={() => handleMarkAsSpam()}
+ onUndoSpam={() => handleUndoSpam()}
onMarkAsRead={async (emailId, read) => {
if (client) {
await markAsRead(client, emailId, read);
diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx
index f66d0f21..689f405a 100644
--- a/components/email/email-viewer.tsx
+++ b/components/email/email-viewer.tsx
@@ -3436,70 +3436,172 @@ export function EmailViewer({
moreMenuOpen ? "translate-x-0" : "translate-x-full"
)}>
-
{ 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')}
-
- {/* Move to folder */}
- {moveTree.length > 0 && onMoveToMailbox && (
+ {moreMenuSub === null && (
<>
+
{ 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')}
+
+ {/* Move to folder (opens sub-view) */}
+ {moveTree.length > 0 && onMoveToMailbox && (
+
setMoreMenuSub('move')}
+ 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('move_to')}
+
+
+ )}
+ {/* Tag (opens sub-view) */}
+ {colorOptions.length > 0 && (
+
setMoreMenuSub('tag')}
+ 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('tag')}
+ {currentColors.length > 0 && (
+
+ {currentColors.slice(0, 3).map((c) => {
+ const opt = colorOptions.find((o) => o.value === c);
+ return opt ? : null;
+ })}
+
+ )}
+
+
+ )}
+ {/* Spam */}
+ {(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')}
+
+ )}
+ {/* Toggle read state */}
+
{ onMarkAsRead?.(email.id, isUnread); 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"
+ >
+ {isUnread ? : }
+ {isUnread ? t('mark_read') : t('mark_unread')}
+
+
{ 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')}
+
+ {effectiveEmailContent.isHtml && (
+
{ setEmailViewDarkOverride(prev => prev === null ? !(resolvedTheme === 'dark') : !prev); 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"
+ >
+ {isDark ? : }
+ {isDark ? 'View in light mode' : 'View in dark mode'}
+
+ )}
-
{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);
- })()}
-
+
{ handleExportEmail(); 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('export_email')}
+
+
{ handleImportEmail(); 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('import_email')}
+
+ {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')}
+
+ )}
>
)}
- {/* Tags */}
- {colorOptions.length > 0 && (
+ {moreMenuSub === 'move' && moveTree.length > 0 && onMoveToMailbox && (() => {
+ 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); setMoreMenuSub(null); }}
+ 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);
+ })()}
+ {moreMenuSub === 'tag' && colorOptions.length > 0 && (
<>
-
-
{t('tag')}
{colorOptions.map((option) => {
const isActive = currentColors.includes(option.value);
return (
{ if (email) onSetColorTag?.(email.id, option.value); setMoreMenuOpen(false); }}
+ onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn(
"w-full px-4 py-2.5 min-h-[44px] text-sm text-left hover:bg-muted flex items-center gap-3",
isActive && "bg-accent font-medium"
@@ -3513,85 +3615,15 @@ export function EmailViewer({
})}
{currentColors.length > 0 && (
{ if (email) onSetColorTag?.(email.id, null); setMoreMenuOpen(false); }}
+ onClick={() => { if (email) onSetColorTag?.(email.id, null); setMoreMenuOpen(false); setMoreMenuSub(null); }}
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')}
)}
-
>
)}
- {/* Spam */}
- {(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')}
-
- )}
- {/* Toggle read state */}
- { onMarkAsRead?.(email.id, isUnread); 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"
- >
- {isUnread ? : }
- {isUnread ? t('mark_read') : t('mark_unread')}
-
- { 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')}
-
- {effectiveEmailContent.isHtml && (
- { setEmailViewDarkOverride(prev => prev === null ? !(resolvedTheme === 'dark') : !prev); 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"
- >
- {isDark ? : }
- {isDark ? 'View in light mode' : 'View in dark mode'}
-
- )}
-
- { handleExportEmail(); 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('export_email')}
-
- { handleImportEmail(); 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('import_email')}
-
- {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')}
-
- )}
)}