import Foundation import SwiftUI @Observable final class ChatViewModel { var messages: [Message] = [] var isLoading = false var isSending = false var errorMessage: String? var searchQuery = "" var searchResults: [String] = [] // message IDs matching search var currentSearchIndex = 0 private var notificationTask: Task? func loadMessages(convId: String, chatClient: ChatClient) async { isLoading = true messages = await chatClient.getMessages(convId: convId, limit: 50) isLoading = false // Mark as read let unreadIds = messages.filter { !$0.isMine(currentUserId: await chatClient.userId ?? "") }.map(\.id) if !unreadIds.isEmpty { await chatClient.markRead(convId: convId, messageIds: unreadIds) } } func loadOlderMessages(convId: String, chatClient: ChatClient) async { let older = await chatClient.getMessages(convId: convId, limit: 50, offset: messages.count) messages.insert(contentsOf: older, at: 0) } func sendMessage(convId: String, text: String, members: [ConversationMember], chatClient: ChatClient, replyTo: String? = nil) async { guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } isSending = true errorMessage = nil let (success, msg) = await chatClient.sendMessage( convId: convId, text: text, members: members, replyTo: replyTo ) isSending = false if !success { errorMessage = msg } else { // Reload messages to get the sent message await loadMessages(convId: convId, chatClient: chatClient) } } func deleteMessage(messageId: String, convId: String, chatClient: ChatClient) async { let success = await chatClient.deleteMessage(messageId: messageId, convId: convId) if success { messages.removeAll { $0.id == messageId } } } func search(query: String) { searchQuery = query if query.isEmpty { searchResults = [] currentSearchIndex = 0 return } let lower = query.lowercased() searchResults = messages.filter { $0.text?.lowercased().contains(lower) == true }.map(\.id) currentSearchIndex = searchResults.isEmpty ? 0 : searchResults.count - 1 } func nextSearchResult() { guard !searchResults.isEmpty else { return } currentSearchIndex = (currentSearchIndex + 1) % searchResults.count } func prevSearchResult() { guard !searchResults.isEmpty else { return } currentSearchIndex = (currentSearchIndex - 1 + searchResults.count) % searchResults.count } func startNotificationListener(convId: String, chatClient: ChatClient) { notificationTask?.cancel() notificationTask = Task { for await notification in await chatClient.notifications { await handleNotification(notification, convId: convId, chatClient: chatClient) } } } @MainActor private func handleNotification(_ notification: ChatNotification, convId: String, chatClient: ChatClient) { switch notification { case .newMessage(let data): if data["conversation_id"] as? String == convId { if let msg = Task.detached(priority: .userInitiated, operation: { await chatClient.decryptNotification(data) }) as? Task { Task { if let message = await msg.value { messages.append(message) // Mark as read immediately since we're viewing this conv await chatClient.markRead(convId: convId, messageIds: [message.id]) } } } } case .messageDeleted(let data): if let msgId = data["message_id"] as? String { messages.removeAll { $0.id == msgId } } case .messagesRead(let data): if let readUserId = data["user_id"] as? String, let msgIds = data["message_ids"] as? [String] { for i in messages.indices { if msgIds.contains(messages[i].id) { messages[i].readBy.insert(readUserId) } } } default: break } } func stop() { notificationTask?.cancel() notificationTask = nil } }