diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index c531dec4f..3ce8e5702 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -7,12 +7,12 @@ import { EML_IMPORT_ACCEPT, expandImportableEmails } from "@/lib/eml-import"; import { TRANSPARENT_BLOCKED_PIXEL, applyNewTabToAnchor, emailIframeCsp, escapeHtml, isExternalResourceUrl, isOpenableLinkHref, plainTextToSafeHtml, sanitizeEmailBodyForIframe, sanitizeEmailHtml, sanitizePlainTextRenderedHtml } from "@/lib/email-sanitization"; import { getRenderableHtmlBody } from "@/lib/email-body-selection"; import { mailLanguage } from "@/lib/mail-language"; -import { applyMailTextSize } from "@/lib/mail-reading"; +import { applyMailTextSize, mailFitOptions } from "@/lib/mail-reading"; import { useMailReading } from "@/hooks/use-mail-reading"; import { collectReferencedCids, isEmbeddedInBody, normalizeCid, rewriteCidReferences } from "@/lib/attachment-visibility"; import { collapsePlainTextQuotes, setupQuoteCollapse } from "@/lib/quote-collapse"; import { DARK_PENDING_CLASS, applyEmailDarkMode, emailColorScheme, emailDarkModeCss, emailHasNativeDarkMode as htmlHasNativeDarkMode, resolveColorSchemeMedia } from "@/lib/email-dark-mode"; -import { fitEmailBodyWidth } from "@/lib/email-fit-width"; +import { fitEmailBodyWidth, type FitOptions } from "@/lib/email-fit-width"; import { hasFullBleedCanvas } from "@/lib/email-full-bleed"; import { stripBidiControls } from "@/lib/bidi"; import { withBasePath } from "@/lib/browser-navigation"; @@ -3141,8 +3141,8 @@ export function EmailViewer({ // legible at 1:1 and scrolls. Held in a ref because the measurement runs from // observers wired up once per document - a plain closure would keep fitting // (or not) across a rotation or a pane resize that crossed the breakpoint. - const fitBodyToWidthRef = useRef(isMobile); - fitBodyToWidthRef.current = isMobile; + // Settings > Accessibility > "Fit wide mail" does it on any screen. + const fitBodyToWidthRef = useRef({ enabled: isMobile }); // Detect if the email HTML has native dark mode support const emailHasNativeDarkMode = useMemo(() => { @@ -3157,6 +3157,7 @@ export function EmailViewer({ const reading = useMailReading(); const readingRef = useRef(reading); readingRef.current = reading; + fitBodyToWidthRef.current = mailFitOptions(isMobile, reading.fitWidth); const [emailViewDarkOverride, setEmailViewDarkOverride] = useState(null); const isDark = emailViewDarkOverride !== null ? emailViewDarkOverride @@ -3442,7 +3443,7 @@ export function EmailViewer({ // Shrink desktop-width mail to fit a phone screen first: it changes // the layout height, and the transform doesn't shrink the measured // (layout) height, so the iframe box has to be scaled by hand. - const scale = fitEmailBodyWidth(doc, { enabled: fitBodyToWidthRef.current }); + const scale = fitEmailBodyWidth(doc, fitBodyToWidthRef.current); const height = Math.ceil( Math.max(doc.documentElement.scrollHeight, doc.body.scrollHeight) * scale ); diff --git a/components/email/thread-conversation-view.tsx b/components/email/thread-conversation-view.tsx index 6d1a98a15..3b9514ea0 100644 --- a/components/email/thread-conversation-view.tsx +++ b/components/email/thread-conversation-view.tsx @@ -16,7 +16,7 @@ import { collectReferencedCids, isEmbeddedInBody, normalizeCid, rewriteCidRefere import { collapsePlainTextQuotes, setupQuoteCollapse } from "@/lib/quote-collapse"; import { fitEmailBodyWidth } from "@/lib/email-fit-width"; import { mailLanguage } from "@/lib/mail-language"; -import { applyMailTextSize } from "@/lib/mail-reading"; +import { applyMailTextSize, mailFitOptions } from "@/lib/mail-reading"; import { useMailReading } from "@/hooks/use-mail-reading"; import { DARK_PENDING_CLASS, applyEmailDarkMode, emailDarkModeCss } from "@/lib/email-dark-mode"; import { useThemeStore } from "@/stores/theme-store"; @@ -841,7 +841,8 @@ export function ConversationEmailBody({ const resize = () => { // Fit desktop-width mail to the screen before measuring: the transform // leaves the layout height untouched, so scale the box by hand. - const scale = fitEmailBodyWidth(doc); + // Phones always; any screen with "Fit wide mail" on. + const scale = fitEmailBodyWidth(doc, reading.fitWidth ? mailFitOptions(true, true) : {}); const height = Math.max(doc.documentElement.scrollHeight, doc.body.scrollHeight); iframe.style.height = Math.ceil(height * scale) + 'px'; }; @@ -881,7 +882,7 @@ export function ConversationEmailBody({ } catch { // contentDocument may be inaccessible under stricter sandboxes; ignore. } - }, [t, isDark, allowExternal, email.id, selectionMenu.openRef, reading.textFactor, reading.minFontSize, reading.overrideStyles]); + }, [t, isDark, allowExternal, email.id, selectionMenu.openRef, reading.textFactor, reading.minFontSize, reading.overrideStyles, reading.fitWidth]); return (
diff --git a/hooks/use-mail-reading.ts b/hooks/use-mail-reading.ts index 39cdd0921..642d1063f 100644 --- a/hooks/use-mail-reading.ts +++ b/hooks/use-mail-reading.ts @@ -35,6 +35,8 @@ export interface MailReading { overrideStyles: boolean; /** Reading width "comfortable": lines of about 80 characters. */ comfortableWidth: boolean; + /** Scale mail wider than the pane down to fit, on any screen. */ + fitWidth: boolean; /** Font size for a plain-text body of `basePx` at 100 %. */ plainFontSize: (basePx: number) => string; /** CSS appended to the mail document for the settings above; may be empty. */ @@ -73,6 +75,7 @@ export function useMailReading(): MailReading { paragraphSpacing: state.paragraphSpacing, underlineLinks: state.underlineLinks, readingWidth: state.readingWidth, + mailFitWidth: state.mailFitWidth, }))); // The theme's colours are read from the page, so a theme change re-reads them. const theme = useThemeStore((state) => `${state.resolvedTheme}/${state.activeThemeId ?? ''}`); @@ -105,13 +108,14 @@ export function useMailReading(): MailReading { textFactor, minFontSize, comfortableWidth, + fitWidth: s.mailFitWidth, forcedColors, reduceMotion, overrideStyles, plainFontSize: (basePx: number) => plainBodyFontSize(basePx, s.mailTextScale, s.fontSize, minFontSize), frameCss: mailFrameCss({ reduceMotion, underlineLinks: s.underlineLinks, comfortableWidth, override, fontFaceCss: fontFaces.css }), fontSources: fontFaces.sources, - frameKey: `${textFactor}/${minFontSize}${overrideStyles ? '/o' : ''}`, + frameKey: `${textFactor}/${minFontSize}${overrideStyles ? '/o' : ''}${s.mailFitWidth ? '/f' : ''}`, }; // `theme` only marks that the page's colours changed. // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/lib/__tests__/mail-reading.test.ts b/lib/__tests__/mail-reading.test.ts index 3e41076fa..3954363ec 100644 --- a/lib/__tests__/mail-reading.test.ts +++ b/lib/__tests__/mail-reading.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { applyMailTextSize, mailFrameCss, mailOverrideActive, mailOverrideCss, mailTextFactor, plainBodyFontSize, resolveSystemToggle, scaledTextSize } from '@/lib/mail-reading'; +import { computeFitScale } from '@/lib/email-fit-width'; +import { applyMailTextSize, mailFitOptions, mailFrameCss, mailOverrideActive, mailOverrideCss, mailTextFactor, plainBodyFontSize, resolveSystemToggle, scaledTextSize } from '@/lib/mail-reading'; describe('mailTextFactor', () => { it('follows the app text size when the mail size is 0', () => { @@ -153,3 +154,16 @@ describe('applyMailTextSize with the override stylesheet', () => { expect(p.style.getPropertyValue('text-align')).toBe('start'); }); }); + +describe('mailFitOptions', () => { + it('fits only on phones by default', () => { + expect(computeFitScale(1200, 800, mailFitOptions(false, false))).toBe(1); + expect(mailFitOptions(false, false).enabled).toBe(false); + expect(mailFitOptions(true, false).enabled).toBe(true); + }); + it('fits a wide mail into a desktop pane with "Fit wide mail" on', () => { + const options = mailFitOptions(false, true); + expect(options.enabled).toBe(true); + expect(computeFitScale(1200, 800, options)).toBeCloseTo(2 / 3); + }); +}); diff --git a/lib/mail-reading.ts b/lib/mail-reading.ts index 9a0a413e1..b5b103886 100644 --- a/lib/mail-reading.ts +++ b/lib/mail-reading.ts @@ -280,3 +280,11 @@ export function collectFontFaces(doc: Document, family: string): { css: string; } return { css: css.join('\n'), sources: [...sources] }; } + +/** + * Scale-to-fit options for a message body: on a phone always (the reader's + * old behaviour), on any screen with "Fit wide mail" on. + */ +export function mailFitOptions(isMobile: boolean, fitWidth: boolean): { enabled: boolean; viewportMax?: number } { + return fitWidth ? { enabled: true, viewportMax: Infinity } : { enabled: isMobile }; +}