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 82de92c0f..c6c957c66 100644
--- a/Account/Sources/Account/Account.swift
+++ b/Account/Sources/Account/Account.swift
@@ -428,23 +428,16 @@ 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) {
- 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(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,18 +446,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..e1097b460 100644
--- a/Account/Sources/Account/AccountDelegate.swift
+++ b/Account/Sources/Account/AccountDelegate.swift
@@ -25,10 +25,10 @@ import Secrets
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any]) async
- func refreshAll(for account: Account, completion: @escaping (Result) -> Void)
- 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))
+ func refreshAll(for account: Account) async throws
+ func syncArticleStatus(for account: Account) async throws
+ 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 73d33070b..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]
@@ -75,58 +75,73 @@ 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))
- }
- }
-
- }
-
- }
- 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))
+ 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):
- 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)
+ }
}
}
}
- 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")
@@ -188,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 a6759bd15..0e1553c86 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,43 +44,46 @@ 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()
+ }
+ }
+ }
}
- 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)) {
- 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 2b7829ed6..bcada0350 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,25 +118,54 @@ 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 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) 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))
}
}
}
- 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")
@@ -208,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 06a37d45a..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
@@ -166,35 +166,61 @@ public enum ReaderAPIAccountDelegateError: LocalizedError {
}
}
}
-
}
-
}
- func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) {
- 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))
- }
+ 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)
}
- case .failure(let error):
- completion?(.failure(error))
}
}
}
- func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) {
+ func syncArticleStatus(for account: Account) async throws {
+ guard variant != .inoreader else {
+ return
+ }
+
+ 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)
+ }
+ }
+ }
+ }
+
+ 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]) {
@@ -241,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 ab9750e30..92512c4d5 100644
--- a/Account/Sources/Account/AccountManager.swift
+++ b/Account/Sources/Account/AccountManager.swift
@@ -245,37 +245,34 @@ 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()
}
}
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()
}
}
@@ -285,18 +282,17 @@ import RSDatabase
}
}
- public func syncArticleStatusAll(completion: (() -> Void)? = nil) {
- let group = DispatchGroup()
-
- for account in activeAccounts {
- group.enter()
- account.syncArticleStatus() { _ in
- group.leave()
- }
- }
- group.notify(queue: DispatchQueue.global(qos: .background)) {
- completion?()
+ public func syncArticleStatusAll() async {
+
+ 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 063778e55..3566058e9 100644
--- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift
+++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift
@@ -78,56 +78,54 @@ 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) {
- 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 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
+
+ let op = CloudKitReceiveStatusOperation(articlesZone: self.articlesZone)
+ op.completionBlock = { mainThreadOperaion in
+ if mainThreadOperaion.isCanceled {
+ continuation.resume(throwing: CloudKitAccountDelegateError.unknown)
+ } else {
+ continuation.resume()
}
- case .failure(let error):
- completion?(.failure(error))
}
+
+ 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(()))
@@ -444,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):
@@ -507,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
@@ -573,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:
@@ -601,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) {
@@ -729,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)")
}
}
}
@@ -795,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 73ed72576..188af7369 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,51 +125,62 @@ 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()
- }
-
- self?.logger.debug("Sync took \(-date.timeIntervalSinceNow, privacy: .public) seconds.")
- completion(result)
- }
-
- currentSyncAllOperation = syncAllOperation
-
- operationQueue.add(syncAllOperation)
- }
-
- 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))
- }
+
+ 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)
}
- 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))
}
+
+ currentSyncAllOperation = syncAllOperation
+
+ operationQueue.add(syncAllOperation)
}
}
- func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) {
+ 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.
- 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(()))
+
+ try await withCheckedThrowingContinuation { continuation 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:
+ continuation.resume()
+ case .failure(let error):
+ continuation.resume(throwing: error)
+ }
}
}
- operationQueue.add(send)
}
/// Attempts to ensure local articles have the same status as they do remotely.
@@ -180,7 +189,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
///
/// - 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(()))
}
@@ -550,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/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!
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/Mac/AppDelegate.swift b/Mac/AppDelegate.swift
index 4cc1e907c..912b1b723 100644
--- a/Mac/AppDelegate.swift
+++ b/Mac/AppDelegate.swift
@@ -581,7 +581,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/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift
index e5d814b3c..1ed8d2ddd 100644
--- a/Mac/MainWindow/Detail/DetailWebViewController.swift
+++ b/Mac/MainWindow/Detail/DetailWebViewController.swift
@@ -100,6 +100,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)
@@ -323,7 +335,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/Mac/MainWindow/MainWindow.swift b/Mac/MainWindow/MainWindow.swift
index 18409f76a..6dd8abf8f 100644
--- a/Mac/MainWindow/MainWindow.swift
+++ b/Mac/MainWindow/MainWindow.swift
@@ -14,19 +14,31 @@ 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
+ }
}
}
+
}
}
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/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj
index 67839fd41..46587d44f 100644
--- a/NetNewsWire.xcodeproj/project.pbxproj
+++ b/NetNewsWire.xcodeproj/project.pbxproj
@@ -5296,7 +5296,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/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.
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/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;
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)
+ }
}
}
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()
+ }
}
-
}
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")),
]
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 @@
-
-
-
-