// // Models.swift // kordophone2 // // Created by James Magahern on 8/24/25. // import Foundation import XPC enum Display { struct Conversation: Identifiable { let id: String let name: String? let participants: [String] let messagePreview: String var displayName: String { if let name, name.count > 0 { return name } else { return participants.joined(separator: ", ") } } init(from c: Serialized.Conversation) { self.id = c.guid self.name = c.displayName self.participants = c.participants self.messagePreview = c.lastMessagePreview ?? "" } init(id: String = UUID().uuidString, name: String? = nil, participants: [String], messagePreview: String) { self.id = id self.name = name self.participants = participants self.messagePreview = messagePreview } } struct Message: Identifiable, Hashable { let id: String let sender: Sender let text: String let date: Date let attachments: [ImageAttachment] var isFromMe: Bool { sender.isMe } init(from m: Serialized.Message) { self.id = m.guid self.text = m.text self.date = m.date let sender: Sender = if m.sender == "(Me)" { .me } else { .counterpart(m.sender) } self.attachments = m.attachments.map { attachment in ImageAttachment(from: attachment, sender: sender) } self.sender = sender } init(id: String = UUID().uuidString, sender: Sender = .me, date: Date = .now, text: String) { self.id = id self.sender = sender self.text = text self.date = date self.attachments = [] } static func == (lhs: Message, rhs: Message) -> Bool { lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } } struct ImageAttachment: Identifiable { let id: String let sender: Sender let data: Serialized.Attachment var size: CGSize? { if let attr = data.metadata?.attributionInfo, let width = attr.width, let height = attr.height { return CGSize(width: width, height: height) } return nil } var isPreviewDownloaded: Bool { data.isPreviewDownloaded } var previewPath: String { data.previewPath } init(from serialized: Serialized.Attachment, sender: Sender) { self.id = serialized.guid self.sender = sender self.data = serialized } } enum Sender { case me case counterpart(String) var isMe: Bool { if case .me = self { true } else { false } } } } enum Serialized { struct Conversation: Decodable { let guid: String let displayName: String? let participants: [String] let lastMessagePreview: String? let unreadCount: Int let date: Date init?(xpc dict: xpc_object_t) { guard let d = XPCDictionary(dict), let g: String = d["guid"] else { return nil } let dn: String? = d["display_name"] let lmp: String? = d["last_message_preview"] let names: [String] = d["participants"] ?? [] let unread: Int = d["unread_count"] ?? 0 let dt: Date = d["date"] ?? Date(timeIntervalSince1970: 0) self.guid = g self.displayName = dn self.participants = names self.lastMessagePreview = lmp self.unreadCount = unread self.date = dt } } struct Message: Decodable { let guid: String let sender: String let text: String let date: Date let attachments: [Attachment] init?(xpc dict: xpc_object_t) { guard let d = XPCDictionary(dict), let g: String = d["id"] else { return nil } let s: String = d["sender"] ?? "" let t: String = d["text"] ?? "" let dd: Date = d["date"] ?? Date(timeIntervalSince1970: 0) let atts: [Attachment] = d["attachments"] ?? [] self.guid = g self.sender = s self.text = t self.date = dd self.attachments = atts } } struct Attachment: Decodable { let guid: String let path: String let previewPath: String let isDownloaded: Bool let isPreviewDownloaded: Bool let metadata: Metadata? struct Metadata: Decodable { let attributionInfo: AttributionInfo? } struct AttributionInfo: Decodable { let width: Int? let height: Int? } } struct Settings: Decodable { let serverUrl: String let username: String } } extension Serialized.Settings: XPCConvertible { static func fromXPC(_ value: xpc_object_t) -> Serialized.Settings? { guard let d = XPCDictionary(value) else { return nil } let su: String = d["server_url"] ?? "" let un: String = d["username"] ?? "" return Serialized.Settings( serverUrl: su, username: un ) } } extension Serialized.Attachment: XPCConvertible { static func fromXPC(_ value: xpc_object_t) -> Serialized.Attachment? { guard let d = XPCDictionary(value), let guid: String = d["guid"] else { return nil } let path: String = d["path"] ?? "" let previewPath: String = d["preview_path"] ?? "" // Booleans are encoded as strings in XPC let downloadedStr: String = d["downloaded"] ?? "false" let previewDownloadedStr: String = d["preview_downloaded"] ?? "false" let isDownloaded = downloadedStr == "true" let isPreviewDownloaded = previewDownloadedStr == "true" var metadata: Serialized.Attachment.Metadata? = nil if let metadataObj = d.object("metadata"), let md = XPCDictionary(metadataObj) { var attribution: Serialized.Attachment.AttributionInfo? = nil if let attrObj = md.object("attribution_info"), let ad = XPCDictionary(attrObj) { let width: Int? = ad["width"] let height: Int? = ad["height"] attribution = Serialized.Attachment.AttributionInfo(width: width, height: height) } metadata = Serialized.Attachment.Metadata(attributionInfo: attribution) } return Serialized.Attachment( guid: guid, path: path, previewPath: previewPath, isDownloaded: isDownloaded, isPreviewDownloaded: isPreviewDownloaded, metadata: metadata ) } }