import Foundation /// One reaction on an issue, a merge request, or one of their comments, /// as `issue show` and `mr show` carry it. nonisolated struct ReactionCount: Decodable, Sendable, Hashable, Identifiable { let reaction: String let count: Int /// The signed-in account gave this one. let me: Bool var id: String { reaction } } /// The reaction vocabulary, named as the server stores it. The order is /// the server's display order, so the picker reads the way the web's does. nonisolated struct Reaction: Sendable, Hashable, Identifiable { let name: String let emoji: String var id: String { name } /// What VoiceOver says; "+1" would be read as "plus one". let spoken: String static let all: [Reaction] = [ .init(name: "+1", emoji: "👍", spoken: "thumbs up"), .init(name: "-1", emoji: "👎", spoken: "thumbs down"), .init(name: "laugh", emoji: "😄", spoken: "laugh"), .init(name: "hooray", emoji: "🎉", spoken: "hooray"), .init(name: "confused", emoji: "😕", spoken: "confused"), .init(name: "heart", emoji: "❤️", spoken: "heart"), .init(name: "rocket", emoji: "🚀", spoken: "rocket"), .init(name: "eyes", emoji: "👀", spoken: "eyes"), ] /// A name a newer server added draws as its name rather than vanishing. static func emoji(for name: String) -> String { all.first { $0.name == name }?.emoji ?? name } static func spoken(for name: String) -> String { all.first { $0.name == name }?.spoken ?? name } /// Picking a reaction the account already gave takes it back, the way /// the web's toggle does. static func remove(_ name: String, given: [ReactionCount]?) -> Bool { given?.contains { $0.reaction == name && $0.me } ?? false } /// `issue react` / `mr react [--comment ] [--remove]`. static func argv(noun: String, repo: String, number: Int64, comment: Int64?, name: String, remove: Bool) -> [String] { var argv = [noun, "react", repo, String(number)] if let comment { argv += ["--comment", String(comment)] } argv.append(name) if remove { argv.append("--remove") } return argv } }