-
Notifications
You must be signed in to change notification settings - Fork 28
feat: in-house What's New feature with GitHub release notes #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import Foundation | ||
|
|
||
| // MARK: - WhatsNew | ||
|
|
||
| /// Represents a "What's New" entry for a specific app version. | ||
| struct WhatsNew: Identifiable { | ||
| /// The app version this entry corresponds to. | ||
| let version: Version | ||
|
|
||
| /// The headline title displayed at the top of the sheet. | ||
| let title: String | ||
|
|
||
| /// The list of features to showcase (used for static/fallback entries). | ||
| let features: [Feature] | ||
|
|
||
| /// Markdown body from release notes (used for dynamic entries from GitHub). | ||
| let releaseNotes: String? | ||
|
|
||
| /// Optional URL to open when the user taps "Learn more". | ||
| let learnMoreURL: URL? | ||
|
|
||
| var id: Version { | ||
| self.version | ||
| } | ||
|
|
||
| init( | ||
| version: Version, | ||
| title: String, | ||
| features: [Feature] = [], | ||
| releaseNotes: String? = nil, | ||
| learnMoreURL: URL? = nil | ||
| ) { | ||
| self.version = version | ||
| self.title = title | ||
| self.features = features | ||
| self.releaseNotes = releaseNotes | ||
| self.learnMoreURL = learnMoreURL | ||
| } | ||
| } | ||
|
|
||
| // MARK: WhatsNew.Version | ||
|
|
||
| extension WhatsNew { | ||
| /// A semantic version with major, minor, and patch components. | ||
| struct Version: Hashable, Comparable, Codable, Sendable { | ||
| let major: Int | ||
| let minor: Int | ||
| let patch: Int | ||
|
|
||
| init(major: Int, minor: Int, patch: Int) { | ||
| self.major = major | ||
| self.minor = minor | ||
| self.patch = patch | ||
| } | ||
|
|
||
| /// Returns the minor-release version (patch set to 0). | ||
| var minorRelease: Version { | ||
| Version(major: self.major, minor: self.minor, patch: 0) | ||
| } | ||
|
|
||
| // MARK: Comparable | ||
|
|
||
| static func < (lhs: Version, rhs: Version) -> Bool { | ||
| if lhs.major != rhs.major { return lhs.major < rhs.major } | ||
| if lhs.minor != rhs.minor { return lhs.minor < rhs.minor } | ||
| return lhs.patch < rhs.patch | ||
| } | ||
|
|
||
| // MARK: CustomStringConvertible | ||
|
|
||
| var description: String { | ||
| "\(self.major).\(self.minor).\(self.patch)" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - WhatsNew.Version + ExpressibleByStringLiteral | ||
|
|
||
| extension WhatsNew.Version: ExpressibleByStringLiteral { | ||
| init(stringLiteral value: String) { | ||
| let parts = value.components(separatedBy: ".").compactMap(Int.init) | ||
| self.major = parts.indices.contains(0) ? parts[0] : 0 | ||
| self.minor = parts.indices.contains(1) ? parts[1] : 0 | ||
| self.patch = parts.indices.contains(2) ? parts[2] : 0 | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Version+current | ||
|
|
||
| extension WhatsNew.Version { | ||
| /// Retrieves the current app version from the main bundle's `CFBundleShortVersionString`. | ||
| static func current(in bundle: Bundle = .main) -> WhatsNew.Version { | ||
| let versionString = bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" | ||
| return .init(stringLiteral: versionString) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - WhatsNew.Feature | ||
|
|
||
| extension WhatsNew { | ||
| /// A single feature to display in the "What's New" sheet. | ||
| struct Feature: Hashable, Sendable { | ||
| /// SF Symbol name for the feature icon. | ||
| let icon: String | ||
|
|
||
| /// Short feature title. | ||
| let title: String | ||
|
|
||
| /// Longer description of the feature. | ||
| let subtitle: String | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import Foundation | ||
|
|
||
| // MARK: - WhatsNewVersionStore | ||
|
|
||
| /// Persists which app versions have had their "What's New" sheet presented. | ||
| struct WhatsNewVersionStore: @unchecked Sendable { | ||
| /// UserDefaults key prefix for presented versions. | ||
| private static let keyPrefix = "com.kaset.whatsNew.presented." | ||
|
|
||
| private let defaults: UserDefaults | ||
|
|
||
| init(defaults: UserDefaults = .standard) { | ||
| self.defaults = defaults | ||
| } | ||
|
|
||
| // MARK: - Read | ||
|
|
||
| /// Whether the given version has already been presented. | ||
| func hasPresented(_ version: WhatsNew.Version) -> Bool { | ||
| self.defaults.bool(forKey: Self.key(for: version)) | ||
| } | ||
|
|
||
| /// All versions that have been marked as presented. | ||
| var presentedVersions: [WhatsNew.Version] { | ||
| self.defaults.dictionaryRepresentation() | ||
| .keys | ||
| .filter { $0.hasPrefix(Self.keyPrefix) } | ||
| .map { key in | ||
| let versionString = String(key.dropFirst(Self.keyPrefix.count)) | ||
| return WhatsNew.Version(stringLiteral: versionString) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Write | ||
|
|
||
| /// Marks a version as presented so it won't be shown again. | ||
| func markPresented(_ version: WhatsNew.Version) { | ||
| self.defaults.set(true, forKey: Self.key(for: version)) | ||
| } | ||
|
|
||
| // MARK: - Private | ||
|
|
||
| private static func key(for version: WhatsNew.Version) -> String { | ||
| "\(self.keyPrefix)\(version.description)" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ struct MainWindow: View { | |||||||
| @Environment(WebKitManager.self) private var webKitManager | ||||||||
| @Environment(AccountService.self) private var accountService | ||||||||
| @Environment(\.showCommandBar) private var showCommandBar | ||||||||
| @Environment(\.showWhatsNew) private var showWhatsNew | ||||||||
|
|
||||||||
| /// Binding to navigation selection for keyboard shortcut control from parent. | ||||||||
| @Binding var navigationSelection: NavigationItem? | ||||||||
|
|
@@ -19,6 +20,7 @@ struct MainWindow: View { | |||||||
|
|
||||||||
| @State private var showLoginSheet = false | ||||||||
| @State private var showCommandBarSheet = false | ||||||||
| @State private var whatsNewToPresent: WhatsNew? | ||||||||
|
|
||||||||
| // MARK: - Cached ViewModels (persist across tab switches) | ||||||||
|
|
||||||||
|
|
@@ -106,6 +108,12 @@ struct MainWindow: View { | |||||||
| .sheet(isPresented: self.$showLoginSheet) { | ||||||||
| LoginSheet() | ||||||||
| } | ||||||||
| .sheet(item: self.$whatsNewToPresent) { whatsNew in | ||||||||
| WhatsNewView(whatsNew: whatsNew) { | ||||||||
| WhatsNewVersionStore().markPresented(whatsNew.version) | ||||||||
|
||||||||
| WhatsNewVersionStore().markPresented(whatsNew.version) | |
| let currentAppVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? whatsNew.version | |
| WhatsNewVersionStore().markPresented(currentAppVersion) |
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
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).
| let whatsNew = await WhatsNewProvider.fetchWhatsNew(for: version, store: WhatsNewVersionStore(defaults: .init())) | |
| let whatsNew = await WhatsNewProvider.fetchWhatsNew(for: version, store: WhatsNewVersionStore(defaults: .standard)) |
There was a problem hiding this comment.
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.helpif it’s meant to live under Help).