211 lines
7.1 KiB
Swift
211 lines
7.1 KiB
Swift
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<String>
|
|
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<String>
|
|
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
|
|
)
|
|
}
|
|
}
|