Skip to content

feat: in-app What's new sheet for #459#496

Merged
rainxchzed merged 12 commits into
mainfrom
feat/459-whats-new-sheet
May 3, 2026
Merged

feat: in-app What's new sheet for #459#496
rainxchzed merged 12 commits into
mainfrom
feat/459-whats-new-sheet

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 3, 2026

Closes #459.

Summary

  • Adds a ModalBottomSheet that appears once per versionCode bump on first launch after an update.
  • Skips the sheet entirely on fresh installs (silently marks the current version as seen so the user lands straight on Home).
  • Skips the sheet for releases marked showAsSheet = false so bug-fix-only patches stay silent and credible.
  • Defers display until the user is on HomeScreen, OAuth/rate-limit/session dialogs are not active, and a 600ms debounce has elapsed after Home becomes the destination.
  • Adds a permanent What's new entry on the Profile screen pointing to a full history (WhatsNewHistoryScreen).
  • Single editorial source of truth: WhatsNewEntries in core/domain — one entry per release, hand-written in concise voice.

Why this shape

UX research (paraphrased): on update users have a low-grade "did anything change?" question. Without a surface, new features stay invisible and "you didn't tell me X existed" turns into support load. But over-using the surface (Slack/Notion-style) trains dismissal. The two big calibrations are:

  • Only show on real updateslastSeenWhatsNewVersionCode < currentVersionCode, with showAsSheet=false per-version flag for silent patches.
  • Get out of the way of competing first-run flows — gated on Home + auth-settled + no other dialog visible + debounce.

Files of note

  • core/domain/.../model/WhatsNewEntry.kt, WhatsNewEntries.kt — model + the entry list (edit this every release).
  • core/domain/.../system/AppVersionInfo.kt — small interface; BuildKonfigAppVersionInfo reads from BuildKonfig.VERSION_CODE (newly exposed).
  • core/presentation/.../components/whatsnew/WhatsNewSheet.kt, WhatsNewHistoryScreen.kt — the sheet and the history screen.
  • composeApp/.../whatsnew/WhatsNewViewModel.kt — controller; computes pendingEntry, persists via TweaksRepository.setLastSeenWhatsNewVersionCode.
  • composeApp/.../Main.kt — host + display gating.

Edge cases covered

  • Fresh install (lastSeen == null) → mark current as seen, no sheet.
  • Downgrade (lastSeen > current) → no-op; setLastSeenWhatsNewVersionCode only writes when the new value is greater, so downgrade can't poison the bookmark.
  • Skipped versions (e.g. 1.5 → 1.8) → show only the current version's entry; older entries reachable via "View previous versions" CTA → Profile → What's new.
  • No entry shipped for the current versionCode → mark seen, no sheet (silent patch).
  • Auth screen / rate-limit / session-expired dialog active → suppressed.
  • OAuth flow on cold start → suppressed by the same gate; user sees Home first.

What's NOT in this PR (resisted scope creep)

  • No images, GIFs, or screenshots in the sheet.
  • No remote content fetching.
  • No analytics or telemetry.
  • No per-locale bullet content (English only — scaffolding strings only). UX research recommended this for v1; community translation issue can come later.
  • No fresh-install welcome sheet (separate concern).
  • No re-show-if-skipped logic.

Test plan

  • Cold start with no DataStore record → no sheet appears, lastSeenWhatsNewVersionCode is set to current.
  • Bump projectVersionCode to 16 in gradle/libs.versions.toml, add a 1.9.0 entry to WhatsNewEntries.all with showAsSheet=true, rebuild → sheet appears on launch after Home settles.
  • Same scenario with showAsSheet=false → no sheet, lastSeen silently advances to 16.
  • Tap Got it → sheet dismisses, does not reappear on subsequent launches.
  • Tap View previous versions → navigates to history screen.
  • Profile → What's new row → opens history screen with 1.8.0 entry visible.
  • Force-quit during OAuth flow → on next launch, no sheet shows on top of AuthenticationScreen.

Summary by CodeRabbit

  • New Features
    • Introduced a "What's New" modal sheet with sections (New/Improved/Fixed/Heads up), dismiss and "View previous versions" actions.
    • Added a changelog history screen accessible from Profile to review past releases.
    • Profile option: tap opens history; long-press previews the latest sheet.
    • App now tracks seen versions so each update sheet shows only once.
    • Changelog updated with v1.8.1 and adjusted v1.8.0 release date.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a "What's New" feature: domain models and entries, ViewModel evaluation/force-show logic, persistence of last-seen version, AppVersionInfo/BuildKonfig wiring and DI, UI sheet and history screen, navigation/profile integration, and a 600ms debounce before showing the sheet.

Changes

What's New changelog feature

Layer / File(s) Summary
Domain models
core/domain/.../model/WhatsNewEntry.kt, core/domain/.../model/WhatsNewEntries.kt
Adds WhatsNewEntry, WhatsNewSection, WhatsNewSectionType and populates WhatsNewEntries.all (v16 and v15 entries).
Version abstraction
core/domain/.../system/AppVersionInfo.kt
Adds AppVersionInfo interface with versionCode and versionName.
Repository contract
core/domain/.../repository/TweaksRepository.kt
Adds getLastSeenWhatsNewVersionCode(): Flow<Int?> and setLastSeenWhatsNewVersionCode(versionCode: Int).
Repository implementation
core/data/.../repository/TweaksRepositoryImpl.kt
Implements LAST_SEEN_WHATS_NEW_VERSION_CODE_KEY, getLastSeenWhatsNewVersionCode() and setLastSeenWhatsNewVersionCode(versionCode) (only writes when provided > stored).
Build/version wiring
build-logic/.../BuildKonfigConventionPlugin.kt, core/data/.../services/BuildKonfigAppVersionInfo.kt, core/data/.../di/SharedModule.kt
Adds VERSION_CODE build config field; adds BuildKonfigAppVersionInfo implementing AppVersionInfo; registers single<AppVersionInfo> in DI.
ViewModel
composeApp/.../whatsnew/WhatsNewViewModel.kt
Starts evaluate() on init (wrapped with error handling); publishes _pendingEntry; provides markSeen(), forceShowLatest(), and hasHistory.
ViewModel registration
composeApp/.../app/di/ViewModelsModule.kt
Registers WhatsNewViewModel with Koin via viewModelOf(::WhatsNewViewModel).
Main app integration
composeApp/.../Main.kt
Instantiates ViewModel, computes gating booleans, applies a 600ms debounce, and conditionally shows WhatsNewSheet; dismiss/history actions call ViewModel and navigate.
Navigation
composeApp/.../navigation/GithubStoreGraph.kt, AppNavigation.kt, NavigationUtils.kt
Adds WhatsNewHistoryScreen route, wires composable in NavHost, updates route matching and profile callbacks for history/preview.
Profile feature wiring
feature/profile/.../ProfileAction.kt, ProfileRoot.kt, ProfileViewModel.kt, components/sections/Options.kt
Adds OnWhatsNewClick action, adds onNavigateToWhatsNew/onPreviewWhatsNewSheet callbacks, intercepts actions to navigate or preview, and adds an options card with click + long-press.
UI components & strings
core/presentation/.../whatsnew/WhatsNewSheet.kt, WhatsNewHistoryScreen.kt, composeResources/values/strings.xml
Adds modal sheet UI, entry card, section/bullet renderers, history screen list/empty state, and new localized strings.
Version bump
gradle/libs.versions.toml
Bumps projectVersionName to 1.8.1 and projectVersionCode to 16.

Sequence Diagram

sequenceDiagram
    participant User
    participant App
    participant WhatsNewVM as WhatsNewViewModel
    participant AppVersionInfo
    participant TweaksRepo as TweaksRepository
    participant WhatsNewEntries
    participant UI as WhatsNewSheet

    User->>App: Open app / navigate to home
    App->>WhatsNewVM: instantiate ViewModel
    WhatsNewVM->>AppVersionInfo: read versionCode
    AppVersionInfo-->>WhatsNewVM: return versionCode
    WhatsNewVM->>TweaksRepo: getLastSeenWhatsNewVersionCode()
    TweaksRepo-->>WhatsNewVM: return lastSeen (nullable)
    alt new or unseen version
        WhatsNewVM->>WhatsNewEntries: lookup entry for versionCode
        WhatsNewEntries-->>WhatsNewVM: return entry?
        alt entry exists and showAsSheet
            WhatsNewVM->>App: emit pendingEntry
        else
            WhatsNewVM->>TweaksRepo: setLastSeenWhatsNewVersionCode(current)
        end
    else already seen
        WhatsNewVM->>TweaksRepo: setLastSeenWhatsNewVersionCode(current)
    end
    App->>App: debounce 600ms
    App->>UI: render WhatsNewSheet when pendingEntry present
    User->>UI: dismiss or view history
    alt dismiss
        UI->>WhatsNewVM: markSeen()
        WhatsNewVM->>TweaksRepo: setLastSeenWhatsNewVersionCode(version)
    else view history
        UI->>App: navigate to WhatsNewHistoryScreen
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • jessiekid187

Poem

🐰 I nibble logs and hop with cheer,
A sheet appears when updates near.
Debounced a moment, then displayed,
Dismiss or peek at versions made—
Hop on, discover what's been dear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly indicates a new 'What's new sheet' feature being added, directly referencing the linked issue #459.
Linked Issues check ✅ Passed PR fully implements the requested feature: in-app changelog UI shown on update, dismissal persistence, history view access, and properly handles edge cases per PR objectives.
Out of Scope Changes check ✅ Passed All changes directly support the What's new sheet feature: version tracking, UI components, domain models, DI wiring, and version bump to 1.8.1 are all within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/459-whats-new-sheet

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt`:
- Around line 22-46: Wrap the body of evaluate() in a safe error-handling block
(use runCatching or try/catch) to prevent an IOException from propagating out of
viewModelScope.launch; catch exceptions from
tweaksRepository.getLastSeenWhatsNewVersionCode().first() and from any
tweaksRepository.setLastSeenWhatsNewWhatsNewVersionCode calls, log the error and
treat it as “seen” by setting last-seen to current (or bail out safely) so the
app won’t crash; update evaluate() accordingly (refer to evaluate(),
getLastSeenWhatsNewVersionCode(),
tweaksRepository.setLastSeenWhatsNewVersionCode(), and the viewModelScope.launch
usage and mirror the runCatching pattern used in ProfileViewModel).

In
`@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewHistoryScreen.kt`:
- Around line 48-55: The back navigation Icon inside WhatsNewHistoryScreen
currently sets contentDescription = null which hides it from accessibility
services; update the Icon (inside the IconButton that calls onNavigateBack) to
provide a localized, descriptive contentDescription (e.g., use a string resource
like R.string.back or a passed-in localized label) so accessibility tools
announce the button as "Back" (or the localized equivalent) while leaving the
IconButton's onClick as-is; ensure the string is obtained via the appropriate
Compose localization API used in this module and referenced in the Icon's
contentDescription property.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 46aaafd7-af93-49f3-9a71-bb66f61cfd4a

📥 Commits

Reviewing files that changed from the base of the PR and between c7c7dc9 and 488a4a1.

📒 Files selected for processing (21)
  • build-logic/convention/src/main/kotlin/BuildKonfigConventionPlugin.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/NavigationUtils.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/BuildKonfigAppVersionInfo.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/WhatsNewEntries.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/WhatsNewEntry.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/AppVersionInfo.kt
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewHistoryScreen.kt
  • core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt
  • feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt
  • feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt
  • feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt
  • feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt

Comment on lines +48 to +55
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
},
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Provide a contentDescription for the back navigation icon.

contentDescription = null silences TalkBack entirely for this IconButton. "When you set the contentDescription parameter to null, you indicate to the Android framework that this element does not have associated actions or state" — the opposite of what a navigation icon button represents. The Android accessibility docs show that an Icon inside an IconButton should carry a localized contentDescription; only Text composables are exempt because accessibility services read the text itself.

🛡️ Proposed fix
- Icon(
-     imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-     contentDescription = null,
- )
+ Icon(
+     imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+     contentDescription = stringResource(Res.string.navigate_back), // add key if missing
+ )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewHistoryScreen.kt`
around lines 48 - 55, The back navigation Icon inside WhatsNewHistoryScreen
currently sets contentDescription = null which hides it from accessibility
services; update the Icon (inside the IconButton that calls onNavigateBack) to
provide a localized, descriptive contentDescription (e.g., use a string resource
like R.string.back or a passed-in localized label) so accessibility tools
announce the button as "Back" (or the localized equivalent) while leaving the
IconButton's onClick as-is; ensure the string is obtained via the appropriate
Compose localization API used in this module and referenced in the Icon's
contentDescription property.

rainxchzed and others added 6 commits May 3, 2026 17:58
Treating a null lastSeenWhatsNewVersionCode as Int.MIN_VALUE so the
first launch after the feature ships still surfaces the sheet for the
current version. Previously the controller silently advanced the
bookmark to current and the sheet never appeared.
…hatsnew/WhatsNewViewModel.kt

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt`:
- Around line 15-17: The class header for WhatsNewViewModel is malformed and
missing members; fix it by closing the constructor and adding the missing
constructor parameter "private val appVersionInfo: AppVersionInfo", change the
header to ") : ViewModel() {" so the class extends ViewModel, and restore the
missing state properties by adding "private val _pendingEntry =
MutableStateFlow<WhatsNewEntry?>(null)" and "val pendingEntry:
StateFlow<WhatsNewEntry?> = _pendingEntry.asStateFlow()"; ensure the imports for
MutableStateFlow, StateFlow and asStateFlow remain and that existing usages of
appVersionInfo, _pendingEntry and pendingEntry compile against these added
members.
- Around line 45-51: The markSeen() coroutine can throw a DataStore IOException
and crash because the call to tweaksRepository.setLastSeenWhatsNewVersionCode
inside viewModelScope.launch has no error handling; update markSeen() to catch
exceptions from that write (specifically IOException but you can catch
Exception/Throwable if preferred), e.g. wrap the repository call in a try/catch
inside the launched coroutine and handle/log the error (use processLogger/Logger
or viewModelScope's logger) so failures (full disk, IO issues) do not propagate
to the UncaughtExceptionHandler and crash the app; locate markSeen(),
viewModelScope.launch, and tweaksRepository.setLastSeenWhatsNewVersionCode to
apply the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: becec074-915c-4541-894f-551fbd616b85

📥 Commits

Reviewing files that changed from the base of the PR and between bf00705 and c097785.

📒 Files selected for processing (1)
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt

DataStore writes can throw IOException (full disk, file lock). Without
a catch, the exception propagated through viewModelScope.launch to the
default uncaught handler and crashed the app on something as benign as
a low-storage device dismissing the what's-new sheet. Wrap the init
evaluate path and markSeen persistence in try/catch and log via Kermit
so failures stay observable but non-fatal.
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.

Feature to check out "changes" at the time of updates

1 participant