Merge pull request #1058 from kielgillard/master

Send and receive unread statuses
This commit is contained in:
Maurice Parker 2019-09-26 18:15:56 -05:00 committed by GitHub
commit 8c3c89030e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 389 additions and 100 deletions

View File

@ -79,11 +79,12 @@
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */; }; 9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */; };
9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */; }; 9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */; };
9E1D15532334304B00F4944C /* FeedlyGetCollectionStreamOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15522334304B00F4944C /* FeedlyGetCollectionStreamOperation.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 */; }; 9E1D15572334355900F4944C /* FeedlyRequestStreamsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15562334355900F4944C /* FeedlyRequestStreamsOperation.swift */; };
9E1D155923343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155823343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift */; }; 9E1D155923343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155823343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift */; };
9E1D155B2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */; }; 9E1D155B2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */; };
9E1D155D233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.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 */; }; 9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */; };
9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; }; 9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; };
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.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 */; }; 9EAEC626233318400085D7C9 /* FeedlyStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC625233318400085D7C9 /* FeedlyStream.swift */; };
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */; }; 9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */; };
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.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 */; }; 9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */; };
9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAuthorizationCodeGranting.swift; sourceTree = "<group>"; };
@ -441,8 +445,18 @@
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */, 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */,
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */, 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */,
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.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 */, 9E1D154C233370D800F4944C /* FeedlySyncStrategy.swift */,
9E1D1554233431A600F4944C /* FeedlySyncOperation.swift */,
9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */, 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */,
9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */, 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */,
9E12B01F2334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift */, 9E12B01F2334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift */,
@ -451,6 +465,14 @@
9E1D155823343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift */, 9E1D155823343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift */,
9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */, 9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */,
9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */, 9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */,
9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */,
);
path = Refresh;
sourceTree = "<group>";
};
9EBC31B32338AC2E002A567B /* Models */ = {
isa = PBXGroup;
children = (
9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */, 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */,
9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */, 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */,
9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */, 9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */,
@ -458,8 +480,8 @@
9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */, 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */,
9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */, 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */,
); );
path = Feedly; path = Models;
sourceTree = SOURCE_ROOT; sourceTree = "<group>";
}; };
D511EEB4202422BB00712EC3 /* xcconfig */ = { D511EEB4202422BB00712EC3 /* xcconfig */ = {
isa = PBXGroup; isa = PBXGroup;
@ -657,6 +679,7 @@
552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */, 552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */,
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */, 9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */,
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */, 84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
9E713653233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift in Sources */,
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */, 841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
510BD113232C3E9D002692E4 /* FeedMetadataFile.swift in Sources */, 510BD113232C3E9D002692E4 /* FeedMetadataFile.swift in Sources */,
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */, 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
@ -702,8 +725,9 @@
846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */, 846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */,
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */, 515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */,
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */, 844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */,
9E1D1555233431A600F4944C /* FeedlySyncOperation.swift in Sources */, 9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */,
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */, 84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
9EBC31B7233987C1002A567B /* FeedlyArticleStatusCoordinator.swift in Sources */,
84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */, 84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@ -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 { guard let accessToken = credentials?.secret else {
return DispatchQueue.main.async { return DispatchQueue.main.async {
completionHandler(.failure(CredentialsError.incompleteCredentials)) completionHandler(.failure(CredentialsError.incompleteCredentials))
@ -98,7 +98,8 @@ final class FeedlyAPICaller {
var components = baseUrlComponents var components = baseUrlComponents
components.path = "/v3/streams/contents" components.path = "/v3/streams/contents"
components.queryItems = [ 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 { 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 { guard let accessToken = credentials?.secret else {
return DispatchQueue.main.async { return DispatchQueue.main.async {
completionHandler(.failure(CredentialsError.incompleteCredentials)) completionHandler(.failure(CredentialsError.incompleteCredentials))
@ -144,37 +171,62 @@ final class FeedlyAPICaller {
} }
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
let json: [String: Any] = [
"action": "markAsRead",
"type": "entries",
"entryIds": articleIds
]
do { 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 { } catch {
return DispatchQueue.main.async { return DispatchQueue.main.async {
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
// URLSession.shared.dataTask(with: request) { (data, response, error) in transport.send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
// let obj = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) switch result {
// let data = try! JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) case .success(let (httpResponse, _)):
// print(String(data: data, encoding: .utf8)!) if httpResponse.statusCode == 200 {
// }.resume() completionHandler(.success(()))
} else {
transport.send(request: request, resultType: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in 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 { switch result {
case .success(let (httpResponse, _)): case .success(let (httpResponse, _)):
if httpResponse.statusCode == 200 { if httpResponse.statusCode == 200 {
completionHandler(.success(())) completionHandler(.success(()))
} else { } else {
// tempror
completionHandler(.failure(URLError(.cannotDecodeContentData))) completionHandler(.failure(URLError(.cannotDecodeContentData)))
} }
case .failure(let error): case .failure(let error):

View File

@ -42,15 +42,12 @@ final class FeedlyAccountDelegate: AccountDelegate {
var refreshProgress = DownloadProgress(numberOfTasks: 0) var refreshProgress = DownloadProgress(numberOfTasks: 0)
private let database: SyncDatabase
private let caller: FeedlyAPICaller private let caller: FeedlyAPICaller
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feedly") private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feedly")
private let articleStatusCoodinator: FeedlyArticleStatusCoordinator
init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API = .default) { 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 { if let transport = transport {
caller = FeedlyAPICaller(transport: transport, api: api) caller = FeedlyAPICaller(transport: transport, api: api)
@ -73,6 +70,9 @@ final class FeedlyAccountDelegate: AccountDelegate {
caller = FeedlyAPICaller(transport: session, api: api) caller = FeedlyAPICaller(transport: session, api: api)
} }
articleStatusCoodinator = FeedlyArticleStatusCoordinator(dataFolderPath: dataFolder,
caller: caller,
log: log)
} }
// MARK: Account API // MARK: Account API
@ -93,17 +93,59 @@ final class FeedlyAccountDelegate: AccountDelegate {
} }
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) { func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
os_log(.debug, log: log, "*** SKIPPING SEND ARTICLE STATUS ***") // Ensure remote articles have the same status as they do locally.
completion() 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)) { func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
os_log(.debug, log: log, "*** SKIPPING REFRESH ARTICLE STATUS ***") refreshAll(for: account) { _ in
completion() completion()
} }
}
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) { 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) { 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>? { 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 { return acceptedStatuses
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
} }
func accountDidInitialize(_ account: Account) { func accountDidInitialize(_ account: Account) {
credentials = try? account.retrieveCredentials(type: .oauthAccessToken) 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. //TODO: Figure out how other accounts get refreshed automatically.
refreshAll(for: account) { result in refreshAll(for: account) { result in

View File

@ -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?()
}
}
}

View File

@ -1,5 +1,5 @@
// //
// FeedlySyncOperation.swift // FeedlyOperation.swift
// Account // Account
// //
// Created by Kiel Gillard on 20/9/19. // Created by Kiel Gillard on 20/9/19.
@ -8,15 +8,15 @@
import Foundation import Foundation
protocol FeedlySyncOperationDelegate: class { protocol FeedlyOperationDelegate: class {
func feedlySyncOperation(_ operation: FeedlySyncOperation, didFailWith error: Error) func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error)
} }
/// Abstract class common to all the tasks required to ingest content from Feedly into NetNewsWire. /// 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. /// 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() { func didFinish() {
self.isExecutingOperation = false self.isExecutingOperation = false
@ -25,7 +25,7 @@ class FeedlySyncOperation: Operation {
func didFinish(_ error: Error) { func didFinish(_ error: Error) {
assert(delegate != nil) assert(delegate != nil)
delegate?.feedlySyncOperation(self, didFailWith: error) delegate?.feedlyOperation(self, didFailWith: error)
didFinish() didFinish()
} }

View File

@ -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. // /// 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? // 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. /// 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 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. // /// 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]? // var tags: [Tag]?

View File

@ -10,7 +10,7 @@ import Foundation
import os.log import os.log
/// Single responsibility is to accurately reflect Collections and their Feeds as Folders and their Feeds. /// 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 account: Account
let collectionsAndFoldersProvider: FeedlyCollectionsAndFoldersProviding let collectionsAndFoldersProvider: FeedlyCollectionsAndFoldersProviding
@ -53,12 +53,8 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlySyncOperation
let metadata = FeedMetadata(feedID: url) let metadata = FeedMetadata(feedID: url)
// TODO: More metadata // 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) let feed = Feed(account: account, url: url, metadata: metadata)
feed.name = collectionFeed.title
// So the same feed isn't created more than once. // So the same feed isn't created more than once.
localFeeds.insert(feed) localFeeds.insert(feed)

View File

@ -14,7 +14,7 @@ protocol FeedlyCollectionStreamProviding: class {
} }
/// Single responsibility is to get the stream content of a Collection from Feedly. /// 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 private(set) var collection: FeedlyCollection
@ -30,11 +30,13 @@ final class FeedlyGetCollectionStreamOperation: FeedlySyncOperation, FeedlyColle
let account: Account let account: Account
let caller: FeedlyAPICaller 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.account = account
self.collection = collection self.collection = collection
self.caller = caller self.caller = caller
self.unreadOnly = unreadOnly
} }
override func main() { override func main() {
@ -44,7 +46,7 @@ final class FeedlyGetCollectionStreamOperation: FeedlySyncOperation, FeedlyColle
} }
//TODO: Use account metadata to get articles newer than some date. //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 { switch result {
case .success(let stream): case .success(let stream):
self.storedStream = stream self.storedStream = stream

View File

@ -7,20 +7,23 @@
// //
import Foundation import Foundation
import os.log
protocol FeedlyCollectionProviding: class { protocol FeedlyCollectionProviding: class {
var collections: [FeedlyCollection] { get } var collections: [FeedlyCollection] { get }
} }
/// Single responsibility is to get Collections from Feedly. /// Single responsibility is to get Collections from Feedly.
final class FeedlyGetCollectionsOperation: FeedlySyncOperation, FeedlyCollectionProviding { final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProviding {
let caller: FeedlyAPICaller let caller: FeedlyAPICaller
let log: OSLog
private(set) var collections = [FeedlyCollection]() private(set) var collections = [FeedlyCollection]()
init(caller: FeedlyAPICaller) { init(caller: FeedlyAPICaller, log: OSLog) {
self.caller = caller self.caller = caller
self.log = log
} }
override func main() { override func main() {
@ -29,13 +32,17 @@ final class FeedlyGetCollectionsOperation: FeedlySyncOperation, FeedlyCollection
return return
} }
os_log(.debug, log: log, "Requesting collections.")
caller.getCollections { result in caller.getCollections { result in
switch result { switch result {
case .success(let collections): case .success(let collections):
os_log(.debug, log: self.log, "Received collections: %@.", collections.map { $0.id })
self.collections = collections self.collections = collections
self.didFinish() self.didFinish()
case .failure(let error): case .failure(let error):
os_log(.debug, log: self.log, "Unable to request collections %@.", error as NSError)
self.didFinish(error) self.didFinish(error)
} }
} }

View File

@ -17,7 +17,7 @@ protocol FeedlyStreamParsedItemsProviding: class {
} }
/// Single responsibility is to model articles as ParsedItems for entries in a Collection's stream from Feedly. /// 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 account: Account
private let caller: FeedlyAPICaller private let caller: FeedlyAPICaller
private let collectionStreamProvider: FeedlyCollectionStreamProviding private let collectionStreamProvider: FeedlyCollectionStreamProviding

View File

@ -7,24 +7,27 @@
// //
import Foundation import Foundation
import os.log
protocol FeedlyCollectionsAndFoldersProviding: class { protocol FeedlyCollectionsAndFoldersProviding: class {
var collectionsAndFolders: [(FeedlyCollection, Folder)] { get } var collectionsAndFolders: [(FeedlyCollection, Folder)] { get }
} }
/// Single responsibility is accurately reflect Collections from Feedly as Folders. /// Single responsibility is accurately reflect Collections from Feedly as Folders.
final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlySyncOperation, FeedlyCollectionsAndFoldersProviding { final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyCollectionsAndFoldersProviding {
let caller: FeedlyAPICaller let caller: FeedlyAPICaller
let account: Account let account: Account
let collectionsProvider: FeedlyCollectionProviding let collectionsProvider: FeedlyCollectionProviding
let log: OSLog
private(set) var collectionsAndFolders = [(FeedlyCollection, Folder)]() 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.collectionsProvider = collectionsProvider
self.account = account self.account = account
self.caller = caller self.caller = caller
self.log = log
} }
override func main() { override func main() {
@ -36,21 +39,16 @@ final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlySyncOperation, Feed
let collections = collectionsProvider.collections let collections = collectionsProvider.collections
let pairs = collections.compactMap { collection -> (FeedlyCollection, Folder)? in let pairs = collections.compactMap { collection -> (FeedlyCollection, Folder)? in
for folder in localFolders { guard let folder = account.ensureFolder(with: collection.label) else {
if folder.name == collection.label { assertionFailure("Why wasn't a folder created?")
return (collection, folder)
}
}
guard let newFolder = account.ensureFolder(with: collection.label) else {
assertionFailure("Try debugging why a folder could not be created.")
return nil return nil
} }
folder.externalID = collection.id
return (collection, newFolder) return (collection, folder)
} }
collectionsAndFolders = pairs collectionsAndFolders = pairs
os_log(.debug, log: log, "Ensured %i folders for %i collections.", pairs.count, collections.count)
// Remove folders without a corresponding collection // Remove folders without a corresponding collection
let collectionFolders = Set(pairs.map { $0.1 }) let collectionFolders = Set(pairs.map { $0.1 })
@ -58,5 +56,7 @@ final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlySyncOperation, Feed
for unmatched in foldersWithoutCollections { for unmatched in foldersWithoutCollections {
account.removeFolder(unmatched) account.removeFolder(unmatched)
} }
os_log(.debug, log: log, "Removed %i folders: %@", foldersWithoutCollections.count, foldersWithoutCollections.map { $0.externalID ?? $0.nameForDisplay })
} }
} }

View File

@ -18,7 +18,7 @@ protocol FeedlyParsedItemsByFeedProviding {
} }
/// Single responsibility is to group articles by their feeds. /// 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 account: Account
private let parsedItemsProvider: FeedlyStreamParsedItemsProviding private let parsedItemsProvider: FeedlyStreamParsedItemsProviding
private let log: OSLog private let log: OSLog

View File

@ -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()
}
}
}

View File

@ -15,7 +15,7 @@ protocol FeedlyRequestStreamsOperationDelegate: class {
/// Single responsibility is to create one stream request operation for one Feedly collection. /// 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. /// 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? weak var queueDelegate: FeedlyRequestStreamsOperationDelegate?

View File

@ -14,13 +14,15 @@ final class FeedlySyncStrategy {
let account: Account let account: Account
let caller: FeedlyAPICaller let caller: FeedlyAPICaller
let operationQueue: OperationQueue let operationQueue: OperationQueue
let articleStatusCoordinator: FeedlyArticleStatusCoordinator
let log: OSLog let log: OSLog
init(account: Account, caller: FeedlyAPICaller, log: OSLog) { init(account: Account, caller: FeedlyAPICaller, articleStatusCoordinator: FeedlyArticleStatusCoordinator, log: OSLog) {
self.account = account self.account = account
self.caller = caller self.caller = caller
self.operationQueue = OperationQueue() self.operationQueue = OperationQueue()
self.log = log self.log = log
self.articleStatusCoordinator = articleStatusCoordinator
} }
func cancel() { func cancel() {
@ -34,17 +36,19 @@ final class FeedlySyncStrategy {
func startSync(completionHandler: @escaping (Result<Void, Error>) -> ()) { func startSync(completionHandler: @escaping (Result<Void, Error>) -> ()) {
guard operationQueue.operationCount == 0 else { guard operationQueue.operationCount == 0 else {
os_log(.debug, log: log, "Reqeusted start sync but ignored because a sync is already in progress.") os_log(.debug, log: log, "Reqeusted start sync but ignored because a sync is already in progress.")
completionHandler(.success(()))
return return
} }
// Since the truth is in the cloud, everything hinges of what Collections the user has. // 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 getCollections.delegate = self
// Ensure a folder exists for each Collection, removing Folders without a corresponding Collection. // Ensure a folder exists for each Collection, removing Folders without a corresponding Collection.
let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account,
collectionsProvider: getCollections, collectionsProvider: getCollections,
caller: caller) caller: caller,
log: log)
mirrorCollectionsAsFolders.delegate = self mirrorCollectionsAsFolders.delegate = self
mirrorCollectionsAsFolders.addDependency(getCollections) mirrorCollectionsAsFolders.addDependency(getCollections)
@ -94,7 +98,7 @@ final class FeedlySyncStrategy {
os_log(.debug, log: log, "Sync started: %@", syncId) os_log(.debug, log: log, "Sync started: %@", syncId)
} }
private var finalOperation: Operation? private weak var finalOperation: Operation?
} }
extension FeedlySyncStrategy: FeedlyRequestStreamsOperationDelegate { extension FeedlySyncStrategy: FeedlyRequestStreamsOperationDelegate {
@ -127,21 +131,30 @@ extension FeedlySyncStrategy: FeedlyRequestStreamsOperationDelegate {
updateOperation.delegate = self updateOperation.delegate = self
updateOperation.addDependency(groupItemsByFeed) 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. // Sync completes successfully when the account has been updated with all the parsedd entries from the stream.
if let operation = finalOperation { 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) operationQueue.addOperations(operations, waitUntilFinished: false)
} }
} }
extension FeedlySyncStrategy: FeedlySyncOperationDelegate { extension FeedlySyncStrategy: FeedlyOperationDelegate {
func feedlySyncOperation(_ operation: FeedlySyncOperation, didFailWith error: Error) { func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
os_log(.debug, log: log, "**** Operation failed! **** %@", error as NSError) os_log(.debug, log: log, "%@ failed so sync failed with error %@", operation, error.localizedDescription)
cancel() cancel()
startSyncCompletionHandler?(.failure(error)) startSyncCompletionHandler?(.failure(error))

View File

@ -11,7 +11,7 @@ import RSParser
import os.log import os.log
/// Single responsibility is to combine the articles with their feeds for a specific account. /// 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 account: Account
private let organisedItemsProvider: FeedlyParsedItemsByFeedProviding private let organisedItemsProvider: FeedlyParsedItemsByFeedProviding
private let log: OSLog private let log: OSLog
@ -40,7 +40,7 @@ final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlySyncOperation {
group.enter() group.enter()
os_log(.debug, log: log, "Updating %i items for feed \"%@\" in collection \"%@\"", items.count, feed.nameForDisplay, organisedItemsProvider.collection.label) 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() group.leave()
} }
} }

View File

@ -70,7 +70,7 @@ class AccountsFeedlyWebWindowController: NSWindowController, WKNavigationDelegat
NSApplication.shared.presentError(error) NSApplication.shared.presentError(error)
} catch { } catch {
NSApplication.shared.presentError(error) print(error)
} }
decisionHandler(.allow) decisionHandler(.allow)