From 32135ddb95cfb440a4984996efed36bd0f1d39dd Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Fri, 1 May 2026 16:29:38 +0200 Subject: [PATCH] fix: collapse below-header attachments to single row with overflow pill --- components/email/email-viewer.tsx | 125 +++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 3 deletions(-) diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index ba9cc93a..fa539625 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -922,6 +922,10 @@ export function EmailViewer({ const [showFullHeaders, setShowFullHeaders] = useState(false); const [showAllBesideAttachments, setShowAllBesideAttachments] = useState(false); const [showAllMobileAttachments, setShowAllMobileAttachments] = useState(false); + const [showAllBelowHeaderAttachments, setShowAllBelowHeaderAttachments] = useState(false); + const [visibleBelowHeaderCount, setVisibleBelowHeaderCount] = useState(null); + const belowHeaderRowRef = useRef(null); + const belowHeaderGhostRef = useRef(null); const [allowExternalContent, setAllowExternalContent] = useState(false); const [hasBlockedContent, setHasBlockedContent] = useState(false); const [cidBlobUrls, setCidBlobUrls] = useState>({}); @@ -2131,6 +2135,43 @@ export function EmailViewer({ return [...jmapAttachments, ...tnefExtracted, ...embeddedExtracted]; }, [email?.attachments, smimeDecryptedAttachments, tnefHtml, tnefText, tnefAttachments, embeddedEmailUnwrapped, embeddedEmailAttachments, calendarInvitationParsingEnabled, hideInlineImageAttachments]); + // Measure attachment chips in the below-header row to determine how many fit + // on a single line; the rest collapse into a "+N attachments" overflow pill. + useLayoutEffect(() => { + if (attachmentPosition !== 'below-header' || effectiveAttachments.length === 0) { + setVisibleBelowHeaderCount(null); + return; + } + const container = belowHeaderRowRef.current; + const ghost = belowHeaderGhostRef.current; + if (!container || !ghost) return; + + const measure = () => { + const containerWidth = container.clientWidth; + const chips = Array.from(ghost.children) as HTMLElement[]; + if (chips.length === 0) return; + const ghostLeft = ghost.getBoundingClientRect().left; + // Reserve space for the "+N attachments" overflow pill + const RESERVED = 140; + let count = chips.length; + for (let i = 0; i < chips.length; i++) { + const right = chips[i].getBoundingClientRect().right - ghostLeft; + const isLast = i === chips.length - 1; + const limit = isLast ? containerWidth : containerWidth - RESERVED; + if (right > limit) { + count = i; + break; + } + } + setVisibleBelowHeaderCount(count >= chips.length ? null : Math.max(1, count)); + }; + + measure(); + const ro = new ResizeObserver(measure); + ro.observe(container); + return () => ro.disconnect(); + }, [effectiveAttachments, attachmentPosition]); + // Generate email source for viewing const generateEmailSource = (email: Email): string => { let source = ''; @@ -4397,15 +4438,41 @@ export function EmailViewer({ {/* === ATTACHMENTS below header (below-header mode, desktop only) === */} {attachmentPosition === 'below-header' && effectiveAttachments.length > 0 && (
-
- {effectiveAttachments.map((attachment) => { +
+ {/* Hidden ghost row used purely for measuring chip widths */} + + {effectiveAttachments + .slice(0, visibleBelowHeaderCount ?? effectiveAttachments.length) + .map((attachment) => { const FileIcon = getFileIcon(attachment.name || undefined, attachment.type); const isPreviewable = isFilePreviewable(attachment.name || undefined, attachment.type); const opensPreview = isPreviewable && mailAttachmentAction === 'preview'; return (
@@ -4435,6 +4502,58 @@ export function EmailViewer({
); })} + {visibleBelowHeaderCount !== null && effectiveAttachments.length > visibleBelowHeaderCount && ( + + )} + {showAllBelowHeaderAttachments && visibleBelowHeaderCount !== null && effectiveAttachments.length > visibleBelowHeaderCount && ( + <> +
setShowAllBelowHeaderAttachments(false)} /> +
+ {effectiveAttachments.slice(visibleBelowHeaderCount).map((attachment) => { + const FileIcon = getFileIcon(attachment.name || undefined, attachment.type); + const isPreviewable = isFilePreviewable(attachment.name || undefined, attachment.type); + const opensPreview = isPreviewable && mailAttachmentAction === 'preview'; + return ( +
+ + + {getAttachmentDisplayName(attachment.name, attachment.type)} + + + {formatFileSize(attachment.size)} + +
+ + {opensPreview && ( + + )} +
+
+ ); + })} +
+ + )}
)}