diff --git a/components/email/email-list-item.tsx b/components/email/email-list-item.tsx index b29819fe..31e4d8e9 100644 --- a/components/email/email-list-item.tsx +++ b/components/email/email-list-item.tsx @@ -40,6 +40,7 @@ export function EmailListItem({ email, selected, onClick, onDoubleClick, onConte const density = useSettingsStore((state) => state.density); const mailLayout = useSettingsStore((state) => state.mailLayout); const emailKeywords = useSettingsStore((state) => state.emailKeywords); + const tintListRowsByTag = useSettingsStore((state) => state.tintListRowsByTag); const showAvatarsInJunk = useSettingsStore((state) => state.showAvatarsInJunk); const { identities } = useAuthStore(); const isChecked = selectedEmailIds.has(email.id); @@ -68,7 +69,7 @@ export function EmailListItem({ email, selected, onClick, onDoubleClick, onConte const keywordDefs = colorTagIds.map(id => emailKeywords.find(k => k.id === id) ?? { id, label: id, color: 'gray' }); // Use first tag for background coloring const keywordDef = keywordDefs[0] ?? null; - const colorTag = keywordDef ? KEYWORD_PALETTE[keywordDef.color]?.bg ?? null : null; + const colorTag = (tintListRowsByTag && keywordDef) ? KEYWORD_PALETTE[keywordDef.color]?.bg ?? null : null; // Drag and drop functionality const { dragHandlers, isDragging } = useEmailDrag({ diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 76ec3c50..4f078c5b 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -2117,6 +2117,12 @@ export function EmailViewer({ fit - the latter wraps header text to one character per line, which reads as 90deg-rotated vertical headers (issue #409). */ html { overflow: hidden; height: auto !important; } + /* Some emails put height:100% on a full-bleed wrapper table/div (not html/body), + which - with body's overflow:hidden - clips the content to a sliver, and the + scrollHeight-based auto-resize then locks the iframe short (a Box.co.il + verification email rendered as a logo-only 150px strip). Neutralise the + full-height trick on any element so the body grows to its content. */ + [style*="height:100%"], [style*="height: 100%"] { height: auto !important; } body { margin: 0; padding: ${bodyPadding}; overflow-x: auto; overflow-y: hidden; height: auto !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; line-height: 1.6; color: #1a1a1a; background: #ffffff; word-wrap: break-word; overflow-wrap: break-word; } @media (max-width: 640px) { body { padding-left: ${mobileBodyPaddingX}; padding-right: ${mobileBodyPaddingX}; } } img { max-width: 100% !important; height: auto !important; } @@ -2164,15 +2170,30 @@ export function EmailViewer({ const doc = iframe.contentDocument; if (doc?.body) { // Auto-resize iframe to fit content - const resizeObserver = new ResizeObserver(() => { - const height = doc.documentElement.scrollHeight; + // Measure max(documentElement, body): a height:100% wrapper can leave + // documentElement.scrollHeight short while the real content lives in body. + const applyHeight = () => { + if (iframe.contentDocument !== doc) return; // navigated away; stale + const height = Math.max(doc.documentElement.scrollHeight, doc.body.scrollHeight); iframe.style.height = height + 'px'; lastBodyHeightRef.current = height; - }); + }; + const resizeObserver = new ResizeObserver(applyHeight); resizeObserver.observe(doc.body); - const initialHeight = doc.documentElement.scrollHeight; - iframe.style.height = initialHeight + 'px'; - lastBodyHeightRef.current = initialHeight; + applyHeight(); + // The ResizeObserver only fires on body's border box; a content overflow + // that grows scrollHeight without resizing that box (e.g. a height:100% + // wrapper, or images that reflow the layout after onload) is otherwise + // missed and the iframe stays short. Re-measure on a fixed cadence over a + // short settle window, then stop — a self-clearing catch-all that does + // not depend on image load/error events firing (blocked images may fire + // neither). Cheap: ~12 scrollHeight reads, no early-stop heuristic to + // mis-trigger on a brief-stable-then-grow reflow. + const poll = window.setInterval(() => { + if (iframe.contentDocument !== doc) { window.clearInterval(poll); return; } + applyHeight(); + }, 200); + window.setTimeout(() => window.clearInterval(poll), 2400); setIframeReady(true); // Hide images that fail to load (dead/mixed-content/unreachable external diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index 69039503..27a81714 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -90,6 +90,7 @@ const SingleEmailItem = React.forwardRef( const showRecipient = currentMailboxRole === 'sent' || currentMailboxRole === 'drafts'; const sender = showRecipient ? (email.to?.[0] ?? email.from?.[0]) : email.from?.[0]; const emailKeywords = useSettingsStore((state) => state.emailKeywords); + const tintListRowsByTag = useSettingsStore((state) => state.tintListRowsByTag); const density = useSettingsStore((state) => state.density); const mailLayout = useSettingsStore((state) => state.mailLayout); const timeFormat = useSettingsStore((state) => state.timeFormat); @@ -113,7 +114,7 @@ const SingleEmailItem = React.forwardRef( const tagIds = getEmailColorTags(email.keywords); const resolvedKeywordDefs = tagIds.map(id => emailKeywords.find(k => k.id === id) ?? { id, label: id, color: 'gray' }); const resolvedKeywordDef = resolvedKeywordDefs[0] ?? null; - const resolvedColorTag = (() => { + const resolvedColorTag = !tintListRowsByTag ? null : (() => { if (colorTag) return colorTag; return resolvedKeywordDef ? KEYWORD_PALETTE[resolvedKeywordDef.color]?.bg ?? null : null; })(); @@ -494,8 +495,9 @@ export const ThreadListItem = React.forwardRef state.emailKeywords); + const tintListRowsByTag = useSettingsStore((state) => state.tintListRowsByTag); const keywordDef = threadColor ? (emailKeywordDefs.find(k => k.id === threadColor) ?? { id: threadColor, label: threadColor, color: 'gray' }) : null; - const colorTag = keywordDef ? KEYWORD_PALETTE[keywordDef.color]?.bg ?? null : null; + const colorTag = (tintListRowsByTag && keywordDef) ? KEYWORD_PALETTE[keywordDef.color]?.bg ?? null : null; const isSelected = selectedEmailId === latestEmail.id || thread.emails.some(e => e.id === selectedEmailId); diff --git a/components/settings/layout-settings.tsx b/components/settings/layout-settings.tsx index 14e39d60..04103f55 100644 --- a/components/settings/layout-settings.tsx +++ b/components/settings/layout-settings.tsx @@ -118,7 +118,7 @@ function MailLayoutPreview({ export function LayoutSettings() { const t = useTranslations('settings.appearance'); const tEmail = useTranslations('settings.email_behavior'); - const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, includeGroupInUnified, enableAllMailView, allMailFolderIds, enableCrossUnreadView, enableCrossStarredView, enableCrossAllView, colorfulSidebarIcons, showFolderTotalCount, mailLayout, proInterface, updateSetting } = useSettingsStore(); + const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, includeGroupInUnified, enableAllMailView, allMailFolderIds, enableCrossUnreadView, enableCrossStarredView, enableCrossAllView, colorfulSidebarIcons, tintListRowsByTag, showFolderTotalCount, mailLayout, proInterface, updateSetting } = useSettingsStore(); const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore(); const accounts = useAccountStore(s => s.accounts); const activeAccountId = useAccountStore(s => s.activeAccountId); @@ -217,6 +217,13 @@ export function LayoutSettings() { /> + + updateSetting('tintListRowsByTag', checked)} + /> + + ()( senderFavicons: state.senderFavicons, showAvatarsInJunk: state.showAvatarsInJunk, colorfulSidebarIcons: state.colorfulSidebarIcons, + tintListRowsByTag: state.tintListRowsByTag, showFolderTotalCount: state.showFolderTotalCount, folderIcons: state.folderIcons, emailKeywords: state.emailKeywords,