132 lines
4.6 KiB
Swift
132 lines
4.6 KiB
Swift
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<Void, Never>?
|
|
|
|
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<Message?, Never> {
|
|
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
|
|
}
|
|
}
|