Skip to content

GT-3014 Account for hidden pages in lesson share links & deep links - #4568

Open
tjohnson009 wants to merge 2 commits into
developfrom
GT-3014-Hidden-Pages
Open

GT-3014 Account for hidden pages in lesson share links & deep links#4568
tjohnson009 wants to merge 2 commits into
developfrom
GT-3014-Hidden-Pages

Conversation

@tjohnson009

Copy link
Copy Markdown
Contributor

Fixes GT-3014

Problem

The lesson pager only contains visible pages, but iOS and knowgod.com reference pages by their position within the full list of manifest pages (including hidden pages). Android was using the pager index in both directions, so whenever a lesson contains hidden pages (e.g. the Listening Lesson in English goes 0 → 1 → 5):

  • share links/QR codes generated on Android pointed at the wrong page on other platforms
  • share links generated on iOS/web opened to the wrong page on Android

Changes

Share links (GT-3014 Build the lesson share link from the current page position)

  • The settled-page tracker now records the current page's position within the full list of manifest pages (currentPagePosition) instead of the pager index, and the share link is built from that. The tracker also observes the pager's visible pages, so the share link stays correct if a hidden page is revealed mid-lesson.
  • pageReached (lesson feedback) intentionally keeps its existing pager-index semantics.

Deep links (GT-3014 Resolve lesson deep link page numbers as manifest page positions)

  • Incoming knowgod.com lesson deep link page numbers are now resolved as manifest page positions and converted to a pager index before being handed to the lesson pager.
  • Hidden pages resolve to the closest previous visible page (matching the existing lesson resume behavior), since hidden pages are only revealed by content events. Invalid/out-of-range positions resolve to the first page, so malformed URLs degrade safely.

Testing

  • New unit tests for Manifest.lessonPagerIndexForPagePosition() covering visible pages, hidden pages, invalid positions, and a hidden first page
  • ./gradlew :ui:lesson-renderer:test and ktlint pass on each commit

Notes

  • Deep linking to a hidden page (revealing it) is intentionally not supported — hidden pages are event-driven and LessonPagerState re-hides them when they're not the current page. If that's ever wanted (per the open question on the ticket), it would need a shared-library change.
  • The tract share link issue from the same Slack thread (always sharing the first page, broken by GT-1446) is a separate ticket and not addressed here.

🤖 Generated with Claude Code

tjohnson009 and others added 2 commits August 19, 2026 21:31
The lesson pager only contains visible pages, so the pager index of the
current page doesn't account for any hidden pages before it. iOS and
knowgod.com use the page position within the full list of manifest
pages, so share links generated on Android pointed at the wrong page
whenever a lesson contains hidden pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lesson share links generated by iOS and knowgod.com reference the page
position within the full list of manifest pages, but Android was using
the number as an index into the visible pages of the lesson pager,
landing on the wrong page when a lesson contains hidden pages.

Hidden pages resolve to the closest previous visible page (matching the
lesson resume behavior) since hidden pages are only revealed by content
events, and invalid positions resolve to the first page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tjohnson009
tjohnson009 requested a review from a team August 20, 2026 01:40
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.50000% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.86%. Comparing base (6298a0d) to head (420594c).
⚠️ Report is 4 commits behind head on develop.

Files with missing lines Patch % Lines
.../org/cru/godtools/tool/lesson/ui/LessonActivity.kt 9.09% 10 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #4568      +/-   ##
===========================================
+ Coverage    52.84%   52.86%   +0.01%     
===========================================
  Files          448      449       +1     
  Lines        12074    12081       +7     
  Branches      2089     2091       +2     
===========================================
+ Hits          6381     6387       +6     
- Misses        5064     5066       +2     
+ Partials       629      628       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines -172 to +179
snapshotFlow { pagerState.settledPage }.collect { page ->
snapshotFlow { pagerState.settledPage to lessonPagerState.pages }.collect { (page, pages) ->
dataModel.pageReached.update { maxOf(it, page) }
dataModel.currentPage.value = page
dataModel.currentPagePosition.value = pages.getOrNull(page)?.position ?: 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a better way to handle this is to probably expose a settledPage: Page property on the LessonPagerState object that is derived from from pages[pagerState.settledPage].

I'm not fully sure if it needs to be nullable or not, pagerState is tightly coupled to the rest of LessonPagerState, so it should be capped to exactly the number of pages available in LessonPagerState.pages. Which means that .getOrNull() may not be necessary to protect against an IndexOutOfBoundsException.

You may also want to separate the highest page reached logic from the logic to determine the current position into 2 separate LaunchedEffects to keep the 2 flows simple and not have to rely on combining multiple values in a Pair

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone back and forth with Claude on this and I'm not sure either. But I think the null check feels like a very small price to pay to avoid a potential crash. I asked Claude if there was ever a scenario where a lesson has no pages, because that doesn't seem like a possibility fro what I can tell and it listed a few, but I don't know how realistic it sounds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll get on the other things first, and then let me know where you land the plane on the nullable part. I think it is a small price to pay, but I might not be seeing this fully

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't post a followup, but thinking about it more we do want to keep it nullable because of the empty pages scenario.

I'm about to open a PR on godtools-shared with a settledPage: LessonPage? property you can use

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay cool. I was mid-flight on doing that, but I'll tell Claude to pause and then look for it in a bit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

2 participants