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 && (