From 43973608d612448030f1f7038b2c634f03338ca8 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 04:05:45 +0100 Subject: [PATCH] fix: show plain-text mail in the dark theme's own colours instead of a fixed black --- src/components/EmailBodyView.tsx | 10 ++++- .../__tests__/plain-text-theme-colors.test.ts | 43 +++++++++++++++++++ src/lib/email-body-document.ts | 12 +++++- src/lib/email-html.ts | 26 ++++++++--- 4 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 src/lib/__tests__/plain-text-theme-colors.test.ts diff --git a/src/components/EmailBodyView.tsx b/src/components/EmailBodyView.tsx index 1082e9d9..78530d4d 100644 --- a/src/components/EmailBodyView.tsx +++ b/src/components/EmailBodyView.tsx @@ -688,6 +688,13 @@ export default function EmailBodyView({ // Sanitised and wrapped once per message, body and settings, and reused // when the same body mounts again (lib/email-body-document). const emptyLabel = t('email_viewer.no_body_content', '(No body content available)'); + // A plain-text body in a dark app theme takes the theme's colours, not a + // fixed #0a0a0a. Only while the app itself is dark: a single message + // switched to dark in a light app has no dark palette to borrow. + const plainDarkColors = React.useMemo( + () => (resolvedTheme === 'dark' ? { background: c.background, text: c.text, link: c.textLink } : undefined), + [resolvedTheme, c.background, c.text, c.textLink], + ); const prepared = React.useMemo(() => bodyDocument({ key: `${jmapAccountId ?? ''}|${email.id}`, rawHtml, @@ -700,9 +707,10 @@ export default function EmailBodyView({ plainTextFont, quoteLabels, textFactor, + plainDarkColors, }), [ jmapAccountId, email.id, rawHtml, text, email.preview, emptyLabel, shouldBlock, cidMap, renderAsDark, - messageSpacing, plainTextFont, quoteLabels, textFactor, + messageSpacing, plainTextFont, quoteLabels, textFactor, plainDarkColors, ]); const source = React.useMemo(() => ({ html: prepared.html }), [prepared.html]); diff --git a/src/lib/__tests__/plain-text-theme-colors.test.ts b/src/lib/__tests__/plain-text-theme-colors.test.ts new file mode 100644 index 00000000..6ee24b3a --- /dev/null +++ b/src/lib/__tests__/plain-text-theme-colors.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { buildBodyDocument } from '../email-body-document'; +import { BUILTIN_THEMES } from '../../theme/builtin-themes'; + +// A plain-text mail read in dark mode sat on a fixed #0a0a0a whatever the +// app's theme was (UX audit 2026-10, "Plain-text mail dark background"). + +const base = { + key: 'k', emptyLabel: '-', blockRemoteImages: false, cidMap: {}, rawHtml: null, text: 'hello', + messageSpacing: 'auto' as const, plainTextFont: 'sans' as const, quoteLabels: { show: 's', hide: 'h' }, +}; + +describe('plain-text mail colours', () => { + it("takes the dark theme's ground, text and link colours", () => { + const nord = { background: '#2e3440', text: '#eceff4', link: '#88c0d0' }; + const html = buildBodyDocument({ ...base, isDark: true, plainDarkColors: nord }).html; + expect(html).toContain('background: #2e3440;'); + expect(html).toContain('color: #eceff4;'); + expect(html).toContain('a { color: #88c0d0;'); + expect(html).not.toContain('#0a0a0a'); + }); + + it('keeps white in light mode and the old dark colours without a theme', () => { + const colors = { background: '#2e3440', text: '#eceff4', link: '#88c0d0' }; + expect(buildBodyDocument({ ...base, isDark: false, plainDarkColors: colors }).html).toContain('background: #ffffff;'); + expect(buildBodyDocument({ ...base, isDark: true }).html).toContain('background: #0a0a0a;'); + }); + + it('cannot close the style sheet with a theme colour', () => { + const html = buildBodyDocument({ + ...base, isDark: true, plainDarkColors: { background: 'red;}', text: '#fff', link: '#00f' }, + }).html; + expect(html).not.toContain('