krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.3.1: Hutch/Models/Todo.swift · raw
1import Foundation
2
3// MARK: - Enums
4
5/// Status of a ticket.
6enum TicketStatus: String, Codable, Sendable, CaseIterable {
7 case reported = "REPORTED"
8 case confirmed = "CONFIRMED"
9 case inProgress = "IN_PROGRESS"
10 case pending = "PENDING"
11 case resolved = "RESOLVED"
12
13 /// Whether this status represents an open (unresolved) ticket.
14 var isOpen: Bool {
15 switch self {
16 case .reported, .confirmed, .inProgress, .pending: true
17 case .resolved: false
18 }
19 }
20
21 var displayName: String {
22 switch self {
23 case .reported: "Reported"
24 case .confirmed: "Confirmed"
25 case .inProgress: "In Progress"
26 case .pending: "Pending"
27 case .resolved: "Resolved"
28 }
29 }
30}
31
32/// Resolution of a ticket.
33enum TicketResolution: String, Codable, Sendable {
34 case unresolved = "UNRESOLVED"
35 case fixed = "FIXED"
36 case implemented = "IMPLEMENTED"
37 case wontFix = "WONT_FIX"
38 case byDesign = "BY_DESIGN"
39 case invalid = "INVALID"
40 case duplicate = "DUPLICATE"
41 case notOurBug = "NOT_OUR_BUG"
42 case closed = "CLOSED"
43 case notApplicable = "NOT_APPLICABLE"
44
45 init(from decoder: Decoder) throws {
46 let value = try decoder.singleValueContainer().decode(String.self)
47 self = TicketResolution(rawValue: value) ?? .unresolved
48 }
49
50 var displayName: String {
51 switch self {
52 case .unresolved: "Unresolved"
53 case .fixed: "Fixed"
54 case .implemented: "Implemented"
55 case .wontFix: "Won't Fix"
56 case .byDesign: "By Design"
57 case .invalid: "Invalid"
58 case .duplicate: "Duplicate"
59 case .notOurBug: "Not Our Bug"
60 case .closed: "Closed"
61 case .notApplicable: "Not Applicable"
62 }
63 }
64}
65
66/// Authenticity of a ticket or comment.
67enum Authenticity: String, Codable, Sendable {
68 case authentic = "AUTHENTIC"
69 case tampered = "TAMPERED"
70 case unauthenticated = "UNAUTHENTICATED"
71}
72
73// MARK: - Label
74
75/// A label that can be applied to tickets.
76/// Named `TicketLabel` to avoid collision with `SwiftUI.Label`.
77struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
78 let id: Int
79 let name: String
80 let backgroundColor: String
81 let foregroundColor: String
82}
83
84// MARK: - Tracker ACL
85
86struct TrackerACLPermissions: Codable, Sendable, Hashable {
87 let browse: Bool
88 let submit: Bool
89 let comment: Bool
90 let edit: Bool
91 let triage: Bool
92}
93
94struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
95 let id: Int
96 let created: Date
97 let entity: Entity
98 let browse: Bool
99 let submit: Bool
100 let comment: Bool
101 let edit: Bool
102 let triage: Bool
103
104 var permissions: TrackerACLPermissions {
105 TrackerACLPermissions(
106 browse: browse,
107 submit: submit,
108 comment: comment,
109 edit: edit,
110 triage: triage
111 )
112 }
113}
114
115struct DefaultTrackerACL: Codable, Sendable, Hashable {
116 let browse: Bool
117 let submit: Bool
118 let comment: Bool
119 let edit: Bool
120 let triage: Bool
121
122 var permissions: TrackerACLPermissions {
123 TrackerACLPermissions(
124 browse: browse,
125 submit: submit,
126 comment: comment,
127 edit: edit,
128 triage: triage
129 )
130 }
131}
132
133// MARK: - Tracker
134
135/// A bug tracker from todo.sr.ht.
136struct Tracker: Codable, Sendable, Identifiable, Hashable {
137 let id: Int
138 let created: Date
139 let updated: Date
140 let name: String
141 let description: String?
142 let visibility: Visibility
143 let owner: Entity
144
145 static func == (lhs: Tracker, rhs: Tracker) -> Bool {
146 lhs.id == rhs.id
147 }
148
149 func hash(into hasher: inout Hasher) {
150 hasher.combine(id)
151 }
152}
153
154// MARK: - Tracker Summary (for list view)
155
156/// Lightweight tracker model matching the fields returned by the trackers list query.
157struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
158 let id: Int
159 /// GraphQL resource identifier used by `tracker(id:)` queries.
160 let rid: String
161 let name: String
162 let description: String?
163 let visibility: Visibility
164 let updated: Date
165 let owner: Entity
166}
167
168// MARK: - Ticket Summary (for list view)
169
170/// Lightweight ticket model for the list query.
171struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
172 let id: Int
173 let title: String
174 let status: TicketStatus
175 let resolution: TicketResolution?
176 let created: Date
177 let submitter: Entity
178 let labels: [TicketLabel]
179 let assignees: [Entity]
180
181 static func == (lhs: TicketSummary, rhs: TicketSummary) -> Bool {
182 lhs.id == rhs.id
183 }
184
185 func hash(into hasher: inout Hasher) {
186 hasher.combine(id)
187 }
188}
189
190// MARK: - Ticket Detail
191
192/// Full ticket model for the detail view.
193struct TicketDetail: Codable, Sendable {
194 let id: Int
195 let created: Date
196 let updated: Date
197 let title: String
198 let description: String?
199 let status: TicketStatus
200 let resolution: TicketResolution?
201 let authenticity: Authenticity
202 let submitter: Entity
203 let assignees: [Entity]
204 let labels: [TicketLabel]
205}
206
207// MARK: - Event
208
209/// A timeline event on a ticket. Each event contains one or more changes.
210struct TicketEvent: Codable, Sendable, Identifiable {
211 let id: Int
212 let created: Date
213 var changes: [EventChange]
214}
215
216/// A single change within an event, decoded from the polymorphic EventDetail
217/// interface using inline fragments.
218struct EventChange: Codable, Sendable, Identifiable {
219 let id: UUID
220
221 let eventType: String
222
223 // Comment fields
224 let author: Entity?
225 var text: String?
226 let authenticity: Authenticity?
227
228 // StatusChange fields
229 let oldStatus: TicketStatus?
230 let newStatus: TicketStatus?
231
232 // LabelUpdate fields
233 let labeler: Entity?
234 let label: EventLabel?
235
236 // Assignment fields
237 let assigner: Entity?
238 let assignee: Entity?
239
240 // Mention fields
241 // TicketMention: mentioned is a Ticket with { id }
242 // UserMention: mentioned is an Entity with { canonicalName }
243 let mentioned: MentionTarget?
244
245 // Created fields (author is shared with Comment)
246
247 private enum CodingKeys: String, CodingKey {
248 case eventType
249 case author, text, authenticity
250 case oldStatus, newStatus
251 case labeler, label
252 case assigner, assignee
253 case mentioned
254 }
255
256 init(from decoder: any Decoder) throws {
257 let container = try decoder.container(keyedBy: CodingKeys.self)
258 self.id = UUID()
259 self.eventType = try container.decode(String.self, forKey: .eventType)
260 self.author = try container.decodeIfPresent(Entity.self, forKey: .author)
261 self.text = try container.decodeIfPresent(String.self, forKey: .text)
262 self.authenticity = try container.decodeIfPresent(Authenticity.self, forKey: .authenticity)
263 self.oldStatus = try container.decodeIfPresent(TicketStatus.self, forKey: .oldStatus)
264 self.newStatus = try container.decodeIfPresent(TicketStatus.self, forKey: .newStatus)
265 self.labeler = try container.decodeIfPresent(Entity.self, forKey: .labeler)
266 self.label = try container.decodeIfPresent(EventLabel.self, forKey: .label)
267 self.assigner = try container.decodeIfPresent(Entity.self, forKey: .assigner)
268 self.assignee = try container.decodeIfPresent(Entity.self, forKey: .assignee)
269 self.mentioned = try container.decodeIfPresent(MentionTarget.self, forKey: .mentioned)
270 }
271}
272
273/// Decoded from the `mentioned` field which may be a Ticket (with `id`) or
274/// an Entity (with `canonicalName`) depending on the event type.
275struct MentionTarget: Codable, Sendable {
276 let id: Int?
277 let canonicalName: String?
278}
279
280/// Label info as returned within a LabelUpdate event change.
281struct EventLabel: Codable, Sendable {
282 let name: String
283}