diff --git a/Account/Sources/Account/AccountError.swift b/Account/Sources/Account/AccountError.swift index af02f3d12..8afc56c94 100644 --- a/Account/Sources/Account/AccountError.swift +++ b/Account/Sources/Account/AccountError.swift @@ -14,7 +14,7 @@ typealias AccountError = CommonError // Temporary, for compatibility with existi public extension CommonError { - @MainActor public var account: Account? { + @MainActor var account: Account? { if case .wrappedError(_, let accountID, _) = self { return AccountManager.shared.existingAccount(with: accountID) } else { @@ -22,7 +22,7 @@ public extension CommonError { } } - @MainActor public static func wrappedError(error: Error, account: Account) -> AccountError { + @MainActor static func wrappedError(error: Error, account: Account) -> CommonError { wrappedError(error: error, accountID: account.accountID, accountName: account.nameForDisplay) } } diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index be319a563..e4f5c9bad 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -47,20 +47,19 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } } - weak var accountMetadata: AccountMetadata? { - didSet { - caller.accountMetadata = accountMetadata - } - } + weak var accountMetadata: AccountMetadata? var refreshProgress = DownloadProgress(numberOfTasks: 0) init(dataFolder: String, transport: Transport?, variant: ReaderAPIVariant, secretsProvider: SecretsProvider) { + let databasePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3") - database = SyncDatabase(databasePath: databasePath) + self.database = SyncDatabase(databasePath: databasePath) + + self.variant = variant if transport != nil { - caller = ReaderAPICaller(transport: transport!, secretsProvider: secretsProvider) + self.caller = ReaderAPICaller(transport: transport!, secretsProvider: secretsProvider) } else { let sessionConfiguration = URLSessionConfiguration.default sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData @@ -75,11 +74,11 @@ final class ReaderAPIAccountDelegate: AccountDelegate { sessionConfiguration.httpAdditionalHeaders = userAgentHeaders } - caller = ReaderAPICaller(transport: URLSession(configuration: sessionConfiguration), secretsProvider: secretsProvider) + self.caller = ReaderAPICaller(transport: URLSession(configuration: sessionConfiguration), secretsProvider: secretsProvider) } + caller.delegate = self caller.variant = variant - self.variant = variant } func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any]) async { @@ -102,7 +101,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { try? await refreshArticleStatus(for: account) refreshProgress.completeTask() - try? await refreshMissingArticles(account) + await refreshMissingArticles(account) refreshProgress.clear() } catch { @@ -177,7 +176,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { do { let articleIDs = try await caller.retrieveItemIDs(type: .starred) - try await syncArticleStarredState(account: account, articleIDs: articleIDs) + await syncArticleStarredState(account: account, articleIDs: articleIDs) } catch { errorOccurred = true os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription) @@ -185,7 +184,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { os_log(.debug, log: self.log, "Done refreshing article statuses.") if errorOccurred { - throw ReaderAPIAccountDelegateError.unknown + throw ReaderAPIError.unknown } } @@ -195,7 +194,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { func createFolder(for account: Account, name: String) async throws -> Folder { guard let folder = account.ensureFolder(with: name) else { - throw ReaderAPIAccountDelegateError.invalidParameter + throw ReaderAPIError.invalidParameter } return folder } @@ -260,15 +259,18 @@ final class ReaderAPIAccountDelegate: AccountDelegate { if self.variant == .theOldReader { account.removeFolder(folder: folder) } else { - try await caller.deleteTag(folder: folder) + if let folderExternalID = folder.externalID { + try await caller.deleteTag(folderExternalID: folderExternalID) + } account.removeFolder(folder: folder) } } + @discardableResult func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool) async throws -> Feed { guard let url = URL(string: url) else { - throw ReaderAPIAccountDelegateError.invalidParameter + throw ReaderAPIError.invalidParameter } refreshProgress.addToNumberOfTasksAndRemaining(2) @@ -284,7 +286,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { throw AccountError.createErrorNotFound } - let subResult = try await caller.createSubscription(url: bestFeedSpecifier.urlString, name: name, folder: container as? Folder) + let subResult = try await caller.createSubscription(url: bestFeedSpecifier.urlString, name: name) refreshProgress.completeTask() switch subResult { @@ -305,7 +307,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { // This error should never happen guard let subscriptionID = feed.externalID else { assert(feed.externalID != nil) - throw ReaderAPIAccountDelegateError.invalidParameter + throw ReaderAPIError.invalidParameter } refreshProgress.addToNumberOfTasksAndRemaining(1) @@ -325,7 +327,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { guard let subscriptionID = feed.externalID else { assert(feed.externalID != nil) - throw ReaderAPIAccountDelegateError.invalidParameter + throw ReaderAPIError.invalidParameter } refreshProgress.addToNumberOfTasksAndRemaining(1) @@ -360,7 +362,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { let sourceTag = (sourceContainer as? Folder)?.name, let destinationTag = (destinationContainer as? Folder)?.name else { - throw ReaderAPIAccountDelegateError.invalidParameter + throw ReaderAPIError.invalidParameter } refreshProgress.addToNumberOfTasksAndRemaining(1) @@ -475,7 +477,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { caller.cancelAll() } - /// Suspend the SQLLite databases + /// Suspend the SQLite databases func suspendDatabase() { Task { @@ -895,3 +897,28 @@ private extension ReaderAPIAccountDelegate { } } } + +extension ReaderAPIAccountDelegate: ReaderAPICallerDelegate { + + var endpointURL: URL? { + accountMetadata?.endpointURL + } + + var lastArticleFetchStartTime: Date? { + get { + accountMetadata?.lastArticleFetchStartTime + } + set { + accountMetadata?.lastArticleFetchStartTime = newValue + } + } + + var lastArticleFetchEndTime: Date? { + get { + accountMetadata?.lastArticleFetchEndTime + } + set { + accountMetadata?.lastArticleFetchEndTime = newValue + } + } +} diff --git a/Mac/Browser.swift b/Mac/Browser.swift index e3b4bac6a..bfdd2caaf 100644 --- a/Mac/Browser.swift +++ b/Mac/Browser.swift @@ -15,7 +15,7 @@ struct Browser { /// /// The user-assigned default browser, or `nil` if none was assigned /// (i.e., the system default should be used). - static var defaultBrowser: MacWebBrowser? { + @MainActor static var defaultBrowser: MacWebBrowser? { if let bundleID = AppDefaults.shared.defaultBrowserID, let browser = MacWebBrowser(bundleIdentifier: bundleID) { return browser } diff --git a/Mac/MainWindow/AddFeed/AddFeedController.swift b/Mac/MainWindow/AddFeed/AddFeedController.swift index 72e415cf8..603e97eeb 100644 --- a/Mac/MainWindow/AddFeed/AddFeedController.swift +++ b/Mac/MainWindow/AddFeed/AddFeedController.swift @@ -12,6 +12,7 @@ import Tree import Articles import Account import Parser +import CommonErrors // Run add-feed sheet. // If it returns with URL and optional name, @@ -69,10 +70,10 @@ import Parser let feed = try await account.createFeed(url: url.absoluteString, name: title, container: container, validateFeed: true) NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed]) - } catch AccountError.createErrorAlreadySubscribed { + } catch CommonError.createErrorAlreadySubscribed { self.showAlreadySubscribedError(url.absoluteString) - } catch AccountError.createErrorNotFound { + } catch CommonError.createErrorNotFound { self.showNoFeedsErrorMessage() } catch { diff --git a/ReaderAPI/Sources/ReaderAPI/ReaderAPICaller.swift b/ReaderAPI/Sources/ReaderAPI/ReaderAPICaller.swift index 06c97f2c3..3742c747a 100644 --- a/ReaderAPI/Sources/ReaderAPI/ReaderAPICaller.swift +++ b/ReaderAPI/Sources/ReaderAPI/ReaderAPICaller.swift @@ -13,10 +13,10 @@ import CommonErrors public protocol ReaderAPICallerDelegate: AnyObject { - var endpointURL: URL? { get } + @MainActor var endpointURL: URL? { get } - var lastArticleFetchStartTime: Date? { get set } - var lastArticleFetchEndTime: Date? { get set } + @MainActor var lastArticleFetchStartTime: Date? { get set } + @MainActor var lastArticleFetchEndTime: Date? { get set } } public enum CreateReaderAPISubscriptionResult: Sendable { @@ -25,15 +25,26 @@ public enum CreateReaderAPISubscriptionResult: Sendable { case notFound } -@MainActor final class ReaderAPICaller { +@MainActor public final class ReaderAPICaller { - enum ItemIDType { + public enum ItemIDType { case unread case starred case allForAccount case allForFeed } + public weak var delegate: ReaderAPICallerDelegate? + + public var variant: ReaderAPIVariant = .generic + public var credentials: Credentials? + + public var server: String? { + get { + return apiBaseURL?.host + } + } + private enum ReaderState: String { case read = "user/-/state/com.google/read" case starred = "user/-/state/com.google/starred" @@ -63,17 +74,6 @@ public enum CreateReaderAPISubscriptionResult: Sendable { private var accessToken: String? - weak var delegate: ReaderAPICallerDelegate? - - var variant: ReaderAPIVariant = .generic - var credentials: Credentials? - - var server: String? { - get { - return apiBaseURL?.host - } - } - private var apiBaseURL: URL? { get { switch variant { @@ -85,7 +85,8 @@ public enum CreateReaderAPISubscriptionResult: Sendable { } } - init(transport: Transport, secretsProvider: SecretsProvider) { + /// The delegate should be set in a subsequent call. + public init(transport: Transport, secretsProvider: SecretsProvider) { self.transport = transport self.secretsProvider = secretsProvider @@ -96,11 +97,11 @@ public enum CreateReaderAPISubscriptionResult: Sendable { self.uriComponentAllowed = urlHostAllowed } - func cancelAll() { + public func cancelAll() { transport.cancelAll() } - func validateCredentials(endpoint: URL) async throws -> Credentials? { + public func validateCredentials(endpoint: URL) async throws -> Credentials? { guard let credentials else { throw CredentialsError.incompleteCredentials @@ -173,7 +174,7 @@ public enum CreateReaderAPISubscriptionResult: Sendable { return accessToken } - func retrieveTags() async throws -> [ReaderAPITag]? { + public func retrieveTags() async throws -> [ReaderAPITag]? { guard let baseURL = apiBaseURL else { throw CredentialsError.incompleteCredentials @@ -198,7 +199,7 @@ public enum CreateReaderAPISubscriptionResult: Sendable { return wrapper?.tags } - func renameTag(oldName: String, newName: String) async throws { + public func renameTag(oldName: String, newName: String) async throws { guard let baseURL = apiBaseURL else { throw CredentialsError.incompleteCredentials @@ -223,7 +224,7 @@ public enum CreateReaderAPISubscriptionResult: Sendable { } - func deleteTag(folderExternalID: String) async throws { + public func deleteTag(folderExternalID: String) async throws { guard let baseURL = apiBaseURL else { throw CredentialsError.incompleteCredentials @@ -241,7 +242,7 @@ public enum CreateReaderAPISubscriptionResult: Sendable { try await self.transport.send(request: request, method: HTTPMethod.post, payload: postData!) } - func retrieveSubscriptions() async throws -> [ReaderAPISubscription]? { + public func retrieveSubscriptions() async throws -> [ReaderAPISubscription]? { guard let baseURL = apiBaseURL else { throw CredentialsError.incompleteCredentials @@ -262,7 +263,7 @@ public enum CreateReaderAPISubscriptionResult: Sendable { return container?.subscriptions } - func createSubscription(url: String, name: String?) async throws -> CreateReaderAPISubscriptionResult { + public func createSubscription(url: String, name: String?) async throws -> CreateReaderAPISubscriptionResult { guard let baseURL = apiBaseURL else { throw CredentialsError.incompleteCredentials @@ -305,12 +306,12 @@ public enum CreateReaderAPISubscriptionResult: Sendable { return .created(subscription) } - func renameSubscription(subscriptionID: String, newName: String) async throws { + public func renameSubscription(subscriptionID: String, newName: String) async throws { try await changeSubscription(subscriptionID: subscriptionID, title: newName) } - func deleteSubscription(subscriptionID: String) async throws { + public func deleteSubscription(subscriptionID: String) async throws { guard let baseURL = apiBaseURL else { throw CredentialsError.incompleteCredentials @@ -328,17 +329,17 @@ public enum CreateReaderAPISubscriptionResult: Sendable { try await self.transport.send(request: request, method: HTTPMethod.post, payload: postData!) } - func createTagging(subscriptionID: String, tagName: String) async throws { + public func createTagging(subscriptionID: String, tagName: String) async throws { try await changeSubscription(subscriptionID: subscriptionID, addTagName: tagName) } - func deleteTagging(subscriptionID: String, tagName: String) async throws { + public func deleteTagging(subscriptionID: String, tagName: String) async throws { try await changeSubscription(subscriptionID: subscriptionID, removeTagName: tagName) } - func moveSubscription(subscriptionID: String, sourceTag: String, destinationTag: String) async throws { + public func moveSubscription(subscriptionID: String, sourceTag: String, destinationTag: String) async throws { try await changeSubscription(subscriptionID: subscriptionID, removeTagName: sourceTag, addTagName: destinationTag) } @@ -374,7 +375,7 @@ public enum CreateReaderAPISubscriptionResult: Sendable { try await transport.send(request: request, method: HTTPMethod.post, payload: postData!) } - func retrieveEntries(articleIDs: [String]) async throws -> [ReaderAPIEntry]? { + public func retrieveEntries(articleIDs: [String]) async throws -> [ReaderAPIEntry]? { guard !articleIDs.isEmpty else { return [ReaderAPIEntry]() @@ -412,7 +413,7 @@ public enum CreateReaderAPISubscriptionResult: Sendable { return entryWrapper.entries } - func retrieveItemIDs(type: ItemIDType, feedID: String? = nil) async throws -> [String] { + public func retrieveItemIDs(type: ItemIDType, feedID: String? = nil) async throws -> [String] { guard let baseURL = apiBaseURL else { throw CredentialsError.incompleteCredentials @@ -510,22 +511,22 @@ public enum CreateReaderAPISubscriptionResult: Sendable { return try await retrieveItemIDs(type: type, url: callURL, dateInfo: dateInfo, itemIDs: totalItemIDs, continuation: entries?.continuation) } - func createUnreadEntries(entries: [String]) async throws { + public func createUnreadEntries(entries: [String]) async throws { try await updateStateToEntries(entries: entries, state: .read, add: false) } - func deleteUnreadEntries(entries: [String]) async throws { - + public func deleteUnreadEntries(entries: [String]) async throws { + try await updateStateToEntries(entries: entries, state: .read, add: true) } - func createStarredEntries(entries: [String]) async throws { - + public func createStarredEntries(entries: [String]) async throws { + try await updateStateToEntries(entries: entries, state: .starred, add: true) } - func deleteStarredEntries(entries: [String]) async throws { + public func deleteStarredEntries(entries: [String]) async throws { try await updateStateToEntries(entries: entries, state: .starred, add: false) }