mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
feat: scale wide mail down to the pane on any screen with fit wide mail on
This commit is contained in:
@@ -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<FitOptions>({ 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<boolean | null>(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
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<div className={className}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user