import Foundation enum DeliveryState { case pending, confirmed, failed, none var icon: String { switch self { case .pending: return "circle.dotted" case .confirmed: return "checkmark" case .failed: return "xmark" case .none: return "" } } } struct Message: Identifiable, Hashable, Decodable { let id: String let text: String? let channel: Int? let fromNodeId: String? let fromNodeNum: Int? let toNodeId: String? let timestamp: Date let requestId: Int? let replyId: Int? let deliveryState: String? let ackFailed: Int? let portnum: Int? let emoji: Int? enum CodingKeys: String, CodingKey { case id, text, channel, fromNodeId, fromNodeNum, toNodeId case timestamp, requestId, replyId, deliveryState, ackFailed case portnum, emoji } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) // id may be a string or an integer; coerce to string. if let s = try? c.decode(String.self, forKey: .id) { id = s } else if let n = try? c.decode(Int.self, forKey: .id) { id = String(n) } else { id = UUID().uuidString } text = try c.decodeIfPresent(String.self, forKey: .text) channel = try c.decodeIfPresent(Int.self, forKey: .channel) fromNodeId = try c.decodeIfPresent(String.self, forKey: .fromNodeId) fromNodeNum = try c.decodeIfPresent(Int.self, forKey: .fromNodeNum) toNodeId = try c.decodeIfPresent(String.self, forKey: .toNodeId) requestId = try c.decodeIfPresent(Int.self, forKey: .requestId) replyId = try c.decodeIfPresent(Int.self, forKey: .replyId) deliveryState = try c.decodeIfPresent(String.self, forKey: .deliveryState) ackFailed = try c.decodeIfPresent(Int.self, forKey: .ackFailed) portnum = try c.decodeIfPresent(Int.self, forKey: .portnum) emoji = try c.decodeIfPresent(Int.self, forKey: .emoji) if let n = try? c.decode(Double.self, forKey: .timestamp) { let secs = n > 9_999_999_999 ? n / 1000.0 : n timestamp = Date(timeIntervalSince1970: secs) } else if let s = try? c.decode(String.self, forKey: .timestamp), let parsed = Self.iso.date(from: s) { timestamp = parsed } else { timestamp = Date() } } private static let iso: ISO8601DateFormatter = { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return f }() var isText: Bool { (portnum ?? 1) == 1 } var isReaction: Bool { emoji == 1 } /// Integer request ID, used for replies and reactions. /// Falls back to parsing the trailing number out of ids like "123_456". var requestIdValue: Int? { if let r = requestId { return r } if let last = id.split(separator: "_").last, let n = Int(last) { return n } return nil } var delivery: DeliveryState { if ackFailed == 1 { return .failed } switch deliveryState { case "confirmed", "delivered": return .confirmed case "failed", "error": return .failed case "pending": return .pending default: return .none } } } struct MessageListResponse: Decodable { let data: [Message] } struct SendMessageData: Decodable { let requestId: Int? let messageId: String? let messageCount: Int? } struct SendMessageResponse: Decodable { let data: SendMessageData? }