From f4d34477a663b6c62902e7e974fc46c65ec31e0d Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:52:01 +0100 Subject: [PATCH] feat(email-viewer): improve message details and contact sidebar - Show sender name and email in message details From field - Add "Add to contacts" button in contact sidebar for unknown senders - Add info tooltips explaining SPF, DKIM, DMARC, and Spam Score - Use portal-based tooltips to prevent overflow clipping - Move desktop sender info into scrollable area to fix scroll when details expanded --- components/email/email-viewer.tsx | 109 +++++++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 10 deletions(-) diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index e154c07a..6e9001a3 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useMemo, useRef, useCallback } from "react"; +import ReactDOM from "react-dom"; import DOMPurify from "dompurify"; import { Email, ContactCard, Mailbox } from "@/lib/jmap/types"; import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/email-sanitization"; @@ -59,6 +60,7 @@ import { Folder, Sun, Moon, + HelpCircle, } from "lucide-react"; import { useTranslations } from "next-intl"; import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store"; @@ -209,13 +211,17 @@ function renderClickableRecipients( function ContactSidebarPanel({ email, contact, + senderName, onClose, + onAddToContacts, }: { email: string; contact: ContactCard | null; + senderName?: string; onClose: () => void; + onAddToContacts?: (email: string, name?: string) => void; }) { - const name = contact ? getContactDisplayName(contact) : null; + const name = contact ? getContactDisplayName(contact) : senderName || null; const primaryEmail = contact ? getContactPrimaryEmail(contact) : email; const emails = contact?.emails ? Object.values(contact.emails) : []; const phones = contact?.phones ? Object.values(contact.phones) : []; @@ -374,10 +380,19 @@ function ContactSidebarPanel({ {/* No contact found message */} {!contact && ( -
+

Not in your contacts

+ {onAddToContacts && ( + + )}
)}
@@ -397,6 +412,46 @@ function SidebarSection({ icon: Icon, title, children }: { icon: React.Component ); } +function InfoTooltip({ text }: { text: string }) { + const [open, setOpen] = useState(false); + const [pos, setPos] = useState<{ top: number; left: number } | null>(null); + const btnRef = useRef(null); + + const show = () => { + if (btnRef.current) { + const rect = btnRef.current.getBoundingClientRect(); + setPos({ top: rect.top - 8, left: rect.left + rect.width / 2 }); + } + setOpen(true); + }; + const hide = () => setOpen(false); + + return ( + + + {open && pos && ReactDOM.createPortal( + + {text} + , + document.body + )} + + ); +} + export function EmailViewer({ email, isLoading = false, @@ -2274,6 +2329,7 @@ export function EmailViewer({ ` : undefined} onViewContact={handleViewContactSidebar} className="text-sm" /> @@ -2372,8 +2428,11 @@ export function EmailViewer({ } {getSecurityStatus(email.authenticationResults.spf.result).icon === 'minus' && } -
-
SPF
+
+
+ SPF + +
{email.authenticationResults.spf.result}
@@ -2403,8 +2462,11 @@ export function EmailViewer({ } {getSecurityStatus(email.authenticationResults.dkim.result).icon === 'minus' && } -
-
DKIM
+
+
+ DKIM + +
{email.authenticationResults.dkim.result}
@@ -2434,8 +2496,11 @@ export function EmailViewer({ } {getSecurityStatus(email.authenticationResults.dmarc.result).icon === 'minus' && } -
-
DMARC
+
+
+ DMARC + +
{email.authenticationResults.dmarc.result}
@@ -2464,8 +2529,11 @@ export function EmailViewer({ email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" : "text-green-700 dark:text-green-400" )} /> -
-
Spam Score
+
+
+ Spam Score + +
5 ? "text-red-700 dark:text-red-400" : @@ -3010,7 +3078,28 @@ export function EmailViewer({ { + const allRecipients = [...(email?.from || []), ...(email?.to || []), ...(email?.cc || []), ...(email?.bcc || []), ...(email?.replyTo || [])]; + return allRecipients.find(r => r.email.toLowerCase() === contactSidebarEmail.toLowerCase())?.name; + })()} onClose={() => setContactSidebarEmail(null)} + onAddToContacts={(addr, name) => { + const { createContact, addLocalContact, supportsSync } = useContactStore.getState(); + const client = useAuthStore.getState().client; + const contactData: Partial = { + emails: { email: { address: addr } }, + ...(name ? { name: { components: name.includes(' ') + ? [{ kind: 'given' as const, value: name.split(' ')[0] }, { kind: 'surname' as const, value: name.split(' ').slice(1).join(' ') }] + : [{ kind: 'given' as const, value: name }] + }} : {}), + }; + if (client && supportsSync) { + createContact(client, contactData).then(() => toast.success('Contact added')); + } else { + addLocalContact({ id: `local-${crypto.randomUUID()}`, addressBookIds: {}, ...contactData } as ContactCard); + toast.success('Contact added'); + } + }} /> )}