krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2.6.0: 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
85
86/// A bug tracker from todo.sr.ht.
87struct Tracker: Codable, Sendable, Identifiable, Hashable {
88 let id: Int
89 let created: Date
90 let updated: Date
91 let name: String
92 let description: String?
93 let visibility: Visibility
94 let owner: Entity
95
96 static func == (lhs: Tracker, rhs: Tracker) -> Bool {
97 lhs.id == rhs.id
98 }
99
100 func hash(into hasher: inout Hasher) {
101 hasher.combine(id)
102 }
103}
104
105// MARK: - Tracker Summary (for list view)
106
107/// Lightweight tracker model matching the fields returned by the trackers list query.
108struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
109 let id: Int
110 /// GraphQL resource identifier used by `tracker(id:)` queries.
111 let rid: String
112 let name: String
113 let description: String?
114 let visibility: Visibility
115 let updated: Date
116 let owner: Entity
117}
118
119// MARK: - Ticket Summary (for list view)
120
121/// Lightweight ticket model for the list query.
122struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
123 let id: Int
124 let title: String
125 let status: TicketStatus
126 let resolution: TicketResolution?
127 let created: Date
128 let submitter: Entity
129 let labels: [TicketLabel]
130 let assignees: [Entity]
131
132 static func == (lhs: TicketSummary, rhs: TicketSummary) -> Bool {
133 lhs.id == rhs.id
134 }
135
136 func hash(into hasher: inout Hasher) {
137 hasher.combine(id)
138 }
139}
140
141// MARK: - Ticket Detail
142
143/// Full ticket model for the detail view.
144struct TicketDetail: Codable, Sendable {
145 let id: Int
146 let created: Date
147 let updated: Date
148 let title: String
149 let description: String?
150 let status: TicketStatus
151 let resolution: TicketResolution?
152 let authenticity: Authenticity
153 let submitter: Entity
154 let assignees: [Entity]
155 let labels: [TicketLabel]
156}
157
158// MARK: - Event
159
160/// A timeline event on a ticket. Each event contains one or more changes.
161struct TicketEvent: Codable, Sendable, Identifiable {
162 let id: Int
163 let created: Date
164 var changes: [EventChange]
165}
166
167/// A single change within an event, decoded from the polymorphic EventDetail
168/// interface using inline fragments.
169struct EventChange: Codable, Sendable, Identifiable {
170 let id: UUID
171
172 let eventType: String
173
174 // Comment fields
175 let author: Entity?
176 var text: String?
177 let authenticity: Authenticity?
178
179 // StatusChange fields
180 let oldStatus: TicketStatus?
181 let newStatus: TicketStatus?
182
183 // LabelUpdate fields
184 let labeler: Entity?
185 let label: EventLabel?
186
187 // Assignment fields
188 let assigner: Entity?
189 let assignee: Entity?
190
191 // Mention fields
192 // TicketMention: mentioned is a Ticket with { id }
193 // UserMention: mentioned is an Entity with { canonicalName }
194 let mentioned: MentionTarget?
195
196 // Created fields (author is shared with Comment)
197
198 private enum CodingKeys: String, CodingKey {
199 case eventType
200 case author, text, authenticity
201 case oldStatus, newStatus
202 case labeler, label
203 case assigner, assignee
204 case mentioned
205 }
206
207 init(from decoder: any Decoder) throws {
208 let container = try decoder.container(keyedBy: CodingKeys.self)
209 self.id = UUID()
210 self.eventType = try container.decode(String.self, forKey: .eventType)
211 self.author = try container.decodeIfPresent(Entity.self, forKey: .author)
212 self.text = try container.decodeIfPresent(String.self, forKey: .text)
213 self.authenticity = try container.decodeIfPresent(Authenticity.self, forKey: .authenticity)
214 self.oldStatus = try container.decodeIfPresent(TicketStatus.self, forKey: .oldStatus)
215 self.newStatus = try container.decodeIfPresent(TicketStatus.self, forKey: .newStatus)
216 self.labeler = try container.decodeIfPresent(Entity.self, forKey: .labeler)
217 self.label = try container.decodeIfPresent(EventLabel.self, forKey: .label)
218 self.assigner = try container.decodeIfPresent(Entity.self, forKey: .assigner)
219 self.assignee = try container.decodeIfPresent(Entity.self, forKey: .assignee)
220 self.mentioned = try container.decodeIfPresent(MentionTarget.self, forKey: .mentioned)
221 }
222}
223
224/// Decoded from the `mentioned` field which may be a Ticket (with `id`) or
225/// an Entity (with `canonicalName`) depending on the event type.
226struct MentionTarget: Codable, Sendable {
227 let id: Int?
228 let canonicalName: String?
229}
230
231/// Label info as returned within a LabelUpdate event change.
232struct EventLabel: Codable, Sendable {
233 let name: String
234}