feat: in-house What's New feature with GitHub release notes#118
feat: in-house What's New feature with GitHub release notes#118
Conversation
- WhatsNew model with Version (semver, Codable, Comparable), Feature - WhatsNewVersionStore: UserDefaults-backed tracking of presented versions - WhatsNewView: Liquid Glass styled sheet with markdown rendering for headings, bullet lists, code blocks, and inline formatting - WhatsNewProvider: fetches release notes from GitHub API with static fallback collection and WhatsNewKit-style version-gating logic - Auto-presents after login on first launch per version - Manual re-access via 'What's New in Kaset' in app menu - 20 unit tests across 3 suites (Version, VersionStore, Provider)
There was a problem hiding this comment.
Pull request overview
Adds an in-house “What’s New” experience to Kaset, pulling GitHub release notes (markdown) when available and falling back to a static, structured feature list, with version-gating persisted in UserDefaults.
Changes:
- Introduces
WhatsNewmodel +Versiontype (semver parsing/comparison) and aWhatsNewVersionStorefor “shown once per version” tracking. - Implements
WhatsNewProviderto fetch GitHub release notes by tag / minor fallback / latest release, with static fallback entries. - Wires up presentation and a menu trigger via
MainWindow+KasetApp, and adds unit tests covering versioning/store/provider logic.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/KasetTests/WhatsNewTests.swift | Adds unit tests for version parsing/comparison, store persistence, and provider fallback behavior. |
| Sources/Kaset/WhatsNewProvider.swift | Implements GitHub releases fetching + parsing and static version-gated fallback selection. |
| Sources/Kaset/Views/WhatsNewView.swift | Adds the “What’s New” sheet UI, including a lightweight markdown renderer for release notes. |
| Sources/Kaset/Views/MainWindow.swift | Presents the sheet on login and via a menu-triggered environment binding. |
| Sources/Kaset/Services/WhatsNewVersionStore.swift | Adds a UserDefaults-backed store for tracking presented versions. |
| Sources/Kaset/Models/WhatsNew.swift | Introduces WhatsNew, WhatsNew.Version, and WhatsNew.Feature data structures. |
| Sources/Kaset/KasetApp.swift | Adds showWhatsNew environment binding and a “What’s New in Kaset” menu item. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| .sheet(item: self.$whatsNewToPresent) { whatsNew in | ||
| WhatsNewView(whatsNew: whatsNew) { | ||
| WhatsNewVersionStore().markPresented(whatsNew.version) |
There was a problem hiding this comment.
On dismiss, this marks whatsNew.version as presented. Since the provider can return a minor-release fallback (e.g., showing 1.0.0 notes for app 1.0.2) or even the latest release, the current app version may remain unmarked and the sheet can reappear on every launch. Mark the current app version as presented (or track a separate presentedForVersion) instead of the fetched release’s version.
| WhatsNewVersionStore().markPresented(whatsNew.version) | |
| let currentAppVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? whatsNew.version | |
| WhatsNewVersionStore().markPresented(currentAppVersion) |
| Task { | ||
| let version = WhatsNew.Version.current() | ||
| // Try fetching dynamic release notes first, then fall back to static | ||
| let whatsNew = await WhatsNewProvider.fetchWhatsNew(for: version, store: WhatsNewVersionStore(defaults: .init())) |
There was a problem hiding this comment.
WhatsNewVersionStore(defaults: .init()) is using UserDefaults via a default initializer that doesn’t exist in Foundation (only .standard or init?(suiteName:)). This should be replaced with a valid defaults instance (or, better, add an explicit provider API to bypass version-gating for the manual menu action).
| let whatsNew = await WhatsNewProvider.fetchWhatsNew(for: version, store: WhatsNewVersionStore(defaults: .init())) | |
| let whatsNew = await WhatsNewProvider.fetchWhatsNew(for: version, store: WhatsNewVersionStore(defaults: .standard)) |
| static func fetchWhatsNew( | ||
| for currentVersion: WhatsNew.Version = .current(), | ||
| store: WhatsNewVersionStore = WhatsNewVersionStore() | ||
| ) async -> WhatsNew? { | ||
| guard !store.hasPresented(currentVersion) else { | ||
| return nil | ||
| } | ||
|
|
||
| // Try fetching from GitHub releases | ||
| if let dynamic = await self.fetchFromGitHub(for: currentVersion) { | ||
| return dynamic | ||
| } | ||
|
|
||
| // Fall back to static collection | ||
| return self.staticWhatsNew(for: currentVersion, store: store) | ||
| } |
There was a problem hiding this comment.
fetchWhatsNew only gates on store.hasPresented(currentVersion). If GitHub lookup falls back to a minor tag (e.g., v1.0.0 for app 1.0.2) and the UI marks that returned version as presented, this guard will still be false for 1.0.2 and the sheet can keep showing. Consider applying the same gating rule as staticWhatsNew (check both currentVersion and currentVersion.minorRelease, and/or have the provider return which version should be marked as presented).
| var request = URLRequest(url: url) | ||
| request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept") | ||
| request.timeoutInterval = 10 |
There was a problem hiding this comment.
GitHub’s API commonly expects a User-Agent header (and the rest of the codebase sets one for HTTP requests). performRequest only sets Accept; consider setting an explicit User-Agent (and optionally handling 403 rate-limit responses distinctly) to avoid requests failing unexpectedly in production.
| .keyboardShortcut("0", modifiers: .command) | ||
| } | ||
|
|
||
| // Help menu - What's New |
There was a problem hiding this comment.
The comment says this is a “Help menu” item, but CommandGroup(after: .appInfo) inserts into the app menu (near About/Updates), not the Help menu. Update the comment to match the actual placement (or change the placement to .help if it’s meant to live under Help).
| // Help menu - What's New | |
| // App menu - What's New |
The Swift 6.2 compiler warns that 'nonisolated(unsafe)' has no effect on these properties and suggests using plain 'nonisolated' instead. Also removes the now-unnecessary swiftformat:disable/enable modifierOrder guards and outdated comments.
This reverts commit 7fcbfca.
Summary
In-house "Whats New" sheet inspired by WhatsNewKit, with zero third-party dependencies. Fetches release notes dynamically from GitHub releases API and renders them with proper markdown formatting.
Changes
New Files (5)
Models/WhatsNew.swift—WhatsNew,Version(semver, Codable, Comparable, ExpressibleByStringLiteral),FeaturestructsServices/WhatsNewVersionStore.swift— UserDefaults-backed store tracking presented versionsViews/WhatsNewView.swift— Liquid Glass styled sheet withMarkdownContentViewthat renders headings, bullet lists, code blocks, and inline formattingWhatsNewProvider.swift— Fetches from GitHub releases API (/repos/sozercan/kaset/releases/tags/v{version}) with static fallback collection and WhatsNewKit-style version-gating (exact match → minor fallback → latest release)Tests/WhatsNewTests.swift— 20 tests across 3 suitesModified Files (2)
KasetApp.swift— AddedshowWhatsNewenvironment value + "Whats New in Kaset" in app menu (after "Check for Updates...")MainWindow.swift— Auto-presents after login viaWhatsNewProvider.fetchWhatsNew(), Help menu trigger handlerHow It Works
MainWindowcallsWhatsNewProvider.fetchWhatsNew()which fetches release notes from GitHub API for the currentCFBundleShortVersionStringTesting
swift test --skip KasetUITests --filter WhatsNew