Implement Feed protocol.

This commit is contained in:
Maurice Parker 2019-11-15 06:19:14 -06:00
parent 3fb1a3b8cc
commit 5283d2efbe
19 changed files with 119 additions and 107 deletions

View File

@ -12,7 +12,7 @@
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; }; 5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; };
510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD110232C3801002692E4 /* AccountMetadataFile.swift */; }; 510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD110232C3801002692E4 /* AccountMetadataFile.swift */; };
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */; }; 510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */; };
511B9804237CD4270028BCAA /* ArticleFetcherType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9803237CD4270028BCAA /* ArticleFetcherType.swift */; }; 511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9803237CD4270028BCAA /* FeedIdentifier.swift */; };
513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */; }; 513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */; };
5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; }; 5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; };
5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; }; 5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; };
@ -32,6 +32,7 @@
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */; }; 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */; };
5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; }; 5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; };
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; }; 51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; };
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; };
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; }; 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; }; 51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; }; 51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; };
@ -212,7 +213,7 @@
5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; }; 5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; };
510BD110232C3801002692E4 /* AccountMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadataFile.swift; sourceTree = "<group>"; }; 510BD110232C3801002692E4 /* AccountMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadataFile.swift; sourceTree = "<group>"; };
510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedMetadataFile.swift; sourceTree = "<group>"; }; 510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedMetadataFile.swift; sourceTree = "<group>"; };
511B9803237CD4270028BCAA /* ArticleFetcherType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleFetcherType.swift; sourceTree = "<group>"; }; 511B9803237CD4270028BCAA /* FeedIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIdentifier.swift; sourceTree = "<group>"; };
513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinSyncTest.swift; sourceTree = "<group>"; }; 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinSyncTest.swift; sourceTree = "<group>"; };
513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = "<group>"; }; 513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = "<group>"; };
5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = "<group>"; }; 5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = "<group>"; };
@ -233,6 +234,7 @@
5170743B232AEDB500A461A3 /* OPMLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLFile.swift; sourceTree = "<group>"; }; 5170743B232AEDB500A461A3 /* OPMLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLFile.swift; sourceTree = "<group>"; };
518B2EA52351306200400001 /* Account_project_test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_test.xcconfig; sourceTree = "<group>"; }; 518B2EA52351306200400001 /* Account_project_test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_test.xcconfig; sourceTree = "<group>"; };
51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = "<group>"; }; 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = "<group>"; };
51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; }; 51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; }; 51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; }; 51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; };
@ -531,16 +533,17 @@
84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */, 84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */,
510BD110232C3801002692E4 /* AccountMetadataFile.swift */, 510BD110232C3801002692E4 /* AccountMetadataFile.swift */,
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */, 84F73CF0202788D80000BCEF /* ArticleFetcher.swift */,
511B9803237CD4270028BCAA /* ArticleFetcherType.swift */,
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */, 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */,
8419740D1F6DD25F006346C4 /* Container.swift */, 8419740D1F6DD25F006346C4 /* Container.swift */,
84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */, 84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */,
84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */, 84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */,
51BC8FCB237EC055004F8B56 /* Feed.swift */,
511B9803237CD4270028BCAA /* FeedIdentifier.swift */,
841974001F6DD1EC006346C4 /* Folder.swift */,
844B297E210CE37E004020B3 /* UnreadCountProvider.swift */,
844B297C2106C7EC004020B3 /* WebFeed.swift */, 844B297C2106C7EC004020B3 /* WebFeed.swift */,
84B2D4CE2238C13D00498ADA /* WebFeedMetadata.swift */, 84B2D4CE2238C13D00498ADA /* WebFeedMetadata.swift */,
510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */, 510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */,
841974001F6DD1EC006346C4 /* Folder.swift */,
844B297E210CE37E004020B3 /* UnreadCountProvider.swift */,
5165D71F22835E9800D9D53D /* FeedFinder */, 5165D71F22835E9800D9D53D /* FeedFinder */,
515E4EB12324FF7D0057B0E7 /* Credentials */, 515E4EB12324FF7D0057B0E7 /* Credentials */,
8419742B1F6DDE84006346C4 /* LocalAccount */, 8419742B1F6DDE84006346C4 /* LocalAccount */,
@ -966,7 +969,7 @@
9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */, 9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */,
552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */, 552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */,
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */, 9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */,
511B9804237CD4270028BCAA /* ArticleFetcherType.swift in Sources */, 511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */,
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */, 84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */, 9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */,
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */, 841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
@ -1002,6 +1005,7 @@
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */, 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */, 84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
9E510D6E234F16A8002E6F1A /* FeedlyAddFeedRequest.swift in Sources */, 9E510D6E234F16A8002E6F1A /* FeedlyAddFeedRequest.swift in Sources */,
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */,
846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */, 846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */,
55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */, 55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */,
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */, 9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */,

View File

@ -11,8 +11,6 @@ import Articles
public protocol ArticleFetcher { public protocol ArticleFetcher {
var articleFetcherType: ArticleFetcherType? { get }
func fetchArticles() -> Set<Article> func fetchArticles() -> Set<Article>
func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock)
func fetchUnreadArticles() -> Set<Article> func fetchUnreadArticles() -> Set<Article>
@ -21,14 +19,6 @@ public protocol ArticleFetcher {
extension WebFeed: ArticleFetcher { extension WebFeed: ArticleFetcher {
public var articleFetcherType: ArticleFetcherType? {
guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.")
return nil
}
return ArticleFetcherType.webFeed(accountID, webFeedID)
}
public func fetchArticles() -> Set<Article> { public func fetchArticles() -> Set<Article> {
return account?.fetchArticles(.webFeed(self)) ?? Set<Article>() return account?.fetchArticles(.webFeed(self)) ?? Set<Article>()
} }
@ -58,14 +48,6 @@ extension WebFeed: ArticleFetcher {
extension Folder: ArticleFetcher { extension Folder: ArticleFetcher {
public var articleFetcherType: ArticleFetcherType? {
guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.")
return nil
}
return ArticleFetcherType.folder(accountID, nameForDisplay)
}
public func fetchArticles() -> Set<Article> { public func fetchArticles() -> Set<Article> {
return fetchUnreadArticles() return fetchUnreadArticles()
} }

View File

@ -0,0 +1,14 @@
//
// Feed.swift
// Account
//
// Created by Maurice Parker on 11/15/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider {
}

View File

@ -8,7 +8,11 @@
import Foundation import Foundation
public enum ArticleFetcherType: CustomStringConvertible { public protocol FeedIdentifiable {
var feedID: FeedIdentifier? { get }
}
public enum FeedIdentifier: CustomStringConvertible {
case smartFeed(String) // String is a unique identifier case smartFeed(String) // String is a unique identifier
case script(String) // String is a unique identifier case script(String) // String is a unique identifier
@ -61,16 +65,16 @@ public enum ArticleFetcherType: CustomStringConvertible {
switch type { switch type {
case "smartFeed": case "smartFeed":
guard let id = userInfo["id"] as? String else { return nil } guard let id = userInfo["id"] as? String else { return nil }
self = ArticleFetcherType.smartFeed(id) self = FeedIdentifier.smartFeed(id)
case "script": case "script":
guard let id = userInfo["id"] as? String else { return nil } guard let id = userInfo["id"] as? String else { return nil }
self = ArticleFetcherType.script(id) self = FeedIdentifier.script(id)
case "feed": case "feed":
guard let accountID = userInfo["accountID"] as? String, let webFeedID = userInfo["webFeedID"] as? String else { return nil } guard let accountID = userInfo["accountID"] as? String, let webFeedID = userInfo["webFeedID"] as? String else { return nil }
self = ArticleFetcherType.webFeed(accountID, webFeedID) self = FeedIdentifier.webFeed(accountID, webFeedID)
case "folder": case "folder":
guard let accountID = userInfo["accountID"] as? String, let folderName = userInfo["folderName"] as? String else { return nil } guard let accountID = userInfo["accountID"] as? String, let folderName = userInfo["folderName"] as? String else { return nil }
self = ArticleFetcherType.folder(accountID, folderName) self = FeedIdentifier.folder(accountID, folderName)
default: default:
return nil return nil
} }

View File

@ -10,7 +10,15 @@ import Foundation
import Articles import Articles
import RSCore import RSCore
public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCountProvider, Hashable { public final class Folder: Feed, Renamable, Container, Hashable {
public var feedID: FeedIdentifier? {
guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.")
return nil
}
return FeedIdentifier.folder(accountID, nameForDisplay)
}
public weak var account: Account? public weak var account: Account?
public var topLevelWebFeeds: Set<WebFeed> = Set<WebFeed>() public var topLevelWebFeeds: Set<WebFeed> = Set<WebFeed>()

View File

@ -11,7 +11,15 @@ import RSCore
import RSWeb import RSWeb
import Articles import Articles
public final class WebFeed: DisplayNameProvider, Renamable, UnreadCountProvider, Hashable { public final class WebFeed: Feed, Renamable, Hashable {
public var feedID: FeedIdentifier? {
guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.")
return nil
}
return FeedIdentifier.webFeed(accountID, webFeedID)
}
public weak var account: Account? public weak var account: Account?
public let url: String public let url: String

View File

@ -481,7 +481,7 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
let detailState: DetailState let detailState: DetailState
if let articles = articles { if let articles = articles {
if articles.count == 1 { if articles.count == 1 {
activityManager.reading(fetcher: nil, article: articles.first) activityManager.reading(feed: nil, article: articles.first)
if articles.first?.webFeed?.isArticleExtractorAlwaysOn ?? false { if articles.first?.webFeed?.isArticleExtractorAlwaysOn ?? false {
detailState = .loading detailState = .loading
startArticleExtractorForCurrentLink() startArticleExtractorForCurrentLink()

View File

@ -38,13 +38,13 @@ class ActivityManager {
invalidateNextUnread() invalidateNextUnread()
} }
func selecting(fetcher: ArticleFetcher) { func selecting(feed: Feed) {
invalidateCurrentActivities() invalidateCurrentActivities()
selectingActivity = makeSelectFeedActivity(fetcher: fetcher) selectingActivity = makeSelectFeedActivity(feed: feed)
if let feed = fetcher as? WebFeed { if let webFeed = feed as? WebFeed {
updateSelectingActivityFeedSearchAttributes(with: feed) updateSelectingActivityFeedSearchAttributes(with: webFeed)
} }
donate(selectingActivity!) donate(selectingActivity!)
@ -76,12 +76,12 @@ class ActivityManager {
nextUnreadActivity = nil nextUnreadActivity = nil
} }
func reading(fetcher: ArticleFetcher?, article: Article?) { func reading(feed: Feed?, article: Article?) {
invalidateReading() invalidateReading()
invalidateNextUnread() invalidateNextUnread()
guard let article = article else { return } guard let article = article else { return }
readingActivity = makeReadArticleActivity(fetcher: fetcher, article: article) readingActivity = makeReadArticleActivity(feed: feed, article: article)
#if os(iOS) #if os(iOS)
updateReadArticleSearchAttributes(with: article) updateReadArticleSearchAttributes(with: article)
@ -151,37 +151,36 @@ class ActivityManager {
private extension ActivityManager { private extension ActivityManager {
func makeSelectFeedActivity(fetcher: ArticleFetcher) -> NSUserActivity { func makeSelectFeedActivity(feed: Feed) -> NSUserActivity {
let activity = NSUserActivity(activityType: ActivityType.selectFeed.rawValue) let activity = NSUserActivity(activityType: ActivityType.selectFeed.rawValue)
let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder") let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder")
let displayName = (fetcher as? DisplayNameProvider)?.nameForDisplay ?? "" let title = NSString.localizedStringWithFormat(localizedText as NSString, feed.nameForDisplay) as String
let title = NSString.localizedStringWithFormat(localizedText as NSString, displayName) as String
activity.title = title activity.title = title
activity.keywords = Set(makeKeywords(title)) activity.keywords = Set(makeKeywords(title))
activity.isEligibleForSearch = true activity.isEligibleForSearch = true
let articleFetcherIdentifierUserInfo = fetcher.articleFetcherType?.userInfo ?? [AnyHashable: Any]() let articleFetcherIdentifierUserInfo = feed.feedID?.userInfo ?? [AnyHashable: Any]()
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo] activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo]
activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String }) activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String })
#if os(iOS) #if os(iOS)
activity.suggestedInvocationPhrase = title activity.suggestedInvocationPhrase = title
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.persistentIdentifier = fetcher.articleFetcherType?.description ?? "" activity.persistentIdentifier = feed.feedID?.description ?? ""
activity.contentAttributeSet?.relatedUniqueIdentifier = fetcher.articleFetcherType?.description ?? "" activity.contentAttributeSet?.relatedUniqueIdentifier = feed.feedID?.description ?? ""
#endif #endif
return activity return activity
} }
func makeReadArticleActivity(fetcher: ArticleFetcher?, article: Article) -> NSUserActivity { func makeReadArticleActivity(feed: Feed?, article: Article) -> NSUserActivity {
let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue) let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue)
activity.title = ArticleStringFormatter.truncatedTitle(article) activity.title = ArticleStringFormatter.truncatedTitle(article)
if let fetcher = fetcher { if let feed = feed {
let articleFetcherIdentifierUserInfo = fetcher.articleFetcherType?.userInfo ?? [AnyHashable: Any]() let articleFetcherIdentifierUserInfo = feed.feedID?.userInfo ?? [AnyHashable: Any]()
let articlePathUserInfo = article.pathUserInfo let articlePathUserInfo = article.pathUserInfo
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo, UserInfoKey.articlePath: articlePathUserInfo] activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo, UserInfoKey.articlePath: articlePathUserInfo]
} else { } else {

View File

@ -13,12 +13,11 @@ import Articles
import Account import Account
import RSCore import RSCore
protocol PseudoFeed: class, DisplayNameProvider, UnreadCountProvider, SmallIconProvider, PasteboardWriterOwner { protocol PseudoFeed: class, Feed, SmallIconProvider, PasteboardWriterOwner {
} }
private var smartFeedIcon: RSImage = { private var smartFeedIcon: RSImage = {
return RSImage(named: NSImage.smartBadgeTemplateName)! return RSImage(named: NSImage.smartBadgeTemplateName)!
}() }()
@ -35,7 +34,7 @@ import Articles
import Account import Account
import RSCore import RSCore
protocol PseudoFeed: class, DisplayNameProvider, UnreadCountProvider, SmallIconProvider { protocol PseudoFeed: class, Feed, SmallIconProvider {
} }

View File

@ -13,8 +13,8 @@ import Articles
struct SearchFeedDelegate: SmartFeedDelegate { struct SearchFeedDelegate: SmartFeedDelegate {
var articleFetcherType: ArticleFetcherType? { var feedID: FeedIdentifier? {
return ArticleFetcherType.smartFeed(String(describing: SearchFeedDelegate.self)) return FeedIdentifier.smartFeed(String(describing: SearchFeedDelegate.self))
} }
var nameForDisplay: String { var nameForDisplay: String {

View File

@ -13,8 +13,8 @@ import Articles
struct SearchTimelineFeedDelegate: SmartFeedDelegate { struct SearchTimelineFeedDelegate: SmartFeedDelegate {
var articleFetcherType: ArticleFetcherType? { var feedID: FeedIdentifier? {
return ArticleFetcherType.smartFeed(String(describing: SearchTimelineFeedDelegate.self)) return FeedIdentifier.smartFeed(String(describing: SearchTimelineFeedDelegate.self))
} }
var nameForDisplay: String { var nameForDisplay: String {

View File

@ -13,6 +13,10 @@ import Account
final class SmartFeed: PseudoFeed { final class SmartFeed: PseudoFeed {
var feedID: FeedIdentifier? {
delegate.feedID
}
var nameForDisplay: String { var nameForDisplay: String {
return delegate.nameForDisplay return delegate.nameForDisplay
} }
@ -72,10 +76,6 @@ final class SmartFeed: PseudoFeed {
extension SmartFeed: ArticleFetcher { extension SmartFeed: ArticleFetcher {
var articleFetcherType: ArticleFetcherType? {
delegate.articleFetcherType
}
func fetchArticles() -> Set<Article> { func fetchArticles() -> Set<Article> {
return delegate.fetchArticles() return delegate.fetchArticles()
} }

View File

@ -11,10 +11,8 @@ import Account
import Articles import Articles
import RSCore import RSCore
protocol SmartFeedDelegate: DisplayNameProvider, ArticleFetcher, SmallIconProvider { protocol SmartFeedDelegate: FeedIdentifiable, DisplayNameProvider, ArticleFetcher, SmallIconProvider {
var fetchType: FetchType { get } var fetchType: FetchType { get }
func fetchUnreadCount(for: Account, callback: @escaping (Int) -> Void) func fetchUnreadCount(for: Account, callback: @escaping (Int) -> Void)
} }

View File

@ -15,8 +15,8 @@ import Account
struct StarredFeedDelegate: SmartFeedDelegate { struct StarredFeedDelegate: SmartFeedDelegate {
var articleFetcherType: ArticleFetcherType? { var feedID: FeedIdentifier? {
return ArticleFetcherType.smartFeed(String(describing: StarredFeedDelegate.self)) return FeedIdentifier.smartFeed(String(describing: StarredFeedDelegate.self))
} }
let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title") let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title")

View File

@ -13,8 +13,8 @@ import Account
struct TodayFeedDelegate: SmartFeedDelegate { struct TodayFeedDelegate: SmartFeedDelegate {
var articleFetcherType: ArticleFetcherType? { var feedID: FeedIdentifier? {
return ArticleFetcherType.smartFeed(String(describing: TodayFeedDelegate.self)) return FeedIdentifier.smartFeed(String(describing: TodayFeedDelegate.self))
} }
let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title") let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title")

View File

@ -19,6 +19,10 @@ import Articles
final class UnreadFeed: PseudoFeed { final class UnreadFeed: PseudoFeed {
var feedID: FeedIdentifier? {
return FeedIdentifier.smartFeed(String(describing: UnreadFeed.self))
}
let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title") let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title")
let fetchType = FetchType.unread let fetchType = FetchType.unread
@ -53,10 +57,6 @@ final class UnreadFeed: PseudoFeed {
extension UnreadFeed: ArticleFetcher { extension UnreadFeed: ArticleFetcher {
var articleFetcherType: ArticleFetcherType? {
return ArticleFetcherType.smartFeed(String(describing: UnreadFeed.self))
}
func fetchArticles() -> Set<Article> { func fetchArticles() -> Set<Article> {
return fetchUnreadArticles() return fetchUnreadArticles()
} }

View File

@ -93,7 +93,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
} }
var node: Node? = nil var node: Node? = nil
if let coordinator = representedObject as? SceneCoordinator, let fetcher = coordinator.timelineFetcher { if let coordinator = representedObject as? SceneCoordinator, let fetcher = coordinator.timelineFeed {
node = coordinator.rootNode.descendantNodeRepresentingObject(fetcher as AnyObject) node = coordinator.rootNode.descendantNodeRepresentingObject(fetcher as AnyObject)
} else { } else {
node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject) node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject)

View File

@ -368,7 +368,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
} }
@objc func displayNameDidChange(_ note: Notification) { @objc func displayNameDidChange(_ note: Notification) {
titleView?.label.text = coordinator.timelineName titleView?.label.text = coordinator.timelineFeed?.nameForDisplay
} }
@objc func scrollPositionDidChange() { @objc func scrollPositionDidChange() {
@ -455,16 +455,16 @@ extension MasterTimelineViewController: UISearchBarDelegate {
private extension MasterTimelineViewController { private extension MasterTimelineViewController {
func resetUI() { func resetUI() {
title = coordinator.timelineName title = coordinator.timelineFeed?.nameForDisplay
if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView { if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView {
self.titleView = titleView self.titleView = titleView
titleView.iconView.iconImage = coordinator.timelineIconImage titleView.iconView.iconImage = coordinator.timelineIconImage
titleView.label.text = coordinator.timelineName titleView.label.text = coordinator.timelineFeed?.nameForDisplay
updateTitleUnreadCount() updateTitleUnreadCount()
if coordinator.timelineFetcher is WebFeed { if coordinator.timelineFeed is WebFeed {
titleView.heightAnchor.constraint(equalToConstant: 44.0).isActive = true titleView.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
let tap = UITapGestureRecognizer(target: self, action:#selector(showFeedInspector(_:))) let tap = UITapGestureRecognizer(target: self, action:#selector(showFeedInspector(_:)))
titleView.addGestureRecognizer(tap) titleView.addGestureRecognizer(tap)
@ -494,7 +494,7 @@ private extension MasterTimelineViewController {
} }
func updateTitleUnreadCount() { func updateTitleUnreadCount() {
if let unreadCountProvider = coordinator.timelineFetcher as? UnreadCountProvider { if let unreadCountProvider = coordinator.timelineFeed as? UnreadCountProvider {
self.titleView?.unreadCountView.unreadCount = unreadCountProvider.unreadCount self.titleView?.unreadCountView.unreadCount = unreadCountProvider.unreadCount
} }
} }

View File

@ -110,7 +110,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
private(set) var currentFeedIndexPath: IndexPath? private(set) var currentFeedIndexPath: IndexPath?
var timelineIconImage: IconImage? { var timelineIconImage: IconImage? {
if let feed = timelineFetcher as? WebFeed { if let feed = timelineFeed as? WebFeed {
let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: feed) let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: feed)
if feedIconImage != nil { if feedIconImage != nil {
@ -123,19 +123,15 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
return (timelineFetcher as? SmallIconProvider)?.smallIcon return (timelineFeed as? SmallIconProvider)?.smallIcon
} }
var timelineName: String? { var timelineFeed: Feed? {
return (timelineFetcher as? DisplayNameProvider)?.nameForDisplay
}
var timelineFetcher: ArticleFetcher? {
didSet { didSet {
timelineMiddleIndexPath = nil timelineMiddleIndexPath = nil
if timelineFetcher is WebFeed { if timelineFeed is WebFeed {
showFeedNames = false showFeedNames = false
} else { } else {
showFeedNames = true showFeedNames = true
@ -259,7 +255,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
var isTimelineUnreadAvailable: Bool { var isTimelineUnreadAvailable: Bool {
if let unreadProvider = timelineFetcher as? UnreadCountProvider { if let unreadProvider = timelineFeed as? UnreadCountProvider {
return unreadProvider.unreadCount > 0 return unreadProvider.unreadCount > 0
} }
return false return false
@ -519,7 +515,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
func masterFeedIndexPathForCurrentTimeline() -> IndexPath? { func masterFeedIndexPathForCurrentTimeline() -> IndexPath? {
guard let node = treeController.rootNode.descendantNodeRepresentingObject(timelineFetcher as AnyObject) else { guard let node = treeController.rootNode.descendantNodeRepresentingObject(timelineFeed as AnyObject) else {
return nil return nil
} }
return indexPathFor(node) return indexPathFor(node)
@ -533,12 +529,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
masterFeedViewController.updateFeedSelection(animated: animated) masterFeedViewController.updateFeedSelection(animated: animated)
if let ip = indexPath, let node = nodeFor(ip), let fetcher = node.representedObject as? ArticleFetcher { if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed {
timelineFetcher = fetcher timelineFeed = feed
activityManager.selecting(fetcher: fetcher) activityManager.selecting(feed: feed)
installTimelineControllerIfNecessary(animated: animated) installTimelineControllerIfNecessary(animated: animated)
} else { } else {
timelineFetcher = nil timelineFeed = nil
activityManager.invalidateSelecting() activityManager.invalidateSelecting()
if rootSplitViewController.isCollapsed && navControllerForTimeline().viewControllers.last is MasterTimelineViewController { if rootSplitViewController.isCollapsed && navControllerForTimeline().viewControllers.last is MasterTimelineViewController {
navControllerForTimeline().popViewController(animated: animated) navControllerForTimeline().popViewController(animated: animated)
@ -582,7 +578,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
stopArticleExtractor() stopArticleExtractor()
currentArticle = article currentArticle = article
activityManager.reading(fetcher: timelineFetcher, article: article) activityManager.reading(feed: timelineFeed, article: article)
if article == nil { if article == nil {
if rootSplitViewController.isCollapsed { if rootSplitViewController.isCollapsed {
@ -621,7 +617,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
func beginSearching() { func beginSearching() {
isSearching = true isSearching = true
searchArticleIds = Set(articles.map { $0.articleID }) searchArticleIds = Set(articles.map { $0.articleID })
timelineFetcher = nil timelineFeed = nil
} }
func endSearching() { func endSearching() {
@ -630,10 +626,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
lastSearchScope = nil lastSearchScope = nil
searchArticleIds = nil searchArticleIds = nil
if let ip = currentFeedIndexPath, let node = nodeFor(ip), let fetcher = node.representedObject as? ArticleFetcher { if let ip = currentFeedIndexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed {
timelineFetcher = fetcher timelineFeed = feed
} else { } else {
timelineFetcher = nil timelineFeed = nil
} }
selectArticle(nil) selectArticle(nil)
@ -644,7 +640,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
guard isSearching else { return } guard isSearching else { return }
if searchString.count < 3 { if searchString.count < 3 {
timelineFetcher = nil timelineFeed = nil
return return
} }
@ -652,9 +648,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
switch searchScope { switch searchScope {
case .global: case .global:
timelineFetcher = SmartFeed(delegate: SearchFeedDelegate(searchString: searchString)) timelineFeed = SmartFeed(delegate: SearchFeedDelegate(searchString: searchString))
case .timeline: case .timeline:
timelineFetcher = SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: searchArticleIds!)) timelineFeed = SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: searchArticleIds!))
} }
lastSearchString = searchString lastSearchString = searchString
@ -806,7 +802,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
func showFeedInspector() { func showFeedInspector() {
guard let feed = timelineFetcher as? WebFeed else { guard let feed = timelineFeed as? WebFeed else {
return return
} }
showFeedInspector(for: feed) showFeedInspector(for: feed)
@ -1362,11 +1358,11 @@ private extension SceneCoordinator {
@objc func fetchAndMergeArticles() { @objc func fetchAndMergeArticles() {
guard let timelineFetcher = timelineFetcher else { guard let timelineFeed = timelineFeed else {
return return
} }
fetchUnsortedArticlesAsync(for: [timelineFetcher]) { [weak self] (unsortedArticles) in fetchUnsortedArticlesAsync(for: [timelineFeed]) { [weak self] (unsortedArticles) in
// Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles. // Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles.
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -1395,7 +1391,7 @@ private extension SceneCoordinator {
// so that the entire display refreshes at once. // so that the entire display refreshes at once.
// Its a better user experience this way. // Its a better user experience this way.
cancelPendingAsyncFetches() cancelPendingAsyncFetches()
guard let timelineFetcher = timelineFetcher else { guard let timelineFetcher = timelineFeed else {
emptyTheTimeline() emptyTheTimeline()
return return
} }
@ -1407,7 +1403,7 @@ private extension SceneCoordinator {
// To be called when we need to do an entire fetch, but an async delay is okay. // To be called when we need to do an entire fetch, but an async delay is okay.
// Example: we have the Today feed selected, and the calendar day just changed. // Example: we have the Today feed selected, and the calendar day just changed.
cancelPendingAsyncFetches() cancelPendingAsyncFetches()
guard let timelineFetcher = timelineFetcher else { guard let timelineFetcher = timelineFeed else {
emptyTheTimeline() emptyTheTimeline()
return return
} }
@ -1447,14 +1443,14 @@ private extension SceneCoordinator {
} }
func timelineFetcherContainsAnyPseudoFeed() -> Bool { func timelineFetcherContainsAnyPseudoFeed() -> Bool {
if timelineFetcher is PseudoFeed { if timelineFeed is PseudoFeed {
return true return true
} }
return false return false
} }
func timelineFetcherContainsAnyFolder() -> Bool { func timelineFetcherContainsAnyFolder() -> Bool {
if timelineFetcher is Folder { if timelineFeed is Folder {
return true return true
} }
return false return false
@ -1464,13 +1460,13 @@ private extension SceneCoordinator {
// Return true if theres a match or if a folder contains (recursively) one of feeds // Return true if theres a match or if a folder contains (recursively) one of feeds
if let feed = timelineFetcher as? WebFeed { if let feed = timelineFeed as? WebFeed {
for oneFeed in feeds { for oneFeed in feeds {
if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url { if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url {
return true return true
} }
} }
} else if let folder = timelineFetcher as? Folder { } else if let folder = timelineFeed as? Folder {
for oneFeed in feeds { for oneFeed in feeds {
if folder.hasWebFeed(with: oneFeed.webFeedID) || folder.hasWebFeed(withURL: oneFeed.url) { if folder.hasWebFeed(with: oneFeed.webFeedID) || folder.hasWebFeed(withURL: oneFeed.url) {
return true return true
@ -1625,7 +1621,7 @@ private extension SceneCoordinator {
func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) { func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) {
guard let userInfo = userInfo, guard let userInfo = userInfo,
let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any], let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any],
let articleFetcherType = ArticleFetcherType(userInfo: feedIdentifierUserInfo) else { let articleFetcherType = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
return return
} }