import Combine import Foundation import GRDB final class DatabaseMiddleware { static let shared = DatabaseMiddleware() private let database = Database.shared private var cancellables: Set = [] private var conversationCancellables: Set = [] private init() { // Database changes ValueObservation .tracking(Roster.fetchAll) .publisher(in: database._db, scheduling: .immediate) .sink { _ in // Handle completion } receiveValue: { rosters in DispatchQueue.main.async { store.dispatch(.databaseAction(.storedRostersLoaded(rosters: rosters))) } } .store(in: &cancellables) ValueObservation .tracking(Chat.fetchAll) .publisher(in: database._db, scheduling: .immediate) .sink { _ in // Handle completion } receiveValue: { chats in DispatchQueue.main.async { store.dispatch(.databaseAction(.storedChatsLoaded(chats: chats))) } } .store(in: &cancellables) } // swiftlint:disable:next function_body_length func middleware(state _: AppState, action: AppAction) -> AnyPublisher { switch action { // MARK: Accounts case .startAction(.loadStoredAccounts): return Future { promise in Task(priority: .background) { [weak self] in guard let database = self?.database else { promise(.success(.databaseAction(.loadingStoredAccountsFailed))) return } do { try database._db.read { db in let accounts = try Account.fetchAll(db) promise(.success(.databaseAction(.storedAccountsLoaded(accounts: accounts)))) } } catch { promise(.success(.databaseAction(.loadingStoredAccountsFailed))) } } } .eraseToAnyPublisher() case .accountsAction(.makeAccountPermanent(let account)): return Future { promise in Task(priority: .background) { [weak self] in guard let database = self?.database else { promise(.success(.databaseAction(.updateAccountFailed))) return } do { try database._db.write { db in // make permanent and store to database var acc = account acc.isTemp = false try acc.insert(db) // Re-Fetch all accounts let accounts = try Account.fetchAll(db) // Use the accounts promise(.success(.databaseAction(.storedAccountsLoaded(accounts: accounts)))) } } catch { promise(.success(.databaseAction(.updateAccountFailed))) } } } .eraseToAnyPublisher() // MARK: Rosters case .rostersAction(.markRosterAsLocallyDeleted(let ownerJID, let contactJID)): return Future { promise in Task(priority: .background) { [weak self] in guard let database = self?.database else { promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError)))) return } do { _ = try database._db.write { db in try Roster .filter(Column("bareJid") == ownerJID) .filter(Column("contactBareJid") == contactJID) .updateAll(db, Column("locallyDeleted").set(to: true)) } promise(.success(.empty)) } catch { promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError)))) } } } .eraseToAnyPublisher() case .rostersAction(.unmarkRosterAsLocallyDeleted(let ownerJID, let contactJID)): return Future { promise in Task(priority: .background) { [weak self] in guard let database = self?.database else { promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError)))) return } do { _ = try database._db.write { db in try Roster .filter(Column("bareJid") == ownerJID) .filter(Column("contactBareJid") == contactJID) .updateAll(db, Column("locallyDeleted").set(to: false)) } promise(.success(.empty)) } catch { promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError)))) } } } .eraseToAnyPublisher() // MARK: Chats case .chatsAction(.createNewChat(let accountJid, let participantJid)): return Future { promise in Task(priority: .background) { [weak self] in guard let database = self?.database else { promise(.success(.chatsAction(.chatCreationFailed(reason: L10n.Global.Error.genericDbError)))) return } do { try database._db.write { db in let chat = Chat( id: UUID().uuidString, account: accountJid, participant: participantJid, type: .chat ) try chat.insert(db) promise(.success(.chatsAction(.chatCreated(chat: chat)))) } } catch { promise(.success(.chatsAction(.chatCreationFailed(reason: L10n.Global.Error.genericDbError)))) } } } .eraseToAnyPublisher() // MARK: Conversation and messages case .conversationAction(.makeConversationActive(let chat, _)): subscribeToMessages(chat: chat) return Empty().eraseToAnyPublisher() case .xmppAction(.xmppMessageReceived(let message)): return Future { promise in Task(priority: .background) { [weak self] in guard let database = self?.database else { promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError)))) return } guard message.contentType != .typing else { promise(.success(.empty)) return } do { try database._db.write { db in try message.insert(db) } promise(.success(.empty)) } catch { promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription)))) } } } .eraseToAnyPublisher() case .conversationAction(.sendMessage(let from, let to, let body)): return Empty().eraseToAnyPublisher() default: return Empty().eraseToAnyPublisher() } } } private extension DatabaseMiddleware { func subscribeToMessages(chat: Chat) { conversationCancellables = [] ValueObservation .tracking( Message .filter( Column("to") == chat.account || (Column("from") == chat.account && Column("to") == chat.participant) ) .order(Column("date").asc) .fetchAll ) .publisher(in: database._db, scheduling: .immediate) .sink { res in print("!!!---Messages received: \(res)") // Handle completion } receiveValue: { messages in DispatchQueue.main.async { store.dispatch(.conversationAction(.messagesUpdated(messages: messages))) } } .store(in: &conversationCancellables) } }