summaryrefslogtreecommitdiff
path: root/Hutch
diff options
context:
space:
mode:
Diffstat (limited to 'Hutch')
-rw-r--r--Hutch/App/HutchIntents.swift78
-rw-r--r--Hutch/Extensions/Date+Relative.swift2
-rw-r--r--Hutch/Extensions/DateFormatter+SRHT.swift2
-rw-r--r--Hutch/Extensions/Foundation+Sendable.swift7
-rw-r--r--Hutch/Extensions/SRHTWebURL.swift2
-rw-r--r--Hutch/Models/AccountEntry.swift2
-rw-r--r--Hutch/Models/ArtifactInfo.swift6
-rw-r--r--Hutch/Models/Builds.swift18
-rw-r--r--Hutch/Models/CommitDetail.swift16
-rw-r--r--Hutch/Models/CommitSummary.swift4
-rw-r--r--Hutch/Models/ContributionCalendar.swift28
-rw-r--r--Hutch/Models/Git.swift40
-rw-r--r--Hutch/Models/Inbox.swift20
-rw-r--r--Hutch/Models/Meta.swift16
-rw-r--r--Hutch/Models/Paste.swift6
-rw-r--r--Hutch/Models/Patchset.swift12
-rw-r--r--Hutch/Models/Project.swift24
-rw-r--r--Hutch/Models/RepositoryACL.swift4
-rw-r--r--Hutch/Models/RepositorySummary.swift8
-rw-r--r--Hutch/Models/SystemStatusModels.swift10
-rw-r--r--Hutch/Models/TicketBulkAction.swift6
-rw-r--r--Hutch/Models/Todo.swift30
-rw-r--r--Hutch/Models/User.swift2
-rw-r--r--Hutch/Networking/RepositoryDeployKeyService.swift100
-rw-r--r--Hutch/Views/Lists/MailingListListView.swift2
-rw-r--r--Hutch/Views/More/TipStoreViewModel.swift4
-rw-r--r--Hutch/Views/Projects/ProjectMailingListView.swift143
-rw-r--r--Hutch/Views/Repositories/RepositoryDeployKeysView.swift272
-rw-r--r--Hutch/Views/Repositories/RepositoryDetailView.swift16
-rw-r--r--Hutch/Views/Repositories/RepositoryDetailViewModel.swift2
-rw-r--r--Hutch/Views/Repositories/SyntaxHighlighter.swift6
-rw-r--r--Hutch/Views/Settings/SettingsViewModel.swift10
32 files changed, 719 insertions, 179 deletions
diff --git a/Hutch/App/HutchIntents.swift b/Hutch/App/HutchIntents.swift
index fe4ebdc..3f4cd83 100644
--- a/Hutch/App/HutchIntents.swift
+++ b/Hutch/App/HutchIntents.swift
@@ -4,9 +4,9 @@ import Foundation
// MARK: - Navigation Intents
struct OpenWorkQueueIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Work Queue"
- static var description = IntentDescription("Opens Hutch to your Work Queue.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Work Queue"
+ static let description = IntentDescription("Opens Hutch to your Work Queue.")
+ static let openAppWhenRun = true
var route: HutchRoute { .workQueue(scope: .all) }
@@ -18,9 +18,9 @@ struct OpenWorkQueueIntent: AppIntent {
}
struct OpenRecentActivityIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Recent Activity"
- static var description = IntentDescription("Opens Hutch to recent activity.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Recent Activity"
+ static let description = IntentDescription("Opens Hutch to recent activity.")
+ static let openAppWhenRun = true
var route: HutchRoute { .recentActivity }
@@ -32,9 +32,9 @@ struct OpenRecentActivityIntent: AppIntent {
}
struct OpenSystemStatusIntent: AppIntent {
- static var title: LocalizedStringResource = "Open System Status"
- static var description = IntentDescription("Opens Hutch to SourceHut system status.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open System Status"
+ static let description = IntentDescription("Opens Hutch to SourceHut system status.")
+ static let openAppWhenRun = true
var route: HutchRoute { .systemStatus }
@@ -46,9 +46,9 @@ struct OpenSystemStatusIntent: AppIntent {
}
struct OpenPinnedResourceIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Pinned Resource"
- static var description = IntentDescription("Opens a pinned Hutch resource.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Pinned Resource"
+ static let description = IntentDescription("Opens a pinned Hutch resource.")
+ static let openAppWhenRun = true
@Parameter(title: "Pinned Resource")
var pinnedResource: PinnedResourceEntity
@@ -63,9 +63,9 @@ struct OpenPinnedResourceIntent: AppIntent {
}
struct OpenProjectDashboardIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Project Dashboard"
- static var description = IntentDescription("Opens a pinned project dashboard in Hutch.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Project Dashboard"
+ static let description = IntentDescription("Opens a pinned project dashboard in Hutch.")
+ static let openAppWhenRun = true
@Parameter(title: "Project")
var project: ProjectEntity
@@ -84,16 +84,16 @@ struct OpenProjectDashboardIntent: AppIntent {
enum HutchShortcutScope: String, AppEnum {
case all
- static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scope")
- static var caseDisplayRepresentations: [HutchShortcutScope: DisplayRepresentation] = [
+ static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scope")
+ static let caseDisplayRepresentations: [HutchShortcutScope: DisplayRepresentation] = [
.all: "All"
]
}
struct OpenFailedBuildsIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Failed Builds"
- static var description = IntentDescription("Opens Hutch to failed builds.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Failed Builds"
+ static let description = IntentDescription("Opens Hutch to failed builds.")
+ static let openAppWhenRun = true
@Parameter(title: "Scope", default: .all)
var scope: HutchShortcutScope
@@ -108,9 +108,9 @@ struct OpenFailedBuildsIntent: AppIntent {
}
struct OpenAssignedTicketsIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Assigned Tickets"
- static var description = IntentDescription("Opens Hutch to tickets assigned to you.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Assigned Tickets"
+ static let description = IntentDescription("Opens Hutch to tickets assigned to you.")
+ static let openAppWhenRun = true
@Parameter(title: "Scope", default: .all)
var scope: HutchShortcutScope
@@ -125,9 +125,9 @@ struct OpenAssignedTicketsIntent: AppIntent {
}
struct SearchHutchIntent: AppIntent {
- static var title: LocalizedStringResource = "Search Hutch"
- static var description = IntentDescription("Opens Hutch lookup with a search query.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Search Hutch"
+ static let description = IntentDescription("Opens Hutch lookup with a search query.")
+ static let openAppWhenRun = true
@Parameter(title: "Query")
var query: String
@@ -155,8 +155,8 @@ struct SearchHutchIntent: AppIntent {
// MARK: - App Entities
struct PinnedResourceEntity: AppEntity, Identifiable {
- static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pinned Resource")
- static var defaultQuery = PinnedResourceQuery()
+ static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pinned Resource")
+ static let defaultQuery = PinnedResourceQuery()
let id: String
let name: String
@@ -181,8 +181,8 @@ struct PinnedResourceQuery: EntityQuery {
}
struct ProjectEntity: AppEntity, Identifiable {
- static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")
- static var defaultQuery = ProjectEntityQuery()
+ static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")
+ static let defaultQuery = ProjectEntityQuery()
let id: String
let name: String
@@ -278,8 +278,8 @@ enum HutchIntentEntityStore {
// MARK: - Existing Read-Only Summary Intents
struct CheckSystemStatusIntent: AppIntent {
- static var title: LocalizedStringResource = "Check SourceHut Status"
- static var description = IntentDescription("Returns the current SourceHut system status.")
+ static let title: LocalizedStringResource = "Check SourceHut Status"
+ static let description = IntentDescription("Returns the current SourceHut system status.")
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
@@ -302,8 +302,8 @@ struct CheckSystemStatusIntent: AppIntent {
}
struct CheckBuildsIntent: AppIntent {
- static var title: LocalizedStringResource = "Check Hutch Builds"
- static var description = IntentDescription("Returns a summary of your recent build status.")
+ static let title: LocalizedStringResource = "Check Hutch Builds"
+ static let description = IntentDescription("Returns a summary of your recent build status.")
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
@@ -337,7 +337,7 @@ struct CheckBuildsIntent: AppIntent {
// MARK: - Search Type
-extension LookupType: @retroactive AppEnum {
+extension LookupType: AppEnum {
public nonisolated static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(name: "Search Type")
}
@@ -357,8 +357,8 @@ extension LookupType: @retroactive AppEnum {
// MARK: - Mutating Intents
struct ClearRecentActivityIntent: AppIntent {
- static var title: LocalizedStringResource = "Clear Recent Activity"
- static var description = IntentDescription("Clears the Recent list on the Hutch Home tab.")
+ static let title: LocalizedStringResource = "Clear Recent Activity"
+ static let description = IntentDescription("Clears the Recent list on the Hutch Home tab.")
@MainActor
func perform() async throws -> some IntentResult & ProvidesDialog {
@@ -368,8 +368,8 @@ struct ClearRecentActivityIntent: AppIntent {
}
struct UnpinResourceIntent: AppIntent {
- static var title: LocalizedStringResource = "Unpin Resource"
- static var description = IntentDescription("Removes a pinned resource from the Hutch Home tab.")
+ static let title: LocalizedStringResource = "Unpin Resource"
+ static let description = IntentDescription("Removes a pinned resource from the Hutch Home tab.")
@Parameter(title: "Pinned Resource")
var pinnedResource: PinnedResourceEntity
diff --git a/Hutch/Extensions/Date+Relative.swift b/Hutch/Extensions/Date+Relative.swift
index f06ba59..b6c28ed 100644
--- a/Hutch/Extensions/Date+Relative.swift
+++ b/Hutch/Extensions/Date+Relative.swift
@@ -1,6 +1,6 @@
import Foundation
-extension Date {
+nonisolated extension Date {
/// A short relative description like "2h ago", "3d ago", or "Jan 5, 2025".
var relativeDescription: String {
let formatter = RelativeDateTimeFormatter()
diff --git a/Hutch/Extensions/DateFormatter+SRHT.swift b/Hutch/Extensions/DateFormatter+SRHT.swift
index c017b11..a06b58a 100644
--- a/Hutch/Extensions/DateFormatter+SRHT.swift
+++ b/Hutch/Extensions/DateFormatter+SRHT.swift
@@ -1,6 +1,6 @@
import Foundation
-extension DateFormatter {
+nonisolated extension DateFormatter {
/// Formatter for the sr.ht `Time` scalar: `%Y-%m-%dT%H:%M:%SZ` (UTC).
static let srht: DateFormatter = {
let formatter = DateFormatter()
diff --git a/Hutch/Extensions/Foundation+Sendable.swift b/Hutch/Extensions/Foundation+Sendable.swift
new file mode 100644
index 0000000..9a12aa6
--- /dev/null
+++ b/Hutch/Extensions/Foundation+Sendable.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+// `UserDefaults` is documented as thread-safe but is not marked `Sendable` by
+// Foundation. Hutch passes it into account sessions and the various per-account
+// stores, all of which need to cross concurrency boundaries. The retroactive
+// unchecked conformance reflects the documented thread-safety.
+extension UserDefaults: @retroactive @unchecked Sendable {}
diff --git a/Hutch/Extensions/SRHTWebURL.swift b/Hutch/Extensions/SRHTWebURL.swift
index fc5a1d5..5715ce4 100644
--- a/Hutch/Extensions/SRHTWebURL.swift
+++ b/Hutch/Extensions/SRHTWebURL.swift
@@ -1,6 +1,6 @@
import Foundation
-enum SRHTWebURL {
+nonisolated enum SRHTWebURL {
static let chat = URL(string: "https://chat.sr.ht")!
static let status = URL(string: "https://status.sr.ht")!
/// RSS/Atom feed for status incidents (HTML snapshot uses `status`).
diff --git a/Hutch/Models/AccountEntry.swift b/Hutch/Models/AccountEntry.swift
index 551cd19..cfc2f62 100644
--- a/Hutch/Models/AccountEntry.swift
+++ b/Hutch/Models/AccountEntry.swift
@@ -1,7 +1,7 @@
import Foundation
/// A stored sr.ht account (token + resolved username).
-struct AccountEntry: Codable, Identifiable, Equatable, Sendable {
+nonisolated struct AccountEntry: Codable, Identifiable, Equatable, Sendable {
let id: String
var username: String
var token: String
diff --git a/Hutch/Models/ArtifactInfo.swift b/Hutch/Models/ArtifactInfo.swift
index dbe9d96..6418ba8 100644
--- a/Hutch/Models/ArtifactInfo.swift
+++ b/Hutch/Models/ArtifactInfo.swift
@@ -1,7 +1,7 @@
import Foundation
/// An artifact with its parent reference name, used in the artifacts tab.
-struct ArtifactInfo: Codable, Sendable, Identifiable {
+nonisolated struct ArtifactInfo: Codable, Sendable, Identifiable {
let id: Int
let filename: String
let checksum: String
@@ -9,13 +9,13 @@ struct ArtifactInfo: Codable, Sendable, Identifiable {
let url: URL
}
-struct ArtifactPage: Codable, Sendable {
+nonisolated struct ArtifactPage: Codable, Sendable {
let results: [ArtifactInfo]
let cursor: String?
}
/// A reference (tag) that has associated artifacts.
-struct ReferenceWithArtifacts: Codable, Sendable, Identifiable {
+nonisolated struct ReferenceWithArtifacts: Codable, Sendable, Identifiable {
var id: String { name }
let name: String
let artifacts: [ArtifactInfo]
diff --git a/Hutch/Models/Builds.swift b/Hutch/Models/Builds.swift
index 3926888..48a9c91 100644
--- a/Hutch/Models/Builds.swift
+++ b/Hutch/Models/Builds.swift
@@ -3,7 +3,7 @@ import Foundation
// MARK: - Enums
/// Status of a build job.
-enum JobStatus: String, Codable, Sendable {
+nonisolated enum JobStatus: String, Codable, Sendable {
case pending = "PENDING"
case queued = "QUEUED"
case running = "RUNNING"
@@ -30,7 +30,7 @@ enum JobStatus: String, Codable, Sendable {
}
/// Status of a single build task within a job.
-enum TaskStatus: String, Codable, Sendable {
+nonisolated enum TaskStatus: String, Codable, Sendable {
case pending = "PENDING"
case running = "RUNNING"
case success = "SUCCESS"
@@ -41,7 +41,7 @@ enum TaskStatus: String, Codable, Sendable {
// MARK: - Build Task
/// A single task within a build job.
-struct BuildTask: Codable, Sendable, Identifiable, Equatable {
+nonisolated struct BuildTask: Codable, Sendable, Identifiable, Equatable {
private(set) var ordinal: Int?
let name: String
let status: TaskStatus
@@ -74,7 +74,7 @@ struct BuildTask: Codable, Sendable, Identifiable, Equatable {
// MARK: - Job Summary (for list view)
/// Lightweight job model matching the fields returned by the jobs list query.
-struct JobSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct JobSummary: Codable, Sendable, Identifiable, Hashable {
let id: Int
let created: Date
let updated: Date
@@ -103,7 +103,7 @@ struct JobSummary: Codable, Sendable, Identifiable, Hashable {
}
/// Minimal task info for the list query.
-struct JobTaskSummary: Codable, Sendable, Hashable {
+nonisolated struct JobTaskSummary: Codable, Sendable, Hashable {
let name: String
let status: TaskStatus
}
@@ -111,7 +111,7 @@ struct JobTaskSummary: Codable, Sendable, Hashable {
// MARK: - Job Detail (for detail view)
/// Full job model with all fields for the detail view.
-struct JobDetail: Codable, Sendable, Equatable {
+nonisolated struct JobDetail: Codable, Sendable, Equatable {
let id: Int
let created: Date
let updated: Date
@@ -128,7 +128,7 @@ struct JobDetail: Codable, Sendable, Equatable {
}
/// A downloadable artifact emitted by a build job.
-struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
+nonisolated struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
let id: Int
let created: Date
let path: String
@@ -145,14 +145,14 @@ struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
}
/// The log associated with a build job.
-struct BuildLog: Codable, Sendable, Equatable {
+nonisolated struct BuildLog: Codable, Sendable, Equatable {
let fullURL: String
}
// MARK: - Job Group
/// A group of related build jobs.
-struct JobGroup: Codable, Sendable, Identifiable {
+nonisolated struct JobGroup: Codable, Sendable, Identifiable {
let id: Int
let created: Date
let note: String?
diff --git a/Hutch/Models/CommitDetail.swift b/Hutch/Models/CommitDetail.swift
index c95199d..962d2a4 100644
--- a/Hutch/Models/CommitDetail.swift
+++ b/Hutch/Models/CommitDetail.swift
@@ -1,7 +1,7 @@
import Foundation
/// Full commit detail returned by the revparse_single query.
-struct CommitDetail: Codable, Sendable, Identifiable {
+nonisolated struct CommitDetail: Codable, Sendable, Identifiable {
let id: String
let shortId: String
let author: CommitAuthor
@@ -26,39 +26,39 @@ struct CommitDetail: Codable, Sendable, Identifiable {
}
}
-struct CommitTrailer: Codable, Sendable, Identifiable {
+nonisolated struct CommitTrailer: Codable, Sendable, Identifiable {
var id: String { "\(name):\(value)" }
let name: String
let value: String
}
-struct ParentCommit: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct ParentCommit: Codable, Sendable, Identifiable, Hashable {
let id: String
let shortId: String
let author: ParentAuthor
}
-struct ParentAuthor: Codable, Sendable, Hashable {
+nonisolated struct ParentAuthor: Codable, Sendable, Hashable {
let name: String
}
-struct CommitTree: Codable, Sendable {
+nonisolated struct CommitTree: Codable, Sendable {
let entries: CommitTreeEntries
}
-struct CommitTreeEntries: Codable, Sendable {
+nonisolated struct CommitTreeEntries: Codable, Sendable {
let results: [CommitTreeEntry]
let cursor: String?
}
-struct CommitTreeEntry: Codable, Sendable, Identifiable {
+nonisolated struct CommitTreeEntry: Codable, Sendable, Identifiable {
let id: String
let name: String
let mode: Int
let object: CommitTreeObject?
}
-struct CommitTreeObject: Codable, Sendable {
+nonisolated struct CommitTreeObject: Codable, Sendable {
let type: String?
let id: String?
let shortId: String?
diff --git a/Hutch/Models/CommitSummary.swift b/Hutch/Models/CommitSummary.swift
index 88c3e7d..99ed233 100644
--- a/Hutch/Models/CommitSummary.swift
+++ b/Hutch/Models/CommitSummary.swift
@@ -2,7 +2,7 @@ import Foundation
/// Lightweight commit model for list views. Matches the subset of fields
/// returned by the repository log query.
-struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
let id: String
let shortId: String
let author: CommitAuthor
@@ -15,7 +15,7 @@ struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
}
/// A compact author representation used in commit list responses.
-struct CommitAuthor: Codable, Sendable, Hashable {
+nonisolated struct CommitAuthor: Codable, Sendable, Hashable {
let name: String
let email: String?
let time: Date
diff --git a/Hutch/Models/ContributionCalendar.swift b/Hutch/Models/ContributionCalendar.swift
index a429fa2..cf87ca7 100644
--- a/Hutch/Models/ContributionCalendar.swift
+++ b/Hutch/Models/ContributionCalendar.swift
@@ -1,6 +1,6 @@
import Foundation
-struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
+nonisolated struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
let actor: String
let from: Date
let to: Date
@@ -9,7 +9,7 @@ struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
let indexingState: ContributionIndexingState
let days: [ContributionDay]
- enum CodingKeys: String, CodingKey {
+ nonisolated enum CodingKeys: String, CodingKey {
case actor
case from
case to
@@ -59,14 +59,14 @@ struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
}
}
-struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
+nonisolated struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
var id: Date { date }
let date: Date
let count: Int
let score: Double
- enum CodingKeys: String, CodingKey {
+ nonisolated enum CodingKeys: String, CodingKey {
case date
case count
case score
@@ -90,7 +90,7 @@ struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
}
}
-struct ContributionStatsResponse: Decodable, Sendable, Hashable {
+nonisolated struct ContributionStatsResponse: Decodable, Sendable, Hashable {
let actor: String
let from: Date
let to: Date
@@ -103,7 +103,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
let longestStreak: Int
let currentStreak: Int
- enum CodingKeys: String, CodingKey {
+ nonisolated enum CodingKeys: String, CodingKey {
case actor
case from
case to
@@ -117,7 +117,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
case currentStreak = "current_streak"
}
- struct StatsWindow: Sendable, Hashable {
+ nonisolated struct StatsWindow: Sendable, Hashable {
let actor: String
let from: Date
let to: Date
@@ -126,7 +126,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
let indexingState: ContributionIndexingState
}
- struct StatsTotals: Sendable, Hashable {
+ nonisolated struct StatsTotals: Sendable, Hashable {
let totalEvents: Int
let totalScore: Double
let activeDays: Int
@@ -181,13 +181,13 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
}
}
-enum ContributionIndexingState: String, Codable, Sendable, Hashable {
+nonisolated enum ContributionIndexingState: String, Codable, Sendable, Hashable {
case pending
case indexed
case error
}
-enum ContributionIntensity: Int, Sendable, CaseIterable {
+nonisolated enum ContributionIntensity: Int, Sendable, CaseIterable {
case empty = 0
case level1 = 1
case level2 = 2
@@ -210,12 +210,12 @@ enum ContributionIntensity: Int, Sendable, CaseIterable {
}
}
-struct ContributionWeek: Sendable, Hashable {
+nonisolated struct ContributionWeek: Sendable, Hashable {
let startDate: Date
let days: [ContributionDay]
}
-enum ContributionCalendarLayout {
+nonisolated enum ContributionCalendarLayout {
static func weekColumns(
from days: [ContributionDay],
calendar: Calendar = .contributionCalendar
@@ -241,7 +241,7 @@ enum ContributionCalendarLayout {
}
}
-enum ContributionDateParser {
+nonisolated enum ContributionDateParser {
static func parse(_ rawValue: String) -> Date? {
let parts = rawValue.split(separator: "-", omittingEmptySubsequences: false)
guard
@@ -348,7 +348,7 @@ enum ContributionDateParser {
}
}
-extension Calendar {
+nonisolated extension Calendar {
static var contributionCalendar: Calendar {
var calendar = Calendar(identifier: .gregorian)
calendar.firstWeekday = 1
diff --git a/Hutch/Models/Git.swift b/Hutch/Models/Git.swift
index 26a5e2d..ae74f19 100644
--- a/Hutch/Models/Git.swift
+++ b/Hutch/Models/Git.swift
@@ -3,14 +3,14 @@ import Foundation
// MARK: - Enums
/// Repository visibility level.
-enum Visibility: String, Codable, Sendable {
+nonisolated enum Visibility: String, Codable, Sendable {
case publicVisibility = "PUBLIC"
case unlisted = "UNLISTED"
case privateVisibility = "PRIVATE"
}
/// Repository access mode.
-enum AccessMode: String, Codable, Sendable, CaseIterable {
+nonisolated enum AccessMode: String, Codable, Sendable, CaseIterable {
case ro = "RO"
case rw = "RW"
}
@@ -19,14 +19,14 @@ enum AccessMode: String, Codable, Sendable, CaseIterable {
/// The `Entity` GraphQL interface from git.sr.ht. Represents the owner of a
/// resource (typically a user).
-struct Entity: Codable, Sendable, Hashable {
+nonisolated struct Entity: Codable, Sendable, Hashable {
let canonicalName: String
}
// MARK: - Repository
/// A git repository from git.sr.ht.
-struct Repository: Codable, Sendable, Identifiable {
+nonisolated struct Repository: Codable, Sendable, Identifiable {
let id: Int
let created: Date
let updated: Date
@@ -37,7 +37,7 @@ struct Repository: Codable, Sendable, Identifiable {
let accessMode: AccessMode
let owner: Entity
- enum CodingKeys: String, CodingKey {
+ nonisolated enum CodingKeys: String, CodingKey {
case id, created, updated, name, description, visibility, readme
case accessMode = "access"
case owner
@@ -47,7 +47,7 @@ struct Repository: Codable, Sendable, Identifiable {
// MARK: - Signature
/// A Git commit/tag signature (author or committer).
-struct Signature: Codable, Sendable {
+nonisolated struct Signature: Codable, Sendable {
let name: String
let email: String
let time: Date
@@ -56,7 +56,7 @@ struct Signature: Codable, Sendable {
// MARK: - Trailer
/// A Git commit trailer (e.g. "Signed-off-by", "Co-authored-by").
-struct Trailer: Codable, Sendable {
+nonisolated struct Trailer: Codable, Sendable {
let name: String
let value: String
}
@@ -64,7 +64,7 @@ struct Trailer: Codable, Sendable {
// MARK: - Commit
/// A Git commit from git.sr.ht.
-struct Commit: Codable, Sendable, Identifiable {
+nonisolated struct Commit: Codable, Sendable, Identifiable {
let id: String
let shortId: String
let author: Signature
@@ -77,13 +77,13 @@ struct Commit: Codable, Sendable, Identifiable {
// MARK: - Reference
/// A Git reference (branch or tag name).
-struct Reference: Codable, Sendable, Hashable {
+nonisolated struct Reference: Codable, Sendable, Hashable {
let name: String
let target: String?
}
/// A Git reference enriched with the date of its tip commit or tag, for display in the Refs tab.
-struct ReferenceDetail: Sendable, Hashable {
+nonisolated struct ReferenceDetail: Sendable, Hashable {
let name: String
let target: String?
let date: Date?
@@ -92,7 +92,7 @@ struct ReferenceDetail: Sendable, Hashable {
// MARK: - TreeEntry
/// An entry in a Git tree (file or directory).
-struct TreeEntry: Codable, Sendable, Identifiable {
+nonisolated struct TreeEntry: Codable, Sendable, Identifiable {
let id: String
let name: String
let mode: Int?
@@ -104,41 +104,41 @@ struct TreeEntry: Codable, Sendable, Identifiable {
/// Both TextBlob and BinaryBlob have type == "BLOB"; they are
/// distinguished by the presence of the "text" key (TextBlob) vs
/// the "content" key (BinaryBlob).
-enum GitObject: Sendable {
+nonisolated enum GitObject: Sendable {
case tree(GitTree)
case textBlob(GitTextBlob)
case binaryBlob(GitBinaryBlob)
case unknown
}
-struct GitTree: Codable, Sendable {
+nonisolated struct GitTree: Codable, Sendable {
let id: String?
let shortId: String?
let entries: GitTreeEntryPage?
}
-struct GitTextBlob: Codable, Sendable {
+nonisolated struct GitTextBlob: Codable, Sendable {
let id: String?
let shortId: String?
let text: String?
let size: Int?
}
-struct GitBinaryBlob: Codable, Sendable {
+nonisolated struct GitBinaryBlob: Codable, Sendable {
let id: String?
let shortId: String?
let size: Int?
let content: String?
}
-struct GitTreeEntryPage: Codable, Sendable {
+nonisolated struct GitTreeEntryPage: Codable, Sendable {
let results: [TreeEntry]
let cursor: String?
}
// MARK: - GitObject Codable
-extension GitObject: Codable {
+nonisolated extension GitObject: Codable {
private enum CodingKeys: String, CodingKey {
case type, id, shortId, entries, text, size, content
case typename = "__typename"
@@ -211,7 +211,7 @@ extension GitObject: Codable {
}
/// Convenience helpers for checking object type.
-extension GitObject {
+nonisolated extension GitObject {
var isTree: Bool {
if case .tree = self { return true }
return false
@@ -230,7 +230,7 @@ extension GitObject {
// MARK: - Tag
/// An annotated Git tag from git.sr.ht.
-struct Tag: Codable, Sendable, Identifiable {
+nonisolated struct Tag: Codable, Sendable, Identifiable {
let id: String
let shortId: String
let name: String
@@ -241,7 +241,7 @@ struct Tag: Codable, Sendable, Identifiable {
// MARK: - Artifact
/// A release artifact attached to a Git tag.
-struct Artifact: Codable, Sendable, Identifiable {
+nonisolated struct Artifact: Codable, Sendable, Identifiable {
let id: Int
let created: Date
let filename: String
diff --git a/Hutch/Models/Inbox.swift b/Hutch/Models/Inbox.swift
index c8e41bb..dfed719 100644
--- a/Hutch/Models/Inbox.swift
+++ b/Hutch/Models/Inbox.swift
@@ -1,6 +1,6 @@
import Foundation
-struct InboxThreadSummary: Identifiable, Hashable, Sendable {
+nonisolated struct InboxThreadSummary: Identifiable, Hashable, Sendable {
let rootEmailID: Int
let rootMessageID: String
let threadRootEmailIDs: [Int]
@@ -76,7 +76,7 @@ struct InboxThreadSummary: Identifiable, Hashable, Sendable {
}
}
-struct InboxMessage: Identifiable, Hashable, Sendable {
+nonisolated struct InboxMessage: Identifiable, Hashable, Sendable {
let id: Int
let author: Entity
let date: Date
@@ -89,12 +89,12 @@ struct InboxMessage: Identifiable, Hashable, Sendable {
let rawMessageURL: URL?
}
-enum InboxMessageContentBlock: Hashable, Sendable {
+nonisolated enum InboxMessageContentBlock: Hashable, Sendable {
case plainText(String)
case diff(String)
}
-struct InboxThreadDetail: Sendable {
+nonisolated struct InboxThreadDetail: Sendable {
let id: String
let rootEmailID: Int
let rootMessageID: String
@@ -114,7 +114,7 @@ struct InboxThreadDetail: Sendable {
}
}
-extension InboxThreadDetail {
+nonisolated extension InboxThreadDetail {
var replyRecipient: String {
"\(listOwner.canonicalName)/\(listName)@lists.sr.ht"
}
@@ -144,7 +144,7 @@ extension InboxThreadDetail {
}
}
-struct MailComposeDraft: Sendable {
+nonisolated struct MailComposeDraft: Sendable {
let recipients: [String]
let ccRecipients: [String]
let subject: String
@@ -155,20 +155,20 @@ struct MailComposeDraft: Sendable {
}
}
-extension MailComposeDraft: Identifiable {}
+nonisolated extension MailComposeDraft: Identifiable {}
-struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable {
+nonisolated struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable {
let id: Int
let rid: String
let name: String
let owner: Entity
}
-struct InboxPatchPreview: Decodable, Sendable, Hashable {
+nonisolated struct InboxPatchPreview: Decodable, Sendable, Hashable {
let subject: String?
}
-enum InboxReadStateStore {
+nonisolated enum InboxReadStateStore {
private static let key = "InboxThreadLastViewed"
private static let baselineKey = "InboxUnreadBaseline"
diff --git a/Hutch/Models/Meta.swift b/Hutch/Models/Meta.swift
index f52fc76..e0a09de 100644
--- a/Hutch/Models/Meta.swift
+++ b/Hutch/Models/Meta.swift
@@ -3,7 +3,7 @@ import Foundation
// MARK: - User Profile (full)
/// Extended user profile from meta.sr.ht with all fields from the `me` query.
-struct UserProfile: Codable, Sendable {
+nonisolated struct UserProfile: Codable, Sendable {
let username: String
let canonicalName: String
let email: String
@@ -20,7 +20,7 @@ struct UserProfile: Codable, Sendable {
// MARK: - SSH Key
-struct SSHKey: Codable, Sendable, Identifiable {
+nonisolated struct SSHKey: Codable, Sendable, Identifiable {
let id: Int
let comment: String?
let created: Date
@@ -34,27 +34,27 @@ struct SSHKey: Codable, Sendable, Identifiable {
}
}
-struct SSHKeyPage: Codable, Sendable {
+nonisolated struct SSHKeyPage: Codable, Sendable {
let results: [SSHKey]
let cursor: String?
}
// MARK: - PGP Key
-struct PGPKey: Codable, Sendable, Identifiable {
+nonisolated struct PGPKey: Codable, Sendable, Identifiable {
let id: Int
let fingerprint: String
let created: Date
}
-struct PGPKeyPage: Codable, Sendable {
+nonisolated struct PGPKeyPage: Codable, Sendable {
let results: [PGPKey]
let cursor: String?
}
// MARK: - Subscription
-struct Subscription: Codable, Sendable {
+nonisolated struct Subscription: Codable, Sendable {
let status: String?
let autorenew: Bool?
let interval: String?
@@ -62,7 +62,7 @@ struct Subscription: Codable, Sendable {
// MARK: - Personal Access Token
-struct PersonalAccessToken: Codable, Sendable, Identifiable {
+nonisolated struct PersonalAccessToken: Codable, Sendable, Identifiable {
let id: Int
let issued: Date
let expires: Date?
@@ -72,7 +72,7 @@ struct PersonalAccessToken: Codable, Sendable, Identifiable {
/// One entry in meta.sr.ht's audit log: a security-relevant action on the
/// account, with the address it came from.
-struct AuditLogEntry: Codable, Sendable, Identifiable {
+nonisolated struct AuditLogEntry: Codable, Sendable, Identifiable {
let id: Int
let created: Date
let ipAddress: String
diff --git a/Hutch/Models/Paste.swift b/Hutch/Models/Paste.swift
index b4c8f18..a8bf926 100644
--- a/Hutch/Models/Paste.swift
+++ b/Hutch/Models/Paste.swift
@@ -1,6 +1,6 @@
import Foundation
-struct PasteFile: Codable, Sendable, Hashable, Identifiable {
+nonisolated struct PasteFile: Codable, Sendable, Hashable, Identifiable {
let filename: String?
let hash: String
let contents: URL?
@@ -8,7 +8,7 @@ struct PasteFile: Codable, Sendable, Hashable, Identifiable {
var id: String { hash }
}
-struct Paste: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct Paste: Codable, Sendable, Identifiable, Hashable {
let id: String
let created: Date
let visibility: Visibility
@@ -16,7 +16,7 @@ struct Paste: Codable, Sendable, Identifiable, Hashable {
let user: Entity
}
-struct PasteUploadDraft: Identifiable, Equatable, Sendable {
+nonisolated struct PasteUploadDraft: Identifiable, Equatable, Sendable {
let id: UUID
var filename: String
var contents: String
diff --git a/Hutch/Models/Patchset.swift b/Hutch/Models/Patchset.swift
index 330bbbc..cc10b35 100644
--- a/Hutch/Models/Patchset.swift
+++ b/Hutch/Models/Patchset.swift
@@ -1,7 +1,7 @@
import Foundation
/// Review state of a patchset on lists.sr.ht.
-enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
+nonisolated enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
case unknown = "UNKNOWN"
case proposed = "PROPOSED"
case needsRevision = "NEEDS_REVISION"
@@ -55,7 +55,7 @@ enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
/// A patchset as it appears in a mailing list listing, derived from the thread's
/// root email rather than a dedicated patchsets query — `MailingList` exposes no
/// such field.
-struct PatchsetSummary: Identifiable, Hashable, Sendable {
+nonisolated struct PatchsetSummary: Identifiable, Hashable, Sendable {
let id: Int
let subject: String
let version: Int
@@ -70,7 +70,7 @@ struct PatchsetSummary: Identifiable, Hashable, Sendable {
}
/// One email within a patchset: either the cover letter or a single patch.
-struct PatchsetEmail: Identifiable, Hashable, Sendable {
+nonisolated struct PatchsetEmail: Identifiable, Hashable, Sendable {
let id: Int
let subject: String
let date: Date?
@@ -88,13 +88,13 @@ struct PatchsetEmail: Identifiable, Hashable, Sendable {
}
/// A build or check reported against a patchset.
-struct PatchsetToolResult: Identifiable, Hashable, Sendable {
+nonisolated struct PatchsetToolResult: Identifiable, Hashable, Sendable {
let id: Int
let icon: PatchsetToolIcon
let details: String
}
-enum PatchsetToolIcon: String, Codable, Sendable {
+nonisolated enum PatchsetToolIcon: String, Codable, Sendable {
case pending = "PENDING"
case waiting = "WAITING"
case success = "SUCCESS"
@@ -112,7 +112,7 @@ enum PatchsetToolIcon: String, Codable, Sendable {
}
/// A patchset with its cover letter, patches, and review context.
-struct PatchsetDetail: Sendable {
+nonisolated struct PatchsetDetail: Sendable {
let id: Int
let created: Date
let updated: Date
diff --git a/Hutch/Models/Project.swift b/Hutch/Models/Project.swift
index 8f5aa68..bd1a688 100644
--- a/Hutch/Models/Project.swift
+++ b/Hutch/Models/Project.swift
@@ -1,7 +1,7 @@
import Foundation
-struct Project: Identifiable, Hashable, Sendable {
- struct MailingList: Identifiable, Hashable, Sendable {
+nonisolated struct Project: Identifiable, Hashable, Sendable {
+ nonisolated struct MailingList: Identifiable, Hashable, Sendable {
let id: String
let name: String
let description: String?
@@ -22,8 +22,8 @@ struct Project: Identifiable, Hashable, Sendable {
}
}
- struct SourceRepo: Identifiable, Hashable, Sendable {
- enum RepoType: String, Decodable, Sendable {
+ nonisolated struct SourceRepo: Identifiable, Hashable, Sendable {
+ nonisolated enum RepoType: String, Decodable, Sendable {
case git = "GIT"
case hg = "HG"
@@ -47,7 +47,7 @@ struct Project: Identifiable, Hashable, Sendable {
}
}
- struct Tracker: Identifiable, Hashable, Sendable {
+ nonisolated struct Tracker: Identifiable, Hashable, Sendable {
let id: String
let name: String
let description: String?
@@ -72,7 +72,7 @@ struct Project: Identifiable, Hashable, Sendable {
let isFullyLoaded: Bool
/// Identity and display fields for a project.
- struct Metadata: Sendable, Hashable {
+ nonisolated struct Metadata: Sendable, Hashable {
let id: String
let name: String
let description: String?
@@ -82,7 +82,7 @@ struct Project: Identifiable, Hashable, Sendable {
let updated: Date
}
- struct Resources: Sendable, Hashable {
+ nonisolated struct Resources: Sendable, Hashable {
let mailingLists: [MailingList]
let sources: [SourceRepo]
let trackers: [Tracker]
@@ -171,7 +171,7 @@ struct Project: Identifiable, Hashable, Sendable {
}
}
-extension Project.MailingList {
+nonisolated extension Project.MailingList {
var displayName: String {
Project.normalizedText(name) ?? "Untitled Mailing List"
}
@@ -185,7 +185,7 @@ extension Project.MailingList {
}
}
-extension Project.SourceRepo {
+nonisolated extension Project.SourceRepo {
var displayName: String {
Project.normalizedText(name) ?? "Untitled Repository"
}
@@ -203,7 +203,7 @@ extension Project.SourceRepo {
}
}
-extension Project.Tracker {
+nonisolated extension Project.Tracker {
var displayName: String {
Project.normalizedText(name) ?? "Untitled Tracker"
}
@@ -221,13 +221,13 @@ extension Project.Tracker {
}
}
-extension String {
+nonisolated extension String {
var srhtUsername: String {
hasPrefix("~") ? String(dropFirst()) : self
}
}
-extension Visibility {
+nonisolated extension Visibility {
var displayName: String {
switch self {
case .publicVisibility:
diff --git a/Hutch/Models/RepositoryACL.swift b/Hutch/Models/RepositoryACL.swift
index e5f5eb1..e5d93f6 100644
--- a/Hutch/Models/RepositoryACL.swift
+++ b/Hutch/Models/RepositoryACL.swift
@@ -1,12 +1,12 @@
import Foundation
-struct RepositoryACLEntry: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct RepositoryACLEntry: Codable, Sendable, Identifiable, Hashable {
let id: Int
let mode: AccessMode
let entity: Entity
}
-extension AccessMode {
+nonisolated extension AccessMode {
var shortLabel: String { rawValue }
var displayName: String {
diff --git a/Hutch/Models/RepositorySummary.swift b/Hutch/Models/RepositorySummary.swift
index b41589b..40ec7a4 100644
--- a/Hutch/Models/RepositorySummary.swift
+++ b/Hutch/Models/RepositorySummary.swift
@@ -3,7 +3,7 @@ import Foundation
/// Lightweight repository model matching the fields returned by the
/// repositories list query. Avoids optionalizing all fields on the full
/// `Repository` model.
-struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
let id: Int
/// GraphQL resource identifier used by `repository(rid:)` queries.
let rid: String
@@ -15,13 +15,13 @@ struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
let owner: Entity
let head: Reference?
- enum CodingKeys: String, CodingKey {
+ nonisolated enum CodingKeys: String, CodingKey {
case id, rid, service, name, description, visibility, updated, owner
case head = "HEAD"
}
/// Grouped initializer fields (single parameter keeps APIs explicit without exceeding parameter-count limits).
- struct Fields: Sendable, Hashable {
+ nonisolated struct Fields: Sendable, Hashable {
let id: Int
let rid: String
let service: SRHTService
@@ -59,7 +59,7 @@ struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
}
}
-extension RepositorySummary {
+nonisolated extension RepositorySummary {
var defaultBranchName: String? {
head?.name.replacingOccurrences(of: "refs/heads/", with: "")
}
diff --git a/Hutch/Models/SystemStatusModels.swift b/Hutch/Models/SystemStatusModels.swift
index 9a664b0..ddd659c 100644
--- a/Hutch/Models/SystemStatusModels.swift
+++ b/Hutch/Models/SystemStatusModels.swift
@@ -1,6 +1,6 @@
import Foundation
-enum StatusLevel: String, Codable, Sendable {
+nonisolated enum StatusLevel: String, Codable, Sendable {
case operational
case degraded
case majorOutage
@@ -32,7 +32,7 @@ enum StatusLevel: String, Codable, Sendable {
}
}
-struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
+nonisolated struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
let id: String
let name: String
let slug: String?
@@ -40,7 +40,7 @@ struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
let description: String?
}
-struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
+nonisolated struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
let id: String
let title: String
let summary: String?
@@ -50,7 +50,7 @@ struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
let isActive: Bool?
}
-struct SystemStatusSnapshot: Hashable, Codable, Sendable {
+nonisolated struct SystemStatusSnapshot: Hashable, Codable, Sendable {
let services: [StatusServiceState]
let activeIncidents: [StatusIncident]
let lastUpdated: Date
@@ -78,7 +78,7 @@ struct SystemStatusSnapshot: Hashable, Codable, Sendable {
}
}
-struct SystemStatusPageData: Sendable {
+nonisolated struct SystemStatusPageData: Sendable {
let snapshot: SystemStatusSnapshot
let recentIncidents: [StatusIncident]
}
diff --git a/Hutch/Models/TicketBulkAction.swift b/Hutch/Models/TicketBulkAction.swift
index dcf7676..ea9cc01 100644
--- a/Hutch/Models/TicketBulkAction.swift
+++ b/Hutch/Models/TicketBulkAction.swift
@@ -1,6 +1,6 @@
import Foundation
-enum TicketBulkActionKind: String, Sendable {
+nonisolated enum TicketBulkActionKind: String, Sendable {
case close
case assign
@@ -23,7 +23,7 @@ enum TicketBulkActionKind: String, Sendable {
}
}
-struct TicketBulkActionResult: Identifiable, Sendable {
+nonisolated struct TicketBulkActionResult: Identifiable, Sendable {
let id = UUID()
let action: TicketBulkActionKind
let totalCount: Int
@@ -82,7 +82,7 @@ struct TicketBulkActionResult: Identifiable, Sendable {
}
}
-struct TicketBulkActionFailure: Sendable {
+nonisolated struct TicketBulkActionFailure: Sendable {
let ticketID: Int
let message: String
}
diff --git a/Hutch/Models/Todo.swift b/Hutch/Models/Todo.swift
index 129d4bf..d2ae889 100644
--- a/Hutch/Models/Todo.swift
+++ b/Hutch/Models/Todo.swift
@@ -3,7 +3,7 @@ import Foundation
// MARK: - Enums
/// Status of a ticket.
-enum TicketStatus: String, Codable, Sendable, CaseIterable {
+nonisolated enum TicketStatus: String, Codable, Sendable, CaseIterable {
case reported = "REPORTED"
case confirmed = "CONFIRMED"
case inProgress = "IN_PROGRESS"
@@ -30,7 +30,7 @@ enum TicketStatus: String, Codable, Sendable, CaseIterable {
}
/// Resolution of a ticket.
-enum TicketResolution: String, Codable, Sendable {
+nonisolated enum TicketResolution: String, Codable, Sendable {
case unresolved = "UNRESOLVED"
case fixed = "FIXED"
case implemented = "IMPLEMENTED"
@@ -64,7 +64,7 @@ enum TicketResolution: String, Codable, Sendable {
}
/// Authenticity of a ticket or comment.
-enum Authenticity: String, Codable, Sendable {
+nonisolated enum Authenticity: String, Codable, Sendable {
case authentic = "AUTHENTIC"
case tampered = "TAMPERED"
case unauthenticated = "UNAUTHENTICATED"
@@ -74,7 +74,7 @@ enum Authenticity: String, Codable, Sendable {
/// A label that can be applied to tickets.
/// Named `TicketLabel` to avoid collision with `SwiftUI.Label`.
-struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
let id: Int
let name: String
let backgroundColor: String
@@ -83,7 +83,7 @@ struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
// MARK: - Tracker ACL
-struct TrackerACLPermissions: Codable, Sendable, Hashable {
+nonisolated struct TrackerACLPermissions: Codable, Sendable, Hashable {
let browse: Bool
let submit: Bool
let comment: Bool
@@ -91,7 +91,7 @@ struct TrackerACLPermissions: Codable, Sendable, Hashable {
let triage: Bool
}
-struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
let id: Int
let created: Date
let entity: Entity
@@ -112,7 +112,7 @@ struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
}
}
-struct DefaultTrackerACL: Codable, Sendable, Hashable {
+nonisolated struct DefaultTrackerACL: Codable, Sendable, Hashable {
let browse: Bool
let submit: Bool
let comment: Bool
@@ -133,7 +133,7 @@ struct DefaultTrackerACL: Codable, Sendable, Hashable {
// MARK: - Tracker
/// A bug tracker from todo.sr.ht.
-struct Tracker: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct Tracker: Codable, Sendable, Identifiable, Hashable {
let id: Int
let created: Date
let updated: Date
@@ -154,7 +154,7 @@ struct Tracker: Codable, Sendable, Identifiable, Hashable {
// MARK: - Tracker Summary (for list view)
/// Lightweight tracker model matching the fields returned by the trackers list query.
-struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
let id: Int
/// GraphQL resource identifier used by `tracker(id:)` queries.
let rid: String
@@ -168,7 +168,7 @@ struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
// MARK: - Ticket Summary (for list view)
/// Lightweight ticket model for the list query.
-struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
let id: Int
let title: String
let status: TicketStatus
@@ -190,7 +190,7 @@ struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
// MARK: - Ticket Detail
/// Full ticket model for the detail view.
-struct TicketDetail: Codable, Sendable {
+nonisolated struct TicketDetail: Codable, Sendable {
let id: Int
let created: Date
let updated: Date
@@ -207,7 +207,7 @@ struct TicketDetail: Codable, Sendable {
// MARK: - Event
/// A timeline event on a ticket. Each event contains one or more changes.
-struct TicketEvent: Codable, Sendable, Identifiable {
+nonisolated struct TicketEvent: Codable, Sendable, Identifiable {
let id: Int
let created: Date
var changes: [EventChange]
@@ -215,7 +215,7 @@ struct TicketEvent: Codable, Sendable, Identifiable {
/// A single change within an event, decoded from the polymorphic EventDetail
/// interface using inline fragments.
-struct EventChange: Codable, Sendable, Identifiable {
+nonisolated struct EventChange: Codable, Sendable, Identifiable {
let id: UUID
let eventType: String
@@ -272,12 +272,12 @@ struct EventChange: Codable, Sendable, Identifiable {
/// Decoded from the `mentioned` field which may be a Ticket (with `id`) or
/// an Entity (with `canonicalName`) depending on the event type.
-struct MentionTarget: Codable, Sendable {
+nonisolated struct MentionTarget: Codable, Sendable {
let id: Int?
let canonicalName: String?
}
/// Label info as returned within a LabelUpdate event change.
-struct EventLabel: Codable, Sendable {
+nonisolated struct EventLabel: Codable, Sendable {
let name: String
}
diff --git a/Hutch/Models/User.swift b/Hutch/Models/User.swift
index 4ab7315..10576ad 100644
--- a/Hutch/Models/User.swift
+++ b/Hutch/Models/User.swift
@@ -1,7 +1,7 @@
import Foundation
/// A Sourcehut user from meta.sr.ht.
-struct User: Decodable, Sendable, Hashable {
+nonisolated struct User: Decodable, Sendable, Hashable {
let id: Int
let created: String?
let updated: String?
diff --git a/Hutch/Networking/RepositoryDeployKeyService.swift b/Hutch/Networking/RepositoryDeployKeyService.swift
new file mode 100644
index 0000000..39e2da1
--- /dev/null
+++ b/Hutch/Networking/RepositoryDeployKeyService.swift
@@ -0,0 +1,100 @@
+import Foundation
+
+/// A per-repository deploy key (git.sr.ht `SSHKey` under `Repository.deployKeys`).
+struct RepositoryDeployKey: Decodable, Sendable, Identifiable, Hashable {
+ let rid: String
+ let keyType: String
+ let fingerprintSHA256: String
+ let comment: String?
+ let access: AccessMode
+
+ var id: String { rid }
+}
+
+private struct DeployKeysQueryResponse: Decodable, Sendable {
+ let repository: DeployKeysRepository?
+}
+
+private struct DeployKeysRepository: Decodable, Sendable {
+ let deployKeys: DeployKeysPage
+}
+
+private struct DeployKeysPage: Decodable, Sendable {
+ let results: [RepositoryDeployKey]
+}
+
+/// `createDeployKey`'s response returns an empty `access` (the stored value is
+/// correct — the list query reports it), so we select only `rid` here and let
+/// callers reload rather than decode the partial key.
+private struct CreateDeployKeyResponse: Decodable, Sendable {}
+
+/// Delete returns the removed key; only success matters here.
+private struct DeleteDeployKeyResponse: Decodable, Sendable {}
+
+/// Deploy keys are a git.sr.ht capability (`createDeployKey` / `deleteDeployKey`),
+/// owner-only, alongside repository ACLs.
+struct RepositoryDeployKeyService: Sendable {
+ private let client: SRHTClient
+
+ init(client: SRHTClient) {
+ self.client = client
+ }
+
+ func fetchDeployKeys(repositoryRid: String) async throws -> [RepositoryDeployKey] {
+ let response = try await client.execute(
+ service: .git,
+ query: Self.deployKeysQuery,
+ variables: ["rid": repositoryRid],
+ responseType: DeployKeysQueryResponse.self
+ )
+ return response.repository?.deployKeys.results ?? []
+ }
+
+ func createDeployKey(repositoryRid: String, mode: AccessMode, key: String) async throws {
+ _ = try await client.execute(
+ service: .git,
+ query: Self.createDeployKeyMutation,
+ variables: ["repo": repositoryRid, "mode": mode.rawValue, "key": key],
+ responseType: CreateDeployKeyResponse.self
+ )
+ }
+
+ func deleteDeployKey(rid: String) async throws {
+ _ = try await client.execute(
+ service: .git,
+ query: Self.deleteDeployKeyMutation,
+ variables: ["rid": rid],
+ responseType: DeleteDeployKeyResponse.self
+ )
+ }
+}
+
+private extension RepositoryDeployKeyService {
+ static let deployKeysQuery = """
+ query repositoryDeployKeys($rid: ID!) {
+ repository(rid: $rid) {
+ deployKeys {
+ results {
+ rid
+ keyType
+ fingerprintSHA256
+ comment
+ access
+ }
+ }
+ }
+ }
+ """
+
+ static let createDeployKeyMutation = """
+ mutation createDeployKey($repo: ID!, $mode: AccessMode!, $key: String!) {
+ createDeployKey(repo: $repo, mode: $mode, key: $key) { rid }
+ }
+ """
+
+ static let deleteDeployKeyMutation = """
+ mutation deleteDeployKey($rid: ID!) {
+ deleteDeployKey(rid: $rid) { rid }
+ }
+ """
+}
diff --git a/Hutch/Views/Lists/MailingListListView.swift b/Hutch/Views/Lists/MailingListListView.swift
index 1b159bf..b6ed2ff 100644
--- a/Hutch/Views/Lists/MailingListListView.swift
+++ b/Hutch/Views/Lists/MailingListListView.swift
@@ -100,7 +100,7 @@ final class MailingListListViewModel {
query: Self.createMailingListMutation,
variables: [
"name": trimmedName,
- "description": trimmedDescription.isEmpty ? nil as String? as Any : trimmedDescription,
+ "description": trimmedDescription.isEmpty ? nil as String? as any Sendable : trimmedDescription,
"visibility": visibility.rawValue
],
responseType: Response.self
diff --git a/Hutch/Views/More/TipStoreViewModel.swift b/Hutch/Views/More/TipStoreViewModel.swift
index bc909c1..043b218 100644
--- a/Hutch/Views/More/TipStoreViewModel.swift
+++ b/Hutch/Views/More/TipStoreViewModel.swift
@@ -32,7 +32,9 @@ final class TipStoreViewModel {
var errorMessage: String?
var statusMessage: String?
- private var transactionUpdatesTask: Task<Void, Never>?
+ // Internal task handle, not observable state; assigned only on the main
+ // actor and read once from the nonisolated deinit.
+ @ObservationIgnored nonisolated(unsafe) private var transactionUpdatesTask: Task<Void, Never>?
init() {
transactionUpdatesTask = Task.detached(priority: .background) {
diff --git a/Hutch/Views/Projects/ProjectMailingListView.swift b/Hutch/Views/Projects/ProjectMailingListView.swift
index 10bb19e..a4decbb 100644
--- a/Hutch/Views/Projects/ProjectMailingListView.swift
+++ b/Hutch/Views/Projects/ProjectMailingListView.swift
@@ -37,6 +37,36 @@ private struct PatchsetSummaryPayload: Decodable, Sendable {
let status: PatchsetStatus
}
+private struct ListMetaResponse: Decodable, Sendable {
+ let list: ListMetaPayload?
+}
+
+private struct ListMetaPayload: Decodable, Sendable {
+ let id: Int
+ let owner: Entity
+}
+
+private struct SubscriptionRidsResponse: Decodable, Sendable {
+ let subscriptions: SubscriptionRidsPage
+}
+
+private struct SubscriptionRidsPage: Decodable, Sendable {
+ let results: [SubscriptionRidEntry]
+ let cursor: String?
+}
+
+private struct SubscriptionRidEntry: Decodable, Sendable {
+ let list: SubscriptionRidList?
+}
+
+private struct SubscriptionRidList: Decodable, Sendable {
+ let rid: String
+}
+
+/// Toggle mutations return the subscription (nullable on unsubscribe); only
+/// success matters.
+private struct SubscriptionToggleResponse: Decodable, Sendable {}
+
@Observable
@MainActor
final class MailingListDetailViewModel {
@@ -47,6 +77,13 @@ final class MailingListDetailViewModel {
var error: String?
var searchText = ""
+ /// Subscription state. `isSubscribed` is `nil` while unknown or unavailable
+ /// (the toggle stays hidden); `isOwnList` hides it for lists you own.
+ private(set) var listNumericID: Int?
+ private(set) var isSubscribed: Bool?
+ private(set) var isOwnList = false
+ private(set) var isTogglingSubscription = false
+
private let mailingList: InboxMailingListReference
private let client: SRHTClient
private let defaults: UserDefaults
@@ -86,6 +123,96 @@ final class MailingListDetailViewModel {
self.accountID = accountID
}
+ // MARK: - Subscription
+
+ private static let listMetaQuery = """
+ query listMeta($rid: ID!) {
+ list(rid: $rid) { id owner { canonicalName } }
+ }
+ """
+
+ // `MailingList.subscription` is unreliable (see the API-traps note), so
+ // subscribe state comes from the authoritative `subscriptions` query.
+ private static let subscriptionRidsQuery = """
+ query subscriptionRids($cursor: Cursor) {
+ subscriptions(cursor: $cursor) {
+ results {
+ ... on MailingListSubscription { list { rid } }
+ }
+ cursor
+ }
+ }
+ """
+
+ private static let subscribeMutation = """
+ mutation mailingListSubscribe($id: Int!) {
+ mailingListSubscribe(listID: $id) { id }
+ }
+ """
+
+ private static let unsubscribeMutation = """
+ mutation mailingListUnsubscribe($id: Int!) {
+ mailingListUnsubscribe(listID: $id) { id }
+ }
+ """
+
+ /// Resolves the list's numeric id, whether the viewer owns it, and — for
+ /// lists they don't own — whether they're subscribed.
+ func loadSubscriptionState(currentUserCanonicalName: String?) async {
+ do {
+ let meta = try await client.execute(
+ service: .lists,
+ query: Self.listMetaQuery,
+ variables: ["rid": mailingList.rid],
+ responseType: ListMetaResponse.self
+ )
+ guard let list = meta.list else { return }
+ listNumericID = list.id
+
+ if let currentUserCanonicalName, list.owner.canonicalName == currentUserCanonicalName {
+ isOwnList = true
+ return
+ }
+ isSubscribed = try await isSubscribed(toRid: mailingList.rid)
+ } catch {
+ // Leave state unknown; the toggle stays hidden rather than lying.
+ }
+ }
+
+ private func isSubscribed(toRid rid: String) async throws -> Bool {
+ var cursor: String?
+ repeat {
+ let response = try await client.execute(
+ service: .lists,
+ query: Self.subscriptionRidsQuery,
+ variables: cursor.map { ["cursor": $0] },
+ responseType: SubscriptionRidsResponse.self
+ )
+ if response.subscriptions.results.contains(where: { $0.list?.rid == rid }) {
+ return true
+ }
+ cursor = response.subscriptions.cursor
+ } while cursor != nil
+ return false
+ }
+
+ func toggleSubscription() async {
+ guard let id = listNumericID, let subscribed = isSubscribed, !isTogglingSubscription else { return }
+ isTogglingSubscription = true
+ defer { isTogglingSubscription = false }
+ do {
+ _ = try await client.execute(
+ service: .lists,
+ query: subscribed ? Self.unsubscribeMutation : Self.subscribeMutation,
+ variables: ["id": id],
+ responseType: SubscriptionToggleResponse.self
+ )
+ isSubscribed = !subscribed
+ } catch {
+ self.error = error.userFacingMessage
+ }
+ }
+
var filteredThreads: [InboxThreadSummary] {
Self.filterThreads(threads, matching: searchText)
}
@@ -386,6 +513,21 @@ struct MailingListDetailView: View {
.accessibilityLabel(isPinnedToHome ? "Unpin from Home" : "Pin to Home")
}
}
+ if let viewModel, !viewModel.isOwnList, let subscribed = viewModel.isSubscribed {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ Task { await viewModel.toggleSubscription() }
+ } label: {
+ if viewModel.isTogglingSubscription {
+ ProgressView().controlSize(.small)
+ } else {
+ Image(systemName: subscribed ? "bell.fill" : "bell")
+ }
+ }
+ .disabled(viewModel.isTogglingSubscription)
+ .accessibilityLabel(subscribed ? "Unsubscribe from list" : "Subscribe to list")
+ }
+ }
}
.task {
if viewModel == nil {
@@ -397,6 +539,7 @@ struct MailingListDetailView: View {
)
self.viewModel = viewModel
await viewModel.loadThreads()
+ await viewModel.loadSubscriptionState(currentUserCanonicalName: currentUserKey)
}
}
.onAppear {
diff --git a/Hutch/Views/Repositories/RepositoryDeployKeysView.swift b/Hutch/Views/Repositories/RepositoryDeployKeysView.swift
new file mode 100644
index 0000000..71e252b
--- /dev/null
+++ b/Hutch/Views/Repositories/RepositoryDeployKeysView.swift
@@ -0,0 +1,272 @@
+import SwiftUI
+
+@Observable
+@MainActor
+final class RepositoryDeployKeysViewModel {
+ private(set) var keys: [RepositoryDeployKey] = []
+ private(set) var isLoading = false
+ private(set) var isSaving = false
+ private(set) var deletingRID: String?
+ var loadError: String?
+ var error: String?
+ var saveError: String?
+
+ let repositoryRid: String
+ private let service: RepositoryDeployKeyService
+
+ init(repositoryRid: String, service: RepositoryDeployKeyService) {
+ self.repositoryRid = repositoryRid
+ self.service = service
+ }
+
+ func load() async {
+ guard !isLoading else { return }
+ isLoading = true
+ loadError = nil
+ defer { isLoading = false }
+ do {
+ keys = try await service.fetchDeployKeys(repositoryRid: repositoryRid)
+ } catch {
+ if keys.isEmpty {
+ loadError = error.userFacingMessage
+ } else {
+ self.error = error.userFacingMessage
+ }
+ }
+ }
+
+ func addKey(publicKey: String, mode: AccessMode) async -> Bool {
+ let trimmed = publicKey.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty, !isSaving else { return false }
+ isSaving = true
+ saveError = nil
+ defer { isSaving = false }
+ do {
+ try await service.createDeployKey(repositoryRid: repositoryRid, mode: mode, key: trimmed)
+ // The create response omits the key's fields, so reload the list.
+ keys = try await service.fetchDeployKeys(repositoryRid: repositoryRid)
+ return true
+ } catch {
+ saveError = error.userFacingMessage
+ return false
+ }
+ }
+
+ func deleteKey(_ key: RepositoryDeployKey) async {
+ guard deletingRID == nil else { return }
+ deletingRID = key.rid
+ error = nil
+ defer { deletingRID = nil }
+ do {
+ try await service.deleteDeployKey(rid: key.rid)
+ keys.removeAll { $0.rid == key.rid }
+ } catch {
+ self.error = error.userFacingMessage
+ }
+ }
+}
+
+struct RepositoryDeployKeysView: View {
+ let repository: RepositorySummary
+ let client: SRHTClient
+ var showsDoneButton = false
+
+ @Environment(\.dismiss) private var dismiss
+ @State private var viewModel: RepositoryDeployKeysViewModel?
+ @State private var showAddSheet = false
+ @State private var pendingDeletion: RepositoryDeployKey?
+
+ var body: some View {
+ Group {
+ if let viewModel {
+ content(viewModel)
+ } else {
+ SRHTLoadingStateView(message: "Loading deploy keys…")
+ }
+ }
+ .navigationTitle("Deploy Keys")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ if showsDoneButton {
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Done") { dismiss() }
+ }
+ }
+ if viewModel != nil {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ showAddSheet = true
+ } label: {
+ Image(systemName: "plus")
+ }
+ .accessibilityLabel("Add deploy key")
+ }
+ }
+ }
+ .task {
+ if viewModel == nil {
+ let vm = RepositoryDeployKeysViewModel(
+ repositoryRid: repository.rid,
+ service: RepositoryDeployKeyService(client: client)
+ )
+ viewModel = vm
+ await vm.load()
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func content(_ viewModel: RepositoryDeployKeysViewModel) -> some View {
+ Group {
+ if viewModel.isLoading, viewModel.keys.isEmpty, viewModel.loadError == nil {
+ SRHTLoadingStateView(message: "Loading deploy keys…")
+ } else if let loadError = viewModel.loadError, viewModel.keys.isEmpty {
+ SRHTErrorStateView(
+ title: "Couldn't Load Deploy Keys",
+ message: loadError,
+ retryAction: { await viewModel.load() }
+ )
+ } else {
+ List {
+ if viewModel.keys.isEmpty {
+ Section {
+ ContentUnavailableView(
+ "No Deploy Keys",
+ systemImage: "key",
+ description: Text("Add an SSH public key to grant this repository read or read/write access for automation.")
+ )
+ .themedRow()
+ }
+ } else {
+ Section {
+ ForEach(viewModel.keys) { key in
+ DeployKeyRow(key: key, isDeleting: viewModel.deletingRID == key.rid)
+ .themedRow()
+ .swipeActions(edge: .trailing, allowsFullSwipe: false) {
+ Button(role: .destructive) {
+ pendingDeletion = key
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ }
+ } footer: {
+ Text("Deploy keys are SSH keys scoped to this repository only.")
+ }
+ }
+ }
+ .themedList()
+ .refreshable { await viewModel.load() }
+ }
+ }
+ .srhtErrorBanner(error: Binding(get: { viewModel.error }, set: { viewModel.error = $0 }))
+ .confirmationDialog(
+ "Delete this deploy key?",
+ isPresented: Binding(get: { pendingDeletion != nil }, set: { if !$0 { pendingDeletion = nil } }),
+ titleVisibility: .visible
+ ) {
+ Button("Cancel", role: .cancel) { pendingDeletion = nil }
+ Button("Delete", role: .destructive) {
+ if let key = pendingDeletion {
+ pendingDeletion = nil
+ Task { await viewModel.deleteKey(key) }
+ }
+ }
+ } message: {
+ Text("This revokes the key's access to \(repository.name). This cannot be undone.")
+ }
+ .sheet(isPresented: $showAddSheet) {
+ AddDeployKeyView(viewModel: viewModel)
+ }
+ }
+}
+
+private struct DeployKeyRow: View {
+ let key: RepositoryDeployKey
+ let isDeleting: Bool
+
+ var body: some View {
+ HStack(spacing: 12) {
+ VStack(alignment: .leading, spacing: 3) {
+ Text(key.comment?.isEmpty == false ? key.comment! : key.keyType)
+ .font(.body)
+ .lineLimit(1)
+ Text(key.fingerprintSHA256)
+ .font(.caption.monospaced())
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ }
+ Spacer()
+ if isDeleting {
+ ProgressView().controlSize(.small)
+ } else {
+ Text(key.access.displayName)
+ .font(.caption.weight(.medium))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.vertical, 2)
+ }
+}
+
+private struct AddDeployKeyView: View {
+ let viewModel: RepositoryDeployKeysViewModel
+
+ @Environment(\.dismiss) private var dismiss
+ @State private var publicKey = ""
+ @State private var mode: AccessMode = .ro
+
+ var body: some View {
+ NavigationStack {
+ Form {
+ Section("SSH Public Key") {
+ TextField("ssh-ed25519 AAAA… comment", text: $publicKey, axis: .vertical)
+ .lineLimit(3...8)
+ .textInputAutocapitalization(.never)
+ .autocorrectionDisabled()
+ .font(.body.monospaced())
+ .themedRow()
+ }
+ Section {
+ Picker("Access", selection: $mode) {
+ Text("Read Only").tag(AccessMode.ro)
+ Text("Read/Write").tag(AccessMode.rw)
+ }
+ .themedRow()
+ } footer: {
+ Text("Read/Write lets the key push to this repository.")
+ }
+ if let saveError = viewModel.saveError, !saveError.isEmpty {
+ Section {
+ Text(saveError).foregroundStyle(.red).themedRow()
+ }
+ }
+ }
+ .themedList()
+ .navigationTitle("Add Deploy Key")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Cancel") { dismiss() }
+ }
+ ToolbarItem(placement: .confirmationAction) {
+ Button {
+ Task {
+ if await viewModel.addKey(publicKey: publicKey, mode: mode) {
+ dismiss()
+ }
+ }
+ } label: {
+ if viewModel.isSaving {
+ ProgressView().controlSize(.small)
+ } else {
+ Text("Add")
+ }
+ }
+ .disabled(publicKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSaving)
+ }
+ }
+ }
+ }
+}
diff --git a/Hutch/Views/Repositories/RepositoryDetailView.swift b/Hutch/Views/Repositories/RepositoryDetailView.swift
index 8f466ba..a4ac25e 100644
--- a/Hutch/Views/Repositories/RepositoryDetailView.swift
+++ b/Hutch/Views/Repositories/RepositoryDetailView.swift
@@ -12,6 +12,7 @@ struct RepositoryDetailView: View {
@State private var selectedTab: RepositoryDetailViewModel.Tab = .summary
@State private var showSettings = false
@State private var showACLs = false
+ @State private var showDeployKeys = false
@State private var currentRepository: RepositorySummary
@State private var pinChangeCount = 0
@@ -82,6 +83,15 @@ struct RepositoryDetailView: View {
)
}
}
+ .sheet(isPresented: $showDeployKeys) {
+ NavigationStack {
+ RepositoryDeployKeysView(
+ repository: currentRepository,
+ client: appState.client,
+ showsDoneButton: true
+ )
+ }
+ }
.task {
if viewModel == nil {
viewModel = RepositoryDetailViewModel(
@@ -201,6 +211,12 @@ struct RepositoryDetailView: View {
}
Button {
+ showDeployKeys = true
+ } label: {
+ Label("Deploy Keys", systemImage: "key")
+ }
+
+ Button {
showSettings = true
} label: {
Label("Repository Settings", systemImage: "gear")
diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
index 82e1592..62a8b12 100644
--- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
+++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
@@ -557,7 +557,7 @@ final class RepositoryDetailViewModel {
variables: [
"repoId": repository.id,
"revspec": revspec,
- "file": nil as String? as Any
+ "file": nil as String? as any Sendable
],
file: MultipartUploadFile(
variablePath: "file",
diff --git a/Hutch/Views/Repositories/SyntaxHighlighter.swift b/Hutch/Views/Repositories/SyntaxHighlighter.swift
index 5ae8f58..dfab443 100644
--- a/Hutch/Views/Repositories/SyntaxHighlighter.swift
+++ b/Hutch/Views/Repositories/SyntaxHighlighter.swift
@@ -3,7 +3,7 @@ import Highlightr
import SwiftUI
import UIKit
-enum SyntaxHighlightTheme {
+nonisolated enum SyntaxHighlightTheme {
case light
case dark
@@ -31,7 +31,7 @@ enum SyntaxHighlightTheme {
/// instance must stay on the thread/task that created it. Callers that fail to
/// resolve a language — or hit an unavailable engine — get `nil` and should
/// fall back to plain, escaped text.
-final class SyntaxHighlighter {
+nonisolated final class SyntaxHighlighter {
private let highlightr: Highlightr?
private let supportedLanguages: Set<String>
@@ -208,7 +208,7 @@ final class SyntaxHighlighter {
]
}
-private extension UIColor {
+nonisolated private extension UIColor {
/// `#rrggbb` for HTML inline styles, or `nil` if the color isn't RGB-convertible.
var hexRGBString: String? {
var red: CGFloat = 0
diff --git a/Hutch/Views/Settings/SettingsViewModel.swift b/Hutch/Views/Settings/SettingsViewModel.swift
index 8536e60..2d57ba5 100644
--- a/Hutch/Views/Settings/SettingsViewModel.swift
+++ b/Hutch/Views/Settings/SettingsViewModel.swift
@@ -198,9 +198,9 @@ final class SettingsViewModel {
do {
let input: [String: any Sendable] = [
"email": email,
- "url": url.isEmpty ? nil as String? as Any : url,
- "location": location.isEmpty ? nil as String? as Any : location,
- "bio": bio.isEmpty ? nil as String? as Any : bio
+ "url": url.isEmpty ? nil as String? as any Sendable : url,
+ "location": location.isEmpty ? nil as String? as any Sendable : location,
+ "bio": bio.isEmpty ? nil as String? as any Sendable : bio
]
let result = try await client.execute(
service: .meta,
@@ -243,7 +243,7 @@ final class SettingsViewModel {
do {
// The input variable has avatar set to null; the actual file
// is sent as a separate multipart part per graphql-multipart-request-spec.
- let input: [String: any Sendable] = ["avatar": nil as String? as Any]
+ let input: [String: any Sendable] = ["avatar": nil as String? as any Sendable]
let result = try await client.executeMultipart(
service: .meta,
query: Self.updateUserMutation,
@@ -286,7 +286,7 @@ final class SettingsViewModel {
error = nil
do {
- let input: [String: any Sendable] = ["avatar": nil as String? as Any]
+ let input: [String: any Sendable] = ["avatar": nil as String? as any Sendable]
let result = try await client.execute(
service: .meta,
query: Self.updateUserMutation,