// // Models.swift // AT-IRC // import Foundation /// A message as returned by chat.atirc.post / chat.atirc.list / chat.atirc.subscribe. struct AtircMessage: Identifiable, Codable, Hashable { var seq: Int64? var channel: String? var did: String? var handle: String? var nick: String? var text: String var ts: String? var source: String? var identity: String? var id: String { if let seq { return "seq-\(seq)" } return "\(did ?? "")-\(ts ?? "")-\(text.hashValue)" } var displayNick: String { nick ?? identity ?? handle ?? "unknown" } /// Best-effort parse of the server timestamp. var date: Date? { guard let ts else { return nil } let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] if let date = formatter.date(from: ts) { return date } formatter.formatOptions = [.withInternetDateTime] return formatter.date(from: ts) } var isMine: Bool { source == "xrpc" } } struct AtircListResponse: Codable { var messages: [AtircMessage] var cursor: Int64 } /// A member currently connected to a channel, from `#presence` frames. /// /// Presence identities are `nick@host` strings. The whole string is unique /// (and is the row id); only the nick half gets a nickname color. struct AtircMember: Identifiable, Hashable { let id: String var nick: String /// Secondary identity shown beside the nick: the host half of `nick@host`. var identity: String? /// Split a presence identity, accepting a bare nick defensively. static func from(presenceIdentity raw: String) -> AtircMember? { let value = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !value.isEmpty else { return nil } guard let at = value.firstIndex(of: "@") else { return AtircMember(id: value, nick: value, identity: nil) } let nick = String(value[..