import Foundation /// One `feed` event. `data` varies by kind; unknown kinds and fields /// must render, not sink the feed. nonisolated struct FeedEvent: Decodable, Sendable, Hashable, Identifiable { let id: Int64 let repo: String let actor: String? let kind: String let data: Payload? let createdAt: Date enum CodingKeys: String, CodingKey { case id, repo, actor, kind, data case createdAt = "created_at" } nonisolated struct Payload: Decodable, Sendable, Hashable { let number: Int64? let sha: String? let tag: String? let title: String? let job: String? } /// The same verb and reference phrasing as the web dashboard. var phrase: String { let n = data?.number.map(String.init) ?? "" switch kind { case "mr.created": return "opened merge request !\(n)" case "mr.merged": return "merged !\(n)" case "mr.commented": return "commented on !\(n)" case "mr.closed": return "closed merge request !\(n)" case "issue.created": return "opened issue #\(n)" case "issue.closed": return "closed issue #\(n)" case "issue.reopened": return "reopened issue #\(n)" case "issue.commented": return "commented on #\(n)" case "build.success": return "build success \(data?.job ?? "")" case "build.failure": return "build failure \(data?.job ?? "")" case "release.created": return "released \(data?.tag ?? "")" case "push": return "pushed" case let value where value.hasPrefix("issue."): return "issue \(value.dropFirst("issue.".count)) #\(n)" case let value where value.hasPrefix("mr."): return "merge request \(value.dropFirst("mr.".count)) !\(n)" case let value where value.hasPrefix("build."): return "build \(value.dropFirst("build.".count)) \(data?.job ?? "")" case let value where value.hasPrefix("repo."): return "repository \(value.dropFirst("repo.".count))" default: return kind } } var displayActor: String { guard let actor, !actor.isEmpty else { return "gitbay" } return actor } var icon: (name: String, isPositive: Bool?) { switch kind { case "mr.created": ("arrow.triangle.merge", nil) case "mr.merged": ("arrow.triangle.merge", true) case "mr.closed": ("xmark.circle", false) case "issue.created", "issue.open", "issue.reopened": ("smallcircle.filled.circle", nil) case "issue.closed": ("checkmark.circle", true) case "issue.commented": ("bubble.left", nil) case "build.success": ("checkmark.circle.fill", true) case "build.failure": ("xmark.circle.fill", false) case "release.created": ("shippingbox", nil) case "push": ("arrow.up.circle", nil) default: ("circle", nil) } } /// Where tapping the row goes, when the event names something the app /// has a screen for. var destination: FeedDestination? { if kind == "release.created", let tag = data?.tag { return .release(repo: repo, tag: tag) } guard let number = data?.number else { return .repo(repo) } switch kind.split(separator: ".").first { case "mr": return .mr(repo: repo, number: number) case "issue": return .issue(repo: repo, number: number) case "build": return .build(repo: repo, number: number) default: return .repo(repo) } } } nonisolated enum FeedDestination: Hashable { case repo(String) case mr(repo: String, number: Int64) case issue(repo: String, number: Int64) case build(repo: String, number: Int64) case release(repo: String, tag: String) }