fix performance

This commit is contained in:
fmodf 2024-08-07 14:49:47 +02:00
parent 794c50fed0
commit 889211683b
10 changed files with 611 additions and 507 deletions

View file

@ -13,6 +13,7 @@ final class Store<State: Stateable, Action: Codable>: ObservableObject {
// Serial queue for performing any actions sequentially // Serial queue for performing any actions sequentially
private let serialQueue = DispatchQueue(label: "im.narayana.conversations.classic.serial.queue", qos: .userInteractive) 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<State, Action> private let reducer: Reducer<State, Action>
private let middlewares: [Middleware<State, Action>] private let middlewares: [Middleware<State, Action>]
@ -71,8 +72,11 @@ final class Store<State: Stateable, Action: Codable>: ObservableObject {
} }
startTime = CFAbsoluteTimeGetCurrent() startTime = CFAbsoluteTimeGetCurrent()
middleware middleware
.subscribe(on: middlewareQueue)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink(receiveValue: dispatch) .sink(receiveValue: { [weak self] action in
self?.dispatch(action)
})
.store(in: &middlewareCancellables) .store(in: &middlewareCancellables)
timeElapsed = CFAbsoluteTimeGetCurrent() - startTime timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
if timeElapsed > 0.05 { if timeElapsed > 0.05 {

View file

@ -22,39 +22,43 @@ final class AccountsMiddleware {
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .xmppAction(.clientConnectionChanged(let jid, let connectionStatus)): case .xmppAction(.clientConnectionChanged(let jid, let connectionStatus)):
return Future<AppAction, Never> { promise in return Deferred {
guard let account = state.accountsState.accounts.first(where: { $0.bareJid == jid }) else { Future<AppAction, Never> { promise in
promise(.success(.info("AccountsMiddleware: account not found for jid \(jid)"))) guard let account = state.accountsState.accounts.first(where: { $0.bareJid == jid }) else {
return promise(.success(.info("AccountsMiddleware: account not found for jid \(jid)")))
} return
if account.isTemp { }
switch connectionStatus { if account.isTemp {
case .connected: switch connectionStatus {
promise(.success(.accountsAction(.makeAccountPermanent(account: account)))) case .connected:
promise(.success(.accountsAction(.makeAccountPermanent(account: account))))
case .disconnected(let reason):
if reason != "No error!" { case .disconnected(let reason):
promise(.success(.accountsAction(.addAccountError(jid: jid, reason: reason)))) if reason != "No error!" {
} else { promise(.success(.accountsAction(.addAccountError(jid: jid, reason: reason))))
promise(.success(.info("AccountsMiddleware: account \(jid) disconnected with no error"))) } else {
} promise(.success(.info("AccountsMiddleware: account \(jid) disconnected with no error")))
}
default:
promise(.success(.info("AccountsMiddleware: account \(jid) connection status changed to \(connectionStatus)"))) 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() .eraseToAnyPublisher()
case .xmppAction(.serverFeaturesLoaded(let jid, let features)): case .xmppAction(.serverFeaturesLoaded(let jid, let features)):
return Future<AppAction, Never> { [weak self] promise in return Deferred {
let serverFeatures = features Future<AppAction, Never> { [weak self] promise in
.compactMap { featureId in let serverFeatures = features
self?.allFeatures.first(where: { $0.xmppId == featureId }) .compactMap { featureId in
} self?.allFeatures.first(where: { $0.xmppId == featureId })
promise(.success(.accountsAction(.clientServerFeaturesUpdated(jid: jid, features: serverFeatures)))) }
promise(.success(.accountsAction(.clientServerFeaturesUpdated(jid: jid, features: serverFeatures))))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()

View file

@ -10,13 +10,15 @@ final class ChatsMiddleware {
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .chatsAction(.startChat(accountJid: let accountJid, participantJid: let participantJid)): case .chatsAction(.startChat(accountJid: let accountJid, participantJid: let participantJid)):
return Future<AppAction, Never> { promise in return Deferred {
if let exist = state.chatsState.chats.first(where: { $0.account == accountJid && $0.participant == participantJid }) { Future<AppAction, Never> { promise in
// open existing chat if let exist = state.chatsState.chats.first(where: { $0.account == accountJid && $0.participant == participantJid }) {
promise(.success(.chatsAction(.chatStarted(chat: exist)))) // open existing chat
} else { promise(.success(.chatsAction(.chatStarted(chat: exist))))
// create new chat } else {
promise(.success(.chatsAction(.createNewChat(accountJid: accountJid, participantJid: participantJid)))) // create new chat
promise(.success(.chatsAction(.createNewChat(accountJid: accountJid, participantJid: participantJid))))
}
} }
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()

View file

@ -6,10 +6,12 @@ final class ConversationMiddleware {
func middleware(state: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> { func middleware(state: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
switch action { switch action {
case .chatsAction(.chatStarted(let chat)): case .chatsAction(.chatStarted(let chat)):
return Future<AppAction, Never> { promise in return Deferred {
let roster = state.rostersState.rosters Future<AppAction, Never> { promise in
.first { $0.bareJid == chat.account && $0.contactBareJid == chat.participant } let roster = state.rostersState.rosters
promise(.success(.conversationAction(.makeConversationActive(chat: chat, roster: roster)))) .first { $0.bareJid == chat.account && $0.contactBareJid == chat.participant }
promise(.success(.conversationAction(.makeConversationActive(chat: chat, roster: roster))))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()

View file

@ -40,46 +40,50 @@ final class DatabaseMiddleware {
switch action { switch action {
// MARK: Accounts // MARK: Accounts
case .startAction(.loadStoredAccounts): case .startAction(.loadStoredAccounts):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.loadingStoredAccountsFailed))) guard let database = self?.database else {
return promise(.success(.databaseAction(.loadingStoredAccountsFailed)))
} return
do { }
try database._db.read { db in do {
let accounts = try Account.fetchAll(db) try database._db.read { db in
promise(.success(.databaseAction(.storedAccountsLoaded(accounts: accounts)))) let accounts = try Account.fetchAll(db)
promise(.success(.databaseAction(.storedAccountsLoaded(accounts: accounts))))
}
} catch {
promise(.success(.databaseAction(.loadingStoredAccountsFailed)))
} }
} catch {
promise(.success(.databaseAction(.loadingStoredAccountsFailed)))
} }
} }
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .accountsAction(.makeAccountPermanent(let account)): case .accountsAction(.makeAccountPermanent(let account)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.updateAccountFailed))) guard let database = self?.database else {
return promise(.success(.databaseAction(.updateAccountFailed)))
} return
do { }
try database._db.write { db in do {
// make permanent and store to database try database._db.write { db in
var acc = account // make permanent and store to database
acc.isTemp = false var acc = account
try acc.insert(db) acc.isTemp = false
try acc.insert(db)
// Re-Fetch all accounts
let accounts = try Account.fetchAll(db) // Re-Fetch all accounts
let accounts = try Account.fetchAll(db)
// Use the accounts
promise(.success(.databaseAction(.storedAccountsLoaded(accounts: accounts)))) // 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 // MARK: Rosters
case .rostersAction(.markRosterAsLocallyDeleted(let ownerJID, let contactJID)): case .rostersAction(.markRosterAsLocallyDeleted(let ownerJID, let contactJID)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError)))) guard let database = self?.database else {
return promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError))))
} return
do { }
_ = try database._db.write { db in do {
try Roster _ = try database._db.write { db in
.filter(Column("bareJid") == ownerJID) try Roster
.filter(Column("contactBareJid") == contactJID) .filter(Column("bareJid") == ownerJID)
.updateAll(db, Column("locallyDeleted").set(to: true)) .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() .eraseToAnyPublisher()
case .rostersAction(.unmarkRosterAsLocallyDeleted(let ownerJID, let contactJID)): case .rostersAction(.unmarkRosterAsLocallyDeleted(let ownerJID, let contactJID)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError)))) guard let database = self?.database else {
return promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError))))
} return
do { }
_ = try database._db.write { db in do {
try Roster _ = try database._db.write { db in
.filter(Column("bareJid") == ownerJID) try Roster
.filter(Column("contactBareJid") == contactJID) .filter(Column("bareJid") == ownerJID)
.updateAll(db, Column("locallyDeleted").set(to: false)) .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 // MARK: Chats
case .chatsAction(.createNewChat(let accountJid, let participantJid)): case .chatsAction(.createNewChat(let accountJid, let participantJid)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.chatsAction(.chatCreationFailed(reason: L10n.Global.Error.genericDbError)))) guard let database = self?.database else {
return promise(.success(.chatsAction(.chatCreationFailed(reason: L10n.Global.Error.genericDbError))))
} return
do { }
try database._db.write { db in do {
let chat = Chat( try database._db.write { db in
id: UUID().uuidString, let chat = Chat(
account: accountJid, id: UUID().uuidString,
participant: participantJid, account: accountJid,
type: .chat participant: participantJid,
) type: .chat
try chat.insert(db) )
promise(.success(.chatsAction(.chatCreated(chat: 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() return Empty().eraseToAnyPublisher()
case .xmppAction(.xmppMessageReceived(let message)): case .xmppAction(.xmppMessageReceived(let message)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError)))) guard let database = self?.database else {
return 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"))) guard message.contentType != .typing, message.body != nil else {
return promise(.success(.info("DatabaseMiddleware: message \(message.id) received as 'typing...' or message body is nil")))
} return
do { }
try database._db.write { db in do {
try message.insert(db) 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() .eraseToAnyPublisher()
case .conversationAction(.sendMessage(let from, let to, let body)): case .conversationAction(.sendMessage(let from, let to, let body)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError)))) guard let database = self?.database else {
return promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))))
} return
do { }
let message = Message( do {
id: UUID().uuidString, let message = Message(
type: .chat, id: UUID().uuidString,
contentType: .text, type: .chat,
from: from, contentType: .text,
to: to, from: from,
body: body, to: to,
subject: nil, body: body,
thread: nil, subject: nil,
oobUrl: nil, thread: nil,
date: Date(), oobUrl: nil,
pending: true, date: Date(),
sentError: false pending: true,
) sentError: false
try database._db.write { db in )
try message.insert(db) 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)): case .xmppAction(.xmppMessageSendSuccess(let msgId)):
// mark message as pending false and sentError false // mark message as pending false and sentError false
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError)))) guard let database = self?.database else {
return promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))))
} return
do { }
_ = try database._db.write { db in do {
try Message _ = try database._db.write { db in
.filter(Column("id") == msgId) try Message
.updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: false)) .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)): case .xmppAction(.xmppMessageSendFailed(let msgId)):
// mark message as pending false and sentError true // mark message as pending false and sentError true
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError)))) guard let database = self?.database else {
return promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))))
} return
do { }
_ = try database._db.write { db in do {
try Message _ = try database._db.write { db in
.filter(Column("id") == msgId) try Message
.updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: true)) .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 // MARK: Attachments
case .fileAction(.downloadAttachmentFile(let id, _)): case .fileAction(.downloadAttachmentFile(let id, _)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) guard let database = self?.database else {
) promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError)))
return )
} return
do { }
_ = try database._db.write { db in do {
try Message _ = try database._db.write { db in
.filter(Column("id") == id) try Message
.updateAll(db, Column("attachmentDownloadFailed").set(to: false)) .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() .eraseToAnyPublisher()
case .fileAction(.downloadingAttachmentFileFailed(let id, _)): case .fileAction(.downloadingAttachmentFileFailed(let id, _)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) guard let database = self?.database else {
) promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError)))
return )
} return
do { }
_ = try database._db.write { db in do {
try Message _ = try database._db.write { db in
.filter(Column("id") == id) try Message
.updateAll(db, Column("attachmentDownloadFailed").set(to: true)) .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() .eraseToAnyPublisher()
case .fileAction(.attachmentFileDownloaded(let id, let localName)): case .fileAction(.attachmentFileDownloaded(let id, let localName)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) guard let database = self?.database else {
) promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError)))
return )
} return
do { }
_ = try database._db.write { db in do {
try Message _ = try database._db.write { db in
.filter(Column("id") == id) try Message
.updateAll(db, Column("attachmentLocalName").set(to: localName), Column("attachmentDownloadFailed").set(to: false)) .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() .eraseToAnyPublisher()
case .fileAction(.attachmentThumbnailCreated(let id, let thumbnailName)): case .fileAction(.attachmentThumbnailCreated(let id, let thumbnailName)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError))) guard let database = self?.database else {
) promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError)))
return )
} return
do { }
_ = try database._db.write { db in do {
try Message _ = try database._db.write { db in
.filter(Column("id") == id) try Message
.updateAll(db, Column("attachmentThumbnailName").set(to: thumbnailName)) .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 // MARK: Sharing
case .conversationAction(.sendMediaMessages(let from, let to, let messageIds, let localFilesNames)): case .conversationAction(.sendMediaMessages(let from, let to, let messageIds, let localFilesNames)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))) guard let database = self?.database else {
) promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError)))
return )
} return
do { }
for (index, id) in messageIds.enumerated() { do {
let message = Message( for (index, id) in messageIds.enumerated() {
id: id, let message = Message(
type: .chat, id: id,
contentType: .attachment, type: .chat,
from: from, contentType: .attachment,
to: to, from: from,
body: nil, to: to,
subject: nil, body: nil,
thread: nil, subject: nil,
oobUrl: nil, thread: nil,
date: Date(), oobUrl: nil,
pending: true, date: Date(),
sentError: false, pending: true,
attachmentType: localFilesNames[index].attachmentType, sentError: false,
attachmentLocalName: localFilesNames[index] 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() .eraseToAnyPublisher()
case .sharingAction(.retrySharing(let id)): case .sharingAction(.retrySharing(let id)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))) guard let database = self?.database else {
) promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError)))
return )
} return
do { }
_ = try database._db.write { db in do {
try Message _ = try database._db.write { db in
.filter(Column("id") == id) try Message
.updateAll(db, Column("pending").set(to: true), Column("sentError").set(to: false)) .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() .eraseToAnyPublisher()
case .xmppAction(.xmppSharingUploadSuccess(let messageId, let remotePath)): case .xmppAction(.xmppSharingUploadSuccess(let messageId, let remotePath)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: L10n.Global.Error.genericDbError))) guard let database = self?.database else {
) promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: L10n.Global.Error.genericDbError)))
return )
} return
do { }
_ = try database._db.write { db in do {
try Message _ = try database._db.write { db in
.filter(Column("id") == messageId) try Message
.updateAll(db, Column("attachmentRemotePath").set(to: remotePath), Column("pending").set(to: false), Column("sentError").set(to: false)) .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() .eraseToAnyPublisher()
case .xmppAction(.xmppSharingUploadFailed(let messageId, _)): case .xmppAction(.xmppSharingUploadFailed(let messageId, _)):
return Future<AppAction, Never> { promise in return Deferred {
Task(priority: .background) { [weak self] in Future<AppAction, Never> { promise in
guard let database = self?.database else { Task(priority: .background) { [weak self] in
promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: L10n.Global.Error.genericDbError))) guard let database = self?.database else {
) promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: L10n.Global.Error.genericDbError)))
return )
} return
do { }
_ = try database._db.write { db in do {
try Message _ = try database._db.write { db in
.filter(Column("id") == messageId) try Message
.updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: true)) .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)))
)
} }
} }
} }

View file

@ -10,108 +10,126 @@ final class FileMiddleware {
switch action { switch action {
// MARK: - For incomig attachments // MARK: - For incomig attachments
case .conversationAction(.messagesUpdated(let messages)): case .conversationAction(.messagesUpdated(let messages)):
return Future { [weak self] promise in return Deferred {
guard let wSelf = self else { Future { [weak self] promise in
promise(.success(.info("FileMiddleware: on checking attachments/shares messages, middleware self is nil"))) guard let wSelf = self else {
return 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
} }
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 incoming messages with attachments
for message in messages where message.attachmentLocalPath != nil && message.attachmentRemotePath == nil && message.pending { for message in messages where message.attachmentRemotePath != nil && message.attachmentLocalPath == nil {
DispatchQueue.main.async { if wSelf.downloadingMessageIDs.contains(message.id) {
store.dispatch(.xmppAction(.xmppSharingTryUpload(message))) continue
} }
} wSelf.downloadingMessageIDs.insert(message.id)
// 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 { DispatchQueue.main.async {
// swiftlint:disable:next force_unwrapping // 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"))) // for outgoing messages with shared attachments
}.eraseToAnyPublisher() 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)): case .fileAction(.downloadAttachmentFile(let id, let attachmentRemotePath)):
return Future { promise in return Deferred {
let localName = "\(id)_\(UUID().uuidString)\(attachmentRemotePath.lastPathComponent)" Future { promise in
let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName) let localName = "\(id)_\(UUID().uuidString)\(attachmentRemotePath.lastPathComponent)"
DownloadManager.shared.enqueueDownload(from: attachmentRemotePath, to: localUrl) { error in let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName)
DispatchQueue.main.async { DownloadManager.shared.enqueueDownload(from: attachmentRemotePath, to: localUrl) { error in
if let error { DispatchQueue.main.async {
store.dispatch(.fileAction(.downloadingAttachmentFileFailed(messageId: id, reason: error.localizedDescription))) if let error {
} else { store.dispatch(.fileAction(.downloadingAttachmentFileFailed(messageId: id, reason: error.localizedDescription)))
store.dispatch(.fileAction(.attachmentFileDownloaded(messageId: id, localName: localName))) } 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)): case .fileAction(.attachmentFileDownloaded(let id, let localName)):
return Future { [weak self] promise in return Deferred {
self?.downloadingMessageIDs.remove(id) Future { [weak self] promise in
promise(.success(.fileAction(.createAttachmentThumbnail(messageId: id, localName: localName)))) self?.downloadingMessageIDs.remove(id)
promise(.success(.fileAction(.createAttachmentThumbnail(messageId: id, localName: localName))))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .fileAction(.createAttachmentThumbnail(let id, let localName)): case .fileAction(.createAttachmentThumbnail(let id, let localName)):
return Future { [weak self] promise in return Deferred {
if let thumbnailName = FileProcessing.shared.createThumbnail(localName: localName) { Future { [weak self] promise in
self?.downloadingMessageIDs.remove(id) if let thumbnailName = FileProcessing.shared.createThumbnail(localName: localName) {
promise(.success(.fileAction(.attachmentThumbnailCreated(messageId: id, thumbnailName: thumbnailName)))) self?.downloadingMessageIDs.remove(id)
} else { promise(.success(.fileAction(.attachmentThumbnailCreated(messageId: id, thumbnailName: thumbnailName))))
self?.downloadingMessageIDs.remove(id) } else {
promise(.success(.info("FileMiddleware: failed to create thumbnail from \(localName) for message \(id)"))) self?.downloadingMessageIDs.remove(id)
promise(.success(.info("FileMiddleware: failed to create thumbnail from \(localName) for message \(id)")))
}
} }
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
// MARK: - For outgoing sharing // MARK: - For outgoing sharing
case .fileAction(.fetchItemsFromGallery): case .fileAction(.fetchItemsFromGallery):
return Future<AppAction, Never> { promise in return Deferred {
let items = FileProcessing.shared.fetchGallery() Future<AppAction, Never> { promise in
promise(.success(.fileAction(.itemsFromGalleryFetched(items: items)))) let items = FileProcessing.shared.fetchGallery()
promise(.success(.fileAction(.itemsFromGalleryFetched(items: items))))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .fileAction(.itemsFromGalleryFetched(let items)): case .fileAction(.itemsFromGalleryFetched(let items)):
return Future { promise in return Deferred {
let newItems = FileProcessing.shared.fillGalleryItemsThumbnails(items: items) Future { promise in
promise(.success(.sharingAction(.galleryItemsUpdated(items: newItems)))) let newItems = FileProcessing.shared.fillGalleryItemsThumbnails(items: items)
promise(.success(.sharingAction(.galleryItemsUpdated(items: newItems))))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .fileAction(.copyGalleryItemsForUploading(let items)): case .fileAction(.copyGalleryItemsForUploading(let items)):
return Future { promise in return Deferred {
let ids = FileProcessing.shared.copyGalleryItemsForUploading(items: items) Future { promise in
promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: ids.map { $0.0 }, localNames: ids.map { $0.1 })))) let ids = FileProcessing.shared.copyGalleryItemsForUploading(items: items)
promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: ids.map { $0.0 }, localNames: ids.map { $0.1 }))))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .fileAction(.copyCameraCapturedForUploading(let media, let type)): case .fileAction(.copyCameraCapturedForUploading(let media, let type)):
return Future { promise in return Deferred {
if let (id, localName) = FileProcessing.shared.copyCameraCapturedForUploading(media: media, type: type) { Future { promise in
promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: [id], localNames: [localName])))) if let (id, localName) = FileProcessing.shared.copyCameraCapturedForUploading(media: media, type: type) {
} else { promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: [id], localNames: [localName]))))
promise(.success(.info("FileMiddleware: failed to copy camera captured media for uploading"))) } else {
promise(.success(.info("FileMiddleware: failed to copy camera captured media for uploading")))
}
} }
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()

View file

@ -55,3 +55,15 @@ private var dateFormatter: DateFormatter {
formatter.dateFormat = "MM-dd HH:mm:ss.SSS" formatter.dateFormat = "MM-dd HH:mm:ss.SSS"
return formatter 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
}

View file

@ -7,18 +7,20 @@ final class MessagesMiddleware {
func middleware(state: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> { func middleware(state: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
switch action { switch action {
case .conversationAction(.makeConversationActive(let chat, let roster)): case .conversationAction(.makeConversationActive(let chat, let roster)):
return Future<AppAction, Never> { promise in return Deferred {
if let currentClient = state.accountsState.accounts.first(where: { $0.bareJid == chat.account }) { Future<AppAction, Never> { promise in
let features = state.accountsState.discoFeatures[currentClient.bareJid] ?? [] if let currentClient = state.accountsState.accounts.first(where: { $0.bareJid == chat.account }) {
if features.map({ $0.xep }).contains("XEP-0313") { let features = state.accountsState.discoFeatures[currentClient.bareJid] ?? []
let oldestMessageDate = state.conversationsState.currentMessages.first?.date ?? Date() if features.map({ $0.xep }).contains("XEP-0313") {
let archivesRequestDate = Calendar.current.date(byAdding: .day, value: -Const.mamRequestLength, to: oldestMessageDate) ?? Date() let oldestMessageDate = state.conversationsState.currentMessages.first?.date ?? Date()
promise(.success(.xmppAction(.xmppLoadArchivedMessages(jid: currentClient.bareJid, to: roster?.bareJid, fromDate: archivesRequestDate)))) 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 { } 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() .eraseToAnyPublisher()

View file

@ -11,43 +11,47 @@ final class SharingMiddleware {
switch action { switch action {
// MARK: - Camera and Gallery Access // MARK: - Camera and Gallery Access
case .sharingAction(.checkCameraAccess): case .sharingAction(.checkCameraAccess):
return Future<AppAction, Never> { promise in return Deferred {
let status = AVCaptureDevice.authorizationStatus(for: .video) Future<AppAction, Never> { promise in
switch status { let status = AVCaptureDevice.authorizationStatus(for: .video)
case .authorized: switch status {
promise(.success(.sharingAction(.setCameraAccess(true)))) case .authorized:
promise(.success(.sharingAction(.setCameraAccess(true))))
case .notDetermined: case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in AVCaptureDevice.requestAccess(for: .video) { granted in
promise(.success(.sharingAction(.setCameraAccess(granted)))) 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() .eraseToAnyPublisher()
case .sharingAction(.checkGalleryAccess): case .sharingAction(.checkGalleryAccess):
return Future<AppAction, Never> { promise in return Deferred {
let status = PHPhotoLibrary.authorizationStatus() Future<AppAction, Never> { promise in
switch status { let status = PHPhotoLibrary.authorizationStatus()
case .authorized, .limited: switch status {
promise(.success(.sharingAction(.setGalleryAccess(true)))) case .authorized, .limited:
promise(.success(.sharingAction(.setGalleryAccess(true))))
case .notDetermined: case .notDetermined:
PHPhotoLibrary.requestAuthorization { status in PHPhotoLibrary.requestAuthorization { status in
promise(.success(.sharingAction(.setGalleryAccess(status == .authorized)))) 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() .eraseToAnyPublisher()
@ -58,9 +62,11 @@ final class SharingMiddleware {
// MARK: - Sharing // MARK: - Sharing
case .sharingAction(.shareMedia(let ids)): case .sharingAction(.shareMedia(let ids)):
return Future { promise in return Deferred {
let items = state.sharingState.galleryItems.filter { ids.contains($0.id) } Future { promise in
promise(.success(.fileAction(.copyGalleryItemsForUploading(items: items)))) let items = state.sharingState.galleryItems.filter { ids.contains($0.id) }
promise(.success(.fileAction(.copyGalleryItemsForUploading(items: items))))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -78,12 +84,14 @@ final class SharingMiddleware {
} }
case .sharingAction(.cameraCaptured(let media, let type)): case .sharingAction(.cameraCaptured(let media, let type)):
return Future { promise in return Deferred {
if let (id, localName) = FileProcessing.shared.copyCameraCapturedForUploading(media: media, type: type) { Future { promise in
promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: [id], localNames: [localName]))) 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"))) } else {
promise(.success(.info("SharingMiddleware: camera's captured file didn't copied")))
}
} }
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -98,10 +106,12 @@ final class SharingMiddleware {
} }
case .sharingAction(.shareDocuments(let data, let extensions)): case .sharingAction(.shareDocuments(let data, let extensions)):
return Future { promise in return Deferred {
let ids = FileProcessing.shared.copyDocumentsForUploading(data: data, extensions: extensions) Future { promise in
promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: ids.map { $0.0 }, localNames: ids.map { $0.1 }))) let ids = FileProcessing.shared.copyDocumentsForUploading(data: data, extensions: extensions)
) promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: ids.map { $0.0 }, localNames: ids.map { $0.1 })))
)
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()

View file

@ -39,70 +39,82 @@ final class XMPPMiddleware {
func middleware(state: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> { func middleware(state: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
switch action { switch action {
case .accountsAction(.tryAddAccountWithCredentials): case .accountsAction(.tryAddAccountWithCredentials):
return Future<AppAction, Never> { [weak self] promise in return Deferred {
self?.service.updateClients(for: state.accountsState.accounts) Future<AppAction, Never> { [weak self] promise in
promise(.success(.info("XMPPMiddleware: clients updated in XMPP service"))) self?.service.updateClients(for: state.accountsState.accounts)
promise(.success(.info("XMPPMiddleware: clients updated in XMPP service")))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .accountsAction(.addAccountError): case .accountsAction(.addAccountError):
return Future<AppAction, Never> { [weak self] promise in return Deferred {
self?.service.updateClients(for: state.accountsState.accounts) Future<AppAction, Never> { [weak self] promise in
promise(.success(.info("XMPPMiddleware: clients updated in XMPP service"))) self?.service.updateClients(for: state.accountsState.accounts)
promise(.success(.info("XMPPMiddleware: clients updated in XMPP service")))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .databaseAction(.storedAccountsLoaded(let accounts)): case .databaseAction(.storedAccountsLoaded(let accounts)):
return Future<AppAction, Never> { [weak self] promise in return Deferred {
self?.service.updateClients(for: accounts.filter { $0.isActive && !$0.isTemp }) Future<AppAction, Never> { [weak self] promise in
promise(.success(.info("XMPPMiddleware: clients updated in XMPP service"))) self?.service.updateClients(for: accounts.filter { $0.isActive && !$0.isTemp })
promise(.success(.info("XMPPMiddleware: clients updated in XMPP service")))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .rostersAction(.addRoster(let ownerJID, let contactJID, let name, let groups)): case .rostersAction(.addRoster(let ownerJID, let contactJID, let name, let groups)):
return Future<AppAction, Never> { [weak self] promise in return Deferred {
guard let service = self?.service, let client = service.clients.first(where: { $0.connectionConfiguration.userJid.stringValue == ownerJID }) else { Future<AppAction, Never> { [weak self] promise in
return promise(.success(.rostersAction(.addRosterError(reason: XMPPError.item_not_found.localizedDescription)))) 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))))
} }
}) 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() .eraseToAnyPublisher()
case .rostersAction(.deleteRoster(let ownerJID, let contactJID)): case .rostersAction(.deleteRoster(let ownerJID, let contactJID)):
return Future<AppAction, Never> { [weak self] promise in return Deferred {
guard let service = self?.service, let client = service.clients.first(where: { $0.connectionConfiguration.userJid.stringValue == ownerJID }) else { Future<AppAction, Never> { [weak self] promise in
return promise(.success(.rostersAction(.rosterDeletingFailed(reason: XMPPError.item_not_found.localizedDescription)))) 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))))
} }
}) 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() .eraseToAnyPublisher()
case .xmppAction(.xmppMessageSent(let message)): case .xmppAction(.xmppMessageSent(let message)):
return Future<AppAction, Never> { [weak self] promise in return Deferred {
DispatchQueue.global().async { Future<AppAction, Never> { [weak self] promise in
self?.service.sendMessage(message: message) { done in DispatchQueue.global().async {
if done { self?.service.sendMessage(message: message) { done in
promise(.success(.xmppAction(.xmppMessageSendSuccess(msgId: message.id)))) if done {
} else { promise(.success(.xmppAction(.xmppMessageSendSuccess(msgId: message.id))))
promise(.success(.xmppAction(.xmppMessageSendFailed(msgId: message.id)))) } else {
promise(.success(.xmppAction(.xmppMessageSendFailed(msgId: message.id))))
}
} }
} }
} }
@ -110,18 +122,20 @@ final class XMPPMiddleware {
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .xmppAction(.xmppSharingTryUpload(let message)): case .xmppAction(.xmppSharingTryUpload(let message)):
return Future<AppAction, Never> { [weak self] promise in return Deferred {
if self?.uploadingMessageIDs.contains(message.id) ?? false { Future<AppAction, Never> { [weak self] promise in
return promise(.success(.info("XMPPMiddleware: attachment in message \(message.id) is already in uploading process"))) if self?.uploadingMessageIDs.contains(message.id) ?? false {
} else { return promise(.success(.info("XMPPMiddleware: attachment in message \(message.id) is already in uploading process")))
self?.uploadingMessageIDs.insert(message.id) } else {
DispatchQueue.global().async { self?.uploadingMessageIDs.insert(message.id)
self?.service.uploadAttachment(message: message) { error, remotePath in DispatchQueue.global().async {
self?.uploadingMessageIDs.remove(message.id) self?.service.uploadAttachment(message: message) { error, remotePath in
if let error { self?.uploadingMessageIDs.remove(message.id)
promise(.success(.xmppAction(.xmppSharingUploadFailed(msgId: message.id, reason: error.localizedDescription)))) if let error {
} else { promise(.success(.xmppAction(.xmppSharingUploadFailed(msgId: message.id, reason: error.localizedDescription))))
promise(.success(.xmppAction(.xmppSharingUploadSuccess(msgId: message.id, attachmentRemotePath: remotePath)))) } else {
promise(.success(.xmppAction(.xmppSharingUploadSuccess(msgId: message.id, attachmentRemotePath: remotePath))))
}
} }
} }
} }
@ -130,9 +144,11 @@ final class XMPPMiddleware {
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .xmppAction(.xmppLoadArchivedMessages(let jid, let to, let fromDate)): case .xmppAction(.xmppLoadArchivedMessages(let jid, let to, let fromDate)):
return Future<AppAction, Never> { [weak self] promise in return Deferred {
self?.service.requestArchivedMessages(jid: jid, to: to, fromDate: fromDate) Future<AppAction, Never> { [weak self] promise in
promise(.success(.info("XMPPMiddleware: archived messages requested for \(jid) from \(fromDate)"))) self?.service.requestArchivedMessages(jid: jid, to: to, fromDate: fromDate)
promise(.success(.info("XMPPMiddleware: archived messages requested for \(jid) from \(fromDate)")))
}
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()