diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 0718a94ff..a27cac954 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -79,11 +79,12 @@ 9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */; }; 9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */; }; 9E1D15532334304B00F4944C /* FeedlyGetCollectionStreamOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15522334304B00F4944C /* FeedlyGetCollectionStreamOperation.swift */; }; - 9E1D1555233431A600F4944C /* FeedlySyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D1554233431A600F4944C /* FeedlySyncOperation.swift */; }; + 9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D1554233431A600F4944C /* FeedlyOperation.swift */; }; 9E1D15572334355900F4944C /* FeedlyRequestStreamsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15562334355900F4944C /* FeedlyRequestStreamsOperation.swift */; }; 9E1D155923343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155823343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift */; }; 9E1D155B2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */; }; 9E1D155D233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */; }; + 9E713653233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */; }; 9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */; }; 9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; }; 9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */; }; @@ -91,6 +92,7 @@ 9EAEC626233318400085D7C9 /* FeedlyStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC625233318400085D7C9 /* FeedlyStream.swift */; }; 9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */; }; 9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */; }; + 9EBC31B7233987C1002A567B /* FeedlyArticleStatusCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBC31B6233987C1002A567B /* FeedlyArticleStatusCoordinator.swift */; }; 9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */; }; 9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */; }; 9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */; }; @@ -217,11 +219,12 @@ 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetCollectionsOperation.swift; sourceTree = ""; }; 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMirrorCollectionsAsFoldersOperation.swift; sourceTree = ""; }; 9E1D15522334304B00F4944C /* FeedlyGetCollectionStreamOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetCollectionStreamOperation.swift; sourceTree = ""; }; - 9E1D1554233431A600F4944C /* FeedlySyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncOperation.swift; sourceTree = ""; }; + 9E1D1554233431A600F4944C /* FeedlyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOperation.swift; sourceTree = ""; }; 9E1D15562334355900F4944C /* FeedlyRequestStreamsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRequestStreamsOperation.swift; sourceTree = ""; }; 9E1D155823343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamParsedItemsOperation.swift; sourceTree = ""; }; 9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrganiseParsedItemsByFeedOperation.swift; sourceTree = ""; }; 9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyUpdateAccountFeedsWithItemsOperation.swift; sourceTree = ""; }; + 9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshStreamEntriesStatusOperation.swift; sourceTree = ""; }; 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegate.swift; sourceTree = ""; }; 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollection.swift; sourceTree = ""; }; 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeed.swift; sourceTree = ""; }; @@ -229,6 +232,7 @@ 9EAEC625233318400085D7C9 /* FeedlyStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyStream.swift; sourceTree = ""; }; 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCategory.swift; sourceTree = ""; }; 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrigin.swift; sourceTree = ""; }; + 9EBC31B6233987C1002A567B /* FeedlyArticleStatusCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyArticleStatusCoordinator.swift; sourceTree = ""; }; 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAPICaller.swift; sourceTree = ""; }; 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedlyAccountDelegate+OAuth.swift"; sourceTree = ""; }; 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAuthorizationCodeGranting.swift; sourceTree = ""; }; @@ -441,8 +445,18 @@ 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */, 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */, 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */, + 9E1D1554233431A600F4944C /* FeedlyOperation.swift */, + 9EBC31B6233987C1002A567B /* FeedlyArticleStatusCoordinator.swift */, + 9EBC31B32338AC2E002A567B /* Models */, + 9EBC31B22338AC0F002A567B /* Refresh */, + ); + path = Feedly; + sourceTree = SOURCE_ROOT; + }; + 9EBC31B22338AC0F002A567B /* Refresh */ = { + isa = PBXGroup; + children = ( 9E1D154C233370D800F4944C /* FeedlySyncStrategy.swift */, - 9E1D1554233431A600F4944C /* FeedlySyncOperation.swift */, 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */, 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */, 9E12B01F2334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift */, @@ -451,6 +465,14 @@ 9E1D155823343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift */, 9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */, 9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */, + 9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */, + ); + path = Refresh; + sourceTree = ""; + }; + 9EBC31B32338AC2E002A567B /* Models */ = { + isa = PBXGroup; + children = ( 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */, 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */, 9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */, @@ -458,8 +480,8 @@ 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */, 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */, ); - path = Feedly; - sourceTree = SOURCE_ROOT; + path = Models; + sourceTree = ""; }; D511EEB4202422BB00712EC3 /* xcconfig */ = { isa = PBXGroup; @@ -657,6 +679,7 @@ 552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */, 9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */, 84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */, + 9E713653233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift in Sources */, 841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */, 510BD113232C3E9D002692E4 /* FeedMetadataFile.swift in Sources */, 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */, @@ -702,8 +725,9 @@ 846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */, 515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */, 844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */, - 9E1D1555233431A600F4944C /* FeedlySyncOperation.swift in Sources */, + 9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */, 84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */, + 9EBC31B7233987C1002A567B /* FeedlyArticleStatusCoordinator.swift in Sources */, 84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Frameworks/Account/Feedly/FeedlyAPICaller.swift b/Frameworks/Account/Feedly/FeedlyAPICaller.swift index 092aa36d5..26988165b 100644 --- a/Frameworks/Account/Feedly/FeedlyAPICaller.swift +++ b/Frameworks/Account/Feedly/FeedlyAPICaller.swift @@ -89,7 +89,7 @@ final class FeedlyAPICaller { } } - func getStream(for collection: FeedlyCollection, completionHandler: @escaping (Result) -> ()) { + func getStream(for collection: FeedlyCollection, unreadOnly: Bool = false, completionHandler: @escaping (Result) -> ()) { guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completionHandler(.failure(CredentialsError.incompleteCredentials)) @@ -98,7 +98,8 @@ final class FeedlyAPICaller { var components = baseUrlComponents components.path = "/v3/streams/contents" components.queryItems = [ - URLQueryItem(name: "streamId", value: collection.id) + URLQueryItem(name: "streamId", value: collection.id), + URLQueryItem(name: "unreadOnly", value: unreadOnly ? "true" : "false") ] guard let url = components.url else { @@ -130,7 +131,33 @@ final class FeedlyAPICaller { } } - func markAsRead(articleIds: [String], completionHandler: @escaping (Result) -> ()) { + enum MarkAction { + case read + case unread + case saved + case unsaved + + var actionValue: String { + switch self { + case .read: + return "markAsRead" + case .unread: + return "keepUnread" + case .saved: + return "markAsSaved" + case .unsaved: + return "markAsUnsaved" + } + } + } + + private struct MarkerEntriesBody: Encodable { + let type = "entries" + var action: String + var entryIds: [String] + } + + func mark(_ articleIds: Set, as action: MarkAction, completionHandler: @escaping (Result) -> ()) { guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completionHandler(.failure(CredentialsError.incompleteCredentials)) @@ -144,37 +171,62 @@ final class FeedlyAPICaller { } var request = URLRequest(url: url) + request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - let json: [String: Any] = [ - "action": "markAsRead", - "type": "entries", - "entryIds": articleIds - ] - do { - request.httpBody = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) + let body = MarkerEntriesBody(action: action.actionValue, entryIds: Array(articleIds)) + let encoder = JSONEncoder() + let data = try encoder.encode(body) + request.httpBody = data } catch { return DispatchQueue.main.async { completionHandler(.failure(error)) } } - // URLSession.shared.dataTask(with: request) { (data, response, error) in - // let obj = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) - // let data = try! JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) - // print(String(data: data, encoding: .utf8)!) - // }.resume() - - transport.send(request: request, resultType: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + transport.send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + switch result { + case .success(let (httpResponse, _)): + if httpResponse.statusCode == 200 { + completionHandler(.success(())) + } else { + completionHandler(.failure(URLError(.cannotDecodeContentData))) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } + + func importOpml(_ opmlData: Data, completionHandler: @escaping (Result) -> ()) { + guard let accessToken = credentials?.secret else { + return DispatchQueue.main.async { + completionHandler(.failure(CredentialsError.incompleteCredentials)) + } + } + var components = baseUrlComponents + components.path = "/v3/opml" + + guard let url = components.url else { + fatalError("\(components) does not produce a valid URL.") + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.addValue("text/xml", forHTTPHeaderField: HTTPRequestHeader.contentType) + request.addValue("application/json", forHTTPHeaderField: "Accept-Type") + request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) + request.httpBody = opmlData + + transport.send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (httpResponse, _)): if httpResponse.statusCode == 200 { completionHandler(.success(())) } else { - // tempror completionHandler(.failure(URLError(.cannotDecodeContentData))) } case .failure(let error): diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 79bbef237..97922ac21 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -42,15 +42,12 @@ final class FeedlyAccountDelegate: AccountDelegate { var refreshProgress = DownloadProgress(numberOfTasks: 0) - private let database: SyncDatabase private let caller: FeedlyAPICaller private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feedly") + private let articleStatusCoodinator: FeedlyArticleStatusCoordinator init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API = .default) { - let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3") - database = SyncDatabase(databaseFilePath: databaseFilePath) - if let transport = transport { caller = FeedlyAPICaller(transport: transport, api: api) @@ -73,6 +70,9 @@ final class FeedlyAccountDelegate: AccountDelegate { caller = FeedlyAPICaller(transport: session, api: api) } + articleStatusCoodinator = FeedlyArticleStatusCoordinator(dataFolderPath: dataFolder, + caller: caller, + log: log) } // MARK: Account API @@ -93,17 +93,59 @@ final class FeedlyAccountDelegate: AccountDelegate { } func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) { - os_log(.debug, log: log, "*** SKIPPING SEND ARTICLE STATUS ***") - completion() + // Ensure remote articles have the same status as they do locally. + articleStatusCoodinator.sendArticleStatus(for: account, completion: completion) } + /// 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. + /// + /// Feedly has no API that allows the app to fetch the identifiers of unread articles only. + /// The only way to identify unread articles is to pull all of the article data, + /// which is effectively equivalent of a full refresh. + /// + /// - Parameter account: The account whose articles have a remote status. + /// - Parameter completion: Call on the main queue. func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) { - os_log(.debug, log: log, "*** SKIPPING REFRESH ARTICLE STATUS ***") - completion() + refreshAll(for: account) { _ in + completion() + } } func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result) -> Void) { - fatalError() + let data: Data + + do { + data = try Data(contentsOf: opmlFile) + } catch { + completion(.failure(error)) + return + } + + os_log(.debug, log: log, "Begin importing OPML...") + isOPMLImportInProgress = true + refreshProgress.addToNumberOfTasksAndRemaining(1) + + caller.importOpml(data) { result in + switch result { + case .success: + os_log(.debug, log: self.log, "Import OPML done.") + self.refreshProgress.completeTask() + self.isOPMLImportInProgress = false + DispatchQueue.main.async { + completion(.success(())) + } + case .failure(let error): + os_log(.debug, log: self.log, "Import OPML failed.") + self.refreshProgress.completeTask() + self.isOPMLImportInProgress = false + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } } func addFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { @@ -148,31 +190,21 @@ final class FeedlyAccountDelegate: AccountDelegate { func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set
? { - let log = self.log + let acceptedStatuses = articleStatusCoodinator.articles(articles, + for: account, + didChangeStatus: statusKey, + flag: flag) - switch statusKey { - case .read: - let ids = articles.map { $0.articleID } - caller.markAsRead(articleIds: ids) { result in - switch result { - case .success: - account.update(articles, statusKey: statusKey, flag: flag) - case .failure(let error): - os_log(.debug, log: log, "*** SKIPPING MARKING ARTICLES READ: %@ %@ ***", error as NSError, ids) - } - - } - default: - os_log(.debug, log: log, "*** SKIPPING STATUS UPDATE FOR ARTICLES: %@ ***", articles) - } - - return nil + return acceptedStatuses } - + func accountDidInitialize(_ account: Account) { credentials = try? account.retrieveCredentials(type: .oauthAccessToken) - syncStrategy = FeedlySyncStrategy(account: account, caller: caller, log: log) + syncStrategy = FeedlySyncStrategy(account: account, + caller: caller, + articleStatusCoordinator: articleStatusCoodinator, + log: log) //TODO: Figure out how other accounts get refreshed automatically. refreshAll(for: account) { result in diff --git a/Frameworks/Account/Feedly/FeedlyArticleStatusCoordinator.swift b/Frameworks/Account/Feedly/FeedlyArticleStatusCoordinator.swift new file mode 100644 index 000000000..d776e006d --- /dev/null +++ b/Frameworks/Account/Feedly/FeedlyArticleStatusCoordinator.swift @@ -0,0 +1,125 @@ +// +// FeedlyArticleStatusCoordinator.swift +// Account +// +// Created by Kiel Gillard on 24/9/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import SyncDatabase +import Articles +import os.log + +final class FeedlyArticleStatusCoordinator { + private let database: SyncDatabase + private let log: OSLog + private let caller: FeedlyAPICaller + + init(dataFolderPath: String, caller: FeedlyAPICaller, log: OSLog) { + let databaseFilePath = (dataFolderPath as NSString).appendingPathComponent("Sync.sqlite3") + self.database = SyncDatabase(databaseFilePath: databaseFilePath) + self.log = log + self.caller = caller + } + + /// Stores a status for a particular article locally. + func articles(_ articles: Set
, for account: Account, didChangeStatus statusKey: ArticleStatus.Key, flag: Bool) -> Set
? { + + let syncStatuses = articles.map { article in + return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag) + } + + database.insertStatuses(syncStatuses) + os_log(.debug, log: log, "Marking %@ as %@.", articles.map { $0.title }, syncStatuses) + + if database.selectPendingCount() > 100 { + sendArticleStatus(for: account) + } + + return account.update(articles, statusKey: statusKey, flag: flag) + } + + /// Ensures local articles have the same status as they do remotely. + func refreshArticleStatus(for account: Account, stream: FeedlyStream, collection: FeedlyCollection, completion: @escaping (() -> Void)) { + + guard let folder = account.existingFolder(with: collection.label) else { + completion() + return + } + + let unreadArticleIds = Set( + stream.items + .filter { $0.unread } + .map { $0.id } + ) + let localArticles = folder.fetchArticles() + let localArticleIds = localArticles.articleIDs() + let readArticleIds = localArticleIds.subtracting(unreadArticleIds) + account.update(localArticles.filter { readArticleIds.contains($0.articleID) }, statusKey: .read, flag: true) +// account.ensureStatuses(readArticleIds, true, .read, true) + account.update(localArticles.filter { unreadArticleIds.contains($0.articleID) }, statusKey: .read, flag: false) +// account.ensureStatuses(unreadArticleIds, false, .read, false) + + os_log(.debug, log: log, "Ensured %i UNREAD and %i read article(s) in \"%@\".", unreadArticleIds.count, readArticleIds.count, collection.label) + + completion() + + // TODO: starred + +// group.enter() +// caller.retrieveStarredEntries() { result in +// switch result { +// case .success(let articleIDs): +// self.syncArticleStarredState(account: account, articleIDs: articleIDs) +// group.leave() +// case .failure(let error): +// os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription) +// group.leave() +// } +// +// } + + } + + /// Ensures remote articles have the same status as they do locally. + func sendArticleStatus(for account: Account, completion: (() -> Void)? = nil) { + os_log(.debug, log: log, "Sending article statuses...") + + let pending = database.selectForProcessing() + + let statuses: [(status: ArticleStatus.Key, flag: Bool, action: FeedlyAPICaller.MarkAction)] = [ + (.read, false, .unread), + (.read, true, .read), + (.starred, true, .saved), + (.starred, false, .unsaved), + ] + + let group = DispatchGroup() + + for pairing in statuses { + let articleIds = pending.filter { $0.key == pairing.status && $0.flag == pairing.flag } + guard !articleIds.isEmpty else { + continue + } + + let ids = Set(articleIds.map { $0.articleID }) + let database = self.database + group.enter() + caller.mark(ids, as: pairing.action) { result in + switch result { + case .success: + database.deleteSelectedForProcessing(Array(ids)) + case .failure: + database.resetSelectedForProcessing(Array(ids)) + } + group.leave() + } + } + + group.notify(queue: DispatchQueue.main) { + os_log(.debug, log: self.log, "Done sending article statuses.") + completion?() + } + } +} diff --git a/Frameworks/Account/Feedly/FeedlySyncOperation.swift b/Frameworks/Account/Feedly/FeedlyOperation.swift similarity index 78% rename from Frameworks/Account/Feedly/FeedlySyncOperation.swift rename to Frameworks/Account/Feedly/FeedlyOperation.swift index eb092fe6a..8ede26a1f 100644 --- a/Frameworks/Account/Feedly/FeedlySyncOperation.swift +++ b/Frameworks/Account/Feedly/FeedlyOperation.swift @@ -1,5 +1,5 @@ // -// FeedlySyncOperation.swift +// FeedlyOperation.swift // Account // // Created by Kiel Gillard on 20/9/19. @@ -8,15 +8,15 @@ import Foundation -protocol FeedlySyncOperationDelegate: class { - func feedlySyncOperation(_ operation: FeedlySyncOperation, didFailWith error: Error) +protocol FeedlyOperationDelegate: class { + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) } /// Abstract class common to all the tasks required to ingest content from Feedly into NetNewsWire. /// Each task should try to have a single responsibility so they can be easily composed with others. -class FeedlySyncOperation: Operation { +class FeedlyOperation: Operation { - weak var delegate: FeedlySyncOperationDelegate? + weak var delegate: FeedlyOperationDelegate? func didFinish() { self.isExecutingOperation = false @@ -25,7 +25,7 @@ class FeedlySyncOperation: Operation { func didFinish(_ error: Error) { assert(delegate != nil) - delegate?.feedlySyncOperation(self, didFailWith: error) + delegate?.feedlyOperation(self, didFailWith: error) didFinish() } diff --git a/Frameworks/Account/Feedly/FeedlyCategory.swift b/Frameworks/Account/Feedly/Models/FeedlyCategory.swift similarity index 100% rename from Frameworks/Account/Feedly/FeedlyCategory.swift rename to Frameworks/Account/Feedly/Models/FeedlyCategory.swift diff --git a/Frameworks/Account/Feedly/FeedlyCollection.swift b/Frameworks/Account/Feedly/Models/FeedlyCollection.swift similarity index 100% rename from Frameworks/Account/Feedly/FeedlyCollection.swift rename to Frameworks/Account/Feedly/Models/FeedlyCollection.swift diff --git a/Frameworks/Account/Feedly/FeedlyEntry.swift b/Frameworks/Account/Feedly/Models/FeedlyEntry.swift similarity index 94% rename from Frameworks/Account/Feedly/FeedlyEntry.swift rename to Frameworks/Account/Feedly/Models/FeedlyEntry.swift index c97571c4e..a84cd9b35 100644 --- a/Frameworks/Account/Feedly/FeedlyEntry.swift +++ b/Frameworks/Account/Feedly/Models/FeedlyEntry.swift @@ -59,8 +59,8 @@ struct FeedlyEntry: Decodable { // /// an image URL for this entry. If present, “url” will contain the image URL, “width” and “height” its dimension, and “contentType” its MIME type. // var visual: Image? // -// /// was this entry read by the user? If an Authorization header is not provided, this will always return false. If an Authorization header is provided, it will reflect if the user has read this entry or not. -// var unread: Bool + /// Was this entry read by the user? If an Authorization header is not provided, this will always return false. If an Authorization header is provided, it will reflect if the user has read this entry or not. + var unread: Bool // // /// a list of tag objects (“id” and “label”) that the user added to this entry. This value is only returned if an Authorization header is provided, and at least one tag has been added. If the entry has been explicitly marked as read (not the feed itself), the “global.read” tag will be present. // var tags: [Tag]? diff --git a/Frameworks/Account/Feedly/FeedlyFeed.swift b/Frameworks/Account/Feedly/Models/FeedlyFeed.swift similarity index 100% rename from Frameworks/Account/Feedly/FeedlyFeed.swift rename to Frameworks/Account/Feedly/Models/FeedlyFeed.swift diff --git a/Frameworks/Account/Feedly/FeedlyOrigin.swift b/Frameworks/Account/Feedly/Models/FeedlyOrigin.swift similarity index 100% rename from Frameworks/Account/Feedly/FeedlyOrigin.swift rename to Frameworks/Account/Feedly/Models/FeedlyOrigin.swift diff --git a/Frameworks/Account/Feedly/FeedlyStream.swift b/Frameworks/Account/Feedly/Models/FeedlyStream.swift similarity index 100% rename from Frameworks/Account/Feedly/FeedlyStream.swift rename to Frameworks/Account/Feedly/Models/FeedlyStream.swift diff --git a/Frameworks/Account/Feedly/FeedlyCreateFeedsForCollectionFoldersOperation.swift b/Frameworks/Account/Feedly/Refresh/FeedlyCreateFeedsForCollectionFoldersOperation.swift similarity index 89% rename from Frameworks/Account/Feedly/FeedlyCreateFeedsForCollectionFoldersOperation.swift rename to Frameworks/Account/Feedly/Refresh/FeedlyCreateFeedsForCollectionFoldersOperation.swift index 5b22a2aa1..46c420c08 100644 --- a/Frameworks/Account/Feedly/FeedlyCreateFeedsForCollectionFoldersOperation.swift +++ b/Frameworks/Account/Feedly/Refresh/FeedlyCreateFeedsForCollectionFoldersOperation.swift @@ -10,7 +10,7 @@ import Foundation import os.log /// Single responsibility is to accurately reflect Collections and their Feeds as Folders and their Feeds. -final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlySyncOperation { +final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation { let account: Account let collectionsAndFoldersProvider: FeedlyCollectionsAndFoldersProviding @@ -52,13 +52,9 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlySyncOperation let url = collectionFeed.id let metadata = FeedMetadata(feedID: url) // TODO: More metadata - - // Kiel, I'm commenting this out as we shouldn't be storing the name - // in the feed metadata. It should be stored in the OPML file. - // You can just set the name directly on the feed itself. -// metadata.name = collectionFeed.title - + let feed = Feed(account: account, url: url, metadata: metadata) + feed.name = collectionFeed.title // So the same feed isn't created more than once. localFeeds.insert(feed) diff --git a/Frameworks/Account/Feedly/FeedlyGetCollectionStreamOperation.swift b/Frameworks/Account/Feedly/Refresh/FeedlyGetCollectionStreamOperation.swift similarity index 82% rename from Frameworks/Account/Feedly/FeedlyGetCollectionStreamOperation.swift rename to Frameworks/Account/Feedly/Refresh/FeedlyGetCollectionStreamOperation.swift index 0b34dd23e..60b69f274 100644 --- a/Frameworks/Account/Feedly/FeedlyGetCollectionStreamOperation.swift +++ b/Frameworks/Account/Feedly/Refresh/FeedlyGetCollectionStreamOperation.swift @@ -14,7 +14,7 @@ protocol FeedlyCollectionStreamProviding: class { } /// Single responsibility is to get the stream content of a Collection from Feedly. -final class FeedlyGetCollectionStreamOperation: FeedlySyncOperation, FeedlyCollectionStreamProviding { +final class FeedlyGetCollectionStreamOperation: FeedlyOperation, FeedlyCollectionStreamProviding { private(set) var collection: FeedlyCollection @@ -30,11 +30,13 @@ final class FeedlyGetCollectionStreamOperation: FeedlySyncOperation, FeedlyColle let account: Account let caller: FeedlyAPICaller + let unreadOnly: Bool - init(account: Account, collection: FeedlyCollection, caller: FeedlyAPICaller) { + init(account: Account, collection: FeedlyCollection, caller: FeedlyAPICaller, unreadOnly: Bool = false) { self.account = account self.collection = collection self.caller = caller + self.unreadOnly = unreadOnly } override func main() { @@ -44,7 +46,7 @@ final class FeedlyGetCollectionStreamOperation: FeedlySyncOperation, FeedlyColle } //TODO: Use account metadata to get articles newer than some date. - caller.getStream(for: collection) { result in + caller.getStream(for: collection, unreadOnly: unreadOnly) { result in switch result { case .success(let stream): self.storedStream = stream diff --git a/Frameworks/Account/Feedly/FeedlyGetCollectionsOperation.swift b/Frameworks/Account/Feedly/Refresh/FeedlyGetCollectionsOperation.swift similarity index 64% rename from Frameworks/Account/Feedly/FeedlyGetCollectionsOperation.swift rename to Frameworks/Account/Feedly/Refresh/FeedlyGetCollectionsOperation.swift index 15db32fd1..f2cbacbfb 100644 --- a/Frameworks/Account/Feedly/FeedlyGetCollectionsOperation.swift +++ b/Frameworks/Account/Feedly/Refresh/FeedlyGetCollectionsOperation.swift @@ -7,20 +7,23 @@ // import Foundation +import os.log protocol FeedlyCollectionProviding: class { var collections: [FeedlyCollection] { get } } /// Single responsibility is to get Collections from Feedly. -final class FeedlyGetCollectionsOperation: FeedlySyncOperation, FeedlyCollectionProviding { +final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProviding { let caller: FeedlyAPICaller + let log: OSLog private(set) var collections = [FeedlyCollection]() - init(caller: FeedlyAPICaller) { + init(caller: FeedlyAPICaller, log: OSLog) { self.caller = caller + self.log = log } override func main() { @@ -29,13 +32,17 @@ final class FeedlyGetCollectionsOperation: FeedlySyncOperation, FeedlyCollection return } + os_log(.debug, log: log, "Requesting collections.") + caller.getCollections { result in switch result { case .success(let collections): + os_log(.debug, log: self.log, "Received collections: %@.", collections.map { $0.id }) self.collections = collections self.didFinish() case .failure(let error): + os_log(.debug, log: self.log, "Unable to request collections %@.", error as NSError) self.didFinish(error) } } diff --git a/Frameworks/Account/Feedly/FeedlyGetStreamParsedItemsOperation.swift b/Frameworks/Account/Feedly/Refresh/FeedlyGetStreamParsedItemsOperation.swift similarity index 95% rename from Frameworks/Account/Feedly/FeedlyGetStreamParsedItemsOperation.swift rename to Frameworks/Account/Feedly/Refresh/FeedlyGetStreamParsedItemsOperation.swift index 8acec8d86..9e05b114a 100644 --- a/Frameworks/Account/Feedly/FeedlyGetStreamParsedItemsOperation.swift +++ b/Frameworks/Account/Feedly/Refresh/FeedlyGetStreamParsedItemsOperation.swift @@ -17,7 +17,7 @@ protocol FeedlyStreamParsedItemsProviding: class { } /// Single responsibility is to model articles as ParsedItems for entries in a Collection's stream from Feedly. -final class FeedlyGetStreamParsedItemsOperation: FeedlySyncOperation, FeedlyStreamParsedItemsProviding { +final class FeedlyGetStreamParsedItemsOperation: FeedlyOperation, FeedlyStreamParsedItemsProviding { private let account: Account private let caller: FeedlyAPICaller private let collectionStreamProvider: FeedlyCollectionStreamProviding diff --git a/Frameworks/Account/Feedly/FeedlyMirrorCollectionsAsFoldersOperation.swift b/Frameworks/Account/Feedly/Refresh/FeedlyMirrorCollectionsAsFoldersOperation.swift similarity index 67% rename from Frameworks/Account/Feedly/FeedlyMirrorCollectionsAsFoldersOperation.swift rename to Frameworks/Account/Feedly/Refresh/FeedlyMirrorCollectionsAsFoldersOperation.swift index f1815a1e0..82aaf6ea0 100644 --- a/Frameworks/Account/Feedly/FeedlyMirrorCollectionsAsFoldersOperation.swift +++ b/Frameworks/Account/Feedly/Refresh/FeedlyMirrorCollectionsAsFoldersOperation.swift @@ -7,24 +7,27 @@ // import Foundation +import os.log protocol FeedlyCollectionsAndFoldersProviding: class { var collectionsAndFolders: [(FeedlyCollection, Folder)] { get } } /// Single responsibility is accurately reflect Collections from Feedly as Folders. -final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlySyncOperation, FeedlyCollectionsAndFoldersProviding { +final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyCollectionsAndFoldersProviding { let caller: FeedlyAPICaller let account: Account let collectionsProvider: FeedlyCollectionProviding + let log: OSLog private(set) var collectionsAndFolders = [(FeedlyCollection, Folder)]() - init(account: Account, collectionsProvider: FeedlyCollectionProviding, caller: FeedlyAPICaller) { + init(account: Account, collectionsProvider: FeedlyCollectionProviding, caller: FeedlyAPICaller, log: OSLog) { self.collectionsProvider = collectionsProvider self.account = account self.caller = caller + self.log = log } override func main() { @@ -36,21 +39,16 @@ final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlySyncOperation, Feed let collections = collectionsProvider.collections let pairs = collections.compactMap { collection -> (FeedlyCollection, Folder)? in - for folder in localFolders { - if folder.name == collection.label { - return (collection, folder) - } - } - - guard let newFolder = account.ensureFolder(with: collection.label) else { - assertionFailure("Try debugging why a folder could not be created.") + guard let folder = account.ensureFolder(with: collection.label) else { + assertionFailure("Why wasn't a folder created?") return nil } - - return (collection, newFolder) + folder.externalID = collection.id + return (collection, folder) } collectionsAndFolders = pairs + os_log(.debug, log: log, "Ensured %i folders for %i collections.", pairs.count, collections.count) // Remove folders without a corresponding collection let collectionFolders = Set(pairs.map { $0.1 }) @@ -58,5 +56,7 @@ final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlySyncOperation, Feed for unmatched in foldersWithoutCollections { account.removeFolder(unmatched) } + + os_log(.debug, log: log, "Removed %i folders: %@", foldersWithoutCollections.count, foldersWithoutCollections.map { $0.externalID ?? $0.nameForDisplay }) } } diff --git a/Frameworks/Account/Feedly/FeedlyOrganiseParsedItemsByFeedOperation.swift b/Frameworks/Account/Feedly/Refresh/FeedlyOrganiseParsedItemsByFeedOperation.swift similarity index 94% rename from Frameworks/Account/Feedly/FeedlyOrganiseParsedItemsByFeedOperation.swift rename to Frameworks/Account/Feedly/Refresh/FeedlyOrganiseParsedItemsByFeedOperation.swift index 271c3976e..a58b33abc 100644 --- a/Frameworks/Account/Feedly/FeedlyOrganiseParsedItemsByFeedOperation.swift +++ b/Frameworks/Account/Feedly/Refresh/FeedlyOrganiseParsedItemsByFeedOperation.swift @@ -18,7 +18,7 @@ protocol FeedlyParsedItemsByFeedProviding { } /// Single responsibility is to group articles by their feeds. -final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlySyncOperation, FeedlyParsedItemsByFeedProviding { +final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyParsedItemsByFeedProviding { private let account: Account private let parsedItemsProvider: FeedlyStreamParsedItemsProviding private let log: OSLog diff --git a/Frameworks/Account/Feedly/Refresh/FeedlyRefreshStreamEntriesStatusOperation.swift b/Frameworks/Account/Feedly/Refresh/FeedlyRefreshStreamEntriesStatusOperation.swift new file mode 100644 index 000000000..6789b86fb --- /dev/null +++ b/Frameworks/Account/Feedly/Refresh/FeedlyRefreshStreamEntriesStatusOperation.swift @@ -0,0 +1,38 @@ +// +// FeedlyRefreshStreamEntriesStatusOperation.swift +// Account +// +// Created by Kiel Gillard on 25/9/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log + +/// Single responsibility is to update the read status of articles stored locally with the unread status of the entries in a Collection's stream from Feedly. +final class FeedlyRefreshStreamEntriesStatusOperation: FeedlyOperation { + private let account: Account + private let collectionStreamProvider: FeedlyCollectionStreamProviding + private let log: OSLog + let articleStatusCoordinator: FeedlyArticleStatusCoordinator + + init(account: Account, collectionStreamProvider: FeedlyCollectionStreamProviding, articleStatusCoordinator: FeedlyArticleStatusCoordinator, log: OSLog) { + self.account = account + self.articleStatusCoordinator = articleStatusCoordinator + self.collectionStreamProvider = collectionStreamProvider + self.log = log + } + + override func main() { + guard !isCancelled else { + didFinish() + return + } + + let collection = collectionStreamProvider.collection + let stream = collectionStreamProvider.stream + articleStatusCoordinator.refreshArticleStatus(for: account, stream: stream, collection: collection) { + self.didFinish() + } + } +} diff --git a/Frameworks/Account/Feedly/FeedlyRequestStreamsOperation.swift b/Frameworks/Account/Feedly/Refresh/FeedlyRequestStreamsOperation.swift similarity index 96% rename from Frameworks/Account/Feedly/FeedlyRequestStreamsOperation.swift rename to Frameworks/Account/Feedly/Refresh/FeedlyRequestStreamsOperation.swift index cd2935c48..b3e5cfde6 100644 --- a/Frameworks/Account/Feedly/FeedlyRequestStreamsOperation.swift +++ b/Frameworks/Account/Feedly/Refresh/FeedlyRequestStreamsOperation.swift @@ -15,7 +15,7 @@ protocol FeedlyRequestStreamsOperationDelegate: class { /// Single responsibility is to create one stream request operation for one Feedly collection. /// This is the start of the process of refreshing the entire contents of a Folder. -final class FeedlyRequestStreamsOperation: FeedlySyncOperation { +final class FeedlyRequestStreamsOperation: FeedlyOperation { weak var queueDelegate: FeedlyRequestStreamsOperationDelegate? diff --git a/Frameworks/Account/Feedly/FeedlySyncStrategy.swift b/Frameworks/Account/Feedly/Refresh/FeedlySyncStrategy.swift similarity index 80% rename from Frameworks/Account/Feedly/FeedlySyncStrategy.swift rename to Frameworks/Account/Feedly/Refresh/FeedlySyncStrategy.swift index 4ef714d98..f708faa28 100644 --- a/Frameworks/Account/Feedly/FeedlySyncStrategy.swift +++ b/Frameworks/Account/Feedly/Refresh/FeedlySyncStrategy.swift @@ -14,13 +14,15 @@ final class FeedlySyncStrategy { let account: Account let caller: FeedlyAPICaller let operationQueue: OperationQueue + let articleStatusCoordinator: FeedlyArticleStatusCoordinator let log: OSLog - init(account: Account, caller: FeedlyAPICaller, log: OSLog) { + init(account: Account, caller: FeedlyAPICaller, articleStatusCoordinator: FeedlyArticleStatusCoordinator, log: OSLog) { self.account = account self.caller = caller self.operationQueue = OperationQueue() self.log = log + self.articleStatusCoordinator = articleStatusCoordinator } func cancel() { @@ -34,17 +36,19 @@ final class FeedlySyncStrategy { func startSync(completionHandler: @escaping (Result) -> ()) { guard operationQueue.operationCount == 0 else { os_log(.debug, log: log, "Reqeusted start sync but ignored because a sync is already in progress.") + completionHandler(.success(())) return } // Since the truth is in the cloud, everything hinges of what Collections the user has. - let getCollections = FeedlyGetCollectionsOperation(caller: caller) + let getCollections = FeedlyGetCollectionsOperation(caller: caller, log: log) getCollections.delegate = self // Ensure a folder exists for each Collection, removing Folders without a corresponding Collection. let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: getCollections, - caller: caller) + caller: caller, + log: log) mirrorCollectionsAsFolders.delegate = self mirrorCollectionsAsFolders.addDependency(getCollections) @@ -94,7 +98,7 @@ final class FeedlySyncStrategy { os_log(.debug, log: log, "Sync started: %@", syncId) } - private var finalOperation: Operation? + private weak var finalOperation: Operation? } extension FeedlySyncStrategy: FeedlyRequestStreamsOperationDelegate { @@ -127,21 +131,30 @@ extension FeedlySyncStrategy: FeedlyRequestStreamsOperationDelegate { updateOperation.delegate = self updateOperation.addDependency(groupItemsByFeed) + // Once the articles are in the account, ensure they have the correct status + let ensureUnreadOperation = FeedlyRefreshStreamEntriesStatusOperation(account: account, + collectionStreamProvider: collectionStreamOperation, + articleStatusCoordinator: articleStatusCoordinator, + log: log) + + ensureUnreadOperation.delegate = self + ensureUnreadOperation.addDependency(updateOperation) + // Sync completes successfully when the account has been updated with all the parsedd entries from the stream. if let operation = finalOperation { - operation.addDependency(updateOperation) + operation.addDependency(ensureUnreadOperation) } - let operations = [collectionStreamOperation, parseItemsOperation, groupItemsByFeed, updateOperation] + let operations = [collectionStreamOperation, parseItemsOperation, groupItemsByFeed, updateOperation, ensureUnreadOperation] operationQueue.addOperations(operations, waitUntilFinished: false) } } -extension FeedlySyncStrategy: FeedlySyncOperationDelegate { +extension FeedlySyncStrategy: FeedlyOperationDelegate { - func feedlySyncOperation(_ operation: FeedlySyncOperation, didFailWith error: Error) { - os_log(.debug, log: log, "**** Operation failed! **** %@", error as NSError) + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { + os_log(.debug, log: log, "%@ failed so sync failed with error %@", operation, error.localizedDescription) cancel() startSyncCompletionHandler?(.failure(error)) diff --git a/Frameworks/Account/Feedly/FeedlyUpdateAccountFeedsWithItemsOperation.swift b/Frameworks/Account/Feedly/Refresh/FeedlyUpdateAccountFeedsWithItemsOperation.swift similarity index 91% rename from Frameworks/Account/Feedly/FeedlyUpdateAccountFeedsWithItemsOperation.swift rename to Frameworks/Account/Feedly/Refresh/FeedlyUpdateAccountFeedsWithItemsOperation.swift index 2b13b4c8b..b7ab54c4a 100644 --- a/Frameworks/Account/Feedly/FeedlyUpdateAccountFeedsWithItemsOperation.swift +++ b/Frameworks/Account/Feedly/Refresh/FeedlyUpdateAccountFeedsWithItemsOperation.swift @@ -11,7 +11,7 @@ import RSParser import os.log /// Single responsibility is to combine the articles with their feeds for a specific account. -final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlySyncOperation { +final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation { private let account: Account private let organisedItemsProvider: FeedlyParsedItemsByFeedProviding private let log: OSLog @@ -40,7 +40,7 @@ final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlySyncOperation { group.enter() os_log(.debug, log: log, "Updating %i items for feed \"%@\" in collection \"%@\"", items.count, feed.nameForDisplay, organisedItemsProvider.collection.label) - account.update(feed, parsedItems: items) { + account.update(feed, parsedItems: items, defaultRead: true) { group.leave() } } diff --git a/Mac/Preferences/Accounts/AccountsFeedlyWebWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedlyWebWindowController.swift index 1e347fcd1..18eb6d262 100644 --- a/Mac/Preferences/Accounts/AccountsFeedlyWebWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsFeedlyWebWindowController.swift @@ -70,7 +70,7 @@ class AccountsFeedlyWebWindowController: NSWindowController, WKNavigationDelegat NSApplication.shared.presentError(error) } catch { - NSApplication.shared.presentError(error) + print(error) } decisionHandler(.allow)