Merge pull request #1058 from kielgillard/master
Send and receive unread statuses
This commit is contained in:
commit
8c3c89030e
|
@ -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 = "<group>"; };
|
||||
9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMirrorCollectionsAsFoldersOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D15522334304B00F4944C /* FeedlyGetCollectionStreamOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetCollectionStreamOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D1554233431A600F4944C /* FeedlySyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D1554233431A600F4944C /* FeedlyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D15562334355900F4944C /* FeedlyRequestStreamsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRequestStreamsOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D155823343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamParsedItemsOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrganiseParsedItemsByFeedOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyUpdateAccountFeedsWithItemsOperation.swift; sourceTree = "<group>"; };
|
||||
9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshStreamEntriesStatusOperation.swift; sourceTree = "<group>"; };
|
||||
9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegate.swift; sourceTree = "<group>"; };
|
||||
9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollection.swift; sourceTree = "<group>"; };
|
||||
9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeed.swift; sourceTree = "<group>"; };
|
||||
|
@ -229,6 +232,7 @@
|
|||
9EAEC625233318400085D7C9 /* FeedlyStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyStream.swift; sourceTree = "<group>"; };
|
||||
9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCategory.swift; sourceTree = "<group>"; };
|
||||
9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrigin.swift; sourceTree = "<group>"; };
|
||||
9EBC31B6233987C1002A567B /* FeedlyArticleStatusCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyArticleStatusCoordinator.swift; sourceTree = "<group>"; };
|
||||
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAPICaller.swift; sourceTree = "<group>"; };
|
||||
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedlyAccountDelegate+OAuth.swift"; sourceTree = "<group>"; };
|
||||
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAuthorizationCodeGranting.swift; sourceTree = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
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;
|
||||
|
|
|
@ -89,7 +89,7 @@ final class FeedlyAPICaller {
|
|||
}
|
||||
}
|
||||
|
||||
func getStream(for collection: FeedlyCollection, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
func getStream(for collection: FeedlyCollection, unreadOnly: Bool = false, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
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<Void, Error>) -> ()) {
|
||||
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<String>, as action: MarkAction, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
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<Void, Error>) -> ()) {
|
||||
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):
|
||||
|
|
|
@ -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, Error>) -> 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<Folder, Error>) -> Void) {
|
||||
|
@ -148,31 +190,21 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
|
||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||
|
||||
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
|
||||
|
|
|
@ -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<Article>, for account: Account, didChangeStatus statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||
|
||||
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?()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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]?
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
|
|
@ -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<Void, Error>) -> ()) {
|
||||
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))
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -70,7 +70,7 @@ class AccountsFeedlyWebWindowController: NSWindowController, WKNavigationDelegat
|
|||
NSApplication.shared.presentError(error)
|
||||
|
||||
} catch {
|
||||
NSApplication.shared.presentError(error)
|
||||
print(error)
|
||||
}
|
||||
|
||||
decisionHandler(.allow)
|
||||
|
|
Loading…
Reference in New Issue