Show release notes in-app instead of opening the web releases page
New "Read Release Notes" entries in the Help menu (current installed version) and the "Check for Updates" alert (the new, not-yet-installed version) render a release's markdown notes in a Confab modal. - UpdateCheckService.fetchReleaseNotes(forTag:) fetches a release's title + body from Gitea's public releases-by-tag API, caching the result by version tag in the settings table (a published release's notes don't change, so no need to refetch on every view). - ReleaseNotesView reuses the existing MarkdownContentView renderer; shows a friendly "not available yet" state for versions with no published Gitea release (e.g. a dev build ahead of the last release). - ReleaseNotesRequest carries which version to show atomically via .sheet(item:), per this project's established sheet-timing pattern. - Removed the redundant "Release Page" button from the update alert now that notes show in-app; added an explicit .keyboardShortcut (.cancelAction) to its cancel button so Escape actually closes it — role: .cancel alone didn't do it, since NSAlert only auto-binds Escape to a button literally titled "Cancel".
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// ReleaseNotesView.swift
|
||||
// Confab
|
||||
//
|
||||
// Shows a Gitea release's markdown notes in-app, instead of opening the web releases page
|
||||
//
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
//
|
||||
// This file is part of Confab.
|
||||
//
|
||||
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
|
||||
// You may use, study, modify, and share it for any noncommercial
|
||||
// purpose. Commercial use — including selling Confab or any part of
|
||||
// it, standalone or bundled into another product or service —
|
||||
// requires a separate commercial license from the copyright holder.
|
||||
//
|
||||
// See the LICENSE file or
|
||||
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
|
||||
// the full license text. For commercial licensing, contact Rune
|
||||
// Olsen via <https://confab.no>.
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ReleaseNotesView: View {
|
||||
let request: ReleaseNotesRequest
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State private var notes: ReleaseNotes?
|
||||
@State private var isLoading = true
|
||||
@State private var error: ReleaseNotesError?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView("Loading release notes...")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let error {
|
||||
errorView(error)
|
||||
} else if let notes {
|
||||
ScrollView {
|
||||
MarkdownContentView(content: notes.body, fontSize: 13)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(request.isCurrentlyInstalled ? "Release Notes" : "What's New")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 520, idealWidth: 600, minHeight: 480, idealHeight: 620)
|
||||
.task {
|
||||
await fetchNotes()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func errorView(_ error: ReleaseNotesError) -> some View {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "doc.text.magnifyingglass")
|
||||
.font(.system(size: 40))
|
||||
.foregroundStyle(.secondary)
|
||||
switch error {
|
||||
case .notFound:
|
||||
Text("No release notes are available yet for \(request.versionTag).")
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
case .networkError:
|
||||
Text("Couldn't load release notes. Check your internet connection and try again.")
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
Button("Retry") {
|
||||
Task { await fetchNotes() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
private func fetchNotes() async {
|
||||
isLoading = true
|
||||
error = nil
|
||||
switch await UpdateCheckService.shared.fetchReleaseNotes(forTag: request.versionTag) {
|
||||
case .success(let fetched):
|
||||
notes = fetched
|
||||
case .failure(let fetchError):
|
||||
error = fetchError
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user