From 54c601a644e694bb00da5f275b733963365892b5 Mon Sep 17 00:00:00 2001 From: Alex Rawlings Date: Mon, 29 Jun 2026 15:26:23 -0600 Subject: [PATCH 1/5] Treat verse 0 as an ordinary verse; remove liveScrRef stickiness guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A chapter's pre-verse-1 superscription is a real, focusable verse-0 segment, but `liveScrRef` held a stickiness guard that swallowed any same-chapter `verseNum: 0` reference naming the verse already shown. That guard was added (b9dec18, "fix verse-0 echo nav") before verse 0 was a parsed verse, to absorb the host's spurious post-verse-nav chapter echo. It could not distinguish that echo from a genuine external `<` (previous-verse) from verse 1 — both are same-chapter, verse 0, and markerless on the global selector path — so it also ate the intentional `<`, leaving the extension stuck on verse 1 instead of moving to the superscription. Verified in a live session (probe logging liveScrRef/rawScrRef) that the host no longer emits the spurious echo: exactly one delivery per navigation, never an unsolicited trailing verse 0. With the echo gone the guard defended against nothing, so remove it. Verse 0 now passes through verbatim; the host's `<` from verse 1 lands on the chapter's superscription (the loader resolves verse 0 to the superscription segment, else to verse 1). The internal-nav marker machinery is retained — it still classifies internal/external navigations for the recenter fade. Only its former role inside the verse-0 guard is gone. Tests: collapse the two sticky-behavior tests into one pass-through regression test; flip the mid-reveal fade test to expect the curtain to re-engage for a verse-0 navigation arriving during fade-in (verse 0 is now an ordinary mid-reveal move). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/InterlinearNavContext.test.tsx | 42 ++++-------- .../components/Interlinearizer.test.tsx | 4 +- src/components/InterlinearNavContext.tsx | 67 ++++++------------- src/components/Interlinearizer.tsx | 10 +-- 4 files changed, 42 insertions(+), 81 deletions(-) diff --git a/src/__tests__/components/InterlinearNavContext.test.tsx b/src/__tests__/components/InterlinearNavContext.test.tsx index 7f6d3218..ef93d0c5 100644 --- a/src/__tests__/components/InterlinearNavContext.test.tsx +++ b/src/__tests__/components/InterlinearNavContext.test.tsx @@ -84,35 +84,17 @@ describe('InterlinearNavContext', () => { expect(result.current.rawScrRef).toEqual(ref); }); - it('keeps the current verse when the host echoes a verse-0 reference for the chapter already shown', () => { - // After a verse navigation the host re-broadcasts the chapter as a separate verse-0 reference. - // Normalizing that to verse 1 would read as a fresh move off the verse the user is on, so a - // verse-0 echo for the same book+chapter must stay sticky on the current verse. + it('passes a same-chapter verse-0 reference through to liveScrRef (host < from verse 1)', () => { + // Verse 0 is a real, focusable verse (a chapter superscription), so a verse-0 reference for the + // chapter already shown passes through verbatim rather than being held on the current verse. This + // is the host's `<` (previous-verse) from verse 1: it must land on the superscription, which the + // loader resolves from verse 0 (or back to verse 1 when the chapter has no verse-0 segment). const { result, setRef, rerender } = renderNavMutable({ book: 'GEN', chapterNum: 3, - verseNum: 7, + verseNum: 1, }); - expect(result.current.liveScrRef).toEqual({ book: 'GEN', chapterNum: 3, verseNum: 7 }); - - act(() => setRef({ book: 'GEN', chapterNum: 3, verseNum: 0 })); - rerender(); - - expect(result.current.liveScrRef).toEqual({ book: 'GEN', chapterNum: 3, verseNum: 7 }); - }); - - it('passes a same-chapter verse-0 echo through when it matches a fresh internal-nav marker', () => { - // Selecting a verse-0 superscription navigates the host to verse 0 of the chapter already shown, - // so the host's echo is shaped exactly like the spurious post-nav chapter echo. A fresh - // internal-nav marker for that verse-0 key marks it as our own deliberate move, so the stickiness - // exception lets it through to the superscription rather than holding the prior verse. - const { result, setRef, rerender } = renderNavMutable({ - book: 'GEN', - chapterNum: 3, - verseNum: 7, - }); - - act(() => result.current.navigate({ book: 'GEN', chapterNum: 3, verseNum: 0 }, 'internal')); + expect(result.current.liveScrRef).toEqual({ book: 'GEN', chapterNum: 3, verseNum: 1 }); act(() => setRef({ book: 'GEN', chapterNum: 3, verseNum: 0 })); rerender(); @@ -542,9 +524,11 @@ describe('InterlinearNavContext', () => { expect(result.current.fadePhase).toBe('out'); }); - it('does not re-engage the curtain for a verse-0 echo during the fade-in', () => { - // The host's chapter re-broadcast (verse 0 for the chapter already shown) is sticky — it - // names the verse currently displayed, so it must not read as a fresh navigation mid-reveal. + it('re-engages the curtain for a verse-0 navigation arriving during the fade-in', () => { + // Verse 0 is an ordinary verse (a chapter superscription), so a verse-0 reference naming a + // different verse than the one shown is a real mid-reveal navigation — e.g. the host's `<` from + // verse 1 landing on the superscription. It re-engages the curtain like any other external move + // arriving while the new book is still fading in, rather than fading the fresh content twice. const { result, rerender, setRef } = renderNavMutable({ book: 'GEN', chapterNum: 1, @@ -559,7 +543,7 @@ describe('InterlinearNavContext', () => { act(() => setRef({ book: 'ZEP', chapterNum: 3, verseNum: 0 })); rerender(); - expect(result.current.fadePhase).toBe('in'); + expect(result.current.fadePhase).toBe('out'); }); it('clears the pending fade-in timer on unmount', () => { diff --git a/src/__tests__/components/Interlinearizer.test.tsx b/src/__tests__/components/Interlinearizer.test.tsx index 9a133829..fde0eb24 100644 --- a/src/__tests__/components/Interlinearizer.test.tsx +++ b/src/__tests__/components/Interlinearizer.test.tsx @@ -535,8 +535,8 @@ describe('Interlinearizer', () => { it('writes a verse-0 reference to the host when a verse-0 token is selected', () => { // Selecting a superscription token navigates the host to verse 0 like any other verse; the - // internal-nav marker keeps the host's chapter echo from bouncing the view (the stickiness - // exception in InterlinearNavContext). Default scrRef is GEN 1:1, so this is a real verse change. + // 'internal' origin records a nav marker so the segment window skips the recenter fade for our + // own move. Default scrRef is GEN 1:1, so this is a real verse change. const mockNavigate = jest.fn(); renderInterlinearizer({ book: GEN_SUPERSCRIPTION_BOOK, navigate: mockNavigate }); diff --git a/src/components/InterlinearNavContext.tsx b/src/components/InterlinearNavContext.tsx index 9e6b3845..3d35ba95 100644 --- a/src/components/InterlinearNavContext.tsx +++ b/src/components/InterlinearNavContext.tsx @@ -113,20 +113,14 @@ export interface InterlinearNav { */ rawScrRef: SerializedVerseRef; /** - * The active reference, equal to `rawScrRef` except that a verse-0 reference naming the chapter - * already shown is held sticky on the current verse — unless it matches a fresh internal-nav - * marker (the extension's own move to that chapter's superscription), which passes through. A - * verse-0 reference is otherwise passed through verbatim: when it names a chapter with a verse-0 - * superscription segment, that segment becomes the active verse. Whether a given chapter actually - * has verse-0 content is unknown here (the book is not loaded at this layer), so the loader - * resolves a verse-0 reference with no matching segment back to the chapter's first numbered - * verse before rendering. - * - * The sticky exception exists because, after a verse navigation, the host re-broadcasts the - * chapter as a separate `verseNum: 0` reference (an echo of the current location, not a real - * move); treating that as a jump to verse 0 would yank the view off the verse the user is on. The - * marker carve-out distinguishes that spurious echo from a deliberate verse-0 navigation the - * extension itself just made (which is shaped identically). + * The active reference. Equal to `rawScrRef`, with object identity reused across the host's + * value-equal duplicate deliveries so a re-send reads as no change. Verse 0 is treated as an + * ordinary verse: a `verseNum: 0` reference passes through verbatim, so the host's `<` + * (previous-verse) from verse 1 lands on the chapter's superscription. When it names a chapter + * with a verse-0 superscription segment, that segment becomes the active verse. Whether a given + * chapter actually has verse-0 content is unknown here (the book is not loaded at this layer), so + * the loader resolves a verse-0 reference with no matching segment back to the chapter's first + * numbered verse before rendering. */ liveScrRef: SerializedVerseRef; /** @@ -226,8 +220,10 @@ export function InterlinearNavProvider({ stableRawScrRefRef.current = rawScrRef; /** - * The last committed {@link liveScrRef}, mirrored so the verse-0 stickiness below can compare the - * incoming `rawScrRef` against the verse currently shown. + * The last committed {@link liveScrRef}, mirrored so the duplicate-delivery guard below can reuse + * the previously committed object identity when the host re-sends a value-equal reference, and so + * the render-phase mid-reveal guard can compare the incoming reference against the verse last + * shown. */ const liveScrRefRef = useRef(rawScrRef); @@ -236,40 +232,21 @@ export function InterlinearNavProvider({ * `Date.now()` stamp. `navigate(ref, 'internal')` records `verseKey(ref)`; `consumeInternalNav` * removes it on match. Keyed (not a single value) so that rapid successive clicks both stay * pending and neither host delivery is misread as external. The stamp gives each marker a TTL - * ({@link INTERNAL_NAV_TTL_MS} — see its doc for why stranded markers must expire), honored by ALL - * readers: `consumeInternalNav` (which also evicts expired markers), the verse-0 stickiness - * exception below, and the render-phase mid-reveal guard (both pure reads — no eviction during - * render). + * ({@link INTERNAL_NAV_TTL_MS} — see its doc for why stranded markers must expire), honored by + * both readers: `consumeInternalNav` (which also evicts expired markers) and the render-phase + * mid-reveal guard (a pure read — no eviction during render). */ const pendingInternalNavRef = useRef>(new Map()); - // After a verse navigation the host re-broadcasts the *chapter* to the scroll group as a separate - // `verseNum: 0` reference (an echo of the current location, not a real move). Treating that as a - // jump to verse 0 would yank the views off the verse the user is actually on, so a verse-0 - // reference that names the book+chapter already shown is held sticky: keep the current `liveScrRef` - // (its real verse). - // - // The exception is a verse-0 reference the extension itself just navigated to — selecting a - // chapter's verse-0 superscription segment writes `verseNum: 0` for the chapter already shown, - // which is shaped exactly like the spurious echo. A fresh internal-nav marker for that verse-0 key - // distinguishes the two: when one exists, this is our own deliberate move to the superscription, so - // it passes through (and `consumeInternalNav` clears the marker downstream). A pure read here — no - // eviction during render, matching the render-phase mid-reveal guard. - // - // Every other reference — including a verse-0 reference for a *different* chapter (a genuine chapter - // jump, which the loader resolves to the verse-0 superscription when one exists, else to verse 1) — - // passes through verbatim. The duplicate-delivery guard reuses the previously committed object when - // a re-send is value-equal, so a duplicate never reads as a fresh navigation. + // Verse 0 is a real, focusable verse (a chapter's pre-verse-1 superscription content; see + // `usjBookExtractor`), so a `verseNum: 0` reference is passed through verbatim like any other verse + // — including the host's `<` (previous-verse) from verse 1, which lands on the chapter's + // superscription. When the chapter has no verse-0 segment, the loader resolves verse 0 to the + // chapter's first numbered verse before rendering. The duplicate-delivery guard reuses the + // previously committed object when a re-send is value-equal, so a duplicate never reads as a fresh + // navigation. const liveScrRef = useMemo(() => { const prev = liveScrRefRef.current; - if ( - rawScrRef.verseNum === 0 && - rawScrRef.book === prev.book && - rawScrRef.chapterNum === prev.chapterNum && - !isInternalNavMarkerFresh(pendingInternalNavRef.current.get(verseKey(rawScrRef))) - ) { - return prev; - } return areScrRefsEqual(rawScrRef, prev) ? prev : rawScrRef; }, [rawScrRef]); /** diff --git a/src/components/Interlinearizer.tsx b/src/components/Interlinearizer.tsx index b5c7338e..f4a2b8ca 100644 --- a/src/components/Interlinearizer.tsx +++ b/src/components/Interlinearizer.tsx @@ -220,9 +220,9 @@ function InterlinearizerInner({ * reference. (The loader's `viewScrRef` freeze normally keeps the two in sync, so this guards a * transient.) * - * A verse-0 segment (a chapter superscription) navigates like any other: the write records an - * internal-nav marker so the host's chapter echo is recognized as our own move rather than - * bouncing the view back. (See the verse-0 stickiness exception in `InterlinearNavContext`.) + * A verse-0 segment (a chapter superscription) navigates like any other verse: the `'internal'` + * origin records a nav marker so the segment window recognizes the host's echo as our own move + * and skips the recenter fade (the target is already on screen). * * @param tokenRef - The word-token ref to focus. */ @@ -245,8 +245,8 @@ function InterlinearizerInner({ * Updates the active scripture reference (when the verse actually changed) and, when a specific * token was clicked, focuses that token. Skips the write to PAPI when the clicked verse matches * the current one, avoiding a gratuitous echo round-trip. A verse-0 segment (a chapter - * superscription) writes like any other verse — the internal-nav marker keeps the host's chapter - * echo from bouncing the view (see the verse-0 stickiness exception in `InterlinearNavContext`). + * superscription) writes like any other verse — the `'internal'` origin records a nav marker so + * the segment window skips the recenter fade for our own move. * * @param ref - The verse coordinate that was selected. * @param tokenRef - The token that was clicked; omitted when the whole segment was selected. From f223dbcc1fde4f14c91aa1d99f1a244a8018c550 Mon Sep 17 00:00:00 2001 From: Alex Rawlings Date: Thu, 2 Jul 2026 11:18:25 -0600 Subject: [PATCH 2/5] Fix inaccurate doc --- src/components/InterlinearNavContext.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/InterlinearNavContext.tsx b/src/components/InterlinearNavContext.tsx index 3d35ba95..5a4e8023 100644 --- a/src/components/InterlinearNavContext.tsx +++ b/src/components/InterlinearNavContext.tsx @@ -259,10 +259,11 @@ export function InterlinearNavProvider({ const navigate = useCallback( (newScrRef: SerializedVerseRef, origin: NavOrigin = 'external') => { - // Invariant: a marker write is ALWAYS paired with the `setScrRef` below. The `liveScrRef` memo - // reads this marker but lists only `[rawScrRef]` as a dependency, relying on every marker write - // also pushing a new ref through the host (which re-runs the memo). Never set a marker here - // without the accompanying `setScrRef`, or the memo could read a stale marker. + // Invariant: a marker write is ALWAYS paired with the `setScrRef` below. The marker readers + // that run during render — the render-phase mid-reveal guard, which lists only `[rawScrRef]` + // in the reactive path that reaches it — rely on every marker write also pushing a new ref + // through the host, so the render that reads the marker is the one that sees it fresh. Never + // set a marker here without the accompanying `setScrRef`, or a reader could see a stale marker. if (origin === 'internal') pendingInternalNavRef.current.set(verseKey(newScrRef), Date.now()); setScrRef(newScrRef); }, From e51905ec8b07b1ab108a215312bba4bfa8f017df Mon Sep 17 00:00:00 2001 From: Alex Rawlings Date: Thu, 2 Jul 2026 11:52:06 -0600 Subject: [PATCH 3/5] Get rid of unnecessary memoization of liveScrRef --- src/components/InterlinearNavContext.tsx | 25 ++++++++++-------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/components/InterlinearNavContext.tsx b/src/components/InterlinearNavContext.tsx index 5a4e8023..134f0470 100644 --- a/src/components/InterlinearNavContext.tsx +++ b/src/components/InterlinearNavContext.tsx @@ -220,10 +220,8 @@ export function InterlinearNavProvider({ stableRawScrRefRef.current = rawScrRef; /** - * The last committed {@link liveScrRef}, mirrored so the duplicate-delivery guard below can reuse - * the previously committed object identity when the host re-sends a value-equal reference, and so - * the render-phase mid-reveal guard can compare the incoming reference against the verse last - * shown. + * The last committed {@link liveScrRef}, mirrored so the render-phase mid-reveal guard can compare + * the incoming reference against the verse last shown. */ const liveScrRefRef = useRef(rawScrRef); @@ -238,17 +236,14 @@ export function InterlinearNavProvider({ */ const pendingInternalNavRef = useRef>(new Map()); - // Verse 0 is a real, focusable verse (a chapter's pre-verse-1 superscription content; see - // `usjBookExtractor`), so a `verseNum: 0` reference is passed through verbatim like any other verse - // — including the host's `<` (previous-verse) from verse 1, which lands on the chapter's - // superscription. When the chapter has no verse-0 segment, the loader resolves verse 0 to the - // chapter's first numbered verse before rendering. The duplicate-delivery guard reuses the - // previously committed object when a re-send is value-equal, so a duplicate never reads as a fresh - // navigation. - const liveScrRef = useMemo(() => { - const prev = liveScrRefRef.current; - return areScrRefsEqual(rawScrRef, prev) ? prev : rawScrRef; - }, [rawScrRef]); + // `liveScrRef` is the active reference the views recenter on. With verse-0 stickiness removed it no + // longer transforms `rawScrRef` — every reference (verse 0 included) passes through verbatim, so + // the host's `<` (previous-verse) from verse 1 lands on the chapter's superscription, and the + // loader resolves a verse-0 reference with no matching segment to the chapter's first numbered + // verse before rendering. The alias keeps the intent-revealing name and marks the seam where any + // future raw→active mapping would live; `rawScrRef` already reuses the previously committed object + // on a value-equal re-send, so a duplicate delivery never reads as a fresh navigation. + const liveScrRef = rawScrRef; /** * The {@link liveScrRef} committed on the previous render, captured before the mirror update below * overwrites it, so the mid-reveal navigation guard further down can compare the incoming From b98d91cb3a271fb23b8e3ed6649f4d3ecf6a525c Mon Sep 17 00:00:00 2001 From: Alex Rawlings Date: Mon, 6 Jul 2026 09:06:19 -0600 Subject: [PATCH 4/5] Resolve out-of-range verse refs to the nearest preceding segment (#147) The host's next-verse button emits verseNum + 1 without clamping, so bumping forward from a chapter's last verse delivered a verse past the chapter's end; the loader only resolved verse-0 refs, and the segment window's anchor fallback jumped to the start of the book. Any unmatched verse now resolves to the nearest preceding segment start in its chapter, so the view holds at the chapter's end (and a verse inside a merged multi-verse segment resolves to its owning segment). --- .../components/InterlinearizerLoader.test.tsx | 96 +++++++++++++++++++ src/components/InterlinearNavContext.tsx | 13 +-- src/components/Interlinearizer.tsx | 13 ++- src/components/InterlinearizerLoader.tsx | 37 +++++-- 4 files changed, 138 insertions(+), 21 deletions(-) diff --git a/src/__tests__/components/InterlinearizerLoader.test.tsx b/src/__tests__/components/InterlinearizerLoader.test.tsx index 54958e97..a2f30210 100644 --- a/src/__tests__/components/InterlinearizerLoader.test.tsx +++ b/src/__tests__/components/InterlinearizerLoader.test.tsx @@ -527,6 +527,102 @@ describe('InterlinearizerLoader', () => { }); }); + it('resolves an out-of-range verse to the last segment of the same chapter', async () => { + // The host's next-verse button emits verseNum + 1 without clamping, so bumping forward from + // the last verse of a chapter delivers a verse past the chapter's end. The loader resolves it + // to the chapter's last segment rather than letting the views fall back to the start of the + // book. Chapter 4 exists so the test proves resolution stays within the requested chapter. + const multiSegmentBook: Book = { + id: 'GEN', + bookRef: 'GEN', + textVersion: 'v1', + segments: [ + { + id: 'GEN 3:1', + startRef: { book: 'GEN', chapter: 3, verse: 1 }, + endRef: { book: 'GEN', chapter: 3, verse: 1 }, + baselineText: 'First verse.', + tokens: [], + }, + { + id: 'GEN 3:2', + startRef: { book: 'GEN', chapter: 3, verse: 2 }, + endRef: { book: 'GEN', chapter: 3, verse: 2 }, + baselineText: 'Last verse of the chapter.', + tokens: [], + }, + { + id: 'GEN 4:1', + startRef: { book: 'GEN', chapter: 4, verse: 1 }, + endRef: { book: 'GEN', chapter: 4, verse: 1 }, + baselineText: 'Next chapter.', + tokens: [], + }, + ], + }; + mockBookData({ book: multiSegmentBook }); + + await act(async () => { + renderLoader({ + useWebViewScrollGroupScrRef: makeScrollGroupHook({ + book: 'GEN', + chapterNum: 3, + verseNum: 3, + }), + }); + }); + + expect(capturedInterlinearizerProps?.scrRef).toEqual({ + book: 'GEN', + chapterNum: 3, + verseNum: 2, + }); + }); + + it('resolves a mid-segment verse to the owning segment start', async () => { + // A merged segment can span several verses; navigating to a verse inside the span has no + // segment starting at that verse, so the loader resolves to the nearest preceding segment + // start — the segment that contains the verse, never a later segment in the chapter. + const mergedSegmentBook: Book = { + id: 'GEN', + bookRef: 'GEN', + textVersion: 'v1', + segments: [ + { + id: 'GEN 3:1', + startRef: { book: 'GEN', chapter: 3, verse: 1 }, + endRef: { book: 'GEN', chapter: 3, verse: 2 }, + baselineText: 'Two verses merged into one segment.', + tokens: [], + }, + { + id: 'GEN 3:3', + startRef: { book: 'GEN', chapter: 3, verse: 3 }, + endRef: { book: 'GEN', chapter: 3, verse: 3 }, + baselineText: 'Verse after the merged segment.', + tokens: [], + }, + ], + }; + mockBookData({ book: mergedSegmentBook }); + + await act(async () => { + renderLoader({ + useWebViewScrollGroupScrRef: makeScrollGroupHook({ + book: 'GEN', + chapterNum: 3, + verseNum: 2, + }), + }); + }); + + expect(capturedInterlinearizerProps?.scrRef).toEqual({ + book: 'GEN', + chapterNum: 3, + verseNum: 1, + }); + }); + it('leaves a verse-0 reference untouched while the book is still loading', async () => { // With no book loaded yet, the verse-0 resolution has nothing to consult, so the loader shows // the loading placeholder and does not render the interlinearizer. diff --git a/src/components/InterlinearNavContext.tsx b/src/components/InterlinearNavContext.tsx index 134f0470..20f2fcc6 100644 --- a/src/components/InterlinearNavContext.tsx +++ b/src/components/InterlinearNavContext.tsx @@ -117,10 +117,11 @@ export interface InterlinearNav { * value-equal duplicate deliveries so a re-send reads as no change. Verse 0 is treated as an * ordinary verse: a `verseNum: 0` reference passes through verbatim, so the host's `<` * (previous-verse) from verse 1 lands on the chapter's superscription. When it names a chapter - * with a verse-0 superscription segment, that segment becomes the active verse. Whether a given - * chapter actually has verse-0 content is unknown here (the book is not loaded at this layer), so - * the loader resolves a verse-0 reference with no matching segment back to the chapter's first - * numbered verse before rendering. + * with a verse-0 superscription segment, that segment becomes the active verse. Which verses + * actually have segments is unknown here (the book is not loaded at this layer), so the loader + * resolves a reference with no matching segment before rendering: verse 0 falls back to the + * chapter's first numbered verse, any other unmatched verse to the nearest preceding segment + * start in its chapter. */ liveScrRef: SerializedVerseRef; /** @@ -239,8 +240,8 @@ export function InterlinearNavProvider({ // `liveScrRef` is the active reference the views recenter on. With verse-0 stickiness removed it no // longer transforms `rawScrRef` — every reference (verse 0 included) passes through verbatim, so // the host's `<` (previous-verse) from verse 1 lands on the chapter's superscription, and the - // loader resolves a verse-0 reference with no matching segment to the chapter's first numbered - // verse before rendering. The alias keeps the intent-revealing name and marks the seam where any + // loader resolves a reference with no matching segment (verse 0, or an unclamped host verse bump + // past the chapter's end) before rendering. The alias keeps the intent-revealing name and marks the seam where any // future raw→active mapping would live; `rawScrRef` already reuses the previously committed object // on a value-equal re-send, so a duplicate delivery never reads as a fresh navigation. const liveScrRef = rawScrRef; diff --git a/src/components/Interlinearizer.tsx b/src/components/Interlinearizer.tsx index f4a2b8ca..5bf1c6b7 100644 --- a/src/components/Interlinearizer.tsx +++ b/src/components/Interlinearizer.tsx @@ -35,11 +35,14 @@ type InterlinearizerProps = Readonly<{ /** When true, the horizontal token strip is shown above the segment list. */ continuousScroll: boolean; /** - * Current scripture reference used to highlight the active verse. Always names a verse that has a - * segment in `book`: the loader keeps `verseNum: 0` when the active chapter has a verse-0 - * superscription segment (so the superscription becomes the active verse) and otherwise resolves - * a whole-chapter (verse 0) selection to the chapter's first numbered verse. So this is normally - * verse >= 1, and is verse 0 only when a matching verse-0 segment exists. + * Current scripture reference used to highlight the active verse. Always names a verse some + * segment in `book` starts at, provided the referenced chapter has segments: the loader keeps + * `verseNum: 0` when the active chapter has a verse-0 superscription segment (so the + * superscription becomes the active verse), resolves a whole-chapter (verse 0) selection to the + * chapter's first numbered verse, and resolves any other unmatched verse — the host's next-verse + * button past the chapter's end, or a verse inside a multi-verse segment — to the nearest + * preceding segment start in the chapter. So this is normally verse >= 1, and is verse 0 only + * when a matching verse-0 segment exists. */ scrRef: SerializedVerseRef; /** diff --git a/src/components/InterlinearizerLoader.tsx b/src/components/InterlinearizerLoader.tsx index 0228b293..644c1869 100644 --- a/src/components/InterlinearizerLoader.tsx +++ b/src/components/InterlinearizerLoader.tsx @@ -7,6 +7,7 @@ import papi, { logger } from '@papi/frontend'; import { useData, useSetting } from '@papi/frontend/react'; import { TabToolbar } from 'platform-bible-react'; import type { SelectMenuItemHandler } from 'platform-bible-react'; +import type { ScriptureRef } from 'interlinearizer'; import { isPlatformError } from 'platform-bible-utils'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useDraftProject from '../hooks/useDraftProject'; @@ -21,7 +22,7 @@ import { WipeModal, type WipeScope } from './modals/WipeModal'; import ScriptureNavControls from './controls/ScriptureNavControls'; import { InterlinearNavProvider, useInterlinearNav } from './InterlinearNavContext'; import { RECENTER_FADE_TRANSITION_STYLE } from './recenter-fade'; -import { isSameVerse } from '../utils/verse-ref'; +import { isSameVerse, toSerializedVerseRef } from '../utils/verse-ref'; /** Host-injected callback to update this WebView's definition (used to toggle the tab title). */ type UpdateWebViewDefinition = WebViewProps['updateWebViewDefinition']; @@ -243,16 +244,32 @@ function InterlinearizerLoaderInner({ scrRef, }); - // The active reference handed to the interlinearizer. The host emits `verseNum: 0` both for a - // chapter's verse-0 superscription (which has its own segment) and for a plain whole-chapter - // selection (which does not). Keep verse 0 when the loaded book actually has a verse-0 segment for - // that chapter — so a Psalm superscription becomes the active verse — and otherwise fall back to - // the chapter's first numbered verse, so an ordinary chapter selection still lands on verse 1 - // rather than leaving nothing highlighted. Non-verse-0 references pass through unchanged. + // The active reference handed to the interlinearizer, resolved so it always names a verse some + // segment starts at (when the requested chapter has segments at all). A reference whose verse is + // a segment start passes through unchanged. Otherwise: + // + // - Verse 0 with no verse-0 segment is a plain whole-chapter selection (the host emits + // `verseNum: 0` for those as well as for superscriptions), so it falls back to the chapter's + // first numbered verse rather than leaving nothing highlighted. + // - Any other unmatched verse resolves to the nearest preceding segment start in the same + // chapter. This covers the host's next-verse button, which emits `verseNum + 1` without + // clamping — bumping forward from a chapter's last verse delivers a verse past the chapter's + // end, which must land on the chapter's last segment, not fall through to the views' anchor + // fallback (the start of the book). It also covers verses inside a multi-verse segment, which + // resolve to the segment that contains them. + // - A reference to a chapter with no segments (including any book mismatch during a cross-book + // swap) passes through unchanged; the views' own fallbacks handle that transient. const activeScrRef = useMemo(() => { - if (scrRef.verseNum !== 0 || !book) return scrRef; - const hasVerseZero = book.segments.some((segment) => isSameVerse(segment.startRef, scrRef)); - return hasVerseZero ? scrRef : { ...scrRef, verseNum: 1 }; + if (!book) return scrRef; + if (book.segments.some((segment) => isSameVerse(segment.startRef, scrRef))) return scrRef; + if (scrRef.verseNum === 0) return { ...scrRef, verseNum: 1 }; + let preceding: ScriptureRef | undefined; + book.segments.forEach(({ startRef }) => { + if (startRef.book !== scrRef.book || startRef.chapter !== scrRef.chapterNum) return; + if (startRef.verse > scrRef.verseNum) return; + if (!preceding || startRef.verse > preceding.verse) preceding = startRef; + }); + return preceding ? toSerializedVerseRef(preceding) : scrRef; }, [scrRef, book]); const hasError = !!bookError || !!tokenizeError; From d3a8d2f14e68d2067c21d26dd0f5550061390a3f Mon Sep 17 00:00:00 2001 From: Alex Rawlings Date: Mon, 6 Jul 2026 09:16:46 -0600 Subject: [PATCH 5/5] Merge the rawScrRef/liveScrRef pair into a single scrRef MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With verse-0 stickiness gone the two refs held the same object at every read, so one ref now serves both the duplicate-delivery identity guard and the mid-reveal prev comparison. The loader's activeScrRef owns the only raw→active mapping, which needs the loaded book anyway. --- .../components/InterlinearNavContext.test.tsx | 42 +++---- .../components/InterlinearizerLoader.test.tsx | 2 +- src/components/InterlinearNavContext.tsx | 117 ++++++++---------- src/components/InterlinearizerLoader.tsx | 13 +- 4 files changed, 73 insertions(+), 101 deletions(-) diff --git a/src/__tests__/components/InterlinearNavContext.test.tsx b/src/__tests__/components/InterlinearNavContext.test.tsx index ef93d0c5..ad6aeb07 100644 --- a/src/__tests__/components/InterlinearNavContext.test.tsx +++ b/src/__tests__/components/InterlinearNavContext.test.tsx @@ -29,7 +29,7 @@ function renderNav(hook: () => ScrollGroupTuple) { /** * Renders the nav hook with a scroll-group stub whose reference can be restaged between rerenders, * so a cross-book navigation can be simulated. A fresh object identity is required on each change - * so the provider's `liveScrRef` memo recomputes. + * so the provider adopts it as a new `scrRef`. * * @param initial - The reference reported on the first render. * @returns The render-hook result plus a `setRef` to stage the next reference (call inside `act`, @@ -51,13 +51,13 @@ function renderNavMutable(initial: SerializedVerseRef) { } describe('InterlinearNavContext', () => { - it('exposes the raw reference and scroll-group plumbing verbatim', () => { + it('exposes the host reference and scroll-group plumbing verbatim', () => { const setScrRef = jest.fn(); const setScrollGroupId = jest.fn(); const ref: SerializedVerseRef = { book: 'GEN', chapterNum: 3, verseNum: 4 }; const { result } = renderNav(makeScrollGroupHook(ref, setScrRef, 2, setScrollGroupId)); - expect(result.current.rawScrRef).toEqual(ref); + expect(result.current.scrRef).toEqual(ref); expect(result.current.scrollGroupId).toBe(2); act(() => result.current.navigate({ book: 'MAT', chapterNum: 1, verseNum: 1 })); @@ -67,24 +67,23 @@ describe('InterlinearNavContext', () => { expect(setScrollGroupId).toHaveBeenCalledWith(3); }); - it('passes a verse-level reference through to liveScrRef unchanged', () => { + it('passes a verse-level reference through to scrRef unchanged', () => { const ref: SerializedVerseRef = { book: 'GEN', chapterNum: 3, verseNum: 4 }; const { result } = renderNav(makeScrollGroupHook(ref)); - expect(result.current.liveScrRef).toEqual(ref); + expect(result.current.scrRef).toEqual(ref); }); - it('passes a chapter-level (verse 0) reference through to liveScrRef unchanged', () => { + it('passes a chapter-level (verse 0) reference through to scrRef unchanged', () => { // Verse 0 is a real verse (a Psalm superscription), so it is no longer mapped to verse 1 here. // The loader resolves it to verse 1 only when the loaded book has no verse-0 segment. const ref: SerializedVerseRef = { book: 'GEN', chapterNum: 3, verseNum: 0 }; const { result } = renderNav(makeScrollGroupHook(ref)); - expect(result.current.liveScrRef).toEqual(ref); - expect(result.current.rawScrRef).toEqual(ref); + expect(result.current.scrRef).toEqual(ref); }); - it('passes a same-chapter verse-0 reference through to liveScrRef (host < from verse 1)', () => { + it('passes a same-chapter verse-0 reference through to scrRef (host < from verse 1)', () => { // Verse 0 is a real, focusable verse (a chapter superscription), so a verse-0 reference for the // chapter already shown passes through verbatim rather than being held on the current verse. This // is the host's `<` (previous-verse) from verse 1: it must land on the superscription, which the @@ -94,12 +93,12 @@ describe('InterlinearNavContext', () => { chapterNum: 3, verseNum: 1, }); - expect(result.current.liveScrRef).toEqual({ book: 'GEN', chapterNum: 3, verseNum: 1 }); + expect(result.current.scrRef).toEqual({ book: 'GEN', chapterNum: 3, verseNum: 1 }); act(() => setRef({ book: 'GEN', chapterNum: 3, verseNum: 0 })); rerender(); - expect(result.current.liveScrRef).toEqual({ book: 'GEN', chapterNum: 3, verseNum: 0 }); + expect(result.current.scrRef).toEqual({ book: 'GEN', chapterNum: 3, verseNum: 0 }); }); it('passes a verse-0 reference for a different chapter through as a real chapter jump', () => { @@ -115,28 +114,26 @@ describe('InterlinearNavContext', () => { act(() => setRef({ book: 'GEN', chapterNum: 4, verseNum: 0 })); rerender(); - expect(result.current.liveScrRef).toEqual({ book: 'GEN', chapterNum: 4, verseNum: 0 }); + expect(result.current.scrRef).toEqual({ book: 'GEN', chapterNum: 4, verseNum: 0 }); }); describe('duplicate host deliveries', () => { - it('keeps rawScrRef and liveScrRef identity when the host re-sends a value-equal reference', () => { + it('keeps scrRef identity when the host re-sends a value-equal reference', () => { // The scripture picker fires each external navigation twice in quick succession, the second // delivery being a fresh object with identical content. The provider must hand back the - // previously adopted objects so the duplicate is invisible to consumers (no context-value + // previously adopted object so the duplicate is invisible to consumers (no context-value // change, no re-render churn mid-recenter). const { result, setRef, rerender } = renderNavMutable({ book: 'GEN', chapterNum: 3, verseNum: 7, }); - const rawBefore = result.current.rawScrRef; - const liveBefore = result.current.liveScrRef; + const before = result.current.scrRef; act(() => setRef({ book: 'GEN', chapterNum: 3, verseNum: 7 })); rerender(); - expect(result.current.rawScrRef).toBe(rawBefore); - expect(result.current.liveScrRef).toBe(liveBefore); + expect(result.current.scrRef).toBe(before); }); it('treats a verse-0 chapter jump and its verse-1 form as two distinct deliveries', () => { @@ -151,13 +148,12 @@ describe('InterlinearNavContext', () => { act(() => setRef({ book: 'GEN', chapterNum: 4, verseNum: 0 })); rerender(); - const liveAfterJump = result.current.liveScrRef; - expect(liveAfterJump).toEqual({ book: 'GEN', chapterNum: 4, verseNum: 0 }); + expect(result.current.scrRef).toEqual({ book: 'GEN', chapterNum: 4, verseNum: 0 }); act(() => setRef({ book: 'GEN', chapterNum: 4, verseNum: 1 })); rerender(); - expect(result.current.liveScrRef).toEqual({ book: 'GEN', chapterNum: 4, verseNum: 1 }); + expect(result.current.scrRef).toEqual({ book: 'GEN', chapterNum: 4, verseNum: 1 }); }); it('reuses the previous reference when a duplicate differs only in the verse segment string', () => { @@ -170,7 +166,7 @@ describe('InterlinearNavContext', () => { act(() => setRef({ book: 'GEN', chapterNum: 3, verseNum: 7, verse: '7a' })); rerender(); - expect(result.current.rawScrRef).toBe(initial); + expect(result.current.scrRef).toBe(initial); }); it('reuses the previous reference when a duplicate differs only in versification', () => { @@ -189,7 +185,7 @@ describe('InterlinearNavContext', () => { ); rerender(); - expect(result.current.rawScrRef).toBe(initial); + expect(result.current.scrRef).toBe(initial); }); }); diff --git a/src/__tests__/components/InterlinearizerLoader.test.tsx b/src/__tests__/components/InterlinearizerLoader.test.tsx index a2f30210..6b28da37 100644 --- a/src/__tests__/components/InterlinearizerLoader.test.tsx +++ b/src/__tests__/components/InterlinearizerLoader.test.tsx @@ -1406,7 +1406,7 @@ describe('InterlinearizerLoader', () => { /** * Builds a scroll-group hook whose reference can be restaged between rerenders. A fresh object - * identity is required each change so the provider's `liveScrRef` memo recomputes. + * identity is required each change so the provider adopts it as a new `scrRef`. * * @param initial - The reference reported on the first render. * @returns A `[hook, setRef]` pair. diff --git a/src/components/InterlinearNavContext.tsx b/src/components/InterlinearNavContext.tsx index 20f2fcc6..890e1dcd 100644 --- a/src/components/InterlinearNavContext.tsx +++ b/src/components/InterlinearNavContext.tsx @@ -62,7 +62,7 @@ function areScrRefsEqual(a: SerializedVerseRef, b: SerializedVerseRef): boolean /** * Builds a stable string key identifying the verse a reference names. Used to match an internal - * navigation against the `liveScrRef` the host later delivers, so the segment window can tell an + * navigation against the reference the host later delivers, so the segment window can tell an * internally-originated change apart from an external one. Verse 0 is keyed verbatim (as its own * verse) so a deliberate navigation to a chapter's verse-0 superscription is distinct from verse * 1. @@ -105,25 +105,19 @@ function isInternalNavMarkerFresh(stampedAt: number | undefined): boolean { */ export interface InterlinearNav { /** - * The reference from the host scroll-group hook, value-verbatim. Drives the editable book/chapter - * nav controls so a chapter selection (verse 0) is reflected exactly as the user entered it. - * Identity-stable across the host's duplicate deliveries: when the host re-sends a value-equal - * reference (the scripture picker fires each navigation twice), the previously adopted object is - * handed back so consumers see no change. + * The reference from the host scroll-group hook, value-verbatim, driving both the editable + * book/chapter nav controls and the views' recentering. Identity-stable across the host's + * duplicate deliveries: when the host re-sends a value-equal reference (the scripture picker + * fires each navigation twice), the previously adopted object is handed back so consumers see no + * change. Verse 0 is treated as an ordinary verse: a `verseNum: 0` reference passes through + * verbatim, so the host's `<` (previous-verse) from verse 1 lands on the chapter's + * superscription. When it names a chapter with a verse-0 superscription segment, that segment + * becomes the active verse. Which verses actually have segments is unknown here (the book is not + * loaded at this layer), so the loader resolves a reference with no matching segment before + * rendering: verse 0 falls back to the chapter's first numbered verse, any other unmatched verse + * to the nearest preceding segment start in its chapter. */ - rawScrRef: SerializedVerseRef; - /** - * The active reference. Equal to `rawScrRef`, with object identity reused across the host's - * value-equal duplicate deliveries so a re-send reads as no change. Verse 0 is treated as an - * ordinary verse: a `verseNum: 0` reference passes through verbatim, so the host's `<` - * (previous-verse) from verse 1 lands on the chapter's superscription. When it names a chapter - * with a verse-0 superscription segment, that segment becomes the active verse. Which verses - * actually have segments is unknown here (the book is not loaded at this layer), so the loader - * resolves a reference with no matching segment before rendering: verse 0 falls back to the - * chapter's first numbered verse, any other unmatched verse to the nearest preceding segment - * start in its chapter. - */ - liveScrRef: SerializedVerseRef; + scrRef: SerializedVerseRef; /** * Sets the scripture reference, writing through to the host scroll-group ref, and records the * navigation's {@link NavOrigin}. An `internal` origin marks the target verse so the segment @@ -203,28 +197,32 @@ export function InterlinearNavProvider({ const [hostScrRef, setScrRef, scrollGroupId, setScrollGroupId] = useWebViewScrollGroupScrRef(); /** - * The last host delivery adopted as `rawScrRef`, kept so the duplicate-delivery guard below can - * hand back the same object when the host re-sends an identical reference. + * The last host delivery adopted as `scrRef`, serving two readers each render: the + * duplicate-delivery guard below hands back the same object when the host re-sends an identical + * reference, and the render-phase mid-reveal guard compares the incoming reference against the + * verse last shown. */ - const stableRawScrRefRef = useRef(hostScrRef); + const prevScrRefRef = useRef(hostScrRef); + + /** + * The {@link scrRef} adopted on the previous render, captured before the write below overwrites + * it, so the mid-reveal navigation guard further down can compare the incoming reference against + * the verse last shown. + */ + const prevScrRef = prevScrRefRef.current; // The host delivers each scripture-picker navigation twice in quick succession: two back-to-back // signals carrying the same reference as distinct objects. Passing the second through verbatim - // would change `rawScrRef`'s identity — and with it the context value — for a navigation that + // would change `scrRef`'s identity — and with it the context value — for a navigation that // already happened, re-rendering every nav consumer mid-recenter for nothing (the same // double-fire whose duplicate USJ payload `useInterlinearizerBookData` already stabilizes). Reuse // the previously adopted object when the delivery is value-equal, so a duplicate is invisible - // downstream. - const rawScrRef = areScrRefsEqual(hostScrRef, stableRawScrRefRef.current) - ? stableRawScrRefRef.current - : hostScrRef; - stableRawScrRefRef.current = rawScrRef; - - /** - * The last committed {@link liveScrRef}, mirrored so the render-phase mid-reveal guard can compare - * the incoming reference against the verse last shown. - */ - const liveScrRefRef = useRef(rawScrRef); + // downstream. Verse 0 is not special-cased: every reference passes through verbatim, so the + // host's `<` (previous-verse) from verse 1 lands on the chapter's superscription, and the loader + // resolves a reference with no matching segment (verse 0, or an unclamped host verse bump past + // the chapter's end) before rendering. + const scrRef = areScrRefsEqual(hostScrRef, prevScrRef) ? prevScrRef : hostScrRef; + prevScrRefRef.current = scrRef; /** * Verse keys of internal navigations still awaiting their host round-trip, each mapped to its @@ -237,26 +235,10 @@ export function InterlinearNavProvider({ */ const pendingInternalNavRef = useRef>(new Map()); - // `liveScrRef` is the active reference the views recenter on. With verse-0 stickiness removed it no - // longer transforms `rawScrRef` — every reference (verse 0 included) passes through verbatim, so - // the host's `<` (previous-verse) from verse 1 lands on the chapter's superscription, and the - // loader resolves a reference with no matching segment (verse 0, or an unclamped host verse bump - // past the chapter's end) before rendering. The alias keeps the intent-revealing name and marks the seam where any - // future raw→active mapping would live; `rawScrRef` already reuses the previously committed object - // on a value-equal re-send, so a duplicate delivery never reads as a fresh navigation. - const liveScrRef = rawScrRef; - /** - * The {@link liveScrRef} committed on the previous render, captured before the mirror update below - * overwrites it, so the mid-reveal navigation guard further down can compare the incoming - * reference against the verse last shown. - */ - const prevLiveScrRef = liveScrRefRef.current; - liveScrRefRef.current = liveScrRef; - const navigate = useCallback( (newScrRef: SerializedVerseRef, origin: NavOrigin = 'external') => { // Invariant: a marker write is ALWAYS paired with the `setScrRef` below. The marker readers - // that run during render — the render-phase mid-reveal guard, which lists only `[rawScrRef]` + // that run during render — the render-phase mid-reveal guard, which lists only `[scrRef]` // in the reactive path that reaches it — rely on every marker write also pushing a new ref // through the host, so the render that reads the marker is the one that sees it fresh. Never // set a marker here without the accompanying `setScrRef`, or a reader could see a stale marker. @@ -285,10 +267,10 @@ export function InterlinearNavProvider({ /** * Book code the curtain currently shows fully faded-in. A book change is detected by comparing - * `liveScrRef.book` against this; `reportSettled` advances it to the new book once the view has - * laid out. Seeded to the book at mount so the initial load shows no fade. + * `scrRef.book` against this; `reportSettled` advances it to the new book once the view has laid + * out. Seeded to the book at mount so the initial load shows no fade. */ - const displayedBookRef = useRef(liveScrRef.book); + const displayedBookRef = useRef(scrRef.book); /** * `true` between detecting a book change and the view reporting settled. Gates `reportSettled` so @@ -299,15 +281,18 @@ export function InterlinearNavProvider({ /** Handle of the in-flight fade-in→idle timer, or `undefined` when none is pending. */ const fadeInTimeoutRef = useRef | undefined>(undefined); - /** Verse key of the live ref, computed once for the cross-book / mid-reveal render guards below. */ - const liveKey = verseKey(liveScrRef); + /** + * Verse key of the adopted ref, computed once for the cross-book / mid-reveal render guards + * below. + */ + const scrRefKey = verseKey(scrRef); // Detect a cross-book navigation *during render* and start the fade-out synchronously, so the // curtain drops in the same commit the book ref changes — never a paint later (an effect-driven // fade-out would let the mounted views render one frame against the new book's ref before the // curtain covers them). Setting state during render is the guarded React pattern: `awaitingSettle` // flips true so this fires once per book change, and `setFadePhase` is batched into this commit. - if (liveScrRef.book !== displayedBookRef.current && !awaitingSettleRef.current) { + if (scrRef.book !== displayedBookRef.current && !awaitingSettleRef.current) { if (fadeInTimeoutRef.current !== undefined) { clearTimeout(fadeInTimeoutRef.current); fadeInTimeoutRef.current = undefined; @@ -316,8 +301,8 @@ export function InterlinearNavProvider({ setFadePhase('out'); } else if ( fadePhase === 'in' && - liveKey !== verseKey(prevLiveScrRef) && - !isInternalNavMarkerFresh(pendingInternalNavRef.current.get(liveKey)) + scrRefKey !== verseKey(prevScrRef) && + !isInternalNavMarkerFresh(pendingInternalNavRef.current.get(scrRefKey)) ) { // A follow-up external navigation landing mid-fade-in: the host resolves one picker selection // as two navigations (book change, then precise target), so the second routinely arrives while @@ -337,7 +322,7 @@ export function InterlinearNavProvider({ const reportSettled = useCallback(() => { if (!awaitingSettleRef.current) return; awaitingSettleRef.current = false; - displayedBookRef.current = liveScrRef.book; + displayedBookRef.current = scrRef.book; setFadePhase('in'); /* v8 ignore next -- defensive: render-time book-change guard always clears any stale timer first */ if (fadeInTimeoutRef.current !== undefined) clearTimeout(fadeInTimeoutRef.current); @@ -345,7 +330,7 @@ export function InterlinearNavProvider({ fadeInTimeoutRef.current = undefined; setFadePhase('idle'); }, RECENTER_FADE_MS); - }, [liveScrRef.book]); + }, [scrRef.book]); const cancelFade = useCallback(() => { if (fadeInTimeoutRef.current !== undefined) { @@ -353,9 +338,9 @@ export function InterlinearNavProvider({ fadeInTimeoutRef.current = undefined; } awaitingSettleRef.current = false; - displayedBookRef.current = liveScrRef.book; + displayedBookRef.current = scrRef.book; setFadePhase('idle'); - }, [liveScrRef.book]); + }, [scrRef.book]); // Clear any pending fade-in timer on unmount so a deferred state update doesn't run on a torn-down // tree. @@ -368,8 +353,7 @@ export function InterlinearNavProvider({ const value = useMemo( () => ({ - rawScrRef, - liveScrRef, + scrRef, navigate, consumeInternalNav, scrollGroupId, @@ -379,8 +363,7 @@ export function InterlinearNavProvider({ cancelFade, }), [ - rawScrRef, - liveScrRef, + scrRef, navigate, consumeInternalNav, scrollGroupId, diff --git a/src/components/InterlinearizerLoader.tsx b/src/components/InterlinearizerLoader.tsx index 644c1869..a1b78792 100644 --- a/src/components/InterlinearizerLoader.tsx +++ b/src/components/InterlinearizerLoader.tsx @@ -108,15 +108,8 @@ function InterlinearizerLoaderInner({ useWebViewState: UseWebViewStateHook; updateWebViewDefinition: UpdateWebViewDefinition; }>) { - const { - rawScrRef, - liveScrRef: scrRef, - navigate, - scrollGroupId, - setScrollGroupId, - fadePhase, - cancelFade, - } = useInterlinearNav(); + const { scrRef, navigate, scrollGroupId, setScrollGroupId, fadePhase, cancelFade } = + useInterlinearNav(); const [interfaceMode] = useSetting('platform.interfaceMode', 'simple'); const [interfaceLanguages] = useSetting('platform.interfaceLanguage', ['und']); @@ -437,7 +430,7 @@ function InterlinearizerLoaderInner({ startAreaChildren={ interfaceMode === 'power' ? (