import Combine import Foundation import GRDB import Martin @MainActor final class MessagesStore: ObservableObject { @Published private(set) var messages: [Message] = [] @Published var replyText = "" private(set) var roster: Roster private let client: Client private var messagesCancellable: AnyCancellable? private let archiveMessageFetcher = ArchiveMessageFetcher() init(roster: Roster, client: Client) { self.client = client self.roster = roster subscribe() } } // MARK: - Send message extension MessagesStore { func sendMessage(_ message: String) { Task { var msg = Message.blank msg.from = roster.bareJid msg.to = roster.contactBareJid msg.body = message // store as pending on db, and send do { try await msg.save() try await client.sendMessage(msg) try await msg.setStatus(.sent) } catch { try? await msg.setStatus(.error) } } } func sendContact(_ jidStr: String) { sendMessage("contact:\(jidStr)") } func sendLocation(_ lat: Double, _ lon: Double) { sendMessage("geo:\(lat),\(lon)") } } // MARK: - Subscriptions private extension MessagesStore { 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 if messages.isEmpty { self?.requestLastArchivedMessages() } } } } // MARK: - Archived messages extension MessagesStore { func requestEarliestArchivedMessages() { guard let beforeId = messages.last?.id else { return } Task { await archiveMessageFetcher.fetchBeforeMessages(roster, client, beforeId: beforeId) } } func requestLatestArchivedMessages() { guard let afterId = messages.first?.id else { return } Task { await archiveMessageFetcher.fetchAfterMessages(roster, client, afterId: afterId) } } private func requestLastArchivedMessages() { Task { await archiveMessageFetcher.fetchLastMessages(roster, client) } } } private actor ArchiveMessageFetcher { private var afterAvailable = true private var beforeAvailable = true private var isFetching = false private var fetchingIsPossinle = true func fetchLastMessages(_ roster: Roster, _ client: Client) async { if !fetchingIsPossinle { return } while isFetching { await Task.yield() } isFetching = true let query: RSM.Query = .init(lastItems: Const.mamRequestLimit) do { _ = try await client.fetchArchiveMessages(for: roster, query: query) } catch AppError.featureNotSupported { fetchingIsPossinle = false } catch { logIt(.error, "Error requesting archived messages: \(error)") } isFetching = false } func fetchBeforeMessages(_ roster: Roster, _ client: Client, beforeId: String) async { if !fetchingIsPossinle || !beforeAvailable { return } while isFetching { await Task.yield() } isFetching = true let query: RSM.Query = .init(before: beforeId, max: Const.mamRequestLimit) do { let result = try await client.fetchArchiveMessages(for: roster, query: query) if result.complete { beforeAvailable = false } } catch AppError.featureNotSupported { fetchingIsPossinle = false } catch { logIt(.error, "Error requesting archived messages: \(error)") } isFetching = false } func fetchAfterMessages(_ roster: Roster, _ client: Client, afterId: String) async { if !fetchingIsPossinle || !afterAvailable { return } while isFetching { await Task.yield() } isFetching = true let query: RSM.Query = .init(after: afterId, max: Const.mamRequestLimit) do { let result = try await client.fetchArchiveMessages(for: roster, query: query) if result.complete { afterAvailable = false } } catch AppError.featureNotSupported { fetchingIsPossinle = false } catch { logIt(.error, "Error requesting archived messages: \(error)") } isFetching = false } }