From 461edb142a9c78a693193a70e51df562e5102c93 Mon Sep 17 00:00:00 2001 From: wuyueyang Date: Wed, 8 Jun 2022 15:38:24 +0800 Subject: [PATCH] Clear or delete conversation with work --- Mixin.xcodeproj/project.pbxproj | 4 + .../Audio/Playlist/PlaylistManager.swift | 12 +-- Mixin/Service/Job/AttachmentUploadJob.swift | 2 +- .../ConversationInputViewController.swift | 7 +- .../Chat/ConversationViewController.swift | 12 ++- .../Common/ConversationCleaner.swift | 77 +++++++++++++++++ .../Common/GroupProfileViewController.swift | 20 ++--- .../Common/ProfileViewController.swift | 7 +- .../Home/DatabaseUpgradeViewController.swift | 1 - .../Controllers/Home/HomeViewController.swift | 13 ++- .../LoginVerificationCodeViewController.swift | 1 - .../Database/User/DAO/ConversationDAO.swift | 34 +------- .../Database/User/DAO/MessageDAO.swift | 13 +-- .../Database/{Work => User/DAO}/WorkDAO.swift | 10 +-- .../Database/User/UserDatabase.swift | 9 ++ .../Database/Work/WorkDatabase.swift | 44 ---------- .../File Management/AppGroupContainer.swift | 4 - .../AppGroupUserDefaults+User.swift | 1 - .../DeleteConversationAttachmentWork.swift | 85 +++++++++++++++++++ ...wift => DeleteMessageAttachmentWork.swift} | 68 +++------------ .../Services/Work/PersistableWork.swift | 5 -- .../Services/Work/WorkManager.swift | 30 +++++-- 22 files changed, 247 insertions(+), 212 deletions(-) create mode 100644 Mixin/UserInterface/Controllers/Common/ConversationCleaner.swift rename MixinServices/MixinServices/Database/{Work => User/DAO}/WorkDAO.swift (78%) delete mode 100644 MixinServices/MixinServices/Database/Work/WorkDatabase.swift create mode 100644 MixinServices/MixinServices/Services/Work/DeleteConversationAttachmentWork.swift rename MixinServices/MixinServices/Services/Work/{DeleteAttachmentMessageWork.swift => DeleteMessageAttachmentWork.swift} (59%) diff --git a/Mixin.xcodeproj/project.pbxproj b/Mixin.xcodeproj/project.pbxproj index 1a25070958..cdf73a7a68 100644 --- a/Mixin.xcodeproj/project.pbxproj +++ b/Mixin.xcodeproj/project.pbxproj @@ -670,6 +670,7 @@ 947E0659279867870002669B /* PINIteratorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947E0658279867870002669B /* PINIteratorTest.swift */; }; 947F4AD625866D6C00B0A5F9 /* InitializeFTSJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947F4AD525866D6C00B0A5F9 /* InitializeFTSJob.swift */; }; 94812DDA26E082C400213F79 /* mixin_condensed.otf in Resources */ = {isa = PBXBuildFile; fileRef = 94812DD926E082C400213F79 /* mixin_condensed.otf */; }; + 948983B1284E66C00065DC5D /* ConversationCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948983B0284E66C00065DC5D /* ConversationCleaner.swift */; }; 9489E3A025C5B150000319F8 /* AcknowledgementsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9489E39F25C5B150000319F8 /* AcknowledgementsViewController.swift */; }; 9492285F25DFB7D60000A19F /* MinimizedPlaylistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9492285E25DFB7D60000A19F /* MinimizedPlaylistViewController.swift */; }; 9492286425DFB7EF0000A19F /* MinimizedPlaylistView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9492286325DFB7EF0000A19F /* MinimizedPlaylistView.xib */; }; @@ -1659,6 +1660,7 @@ 947F4AD525866D6C00B0A5F9 /* InitializeFTSJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitializeFTSJob.swift; sourceTree = ""; }; 94812DD926E082C400213F79 /* mixin_condensed.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = mixin_condensed.otf; sourceTree = ""; }; 94841CC72797D89500B3593B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 948983B0284E66C00065DC5D /* ConversationCleaner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCleaner.swift; sourceTree = ""; }; 9489E39F25C5B150000319F8 /* AcknowledgementsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsViewController.swift; sourceTree = ""; }; 9492285E25DFB7D60000A19F /* MinimizedPlaylistViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinimizedPlaylistViewController.swift; sourceTree = ""; }; 9492286325DFB7EF0000A19F /* MinimizedPlaylistView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MinimizedPlaylistView.xib; sourceTree = ""; }; @@ -2412,6 +2414,7 @@ E0A14D3B2372F0340044D131 /* GroupProfileViewController.swift */, 7B5E9B3F243700CD000AE24E /* ConversationCircleEditorViewController.swift */, 7CB2A57E27C386F0007D9DEE /* GroupsInCommonViewController.swift */, + 948983B0284E66C00065DC5D /* ConversationCleaner.swift */, ); path = Common; sourceTree = ""; @@ -4757,6 +4760,7 @@ 7BC80A62221D1C07008586AD /* AssetTableHeaderView.swift in Sources */, 7B9553402243860C00CE95E6 /* PinValidationPresentationManager.swift in Sources */, 94046B93272DC28B007C1D4A /* GroupCallMembersManager.swift in Sources */, + 948983B1284E66C00065DC5D /* ConversationCleaner.swift in Sources */, 7B4C6571242358D5003B78F9 /* LocationSearchNoResultView.swift in Sources */, DF5D9F291F9C79E10036D5FD /* UIColorExtension.swift in Sources */, DF0A0A5E24476EA000378B4F /* RefreshOffsetJob.swift in Sources */, diff --git a/Mixin/Service/Audio/Playlist/PlaylistManager.swift b/Mixin/Service/Audio/Playlist/PlaylistManager.swift index e365d2e217..3f6344dc45 100644 --- a/Mixin/Service/Audio/Playlist/PlaylistManager.swift +++ b/Mixin/Service/Audio/Playlist/PlaylistManager.swift @@ -168,11 +168,11 @@ class PlaylistManager: NSObject { object: nil) notificationCenter.addObserver(self, selector: #selector(messageWillDelete(_:)), - name: DeleteAttachmentMessageWork.willDeleteNotification, + name: DeleteMessageAttachmentWork.willDeleteNotification, object: nil) notificationCenter.addObserver(self, - selector: #selector(conversationDAOWillClearConversation(_:)), - name: ConversationDAO.willClearConversationNotification, + selector: #selector(conversationWillClean(_:)), + name: ConversationCleaner.willCleanNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(messageServiceWillRecallMessage(_:)), @@ -774,7 +774,7 @@ extension PlaylistManager { } @objc private func messageWillDelete(_ notification: Notification) { - guard let messageId = notification.userInfo?[DeleteAttachmentMessageWork.messageIdUserInfoKey] as? String else { + guard let messageId = notification.userInfo?[DeleteMessageAttachmentWork.messageIdUserInfoKey] as? String else { return } guard case .conversation = source else { @@ -783,8 +783,8 @@ extension PlaylistManager { removeItem(with: messageId) } - @objc private func conversationDAOWillClearConversation(_ notification: Notification) { - guard let id = notification.userInfo?[ConversationDAO.conversationIdUserInfoKey] as? String else { + @objc private func conversationWillClean(_ notification: Notification) { + guard let id = notification.userInfo?[ConversationCleaner.conversationIdUserInfoKey] as? String else { return } guard case let .conversation(conversationId) = source, conversationId == id else { diff --git a/Mixin/Service/Job/AttachmentUploadJob.swift b/Mixin/Service/Job/AttachmentUploadJob.swift index aaaa97b4de..08dad86735 100644 --- a/Mixin/Service/Job/AttachmentUploadJob.swift +++ b/Mixin/Service/Job/AttachmentUploadJob.swift @@ -78,7 +78,7 @@ class AttachmentUploadJob: AttachmentLoadingJob { return false } guard let fileUrl = fileUrl else { - let work = DeleteAttachmentMessageWork(message: message) + let work = DeleteMessageAttachmentWork(message: message) WorkManager.general.addWork(work) return false } diff --git a/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift b/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift index 1774c3d549..77416bf322 100644 --- a/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift +++ b/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift @@ -272,11 +272,8 @@ class ConversationInputViewController: UIViewController { self.deleteConversationButton.isBusy = false })) alert.addAction(UIAlertAction(title: R.string.localizable.delete_chat(), style: .destructive, handler: { (_) in - DispatchQueue.global().async { [weak self] in - ConversationDAO.shared.deleteChat(conversationId: conversationId) - DispatchQueue.main.async { - self?.navigationController?.backToHome() - } + ConversationCleaner.clean(conversationId: conversationId, intent: .delete) { + self.navigationController?.backToHome() } })) present(alert, animated: true, completion: nil) diff --git a/Mixin/UserInterface/Controllers/Chat/ConversationViewController.swift b/Mixin/UserInterface/Controllers/Chat/ConversationViewController.swift index ef3bb99450..55820e8973 100644 --- a/Mixin/UserInterface/Controllers/Chat/ConversationViewController.swift +++ b/Mixin/UserInterface/Controllers/Chat/ConversationViewController.swift @@ -2707,13 +2707,11 @@ extension ConversationViewController { guard let weakSelf = self, let indexPath = weakSelf.dataSource.indexPath(where: { $0.messageId == message.messageId }) else { return } - if DeleteAttachmentMessageWork.capableMessageCategories.contains(message.category) { - let work = DeleteAttachmentMessageWork(message: message) - WorkManager.general.addWork(work) - } else { - MessageDAO.shared.delete(id: message.messageId, - conversationId: message.conversationId, - deleteTranscriptChildren: true) + MessageDAO.shared.delete(id: message.messageId, conversationId: message.conversationId, deleteTranscriptChildren: false) { db in + if DeleteMessageAttachmentWork.capableMessageCategories.contains(message.category) { + let work = DeleteMessageAttachmentWork(message: message) + WorkManager.general.addPersistableWork(work, alongsideTransactionWith: db) + } } DispatchQueue.main.sync { _ = weakSelf.dataSource?.removeViewModel(at: indexPath) diff --git a/Mixin/UserInterface/Controllers/Common/ConversationCleaner.swift b/Mixin/UserInterface/Controllers/Common/ConversationCleaner.swift new file mode 100644 index 0000000000..953276745d --- /dev/null +++ b/Mixin/UserInterface/Controllers/Common/ConversationCleaner.swift @@ -0,0 +1,77 @@ +import Foundation +import MixinServices +import GRDB + +enum ConversationCleaner { + + public static let willCleanNotification = Notification.Name("one.mixin.messenger.ConversationCleaner.willClean") + public static let conversationIdUserInfoKey = "cid" + + enum Intent { + case delete + case clear + } + + static func clean(conversationId: String, intent: Intent, completion: (() -> Void)? = nil) { + let hud = Hud() + hud.show(style: .busy, text: "", on: AppDelegate.current.mainWindow) + NotificationCenter.default.post(name: Self.willCleanNotification, + object: self, + userInfo: [Self.conversationIdUserInfoKey: conversationId]) + DispatchQueue.global().async { + UserDatabase.current.write { db in + let categories = MessageCategory.allMediaCategoriesString.joined(separator: "', '") + let sql = "SELECT media_url, category FROM messages WHERE conversation_id = ? AND category IN ('\(categories)') AND media_url IS NOT NULL" + let attachments = try DeleteConversationAttachmentWork.Attachment.fetchAll(db, sql: sql, arguments: [conversationId]) + let transcriptMessageIds = try MessageDAO.shared.getTranscriptMessageIds(conversationId: conversationId, database: db) + if !attachments.isEmpty || !transcriptMessageIds.isEmpty { + let work = DeleteConversationAttachmentWork(attachments: attachments, transcriptMessageIds: transcriptMessageIds) + WorkManager.general.addPersistableWork(work, alongsideTransactionWith: db) + } + + try Message + .filter(Message.column(of: .conversationId) == conversationId) + .deleteAll(db) + try MessageMention + .filter(MessageMention.column(of: .conversationId) == conversationId) + .deleteAll(db) + + switch intent { + case .delete: + try Conversation + .filter(Conversation.column(of: .conversationId) == conversationId) + .deleteAll(db) + try Participant + .filter(Participant.column(of: .conversationId) == conversationId) + .deleteAll(db) + try ParticipantSession + .filter(ParticipantSession.column(of: .conversationId) == conversationId) + .deleteAll(db) + case .clear: + try Conversation + .filter(Conversation.column(of: .conversationId) == conversationId) + .updateAll(db, [Conversation.column(of: .unseenMessageCount).set(to: 0)]) + } + + try ConversationDAO.shared.deleteFTSContent(with: conversationId, from: db) + try PinMessageDAO.shared.deleteAll(conversationId: conversationId, from: db) + db.afterNextTransactionCommit { (_) in + DispatchQueue.main.async { + switch intent { + case .delete: + NotificationCenter.default.post(name: conversationDidChangeNotification, object: nil) + hud.set(style: .notification, text: R.string.localizable.deleted()) + case .clear: + let change = ConversationChange(conversationId: conversationId, action: .reload) + NotificationCenter.default.post(name: conversationDidChangeNotification, object: change) + hud.set(style: .notification, text: R.string.localizable.cleared()) + } + hud.scheduleAutoHidden() + completion?() + } + } + } + } + } + +} diff --git a/Mixin/UserInterface/Controllers/Common/GroupProfileViewController.swift b/Mixin/UserInterface/Controllers/Common/GroupProfileViewController.swift index 28a153c0c8..9824dfd03a 100644 --- a/Mixin/UserInterface/Controllers/Common/GroupProfileViewController.swift +++ b/Mixin/UserInterface/Controllers/Common/GroupProfileViewController.swift @@ -207,21 +207,11 @@ extension GroupProfileViewController { let conversationId = conversation.conversationId let alert = UIAlertController(title: R.string.localizable.delete_group_chat_confirmation(conversation.name), message: nil, preferredStyle: .actionSheet) alert.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .cancel, handler: nil)) - alert.addAction(UIAlertAction(title: R.string.localizable.delete_chat(), style: .destructive, handler: { [weak self](_) in - let hud = Hud() - hud.show(style: .busy, text: "", on: AppDelegate.current.mainWindow) - DispatchQueue.global().async { - ConversationDAO.shared.deleteChat(conversationId: conversationId) - DispatchQueue.main.async { - guard let self = self else { - return - } - self.dismiss(animated: true) { - hud.set(style: .notification, text: R.string.localizable.done()) - hud.scheduleAutoHidden() - if UIApplication.currentConversationId() == conversationId { - UIApplication.homeNavigationController?.backToHome() - } + alert.addAction(UIAlertAction(title: R.string.localizable.delete_chat(), style: .destructive, handler: { _ in + ConversationCleaner.clean(conversationId: conversationId, intent: .delete) { + self.dismiss(animated: true) { + if UIApplication.currentConversationId() == conversationId { + UIApplication.homeNavigationController?.backToHome() } } } diff --git a/Mixin/UserInterface/Controllers/Common/ProfileViewController.swift b/Mixin/UserInterface/Controllers/Common/ProfileViewController.swift index 65560c3eb6..e97380cd00 100644 --- a/Mixin/UserInterface/Controllers/Common/ProfileViewController.swift +++ b/Mixin/UserInterface/Controllers/Common/ProfileViewController.swift @@ -250,12 +250,7 @@ extension ProfileViewController { alert.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .cancel, handler: nil)) alert.addAction(UIAlertAction(title: R.string.localizable.clear_chat(), style: .destructive, handler: { (_) in self.dismiss(animated: true, completion: nil) - DispatchQueue.global().async { - ConversationDAO.shared.clearChat(conversationId: conversationId) - DispatchQueue.main.async { - showAutoHiddenHud(style: .notification, text: R.string.localizable.cleared()) - } - } + ConversationCleaner.clean(conversationId: conversationId, intent: .clear) })) present(alert, animated: true, completion: nil) } diff --git a/Mixin/UserInterface/Controllers/Home/DatabaseUpgradeViewController.swift b/Mixin/UserInterface/Controllers/Home/DatabaseUpgradeViewController.swift index 16051d537a..1491aceecc 100644 --- a/Mixin/UserInterface/Controllers/Home/DatabaseUpgradeViewController.swift +++ b/Mixin/UserInterface/Controllers/Home/DatabaseUpgradeViewController.swift @@ -41,7 +41,6 @@ class DatabaseUpgradeViewController: UIViewController { AppGroupContainer.migrateIfNeeded() TaskDatabase.reloadCurrent() UserDatabase.reloadCurrent() - WorkDatabase.reloadCurrent() if !AppGroupUserDefaults.Database.isSentSenderKeyCleared { UserDatabase.current.clearSentSenderKey() diff --git a/Mixin/UserInterface/Controllers/Home/HomeViewController.swift b/Mixin/UserInterface/Controllers/Home/HomeViewController.swift index 7f5e2ac1d8..d9de9400dd 100644 --- a/Mixin/UserInterface/Controllers/Home/HomeViewController.swift +++ b/Mixin/UserInterface/Controllers/Home/HomeViewController.swift @@ -145,7 +145,10 @@ class HomeViewController: UIViewController { if AppGroupUserDefaults.User.hasRecoverMedia { ConcurrentJobQueue.shared.addJob(job: RecoverMediaJob()) } - WorkManager.general.wakeUpPersistedWorks(with: [DeleteAttachmentMessageWork.self]) + WorkManager.general.wakeUpPersistedWorks(with: [ + DeleteMessageAttachmentWork.self, + DeleteConversationAttachmentWork.self + ]) initializeFTSIfNeeded() refreshExternalSchemesIfNeeded() } @@ -740,9 +743,7 @@ extension HomeViewController { self.conversations.remove(at: indexPath.row) self.tableView.deleteRows(at: [indexPath], with: .fade) self.tableView.endUpdates() - DispatchQueue.global().async { - ConversationDAO.shared.deleteChat(conversationId: conversationId) - } + ConversationCleaner.clean(conversationId: conversationId, intent: .delete) })) present(alert, animated: true, completion: nil) } @@ -764,9 +765,7 @@ extension HomeViewController { self.conversations[indexPath.row].unseenMessageCount = 0 self.tableView.reloadRows(at: [indexPath], with: .automatic) self.tableView.endUpdates() - DispatchQueue.global().async { - ConversationDAO.shared.clearChat(conversationId: conversationId) - } + ConversationCleaner.clean(conversationId: conversationId, intent: .clear) })) present(alert, animated: true, completion: nil) } diff --git a/Mixin/UserInterface/Controllers/Login/LoginVerificationCodeViewController.swift b/Mixin/UserInterface/Controllers/Login/LoginVerificationCodeViewController.swift index 817d0319f2..f9f8e19105 100644 --- a/Mixin/UserInterface/Controllers/Login/LoginVerificationCodeViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/LoginVerificationCodeViewController.swift @@ -100,7 +100,6 @@ class LoginVerificationCodeViewController: VerificationCodeViewController { TaskDatabase.reloadCurrent() UserDatabase.reloadCurrent() - WorkDatabase.reloadCurrent() if AppGroupUserDefaults.User.isLogoutByServer { UserDatabase.current.clearSentSenderKey() AppGroupUserDefaults.Database.isSentSenderKeyCleared = true diff --git a/MixinServices/MixinServices/Database/User/DAO/ConversationDAO.swift b/MixinServices/MixinServices/Database/User/DAO/ConversationDAO.swift index 7a155afb3b..7b9328583f 100644 --- a/MixinServices/MixinServices/Database/User/DAO/ConversationDAO.swift +++ b/MixinServices/MixinServices/Database/User/DAO/ConversationDAO.swift @@ -4,9 +4,6 @@ public final class ConversationDAO: UserDatabaseDAO { public static let shared = ConversationDAO() - public static let willClearConversationNotification = Notification.Name("one.mixin.service.ConversationDAO.willClearConversation") - public static let conversationIdUserInfoKey = "cid" - private static let sqlQueryColumns = """ SELECT c.conversation_id as conversationId, c.owner_id as ownerId, c.icon_url as iconUrl, c.announcement as announcement, c.category as category, c.name as name, c.status as status, @@ -226,35 +223,6 @@ public final class ConversationDAO: UserDatabaseDAO { } } - public func clearChat(conversationId: String) { - let mediaUrls = MessageDAO.shared.getMediaUrls(conversationId: conversationId, categories: MessageCategory.allMediaCategories) - db.write { db in - let deletedTranscriptIds = try deleteTranscriptChildrenReferenced(by: conversationId, from: db) - NotificationCenter.default.post(onMainThread: Self.willClearConversationNotification, - object: self, - userInfo: [Self.conversationIdUserInfoKey: conversationId]) - try Message - .filter(Message.column(of: .conversationId) == conversationId) - .deleteAll(db) - try MessageMention - .filter(MessageMention.column(of: .conversationId) == conversationId) - .deleteAll(db) - try Conversation - .filter(Conversation.column(of: .conversationId) == conversationId) - .updateAll(db, [Conversation.column(of: .unseenMessageCount).set(to: 0)]) - try deleteFTSContent(with: conversationId, from: db) - try PinMessageDAO.shared.deleteAll(conversationId: conversationId, from: db) - db.afterNextTransactionCommit { (_) in - let job = AttachmentCleanUpJob(conversationId: conversationId, - mediaUrls: mediaUrls, - transcriptIds: deletedTranscriptIds) - ConcurrentJobQueue.shared.addJob(job: job) - let change = ConversationChange(conversationId: conversationId, action: .reload) - NotificationCenter.default.post(onMainThread: conversationDidChangeNotification, object: change) - } - } - } - public func getConversation(conversationId: String) -> ConversationItem? { guard !conversationId.isEmpty else { return nil @@ -588,7 +556,7 @@ public final class ConversationDAO: UserDatabaseDAO { extension ConversationDAO { - private func deleteFTSContent(with conversationId: String, from db: GRDB.Database) throws { + public func deleteFTSContent(with conversationId: String, from db: GRDB.Database) throws { let sql = "DELETE FROM \(Message.ftsTableName) WHERE conversation_id MATCH ?" try db.execute(sql: sql, arguments: [uuidTokenString(uuidString: conversationId)]) } diff --git a/MixinServices/MixinServices/Database/User/DAO/MessageDAO.swift b/MixinServices/MixinServices/Database/User/DAO/MessageDAO.swift index a115079e95..3b8ac87d58 100644 --- a/MixinServices/MixinServices/Database/User/DAO/MessageDAO.swift +++ b/MixinServices/MixinServices/Database/User/DAO/MessageDAO.swift @@ -786,15 +786,10 @@ public final class MessageDAO: UserDatabaseDAO { } @discardableResult - public func delete(id: String, conversationId: String, deleteTranscriptChildren: Bool, completion: (() -> Void)? = nil) -> (deleted: Bool, childMessageIds: [String]) { - try! db.writeAndReturnError { db in - let (deleted, ids) = try delete(id: id, conversationId: conversationId, deleteTranscriptChildren: deleteTranscriptChildren, database: db) - if let completion = completion { - db.afterNextTransactionCommit { _ in - completion() - } - } - return (deleted, ids) + public func delete(id: String, conversationId: String, deleteTranscriptChildren: Bool, alongsideTransaction work: ((GRDB.Database) -> Void)) { + db.write { db in + try delete(id: id, conversationId: conversationId, deleteTranscriptChildren: deleteTranscriptChildren, database: db) + work(db) } } diff --git a/MixinServices/MixinServices/Database/Work/WorkDAO.swift b/MixinServices/MixinServices/Database/User/DAO/WorkDAO.swift similarity index 78% rename from MixinServices/MixinServices/Database/Work/WorkDAO.swift rename to MixinServices/MixinServices/Database/User/DAO/WorkDAO.swift index 5f182c9273..9807961c58 100644 --- a/MixinServices/MixinServices/Database/Work/WorkDAO.swift +++ b/MixinServices/MixinServices/Database/User/DAO/WorkDAO.swift @@ -6,20 +6,18 @@ public class WorkDAO { public static let shared = WorkDAO() public var db: Database { - WorkDatabase.current + UserDatabase.current } - public func save(work: PersistedWork, completion: @escaping () -> Void) { + public func save(work: PersistedWork) { db.write { db in try work.save(db) - db.afterNextTransactionCommit { _ in - completion() - } } } public func works(with types: [String]) -> [PersistedWork] { - db.select(where: types.contains(PersistedWork.column(of: .type))) + db.select(where: types.contains(PersistedWork.column(of: .type)), + order: [PersistedWork.column(of: .priority).desc]) } public func delete(id: String) { diff --git a/MixinServices/MixinServices/Database/User/UserDatabase.swift b/MixinServices/MixinServices/Database/User/UserDatabase.swift index 1f6659b9d8..aaaa84c90d 100644 --- a/MixinServices/MixinServices/Database/User/UserDatabase.swift +++ b/MixinServices/MixinServices/Database/User/UserDatabase.swift @@ -493,6 +493,15 @@ public final class UserDatabase: Database { } } + migrator.registerMigration("work") { db in + try db.create(table: "works") { td in + td.column("id", .text).primaryKey().notNull() + td.column("type", .text).notNull() + td.column("context", .blob) + td.column("priority", .integer).notNull() + } + } + return migrator } diff --git a/MixinServices/MixinServices/Database/Work/WorkDatabase.swift b/MixinServices/MixinServices/Database/Work/WorkDatabase.swift deleted file mode 100644 index ce34edac01..0000000000 --- a/MixinServices/MixinServices/Database/Work/WorkDatabase.swift +++ /dev/null @@ -1,44 +0,0 @@ -import GRDB - -public final class WorkDatabase: Database { - - public private(set) static var current: WorkDatabase! = try! WorkDatabase(url: AppGroupContainer.workDatabaseURL) - - public override class var config: Configuration { - var config = super.config - config.label = "Work" - return config - } - - public override var needsMigration: Bool { - try! pool.read({ (db) -> Bool in - let migrationsCompleted = try migrator.hasCompletedMigrations(db) - return !migrationsCompleted - }) - } - - private var migrator: DatabaseMigrator { - var migrator = DatabaseMigrator() - - migrator.registerMigration("create_table") { db in - try db.create(table: "works") { td in - td.column("id", .text).primaryKey().notNull() - td.column("type", .text).notNull() - td.column("context", .blob) - td.column("priority", .integer).notNull() - } - } - - return migrator - } - - public static func reloadCurrent() { - current = try! WorkDatabase(url: AppGroupContainer.workDatabaseURL) - current.migrate() - } - - private func migrate() { - try! migrator.migrate(pool) - } - -} diff --git a/MixinServices/MixinServices/Foundation/File Management/AppGroupContainer.swift b/MixinServices/MixinServices/Foundation/File Management/AppGroupContainer.swift index ef372cc2c5..d667fd4ea1 100644 --- a/MixinServices/MixinServices/Foundation/File Management/AppGroupContainer.swift +++ b/MixinServices/MixinServices/Foundation/File Management/AppGroupContainer.swift @@ -45,10 +45,6 @@ public enum AppGroupContainer { accountUrl.appendingPathComponent("task.db", isDirectory: false) } - public static var workDatabaseURL: URL { - accountUrl.appendingPathComponent("work.db", isDirectory: false) - } - @available(iOSApplicationExtension, unavailable) public static func migrateIfNeeded() { guard !AppGroupUserDefaults.isDocumentsMigrated else { diff --git a/MixinServices/MixinServices/Foundation/User Defaults/AppGroupUserDefaults+User.swift b/MixinServices/MixinServices/Foundation/User Defaults/AppGroupUserDefaults+User.swift index dc18a4d1d2..273d97bf84 100644 --- a/MixinServices/MixinServices/Foundation/User Defaults/AppGroupUserDefaults+User.swift +++ b/MixinServices/MixinServices/Foundation/User Defaults/AppGroupUserDefaults+User.swift @@ -84,7 +84,6 @@ extension AppGroupUserDefaults { || TaskDatabase.current.needsMigration || SignalDatabase.current.needsMigration || UserDatabase.current.needsMigration - || WorkDatabase.current.needsMigration } @Default(namespace: .user, key: Key.localVersion, defaultValue: uninitializedVersion) diff --git a/MixinServices/MixinServices/Services/Work/DeleteConversationAttachmentWork.swift b/MixinServices/MixinServices/Services/Work/DeleteConversationAttachmentWork.swift new file mode 100644 index 0000000000..56a13a39ec --- /dev/null +++ b/MixinServices/MixinServices/Services/Work/DeleteConversationAttachmentWork.swift @@ -0,0 +1,85 @@ +import Foundation +import GRDB + +public final class DeleteConversationAttachmentWork: Work { + + public struct Attachment: Codable, TableRecord, FetchableRecord { + + public static let databaseTableName = Message.databaseTableName + + let url: String + let category: String + + public init(row: Row) { + url = row["media_url"] + category = row["category"] + } + + } + + private let batchLimit = 50 + + private var attachments: [Attachment] + private var transcriptMessageIds: [String] + + public init(id: String = UUID().uuidString.lowercased(), attachments: [Attachment], transcriptMessageIds: [String]) { + self.attachments = attachments + self.transcriptMessageIds = transcriptMessageIds + super.init(id: id, state: .ready) + } + + public override func main() throws { + Logger.general.debug(category: "DeleteConversationAttachmentWork", message: "[\(id)] Delete \(attachments.count) attachments, \(transcriptMessageIds.count) transcripts") + if !attachments.isEmpty { + repeat { + let items = attachments.suffix(batchLimit) + for item in items { + AttachmentContainer.removeMediaFiles(mediaUrl: item.url, category: item.category) + } + attachments.removeLast(items.count) + updatePersistedContext() + Logger.general.debug(category: "DeleteConversationAttachmentWork", message: "[\(id)] Updated with \(attachments.count) attachments, \(transcriptMessageIds.count) transcripts") + } while !attachments.isEmpty + } + if !transcriptMessageIds.isEmpty { + repeat { + let ids = transcriptMessageIds.suffix(batchLimit) + ids.forEach(AttachmentContainer.removeAll(transcriptId:)) + transcriptMessageIds.removeLast(ids.count) + updatePersistedContext() + Logger.general.debug(category: "DeleteConversationAttachmentWork", message: "[\(id)] Updated with \(attachments.count) attachments, \(transcriptMessageIds.count) transcripts") + } while !transcriptMessageIds.isEmpty + } + } + +} + +extension DeleteConversationAttachmentWork: PersistableWork { + + private struct Context: Codable { + let attachments: [Attachment] + let transcriptMessageIds: [String] + } + + public static let typeIdentifier = "delete_conversation_attachment" + + public var context: Data? { + let context = Context(attachments: attachments, transcriptMessageIds: transcriptMessageIds) + return try? JSONEncoder.default.encode(context) + } + + public var priority: PersistedWork.Priority { + .low + } + + public convenience init(id: String, context: Data?) throws { + guard + let context = context, + let context = try? JSONDecoder.default.decode(Context.self, from: context) + else { + throw PersistableWorkError.invalidContext + } + self.init(id: id, attachments: context.attachments, transcriptMessageIds: context.transcriptMessageIds) + } + +} diff --git a/MixinServices/MixinServices/Services/Work/DeleteAttachmentMessageWork.swift b/MixinServices/MixinServices/Services/Work/DeleteMessageAttachmentWork.swift similarity index 59% rename from MixinServices/MixinServices/Services/Work/DeleteAttachmentMessageWork.swift rename to MixinServices/MixinServices/Services/Work/DeleteMessageAttachmentWork.swift index 140fdb08e4..5cda535f89 100644 --- a/MixinServices/MixinServices/Services/Work/DeleteAttachmentMessageWork.swift +++ b/MixinServices/MixinServices/Services/Work/DeleteMessageAttachmentWork.swift @@ -17,31 +17,15 @@ extension MessageItem: DeletableMessage { } -/* - Straight execute - ┌───────────────┐ ┌───────────┐Completed┌────────────────┐ - │Init(preparing)├─►│Persistence├────────►│Delete DB Record│ - └───────────────┘ └───────────┘ └────┬───────────┘ - │ - ┌───────────┐Execute┌─────┐ │Completed - │Delete file│◄──────┤Ready│◄──┘ - └───────────┘ └─────┘ - - Awake from persistence - ┌────────────┐ ┌────────────────┐Completed┌───────────┐ - │Awake(ready)├─►│Delete DB Record├────────►│Delete file│ - └────────────┘ └────────────────┘ └───────────┘ - */ - -public final class DeleteAttachmentMessageWork: Work { +public final class DeleteMessageAttachmentWork: Work { private enum Attachment: Codable { case media(category: String, filename: String) case transcript } - public static let willDeleteNotification = Notification.Name("one.mixin.services.DeleteAttachmentMessageWork.willDelete") - public static let messageIdUserInfoKey = "msg" + public static let willDeleteNotification = Notification.Name("one.mixin.services.DeleteMessageAttachmentWork.willDelete") + public static let messageIdUserInfoKey = "mid" public static let capableMessageCategories: Set = [ MessageCategory.SIGNAL_IMAGE.rawValue, MessageCategory.PLAIN_IMAGE.rawValue, MessageCategory.ENCRYPTED_IMAGE.rawValue, MessageCategory.SIGNAL_VIDEO.rawValue, MessageCategory.PLAIN_VIDEO.rawValue, MessageCategory.ENCRYPTED_VIDEO.rawValue, @@ -54,9 +38,6 @@ public final class DeleteAttachmentMessageWork: Work { private let conversationId: String private let attachment: Attachment? - @Synchronized(value: false) - private var hasDatabaseRecordDeleted: Bool - public convenience init(message: DeletableMessage) { let attachment: Attachment? if MessageCategory.allMediaCategoriesString.contains(message.category), let filename = message.mediaUrl { @@ -66,31 +47,20 @@ public final class DeleteAttachmentMessageWork: Work { } else { attachment = nil } - self.init(messageId: message.messageId, conversationId: message.conversationId, attachment: attachment, state: .preparing) + self.init(messageId: message.messageId, conversationId: message.conversationId, attachment: attachment) } - private init(messageId: String, conversationId: String, attachment: Attachment?, state: State) { + private init(messageId: String, conversationId: String, attachment: Attachment?) { self.messageId = messageId self.conversationId = conversationId self.attachment = attachment - super.init(id: "delete-message-\(messageId)", state: state) - } - - public override func start() { - state = .executing - if hasDatabaseRecordDeleted { - deleteFile() - state = .finished(.success) - } else { - MessageDAO.shared.delete(id: messageId, conversationId: conversationId, deleteTranscriptChildren: false) { - Logger.general.debug(category: "DeleteAttachmentMessageWork", message: "\(self.messageId) Message deleted from database") - self.deleteFile() - self.state = .finished(.success) - } - } + super.init(id: "delete-message-\(messageId)", state: .ready) } - private func deleteFile() { + public override func main() throws { + NotificationCenter.default.post(onMainThread: Self.willDeleteNotification, + object: self, + userInfo: [Self.messageIdUserInfoKey: messageId]) switch attachment { case let .media(category, filename): AttachmentContainer.removeMediaFiles(mediaUrl: filename, category: category) @@ -112,7 +82,7 @@ public final class DeleteAttachmentMessageWork: Work { } -extension DeleteAttachmentMessageWork: PersistableWork { +extension DeleteMessageAttachmentWork: PersistableWork { private struct Context: Codable { let messageId: String @@ -120,7 +90,7 @@ extension DeleteAttachmentMessageWork: PersistableWork { let attachment: Attachment? } - public static let typeIdentifier: String = "delete_message" + public static let typeIdentifier: String = "delete_message_attachment" public var context: Data? { let context = Context(messageId: messageId, @@ -142,19 +112,7 @@ extension DeleteAttachmentMessageWork: PersistableWork { } self.init(messageId: context.messageId, conversationId: context.conversationId, - attachment: context.attachment, - state: .ready) - } - - public func persistenceDidComplete() { - NotificationCenter.default.post(onMainThread: Self.willDeleteNotification, - object: self, - userInfo: [Self.messageIdUserInfoKey: messageId]) - MessageDAO.shared.delete(id: messageId, conversationId: conversationId, deleteTranscriptChildren: false) { - self.state = .ready - } - hasDatabaseRecordDeleted = true - Logger.general.debug(category: "DeleteAttachmentMessageWork", message: "\(messageId) Message deleted from database") + attachment: context.attachment) } } diff --git a/MixinServices/MixinServices/Services/Work/PersistableWork.swift b/MixinServices/MixinServices/Services/Work/PersistableWork.swift index 1a6caeb1ea..5edd1a79c0 100644 --- a/MixinServices/MixinServices/Services/Work/PersistableWork.swift +++ b/MixinServices/MixinServices/Services/Work/PersistableWork.swift @@ -14,7 +14,6 @@ public protocol PersistableWork: Work { init(id: String, context: Data?) throws func updatePersistedContext() - func persistenceDidComplete() } @@ -24,8 +23,4 @@ extension PersistableWork { WorkDAO.shared.update(context: context, forWorkWith: id) } - public func persistenceDidComplete() { - - } - } diff --git a/MixinServices/MixinServices/Services/Work/WorkManager.swift b/MixinServices/MixinServices/Services/Work/WorkManager.swift index ea1136fd21..3c6af8f75c 100644 --- a/MixinServices/MixinServices/Services/Work/WorkManager.swift +++ b/MixinServices/MixinServices/Services/Work/WorkManager.swift @@ -1,7 +1,13 @@ import Foundation +import GRDB public class WorkManager { + private enum Persistence { + case standalone + case alongsideTransaction(GRDB.Database) + } + public static let general = WorkManager(label: "General", maxConcurrentWorkCount: 6) let maxConcurrentWorkCount: Int @@ -41,7 +47,7 @@ public class WorkManager { } do { let work = try Work.init(id: persisted.id, context: persisted.context) - self.addWork(work, persistIfAvailable: false) + self.addWork(work, persistence: .none) } catch { Logger.general.error(category: "WorkManager", message: "[\(self.label)] Failed to init \(persisted)") } @@ -51,7 +57,11 @@ public class WorkManager { } public func addWork(_ work: Work) { - addWork(work, persistIfAvailable: true) + addWork(work, persistence: .standalone) + } + + public func addPersistableWork(_ work: PersistableWork, alongsideTransactionWith database: GRDB.Database) { + addWork(work, persistence: .alongsideTransaction(database)) } public func cancelAllWorks() { @@ -68,7 +78,7 @@ public class WorkManager { work.cancel() } - private func addWork(_ work: Work, persistIfAvailable: Bool) { + private func addWork(_ work: Work, persistence: Persistence?) { guard work.setStateMonitor(self) else { assertionFailure("Adding work to multiple manager is not supported") return @@ -82,13 +92,21 @@ public class WorkManager { Logger.general.warn(category: "WorkManager", message: "[\(label)] Add a duplicated work: \(work)") return } - if persistIfAvailable, let work = work as? PersistableWork { + if let work = work as? PersistableWork, let persistence = persistence { let persisted = PersistedWork(id: work.id, type: type(of: work).typeIdentifier, context: work.context, priority: work.priority) - WorkDAO.shared.save(work: persisted, - completion: work.persistenceDidComplete) + switch persistence { + case .standalone: + WorkDAO.shared.save(work: persisted) + case .alongsideTransaction(let database): + do { + try persisted.save(database) + } catch { + Logger.general.error(category: "WorkManager", message: "[\(label)] Failed to save: \(work), error: \(error)") + } + } } if work.isReady, executingWorks.count < maxConcurrentWorkCount { Logger.general.debug(category: "WorkManager", message: "[\(label)] Start \(work) because of adding to queue")