Skip to content

feat(mobile): native reader v1 — pure-RN selection + highlights (behind a toggle) - #26

Open
conoremclaughlin wants to merge 5 commits into
wren/feat/ios-portfrom
wren/feat/native-reader-selection
Open

feat(mobile): native reader v1 — pure-RN selection + highlights (behind a toggle)#26
conoremclaughlin wants to merge 5 commits into
wren/feat/ios-portfrom
wren/feat/native-reader-selection

Conversation

@conoremclaughlin

Copy link
Copy Markdown
Owner

First real step of the WebView → native migration. Spec (source of truth): ink://inkread/specs/native-reader.

The insight

Text flow was never the blocker — RN <Text> + core's tested paginate() handle wrapping/justification/pagination. The only gap was turning a selection gesture into [start,end) character offsets, which RN core's <Text selectable> can't give (react/react-native#23147).

The fix (researched + validated)

Adopt @bsky.app/react-native-uitextview (Bluesky) — native iOS UITextView selection returning exact {start,end} offsets; New-Architecture-native, tested against RN 0.86, in production in Bluesky. Research ruled out the Astrocoders selectable-text family (archived/Paper-only) and the read-only-TextInput route (can't render sub-range highlights).

Both open questions validated on the iPhone 17 sim before wiring anything in:

  • Offsets exact — selecting "recondite" reported start=83,end=92, slice(83,92)==="recondite".
  • Highlight backgroundColor paints on a nested run → highlights render natively, no overlay hack.

What's in this PR

  • NativeReaderView.tsx — each paragraph is a native UITextView; highlights render via the shared @inkread/core segmentChapterRuns (same offset model as the WebView, so a highlight lands on identical characters); selection reported as chapter-relative offsets (paragraph base + local). Serif to match the classic reader.
  • ReaderScreenreaderEngine toggle at Aa › Reader · beta (iOS-only). In native mode the shared selection bar / color picker / tap-a-highlight all work; Extend stays WebView-only for now.
  • preferencesreaderEngine: 'webview' | 'native', default webview.
  • @bsky.app/react-native-uitextview@2.5.0 added (autolinks via pod install; ios/ is gitignored/regenerated, so the dep in package.json is the source of truth).

Verified end-to-end on the sim

Toggle → native reader renders (serif, justified, highlights paint) → long-press selects → color bar → created a yellow highlight on "principles", which persisted across an app reload. Screenshots in the thread.

Scope / still on the WebView (default) until parity

Paged mode, cross-page extend, TTS sentence marks, precise position persistence. The WebView remains the default engine — this is opt-in and can't regress the shipped reader.

Tests

core 90, mobile 31, type-check clean (core/mobile/web). The pure offset model (segmentChapterRuns) is unit-tested; the native view is device-verified (components aren't node-testable in this setup).

🤖 Generated with Claude Code

Wren and others added 5 commits July 29, 2026 18:44
…d a toggle

First real step of the WebView→native migration (spec: ink://inkread/specs/native-reader).
Text flow was never the blocker — the gap was gesture→character-offsets, which
RN core's <Text> can't give. Adopt @bsky.app/react-native-uitextview: native iOS
UITextView selection that returns exact {start,end} offsets, New-Arch/RN 0.86,
validated on-device (offsets exact; highlight backgroundColor paints on nested runs).

- NativeReaderView.tsx: renders each paragraph as a UITextView, highlights via the
  shared core `segmentChapterRuns` (same offset model as the WebView), reports
  selection as chapter-relative offsets (paragraph base + local offset). Serif to
  match the classic reader.
- ReaderScreen: `readerEngine` toggle (Aa › Reader · beta, iOS-only). In native
  mode the shared selection bar / color picker / tap-a-highlight all work; Extend
  stays WebView-only for now.
- preferences: `readerEngine: 'webview' | 'native'` (default webview).

Scope: scroll mode + native selection → create/tap highlight, verified end-to-end
on the iPhone 17 sim (select "principles" → yellow highlight, persists across
reload). Still on the WebView until parity: paged mode, cross-page extend, TTS
sentence marks, precise position persistence. WebView remains the default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…chapter nav

Ports more WebView reader behaviour to the native (scroll-mode) reader:
- Reading position: report the top-visible paragraph's chapter offset on scroll
  (shared `recordOffset` with the WebView path — same furthest-mark + persist
  logic), and restore to the saved offset on open (keyed per chapter so each
  chapter remounts and restores fresh, mirroring the WebView's per-chapter reload).
- Chrome: shown on open and when scrolling up, hidden when scrolling down into
  the text — a scroll-direction model, since a tap toggle would fight the native
  selection gesture (and RNGH isn't a dep). Verified chrome shows on open.
- Chapter nav: Prev/Next move between chapters in native mode (paged page-turns
  are WebView-only for now), with correct boundary disabling.

Still to port: paged mode, native cross-page extend. TTS marks deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Paged reading in the pure-RN reader. Instead of clipping + translating a
selectable view (which breaks native selection), the chapter sits in a
non-scrollable ScrollView paged by programmatic scrollTo — so selection behaves
exactly as in scroll mode while turns move a screenful. Body is one UITextView
with a uniform lineHeight so a whole-line page step snaps cleanly; edge taps
(left/right) turn pages and flow into the neighbouring chapter at the ends.

Verified on the sim: paged render, native selection + highlight bar, edge-tap
page turn flowing into the next chapter.

Known v1 compromises (follow-up): paragraphs join on '\n' (tight spacing, offset
model intact) so no blank-line gap yet; chapter title not shown on page 1 (it's
in the bottom bar); no center-tap immersive chrome-hide in paged (chrome shows on
open). Scroll mode keeps proper paragraph spacing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports the last interactive WebView feature (bar the deferred TTS marks): extend.
The WebView flow is anchor → flip pages → tap the end word; the native flow is
anchor → make a *second* native selection at the far end, and the highlight spans
from the anchor to it (handleNativeSelection folds the second selection into the
extend range — no offset probing, works in scroll and paged). The Extend button
now shows in native mode; the extend bar drops the WebView-only page arrows and
shows a native hint ("Select where the highlight ends, then pick a colour").

Verified on the sim: select "some" → Extend → select "Machine" → green highlight
spans "some … Time Machine" across lines, overlapping an existing highlight.

Native reader now has functional parity with the WebView except TTS sentence
marks (deferred). Scroll + paged, selection, create/render/tap/note highlights,
position persistence, chrome, chapter nav, and extend all run natively.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Paged mode rendered paragraphs on a single '\n' (no blank-line gap — walls of
text for real prose). Now it joins them with '\n\n' for a proper gap, and maps
rendered selection offsets back to the model: paragraph i sits i chars later in
the rendered text, so model = rendered - (its paragraph index). Extracted the
mapping to `lib/pagedOffsets.ts` with unit tests (mobile 31 → 36).

Verified on the sim: paged mode shows paragraph gaps and every highlight
(including a cross-line native-extend one) renders at the correct words.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@conoremclaughlin

Copy link
Copy Markdown
Owner Author

Full port to native — functional parity reached (device-verified)

Per Conor's direction (don't merge yet — port everything first, extend last), the native reader now matches the WebView except TTS sentence marks (deferred). All verified on the iPhone 17 sim.

Added since the v1 commit:

  • Reading position — report top-visible offset on scroll (shared recordOffset with the WebView) + restore on open (keyed per chapter). Furthest/resume works.
  • Chrome — shown on open, hidden on scroll-down, shown on scroll-up (a tap toggle would fight the native selection gesture; RNGH isn't a dep).
  • Chapter nav — Prev/Next between chapters with correct boundary disabling.
  • Paged mode — the hard one. A non-scrollable ScrollView paged by scrollTo (so native selection survives — no clip/translate). Edge-tap turns flow into the neighbouring chapter. Paragraph spacing via '\n\n' with offsets mapped back (lib/pagedOffsets.ts, unit-tested). Verified: paged render + selection + highlight + page/chapter turn + gaps.
  • Native extend — anchor selection → a second native selection at the far end; the highlight spans both (no offset probing; works scroll + paged). Verified spanning multiple lines and overlapping an existing highlight.

Tests: core 90, mobile 36 (+5 for the paged offset mapping), type-check clean.

Known follow-ups (not blockers)

  • Paged: chapter title on page 1 (currently in the bottom bar); center-tap immersive chrome-hide (needs RNGH).
  • TTS sentence marks in native (deferred — separate from the per-character-voices idea).
  • iOS-only (lib falls back to base Text on Android); Android native selection later.

Spec (source of truth) updated to v3: ink://inkread/specs/native-reader.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant