import AVFoundation import Combine import Foundation import GRDB import Photos @MainActor final class ConversationStore: ObservableObject { @Published private(set) var messages: [Message] = [] @Published var replyText = "" private(set) var roster: Roster private let client: Client private let blockSize = Const.messagesPageSize private let messagesMax = Const.messagesMaxSize private var messagesCancellable: AnyCancellable? init(roster: Roster, client: Client) { self.client = client self.roster = roster subscribe() } } extension ConversationStore { func sendMessage(_ message: String) async { // prepare message let message = Message( id: UUID().uuidString, type: .chat, date: Date(), contentType: .text, status: .pending, from: roster.bareJid, to: roster.contactBareJid, body: message, subject: nil, thread: nil, oobUrl: nil ) // store as pending on db, and send do { try await message.save() try await client.sendMessage(message) try await message.setStatus(.sent) } catch { try? await message.setStatus(.error) } } } extension ConversationStore { var attachmentsStore: AttachmentsStore { AttachmentsStore() } func sendMedia(_ items: [GalleryItem]) async { print("media!", items) // guard !ids.isEmpty else { return } // let items = galleryItems.filter { ids.contains($0.id) } // for item in items { // await client.uploadMedia(item.url) // } } func sendCaptured(_ data: Data, _ type: GalleryMediaType) async { // save locally and make message let messageId = UUID().uuidString let localName: String let msgType: AttachmentType do { (localName, msgType) = try await Task { // local name let fileId = UUID().uuidString let localName: String let msgType: AttachmentType switch type { case .photo: localName = "\(messageId)_\(fileId).jpg" msgType = .image case .video: localName = "\(messageId)_\(fileId).mov" msgType = .video } // save let localUrl = Const.fileFolder.appendingPathComponent(localName) try data.write(to: localUrl) return (localName, msgType) }.value } catch { logIt(.error, "Can't save file for uploading: \(error)") return } // save message let message = Message( id: UUID().uuidString, type: .chat, date: Date(), contentType: .attachment( Attachment( type: msgType, localName: localName, thumbnailName: nil, remotePath: nil ) ), status: .pending, from: roster.bareJid, to: roster.contactBareJid, body: nil, subject: nil, thread: nil, oobUrl: nil ) do { try await message.save() } catch { logIt(.error, "Can't save message: \(error)") return } // upload and save await upload(message) } func sendDocuments(_ data: [Data], _ extensions: [String]) async { // do { // let newMessageId = UUID().uuidString // let fileId = UUID().uuidString // let localName = "\(newMessageId)_\(fileId).\(ext)" // try await FileStore.shared.storeForUploading(data, localName) // // } catch { // print("error", error) // } print("documents!", data, extensions) // // // } func sendContact(_ jidStr: String) async { await sendMessage("contact:\(jidStr)") } func sendLocation(_ lat: Double, _ lon: Double) async { await sendMessage("geo:\(lat),\(lon)") } private func upload(_ message: Message) async { do { try await message.setStatus(.pending) var message = message guard case .attachment(let attachment) = message.contentType else { throw ClientStoreError.invalidContentType } guard let localName = attachment.localPath else { throw ClientStoreError.invalidLocalName } let remotePath = try await client.uploadFile(localName) message.contentType = .attachment( Attachment( type: attachment.type, localName: attachment.localName, thumbnailName: nil, remotePath: remotePath ) ) message.body = remotePath message.oobUrl = remotePath try await message.save() try await client.sendMessage(message) try await message.setStatus(.sent) } catch { try? await message.setStatus(.error) } } } extension ConversationStore { var contacts: [Roster] { get async { do { let rosters = try await Database.shared.dbQueue.read { db in try Roster .filter(Column("locallyDeleted") == false) .fetchAll(db) } return rosters } catch { return [] } } } } private extension ConversationStore { func subscribe() { messagesCancellable = ValueObservation.tracking(Message .filter( (Column("to") == roster.bareJid && Column("from") == roster.contactBareJid) || (Column("from") == roster.bareJid && Column("to") == roster.contactBareJid) ) .order(Column("date").desc) .fetchAll ) .publisher(in: Database.shared.dbQueue, scheduling: .immediate) .receive(on: DispatchQueue.main) .sink { _ in } receiveValue: { [weak self] messages in self?.messages = messages } } }