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}