From caed2d313a91cced1ac612e9240eccb63ac67b9b Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:21:31 +0100 Subject: [PATCH] feat: open a message in a reader view with the app's font, spacing and colours --- .../email/__tests__/viewer-reading.test.tsx | 32 +++++++ components/email/email-viewer.tsx | 35 ++++++- components/email/reader-view-dialog.tsx | 93 +++++++++++++++++++ locales/ar/common.json | 3 +- locales/ca/common.json | 3 +- locales/cs/common.json | 3 +- locales/da/common.json | 3 +- locales/de/common.json | 3 +- locales/en/common.json | 3 +- locales/es/common.json | 3 +- locales/et/common.json | 3 +- locales/fa/common.json | 3 +- locales/fr/common.json | 3 +- locales/he/common.json | 3 +- locales/hu/common.json | 3 +- locales/it/common.json | 3 +- locales/ja/common.json | 3 +- locales/ko/common.json | 3 +- locales/lv/common.json | 3 +- locales/mn/common.json | 3 +- locales/nb/common.json | 3 +- locales/nl/common.json | 3 +- locales/pl/common.json | 3 +- locales/pt/common.json | 3 +- locales/ro/common.json | 3 +- locales/ru/common.json | 3 +- locales/sk/common.json | 3 +- locales/th/common.json | 3 +- locales/tr/common.json | 3 +- locales/uk/common.json | 3 +- locales/zh-TW/common.json | 3 +- locales/zh/common.json | 3 +- 32 files changed, 213 insertions(+), 34 deletions(-) create mode 100644 components/email/reader-view-dialog.tsx diff --git a/components/email/__tests__/viewer-reading.test.tsx b/components/email/__tests__/viewer-reading.test.tsx index 6beb7d355..bc836a301 100644 --- a/components/email/__tests__/viewer-reading.test.tsx +++ b/components/email/__tests__/viewer-reading.test.tsx @@ -274,3 +274,35 @@ describe('pausing animated images', () => { expect(await frameKey()).not.toContain('/p'); }); }); + +describe('reader toolbar actions', () => { + async function openMenu() { + await act(async () => { screen.getByRole('button', { name: 'more_actions' }).click(); }); + } + const item = (name: string) => screen.getAllByRole('menuitem').find((el) => el.textContent?.includes(name)); + + it('switches one HTML message to plain text and back', async () => { + const { container } = await show(message({ html: '
Fancy HTML version
', text: 'The plain version' })); + await openMenu(); + await act(async () => { item('show_as_plain_text')!.click(); }); + expect(container.querySelector('iframe')).toBeNull(); + expect(screen.getByText('The plain version')).toBeInTheDocument(); + await openMenu(); + expect(item('show_formatted')).toBeTruthy(); + }); + + it('opens the reader view as a dialog that Escape closes, with focus back', async () => { + await show(message({ html: 'Read this in peace.
' })); + const more = screen.getByRole('button', { name: 'more_actions' }); + await openMenu(); + await act(async () => { item('reader_view')!.click(); }); + const dialog = screen.getByRole('dialog', { name: 'Bonjour' }); + expect(dialog).toHaveAttribute('aria-modal', 'true'); + expect(screen.getByTestId('reader-view-body').textContent).toContain('Read this (https://ex.com/r) in peace.'); + expect(dialog.contains(document.activeElement)).toBe(true); + await act(async () => { document.activeElement!.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); }); + expect(screen.queryByRole('dialog', { name: 'Bonjour' })).toBeNull(); + await act(async () => { await new Promise((r) => setTimeout(r, 0)); }); + expect(document.activeElement === more || more.contains(document.activeElement)).toBe(true); + }); +}); diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index f33bf24a5..dcf28eeed 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -13,6 +13,7 @@ import { applyMailTextSize, mailFitOptions } from "@/lib/mail-reading"; import { useMailReading } from "@/hooks/use-mail-reading"; import { useReadAloud } from "@/hooks/use-read-aloud"; import { ReadAloudBar } from "./read-aloud-bar"; +import { ReaderViewDialog } from "./reader-view-dialog"; 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"; @@ -108,6 +109,7 @@ import { Minimize2, Type, Volume2, + BookOpen, ArrowLeft, Pencil, } from "@/components/icons"; @@ -2224,6 +2226,7 @@ export function EmailViewer({ setShowSourceModal(false); setEmailViewDarkOverride(null); setPlainViewOverride(null); + setReaderViewOpen(false); setPluginRenderedHtml(null); setPluginRenderedText(null); setPluginRenderedAttachments(NO_POSTAL_ATTACHMENTS); @@ -2860,8 +2863,8 @@ export function EmailViewer({ const [plainViewOverride, setPlainViewOverride] = useState