mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
fix: show plain-text mail in the dark theme's own colours instead of a fixed black
This commit is contained in:
@@ -688,6 +688,13 @@ export default function EmailBodyView({
|
|||||||
// Sanitised and wrapped once per message, body and settings, and reused
|
// Sanitised and wrapped once per message, body and settings, and reused
|
||||||
// when the same body mounts again (lib/email-body-document).
|
// when the same body mounts again (lib/email-body-document).
|
||||||
const emptyLabel = t('email_viewer.no_body_content', '(No body content available)');
|
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({
|
const prepared = React.useMemo(() => bodyDocument({
|
||||||
key: `${jmapAccountId ?? ''}|${email.id}`,
|
key: `${jmapAccountId ?? ''}|${email.id}`,
|
||||||
rawHtml,
|
rawHtml,
|
||||||
@@ -700,9 +707,10 @@ export default function EmailBodyView({
|
|||||||
plainTextFont,
|
plainTextFont,
|
||||||
quoteLabels,
|
quoteLabels,
|
||||||
textFactor,
|
textFactor,
|
||||||
|
plainDarkColors,
|
||||||
}), [
|
}), [
|
||||||
jmapAccountId, email.id, rawHtml, text, email.preview, emptyLabel, shouldBlock, cidMap, renderAsDark,
|
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]);
|
const source = React.useMemo(() => ({ html: prepared.html }), [prepared.html]);
|
||||||
|
|||||||
@@ -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;}</style><script>x()</script>', text: '#fff', link: '#00f' },
|
||||||
|
}).html;
|
||||||
|
expect(html).not.toContain('</style><script>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is fed from the app theme by the body view', () => {
|
||||||
|
expect(BUILTIN_THEMES.length).toBeGreaterThan(0);
|
||||||
|
const view = fs.readFileSync(path.resolve(__dirname, '../../components/EmailBodyView.tsx'), 'utf8');
|
||||||
|
expect(view).toMatch(/background: c\.background, text: c\.text, link: c\.textLink/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
prepareEmailHtml, wrapPlainTextEmail, plainTextToSafeHtml,
|
prepareEmailHtml, wrapPlainTextEmail, plainTextToSafeHtml,
|
||||||
type MessageSpacing, type PlainTextFont,
|
type MessageSpacing, type PlainTextFont, type PlainTextColors,
|
||||||
} from './email-html';
|
} from './email-html';
|
||||||
import { collapsePlainTextQuotes, type QuoteCollapseLabels } from './quote-collapse';
|
import { collapsePlainTextQuotes, type QuoteCollapseLabels } from './quote-collapse';
|
||||||
import { mailBaseFontPx } from './mail-reading';
|
import { mailBaseFontPx } from './mail-reading';
|
||||||
@@ -31,6 +31,8 @@ export interface BodyDocumentInput {
|
|||||||
* body. An HTML body is scaled by an injected pass instead. Default 1.
|
* body. An HTML body is scaled by an injected pass instead. Default 1.
|
||||||
*/
|
*/
|
||||||
textFactor?: number;
|
textFactor?: number;
|
||||||
|
/** The app theme's colours for a plain-text body in dark mode. */
|
||||||
|
plainDarkColors?: PlainTextColors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BodyDocument {
|
export interface BodyDocument {
|
||||||
@@ -61,7 +63,12 @@ export function buildBodyDocument(input: BodyDocumentInput): BodyDocument {
|
|||||||
}
|
}
|
||||||
const safe = collapsePlainTextQuotes(plainTextToSafeHtml(text), input.quoteLabels);
|
const safe = collapsePlainTextQuotes(plainTextToSafeHtml(text), input.quoteLabels);
|
||||||
return {
|
return {
|
||||||
html: wrapPlainTextEmail(safe, { isDark, font: input.plainTextFont, fontPx: mailBaseFontPx(input.textFactor ?? 1) }),
|
html: wrapPlainTextEmail(safe, {
|
||||||
|
isDark,
|
||||||
|
font: input.plainTextFont,
|
||||||
|
fontPx: mailBaseFontPx(input.textFactor ?? 1),
|
||||||
|
darkColors: input.plainDarkColors,
|
||||||
|
}),
|
||||||
applyInversion: false,
|
applyInversion: false,
|
||||||
hasNativeDark: false,
|
hasNativeDark: false,
|
||||||
blockedExternal: false,
|
blockedExternal: false,
|
||||||
@@ -90,6 +97,7 @@ export function bodyDocument(input: BodyDocumentInput): BodyDocument {
|
|||||||
const key = JSON.stringify([
|
const key = JSON.stringify([
|
||||||
input.key, input.blockRemoteImages, input.isDark, input.messageSpacing, input.plainTextFont,
|
input.key, input.blockRemoteImages, input.isDark, input.messageSpacing, input.plainTextFont,
|
||||||
input.quoteLabels.show, input.quoteLabels.hide, input.emptyLabel, input.textFactor ?? 1,
|
input.quoteLabels.show, input.quoteLabels.hide, input.emptyLabel, input.textFactor ?? 1,
|
||||||
|
input.plainDarkColors ?? null,
|
||||||
]);
|
]);
|
||||||
const hit = documents.get(key);
|
const hit = documents.get(key);
|
||||||
if (hit && sameSources(hit.input, input)) {
|
if (hit && sameSources(hit.input, input)) {
|
||||||
|
|||||||
+21
-5
@@ -953,16 +953,32 @@ export type PlainTextFont = 'sans' | 'mono';
|
|||||||
// WebView. Mirrors the webmail's plain-text render path: app font by default
|
// WebView. Mirrors the webmail's plain-text render path: app font by default
|
||||||
// (monospace on request, #830), `white-space: pre-wrap`, native dark theme
|
// (monospace on request, #830), `white-space: pre-wrap`, native dark theme
|
||||||
// (no filter inversion), and a dark-mode-appropriate link colour.
|
// (no filter inversion), and a dark-mode-appropriate link colour.
|
||||||
|
/** The app theme's colours for a plain-text body read in dark mode. */
|
||||||
|
export interface PlainTextColors {
|
||||||
|
background: string;
|
||||||
|
text: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A theme colour for a style sheet: nothing that could end the declaration or the element. */
|
||||||
|
function cssColor(value: string, fallback: string): string {
|
||||||
|
const cleaned = value.replace(/[<>{};\\]/g, '').trim();
|
||||||
|
return cleaned || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
export function wrapPlainTextEmail(
|
export function wrapPlainTextEmail(
|
||||||
innerHtml: string,
|
innerHtml: string,
|
||||||
options: { isDark?: boolean; font?: PlainTextFont; fontPx?: number } = {},
|
options: { isDark?: boolean; font?: PlainTextFont; fontPx?: number; darkColors?: PlainTextColors } = {},
|
||||||
): string {
|
): string {
|
||||||
const { isDark = true, font = 'sans', fontPx = 14 } = options;
|
const { isDark = true, font = 'sans', fontPx = 14, darkColors } = options;
|
||||||
const cleaned = stripDangerousTags(innerHtml);
|
const cleaned = stripDangerousTags(innerHtml);
|
||||||
|
|
||||||
const bg = isDark ? '#0a0a0a' : '#ffffff';
|
// In dark mode the body takes the app theme's ground, text and link colours
|
||||||
const fg = isDark ? '#fafafa' : '#1a1a1a';
|
// when they are given (a built-in dark theme isn't #0a0a0a); light mode
|
||||||
const link = isDark ? '#60a5fa' : '#1a73e8';
|
// stays on white like HTML mail.
|
||||||
|
const bg = isDark ? cssColor(darkColors?.background ?? '', '#0a0a0a') : '#ffffff';
|
||||||
|
const fg = isDark ? cssColor(darkColors?.text ?? '', '#fafafa') : '#1a1a1a';
|
||||||
|
const link = isDark ? cssColor(darkColors?.link ?? '', '#60a5fa') : '#1a73e8';
|
||||||
const fontFamily = font === 'mono'
|
const fontFamily = font === 'mono'
|
||||||
? 'ui-monospace, Menlo, Consolas, "SF Mono", monospace'
|
? 'ui-monospace, Menlo, Consolas, "SF Mono", monospace'
|
||||||
: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||||
|
|||||||
Reference in New Issue
Block a user