From 98c8135d045b74d9b16ea38ed619f793d9a333f3 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 27 Oct 2023 21:49:23 -0700 Subject: [PATCH 01/10] Convert AccountDelegate.refreshAll to async/await. --- Account/Sources/Account/Account.swift | 29 +++++++----- Account/Sources/Account/AccountDelegate.swift | 2 +- .../FeedbinAccountDelegate.swift | 46 +++++++++---------- .../LocalAccountDelegate.swift | 35 +++++++------- .../NewsBlurAccountDelegate.swift | 15 +++++- .../ReaderAPIAccountDelegate.swift | 15 +++++- Account/Sources/Account/AccountManager.swift | 31 ++++++------- .../CloudKit/CloudKitAccountDelegate.swift | 17 +++++-- .../Feedly/FeedlyAccountDelegate.swift | 34 ++++++++------ Mac/AppDelegate.swift | 4 +- .../AccountsFeedbinWindowController.swift | 11 ++--- .../AccountsNewsBlurWindowController.swift | 11 ++--- .../AccountsPreferencesViewController.swift | 11 ++--- .../AccountsReaderAPIWindowController.swift | 11 ++--- Shared/Timer/AccountRefreshTimer.swift | 4 +- 15 files changed, 154 insertions(+), 122 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 82de92c0f..086ae5f1d 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -428,8 +428,8 @@ public enum FetchType { await delegate.receiveRemoteNotification(for: self, userInfo: userInfo) } - public func refreshAll(completion: @escaping (Result) -> Void) { - delegate.refreshAll(for: self, completion: completion) + public func refreshAll() async throws { + try await delegate.refreshAll(for: self) } public func sendArticleStatus(completion: ((Result) -> Void)? = nil) { @@ -453,18 +453,23 @@ public enum FetchType { return } - delegate.importOPML(for: self, opmlFile: opmlFile) { [weak self] result in - switch result { - case .success: - guard let self = self else { return } - // Reset the last fetch date to get the article history for the added feeds. - self.metadata.lastArticleFetchStartTime = nil - self.delegate.refreshAll(for: self, completion: completion) - case .failure(let error): - completion(.failure(error)) + delegate.importOPML(for: self, opmlFile: opmlFile) { result in + Task { @MainActor in + switch result { + case .success: + // Reset the last fetch date to get the article history for the added feeds. + self.metadata.lastArticleFetchStartTime = nil + do { + try await self.delegate.refreshAll(for: self) + completion(result) + } catch { + completion(.failure(error)) + } + case .failure: + completion(result) + } } } - } public func suspendNetwork() { diff --git a/Account/Sources/Account/AccountDelegate.swift b/Account/Sources/Account/AccountDelegate.swift index 227c0dd5c..44aad2272 100644 --- a/Account/Sources/Account/AccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegate.swift @@ -25,7 +25,7 @@ import Secrets func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any]) async - func refreshAll(for account: Account, completion: @escaping (Result) -> Void) + func refreshAll(for account: Account) async throws func syncArticleStatus(for account: Account, completion: ((Result) -> Void)?) func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) diff --git a/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift index 73d33070b..b88cdfe34 100644 --- a/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift @@ -75,37 +75,37 @@ public enum FeedbinAccountDelegateError: String, Error { return } - func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { - + func refreshAll(for account: Account) async throws { + refreshProgress.addToNumberOfTasksAndRemaining(5) - refreshAccount(account) { result in - switch result { - case .success(): + try await withCheckedThrowingContinuation { continuation in + refreshAccount(account) { result in + switch result { + case .success(): - self.refreshArticlesAndStatuses(account) { result in - switch result { - case .success(): - completion(.success(())) - case .failure(let error): - DispatchQueue.main.async { - self.refreshProgress.clear() - let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error) - completion(.failure(wrappedError)) + self.refreshArticlesAndStatuses(account) { result in + switch result { + case .success(): + continuation.resume() + case .failure(let error): + Task { @MainActor in + self.refreshProgress.clear() + let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error) + continuation.resume(throwing: wrappedError) + } } } - } - - case .failure(let error): - DispatchQueue.main.async { - self.refreshProgress.clear() - let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error) - completion(.failure(wrappedError)) + + case .failure(let error): + Task { @MainActor in + self.refreshProgress.clear() + let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error) + continuation.resume(throwing: wrappedError) + } } } - } - } func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { diff --git a/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift index a6759bd15..73cb92dd7 100644 --- a/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift @@ -24,7 +24,7 @@ public enum LocalAccountDelegateError: String, Error { final class LocalAccountDelegate: AccountDelegate, Logging { weak var account: Account? - + private lazy var refresher: LocalAccountRefresher? = { let refresher = LocalAccountRefresher() refresher.delegate = self @@ -44,28 +44,31 @@ final class LocalAccountDelegate: AccountDelegate, Logging { return } - func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { + func refreshAll(for account: Account) async throws { guard refreshProgress.isComplete else { - completion(.success(())) return } - let feeds = account.flattenedFeeds() - let feedURLs = Set(feeds.map{ $0.url }) - refreshProgress.addToNumberOfTasksAndRemaining(feedURLs.count) + try await withCheckedThrowingContinuation { continuation in + Task { @MainActor in + let feeds = account.flattenedFeeds() + let feedURLs = Set(feeds.map{ $0.url }) + refreshProgress.addToNumberOfTasksAndRemaining(feedURLs.count) - let group = DispatchGroup() + let group = DispatchGroup() - group.enter() - refresher?.refreshFeedURLs(feedURLs) { - group.leave() - } + group.enter() + refresher?.refreshFeedURLs(feedURLs) { + group.leave() + } - group.notify(queue: DispatchQueue.main) { - self.refreshProgress.clear() - account.metadata.lastArticleFetchEndTime = Date() - completion(.success(())) - } + group.notify(queue: DispatchQueue.main) { + self.refreshProgress.clear() + account.metadata.lastArticleFetchEndTime = Date() + continuation.resume() + } + } + } } diff --git a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift index 2b7829ed6..b0fa288f1 100644 --- a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift @@ -66,7 +66,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { return } - func refreshAll(for account: Account, completion: @escaping (Result) -> ()) { + private func refreshAll(for account: Account, completion: @escaping (Result) -> ()) { self.refreshProgress.addToNumberOfTasksAndRemaining(4) refreshFeeds(for: account) { result in @@ -118,6 +118,19 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { } } + func refreshAll(for account: Account) async throws { + try await withCheckedThrowingContinuation { continuation in + self.refreshAll(for: account) { result in + switch result { + case .success(): + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { sendArticleStatus(for: account) { result in switch result { diff --git a/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift index 06a37d45a..5c37d36c6 100644 --- a/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift @@ -166,11 +166,22 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { } } } - } - } + func refreshAll(for account: Account) async throws { + try await withCheckedThrowingContinuation { continuation in + self.refreshAll(for: account) { result in + switch result { + case .success(): + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { guard variant != .inoreader else { completion?(.success(())) diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index ab9750e30..c195e7568 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -245,28 +245,23 @@ import RSDatabase } } - public func refreshAll(errorHandler: @escaping @MainActor (Error) -> Void, completion: (() -> Void)? = nil) { + public func refreshAll(errorHandler: @escaping @MainActor (Error) -> Void) async { + guard let reachability = try? Reachability(hostname: "apple.com"), reachability.connection != .unavailable else { return } - let group = DispatchGroup() - - for account in activeAccounts { - group.enter() - account.refreshAll() { result in - group.leave() - switch result { - case .success: - break - case .failure(let error): - Task { @MainActor in - errorHandler(error) - } + await withTaskGroup(of: Void.self) { group in + for account in activeAccounts { + group.addTask { + do { + try await account.refreshAll() + } catch { + Task { @MainActor in + errorHandler(error) + } + } } } - } - - group.notify(queue: DispatchQueue.main) { - completion?() + await group.waitForAll() } } diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 063778e55..889e11024 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -78,20 +78,27 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging { } } - func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { + func refreshAll(for account: Account) async throws { guard refreshProgress.isComplete else { - completion(.success(())) return } let reachability = SCNetworkReachabilityCreateWithName(nil, "apple.com") var flags = SCNetworkReachabilityFlags() guard SCNetworkReachabilityGetFlags(reachability!, &flags), flags.contains(.reachable) else { - completion(.success(())) return } - - standardRefreshAll(for: account, completion: completion) + + try await withCheckedThrowingContinuation { continuation in + standardRefreshAll(for: account) { result in + switch result { + case .success(): + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } } func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift index 73ed72576..50485d2b2 100644 --- a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift @@ -107,19 +107,17 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging { return } - func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { + func refreshAll(for account: Account) async throws { assert(Thread.isMainThread) guard currentSyncAllOperation == nil else { self.logger.debug("Ignoring refreshAll: Feedly sync already in progress.") - completion(.success(())) return } guard let credentials = credentials else { self.logger.debug("Ignoring refreshAll: Feedly account has no credentials.") - completion(.failure(FeedlyAccountDelegateError.notLoggedIn)) - return + throw FeedlyAccountDelegateError.notLoggedIn } let syncAllOperation = FeedlySyncAllOperation(account: account, feedlyUserID: credentials.username, caller: caller, database: database, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetchStartTime, downloadProgress: refreshProgress) @@ -127,19 +125,25 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging { syncAllOperation.downloadProgress = refreshProgress let date = Date() - syncAllOperation.syncCompletionHandler = { [weak self] result in - if case .success = result { - self?.accountMetadata?.lastArticleFetchStartTime = date - self?.accountMetadata?.lastArticleFetchEndTime = Date() + + try await withCheckedThrowingContinuation { continuation in + syncAllOperation.syncCompletionHandler = { result in + self.logger.debug("Sync took \(-date.timeIntervalSinceNow, privacy: .public) seconds.") + + switch result { + case .success(): + self.accountMetadata?.lastArticleFetchStartTime = date + self.accountMetadata?.lastArticleFetchEndTime = Date() + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } } - - self?.logger.debug("Sync took \(-date.timeIntervalSinceNow, privacy: .public) seconds.") - completion(result) + + currentSyncAllOperation = syncAllOperation + + operationQueue.add(syncAllOperation) } - - currentSyncAllOperation = syncAllOperation - - operationQueue.add(syncAllOperation) } func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index eaa2181e2..237849b52 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -577,7 +577,9 @@ var appDelegate: AppDelegate! } @IBAction func refreshAll(_ sender: Any?) { - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present) + Task { + await AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present) + } } @IBAction func showAddFeedWindow(_ sender: Any?) { diff --git a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift index 61328690d..d2fdf7c5f 100644 --- a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift @@ -94,13 +94,10 @@ import Secrets try self.account?.removeCredentials(type: .basic) try self.account?.storeCredentials(validatedCredentials) - self.account?.refreshAll() { result in - switch result { - case .success: - break - case .failure(let error): - NSApplication.shared.presentError(error) - } + do { + try await self.account?.refreshAll() + } catch { + NSApplication.shared.presentError(error) } self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) diff --git a/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift b/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift index fe9602069..ac9b86646 100644 --- a/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift @@ -92,13 +92,10 @@ import Secrets try self.account?.storeCredentials(credentials) try self.account?.storeCredentials(validatedCredentials) - self.account?.refreshAll() { result in - switch result { - case .success: - break - case .failure(let error): - NSApplication.shared.presentError(error) - } + do { + try await self.account?.refreshAll() + } catch { + NSApplication.shared.presentError(error) } self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) diff --git a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift index 05aaa2dff..b2f77f2ee 100644 --- a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift +++ b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift @@ -272,12 +272,11 @@ extension AccountsPreferencesViewController: OAuthAccountAuthorizationOperationD // because the user probably wants to see the result of authorizing NetNewsWire to act on their behalf. NSApp.activate(ignoringOtherApps: true) - account.refreshAll { [weak self] result in - switch result { - case .success: - break - case .failure(let error): - self?.presentError(error) + Task { + do { + try await account.refreshAll() + } catch { + self.presentError(error) } } } diff --git a/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift b/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift index 7f5b52031..4e431d12f 100644 --- a/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift @@ -151,13 +151,10 @@ import ReaderAPI try self.account?.storeCredentials(credentials) try self.account?.storeCredentials(validatedCredentials) - self.account?.refreshAll() { result in - switch result { - case .success: - break - case .failure(let error): - NSApplication.shared.presentError(error) - } + do { + try await self.account?.refreshAll() + } catch { + NSApplication.shared.presentError(error) } self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) diff --git a/Shared/Timer/AccountRefreshTimer.swift b/Shared/Timer/AccountRefreshTimer.swift index 9b5310fc1..4ec6e0da8 100644 --- a/Shared/Timer/AccountRefreshTimer.swift +++ b/Shared/Timer/AccountRefreshTimer.swift @@ -73,6 +73,8 @@ import Account lastTimedRefresh = Date() update() - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log, completion: nil) + Task { + await AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) + } } } From 6cd8715eb08252a7332db55b8c9b8c657485d8ce Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 27 Oct 2023 22:13:29 -0700 Subject: [PATCH 02/10] Convert AccountDelegate.syncArticleStatus to async/await. --- Account/Sources/Account/Account.swift | 6 ++-- Account/Sources/Account/AccountDelegate.swift | 2 +- .../FeedbinAccountDelegate.swift | 26 +++++++++------- .../LocalAccountDelegate.swift | 4 +-- .../NewsBlurAccountDelegate.swift | 26 +++++++++------- .../ReaderAPIAccountDelegate.swift | 27 ++++++++-------- Account/Sources/Account/AccountManager.swift | 20 ++++++------ .../CloudKit/CloudKitAccountDelegate.swift | 27 +++++++++------- .../Feedly/FeedlyAccountDelegate.swift | 31 ++++++++++--------- Shared/Timer/ArticleStatusSyncTimer.swift | 6 ++-- 10 files changed, 92 insertions(+), 83 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 086ae5f1d..891c57c9f 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -443,8 +443,8 @@ public enum FetchType { } } - public func syncArticleStatus(completion: ((Result) -> Void)? = nil) { - delegate.syncArticleStatus(for: self, completion: completion) + public func syncArticleStatus() async throws { + try await delegate.syncArticleStatus(for: self) } public func importOPML(_ opmlFile: URL, completion: @escaping (Result) -> Void) { @@ -453,7 +453,7 @@ public enum FetchType { return } - delegate.importOPML(for: self, opmlFile: opmlFile) { result in + delegate.importOPML(for: self, opmlFile: opmlFile) { result in Task { @MainActor in switch result { case .success: diff --git a/Account/Sources/Account/AccountDelegate.swift b/Account/Sources/Account/AccountDelegate.swift index 44aad2272..403df0d80 100644 --- a/Account/Sources/Account/AccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegate.swift @@ -26,7 +26,7 @@ import Secrets func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any]) async func refreshAll(for account: Account) async throws - func syncArticleStatus(for account: Account, completion: ((Result) -> Void)?) + func syncArticleStatus(for account: Account) async throws func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) diff --git a/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift index b88cdfe34..b67f10729 100644 --- a/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift @@ -108,20 +108,22 @@ public enum FeedbinAccountDelegateError: String, Error { } } - func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { - sendArticleStatus(for: account) { result in - switch result { - case .success: - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - completion?(.success(())) - case .failure(let error): - completion?(.failure(error)) + func syncArticleStatus(for account: Account) async throws { + try await withCheckedThrowingContinuation { continuation in + sendArticleStatus(for: account) { result in + switch result { + case .success: + self.refreshArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } } + case .failure(let error): + continuation.resume(throwing: error) } - case .failure(let error): - completion?(.failure(error)) } } } diff --git a/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift index 73cb92dd7..06eda029a 100644 --- a/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift @@ -72,8 +72,8 @@ final class LocalAccountDelegate: AccountDelegate, Logging { } - func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { - completion?(.success(())) + func syncArticleStatus(for account: Account) async throws { + return } func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { diff --git a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift index b0fa288f1..e1ea7eb7e 100644 --- a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift @@ -131,20 +131,22 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { } } - func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { - sendArticleStatus(for: account) { result in - switch result { - case .success: - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - completion?(.success(())) - case .failure(let error): - completion?(.failure(error)) + func syncArticleStatus(for account: Account) async throws { + try await withCheckedThrowingContinuation { continuation in + sendArticleStatus(for: account) { result in + switch result { + case .success: + self.refreshArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } } + case .failure(let error): + continuation.resume(throwing: error) } - case .failure(let error): - completion?(.failure(error)) } } } diff --git a/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift index 5c37d36c6..2bfc7d76a 100644 --- a/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift @@ -182,25 +182,26 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { } } - func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { + func syncArticleStatus(for account: Account) async throws { guard variant != .inoreader else { - completion?(.success(())) return } - sendArticleStatus(for: account) { result in - switch result { - case .success: - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - completion?(.success(())) - case .failure(let error): - completion?(.failure(error)) + try await withCheckedThrowingContinuation { continuation in + sendArticleStatus(for: account) { result in + switch result { + case .success: + self.refreshArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } } + case .failure(let error): + continuation.resume(throwing: error) } - case .failure(let error): - completion?(.failure(error)) } } } diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index c195e7568..835989fb4 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -280,18 +280,16 @@ import RSDatabase } } - public func syncArticleStatusAll(completion: (() -> Void)? = nil) { - let group = DispatchGroup() - - for account in activeAccounts { - group.enter() - account.syncArticleStatus() { _ in - group.leave() - } - } + public func syncArticleStatusAll() async { - group.notify(queue: DispatchQueue.global(qos: .background)) { - completion?() + await withTaskGroup(of: Void.self) { group in + for account in activeAccounts { + group.addTask { + try? await account.syncArticleStatus() + } + } + + await group.waitForAll() } } diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 889e11024..423f86e52 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -101,20 +101,23 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging { } } - func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { - sendArticleStatus(for: account) { result in - switch result { - case .success: - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - completion?(.success(())) - case .failure(let error): - completion?(.failure(error)) + func syncArticleStatus(for account: Account) async throws { + + try await withCheckedThrowingContinuation { continuation in + sendArticleStatus(for: account) { result in + switch result { + case .success: + self.refreshArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } } + case .failure(let error): + continuation.resume(throwing: error) } - case .failure(let error): - completion?(.failure(error)) } } } diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift index 50485d2b2..701b3a1e2 100644 --- a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift @@ -146,22 +146,25 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging { } } - func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { - sendArticleStatus(for: account) { result in - switch result { - case .success: - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - completion?(.success(())) - case .failure(let error): - self.logger.error("Failed to refresh article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)") - completion?(.failure(error)) + func syncArticleStatus(for account: Account) async throws { + + try await withCheckedThrowingContinuation { continuation in + sendArticleStatus(for: account) { result in + switch result { + case .success: + self.refreshArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + self.logger.error("Failed to refresh article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)") + continuation.resume(throwing: error) + } } + case .failure(let error): + self.logger.error("Failed to send article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)") + continuation.resume(throwing: error) } - case .failure(let error): - self.logger.error("Failed to send article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)") - completion?(.failure(error)) } } } diff --git a/Shared/Timer/ArticleStatusSyncTimer.swift b/Shared/Timer/ArticleStatusSyncTimer.swift index 5e0d8ffae..0bbe178ce 100644 --- a/Shared/Timer/ArticleStatusSyncTimer.swift +++ b/Shared/Timer/ArticleStatusSyncTimer.swift @@ -68,8 +68,8 @@ class ArticleStatusSyncTimer { lastTimedRefresh = Date() update() - AccountManager.shared.syncArticleStatusAll() - + Task { + await AccountManager.shared.syncArticleStatusAll() + } } - } From e34f002a1b54645c4a16412a9222ae91f1ef2d0c Mon Sep 17 00:00:00 2001 From: Teddy Bradford <3684553+teddybradford@users.noreply.github.com> Date: Thu, 2 Nov 2023 03:25:32 -0400 Subject: [PATCH 03/10] Re-add spacing between figure img and caption --- Shared/Article Rendering/stylesheet.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Shared/Article Rendering/stylesheet.css b/Shared/Article Rendering/stylesheet.css index eabf7a21b..0d17ad8bd 100644 --- a/Shared/Article Rendering/stylesheet.css +++ b/Shared/Article Rendering/stylesheet.css @@ -263,6 +263,10 @@ figure { margin-top: 1em; } +figure > * + * { + margin-top: 0.5em; +} + figcaption { font-size: 14px; line-height: 1.3em; From f9e7de718d1911d7c46db2bbeedfadaff48fd4b5 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 11 Nov 2023 12:43:02 -0600 Subject: [PATCH 04/10] Update right click toolbar code for Sonoma --- Mac/MainWindow/MainWindow.swift | 35 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Mac/MainWindow/MainWindow.swift b/Mac/MainWindow/MainWindow.swift index 18409f76a..2035a1ff0 100644 --- a/Mac/MainWindow/MainWindow.swift +++ b/Mac/MainWindow/MainWindow.swift @@ -14,23 +14,34 @@ import Foundation // Since the Toolbar intercepts right clicks we need to stop it from doing that here // so that the ArticleExtractorButton can receive right click events. - if event.isRightClick, - let frameView = contentView?.superview, - let view = frameView.hitTest(frameView.convert(event.locationInWindow, from: nil)), - type(of: view).description() == "NSToolbarView" { + if #available(macOS 14.0, *) { + if event.isRightClick, + let frameView = contentView?.superview, + let view = frameView.hitTest(frameView.convert(event.locationInWindow, from: nil)), + let articleExtractorButton = view as? ArticleExtractorButton { - for subview in view.subviews { - for subsubview in subview.subviews { - let candidateView = subsubview.hitTest(subsubview.convert(event.locationInWindow, from: nil)) - if candidateView is ArticleExtractorButton { - candidateView?.rightMouseDown(with: event) - return + articleExtractorButton.rightMouseDown(with: event) + return + } + } else { + if event.isRightClick, + let frameView = contentView?.superview, + let view = frameView.hitTest(frameView.convert(event.locationInWindow, from: nil)), + type(of: view).description() == "NSToolbarView" { + + for subview in view.subviews { + for subsubview in subview.subviews { + let candidateView = subsubview.hitTest(subsubview.convert(event.locationInWindow, from: nil)) + if candidateView is ArticleExtractorButton { + candidateView?.rightMouseDown(with: event) + return + } } } + } + super.sendEvent(event) } - - super.sendEvent(event) } } From 96dd6cea16cf4337aadb2fe7f1c7fcffc8c44e11 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 11 Nov 2023 12:53:12 -0600 Subject: [PATCH 05/10] Fix regression that didn't allow any events to register --- Mac/MainWindow/MainWindow.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Mac/MainWindow/MainWindow.swift b/Mac/MainWindow/MainWindow.swift index 2035a1ff0..9390a8f7f 100644 --- a/Mac/MainWindow/MainWindow.swift +++ b/Mac/MainWindow/MainWindow.swift @@ -41,7 +41,9 @@ import Foundation } - super.sendEvent(event) } + + super.sendEvent(event) } } +s From c0f11ea91ac50143c98605cd424cf9855bb8b7c2 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 11 Nov 2023 12:57:10 -0600 Subject: [PATCH 06/10] Remove extraneous character --- Mac/MainWindow/MainWindow.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Mac/MainWindow/MainWindow.swift b/Mac/MainWindow/MainWindow.swift index 9390a8f7f..6dd8abf8f 100644 --- a/Mac/MainWindow/MainWindow.swift +++ b/Mac/MainWindow/MainWindow.swift @@ -46,4 +46,3 @@ import Foundation super.sendEvent(event) } } -s From c7b036f3644aae085a98efdd30eec18f3441450d Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 17 Nov 2023 22:23:33 -0800 Subject: [PATCH 07/10] Use RSCore 3. --- Account/Package.swift | 2 +- Account/Sources/Account/Account.swift | 11 +- Account/Sources/Account/AccountDelegate.swift | 4 +- .../FeedbinAccountDelegate.swift | 33 ++- .../LocalAccountDelegate.swift | 10 +- .../NewsBlurAccountDelegate+Internal.swift | 9 +- .../NewsBlurAccountDelegate.swift | 32 ++- .../ReaderAPIAccountDelegate.swift | 34 ++- Account/Sources/Account/AccountManager.swift | 7 +- .../CloudKit/CloudKitAccountDelegate.swift | 270 +++++++++++------- .../CloudKit/CloudKitAccountZone.swift | 38 +++ .../CloudKitAccountZoneDelegate.swift | 61 ++-- .../CloudKit/CloudKitArticlesZone.swift | 25 +- .../CloudKitArticlesZoneDelegate.swift | 19 +- .../CloudKitReceiveStatusOperation.swift | 13 +- .../CloudKitRemoteNotificationOperation.swift | 12 +- .../Feedly/FeedlyAccountDelegate.swift | 52 ++-- Articles/Package.swift | 2 +- ArticlesDatabase/Package.swift | 2 +- FeedFinder/Package.swift | 2 +- NetNewsWire.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- SyncClients/Feedbin/Package.swift | 2 +- SyncClients/LocalAccount/Package.swift | 2 +- SyncClients/NewsBlur/Package.swift | 2 +- SyncClients/ReaderAPI/Package.swift | 2 +- SyncDatabase/Package.swift | 2 +- 27 files changed, 446 insertions(+), 208 deletions(-) diff --git a/Account/Package.swift b/Account/Package.swift index 4e4aa9649..6cb758680 100644 --- a/Account/Package.swift +++ b/Account/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["Account"]), ], dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 891c57c9f..c6c957c66 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -432,15 +432,8 @@ public enum FetchType { try await delegate.refreshAll(for: self) } - public func sendArticleStatus(completion: ((Result) -> Void)? = nil) { - delegate.sendArticleStatus(for: self) { result in - switch result { - case .success: - completion?(.success(())) - case .failure(let error): - completion?(.failure(error)) - } - } + public func sendArticleStatus() async throws { + try await delegate.sendArticleStatus(for: self) } public func syncArticleStatus() async throws { diff --git a/Account/Sources/Account/AccountDelegate.swift b/Account/Sources/Account/AccountDelegate.swift index 403df0d80..e1097b460 100644 --- a/Account/Sources/Account/AccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegate.swift @@ -27,8 +27,8 @@ import Secrets func refreshAll(for account: Account) async throws func syncArticleStatus(for account: Account) async throws - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) + func sendArticleStatus(for account: Account) async throws + func refreshArticleStatus(for account: Account) async throws func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) diff --git a/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift index b67f10729..42540234b 100644 --- a/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift @@ -25,7 +25,7 @@ public enum FeedbinAccountDelegateError: String, Error { @MainActor final class FeedbinAccountDelegate: AccountDelegate, Logging { private let database: SyncDatabase - + private let caller: FeedbinAPICaller let behaviors: AccountBehaviors = [.disallowFeedCopyInRootFolder] @@ -128,7 +128,20 @@ public enum FeedbinAccountDelegateError: String, Error { } } - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { + func sendArticleStatus(for account: Account) async throws { + try await withCheckedThrowingContinuation { continuation in + self.sendArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { Task { @MainActor in logger.debug("Sending article statuses") @@ -190,7 +203,21 @@ public enum FeedbinAccountDelegateError: String, Error { } } - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { + func refreshArticleStatus(for account: Account) async throws { + + try await withCheckedThrowingContinuation { continuation in + self.refreshArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { logger.debug("Refreshing article statuses...") diff --git a/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift index 06eda029a..0e1553c86 100644 --- a/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift @@ -76,14 +76,14 @@ final class LocalAccountDelegate: AccountDelegate, Logging { return } - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - completion(.success(())) + func sendArticleStatus(for account: Account) async throws { + return } - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - completion(.success(())) + func refreshArticleStatus(for account: Account) async throws { + return } - + func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { var fileData: Data? diff --git a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate+Internal.swift b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate+Internal.swift index b93d06dfb..610dca162 100644 --- a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate+Internal.swift +++ b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate+Internal.swift @@ -469,9 +469,9 @@ extension NewsBlurAccountDelegate { // Download the initial articles downloadFeed(account: account, feed: feed, page: 1) { result in - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: + Task { @MainActor in + do { + try await self.refreshArticleStatus(for: account) self.refreshMissingStories(for: account) { result in switch result { case .success: @@ -485,8 +485,7 @@ extension NewsBlurAccountDelegate { completion(.failure(error)) } } - - case .failure(let error): + } catch { completion(.failure(error)) } } diff --git a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift index e1ea7eb7e..bcada0350 100644 --- a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift @@ -151,7 +151,21 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { } } - func sendArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { + func sendArticleStatus(for account: Account) async throws { + + try await withCheckedThrowingContinuation { continuation in + self.sendArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func sendArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { Task { @MainActor in logger.debug("Sending story statuses") @@ -223,7 +237,21 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { } } - func refreshArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { + func refreshArticleStatus(for account: Account) async throws { + + try await withCheckedThrowingContinuation { continuation in + self.refreshArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func refreshArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { logger.debug("Refreshing story statuses...") let group = DispatchGroup() diff --git a/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift index 2bfc7d76a..e1993d908 100644 --- a/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift @@ -39,7 +39,7 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { @MainActor final class ReaderAPIAccountDelegate: AccountDelegate, Logging { private let variant: ReaderAPIVariant - + private let database: SyncDatabase private let caller: ReaderAPICaller @@ -206,7 +206,21 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { } } - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { + func sendArticleStatus(for account: Account) async throws { + + try await withCheckedThrowingContinuation { continuation in + self.sendArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { logger.debug("Sending article statuses") @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { @@ -253,7 +267,21 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { } } - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { + func refreshArticleStatus(for account: Account) async throws { + + try await withCheckedThrowingContinuation { continuation in + self.refreshArticleStatus(for: account) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { logger.debug("Refreshing article statuses...") let group = DispatchGroup() diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index 835989fb4..92512c4d5 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -267,10 +267,12 @@ import RSDatabase public func sendArticleStatusAll(completion: (() -> Void)? = nil) { let group = DispatchGroup() - + for account in activeAccounts { group.enter() - account.sendArticleStatus() { _ in + + Task { @MainActor in + try? await account.sendArticleStatus() group.leave() } } @@ -280,6 +282,7 @@ import RSDatabase } } + public func syncArticleStatusAll() async { await withTaskGroup(of: Void.self) { group in diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 423f86e52..3566058e9 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -102,42 +102,30 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging { } func syncArticleStatus(for account: Account) async throws { + try await sendArticleStatus(for: account) + } + + func sendArticleStatus(for account: Account) async throws { + try await sendArticleStatus(for: account, showProgress: false) + } + + func refreshArticleStatus(for account: Account) async throws { try await withCheckedThrowingContinuation { continuation in - sendArticleStatus(for: account) { result in - switch result { - case .success: - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - continuation.resume(throwing: error) - } - } - case .failure(let error): - continuation.resume(throwing: error) + + let op = CloudKitReceiveStatusOperation(articlesZone: self.articlesZone) + op.completionBlock = { mainThreadOperaion in + if mainThreadOperaion.isCanceled { + continuation.resume(throwing: CloudKitAccountDelegateError.unknown) + } else { + continuation.resume() } } + + mainThreadOperationQueue.add(op) } } - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - sendArticleStatus(for: account, showProgress: false, completion: completion) - } - - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - let op = CloudKitReceiveStatusOperation(articlesZone: articlesZone) - op.completionBlock = { mainThreadOperaion in - if mainThreadOperaion.isCanceled { - completion(.failure(CloudKitAccountDelegateError.unknown)) - } else { - completion(.success(())) - } - } - mainThreadOperationQueue.add(op) - } - func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { guard refreshProgress.isComplete else { completion(.success(())) @@ -454,7 +442,7 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging { try? await self.database.insertStatuses(syncStatuses) if let count = try? await self.database.selectPendingCount(), count > 100 { - self.sendArticleStatus(for: account, showProgress: false) { _ in } + try? await self.sendArticleStatus(for: account, showProgress: false) } completion(.success(())) case .failure(let error): @@ -517,52 +505,92 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging { } private extension CloudKitAccountDelegate { - - func initialRefreshAll(for account: Account, completion: @escaping (Result) -> Void) { - - func fail(_ error: Error) { - self.processAccountError(account, error) - self.refreshProgress.clear() - completion(.failure(error)) - } - - refreshProgress.isIndeterminate = true - refreshProgress.addToNumberOfTasksAndRemaining(3) - accountZone.fetchChangesInZone() { result in - self.refreshProgress.completeTask() - let feeds = account.flattenedFeeds() - self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count) + func fetchChangesInZone() async throws { - switch result { - case .success: - self.refreshArticleStatus(for: account) { result in - self.refreshProgress.completeTask() - self.refreshProgress.isIndeterminate = false - switch result { - case .success: - - self.combinedRefresh(account, feeds) { result in - self.refreshProgress.clear() - switch result { - case .success: - account.metadata.lastArticleFetchEndTime = Date() - case .failure(let error): - fail(error) - } - } - - case .failure(let error): - fail(error) - } + try await withCheckedThrowingContinuation { continuation in + self.accountZone.fetchChangesInZone { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) } - case .failure(let error): - fail(error) } } - } + @MainActor func initialRefreshAll(for account : Account) async throws { + + refreshProgress.isIndeterminate = true + refreshProgress.addToNumberOfTasksAndRemaining(3) + + do { + try await fetchChangesInZone() + refreshProgress.completeTask() + + let feeds = account.flattenedFeeds() + refreshProgress.addToNumberOfTasksAndRemaining(feeds.count) + + try await refreshArticleStatus(for: account) + refreshProgress.completeTask() + refreshProgress.isIndeterminate = false + + await combinedRefresh(account, feeds) + refreshProgress.clear() + account.metadata.lastArticleFetchEndTime = Date() + } catch { + processAccountError(account, error) + refreshProgress.clear() + throw error + } + } + +// func initialRefreshAll(for account: Account, completion: @escaping (Result) -> Void) { +// +// func fail(_ error: Error) { +// self.processAccountError(account, error) +// self.refreshProgress.clear() +// completion(.failure(error)) +// } +// +// refreshProgress.isIndeterminate = true +// refreshProgress.addToNumberOfTasksAndRemaining(3) +// accountZone.fetchChangesInZone() { result in +// self.refreshProgress.completeTask() +// +// let feeds = account.flattenedFeeds() +// self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count) +// +// switch result { +// case .success: +// self.refreshArticleStatus(for: account) { result in +// self.refreshProgress.completeTask() +// self.refreshProgress.isIndeterminate = false +// switch result { +// case .success: +// +// self.combinedRefresh(account, feeds) { result in +// self.refreshProgress.clear() +// switch result { +// case .success: +// account.metadata.lastArticleFetchEndTime = Date() +// case .failure(let error): +// fail(error) +// } +// } +// +// case .failure(let error): +// fail(error) +// } +// } +// case .failure(let error): +// fail(error) +// } +// } +// +// } + func standardRefreshAll(for account: Account, completion: @escaping (Result) -> Void) { let intialFeedsCount = account.flattenedFeeds().count @@ -583,6 +611,36 @@ private extension CloudKitAccountDelegate { let feeds = account.flattenedFeeds() self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count - intialFeedsCount) + Task { @MainActor in + do { + try await self.refreshArticleStatus(for: account) + self.refreshProgress.completeTask() + self.refreshProgress.isIndeterminate = false + + self.combinedRefresh(account, feeds) { result in + do { + try await self.sendArticleStatus(for: account, showProgress: true) + if case .failure(let error) = result { + fail(error) + } else { + account.metadata.lastArticleFetchEndTime = Date() + completion(.success(())) + } + } catch { + fail(error) + } + } + + } catch { + self.refreshProgress.completeTask() + self.refreshProgress.isIndeterminate = false + } + + + + + } + self.refreshArticleStatus(for: account) { result in switch result { case .success: @@ -611,20 +669,31 @@ private extension CloudKitAccountDelegate { } - func combinedRefresh(_ account: Account, _ feeds: Set, completion: @escaping (Result) -> Void) { + func combinedRefresh(_ account: Account, _ feeds: Set) async { let feedURLs = Set(feeds.map{ $0.url }) - let group = DispatchGroup() + + try await withCheckedContinuation { continuation in + refresher.refreshFeedURLs(feedURLs) { + continuation.resume() + } + } + } - group.enter() - refresher.refreshFeedURLs(feedURLs) { - group.leave() - } - - group.notify(queue: DispatchQueue.main) { - completion(.success(())) - } - } +// func combinedRefresh(_ account: Account, _ feeds: Set, completion: @escaping (Result) -> Void) { +// +// let feedURLs = Set(feeds.map{ $0.url }) +// let group = DispatchGroup() +// +// group.enter() +// refresher.refreshFeedURLs(feedURLs) { +// group.leave() +// } +// +// group.notify(queue: DispatchQueue.main) { +// completion(.success(())) +// } +// } func createRSSFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { @@ -739,12 +808,13 @@ private extension CloudKitAccountDelegate { case .success(let articles): self.storeArticleChanges(new: articles, updated: Set
(), deleted: Set
()) { self.refreshProgress.completeTask() - self.sendArticleStatus(for: account, showProgress: true) { result in - switch result { - case .success: - self.refreshArticleStatus(for: account) { _ in } - case .failure(let error): - self.logger.error("CloudKit Feed send articles error: \(error.localizedDescription, privacy: .public)") + + Task { @MainActor in + do { + try await self.sendArticleStatus(for: account, showProgress: true) + try await self.refreshArticleStatus(for: account) + } catch { + self.logger.error("CloudKit Feed send articles error: \(error.localizedDescription, privacy: .public)") } } } @@ -805,20 +875,24 @@ private extension CloudKitAccountDelegate { } } - func sendArticleStatus(for account: Account, showProgress: Bool, completion: @escaping ((Result) -> Void)) { - let op = CloudKitSendStatusOperation(account: account, - articlesZone: articlesZone, - refreshProgress: refreshProgress, - showProgress: showProgress, - database: database) - op.completionBlock = { mainThreadOperaion in - if mainThreadOperaion.isCanceled { - completion(.failure(CloudKitAccountDelegateError.unknown)) - } else { - completion(.success(())) + func sendArticleStatus(for account: Account, showProgress: Bool) async throws { + + try await withCheckedThrowingContinuation { continuation in + let op = CloudKitSendStatusOperation(account: account, + articlesZone: self.articlesZone, + refreshProgress: self.refreshProgress, + showProgress: showProgress, + database: self.database) + op.completionBlock = { mainThreadOperation in + if mainThreadOperation.isCanceled { + continuation.resume(throwing: CloudKitAccountDelegateError.unknown) + } else { + continuation.resume() + } } + + mainThreadOperationQueue.add(op) } - mainThreadOperationQueue.add(op) } diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift index 812fb90f8..bbb3e0e15 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift @@ -253,6 +253,29 @@ final class CloudKitAccountZone: CloudKitZone { } } + func findOrCreateAccount() async throws -> String { + + guard let database else { + return + } + + let predicate = NSPredicate(format: "isAccount = \"1\"") + let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate) + + do { + let records = try await database.perform(ckQuery, inZoneWith: zoneID) + if records.count > 0 { + return records[0].externalID + } else { + createContainer(name: "Account", isAccount: true, completion: completion) + } + } catch { + switch CloudKitZoneResult.resolve(error) { + } + } + + } + func findOrCreateAccount(completion: @escaping (Result) -> Void) { let predicate = NSPredicate(format: "isAccount = \"1\"") let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate) @@ -343,6 +366,21 @@ private extension CloudKitAccountZone { return record } + func createContainer(name: String, isAccount: Bool) async throws -> String { + let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID()) + record[CloudKitContainer.Fields.name] = name + record[CloudKitContainer.Fields.isAccount] = isAccount ? "1" : "0" + + save(record) { result in + switch result { + case .success: + completion(.success(record.externalID)) + case .failure(let error): + completion(.failure(error)) + } + } + } + func createContainer(name: String, isAccount: Bool, completion: @escaping (Result) -> Void) { let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID()) record[CloudKitContainer.Fields.name] = name diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift index bd676c6d9..6ccd89865 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift @@ -31,32 +31,19 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { self.articlesZone = articlesZone } - @MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) { - for deletedRecordKey in deleted { - switch deletedRecordKey.recordType { - case CloudKitAccountZone.CloudKitFeed.recordType: - removeFeed(deletedRecordKey.recordID.externalID) - case CloudKitAccountZone.CloudKitContainer.recordType: - removeContainer(deletedRecordKey.recordID.externalID) - default: - assertionFailure("Unknown record type: \(deletedRecordKey.recordType)") + @MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey]) async throws { + try await withCheckedThrowingContinuation { continuation in + self.cloudKitWasChanged(updated: updated, deleted: deleted) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } } } - - for changedRecord in updated { - switch changedRecord.recordType { - case CloudKitAccountZone.CloudKitFeed.recordType: - addOrUpdateFeed(changedRecord) - case CloudKitAccountZone.CloudKitContainer.recordType: - addOrUpdateContainer(changedRecord) - default: - assertionFailure("Unknown record type: \(changedRecord.recordType)") - } - } - - completion(.success(())) } - + @MainActor func addOrUpdateFeed(_ record: CKRecord) { guard let account = account, let urlString = record[CloudKitAccountZone.CloudKitFeed.Fields.url] as? String, @@ -140,7 +127,33 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { private extension CloudKitAcountZoneDelegate { - @MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) { + @MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) { + for deletedRecordKey in deleted { + switch deletedRecordKey.recordType { + case CloudKitAccountZone.CloudKitFeed.recordType: + removeFeed(deletedRecordKey.recordID.externalID) + case CloudKitAccountZone.CloudKitContainer.recordType: + removeContainer(deletedRecordKey.recordID.externalID) + default: + assertionFailure("Unknown record type: \(deletedRecordKey.recordType)") + } + } + + for changedRecord in updated { + switch changedRecord.recordType { + case CloudKitAccountZone.CloudKitFeed.recordType: + addOrUpdateFeed(changedRecord) + case CloudKitAccountZone.CloudKitContainer.recordType: + addOrUpdateContainer(changedRecord) + default: + assertionFailure("Unknown record type: \(changedRecord.recordType)") + } + } + + completion(.success(())) + } + + @MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) { guard let account = account else { return } feed.name = name diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift index a565ab9c8..590b25d37 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift @@ -84,7 +84,30 @@ final class CloudKitArticlesZone: CloudKitZone { self.save(compressedRecords, completion: completion) } } - + + @MainActor func saveNewArticles(_ articles: Set
) async throws { + guard !articles.isEmpty else { + return + } + + let records: [CKRecord] = { + var recordsAccumulator = [CKRecord]() + + let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true } + for saveArticle in saveArticles { + recordsAccumulator.append(makeStatusRecord(saveArticle)) + recordsAccumulator.append(makeArticleRecord(saveArticle)) + } + return recordsAccumulator + }() + + compressionQueue.async { + let compressedRecords = self.compressArticleRecords(records) + self.save(compressedRecords, completion: completion) + } + } + + func deleteArticles(_ feedExternalID: String, completion: @escaping ((Result) -> Void)) { let predicate = NSPredicate(format: "webFeedExternalID = %@", feedExternalID) let ckQuery = CKQuery(recordType: CloudKitArticleStatus.recordType, predicate: predicate) diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift index a86568a25..4af872619 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift @@ -28,6 +28,22 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging { self.articlesZone = articlesZone } + func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey]) async throws { + try await withCheckedThrowingContinuation { continuation in + cloudKitWasChanged(updated: updated, deleted: deleted) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } +} + +private extension CloudKitArticlesZoneDelegate { + func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) { Task { @MainActor in @@ -54,9 +70,6 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging { } } } -} - -private extension CloudKitArticlesZoneDelegate { func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set, completion: @escaping (Error?) -> Void) { let receivedRecordIDs = recordKeys.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticleStatus.recordType }).map({ $0.recordID }) diff --git a/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift b/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift index 91d0f0789..f58963c00 100644 --- a/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift @@ -32,16 +32,15 @@ class CloudKitReceiveStatusOperation: MainThreadOperation, Logging { logger.debug("Refreshing article statuses...") - articlesZone.fetchChangesInZone() { result in - self.logger.debug("Done refreshing article statuses.") - switch result { - case .success: + Task { @MainActor in + do { + try await articlesZone.fetchChangesInZone() + self.logger.debug("Done refreshing article statuses.") self.operationDelegate?.operationDidComplete(self) - case .failure(let error): - self.logger.error("Receive status error: \(error.localizedDescription, privacy: .public)") + } catch { + self.logger.error("Receive status error: \(error.localizedDescription, privacy: .public)") self.operationDelegate?.cancelOperation(self) } } } - } diff --git a/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift b/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift index 73833af34..62ff6c5f0 100644 --- a/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift @@ -36,13 +36,11 @@ class CloudKitRemoteNotificationOperation: MainThreadOperation, Logging { logger.debug("Processing remote notification...") - accountZone.receiveRemoteNotification(userInfo: userInfo) { - articlesZone.receiveRemoteNotification(userInfo: self.userInfo) { - self.logger.debug("Done processing remote notification.") - self.operationDelegate?.operationDidComplete(self) - } + Task { @MainActor in + await accountZone.receiveRemoteNotification(userInfo: self.userInfo) + await articlesZone.receiveRemoteNotification(userInfo: self.userInfo) + self.logger.debug("Done processing remote notification.") + self.operationDelegate?.operationDidComplete(self) } - } - } diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift index 701b3a1e2..188af7369 100644 --- a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift @@ -148,46 +148,48 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging { func syncArticleStatus(for account: Account) async throws { + do { + try await sendArticleStatus(for: account) + try await refreshArticleStatus(for: account) + } catch { + self.logger.error("Failed to send article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)") + throw error + } + } + + func sendArticleStatus(for account: Account) async throws { + // Ensure remote articles have the same status as they do locally. + try await withCheckedThrowingContinuation { continuation in - sendArticleStatus(for: account) { result in + let send = FeedlySendArticleStatusesOperation(database: database, service: caller) + send.completionBlock = { operation in + continuation.resume() + } + + operationQueue.add(send) + } + } + + func refreshArticleStatus(for account: Account) async throws { + try await withCheckedThrowingContinuation { continuation in + self.refreshArticleStatus(for: account) { result in switch result { case .success: - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - self.logger.error("Failed to refresh article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)") - continuation.resume(throwing: error) - } - } + continuation.resume() case .failure(let error): - self.logger.error("Failed to send article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)") continuation.resume(throwing: error) } } } } - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - // Ensure remote articles have the same status as they do locally. - let send = FeedlySendArticleStatusesOperation(database: database, service: caller) - send.completionBlock = { operation in - // TODO: not call with success if operation was canceled? Not sure. - DispatchQueue.main.async { - completion(.success(())) - } - } - operationQueue.add(send) - } - /// Attempts to ensure local articles have the same status as they do remotely. /// So if the user is using another client roughly simultaneously with this app, /// this app does its part to ensure the articles have a consistent status between both. /// /// - Parameter account: The account whose articles have a remote status. /// - Parameter completion: Call on the main queue. - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { + private func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { guard let credentials = credentials else { return completion(.success(())) } @@ -557,7 +559,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging { try await self.database.insertStatuses(syncStatuses) let count = try await self.database.selectPendingCount() if count > 100 { - self.sendArticleStatus(for: account) { _ in } + try? await self.sendArticleStatus(for: account) } completion(.success(())) } catch { diff --git a/Articles/Package.swift b/Articles/Package.swift index 2de47b65a..e76db645c 100644 --- a/Articles/Package.swift +++ b/Articles/Package.swift @@ -11,7 +11,7 @@ let package = Package( targets: ["Articles"]), ], dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), ], targets: [ .target( diff --git a/ArticlesDatabase/Package.swift b/ArticlesDatabase/Package.swift index 321bdf053..1eca4bf7f 100644 --- a/ArticlesDatabase/Package.swift +++ b/ArticlesDatabase/Package.swift @@ -4,7 +4,7 @@ import PackageDescription var dependencies: [Package.Dependency] = [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), ] diff --git a/FeedFinder/Package.swift b/FeedFinder/Package.swift index ffc034913..ede1458cd 100644 --- a/FeedFinder/Package.swift +++ b/FeedFinder/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: ["FeedFinder"]), ], dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), .package(path: "../AccountError"), diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index f7441f472..10c67560e 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -5272,7 +5272,7 @@ repositoryURL = "https://github.com/Ranchero-Software/RSCore.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.0.1; + minimumVersion = 3.0.0; }; }; 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */ = { diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0fa8ca3b9..9238f1837 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Ranchero-Software/RSCore.git", "state" : { - "revision" : "dcaa40ceb2c8acd182fbcd69d1f8e56df97e38b1", - "version" : "2.0.1" + "revision" : "cee6d96e036cc4ad08ad5f79364f87f9c291c43c", + "version" : "3.0.0" } }, { diff --git a/SyncClients/Feedbin/Package.swift b/SyncClients/Feedbin/Package.swift index 78fb37d03..74a2ec88b 100644 --- a/SyncClients/Feedbin/Package.swift +++ b/SyncClients/Feedbin/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(path: "../../Secrets"), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")) ], targets: [ diff --git a/SyncClients/LocalAccount/Package.swift b/SyncClients/LocalAccount/Package.swift index be98a5944..2b80c9a6a 100644 --- a/SyncClients/LocalAccount/Package.swift +++ b/SyncClients/LocalAccount/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: ["LocalAccount"]), ], dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), ], diff --git a/SyncClients/NewsBlur/Package.swift b/SyncClients/NewsBlur/Package.swift index 6b151adbe..c31427dcd 100644 --- a/SyncClients/NewsBlur/Package.swift +++ b/SyncClients/NewsBlur/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(path: "../../Secrets"), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")) ], targets: [ diff --git a/SyncClients/ReaderAPI/Package.swift b/SyncClients/ReaderAPI/Package.swift index 32102ad88..045a21569 100644 --- a/SyncClients/ReaderAPI/Package.swift +++ b/SyncClients/ReaderAPI/Package.swift @@ -16,7 +16,7 @@ let package = Package( .package(path: "../../Secrets"), .package(path: "../../AccountError"), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")) ], targets: [ diff --git a/SyncDatabase/Package.swift b/SyncDatabase/Package.swift index ea12311b5..49f6e2dcd 100644 --- a/SyncDatabase/Package.swift +++ b/SyncDatabase/Package.swift @@ -2,7 +2,7 @@ import PackageDescription var dependencies: [Package.Dependency] = [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")), ] From bc15440ded7d12b5a943bfb42195d51bdf48940f Mon Sep 17 00:00:00 2001 From: Wade Tregaskis Date: Wed, 22 Nov 2023 13:41:58 -0800 Subject: [PATCH 08/10] Now set the correct base URL for each article's webview, and now load app JavaScripts as WebKit "user" scripts. Setting the real base URL (rather than using a file URL pointing to the app's Resources folder) allows relative URLs to work correctly within the article, such as for images, and is compatible with Cross-Site-Origin policies that restrict use of resources outside of the origin domain. It also implicitly eliminates access to the local file system from within the webview, as the use of a non-file base URL makes WebKit treats the webview's content as being from a remote server, and its default security policy is to then disallow local file access (except with explicit user action, such as drag-and-drop or via an `input` form element). Note: the base URL is currently typically taken from the feed itself (specifically the "link" feed (channel) metadata). That is controlled by the feed author (or a man-in-the-middle attacker). It should perhaps be validated to ensure it's actually an HTTP/HTTPS URL, to prevent security problems. The app-specific JavaScripts - used for fixing styling issues and the like - are now formally loaded as extensions to the web page, "user scripts" in WebKit parlance. They're isolated to their own JavaScript world - meaning they can't be seen or manipulated by JavaScript from the feed article itself, and are more secure as a result. Fixes #4156. Co-Authored-By: Brent Simmons <1297121+brentsimmons@users.noreply.github.com> --- .../Detail/DetailWebViewController.swift | 14 +++++++++++++- Mac/MainWindow/Detail/page.html | 8 -------- Shared/Article Rendering/main.js | 5 +++++ iOS/Article/WebViewController.swift | 18 ++++++++++++++++-- iOS/Resources/page.html | 9 --------- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index c365727c6..c5b6e20c2 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -96,6 +96,18 @@ protocol DetailWebViewControllerDelegate: AnyObject { userContentController.add(self, name: MessageName.windowDidScroll) userContentController.add(self, name: MessageName.mouseDidEnter) userContentController.add(self, name: MessageName.mouseDidExit) + + let baseURL = ArticleRenderer.page.baseURL + let appScriptsWorld = WKContentWorld.world(name: "NetNewsWire") + for fileName in ["main.js", "main_mac.js", "newsfoot.js"] { + userContentController.addUserScript( + .init(source: try! String(contentsOf: baseURL.appending(path: fileName, + directoryHint: .notDirectory)), + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + in: appScriptsWorld)) + } + configuration.userContentController = userContentController webView = DetailWebView(frame: NSRect.zero, configuration: configuration) @@ -326,7 +338,7 @@ private extension DetailWebViewController { ] let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions) - webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL) + webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL)) } func scrollInfo() async -> ScrollInfo? { diff --git a/Mac/MainWindow/Detail/page.html b/Mac/MainWindow/Detail/page.html index ceaa13f7f..3d71f2c98 100644 --- a/Mac/MainWindow/Detail/page.html +++ b/Mac/MainWindow/Detail/page.html @@ -4,14 +4,6 @@ - - - - diff --git a/Shared/Article Rendering/main.js b/Shared/Article Rendering/main.js index 13f90b638..1cf76b59c 100644 --- a/Shared/Article Rendering/main.js +++ b/Shared/Article Rendering/main.js @@ -168,3 +168,8 @@ function processPage() { removeWpSmiley() postRenderProcessing(); } + +document.addEventListener("DOMContentLoaded", function(event) { + window.scrollTo(0, [[windowScrollY]]); + processPage(); +}) diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 530108f6a..c87b5a870 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -529,6 +529,20 @@ private extension WebViewController { } configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) + let userContentController = WKUserContentController() + let baseURL = ArticleRenderer.page.baseURL + let appScriptsWorld = WKContentWorld.world(name: "NetNewsWire") + for fileName in ["main.js", "main_ios.js", "newsfoot.js"] { + userContentController.addUserScript( + .init(source: try! String(contentsOf: baseURL.appending(path: fileName, + directoryHint: .notDirectory)), + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + in: appScriptsWorld)) + } + + configuration.userContentController = userContentController + let webView = WKWebView(frame: self.view.bounds, configuration: configuration) webView.isOpaque = false; webView.backgroundColor = .clear; @@ -591,8 +605,8 @@ private extension WebViewController { ] let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions) - webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL) - + webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL)) + } func finalScrollPosition(scrollingUp: Bool) -> CGFloat { diff --git a/iOS/Resources/page.html b/iOS/Resources/page.html index 686c4612d..b6513009a 100644 --- a/iOS/Resources/page.html +++ b/iOS/Resources/page.html @@ -5,15 +5,6 @@ - - - - From 44f1f594c30f6ce80ef4dd4588f844dbe033034f Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 1 Dec 2023 17:50:33 -0800 Subject: [PATCH 09/10] Update appcast for 6.1.4b1. --- Appcasts/netnewswire-beta.xml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Appcasts/netnewswire-beta.xml b/Appcasts/netnewswire-beta.xml index 0b47f217f..18d275e7f 100755 --- a/Appcasts/netnewswire-beta.xml +++ b/Appcasts/netnewswire-beta.xml @@ -6,7 +6,21 @@ Most recent NetNewsWire changes with links to updates. en - + + NetNewsWire 6.1.5b1 + This builds adds a new setting — you can turn on/off JavaScript for the article pane. It’s on by default, which matches previous behavior.

+

Note that some content — videos and embedded social media posts, for instance — will often require JavaScript to be on in order to work properly.

+

However, for those who want or need greater security and privacy, we’ve made this setting available.

+ +

This build also fixes a case where images might not load in the article pane.

+ ]]>
+ Fri, 01 Dec 2023 17:30:00 -0700 + + 11.0.0 +
+ + NetNewsWire 6.1.4b1 This build removes Reddit API integration! Don’t install it if you’re not ready for that to happen!

From 18087c19efd8eb520e67d7d667cc5fb127d1e2e2 Mon Sep 17 00:00:00 2001 From: hnharejin Date: Sun, 3 Dec 2023 13:56:32 +0800 Subject: [PATCH 10/10] Update README.md to fix hyperlink of Roadmap --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a72210a5b..df749891b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ It supports [RSS](https://cyber.harvard.edu/rss/rss.html), [Atom](https://datatr More info: [https://netnewswire.com/](https://netnewswire.com/) -Also see the [Technotes](Technotes/) and the [Roadmap](Technotes/Roadmap.md). +Also see the [Technotes](Technotes/) and the [Roadmap/Milestones](https://github.com/Ranchero-Software/NetNewsWire/milestones). Note: NetNewsWire’s Help menu has a bunch of these links, so you don’t have to remember to come back to this page.