From 49cd7f81307e9adb893c4859f61c629d8b2cde16 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sun, 17 May 2026 17:15:19 +0200
Subject: [PATCH] feat: show details toggle and panel on mobile sender info
---
components/email/email-viewer.tsx | 627 +++++++++++++++---------------
1 file changed, 323 insertions(+), 304 deletions(-)
diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx
index 3a185bd2..1ab56931 100644
--- a/components/email/email-viewer.tsx
+++ b/components/email/email-viewer.tsx
@@ -4143,310 +4143,6 @@ export function EmailViewer({
- {/* Expandable Details */}
- {showFullHeaders && (() => {
- const translateAuthResult = (result?: string) => {
- const r = (result || '').toLowerCase();
- switch (r) {
- case 'pass': return t('authentication.result.pass');
- case 'fail': return t('authentication.result.fail');
- case 'softfail': return t('authentication.result.softfail');
- case 'neutral': return t('authentication.result.neutral');
- case 'permerror': return t('authentication.result.permerror');
- case 'temperror': return t('authentication.result.temperror');
- case 'none': return t('authentication.result.none');
- default: return result || '';
- }
- };
- const replyToDifferent = !!email.replyTo?.length &&
- (!email.from || email.replyTo[0].email !== email.from[0]?.email);
- const deliveryDeltaMs = email.sentAt && email.receivedAt
- ? Math.abs(new Date(email.receivedAt).getTime() - new Date(email.sentAt).getTime())
- : 0;
- const formatDelta = (diff: number) => {
- const minutes = Math.floor(diff / 60000);
- const hours = Math.floor(minutes / 60);
- const days = Math.floor(hours / 24);
- const dayUnit = days > 1 ? t('time.days') : t('time.day');
- const hourUnit = (hours % 24) > 1 ? t('time.hours') : t('time.hour');
- const minuteUnit = (minutes % 60) > 1 ? t('time.minutes') : t('time.minute');
- const minuteUnitSingle = minutes > 1 ? t('time.minutes') : t('time.minute');
- if (days > 0) return `${days} ${dayUnit} ${hours % 24} ${hourUnit}`;
- if (hours > 0) return `${hours} ${hourUnit} ${minutes % 60} ${minuteUnit}`;
- return `${minutes} ${minuteUnitSingle}`;
- };
- const fullDate = (iso?: string) => iso
- ? formatDateTime(iso, timeFormat, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', second: '2-digit', timeZoneName: 'short' })
- : '-';
- const auth = email.authenticationResults;
- const totalAttachmentSize = effectiveAttachments.reduce((s, a) => s + (a.size || 0), 0);
- const topMimeType = email.bodyStructure?.type;
- const SectionHeader = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
- );
- const Row = ({ label, children, mono }: { label: string; children: React.ReactNode; mono?: boolean }) => (
- <>
- {label}
- {children}
- >
- );
- const AuthChip = ({ name, result, extra, tooltip }: { name: string; result?: string; extra?: React.ReactNode; tooltip?: string }) => {
- if (!result) return null;
- const status = getSecurityStatus(result);
- const Icon = status.icon === 'check' ? Check
- : status.icon === 'x' ? X
- : status.icon === 'alert' ? AlertTriangle
- : Minus;
- return (
-
-
- {name}
-
- {translateAuthResult(result)}
-
- {extra && (
- <>
- ·
- {extra}
- >
- )}
-
- );
- };
-
- const hasIdentifiers = !!(email.messageId || email.inReplyTo?.length || email.references?.length || email.threadId);
- const hasListInfo = !!(listHeaders?.listId || listHeaders?.listUnsubscribe || listHeaders?.listHelp || listHeaders?.listPost);
- const hasAuthSection = !!(auth?.spf || auth?.dkim || auth?.dmarc || auth?.iprev || email.spamScore !== undefined || email.spamLLM);
-
- return (
-
-
- {t('details.recipients_routing')}
-
-
-
- ` : undefined}
- onViewContact={handleViewContactSidebar}
- className="text-sm text-left"
- />
-
-
- {replyToDifferent && (
-
-
- {email.replyTo!.map((r, i) => (
-
- ))}
-
-
- )}
- {email.to && email.to.length > 0 && (
-
-
- {renderClickableRecipients(email.to, currentUserEmail, t, handleViewContactSidebar, 100)}
-
-
- )}
- {email.cc && email.cc.length > 0 && (
-
-
- {renderClickableRecipients(email.cc, currentUserEmail, t, handleViewContactSidebar, 100)}
-
-
- )}
- {email.bcc && email.bcc.length > 0 && (
-
-
- {renderClickableRecipients(email.bcc, currentUserEmail, t, handleViewContactSidebar, 100)}
-
-
- )}
- {email.sentAt && (
- {fullDate(email.sentAt)}
- )}
-
- {fullDate(email.receivedAt)}
- {deliveryDeltaMs > 60000 && (
- · {formatDelta(deliveryDeltaMs)} {t('details.delivery_time').toLowerCase()}
- )}
-
-
-
-
- {hasAuthSection && (
-
- {t('details.authentication_security')}
-
- {auth?.spf && (
-
- )}
- {auth?.dkim && (
-
- )}
- {auth?.dmarc && (
-
- )}
- {auth?.iprev && (
-
- )}
- {email.spamScore !== undefined && (
-
5 ? "bg-red-500/[0.07] border-red-500/30" :
- email.spamScore > 2 ? "bg-amber-500/[0.07] border-amber-500/30" :
- "bg-green-500/[0.07] border-green-500/30",
- )}>
- 5 ? "text-red-700 dark:text-red-400" :
- email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" :
- "text-green-700 dark:text-green-400",
- )} />
- {t('authentication.spam_score')}
- 5 ? "text-red-700 dark:text-red-400" :
- email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" :
- "text-green-700 dark:text-green-400",
- )}>
- {email.spamScore.toFixed(1)}
-
- {email.spamStatus && (
- <>
- ·
- {email.spamStatus}
- >
- )}
-
- )}
-
- {email.spamLLM && (
-
- {email.spamLLM.verdict === 'LEGITIMATE' ?
:
- email.spamLLM.verdict === 'SPAM' ?
:
-
}
-
-
- {email.spamLLM.verdict}
-
- · {email.spamLLM.explanation}
-
-
- )}
-
- )}
-
- {hasIdentifiers && (
-
- {t('details.identifiers_threading')}
-
- {email.messageId && (
- {email.messageId}
- )}
- {email.inReplyTo && email.inReplyTo.length > 0 && (
-
-
- {email.inReplyTo.map((id, i) =>
{id}
)}
-
-
- )}
- {email.references && email.references.length > 0 && (
-
-
-
-
- {t(email.references.length === 1 ? 'previous_messages' : 'previous_messages_plural', { count: email.references.length })}
-
-
- {email.references.map((id, i) =>
{id}
)}
-
-
-
- )}
- {email.threadId && (
- {email.threadId}
- )}
-
-
- )}
-
-
- {t('details.message_properties')}
-
- {email.subject !== undefined && (
- {email.subject || {t('details.no_subject')}}
- )}
-
- {formatFileSize(email.size)}
- {topMimeType && (
- · {topMimeType}
- )}
-
- {effectiveAttachments.length > 0 && (
-
- {t('details.attachments_summary', {
- count: effectiveAttachments.length,
- size: formatFileSize(totalAttachmentSize),
- })}
-
- )}
- {email.accountLabel && (
- {email.accountLabel}
- )}
-
-
-
- {hasListInfo && (
-
- {t('details.mailing_list')}
-
- {listHeaders?.listId && (
- {listHeaders.listId}
- )}
- {listHeaders?.listUnsubscribe?.preferred && (
-
-
- {listHeaders.listUnsubscribe.preferred === 'http'
- ? listHeaders.listUnsubscribe.http
- : listHeaders.listUnsubscribe.mailto}
-
-
- )}
- {listHeaders?.listHelp && (
- {listHeaders.listHelp}
- )}
- {listHeaders?.listPost && (
- {listHeaders.listPost}
- )}
-
-
- )}
-
- );
- })()}
{/* Attachments on the right (beside-sender mode) */}
@@ -4648,6 +4344,22 @@ export function EmailViewer({
)}
>
)}
+
{/* Date/time + size on the right (mobile) */}
@@ -4664,6 +4376,313 @@ export function EmailViewer({
+ {/* Expandable Details (shared across mobile/tablet/desktop) */}
+ {showFullHeaders && (() => {
+ const translateAuthResult = (result?: string) => {
+ const r = (result || '').toLowerCase();
+ switch (r) {
+ case 'pass': return t('authentication.result.pass');
+ case 'fail': return t('authentication.result.fail');
+ case 'softfail': return t('authentication.result.softfail');
+ case 'neutral': return t('authentication.result.neutral');
+ case 'permerror': return t('authentication.result.permerror');
+ case 'temperror': return t('authentication.result.temperror');
+ case 'none': return t('authentication.result.none');
+ default: return result || '';
+ }
+ };
+ const replyToDifferent = !!email.replyTo?.length &&
+ (!email.from || email.replyTo[0].email !== email.from[0]?.email);
+ const deliveryDeltaMs = email.sentAt && email.receivedAt
+ ? Math.abs(new Date(email.receivedAt).getTime() - new Date(email.sentAt).getTime())
+ : 0;
+ const formatDelta = (diff: number) => {
+ const minutes = Math.floor(diff / 60000);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+ const dayUnit = days > 1 ? t('time.days') : t('time.day');
+ const hourUnit = (hours % 24) > 1 ? t('time.hours') : t('time.hour');
+ const minuteUnit = (minutes % 60) > 1 ? t('time.minutes') : t('time.minute');
+ const minuteUnitSingle = minutes > 1 ? t('time.minutes') : t('time.minute');
+ if (days > 0) return `${days} ${dayUnit} ${hours % 24} ${hourUnit}`;
+ if (hours > 0) return `${hours} ${hourUnit} ${minutes % 60} ${minuteUnit}`;
+ return `${minutes} ${minuteUnitSingle}`;
+ };
+ const fullDate = (iso?: string) => iso
+ ? formatDateTime(iso, timeFormat, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', second: '2-digit', timeZoneName: 'short' })
+ : '-';
+ const auth = email.authenticationResults;
+ const totalAttachmentSize = effectiveAttachments.reduce((s, a) => s + (a.size || 0), 0);
+ const topMimeType = email.bodyStructure?.type;
+ const SectionHeader = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+ );
+ const Row = ({ label, children, mono }: { label: string; children: React.ReactNode; mono?: boolean }) => (
+ <>
+ {label}
+ {children}
+ >
+ );
+ const AuthChip = ({ name, result, extra, tooltip }: { name: string; result?: string; extra?: React.ReactNode; tooltip?: string }) => {
+ if (!result) return null;
+ const status = getSecurityStatus(result);
+ const Icon = status.icon === 'check' ? Check
+ : status.icon === 'x' ? X
+ : status.icon === 'alert' ? AlertTriangle
+ : Minus;
+ return (
+
+
+ {name}
+
+ {translateAuthResult(result)}
+
+ {extra && (
+ <>
+ ·
+ {extra}
+ >
+ )}
+
+ );
+ };
+
+ const hasIdentifiers = !!(email.messageId || email.inReplyTo?.length || email.references?.length || email.threadId);
+ const hasListInfo = !!(listHeaders?.listId || listHeaders?.listUnsubscribe || listHeaders?.listHelp || listHeaders?.listPost);
+ const hasAuthSection = !!(auth?.spf || auth?.dkim || auth?.dmarc || auth?.iprev || email.spamScore !== undefined || email.spamLLM);
+
+ return (
+
+
+
+ {t('details.recipients_routing')}
+
+
+
+ ` : undefined}
+ onViewContact={handleViewContactSidebar}
+ className="text-sm text-left"
+ />
+
+
+ {replyToDifferent && (
+
+
+ {email.replyTo!.map((r, i) => (
+
+ ))}
+
+
+ )}
+ {email.to && email.to.length > 0 && (
+
+
+ {renderClickableRecipients(email.to, currentUserEmail, t, handleViewContactSidebar, 100)}
+
+
+ )}
+ {email.cc && email.cc.length > 0 && (
+
+
+ {renderClickableRecipients(email.cc, currentUserEmail, t, handleViewContactSidebar, 100)}
+
+
+ )}
+ {email.bcc && email.bcc.length > 0 && (
+
+
+ {renderClickableRecipients(email.bcc, currentUserEmail, t, handleViewContactSidebar, 100)}
+
+
+ )}
+ {email.sentAt && (
+ {fullDate(email.sentAt)}
+ )}
+
+ {fullDate(email.receivedAt)}
+ {deliveryDeltaMs > 60000 && (
+ · {formatDelta(deliveryDeltaMs)} {t('details.delivery_time').toLowerCase()}
+ )}
+
+
+
+
+ {hasAuthSection && (
+
+ {t('details.authentication_security')}
+
+ {auth?.spf && (
+
+ )}
+ {auth?.dkim && (
+
+ )}
+ {auth?.dmarc && (
+
+ )}
+ {auth?.iprev && (
+
+ )}
+ {email.spamScore !== undefined && (
+
5 ? "bg-red-500/[0.07] border-red-500/30" :
+ email.spamScore > 2 ? "bg-amber-500/[0.07] border-amber-500/30" :
+ "bg-green-500/[0.07] border-green-500/30",
+ )}>
+ 5 ? "text-red-700 dark:text-red-400" :
+ email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" :
+ "text-green-700 dark:text-green-400",
+ )} />
+ {t('authentication.spam_score')}
+ 5 ? "text-red-700 dark:text-red-400" :
+ email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" :
+ "text-green-700 dark:text-green-400",
+ )}>
+ {email.spamScore.toFixed(1)}
+
+ {email.spamStatus && (
+ <>
+ ·
+ {email.spamStatus}
+ >
+ )}
+
+ )}
+
+ {email.spamLLM && (
+
+ {email.spamLLM.verdict === 'LEGITIMATE' ?
:
+ email.spamLLM.verdict === 'SPAM' ?
:
+
}
+
+
+ {email.spamLLM.verdict}
+
+ · {email.spamLLM.explanation}
+
+
+ )}
+
+ )}
+
+ {hasIdentifiers && (
+
+ {t('details.identifiers_threading')}
+
+ {email.messageId && (
+ {email.messageId}
+ )}
+ {email.inReplyTo && email.inReplyTo.length > 0 && (
+
+
+ {email.inReplyTo.map((id, i) =>
{id}
)}
+
+
+ )}
+ {email.references && email.references.length > 0 && (
+
+
+
+
+ {t(email.references.length === 1 ? 'previous_messages' : 'previous_messages_plural', { count: email.references.length })}
+
+
+ {email.references.map((id, i) =>
{id}
)}
+
+
+
+ )}
+ {email.threadId && (
+ {email.threadId}
+ )}
+
+
+ )}
+
+
+ {t('details.message_properties')}
+
+ {email.subject !== undefined && (
+ {email.subject || {t('details.no_subject')}}
+ )}
+
+ {formatFileSize(email.size)}
+ {topMimeType && (
+ · {topMimeType}
+ )}
+
+ {effectiveAttachments.length > 0 && (
+
+ {t('details.attachments_summary', {
+ count: effectiveAttachments.length,
+ size: formatFileSize(totalAttachmentSize),
+ })}
+
+ )}
+ {email.accountLabel && (
+ {email.accountLabel}
+ )}
+
+
+
+ {hasListInfo && (
+
+ {t('details.mailing_list')}
+
+ {listHeaders?.listId && (
+ {listHeaders.listId}
+ )}
+ {listHeaders?.listUnsubscribe?.preferred && (
+
+
+ {listHeaders.listUnsubscribe.preferred === 'http'
+ ? listHeaders.listUnsubscribe.http
+ : listHeaders.listUnsubscribe.mailto}
+
+
+ )}
+ {listHeaders?.listHelp && (
+ {listHeaders.listHelp}
+ )}
+ {listHeaders?.listPost && (
+ {listHeaders.listPost}
+ )}
+
+
+ )}
+
+
+ );
+ })()}
+
{/* S/MIME Status Banner */}
{smimeStatus && (