From 889211683b67312bd3abfff38075c8c765e776fa Mon Sep 17 00:00:00 2001 From: fmodf Date: Wed, 7 Aug 2024 14:49:47 +0200 Subject: [PATCH] fix performance --- ConversationsClassic/AppCore/AppStore.swift | 6 +- .../Middlewares/AccountsMiddleware.swift | 58 +- .../AppCore/Middlewares/ChatsMiddleware.swift | 16 +- .../Middlewares/ConversationMiddleware.swift | 10 +- .../Middlewares/DatabaseMiddleware.swift | 622 +++++++++--------- .../AppCore/Middlewares/FileMiddleware.swift | 150 +++-- .../Middlewares/LoggerMiddleware.swift | 12 + .../Middlewares/MessagesMiddleware.swift | 22 +- .../Middlewares/SharingMiddleware.swift | 92 +-- .../AppCore/Middlewares/XMPPMiddleware.swift | 130 ++-- 10 files changed, 611 insertions(+), 507 deletions(-) diff --git a/ConversationsClassic/AppCore/AppStore.swift b/ConversationsClassic/AppCore/AppStore.swift index adb7451..11ad860 100644 --- a/ConversationsClassic/AppCore/AppStore.swift +++ b/ConversationsClassic/AppCore/AppStore.swift @@ -13,6 +13,7 @@ final class Store: ObservableObject { // Serial queue for performing any actions sequentially private let serialQueue = DispatchQueue(label: "im.narayana.conversations.classic.serial.queue", qos: .userInteractive) + private let middlewareQueue = DispatchQueue(label: "im.narayana.conversations.classic.middleware.queue", qos: .default, attributes: .concurrent) private let reducer: Reducer private let middlewares: [Middleware] @@ -71,8 +72,11 @@ final class Store: ObservableObject { } startTime = CFAbsoluteTimeGetCurrent() middleware + .subscribe(on: middlewareQueue) .receive(on: DispatchQueue.main) - .sink(receiveValue: dispatch) + .sink(receiveValue: { [weak self] action in + self?.dispatch(action) + }) .store(in: &middlewareCancellables) timeElapsed = CFAbsoluteTimeGetCurrent() - startTime if timeElapsed > 0.05 { diff --git a/ConversationsClassic/AppCore/Middlewares/AccountsMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/AccountsMiddleware.swift index 1f0097c..04eb2db 100644 --- a/ConversationsClassic/AppCore/Middlewares/AccountsMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/AccountsMiddleware.swift @@ -22,39 +22,43 @@ final class AccountsMiddleware { .eraseToAnyPublisher() case .xmppAction(.clientConnectionChanged(let jid, let connectionStatus)): - return Future { promise in - guard let account = state.accountsState.accounts.first(where: { $0.bareJid == jid }) else { - promise(.success(.info("AccountsMiddleware: account not found for jid \(jid)"))) - return - } - if account.isTemp { - switch connectionStatus { - case .connected: - promise(.success(.accountsAction(.makeAccountPermanent(account: account)))) - - case .disconnected(let reason): - if reason != "No error!" { - promise(.success(.accountsAction(.addAccountError(jid: jid, reason: reason)))) - } else { - promise(.success(.info("AccountsMiddleware: account \(jid) disconnected with no error"))) - } - - default: - promise(.success(.info("AccountsMiddleware: account \(jid) connection status changed to \(connectionStatus)"))) + return Deferred { + Future { promise in + guard let account = state.accountsState.accounts.first(where: { $0.bareJid == jid }) else { + promise(.success(.info("AccountsMiddleware: account not found for jid \(jid)"))) + return + } + if account.isTemp { + switch connectionStatus { + case .connected: + promise(.success(.accountsAction(.makeAccountPermanent(account: account)))) + + case .disconnected(let reason): + if reason != "No error!" { + promise(.success(.accountsAction(.addAccountError(jid: jid, reason: reason)))) + } else { + promise(.success(.info("AccountsMiddleware: account \(jid) disconnected with no error"))) + } + + default: + promise(.success(.info("AccountsMiddleware: account \(jid) connection status changed to \(connectionStatus)"))) + } + } else { + promise(.success(.info("AccountsMiddleware: account \(jid) is not temporary, ignoring"))) } - } else { - promise(.success(.info("AccountsMiddleware: account \(jid) is not temporary, ignoring"))) } } .eraseToAnyPublisher() case .xmppAction(.serverFeaturesLoaded(let jid, let features)): - return Future { [weak self] promise in - let serverFeatures = features - .compactMap { featureId in - self?.allFeatures.first(where: { $0.xmppId == featureId }) - } - promise(.success(.accountsAction(.clientServerFeaturesUpdated(jid: jid, features: serverFeatures)))) + return Deferred { + Future { [weak self] promise in + let serverFeatures = features + .compactMap { featureId in + self?.allFeatures.first(where: { $0.xmppId == featureId }) + } + promise(.success(.accountsAction(.clientServerFeaturesUpdated(jid: jid, features: serverFeatures)))) + } } .eraseToAnyPublisher() diff --git a/ConversationsClassic/AppCore/Middlewares/ChatsMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/ChatsMiddleware.swift index 5eea0fd..797f93d 100644 --- a/ConversationsClassic/AppCore/Middlewares/ChatsMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/ChatsMiddleware.swift @@ -10,13 +10,15 @@ final class ChatsMiddleware { .eraseToAnyPublisher() case .chatsAction(.startChat(accountJid: let accountJid, participantJid: let participantJid)): - return Future { promise in - if let exist = state.chatsState.chats.first(where: { $0.account == accountJid && $0.participant == participantJid }) { - // open existing chat - promise(.success(.chatsAction(.chatStarted(chat: exist)))) - } else { - // create new chat - promise(.success(.chatsAction(.createNewChat(accountJid: accountJid, participantJid: participantJid)))) + return Deferred { + Future { promise in + if let exist = state.chatsState.chats.first(where: { $0.account == accountJid && $0.participant == participantJid }) { + // open existing chat + promise(.success(.chatsAction(.chatStarted(chat: exist)))) + } else { + // create new chat + promise(.success(.chatsAction(.createNewChat(accountJid: accountJid, participantJid: participantJid)))) + } } } .eraseToAnyPublisher() diff --git a/ConversationsClassic/AppCore/Middlewares/ConversationMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/ConversationMiddleware.swift index cd08b92..e03839d 100644 --- a/ConversationsClassic/AppCore/Middlewares/ConversationMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/ConversationMiddleware.swift @@ -6,10 +6,12 @@ final class ConversationMiddleware { func middleware(state: AppState, action: AppAction) -> AnyPublisher { switch action { case .chatsAction(.chatStarted(let chat)): - return Future { promise in - let roster = state.rostersState.rosters - .first { $0.bareJid == chat.account && $0.contactBareJid == chat.participant } - promise(.success(.conversationAction(.makeConversationActive(chat: chat, roster: roster)))) + return Deferred { + Future { promise in + let roster = state.rostersState.rosters + .first { $0.bareJid == chat.account && $0.contactBareJid == chat.participant } + promise(.success(.conversationAction(.makeConversationActive(chat: chat, roster: roster)))) + } } .eraseToAnyPublisher() diff --git a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift index 3aa6353..c14dce8 100644 --- a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift @@ -40,46 +40,50 @@ final class DatabaseMiddleware { 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)))) + return Deferred { + 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))) } - } 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)))) + return Deferred { + 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))) } - } catch { - promise(.success(.databaseAction(.updateAccountFailed))) } } } @@ -87,44 +91,48 @@ final class DatabaseMiddleware { // 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)) + return Deferred { + 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(.info("DatabaseMiddleware: roster \(contactJID) for account \(ownerJID) marked as locally deleted"))) + } catch { + promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError)))) } - promise(.success(.info("DatabaseMiddleware: roster \(contactJID) for account \(ownerJID) marked as locally deleted"))) - } 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)) + return Deferred { + 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(.info("DatabaseMiddleware: roster \(contactJID) for account \(ownerJID) unmarked as locally deleted"))) + } catch { + promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError)))) } - promise(.success(.info("DatabaseMiddleware: roster \(contactJID) for account \(ownerJID) unmarked as locally deleted"))) - } catch { - promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError)))) } } } @@ -132,25 +140,27 @@ final class DatabaseMiddleware { // 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)))) + return Deferred { + 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)))) } - } catch { - promise(.success(.chatsAction(.chatCreationFailed(reason: L10n.Global.Error.genericDbError)))) } } } @@ -162,56 +172,60 @@ final class DatabaseMiddleware { 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, message.body != nil else { - promise(.success(.info("DatabaseMiddleware: message \(message.id) received as 'typing...' or message body is nil"))) - return - } - do { - try database._db.write { db in - try message.insert(db) + return Deferred { + 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, message.body != nil else { + promise(.success(.info("DatabaseMiddleware: message \(message.id) received as 'typing...' or message body is nil"))) + return + } + do { + try database._db.write { db in + try message.insert(db) + } + promise(.success(.info("DatabaseMiddleware: message \(message.id) stored in db"))) + } catch { + promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription)))) } - promise(.success(.info("DatabaseMiddleware: message \(message.id) stored in db"))) - } catch { - promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription)))) } } } .eraseToAnyPublisher() case .conversationAction(.sendMessage(let from, let to, let body)): - 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 - } - do { - let message = Message( - id: UUID().uuidString, - type: .chat, - contentType: .text, - from: from, - to: to, - body: body, - subject: nil, - thread: nil, - oobUrl: nil, - date: Date(), - pending: true, - sentError: false - ) - try database._db.write { db in - try message.insert(db) + return Deferred { + 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 + } + do { + let message = Message( + id: UUID().uuidString, + type: .chat, + contentType: .text, + from: from, + to: to, + body: body, + subject: nil, + thread: nil, + oobUrl: nil, + date: Date(), + pending: true, + sentError: false + ) + try database._db.write { db in + try message.insert(db) + } + promise(.success(.xmppAction(.xmppMessageSent(message)))) + } catch { + promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription)))) } - promise(.success(.xmppAction(.xmppMessageSent(message)))) - } catch { - promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription)))) } } } @@ -219,22 +233,24 @@ final class DatabaseMiddleware { case .xmppAction(.xmppMessageSendSuccess(let msgId)): // mark message as pending false and sentError false - 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 - } - do { - _ = try database._db.write { db in - try Message - .filter(Column("id") == msgId) - .updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: false)) + return Deferred { + 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 + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == msgId) + .updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: false)) + } + promise(.success(.info("DatabaseMiddleware: message \(msgId) marked in db as sent"))) + } catch { + promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))) + ) } - promise(.success(.info("DatabaseMiddleware: message \(msgId) marked in db as sent"))) - } catch { - promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))) - ) } } } @@ -242,21 +258,23 @@ final class DatabaseMiddleware { case .xmppAction(.xmppMessageSendFailed(let msgId)): // mark message as pending false and sentError true - 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 - } - do { - _ = try database._db.write { db in - try Message - .filter(Column("id") == msgId) - .updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: true)) + return Deferred { + 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 + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == msgId) + .updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: true)) + } + promise(.success(.info("DatabaseMiddleware: message \(msgId) marked in db as failed to send"))) + } catch { + promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription)))) } - promise(.success(.info("DatabaseMiddleware: message \(msgId) marked in db as failed to send"))) - } catch { - promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription)))) } } } @@ -264,92 +282,100 @@ final class DatabaseMiddleware { // MARK: Attachments case .fileAction(.downloadAttachmentFile(let id, _)): - return Future { promise in - Task(priority: .background) { [weak self] in - guard let database = self?.database else { - promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) - ) - return - } - do { - _ = try database._db.write { db in - try Message - .filter(Column("id") == id) - .updateAll(db, Column("attachmentDownloadFailed").set(to: false)) + return Deferred { + Future { promise in + Task(priority: .background) { [weak self] in + guard let database = self?.database else { + promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) + ) + return + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == id) + .updateAll(db, Column("attachmentDownloadFailed").set(to: false)) + } + promise(.success(.info("DatabaseMiddleware: message \(id) marked in db as starting downloading attachment"))) + } catch { + promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription))) + ) } - promise(.success(.info("DatabaseMiddleware: message \(id) marked in db as starting downloading attachment"))) - } catch { - promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription))) - ) } } } .eraseToAnyPublisher() case .fileAction(.downloadingAttachmentFileFailed(let id, _)): - return Future { promise in - Task(priority: .background) { [weak self] in - guard let database = self?.database else { - promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) - ) - return - } - do { - _ = try database._db.write { db in - try Message - .filter(Column("id") == id) - .updateAll(db, Column("attachmentDownloadFailed").set(to: true)) + return Deferred { + Future { promise in + Task(priority: .background) { [weak self] in + guard let database = self?.database else { + promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) + ) + return + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == id) + .updateAll(db, Column("attachmentDownloadFailed").set(to: true)) + } + promise(.success(.info("DatabaseMiddleware: message \(id) marked in db as failed to download attachment"))) + } catch { + promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription))) + ) } - promise(.success(.info("DatabaseMiddleware: message \(id) marked in db as failed to download attachment"))) - } catch { - promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription))) - ) } } } .eraseToAnyPublisher() case .fileAction(.attachmentFileDownloaded(let id, let localName)): - return Future { promise in - Task(priority: .background) { [weak self] in - guard let database = self?.database else { - promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) - ) - return - } - do { - _ = try database._db.write { db in - try Message - .filter(Column("id") == id) - .updateAll(db, Column("attachmentLocalName").set(to: localName), Column("attachmentDownloadFailed").set(to: false)) + return Deferred { + Future { promise in + Task(priority: .background) { [weak self] in + guard let database = self?.database else { + promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) + ) + return + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == id) + .updateAll(db, Column("attachmentLocalName").set(to: localName), Column("attachmentDownloadFailed").set(to: false)) + } + promise(.success(.info("DatabaseMiddleware: message \(id) marked in db as downloaded attachment"))) + } catch { + promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription))) + ) } - promise(.success(.info("DatabaseMiddleware: message \(id) marked in db as downloaded attachment"))) - } catch { - promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription))) - ) } } } .eraseToAnyPublisher() case .fileAction(.attachmentThumbnailCreated(let id, let thumbnailName)): - return Future { promise in - Task(priority: .background) { [weak self] in - guard let database = self?.database else { - promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) - ) - return - } - do { - _ = try database._db.write { db in - try Message - .filter(Column("id") == id) - .updateAll(db, Column("attachmentThumbnailName").set(to: thumbnailName)) + return Deferred { + Future { promise in + Task(priority: .background) { [weak self] in + guard let database = self?.database else { + promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) + ) + return + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == id) + .updateAll(db, Column("attachmentThumbnailName").set(to: thumbnailName)) + } + promise(.success(.info("DatabaseMiddleware: message \(id) marked in db as thumbnail created"))) + } catch { + promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription))) + ) } - promise(.success(.info("DatabaseMiddleware: message \(id) marked in db as thumbnail created"))) - } catch { - promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription))) - ) } } } @@ -357,108 +383,116 @@ final class DatabaseMiddleware { // MARK: Sharing case .conversationAction(.sendMediaMessages(let from, let to, let messageIds, let localFilesNames)): - 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 - } - do { - for (index, id) in messageIds.enumerated() { - let message = Message( - id: id, - type: .chat, - contentType: .attachment, - from: from, - to: to, - body: nil, - subject: nil, - thread: nil, - oobUrl: nil, - date: Date(), - pending: true, - sentError: false, - attachmentType: localFilesNames[index].attachmentType, - attachmentLocalName: localFilesNames[index] + return Deferred { + 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 + } + do { + for (index, id) in messageIds.enumerated() { + let message = Message( + id: id, + type: .chat, + contentType: .attachment, + from: from, + to: to, + body: nil, + subject: nil, + thread: nil, + oobUrl: nil, + date: Date(), + pending: true, + sentError: false, + attachmentType: localFilesNames[index].attachmentType, + attachmentLocalName: localFilesNames[index] + ) + try database._db.write { db in + try message.insert(db) + } + } + promise(.success(.info("DatabaseMiddleware: messages with sharings stored in db"))) + } catch { + promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))) ) - try database._db.write { db in - try message.insert(db) - } } - promise(.success(.info("DatabaseMiddleware: messages with sharings stored in db"))) - } catch { - promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))) - ) } } } .eraseToAnyPublisher() case .sharingAction(.retrySharing(let id)): - 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 - } - do { - _ = try database._db.write { db in - try Message - .filter(Column("id") == id) - .updateAll(db, Column("pending").set(to: true), Column("sentError").set(to: false)) + return Deferred { + 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 + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == id) + .updateAll(db, Column("pending").set(to: true), Column("sentError").set(to: false)) + } + promise(.success(.info("DatabaseMiddleware: message \(id) with shares marked in db as pending to send"))) + } catch { + promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))) + ) } - promise(.success(.info("DatabaseMiddleware: message \(id) with shares marked in db as pending to send"))) - } catch { - promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))) - ) } } } .eraseToAnyPublisher() case .xmppAction(.xmppSharingUploadSuccess(let messageId, let remotePath)): - return Future { promise in - Task(priority: .background) { [weak self] in - guard let database = self?.database else { - promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: L10n.Global.Error.genericDbError))) - ) - return - } - do { - _ = try database._db.write { db in - try Message - .filter(Column("id") == messageId) - .updateAll(db, Column("attachmentRemotePath").set(to: remotePath), Column("pending").set(to: false), Column("sentError").set(to: false)) + return Deferred { + Future { promise in + Task(priority: .background) { [weak self] in + guard let database = self?.database else { + promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: L10n.Global.Error.genericDbError))) + ) + return + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == messageId) + .updateAll(db, Column("attachmentRemotePath").set(to: remotePath), Column("pending").set(to: false), Column("sentError").set(to: false)) + } + promise(.success(.info("DatabaseMiddleware: shared file uploaded and message \(messageId) marked in db as sent"))) + } catch { + promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: error.localizedDescription))) + ) } - promise(.success(.info("DatabaseMiddleware: shared file uploaded and message \(messageId) marked in db as sent"))) - } catch { - promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: error.localizedDescription))) - ) } } } .eraseToAnyPublisher() case .xmppAction(.xmppSharingUploadFailed(let messageId, _)): - return Future { promise in - Task(priority: .background) { [weak self] in - guard let database = self?.database else { - promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: L10n.Global.Error.genericDbError))) - ) - return - } - do { - _ = try database._db.write { db in - try Message - .filter(Column("id") == messageId) - .updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: true)) + return Deferred { + Future { promise in + Task(priority: .background) { [weak self] in + guard let database = self?.database else { + promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: L10n.Global.Error.genericDbError))) + ) + return + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == messageId) + .updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: true)) + } + promise(.success(.info("DatabaseMiddleware: shared file upload failed and message \(messageId) marked in db as failed to send"))) + } catch { + promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: error.localizedDescription))) + ) } - promise(.success(.info("DatabaseMiddleware: shared file upload failed and message \(messageId) marked in db as failed to send"))) - } catch { - promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: error.localizedDescription))) - ) } } } diff --git a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift index 9b25e36..8237ac2 100644 --- a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift @@ -10,108 +10,126 @@ final class FileMiddleware { switch action { // MARK: - For incomig attachments case .conversationAction(.messagesUpdated(let messages)): - return Future { [weak self] promise in - guard let wSelf = self else { - promise(.success(.info("FileMiddleware: on checking attachments/shares messages, middleware self is nil"))) - return - } - - // for incoming messages with attachments - for message in messages where message.attachmentRemotePath != nil && message.attachmentLocalPath == nil { - if wSelf.downloadingMessageIDs.contains(message.id) { - continue + return Deferred { + Future { [weak self] promise in + guard let wSelf = self else { + promise(.success(.info("FileMiddleware: on checking attachments/shares messages, middleware self is nil"))) + return } - wSelf.downloadingMessageIDs.insert(message.id) - DispatchQueue.main.async { - // swiftlint:disable:next force_unwrapping - store.dispatch(.fileAction(.downloadAttachmentFile(messageId: message.id, attachmentRemotePath: message.attachmentRemotePath!))) - } - } - // for outgoing messages with shared attachments - for message in messages where message.attachmentLocalPath != nil && message.attachmentRemotePath == nil && message.pending { - DispatchQueue.main.async { - store.dispatch(.xmppAction(.xmppSharingTryUpload(message))) - } - } - - // for outgoing messages with shared attachments which are already uploaded - // but have no thumbnail (only for images) - for message in messages where !message.pending && !message.sentError && message.attachmentType == .image { - if message.attachmentLocalName != nil && message.attachmentRemotePath != nil && message.attachmentThumbnailName == nil { + // for incoming messages with attachments + for message in messages where message.attachmentRemotePath != nil && message.attachmentLocalPath == nil { + if wSelf.downloadingMessageIDs.contains(message.id) { + continue + } + wSelf.downloadingMessageIDs.insert(message.id) DispatchQueue.main.async { // swiftlint:disable:next force_unwrapping - store.dispatch(.fileAction(.createAttachmentThumbnail(messageId: message.id, localName: message.attachmentLocalName!))) + store.dispatch(.fileAction(.downloadAttachmentFile(messageId: message.id, attachmentRemotePath: message.attachmentRemotePath!))) } } - } - promise(.success(.info("FileMiddleware: attachments/shares messages processed"))) - }.eraseToAnyPublisher() + // for outgoing messages with shared attachments + for message in messages where message.attachmentLocalPath != nil && message.attachmentRemotePath == nil && message.pending { + DispatchQueue.main.async { + store.dispatch(.xmppAction(.xmppSharingTryUpload(message))) + } + } + + // for outgoing messages with shared attachments which are already uploaded + // but have no thumbnail (only for images) + for message in messages where !message.pending && !message.sentError && message.attachmentType == .image { + if message.attachmentLocalName != nil && message.attachmentRemotePath != nil && message.attachmentThumbnailName == nil { + DispatchQueue.main.async { + // swiftlint:disable:next force_unwrapping + store.dispatch(.fileAction(.createAttachmentThumbnail(messageId: message.id, localName: message.attachmentLocalName!))) + } + } + } + + promise(.success(.info("FileMiddleware: attachments/shares messages processed"))) + } + } + .eraseToAnyPublisher() case .fileAction(.downloadAttachmentFile(let id, let attachmentRemotePath)): - return Future { promise in - let localName = "\(id)_\(UUID().uuidString)\(attachmentRemotePath.lastPathComponent)" - let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName) - DownloadManager.shared.enqueueDownload(from: attachmentRemotePath, to: localUrl) { error in - DispatchQueue.main.async { - if let error { - store.dispatch(.fileAction(.downloadingAttachmentFileFailed(messageId: id, reason: error.localizedDescription))) - } else { - store.dispatch(.fileAction(.attachmentFileDownloaded(messageId: id, localName: localName))) + return Deferred { + Future { promise in + let localName = "\(id)_\(UUID().uuidString)\(attachmentRemotePath.lastPathComponent)" + let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName) + DownloadManager.shared.enqueueDownload(from: attachmentRemotePath, to: localUrl) { error in + DispatchQueue.main.async { + if let error { + store.dispatch(.fileAction(.downloadingAttachmentFileFailed(messageId: id, reason: error.localizedDescription))) + } else { + store.dispatch(.fileAction(.attachmentFileDownloaded(messageId: id, localName: localName))) + } } } + promise(.success(.info("FileMiddleware: started downloading attachment for message \(id)"))) } - promise(.success(.info("FileMiddleware: started downloading attachment for message \(id)"))) - }.eraseToAnyPublisher() + } + .eraseToAnyPublisher() case .fileAction(.attachmentFileDownloaded(let id, let localName)): - return Future { [weak self] promise in - self?.downloadingMessageIDs.remove(id) - promise(.success(.fileAction(.createAttachmentThumbnail(messageId: id, localName: localName)))) + return Deferred { + Future { [weak self] promise in + self?.downloadingMessageIDs.remove(id) + promise(.success(.fileAction(.createAttachmentThumbnail(messageId: id, localName: localName)))) + } } .eraseToAnyPublisher() case .fileAction(.createAttachmentThumbnail(let id, let localName)): - return Future { [weak self] promise in - if let thumbnailName = FileProcessing.shared.createThumbnail(localName: localName) { - self?.downloadingMessageIDs.remove(id) - promise(.success(.fileAction(.attachmentThumbnailCreated(messageId: id, thumbnailName: thumbnailName)))) - } else { - self?.downloadingMessageIDs.remove(id) - promise(.success(.info("FileMiddleware: failed to create thumbnail from \(localName) for message \(id)"))) + return Deferred { + Future { [weak self] promise in + if let thumbnailName = FileProcessing.shared.createThumbnail(localName: localName) { + self?.downloadingMessageIDs.remove(id) + promise(.success(.fileAction(.attachmentThumbnailCreated(messageId: id, thumbnailName: thumbnailName)))) + } else { + self?.downloadingMessageIDs.remove(id) + promise(.success(.info("FileMiddleware: failed to create thumbnail from \(localName) for message \(id)"))) + } } } .eraseToAnyPublisher() // MARK: - For outgoing sharing case .fileAction(.fetchItemsFromGallery): - return Future { promise in - let items = FileProcessing.shared.fetchGallery() - promise(.success(.fileAction(.itemsFromGalleryFetched(items: items)))) + return Deferred { + Future { promise in + let items = FileProcessing.shared.fetchGallery() + promise(.success(.fileAction(.itemsFromGalleryFetched(items: items)))) + } } .eraseToAnyPublisher() case .fileAction(.itemsFromGalleryFetched(let items)): - return Future { promise in - let newItems = FileProcessing.shared.fillGalleryItemsThumbnails(items: items) - promise(.success(.sharingAction(.galleryItemsUpdated(items: newItems)))) + return Deferred { + Future { promise in + let newItems = FileProcessing.shared.fillGalleryItemsThumbnails(items: items) + promise(.success(.sharingAction(.galleryItemsUpdated(items: newItems)))) + } } .eraseToAnyPublisher() case .fileAction(.copyGalleryItemsForUploading(let items)): - return Future { promise in - let ids = FileProcessing.shared.copyGalleryItemsForUploading(items: items) - promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: ids.map { $0.0 }, localNames: ids.map { $0.1 })))) + return Deferred { + Future { promise in + let ids = FileProcessing.shared.copyGalleryItemsForUploading(items: items) + promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: ids.map { $0.0 }, localNames: ids.map { $0.1 })))) + } } .eraseToAnyPublisher() case .fileAction(.copyCameraCapturedForUploading(let media, let type)): - return Future { promise in - if let (id, localName) = FileProcessing.shared.copyCameraCapturedForUploading(media: media, type: type) { - promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: [id], localNames: [localName])))) - } else { - promise(.success(.info("FileMiddleware: failed to copy camera captured media for uploading"))) + return Deferred { + Future { promise in + if let (id, localName) = FileProcessing.shared.copyCameraCapturedForUploading(media: media, type: type) { + promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: [id], localNames: [localName])))) + } else { + promise(.success(.info("FileMiddleware: failed to copy camera captured media for uploading"))) + } } } .eraseToAnyPublisher() diff --git a/ConversationsClassic/AppCore/Middlewares/LoggerMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/LoggerMiddleware.swift index 811151b..f9fec20 100644 --- a/ConversationsClassic/AppCore/Middlewares/LoggerMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/LoggerMiddleware.swift @@ -55,3 +55,15 @@ private var dateFormatter: DateFormatter { formatter.dateFormat = "MM-dd HH:mm:ss.SSS" return formatter } + +// For thread debugging +func ptInfo(_ message: String) { + #if DEBUG + let timeStr = dateFormatter.string(from: Date()) + let str = "\(timeStr) \(message) -> \(Thread.current), \(String(validatingUTF8: __dispatch_queue_get_label(nil)) ?? "no queue label")" + print(str) + if isConsoleLoggingEnabled { + NSLog(str) + } + #endif +} diff --git a/ConversationsClassic/AppCore/Middlewares/MessagesMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/MessagesMiddleware.swift index f842413..12aa1b8 100644 --- a/ConversationsClassic/AppCore/Middlewares/MessagesMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/MessagesMiddleware.swift @@ -7,18 +7,20 @@ final class MessagesMiddleware { func middleware(state: AppState, action: AppAction) -> AnyPublisher { switch action { case .conversationAction(.makeConversationActive(let chat, let roster)): - return Future { promise in - if let currentClient = state.accountsState.accounts.first(where: { $0.bareJid == chat.account }) { - let features = state.accountsState.discoFeatures[currentClient.bareJid] ?? [] - if features.map({ $0.xep }).contains("XEP-0313") { - let oldestMessageDate = state.conversationsState.currentMessages.first?.date ?? Date() - let archivesRequestDate = Calendar.current.date(byAdding: .day, value: -Const.mamRequestLength, to: oldestMessageDate) ?? Date() - promise(.success(.xmppAction(.xmppLoadArchivedMessages(jid: currentClient.bareJid, to: roster?.bareJid, fromDate: archivesRequestDate)))) + return Deferred { + Future { promise in + if let currentClient = state.accountsState.accounts.first(where: { $0.bareJid == chat.account }) { + let features = state.accountsState.discoFeatures[currentClient.bareJid] ?? [] + if features.map({ $0.xep }).contains("XEP-0313") { + let oldestMessageDate = state.conversationsState.currentMessages.first?.date ?? Date() + let archivesRequestDate = Calendar.current.date(byAdding: .day, value: -Const.mamRequestLength, to: oldestMessageDate) ?? Date() + promise(.success(.xmppAction(.xmppLoadArchivedMessages(jid: currentClient.bareJid, to: roster?.bareJid, fromDate: archivesRequestDate)))) + } else { + promise(.success(.info("MessageMiddleware: XEP-0313 not supported for client \(currentClient.bareJid)"))) + } } else { - promise(.success(.info("MessageMiddleware: XEP-0313 not supported for client \(currentClient.bareJid)"))) + promise(.success(.info("MessageMiddleware: No client found for account \(chat.account), probably some error here"))) } - } else { - promise(.success(.info("MessageMiddleware: No client found for account \(chat.account), probably some error here"))) } } .eraseToAnyPublisher() diff --git a/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift index 73ad01c..7d6e788 100644 --- a/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift @@ -11,43 +11,47 @@ final class SharingMiddleware { switch action { // MARK: - Camera and Gallery Access case .sharingAction(.checkCameraAccess): - return Future { promise in - let status = AVCaptureDevice.authorizationStatus(for: .video) - switch status { - case .authorized: - promise(.success(.sharingAction(.setCameraAccess(true)))) + return Deferred { + Future { promise in + let status = AVCaptureDevice.authorizationStatus(for: .video) + switch status { + case .authorized: + promise(.success(.sharingAction(.setCameraAccess(true)))) - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video) { granted in - promise(.success(.sharingAction(.setCameraAccess(granted)))) + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { granted in + promise(.success(.sharingAction(.setCameraAccess(granted)))) + } + + case .denied, .restricted: + promise(.success(.sharingAction(.setCameraAccess(false)))) + + @unknown default: + promise(.success(.sharingAction(.setCameraAccess(false)))) } - - case .denied, .restricted: - promise(.success(.sharingAction(.setCameraAccess(false)))) - - @unknown default: - promise(.success(.sharingAction(.setCameraAccess(false)))) } } .eraseToAnyPublisher() case .sharingAction(.checkGalleryAccess): - return Future { promise in - let status = PHPhotoLibrary.authorizationStatus() - switch status { - case .authorized, .limited: - promise(.success(.sharingAction(.setGalleryAccess(true)))) + return Deferred { + Future { promise in + let status = PHPhotoLibrary.authorizationStatus() + switch status { + case .authorized, .limited: + promise(.success(.sharingAction(.setGalleryAccess(true)))) - case .notDetermined: - PHPhotoLibrary.requestAuthorization { status in - promise(.success(.sharingAction(.setGalleryAccess(status == .authorized)))) + case .notDetermined: + PHPhotoLibrary.requestAuthorization { status in + promise(.success(.sharingAction(.setGalleryAccess(status == .authorized)))) + } + + case .denied, .restricted: + promise(.success(.sharingAction(.setGalleryAccess(false)))) + + @unknown default: + promise(.success(.sharingAction(.setGalleryAccess(false)))) } - - case .denied, .restricted: - promise(.success(.sharingAction(.setGalleryAccess(false)))) - - @unknown default: - promise(.success(.sharingAction(.setGalleryAccess(false)))) } } .eraseToAnyPublisher() @@ -58,9 +62,11 @@ final class SharingMiddleware { // MARK: - Sharing case .sharingAction(.shareMedia(let ids)): - return Future { promise in - let items = state.sharingState.galleryItems.filter { ids.contains($0.id) } - promise(.success(.fileAction(.copyGalleryItemsForUploading(items: items)))) + return Deferred { + Future { promise in + let items = state.sharingState.galleryItems.filter { ids.contains($0.id) } + promise(.success(.fileAction(.copyGalleryItemsForUploading(items: items)))) + } } .eraseToAnyPublisher() @@ -78,12 +84,14 @@ final class SharingMiddleware { } case .sharingAction(.cameraCaptured(let media, let type)): - return Future { promise in - if let (id, localName) = FileProcessing.shared.copyCameraCapturedForUploading(media: media, type: type) { - promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: [id], localNames: [localName]))) - ) - } else { - promise(.success(.info("SharingMiddleware: camera's captured file didn't copied"))) + return Deferred { + Future { promise in + if let (id, localName) = FileProcessing.shared.copyCameraCapturedForUploading(media: media, type: type) { + promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: [id], localNames: [localName]))) + ) + } else { + promise(.success(.info("SharingMiddleware: camera's captured file didn't copied"))) + } } } .eraseToAnyPublisher() @@ -98,10 +106,12 @@ final class SharingMiddleware { } case .sharingAction(.shareDocuments(let data, let extensions)): - return Future { promise in - let ids = FileProcessing.shared.copyDocumentsForUploading(data: data, extensions: extensions) - promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: ids.map { $0.0 }, localNames: ids.map { $0.1 }))) - ) + return Deferred { + Future { promise in + let ids = FileProcessing.shared.copyDocumentsForUploading(data: data, extensions: extensions) + promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: ids.map { $0.0 }, localNames: ids.map { $0.1 }))) + ) + } } .eraseToAnyPublisher() diff --git a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift index 83393a4..40a6e95 100644 --- a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift @@ -39,70 +39,82 @@ final class XMPPMiddleware { func middleware(state: AppState, action: AppAction) -> AnyPublisher { switch action { case .accountsAction(.tryAddAccountWithCredentials): - return Future { [weak self] promise in - self?.service.updateClients(for: state.accountsState.accounts) - promise(.success(.info("XMPPMiddleware: clients updated in XMPP service"))) + return Deferred { + Future { [weak self] promise in + self?.service.updateClients(for: state.accountsState.accounts) + promise(.success(.info("XMPPMiddleware: clients updated in XMPP service"))) + } } .eraseToAnyPublisher() case .accountsAction(.addAccountError): - return Future { [weak self] promise in - self?.service.updateClients(for: state.accountsState.accounts) - promise(.success(.info("XMPPMiddleware: clients updated in XMPP service"))) + return Deferred { + Future { [weak self] promise in + self?.service.updateClients(for: state.accountsState.accounts) + promise(.success(.info("XMPPMiddleware: clients updated in XMPP service"))) + } } .eraseToAnyPublisher() case .databaseAction(.storedAccountsLoaded(let accounts)): - return Future { [weak self] promise in - self?.service.updateClients(for: accounts.filter { $0.isActive && !$0.isTemp }) - promise(.success(.info("XMPPMiddleware: clients updated in XMPP service"))) + return Deferred { + Future { [weak self] promise in + self?.service.updateClients(for: accounts.filter { $0.isActive && !$0.isTemp }) + promise(.success(.info("XMPPMiddleware: clients updated in XMPP service"))) + } } .eraseToAnyPublisher() case .rostersAction(.addRoster(let ownerJID, let contactJID, let name, let groups)): - return Future { [weak self] promise in - guard let service = self?.service, let client = service.clients.first(where: { $0.connectionConfiguration.userJid.stringValue == ownerJID }) else { - return promise(.success(.rostersAction(.addRosterError(reason: XMPPError.item_not_found.localizedDescription)))) - } - let module = client.modulesManager.module(RosterModule.self) - module.addItem(jid: JID(contactJID), name: name, groups: groups, completionHandler: { result in - switch result { - case .success: - promise(.success(.rostersAction(.addRosterDone(jid: contactJID)))) - - case .failure(let error): - promise(.success(.rostersAction(.addRosterError(reason: error.localizedDescription)))) + return Deferred { + Future { [weak self] promise in + guard let service = self?.service, let client = service.clients.first(where: { $0.connectionConfiguration.userJid.stringValue == ownerJID }) else { + return promise(.success(.rostersAction(.addRosterError(reason: XMPPError.item_not_found.localizedDescription)))) } - }) + let module = client.modulesManager.module(RosterModule.self) + module.addItem(jid: JID(contactJID), name: name, groups: groups, completionHandler: { result in + switch result { + case .success: + promise(.success(.rostersAction(.addRosterDone(jid: contactJID)))) + + case .failure(let error): + promise(.success(.rostersAction(.addRosterError(reason: error.localizedDescription)))) + } + }) + } } .eraseToAnyPublisher() case .rostersAction(.deleteRoster(let ownerJID, let contactJID)): - return Future { [weak self] promise in - guard let service = self?.service, let client = service.clients.first(where: { $0.connectionConfiguration.userJid.stringValue == ownerJID }) else { - return promise(.success(.rostersAction(.rosterDeletingFailed(reason: XMPPError.item_not_found.localizedDescription)))) - } - let module = client.modulesManager.module(RosterModule.self) - module.removeItem(jid: JID(contactJID), completionHandler: { result in - switch result { - case .success: - promise(.success(.info("XMPPMiddleware: roster \(contactJID) deleted from \(ownerJID)"))) - - case .failure(let error): - promise(.success(.rostersAction(.rosterDeletingFailed(reason: error.localizedDescription)))) + return Deferred { + Future { [weak self] promise in + guard let service = self?.service, let client = service.clients.first(where: { $0.connectionConfiguration.userJid.stringValue == ownerJID }) else { + return promise(.success(.rostersAction(.rosterDeletingFailed(reason: XMPPError.item_not_found.localizedDescription)))) } - }) + let module = client.modulesManager.module(RosterModule.self) + module.removeItem(jid: JID(contactJID), completionHandler: { result in + switch result { + case .success: + promise(.success(.info("XMPPMiddleware: roster \(contactJID) deleted from \(ownerJID)"))) + + case .failure(let error): + promise(.success(.rostersAction(.rosterDeletingFailed(reason: error.localizedDescription)))) + } + }) + } } .eraseToAnyPublisher() case .xmppAction(.xmppMessageSent(let message)): - return Future { [weak self] promise in - DispatchQueue.global().async { - self?.service.sendMessage(message: message) { done in - if done { - promise(.success(.xmppAction(.xmppMessageSendSuccess(msgId: message.id)))) - } else { - promise(.success(.xmppAction(.xmppMessageSendFailed(msgId: message.id)))) + return Deferred { + Future { [weak self] promise in + DispatchQueue.global().async { + self?.service.sendMessage(message: message) { done in + if done { + promise(.success(.xmppAction(.xmppMessageSendSuccess(msgId: message.id)))) + } else { + promise(.success(.xmppAction(.xmppMessageSendFailed(msgId: message.id)))) + } } } } @@ -110,18 +122,20 @@ final class XMPPMiddleware { .eraseToAnyPublisher() case .xmppAction(.xmppSharingTryUpload(let message)): - return Future { [weak self] promise in - if self?.uploadingMessageIDs.contains(message.id) ?? false { - return promise(.success(.info("XMPPMiddleware: attachment in message \(message.id) is already in uploading process"))) - } else { - self?.uploadingMessageIDs.insert(message.id) - DispatchQueue.global().async { - self?.service.uploadAttachment(message: message) { error, remotePath in - self?.uploadingMessageIDs.remove(message.id) - if let error { - promise(.success(.xmppAction(.xmppSharingUploadFailed(msgId: message.id, reason: error.localizedDescription)))) - } else { - promise(.success(.xmppAction(.xmppSharingUploadSuccess(msgId: message.id, attachmentRemotePath: remotePath)))) + return Deferred { + Future { [weak self] promise in + if self?.uploadingMessageIDs.contains(message.id) ?? false { + return promise(.success(.info("XMPPMiddleware: attachment in message \(message.id) is already in uploading process"))) + } else { + self?.uploadingMessageIDs.insert(message.id) + DispatchQueue.global().async { + self?.service.uploadAttachment(message: message) { error, remotePath in + self?.uploadingMessageIDs.remove(message.id) + if let error { + promise(.success(.xmppAction(.xmppSharingUploadFailed(msgId: message.id, reason: error.localizedDescription)))) + } else { + promise(.success(.xmppAction(.xmppSharingUploadSuccess(msgId: message.id, attachmentRemotePath: remotePath)))) + } } } } @@ -130,9 +144,11 @@ final class XMPPMiddleware { .eraseToAnyPublisher() case .xmppAction(.xmppLoadArchivedMessages(let jid, let to, let fromDate)): - return Future { [weak self] promise in - self?.service.requestArchivedMessages(jid: jid, to: to, fromDate: fromDate) - promise(.success(.info("XMPPMiddleware: archived messages requested for \(jid) from \(fromDate)"))) + return Deferred { + Future { [weak self] promise in + self?.service.requestArchivedMessages(jid: jid, to: to, fromDate: fromDate) + promise(.success(.info("XMPPMiddleware: archived messages requested for \(jid) from \(fromDate)"))) + } } .eraseToAnyPublisher()