From bedce4946f54d1174beaa742e7c57ca7b7d2226b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 29 Apr 2020 05:24:35 -0500 Subject: [PATCH] Allow for multiple sync status records in sync database. --- .../Account/Account.xcodeproj/project.pbxproj | 4 ++ .../CloudKit/CloudKitAccountDelegate.swift | 7 +- .../CloudKitArticleStatusUpdate.swift | 66 +++++++++++++++++++ .../CloudKit/CloudKitArticlesZone.swift | 61 ++++++----------- 4 files changed, 95 insertions(+), 43 deletions(-) create mode 100644 Frameworks/Account/CloudKit/CloudKitArticleStatusUpdate.swift diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 6573d99b1..0fdcc86f4 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */; }; 5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; }; 5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; }; + 5139A6382459822D004D960C /* CloudKitArticleStatusUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5139A6372459822D004D960C /* CloudKitArticleStatusUpdate.swift */; }; 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */; }; 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */; }; 514BF5202391B0DB00902FE8 /* SingleArticleFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514BF51F2391B0DB00902FE8 /* SingleArticleFetcher.swift */; }; @@ -294,6 +295,7 @@ 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinSyncTest.swift; sourceTree = ""; }; 513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = ""; }; 5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = ""; }; + 5139A6372459822D004D960C /* CloudKitArticleStatusUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitArticleStatusUpdate.swift; sourceTree = ""; }; 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = ""; }; 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = ""; }; 514BF51F2391B0DB00902FE8 /* SingleArticleFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleArticleFetcher.swift; sourceTree = ""; }; @@ -556,6 +558,7 @@ 519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */, 519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */, 5150FFFD243823B800C1A442 /* CloudKitError.swift */, + 5139A6372459822D004D960C /* CloudKitArticleStatusUpdate.swift */, 51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */, 51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */, ); @@ -1158,6 +1161,7 @@ 9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */, 552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */, 9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */, + 5139A6382459822D004D960C /* CloudKitArticleStatusUpdate.swift in Sources */, 9E5EC15B23E01DEF00A4E503 /* FeedlyRTLTextSanitizer.swift in Sources */, 51B36305244B6135000DEF2A /* TwitterEntities.swift in Sources */, 5132DE832449306F00806ADE /* TwitterStatus.swift in Sources */, diff --git a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift index 00fba6788..362bbf66d 100644 --- a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift @@ -120,12 +120,15 @@ final class CloudKitAccountDelegate: AccountDelegate { func processWithArticles(_ articles: Set
) { + let syncStatusesDict = Dictionary(grouping: syncStatuses, by: { $0.articleID }) let articlesDict = articles.reduce(into: [String: Article]()) { result, article in result[article.articleID] = article } - let statusedArticles = syncStatuses.map { ($0, articlesDict[$0.articleID]) } + let statusUpdates = syncStatusesDict.map { (key, value) in + return CloudKitArticleStatusUpdate(articleID: key, statuses: value, article: articlesDict[key]) + } - self.articlesZone.modifyArticles(statusedArticles) { result in + self.articlesZone.modifyArticles(statusUpdates) { result in switch result { case .success: self.database.deleteSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in diff --git a/Frameworks/Account/CloudKit/CloudKitArticleStatusUpdate.swift b/Frameworks/Account/CloudKit/CloudKitArticleStatusUpdate.swift new file mode 100644 index 000000000..66720476a --- /dev/null +++ b/Frameworks/Account/CloudKit/CloudKitArticleStatusUpdate.swift @@ -0,0 +1,66 @@ +// +// CloudKitArticleStatusUpdate.swift +// Account +// +// Created by Maurice Parker on 4/29/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import SyncDatabase +import Articles + +struct CloudKitArticleStatusUpdate { + + enum Record { + case all + case statusOnly + case delete + } + + var articleID: String + var statuses: [SyncStatus] + var article: Article? + + var record: Record { + if statuses.contains(where: { $0.key == .deleted }) { + return .delete + } + + if let article = article { + if statuses.contains(where: { $0.key == .new }) { + return .all + } + if article.status.read == false || article.status.starred == true { + return .all + } + } + + return .statusOnly + } + + var isRead: Bool { + if let article = article { + return article.status.read + } + + if let status = statuses.first(where: { $0.key == .read }) { + return status.flag + } + + return true + } + + var isStarred: Bool { + if let article = article { + return article.status.starred + } + + if let status = statuses.first(where: { $0.key == .starred }) { + return status.flag + } + + return false + } + +} diff --git a/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift b/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift index ef9406dbd..8c84e556d 100644 --- a/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift +++ b/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift @@ -104,8 +104,8 @@ final class CloudKitArticlesZone: CloudKitZone { delete(ckQuery: ckQuery, completion: completion) } - func modifyArticles(_ statusArticles: [(status: SyncStatus, article: Article?)], completion: @escaping ((Result) -> Void)) { - guard !statusArticles.isEmpty else { + func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result) -> Void)) { + guard !statusUpdates.isEmpty else { completion(.success(())) return } @@ -113,25 +113,16 @@ final class CloudKitArticlesZone: CloudKitZone { var modifyRecords = [CKRecord]() var deleteRecordIDs = [CKRecord.ID]() - for statusArticle in statusArticles { - switch (statusArticle.status.key, statusArticle.status.flag) { - case (.new, _): - modifyRecords.append(makeStatusRecord(statusArticle)) - if let article = statusArticle.article { - if article.status.read == false || article.status.starred == true { - modifyRecords.append(makeArticleRecord(article)) - } - } - case (.starred, true), (.read, false): - modifyRecords.append(makeStatusRecord(statusArticle)) - if let article = statusArticle.article { - modifyRecords.append(makeArticleRecord(article)) - } - case (.deleted, true): - deleteRecordIDs.append(CKRecord.ID(recordName: statusID(statusArticle.status.articleID), zoneID: Self.zoneID)) - default: - modifyRecords.append(makeStatusRecord(statusArticle)) - deleteRecordIDs.append(CKRecord.ID(recordName: articleID(statusArticle.status.articleID), zoneID: Self.zoneID)) + for statusUpdate in statusUpdates { + switch statusUpdate.record { + case .all: + modifyRecords.append(makeStatusRecord(statusUpdate)) + modifyRecords.append(makeArticleRecord(statusUpdate.article!)) + case .delete: + deleteRecordIDs.append(CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: Self.zoneID)) + case .statusOnly: + modifyRecords.append(makeStatusRecord(statusUpdate)) + deleteRecordIDs.append(CKRecord.ID(recordName: articleID(statusUpdate.articleID), zoneID: Self.zoneID)) } } @@ -140,7 +131,7 @@ final class CloudKitArticlesZone: CloudKitZone { case .success: completion(.success(())) case .failure(let error): - self.handleModifyArticlesError(error, statusArticles: statusArticles, completion: completion) + self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion) } } } @@ -149,12 +140,12 @@ final class CloudKitArticlesZone: CloudKitZone { private extension CloudKitArticlesZone { - func handleModifyArticlesError(_ error: Error, statusArticles: [(status: SyncStatus, article: Article?)], completion: @escaping ((Result) -> Void)) { + func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result) -> Void)) { if case CloudKitZoneError.userDeletedZone = error { self.createZoneRecord() { result in switch result { case .success: - self.modifyArticles(statusArticles, completion: completion) + self.modifyArticles(statusUpdates, completion: completion) case .failure(let error): completion(.failure(error)) } @@ -183,28 +174,16 @@ private extension CloudKitArticlesZone { return record } - func makeStatusRecord(_ statusArticle: (status: SyncStatus, article: Article?)) -> CKRecord { - let status = statusArticle.status - let recordID = CKRecord.ID(recordName: statusID(status.articleID), zoneID: Self.zoneID) + func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord { + let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: Self.zoneID) let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID) - if let webFeedExternalID = statusArticle.article?.webFeed?.externalID { + if let webFeedExternalID = statusUpdate.article?.webFeed?.externalID { record[CloudKitArticleStatus.Fields.webFeedExternalID] = webFeedExternalID } - if let article = statusArticle.article { - record[CloudKitArticleStatus.Fields.read] = article.status.read ? "1" : "0" - record[CloudKitArticleStatus.Fields.starred] = article.status.starred ? "1" : "0" - } else { - switch status.key { - case .read: - record[CloudKitArticleStatus.Fields.read] = status.flag ? "1" : "0" - case .starred: - record[CloudKitArticleStatus.Fields.starred] = status.flag ? "1" : "0" - default: - break - } - } + record[CloudKitArticleStatus.Fields.read] = statusUpdate.isRead ? "1" : "0" + record[CloudKitArticleStatus.Fields.starred] = statusUpdate.isStarred ? "1" : "0" return record }