From 59bf57bc90c5c9c009443ee4444c5e5253047dbe Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 18 Mar 2026 19:48:20 -0500 Subject: fix Hutch link from git.sr.ht to sr.ht --- Hutch/Views/Settings/SettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Hutch/Views/Settings') diff --git a/Hutch/Views/Settings/SettingsView.swift b/Hutch/Views/Settings/SettingsView.swift index 5bbded8..802e12d 100644 --- a/Hutch/Views/Settings/SettingsView.swift +++ b/Hutch/Views/Settings/SettingsView.swift @@ -695,7 +695,7 @@ private struct AboutView: View { Link(destination: URL(string: "https://man.sr.ht")!) { SwiftUI.Label("SourceHut Manuals", systemImage: "book") } - Link(destination: URL(string: "https://git.sr.ht/~ccleberg/Hutch")!) { + Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) { SwiftUI.Label("Project Repository", systemImage: "folder") } } -- cgit v1.2.3 From 01ce5ee73feb8bda35749a186c0efcd3d6d7a060 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 18 Mar 2026 19:52:01 -0500 Subject: use native markdown rendering for settings bio --- Hutch.xcodeproj/project.pbxproj | 8 ++++---- Hutch/Views/Settings/SettingsView.swift | 33 ++++++++++++++++++++++++++------- HutchTests/SettingsViewTests.swift | 22 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 HutchTests/SettingsViewTests.swift (limited to 'Hutch/Views/Settings') diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj index 3cd0646..6368778 100644 --- a/Hutch.xcodeproj/project.pbxproj +++ b/Hutch.xcodeproj/project.pbxproj @@ -363,7 +363,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -380,7 +380,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -399,7 +399,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -416,7 +416,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/Hutch/Views/Settings/SettingsView.swift b/Hutch/Views/Settings/SettingsView.swift index 802e12d..9b7d41f 100644 --- a/Hutch/Views/Settings/SettingsView.swift +++ b/Hutch/Views/Settings/SettingsView.swift @@ -1,6 +1,10 @@ import PhotosUI import SwiftUI +private let settingsBioMarkdownOptions = AttributedString.MarkdownParsingOptions( + interpretedSyntax: .inlineOnlyPreservingWhitespace +) + struct SettingsView: View { @Environment(AppState.self) private var appState @Environment(\.colorScheme) private var colorScheme @@ -163,13 +167,7 @@ struct SettingsView: View { Text("Bio") .font(.caption) .foregroundStyle(.secondary) - RenderedMarkupContentView( - content: .markdown(bio), - readmePath: nil, - colorScheme: colorScheme, - ownerCanonicalName: "", - repositoryName: "" - ) + SettingsBioView(markdown: bio) } } @@ -424,6 +422,27 @@ struct SettingsView: View { } } +private struct SettingsBioView: View { + let markdown: String + + var body: some View { + Text(settingsBioAttributedString(markdown)) + .frame(maxWidth: .infinity, alignment: .leading) + .tint(.accentColor) + .textSelection(.enabled) + } +} + +func settingsBioAttributedString(_ markdown: String) -> AttributedString { + guard let attributed = try? AttributedString( + markdown: markdown, + options: settingsBioMarkdownOptions + ) else { + return AttributedString(markdown) + } + return attributed +} + // MARK: - Edit Profile Sheet private struct EditProfileSheet: View { diff --git a/HutchTests/SettingsViewTests.swift b/HutchTests/SettingsViewTests.swift new file mode 100644 index 0000000..950c3dc --- /dev/null +++ b/HutchTests/SettingsViewTests.swift @@ -0,0 +1,22 @@ +import Foundation +import Testing +@testable import Hutch + +struct SettingsViewTests { + + @Test + @MainActor + func settingsBioAttributedStringPreservesInlineMarkdown() { + let attributed = settingsBioAttributedString("Hello **world** and [link](https://example.com)") + + #expect(String(attributed.characters).contains("Hello world and link")) + } + + @Test + @MainActor + func settingsBioAttributedStringFallsBackForInvalidMarkdown() { + let attributed = settingsBioAttributedString("[broken") + + #expect(String(attributed.characters) == "[broken") + } +} -- cgit v1.2.3 From 090c829a5f800266da7f4ff0e59fb389f0dbfdd8 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 18 Mar 2026 20:03:39 -0500 Subject: show tracker creation errors inside the creation sheet --- Hutch/Views/Settings/SettingsViewModel.swift | 4 +-- Hutch/Views/Tickets/TrackerListView.swift | 27 +++++++++++++++++- Hutch/Views/Tickets/TrackerListViewModel.swift | 19 ++++++++++++- HutchTests/SettingsViewModelTests.swift | 39 ++++++++++++++++++++++++++ HutchTests/TrackerListViewModelTests.swift | 16 +++++++++++ 5 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 HutchTests/SettingsViewModelTests.swift create mode 100644 HutchTests/TrackerListViewModelTests.swift (limited to 'Hutch/Views/Settings') diff --git a/Hutch/Views/Settings/SettingsViewModel.swift b/Hutch/Views/Settings/SettingsViewModel.swift index 204afbf..1cc5426 100644 --- a/Hutch/Views/Settings/SettingsViewModel.swift +++ b/Hutch/Views/Settings/SettingsViewModel.swift @@ -24,7 +24,7 @@ private struct CreateSSHKeyResponse: Decodable, Sendable { } private struct DeleteSSHKeyResponse: Decodable, Sendable { - let deleteSSHKey: DeleteResult + let deleteSSHKey: DeleteResult? } private struct CreatePGPKeyResponse: Decodable, Sendable { @@ -32,7 +32,7 @@ private struct CreatePGPKeyResponse: Decodable, Sendable { } private struct DeletePGPKeyResponse: Decodable, Sendable { - let deletePGPKey: DeleteResult + let deletePGPKey: DeleteResult? } private struct DeleteResult: Decodable, Sendable { diff --git a/Hutch/Views/Tickets/TrackerListView.swift b/Hutch/Views/Tickets/TrackerListView.swift index d16246b..ee253ec 100644 --- a/Hutch/Views/Tickets/TrackerListView.swift +++ b/Hutch/Views/Tickets/TrackerListView.swift @@ -124,10 +124,17 @@ private struct CreateTrackerSheet: View { let onCreated: (TrackerSummary) -> Void @Environment(\.dismiss) private var dismiss + @Bindable var viewModelBindable: TrackerListViewModel @State private var name = "" @State private var description = "" @State private var visibility: Visibility = .public + init(viewModel: TrackerListViewModel, onCreated: @escaping (TrackerSummary) -> Void) { + self.viewModel = viewModel + self._viewModelBindable = Bindable(viewModel) + self.onCreated = onCreated + } + var body: some View { NavigationStack { Form { @@ -143,12 +150,30 @@ private struct CreateTrackerSheet: View { Text("Private").tag(Visibility.private) } } + + if let error = viewModel.error { + Section { + Label { + Text(error) + } icon: { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(.red) + } + .foregroundStyle(.red) + } + } } .navigationTitle("New Tracker") .navigationBarTitleDisplayMode(.inline) + .onDisappear { + viewModelBindable.error = nil + } .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { dismiss() } + Button("Cancel") { + viewModelBindable.error = nil + dismiss() + } } ToolbarItem(placement: .confirmationAction) { Button { diff --git a/Hutch/Views/Tickets/TrackerListViewModel.swift b/Hutch/Views/Tickets/TrackerListViewModel.swift index 9704071..978a610 100644 --- a/Hutch/Views/Tickets/TrackerListViewModel.swift +++ b/Hutch/Views/Tickets/TrackerListViewModel.swift @@ -139,7 +139,7 @@ final class TrackerListViewModel { trackers.insert(tracker, at: 0) return tracker } catch { - self.error = "Couldn’t create the tracker. \(error.localizedDescription)" + self.error = trackerCreationErrorMessage(for: error) return nil } } @@ -163,4 +163,21 @@ final class TrackerListViewModel { private struct CreateTrackerResponse: Decodable, Sendable { let createTracker: TrackerSummary } + + private func trackerCreationErrorMessage(for error: Error) -> String { + let message: String + + if let srhtError = error as? SRHTError { + switch srhtError { + case .graphQLErrors(let errors): + message = errors.map(\.message).joined(separator: "\n") + default: + message = srhtError.localizedDescription + } + } else { + message = error.localizedDescription + } + + return "Couldn’t create the tracker. \(message)" + } } diff --git a/HutchTests/SettingsViewModelTests.swift b/HutchTests/SettingsViewModelTests.swift new file mode 100644 index 0000000..fc18162 --- /dev/null +++ b/HutchTests/SettingsViewModelTests.swift @@ -0,0 +1,39 @@ +import Foundation +import Testing +@testable import Hutch + +private struct DeletePGPKeyEnvelope: Decodable { + let deletePGPKey: DeleteResultPayload? +} + +private struct DeleteResultPayload: Decodable { + let id: Int? +} + +struct SettingsViewModelTests { + + @Test + @MainActor + func deletePGPKeyResponseDecodesNullPayloadWithGraphQLErrors() throws { + let json = """ + { + "errors": [ + { + "message": "PGP key ID 13629 is set as the user's preferred PGP key - it must be unset before removing the key" + } + ], + "data": { + "deletePGPKey": null + } + } + """ + + let decoded = try JSONDecoder().decode( + GraphQLResponse.self, + from: Data(json.utf8) + ) + + #expect(decoded.data?.deletePGPKey == nil) + #expect(decoded.errors?.first?.message.contains("preferred PGP key") == true) + } +} diff --git a/HutchTests/TrackerListViewModelTests.swift b/HutchTests/TrackerListViewModelTests.swift new file mode 100644 index 0000000..1caedea --- /dev/null +++ b/HutchTests/TrackerListViewModelTests.swift @@ -0,0 +1,16 @@ +import Foundation +import Testing +@testable import Hutch + +struct TrackerListViewModelTests { + + @Test + @MainActor + func graphQLErrorDescriptionIsPreservedForTrackerCreationFailures() { + let error = SRHTError.graphQLErrors([ + GraphQLError(message: "A tracker named bugs already exists", locations: nil) + ]) + + #expect(error.localizedDescription == "GraphQL error: A tracker named bugs already exists") + } +} -- cgit v1.2.3