diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index fcbb97a28..088b09f1e 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -56,6 +56,8 @@ 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */; }; 5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; }; 519E84A62433D49000D238B0 /* OPMLNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84A52433D49000D238B0 /* OPMLNormalizer.swift */; }; + 519E84A82434C5EF00D238B0 /* CloudKitArticlesZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */; }; + 519E84AA2434C60400D238B0 /* CloudKitArticlesZoneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84A92434C60400D238B0 /* CloudKitArticlesZoneDelegate.swift */; }; 51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; }; 51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; }; 51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */; }; @@ -290,6 +292,8 @@ 5170743B232AEDB500A461A3 /* OPMLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLFile.swift; sourceTree = ""; }; 518B2EA52351306200400001 /* Account_project_test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_test.xcconfig; sourceTree = ""; }; 519E84A52433D49000D238B0 /* OPMLNormalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLNormalizer.swift; sourceTree = ""; }; + 519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitArticlesZone.swift; sourceTree = ""; }; + 519E84A92434C60400D238B0 /* CloudKitArticlesZoneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitArticlesZoneDelegate.swift; sourceTree = ""; }; 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = ""; }; 51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerIdentifier.swift; sourceTree = ""; }; @@ -522,6 +526,8 @@ 5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */, 51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */, 512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */, + 519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */, + 519E84A92434C60400D238B0 /* CloudKitArticlesZoneDelegate.swift */, 51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */, 51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */, ); @@ -1085,6 +1091,7 @@ 9EA643D5239306AC0018A28C /* FeedlyFeedsSearchResponse.swift in Sources */, 9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */, 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */, + 519E84AA2434C60400D238B0 /* CloudKitArticlesZoneDelegate.swift in Sources */, 512DD4CB2431000600C17B1F /* CKRecord+Extensions.swift in Sources */, 3B826DAF2385C81C00FC1ADB /* FeedWranglerGenericResult.swift in Sources */, 9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */, @@ -1165,6 +1172,7 @@ 51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */, 9E1773D923458D590056A5A8 /* FeedlyResourceId.swift in Sources */, 9EE4CCFA234F106600FBAE4B /* FeedlyFeedContainerValidator.swift in Sources */, + 519E84A82434C5EF00D238B0 /* CloudKitArticlesZone.swift in Sources */, 552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */, 9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */, 9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */, diff --git a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift index 2521a4ff3..77ca681ee 100644 --- a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift @@ -30,8 +30,9 @@ final class CloudKitAccountDelegate: AccountDelegate { return CKContainer(identifier: "iCloud.\(orgID).NetNewsWire") }() - private lazy var zones = [accountZone] + private lazy var zones: [CloudKitZone] = [accountZone, articlesZone] private let accountZone: CloudKitAccountZone + private let articlesZone: CloudKitArticlesZone private let refresher = LocalAccountRefresher() @@ -48,8 +49,11 @@ final class CloudKitAccountDelegate: AccountDelegate { init(dataFolder: String) { accountZone = CloudKitAccountZone(container: container) + articlesZone = CloudKitArticlesZone(container: container) + let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3") database = SyncDatabase(databaseFilePath: databaseFilePath) + accountZone.refreshProgress = refreshProgress } @@ -75,10 +79,29 @@ final class CloudKitAccountDelegate: AccountDelegate { accountZone.fetchChangesInZone() { result in switch result { case .success: - self.refresher.refreshFeeds(account.flattenedWebFeeds()) { - BatchUpdate.shared.end() - account.metadata.lastArticleFetchEndTime = Date() - completion(.success(())) + + self.sendArticleStatus(for: account) { result in + switch result { + case .success: + + self.refreshArticleStatus(for: account) { result in + switch result { + case .success: + + self.refresher.refreshFeeds(account.flattenedWebFeeds()) { + BatchUpdate.shared.end() + account.metadata.lastArticleFetchEndTime = Date() + completion(.success(())) + } + + case .failure(let error): + completion(.failure(error)) + } + } + case .failure(let error): + completion(.failure(error)) + } + } case .failure(let error): BatchUpdate.shared.end() @@ -88,11 +111,44 @@ final class CloudKitAccountDelegate: AccountDelegate { } func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - completion(.success(())) + os_log(.debug, log: log, "Sending article statuses...") + + database.selectForProcessing { result in + + func processStatuses(_ syncStatuses: [SyncStatus]) { + self.articlesZone.sendArticleStatus(syncStatuses) { result in + switch result { + case .success: + os_log(.debug, log: self.log, "Done sending article statuses.") + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + } + + switch result { + case .success(let syncStatuses): + processStatuses(syncStatuses) + case .failure(let databaseError): + completion(.failure(databaseError)) + } + } } + func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - completion(.success(())) + os_log(.debug, log: log, "Refreshing article statuses...") + + articlesZone.fetchChangesInZone() { result in + os_log(.debug, log: self.log, "Done refreshing article statuses.") + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } } func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { @@ -353,6 +409,7 @@ final class CloudKitAccountDelegate: AccountDelegate { func accountDidInitialize(_ account: Account) { accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress) + articlesZone.delegate = CloudKitArticlesZoneDelegate(account: account) if account.externalID == nil { accountZone.findOrCreateAccount() { result in diff --git a/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift b/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift new file mode 100644 index 000000000..27afa65a0 --- /dev/null +++ b/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift @@ -0,0 +1,68 @@ +// +// CloudKitArticlesZone.swift +// Account +// +// Created by Maurice Parker on 4/1/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log +import RSWeb +import CloudKit +import SyncDatabase + +final class CloudKitArticlesZone: CloudKitZone { + + + static var zoneID: CKRecordZone.ID { + return CKRecordZone.ID(zoneName: "Articles", ownerName: CKCurrentUserDefaultName) + } + + var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") + + weak var container: CKContainer? + weak var database: CKDatabase? + weak var refreshProgress: DownloadProgress? = nil + var delegate: CloudKitZoneDelegate? = nil + + struct CloudKitArticleStatus { + static let recordType = "ArticleStatus" + struct Fields { + static let read = "read" + static let starred = "starred" + static let userDeleted = "userDeleted" + } + } + + init(container: CKContainer) { + self.container = container + self.database = container.privateCloudDatabase + } + + func sendArticleStatus(_ syncStatuses: [SyncStatus], completion: @escaping ((Result) -> Void)) { + var records = [String: CKRecord]() + + for status in syncStatuses { + + var record = records[status.articleID] + if record == nil { + let recordID = CKRecord.ID(recordName: status.articleID, zoneID: Self.zoneID) + record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID) + records[status.articleID] = record + } + + switch status.key { + case .read: + record![CloudKitArticleStatus.Fields.read] = status.flag ? "1" : "0" + case .starred: + record![CloudKitArticleStatus.Fields.starred] = status.flag ? "1" : "0" + case .userDeleted: + record![CloudKitArticleStatus.Fields.userDeleted] = status.flag ? "1" : "0" + } + } + + modify(recordsToSave: Array(records.values), recordIDsToDelete: [], completion: completion) + } + +} diff --git a/Frameworks/Account/CloudKit/CloudKitArticlesZoneDelegate.swift b/Frameworks/Account/CloudKit/CloudKitArticlesZoneDelegate.swift new file mode 100644 index 000000000..5c6cd8f44 --- /dev/null +++ b/Frameworks/Account/CloudKit/CloudKitArticlesZoneDelegate.swift @@ -0,0 +1,35 @@ +// +// CloudKitArticlesZoneDelegate.swift +// Account +// +// Created by Maurice Parker on 4/1/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log +import CloudKit + +class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate { + + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") + + weak var account: Account? + + init(account: Account) { + self.account = account + } + + func cloudKitDidChange(record: CKRecord) { +// switch record.recordType { +// case CloudKitAccountZone.CloudKitWebFeed.recordType: +// default: +// assertionFailure("Unknown record type: \(record.recordType)") +// } + } + + func cloudKitDidDelete(recordType: CKRecord.RecordType, recordID: CKRecord.ID) { + // Article downloads clean up old articles and statuses + } + +}