Merge branch 'upstream-ios-candidate' into ios-candidate
This commit is contained in:
commit
1a58118a73
|
@ -689,7 +689,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
database.fetchStarredArticleIDsAsync(webFeedIDs: flattenedWebFeeds().webFeedIDs(), completion: completion)
|
||||
}
|
||||
|
||||
/// Fetch articleIDs for articles that we should have, but don’t. These articles are not userDeleted, and they are either (starred) or (unread and newer than the article cutoff date).
|
||||
/// Fetch articleIDs for articles that we should have, but don’t. These articles are not userDeleted, and they are either (starred) or (newer than the article cutoff date).
|
||||
public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) {
|
||||
database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
|
||||
}
|
||||
|
@ -791,6 +791,24 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
return updatedArticles
|
||||
}
|
||||
|
||||
/// Make sure statuses exist. Any existing statuses won’t be touched.
|
||||
/// All created statuses will be marked as read and not starred.
|
||||
/// Sends a .StatusesDidChange notification.
|
||||
func createStatusesIfNeeded(articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
|
||||
guard !articleIDs.isEmpty else {
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
database.createStatusesIfNeeded(articleIDs: articleIDs) { error in
|
||||
if let error = error {
|
||||
completion?(error)
|
||||
return
|
||||
}
|
||||
self.noteStatusesForArticleIDsDidChange(articleIDs)
|
||||
completion?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark articleIDs statuses based on statusKey and flag.
|
||||
/// Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
|
||||
func mark(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: DatabaseCompletionBlock? = nil) {
|
||||
|
@ -1245,10 +1263,10 @@ extension Account: OPMLRepresentable {
|
|||
|
||||
public func OPMLString(indentLevel: Int, strictConformance: Bool) -> String {
|
||||
var s = ""
|
||||
for feed in topLevelWebFeeds.sorted(by: { $0.nameForDisplay < $1.nameForDisplay }) {
|
||||
for feed in topLevelWebFeeds.sorted() {
|
||||
s += feed.OPMLString(indentLevel: indentLevel + 1, strictConformance: strictConformance)
|
||||
}
|
||||
for folder in folders!.sorted(by: { $0.nameForDisplay < $1.nameForDisplay }) {
|
||||
for folder in folders!.sorted() {
|
||||
s += folder.OPMLString(indentLevel: indentLevel + 1, strictConformance: strictConformance)
|
||||
}
|
||||
return s
|
||||
|
|
|
@ -99,7 +99,6 @@
|
|||
9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */; };
|
||||
9E1773D923458D590056A5A8 /* FeedlyResourceId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */; };
|
||||
9E1773DB234593CF0056A5A8 /* FeedlyResourceIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */; };
|
||||
9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */; };
|
||||
9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */; };
|
||||
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */; };
|
||||
9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */; };
|
||||
|
@ -113,15 +112,14 @@
|
|||
9E1FF8642368EC2400834C24 /* FeedlySyncAllOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */; };
|
||||
9E1FF8662368ED7E00834C24 /* TestMarkArticlesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */; };
|
||||
9E1FF8682368EE4900834C24 /* TestGetCollectionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */; };
|
||||
9E3CFFFD2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */; };
|
||||
9E4828F223617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */; };
|
||||
9E44C90F23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E44C90E23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift */; };
|
||||
9E489E8D2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */; };
|
||||
9E489E912360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */; };
|
||||
9E489E93236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */; };
|
||||
9E5ABE9A236BE6BD00B5DE9F /* feedly-1-initial in Resources */ = {isa = PBXBuildFile; fileRef = 9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */; };
|
||||
9E5DE60E23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */; };
|
||||
9E672394236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */; };
|
||||
9E672396236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */; };
|
||||
9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */; };
|
||||
9E7299D723505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D623505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift */; };
|
||||
9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */; };
|
||||
9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */; };
|
||||
|
@ -129,9 +127,8 @@
|
|||
9E79F7742395C9F00031DB98 /* feedly-add-new-feed in Resources */ = {isa = PBXBuildFile; fileRef = 9E79F7732395C9EF0031DB98 /* feedly-add-new-feed */; };
|
||||
9E7F88AC235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */; };
|
||||
9E7F88AE235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */; };
|
||||
9E84DC472359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */; };
|
||||
9E84DC472359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC462359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift */; };
|
||||
9E84DC492359A73600D6E809 /* FeedlyCheckpointOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */; };
|
||||
9E85C8E42366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */; };
|
||||
9E85C8E62366FED600D0F1F7 /* TestGetStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E52366FED600D0F1F7 /* TestGetStreamContentsService.swift */; };
|
||||
9E85C8E82366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */; };
|
||||
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */; };
|
||||
|
@ -143,6 +140,7 @@
|
|||
9EA643D3239305680018A28C /* FeedlySearchOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D2239305680018A28C /* FeedlySearchOperation.swift */; };
|
||||
9EA643D5239306AC0018A28C /* FeedlyFeedsSearchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */; };
|
||||
9EA643D923945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D823945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift */; };
|
||||
9EAADA1023C93144003A801F /* TestGetEntriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAADA0F23C93144003A801F /* TestGetEntriesService.swift */; };
|
||||
9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; };
|
||||
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */; };
|
||||
9EAEC624233315F60085D7C9 /* FeedlyEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */; };
|
||||
|
@ -150,7 +148,8 @@
|
|||
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */; };
|
||||
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */; };
|
||||
9EB1D576238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB1D575238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift */; };
|
||||
9EC228552362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */; };
|
||||
9EBD49C023C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBD49BF23C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift */; };
|
||||
9EBD49C223C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBD49C123C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift */; };
|
||||
9EC228572362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */; };
|
||||
9EC228592362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */; };
|
||||
9EC2285B23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */; };
|
||||
|
@ -173,10 +172,11 @@
|
|||
9EEAE073235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE072235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift */; };
|
||||
9EEAE075235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE074235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift */; };
|
||||
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */; };
|
||||
9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */; };
|
||||
9EEEF7212355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF7202355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift */; };
|
||||
9EF1B10323584B4C000A486A /* FeedlySyncStreamContentsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */; };
|
||||
9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */; };
|
||||
9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */; };
|
||||
9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */; };
|
||||
9EF35F7A234E830E003AE2AE /* FeedlyCompoundOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -322,7 +322,6 @@
|
|||
9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyTag.swift; sourceTree = "<group>"; };
|
||||
9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceId.swift; sourceTree = "<group>"; };
|
||||
9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceIdTests.swift; sourceTree = "<group>"; };
|
||||
9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetStarredArticlesOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncAllOperation.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>"; };
|
||||
|
@ -336,15 +335,14 @@
|
|||
9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncAllOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMarkArticlesService.swift; sourceTree = "<group>"; };
|
||||
9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetCollectionsService.swift; sourceTree = "<group>"; };
|
||||
9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncUnreadStatusesOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetUnreadArticlesOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E44C90E23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyIngestStreamArticleIdsOperation.swift; sourceTree = "<group>"; };
|
||||
9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrganiseParsedItemsByFeedOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyUpdateAccountFeedsWithItemsOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-1-initial"; sourceTree = "<group>"; };
|
||||
9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFetchIdsForMissingArticlesOperation.swift; sourceTree = "<group>"; };
|
||||
9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshAccessTokenOperation.swift; sourceTree = "<group>"; };
|
||||
9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAcessTokenRefreshing.swift; sourceTree = "<group>"; };
|
||||
9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetUnreadArticlesOperation.swift; sourceTree = "<group>"; };
|
||||
9E7299D623505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddFeedToCollectionOperation.swift; sourceTree = "<group>"; };
|
||||
9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceProviding.swift; sourceTree = "<group>"; };
|
||||
9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyLogoutOperation.swift; sourceTree = "<group>"; };
|
||||
|
@ -352,9 +350,8 @@
|
|||
9E79F7732395C9EF0031DB98 /* feedly-add-new-feed */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-add-new-feed"; sourceTree = "<group>"; };
|
||||
9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCreateFeedsForCollectionFoldersOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamContentsOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncUnreadStatusesOperation.swift; sourceTree = "<group>"; };
|
||||
9E84DC462359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyIngestUnreadArticleIdsOperation.swift; sourceTree = "<group>"; };
|
||||
9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCheckpointOperation.swift; sourceTree = "<group>"; };
|
||||
9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStarredArticlesOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E85C8E52366FED600D0F1F7 /* TestGetStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetStreamContentsService.swift; sourceTree = "<group>"; };
|
||||
9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetPagedStreamContentsService.swift; sourceTree = "<group>"; };
|
||||
9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesOperation.swift; sourceTree = "<group>"; };
|
||||
|
@ -366,6 +363,7 @@
|
|||
9EA643D2239305680018A28C /* FeedlySearchOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySearchOperation.swift; sourceTree = "<group>"; };
|
||||
9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeedsSearchResponse.swift; sourceTree = "<group>"; };
|
||||
9EA643D823945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddNewFeedOperationTests.swift; sourceTree = "<group>"; };
|
||||
9EAADA0F23C93144003A801F /* TestGetEntriesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetEntriesService.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>"; };
|
||||
9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntry.swift; sourceTree = "<group>"; };
|
||||
|
@ -373,7 +371,8 @@
|
|||
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>"; };
|
||||
9EB1D575238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddNewFeedOperation.swift; sourceTree = "<group>"; };
|
||||
9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetStarredArticlesOperationTests.swift; sourceTree = "<group>"; };
|
||||
9EBD49BF23C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyDownloadArticlesOperation.swift; sourceTree = "<group>"; };
|
||||
9EBD49C123C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntryIdentifierProviding.swift; sourceTree = "<group>"; };
|
||||
9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCheckpointOperationTests.swift; sourceTree = "<group>"; };
|
||||
9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySendArticleStatusesOperationTests.swift; sourceTree = "<group>"; };
|
||||
9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStreamContentsOperationTests.swift; sourceTree = "<group>"; };
|
||||
|
@ -396,10 +395,11 @@
|
|||
9EEAE072235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsService.swift; sourceTree = "<group>"; };
|
||||
9EEAE074235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMarkArticlesService.swift; sourceTree = "<group>"; };
|
||||
9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySendArticleStatusesOperation.swift; sourceTree = "<group>"; };
|
||||
9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStarredArticlesOperation.swift; sourceTree = "<group>"; };
|
||||
9EEEF7202355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyIngestStarredArticleIdsOperation.swift; sourceTree = "<group>"; };
|
||||
9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStreamContentsOperation.swift; sourceTree = "<group>"; };
|
||||
9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsOperation.swift; sourceTree = "<group>"; };
|
||||
9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyStreamIds.swift; sourceTree = "<group>"; };
|
||||
9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetUpdatedArticleIdsOperation.swift; sourceTree = "<group>"; };
|
||||
9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCompoundOperation.swift; sourceTree = "<group>"; };
|
||||
D511EEB5202422BB00712EC3 /* Account_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_debug.xcconfig; sourceTree = "<group>"; };
|
||||
D511EEB6202422BB00712EC3 /* Account_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_target.xcconfig; sourceTree = "<group>"; };
|
||||
|
@ -648,6 +648,7 @@
|
|||
9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */,
|
||||
9E1FF8612368219B00834C24 /* TestGetPagedStreamIdsService.swift */,
|
||||
9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */,
|
||||
9EAADA0F23C93144003A801F /* TestGetEntriesService.swift */,
|
||||
9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */,
|
||||
9E03C11B235D921400FB6D9E /* FeedlyOperationTests.swift */,
|
||||
9E03C11D235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift */,
|
||||
|
@ -657,13 +658,9 @@
|
|||
9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */,
|
||||
9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */,
|
||||
9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */,
|
||||
9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */,
|
||||
9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */,
|
||||
9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */,
|
||||
9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */,
|
||||
9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */,
|
||||
9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */,
|
||||
9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */,
|
||||
9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */,
|
||||
9EC804E2236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift */,
|
||||
9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */,
|
||||
|
@ -718,16 +715,18 @@
|
|||
9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */,
|
||||
9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */,
|
||||
9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */,
|
||||
9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */,
|
||||
9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */,
|
||||
9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */,
|
||||
9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */,
|
||||
9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */,
|
||||
9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */,
|
||||
9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */,
|
||||
9E44C90E23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift */,
|
||||
9EEEF7202355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift */,
|
||||
9E84DC462359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift */,
|
||||
9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */,
|
||||
9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */,
|
||||
9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */,
|
||||
9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */,
|
||||
9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */,
|
||||
9EBD49BF23C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift */,
|
||||
);
|
||||
path = Operations;
|
||||
sourceTree = "<group>";
|
||||
|
@ -747,6 +746,7 @@
|
|||
9E1773D22345700E0056A5A8 /* FeedlyLink.swift */,
|
||||
9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */,
|
||||
9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */,
|
||||
9EBD49C123C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1000,12 +1000,12 @@
|
|||
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */,
|
||||
511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */,
|
||||
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
|
||||
9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */,
|
||||
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
|
||||
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */,
|
||||
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
|
||||
9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */,
|
||||
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */,
|
||||
9EBD49C223C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift in Sources */,
|
||||
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */,
|
||||
515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */,
|
||||
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */,
|
||||
|
@ -1015,6 +1015,7 @@
|
|||
9E84DC492359A73600D6E809 /* FeedlyCheckpointOperation.swift in Sources */,
|
||||
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */,
|
||||
9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */,
|
||||
9E44C90F23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift in Sources */,
|
||||
844B297D2106C7EC004020B3 /* WebFeed.swift in Sources */,
|
||||
3B826DA72385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift in Sources */,
|
||||
9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */,
|
||||
|
@ -1031,10 +1032,10 @@
|
|||
9EF1B10323584B4C000A486A /* FeedlySyncStreamContentsOperation.swift in Sources */,
|
||||
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */,
|
||||
84B2D4D02238CD8A00498ADA /* WebFeedMetadata.swift in Sources */,
|
||||
9E84DC472359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift in Sources */,
|
||||
9E84DC472359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift in Sources */,
|
||||
9EAEC624233315F60085D7C9 /* FeedlyEntry.swift in Sources */,
|
||||
9EEAE073235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift in Sources */,
|
||||
9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */,
|
||||
9EEEF7212355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift in Sources */,
|
||||
3BC23AB92385ECB100371CBA /* FeedWranglerSubscriptionResult.swift in Sources */,
|
||||
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
|
||||
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
|
||||
|
@ -1044,6 +1045,7 @@
|
|||
55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */,
|
||||
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */,
|
||||
9EAEC626233318400085D7C9 /* FeedlyStream.swift in Sources */,
|
||||
9E5DE60E23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift in Sources */,
|
||||
3B826DA92385C81C00FC1ADB /* FeedWranglerAPICaller.swift in Sources */,
|
||||
9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */,
|
||||
51E3EB41229AF61B00645299 /* AccountError.swift in Sources */,
|
||||
|
@ -1052,6 +1054,7 @@
|
|||
552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */,
|
||||
552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */,
|
||||
5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */,
|
||||
9EBD49C023C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift in Sources */,
|
||||
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */,
|
||||
9E1D155B2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift in Sources */,
|
||||
552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */,
|
||||
|
@ -1079,10 +1082,10 @@
|
|||
9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */,
|
||||
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */,
|
||||
9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */,
|
||||
9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */,
|
||||
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
|
||||
9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */,
|
||||
84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */,
|
||||
9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */,
|
||||
3B826DAA2385C81C00FC1ADB /* FeedWranglerSubscription.swift in Sources */,
|
||||
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */,
|
||||
);
|
||||
|
@ -1094,9 +1097,8 @@
|
|||
files = (
|
||||
9EC228572362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift in Sources */,
|
||||
9E03C122235E62E100FB6D9E /* FeedlyTestSupport.swift in Sources */,
|
||||
9E3CFFFD2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift in Sources */,
|
||||
9EAADA1023C93144003A801F /* TestGetEntriesService.swift in Sources */,
|
||||
9E784EC0237E8BE100099B1B /* FeedlyLogoutOperationTests.swift in Sources */,
|
||||
9EC228552362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift in Sources */,
|
||||
9E03C120235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift in Sources */,
|
||||
9E489E912360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift in Sources */,
|
||||
9E0260CB236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift in Sources */,
|
||||
|
@ -1117,10 +1119,8 @@
|
|||
51D5875E227F643C00900287 /* AccountFeedbinFolderSyncTest.swift in Sources */,
|
||||
9EC804E3236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift in Sources */,
|
||||
9E1FF8682368EE4900834C24 /* TestGetCollectionsService.swift in Sources */,
|
||||
9E4828F223617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift in Sources */,
|
||||
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */,
|
||||
513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */,
|
||||
9E85C8E42366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift in Sources */,
|
||||
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */,
|
||||
5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */,
|
||||
9EC2285B23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift in Sources */,
|
||||
|
|
|
@ -28,6 +28,7 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase {
|
|||
}
|
||||
|
||||
struct TestParsedItemsProvider: FeedlyParsedItemProviding {
|
||||
let parsedItemProviderName = "TestParsedItemsProvider"
|
||||
var resource: FeedlyResourceId
|
||||
var parsedEntries: Set<ParsedItem>
|
||||
}
|
||||
|
@ -51,7 +52,6 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase {
|
|||
|
||||
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
|
||||
XCTAssertEqual(itemsAndFeedIds, entries)
|
||||
XCTAssertEqual(resource.id, organise.providerName)
|
||||
}
|
||||
|
||||
func testGroupsOneEntryByFeedId() {
|
||||
|
@ -73,7 +73,6 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase {
|
|||
|
||||
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
|
||||
XCTAssertEqual(itemsAndFeedIds, entries)
|
||||
XCTAssertEqual(resource.id, organise.providerName)
|
||||
}
|
||||
|
||||
func testGroupsManyEntriesByFeedId() {
|
||||
|
@ -95,6 +94,5 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase {
|
|||
|
||||
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
|
||||
XCTAssertEqual(itemsAndFeedIds, entries)
|
||||
XCTAssertEqual(resource.id, organise.providerName)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,489 +0,0 @@
|
|||
//
|
||||
// FeedlySetStarredArticlesOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 25/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import RSParser
|
||||
|
||||
class FeedlySetStarredArticlesOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Ensuring Unread Status By Id
|
||||
|
||||
struct TestStarredArticleProvider: FeedlyStarredEntryIdProviding {
|
||||
var entryIds: Set<String>
|
||||
}
|
||||
|
||||
func testEmptyArticleIds() {
|
||||
let testIds = Set<String>()
|
||||
let provider = TestStarredArticleProvider(entryIds: testIds)
|
||||
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertTrue(accountArticlesIDs.isEmpty)
|
||||
XCTAssertEqual(accountArticlesIDs, testIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking articles IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetOneArticleIdStarred() {
|
||||
let testIds = Set<String>(["feed/0/article/0"])
|
||||
let provider = TestStarredArticleProvider(entryIds: testIds)
|
||||
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking articles IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetManyArticleIdsStarred() {
|
||||
let testIds = Set<String>((0..<10_000).map { "feed/0/article/\($0)" })
|
||||
let provider = TestStarredArticleProvider(entryIds: testIds)
|
||||
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking articles IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetSomeArticleIdsUnstarred() {
|
||||
let initialStarredIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
|
||||
|
||||
do {
|
||||
let provider = TestStarredArticleProvider(entryIds: initialStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingStarredIds = Set(initialStarredIds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { remainingAccountArticlesIDsResult in
|
||||
do {
|
||||
let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingStarredIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking articles IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetAllArticleIdsUnstarred() {
|
||||
let initialStarredIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
|
||||
|
||||
do {
|
||||
let provider = TestStarredArticleProvider(entryIds: initialStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingStarredIds = Set<String>()
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { remainingAccountArticlesIDsResult in
|
||||
do {
|
||||
let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingStarredIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking articles IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
// MARK: - Updating Article Unread Status
|
||||
|
||||
struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding {
|
||||
var providerName: String
|
||||
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>]
|
||||
}
|
||||
|
||||
func testSetAllArticlesStarred() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let remainingStarredIds = Set(testItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(testItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(try self.account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking articles IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetManyArticlesUnread() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let unreadItems = testItems
|
||||
.enumerated()
|
||||
.filter { $0.offset % 2 > 0 }
|
||||
.map { $0.element }
|
||||
|
||||
let remainingStarredIds = Set(unreadItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(unreadItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(try self.account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking articles IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetOneArticleUnread() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
// Since the test data is completely under the developer's control, not having at least one can be a programmer error.
|
||||
let remainingStarredIds = Set([testItems.compactMap { $0.syncServiceID }.first!])
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(try self.account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking articles IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetNoArticlesRead() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingStarredIds = Set<String>()
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(try self.account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking articles IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetAllArticlesAndArticleIdsWithSomeArticlesIngested() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
let someItemsAndFeeds = Dictionary(uniqueKeysWithValues: testItemsAndFeeds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: someItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let remainingStarredIds = Set(testItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(testItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let someTestItems = Set(someItemsAndFeeds.flatMap { $0.value })
|
||||
let someRemainingStarredIdsOfIngestedArticles = Set(someTestItems.compactMap { $0.syncServiceID })
|
||||
let idsOfStarredArticles = Set(try self.account
|
||||
.fetchArticles(.articleIDs(someRemainingStarredIdsOfIngestedArticles))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, someRemainingStarredIdsOfIngestedArticles)
|
||||
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking articles IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
}
|
|
@ -1,485 +0,0 @@
|
|||
//
|
||||
// FeedlySetUnreadArticlesOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 24/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import RSParser
|
||||
|
||||
class FeedlySetUnreadArticlesOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Ensuring Unread Status By Id
|
||||
|
||||
struct TestUnreadArticleIdProvider: FeedlyUnreadEntryIdProviding {
|
||||
var entryIds: Set<String>
|
||||
}
|
||||
|
||||
func testEmptyArticleIds() {
|
||||
let testIds = Set<String>()
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: testIds)
|
||||
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
|
||||
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertTrue(accountArticlesIDs.isEmpty)
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking account articles IDs result: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetOneArticleIdUnread() {
|
||||
let testIds = Set<String>(["feed/0/article/0"])
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: testIds)
|
||||
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
|
||||
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking account articles IDs result: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetManyArticleIdsUnread() {
|
||||
let testIds = Set<String>((0..<10_000).map { "feed/0/article/\($0)" })
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: testIds)
|
||||
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
|
||||
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking account articles IDs result: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetSomeArticleIdsRead() {
|
||||
let initialUnreadIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
|
||||
|
||||
do {
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: initialUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingUnreadIds = Set(initialUnreadIds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
|
||||
account.fetchUnreadArticleIDs { remainingAccountArticlesIDsResult in
|
||||
do {
|
||||
let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingUnreadIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking account articles IDs result: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetAllArticleIdsRead() {
|
||||
let initialUnreadIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
|
||||
|
||||
do {
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: initialUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingUnreadIds = Set<String>()
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
|
||||
account.fetchUnreadArticleIDs { remainingAccountArticlesIDsResult in
|
||||
do {
|
||||
let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingUnreadIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking account articles IDs result: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
// MARK: - Updating Article Unread Status
|
||||
|
||||
struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding {
|
||||
var providerName: String
|
||||
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>]
|
||||
}
|
||||
|
||||
func testSetAllArticlesUnread() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let remainingUnreadIds = Set(testItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(testItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
|
||||
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
let idsOfUnreadArticles = Set(try self.account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking account articles IDs result: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetManyArticlesUnread() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let unreadItems = testItems
|
||||
.enumerated()
|
||||
.filter { $0.offset % 2 > 0 }
|
||||
.map { $0.element }
|
||||
|
||||
let remainingUnreadIds = Set(unreadItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(unreadItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
|
||||
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(try self.account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking account articles IDs result: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSetOneArticleUnread() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
// Since the test data is completely under the developer's control, not having at least one can be a programmer error.
|
||||
let remainingUnreadIds = Set([testItems.compactMap { $0.syncServiceID }.first!])
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
|
||||
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(try self.account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking account articles IDs result: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSetNoArticlesRead() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingUnreadIds = Set<String>()
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
|
||||
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(try self.account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking account articles IDs result: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSetAllArticlesAndArticleIdsWithSomeArticlesIngested() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
let someItemsAndFeeds = Dictionary(uniqueKeysWithValues: testItemsAndFeeds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: someItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let remainingUnreadIds = Set(testItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(testItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
|
||||
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
|
||||
do {
|
||||
let accountArticlesIDs = try accountArticlesIDsResult.get()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let someTestItems = Set(someItemsAndFeeds.flatMap { $0.value })
|
||||
let someRemainingUnreadIdsOfIngestedArticles = Set(someTestItems.compactMap { $0.syncServiceID })
|
||||
let idsOfUnreadArticles = Set(try self.account
|
||||
.fetchArticles(.articleIDs(someRemainingUnreadIdsOfIngestedArticles))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, someRemainingUnreadIdsOfIngestedArticles)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking account articles IDs result: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,25 +57,31 @@ class FeedlySyncAllOperationTests: XCTestCase {
|
|||
getGlobalStreamContents.getStreamContentsExpectation = expectation(description: "Get Contents of global.all")
|
||||
getGlobalStreamContents.getStreamContentsExpectation?.isInverted = true
|
||||
|
||||
let getStarredContents = TestGetStreamContentsService()
|
||||
getStarredContents.getStreamContentsExpectation = expectation(description: "Get Contents of global.saved")
|
||||
getStarredContents.getStreamContentsExpectation?.isInverted = true
|
||||
let getStarredIds = TestGetStreamIdsService()
|
||||
getStarredIds.getStreamIdsExpectation = expectation(description: "Get Ids of global.saved")
|
||||
getStarredIds.getStreamIdsExpectation?.isInverted = true
|
||||
|
||||
let getEntriesService = TestGetEntriesService()
|
||||
getEntriesService.getEntriesExpectation = expectation(description: "Get Entries")
|
||||
getEntriesService.getEntriesExpectation?.isInverted = true
|
||||
|
||||
let progress = DownloadProgress(numberOfTasks: 0)
|
||||
let _ = expectationForCompletion(of: progress)
|
||||
|
||||
let container = support.makeTestDatabaseContainer()
|
||||
let syncAll = FeedlySyncAllOperation(account: account,
|
||||
credentials: support.accessToken,
|
||||
lastSuccessfulFetchStartDate: nil,
|
||||
markArticlesService: markArticlesService,
|
||||
getUnreadService: getStreamIdsService,
|
||||
getCollectionsService: getCollectionsService,
|
||||
getStreamContentsService: getGlobalStreamContents,
|
||||
getStarredArticlesService: getStarredContents,
|
||||
database: container.database,
|
||||
downloadProgress: progress,
|
||||
log: support.log)
|
||||
credentials: support.accessToken,
|
||||
lastSuccessfulFetchStartDate: nil,
|
||||
markArticlesService: markArticlesService,
|
||||
getUnreadService: getStreamIdsService,
|
||||
getCollectionsService: getCollectionsService,
|
||||
getStreamContentsService: getGlobalStreamContents,
|
||||
getStarredService: getStarredIds,
|
||||
getStreamIdsService: getStreamIdsService,
|
||||
getEntriesService: getEntriesService,
|
||||
database: container.database,
|
||||
downloadProgress: progress,
|
||||
log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
|
|
|
@ -1,189 +0,0 @@
|
|||
//
|
||||
// FeedlySyncStarredArticlesOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 28/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlySyncStarredArticlesOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testIngestsOnePageSuccess() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let items = service.makeMockFeedlyEntryItem()
|
||||
service.mockResult = .success(FeedlyStream(id: resource.id, updated: nil, continuation: nil, items: items))
|
||||
|
||||
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamContentsExpectation.expectedFulfillmentCount = 1
|
||||
|
||||
service.getStreamContentsExpectation = getStreamContentsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
}
|
||||
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let expectedArticleIds = Set(items.map { $0.id })
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { starredArticleIdsResult in
|
||||
do {
|
||||
let starredArticleIds = try starredArticleIdsResult.get()
|
||||
let missingIds = expectedArticleIds.subtracting(starredArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as starred.")
|
||||
|
||||
// Fetch articles directly because account.fetchArticles(.starred) fetches starred articles for feeds subscribed to.
|
||||
let expectedArticles = try self.account.fetchArticles(.articleIDs(expectedArticleIds))
|
||||
XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.")
|
||||
|
||||
let starredArticles = try self.account.fetchArticles(.articleIDs(starredArticleIds))
|
||||
XCTAssertEqual(expectedArticleIds.count, expectedArticles.count)
|
||||
let missingArticles = expectedArticles.subtracting(starredArticles)
|
||||
XCTAssertTrue(missingArticles.isEmpty, "These articles should be starred and fetched.")
|
||||
XCTAssertEqual(expectedArticles, starredArticles)
|
||||
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking starred article IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testIngestsOnePageFailure() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
|
||||
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamContentsExpectation.expectedFulfillmentCount = 1
|
||||
|
||||
service.getStreamContentsExpectation = getStreamContentsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
}
|
||||
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { starredArticleIdsResult in
|
||||
do {
|
||||
let starredArticleIds = try starredArticleIdsResult.get()
|
||||
XCTAssertTrue(starredArticleIds.isEmpty)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking starred article IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testIngestsManyPagesSuccess() {
|
||||
let service = TestGetPagedStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let continuations = (1...10).map { "\($0)" }
|
||||
service.addAtLeastOnePage(for: resource, continuations: continuations, numberOfEntriesPerPage: 10)
|
||||
|
||||
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamContentsExpectation.expectedFulfillmentCount = 1 + continuations.count
|
||||
|
||||
var remainingContinuations = Set(continuations)
|
||||
let getStreamPageExpectation = expectation(description: "Did Request Page")
|
||||
getStreamPageExpectation.expectedFulfillmentCount = 1 + continuations.count
|
||||
|
||||
service.getStreamContentsExpectation = getStreamContentsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
|
||||
if let continuation = continuation {
|
||||
XCTAssertTrue(remainingContinuations.contains(continuation))
|
||||
remainingContinuations.remove(continuation)
|
||||
}
|
||||
|
||||
getStreamPageExpectation.fulfill()
|
||||
}
|
||||
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
// Find articles inserted.
|
||||
let expectedArticleIds = Set(service.pages.values.map { $0.items }.flatMap { $0 }.map { $0.id })
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchStarredArticleIDs { starredArticleIdsResult in
|
||||
do {
|
||||
let starredArticleIds = try starredArticleIdsResult.get()
|
||||
let missingIds = expectedArticleIds.subtracting(starredArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as starred.")
|
||||
|
||||
// Fetch articles directly because account.fetchArticles(.starred) fetches starred articles for feeds subscribed to.
|
||||
let expectedArticles = try self.account.fetchArticles(.articleIDs(expectedArticleIds))
|
||||
XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.")
|
||||
|
||||
let starredArticles = try self.account.fetchArticles(.articleIDs(starredArticleIds))
|
||||
XCTAssertEqual(expectedArticleIds.count, expectedArticles.count)
|
||||
let missingArticles = expectedArticles.subtracting(starredArticles)
|
||||
XCTAssertTrue(missingArticles.isEmpty, "These articles should be starred and fetched.")
|
||||
XCTAssertEqual(expectedArticles, starredArticles)
|
||||
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking starred article IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
//
|
||||
// FeedlySyncUnreadStatusesOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 29/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlySyncUnreadStatusesOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testIngestsOnePageSuccess() {
|
||||
let service = TestGetStreamIdsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let ids = [UUID().uuidString]
|
||||
service.mockResult = .success(FeedlyStreamIds(continuation: nil, ids: ids))
|
||||
|
||||
let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Ids")
|
||||
getStreamIdsExpectation.expectedFulfillmentCount = 1
|
||||
|
||||
service.getStreamIdsExpectation = getStreamIdsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertEqual(serviceUnreadOnly, true)
|
||||
}
|
||||
|
||||
let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncUnreads.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncUnreads)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let expectedArticleIds = Set(ids)
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchUnreadArticleIDs { unreadArticleIdsResult in
|
||||
do {
|
||||
let unreadArticleIds = try unreadArticleIdsResult.get()
|
||||
let missingIds = expectedArticleIds.subtracting(unreadArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as unread.")
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking unread article IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testIngestsOnePageFailure() {
|
||||
let service = TestGetStreamIdsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
|
||||
let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamIdsExpectation.expectedFulfillmentCount = 1
|
||||
|
||||
service.getStreamIdsExpectation = getStreamIdsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertEqual(serviceUnreadOnly, true)
|
||||
}
|
||||
|
||||
let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncUnreads.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncUnreads)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchUnreadArticleIDs { unreadArticleIdsResult in
|
||||
do {
|
||||
let unreadArticleIds = try unreadArticleIdsResult.get()
|
||||
XCTAssertTrue(unreadArticleIds.isEmpty)
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking unread article IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testIngestsManyPagesSuccess() {
|
||||
let service = TestGetPagedStreamIdsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let continuations = (1...10).map { "\($0)" }
|
||||
service.addAtLeastOnePage(for: resource, continuations: continuations, numberOfEntriesPerPage: 1000)
|
||||
|
||||
let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamIdsExpectation.expectedFulfillmentCount = 1 + continuations.count
|
||||
|
||||
var remainingContinuations = Set(continuations)
|
||||
let getStreamPageExpectation = expectation(description: "Did Request Page")
|
||||
getStreamPageExpectation.expectedFulfillmentCount = 1 + continuations.count
|
||||
|
||||
service.getStreamIdsExpectation = getStreamIdsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertEqual(serviceUnreadOnly, true)
|
||||
|
||||
if let continuation = continuation {
|
||||
XCTAssertTrue(remainingContinuations.contains(continuation))
|
||||
remainingContinuations.remove(continuation)
|
||||
}
|
||||
|
||||
getStreamPageExpectation.fulfill()
|
||||
}
|
||||
|
||||
let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncUnreads.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncUnreads)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
// Find statuses inserted.
|
||||
let expectedArticleIds = Set(service.pages.values.map { $0.ids }.flatMap { $0 })
|
||||
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
|
||||
account.fetchUnreadArticleIDs { unreadArticleIdsResult in
|
||||
do {
|
||||
let unreadArticleIds = try unreadArticleIdsResult.get()
|
||||
let missingIds = expectedArticleIds.subtracting(unreadArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as unread.")
|
||||
fetchIdsExpectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Error checking unread article IDs: \(error)")
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
}
|
|
@ -28,14 +28,14 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase {
|
|||
}
|
||||
|
||||
struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding {
|
||||
var providerName: String
|
||||
var parsedItemsByFeedProviderName: String
|
||||
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>]
|
||||
}
|
||||
|
||||
func testUpdateAccountWithEmptyItems() throws {
|
||||
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 0, numberOfItemsInFeeds: 0)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
|
@ -59,7 +59,7 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase {
|
|||
func testUpdateAccountWithOneItem() throws {
|
||||
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
|
@ -87,7 +87,7 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase {
|
|||
func testUpdateAccountWithManyItems() throws {
|
||||
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 100, numberOfItemsInFeeds: 100)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
|
@ -115,7 +115,7 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase {
|
|||
func testCancelUpdateAccount() throws {
|
||||
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// TestGetEntriesService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 11/1/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetEntriesService: FeedlyGetEntriesService {
|
||||
var mockResult: Result<[FeedlyEntry], Error>?
|
||||
var getEntriesExpectation: XCTestExpectation?
|
||||
|
||||
func getEntries(for ids: Set<String>, completion: @escaping (Result<[FeedlyEntry], Error>) -> ()) {
|
||||
guard let result = mockResult else {
|
||||
XCTFail("Missing mock result. Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion(result)
|
||||
self.getEntriesExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -637,7 +637,7 @@ extension FeedlyAPICaller: FeedlyGetStreamIdsService {
|
|||
}
|
||||
|
||||
queryItems.append(contentsOf: [
|
||||
URLQueryItem(name: "count", value: "1000"),
|
||||
URLQueryItem(name: "count", value: "10000"),
|
||||
URLQueryItem(name: "streamId", value: resource.id),
|
||||
])
|
||||
|
||||
|
|
|
@ -147,10 +147,6 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
/// 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 ((Result<Void, Error>) -> Void)) {
|
||||
|
@ -160,18 +156,18 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
|
||||
let group = DispatchGroup()
|
||||
|
||||
let syncUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log)
|
||||
let ingestUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log)
|
||||
|
||||
group.enter()
|
||||
syncUnread.completionBlock = {
|
||||
ingestUnread.completionBlock = {
|
||||
group.leave()
|
||||
|
||||
}
|
||||
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, credentials: credentials, service: caller, log: log)
|
||||
let ingestStarred = FeedlyIngestStarredArticleIdsOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log)
|
||||
|
||||
group.enter()
|
||||
syncStarred.completionBlock = {
|
||||
ingestStarred.completionBlock = {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
|
@ -179,7 +175,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
completion(.success(()))
|
||||
}
|
||||
|
||||
operationQueue.addOperations([syncUnread, syncStarred], waitUntilFinished: false)
|
||||
operationQueue.addOperations([ingestUnread, ingestStarred], waitUntilFinished: false)
|
||||
}
|
||||
|
||||
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// FeedlyEntryIdentifierProviding.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 9/1/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyEntryIdentifierProviding: class {
|
||||
var entryIds: Set<String> { get }
|
||||
}
|
||||
|
||||
final class FeedlyEntryIdentifierProvider: FeedlyEntryIdentifierProviding {
|
||||
private (set) var entryIds: Set<String>
|
||||
|
||||
init(entryIds: Set<String> = Set()) {
|
||||
self.entryIds = entryIds
|
||||
}
|
||||
|
||||
func addEntryIds(from provider: FeedlyEntryIdentifierProviding) {
|
||||
entryIds.formUnion(provider.entryIds)
|
||||
}
|
||||
|
||||
func addEntryIds(in articleIds: [String]) {
|
||||
entryIds.formUnion(articleIds)
|
||||
}
|
||||
}
|
|
@ -92,7 +92,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
|
|||
createFeeds.downloadProgress = downloadProgress
|
||||
self.operationQueue.addOperation(createFeeds)
|
||||
|
||||
let syncUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: syncUnreadIdsService, newerThan: nil, log: log)
|
||||
let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: syncUnreadIdsService, newerThan: nil, log: log)
|
||||
syncUnread.addDependency(createFeeds)
|
||||
syncUnread.downloadProgress = downloadProgress
|
||||
self.operationQueue.addOperation(syncUnread)
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// FeedlyDownloadArticlesOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 9/1/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
class FeedlyDownloadArticlesOperation: FeedlyOperation {
|
||||
private let account: Account
|
||||
private let log: OSLog
|
||||
private let missingArticleEntryIdProvider: FeedlyEntryIdentifierProviding
|
||||
private let updatedArticleEntryIdProvider: FeedlyEntryIdentifierProviding
|
||||
private let getEntriesService: FeedlyGetEntriesService
|
||||
private let operationQueue: OperationQueue
|
||||
private let finishOperation: FeedlyCheckpointOperation
|
||||
|
||||
init(account: Account, missingArticleEntryIdProvider: FeedlyEntryIdentifierProviding, updatedArticleEntryIdProvider: FeedlyEntryIdentifierProviding, getEntriesService: FeedlyGetEntriesService, log: OSLog) {
|
||||
self.account = account
|
||||
self.operationQueue = OperationQueue()
|
||||
self.operationQueue.isSuspended = true
|
||||
self.missingArticleEntryIdProvider = missingArticleEntryIdProvider
|
||||
self.updatedArticleEntryIdProvider = updatedArticleEntryIdProvider
|
||||
self.getEntriesService = getEntriesService
|
||||
self.finishOperation = FeedlyCheckpointOperation()
|
||||
self.log = log
|
||||
|
||||
super.init()
|
||||
|
||||
self.finishOperation.checkpointDelegate = self
|
||||
self.operationQueue.addOperation(self.finishOperation)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
os_log(.debug, log: log, "Cancelling %{public}@.", self)
|
||||
operationQueue.cancelAllOperations()
|
||||
super.cancel()
|
||||
didFinish()
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
// override of cancel calls didFinish().
|
||||
return
|
||||
}
|
||||
|
||||
var articleIds = missingArticleEntryIdProvider.entryIds
|
||||
articleIds.formUnion(updatedArticleEntryIdProvider.entryIds)
|
||||
|
||||
os_log(.debug, log: log, "Requesting %{public}i articles.", articleIds.count)
|
||||
|
||||
let feedlyAPILimitBatchSize = 1000
|
||||
for articleIds in Array(articleIds).chunked(into: feedlyAPILimitBatchSize) {
|
||||
|
||||
let provider = FeedlyEntryIdentifierProvider(entryIds: Set(articleIds))
|
||||
let getEntries = FeedlyGetEntriesOperation(account: account, service: getEntriesService, provider: provider, log: log)
|
||||
getEntries.delegate = self
|
||||
self.operationQueue.addOperation(getEntries)
|
||||
|
||||
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
|
||||
parsedItemProvider: getEntries,
|
||||
log: log)
|
||||
organiseByFeed.delegate = self
|
||||
organiseByFeed.addDependency(getEntries)
|
||||
self.operationQueue.addOperation(organiseByFeed)
|
||||
|
||||
let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account,
|
||||
organisedItemsProvider: organiseByFeed,
|
||||
log: log)
|
||||
|
||||
updateAccount.delegate = self
|
||||
updateAccount.addDependency(organiseByFeed)
|
||||
self.operationQueue.addOperation(updateAccount)
|
||||
|
||||
finishOperation.addDependency(updateAccount)
|
||||
}
|
||||
|
||||
operationQueue.isSuspended = false
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyDownloadArticlesOperation: FeedlyCheckpointOperationDelegate {
|
||||
|
||||
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
|
||||
didFinish()
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyDownloadArticlesOperation: FeedlyOperationDelegate {
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
assert(Thread.isMainThread)
|
||||
os_log(.debug, log: log, "%{public}@ failed with error: %{public}@.", operation, error as NSError)
|
||||
cancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// FeedlyFetchIdsForMissingArticlesOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 7/1/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
final class FeedlyFetchIdsForMissingArticlesOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
|
||||
private let account: Account
|
||||
private let log: OSLog
|
||||
|
||||
private(set) var entryIds = Set<String>()
|
||||
|
||||
init(account: Account, log: OSLog) {
|
||||
self.account = account
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
|
||||
switch result {
|
||||
case .success(let articleIds):
|
||||
self.entryIds.formUnion(articleIds)
|
||||
self.didFinish()
|
||||
|
||||
case .failure(let error):
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,15 +8,16 @@
|
|||
|
||||
import Foundation
|
||||
import os.log
|
||||
import RSParser
|
||||
|
||||
/// Single responsibility is to get full entries for the entry identifiers.
|
||||
final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding {
|
||||
final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
|
||||
let account: Account
|
||||
let service: FeedlyGetEntriesService
|
||||
let provider: FeedlyEntryIdenifierProviding
|
||||
let provider: FeedlyEntryIdentifierProviding
|
||||
let log: OSLog
|
||||
|
||||
init(account: Account, service: FeedlyGetEntriesService, provider: FeedlyEntryIdenifierProviding, log: OSLog) {
|
||||
init(account: Account, service: FeedlyGetEntriesService, provider: FeedlyEntryIdentifierProviding, log: OSLog) {
|
||||
self.account = account
|
||||
self.service = service
|
||||
self.provider = provider
|
||||
|
@ -25,6 +26,33 @@ final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding {
|
|||
|
||||
private (set) var entries = [FeedlyEntry]()
|
||||
|
||||
private var storedParsedEntries: Set<ParsedItem>?
|
||||
|
||||
var parsedEntries: Set<ParsedItem> {
|
||||
if let entries = storedParsedEntries {
|
||||
return entries
|
||||
}
|
||||
|
||||
let parsed = Set(entries.compactMap {
|
||||
FeedlyEntryParser(entry: $0).parsedItemRepresentation
|
||||
})
|
||||
|
||||
if parsed.count != entries.count {
|
||||
let entryIds = Set(entries.map { $0.id })
|
||||
let parsedIds = Set(parsed.map { $0.uniqueID })
|
||||
let difference = entryIds.subtracting(parsedIds)
|
||||
os_log(.debug, log: log, "%{public}@ dropping articles with ids: %{public}@.", self, difference)
|
||||
}
|
||||
|
||||
storedParsedEntries = parsed
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
var parsedItemProviderName: String {
|
||||
return name ?? String(describing: Self.self)
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
|
|
|
@ -15,7 +15,7 @@ protocol FeedlyEntryProviding {
|
|||
}
|
||||
|
||||
protocol FeedlyParsedItemProviding {
|
||||
var resource: FeedlyResourceId { get }
|
||||
var parsedItemProviderName: String { get }
|
||||
var parsedEntries: Set<ParsedItem> { get }
|
||||
}
|
||||
|
||||
|
@ -32,8 +32,8 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid
|
|||
|
||||
let resourceProvider: FeedlyResourceProviding
|
||||
|
||||
var resource: FeedlyResourceId {
|
||||
return resourceProvider.resource
|
||||
var parsedItemProviderName: String {
|
||||
return resourceProvider.resource.id
|
||||
}
|
||||
|
||||
var entries: [FeedlyEntry] {
|
||||
|
|
|
@ -9,17 +9,12 @@
|
|||
import Foundation
|
||||
import os.log
|
||||
|
||||
protocol FeedlyEntryIdenifierProviding: class {
|
||||
var resource: FeedlyResourceId { get }
|
||||
var entryIds: Set<String> { get }
|
||||
}
|
||||
|
||||
protocol FeedlyGetStreamIdsOperationDelegate: class {
|
||||
func feedlyGetStreamIdsOperation(_ operation: FeedlyGetStreamIdsOperation, didGet streamIds: FeedlyStreamIds)
|
||||
}
|
||||
|
||||
/// Single responsibility is to get the stream ids from Feedly.
|
||||
final class FeedlyGetStreamIdsOperation: FeedlyOperation, FeedlyEntryIdenifierProviding, FeedlyUnreadEntryIdProviding {
|
||||
final class FeedlyGetStreamIdsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
|
||||
|
||||
var entryIds: Set<String> {
|
||||
guard let ids = streamIds?.ids else {
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// FeedlyGetUpdatedArticleIdsOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 11/1/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
/// Single responsibility is to identify articles that have changed since a particular date.
|
||||
///
|
||||
/// Typically, it pages through the article ids of the global.all stream.
|
||||
/// When all the article ids are collected, it is the responsibility of another operation to download them when appropriate.
|
||||
class FeedlyGetUpdatedArticleIdsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
|
||||
private let account: Account
|
||||
private let resource: FeedlyResourceId
|
||||
private let service: FeedlyGetStreamIdsService
|
||||
private let newerThan: Date?
|
||||
private let log: OSLog
|
||||
|
||||
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
|
||||
self.account = account
|
||||
self.resource = resource
|
||||
self.service = service
|
||||
self.newerThan = newerThan
|
||||
self.log = log
|
||||
}
|
||||
|
||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
|
||||
let all = FeedlyCategoryResourceId.Global.all(for: credentials.username)
|
||||
self.init(account: account, resource: all, service: service, newerThan: newerThan, log: log)
|
||||
}
|
||||
|
||||
var entryIds: Set<String> {
|
||||
return storedUpdatedArticleIds
|
||||
}
|
||||
|
||||
private var storedUpdatedArticleIds = Set<String>()
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
getStreamIds(nil)
|
||||
}
|
||||
|
||||
private func getStreamIds(_ continuation: String?) {
|
||||
guard let date = newerThan else {
|
||||
os_log(.debug, log: log, "No date provided so everything must be new (nothing is updated).")
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
service.getStreamIds(for: resource, continuation: continuation, newerThan: date, unreadOnly: nil, completion: didGetStreamIds(_:))
|
||||
}
|
||||
|
||||
private func didGetStreamIds(_ result: Result<FeedlyStreamIds, Error>) {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let streamIds):
|
||||
storedUpdatedArticleIds.formUnion(streamIds.ids)
|
||||
|
||||
guard let continuation = streamIds.continuation else {
|
||||
os_log(.debug, log: log, "%{public}i articles updated since last successful sync start date.", storedUpdatedArticleIds.count)
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
getStreamIds(continuation)
|
||||
|
||||
case .failure(let error):
|
||||
didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
//
|
||||
// FeedlyIngestStarredArticleIdsOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 15/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
/// Single responsibility is to clone locally the remote starred article state.
|
||||
///
|
||||
/// Typically, it pages through the article ids of the global.saved stream.
|
||||
/// When all the article ids are collected, a status is created for each.
|
||||
/// The article ids previously marked as starred but not collected become unstarred.
|
||||
/// So this operation has side effects *for the entire account* it operates on.
|
||||
final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation {
|
||||
private let account: Account
|
||||
private let resource: FeedlyResourceId
|
||||
private let service: FeedlyGetStreamIdsService
|
||||
private let entryIdsProvider: FeedlyEntryIdentifierProvider
|
||||
private let log: OSLog
|
||||
|
||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
|
||||
let resource = FeedlyTagResourceId.Global.saved(for: credentials.username)
|
||||
self.init(account: account, resource: resource, service: service, newerThan: newerThan, log: log)
|
||||
}
|
||||
|
||||
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
|
||||
self.account = account
|
||||
self.resource = resource
|
||||
self.service = service
|
||||
self.entryIdsProvider = FeedlyEntryIdentifierProvider()
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
getStreamIds(nil)
|
||||
}
|
||||
|
||||
private func getStreamIds(_ continuation: String?) {
|
||||
service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: nil, completion: didGetStreamIds(_:))
|
||||
}
|
||||
|
||||
private func didGetStreamIds(_ result: Result<FeedlyStreamIds, Error>) {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let streamIds):
|
||||
|
||||
entryIdsProvider.addEntryIds(in: streamIds.ids)
|
||||
|
||||
guard let continuation = streamIds.continuation else {
|
||||
updateStarredStatuses()
|
||||
return
|
||||
}
|
||||
|
||||
getStreamIds(continuation)
|
||||
|
||||
case .failure(let error):
|
||||
didFinish(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStarredStatuses() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
account.fetchStarredArticleIDs { result in
|
||||
switch result {
|
||||
case .success(let localStarredArticleIDs):
|
||||
self.processStarredArticleIDs(localStarredArticleIDs)
|
||||
|
||||
case .failure(let error):
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processStarredArticleIDs(_ localStarredArticleIDs: Set<String>) {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
let remoteStarredArticleIDs = entryIdsProvider.entryIds
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
final class StarredStatusResults {
|
||||
var markAsStarredError: Error?
|
||||
var markAsUnstarredError: Error?
|
||||
}
|
||||
|
||||
let results = StarredStatusResults()
|
||||
|
||||
group.enter()
|
||||
account.markAsStarred(remoteStarredArticleIDs) { error in
|
||||
results.markAsStarredError = error
|
||||
group.leave()
|
||||
}
|
||||
|
||||
let deltaUnstarredArticleIDs = localStarredArticleIDs.subtracting(remoteStarredArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { error in
|
||||
results.markAsUnstarredError = error
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
let markingError = results.markAsStarredError ?? results.markAsUnstarredError
|
||||
guard let error = markingError else {
|
||||
self.didFinish()
|
||||
return
|
||||
}
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// FeedlyIngestStreamArticleIdsOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 9/1/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
/// Single responsibility is to ensure a status exists for every article id the user might be interested in.
|
||||
///
|
||||
/// Typically, it pages through the article ids of the global.all stream.
|
||||
/// As the article ids are collected, a default read status is created for each.
|
||||
/// So this operation has side effects *for the entire account* it operates on.
|
||||
class FeedlyIngestStreamArticleIdsOperation: FeedlyOperation {
|
||||
private let account: Account
|
||||
private let resource: FeedlyResourceId
|
||||
private let service: FeedlyGetStreamIdsService
|
||||
private let log: OSLog
|
||||
|
||||
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, log: OSLog) {
|
||||
self.account = account
|
||||
self.resource = resource
|
||||
self.service = service
|
||||
self.log = log
|
||||
}
|
||||
|
||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, log: OSLog) {
|
||||
let all = FeedlyCategoryResourceId.Global.all(for: credentials.username)
|
||||
self.init(account: account, resource: all, service: service, log: log)
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
getStreamIds(nil)
|
||||
}
|
||||
|
||||
private func getStreamIds(_ continuation: String?) {
|
||||
service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: nil, completion: didGetStreamIds(_:))
|
||||
}
|
||||
|
||||
private func didGetStreamIds(_ result: Result<FeedlyStreamIds, Error>) {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let streamIds):
|
||||
account.createStatusesIfNeeded(articleIDs: Set(streamIds.ids)) { databaseError in
|
||||
|
||||
if let error = databaseError {
|
||||
self.didFinish(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let continuation = streamIds.continuation else {
|
||||
os_log(.debug, log: self.log, "Reached end of stream for %@", self.resource.id)
|
||||
self.didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
self.getStreamIds(continuation)
|
||||
}
|
||||
case .failure(let error):
|
||||
didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
//
|
||||
// FeedlyIngestUnreadArticleIdsOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 18/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
import RSParser
|
||||
|
||||
/// Single responsibility is to clone locally the remote unread article state.
|
||||
///
|
||||
/// Typically, it pages through the unread article ids of the global.all stream.
|
||||
/// When all the unread article ids are collected, a status is created for each.
|
||||
/// The article ids previously marked as unread but not collected become read.
|
||||
/// So this operation has side effects *for the entire account* it operates on.
|
||||
final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation {
|
||||
private let account: Account
|
||||
private let resource: FeedlyResourceId
|
||||
private let service: FeedlyGetStreamIdsService
|
||||
private let entryIdsProvider: FeedlyEntryIdentifierProvider
|
||||
private let log: OSLog
|
||||
|
||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
|
||||
let resource = FeedlyCategoryResourceId.Global.all(for: credentials.username)
|
||||
self.init(account: account, resource: resource, service: service, newerThan: newerThan, log: log)
|
||||
}
|
||||
|
||||
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
|
||||
self.account = account
|
||||
self.resource = resource
|
||||
self.service = service
|
||||
self.entryIdsProvider = FeedlyEntryIdentifierProvider()
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
getStreamIds(nil)
|
||||
}
|
||||
|
||||
private func getStreamIds(_ continuation: String?) {
|
||||
service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: true, completion: didGetStreamIds(_:))
|
||||
}
|
||||
|
||||
private func didGetStreamIds(_ result: Result<FeedlyStreamIds, Error>) {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let streamIds):
|
||||
|
||||
entryIdsProvider.addEntryIds(in: streamIds.ids)
|
||||
|
||||
guard let continuation = streamIds.continuation else {
|
||||
updateUnreadStatuses()
|
||||
return
|
||||
}
|
||||
|
||||
getStreamIds(continuation)
|
||||
|
||||
case .failure(let error):
|
||||
didFinish(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateUnreadStatuses() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
account.fetchUnreadArticleIDs { result in
|
||||
switch result {
|
||||
case .success(let localUnreadArticleIDs):
|
||||
self.processUnreadArticleIDs(localUnreadArticleIDs)
|
||||
|
||||
case .failure(let error):
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processUnreadArticleIDs(_ localUnreadArticleIDs: Set<String>) {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
let remoteUnreadArticleIDs = entryIdsProvider.entryIds
|
||||
let group = DispatchGroup()
|
||||
|
||||
final class ReadStatusResults {
|
||||
var markAsUnreadError: Error?
|
||||
var markAsReadError: Error?
|
||||
}
|
||||
|
||||
let results = ReadStatusResults()
|
||||
|
||||
group.enter()
|
||||
account.markAsUnread(remoteUnreadArticleIDs) { error in
|
||||
results.markAsUnreadError = error
|
||||
group.leave()
|
||||
}
|
||||
|
||||
let articleIDsToMarkRead = localUnreadArticleIDs.subtracting(remoteUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsRead(articleIDsToMarkRead) { error in
|
||||
results.markAsReadError = error
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
let markingError = results.markAsReadError ?? results.markAsUnreadError
|
||||
guard let error = markingError else {
|
||||
self.didFinish()
|
||||
return
|
||||
}
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -94,19 +94,19 @@ class FeedlyOperation: Operation {
|
|||
let isFinishedDidChange = finished != isFinishedOperation
|
||||
|
||||
if isFinishedDidChange {
|
||||
willChangeValue(for: \.isFinished)
|
||||
willChangeValue(forKey: #keyPath(isFinished))
|
||||
}
|
||||
if isExecutingDidChange {
|
||||
willChangeValue(for: \.isExecuting)
|
||||
willChangeValue(forKey: #keyPath(isExecuting))
|
||||
}
|
||||
isExecutingOperation = executing
|
||||
isFinishedOperation = finished
|
||||
|
||||
if isExecutingDidChange {
|
||||
didChangeValue(for: \.isExecuting)
|
||||
didChangeValue(forKey: #keyPath(isExecuting))
|
||||
}
|
||||
if isFinishedDidChange {
|
||||
didChangeValue(for: \.isFinished)
|
||||
didChangeValue(forKey: #keyPath(isFinished))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import RSParser
|
|||
import os.log
|
||||
|
||||
protocol FeedlyParsedItemsByFeedProviding {
|
||||
var providerName: String { get }
|
||||
var parsedItemsByFeedProviderName: String { get }
|
||||
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>] { get }
|
||||
}
|
||||
|
||||
|
@ -21,15 +21,15 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar
|
|||
private let parsedItemProvider: FeedlyParsedItemProviding
|
||||
private let log: OSLog
|
||||
|
||||
var parsedItemsByFeedProviderName: String {
|
||||
return name ?? String(describing: Self.self)
|
||||
}
|
||||
|
||||
var parsedItemsKeyedByFeedId: [String : Set<ParsedItem>] {
|
||||
assert(Thread.isMainThread) // Needs to be on main thread because Feed is a main-thread-only model type.
|
||||
return itemsKeyedByFeedId
|
||||
}
|
||||
|
||||
var providerName: String {
|
||||
return parsedItemProvider.resource.id
|
||||
}
|
||||
|
||||
private var itemsKeyedByFeedId = [String: Set<ParsedItem>]()
|
||||
|
||||
init(account: Account, parsedItemProvider: FeedlyParsedItemProviding, log: OSLog) {
|
||||
|
@ -61,7 +61,7 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar
|
|||
guard !isCancelled else { return }
|
||||
}
|
||||
|
||||
os_log(.debug, log: log, "Grouped %i items by %i feeds for %@", items.count, dict.count, parsedItemProvider.resource.id)
|
||||
os_log(.debug, log: log, "Grouped %i items by %i feeds for %@", items.count, dict.count, parsedItemProvider.parsedItemProviderName)
|
||||
|
||||
itemsKeyedByFeedId = dict
|
||||
}
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
//
|
||||
// FeedlySetStarredArticlesOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 14/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
protocol FeedlyStarredEntryIdProviding {
|
||||
var entryIds: Set<String> { get }
|
||||
}
|
||||
|
||||
/// Single responsibility is to associate a starred status for ingested and remote
|
||||
/// articles identfied by the provided identifiers *for the entire account.*
|
||||
final class FeedlySetStarredArticlesOperation: FeedlyOperation {
|
||||
private let account: Account
|
||||
private let allStarredEntryIdsProvider: FeedlyStarredEntryIdProviding
|
||||
private let log: OSLog
|
||||
|
||||
init(account: Account, allStarredEntryIdsProvider: FeedlyStarredEntryIdProviding, log: OSLog) {
|
||||
self.account = account
|
||||
self.allStarredEntryIdsProvider = allStarredEntryIdsProvider
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
account.fetchStarredArticleIDs { result in
|
||||
switch result {
|
||||
case .success(let localStarredArticleIDs):
|
||||
self.processStarredArticleIDs(localStarredArticleIDs)
|
||||
|
||||
case .failure(let error):
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension FeedlySetStarredArticlesOperation {
|
||||
|
||||
func processStarredArticleIDs(_ localStarredArticleIDs: Set<String>) {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
let remoteStarredArticleIDs = allStarredEntryIdsProvider.entryIds
|
||||
guard !remoteStarredArticleIDs.isEmpty else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
final class StarredStatusResults {
|
||||
var markAsStarredError: Error?
|
||||
var markAsUnstarredError: Error?
|
||||
}
|
||||
|
||||
let results = StarredStatusResults()
|
||||
|
||||
group.enter()
|
||||
account.markAsStarred(remoteStarredArticleIDs) { error in
|
||||
results.markAsStarredError = error
|
||||
group.leave()
|
||||
}
|
||||
|
||||
let deltaUnstarredArticleIDs = localStarredArticleIDs.subtracting(remoteStarredArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { error in
|
||||
results.markAsUnstarredError = error
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
let markingError = results.markAsStarredError ?? results.markAsUnstarredError
|
||||
guard let error = markingError else {
|
||||
self.didFinish()
|
||||
return
|
||||
}
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
//
|
||||
// FeedlySetUnreadArticlesOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 25/9/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
protocol FeedlyUnreadEntryIdProviding {
|
||||
var entryIds: Set<String> { get }
|
||||
}
|
||||
|
||||
/// Single responsibility is to associate a read status for ingested and remote articles
|
||||
/// where the provided article identifers identify the unread articles *for the entire account.*
|
||||
final class FeedlySetUnreadArticlesOperation: FeedlyOperation {
|
||||
private let account: Account
|
||||
private let allUnreadIdsProvider: FeedlyUnreadEntryIdProviding
|
||||
private let log: OSLog
|
||||
|
||||
init(account: Account, allUnreadIdsProvider: FeedlyUnreadEntryIdProviding, log: OSLog) {
|
||||
self.account = account
|
||||
self.allUnreadIdsProvider = allUnreadIdsProvider
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
account.fetchUnreadArticleIDs { result in
|
||||
switch result {
|
||||
case .success(let localUnreadArticleIDs):
|
||||
self.processUnreadArticleIDs(localUnreadArticleIDs)
|
||||
|
||||
case .failure(let error):
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension FeedlySetUnreadArticlesOperation {
|
||||
|
||||
private func processUnreadArticleIDs(_ localUnreadArticleIDs: Set<String>) {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
let remoteUnreadArticleIDs = allUnreadIdsProvider.entryIds
|
||||
guard !remoteUnreadArticleIDs.isEmpty else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
final class ReadStatusResults {
|
||||
var markAsUnreadError: Error?
|
||||
var markAsReadError: Error?
|
||||
}
|
||||
|
||||
let results = ReadStatusResults()
|
||||
|
||||
group.enter()
|
||||
account.markAsUnread(remoteUnreadArticleIDs) { error in
|
||||
results.markAsUnreadError = error
|
||||
group.leave()
|
||||
}
|
||||
|
||||
let articleIDsToMarkRead = localUnreadArticleIDs.subtracting(remoteUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsRead(articleIDsToMarkRead) { error in
|
||||
results.markAsReadError = error
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
let markingError = results.markAsReadError ?? results.markAsUnreadError
|
||||
guard let error = markingError else {
|
||||
self.didFinish()
|
||||
return
|
||||
}
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,19 @@ final class FeedlySyncAllOperation: FeedlyOperation {
|
|||
|
||||
var syncCompletionHandler: ((Result<Void, Error>) -> ())?
|
||||
|
||||
init(account: Account, credentials: Credentials, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredArticlesService: FeedlyGetStreamContentsService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) {
|
||||
/// These requests to Feedly determine which articles to download:
|
||||
/// 1. The set of all article ids we might need or show.
|
||||
/// 2. The set of all unread article ids we might need or show (a subset of 1).
|
||||
/// 3. The set of all article ids changed since the last sync (a subset of 1).
|
||||
/// 4. The set of all starred article ids.
|
||||
///
|
||||
/// On the response for 1, create statuses for each article id.
|
||||
/// On the response for 2, create unread statuses for each article id and mark as read those no longer in the response.
|
||||
/// On the response for 4, create starred statuses for each article id and mark as unstarred those no longer in the response.
|
||||
///
|
||||
/// Download articles for statuses at the union of those statuses without its corresponding article and those included in 3 (changed since last successful sync).
|
||||
///
|
||||
init(account: Account, credentials: Credentials, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredService: FeedlyGetStreamIdsService, getStreamIdsService: FeedlyGetStreamIdsService, getEntriesService: FeedlyGetEntriesService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) {
|
||||
self.syncUUID = UUID()
|
||||
self.log = log
|
||||
self.operationQueue = OperationQueue()
|
||||
|
@ -54,48 +66,66 @@ final class FeedlySyncAllOperation: FeedlyOperation {
|
|||
createFeedsOperation.addDependency(mirrorCollectionsAsFolders)
|
||||
self.operationQueue.addOperation(createFeedsOperation)
|
||||
|
||||
let getAllArticleIds = FeedlyIngestStreamArticleIdsOperation(account: account, credentials: credentials, service: getStreamIdsService, log: log)
|
||||
getAllArticleIds.delegate = self
|
||||
getAllArticleIds.downloadProgress = downloadProgress
|
||||
getAllArticleIds.addDependency(createFeedsOperation)
|
||||
self.operationQueue.addOperation(getAllArticleIds)
|
||||
|
||||
// Get each page of unread article ids in the global.all stream for the last 31 days (nil = Feedly API default).
|
||||
let getUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: getUnreadService, newerThan: nil, log: log)
|
||||
let getUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: getUnreadService, newerThan: nil, log: log)
|
||||
getUnread.delegate = self
|
||||
getUnread.addDependency(createFeedsOperation)
|
||||
getUnread.addDependency(getAllArticleIds)
|
||||
getUnread.downloadProgress = downloadProgress
|
||||
self.operationQueue.addOperation(getUnread)
|
||||
|
||||
// Get each page of the global.all stream until we get either the content from the last sync or the last 31 days.
|
||||
let getStreamContents = FeedlySyncStreamContentsOperation(account: account, credentials: credentials, service: getStreamContentsService, newerThan: lastSuccessfulFetchStartDate, log: log)
|
||||
getStreamContents.delegate = self
|
||||
getStreamContents.downloadProgress = downloadProgress
|
||||
getStreamContents.addDependency(getUnread)
|
||||
self.operationQueue.addOperation(getStreamContents)
|
||||
// Get each page of the article ids which have been update since the last successful fetch start date.
|
||||
// If the date is nil, this operation provides an empty set (everything is new, nothing is updated).
|
||||
let getUpdated = FeedlyGetUpdatedArticleIdsOperation(account: account, credentials: credentials, service: getStreamIdsService, newerThan: lastSuccessfulFetchStartDate, log: log)
|
||||
getUpdated.delegate = self
|
||||
getUpdated.downloadProgress = downloadProgress
|
||||
getUpdated.addDependency(createFeedsOperation)
|
||||
self.operationQueue.addOperation(getUpdated)
|
||||
|
||||
// Get each and every starred article.
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, credentials: credentials, service: getStarredArticlesService, log: log)
|
||||
syncStarred.delegate = self
|
||||
syncStarred.downloadProgress = downloadProgress
|
||||
syncStarred.addDependency(createFeedsOperation)
|
||||
self.operationQueue.addOperation(syncStarred)
|
||||
// Get each page of the article ids for starred articles.
|
||||
let getStarred = FeedlyIngestStarredArticleIdsOperation(account: account, credentials: credentials, service: getStarredService, newerThan: nil, log: log)
|
||||
getStarred.delegate = self
|
||||
getStarred.downloadProgress = downloadProgress
|
||||
getStarred.addDependency(createFeedsOperation)
|
||||
self.operationQueue.addOperation(getStarred)
|
||||
|
||||
// Now all the possible article ids we need have a status, fetch the article ids for missing articles.
|
||||
let getMissingIds = FeedlyFetchIdsForMissingArticlesOperation(account: account, log: log)
|
||||
getMissingIds.delegate = self
|
||||
getMissingIds.downloadProgress = downloadProgress
|
||||
getMissingIds.addDependency(getAllArticleIds)
|
||||
getMissingIds.addDependency(getUnread)
|
||||
getMissingIds.addDependency(getStarred)
|
||||
getMissingIds.addDependency(getUpdated)
|
||||
self.operationQueue.addOperation(getMissingIds)
|
||||
|
||||
// Download all the missing and updated articles
|
||||
let downloadMissingArticles = FeedlyDownloadArticlesOperation(account: account,
|
||||
missingArticleEntryIdProvider: getMissingIds,
|
||||
updatedArticleEntryIdProvider: getUpdated,
|
||||
getEntriesService: getEntriesService,
|
||||
log: log)
|
||||
downloadMissingArticles.delegate = self
|
||||
downloadMissingArticles.downloadProgress = downloadProgress
|
||||
downloadMissingArticles.addDependency(getMissingIds)
|
||||
downloadMissingArticles.addDependency(getUpdated)
|
||||
self.operationQueue.addOperation(downloadMissingArticles)
|
||||
|
||||
// Once this operation's dependencies, their dependencies etc finish, we can finish.
|
||||
let finishOperation = FeedlyCheckpointOperation()
|
||||
finishOperation.checkpointDelegate = self
|
||||
finishOperation.downloadProgress = downloadProgress
|
||||
finishOperation.addDependency(getStreamContents)
|
||||
finishOperation.addDependency(syncStarred)
|
||||
|
||||
finishOperation.addDependency(downloadMissingArticles)
|
||||
self.operationQueue.addOperation(finishOperation)
|
||||
}
|
||||
|
||||
convenience init(account: Account, credentials: Credentials, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) {
|
||||
|
||||
let newerThan: Date? = {
|
||||
if let date = lastSuccessfulFetchStartDate {
|
||||
return date
|
||||
} else {
|
||||
return Calendar.current.date(byAdding: .day, value: -31, to: Date())
|
||||
}
|
||||
}()
|
||||
|
||||
self.init(account: account, credentials: credentials, lastSuccessfulFetchStartDate: newerThan, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredArticlesService: caller, database: database, downloadProgress: downloadProgress, log: log)
|
||||
self.init(account: account, credentials: credentials, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredService: caller, getStreamIdsService: caller, getEntriesService: caller, database: database, downloadProgress: downloadProgress, log: log)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
|
|
|
@ -1,150 +0,0 @@
|
|||
//
|
||||
// FeedlySyncStarredArticlesOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 15/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
import RSParser
|
||||
|
||||
final class FeedlySyncStarredArticlesOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamContentsOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
private let account: Account
|
||||
private let operationQueue: OperationQueue
|
||||
private let service: FeedlyGetStreamContentsService
|
||||
private let log: OSLog
|
||||
|
||||
private let setStatuses: FeedlySetStarredArticlesOperation
|
||||
private let finishOperation: FeedlyCheckpointOperation
|
||||
|
||||
/// Buffers every starred/saved entry from every page.
|
||||
private class StarredEntryProvider: FeedlyEntryProviding, FeedlyStarredEntryIdProviding, FeedlyParsedItemProviding {
|
||||
var resource: FeedlyResourceId
|
||||
|
||||
private(set) var parsedEntries = Set<ParsedItem>()
|
||||
private(set) var entries = [FeedlyEntry]()
|
||||
|
||||
init(resource: FeedlyResourceId) {
|
||||
self.resource = resource
|
||||
}
|
||||
|
||||
func addEntries(from provider: FeedlyEntryProviding & FeedlyParsedItemProviding) {
|
||||
entries.append(contentsOf: provider.entries)
|
||||
parsedEntries.formUnion(provider.parsedEntries)
|
||||
}
|
||||
|
||||
var entryIds: Set<String> {
|
||||
return Set(entries.map { $0.id })
|
||||
}
|
||||
}
|
||||
|
||||
private let entryProvider: StarredEntryProvider
|
||||
|
||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamContentsService, log: OSLog) {
|
||||
let saved = FeedlyTagResourceId.Global.saved(for: credentials.username)
|
||||
self.init(account: account, resource: saved, service: service, log: log)
|
||||
}
|
||||
|
||||
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamContentsService, log: OSLog) {
|
||||
self.account = account
|
||||
self.service = service
|
||||
self.operationQueue = OperationQueue()
|
||||
self.operationQueue.isSuspended = true
|
||||
self.finishOperation = FeedlyCheckpointOperation()
|
||||
self.log = log
|
||||
|
||||
let provider = StarredEntryProvider(resource: resource)
|
||||
self.entryProvider = provider
|
||||
self.setStatuses = FeedlySetStarredArticlesOperation(account: account,
|
||||
allStarredEntryIdsProvider: provider,
|
||||
log: log)
|
||||
|
||||
super.init()
|
||||
|
||||
let getFirstPage = FeedlyGetStreamContentsOperation(account: account,
|
||||
resource: resource,
|
||||
service: service,
|
||||
newerThan: nil,
|
||||
log: log)
|
||||
|
||||
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
|
||||
parsedItemProvider: provider,
|
||||
log: log)
|
||||
|
||||
let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account,
|
||||
organisedItemsProvider: organiseByFeed,
|
||||
log: log)
|
||||
|
||||
getFirstPage.delegate = self
|
||||
getFirstPage.streamDelegate = self
|
||||
|
||||
setStatuses.addDependency(getFirstPage)
|
||||
setStatuses.delegate = self
|
||||
|
||||
organiseByFeed.addDependency(setStatuses)
|
||||
organiseByFeed.delegate = self
|
||||
|
||||
updateAccount.addDependency(organiseByFeed)
|
||||
updateAccount.delegate = self
|
||||
|
||||
finishOperation.checkpointDelegate = self
|
||||
finishOperation.addDependency(updateAccount)
|
||||
|
||||
let operations = [getFirstPage, setStatuses, organiseByFeed, updateAccount, finishOperation]
|
||||
operationQueue.addOperations(operations, waitUntilFinished: false)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
os_log(.debug, log: log, "Canceling sync starred articles")
|
||||
operationQueue.cancelAllOperations()
|
||||
super.cancel()
|
||||
didFinish()
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
// override of cancel calls didFinish().
|
||||
return
|
||||
}
|
||||
|
||||
operationQueue.isSuspended = false
|
||||
}
|
||||
|
||||
func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream) {
|
||||
guard !isCancelled else {
|
||||
os_log(.debug, log: log, "Cancelled starred stream contents for %@", stream.id)
|
||||
return
|
||||
}
|
||||
|
||||
entryProvider.addEntries(from: operation)
|
||||
os_log(.debug, log: log, "Collecting %i items from %@", stream.items.count, stream.id)
|
||||
|
||||
guard let continuation = stream.continuation else {
|
||||
return
|
||||
}
|
||||
|
||||
let nextPageOperation = FeedlyGetStreamContentsOperation(account: operation.account,
|
||||
resource: operation.resource,
|
||||
service: operation.service,
|
||||
continuation: continuation,
|
||||
newerThan: operation.newerThan,
|
||||
log: log)
|
||||
nextPageOperation.delegate = self
|
||||
nextPageOperation.streamDelegate = self
|
||||
|
||||
setStatuses.addDependency(nextPageOperation)
|
||||
operationQueue.addOperation(nextPageOperation)
|
||||
}
|
||||
|
||||
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
|
||||
didFinish()
|
||||
}
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
os_log(.debug, log: log, "%{public}@ failing and cancelling other operations because %{public}@.", operation, error.localizedDescription)
|
||||
operationQueue.cancelAllOperations()
|
||||
didFinish(error)
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
//
|
||||
// FeedlySyncUnreadStatusesOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 18/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
import RSParser
|
||||
|
||||
/// Makes one or more requests to get the complete set of unread article ids to update the status of those articles *for the entire account.*
|
||||
final class FeedlySyncUnreadStatusesOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamIdsOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
private let account: Account
|
||||
private let resource: FeedlyResourceId
|
||||
private let operationQueue: OperationQueue
|
||||
private let service: FeedlyGetStreamIdsService
|
||||
private let log: OSLog
|
||||
|
||||
/// Buffers every unread article id from every page of the resource's stream.
|
||||
private class UnreadEntryIdsProvider: FeedlyUnreadEntryIdProviding {
|
||||
let resource: FeedlyResourceId
|
||||
private(set) var entryIds = Set<String>()
|
||||
|
||||
init(resource: FeedlyResourceId) {
|
||||
self.resource = resource
|
||||
}
|
||||
|
||||
func addEntryIds(from provider: FeedlyEntryIdenifierProviding) {
|
||||
entryIds.formUnion(provider.entryIds)
|
||||
}
|
||||
}
|
||||
|
||||
private let unreadEntryIdsProvider: UnreadEntryIdsProvider
|
||||
private let setStatuses: FeedlySetUnreadArticlesOperation
|
||||
|
||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
|
||||
let resource = FeedlyCategoryResourceId.Global.all(for: credentials.username)
|
||||
self.init(account: account, resource: resource, service: service, newerThan: newerThan, log: log)
|
||||
}
|
||||
|
||||
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
|
||||
self.account = account
|
||||
self.resource = resource
|
||||
self.service = service
|
||||
self.operationQueue = OperationQueue()
|
||||
self.operationQueue.isSuspended = true
|
||||
self.log = log
|
||||
|
||||
let provider = UnreadEntryIdsProvider(resource: resource)
|
||||
self.unreadEntryIdsProvider = provider
|
||||
self.setStatuses = FeedlySetUnreadArticlesOperation(account: account,
|
||||
allUnreadIdsProvider: unreadEntryIdsProvider,
|
||||
log: log)
|
||||
|
||||
super.init()
|
||||
|
||||
let getFirstPageOfUnreadIds = FeedlyGetStreamIdsOperation(account: account,
|
||||
resource: resource,
|
||||
service: service,
|
||||
newerThan: nil,
|
||||
unreadOnly: true,
|
||||
log: log)
|
||||
|
||||
getFirstPageOfUnreadIds.delegate = self
|
||||
getFirstPageOfUnreadIds.streamIdsDelegate = self
|
||||
|
||||
setStatuses.addDependency(getFirstPageOfUnreadIds)
|
||||
setStatuses.delegate = self
|
||||
|
||||
let finishOperation = FeedlyCheckpointOperation()
|
||||
finishOperation.checkpointDelegate = self
|
||||
finishOperation.addDependency(setStatuses)
|
||||
|
||||
let operations = [getFirstPageOfUnreadIds, setStatuses, finishOperation]
|
||||
operationQueue.addOperations(operations, waitUntilFinished: false)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
os_log(.debug, log: log, "Canceling sync unread statuses")
|
||||
operationQueue.cancelAllOperations()
|
||||
super.cancel()
|
||||
didFinish()
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
// override of cancel calls didFinish().
|
||||
return
|
||||
}
|
||||
|
||||
operationQueue.isSuspended = false
|
||||
}
|
||||
|
||||
func feedlyGetStreamIdsOperation(_ operation: FeedlyGetStreamIdsOperation, didGet streamIds: FeedlyStreamIds) {
|
||||
guard !isCancelled else {
|
||||
os_log(.debug, log: log, "Cancelled unread stream ids.")
|
||||
return
|
||||
}
|
||||
|
||||
os_log(.debug, log: log, "Collecting %i unread article ids from %@", streamIds.ids.count, resource.id)
|
||||
unreadEntryIdsProvider.addEntryIds(from: operation)
|
||||
|
||||
guard let continuation = streamIds.continuation else {
|
||||
return
|
||||
}
|
||||
|
||||
let nextPageOperation = FeedlyGetStreamIdsOperation(account: operation.account,
|
||||
resource: operation.resource,
|
||||
service: operation.service,
|
||||
continuation: continuation,
|
||||
newerThan: operation.newerThan,
|
||||
unreadOnly: operation.unreadOnly,
|
||||
log: log)
|
||||
nextPageOperation.delegate = self
|
||||
nextPageOperation.streamIdsDelegate = self
|
||||
|
||||
setStatuses.addDependency(nextPageOperation)
|
||||
operationQueue.addOperation(nextPageOperation)
|
||||
}
|
||||
|
||||
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
|
||||
didFinish()
|
||||
}
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
operationQueue.cancelAllOperations()
|
||||
didFinish(error)
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation {
|
|||
return
|
||||
}
|
||||
|
||||
os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", webFeedIDsAndItems.count, self.organisedItemsProvider.providerName)
|
||||
os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", webFeedIDsAndItems.count, self.organisedItemsProvider.parsedItemsByFeedProviderName)
|
||||
self.didFinish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ extension Folder: OPMLRepresentable {
|
|||
|
||||
var hasAtLeastOneChild = false
|
||||
|
||||
for feed in topLevelWebFeeds.sorted(by: { $0.nameForDisplay < $1.nameForDisplay }) {
|
||||
for feed in topLevelWebFeeds.sorted() {
|
||||
s += feed.OPMLString(indentLevel: indentLevel + 1, strictConformance: strictConformance)
|
||||
hasAtLeastOneChild = true
|
||||
}
|
||||
|
@ -206,3 +206,14 @@ extension Folder: OPMLRepresentable {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Set
|
||||
|
||||
extension Set where Element == Folder {
|
||||
|
||||
func sorted() -> Array<Folder> {
|
||||
return sorted(by: { (folder1, folder2) -> Bool in
|
||||
return folder1.nameForDisplay.localizedStandardCompare(folder2.nameForDisplay) == .orderedAscending
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -276,4 +276,14 @@ extension Set where Element == WebFeed {
|
|||
func webFeedIDs() -> Set<String> {
|
||||
return Set<String>(map { $0.webFeedID })
|
||||
}
|
||||
|
||||
func sorted() -> Array<WebFeed> {
|
||||
return sorted(by: { (webFeed1, webFeed2) -> Bool in
|
||||
if webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedSame {
|
||||
return webFeed1.url < webFeed2.url
|
||||
}
|
||||
return webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedAscending
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ PROVISIONING_PROFILE_SPECIFIER =
|
|||
#include? "../../../SharedXcodeSettings/DeveloperSettings.xcconfig"
|
||||
|
||||
SDKROOT = macosx
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0
|
||||
SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator
|
||||
|
||||
|
@ -18,7 +18,6 @@ SWIFT_VERSION = 5.1
|
|||
COMBINE_HIDPI_IMAGES = YES
|
||||
|
||||
COPY_PHASE_STRIP = NO
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13
|
||||
ALWAYS_SEARCH_USER_PATHS = NO
|
||||
CURRENT_PROJECT_VERSION = 1
|
||||
VERSION_INFO_PREFIX =
|
||||
|
|
|
@ -176,7 +176,7 @@ public final class ArticlesDatabase {
|
|||
articlesTable.fetchStarredArticleIDsAsync(webFeedIDs, completion)
|
||||
}
|
||||
|
||||
/// Fetch articleIDs for articles that we should have, but don’t. These articles are not userDeleted, and they are either (starred) or (unread and newer than the article cutoff date).
|
||||
/// Fetch articleIDs for articles that we should have, but don’t. These articles are not userDeleted, and they are either (starred) or (newer than the article cutoff date).
|
||||
public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) {
|
||||
articlesTable.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
|
||||
}
|
||||
|
@ -189,6 +189,12 @@ public final class ArticlesDatabase {
|
|||
articlesTable.mark(articleIDs, statusKey, flag, completion)
|
||||
}
|
||||
|
||||
/// Create statuses for specified articleIDs. For existing statuses, don’t do anything.
|
||||
/// For newly-created statuses, mark them as read and not-starred.
|
||||
public func createStatusesIfNeeded(articleIDs: Set<String>, completion: @escaping DatabaseCompletionBlock) {
|
||||
articlesTable.createStatusesIfNeeded(articleIDs, completion)
|
||||
}
|
||||
|
||||
// MARK: - Suspend and Resume (for iOS)
|
||||
|
||||
/// Close the database and stop running database calls.
|
||||
|
@ -216,6 +222,7 @@ public final class ArticlesDatabase {
|
|||
|
||||
/// Calls the various clean-up functions.
|
||||
public func cleanupDatabaseAtStartup(subscribedToWebFeedIDs: Set<String>) {
|
||||
articlesTable.deleteOldArticles()
|
||||
articlesTable.deleteArticlesNotInSubscribedToFeedIDs(subscribedToWebFeedIDs)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -421,6 +421,22 @@ final class ArticlesTable: DatabaseTable {
|
|||
}
|
||||
}
|
||||
|
||||
func createStatusesIfNeeded(_ articleIDs: Set<String>, _ completion: @escaping DatabaseCompletionBlock) {
|
||||
queue.runInTransaction { databaseResult in
|
||||
switch databaseResult {
|
||||
case .success(let database):
|
||||
let _ = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, true, database)
|
||||
DispatchQueue.main.async {
|
||||
completion(nil)
|
||||
}
|
||||
case .failure(let databaseError):
|
||||
DispatchQueue.main.async {
|
||||
completion(databaseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Indexing
|
||||
|
||||
func indexUnindexedArticles() {
|
||||
|
@ -458,6 +474,24 @@ final class ArticlesTable: DatabaseTable {
|
|||
|
||||
// MARK: - Cleanup
|
||||
|
||||
/// Delete articles that we won’t show in the UI any longer
|
||||
/// — their arrival date is before our 90-day recency window.
|
||||
/// Keep all starred articles, no matter their age.
|
||||
func deleteOldArticles() {
|
||||
queue.runInTransaction { databaseResult in
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
let sql = "delete from articles where articleID in (select articleID from articles natural join statuses where dateArrived<? and starred=0);"
|
||||
let parameters = [self.articleCutoffDate] as [Any]
|
||||
database.executeUpdate(sql, withArgumentsIn: parameters)
|
||||
}
|
||||
|
||||
if let database = databaseResult.database {
|
||||
makeDatabaseCalls(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete articles from feeds that are no longer in the current set of subscribed-to feeds.
|
||||
/// This deletes from the articles and articleStatuses tables,
|
||||
/// and, via a trigger, it also deletes from the search index.
|
||||
|
|
|
@ -108,7 +108,7 @@ final class StatusesTable: DatabaseTable {
|
|||
var articleIDs = Set<String>()
|
||||
|
||||
func makeDatabaseCall(_ database: FMDatabase) {
|
||||
let sql = "select articleID from statuses s where ((starred=1) || (read=0 and dateArrived > ?)) and userDeleted=0 and not exists (select 1 from articles a where a.articleID = s.articleID);"
|
||||
let sql = "select articleID from statuses s where (starred=1 or dateArrived>?) and userDeleted=0 and not exists (select 1 from articles a where a.articleID = s.articleID);"
|
||||
if let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) {
|
||||
articleIDs = resultSet.mapToSet(self.articleIDWithRow)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ PROVISIONING_PROFILE_SPECIFIER =
|
|||
#include? "../../../SharedXcodeSettings/DeveloperSettings.xcconfig"
|
||||
|
||||
SDKROOT = macosx
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0
|
||||
SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator
|
||||
|
||||
|
@ -18,7 +18,6 @@ SWIFT_VERSION = 5.1
|
|||
COMBINE_HIDPI_IMAGES = YES
|
||||
|
||||
COPY_PHASE_STRIP = NO
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13
|
||||
ALWAYS_SEARCH_USER_PATHS = NO
|
||||
CURRENT_PROJECT_VERSION = 1
|
||||
VERSION_INFO_PREFIX =
|
||||
|
|
|
@ -85,7 +85,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
|||
NSWindow.allowsAutomaticWindowTabbing = false
|
||||
super.init()
|
||||
|
||||
AccountManager.shared = AccountManager(accountsFolder: RSDataSubfolder(nil, "Accounts")!)
|
||||
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil)
|
||||
|
@ -107,12 +107,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
|||
}
|
||||
|
||||
func logDebugMessage(_ message: String) {
|
||||
|
||||
logMessage(message, type: .debug)
|
||||
}
|
||||
|
||||
func showAddFolderSheetOnWindow(_ window: NSWindow) {
|
||||
|
||||
addFolderWindowController = AddFolderWindowController()
|
||||
addFolderWindowController!.runSheetOnWindow(window)
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
|||
private extension AddFeedController {
|
||||
|
||||
var urlStringFromPasteboard: String? {
|
||||
if let urlString = NSPasteboard.rs_urlString(from: NSPasteboard.general) {
|
||||
if let urlString = NSPasteboard.urlString(from: NSPasteboard.general) {
|
||||
return urlString.rs_normalizedURL()
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -63,7 +63,7 @@ final class IconView: NSView {
|
|||
}
|
||||
|
||||
override func resizeSubviews(withOldSize oldSize: NSSize) {
|
||||
imageView.rs_setFrameIfNotEqual(rectForImageView())
|
||||
imageView.setFrame(ifNotEqualTo: rectForImageView())
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
|
|
|
@ -142,9 +142,9 @@ private extension SidebarCell {
|
|||
}
|
||||
|
||||
func layoutWith(_ layout: SidebarCellLayout) {
|
||||
faviconImageView.rs_setFrameIfNotEqual(layout.faviconRect)
|
||||
titleView.rs_setFrameIfNotEqual(layout.titleRect)
|
||||
unreadCountView.rs_setFrameIfNotEqual(layout.unreadCountRect)
|
||||
faviconImageView.setFrame(ifNotEqualTo: layout.faviconRect)
|
||||
titleView.setFrame(ifNotEqualTo: layout.titleRect)
|
||||
unreadCountView.setFrame(ifNotEqualTo: layout.unreadCountRect)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ struct SidebarCellLayout {
|
|||
var rFavicon = NSRect.zero
|
||||
if shouldShowImage {
|
||||
rFavicon = NSRect(x: 0.0, y: 0.0, width: appearance.imageSize.width, height: appearance.imageSize.height)
|
||||
rFavicon = RSRectCenteredVerticallyInRect(rFavicon, bounds)
|
||||
rFavicon = rFavicon.centeredVertically(in: bounds)
|
||||
}
|
||||
self.faviconRect = rFavicon
|
||||
|
||||
|
@ -34,7 +34,7 @@ struct SidebarCellLayout {
|
|||
if shouldShowImage {
|
||||
rTextField.origin.x = NSMaxX(rFavicon) + appearance.imageMarginRight
|
||||
}
|
||||
rTextField = RSRectCenteredVerticallyInRect(rTextField, bounds)
|
||||
rTextField = rTextField.centeredVertically(in: bounds)
|
||||
|
||||
let unreadCountSize = unreadCountView.intrinsicContentSize
|
||||
let unreadCountIsHidden = unreadCountView.unreadCount < 1
|
||||
|
@ -43,7 +43,7 @@ struct SidebarCellLayout {
|
|||
if !unreadCountIsHidden {
|
||||
rUnread.size = unreadCountSize
|
||||
rUnread.origin.x = NSMaxX(bounds) - unreadCountSize.width
|
||||
rUnread = RSRectCenteredVerticallyInRect(rUnread, bounds)
|
||||
rUnread = rUnread.centeredVertically(in: bounds)
|
||||
let textFieldMaxX = NSMinX(rUnread) - appearance.unreadCountMarginLeft
|
||||
if NSMaxX(rTextField) > textFieldMaxX {
|
||||
rTextField.size.width = textFieldMaxX - NSMinX(rTextField)
|
||||
|
|
|
@ -116,12 +116,12 @@ class TimelineTableCellView: NSTableCellView {
|
|||
setFrame(for: summaryView, rect: layoutRects.summaryRect)
|
||||
setFrame(for: textView, rect: layoutRects.textRect)
|
||||
|
||||
dateView.rs_setFrameIfNotEqual(layoutRects.dateRect)
|
||||
unreadIndicatorView.rs_setFrameIfNotEqual(layoutRects.unreadIndicatorRect)
|
||||
feedNameView.rs_setFrameIfNotEqual(layoutRects.feedNameRect)
|
||||
iconView.rs_setFrameIfNotEqual(layoutRects.iconImageRect)
|
||||
starView.rs_setFrameIfNotEqual(layoutRects.starRect)
|
||||
separatorView.rs_setFrameIfNotEqual(layoutRects.separatorRect)
|
||||
dateView.setFrame(ifNotEqualTo: layoutRects.dateRect)
|
||||
unreadIndicatorView.setFrame(ifNotEqualTo: layoutRects.unreadIndicatorRect)
|
||||
feedNameView.setFrame(ifNotEqualTo: layoutRects.feedNameRect)
|
||||
iconView.setFrame(ifNotEqualTo: layoutRects.iconImageRect)
|
||||
starView.setFrame(ifNotEqualTo: layoutRects.starRect)
|
||||
separatorView.setFrame(ifNotEqualTo: layoutRects.separatorRect)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,7 @@ private extension TimelineTableCellView {
|
|||
}
|
||||
else {
|
||||
showView(textField)
|
||||
textField.rs_setFrameIfNotEqual(rect)
|
||||
textField.setFrame(ifNotEqualTo: rect)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@ private extension AccountsPreferencesViewController {
|
|||
addChild(controller)
|
||||
controller.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
detailView.addSubview(controller.view)
|
||||
detailView.rs_addFullSizeConstraints(forSubview: controller.view)
|
||||
detailView.addFullSizeConstraints(forSubview: controller.view)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
5127B23A222B4849006D641D /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; };
|
||||
512AF9C2236ED52C0066F8BE /* ImageHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */; };
|
||||
512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512AF9DC236F05230066F8BE /* InteractiveLabel.swift */; };
|
||||
512D554423C804DE0023FFFA /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */; };
|
||||
512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; };
|
||||
512E08E72268801200BDCFDD /* WebFeedTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97611ED9EB96007D329B /* WebFeedTreeControllerDelegate.swift */; };
|
||||
512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */; };
|
||||
|
@ -635,6 +636,8 @@
|
|||
B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
|
||||
B2B80779239C4C7300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
|
||||
B528F81E23333C7E00E735DD /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = B528F81D23333C7E00E735DD /* page.html */; };
|
||||
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5A6ED5123C9AF4300AB6BE2 /* TitleActivityItemSource.swift */; };
|
||||
C5A6ED6D23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5A6ED6C23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift */; };
|
||||
D553738B20186C20006D8857 /* Article+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D553737C20186C1F006D8857 /* Article+Scriptability.swift */; };
|
||||
D57BE6E0204CD35F00D11AAC /* NSScriptCommand+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57BE6DF204CD35F00D11AAC /* NSScriptCommand+NetNewsWire.swift */; };
|
||||
D5907D7F2004AC00005947E5 /* NSApplication+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5907D7E2004AC00005947E5 /* NSApplication+Scriptability.swift */; };
|
||||
|
@ -650,7 +653,7 @@
|
|||
FF3ABF13232599810074C542 /* ArticleSorterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF09232599450074C542 /* ArticleSorterTests.swift */; };
|
||||
FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; };
|
||||
FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; };
|
||||
FFD43E412340F488009E5CA3 /* UndoAvailableAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD43E372340F320009E5CA3 /* UndoAvailableAlertController.swift */; };
|
||||
FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -1245,6 +1248,7 @@
|
|||
5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DetailKeyboardShortcuts.plist; sourceTree = "<group>"; };
|
||||
512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageHeaderView.swift; sourceTree = "<group>"; };
|
||||
512AF9DC236F05230066F8BE /* InteractiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveLabel.swift; sourceTree = "<group>"; };
|
||||
512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
|
||||
512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewSectionHeader.swift; sourceTree = "<group>"; };
|
||||
51314617235A797400387FDC /* NetNewsWire_iOSintentextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSintentextension_target.xcconfig; sourceTree = "<group>"; };
|
||||
51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire iOS Intents Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -1570,6 +1574,8 @@
|
|||
B24EFD5923310109006C6242 /* WKPreferencesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKPreferencesPrivate.h; sourceTree = "<group>"; };
|
||||
B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-AppIcons.swift"; sourceTree = "<group>"; };
|
||||
B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
|
||||
C5A6ED5123C9AF4300AB6BE2 /* TitleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
C5A6ED6C23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivityViewController-Extensions.swift"; sourceTree = "<group>"; };
|
||||
D519E74722EE553300923F27 /* NetNewsWire_safariextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_safariextension_target.xcconfig; sourceTree = "<group>"; };
|
||||
D553737C20186C1F006D8857 /* Article+Scriptability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Article+Scriptability.swift"; sourceTree = "<group>"; };
|
||||
D57BE6DF204CD35F00D11AAC /* NSScriptCommand+NetNewsWire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScriptCommand+NetNewsWire.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1590,7 +1596,7 @@
|
|||
DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.swift; sourceTree = "<group>"; };
|
||||
FF3ABF09232599450074C542 /* ArticleSorterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorterTests.swift; sourceTree = "<group>"; };
|
||||
FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorter.swift; sourceTree = "<group>"; };
|
||||
FFD43E372340F320009E5CA3 /* UndoAvailableAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoAvailableAlertController.swift; sourceTree = "<group>"; };
|
||||
FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAsReadAlertController.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -1889,6 +1895,7 @@
|
|||
51FFF0C3235EE8E5002762AA /* VibrantButton.swift */,
|
||||
5186A634235EF3A800C97195 /* VibrantLabel.swift */,
|
||||
5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */,
|
||||
C5A6ED6C23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift */,
|
||||
);
|
||||
path = "UIKit Extensions";
|
||||
sourceTree = "<group>";
|
||||
|
@ -1927,7 +1934,7 @@
|
|||
5148F4542336DB7000F8CD8B /* MasterTimelineTitleView.swift */,
|
||||
5148F44A2336DB4700F8CD8B /* MasterTimelineTitleView.xib */,
|
||||
51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */,
|
||||
FFD43E372340F320009E5CA3 /* UndoAvailableAlertController.swift */,
|
||||
FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */,
|
||||
51C4526F2265091600C03939 /* Cell */,
|
||||
);
|
||||
path = MasterTimeline;
|
||||
|
@ -1951,14 +1958,15 @@
|
|||
51C4527D2265092C00C03939 /* Article */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
51C4527E2265092C00C03939 /* ArticleViewController.swift */,
|
||||
51AB8AB223B7F4C6008F147D /* WebViewController.swift */,
|
||||
517630222336657E00E15FFF /* WebViewProvider.swift */,
|
||||
51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */,
|
||||
51C4527E2265092C00C03939 /* ArticleViewController.swift */,
|
||||
51C266E9238C334800F53014 /* ContextMenuPreviewViewController.swift */,
|
||||
5142192923522B5500E07E2C /* ImageViewController.swift */,
|
||||
514219362352510100E07E2C /* ImageScrollView.swift */,
|
||||
518651D9235621840078E021 /* ImageTransition.swift */,
|
||||
5142192923522B5500E07E2C /* ImageViewController.swift */,
|
||||
512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */,
|
||||
51AB8AB223B7F4C6008F147D /* WebViewController.swift */,
|
||||
517630222336657E00E15FFF /* WebViewProvider.swift */,
|
||||
);
|
||||
path = Article;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2588,6 +2596,7 @@
|
|||
51C45255226507D200C03939 /* AppDefaults.swift */,
|
||||
51E3EB3C229AB08300645299 /* ErrorHandler.swift */,
|
||||
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */,
|
||||
C5A6ED5123C9AF4300AB6BE2 /* TitleActivityItemSource.swift */,
|
||||
51B62E67233186730085F949 /* IconView.swift */,
|
||||
51C4525D226508F600C03939 /* MasterFeed */,
|
||||
51C4526D2265091600C03939 /* MasterTimeline */,
|
||||
|
@ -3901,6 +3910,7 @@
|
|||
51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */,
|
||||
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */,
|
||||
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */,
|
||||
C5A6ED6D23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift in Sources */,
|
||||
5108F6D42375EEEF001ABC45 /* TimelinePreviewTableViewController.swift in Sources */,
|
||||
84CAFCA522BC8C08007694F0 /* FetchRequestQueue.swift in Sources */,
|
||||
51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */,
|
||||
|
@ -3917,6 +3927,7 @@
|
|||
51C452AC22650FD200C03939 /* AppNotifications.swift in Sources */,
|
||||
51EF0F7E2277A57D0050506E /* MasterTimelineAccessibilityCellLayout.swift in Sources */,
|
||||
51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */,
|
||||
512D554423C804DE0023FFFA /* OpenInSafariActivity.swift in Sources */,
|
||||
51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */,
|
||||
5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */,
|
||||
51C452882265093600C03939 /* AddWebFeedViewController.swift in Sources */,
|
||||
|
@ -3928,6 +3939,7 @@
|
|||
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */,
|
||||
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
|
||||
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
|
||||
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
|
||||
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
|
||||
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
|
||||
512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */,
|
||||
|
@ -3937,7 +3949,7 @@
|
|||
51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */,
|
||||
5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */,
|
||||
519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */,
|
||||
FFD43E412340F488009E5CA3 /* UndoAvailableAlertController.swift in Sources */,
|
||||
FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */,
|
||||
51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */,
|
||||
51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */,
|
||||
51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */,
|
||||
|
|
|
@ -79,7 +79,7 @@ struct ArticleRenderer {
|
|||
private extension ArticleRenderer {
|
||||
|
||||
private var articleHTML: String {
|
||||
let body = RSMacroProcessor.renderedText(withTemplate: template(), substitutions: articleSubstitutions(), macroStart: "[[", macroEnd: "]]")
|
||||
let body = try! MacroProcessor.renderedText(withTemplate: template(), substitutions: articleSubstitutions())
|
||||
return renderHTML(withBody: body)
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ private let styleSuffixes = [styleSuffix, nnwStyleSuffix, cssStyleSuffix];
|
|||
final class ArticleStylesManager {
|
||||
|
||||
static let shared = ArticleStylesManager()
|
||||
private let folderPath = RSDataSubfolder(nil, stylesFolderName)!
|
||||
private let folderPath = Platform.dataSubfolder(forApplication: nil, folderName: stylesFolderName)!
|
||||
|
||||
var currentStyleName: String {
|
||||
get {
|
||||
|
|
|
@ -52,15 +52,22 @@ extension CGImage {
|
|||
let r = ptr[i]
|
||||
let g = ptr[i + 1]
|
||||
let b = ptr[i + 2]
|
||||
let a = ptr[i + 3]
|
||||
let luminance = (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b))
|
||||
|
||||
totalLuminance += luminance
|
||||
pixelCount += 1
|
||||
if Double(a) > 0 {
|
||||
totalLuminance += luminance
|
||||
pixelCount += 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let avgLuminance = totalLuminance / Double(pixelCount)
|
||||
return avgLuminance < 37.5
|
||||
if totalLuminance == 0 {
|
||||
return true
|
||||
} else {
|
||||
return avgLuminance < 40
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ private extension SingleFaviconDownloader {
|
|||
queue.async {
|
||||
|
||||
if let data = self.diskCache[self.diskKey], !data.isEmpty {
|
||||
RSImage.rs_image(with: data, imageResultBlock: completion)
|
||||
RSImage.image(with: data, imageResultBlock: completion)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -138,7 +138,7 @@ private extension SingleFaviconDownloader {
|
|||
|
||||
if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil {
|
||||
self.saveToDisk(data)
|
||||
RSImage.rs_image(with: data, imageResultBlock: completion)
|
||||
RSImage.image(with: data, imageResultBlock: completion)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -109,5 +109,30 @@ extension Array where Element == Article {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func articlesAbove(article: Article) -> [Article] {
|
||||
guard let position = firstIndex(of: article) else {
|
||||
return []
|
||||
}
|
||||
|
||||
let articlesAbove = self[..<position]
|
||||
return Array(articlesAbove)
|
||||
}
|
||||
|
||||
func articlesBelow(article: Article) -> [Article] {
|
||||
guard let position = firstIndex(of: article) else {
|
||||
return []
|
||||
}
|
||||
|
||||
var articlesBelow = Array(self[position...])
|
||||
|
||||
guard !articlesBelow.isEmpty else {
|
||||
return []
|
||||
}
|
||||
|
||||
articlesBelow.removeFirst()
|
||||
|
||||
return articlesBelow
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,10 +32,10 @@ final class UserNotificationManager: NSObject {
|
|||
}
|
||||
|
||||
@objc func statusesDidChange(_ note: Notification) {
|
||||
guard let articles = note.userInfo?[Account.UserInfoKey.articles] as? Set<Article> else {
|
||||
guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String>, !articleIDs.isEmpty else {
|
||||
return
|
||||
}
|
||||
let identifiers = articles.filter({ $0.status.read }).map { "articleID:\($0.articleID)" }
|
||||
let identifiers = articleIDs.map { "articleID:\($0)" }
|
||||
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
|
||||
}
|
||||
|
||||
|
|
|
@ -113,8 +113,8 @@ struct AppAssets {
|
|||
UIImage(systemName: "info.circle")!
|
||||
}()
|
||||
|
||||
static var markAllInFeedAsReadImage: UIImage = {
|
||||
return UIImage(systemName: "asterisk.circle")!
|
||||
static var markAllAsReadImage: UIImage = {
|
||||
return UIImage(named: "markAllAsRead")!
|
||||
}()
|
||||
|
||||
static var markBelowAsReadImage: UIImage = {
|
||||
|
|
|
@ -20,11 +20,12 @@ struct AppDefaults {
|
|||
static let lastImageCacheFlushDate = "lastImageCacheFlushDate"
|
||||
static let firstRunDate = "firstRunDate"
|
||||
static let timelineGroupByFeed = "timelineGroupByFeed"
|
||||
static let refreshClearsReadArticles = "refreshClearsReadArticles"
|
||||
static let timelineNumberOfLines = "timelineNumberOfLines"
|
||||
static let timelineIconSize = "timelineIconSize"
|
||||
static let timelineSortDirection = "timelineSortDirection"
|
||||
static let articleFullscreenEnabled = "articleFullscreenEnabled"
|
||||
static let displayUndoAvailableTip = "displayUndoAvailableTip"
|
||||
static let confirmMarkAllAsRead = "confirmMarkAllAsRead"
|
||||
static let lastRefresh = "lastRefresh"
|
||||
static let addWebFeedAccountID = "addWebFeedAccountID"
|
||||
static let addWebFeedFolderName = "addWebFeedFolderName"
|
||||
|
@ -84,6 +85,15 @@ struct AppDefaults {
|
|||
}
|
||||
}
|
||||
|
||||
static var refreshClearsReadArticles: Bool {
|
||||
get {
|
||||
return bool(for: Key.refreshClearsReadArticles)
|
||||
}
|
||||
set {
|
||||
setBool(for: Key.refreshClearsReadArticles, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
static var timelineSortDirection: ComparisonResult {
|
||||
get {
|
||||
return sortDirection(for: Key.timelineSortDirection)
|
||||
|
@ -102,12 +112,12 @@ struct AppDefaults {
|
|||
}
|
||||
}
|
||||
|
||||
static var displayUndoAvailableTip: Bool {
|
||||
static var confirmMarkAllAsRead: Bool {
|
||||
get {
|
||||
return bool(for: Key.displayUndoAvailableTip)
|
||||
return bool(for: Key.confirmMarkAllAsRead)
|
||||
}
|
||||
set {
|
||||
setBool(for: Key.displayUndoAvailableTip, newValue)
|
||||
setBool(for: Key.confirmMarkAllAsRead, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,11 +152,12 @@ struct AppDefaults {
|
|||
static func registerDefaults() {
|
||||
let defaults: [String : Any] = [Key.lastImageCacheFlushDate: Date(),
|
||||
Key.timelineGroupByFeed: false,
|
||||
Key.refreshClearsReadArticles: false,
|
||||
Key.timelineNumberOfLines: 2,
|
||||
Key.timelineIconSize: IconSize.medium.rawValue,
|
||||
Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue,
|
||||
Key.articleFullscreenEnabled: false,
|
||||
Key.displayUndoAvailableTip: true]
|
||||
Key.confirmMarkAllAsRead: true]
|
||||
AppDefaults.shared.register(defaults: defaults)
|
||||
}
|
||||
|
||||
|
|
|
@ -126,6 +126,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
}
|
||||
|
||||
// MARK: - API
|
||||
func resumeDatabaseProcessingIfNecessary() {
|
||||
if AccountManager.shared.isSuspended {
|
||||
AccountManager.shared.resumeAll()
|
||||
os_log("Application processing resumed.", log: self.log, type: .info)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAccountsForBackground() {
|
||||
syncTimer?.invalidate()
|
||||
|
@ -135,11 +141,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
}
|
||||
|
||||
func prepareAccountsForForeground() {
|
||||
if AccountManager.shared.isSuspended {
|
||||
AccountManager.shared.resumeAll()
|
||||
os_log("Application processing resumed.", log: self.log, type: .info)
|
||||
}
|
||||
|
||||
if let lastRefresh = AppDefaults.lastRefresh {
|
||||
if Date() > lastRefresh.addingTimeInterval(15 * 60) {
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
||||
|
|
|
@ -38,6 +38,24 @@ class ArticleExtractorButton: UIButton {
|
|||
}
|
||||
}
|
||||
|
||||
override var accessibilityLabel: String? {
|
||||
get {
|
||||
switch buttonState {
|
||||
case .error:
|
||||
return NSLocalizedString("Error - Reader View", comment: "Error - Reader View")
|
||||
case .animated:
|
||||
return NSLocalizedString("Processing - Reader View", comment: "Processing - Reader View")
|
||||
case .on:
|
||||
return NSLocalizedString("Selected - Reader View", comment: "Selected - Reader View")
|
||||
case .off:
|
||||
return NSLocalizedString("Reader View", comment: "Reader View")
|
||||
}
|
||||
}
|
||||
set {
|
||||
super.accessibilityLabel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard case .animated = buttonState else {
|
||||
|
|
|
@ -135,11 +135,21 @@ class ArticleViewController: UIViewController {
|
|||
starBarButtonItem.isEnabled = true
|
||||
actionBarButtonItem.isEnabled = true
|
||||
|
||||
let readImage = article.status.read ? AppAssets.circleOpenImage : AppAssets.circleClosedImage
|
||||
readBarButtonItem.image = readImage
|
||||
if article.status.read {
|
||||
readBarButtonItem.image = AppAssets.circleOpenImage
|
||||
readBarButtonItem.accLabelText = NSLocalizedString("Mark Article Unread", comment: "Mark Article Unread")
|
||||
} else {
|
||||
readBarButtonItem.image = AppAssets.circleClosedImage
|
||||
readBarButtonItem.accLabelText = NSLocalizedString("Selected - Mark Article Unread", comment: "Selected - Mark Article Unread")
|
||||
}
|
||||
|
||||
let starImage = article.status.starred ? AppAssets.starClosedImage : AppAssets.starOpenImage
|
||||
starBarButtonItem.image = starImage
|
||||
if article.status.starred {
|
||||
starBarButtonItem.image = AppAssets.starClosedImage
|
||||
starBarButtonItem.accLabelText = NSLocalizedString("Selected - Star Article", comment: "Selected - Star Article")
|
||||
} else {
|
||||
starBarButtonItem.image = AppAssets.starOpenImage
|
||||
starBarButtonItem.accLabelText = NSLocalizedString("Star Article", comment: "Star Article")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -42,9 +42,24 @@ class ContextMenuPreviewViewController: UIViewController {
|
|||
dateFormatter.timeStyle = .medium
|
||||
dateTimeLabel.text = dateFormatter.string(from: article.logicalDatePublished)
|
||||
|
||||
// When in landscape the context menu preview will force this controller into a tiny
|
||||
// view space. If it is documented anywhere what that is, I haven't found it. This
|
||||
// set of magic numbers is what I worked out by testing a variety of phones.
|
||||
|
||||
let width: CGFloat
|
||||
let heightPadding: CGFloat
|
||||
if view.bounds.width > view.bounds.height {
|
||||
width = 260
|
||||
heightPadding = 16
|
||||
view.widthAnchor.constraint(equalToConstant: width).isActive = true
|
||||
} else {
|
||||
width = view.bounds.width
|
||||
heightPadding = 8
|
||||
}
|
||||
|
||||
view.setNeedsLayout()
|
||||
view.layoutIfNeeded()
|
||||
preferredContentSize = CGSize(width: view.bounds.width, height: dateTimeLabel.frame.maxY + 8)
|
||||
preferredContentSize = CGSize(width: width, height: dateTimeLabel.frame.maxY + heightPadding)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// OpenInSafariActivity.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 1/9/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class OpenInSafariActivity: UIActivity {
|
||||
|
||||
private var activityItems: [Any]?
|
||||
|
||||
override var activityTitle: String? {
|
||||
return NSLocalizedString("Open in Safari", comment: "Open in Safari")
|
||||
}
|
||||
|
||||
override var activityImage: UIImage? {
|
||||
return UIImage(systemName: "safari", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))
|
||||
}
|
||||
|
||||
override var activityType: UIActivity.ActivityType? {
|
||||
return UIActivity.ActivityType(rawValue: "com.rancharo.NetNewsWire-Evergreen.safari")
|
||||
}
|
||||
|
||||
override class var activityCategory: UIActivity.Category {
|
||||
return .action
|
||||
}
|
||||
|
||||
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func prepare(withActivityItems activityItems: [Any]) {
|
||||
self.activityItems = activityItems
|
||||
}
|
||||
|
||||
override func perform() {
|
||||
guard let url = activityItems?.first as? URL else { return }
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
activityDidFinish(true)
|
||||
}
|
||||
|
||||
}
|
|
@ -48,12 +48,12 @@ class WebViewController: UIViewController {
|
|||
|
||||
var articleExtractorButtonState: ArticleExtractorButtonState = .off {
|
||||
didSet {
|
||||
delegate.webViewController(self, articleExtractorButtonStateDidUpdate: articleExtractorButtonState)
|
||||
delegate?.webViewController(self, articleExtractorButtonStateDidUpdate: articleExtractorButtonState)
|
||||
}
|
||||
}
|
||||
|
||||
weak var coordinator: SceneCoordinator!
|
||||
weak var delegate: WebViewControllerDelegate!
|
||||
weak var delegate: WebViewControllerDelegate?
|
||||
|
||||
var article: Article? {
|
||||
didSet {
|
||||
|
@ -227,23 +227,21 @@ class WebViewController: UIViewController {
|
|||
}
|
||||
|
||||
func showBars() {
|
||||
if isFullScreenAvailable {
|
||||
AppDefaults.articleFullscreenEnabled = false
|
||||
coordinator.showStatusBar()
|
||||
topShowBarsViewConstraint.constant = 0
|
||||
bottomShowBarsViewConstraint.constant = 0
|
||||
navigationController?.setNavigationBarHidden(false, animated: true)
|
||||
navigationController?.setToolbarHidden(false, animated: true)
|
||||
configureContextMenuInteraction()
|
||||
}
|
||||
AppDefaults.articleFullscreenEnabled = false
|
||||
coordinator.showStatusBar()
|
||||
topShowBarsViewConstraint?.constant = 0
|
||||
bottomShowBarsViewConstraint?.constant = 0
|
||||
navigationController?.setNavigationBarHidden(false, animated: true)
|
||||
navigationController?.setToolbarHidden(false, animated: true)
|
||||
configureContextMenuInteraction()
|
||||
}
|
||||
|
||||
func hideBars() {
|
||||
if isFullScreenAvailable {
|
||||
AppDefaults.articleFullscreenEnabled = true
|
||||
coordinator.hideStatusBar()
|
||||
topShowBarsViewConstraint.constant = -44.0
|
||||
bottomShowBarsViewConstraint.constant = 44.0
|
||||
topShowBarsViewConstraint?.constant = -44.0
|
||||
bottomShowBarsViewConstraint?.constant = 44.0
|
||||
navigationController?.setNavigationBarHidden(true, animated: true)
|
||||
navigationController?.setToolbarHidden(true, animated: true)
|
||||
configureContextMenuInteraction()
|
||||
|
@ -282,9 +280,8 @@ class WebViewController: UIViewController {
|
|||
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else {
|
||||
return
|
||||
}
|
||||
|
||||
let itemSource = ArticleActivityItemSource(url: url, subject: article!.title)
|
||||
let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil)
|
||||
|
||||
let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [OpenInSafariActivity()])
|
||||
activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem
|
||||
present(activityViewController, animated: true)
|
||||
}
|
||||
|
|
|
@ -65,11 +65,11 @@
|
|||
</connections>
|
||||
</tableView>
|
||||
<toolbarItems>
|
||||
<barButtonItem title="Item" image="gear" catalog="system" id="AK3-N5-4ke"/>
|
||||
<barButtonItem style="plain" systemItem="flexibleSpace" id="Kjl-Sb-QP1"/>
|
||||
<barButtonItem systemItem="add" id="PVr-3K-nPg"/>
|
||||
</toolbarItems>
|
||||
<navigationItem key="navigationItem" title="Feeds" largeTitleDisplayMode="never" id="lE1-xw-gjH">
|
||||
<barButtonItem key="leftBarButtonItem" title="Item" image="gear" catalog="system" id="AK3-N5-4ke"/>
|
||||
<barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="Khk-Hd-iNS"/>
|
||||
</navigationItem>
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||
|
|
|
@ -65,11 +65,11 @@
|
|||
</connections>
|
||||
</tableView>
|
||||
<toolbarItems>
|
||||
<barButtonItem title="Item" image="gear" catalog="system" id="AK3-N5-4ke"/>
|
||||
<barButtonItem style="plain" systemItem="flexibleSpace" id="Kjl-Sb-QP1"/>
|
||||
<barButtonItem systemItem="add" id="PVr-3K-nPg"/>
|
||||
</toolbarItems>
|
||||
<navigationItem key="navigationItem" title="Feeds" largeTitleDisplayMode="always" id="lE1-xw-gjH">
|
||||
<barButtonItem key="leftBarButtonItem" title="Item" image="gear" catalog="system" id="AK3-N5-4ke"/>
|
||||
<barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="Khk-Hd-iNS"/>
|
||||
</navigationItem>
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<barButtonItem image="square.and.arrow.up" catalog="system" id="9Ut-5B-JKP">
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accEnabled" value="YES"/>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="accLabelText" value="Action"/>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="accLabelText" value="Share"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="showActivityDialog:" destination="JEX-9P-axG" id="t7U-uT-fs5"/>
|
||||
|
@ -119,7 +119,7 @@
|
|||
</connections>
|
||||
</tableView>
|
||||
<toolbarItems>
|
||||
<barButtonItem image="asterisk.circle" catalog="system" id="fTv-eX-72r">
|
||||
<barButtonItem title="Item" image="markAllAsRead" id="fTv-eX-72r">
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="accLabelText" value="Mark All as Read"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
|
@ -140,6 +140,9 @@
|
|||
</toolbarItems>
|
||||
<navigationItem key="navigationItem" title="Timeline" largeTitleDisplayMode="never" id="wcC-1L-ug4">
|
||||
<barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="af2-lj-EcA">
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="accLabelText" value="FIlter Articles"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="toggleFilter:" destination="Kyk-vK-QRX" id="jxP-b2-V1n"/>
|
||||
</connections>
|
||||
|
@ -182,9 +185,8 @@
|
|||
<outlet property="delegate" destination="7bK-jq-Zjz" id="RA6-mI-bju"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<toolbarItems/>
|
||||
<navigationItem key="navigationItem" title="Feeds" id="Zdf-7t-Un8">
|
||||
<barButtonItem key="leftBarButtonItem" title="Settings" image="gear" catalog="system" id="TlU-Pg-ATe">
|
||||
<toolbarItems>
|
||||
<barButtonItem title="Settings" image="gear" catalog="system" id="TlU-Pg-ATe">
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="accLabelText" value="Settings"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
|
@ -192,16 +194,32 @@
|
|||
<action selector="settings:" destination="7bK-jq-Zjz" id="Y8a-lz-Im7"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="ZJu-oJ-c1R">
|
||||
<barButtonItem style="plain" systemItem="flexibleSpace" id="Rbh-Vg-Wo8"/>
|
||||
<barButtonItem style="plain" systemItem="flexibleSpace" id="Vhj-bc-20A"/>
|
||||
<barButtonItem systemItem="add" id="YFE-wd-vFC">
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="accLabelText" value="Add Item"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="toggleFilter:" destination="7bK-jq-Zjz" id="7lh-Bz-nfD"/>
|
||||
<action selector="add:" destination="7bK-jq-Zjz" id="d1n-0d-2gR"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</toolbarItems>
|
||||
<navigationItem key="navigationItem" title="Feeds" id="Zdf-7t-Un8">
|
||||
<barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="9ro-XY-5xU">
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="accLabelText" value="Feeds Filter"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="toggleFilter:" destination="7bK-jq-Zjz" id="jmL-ei-avl"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="filterButton" destination="ZJu-oJ-c1R" id="jiO-wg-qrG"/>
|
||||
<outlet property="addNewItemButton" destination="YFE-wd-vFC" id="NMJ-uE-zGh"/>
|
||||
<outlet property="filterButton" destination="9ro-XY-5xU" id="PSL-lE-ITK"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Rux-fX-hf1" sceneMemberID="firstResponder"/>
|
||||
|
@ -221,6 +239,14 @@
|
|||
<viewLayoutGuide key="contentLayoutGuide" id="phv-DN-krZ"/>
|
||||
<viewLayoutGuide key="frameLayoutGuide" id="NNU-C8-Fsz"/>
|
||||
</scrollView>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bHh-pW-oTS">
|
||||
<rect key="frame" x="0.0" y="862" width="414" height="0.0"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="StS-kO-TuW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="0.0"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
</view>
|
||||
<blurEffect style="systemUltraThinMaterial"/>
|
||||
</visualEffectView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eMj-1g-3xm">
|
||||
<rect key="frame" x="0.0" y="862" width="414" height="0.0"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
|
@ -257,12 +283,16 @@
|
|||
<constraint firstItem="RmY-a3-hUg" firstAttribute="top" secondItem="mbY-02-GFL" secondAttribute="top" id="A0i-Hs-1Ac"/>
|
||||
<constraint firstAttribute="bottom" secondItem="msG-pz-EKk" secondAttribute="bottom" id="AtA-bA-jDr"/>
|
||||
<constraint firstItem="eMj-1g-3xm" firstAttribute="trailing" secondItem="mbY-02-GFL" secondAttribute="trailing" id="E7e-Lv-6ZA"/>
|
||||
<constraint firstItem="bHh-pW-oTS" firstAttribute="bottom" secondItem="eMj-1g-3xm" secondAttribute="bottom" id="P3m-i2-3pJ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="msG-pz-EKk" secondAttribute="trailing" id="R49-qV-8nm"/>
|
||||
<constraint firstItem="msG-pz-EKk" firstAttribute="leading" secondItem="w6Q-vH-063" secondAttribute="leading" id="XN1-xN-hYS"/>
|
||||
<constraint firstItem="eMj-1g-3xm" firstAttribute="leading" secondItem="mbY-02-GFL" secondAttribute="leading" id="Xni-Dn-I3Z"/>
|
||||
<constraint firstItem="mbY-02-GFL" firstAttribute="trailing" secondItem="RmY-a3-hUg" secondAttribute="trailing" constant="8" id="Zlz-lM-LV8"/>
|
||||
<constraint firstItem="mbY-02-GFL" firstAttribute="bottom" secondItem="eMj-1g-3xm" secondAttribute="bottom" id="eaS-iG-yMv"/>
|
||||
<constraint firstItem="bHh-pW-oTS" firstAttribute="leading" secondItem="eMj-1g-3xm" secondAttribute="leading" id="f8r-dq-Irr"/>
|
||||
<constraint firstItem="bHh-pW-oTS" firstAttribute="top" secondItem="eMj-1g-3xm" secondAttribute="top" id="gTP-i5-FYQ"/>
|
||||
<constraint firstItem="msG-pz-EKk" firstAttribute="top" secondItem="w6Q-vH-063" secondAttribute="top" id="p1a-s0-wdK"/>
|
||||
<constraint firstItem="bHh-pW-oTS" firstAttribute="trailing" secondItem="eMj-1g-3xm" secondAttribute="trailing" id="qB9-zk-5JN"/>
|
||||
<constraint firstItem="cXR-ll-xBx" firstAttribute="leading" secondItem="mbY-02-GFL" secondAttribute="leading" constant="8" id="vJs-LN-Ydd"/>
|
||||
<constraint firstItem="cXR-ll-xBx" firstAttribute="top" secondItem="mbY-02-GFL" secondAttribute="top" id="xVN-Qt-WYA"/>
|
||||
</constraints>
|
||||
|
@ -367,13 +397,13 @@
|
|||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="asterisk.circle" catalog="system" width="64" height="60"/>
|
||||
<image name="chevron.down" catalog="system" width="64" height="36"/>
|
||||
<image name="chevron.down.circle" catalog="system" width="64" height="60"/>
|
||||
<image name="chevron.up" catalog="system" width="64" height="36"/>
|
||||
<image name="circle" catalog="system" width="64" height="60"/>
|
||||
<image name="gear" catalog="system" width="64" height="58"/>
|
||||
<image name="line.horizontal.3.decrease.circle" catalog="system" width="64" height="60"/>
|
||||
<image name="markAllAsRead" width="17" height="26"/>
|
||||
<image name="multiply.circle.fill" catalog="system" width="64" height="60"/>
|
||||
<image name="square.and.arrow.up" catalog="system" width="56" height="64"/>
|
||||
<image name="square.and.arrow.up.fill" catalog="system" width="56" height="64"/>
|
||||
|
|
|
@ -25,7 +25,7 @@ class WebFeedInspectorViewController: UITableViewController {
|
|||
if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: webFeed) {
|
||||
return feedIcon
|
||||
}
|
||||
if let favicon = appDelegate.faviconDownloader.favicon(for: webFeed) {
|
||||
if let favicon = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
|
||||
return favicon
|
||||
}
|
||||
return FaviconGenerator.favicon(webFeed)
|
||||
|
|
|
@ -168,8 +168,11 @@ private extension KeyboardManager {
|
|||
let toggleReadTitle = NSLocalizedString("Toggle Read Status", comment: "Toggle Read Status")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: toggleReadTitle, action: "toggleRead:", input: "u", modifiers: [.command, .shift]))
|
||||
|
||||
let markOlderAsReadTitle = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: markOlderAsReadTitle, action: "markBelowAsRead:", input: "k", modifiers: [.command, .shift]))
|
||||
let markAboveAsReadTitle = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: markAboveAsReadTitle, action: "markAboveAsRead:", input: "k", modifiers: [.command, .control]))
|
||||
|
||||
let markBelowAsReadTitle = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: markBelowAsReadTitle, action: "markBelowAsRead:", input: "k", modifiers: [.command, .shift]))
|
||||
|
||||
let toggleStarredTitle = NSLocalizedString("Toggle Starred Status", comment: "Toggle Starred Status")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: toggleStarredTitle, action: "toggleStarred:", input: "l", modifiers: [.command, .shift]))
|
||||
|
|
|
@ -16,7 +16,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||
|
||||
@IBOutlet weak var filterButton: UIBarButtonItem!
|
||||
private var refreshProgressView: RefreshProgressView?
|
||||
private var addNewItemButton: UIBarButtonItem!
|
||||
@IBOutlet weak var addNewItemButton: UIBarButtonItem!
|
||||
|
||||
lazy var dataSource = makeDataSource()
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
|
@ -281,6 +281,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||
alert.addAction(action)
|
||||
}
|
||||
|
||||
if let action = self.markAllAsReadAlertAction(indexPath: indexPath, completion: completion) {
|
||||
alert.addAction(action)
|
||||
}
|
||||
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel) { _ in
|
||||
completion(true)
|
||||
|
@ -402,10 +406,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||
|
||||
@IBAction func toggleFilter(_ sender: Any) {
|
||||
if coordinator.isReadFeedsFiltered {
|
||||
filterButton.image = AppAssets.filterInactiveImage
|
||||
setFilterButtonToInactive()
|
||||
coordinator.showAllFeeds()
|
||||
} else {
|
||||
filterButton.image = AppAssets.filterActiveImage
|
||||
setFilterButtonToActive()
|
||||
coordinator.hideReadFeeds()
|
||||
}
|
||||
}
|
||||
|
@ -437,10 +441,15 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||
|
||||
@objc func refreshAccounts(_ sender: Any) {
|
||||
refreshControl?.endRefreshing()
|
||||
|
||||
// This is a hack to make sure that an error dialog doesn't interfere with dismissing the refreshControl.
|
||||
// If the error dialog appears too closely to the call to endRefreshing, then the refreshControl never disappears.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self))
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self)) {
|
||||
if AppDefaults.refreshClearsReadArticles {
|
||||
self.coordinator.refreshTimeline(resetScroll: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -591,7 +600,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||
!coordinator.isExpanded(accountNode) {
|
||||
|
||||
coordinator.expand(accountNode)
|
||||
applyChanges(animated: false) {
|
||||
applyChanges(animated: true) {
|
||||
discloseFeedInAccount()
|
||||
}
|
||||
|
||||
|
@ -668,29 +677,30 @@ private extension MasterFeedViewController {
|
|||
}
|
||||
|
||||
self.refreshProgressView = refreshProgressView
|
||||
|
||||
let spaceItemButton1 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
let refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
|
||||
let spaceItemButton2 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
addNewItemButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(add(_:)))
|
||||
|
||||
setToolbarItems([spaceItemButton1,
|
||||
refreshProgressItemButton,
|
||||
spaceItemButton2,
|
||||
addNewItemButton
|
||||
], animated: false)
|
||||
toolbarItems?.insert(refreshProgressItemButton, at: 2)
|
||||
}
|
||||
|
||||
func updateUI() {
|
||||
if coordinator.isReadFeedsFiltered {
|
||||
filterButton.image = AppAssets.filterActiveImage
|
||||
setFilterButtonToActive()
|
||||
} else {
|
||||
filterButton.image = AppAssets.filterInactiveImage
|
||||
setFilterButtonToInactive()
|
||||
}
|
||||
refreshProgressView?.updateRefreshLabel()
|
||||
addNewItemButton?.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
|
||||
}
|
||||
|
||||
func setFilterButtonToActive() {
|
||||
filterButton?.image = AppAssets.filterActiveImage
|
||||
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Feeds", comment: "Selected - Filter Read Feeds")
|
||||
}
|
||||
|
||||
func setFilterButtonToInactive() {
|
||||
filterButton?.image = AppAssets.filterInactiveImage
|
||||
filterButton?.accLabelText = NSLocalizedString("Filter Read Feeds", comment: "Filter Read Feeds")
|
||||
}
|
||||
|
||||
func reloadNode(_ node: Node) {
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reloadItems([node])
|
||||
|
@ -773,7 +783,7 @@ private extension MasterFeedViewController {
|
|||
return feedIconImage
|
||||
}
|
||||
|
||||
if let faviconImage = appDelegate.faviconDownloader.favicon(for: webFeed) {
|
||||
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
|
||||
return faviconImage
|
||||
}
|
||||
|
||||
|
@ -1011,6 +1021,29 @@ private extension MasterFeedViewController {
|
|||
return action
|
||||
}
|
||||
|
||||
func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let node = dataSource.itemIdentifier(for: indexPath),
|
||||
coordinator.unreadCountFor(node) > 0,
|
||||
let feed = node.representedObject as? WebFeed,
|
||||
let articles = try? feed.fetchArticles() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
let cancel = {
|
||||
completion(true)
|
||||
}
|
||||
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func deleteAction(indexPath: IndexPath) -> UIAction {
|
||||
let title = NSLocalizedString("Delete", comment: "Delete")
|
||||
|
||||
|
@ -1101,8 +1134,10 @@ private extension MasterFeedViewController {
|
|||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, nameForDisplay) as String
|
||||
|
||||
let action = UIAction(title: title, image: AppAssets.markAllInFeedAsReadImage) { [weak self] action in
|
||||
self?.coordinator.markAllAsRead(articles)
|
||||
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in
|
||||
self?.coordinator.markAllAsRead(articles)
|
||||
}
|
||||
}
|
||||
|
||||
return action
|
||||
|
@ -1153,6 +1188,7 @@ private extension MasterFeedViewController {
|
|||
alertController.addAction(renameAction)
|
||||
|
||||
alertController.addTextField() { textField in
|
||||
textField.text = name
|
||||
textField.placeholder = NSLocalizedString("Name", comment: "Name")
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ class RefreshProgressView: UIView {
|
|||
@IBOutlet weak var progressView: UIProgressView!
|
||||
@IBOutlet weak var label: UILabel!
|
||||
private lazy var progressWidth = progressView.widthAnchor.constraint(equalToConstant: 100.0)
|
||||
private var lastLabelDisplayedTime: Date? = nil
|
||||
|
||||
override func awakeFromNib() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
||||
|
@ -24,18 +23,14 @@ class RefreshProgressView: UIView {
|
|||
} else {
|
||||
updateRefreshLabel()
|
||||
}
|
||||
|
||||
scheduleUpdateRefreshLabel()
|
||||
}
|
||||
|
||||
func updateRefreshLabel() {
|
||||
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
|
||||
|
||||
if let lastLabelDisplayedTime = lastLabelDisplayedTime, lastLabelDisplayedTime.addingTimeInterval(2) > Date() {
|
||||
return
|
||||
}
|
||||
|
||||
lastLabelDisplayedTime = Date()
|
||||
|
||||
if Date() > accountLastArticleFetchEndTime.addingTimeInterval(1) {
|
||||
if Date() > accountLastArticleFetchEndTime.addingTimeInterval(60) {
|
||||
|
||||
let relativeDateTimeFormatter = RelativeDateTimeFormatter()
|
||||
relativeDateTimeFormatter.dateTimeStyle = .named
|
||||
|
@ -45,7 +40,7 @@ class RefreshProgressView: UIView {
|
|||
label.text = refreshText
|
||||
|
||||
} else {
|
||||
label.text = NSLocalizedString("Updated just now", comment: "Updated Just Now")
|
||||
label.text = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -72,20 +67,34 @@ private extension RefreshProgressView {
|
|||
let progress = AccountManager.shared.combinedRefreshProgress
|
||||
|
||||
if progress.isComplete {
|
||||
progressView.progress = 1
|
||||
progressView.setProgress(1, animated: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.updateRefreshLabel()
|
||||
self.label.isHidden = false
|
||||
self.progressView.isHidden = true
|
||||
self.progressWidth.isActive = false
|
||||
self.progressView.setProgress(0, animated: true)
|
||||
}
|
||||
} else {
|
||||
lastLabelDisplayedTime = nil
|
||||
label.isHidden = true
|
||||
progressView.isHidden = false
|
||||
self.progressWidth.isActive = true
|
||||
self.progressView.setNeedsLayout()
|
||||
self.progressView.layoutIfNeeded()
|
||||
let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks)
|
||||
progressView.progress = percent
|
||||
|
||||
// Don't let the progress bar go backwards unless we need to go back more than 25%
|
||||
if percent > progressView.progress || progressView.progress - percent > 0.25 {
|
||||
progressView.setProgress(percent, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scheduleUpdateRefreshLabel() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { [weak self] in
|
||||
self?.updateRefreshLabel()
|
||||
self?.scheduleUpdateRefreshLabel()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -200,6 +200,10 @@ private extension MasterTimelineTableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
func updateAccessiblityLabel() {
|
||||
accessibilityLabel = "\(cellData.feedName), \(cellData.title), \(cellData.summary), \(cellData.dateString)"
|
||||
}
|
||||
|
||||
func makeIconEmpty() {
|
||||
if iconView.iconImage != nil {
|
||||
iconView.iconImage = nil
|
||||
|
@ -232,6 +236,7 @@ private extension MasterTimelineTableViewCell {
|
|||
updateUnreadIndicator()
|
||||
updateStarView()
|
||||
updateIconImage()
|
||||
updateAccessiblityLabel()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// UndoAvailableAlertController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Phil Viso on 9/29/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct MarkAsReadAlertController {
|
||||
|
||||
static func confirm(_ controller: UIViewController?,
|
||||
coordinator: SceneCoordinator?,
|
||||
confirmTitle: String,
|
||||
cancelCompletion: (() -> Void)? = nil,
|
||||
completion: @escaping () -> Void) {
|
||||
|
||||
guard let controller = controller, let coordinator = coordinator else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
if AppDefaults.confirmMarkAllAsRead {
|
||||
let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion) { _ in
|
||||
completion()
|
||||
}
|
||||
controller.present(alertController, animated: true)
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
private static func alert(coordinator: SceneCoordinator,
|
||||
confirmTitle: String,
|
||||
cancelCompletion: (() -> Void)?,
|
||||
completion: @escaping (UIAlertAction) -> Void) -> UIAlertController {
|
||||
|
||||
let title = NSLocalizedString("Mark As Read", comment: "Mark As Read")
|
||||
let message = NSLocalizedString("You can turn this confirmation off in settings.",
|
||||
comment: "You can turn this confirmation off in settings.")
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings")
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in
|
||||
cancelCompletion?()
|
||||
}
|
||||
let settingsAction = UIAlertAction(title: settingsTitle, style: .default) { _ in
|
||||
coordinator.showSettings(scrollToArticlesSection: true)
|
||||
}
|
||||
let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: completion)
|
||||
|
||||
alertController.addAction(markAction)
|
||||
alertController.addAction(settingsAction)
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
return alertController
|
||||
}
|
||||
|
||||
}
|
|
@ -51,6 +51,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
|
||||
// Setup the Search Controller
|
||||
searchController.delegate = self
|
||||
|
@ -111,24 +112,18 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
// MARK: Actions
|
||||
@IBAction func toggleFilter(_ sender: Any) {
|
||||
if coordinator.isReadArticlesFiltered {
|
||||
filterButton.image = AppAssets.filterInactiveImage
|
||||
setFilterButtonToInactive()
|
||||
coordinator.showAllArticles()
|
||||
} else {
|
||||
filterButton.image = AppAssets.filterActiveImage
|
||||
setFilterButtonToActive()
|
||||
coordinator.hideReadArticles()
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func markAllAsRead(_ sender: Any) {
|
||||
if coordinator.displayUndoAvailableTip {
|
||||
let alertController = UndoAvailableAlertController.alert { [weak self] _ in
|
||||
self?.coordinator.displayUndoAvailableTip = false
|
||||
self?.coordinator.markAllAsReadInTimeline()
|
||||
}
|
||||
|
||||
present(alertController, animated: true)
|
||||
} else {
|
||||
coordinator.markAllAsReadInTimeline()
|
||||
let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
|
||||
MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title) { [weak self] in
|
||||
self?.coordinator.markAllAsReadInTimeline()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,10 +133,15 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
|
||||
@objc func refreshAccounts(_ sender: Any) {
|
||||
refreshControl?.endRefreshing()
|
||||
|
||||
// This is a hack to make sure that an error dialog doesn't interfere with dismissing the refreshControl.
|
||||
// If the error dialog appears too closely to the call to endRefreshing, then the refreshControl never disappears.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self))
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self)) {
|
||||
if AppDefaults.refreshClearsReadArticles {
|
||||
self.coordinator.refreshTimeline(resetScroll: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -448,6 +448,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
}
|
||||
|
||||
@objc func willEnterForeground(_ note: Notification) {
|
||||
updateUI()
|
||||
}
|
||||
|
||||
@objc func scrollPositionDidChange() {
|
||||
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
|
||||
}
|
||||
|
@ -559,8 +563,10 @@ private extension MasterTimelineViewController {
|
|||
if coordinator.timelineFeed is WebFeed {
|
||||
titleView.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
|
||||
titleView.addGestureRecognizer(feedTapGestureRecognizer)
|
||||
titleView.accessibilityTraits = .button
|
||||
} else {
|
||||
titleView.removeGestureRecognizer(feedTapGestureRecognizer)
|
||||
titleView.accessibilityTraits.remove(.button)
|
||||
}
|
||||
|
||||
navigationItem.titleView = titleView
|
||||
|
@ -574,9 +580,9 @@ private extension MasterTimelineViewController {
|
|||
}
|
||||
|
||||
if coordinator.isReadArticlesFiltered {
|
||||
filterButton.image = AppAssets.filterActiveImage
|
||||
setFilterButtonToActive()
|
||||
} else {
|
||||
filterButton.image = AppAssets.filterInactiveImage
|
||||
setFilterButtonToInactive()
|
||||
}
|
||||
|
||||
tableView.selectRow(at: nil, animated: false, scrollPosition: .top)
|
||||
|
@ -594,6 +600,16 @@ private extension MasterTimelineViewController {
|
|||
updateToolbar()
|
||||
}
|
||||
|
||||
func setFilterButtonToActive() {
|
||||
filterButton?.image = AppAssets.filterActiveImage
|
||||
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Articles", comment: "Selected - Filter Read Articles")
|
||||
}
|
||||
|
||||
func setFilterButtonToInactive() {
|
||||
filterButton?.image = AppAssets.filterInactiveImage
|
||||
filterButton?.accLabelText = NSLocalizedString("Filter Read Articles", comment: "Filter Read Articles")
|
||||
}
|
||||
|
||||
func updateToolbar() {
|
||||
markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
||||
firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
||||
|
@ -688,7 +704,9 @@ private extension MasterTimelineViewController {
|
|||
let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read")
|
||||
let image = AppAssets.markAboveAsReadImage
|
||||
let action = UIAction(title: title, image: image) { [weak self] action in
|
||||
self?.coordinator.markAboveAsRead(article)
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in
|
||||
self?.coordinator.markAboveAsRead(article)
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
@ -701,7 +719,9 @@ private extension MasterTimelineViewController {
|
|||
let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read")
|
||||
let image = AppAssets.markBelowAsReadImage
|
||||
let action = UIAction(title: title, image: image) { [weak self] action in
|
||||
self?.coordinator.markBelowAsRead(article)
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in
|
||||
self?.coordinator.markBelowAsRead(article)
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
@ -712,10 +732,16 @@ private extension MasterTimelineViewController {
|
|||
}
|
||||
|
||||
let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read")
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
self?.coordinator.markAboveAsRead(article)
|
||||
let cancel = {
|
||||
completion(true)
|
||||
}
|
||||
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in
|
||||
self?.coordinator.markAboveAsRead(article)
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
@ -725,10 +751,16 @@ private extension MasterTimelineViewController {
|
|||
}
|
||||
|
||||
let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read")
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
self?.coordinator.markBelowAsRead(article)
|
||||
let cancel = {
|
||||
completion(true)
|
||||
}
|
||||
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in
|
||||
self?.coordinator.markBelowAsRead(article)
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
@ -767,8 +799,10 @@ private extension MasterTimelineViewController {
|
|||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
|
||||
|
||||
let action = UIAction(title: title, image: AppAssets.markAllInFeedAsReadImage) { [weak self] action in
|
||||
self?.coordinator.markAllAsRead(articles)
|
||||
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in
|
||||
self?.coordinator.markAllAsRead(articles)
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
@ -786,10 +820,15 @@ private extension MasterTimelineViewController {
|
|||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Mark All as Read in Feed")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
|
||||
let cancel = {
|
||||
completion(true)
|
||||
}
|
||||
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
self?.coordinator.markAllAsRead(articles)
|
||||
completion(true)
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in
|
||||
self?.coordinator.markAllAsRead(articles)
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
@ -818,8 +857,7 @@ private extension MasterTimelineViewController {
|
|||
}
|
||||
|
||||
func shareDialogForTableCell(indexPath: IndexPath, url: URL, title: String?) {
|
||||
let itemSource = ArticleActivityItemSource(url: url, subject: title)
|
||||
let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil)
|
||||
let activityViewController = UIActivityViewController(url: url, title: title, applicationActivities: nil)
|
||||
|
||||
guard let cell = tableView.cellForRow(at: indexPath) else { return }
|
||||
let popoverController = activityViewController.popoverPresentationController
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
//
|
||||
// UndoAvailableAlertController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Phil Viso on 9/29/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct UndoAvailableAlertController {
|
||||
|
||||
static func alert(handler: @escaping (UIAlertAction) -> Void) -> UIAlertController {
|
||||
let title = NSLocalizedString("Undo Available", comment: "Undo Available")
|
||||
let message = NSLocalizedString("You can undo this and other actions with a three finger swipe to the left.",
|
||||
comment: "Mark all articles")
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
let confirmTitle = NSLocalizedString("Got It", comment: "Got It")
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
|
||||
let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: handler)
|
||||
|
||||
alertController.addAction(cancelAction)
|
||||
alertController.addAction(markAction)
|
||||
|
||||
return alertController
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"symbols" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "markAllAsRead.svg"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="3300px" height="2200px" viewBox="0 0 3300 2200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||
<title>Untitled</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="markAllAsRead">
|
||||
<g id="Notes">
|
||||
<rect id="artboard" fill="#FFFFFF" fill-rule="nonzero" x="0" y="0" width="3300" height="2200"></rect>
|
||||
<line x1="263" y1="292" x2="3036" y2="292" id="Path" stroke="#000000" stroke-width="0.5"></line>
|
||||
<text id="Weight/Scale-Variations" fill="#000000" font-family="Helvetica-Bold, Helvetica" font-size="13" font-weight="bold">
|
||||
<tspan x="263" y="322">Weight/Scale Variations</tspan>
|
||||
</text>
|
||||
<text id="Ultralight" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="533.711" y="322">Ultralight</tspan>
|
||||
</text>
|
||||
<text id="Thin" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="843.422" y="322">Thin</tspan>
|
||||
</text>
|
||||
<text id="Light" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="1138.63" y="322">Light</tspan>
|
||||
</text>
|
||||
<text id="Regular" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="1426.84" y="322">Regular</tspan>
|
||||
</text>
|
||||
<text id="Medium" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="1723.06" y="322">Medium</tspan>
|
||||
</text>
|
||||
<text id="Semibold" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="2015.77" y="322">Semibold</tspan>
|
||||
</text>
|
||||
<text id="Bold" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="2326.48" y="322">Bold</tspan>
|
||||
</text>
|
||||
<text id="Heavy" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="2618.19" y="322">Heavy</tspan>
|
||||
</text>
|
||||
<text id="Black" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="2917.4" y="322">Black</tspan>
|
||||
</text>
|
||||
<line x1="263" y1="1903" x2="3036" y2="1903" id="Path" stroke="#000000" stroke-width="0.5"></line>
|
||||
<g id="Group" transform="translate(264.000000, 1918.000000)" fill="#000000" fill-rule="nonzero">
|
||||
<path d="M8.24805,15.830078 C12.5547,15.830078 16.1387,12.25586 16.1387,7.94922 C16.1387,3.6426 12.5449,0.0684 8.23828,0.0684 C3.94141,0.0684 0.36719,3.6426 0.36719,7.94922 C0.36719,12.25586 3.95117,15.830078 8.24805,15.830078 Z M8.24805,14.345703 C4.70312,14.345703 1.87109,11.50391 1.87109,7.94922 C1.87109,4.3945 4.69336,1.5527 8.23828,1.5527 C11.793,1.5527 14.6348,4.3945 14.6445252,7.94922 C14.6543,11.50391 11.8027,14.345703 8.24805,14.345703 Z M8.22852,11.57227 C8.69727,11.57227 8.9707,11.25977 8.9707,10.74219 L8.9707,8.68164 L11.1973,8.68164 C11.6953,8.68164 12.0371,8.42773 12.0371,7.95898 C12.0371,7.48047 11.7148,7.2168 11.1973,7.2168 L8.9707,7.2168 L8.9707,4.9902 C8.9707,4.4727 8.69727,4.1504 8.22852,4.1504 C7.75977,4.1504 7.50586,4.4922 7.50586,4.9902 L7.50586,7.2168 L5.29883,7.2168 C4.78125,7.2168 4.44922,7.48047 4.44922,7.95898 C4.44922,8.42773 4.80078,8.68164 5.29883,8.68164 L7.50586,8.68164 L7.50586,10.74219 C7.50586,11.24023 7.75977,11.57227 8.22852,11.57227 Z" id="Shape"></path>
|
||||
</g>
|
||||
<g id="Group" transform="translate(282.506000, 1915.000000)" fill="#000000" fill-rule="nonzero">
|
||||
<path d="M10.709,20.91016 C16.1582,20.91016 20.6699,16.39844 20.6699,10.94922 C20.6699,5.5098 16.1484,0.9883 10.6992,0.9883 C5.25977,0.9883 0.74805,5.5098 0.74805,10.94922 C0.74805,16.39844 5.26953,20.91016 10.709,20.91016 Z M10.709,19.25 C6.09961,19.25 2.41797,15.55859 2.41797,10.94922 C2.41797,6.3496 6.08984,2.6484 10.6992,2.6484 C15.3086,2.6484 19,6.3496 19.009819,10.94922 C19.0195,15.55859 15.3184,19.25 10.709,19.25 Z M10.6895,15.58789 C11.207,15.58789 11.5195,15.22656 11.5195,14.66016 L11.5195,11.76953 L14.5762,11.76953 C15.123,11.76953 15.5039,11.48633 15.5039,10.96875 C15.5039,10.44141 15.1426,10.13867 14.5762,10.13867 L11.5195,10.13867 L11.5195,7.0723 C11.5195,6.4961 11.207,6.1445 10.6895,6.1445 C10.1719,6.1445 9.8789,6.5156 9.8789,7.0723 L9.8789,10.13867 L6.83203,10.13867 C6.26562,10.13867 5.89453,10.44141 5.89453,10.96875 C5.89453,11.48633 6.28516,11.76953 6.83203,11.76953 L9.8789,11.76953 L9.8789,14.66016 C9.8789,15.20703 10.1719,15.58789 10.6895,15.58789 Z" id="Shape"></path>
|
||||
</g>
|
||||
<g id="Group" transform="translate(306.924000, 1913.000000)" fill="#000000" fill-rule="nonzero">
|
||||
<path d="M12.9707,25.67383 C19.9336,25.67383 25.6953,19.921875 25.6953,12.95898 C25.6953,5.9961 19.9238,0.2441 12.9609,0.2441 C6.00781,0.2441 0.25586,5.9961 0.25586,12.95898 C0.25586,19.921875 6.01758,25.67383 12.9707,25.67383 Z M12.9707,23.85742 C6.93555,23.85742 2.08203,18.99414 2.08203,12.95898 C2.08203,6.9238 6.92578,2.0605 12.9609,2.0605 C19.0059,2.0605 23.8594,6.9238 23.8691148,12.95898 C23.8789,18.99414 19.0156,23.85742 12.9707,23.85742 Z M12.9512,18.93555 C13.5176,18.93555 13.8691,18.54492 13.8691,17.93945 L13.8691,13.86719 L18.1074,13.86719 C18.6934,13.86719 19.1133,13.53516 19.1133,12.97852 C19.1133,12.40234 18.7227,12.06055 18.1074,12.06055 L13.8691,12.06055 L13.8691,7.8125 C13.8691,7.1973 13.5176,6.8066 12.9512,6.8066 C12.3848,6.8066 12.0625,7.2168 12.0625,7.8125 L12.0625,12.06055 L7.83398,12.06055 C7.21875,12.06055 6.80859,12.40234 6.80859,12.97852 C6.80859,13.53516 7.23828,13.86719 7.83398,13.86719 L12.0625,13.86719 L12.0625,17.93945 C12.0625,18.52539 12.3848,18.93555 12.9512,18.93555 Z" id="Shape"></path>
|
||||
</g>
|
||||
<text id="Design-Variations" fill="#000000" font-family="Helvetica-Bold, Helvetica" font-size="13" font-weight="bold">
|
||||
<tspan x="263" y="1953">Design Variations</tspan>
|
||||
</text>
|
||||
<text id="Symbols-are-supported-in-up-to-nine-weights-and-three-scales." fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="1971">Symbols are supported in up to nine weights and three scales.</tspan>
|
||||
</text>
|
||||
<text id="For-optimal-layout-with-text-and-other-symbols,-vertically-align" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="1989">For optimal layout with text and other symbols, vertically align</tspan>
|
||||
</text>
|
||||
<text id="symbols-with-the-adjacent-text." fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="2007">symbols with the adjacent text.</tspan>
|
||||
</text>
|
||||
<rect id="Rectangle" fill="#00AEEF" fill-rule="nonzero" opacity="0.4" x="776" y="1919" width="3" height="14"></rect>
|
||||
<g id="Group" transform="translate(779.000000, 1918.000000)" fill="#000000" fill-rule="nonzero">
|
||||
<path d="M10.5273,15 L12.373,15 L7.17773,0.9082 L5.43945,0.9082 L0.244141,15 L2.08984,15 L3.50586,10.9668 L9.11133,10.9668 L10.5273,15 Z M6.2793,3.0469 L6.33789,3.0469 L8.59375,9.47266 L4.02344,9.47266 L6.2793,3.0469 Z" id="Shape"></path>
|
||||
</g>
|
||||
<rect id="Rectangle" fill="#00AEEF" fill-rule="nonzero" opacity="0.4" x="791.617" y="1919" width="3" height="14"></rect>
|
||||
<text id="Margins" fill="#000000" font-family="Helvetica-Bold, Helvetica" font-size="13" font-weight="bold">
|
||||
<tspan x="776" y="1953">Margins</tspan>
|
||||
</text>
|
||||
<text id="Leading-and-trailing-margins-on-the-left-and-right-side-of-each-symbol" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="776" y="1971">Leading and trailing margins on the left and right side of each symbol</tspan>
|
||||
</text>
|
||||
<text id="can-be-adjusted-by-modifying-the-width-of-the-blue-rectangles." fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="776" y="1989">can be adjusted by modifying the width of the blue rectangles.</tspan>
|
||||
</text>
|
||||
<text id="Modifications-are-automatically-applied-proportionally-to-all" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="776" y="2007">Modifications are automatically applied proportionally to all</tspan>
|
||||
</text>
|
||||
<text id="scales-and-weights." fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="776" y="2025">scales and weights.</tspan>
|
||||
</text>
|
||||
<g id="Group" transform="translate(1291.000000, 1915.000000)" fill="#000000" fill-rule="nonzero">
|
||||
<path d="M0.83203,21.11523 L2.375,22.6582 C3.22461,23.48828 4.19141,23.41992 5.06055,22.46289 L15.2754,11.33984 C15.7051,11.63281 16.0957,11.62305 16.5645,11.52539 L17.6094,11.31055 L18.3027,12.00391 L18.2539,12.52148 C18.1855,13.04883 18.3516,13.46875 18.8496,13.9668 L19.6602,14.77734 C20.168,15.28516 20.8223,15.31445 21.3008,14.83594 L24.5527,11.58398 C25.0312,11.10547 25.0117,10.45117 24.5039,9.94336 L23.6836,9.12305 C23.1855,8.625 22.7754,8.44922 22.2383,8.52734 L21.7109,8.58594 L21.0566,7.9219 L21.3398,6.7793 C21.4863,6.2129 21.3398,5.7441 20.7148,5.1387 L18.3027,2.7461 C14.7578,-0.7793 10.2266,-0.6719 7.11133,2.4629 C6.69141,2.8926 6.64258,3.4785 6.91602,3.9082 C7.15039,4.2793 7.62891,4.5039 8.2734,4.3379 C9.7871,3.957 11.3008,4.0742 12.7852,5.0801 L12.1602,6.6621 C11.9258,7.248 11.9453,7.7266 12.1797,8.16602 L1.01758,18.439453 C0.08008,19.30859 -0.02734,20.25586 0.83203,21.11523 Z M8.6738,2.8535 C11.3398,0.8613 14.6504,1.1738 17.0527,3.5859 L19.6797,6.1934 C19.9141,6.4277 19.9434,6.6133 19.8848,6.9062 L19.5039,8.46875 L21.0762,10.04102 L22.043,9.95312 C22.3262,9.92383 22.4141,9.94336 22.6387,10.16797 L23.2637,10.79297 L20.5098,13.53711 L19.8848,12.92188 C19.6602,12.69727 19.6406,12.59961 19.6699,12.31641 L19.7578,11.35938 L18.1953,9.79688 L16.5742,10.10938 C16.291,10.16797 16.1445,10.16797 15.9102,9.92383 L13.7324,7.7461 C13.5078,7.5117 13.4785,7.375 13.6055,7.0527 L14.5527,4.7773 C12.9512,3.2441 10.8418,2.3945 8.8008,3.0488 C8.7129,3.0781 8.6445,3.0586 8.6152,3.0195 C8.5859,2.9707 8.5859,2.9219 8.6738,2.8535 Z M2.10156,20.41211 C1.61328,19.91406 1.78906,19.61133 2.12109,19.30859 L13.0781,9.19141 L14.3086,10.42188 L4.15234,21.34961 C3.84961,21.68164 3.46875,21.7793 3.06836,21.37891 L2.10156,20.41211 Z" id="Shape"></path>
|
||||
</g>
|
||||
<text id="Exporting" fill="#000000" font-family="Helvetica-Bold, Helvetica" font-size="13" font-weight="bold">
|
||||
<tspan x="1289" y="1953">Exporting</tspan>
|
||||
</text>
|
||||
<text id="Symbols-should-be-outlined-when-exporting-to-ensure-the" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="1289" y="1971">Symbols should be outlined when exporting to ensure the</tspan>
|
||||
</text>
|
||||
<text id="design-is-preserved-when-submitting-to-Xcode." fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="1289" y="1989">design is preserved when submitting to Xcode.</tspan>
|
||||
</text>
|
||||
<text id="template-version" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="2952" y="1933">Template v.1.0</tspan>
|
||||
</text>
|
||||
<text id="Generated-from-circle" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="2911" y="1951">Generated from circle</tspan>
|
||||
</text>
|
||||
<text id="Typeset-at-100-points" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="2912" y="1969">Typeset at 100 points</tspan>
|
||||
</text>
|
||||
<text id="Small" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="726">Small</tspan>
|
||||
</text>
|
||||
<text id="Medium" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="1156">Medium</tspan>
|
||||
</text>
|
||||
<text id="Large" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="1586">Large</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Guides" transform="translate(263.000000, 625.000000)">
|
||||
<g id="H-reference" transform="translate(76.000000, 0.000000)" fill="#27AAE1" fill-rule="nonzero">
|
||||
<path d="M54.9316,71 L57.666,71 L30.5664,0.541 L28.0762,0.541 L0.976562,71 L3.66211,71 L12.9395,46.5371 L45.7031,46.5371 L54.9316,71 Z M29.1992,3.9102 L29.4434,3.9102 L44.8242,44.291 L13.8184,44.291 L29.1992,3.9102 Z" id="Shape"></path>
|
||||
</g>
|
||||
<line x1="0" y1="71" x2="2773" y2="71" id="Baseline-S" stroke="#27AAE1" stroke-width="0.577"></line>
|
||||
<line x1="0" y1="0.541" x2="2773" y2="0.541" id="Capline-S" stroke="#27AAE1" stroke-width="0.577"></line>
|
||||
<g id="H-reference" transform="translate(76.000000, 430.000000)" fill="#27AAE1" fill-rule="nonzero">
|
||||
<path d="M54.9316,71 L57.666,71 L30.5664,0.541 L28.0762,0.541 L0.976562,71 L3.66211,71 L12.9395,46.5371 L45.7031,46.5371 L54.9316,71 Z M29.1992,3.9102 L29.4434,3.9102 L44.8242,44.291 L13.8184,44.291 L29.1992,3.9102 Z" id="Shape"></path>
|
||||
</g>
|
||||
<line x1="0" y1="501" x2="2773" y2="501" id="Baseline-M" stroke="#27AAE1" stroke-width="0.577"></line>
|
||||
<line x1="0" y1="430.54" x2="2773" y2="430.54" id="Capline-M" stroke="#27AAE1" stroke-width="0.577"></line>
|
||||
<g id="H-reference" transform="translate(76.000000, 860.000000)" fill="#27AAE1" fill-rule="nonzero">
|
||||
<path d="M54.9316,71 L57.666,71 L30.5664,0.541 L28.0762,0.541 L0.976562,71 L3.66211,71 L12.9395,46.5371 L45.7031,46.5371 L54.9316,71 Z M29.1992,3.9102 L29.4434,3.9102 L44.8242,44.291 L13.8184,44.291 L29.1992,3.9102 Z" id="Shape"></path>
|
||||
</g>
|
||||
<line x1="0" y1="931" x2="2773" y2="931" id="Baseline-L" stroke="#27AAE1" stroke-width="0.577"></line>
|
||||
<line x1="0" y1="860.54" x2="2773" y2="860.54" id="Capline-L" stroke="#27AAE1" stroke-width="0.577"></line>
|
||||
<rect id="left-margin" fill="#00AEEF" fill-rule="nonzero" opacity="0.4" x="1128.3" y="405.79" width="8.74023" height="119.336"></rect>
|
||||
<rect id="right-margin" fill="#00AEEF" fill-rule="nonzero" opacity="0.4" x="1236.65" y="405.79" width="8.74023" height="119.336"></rect>
|
||||
</g>
|
||||
<g id="Symbols" transform="translate(1396.000000, 1003.000000)" fill="#000000" fill-rule="nonzero">
|
||||
<g id="Regular-M" transform="translate(0.300000, 0.000000)">
|
||||
<path d="M50.50467,149.6094 C77.75077,149.6094 100.30977,127.05079 100.30977,99.8047 C100.30977,72.6074 77.70197,50 50.45587,50 C23.25857,50 0.7,72.6074 0.7,99.8047 C0.7,127.05079 23.30747,149.6094 50.50467,149.6094 Z M50.50467,141.3086 C27.45777,141.3086 9.04957,122.8516 9.04957,99.8047 C9.04957,76.8066 27.40897,58.3008 50.45587,58.3008 C73.50277,58.3008 91.95977,76.8066 92.0088671,99.8047 C92.05777,122.8516 73.55157,141.3086 50.50467,141.3086 Z" id="Shape"></path>
|
||||
<path d="M50.45587,25 C77.70197,25 100.30977,47.6074 100.30977,74.8047 C100.30977,74.870144 100.30964,74.935561 100.30938,75.0009507 L99.8322178,75.0006156 C97.6995557,51.580586 76.5280991,33.3008 50.4213361,33.3008 C24.3145732,33.3008 3.24806132,51.580586 1.17616722,75.0006156 L0.701,75 L0.7,74.8047 C0.7,47.879373 22.8096545,25.452607 49.6413605,25.0067642 Z" id="Combined-Shape"></path>
|
||||
<path d="M50.45587,-1.77635684e-14 C77.70197,-1.77635684e-14 100.30977,22.6074 100.30977,49.8047 C100.30977,49.870144 100.30964,49.935561 100.30938,50.0009507 L99.8322178,50.0006156 C97.6995557,26.580586 76.5280991,8.3008 50.4213361,8.3008 C24.3145732,8.3008 3.24806132,26.580586 1.17616722,50.0006156 L0.701,50 L0.7,49.8047 C0.7,22.879373 22.8096545,0.45260699 49.6413605,0.0067642025 Z" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 17 KiB |
|
@ -142,5 +142,27 @@
|
|||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.xml</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>OPML</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.opml.opml</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>opml</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -1,110 +1,137 @@
|
|||
var controller = new AbortController()
|
||||
var activeImageViewer = null;
|
||||
|
||||
// Cancel any pending image loads (there might be none) and reset the controller
|
||||
function cancelImageLoad() {
|
||||
controller.abort();
|
||||
controller = new AbortController();
|
||||
}
|
||||
|
||||
// Used to pop a resizable image view
|
||||
async function imageWasClicked(img) {
|
||||
cancelImageLoad();
|
||||
showNetworkLoading(img);
|
||||
|
||||
try {
|
||||
|
||||
const signal = controller.signal;
|
||||
const response = await fetch(img.src, { signal: signal });
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok.');
|
||||
}
|
||||
|
||||
const imgBlob = await response.blob();
|
||||
if (signal.aborted) {
|
||||
throw new Error('Network response was aborted.');
|
||||
}
|
||||
|
||||
hideNetworkLoading(img);
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.readAsDataURL(imgBlob);
|
||||
reader.onloadend = function() {
|
||||
|
||||
img.classList.add("nnwClicked");
|
||||
|
||||
const rect = img.getBoundingClientRect();
|
||||
var message = {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
imageTitle: img.title
|
||||
};
|
||||
message.imageURL = reader.result;
|
||||
|
||||
var jsonMessage = JSON.stringify(message);
|
||||
window.webkit.messageHandlers.imageWasClicked.postMessage(jsonMessage);
|
||||
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
hideNetworkLoading(img);
|
||||
console.log('There has been a problem with your fetch operation: ', error.message);
|
||||
class ImageViewer {
|
||||
constructor(img) {
|
||||
this.img = img;
|
||||
this.loadingInterval = null;
|
||||
this.activityIndicator = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjAiIHdpZHRoPSI2NHB4IiBoZWlnaHQ9IjY0cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMwMDAwMDAiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDMwIDY0IDY0KSIvPjxwYXRoIGQ9Ik01OS42IDBoOHY0MGgtOFYweiIgZmlsbD0iI2NjY2NjYyIgdHJhbnNmb3JtPSJyb3RhdGUoNjAgNjQgNjQpIi8+PHBhdGggZD0iTTU5LjYgMGg4djQwaC04VjB6IiBmaWxsPSIjY2NjY2NjIiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDEyMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNiMmIyYjIiIHRyYW5zZm9ybT0icm90YXRlKDE1MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM5OTk5OTkiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM3ZjdmN2YiIHRyYW5zZm9ybT0icm90YXRlKDIxMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM2NjY2NjYiIHRyYW5zZm9ybT0icm90YXRlKDI0MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM0YzRjNGMiIHRyYW5zZm9ybT0icm90YXRlKDI3MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMzMzMzMzMiIHRyYW5zZm9ybT0icm90YXRlKDMwMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMxOTE5MTkiIHRyYW5zZm9ybT0icm90YXRlKDMzMCA2NCA2NCkiLz48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgdmFsdWVzPSIwIDY0IDY0OzMwIDY0IDY0OzYwIDY0IDY0OzkwIDY0IDY0OzEyMCA2NCA2NDsxNTAgNjQgNjQ7MTgwIDY0IDY0OzIxMCA2NCA2NDsyNDAgNjQgNjQ7MjcwIDY0IDY0OzMwMCA2NCA2NDszMzAgNjQgNjQiIGNhbGNNb2RlPSJkaXNjcmV0ZSIgZHVyPSIxMDgwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIj48L2FuaW1hdGVUcmFuc2Zvcm0+PC9nPjwvc3ZnPg==";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function showNetworkLoading(img) {
|
||||
isLoaded() {
|
||||
return this.img.classList.contains("nnwLoaded");
|
||||
}
|
||||
|
||||
var wrapper = document.createElement("div");
|
||||
wrapper.classList.add("activityIndicatorWrap");
|
||||
img.parentNode.insertBefore(wrapper, img);
|
||||
wrapper.appendChild(img);
|
||||
clicked() {
|
||||
this.showLoadingIndicator();
|
||||
if (this.isLoaded()) {
|
||||
this.showViewer();
|
||||
} else {
|
||||
var callback = () => {
|
||||
if (this.isLoaded()) {
|
||||
clearInterval(this.loadingInterval);
|
||||
this.showViewer();
|
||||
}
|
||||
}
|
||||
this.loadingInterval = setInterval(callback, 100);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
clearInterval(this.loadingInterval);
|
||||
this.hideLoadingIndicator();
|
||||
}
|
||||
|
||||
showViewer() {
|
||||
this.hideLoadingIndicator();
|
||||
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = this.img.naturalWidth;
|
||||
canvas.height = this.img.naturalHeight;
|
||||
canvas.getContext("2d").drawImage(this.img, 0, 0);
|
||||
|
||||
const rect = this.img.getBoundingClientRect();
|
||||
const message = {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
imageTitle: this.img.title,
|
||||
imageURL: canvas.toDataURL(),
|
||||
};
|
||||
|
||||
var jsonMessage = JSON.stringify(message);
|
||||
window.webkit.messageHandlers.imageWasClicked.postMessage(jsonMessage);
|
||||
}
|
||||
|
||||
hideImage() {
|
||||
this.img.style.opacity = 0;
|
||||
}
|
||||
|
||||
showImage() {
|
||||
this.img.style.opacity = 1
|
||||
window.webkit.messageHandlers.imageWasShown.postMessage("");
|
||||
}
|
||||
|
||||
showLoadingIndicator() {
|
||||
var wrapper = document.createElement("div");
|
||||
wrapper.classList.add("activityIndicatorWrap");
|
||||
this.img.parentNode.insertBefore(wrapper, this.img);
|
||||
wrapper.appendChild(this.img);
|
||||
|
||||
var activityIndicatorImg = document.createElement("img");
|
||||
activityIndicatorImg.classList.add("activityIndicator");
|
||||
activityIndicatorImg.style.opacity = 0;
|
||||
activityIndicatorImg.src = this.activityIndicator;
|
||||
wrapper.appendChild(activityIndicatorImg);
|
||||
|
||||
var activityIndicatorImg = document.createElement("img");
|
||||
activityIndicatorImg.classList.add("activityIndicator");
|
||||
activityIndicatorImg.style.opacity = 0;
|
||||
activityIndicatorImg.src = activityIndicator;
|
||||
wrapper.appendChild(activityIndicatorImg);
|
||||
|
||||
// Wait a bit before showing the indicator
|
||||
function showActivityIndicator() {
|
||||
activityIndicatorImg.style.opacity = 1;
|
||||
}
|
||||
setTimeout(showActivityIndicator, 300);
|
||||
|
||||
hideLoadingIndicator() {
|
||||
var wrapper = this.img.parentNode;
|
||||
if (wrapper.classList.contains("activityIndicatorWrap")) {
|
||||
var wrapperParent = wrapper.parentNode;
|
||||
wrapperParent.insertBefore(this.img, wrapper);
|
||||
wrapperParent.removeChild(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
static init() {
|
||||
cancelImageLoad();
|
||||
|
||||
// keep track of when an image has finished downloading for ImageViewer
|
||||
document.querySelectorAll("img").forEach(element => {
|
||||
element.onload = function() {
|
||||
this.classList.add("nnwLoaded");
|
||||
}
|
||||
});
|
||||
|
||||
// Add the click listener for images
|
||||
window.onclick = function(event) {
|
||||
if (event.target.matches("img")) {
|
||||
if (activeImageViewer && activeImageViewer.img === event.target) {
|
||||
cancelImageLoad();
|
||||
} else {
|
||||
cancelImageLoad();
|
||||
activeImageViewer = new ImageViewer(event.target);
|
||||
activeImageViewer.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideNetworkLoading(img) {
|
||||
var wrapper = img.parentNode;
|
||||
var wrapperParent = wrapper.parentNode;
|
||||
wrapperParent.insertBefore(img, wrapper);
|
||||
wrapperParent.removeChild(wrapper);
|
||||
function cancelImageLoad() {
|
||||
if (activeImageViewer) {
|
||||
activeImageViewer.cancel();
|
||||
activeImageViewer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Used to animate the transition to a fullscreen image
|
||||
function hideClickedImage() {
|
||||
var img = document.querySelector('.nnwClicked')
|
||||
img.style.opacity = 0
|
||||
if (activeImageViewer) {
|
||||
activeImageViewer.hideImage();
|
||||
}
|
||||
}
|
||||
|
||||
// Used to animate the transition from a fullscreen image
|
||||
function showClickedImage() {
|
||||
var img = document.querySelector('.nnwClicked')
|
||||
img.classList.remove("nnwClicked");
|
||||
img.style.opacity = 1
|
||||
window.webkit.messageHandlers.imageWasShown.postMessage("");
|
||||
}
|
||||
|
||||
// Add the click listener for images
|
||||
function imageClicks() {
|
||||
window.onclick = function(event) {
|
||||
if (event.target.matches('img')) {
|
||||
imageWasClicked(event.target);
|
||||
}
|
||||
if (activeImageViewer) {
|
||||
activeImageViewer.showImage();
|
||||
}
|
||||
}
|
||||
|
||||
// Add the playsinline attribute to any HTML5 videos that don't have it.
|
||||
// Add the playsinline attribute to any HTML5 videos that don"t have it.
|
||||
// Without this attribute videos may autoplay and take over the whole screen
|
||||
// on an iphone when viewing an article.
|
||||
function inlineVideos() {
|
||||
|
@ -114,9 +141,6 @@ function inlineVideos() {
|
|||
}
|
||||
|
||||
function postRenderProcessing() {
|
||||
cancelImageLoad();
|
||||
imageClicks();
|
||||
ImageViewer.init();
|
||||
inlineVideos();
|
||||
}
|
||||
|
||||
const activityIndicator = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjAiIHdpZHRoPSI2NHB4IiBoZWlnaHQ9IjY0cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMwMDAwMDAiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDMwIDY0IDY0KSIvPjxwYXRoIGQ9Ik01OS42IDBoOHY0MGgtOFYweiIgZmlsbD0iI2NjY2NjYyIgdHJhbnNmb3JtPSJyb3RhdGUoNjAgNjQgNjQpIi8+PHBhdGggZD0iTTU5LjYgMGg4djQwaC04VjB6IiBmaWxsPSIjY2NjY2NjIiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDEyMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNiMmIyYjIiIHRyYW5zZm9ybT0icm90YXRlKDE1MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM5OTk5OTkiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM3ZjdmN2YiIHRyYW5zZm9ybT0icm90YXRlKDIxMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM2NjY2NjYiIHRyYW5zZm9ybT0icm90YXRlKDI0MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM0YzRjNGMiIHRyYW5zZm9ybT0icm90YXRlKDI3MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMzMzMzMzMiIHRyYW5zZm9ybT0icm90YXRlKDMwMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMxOTE5MTkiIHRyYW5zZm9ybT0icm90YXRlKDMzMCA2NCA2NCkiLz48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgdmFsdWVzPSIwIDY0IDY0OzMwIDY0IDY0OzYwIDY0IDY0OzkwIDY0IDY0OzEyMCA2NCA2NDsxNTAgNjQgNjQ7MTgwIDY0IDY0OzIxMCA2NCA2NDsyNDAgNjQgNjQ7MjcwIDY0IDY0OzMwMCA2NCA2NDszMzAgNjQgNjQiIGNhbGNNb2RlPSJkaXNjcmV0ZSIgZHVyPSIxMDgwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIj48L2FuaW1hdGVUcmFuc2Zvcm0+PC9nPjwvc3ZnPg==";
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
:root {
|
||||
font: -apple-system-body;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
font: -apple-system-body;
|
||||
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
|
@ -91,6 +94,7 @@ body > .systemMessage {
|
|||
}
|
||||
.avatar img {
|
||||
border-radius: 4px;
|
||||
max-width: none;
|
||||
}
|
||||
.feedIcon {
|
||||
border-radius: 4px;
|
||||
|
@ -124,7 +128,7 @@ pre {
|
|||
margin: 0;
|
||||
overflow: auto;
|
||||
overflow-y: hidden;
|
||||
line-height: 20px;
|
||||
line-height: 1.4286em;
|
||||
border: 1px solid var(--secondary-accent-color);
|
||||
padding: 5px;
|
||||
word-wrap: normal;
|
||||
|
@ -136,7 +140,7 @@ pre {
|
|||
}
|
||||
code, pre {
|
||||
font-family: "SF Mono", Menlo, "Courier New", Courier, monospace;
|
||||
font-size: 14px;
|
||||
font-size: .8235rem;
|
||||
}
|
||||
img, figure, video, iframe, div {
|
||||
max-width: 100%;
|
||||
|
|
|
@ -53,6 +53,10 @@ class RootSplitViewController: UISplitViewController {
|
|||
coordinator.markAllAsReadInTimeline()
|
||||
coordinator.selectNextUnread()
|
||||
}
|
||||
|
||||
@objc func markAboveAsRead(_ sender: Any?) {
|
||||
coordinator.markAboveAsRead()
|
||||
}
|
||||
|
||||
@objc func markBelowAsRead(_ sender: Any?) {
|
||||
coordinator.markBelowAsRead()
|
||||
|
|
|
@ -92,11 +92,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||
|
||||
var prefersStatusBarHidden = false
|
||||
|
||||
var displayUndoAvailableTip: Bool {
|
||||
get { AppDefaults.displayUndoAvailableTip }
|
||||
set { AppDefaults.displayUndoAvailableTip = newValue }
|
||||
}
|
||||
|
||||
private let treeControllerDelegate = WebFeedTreeControllerDelegate()
|
||||
private let treeController: TreeController
|
||||
|
||||
|
@ -257,7 +252,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||
|
||||
var currentArticle: Article?
|
||||
|
||||
private(set) var articles = ArticleArray()
|
||||
private(set) var articles = ArticleArray() {
|
||||
didSet {
|
||||
timelineMiddleIndexPath = nil
|
||||
}
|
||||
}
|
||||
|
||||
private var currentArticleRow: Int? {
|
||||
guard let article = currentArticle else { return nil }
|
||||
return articles.firstIndex(of: article)
|
||||
|
@ -872,20 +872,26 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||
}
|
||||
|
||||
func canMarkAboveAsRead(for article: Article) -> Bool {
|
||||
return articles.first != article
|
||||
let articlesAboveArray = articles.articlesAbove(article: article)
|
||||
return articlesAboveArray.canMarkAllAsRead()
|
||||
}
|
||||
|
||||
func markAboveAsRead(_ article: Article) {
|
||||
guard let position = articles.firstIndex(of: article) else {
|
||||
func markAboveAsRead() {
|
||||
guard let currentArticle = currentArticle else {
|
||||
return
|
||||
}
|
||||
|
||||
let articlesAbove = articles[..<position]
|
||||
markAllAsRead(Array(articlesAbove))
|
||||
markAboveAsRead(currentArticle)
|
||||
}
|
||||
|
||||
func markAboveAsRead(_ article: Article) {
|
||||
let articlesAboveArray = articles.articlesAbove(article: article)
|
||||
markAllAsRead(articlesAboveArray)
|
||||
}
|
||||
|
||||
func canMarkBelowAsRead(for article: Article) -> Bool {
|
||||
return articles.last != article
|
||||
let articleBelowArray = articles.articlesBelow(article: article)
|
||||
return articleBelowArray.canMarkAllAsRead()
|
||||
}
|
||||
|
||||
func markBelowAsRead() {
|
||||
|
@ -897,18 +903,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||
}
|
||||
|
||||
func markBelowAsRead(_ article: Article) {
|
||||
guard let position = articles.firstIndex(of: article) else {
|
||||
return
|
||||
}
|
||||
|
||||
var articlesBelow = Array(articles[position...])
|
||||
|
||||
guard !articlesBelow.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
articlesBelow.removeFirst()
|
||||
markAllAsRead(articlesBelow)
|
||||
let articleBelowArray = articles.articlesBelow(article: article)
|
||||
markAllAsRead(articleBelowArray)
|
||||
}
|
||||
|
||||
func markAsReadForCurrentArticle() {
|
||||
|
@ -963,9 +959,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||
}
|
||||
}
|
||||
|
||||
func showSettings() {
|
||||
func showSettings(scrollToArticlesSection: Bool = false) {
|
||||
let settingsNavController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController
|
||||
let settingsViewController = settingsNavController.topViewController as! SettingsViewController
|
||||
settingsViewController.scrollToArticlesSection = scrollToArticlesSection
|
||||
settingsNavController.modalPresentationStyle = .formSheet
|
||||
settingsViewController.presentingParentController = rootSplitViewController
|
||||
rootSplitViewController.present(settingsNavController, animated: true)
|
||||
|
@ -1101,6 +1098,7 @@ extension SceneCoordinator: UISplitViewControllerDelegate {
|
|||
}
|
||||
|
||||
if let articleViewController = masterNavigationController.viewControllers.last as? ArticleViewController {
|
||||
articleViewController.showBars(self)
|
||||
masterNavigationController.popViewController(animated: false)
|
||||
let controller = addNavControllerIfNecessary(articleViewController, showButton: true)
|
||||
return controller
|
||||
|
@ -1234,7 +1232,6 @@ private extension SceneCoordinator {
|
|||
|
||||
func setTimelineFeed(_ feed: Feed?, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
timelineFeed = feed
|
||||
timelineMiddleIndexPath = nil
|
||||
|
||||
fetchAndReplaceArticlesAsync(animated: animated) {
|
||||
self.masterTimelineViewController?.reinitializeArticles(resetScroll: true)
|
||||
|
@ -1501,7 +1498,6 @@ private extension SceneCoordinator {
|
|||
|
||||
func emptyTheTimeline() {
|
||||
if !articles.isEmpty {
|
||||
timelineMiddleIndexPath = nil
|
||||
replaceArticles(with: Set<Article>(), animated: false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,11 +45,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
}
|
||||
|
||||
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||
appDelegate.resumeDatabaseProcessingIfNecessary()
|
||||
handleShortcutItem(shortcutItem)
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
||||
appDelegate.resumeDatabaseProcessingIfNecessary()
|
||||
coordinator.handle(userActivity)
|
||||
}
|
||||
|
||||
|
@ -59,6 +61,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
appDelegate.resumeDatabaseProcessingIfNecessary()
|
||||
appDelegate.prepareAccountsForForeground()
|
||||
self.coordinator.configurePanelMode(for: window!.frame.size)
|
||||
}
|
||||
|
@ -70,6 +73,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
// API
|
||||
|
||||
func handle(_ response: UNNotificationResponse) {
|
||||
appDelegate.resumeDatabaseProcessingIfNecessary()
|
||||
coordinator.handle(response)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<tableViewSection headerTitle="Notifications, Badge, Data, & More" id="Bmb-Oi-RZK">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="F9H-Kr-npj" style="IBUITableViewCellStyleDefault" id="zvg-7C-BlH" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="55.5" width="374" height="44"/>
|
||||
<rect key="frame" x="20" y="55.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="zvg-7C-BlH" id="Tqk-Tu-E6K">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
|
@ -177,36 +177,99 @@
|
|||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="6C6-JQ-lfQ" style="IBUITableViewCellStyleDefault" id="5wo-fM-0l6" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="5wo-fM-0l6" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="531.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5wo-fM-0l6" id="XAn-lK-LoN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Refresh to Clear Read Articles" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="KtJ-tk-DlD">
|
||||
<rect key="frame" x="20" y="12" width="229" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="duV-CN-JmH">
|
||||
<rect key="frame" x="305" y="6.5" width="51" height="31"/>
|
||||
<color key="onTintColor" name="primaryAccentColor"/>
|
||||
<connections>
|
||||
<action selector="switchClearsReadArticles:" destination="a0p-rk-skQ" eventType="valueChanged" id="Nel-mq-9fP"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="KtJ-tk-DlD" firstAttribute="leading" secondItem="XAn-lK-LoN" secondAttribute="leadingMargin" id="AOT-A0-ak0"/>
|
||||
<constraint firstItem="KtJ-tk-DlD" firstAttribute="centerY" secondItem="XAn-lK-LoN" secondAttribute="centerY" id="DrO-Rb-Hgg"/>
|
||||
<constraint firstAttribute="trailing" secondItem="duV-CN-JmH" secondAttribute="trailing" constant="20" symbolic="YES" id="Qkh-LF-zez"/>
|
||||
<constraint firstItem="duV-CN-JmH" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="KtJ-tk-DlD" secondAttribute="trailing" constant="8" id="cCz-fb-lta"/>
|
||||
<constraint firstItem="duV-CN-JmH" firstAttribute="centerY" secondItem="XAn-lK-LoN" secondAttribute="centerY" id="eui-vJ-Bp8"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" id="8Gj-qz-NMY" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="575.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="8Gj-qz-NMY" id="OTe-tG-sb4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Timeline Layout" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6C6-JQ-lfQ">
|
||||
<rect key="frame" x="20" y="0.0" width="315" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Timeline Layout" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YHt-eS-KrX">
|
||||
<rect key="frame" x="20" y="11.5" width="120.5" height="21"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="YHt-eS-KrX" firstAttribute="centerY" secondItem="OTe-tG-sb4" secondAttribute="centerY" id="HpK-qo-s57"/>
|
||||
<constraint firstItem="YHt-eS-KrX" firstAttribute="leading" secondItem="OTe-tG-sb4" secondAttribute="leadingMargin" id="STg-aB-F7n"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Articles" id="TRr-Ew-IvU">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="SXs-NQ-y3U" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="675.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="SXs-NQ-y3U" id="BpI-Hz-KH2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Confirm Mark All as Read" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5tY-5k-v2g">
|
||||
<rect key="frame" x="20" y="11.5" width="193" height="21"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="UOo-9z-IuL">
|
||||
<rect key="frame" x="305" y="6.5" width="51" height="31"/>
|
||||
<color key="onTintColor" name="primaryAccentColor"/>
|
||||
<connections>
|
||||
<action selector="switchConfirmMarkAllAsRead:" destination="a0p-rk-skQ" eventType="valueChanged" id="7wW-hF-2OY"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="UOo-9z-IuL" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="5tY-5k-v2g" secondAttribute="trailing" constant="8" id="KeP-ft-0GH"/>
|
||||
<constraint firstItem="5tY-5k-v2g" firstAttribute="centerY" secondItem="BpI-Hz-KH2" secondAttribute="centerY" id="Pna-0P-cno"/>
|
||||
<constraint firstItem="UOo-9z-IuL" firstAttribute="centerY" secondItem="BpI-Hz-KH2" secondAttribute="centerY" id="V2L-l3-s1E"/>
|
||||
<constraint firstAttribute="trailing" secondItem="UOo-9z-IuL" secondAttribute="trailing" constant="20" symbolic="YES" id="mNk-x8-oJx"/>
|
||||
<constraint firstItem="5tY-5k-v2g" firstAttribute="leading" secondItem="BpI-Hz-KH2" secondAttribute="leadingMargin" id="v4X-Nd-cpC"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="WR6-xo-ty2" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="631.5" width="374" height="44"/>
|
||||
<rect key="frame" x="20" y="719.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="WR6-xo-ty2" id="zX8-l2-bVH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Show Articles in Full Screen" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="79e-5s-vd0">
|
||||
<rect key="frame" x="20" y="11.5" width="206" height="21"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" text="Show Articles in Full Screen" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="79e-5s-vd0">
|
||||
<rect key="frame" x="20" y="12" width="212" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
|
@ -233,7 +296,7 @@
|
|||
<tableViewSection headerTitle="Help" id="TkH-4v-yhk">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="40W-2p-ne4" style="IBUITableViewCellStyleDefault" id="Om7-lH-RUh" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="731.5" width="374" height="44"/>
|
||||
<rect key="frame" x="20" y="819.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Om7-lH-RUh" id="vrJ-nE-HMP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
|
@ -250,7 +313,7 @@
|
|||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="lOk-Dh-GfZ" style="IBUITableViewCellStyleDefault" id="GWZ-jk-qU6" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="775.5" width="374" height="44"/>
|
||||
<rect key="frame" x="20" y="863.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GWZ-jk-qU6" id="ZgS-bo-xDl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
|
@ -267,7 +330,7 @@
|
|||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Pm8-6D-fdE" style="IBUITableViewCellStyleDefault" id="3cU-BG-6kK" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="819.5" width="374" height="44"/>
|
||||
<rect key="frame" x="0.0" y="907.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3cU-BG-6kK" id="Qm0-SY-0vx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
|
@ -284,14 +347,14 @@
|
|||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="TEA-EG-V6d" style="IBUITableViewCellStyleDefault" id="4yc-ig-I61" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="863.5" width="374" height="44"/>
|
||||
<rect key="frame" x="0.0" y="951.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="4yc-ig-I61" id="uQl-VP-9p9">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="GitHub Repository" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="TEA-EG-V6d">
|
||||
<rect key="frame" x="20" y="0.0" width="334" height="44"/>
|
||||
<rect key="frame" x="15" y="0.0" width="351" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
|
@ -301,14 +364,14 @@
|
|||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Q9a-Pi-uCc" style="IBUITableViewCellStyleDefault" id="mSW-A7-8lf" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="907.5" width="374" height="44"/>
|
||||
<rect key="frame" x="0.0" y="995.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="mSW-A7-8lf" id="shF-ro-Zpx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Bug Tracker" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="Q9a-Pi-uCc">
|
||||
<rect key="frame" x="20" y="0.0" width="334" height="44"/>
|
||||
<rect key="frame" x="15" y="0.0" width="351" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
|
@ -318,14 +381,14 @@
|
|||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dWz-1o-EpJ" style="IBUITableViewCellStyleDefault" id="2MG-qn-idJ" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="951.5" width="374" height="44"/>
|
||||
<rect key="frame" x="0.0" y="1039.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2MG-qn-idJ" id="gP9-ry-keC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Technotes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="dWz-1o-EpJ">
|
||||
<rect key="frame" x="20" y="0.0" width="334" height="44"/>
|
||||
<rect key="frame" x="15" y="0.0" width="351" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
|
@ -335,14 +398,14 @@
|
|||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="2o6-8W-nyK" style="IBUITableViewCellStyleDefault" id="he9-Ql-yfa" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="995.5" width="374" height="44"/>
|
||||
<rect key="frame" x="0.0" y="1083.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="he9-Ql-yfa" id="q6L-C8-H9a">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="NetNewsWire Slack" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="2o6-8W-nyK">
|
||||
<rect key="frame" x="20" y="0.0" width="334" height="44"/>
|
||||
<rect key="frame" x="15" y="0.0" width="351" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
|
@ -352,14 +415,14 @@
|
|||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="Uwu-af-31r" style="IBUITableViewCellStyleDefault" id="EvG-yE-gDF" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="1039.5" width="374" height="44"/>
|
||||
<rect key="frame" x="0.0" y="1127.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EvG-yE-gDF" id="wBN-zJ-6pN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="355" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="About NetNewsWire" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="Uwu-af-31r">
|
||||
<rect key="frame" x="20" y="0.0" width="315" height="44"/>
|
||||
<rect key="frame" x="15" y="0.0" width="332" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
|
@ -384,7 +447,9 @@
|
|||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="confirmMarkAllAsReadSwitch" destination="UOo-9z-IuL" id="yLZ-Kf-wDt"/>
|
||||
<outlet property="groupByFeedSwitch" destination="JNi-Wz-RbU" id="TwH-Kd-o6N"/>
|
||||
<outlet property="refreshClearsReadArticlesSwitch" destination="duV-CN-JmH" id="xTd-jF-Ei1"/>
|
||||
<outlet property="showFullscreenArticlesSwitch" destination="2Md-2E-7Z4" id="lEN-VP-wEO"/>
|
||||
<outlet property="timelineSortOrderSwitch" destination="Keq-Np-l9O" id="Zm7-HG-r5h"/>
|
||||
</connections>
|
||||
|
|
|
@ -17,8 +17,11 @@ class SettingsViewController: UITableViewController {
|
|||
|
||||
@IBOutlet weak var timelineSortOrderSwitch: UISwitch!
|
||||
@IBOutlet weak var groupByFeedSwitch: UISwitch!
|
||||
@IBOutlet weak var refreshClearsReadArticlesSwitch: UISwitch!
|
||||
@IBOutlet weak var confirmMarkAllAsReadSwitch: UISwitch!
|
||||
@IBOutlet weak var showFullscreenArticlesSwitch: UISwitch!
|
||||
|
||||
var scrollToArticlesSection = false
|
||||
weak var presentingParentController: UIViewController?
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
@ -33,7 +36,7 @@ class SettingsViewController: UITableViewController {
|
|||
|
||||
tableView.register(UINib(nibName: "SettingsAccountTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsAccountTableViewCell")
|
||||
tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell")
|
||||
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -51,6 +54,18 @@ class SettingsViewController: UITableViewController {
|
|||
groupByFeedSwitch.isOn = false
|
||||
}
|
||||
|
||||
if AppDefaults.refreshClearsReadArticles {
|
||||
refreshClearsReadArticlesSwitch.isOn = true
|
||||
} else {
|
||||
refreshClearsReadArticlesSwitch.isOn = false
|
||||
}
|
||||
|
||||
if AppDefaults.confirmMarkAllAsRead {
|
||||
confirmMarkAllAsReadSwitch.isOn = true
|
||||
} else {
|
||||
confirmMarkAllAsReadSwitch.isOn = false
|
||||
}
|
||||
|
||||
if AppDefaults.articleFullscreenEnabled {
|
||||
showFullscreenArticlesSwitch.isOn = true
|
||||
} else {
|
||||
|
@ -74,6 +89,12 @@ class SettingsViewController: UITableViewController {
|
|||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
|
||||
if scrollToArticlesSection {
|
||||
tableView.scrollToRow(at: IndexPath(row: 0, section: 4), at: .top, animated: true)
|
||||
scrollToArticlesSection = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: UITableView
|
||||
|
@ -188,7 +209,7 @@ class SettingsViewController: UITableViewController {
|
|||
}
|
||||
case 3:
|
||||
switch indexPath.row {
|
||||
case 2:
|
||||
case 4:
|
||||
let timeline = UIStoryboard.settings.instantiateController(ofType: TimelineCustomizerViewController.self)
|
||||
self.navigationController?.pushViewController(timeline, animated: true)
|
||||
default:
|
||||
|
@ -270,6 +291,22 @@ class SettingsViewController: UITableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
@IBAction func switchClearsReadArticles(_ sender: Any) {
|
||||
if refreshClearsReadArticlesSwitch.isOn {
|
||||
AppDefaults.refreshClearsReadArticles = true
|
||||
} else {
|
||||
AppDefaults.refreshClearsReadArticles = false
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func switchConfirmMarkAllAsRead(_ sender: Any) {
|
||||
if confirmMarkAllAsReadSwitch.isOn {
|
||||
AppDefaults.confirmMarkAllAsRead = true
|
||||
} else {
|
||||
AppDefaults.confirmMarkAllAsRead = false
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func switchFullscreenArticles(_ sender: Any) {
|
||||
if showFullscreenArticlesSwitch.isOn {
|
||||
AppDefaults.articleFullscreenEnabled = true
|
||||
|
|
|
@ -112,21 +112,19 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
|||
|
||||
let feedName = contentText.isEmpty ? nil : contentText
|
||||
|
||||
account!.createWebFeed(url: url!.absoluteString, name: feedName, container: container!) { result in
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
account!.save()
|
||||
AccountManager.shared.suspendNetworkAll()
|
||||
AccountManager.shared.suspendDatabaseAll()
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
case .failure(let error):
|
||||
self.presentError(error) {
|
||||
self.extensionContext!.cancelRequest(withError: error)
|
||||
ProcessInfo.processInfo.performExpiringActivity(withReason: "Adding web feed to account.") { expired in
|
||||
guard !expired else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
account!.createWebFeed(url: self.url!.absoluteString, name: feedName, container: self.container!) { result in
|
||||
account!.save()
|
||||
AccountManager.shared.suspendNetworkAll()
|
||||
AccountManager.shared.suspendDatabaseAll()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
|
||||
func shareFolderPickerDidSelect(_ container: Container) {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// TitleActivityItemSource.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Martin Hartl on 01/11/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class TitleActivityItemSource: NSObject, UIActivityItemSource {
|
||||
|
||||
private let title: String?
|
||||
|
||||
init(title: String?) {
|
||||
self.title = title
|
||||
}
|
||||
|
||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||
return title as Any
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||
guard let activityType = activityType,
|
||||
let title = title else {
|
||||
return NSNull()
|
||||
}
|
||||
|
||||
switch activityType.rawValue {
|
||||
case "com.omnigroup.OmniFocus3.iOS.QuickEntry",
|
||||
"com.culturedcode.ThingsiPhone.ShareExtension":
|
||||
return title
|
||||
default:
|
||||
return NSNull()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// ShareArticleActivityViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Martin Hartl on 01/11/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIActivityViewController {
|
||||
convenience init(url: URL, title: String?, applicationActivities: [UIActivity]?) {
|
||||
let itemSource = ArticleActivityItemSource(url: url, subject: title)
|
||||
let titleSource = TitleActivityItemSource(title: title)
|
||||
|
||||
self.init(activityItems: [titleSource, itemSource], applicationActivities: applicationActivities)
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
Subproject commit a23d10cbd7adce19841300ad9e5d57c54ea150f6
|
||||
Subproject commit 806d1fdc16511a9469bf2a18348caf9f20ff8cdc
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
// High Level Settings common to both the iOS application and any extensions we bundle with it
|
||||
MARKETING_VERSION = 5.0
|
||||
CURRENT_PROJECT_VERSION = 24
|
||||
CURRENT_PROJECT_VERSION = 27
|
||||
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
||||
|
|
Loading…
Reference in New Issue