From e69bfc502ba0d8b8827ff2fc6ce3f3704ebc4939 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 13:56:46 +0100 Subject: [PATCH] fix: lay out the reader's pages at the pane's width so a tablet pane is not left empty --- .../__tests__/reader-pager-width.test.ts | 29 +++++++++++++++++++ src/screens/EmailThreadScreen.tsx | 23 ++++++++++----- 2 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 src/components/__tests__/reader-pager-width.test.ts diff --git a/src/components/__tests__/reader-pager-width.test.ts b/src/components/__tests__/reader-pager-width.test.ts new file mode 100644 index 00000000..4cdee14b --- /dev/null +++ b/src/components/__tests__/reader-pager-width.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +// Tablet, landscape: the first message opened after a relaunch and a +// rotation showed an empty reading pane (toolbar and bottom bar only). The +// pager laid its pages out at the window's width before it knew the pane's, +// so initialScrollIndex put the open message at the wrong offset, and +// removeClippedSubviews could leave that page detached. The pages now wait +// for the measured width. No React Native renderer in the test env, so this +// checks the wiring. + +const src = fs.readFileSync(path.resolve(__dirname, '..', '..', 'screens/EmailThreadScreen.tsx'), 'utf8'); + +describe('reader pager width', () => { + it('renders the pages only once the pager is measured', () => { + expect(src).toMatch(/\s+\{measuredWidth > 0 && \(\s+ { + const pager = src.slice(src.indexOf(' { + expect(src).toMatch(/\(\) => \[cacheVersion, neighboursReady, pageWidth\]/); + }); +}); diff --git a/src/screens/EmailThreadScreen.tsx b/src/screens/EmailThreadScreen.tsx index e924ca91..04707a78 100644 --- a/src/screens/EmailThreadScreen.tsx +++ b/src/screens/EmailThreadScreen.tsx @@ -119,8 +119,13 @@ function EmailViewer({ route, navigation, embedded = false, onActiveChange }: Pr const { width: windowWidth } = useWindowDimensions(); // Pages are as wide as the pager itself, which is the window on a phone // but only the detail pane in a split view. Measured, so a rotation or a - // pane resize re-sizes them. - const [pageWidth, setPageWidth] = React.useState(windowWidth); + // pane resize re-sizes them. The pages render only once it is measured: + // laid out at the window's width first, a split view's pager scrolled the + // opened message to the wrong offset, and on a tablet the pane once stayed + // empty (just the toolbar and the bottom bar) until the message was + // reopened. + const [measuredWidth, setMeasuredWidth] = React.useState(0); + const pageWidth = measuredWidth || windowWidth; const markRead = useEmailStore((s) => s.markRead); const setKeywordForEmails = useEmailStore((s) => s.setKeywordForEmails); const deleteEmail = useEmailStore((s) => s.deleteEmail); @@ -336,12 +341,12 @@ function EmailViewer({ route, navigation, embedded = false, onActiveChange }: Pr }, [pageWidth]); const onPagerLayout = React.useCallback((e: LayoutChangeEvent) => { const width = Math.round(e.nativeEvent.layout.width); - if (width > 0) setPageWidth((prev) => (prev === width ? prev : width)); + if (width > 0) setMeasuredWidth((prev) => (prev === width ? prev : width)); }, []); const pagerExtraData = React.useMemo( - () => [cacheVersion, neighboursReady], - [cacheVersion, neighboursReady], + () => [cacheVersion, neighboursReady, pageWidth], + [cacheVersion, neighboursReady, pageWidth], ); // While the body is pinch-zoomed the pager must not treat horizontal @@ -815,10 +820,11 @@ function EmailViewer({ route, navigation, embedded = false, onActiveChange }: Pr ) : ( <> + + {measuredWidth > 0 && ( )} /> + )} + {/* Bottom action bar (hidden while keyboard is up so quick reply stays visible) */} {kbObstruction === 0 && (