From 6be6c6a682ff9fdd909c2752d44cd623f6501f76 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 15 May 2019 11:52:56 -0500 Subject: [PATCH] Add send sync database contents to Feedbin --- Frameworks/Account/Account.swift | 12 ++- .../Account/Account.xcodeproj/project.pbxproj | 8 ++ Frameworks/Account/AccountDelegate.swift | 3 + .../Account/Feedbin/FeedbinAPICaller.swift | 44 ++++++++-- .../Feedbin/FeedbinAccountDelegate.swift | 82 +++++++++++++++++-- .../Account/Feedbin/FeedbinStarredEntry.swift | 19 +++++ .../Account/Feedbin/FeedbinUnreadEntry.swift | 19 +++++ .../LocalAccount/LocalAccountDelegate.swift | 8 ++ Frameworks/SyncDatabase/SyncDatabase.swift | 12 +++ Frameworks/SyncDatabase/SyncStatus.swift | 4 +- Frameworks/SyncDatabase/SyncStatusTable.swift | 59 ++++++++++++- submodules/RSCore | 2 +- 12 files changed, 252 insertions(+), 20 deletions(-) create mode 100644 Frameworks/Account/Feedbin/FeedbinStarredEntry.swift create mode 100644 Frameworks/Account/Feedbin/FeedbinUnreadEntry.swift diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 29ce4694c..0f776ef35 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -286,10 +286,18 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } - public func refreshAll(completionHandler completion: (() -> Void)? = nil) { - delegate.refreshAll(for: self, completion: completion) + public func refreshAll(completion: (() -> Void)? = nil) { + self.delegate.refreshAll(for: self, completion: completion) } + public func syncArticleStatus(completion: (() -> Void)? = nil) { + delegate.sendArticleStatus(for: self) { [unowned self] in + self.delegate.refreshArticleStatus(for: self) { + completion?() + } + } + } + public func importOPML(_ opmlFile: URL, completion: @escaping (Result) -> Void) { delegate.importOPML(for: self, opmlFile: opmlFile) { [weak self] result in switch result { diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index db16770ea..bb4424072 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ 51D5875C227F630B00900287 /* tags_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58759227F630B00900287 /* tags_initial.json */; }; 51D5875E227F643C00900287 /* AccountFolderSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */; }; 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E490352288C37100C791F0 /* FeedbinDate.swift */; }; + 51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; }; + 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; }; 841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973EF1F6DD19E006346C4 /* RSCore.framework */; }; 841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973FA1F6DD1AC006346C4 /* RSParser.framework */; }; 841974011F6DD1EC006346C4 /* Folder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841974001F6DD1EC006346C4 /* Folder.swift */; }; @@ -128,6 +130,8 @@ 51D58759227F630B00900287 /* tags_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_initial.json; sourceTree = ""; }; 51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderSyncTest.swift; sourceTree = ""; }; 51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = ""; }; + 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = ""; }; + 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = ""; }; 841973E81F6DD19E006346C4 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = ""; }; 841973F41F6DD1AC006346C4 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; 841974001F6DD1EC006346C4 /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = ""; }; @@ -251,9 +255,11 @@ 51E490352288C37100C791F0 /* FeedbinDate.swift */, 84CAD7151FDF2E22000F0755 /* FeedbinEntry.swift */, 5133230F22810E5700C30F19 /* FeedbinIcon.swift */, + 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */, 84245C841FDDD8CB0074AFBB /* FeedbinSubscription.swift */, 51D58754227F53BE00900287 /* FeedbinTag.swift */, 84D09622217418DC00D77525 /* FeedbinTagging.swift */, + 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */, ); path = Feedbin; sourceTree = ""; @@ -506,6 +512,7 @@ 84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */, 8469F81C1F6DD15E0084783E /* Account.swift in Sources */, 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */, + 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */, 846E77451F6EF9B900A165E2 /* Container.swift in Sources */, 84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */, 841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */, @@ -519,6 +526,7 @@ 84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */, 5133231122810EB200C30F19 /* FeedbinIcon.swift in Sources */, 846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */, + 51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */, 5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */, 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */, 84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */, diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index b13a76a01..ffdc6e0c0 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -21,6 +21,9 @@ protocol AccountDelegate { var refreshProgress: DownloadProgress { get } func refreshAll(for account: Account, completion: (() -> Void)?) + func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) + func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) + func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) diff --git a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift index 3701abb30..17a043433 100644 --- a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift +++ b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift @@ -42,7 +42,7 @@ final class FeedbinAPICaller: NSObject { self.transport = transport } - func validateCredentials(completionHandler completion: @escaping (Result) -> Void) { + func validateCredentials(completion: @escaping (Result) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("authentication.json") let request = URLRequest(url: callURL, credentials: credentials) @@ -67,7 +67,7 @@ final class FeedbinAPICaller: NSObject { } - func retrieveTags(completionHandler completion: @escaping (Result<[FeedbinTag]?, Error>) -> Void) { + func retrieveTags(completion: @escaping (Result<[FeedbinTag]?, Error>) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("tags.json") let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] @@ -113,7 +113,7 @@ final class FeedbinAPICaller: NSObject { } - func retrieveSubscriptions(completionHandler completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) { + func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json") let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.subscriptions] @@ -133,7 +133,7 @@ final class FeedbinAPICaller: NSObject { } - func createSubscription(url: String, completionHandler completion: @escaping (Result) -> Void) { + func createSubscription(url: String, completion: @escaping (Result) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json") var request = URLRequest(url: callURL, credentials: credentials) @@ -218,7 +218,7 @@ final class FeedbinAPICaller: NSObject { transport.send(request: request, method: HTTPMethod.delete, completion: completion) } - func retrieveTaggings(completionHandler completion: @escaping (Result<[FeedbinTagging]?, Error>) -> Void) { + func retrieveTaggings(completion: @escaping (Result<[FeedbinTagging]?, Error>) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("taggings.json") let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.taggings] @@ -279,7 +279,7 @@ final class FeedbinAPICaller: NSObject { transport.send(request: request, method: HTTPMethod.delete, completion: completion) } - func retrieveIcons(completionHandler completion: @escaping (Result<[FeedbinIcon]?, Error>) -> Void) { + func retrieveIcons(completion: @escaping (Result<[FeedbinIcon]?, Error>) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("icons.json") let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.icons] @@ -403,7 +403,7 @@ final class FeedbinAPICaller: NSObject { } - func retrieveUnreadEntries(completionHandler completion: @escaping (Result<[Int]?, Error>) -> Void) { + func retrieveUnreadEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("unread_entries.json") let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.unreadEntries] @@ -423,7 +423,21 @@ final class FeedbinAPICaller: NSObject { } - func retrieveStarredEntries(completionHandler completion: @escaping (Result<[Int]?, Error>) -> Void) { + func createUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { + let callURL = feedbinBaseURL.appendingPathComponent("unread_entries.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = FeedbinUnreadEntry(unreadEntries: entries) + transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) + } + + func deleteUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { + let callURL = feedbinBaseURL.appendingPathComponent("unread_entries.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = FeedbinUnreadEntry(unreadEntries: entries) + transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion) + } + + func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("starred_entries.json") let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.starredEntries] @@ -443,6 +457,20 @@ final class FeedbinAPICaller: NSObject { } + func createStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { + let callURL = feedbinBaseURL.appendingPathComponent("starred_entries.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = FeedbinStarredEntry(starredEntries: entries) + transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) + } + + func deleteStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { + let callURL = feedbinBaseURL.appendingPathComponent("starred_entries.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = FeedbinStarredEntry(starredEntries: entries) + transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion) + } + } // MARK: Private diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index c3e788243..a49e9a55a 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -104,8 +104,48 @@ final class FeedbinAccountDelegate: AccountDelegate { } - func refreshArticleStatus(for account: Account, completion: (() -> Void)? = nil) { + func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) { + os_log(.debug, log: log, "Sending article statuses...") + + let syncStatuses = database.selectForProcessing() + let createUnreadStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.read && $0.flag == false } + let deleteUnreadStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.read && $0.flag == true } + let createStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == true } + let deleteStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == false } + + let group = DispatchGroup() + + group.enter() + sendArticleStatuses(createUnreadStatuses, apiCall: caller.createUnreadEntries) { + group.leave() + } + + group.enter() + sendArticleStatuses(deleteUnreadStatuses, apiCall: caller.deleteUnreadEntries) { + group.leave() + } + + group.enter() + sendArticleStatuses(createStarredStatuses, apiCall: caller.createStarredEntries) { + group.leave() + } + + group.enter() + sendArticleStatuses(deleteStarredStatuses, apiCall: caller.deleteStarredEntries) { + group.leave() + } + + group.notify(queue: DispatchQueue.main) { [weak self] in + guard let self = self else { return } + os_log(.debug, log: self.log, "Done sending article statuses.") + completion() + } + + } + + func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) { + os_log(.debug, log: log, "Refreshing article statuses...") let group = DispatchGroup() @@ -139,7 +179,7 @@ final class FeedbinAccountDelegate: AccountDelegate { group.notify(queue: DispatchQueue.main) { [weak self] in guard let self = self else { return } os_log(.debug, log: self.log, "Done refreshing article statuses.") - completion?() + completion() } } @@ -701,6 +741,36 @@ private extension FeedbinAccountDelegate { } + + func sendArticleStatuses(_ statuses: [SyncStatus], + apiCall: ([Int], @escaping (Result) -> Void) -> Void, + completion: @escaping (() -> Void)) { + + guard !statuses.isEmpty else { + completion() + return + } + + let articleIDs = statuses.compactMap { Int($0.articleID) } + let articleIDGroups = articleIDs.chunked(into: 1000) + for articleIDGroup in articleIDGroups { + + apiCall(articleIDGroup) { [weak self] result in + switch result { + case .success: + self?.database.deleteSelectedForProcessing(articleIDGroup.map { String($0) } ) + completion() + case .failure(let error): + guard let self = self else { return } + os_log(.error, log: self.log, "Article status sync call failed: %@.", error.localizedDescription) + self.database.resetSelectedForProcessing(articleIDGroup.map { String($0) } ) + completion() + } + } + + } + } + func importOPMLItems(_ account: Account, items: [RSOPMLItem], parentFolder: Folder?) { items.forEach { (item) in @@ -1058,14 +1128,14 @@ private extension FeedbinAccountDelegate { let deltaUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs) let markUnreadArticles = account.fetchArticles(forArticleIDs: deltaUnreadArticleIDs) DispatchQueue.main.async { - _ = account.markArticles(markUnreadArticles, statusKey: .read, flag: false) + _ = account.update(markUnreadArticles, statusKey: .read, flag: false) } // Mark articles as read let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(feedbinUnreadArticleIDs) let markReadArticles = account.fetchArticles(forArticleIDs: deltaReadArticleIDs) DispatchQueue.main.async { - _ = account.markArticles(markReadArticles, statusKey: .read, flag: true) + _ = account.update(markReadArticles, statusKey: .read, flag: true) } // Save any unread statuses for articles we haven't yet received @@ -1092,14 +1162,14 @@ private extension FeedbinAccountDelegate { let deltaStarredArticleIDs = feedbinStarredArticleIDs.subtracting(currentStarredArticleIDs) let markStarredArticles = account.fetchArticles(forArticleIDs: deltaStarredArticleIDs) DispatchQueue.main.async { - _ = account.markArticles(markStarredArticles, statusKey: .starred, flag: true) + _ = account.update(markStarredArticles, statusKey: .starred, flag: true) } // Mark articles as unstarred let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(feedbinStarredArticleIDs) let markUnstarredArticles = account.fetchArticles(forArticleIDs: deltaUnstarredArticleIDs) DispatchQueue.main.async { - _ = account.markArticles(markUnstarredArticles, statusKey: .starred, flag: false) + _ = account.update(markUnstarredArticles, statusKey: .starred, flag: false) } // Save any starred statuses for articles we haven't yet received diff --git a/Frameworks/Account/Feedbin/FeedbinStarredEntry.swift b/Frameworks/Account/Feedbin/FeedbinStarredEntry.swift new file mode 100644 index 000000000..a070a89f6 --- /dev/null +++ b/Frameworks/Account/Feedbin/FeedbinStarredEntry.swift @@ -0,0 +1,19 @@ +// +// FeedbinStarredEntry.swift +// Account +// +// Created by Maurice Parker on 5/15/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct FeedbinStarredEntry: Codable { + + let starredEntries: [Int] + + enum CodingKeys: String, CodingKey { + case starredEntries = "starred_entries" + } + +} diff --git a/Frameworks/Account/Feedbin/FeedbinUnreadEntry.swift b/Frameworks/Account/Feedbin/FeedbinUnreadEntry.swift new file mode 100644 index 000000000..ff979a0e9 --- /dev/null +++ b/Frameworks/Account/Feedbin/FeedbinUnreadEntry.swift @@ -0,0 +1,19 @@ +// +// FeedbinUnreadEntry.swift +// Account +// +// Created by Maurice Parker on 5/15/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct FeedbinUnreadEntry: Codable { + + let unreadEntries: [Int] + + enum CodingKeys: String, CodingKey { + case unreadEntries = "unread_entries" + } + +} diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index ea3dd2606..150fbf3ea 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -37,6 +37,14 @@ final class LocalAccountDelegate: AccountDelegate { refresher.refreshFeeds(account.flattenedFeeds()) completion?() } + + func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) { + completion() + } + + func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) { + completion() + } func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { diff --git a/Frameworks/SyncDatabase/SyncDatabase.swift b/Frameworks/SyncDatabase/SyncDatabase.swift index 3afde1eae..a2355c881 100644 --- a/Frameworks/SyncDatabase/SyncDatabase.swift +++ b/Frameworks/SyncDatabase/SyncDatabase.swift @@ -27,6 +27,18 @@ public final class SyncDatabase { syncStatusTable.insertStatuses(statuses) } + public func selectForProcessing() -> [SyncStatus] { + return syncStatusTable.selectForProcessing() + } + + public func resetSelectedForProcessing(_ articleIDs: [String]) { + syncStatusTable.resetSelectedForProcessing(articleIDs) + } + + public func deleteSelectedForProcessing(_ articleIDs: [String]) { + syncStatusTable.deleteSelectedForProcessing(articleIDs) + } + } // MARK: - Private diff --git a/Frameworks/SyncDatabase/SyncStatus.swift b/Frameworks/SyncDatabase/SyncStatus.swift index 1e024f1f9..3b9db3cd9 100644 --- a/Frameworks/SyncDatabase/SyncStatus.swift +++ b/Frameworks/SyncDatabase/SyncStatus.swift @@ -10,7 +10,7 @@ import Foundation import Articles import RSDatabase -public struct SyncStatus { +public struct SyncStatus: Hashable, Equatable { public let articleID: String public let key: ArticleStatus.Key @@ -24,7 +24,7 @@ public struct SyncStatus { self.selected = selected } - public func databaseDictionary() -> DatabaseDictionary? { + public func databaseDictionary() -> DatabaseDictionary { return [DatabaseKey.articleID: articleID, DatabaseKey.key: key.rawValue, DatabaseKey.flag: flag, DatabaseKey.selected: selected] } diff --git a/Frameworks/SyncDatabase/SyncStatusTable.swift b/Frameworks/SyncDatabase/SyncStatusTable.swift index ad0eca26f..cef4c80da 100644 --- a/Frameworks/SyncDatabase/SyncStatusTable.swift +++ b/Frameworks/SyncDatabase/SyncStatusTable.swift @@ -7,6 +7,7 @@ // import Foundation +import Articles import RSDatabase final class SyncStatusTable: DatabaseTable { @@ -18,11 +19,67 @@ final class SyncStatusTable: DatabaseTable { self.queue = queue } + func selectForProcessing() -> [SyncStatus] { + + var statuses: Set? = nil + + self.queue.updateSync { database in + + let updateSQL = "update syncStatus set selected = true" + database.executeUpdate(updateSQL, withArgumentsIn: nil) + + let selectSQL = "select * from syncStatus where selected == true" + if let resultSet = database.executeQuery(selectSQL, withArgumentsIn: nil) { + statuses = resultSet.mapToSet(self.statusWithRow) + } + + } + + return statuses != nil ? Array(statuses!) : [SyncStatus]() + + } + + func resetSelectedForProcessing(_ articleIDs: [String]) { + self.queue.update { database in + let parameters = articleIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! + let updateSQL = "update syncStatus set selected = false where articleID in \(placeholders)" + database.executeUpdate(updateSQL, withArgumentsIn: parameters) + } + } + + func deleteSelectedForProcessing(_ articleIDs: [String]) { + self.queue.update { database in + let parameters = articleIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! + let deleteSQL = "delete from syncStatus where articleID in \(placeholders)" + database.executeUpdate(deleteSQL, withArgumentsIn: parameters) + } + } + func insertStatuses(_ statuses: [SyncStatus]) { self.queue.update { database in - let statusArray = statuses.map { $0.databaseDictionary()! } + let statusArray = statuses.map { $0.databaseDictionary() } self.insertRows(statusArray, insertType: .orReplace, in: database) } } } + +private extension SyncStatusTable { + + func statusWithRow(_ row: FMResultSet) -> SyncStatus? { + + guard let articleID = row.string(forColumn: DatabaseKey.articleID), + let rawKey = row.string(forColumn: DatabaseKey.key), + let key = ArticleStatus.Key(rawValue: rawKey) else { + return nil + } + + let flag = row.bool(forColumn: DatabaseKey.flag) + let selected = row.bool(forColumn: DatabaseKey.selected) + + return SyncStatus(articleID: articleID, key: key, flag: flag, selected: selected) + + } +} diff --git a/submodules/RSCore b/submodules/RSCore index 9c268f00e..4682329b7 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit 9c268f00e93f758a79dae04dd8f18d27449221b0 +Subproject commit 4682329b70eba64f94500885307f99ed8cbcc938