Skip to content

feat: in-house What's New feature with GitHub release notes#118

Open
sozercan wants to merge 3 commits intomainfrom
whatsnewkit
Open

feat: in-house What's New feature with GitHub release notes#118
sozercan wants to merge 3 commits intomainfrom
whatsnewkit

Conversation

@sozercan
Copy link
Owner

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.swiftWhatsNew, Version (semver, Codable, Comparable, ExpressibleByStringLiteral), Feature structs
  • Services/WhatsNewVersionStore.swift — UserDefaults-backed store tracking presented versions
  • Views/WhatsNewView.swift — Liquid Glass styled sheet with MarkdownContentView that renders headings, bullet lists, code blocks, and inline formatting
  • WhatsNewProvider.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 suites

Modified Files (2)

  • KasetApp.swift — Added showWhatsNew environment value + "Whats New in Kaset" in app menu (after "Check for Updates...")
  • MainWindow.swift — Auto-presents after login via WhatsNewProvider.fetchWhatsNew(), Help menu trigger handler

How It Works

  1. On login, MainWindow calls WhatsNewProvider.fetchWhatsNew() which fetches release notes from GitHub API for the current CFBundleShortVersionString
  2. If GitHub is unreachable, falls back to static feature list
  3. Sheet renders markdown release notes (headings, lists, code blocks) or structured feature rows
  4. On dismiss, version is saved to UserDefaults so it only shows once per version
  5. "Whats New in Kaset" menu item allows manual re-access

Testing

  • 20 unit tests: Version parsing/comparison/codable, VersionStore persistence, Provider version-gating logic
  • All pass: swift test --skip KasetUITests --filter WhatsNew
  • Build clean, swiftlint clean, swiftformat clean

- 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)
Copilot AI review requested due to automatic review settings February 27, 2026 15:40
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 WhatsNew model + Version type (semver parsing/comparison) and a WhatsNewVersionStore for “shown once per version” tracking.
  • Implements WhatsNewProvider to 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)
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
WhatsNewVersionStore().markPresented(whatsNew.version)
let currentAppVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? whatsNew.version
WhatsNewVersionStore().markPresented(currentAppVersion)

Copilot uses AI. Check for mistakes.
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()))
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
let whatsNew = await WhatsNewProvider.fetchWhatsNew(for: version, store: WhatsNewVersionStore(defaults: .init()))
let whatsNew = await WhatsNewProvider.fetchWhatsNew(for: version, store: WhatsNewVersionStore(defaults: .standard))

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +61
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)
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +123
var request = URLRequest(url: url)
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
request.timeoutInterval = 10
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
.keyboardShortcut("0", modifiers: .command)
}

// Help menu - What's New
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
// Help menu - What's New
// App menu - What's New

Copilot uses AI. Check for mistakes.
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.
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