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.