Skip to content

feat(reading): V2 Android reader bars, tap zones, back, keep-awake, save on close (#1264) - #1288

Merged
mrviduus merged 7 commits into
mainfrom
v2/1263-1264-android-chrome
Nov 13, 2026
Merged

mrviduus merged 7 commits into
mainfrom
v2/1263-1264-android-chrome

Conversation

@mrviduus

Copy link
Copy Markdown
Owner

Summary

The V2 Android reader gets its reading frame: top and bottom bars that hide while reading, tap zones, hardware back, keep-awake, safe areas, and a save when a book is closed or the app goes to the background. The route is still dev-only (__DEV__).

Stack: 3 of 4. Base is #1287 (page protocol), which sits on #1285. Merge order: #1285 → #1287 → this → web bars. Full CI runs here only after GitHub retargets this PR to main.

Slice

Wave 14, B4 (Android half) + B5. Fixes #1264. Refs #1263 (closed by the web bars PR).

Changes

V2ReaderScreen.tsx

  • With a book open: a top bar (back, title) and a bottom bar (book %, section i/N, section title, previous / next page) over the WebView. They hide 3 s after an open, on an edge tap, a key turn, and on any relocate the reader caused (relocate.moved, either flow). A centre tap toggles them. A bar button, the taken offer, a re-layout or a sync jump does not hide them. Reduced motion: no slide or fade.
  • Tap zones through the shared tapZone.mjs tapAction: the outer 25 % of each side turns the page in paginated flow and always for a fixed-layout book; the centre toggles; direction from the page's latest dir. A tap that dismisses a selection never arrives as a tap.
  • A tap on text acts as a tap at the word's centre for now (readWordTap), so text, which is most of the screen, is not a dead zone. Slice V2 reader: word popup, translation and explain through the host #1269 (word popup) replaces this.
  • Hardware keys through the shared keyAction; one press, one turn.
  • Back: closes the offer, then the book (back to the list), then leaves the route. No exit prompt.
  • Closing a book ends it: the page flushes its pending save, then answers book-closed; the host clears the open book and its per-book state. A later ready (renderer reclaimed in the background, Retry) reopens nothing. While no book is open the WebView is hidden from accessibility and its tap / key / word / selection messages are dropped.
  • Keep-awake while a book is open; status bar and safe areas follow the bars without re-laying out the book.
  • Saves: __fol.flush() when the app goes to the background and before a book is closed.
  • Notices over a book can be dismissed (× "Dismiss").

chrome.mjs (shared, no React Native types): readRelocate (everything the chrome displays from relocate is validated: the page renders untrusted content), readWordTap, hidesBarsOnRelocate, createHostMoves, backAction, reopensOnReady, takenWithoutBook, CLOSE_BOOK_JS.

tapZone.mjs (+ .d.mts, test): byte-identical to the web bars branch's copy, so the two PRs merge cleanly.

Reused from the legacy reader: ReaderTopBar (highlights / TOC / settings buttons now optional, back button takes an accessible name), ReaderFooter (prev / next names can be passed in), useReaderBars (reduced motion, and a count as its re-arm trigger; the legacy reader gets reduced motion too), useKeepReaderAwake, useFlushOnBackground. New: useFlushOnLeave.

No native change: expo-keep-awake, expo-status-bar, react-native-safe-area-context are already in the app. The Expo fingerprint does not move.

Strings: reader.chrome.back, .prevPage, .nextPage, .dismiss in packages/shared/src/i18n/en.json (+ both golden fixtures).

Tests

  • chrome.test.ts, tapZone.test.ts, useFlushOnLeave.test.ts, useReaderBars.test.ts: readers incl. hostile payloads, bars rule, back order, close script order (store before book-closed), a closed book not reopened on ready, messages dropped without a book, timer re-armed on a replayed open.
  • Every guard was removed in turn; each removal fails a named test.
  • mobile tsc 0, vitest 1488; shared vitest 755; web locale tests 28; check:v2-reader up to date (no page source edits).
  • Not covered by a test (the screen cannot be rendered under vitest; each stands on a tested helper): endBook on book-closed, the effective flow passed to the zones, the crash count gated on a book being on screen, the accessibility props, the Dismiss button.
  • Not run: no device or emulator.

Rollback plan

Revert the PR. No server, schema or stored-data change; the route is dev-only.

Docs: updated (apps/mobile/src/lib/v2reader/README.md "Reader chrome")

Notes

What the review changed (/code-review high, 10 findings; 8 fixed, 1 documented, 1 skipped)

  • "Back to the list" only hid the book: it stayed loaded, focused and reopenable. A renderer reload pulled the reader back into a book they had closed; TalkBack and a hardware key still reached it.
  • relocate fields went unvalidated into React children and styles (a non-string toc would crash the screen).
  • The host guessed "the reader moved" from CFI differences; the page now says so (relocate.moved, feat(reading): V2 reader page protocol for bars: tap position, direction, keys, flush (#1263) #1287).
  • Skipped: optional props on the legacy ReaderTopBar (deleted at R8).

Known and left

Device checks owed by the owner

  1. Edge taps at the very edge turn pages, portrait and landscape with a cutout (tap.x and the WebView width are in the same units).
  2. Text tap: edge turns, centre toggles; a link tap never turns.
  3. A CBZ or PDF turns at the edges even with the flow setting on scrolled.
  4. A right-to-left book turns by its left edge and ArrowLeft.
  5. Select text, tap an edge: the selection clears, no turn.
  6. Hardware keyboard: one turn per press, Escape shows the bars.
  7. A swipe or a scroll hides the bars; rotation, a font change, a sync jump, the ‹ › buttons do not.
  8. Bars hide 3 s after open, also after reopening from the list; instant with "Remove animations".
  9. Status bar follows the bars; no re-layout; nothing under the cutout or the gesture bar.
  10. Back: offer → list → leave.
  11. After closing a book: TalkBack does not reach it, keys do not turn it, the flow toggle works after a PDF, backgrounding never reopens it.
  12. Turn a page, press back within 400 ms: the place holds. Turn, Home within 400 ms, kill, reopen: holds.
  13. Full disk: the notice over the book can be dismissed.
  14. Screen stays awake in a book, sleeps on the list.
  15. TalkBack names back, previous page, next page, Dismiss.

🤖 Generated with Claude Code

mrviduus and others added 6 commits November 12, 2026 14:12
…ave on leave

V2 Android host (#1263 Android half, #1264):
- top/bottom bars over the WebView (legacy ReaderTopBar/ReaderFooter/useReaderBars), auto-hide,
  centre tap toggles, scrolling hides in scrolled flow; reduced motion skips the animation
- tap zones in paginated flow (chrome.mjs, pure, for web too): word/link/selection-dismissing tap
  never turns the page; the page's tap has no place, so the host pairs it with the touch
- hardware back: offer, then the book (to the list), then the route; keep-awake while a book is on
  screen; status bar follows the bars; stable top inset
- __fol.flush() injected on background, unmount and close (page does not have it yet)
- reader.chrome.* strings, golden fixtures

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ared tapZone

Merges origin/v2/1263-page-chrome-protocol. Drops the touch pairing and long-press guess:
zones from tap.x (readTap) + WebView width, rtl from opened.dir, tapZone.mjs taken verbatim
from v2/1263-web-bars. Host decisions (hidesBarsOnRelocate, backAction) added to the page
branch's chrome.mjs. flush() now exists in the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bars, Escape shows bars

Tap zones use the same latest direction as keys (opened.dir, then relocate.dir).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… relocate, bars re-arm

- closing a book: list at once, CLOSE_BOOK_JS (flush, then book-closed), then the book ends as the
  open one; a ready reopens only a book on screen; per-book state reset; hidden page out of the
  accessibility tree, its tap/key/word/selection dropped
- readRelocate (bounded numbers, capped toc, dir) for everything the chrome shows
- one effective flow for tap zones and scroll-hide; host goTo/bar moves never hide the bars
- bars auto-hide re-armed on every open (count trigger)
- dismissible notices over a book; unmount flush documented as best effort

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…taps act as taps until #1269

- readRelocate gains strict moved; hidesBarsOnRelocate is one rule for both flows (moved, not
  under a bar button); the host's CFI-difference guess and the re-layout known item are gone
- createHostMoves kept for bar buttons and the chosen goTo only (sync goTo is moved: false)
- readWordTap: a word acts as a tap at its rect's centre until the word popup (#1269)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mrviduus mrviduus added the reading Area: reading (reading screen, reading position, offline) label Nov 12, 2026
Base automatically changed from v2/1263-page-chrome-protocol to main November 13, 2026 01:52
@mrviduus
mrviduus enabled auto-merge November 13, 2026 01:52
@mrviduus
mrviduus merged commit ca0445c into main Nov 13, 2026
25 of 26 checks passed
@mrviduus
mrviduus deleted the v2/1263-1264-android-chrome branch November 13, 2026 02:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

reading Area: reading (reading screen, reading position, offline)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

V2 reader Android host: back/exit, keep-awake, safe areas, save on background

1 participant