import Foundation struct MessageReaction: Equatable { let userId: String let reaction: String let createdAt: Date } struct ForwardedFrom: Equatable { let sender: String let conversationId: String let messageId: String } enum ReactionEmoji { static let allowed = ["thumbsup", "heart", "laugh", "surprised", "sad", "thumbsdown"] static let display: [String: String] = [ "thumbsup": "👍", "heart": "❤️", "laugh": "😂", "surprised": "😮", "sad": "😢", "thumbsdown": "👎", ] } struct Message: Identifiable, Equatable { let id: String let conversationId: String let senderId: String var senderUsername: String let createdAt: Date var text: String? var replyTo: String? var imageFileId: String? var file: FileInfo? var image: ImageInfo? var isDeleted: Bool var readBy: Set var reactions: [MessageReaction] var forwardedFrom: ForwardedFrom? var pinnedAt: Date? var pinnedBy: String? /// Whether this is a self-sent message func isMine(currentUserId: String) -> Bool { senderId == currentUserId } static func == (lhs: Message, rhs: Message) -> Bool { lhs.id == rhs.id } } struct FileInfo: Equatable, Codable { let fileId: String let aesKey: String // base64 let iv: String // base64 let filename: String let size: Int let mimeType: String } struct ImageInfo: Equatable { let fileId: String let aesKey: String // base64 let iv: String // base64 let thumbnail: String? // base64 JPEG thumbnail let filename: String let size: Int } // MARK: - Cache Dictionary Conversion extension Message { /// Convert to dictionary matching server JSON format for MessageCache storage func toCacheDict() -> [String: Any] { var dict: [String: Any] = [ "message_id": id, "conversation_id": conversationId, "sender_id": senderId, "sender_username": senderUsername, "created_at": DateParsing.format(createdAt), "is_deleted": isDeleted, ] if let text = text { dict["text"] = text } if let replyTo = replyTo { dict["reply_to"] = replyTo } if let imageFileId = imageFileId { dict["image_file_id"] = imageFileId } if let file = file { dict["file"] = [ "file_id": file.fileId, "aes_key": file.aesKey, "iv": file.iv, "filename": file.filename, "size": file.size, "mime_type": file.mimeType, ] as [String: Any] } if let image = image { var imgDict: [String: Any] = [ "file_id": image.fileId, "aes_key": image.aesKey, "iv": image.iv, "filename": image.filename, "size": image.size, ] if let thumbnail = image.thumbnail { imgDict["thumbnail"] = thumbnail } dict["image"] = imgDict } if !readBy.isEmpty { dict["read_by"] = Array(readBy) } if !reactions.isEmpty { dict["reactions"] = reactions.map { ["user_id": $0.userId, "reaction": $0.reaction, "created_at": DateParsing.format($0.createdAt)] as [String: Any] } } if let fwd = forwardedFrom { dict["forwarded_from"] = ["sender": fwd.sender, "conversation_id": fwd.conversationId, "message_id": fwd.messageId] as [String: Any] } if let pinnedAt { dict["pinned_at"] = DateParsing.format(pinnedAt) } if let pinnedBy { dict["pinned_by"] = pinnedBy } return dict } /// Create Message from cache dictionary (server JSON format) static func fromCacheDict(_ dict: [String: Any]) -> Message? { guard let id = dict["message_id"] as? String, let conversationId = dict["conversation_id"] as? String, let senderId = dict["sender_id"] as? String, let createdAtStr = dict["created_at"] as? String, let createdAt = DateParsing.parse(createdAtStr) else { return nil } let senderUsername = dict["sender_username"] as? String ?? "" var file: FileInfo? if let fileDict = dict["file"] as? [String: Any], let fileId = fileDict["file_id"] as? String { file = FileInfo( fileId: fileId, aesKey: fileDict["aes_key"] as? String ?? "", iv: fileDict["iv"] as? String ?? "", filename: fileDict["filename"] as? String ?? "", size: fileDict["size"] as? Int ?? 0, mimeType: fileDict["mime_type"] as? String ?? "" ) } var image: ImageInfo? if let imgDict = dict["image"] as? [String: Any], let imgFileId = imgDict["file_id"] as? String { image = ImageInfo( fileId: imgFileId, aesKey: imgDict["aes_key"] as? String ?? "", iv: imgDict["iv"] as? String ?? "", thumbnail: imgDict["thumbnail"] as? String, filename: imgDict["filename"] as? String ?? "image.jpg", size: imgDict["size"] as? Int ?? 0 ) } let readBy: Set if let readByArray = dict["read_by"] as? [String] { readBy = Set(readByArray) } else { readBy = [] } var reactions: [MessageReaction] = [] if let reactionsArr = dict["reactions"] as? [[String: Any]] { reactions = reactionsArr.compactMap { r in guard let userId = r["user_id"] as? String, let reaction = r["reaction"] as? String else { return nil } let createdAt = (r["created_at"] as? String).flatMap { DateParsing.parse($0) } ?? Date() return MessageReaction(userId: userId, reaction: reaction, createdAt: createdAt) } } var forwardedFrom: ForwardedFrom? if let fwd = dict["forwarded_from"] as? [String: Any], let sender = fwd["sender"] as? String { forwardedFrom = ForwardedFrom( sender: sender, conversationId: fwd["conversation_id"] as? String ?? "", messageId: fwd["message_id"] as? String ?? "" ) } let pinnedAt = (dict["pinned_at"] as? String).flatMap { DateParsing.parse($0) } let pinnedBy = dict["pinned_by"] as? String return Message( id: id, conversationId: conversationId, senderId: senderId, senderUsername: senderUsername, createdAt: createdAt, text: dict["text"] as? String, replyTo: dict["reply_to"] as? String, imageFileId: dict["image_file_id"] as? String, file: file, image: image, isDeleted: dict["is_deleted"] as? Bool ?? false, readBy: readBy, reactions: reactions, forwardedFrom: forwardedFrom, pinnedAt: pinnedAt, pinnedBy: pinnedBy ) } }