mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Use RSCore 3.
This commit is contained in:
@@ -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")),
|
||||
|
||||
@@ -432,15 +432,8 @@ public enum FetchType {
|
||||
try await delegate.refreshAll(for: self)
|
||||
}
|
||||
|
||||
public func sendArticleStatus(completion: ((Result<Void, Error>) -> 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 {
|
||||
|
||||
@@ -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, Error>) -> Void))
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> 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, Error>) -> Void)
|
||||
|
||||
|
||||
@@ -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, Error>) -> 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, Error>) -> 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, Error>) -> 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, Error>) -> Void)) {
|
||||
|
||||
logger.debug("Refreshing article statuses...")
|
||||
|
||||
|
||||
@@ -76,14 +76,14 @@ final class LocalAccountDelegate: AccountDelegate, Logging {
|
||||
return
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
completion(.success(()))
|
||||
func sendArticleStatus(for account: Account) async throws {
|
||||
return
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
completion(.success(()))
|
||||
func refreshArticleStatus(for account: Account) async throws {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
var fileData: Data?
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,21 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping (Result<Void, 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, Error>) -> ()) {
|
||||
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<Void, Error>) -> ()) {
|
||||
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, Error>) -> ()) {
|
||||
logger.debug("Refreshing story statuses...")
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
@@ -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, Error>) -> 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, Error>) -> 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, Error>) -> 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, Error>) -> Void)) {
|
||||
logger.debug("Refreshing article statuses...")
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, Error>) -> Void)) {
|
||||
sendArticleStatus(for: account, showProgress: false, completion: completion)
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> 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, Error>) -> 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, Error>) -> 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, Error>) -> 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, Error>) -> 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<Feed>, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func combinedRefresh(_ account: Account, _ feeds: Set<Feed>) 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<Feed>, completion: @escaping (Result<Void, Error>) -> 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<Feed, Error>) -> Void) {
|
||||
|
||||
@@ -739,12 +808,13 @@ private extension CloudKitAccountDelegate {
|
||||
case .success(let articles):
|
||||
self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
|
||||
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, Error>) -> 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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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<String, Error>) -> 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<String, Error>) -> Void) {
|
||||
let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID())
|
||||
record[CloudKitContainer.Fields.name] = name
|
||||
|
||||
@@ -31,32 +31,19 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
self.articlesZone = articlesZone
|
||||
}
|
||||
|
||||
@MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> 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, Error>) -> 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
|
||||
|
||||
@@ -84,7 +84,30 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
self.save(compressedRecords, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainActor func saveNewArticles(_ articles: Set<Article>) 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, Error>) -> Void)) {
|
||||
let predicate = NSPredicate(format: "webFeedExternalID = %@", feedExternalID)
|
||||
let ckQuery = CKQuery(recordType: CloudKitArticleStatus.recordType, predicate: predicate)
|
||||
|
||||
@@ -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, Error>) -> Void) {
|
||||
|
||||
Task { @MainActor in
|
||||
@@ -54,9 +70,6 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CloudKitArticlesZoneDelegate {
|
||||
|
||||
func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Error?) -> Void) {
|
||||
let receivedRecordIDs = recordKeys.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticleStatus.recordType }).map({ $0.recordID })
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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, Error>) -> 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, Error>) -> Void)) {
|
||||
private func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> 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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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" */ = {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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")),
|
||||
],
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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")),
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user