ios_client
This commit is contained in:
54
ios_client 0.8.5/Kecalek/Models/Conversation.swift
Normal file
54
ios_client 0.8.5/Kecalek/Models/Conversation.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
|
||||
struct Conversation: Identifiable, Equatable, Hashable, Codable {
|
||||
let id: String
|
||||
var name: String?
|
||||
var members: [ConversationMember]
|
||||
var createdBy: String?
|
||||
var avatarFile: String?
|
||||
var unreadCount: Int
|
||||
var isFavorite: Bool
|
||||
var lastMessageTime: Date?
|
||||
|
||||
var isGroup: Bool {
|
||||
name != nil || members.count > 2
|
||||
}
|
||||
|
||||
/// Display name: group name, or DM partner username
|
||||
func displayName(currentUserId: String) -> String {
|
||||
if let name = name, !name.isEmpty {
|
||||
return name
|
||||
}
|
||||
// DM: show the other person's name
|
||||
if let other = members.first(where: { $0.userId != currentUserId }) {
|
||||
return other.username
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
/// DM partner user ID (nil for groups)
|
||||
func dmPartnerId(currentUserId: String) -> String? {
|
||||
guard !isGroup else { return nil }
|
||||
return members.first(where: { $0.userId != currentUserId })?.userId
|
||||
}
|
||||
|
||||
static func == (lhs: Conversation, rhs: Conversation) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
&& lhs.name == rhs.name
|
||||
&& lhs.members == rhs.members
|
||||
&& lhs.avatarFile == rhs.avatarFile
|
||||
&& lhs.unreadCount == rhs.unreadCount
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
struct ConversationMember: Identifiable, Equatable, Codable {
|
||||
let userId: String
|
||||
var username: String
|
||||
var email: String
|
||||
|
||||
var id: String { userId }
|
||||
}
|
||||
69
ios_client 0.8.5/Kecalek/Models/DeviceBundle.swift
Normal file
69
ios_client 0.8.5/Kecalek/Models/DeviceBundle.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
import Foundation
|
||||
|
||||
/// Key bundle for one device, used in X3DH
|
||||
struct DeviceBundle {
|
||||
let deviceId: String
|
||||
let identityKey: Data // Ed25519 public key (32 bytes)
|
||||
let spk: Data // X25519 public key (32 bytes)
|
||||
let spkSignature: Data // Ed25519 signature (64 bytes)
|
||||
let spkId: String
|
||||
let opk: Data? // X25519 public key (32 bytes), optional
|
||||
let opkId: String?
|
||||
|
||||
/// Parse from server response dictionary
|
||||
/// Server uses: signed_prekey, signed_prekey_id, one_time_prekey, one_time_prekey_id (base64)
|
||||
static func fromDict(_ dict: [String: Any], identityKey: Data? = nil) throws -> DeviceBundle {
|
||||
guard let deviceId = dict["device_id"] as? String else {
|
||||
throw ChatError.invalidData("Missing device_id")
|
||||
}
|
||||
|
||||
// Identity key can be passed in (from parent) or in dict
|
||||
let ik: Data
|
||||
if let passedIk = identityKey {
|
||||
ik = passedIk
|
||||
} else if let ikB64 = dict["identity_key"] as? String,
|
||||
let ikData = Data(base64Encoded: ikB64) {
|
||||
ik = ikData
|
||||
} else {
|
||||
throw ChatError.invalidData("Missing identity_key")
|
||||
}
|
||||
|
||||
// SPK - try both naming conventions, base64 encoded
|
||||
let spkB64 = dict["signed_prekey"] as? String ?? dict["spk"] as? String
|
||||
guard let spkB64 = spkB64,
|
||||
let spk = Data(base64Encoded: spkB64) else {
|
||||
throw ChatError.invalidData("Missing signed_prekey")
|
||||
}
|
||||
|
||||
// SPK signature - base64 encoded
|
||||
guard let spkSigB64 = dict["spk_signature"] as? String,
|
||||
let spkSig = Data(base64Encoded: spkSigB64) else {
|
||||
throw ChatError.invalidData("Missing spk_signature")
|
||||
}
|
||||
|
||||
// SPK ID - try both naming conventions
|
||||
let spkId = dict["signed_prekey_id"] as? String ?? dict["spk_id"] as? String
|
||||
guard let spkId = spkId else {
|
||||
throw ChatError.invalidData("Missing signed_prekey_id")
|
||||
}
|
||||
|
||||
// OPK - optional, base64 encoded
|
||||
var opk: Data?
|
||||
var opkId: String?
|
||||
let opkB64 = dict["one_time_prekey"] as? String ?? dict["opk"] as? String
|
||||
if let opkB64 = opkB64, let opkData = Data(base64Encoded: opkB64) {
|
||||
opk = opkData
|
||||
opkId = dict["one_time_prekey_id"] as? String ?? dict["opk_id"] as? String
|
||||
}
|
||||
|
||||
return DeviceBundle(
|
||||
deviceId: deviceId,
|
||||
identityKey: ik,
|
||||
spk: spk,
|
||||
spkSignature: spkSig,
|
||||
spkId: spkId,
|
||||
opk: opk,
|
||||
opkId: opkId
|
||||
)
|
||||
}
|
||||
}
|
||||
9
ios_client 0.8.5/Kecalek/Models/Invitation.swift
Normal file
9
ios_client 0.8.5/Kecalek/Models/Invitation.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
struct Invitation: Identifiable {
|
||||
let id: String // invitation id (from server) or conversationId
|
||||
let conversationId: String
|
||||
let conversationName: String
|
||||
let invitedBy: String
|
||||
let invitedByUsername: String
|
||||
}
|
||||
210
ios_client 0.8.5/Kecalek/Models/Message.swift
Normal file
210
ios_client 0.8.5/Kecalek/Models/Message.swift
Normal file
@@ -0,0 +1,210 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
19
ios_client 0.8.5/Kecalek/Models/User.swift
Normal file
19
ios_client 0.8.5/Kecalek/Models/User.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
|
||||
struct User: Identifiable, Equatable {
|
||||
let id: String
|
||||
var username: String
|
||||
var email: String
|
||||
var identityKey: Data? // Ed25519 public key (32 bytes)
|
||||
}
|
||||
|
||||
struct UserProfile: Equatable {
|
||||
var userId: String
|
||||
var username: String?
|
||||
var email: String?
|
||||
var phone: String?
|
||||
var phoneVisible: Bool
|
||||
var location: String?
|
||||
var locationVisible: Bool
|
||||
var avatarFile: String?
|
||||
}
|
||||
Reference in New Issue
Block a user