diff --git a/Account/Sources/Account/AccountDelegates/CloudKitAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/CloudKitAccountDelegate.swift index 969730589..a7578b3ee 100644 --- a/Account/Sources/Account/AccountDelegates/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/CloudKitAccountDelegate.swift @@ -21,6 +21,7 @@ import CloudKitExtras import CommonErrors import FeedFinder import LocalAccount +import CloudKitSync enum CloudKitAccountDelegateError: LocalizedError { case invalidParameter @@ -204,13 +205,17 @@ enum CloudKitAccountDelegateError: LocalizedError { func renameFeed(for account: Account, with feed: Feed, to name: String) async throws { - refreshProgress.addToNumberOfTasksAndRemaining(1) + guard let feedExternalID = feed.externalID else { + throw LocalAccountDelegateError.invalidParameter + } + + refreshProgress.addTask() defer { refreshProgress.completeTask() } let editedName = name.isEmpty ? nil : name - + do { - try await accountZone.renameFeed(feed, editedName: editedName) + try await accountZone.renameFeed(externalID: feedExternalID, editedName: editedName) feed.editedName = name } catch { processAccountError(account, error) @@ -240,11 +245,15 @@ enum CloudKitAccountDelegateError: LocalizedError { func moveFeed(for account: Account, with feed: Feed, from sourceContainer: Container, to destinationContainer: Container) async throws { + guard let feedExternalID = feed.externalID, let sourceContainerExternalID = sourceContainer.externalID, let destinationContainerExternalID = destinationContainer.externalID else { + throw LocalAccountDelegateError.invalidParameter + } + refreshProgress.addToNumberOfTasksAndRemaining(1) defer { refreshProgress.completeTask() } do { - try await accountZone.moveFeed(feed, from: sourceContainer, to: destinationContainer) + try await accountZone.moveFeed(externalID: feedExternalID, from: sourceContainerExternalID, to: destinationContainerExternalID) sourceContainer.removeFeed(feed) destinationContainer.addFeed(feed) } catch { @@ -255,11 +264,15 @@ enum CloudKitAccountDelegateError: LocalizedError { func addFeed(for account: Account, with feed: Feed, to container: any Container) async throws { + guard let feedExternalID = feed.externalID, let containerExternalID = container.externalID else { + throw LocalAccountDelegateError.invalidParameter + } + refreshProgress.addToNumberOfTasksAndRemaining(1) defer { refreshProgress.completeTask() } do { - try await accountZone.addFeed(feed, to: container) + try await accountZone.addFeed(externalID: feedExternalID, to: containerExternalID) container.addFeed(feed) } catch { processAccountError(account, error) @@ -296,11 +309,15 @@ enum CloudKitAccountDelegateError: LocalizedError { func renameFolder(for account: Account, with folder: Folder, to name: String) async throws { + guard let folderExternalID = folder.externalID else { + throw CloudKitAccountDelegateError.invalidParameter + } + refreshProgress.addToNumberOfTasksAndRemaining(1) defer { refreshProgress.completeTask() } do { - try await accountZone.renameFolder(folder, to: name) + try await accountZone.renameFolder(externalID: folderExternalID, to: name) folder.name = name } catch { processAccountError(account, error) @@ -310,11 +327,15 @@ enum CloudKitAccountDelegateError: LocalizedError { func removeFolder(for account: Account, with folder: Folder) async throws { + guard let folderExternalID = folder.externalID else { + throw CloudKitAccountDelegateError.invalidParameter + } + refreshProgress.addToNumberOfTasksAndRemaining(1) defer { refreshProgress.completeTask() } do { - let feedExternalIDs = try await accountZone.findFeedExternalIDs(for: folder) + let feedExternalIDs = try await accountZone.findFeedExternalIDs(for: folderExternalID) let feeds = feedExternalIDs.compactMap { account.existingFeed(withExternalID: $0) } var errorOccurred = false @@ -334,7 +355,7 @@ enum CloudKitAccountDelegateError: LocalizedError { throw CloudKitAccountDelegateError.unknown } - try await accountZone.removeFolder(folder) + try await accountZone.removeFolder(externalID: folderExternalID) account.removeFolder(folder: folder) } catch { @@ -519,6 +540,10 @@ private extension CloudKitAccountDelegate { func createRSSFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool) async throws -> Feed { + guard let containerExternalID = container.externalID else { + throw CloudKitAccountDelegateError.invalidParameter + } + func addDeadFeed() async throws -> Feed { let feed = account.createFeed(with: editedName, @@ -531,7 +556,7 @@ private extension CloudKitAccountDelegate { let externalID = try await accountZone.createFeed(url: url.absoluteString, name: editedName, editedName: nil, homePageURL: nil, - container: container) + containerExternalID: containerExternalID) feed.externalID = externalID return feed } catch { @@ -579,11 +604,11 @@ private extension CloudKitAccountDelegate { do { try await account.update(feed: feed, with: parsedFeed) - let externalID = try await self.accountZone.createFeed(url: bestFeedSpecifier.urlString, - name: parsedFeed.title, - editedName: editedName, - homePageURL: parsedFeed.homePageURL, - container: container) + let externalID = try await accountZone.createFeed(url: bestFeedSpecifier.urlString, + name: parsedFeed.title, + editedName: editedName, + homePageURL: parsedFeed.homePageURL, + containerExternalID: containerExternalID) feed.externalID = externalID sendNewArticlesToTheCloud(account, feed) @@ -687,15 +712,15 @@ private extension CloudKitAccountDelegate { func removeFeedFromCloud(for account: Account, with feed: Feed, from container: Container) async throws { + guard let feedExternalID = feed.externalID, let containerExternalID = container.externalID else { + return + } + refreshProgress.addToNumberOfTasksAndRemaining(1) defer { refreshProgress.completeTask() } do { - try await accountZone.removeFeed(feed, from: container) - - guard let feedExternalID = feed.externalID else { - return - } + try await accountZone.removeFeed(externalID: feedExternalID, from: containerExternalID) try await articlesZone.deleteArticles(feedExternalID) feed.dropConditionalGetInfo() diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift index 878d8392f..4797a3b57 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift @@ -12,6 +12,7 @@ import Web import CloudKit import Articles import CloudKitExtras +import CloudKitSync @MainActor final class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { diff --git a/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift b/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift index 3109f0e0b..abeec868b 100644 --- a/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift @@ -9,6 +9,7 @@ import Foundation import os.log import Core +import CloudKitSync class CloudKitRemoteNotificationOperation: MainThreadOperation { diff --git a/CloudKitSync/Sources/CloudKitSync/CloudKitAccountZone.swift b/CloudKitSync/Sources/CloudKitSync/CloudKitAccountZone.swift index 95b3ce8c7..7a9d4fc7a 100644 --- a/CloudKitSync/Sources/CloudKitSync/CloudKitAccountZone.swift +++ b/CloudKitSync/Sources/CloudKitSync/CloudKitAccountZone.swift @@ -22,43 +22,43 @@ enum CloudKitAccountZoneError: LocalizedError { } } -@MainActor final class CloudKitAccountZone: CloudKitZone { +@MainActor public final class CloudKitAccountZone: CloudKitZone { - let zoneID: CKRecordZone.ID + public let zoneID: CKRecordZone.ID - let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") + public let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") - weak var container: CKContainer? - weak var database: CKDatabase? - var delegate: CloudKitZoneDelegate? - - struct CloudKitFeed { - static let recordType = "AccountWebFeed" - struct Fields { - static let url = "url" - static let name = "name" - static let editedName = "editedName" - static let homePageURL = "homePageURL" - static let containerExternalIDs = "containerExternalIDs" + public weak var container: CKContainer? + public weak var database: CKDatabase? + public var delegate: CloudKitZoneDelegate? + + public struct CloudKitFeed { + public static let recordType = "AccountWebFeed" + public struct Fields { + public static let url = "url" + public static let name = "name" + public static let editedName = "editedName" + public static let homePageURL = "homePageURL" + public static let containerExternalIDs = "containerExternalIDs" } } - struct CloudKitContainer { - static let recordType = "AccountContainer" - struct Fields { - static let isAccount = "isAccount" - static let name = "name" + public struct CloudKitContainer { + public static let recordType = "AccountContainer" + public struct Fields { + public static let isAccount = "isAccount" + public static let name = "name" } } - init(container: CKContainer) { + public init(container: CKContainer) { self.container = container self.database = container.privateCloudDatabase self.zoneID = CKRecordZone.ID(zoneName: "Account", ownerName: CKCurrentUserDefaultName) migrateChangeToken() } - func importOPML(rootExternalID: String, items: [RSOPMLItem]) async throws { + public func importOPML(rootExternalID: String, items: [RSOPMLItem]) async throws { var records = [CKRecord]() var feedRecords = [String: CKRecord]() @@ -94,7 +94,7 @@ enum CloudKitAccountZoneError: LocalizedError { } /// Persist a web feed record to iCloud and return the external key - func createFeed(url: String, name: String?, editedName: String?, homePageURL: String?, containerExternalID: String) async throws -> String { + public func createFeed(url: String, name: String?, editedName: String?, homePageURL: String?, containerExternalID: String) async throws -> String { let recordID = CKRecord.ID(recordName: url.md5String, zoneID: zoneID) let record = CKRecord(recordType: CloudKitFeed.recordType, recordID: recordID) @@ -114,7 +114,7 @@ enum CloudKitAccountZoneError: LocalizedError { } /// Rename the given web feed - func renameFeed(externalID: String, editedName: String?) async throws { + public func renameFeed(externalID: String, editedName: String?) async throws { let recordID = CKRecord.ID(recordName: externalID, zoneID: zoneID) let record = CKRecord(recordType: CloudKitFeed.recordType, recordID: recordID) @@ -125,7 +125,7 @@ enum CloudKitAccountZoneError: LocalizedError { /// Removes a web feed from a container and optionally deletes it, returning true if deleted @discardableResult - func removeFeed(externalID: String, from containerExternalID: String) async throws -> Bool { + public func removeFeed(externalID: String, from containerExternalID: String) async throws -> Bool { do { let record = try await fetch(externalID: externalID) @@ -155,7 +155,7 @@ enum CloudKitAccountZoneError: LocalizedError { } } - func moveFeed(externalID: String, from sourceContainerExternalID: String, to destinationContainerExternalID: String) async throws { + public func moveFeed(externalID: String, from sourceContainerExternalID: String, to destinationContainerExternalID: String) async throws { let record = try await fetch(externalID: externalID) @@ -168,7 +168,7 @@ enum CloudKitAccountZoneError: LocalizedError { } } - func addFeed(externalID: String, to containerExternalID: String) async throws { + public func addFeed(externalID: String, to containerExternalID: String) async throws { let record = try await fetch(externalID: externalID) @@ -180,7 +180,7 @@ enum CloudKitAccountZoneError: LocalizedError { } } - func findFeedExternalIDs(for folderExternalID: String) async throws -> [String] { + public func findFeedExternalIDs(for folderExternalID: String) async throws -> [String] { let predicate = NSPredicate(format: "containerExternalIDs CONTAINS %@", folderExternalID) let ckQuery = CKQuery(recordType: CloudKitFeed.recordType, predicate: predicate) @@ -191,7 +191,7 @@ enum CloudKitAccountZoneError: LocalizedError { return feedExternalIDs } - func findOrCreateAccount() async throws -> String { + public func findOrCreateAccount() async throws -> String { guard let database else { throw CloudKitAccountZoneError.unknown } @@ -224,12 +224,12 @@ enum CloudKitAccountZoneError: LocalizedError { } } - func createFolder(name: String) async throws -> String { + public func createFolder(name: String) async throws -> String { return try await createContainer(name: name, isAccount: false) } - func renameFolder(externalID: String, to name: String) async throws { + public func renameFolder(externalID: String, to name: String) async throws { let recordID = CKRecord.ID(recordName: externalID, zoneID: zoneID) let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: recordID) @@ -238,7 +238,7 @@ enum CloudKitAccountZoneError: LocalizedError { try await save(record) } - func removeFolder(externalID: String) async throws { + public func removeFolder(externalID: String) async throws { try await delete(externalID: externalID) } diff --git a/Mac/Preferences/Accounts/AddAccountsView.swift b/Mac/Preferences/Accounts/AddAccountsView.swift index 18db13ec5..cf67e81f6 100644 --- a/Mac/Preferences/Accounts/AddAccountsView.swift +++ b/Mac/Preferences/Accounts/AddAccountsView.swift @@ -129,7 +129,7 @@ struct AddAccountsView: View { .padding() } - var localAccount: some View { + @MainActor var localAccount: some View { VStack(alignment: .leading) { Text("Local") .font(.headline) @@ -192,7 +192,7 @@ struct AddAccountsView: View { } @ViewBuilder - var webAccounts: some View { + @MainActor var webAccounts: some View { VStack(alignment: .leading) { Text("Web") .font(.headline) @@ -230,7 +230,7 @@ struct AddAccountsView: View { } } - var selfhostedAccounts: some View { + @MainActor var selfhostedAccounts: some View { VStack(alignment: .leading) { Text("Self-hosted") .font(.headline)