another.im-ios/old/AppCore/Middlewares/DatabaseMiddleware.swift

531 lines
25 KiB
Swift
Raw Normal View History

2024-06-19 15:15:27 +00:00
import Combine
import Foundation
import GRDB
2024-07-13 01:29:46 +00:00
// swiftlint:disable:next type_body_length
2024-06-19 15:15:27 +00:00
final class DatabaseMiddleware {
static let shared = DatabaseMiddleware()
private let database = Database.shared
private var cancellables: Set<AnyCancellable> = []
2024-06-26 08:00:59 +00:00
private var conversationCancellables: Set<AnyCancellable> = []
2024-06-19 15:15:27 +00:00
private init() {
// Database changes
ValueObservation
.tracking(Roster.fetchAll)
.publisher(in: database._db, scheduling: .immediate)
.sink { _ in
// Handle completion
} receiveValue: { rosters in
DispatchQueue.main.async {
store.dispatch(.databaseAction(.storedRostersLoaded(rosters: rosters)))
}
}
.store(in: &cancellables)
ValueObservation
.tracking(Chat.fetchAll)
.publisher(in: database._db, scheduling: .immediate)
.sink { _ in
// Handle completion
} receiveValue: { chats in
DispatchQueue.main.async {
store.dispatch(.databaseAction(.storedChatsLoaded(chats: chats)))
}
}
.store(in: &cancellables)
}
2024-07-14 10:08:51 +00:00
// swiftlint:disable:next function_body_length cyclomatic_complexity
2024-06-19 15:15:27 +00:00
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
switch action {
2024-06-26 08:00:59 +00:00
// MARK: Accounts
2024-06-19 15:15:27 +00:00
case .startAction(.loadStoredAccounts):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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)))
2024-06-19 15:15:27 +00:00
}
}
}
}
.eraseToAnyPublisher()
case .accountsAction(.makeAccountPermanent(let account)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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)
2024-06-19 15:15:27 +00:00
2024-08-07 12:49:47 +00:00
// Re-Fetch all accounts
let accounts = try Account.fetchAll(db)
2024-06-19 15:15:27 +00:00
2024-08-07 12:49:47 +00:00
// Use the accounts
promise(.success(.databaseAction(.storedAccountsLoaded(accounts: accounts))))
}
} catch {
promise(.success(.databaseAction(.updateAccountFailed)))
2024-06-19 15:15:27 +00:00
}
}
}
}
.eraseToAnyPublisher()
2024-06-26 08:00:59 +00:00
// MARK: Rosters
2024-06-19 15:15:27 +00:00
case .rostersAction(.markRosterAsLocallyDeleted(let ownerJID, let contactJID)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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))))
}
2024-06-19 15:15:27 +00:00
}
}
}
.eraseToAnyPublisher()
case .rostersAction(.unmarkRosterAsLocallyDeleted(let ownerJID, let contactJID)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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))))
}
2024-06-19 15:15:27 +00:00
}
}
}
.eraseToAnyPublisher()
2024-06-26 08:00:59 +00:00
// MARK: Chats
2024-06-20 05:43:49 +00:00
case .chatsAction(.createNewChat(let accountJid, let participantJid)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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))))
2024-06-20 05:43:49 +00:00
}
}
}
}
.eraseToAnyPublisher()
2024-06-26 08:00:59 +00:00
// MARK: Conversation and messages
case .conversationAction(.makeConversationActive(let chat, _)):
2024-06-26 09:29:30 +00:00
subscribeToMessages(chat: chat)
return Empty().eraseToAnyPublisher()
2024-06-26 08:00:59 +00:00
2024-06-24 13:28:26 +00:00
case .xmppAction(.xmppMessageReceived(let message)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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))))
2024-06-26 08:00:59 +00:00
}
}
}
2024-06-24 13:28:26 +00:00
}
2024-06-26 08:00:59 +00:00
.eraseToAnyPublisher()
case .conversationAction(.sendMessage(let from, let to, let body)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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))))
}
2024-06-26 10:26:04 +00:00
}
}
}
.eraseToAnyPublisher()
case .xmppAction(.xmppMessageSendSuccess(let msgId)):
// mark message as pending false and sentError false
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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)))
)
}
2024-06-26 10:26:04 +00:00
}
}
}
.eraseToAnyPublisher()
case .xmppAction(.xmppMessageSendFailed(let msgId)):
// mark message as pending false and sentError true
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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))))
}
2024-07-12 11:43:14 +00:00
}
}
}
.eraseToAnyPublisher()
2024-07-13 01:29:46 +00:00
// MARK: Attachments
2024-07-14 08:52:15 +00:00
case .fileAction(.downloadAttachmentFile(let id, _)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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)))
)
}
2024-07-14 08:52:15 +00:00
}
}
}
.eraseToAnyPublisher()
2024-07-13 01:29:46 +00:00
case .fileAction(.downloadingAttachmentFileFailed(let id, _)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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)))
)
}
2024-07-13 01:29:46 +00:00
}
}
}
.eraseToAnyPublisher()
2024-07-14 10:08:51 +00:00
case .fileAction(.attachmentFileDownloaded(let id, let localName)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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)))
)
}
2024-07-13 01:29:46 +00:00
}
}
}
.eraseToAnyPublisher()
2024-07-14 10:08:51 +00:00
case .fileAction(.attachmentThumbnailCreated(let id, let thumbnailName)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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)))
)
}
2024-06-26 10:26:04 +00:00
}
}
}
.eraseToAnyPublisher()
2024-07-14 17:08:43 +00:00
2024-07-14 19:22:46 +00:00
// MARK: Sharing
2024-07-14 17:08:43 +00:00
case .conversationAction(.sendMediaMessages(let from, let to, let messageIds, let localFilesNames)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { promise in
Task(priority: .background) { [weak self] in
guard let database = self?.database else {
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError)))
2024-07-14 17:08:43 +00:00
)
2024-08-07 12:49:47 +00:00
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)
}
2024-07-14 17:08:43 +00:00
}
2024-08-07 12:49:47 +00:00
promise(.success(.info("DatabaseMiddleware: messages with sharings stored in db")))
} catch {
promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription)))
)
2024-07-14 17:08:43 +00:00
}
}
}
}
.eraseToAnyPublisher()
2024-07-14 19:22:46 +00:00
2024-07-16 12:36:57 +00:00
case .sharingAction(.retrySharing(let id)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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)))
)
}
2024-07-16 12:36:57 +00:00
}
}
}
.eraseToAnyPublisher()
2024-07-16 13:01:27 +00:00
case .xmppAction(.xmppSharingUploadSuccess(let messageId, let remotePath)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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)))
)
}
2024-07-14 19:22:46 +00:00
}
}
}
.eraseToAnyPublisher()
2024-07-16 13:01:27 +00:00
case .xmppAction(.xmppSharingUploadFailed(let messageId, _)):
2024-08-07 12:49:47 +00:00
return Deferred {
Future<AppAction, Never> { 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)))
)
}
2024-07-14 19:22:46 +00:00
}
}
}
.eraseToAnyPublisher()
2024-06-24 13:28:26 +00:00
2024-06-19 15:15:27 +00:00
default:
return Empty().eraseToAnyPublisher()
}
}
}
2024-06-26 09:29:30 +00:00
private extension DatabaseMiddleware {
func subscribeToMessages(chat: Chat) {
conversationCancellables = []
ValueObservation
.tracking(
Message
.filter(
2024-07-02 06:32:23 +00:00
(Column("to") == chat.account && Column("from") == chat.participant) ||
2024-06-26 09:29:30 +00:00
(Column("from") == chat.account && Column("to") == chat.participant)
)
2024-07-01 10:05:39 +00:00
.order(Column("date").desc)
2024-06-26 09:29:30 +00:00
.fetchAll
)
.publisher(in: database._db, scheduling: .immediate)
2024-06-26 10:26:04 +00:00
.sink { _ in
2024-06-26 09:29:30 +00:00
} receiveValue: { messages in
2024-07-11 15:46:57 +00:00
// messages
DispatchQueue.main.async {
2024-07-23 14:55:38 +00:00
store.dispatch(.conversationAction(.messagesUpdated(messages: messages)))
2024-07-11 15:46:57 +00:00
}
2024-06-26 09:29:30 +00:00
}
.store(in: &conversationCancellables)
}
}