Commit 81520e73a1

81520e73a16455cb269b916e0866741ace2bc8fc

parent: c3d0407d15

Unsigned

cmc <hello@cleberg.net> · 2026-08-07 22:33 UTC

Adopt Swift 6 language mode

Flip SWIFT_VERSION to 6.0 (keeping SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor)
and resolve every resulting diagnostic. Mechanical, no behaviour change:

- Models: pure value types in Models/ and utility extensions (Date, DateFormatter,
  SRHTWebURL) marked nonisolated so the nonisolated networking layer can use them.
- App Intents: stored 'static var' (title/description/openAppWhenRun/
  typeDisplayRepresentation/defaultQuery/caseDisplayRepresentations) -> 'static let';
  drop the invalid @retroactive on the in-module LookupType: AppEnum.
- Sendable dicts: 'nil as String? as Any' -> 'nil as String? as any Sendable'.
- UserDefaults: retroactive @unchecked Sendable (documented thread-safe).
- WidgetKit completion handlers rebound nonisolated(unsafe) to cross into Task {};
  TipStoreViewModel task handle is @ObservationIgnored nonisolated(unsafe) for
  its nonisolated deinit.
- Tests run on @MainActor; a few constant fixtures marked nonisolated for use in
  @Sendable stub closures.

Builds and the full suite are clean in Swift 6 mode. Roadmap updated.

Layout: unified · split

Hutch.xcodeproj/project.pbxproj +8 −8
@@ -622,7 +622,7 @@
622 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; 622 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
623 SWIFT_EMIT_LOC_STRINGS = YES; 623 SWIFT_EMIT_LOC_STRINGS = YES;
624 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 624 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
625 SWIFT_VERSION = 5.0; 625 SWIFT_VERSION = 6.0;
626 TARGETED_DEVICE_FAMILY = "1,2"; 626 TARGETED_DEVICE_FAMILY = "1,2";
627 }; 627 };
628 name = Debug; 628 name = Debug;
@@ -659,7 +659,7 @@
659 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; 659 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
660 SWIFT_EMIT_LOC_STRINGS = YES; 660 SWIFT_EMIT_LOC_STRINGS = YES;
661 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 661 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
662 SWIFT_VERSION = 5.0; 662 SWIFT_VERSION = 6.0;
663 TARGETED_DEVICE_FAMILY = "1,2"; 663 TARGETED_DEVICE_FAMILY = "1,2";
664 }; 664 };
665 name = Release; 665 name = Release;
@@ -680,7 +680,7 @@
680 SWIFT_APPROACHABLE_CONCURRENCY = YES; 680 SWIFT_APPROACHABLE_CONCURRENCY = YES;
681 SWIFT_EMIT_LOC_STRINGS = NO; 681 SWIFT_EMIT_LOC_STRINGS = NO;
682 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 682 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
683 SWIFT_VERSION = 5.0; 683 SWIFT_VERSION = 6.0;
684 TARGETED_DEVICE_FAMILY = "1,2"; 684 TARGETED_DEVICE_FAMILY = "1,2";
685 TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch"; 685 TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
686 }; 686 };
@@ -702,7 +702,7 @@
702 SWIFT_APPROACHABLE_CONCURRENCY = YES; 702 SWIFT_APPROACHABLE_CONCURRENCY = YES;
703 SWIFT_EMIT_LOC_STRINGS = NO; 703 SWIFT_EMIT_LOC_STRINGS = NO;
704 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 704 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
705 SWIFT_VERSION = 5.0; 705 SWIFT_VERSION = 6.0;
706 TARGETED_DEVICE_FAMILY = "1,2"; 706 TARGETED_DEVICE_FAMILY = "1,2";
707 TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch"; 707 TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
708 }; 708 };
@@ -732,7 +732,7 @@
732 SWIFT_APPROACHABLE_CONCURRENCY = YES; 732 SWIFT_APPROACHABLE_CONCURRENCY = YES;
733 SWIFT_EMIT_LOC_STRINGS = YES; 733 SWIFT_EMIT_LOC_STRINGS = YES;
734 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 734 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
735 SWIFT_VERSION = 5.0; 735 SWIFT_VERSION = 6.0;
736 TARGETED_DEVICE_FAMILY = "1,2"; 736 TARGETED_DEVICE_FAMILY = "1,2";
737 }; 737 };
738 name = Debug; 738 name = Debug;
@@ -761,7 +761,7 @@
761 SWIFT_APPROACHABLE_CONCURRENCY = YES; 761 SWIFT_APPROACHABLE_CONCURRENCY = YES;
762 SWIFT_EMIT_LOC_STRINGS = YES; 762 SWIFT_EMIT_LOC_STRINGS = YES;
763 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 763 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
764 SWIFT_VERSION = 5.0; 764 SWIFT_VERSION = 6.0;
765 TARGETED_DEVICE_FAMILY = "1,2"; 765 TARGETED_DEVICE_FAMILY = "1,2";
766 }; 766 };
767 name = Release; 767 name = Release;
@@ -790,7 +790,7 @@
790 SWIFT_APPROACHABLE_CONCURRENCY = YES; 790 SWIFT_APPROACHABLE_CONCURRENCY = YES;
791 SWIFT_EMIT_LOC_STRINGS = YES; 791 SWIFT_EMIT_LOC_STRINGS = YES;
792 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 792 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
793 SWIFT_VERSION = 5.0; 793 SWIFT_VERSION = 6.0;
794 TARGETED_DEVICE_FAMILY = "1,2"; 794 TARGETED_DEVICE_FAMILY = "1,2";
795 }; 795 };
796 name = Debug; 796 name = Debug;
@@ -819,7 +819,7 @@
819 SWIFT_APPROACHABLE_CONCURRENCY = YES; 819 SWIFT_APPROACHABLE_CONCURRENCY = YES;
820 SWIFT_EMIT_LOC_STRINGS = YES; 820 SWIFT_EMIT_LOC_STRINGS = YES;
821 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 821 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
822 SWIFT_VERSION = 5.0; 822 SWIFT_VERSION = 6.0;
823 TARGETED_DEVICE_FAMILY = "1,2"; 823 TARGETED_DEVICE_FAMILY = "1,2";
824 }; 824 };
825 name = Release; 825 name = Release;
Hutch/App/HutchIntents.swift +38 −38
@@ -4,9 +4,9 @@ import Foundation
4// MARK: - Navigation Intents 4// MARK: - Navigation Intents
5 5
6struct OpenWorkQueueIntent: AppIntent { 6struct OpenWorkQueueIntent: AppIntent {
7 static var title: LocalizedStringResource = "Open Work Queue" 7 static let title: LocalizedStringResource = "Open Work Queue"
8 static var description = IntentDescription("Opens Hutch to your Work Queue.") 8 static let description = IntentDescription("Opens Hutch to your Work Queue.")
9 static var openAppWhenRun = true 9 static let openAppWhenRun = true
10 10
11 var route: HutchRoute { .workQueue(scope: .all) } 11 var route: HutchRoute { .workQueue(scope: .all) }
12 12
@@ -18,9 +18,9 @@ struct OpenWorkQueueIntent: AppIntent {
18} 18}
19 19
20struct OpenRecentActivityIntent: AppIntent { 20struct OpenRecentActivityIntent: AppIntent {
21 static var title: LocalizedStringResource = "Open Recent Activity" 21 static let title: LocalizedStringResource = "Open Recent Activity"
22 static var description = IntentDescription("Opens Hutch to recent activity.") 22 static let description = IntentDescription("Opens Hutch to recent activity.")
23 static var openAppWhenRun = true 23 static let openAppWhenRun = true
24 24
25 var route: HutchRoute { .recentActivity } 25 var route: HutchRoute { .recentActivity }
26 26
@@ -32,9 +32,9 @@ struct OpenRecentActivityIntent: AppIntent {
32} 32}
33 33
34struct OpenSystemStatusIntent: AppIntent { 34struct OpenSystemStatusIntent: AppIntent {
35 static var title: LocalizedStringResource = "Open System Status" 35 static let title: LocalizedStringResource = "Open System Status"
36 static var description = IntentDescription("Opens Hutch to SourceHut system status.") 36 static let description = IntentDescription("Opens Hutch to SourceHut system status.")
37 static var openAppWhenRun = true 37 static let openAppWhenRun = true
38 38
39 var route: HutchRoute { .systemStatus } 39 var route: HutchRoute { .systemStatus }
40 40
@@ -46,9 +46,9 @@ struct OpenSystemStatusIntent: AppIntent {
46} 46}
47 47
48struct OpenPinnedResourceIntent: AppIntent { 48struct OpenPinnedResourceIntent: AppIntent {
49 static var title: LocalizedStringResource = "Open Pinned Resource" 49 static let title: LocalizedStringResource = "Open Pinned Resource"
50 static var description = IntentDescription("Opens a pinned Hutch resource.") 50 static let description = IntentDescription("Opens a pinned Hutch resource.")
51 static var openAppWhenRun = true 51 static let openAppWhenRun = true
52 52
53 @Parameter(title: "Pinned Resource") 53 @Parameter(title: "Pinned Resource")
54 var pinnedResource: PinnedResourceEntity 54 var pinnedResource: PinnedResourceEntity
@@ -63,9 +63,9 @@ struct OpenPinnedResourceIntent: AppIntent {
63} 63}
64 64
65struct OpenProjectDashboardIntent: AppIntent { 65struct OpenProjectDashboardIntent: AppIntent {
66 static var title: LocalizedStringResource = "Open Project Dashboard" 66 static let title: LocalizedStringResource = "Open Project Dashboard"
67 static var description = IntentDescription("Opens a pinned project dashboard in Hutch.") 67 static let description = IntentDescription("Opens a pinned project dashboard in Hutch.")
68 static var openAppWhenRun = true 68 static let openAppWhenRun = true
69 69
70 @Parameter(title: "Project") 70 @Parameter(title: "Project")
71 var project: ProjectEntity 71 var project: ProjectEntity
@@ -84,16 +84,16 @@ struct OpenProjectDashboardIntent: AppIntent {
84enum HutchShortcutScope: String, AppEnum { 84enum HutchShortcutScope: String, AppEnum {
85 case all 85 case all
86 86
87 static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scope") 87 static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scope")
88 static var caseDisplayRepresentations: [HutchShortcutScope: DisplayRepresentation] = [ 88 static let caseDisplayRepresentations: [HutchShortcutScope: DisplayRepresentation] = [
89 .all: "All" 89 .all: "All"
90 ] 90 ]
91} 91}
92 92
93struct OpenFailedBuildsIntent: AppIntent { 93struct OpenFailedBuildsIntent: AppIntent {
94 static var title: LocalizedStringResource = "Open Failed Builds" 94 static let title: LocalizedStringResource = "Open Failed Builds"
95 static var description = IntentDescription("Opens Hutch to failed builds.") 95 static let description = IntentDescription("Opens Hutch to failed builds.")
96 static var openAppWhenRun = true 96 static let openAppWhenRun = true
97 97
98 @Parameter(title: "Scope", default: .all) 98 @Parameter(title: "Scope", default: .all)
99 var scope: HutchShortcutScope 99 var scope: HutchShortcutScope
@@ -108,9 +108,9 @@ struct OpenFailedBuildsIntent: AppIntent {
108} 108}
109 109
110struct OpenAssignedTicketsIntent: AppIntent { 110struct OpenAssignedTicketsIntent: AppIntent {
111 static var title: LocalizedStringResource = "Open Assigned Tickets" 111 static let title: LocalizedStringResource = "Open Assigned Tickets"
112 static var description = IntentDescription("Opens Hutch to tickets assigned to you.") 112 static let description = IntentDescription("Opens Hutch to tickets assigned to you.")
113 static var openAppWhenRun = true 113 static let openAppWhenRun = true
114 114
115 @Parameter(title: "Scope", default: .all) 115 @Parameter(title: "Scope", default: .all)
116 var scope: HutchShortcutScope 116 var scope: HutchShortcutScope
@@ -125,9 +125,9 @@ struct OpenAssignedTicketsIntent: AppIntent {
125} 125}
126 126
127struct SearchHutchIntent: AppIntent { 127struct SearchHutchIntent: AppIntent {
128 static var title: LocalizedStringResource = "Search Hutch" 128 static let title: LocalizedStringResource = "Search Hutch"
129 static var description = IntentDescription("Opens Hutch lookup with a search query.") 129 static let description = IntentDescription("Opens Hutch lookup with a search query.")
130 static var openAppWhenRun = true 130 static let openAppWhenRun = true
131 131
132 @Parameter(title: "Query") 132 @Parameter(title: "Query")
133 var query: String 133 var query: String
@@ -155,8 +155,8 @@ struct SearchHutchIntent: AppIntent {
155// MARK: - App Entities 155// MARK: - App Entities
156 156
157struct PinnedResourceEntity: AppEntity, Identifiable { 157struct PinnedResourceEntity: AppEntity, Identifiable {
158 static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pinned Resource") 158 static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pinned Resource")
159 static var defaultQuery = PinnedResourceQuery() 159 static let defaultQuery = PinnedResourceQuery()
160 160
161 let id: String 161 let id: String
162 let name: String 162 let name: String
@@ -181,8 +181,8 @@ struct PinnedResourceQuery: EntityQuery {
181} 181}
182 182
183struct ProjectEntity: AppEntity, Identifiable { 183struct ProjectEntity: AppEntity, Identifiable {
184 static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project") 184 static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")
185 static var defaultQuery = ProjectEntityQuery() 185 static let defaultQuery = ProjectEntityQuery()
186 186
187 let id: String 187 let id: String
188 let name: String 188 let name: String
@@ -278,8 +278,8 @@ enum HutchIntentEntityStore {
278// MARK: - Existing Read-Only Summary Intents 278// MARK: - Existing Read-Only Summary Intents
279 279
280struct CheckSystemStatusIntent: AppIntent { 280struct CheckSystemStatusIntent: AppIntent {
281 static var title: LocalizedStringResource = "Check SourceHut Status" 281 static let title: LocalizedStringResource = "Check SourceHut Status"
282 static var description = IntentDescription("Returns the current SourceHut system status.") 282 static let description = IntentDescription("Returns the current SourceHut system status.")
283 283
284 @MainActor 284 @MainActor
285 func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog { 285 func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
@@ -302,8 +302,8 @@ struct CheckSystemStatusIntent: AppIntent {
302} 302}
303 303
304struct CheckBuildsIntent: AppIntent { 304struct CheckBuildsIntent: AppIntent {
305 static var title: LocalizedStringResource = "Check Hutch Builds" 305 static let title: LocalizedStringResource = "Check Hutch Builds"
306 static var description = IntentDescription("Returns a summary of your recent build status.") 306 static let description = IntentDescription("Returns a summary of your recent build status.")
307 307
308 @MainActor 308 @MainActor
309 func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog { 309 func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
@@ -357,8 +357,8 @@ extension LookupType: AppEnum {
357// MARK: - Mutating Intents 357// MARK: - Mutating Intents
358 358
359struct ClearRecentActivityIntent: AppIntent { 359struct ClearRecentActivityIntent: AppIntent {
360 static var title: LocalizedStringResource = "Clear Recent Activity" 360 static let title: LocalizedStringResource = "Clear Recent Activity"
361 static var description = IntentDescription("Clears the Recent list on the Hutch Home tab.") 361 static let description = IntentDescription("Clears the Recent list on the Hutch Home tab.")
362 362
363 @MainActor 363 @MainActor
364 func perform() async throws -> some IntentResult & ProvidesDialog { 364 func perform() async throws -> some IntentResult & ProvidesDialog {
@@ -368,8 +368,8 @@ struct ClearRecentActivityIntent: AppIntent {
368} 368}
369 369
370struct UnpinResourceIntent: AppIntent { 370struct UnpinResourceIntent: AppIntent {
371 static var title: LocalizedStringResource = "Unpin Resource" 371 static let title: LocalizedStringResource = "Unpin Resource"
372 static var description = IntentDescription("Removes a pinned resource from the Hutch Home tab.") 372 static let description = IntentDescription("Removes a pinned resource from the Hutch Home tab.")
373 373
374 @Parameter(title: "Pinned Resource") 374 @Parameter(title: "Pinned Resource")
375 var pinnedResource: PinnedResourceEntity 375 var pinnedResource: PinnedResourceEntity
Hutch/Extensions/Date+Relative.swift +1 −1
@@ -1,6 +1,6 @@
1import Foundation 1import Foundation
2 2
3extension Date { 3nonisolated extension Date {
4 /// A short relative description like "2h ago", "3d ago", or "Jan 5, 2025". 4 /// A short relative description like "2h ago", "3d ago", or "Jan 5, 2025".
5 var relativeDescription: String { 5 var relativeDescription: String {
6 let formatter = RelativeDateTimeFormatter() 6 let formatter = RelativeDateTimeFormatter()
Hutch/Extensions/DateFormatter+SRHT.swift +1 −1
@@ -1,6 +1,6 @@
1import Foundation 1import Foundation
2 2
3extension DateFormatter { 3nonisolated extension DateFormatter {
4 /// Formatter for the sr.ht `Time` scalar: `%Y-%m-%dT%H:%M:%SZ` (UTC). 4 /// Formatter for the sr.ht `Time` scalar: `%Y-%m-%dT%H:%M:%SZ` (UTC).
5 static let srht: DateFormatter = { 5 static let srht: DateFormatter = {
6 let formatter = DateFormatter() 6 let formatter = DateFormatter()
Hutch/Extensions/Foundation+Sendable.swift added +7
@@ -0,0 +1,7 @@
1import Foundation
2
3// `UserDefaults` is documented as thread-safe but is not marked `Sendable` by
4// Foundation. Hutch passes it into account sessions and the various per-account
5// stores, all of which need to cross concurrency boundaries. The retroactive
6// unchecked conformance reflects the documented thread-safety.
7extension UserDefaults: @retroactive @unchecked Sendable {}
Hutch/Extensions/SRHTWebURL.swift +1 −1
@@ -1,6 +1,6 @@
1import Foundation 1import Foundation
2 2
3enum SRHTWebURL { 3nonisolated enum SRHTWebURL {
4 static let chat = URL(string: "https://chat.sr.ht")! 4 static let chat = URL(string: "https://chat.sr.ht")!
5 static let status = URL(string: "https://status.sr.ht")! 5 static let status = URL(string: "https://status.sr.ht")!
6 /// RSS/Atom feed for status incidents (HTML snapshot uses `status`). 6 /// RSS/Atom feed for status incidents (HTML snapshot uses `status`).
Hutch/Models/AccountEntry.swift +1 −1
@@ -1,7 +1,7 @@
1import Foundation 1import Foundation
2 2
3/// A stored sr.ht account (token + resolved username). 3/// A stored sr.ht account (token + resolved username).
4struct AccountEntry: Codable, Identifiable, Equatable, Sendable { 4nonisolated struct AccountEntry: Codable, Identifiable, Equatable, Sendable {
5 let id: String 5 let id: String
6 var username: String 6 var username: String
7 var token: String 7 var token: String
Hutch/Models/ArtifactInfo.swift +3 −3
@@ -1,7 +1,7 @@
1import Foundation 1import Foundation
2 2
3/// An artifact with its parent reference name, used in the artifacts tab. 3/// An artifact with its parent reference name, used in the artifacts tab.
4struct ArtifactInfo: Codable, Sendable, Identifiable { 4nonisolated struct ArtifactInfo: Codable, Sendable, Identifiable {
5 let id: Int 5 let id: Int
6 let filename: String 6 let filename: String
7 let checksum: String 7 let checksum: String
@@ -9,13 +9,13 @@ struct ArtifactInfo: Codable, Sendable, Identifiable {
9 let url: URL 9 let url: URL
10} 10}
11 11
12struct ArtifactPage: Codable, Sendable { 12nonisolated struct ArtifactPage: Codable, Sendable {
13 let results: [ArtifactInfo] 13 let results: [ArtifactInfo]
14 let cursor: String? 14 let cursor: String?
15} 15}
16 16
17/// A reference (tag) that has associated artifacts. 17/// A reference (tag) that has associated artifacts.
18struct ReferenceWithArtifacts: Codable, Sendable, Identifiable { 18nonisolated struct ReferenceWithArtifacts: Codable, Sendable, Identifiable {
19 var id: String { name } 19 var id: String { name }
20 let name: String 20 let name: String
21 let artifacts: [ArtifactInfo] 21 let artifacts: [ArtifactInfo]
Hutch/Models/Builds.swift +9 −9
@@ -3,7 +3,7 @@ import Foundation
3// MARK: - Enums 3// MARK: - Enums
4 4
5/// Status of a build job. 5/// Status of a build job.
6enum JobStatus: String, Codable, Sendable { 6nonisolated enum JobStatus: String, Codable, Sendable {
7 case pending = "PENDING" 7 case pending = "PENDING"
8 case queued = "QUEUED" 8 case queued = "QUEUED"
9 case running = "RUNNING" 9 case running = "RUNNING"
@@ -30,7 +30,7 @@ enum JobStatus: String, Codable, Sendable {
30} 30}
31 31
32/// Status of a single build task within a job. 32/// Status of a single build task within a job.
33enum TaskStatus: String, Codable, Sendable { 33nonisolated enum TaskStatus: String, Codable, Sendable {
34 case pending = "PENDING" 34 case pending = "PENDING"
35 case running = "RUNNING" 35 case running = "RUNNING"
36 case success = "SUCCESS" 36 case success = "SUCCESS"
@@ -41,7 +41,7 @@ enum TaskStatus: String, Codable, Sendable {
41// MARK: - Build Task 41// MARK: - Build Task
42 42
43/// A single task within a build job. 43/// A single task within a build job.
44struct BuildTask: Codable, Sendable, Identifiable, Equatable { 44nonisolated struct BuildTask: Codable, Sendable, Identifiable, Equatable {
45 private(set) var ordinal: Int? 45 private(set) var ordinal: Int?
46 let name: String 46 let name: String
47 let status: TaskStatus 47 let status: TaskStatus
@@ -74,7 +74,7 @@ struct BuildTask: Codable, Sendable, Identifiable, Equatable {
74// MARK: - Job Summary (for list view) 74// MARK: - Job Summary (for list view)
75 75
76/// Lightweight job model matching the fields returned by the jobs list query. 76/// Lightweight job model matching the fields returned by the jobs list query.
77struct JobSummary: Codable, Sendable, Identifiable, Hashable { 77nonisolated struct JobSummary: Codable, Sendable, Identifiable, Hashable {
78 let id: Int 78 let id: Int
79 let created: Date 79 let created: Date
80 let updated: Date 80 let updated: Date
@@ -103,7 +103,7 @@ struct JobSummary: Codable, Sendable, Identifiable, Hashable {
103} 103}
104 104
105/// Minimal task info for the list query. 105/// Minimal task info for the list query.
106struct JobTaskSummary: Codable, Sendable, Hashable { 106nonisolated struct JobTaskSummary: Codable, Sendable, Hashable {
107 let name: String 107 let name: String
108 let status: TaskStatus 108 let status: TaskStatus
109} 109}
@@ -111,7 +111,7 @@ struct JobTaskSummary: Codable, Sendable, Hashable {
111// MARK: - Job Detail (for detail view) 111// MARK: - Job Detail (for detail view)
112 112
113/// Full job model with all fields for the detail view. 113/// Full job model with all fields for the detail view.
114struct JobDetail: Codable, Sendable, Equatable { 114nonisolated struct JobDetail: Codable, Sendable, Equatable {
115 let id: Int 115 let id: Int
116 let created: Date 116 let created: Date
117 let updated: Date 117 let updated: Date
@@ -128,7 +128,7 @@ struct JobDetail: Codable, Sendable, Equatable {
128} 128}
129 129
130/// A downloadable artifact emitted by a build job. 130/// A downloadable artifact emitted by a build job.
131struct BuildArtifact: Codable, Sendable, Identifiable, Equatable { 131nonisolated struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
132 let id: Int 132 let id: Int
133 let created: Date 133 let created: Date
134 let path: String 134 let path: String
@@ -145,14 +145,14 @@ struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
145} 145}
146 146
147/// The log associated with a build job. 147/// The log associated with a build job.
148struct BuildLog: Codable, Sendable, Equatable { 148nonisolated struct BuildLog: Codable, Sendable, Equatable {
149 let fullURL: String 149 let fullURL: String
150} 150}
151 151
152// MARK: - Job Group 152// MARK: - Job Group
153 153
154/// A group of related build jobs. 154/// A group of related build jobs.
155struct JobGroup: Codable, Sendable, Identifiable { 155nonisolated struct JobGroup: Codable, Sendable, Identifiable {
156 let id: Int 156 let id: Int
157 let created: Date 157 let created: Date
158 let note: String? 158 let note: String?
Hutch/Models/CommitDetail.swift +8 −8
@@ -1,7 +1,7 @@
1import Foundation 1import Foundation
2 2
3/// Full commit detail returned by the revparse_single query. 3/// Full commit detail returned by the revparse_single query.
4struct CommitDetail: Codable, Sendable, Identifiable { 4nonisolated struct CommitDetail: Codable, Sendable, Identifiable {
5 let id: String 5 let id: String
6 let shortId: String 6 let shortId: String
7 let author: CommitAuthor 7 let author: CommitAuthor
@@ -26,39 +26,39 @@ struct CommitDetail: Codable, Sendable, Identifiable {
26 } 26 }
27} 27}
28 28
29struct CommitTrailer: Codable, Sendable, Identifiable { 29nonisolated struct CommitTrailer: Codable, Sendable, Identifiable {
30 var id: String { "\(name):\(value)" } 30 var id: String { "\(name):\(value)" }
31 let name: String 31 let name: String
32 let value: String 32 let value: String
33} 33}
34 34
35struct ParentCommit: Codable, Sendable, Identifiable, Hashable { 35nonisolated struct ParentCommit: Codable, Sendable, Identifiable, Hashable {
36 let id: String 36 let id: String
37 let shortId: String 37 let shortId: String
38 let author: ParentAuthor 38 let author: ParentAuthor
39} 39}
40 40
41struct ParentAuthor: Codable, Sendable, Hashable { 41nonisolated struct ParentAuthor: Codable, Sendable, Hashable {
42 let name: String 42 let name: String
43} 43}
44 44
45struct CommitTree: Codable, Sendable { 45nonisolated struct CommitTree: Codable, Sendable {
46 let entries: CommitTreeEntries 46 let entries: CommitTreeEntries
47} 47}
48 48
49struct CommitTreeEntries: Codable, Sendable { 49nonisolated struct CommitTreeEntries: Codable, Sendable {
50 let results: [CommitTreeEntry] 50 let results: [CommitTreeEntry]
51 let cursor: String? 51 let cursor: String?
52} 52}
53 53
54struct CommitTreeEntry: Codable, Sendable, Identifiable { 54nonisolated struct CommitTreeEntry: Codable, Sendable, Identifiable {
55 let id: String 55 let id: String
56 let name: String 56 let name: String
57 let mode: Int 57 let mode: Int
58 let object: CommitTreeObject? 58 let object: CommitTreeObject?
59} 59}
60 60
61struct CommitTreeObject: Codable, Sendable { 61nonisolated struct CommitTreeObject: Codable, Sendable {
62 let type: String? 62 let type: String?
63 let id: String? 63 let id: String?
64 let shortId: String? 64 let shortId: String?
Hutch/Models/CommitSummary.swift +2 −2
@@ -2,7 +2,7 @@ import Foundation
2 2
3/// Lightweight commit model for list views. Matches the subset of fields 3/// Lightweight commit model for list views. Matches the subset of fields
4/// returned by the repository log query. 4/// returned by the repository log query.
5struct CommitSummary: Codable, Sendable, Identifiable, Hashable { 5nonisolated struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
6 let id: String 6 let id: String
7 let shortId: String 7 let shortId: String
8 let author: CommitAuthor 8 let author: CommitAuthor
@@ -15,7 +15,7 @@ struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
15} 15}
16 16
17/// A compact author representation used in commit list responses. 17/// A compact author representation used in commit list responses.
18struct CommitAuthor: Codable, Sendable, Hashable { 18nonisolated struct CommitAuthor: Codable, Sendable, Hashable {
19 let name: String 19 let name: String
20 let email: String? 20 let email: String?
21 let time: Date 21 let time: Date
Hutch/Models/ContributionCalendar.swift +14 −14
@@ -1,6 +1,6 @@
1import Foundation 1import Foundation
2 2
3struct ContributionCalendarResponse: Decodable, Sendable, Hashable { 3nonisolated struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
4 let actor: String 4 let actor: String
5 let from: Date 5 let from: Date
6 let to: Date 6 let to: Date
@@ -9,7 +9,7 @@ struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
9 let indexingState: ContributionIndexingState 9 let indexingState: ContributionIndexingState
10 let days: [ContributionDay] 10 let days: [ContributionDay]
11 11
12 enum CodingKeys: String, CodingKey { 12 nonisolated enum CodingKeys: String, CodingKey {
13 case actor 13 case actor
14 case from 14 case from
15 case to 15 case to
@@ -59,14 +59,14 @@ struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
59 } 59 }
60} 60}
61 61
62struct ContributionDay: Decodable, Sendable, Hashable, Identifiable { 62nonisolated struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
63 var id: Date { date } 63 var id: Date { date }
64 64
65 let date: Date 65 let date: Date
66 let count: Int 66 let count: Int
67 let score: Double 67 let score: Double
68 68
69 enum CodingKeys: String, CodingKey { 69 nonisolated enum CodingKeys: String, CodingKey {
70 case date 70 case date
71 case count 71 case count
72 case score 72 case score
@@ -90,7 +90,7 @@ struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
90 } 90 }
91} 91}
92 92
93struct ContributionStatsResponse: Decodable, Sendable, Hashable { 93nonisolated struct ContributionStatsResponse: Decodable, Sendable, Hashable {
94 let actor: String 94 let actor: String
95 let from: Date 95 let from: Date
96 let to: Date 96 let to: Date
@@ -103,7 +103,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
103 let longestStreak: Int 103 let longestStreak: Int
104 let currentStreak: Int 104 let currentStreak: Int
105 105
106 enum CodingKeys: String, CodingKey { 106 nonisolated enum CodingKeys: String, CodingKey {
107 case actor 107 case actor
108 case from 108 case from
109 case to 109 case to
@@ -117,7 +117,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
117 case currentStreak = "current_streak" 117 case currentStreak = "current_streak"
118 } 118 }
119 119
120 struct StatsWindow: Sendable, Hashable { 120 nonisolated struct StatsWindow: Sendable, Hashable {
121 let actor: String 121 let actor: String
122 let from: Date 122 let from: Date
123 let to: Date 123 let to: Date
@@ -126,7 +126,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
126 let indexingState: ContributionIndexingState 126 let indexingState: ContributionIndexingState
127 } 127 }
128 128
129 struct StatsTotals: Sendable, Hashable { 129 nonisolated struct StatsTotals: Sendable, Hashable {
130 let totalEvents: Int 130 let totalEvents: Int
131 let totalScore: Double 131 let totalScore: Double
132 let activeDays: Int 132 let activeDays: Int
@@ -181,13 +181,13 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
181 } 181 }
182} 182}
183 183
184enum ContributionIndexingState: String, Codable, Sendable, Hashable { 184nonisolated enum ContributionIndexingState: String, Codable, Sendable, Hashable {
185 case pending 185 case pending
186 case indexed 186 case indexed
187 case error 187 case error
188} 188}
189 189
190enum ContributionIntensity: Int, Sendable, CaseIterable { 190nonisolated enum ContributionIntensity: Int, Sendable, CaseIterable {
191 case empty = 0 191 case empty = 0
192 case level1 = 1 192 case level1 = 1
193 case level2 = 2 193 case level2 = 2
@@ -210,12 +210,12 @@ enum ContributionIntensity: Int, Sendable, CaseIterable {
210 } 210 }
211} 211}
212 212
213struct ContributionWeek: Sendable, Hashable { 213nonisolated struct ContributionWeek: Sendable, Hashable {
214 let startDate: Date 214 let startDate: Date
215 let days: [ContributionDay] 215 let days: [ContributionDay]
216} 216}
217 217
218enum ContributionCalendarLayout { 218nonisolated enum ContributionCalendarLayout {
219 static func weekColumns( 219 static func weekColumns(
220 from days: [ContributionDay], 220 from days: [ContributionDay],
221 calendar: Calendar = .contributionCalendar 221 calendar: Calendar = .contributionCalendar
@@ -241,7 +241,7 @@ enum ContributionCalendarLayout {
241 } 241 }
242} 242}
243 243
244enum ContributionDateParser { 244nonisolated enum ContributionDateParser {
245 static func parse(_ rawValue: String) -> Date? { 245 static func parse(_ rawValue: String) -> Date? {
246 let parts = rawValue.split(separator: "-", omittingEmptySubsequences: false) 246 let parts = rawValue.split(separator: "-", omittingEmptySubsequences: false)
247 guard 247 guard
@@ -348,7 +348,7 @@ enum ContributionDateParser {
348 } 348 }
349} 349}
350 350
351extension Calendar { 351nonisolated extension Calendar {
352 static var contributionCalendar: Calendar { 352 static var contributionCalendar: Calendar {
353 var calendar = Calendar(identifier: .gregorian) 353 var calendar = Calendar(identifier: .gregorian)
354 calendar.firstWeekday = 1 354 calendar.firstWeekday = 1
Hutch/Models/Git.swift +20 −20
@@ -3,14 +3,14 @@ import Foundation
3// MARK: - Enums 3// MARK: - Enums
4 4
5/// Repository visibility level. 5/// Repository visibility level.
6enum Visibility: String, Codable, Sendable { 6nonisolated enum Visibility: String, Codable, Sendable {
7 case publicVisibility = "PUBLIC" 7 case publicVisibility = "PUBLIC"
8 case unlisted = "UNLISTED" 8 case unlisted = "UNLISTED"
9 case privateVisibility = "PRIVATE" 9 case privateVisibility = "PRIVATE"
10} 10}
11 11
12/// Repository access mode. 12/// Repository access mode.
13enum AccessMode: String, Codable, Sendable, CaseIterable { 13nonisolated enum AccessMode: String, Codable, Sendable, CaseIterable {
14 case ro = "RO" 14 case ro = "RO"
15 case rw = "RW" 15 case rw = "RW"
16} 16}
@@ -19,14 +19,14 @@ enum AccessMode: String, Codable, Sendable, CaseIterable {
19 19
20/// The `Entity` GraphQL interface from git.sr.ht. Represents the owner of a 20/// The `Entity` GraphQL interface from git.sr.ht. Represents the owner of a
21/// resource (typically a user). 21/// resource (typically a user).
22struct Entity: Codable, Sendable, Hashable { 22nonisolated struct Entity: Codable, Sendable, Hashable {
23 let canonicalName: String 23 let canonicalName: String
24} 24}
25 25
26// MARK: - Repository 26// MARK: - Repository
27 27
28/// A git repository from git.sr.ht. 28/// A git repository from git.sr.ht.
29struct Repository: Codable, Sendable, Identifiable { 29nonisolated struct Repository: Codable, Sendable, Identifiable {
30 let id: Int 30 let id: Int
31 let created: Date 31 let created: Date
32 let updated: Date 32 let updated: Date
@@ -37,7 +37,7 @@ struct Repository: Codable, Sendable, Identifiable {
37 let accessMode: AccessMode 37 let accessMode: AccessMode
38 let owner: Entity 38 let owner: Entity
39 39
40 enum CodingKeys: String, CodingKey { 40 nonisolated enum CodingKeys: String, CodingKey {
41 case id, created, updated, name, description, visibility, readme 41 case id, created, updated, name, description, visibility, readme
42 case accessMode = "access" 42 case accessMode = "access"
43 case owner 43 case owner
@@ -47,7 +47,7 @@ struct Repository: Codable, Sendable, Identifiable {
47// MARK: - Signature 47// MARK: - Signature
48 48
49/// A Git commit/tag signature (author or committer). 49/// A Git commit/tag signature (author or committer).
50struct Signature: Codable, Sendable { 50nonisolated struct Signature: Codable, Sendable {
51 let name: String 51 let name: String
52 let email: String 52 let email: String
53 let time: Date 53 let time: Date
@@ -56,7 +56,7 @@ struct Signature: Codable, Sendable {
56// MARK: - Trailer 56// MARK: - Trailer
57 57
58/// A Git commit trailer (e.g. "Signed-off-by", "Co-authored-by"). 58/// A Git commit trailer (e.g. "Signed-off-by", "Co-authored-by").
59struct Trailer: Codable, Sendable { 59nonisolated struct Trailer: Codable, Sendable {
60 let name: String 60 let name: String
61 let value: String 61 let value: String
62} 62}
@@ -64,7 +64,7 @@ struct Trailer: Codable, Sendable {
64// MARK: - Commit 64// MARK: - Commit
65 65
66/// A Git commit from git.sr.ht. 66/// A Git commit from git.sr.ht.
67struct Commit: Codable, Sendable, Identifiable { 67nonisolated struct Commit: Codable, Sendable, Identifiable {
68 let id: String 68 let id: String
69 let shortId: String 69 let shortId: String
70 let author: Signature 70 let author: Signature
@@ -77,13 +77,13 @@ struct Commit: Codable, Sendable, Identifiable {
77// MARK: - Reference 77// MARK: - Reference
78 78
79/// A Git reference (branch or tag name). 79/// A Git reference (branch or tag name).
80struct Reference: Codable, Sendable, Hashable { 80nonisolated struct Reference: Codable, Sendable, Hashable {
81 let name: String 81 let name: String
82 let target: String? 82 let target: String?
83} 83}
84 84
85/// A Git reference enriched with the date of its tip commit or tag, for display in the Refs tab. 85/// A Git reference enriched with the date of its tip commit or tag, for display in the Refs tab.
86struct ReferenceDetail: Sendable, Hashable { 86nonisolated struct ReferenceDetail: Sendable, Hashable {
87 let name: String 87 let name: String
88 let target: String? 88 let target: String?
89 let date: Date? 89 let date: Date?
@@ -92,7 +92,7 @@ struct ReferenceDetail: Sendable, Hashable {
92// MARK: - TreeEntry 92// MARK: - TreeEntry
93 93
94/// An entry in a Git tree (file or directory). 94/// An entry in a Git tree (file or directory).
95struct TreeEntry: Codable, Sendable, Identifiable { 95nonisolated struct TreeEntry: Codable, Sendable, Identifiable {
96 let id: String 96 let id: String
97 let name: String 97 let name: String
98 let mode: Int? 98 let mode: Int?
@@ -104,41 +104,41 @@ struct TreeEntry: Codable, Sendable, Identifiable {
104/// Both TextBlob and BinaryBlob have type == "BLOB"; they are 104/// Both TextBlob and BinaryBlob have type == "BLOB"; they are
105/// distinguished by the presence of the "text" key (TextBlob) vs 105/// distinguished by the presence of the "text" key (TextBlob) vs
106/// the "content" key (BinaryBlob). 106/// the "content" key (BinaryBlob).
107enum GitObject: Sendable { 107nonisolated enum GitObject: Sendable {
108 case tree(GitTree) 108 case tree(GitTree)
109 case textBlob(GitTextBlob) 109 case textBlob(GitTextBlob)
110 case binaryBlob(GitBinaryBlob) 110 case binaryBlob(GitBinaryBlob)
111 case unknown 111 case unknown
112} 112}
113 113
114struct GitTree: Codable, Sendable { 114nonisolated struct GitTree: Codable, Sendable {
115 let id: String? 115 let id: String?
116 let shortId: String? 116 let shortId: String?
117 let entries: GitTreeEntryPage? 117 let entries: GitTreeEntryPage?
118} 118}
119 119
120struct GitTextBlob: Codable, Sendable { 120nonisolated struct GitTextBlob: Codable, Sendable {
121 let id: String? 121 let id: String?
122 let shortId: String? 122 let shortId: String?
123 let text: String? 123 let text: String?
124 let size: Int? 124 let size: Int?
125} 125}
126 126
127struct GitBinaryBlob: Codable, Sendable { 127nonisolated struct GitBinaryBlob: Codable, Sendable {
128 let id: String? 128 let id: String?
129 let shortId: String? 129 let shortId: String?
130 let size: Int? 130 let size: Int?
131 let content: String? 131 let content: String?
132} 132}
133 133
134struct GitTreeEntryPage: Codable, Sendable { 134nonisolated struct GitTreeEntryPage: Codable, Sendable {
135 let results: [TreeEntry] 135 let results: [TreeEntry]
136 let cursor: String? 136 let cursor: String?
137} 137}
138 138
139// MARK: - GitObject Codable 139// MARK: - GitObject Codable
140 140
141extension GitObject: Codable { 141nonisolated extension GitObject: Codable {
142 private enum CodingKeys: String, CodingKey { 142 private enum CodingKeys: String, CodingKey {
143 case type, id, shortId, entries, text, size, content 143 case type, id, shortId, entries, text, size, content
144 case typename = "__typename" 144 case typename = "__typename"
@@ -211,7 +211,7 @@ extension GitObject: Codable {
211} 211}
212 212
213/// Convenience helpers for checking object type. 213/// Convenience helpers for checking object type.
214extension GitObject { 214nonisolated extension GitObject {
215 var isTree: Bool { 215 var isTree: Bool {
216 if case .tree = self { return true } 216 if case .tree = self { return true }
217 return false 217 return false
@@ -230,7 +230,7 @@ extension GitObject {
230// MARK: - Tag 230// MARK: - Tag
231 231
232/// An annotated Git tag from git.sr.ht. 232/// An annotated Git tag from git.sr.ht.
233struct Tag: Codable, Sendable, Identifiable { 233nonisolated struct Tag: Codable, Sendable, Identifiable {
234 let id: String 234 let id: String
235 let shortId: String 235 let shortId: String
236 let name: String 236 let name: String
@@ -241,7 +241,7 @@ struct Tag: Codable, Sendable, Identifiable {
241// MARK: - Artifact 241// MARK: - Artifact
242 242
243/// A release artifact attached to a Git tag. 243/// A release artifact attached to a Git tag.
244struct Artifact: Codable, Sendable, Identifiable { 244nonisolated struct Artifact: Codable, Sendable, Identifiable {
245 let id: Int 245 let id: Int
246 let created: Date 246 let created: Date
247 let filename: String 247 let filename: String
Hutch/Models/Inbox.swift +10 −10
@@ -1,6 +1,6 @@
1import Foundation 1import Foundation
2 2
3struct InboxThreadSummary: Identifiable, Hashable, Sendable { 3nonisolated struct InboxThreadSummary: Identifiable, Hashable, Sendable {
4 let rootEmailID: Int 4 let rootEmailID: Int
5 let rootMessageID: String 5 let rootMessageID: String
6 let threadRootEmailIDs: [Int] 6 let threadRootEmailIDs: [Int]
@@ -76,7 +76,7 @@ struct InboxThreadSummary: Identifiable, Hashable, Sendable {
76 } 76 }
77} 77}
78 78
79struct InboxMessage: Identifiable, Hashable, Sendable { 79nonisolated struct InboxMessage: Identifiable, Hashable, Sendable {
80 let id: Int 80 let id: Int
81 let author: Entity 81 let author: Entity
82 let date: Date 82 let date: Date
@@ -89,12 +89,12 @@ struct InboxMessage: Identifiable, Hashable, Sendable {
89 let rawMessageURL: URL? 89 let rawMessageURL: URL?
90} 90}
91 91
92enum InboxMessageContentBlock: Hashable, Sendable { 92nonisolated enum InboxMessageContentBlock: Hashable, Sendable {
93 case plainText(String) 93 case plainText(String)
94 case diff(String) 94 case diff(String)
95} 95}
96 96
97struct InboxThreadDetail: Sendable { 97nonisolated struct InboxThreadDetail: Sendable {
98 let id: String 98 let id: String
99 let rootEmailID: Int 99 let rootEmailID: Int
100 let rootMessageID: String 100 let rootMessageID: String
@@ -114,7 +114,7 @@ struct InboxThreadDetail: Sendable {
114 } 114 }
115} 115}
116 116
117extension InboxThreadDetail { 117nonisolated extension InboxThreadDetail {
118 var replyRecipient: String { 118 var replyRecipient: String {
119 "\(listOwner.canonicalName)/\(listName)@lists.sr.ht" 119 "\(listOwner.canonicalName)/\(listName)@lists.sr.ht"
120 } 120 }
@@ -144,7 +144,7 @@ extension InboxThreadDetail {
144 } 144 }
145} 145}
146 146
147struct MailComposeDraft: Sendable { 147nonisolated struct MailComposeDraft: Sendable {
148 let recipients: [String] 148 let recipients: [String]
149 let ccRecipients: [String] 149 let ccRecipients: [String]
150 let subject: String 150 let subject: String
@@ -155,20 +155,20 @@ struct MailComposeDraft: Sendable {
155 } 155 }
156} 156}
157 157
158extension MailComposeDraft: Identifiable {} 158nonisolated extension MailComposeDraft: Identifiable {}
159 159
160struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable { 160nonisolated struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable {
161 let id: Int 161 let id: Int
162 let rid: String 162 let rid: String
163 let name: String 163 let name: String
164 let owner: Entity 164 let owner: Entity
165} 165}
166 166
167struct InboxPatchPreview: Decodable, Sendable, Hashable { 167nonisolated struct InboxPatchPreview: Decodable, Sendable, Hashable {
168 let subject: String? 168 let subject: String?
169} 169}
170 170
171enum InboxReadStateStore { 171nonisolated enum InboxReadStateStore {
172 private static let key = "InboxThreadLastViewed" 172 private static let key = "InboxThreadLastViewed"
173 private static let baselineKey = "InboxUnreadBaseline" 173 private static let baselineKey = "InboxUnreadBaseline"
174 174
Hutch/Models/Meta.swift +8 −8
@@ -3,7 +3,7 @@ import Foundation
3// MARK: - User Profile (full) 3// MARK: - User Profile (full)
4 4
5/// Extended user profile from meta.sr.ht with all fields from the `me` query. 5/// Extended user profile from meta.sr.ht with all fields from the `me` query.
6struct UserProfile: Codable, Sendable { 6nonisolated struct UserProfile: Codable, Sendable {
7 let username: String 7 let username: String
8 let canonicalName: String 8 let canonicalName: String
9 let email: String 9 let email: String
@@ -20,7 +20,7 @@ struct UserProfile: Codable, Sendable {
20 20
21// MARK: - SSH Key 21// MARK: - SSH Key
22 22
23struct SSHKey: Codable, Sendable, Identifiable { 23nonisolated struct SSHKey: Codable, Sendable, Identifiable {
24 let id: Int 24 let id: Int
25 let comment: String? 25 let comment: String?
26 let created: Date 26 let created: Date
@@ -34,27 +34,27 @@ struct SSHKey: Codable, Sendable, Identifiable {
34 } 34 }
35} 35}
36 36
37struct SSHKeyPage: Codable, Sendable { 37nonisolated struct SSHKeyPage: Codable, Sendable {
38 let results: [SSHKey] 38 let results: [SSHKey]
39 let cursor: String? 39 let cursor: String?
40} 40}
41 41
42// MARK: - PGP Key 42// MARK: - PGP Key
43 43
44struct PGPKey: Codable, Sendable, Identifiable { 44nonisolated struct PGPKey: Codable, Sendable, Identifiable {
45 let id: Int 45 let id: Int
46 let fingerprint: String 46 let fingerprint: String
47 let created: Date 47 let created: Date
48} 48}
49 49
50struct PGPKeyPage: Codable, Sendable { 50nonisolated struct PGPKeyPage: Codable, Sendable {
51 let results: [PGPKey] 51 let results: [PGPKey]
52 let cursor: String? 52 let cursor: String?
53} 53}
54 54
55// MARK: - Subscription 55// MARK: - Subscription
56 56
57struct Subscription: Codable, Sendable { 57nonisolated struct Subscription: Codable, Sendable {
58 let status: String? 58 let status: String?
59 let autorenew: Bool? 59 let autorenew: Bool?
60 let interval: String? 60 let interval: String?
@@ -62,7 +62,7 @@ struct Subscription: Codable, Sendable {
62 62
63// MARK: - Personal Access Token 63// MARK: - Personal Access Token
64 64
65struct PersonalAccessToken: Codable, Sendable, Identifiable { 65nonisolated struct PersonalAccessToken: Codable, Sendable, Identifiable {
66 let id: Int 66 let id: Int
67 let issued: Date 67 let issued: Date
68 let expires: Date? 68 let expires: Date?
@@ -72,7 +72,7 @@ struct PersonalAccessToken: Codable, Sendable, Identifiable {
72 72
73/// One entry in meta.sr.ht's audit log: a security-relevant action on the 73/// One entry in meta.sr.ht's audit log: a security-relevant action on the
74/// account, with the address it came from. 74/// account, with the address it came from.
75struct AuditLogEntry: Codable, Sendable, Identifiable { 75nonisolated struct AuditLogEntry: Codable, Sendable, Identifiable {
76 let id: Int 76 let id: Int
77 let created: Date 77 let created: Date
78 let ipAddress: String 78 let ipAddress: String
Hutch/Models/Paste.swift +3 −3
@@ -1,6 +1,6 @@
1import Foundation 1import Foundation
2 2
3struct PasteFile: Codable, Sendable, Hashable, Identifiable { 3nonisolated struct PasteFile: Codable, Sendable, Hashable, Identifiable {
4 let filename: String? 4 let filename: String?
5 let hash: String 5 let hash: String
6 let contents: URL? 6 let contents: URL?
@@ -8,7 +8,7 @@ struct PasteFile: Codable, Sendable, Hashable, Identifiable {
8 var id: String { hash } 8 var id: String { hash }
9} 9}
10 10
11struct Paste: Codable, Sendable, Identifiable, Hashable { 11nonisolated struct Paste: Codable, Sendable, Identifiable, Hashable {
12 let id: String 12 let id: String
13 let created: Date 13 let created: Date
14 let visibility: Visibility 14 let visibility: Visibility
@@ -16,7 +16,7 @@ struct Paste: Codable, Sendable, Identifiable, Hashable {
16 let user: Entity 16 let user: Entity
17} 17}
18 18
19struct PasteUploadDraft: Identifiable, Equatable, Sendable { 19nonisolated struct PasteUploadDraft: Identifiable, Equatable, Sendable {
20 let id: UUID 20 let id: UUID
21 var filename: String 21 var filename: String
22 var contents: String 22 var contents: String
Hutch/Models/Patchset.swift +6 −6
@@ -1,7 +1,7 @@
1import Foundation 1import Foundation
2 2
3/// Review state of a patchset on lists.sr.ht. 3/// Review state of a patchset on lists.sr.ht.
4enum PatchsetStatus: String, Codable, Sendable, CaseIterable { 4nonisolated enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
5 case unknown = "UNKNOWN" 5 case unknown = "UNKNOWN"
6 case proposed = "PROPOSED" 6 case proposed = "PROPOSED"
7 case needsRevision = "NEEDS_REVISION" 7 case needsRevision = "NEEDS_REVISION"
@@ -55,7 +55,7 @@ enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
55/// A patchset as it appears in a mailing list listing, derived from the thread's 55/// A patchset as it appears in a mailing list listing, derived from the thread's
56/// root email rather than a dedicated patchsets query — `MailingList` exposes no 56/// root email rather than a dedicated patchsets query — `MailingList` exposes no
57/// such field. 57/// such field.
58struct PatchsetSummary: Identifiable, Hashable, Sendable { 58nonisolated struct PatchsetSummary: Identifiable, Hashable, Sendable {
59 let id: Int 59 let id: Int
60 let subject: String 60 let subject: String
61 let version: Int 61 let version: Int
@@ -70,7 +70,7 @@ struct PatchsetSummary: Identifiable, Hashable, Sendable {
70} 70}
71 71
72/// One email within a patchset: either the cover letter or a single patch. 72/// One email within a patchset: either the cover letter or a single patch.
73struct PatchsetEmail: Identifiable, Hashable, Sendable { 73nonisolated struct PatchsetEmail: Identifiable, Hashable, Sendable {
74 let id: Int 74 let id: Int
75 let subject: String 75 let subject: String
76 let date: Date? 76 let date: Date?
@@ -88,13 +88,13 @@ struct PatchsetEmail: Identifiable, Hashable, Sendable {
88} 88}
89 89
90/// A build or check reported against a patchset. 90/// A build or check reported against a patchset.
91struct PatchsetToolResult: Identifiable, Hashable, Sendable { 91nonisolated struct PatchsetToolResult: Identifiable, Hashable, Sendable {
92 let id: Int 92 let id: Int
93 let icon: PatchsetToolIcon 93 let icon: PatchsetToolIcon
94 let details: String 94 let details: String
95} 95}
96 96
97enum PatchsetToolIcon: String, Codable, Sendable { 97nonisolated enum PatchsetToolIcon: String, Codable, Sendable {
98 case pending = "PENDING" 98 case pending = "PENDING"
99 case waiting = "WAITING" 99 case waiting = "WAITING"
100 case success = "SUCCESS" 100 case success = "SUCCESS"
@@ -112,7 +112,7 @@ enum PatchsetToolIcon: String, Codable, Sendable {
112} 112}
113 113
114/// A patchset with its cover letter, patches, and review context. 114/// A patchset with its cover letter, patches, and review context.
115struct PatchsetDetail: Sendable { 115nonisolated struct PatchsetDetail: Sendable {
116 let id: Int 116 let id: Int
117 let created: Date 117 let created: Date
118 let updated: Date 118 let updated: Date
Hutch/Models/Project.swift +12 −12
@@ -1,7 +1,7 @@
1import Foundation 1import Foundation
2 2
3struct Project: Identifiable, Hashable, Sendable { 3nonisolated struct Project: Identifiable, Hashable, Sendable {
4 struct MailingList: Identifiable, Hashable, Sendable { 4 nonisolated struct MailingList: Identifiable, Hashable, Sendable {
5 let id: String 5 let id: String
6 let name: String 6 let name: String
7 let description: String? 7 let description: String?
@@ -22,8 +22,8 @@ struct Project: Identifiable, Hashable, Sendable {
22 } 22 }
23 } 23 }
24 24
25 struct SourceRepo: Identifiable, Hashable, Sendable { 25 nonisolated struct SourceRepo: Identifiable, Hashable, Sendable {
26 enum RepoType: String, Decodable, Sendable { 26 nonisolated enum RepoType: String, Decodable, Sendable {
27 case git = "GIT" 27 case git = "GIT"
28 case hg = "HG" 28 case hg = "HG"
29 29
@@ -47,7 +47,7 @@ struct Project: Identifiable, Hashable, Sendable {
47 } 47 }
48 } 48 }
49 49
50 struct Tracker: Identifiable, Hashable, Sendable { 50 nonisolated struct Tracker: Identifiable, Hashable, Sendable {
51 let id: String 51 let id: String
52 let name: String 52 let name: String
53 let description: String? 53 let description: String?
@@ -72,7 +72,7 @@ struct Project: Identifiable, Hashable, Sendable {
72 let isFullyLoaded: Bool 72 let isFullyLoaded: Bool
73 73
74 /// Identity and display fields for a project. 74 /// Identity and display fields for a project.
75 struct Metadata: Sendable, Hashable { 75 nonisolated struct Metadata: Sendable, Hashable {
76 let id: String 76 let id: String
77 let name: String 77 let name: String
78 let description: String? 78 let description: String?
@@ -82,7 +82,7 @@ struct Project: Identifiable, Hashable, Sendable {
82 let updated: Date 82 let updated: Date
83 } 83 }
84 84
85 struct Resources: Sendable, Hashable { 85 nonisolated struct Resources: Sendable, Hashable {
86 let mailingLists: [MailingList] 86 let mailingLists: [MailingList]
87 let sources: [SourceRepo] 87 let sources: [SourceRepo]
88 let trackers: [Tracker] 88 let trackers: [Tracker]
@@ -171,7 +171,7 @@ struct Project: Identifiable, Hashable, Sendable {
171 } 171 }
172} 172}
173 173
174extension Project.MailingList { 174nonisolated extension Project.MailingList {
175 var displayName: String { 175 var displayName: String {
176 Project.normalizedText(name) ?? "Untitled Mailing List" 176 Project.normalizedText(name) ?? "Untitled Mailing List"
177 } 177 }
@@ -185,7 +185,7 @@ extension Project.MailingList {
185 } 185 }
186} 186}
187 187
188extension Project.SourceRepo { 188nonisolated extension Project.SourceRepo {
189 var displayName: String { 189 var displayName: String {
190 Project.normalizedText(name) ?? "Untitled Repository" 190 Project.normalizedText(name) ?? "Untitled Repository"
191 } 191 }
@@ -203,7 +203,7 @@ extension Project.SourceRepo {
203 } 203 }
204} 204}
205 205
206extension Project.Tracker { 206nonisolated extension Project.Tracker {
207 var displayName: String { 207 var displayName: String {
208 Project.normalizedText(name) ?? "Untitled Tracker" 208 Project.normalizedText(name) ?? "Untitled Tracker"
209 } 209 }
@@ -221,13 +221,13 @@ extension Project.Tracker {
221 } 221 }
222} 222}
223 223
224extension String { 224nonisolated extension String {
225 var srhtUsername: String { 225 var srhtUsername: String {
226 hasPrefix("~") ? String(dropFirst()) : self 226 hasPrefix("~") ? String(dropFirst()) : self
227 } 227 }
228} 228}
229 229
230extension Visibility { 230nonisolated extension Visibility {
231 var displayName: String { 231 var displayName: String {
232 switch self { 232 switch self {
233 case .publicVisibility: 233 case .publicVisibility:
Hutch/Models/RepositoryACL.swift +2 −2
@@ -1,12 +1,12 @@
1import Foundation 1import Foundation
2 2
3struct RepositoryACLEntry: Codable, Sendable, Identifiable, Hashable { 3nonisolated struct RepositoryACLEntry: Codable, Sendable, Identifiable, Hashable {
4 let id: Int 4 let id: Int
5 let mode: AccessMode 5 let mode: AccessMode
6 let entity: Entity 6 let entity: Entity
7} 7}
8 8
9extension AccessMode { 9nonisolated extension AccessMode {
10 var shortLabel: String { rawValue } 10 var shortLabel: String { rawValue }
11 11
12 var displayName: String { 12 var displayName: String {
Hutch/Models/RepositorySummary.swift +4 −4
@@ -3,7 +3,7 @@ import Foundation
3/// Lightweight repository model matching the fields returned by the 3/// Lightweight repository model matching the fields returned by the
4/// repositories list query. Avoids optionalizing all fields on the full 4/// repositories list query. Avoids optionalizing all fields on the full
5/// `Repository` model. 5/// `Repository` model.
6struct RepositorySummary: Codable, Sendable, Identifiable, Hashable { 6nonisolated struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
7 let id: Int 7 let id: Int
8 /// GraphQL resource identifier used by `repository(rid:)` queries. 8 /// GraphQL resource identifier used by `repository(rid:)` queries.
9 let rid: String 9 let rid: String
@@ -15,13 +15,13 @@ struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
15 let owner: Entity 15 let owner: Entity
16 let head: Reference? 16 let head: Reference?
17 17
18 enum CodingKeys: String, CodingKey { 18 nonisolated enum CodingKeys: String, CodingKey {
19 case id, rid, service, name, description, visibility, updated, owner 19 case id, rid, service, name, description, visibility, updated, owner
20 case head = "HEAD" 20 case head = "HEAD"
21 } 21 }
22 22
23 /// Grouped initializer fields (single parameter keeps APIs explicit without exceeding parameter-count limits). 23 /// Grouped initializer fields (single parameter keeps APIs explicit without exceeding parameter-count limits).
24 struct Fields: Sendable, Hashable { 24 nonisolated struct Fields: Sendable, Hashable {
25 let id: Int 25 let id: Int
26 let rid: String 26 let rid: String
27 let service: SRHTService 27 let service: SRHTService
@@ -59,7 +59,7 @@ struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
59 } 59 }
60} 60}
61 61
62extension RepositorySummary { 62nonisolated extension RepositorySummary {
63 var defaultBranchName: String? { 63 var defaultBranchName: String? {
64 head?.name.replacingOccurrences(of: "refs/heads/", with: "") 64 head?.name.replacingOccurrences(of: "refs/heads/", with: "")
65 } 65 }
Hutch/Models/SystemStatusModels.swift +5 −5
@@ -1,6 +1,6 @@
1import Foundation 1import Foundation
2 2
3enum StatusLevel: String, Codable, Sendable { 3nonisolated enum StatusLevel: String, Codable, Sendable {
4 case operational 4 case operational
5 case degraded 5 case degraded
6 case majorOutage 6 case majorOutage
@@ -32,7 +32,7 @@ enum StatusLevel: String, Codable, Sendable {
32 } 32 }
33} 33}
34 34
35struct StatusServiceState: Identifiable, Hashable, Codable, Sendable { 35nonisolated struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
36 let id: String 36 let id: String
37 let name: String 37 let name: String
38 let slug: String? 38 let slug: String?
@@ -40,7 +40,7 @@ struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
40 let description: String? 40 let description: String?
41} 41}
42 42
43struct StatusIncident: Identifiable, Hashable, Codable, Sendable { 43nonisolated struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
44 let id: String 44 let id: String
45 let title: String 45 let title: String
46 let summary: String? 46 let summary: String?
@@ -50,7 +50,7 @@ struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
50 let isActive: Bool? 50 let isActive: Bool?
51} 51}
52 52
53struct SystemStatusSnapshot: Hashable, Codable, Sendable { 53nonisolated struct SystemStatusSnapshot: Hashable, Codable, Sendable {
54 let services: [StatusServiceState] 54 let services: [StatusServiceState]
55 let activeIncidents: [StatusIncident] 55 let activeIncidents: [StatusIncident]
56 let lastUpdated: Date 56 let lastUpdated: Date
@@ -78,7 +78,7 @@ struct SystemStatusSnapshot: Hashable, Codable, Sendable {
78 } 78 }
79} 79}
80 80
81struct SystemStatusPageData: Sendable { 81nonisolated struct SystemStatusPageData: Sendable {
82 let snapshot: SystemStatusSnapshot 82 let snapshot: SystemStatusSnapshot
83 let recentIncidents: [StatusIncident] 83 let recentIncidents: [StatusIncident]
84} 84}
Hutch/Models/TicketBulkAction.swift +3 −3
@@ -1,6 +1,6 @@
1import Foundation 1import Foundation
2 2
3enum TicketBulkActionKind: String, Sendable { 3nonisolated enum TicketBulkActionKind: String, Sendable {
4 case close 4 case close
5 case assign 5 case assign
6 6
@@ -23,7 +23,7 @@ enum TicketBulkActionKind: String, Sendable {
23 } 23 }
24} 24}
25 25
26struct TicketBulkActionResult: Identifiable, Sendable { 26nonisolated struct TicketBulkActionResult: Identifiable, Sendable {
27 let id = UUID() 27 let id = UUID()
28 let action: TicketBulkActionKind 28 let action: TicketBulkActionKind
29 let totalCount: Int 29 let totalCount: Int
@@ -82,7 +82,7 @@ struct TicketBulkActionResult: Identifiable, Sendable {
82 } 82 }
83} 83}
84 84
85struct TicketBulkActionFailure: Sendable { 85nonisolated struct TicketBulkActionFailure: Sendable {
86 let ticketID: Int 86 let ticketID: Int
87 let message: String 87 let message: String
88} 88}
Hutch/Models/Todo.swift +15 −15
@@ -3,7 +3,7 @@ import Foundation
3// MARK: - Enums 3// MARK: - Enums
4 4
5/// Status of a ticket. 5/// Status of a ticket.
6enum TicketStatus: String, Codable, Sendable, CaseIterable { 6nonisolated enum TicketStatus: String, Codable, Sendable, CaseIterable {
7 case reported = "REPORTED" 7 case reported = "REPORTED"
8 case confirmed = "CONFIRMED" 8 case confirmed = "CONFIRMED"
9 case inProgress = "IN_PROGRESS" 9 case inProgress = "IN_PROGRESS"
@@ -30,7 +30,7 @@ enum TicketStatus: String, Codable, Sendable, CaseIterable {
30} 30}
31 31
32/// Resolution of a ticket. 32/// Resolution of a ticket.
33enum TicketResolution: String, Codable, Sendable { 33nonisolated enum TicketResolution: String, Codable, Sendable {
34 case unresolved = "UNRESOLVED" 34 case unresolved = "UNRESOLVED"
35 case fixed = "FIXED" 35 case fixed = "FIXED"
36 case implemented = "IMPLEMENTED" 36 case implemented = "IMPLEMENTED"
@@ -64,7 +64,7 @@ enum TicketResolution: String, Codable, Sendable {
64} 64}
65 65
66/// Authenticity of a ticket or comment. 66/// Authenticity of a ticket or comment.
67enum Authenticity: String, Codable, Sendable { 67nonisolated enum Authenticity: String, Codable, Sendable {
68 case authentic = "AUTHENTIC" 68 case authentic = "AUTHENTIC"
69 case tampered = "TAMPERED" 69 case tampered = "TAMPERED"
70 case unauthenticated = "UNAUTHENTICATED" 70 case unauthenticated = "UNAUTHENTICATED"
@@ -74,7 +74,7 @@ enum Authenticity: String, Codable, Sendable {
74 74
75/// A label that can be applied to tickets. 75/// A label that can be applied to tickets.
76/// Named `TicketLabel` to avoid collision with `SwiftUI.Label`. 76/// Named `TicketLabel` to avoid collision with `SwiftUI.Label`.
77struct TicketLabel: Codable, Sendable, Identifiable, Hashable { 77nonisolated struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
78 let id: Int 78 let id: Int
79 let name: String 79 let name: String
80 let backgroundColor: String 80 let backgroundColor: String
@@ -83,7 +83,7 @@ struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
83 83
84// MARK: - Tracker ACL 84// MARK: - Tracker ACL
85 85
86struct TrackerACLPermissions: Codable, Sendable, Hashable { 86nonisolated struct TrackerACLPermissions: Codable, Sendable, Hashable {
87 let browse: Bool 87 let browse: Bool
88 let submit: Bool 88 let submit: Bool
89 let comment: Bool 89 let comment: Bool
@@ -91,7 +91,7 @@ struct TrackerACLPermissions: Codable, Sendable, Hashable {
91 let triage: Bool 91 let triage: Bool
92} 92}
93 93
94struct TrackerACL: Codable, Sendable, Identifiable, Hashable { 94nonisolated struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
95 let id: Int 95 let id: Int
96 let created: Date 96 let created: Date
97 let entity: Entity 97 let entity: Entity
@@ -112,7 +112,7 @@ struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
112 } 112 }
113} 113}
114 114
115struct DefaultTrackerACL: Codable, Sendable, Hashable { 115nonisolated struct DefaultTrackerACL: Codable, Sendable, Hashable {
116 let browse: Bool 116 let browse: Bool
117 let submit: Bool 117 let submit: Bool
118 let comment: Bool 118 let comment: Bool
@@ -133,7 +133,7 @@ struct DefaultTrackerACL: Codable, Sendable, Hashable {
133// MARK: - Tracker 133// MARK: - Tracker
134 134
135/// A bug tracker from todo.sr.ht. 135/// A bug tracker from todo.sr.ht.
136struct Tracker: Codable, Sendable, Identifiable, Hashable { 136nonisolated struct Tracker: Codable, Sendable, Identifiable, Hashable {
137 let id: Int 137 let id: Int
138 let created: Date 138 let created: Date
139 let updated: Date 139 let updated: Date
@@ -154,7 +154,7 @@ struct Tracker: Codable, Sendable, Identifiable, Hashable {
154// MARK: - Tracker Summary (for list view) 154// MARK: - Tracker Summary (for list view)
155 155
156/// Lightweight tracker model matching the fields returned by the trackers list query. 156/// Lightweight tracker model matching the fields returned by the trackers list query.
157struct TrackerSummary: Codable, Sendable, Identifiable, Hashable { 157nonisolated struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
158 let id: Int 158 let id: Int
159 /// GraphQL resource identifier used by `tracker(id:)` queries. 159 /// GraphQL resource identifier used by `tracker(id:)` queries.
160 let rid: String 160 let rid: String
@@ -168,7 +168,7 @@ struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
168// MARK: - Ticket Summary (for list view) 168// MARK: - Ticket Summary (for list view)
169 169
170/// Lightweight ticket model for the list query. 170/// Lightweight ticket model for the list query.
171struct TicketSummary: Codable, Sendable, Identifiable, Hashable { 171nonisolated struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
172 let id: Int 172 let id: Int
173 let title: String 173 let title: String
174 let status: TicketStatus 174 let status: TicketStatus
@@ -190,7 +190,7 @@ struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
190// MARK: - Ticket Detail 190// MARK: - Ticket Detail
191 191
192/// Full ticket model for the detail view. 192/// Full ticket model for the detail view.
193struct TicketDetail: Codable, Sendable { 193nonisolated struct TicketDetail: Codable, Sendable {
194 let id: Int 194 let id: Int
195 let created: Date 195 let created: Date
196 let updated: Date 196 let updated: Date
@@ -207,7 +207,7 @@ struct TicketDetail: Codable, Sendable {
207// MARK: - Event 207// MARK: - Event
208 208
209/// A timeline event on a ticket. Each event contains one or more changes. 209/// A timeline event on a ticket. Each event contains one or more changes.
210struct TicketEvent: Codable, Sendable, Identifiable { 210nonisolated struct TicketEvent: Codable, Sendable, Identifiable {
211 let id: Int 211 let id: Int
212 let created: Date 212 let created: Date
213 var changes: [EventChange] 213 var changes: [EventChange]
@@ -215,7 +215,7 @@ struct TicketEvent: Codable, Sendable, Identifiable {
215 215
216/// A single change within an event, decoded from the polymorphic EventDetail 216/// A single change within an event, decoded from the polymorphic EventDetail
217/// interface using inline fragments. 217/// interface using inline fragments.
218struct EventChange: Codable, Sendable, Identifiable { 218nonisolated struct EventChange: Codable, Sendable, Identifiable {
219 let id: UUID 219 let id: UUID
220 220
221 let eventType: String 221 let eventType: String
@@ -272,12 +272,12 @@ struct EventChange: Codable, Sendable, Identifiable {
272 272
273/// Decoded from the `mentioned` field which may be a Ticket (with `id`) or 273/// Decoded from the `mentioned` field which may be a Ticket (with `id`) or
274/// an Entity (with `canonicalName`) depending on the event type. 274/// an Entity (with `canonicalName`) depending on the event type.
275struct MentionTarget: Codable, Sendable { 275nonisolated struct MentionTarget: Codable, Sendable {
276 let id: Int? 276 let id: Int?
277 let canonicalName: String? 277 let canonicalName: String?
278} 278}
279 279
280/// Label info as returned within a LabelUpdate event change. 280/// Label info as returned within a LabelUpdate event change.
281struct EventLabel: Codable, Sendable { 281nonisolated struct EventLabel: Codable, Sendable {
282 let name: String 282 let name: String
283} 283}
Hutch/Models/User.swift +1 −1
@@ -1,7 +1,7 @@
1import Foundation 1import Foundation
2 2
3/// A Sourcehut user from meta.sr.ht. 3/// A Sourcehut user from meta.sr.ht.
4struct User: Decodable, Sendable, Hashable { 4nonisolated struct User: Decodable, Sendable, Hashable {
5 let id: Int 5 let id: Int
6 let created: String? 6 let created: String?
7 let updated: String? 7 let updated: String?
Hutch/Views/Lists/MailingListListView.swift +1 −1
@@ -100,7 +100,7 @@ final class MailingListListViewModel {
100 query: Self.createMailingListMutation, 100 query: Self.createMailingListMutation,
101 variables: [ 101 variables: [
102 "name": trimmedName, 102 "name": trimmedName,
103 "description": trimmedDescription.isEmpty ? nil as String? as Any : trimmedDescription, 103 "description": trimmedDescription.isEmpty ? nil as String? as any Sendable : trimmedDescription,
104 "visibility": visibility.rawValue 104 "visibility": visibility.rawValue
105 ], 105 ],
106 responseType: Response.self 106 responseType: Response.self
Hutch/Views/More/TipStoreViewModel.swift +3 −1
@@ -32,7 +32,9 @@ final class TipStoreViewModel {
32 var errorMessage: String? 32 var errorMessage: String?
33 var statusMessage: String? 33 var statusMessage: String?
34 34
35 private var transactionUpdatesTask: Task<Void, Never>? 35 // Internal task handle, not observable state; assigned only on the main
36 // actor and read once from the nonisolated deinit.
37 @ObservationIgnored nonisolated(unsafe) private var transactionUpdatesTask: Task<Void, Never>?
36 38
37 init() { 39 init() {
38 transactionUpdatesTask = Task.detached(priority: .background) { 40 transactionUpdatesTask = Task.detached(priority: .background) {
Hutch/Views/Repositories/RepositoryDetailViewModel.swift +1 −1
@@ -557,7 +557,7 @@ final class RepositoryDetailViewModel {
557 variables: [ 557 variables: [
558 "repoId": repository.id, 558 "repoId": repository.id,
559 "revspec": revspec, 559 "revspec": revspec,
560 "file": nil as String? as Any 560 "file": nil as String? as any Sendable
561 ], 561 ],
562 file: MultipartUploadFile( 562 file: MultipartUploadFile(
563 variablePath: "file", 563 variablePath: "file",
Hutch/Views/Repositories/SyntaxHighlighter.swift +1 −1
@@ -208,7 +208,7 @@ nonisolated final class SyntaxHighlighter {
208 ] 208 ]
209} 209}
210 210
211private extension UIColor { 211nonisolated private extension UIColor {
212 /// `#rrggbb` for HTML inline styles, or `nil` if the color isn't RGB-convertible. 212 /// `#rrggbb` for HTML inline styles, or `nil` if the color isn't RGB-convertible.
213 var hexRGBString: String? { 213 var hexRGBString: String? {
214 var red: CGFloat = 0 214 var red: CGFloat = 0
Hutch/Views/Settings/SettingsViewModel.swift +5 −5
@@ -198,9 +198,9 @@ final class SettingsViewModel {
198 do { 198 do {
199 let input: [String: any Sendable] = [ 199 let input: [String: any Sendable] = [
200 "email": email, 200 "email": email,
201 "url": url.isEmpty ? nil as String? as Any : url, 201 "url": url.isEmpty ? nil as String? as any Sendable : url,
202 "location": location.isEmpty ? nil as String? as Any : location, 202 "location": location.isEmpty ? nil as String? as any Sendable : location,
203 "bio": bio.isEmpty ? nil as String? as Any : bio 203 "bio": bio.isEmpty ? nil as String? as any Sendable : bio
204 ] 204 ]
205 let result = try await client.execute( 205 let result = try await client.execute(
206 service: .meta, 206 service: .meta,
@@ -243,7 +243,7 @@ final class SettingsViewModel {
243 do { 243 do {
244 // The input variable has avatar set to null; the actual file 244 // The input variable has avatar set to null; the actual file
245 // is sent as a separate multipart part per graphql-multipart-request-spec. 245 // is sent as a separate multipart part per graphql-multipart-request-spec.
246 let input: [String: any Sendable] = ["avatar": nil as String? as Any] 246 let input: [String: any Sendable] = ["avatar": nil as String? as any Sendable]
247 let result = try await client.executeMultipart( 247 let result = try await client.executeMultipart(
248 service: .meta, 248 service: .meta,
249 query: Self.updateUserMutation, 249 query: Self.updateUserMutation,
@@ -286,7 +286,7 @@ final class SettingsViewModel {
286 error = nil 286 error = nil
287 287
288 do { 288 do {
289 let input: [String: any Sendable] = ["avatar": nil as String? as Any] 289 let input: [String: any Sendable] = ["avatar": nil as String? as any Sendable]
290 let result = try await client.execute( 290 let result = try await client.execute(
291 service: .meta, 291 service: .meta,
292 query: Self.updateUserMutation, 292 query: Self.updateUserMutation,
HutchTests/APICacheTests.swift +1
@@ -3,6 +3,7 @@ import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@Suite(.serialized) 5@Suite(.serialized)
6@MainActor
6struct APICacheTests { 7struct APICacheTests {
7 private struct Payload: Codable, Sendable, Equatable { 8 private struct Payload: Codable, Sendable, Equatable {
8 let value: String 9 let value: String
HutchTests/AppStateTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct AppStateTests { 6struct AppStateTests {
6 7
7 @Test 8 @Test
HutchTests/BuildListViewModelTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct BuildListViewModelTests { 6struct BuildListViewModelTests {
6 7
7 @Test 8 @Test
HutchTests/BuildTaskLogSearchTests.swift +1
@@ -1,6 +1,7 @@
1import Testing 1import Testing
2@testable import Hutch 2@testable import Hutch
3 3
4@MainActor
4struct BuildTaskLogSearchTests { 5struct BuildTaskLogSearchTests {
5 6
6 @Test 7 @Test
HutchTests/BuildTaskTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct BuildTaskTests { 6struct BuildTaskTests {
6 7
7 @Test 8 @Test
HutchTests/BundleUserAgentTests.swift +1
@@ -40,6 +40,7 @@ private final class CapturingURLProtocol: URLProtocol, @unchecked Sendable {
40 40
41/// Tests are serialized because CapturingURLProtocol uses shared static state. 41/// Tests are serialized because CapturingURLProtocol uses shared static state.
42@Suite(.serialized) 42@Suite(.serialized)
43@MainActor
43struct BundleUserAgentTests { 44struct BundleUserAgentTests {
44 45
45 // MARK: Bundle extension 46 // MARK: Bundle extension
HutchTests/ContributionCalendarTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct ContributionCalendarTests { 6struct ContributionCalendarTests {
6 7
7 @Test 8 @Test
HutchTests/DeepLinkTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct DeepLinkTests { 6struct DeepLinkTests {
6 7
7 @Test 8 @Test
HutchTests/GitObjectTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct GitObjectTests { 6struct GitObjectTests {
6 7
7 @Test 8 @Test
HutchTests/HomeViewModelTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct HomeViewModelTests { 6struct HomeViewModelTests {
6 7
7 @Test 8 @Test
HutchTests/HutchTests.swift +1
@@ -7,6 +7,7 @@
7 7
8import Testing 8import Testing
9 9
10@MainActor
10struct HutchTests { 11struct HutchTests {
11 12
12 @Test func example() async throws { 13 @Test func example() async throws {
HutchTests/InboxViewModelTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct InboxViewModelTests { 6struct InboxViewModelTests {
6 7
7 @Test 8 @Test
HutchTests/LookupHistoryStoreTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct LookupHistoryStoreTests { 6struct LookupHistoryStoreTests {
6 7
7 @Test 8 @Test
HutchTests/MailingListActivityTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct MailingListActivityTests { 6struct MailingListActivityTests {
6 7
7 @Test 8 @Test
HutchTests/ManPageCatalogTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct ManPageCatalogTests { 6struct ManPageCatalogTests {
6 @Test 7 @Test
7 func loadsBundledCatalog() { 8 func loadsBundledCatalog() {
HutchTests/PatchsetTests.swift +2
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct PatchsetStatusTests { 6struct PatchsetStatusTests {
6 7
7 @Test 8 @Test
@@ -46,6 +47,7 @@ struct PatchsetStatusTests {
46 } 47 }
47} 48}
48 49
50@MainActor
49struct PatchsetSummaryTests { 51struct PatchsetSummaryTests {
50 52
51 @Test 53 @Test
HutchTests/ProjectFormTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct ProjectFormTests { 6struct ProjectFormTests {
6 @Test 7 @Test
7 func parsesTagsSplittingAndTrimming() { 8 func parsesTagsSplittingAndTrimming() {
HutchTests/ProjectPinStoreTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct ProjectPinStoreTests { 6struct ProjectPinStoreTests {
6 @Test 7 @Test
7 func storesPinnedProjectsPerUser() { 8 func storesPinnedProjectsPerUser() {
HutchTests/ProjectTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct ProjectTests { 6struct ProjectTests {
6 private enum Fixture { 7 private enum Fixture {
7 static let exampleWebsite = "https://example.com" 8 static let exampleWebsite = "https://example.com"
HutchTests/ReadmeViewTests.swift +5
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct ReadmeViewTests { 6struct ReadmeViewTests {
6 7
7 @Test 8 @Test
@@ -28,6 +29,7 @@ struct ReadmeViewTests {
28 } 29 }
29} 30}
30 31
32@MainActor
31struct MarkdownRenderingTests { 33struct MarkdownRenderingTests {
32 34
33 @Test 35 @Test
@@ -219,6 +221,7 @@ struct MarkdownRenderingTests {
219 } 221 }
220} 222}
221 223
224@MainActor
222struct OrgRenderingTests { 225struct OrgRenderingTests {
223 226
224 @Test 227 @Test
@@ -376,6 +379,7 @@ struct OrgRenderingTests {
376 } 379 }
377} 380}
378 381
382@MainActor
379struct RepositoryAssetURLTests { 383struct RepositoryAssetURLTests {
380 384
381 @Test 385 @Test
@@ -391,6 +395,7 @@ struct RepositoryAssetURLTests {
391 } 395 }
392} 396}
393 397
398@MainActor
394struct RepositoryLinkURLTests { 399struct RepositoryLinkURLTests {
395 400
396 @Test 401 @Test
HutchTests/RecentActivityStoreTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct RecentActivityStoreTests { 6struct RecentActivityStoreTests {
6 @Test 7 @Test
7 func removeDropsMatchingEntryKeepingOthers() { 8 func removeDropsMatchingEntryKeepingOthers() {
HutchTests/RepositoryACLViewModelTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct RepositoryACLViewModelTests { 6struct RepositoryACLViewModelTests {
6 7
7 @Test 8 @Test
HutchTests/RepositoryDeployKeyTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct RepositoryDeployKeyTests { 6struct RepositoryDeployKeyTests {
6 @Test 7 @Test
7 func decodesDeployKey() throws { 8 func decodesDeployKey() throws {
HutchTests/RepositoryListViewModelTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct RepositoryListViewModelTests { 6struct RepositoryListViewModelTests {
6 7
7 @Test 8 @Test
HutchTests/RepositorySettingsViewModelTests.swift +1
@@ -10,6 +10,7 @@ private struct UpdateRepositoryInfoPayload: Decodable {
10 let id: Int 10 let id: Int
11} 11}
12 12
13@MainActor
13struct RepositorySettingsViewModelTests { 14struct RepositorySettingsViewModelTests {
14 15
15 @Test 16 @Test
HutchTests/SRHTClientTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct SRHTClientTests { 6struct SRHTClientTests {
6 7
7 @Test 8 @Test
HutchTests/SRHTWebURLTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct SRHTWebURLTests { 6struct SRHTWebURLTests {
6 private enum Expected { 7 private enum Expected {
7 static let chatOrigin = "https://chat.sr.ht" 8 static let chatOrigin = "https://chat.sr.ht"
HutchTests/ScopedSearchHistoryStoreTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct ScopedSearchHistoryStoreTests { 6struct ScopedSearchHistoryStoreTests {
6 7
7 @Test 8 @Test
HutchTests/SettingsViewModelTests.swift +1
@@ -64,6 +64,7 @@ private struct DeleteResultPayload: Decodable {
64} 64}
65 65
66@Suite(.serialized) 66@Suite(.serialized)
67@MainActor
67struct SettingsViewModelTests { 68struct SettingsViewModelTests {
68 69
69 @Test 70 @Test
HutchTests/SettingsViewTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct SettingsViewTests { 6struct SettingsViewTests {
6 7
7 @Test 8 @Test
HutchTests/SourceHutWebDeepLinkMapperTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct SourceHutWebDeepLinkMapperTests { 6struct SourceHutWebDeepLinkMapperTests {
6 @Test 7 @Test
7 func mapsRepositoryURLs() { 8 func mapsRepositoryURLs() {
HutchTests/SyntaxHighlighterTests.swift +4 −5
@@ -3,6 +3,7 @@ import Testing
3import UIKit 3import UIKit
4@testable import Hutch 4@testable import Hutch
5 5
6@MainActor
6struct SyntaxHighlighterTests { 7struct SyntaxHighlighterTests {
7 @Test 8 @Test
8 func mapsFileNamesToLanguages() { 9 func mapsFileNamesToLanguages() {
@@ -24,9 +25,8 @@ struct SyntaxHighlighterTests {
24 let highlighter = SyntaxHighlighter(theme: .light) 25 let highlighter = SyntaxHighlighter(theme: .light)
25 let html = highlighter.highlightedHTML(for: "let answer = 42", language: "swift") 26 let html = highlighter.highlightedHTML(for: "let answer = 42", language: "swift")
26 27
27 let unwrapped = try? #require(html) 28 #expect(html?.contains("<span style=\"color:") == true)
28 #expect(unwrapped?.contains("<span style=\"color:") == true) 29 #expect(html?.contains("answer") == true)
29 #expect(unwrapped?.contains("answer") == true)
30 } 30 }
31 31
32 @Test 32 @Test
@@ -42,7 +42,6 @@ struct SyntaxHighlighterTests {
42 let font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular) 42 let font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
43 let attributed = highlighter.attributedText(for: "print(\"hi\")", language: "python", font: font) 43 let attributed = highlighter.attributedText(for: "print(\"hi\")", language: "python", font: font)
44 44
45 let unwrapped = try? #require(attributed) 45 #expect(attributed?.string.contains("print") == true)
46 #expect(unwrapped?.string.contains("print") == true)
47 } 46 }
48} 47}
HutchTests/SystemStatusRepositoryTests.swift +5 −4
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct SystemStatusRepositoryTests { 6struct SystemStatusRepositoryTests {
6 7
7 @Test 8 @Test
@@ -111,21 +112,21 @@ private struct TestSystemStatusService: SystemStatusServing {
111} 112}
112 113
113private extension SystemStatusRepositoryTests { 114private extension SystemStatusRepositoryTests {
114 static let cachedSnapshotHTML = #""" 115 nonisolated static let cachedSnapshotHTML = #"""
115 <div class="component" data-status="disrupted"> 116 <div class="component" data-status="disrupted">
116 <a href="/affected/git.sr.ht/">git.sr.ht</a> 117 <a href="/affected/git.sr.ht/">git.sr.ht</a>
117 <span class="component-status">Disrupted</span> 118 <span class="component-status">Disrupted</span>
118 </div> 119 </div>
119 """# 120 """#
120 121
121 static let cachedOperationalHTML = #""" 122 nonisolated static let cachedOperationalHTML = #"""
122 <div class="component" data-status="ok"> 123 <div class="component" data-status="ok">
123 <a href="/affected/meta.sr.ht/">meta.sr.ht</a> 124 <a href="/affected/meta.sr.ht/">meta.sr.ht</a>
124 <span class="component-status">Operational</span> 125 <span class="component-status">Operational</span>
125 </div> 126 </div>
126 """# 127 """#
127 128
128 static let cachedIncidentRSS = #""" 129 nonisolated static let cachedIncidentRSS = #"""
129 <rss version="2.0"> 130 <rss version="2.0">
130 <channel> 131 <channel>
131 <item> 132 <item>
@@ -139,7 +140,7 @@ private extension SystemStatusRepositoryTests {
139 </rss> 140 </rss>
140 """# 141 """#
141 142
142 static let emptyRSS = #""" 143 nonisolated static let emptyRSS = #"""
143 <rss version="2.0"> 144 <rss version="2.0">
144 <channel></channel> 145 <channel></channel>
145 </rss> 146 </rss>
HutchTests/SystemStatusServiceTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct SystemStatusServiceTests { 6struct SystemStatusServiceTests {
6 private enum Fixture { 7 private enum Fixture {
7 static let ddosIssueURL = "https://status.sr.ht/issues/2026-04-06-ddos-attack/" 8 static let ddosIssueURL = "https://status.sr.ht/issues/2026-04-06-ddos-attack/"
HutchTests/TicketBulkActionTests.swift +1
@@ -1,6 +1,7 @@
1import Testing 1import Testing
2@testable import Hutch 2@testable import Hutch
3 3
4@MainActor
4struct TicketBulkActionTests { 5struct TicketBulkActionTests {
5 6
6 @Test 7 @Test
HutchTests/TicketDetailViewModelTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct TicketDetailViewModelTests { 6struct TicketDetailViewModelTests {
6 7
7 @Test 8 @Test
HutchTests/TicketSavedFilterStoreTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct TicketSavedFilterStoreTests { 6struct TicketSavedFilterStoreTests {
6 7
7 @Test 8 @Test
HutchTests/TipStoreViewModelTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct TipStoreViewModelTests { 6struct TipStoreViewModelTests {
6 7
7 @Test 8 @Test
HutchTests/TrackerListViewModelTests.swift +1
@@ -2,6 +2,7 @@ import Foundation
2import Testing 2import Testing
3@testable import Hutch 3@testable import Hutch
4 4
5@MainActor
5struct TrackerListViewModelTests { 6struct TrackerListViewModelTests {
6 7
7 @Test 8 @Test
HutchTests/TrackerManagementViewModelTests.swift +1
@@ -1,6 +1,7 @@
1import Testing 1import Testing
2@testable import Hutch 2@testable import Hutch
3 3
4@MainActor
4struct TrackerManagementViewModelTests { 5struct TrackerManagementViewModelTests {
5 6
6 @Test 7 @Test
HutchWidgetExtension/ContributionGraphWidget.swift +4
@@ -28,12 +28,16 @@ struct ContributionGraphTimelineProvider: TimelineProvider {
28 } 28 }
29 29
30 func getSnapshot(in _: Context, completion: @escaping (ContributionGraphEntry) -> Void) { 30 func getSnapshot(in _: Context, completion: @escaping (ContributionGraphEntry) -> Void) {
31 // WidgetKit's completion is safe to call from the task; rebind as
32 // nonisolated(unsafe) so it can cross into the `sending` Task closure.
33 nonisolated(unsafe) let completion = completion
31 Task { 34 Task {
32 completion(await loadEntry()) 35 completion(await loadEntry())
33 } 36 }
34 } 37 }
35 38
36 func getTimeline(in _: Context, completion: @escaping (Timeline<ContributionGraphEntry>) -> Void) { 39 func getTimeline(in _: Context, completion: @escaping (Timeline<ContributionGraphEntry>) -> Void) {
40 nonisolated(unsafe) let completion = completion
37 Task { 41 Task {
38 let entry = await loadEntry() 42 let entry = await loadEntry()
39 let refreshDate = Calendar.contributionCalendar.date(byAdding: .hour, value: 1, to: Date()) ?? Date().addingTimeInterval(3600) 43 let refreshDate = Calendar.contributionCalendar.date(byAdding: .hour, value: 1, to: Date()) ?? Date().addingTimeInterval(3600)
ROADMAP.txt +24 −16
@@ -158,7 +158,7 @@ so "breaking change" does not apply. These buckets track *user-visible scale*.
158| v3.11.0 | ~~Mailing list subscribe/unsubscribe toggle~~ (shipped) | Ingest-surfaced; state via the `subscriptions` query (the `subscription` field is a trap) | 158| v3.11.0 | ~~Mailing list subscribe/unsubscribe toggle~~ (shipped) | Ingest-surfaced; state via the `subscriptions` query (the `subscription` field is a trap) |
159| v3.12.0 | Accessibility | Independent, device-verified | 159| v3.12.0 | Accessibility | Independent, device-verified |
160| v4.0.0 | Localization *with* translations | The only true re-presentation | 160| v4.0.0 | Localization *with* translations | The only true re-presentation |
161| — | Swift 6 language mode; cache reads | Internal; ride along, no tag | 161| — | ~~Swift 6 language mode~~ (done); cache reads | Internal; ride along, no tag |
162 162
1633.9.0 was cut this session, bundling the hub.sr.ht writes with the other 1633.9.0 was cut this session, bundling the hub.sr.ht writes with the other
164features listed. That reorders the original plan: the "What's cooking" ingest 164features listed. That reorders the original plan: the "What's cooking" ingest
@@ -384,21 +384,29 @@ rather than a bare `TODO`.)
384 saved-search store for an intent to open. Add the intent once global 384 saved-search store for an intent to open. Add the intent once global
385 saved-search persistence exists. 385 saved-search persistence exists.
386 386
387### Swift 6 language mode — no release of its own 387### Swift 6 language mode — done
388 388
389The project builds in Swift 5 language mode with 389`SWIFT_VERSION` is now `6.0` (keeping `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`).
390`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`. Moving to Swift 6 is blocked on 390The migration was mostly mechanical, in a few buckets:
391concurrency diagnostics that are warnings today and errors there: 391
392 392- **Models are `nonisolated`.** Under MainActor-default, every value-type model
393- `APICacheTests` and `BundleUserAgentTests` call main-actor-isolated 393 was implicitly `@MainActor`; the pure data types in `Models/` (and utility
394 initialisers and properties from nonisolated contexts, and `await` a few 394 extensions like `Date.relativeDescription`, `DateFormatter+SRHT`, `SRHTWebURL`)
395 expressions without marking them. Roughly 20 warnings, all in tests. 395 are now `nonisolated`, so the nonisolated networking layer can use them.
396- Response types are implicitly `@MainActor` under the default isolation, so 396- **App Intents statics.** `AppIntent`/`AppEntity`/`AppEnum` `static var`s were
397 their `Decodable` conformances are too. Decoding one from a nonisolated 397 "global shared mutable state"; the stored ones are now `static let`.
398 context — an `async let` over a raw `client.execute`, say — warns now and 398- **Sendable dictionaries.** `nil as String? as Any` in `[String: any Sendable]`
399 fails then. The pattern that avoids it is `async let` over `@MainActor` 399 GraphQL variable dicts became `nil as String? as any Sendable`.
400 methods, as in `HomeViewModel.loadDashboard` and 400- **`UserDefaults`** gets a retroactive `@unchecked Sendable` (documented
401 `NotificationPreferencesViewModel.load`. 401 thread-safe) since it threads through account sessions and stores.
402- **WidgetKit** completion handlers are rebound `nonisolated(unsafe)` to cross
403 into their `Task {}`; the `@Observable` `TipStoreViewModel` task handle is
404 `@ObservationIgnored nonisolated(unsafe)` for its nonisolated `deinit`.
405- **Tests** run on `@MainActor` (they exercise MainActor app code), with a few
406 constant fixtures marked `nonisolated` for use inside `@Sendable` stub
407 closures.
408
409Builds and the full suite are clean in Swift 6 mode with no behaviour change.
402 410
403### Cache reads that bypass the client — no release of its own 411### Cache reads that bypass the client — no release of its own
404 412