Add ArticleFetcherType and change state restoration to use new type.

This commit is contained in:
Maurice Parker 2019-11-14 15:06:32 -06:00
parent 48fef65bc4
commit 0d66259650
22 changed files with 290 additions and 230 deletions

View File

@ -12,6 +12,7 @@
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; };
510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD110232C3801002692E4 /* AccountMetadataFile.swift */; };
510BD113232C3E9D002692E4 /* FeedMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */; };
511B9804237CD4270028BCAA /* ArticleFetcherType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9803237CD4270028BCAA /* ArticleFetcherType.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 */; };
5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; };
@ -41,7 +42,6 @@
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E490352288C37100C791F0 /* FeedbinDate.swift */; };
51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; };
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; };
51FE1008234635A20056195D /* DeepLinkProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE1007234635A20056195D /* DeepLinkProvider.swift */; };
552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */; };
552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */; };
552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */; };
@ -210,6 +210,7 @@
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>"; };
510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedMetadataFile.swift; sourceTree = "<group>"; };
511B9803237CD4270028BCAA /* ArticleFetcherType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleFetcherType.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>"; };
5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = "<group>"; };
@ -240,7 +241,6 @@
51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = "<group>"; };
51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = "<group>"; };
51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = "<group>"; };
51FE1007234635A20056195D /* DeepLinkProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkProvider.swift; sourceTree = "<group>"; };
552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIEntry.swift; sourceTree = "<group>"; };
552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPISubscription.swift; sourceTree = "<group>"; };
552032F0229D5D5A009559E0 /* ReaderAPITag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITag.swift; sourceTree = "<group>"; };
@ -527,6 +527,7 @@
84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */,
510BD110232C3801002692E4 /* AccountMetadataFile.swift */,
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */,
511B9803237CD4270028BCAA /* ArticleFetcherType.swift */,
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */,
8419740D1F6DD25F006346C4 /* Container.swift */,
84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */,
@ -536,7 +537,6 @@
510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */,
841974001F6DD1EC006346C4 /* Folder.swift */,
844B297E210CE37E004020B3 /* UnreadCountProvider.swift */,
51FE1007234635A20056195D /* DeepLinkProvider.swift */,
5165D71F22835E9800D9D53D /* FeedFinder */,
515E4EB12324FF7D0057B0E7 /* Credentials */,
8419742B1F6DDE84006346C4 /* LocalAccount */,
@ -960,6 +960,7 @@
9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */,
552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */,
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */,
511B9804237CD4270028BCAA /* ArticleFetcherType.swift in Sources */,
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */,
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
@ -1013,7 +1014,6 @@
9E1773D923458D590056A5A8 /* FeedlyResourceId.swift in Sources */,
9EE4CCFA234F106600FBAE4B /* FeedlyFeedContainerValidator.swift in Sources */,
552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */,
51FE1008234635A20056195D /* DeepLinkProvider.swift in Sources */,
9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */,
9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */,
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */,

View File

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

View File

@ -0,0 +1,79 @@
//
// ArticleFetcherType.swift
// Account
//
// Created by Maurice Parker on 11/13/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public enum ArticleFetcherType: CustomStringConvertible {
case smartFeed(String) // String is a unique identifier
case script(String) // String is a unique identifier
case feed(String, String) // accountID, feedID
case folder(String, String) // accountID, folderName
public var description: String {
switch self {
case .smartFeed(let id):
return "smartFeed: \(id)"
case .script(let id):
return "script: \(id)"
case .feed(let accountID, let feedID):
return "feed: \(accountID)_\(feedID)"
case .folder(let accountID, let folderName):
return "folder: \(accountID)_\(folderName)"
}
}
public var userInfo: [AnyHashable: Any] {
switch self {
case .smartFeed(let id):
return [
"type": "smartFeed",
"id": id
]
case .script(let id):
return [
"type": "script",
"id": id
]
case .feed(let accountID, let feedID):
return [
"type": "feed",
"accountID": accountID,
"feedID": feedID
]
case .folder(let accountID, let folderName):
return [
"type": "folder",
"accountID": accountID,
"folderName": folderName
]
}
}
public init?(userInfo: [AnyHashable: Any]) {
guard let type = userInfo["type"] as? String else { return nil }
switch type {
case "smartFeed":
guard let id = userInfo["id"] as? String else { return nil }
self = ArticleFetcherType.smartFeed(id)
case "script":
guard let id = userInfo["id"] as? String else { return nil }
self = ArticleFetcherType.script(id)
case "feed":
guard let accountID = userInfo["accountID"] as? String, let feedID = userInfo["feedID"] as? String else { return nil }
self = ArticleFetcherType.feed(accountID, feedID)
case "folder":
guard let accountID = userInfo["accountID"] as? String, let folderName = userInfo["folderName"] as? String else { return nil }
self = ArticleFetcherType.folder(accountID, folderName)
default:
return nil
}
}
}

View File

@ -1,21 +0,0 @@
//
// DeepLinkProvider.swift
// Account
//
// Created by Maurice Parker on 10/3/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public enum DeepLinkKey: String {
case accountID = "accountID"
case accountName = "accountName"
case feedID = "feedID"
case articleID = "articleID"
case folderName = "folderName"
}
public protocol DeepLinkProvider {
var deepLinkUserInfo: [AnyHashable : Any] { get }
}

View File

@ -11,7 +11,7 @@ import RSCore
import RSWeb
import Articles
public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, DeepLinkProvider, Hashable {
public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Hashable {
public weak var account: Account?
public let url: String
@ -179,16 +179,6 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, De
account.renameFeed(self, to: newName, completion: completion)
}
// MARK: - DeepLinkProvider
public var deepLinkUserInfo: [AnyHashable : Any] {
return [
DeepLinkKey.accountID.rawValue: account?.accountID ?? "",
DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "",
DeepLinkKey.feedID.rawValue: feedID
]
}
// MARK: - UnreadCountProvider
public var unreadCount: Int {

View File

@ -10,7 +10,7 @@ import Foundation
import Articles
import RSCore
public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCountProvider, DeepLinkProvider, Hashable {
public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCountProvider, Hashable {
public weak var account: Account?
public var topLevelFeeds: Set<Feed> = Set<Feed>()
@ -33,16 +33,6 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun
return name ?? Folder.untitledName
}
// MARK: - DeepLinkProvider
public var deepLinkUserInfo: [AnyHashable : Any] {
return [
DeepLinkKey.accountID.rawValue: account?.accountID ?? "",
DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "",
DeepLinkKey.folderName.rawValue: nameForDisplay
]
}
// MARK: - UnreadCountProvider
public var unreadCount = 0 {

View File

@ -19,6 +19,8 @@
5110C37D2373A8D100A9C04F /* InspectorIconHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5110C37C2373A8D100A9C04F /* InspectorIconHeaderView.swift */; };
51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; };
5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */; };
511B9806237DCAC90028BCAA /* UserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9805237DCAC90028BCAA /* UserInfoKey.swift */; };
511B9807237DCAC90028BCAA /* UserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9805237DCAC90028BCAA /* UserInfoKey.swift */; };
511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; };
511D43D0231FA62500FB1562 /* TimelineKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */; };
511D43D1231FA62800FB1562 /* SidebarKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */; };
@ -1210,6 +1212,7 @@
51121AA12265430A00BC0EC1 /* NetNewsWire_iOSapp_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSapp_target.xcconfig; sourceTree = "<group>"; };
51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = "<group>"; };
51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = "<group>"; };
511B9805237DCAC90028BCAA /* UserInfoKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoKey.swift; sourceTree = "<group>"; };
511D43EE231FBDE800FB1562 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreenPad.storyboard; sourceTree = "<group>"; };
511D4410231FC02D00FB1562 /* KeyboardManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardManager.swift; sourceTree = "<group>"; };
512363372369155100951F16 /* RoundedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedProgressView.swift; sourceTree = "<group>"; };
@ -2193,8 +2196,8 @@
children = (
849A97731ED9EC04007D329B /* ArticleStringFormatter.swift */,
849A97581ED9EB0D007D329B /* ArticleUtilities.swift */,
84411E701FE5FBFA004B527F /* SmallIconProvider.swift */,
5108F6B52375E612001ABC45 /* CacheCleaner.swift */,
84411E701FE5FBFA004B527F /* SmallIconProvider.swift */,
);
path = Data;
sourceTree = "<group>";
@ -2424,6 +2427,7 @@
841D4D5E2106B3E100DD04E6 /* ArticlesDatabase.xcodeproj */,
51554BFC228B6EB50055115A /* SyncDatabase.xcodeproj */,
842E45CD1ED8C308000A8B52 /* AppNotifications.swift */,
511B9805237DCAC90028BCAA /* UserInfoKey.swift */,
51C452AD2265102800C03939 /* Timeline */,
84702AB31FA27AE8006B8943 /* Commands */,
51934CCC231078DC006127BE /* Activity */,
@ -3881,6 +3885,7 @@
51C45296226509D300C03939 /* OPMLExporter.swift in Sources */,
51C45291226509C800C03939 /* SmartFeed.swift in Sources */,
51C452A722650A3D00C03939 /* RSImage-Extensions.swift in Sources */,
511B9807237DCAC90028BCAA /* UserInfoKey.swift in Sources */,
51C45269226508F600C03939 /* MasterFeedTableViewCell.swift in Sources */,
51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */,
517630232336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift in Sources */,
@ -4074,6 +4079,7 @@
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */,
845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */,
848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */,
511B9806237DCAC90028BCAA /* UserInfoKey.swift in Sources */,
84C9FC7722629E1200D921D6 /* AdvancedPreferencesViewController.swift in Sources */,
849EE72120391F560082A1EA /* SharingServicePickerDelegate.swift in Sources */,
5108F6B62375E612001ABC45 /* CacheCleaner.swift in Sources */,

View File

@ -9,6 +9,7 @@
import Foundation
import CoreSpotlight
import CoreServices
import RSCore
import Account
import Articles
import Intents
@ -37,54 +38,15 @@ class ActivityManager {
invalidateNextUnread()
}
func selectingToday() {
func selecting(fetcher: ArticleFetcher) {
invalidateCurrentActivities()
let title = NSLocalizedString("See articles for “Today”", comment: "Today")
selectingActivity = makeSelectingActivity(type: ActivityType.selectToday, title: title, identifier: "smartfeed.today")
donate(selectingActivity!)
}
selectingActivity = makeSelectFeedActivity(fetcher: fetcher)
func selectingAllUnread() {
invalidateCurrentActivities()
let title = NSLocalizedString("See articles in “All Unread”", comment: "All Unread")
selectingActivity = makeSelectingActivity(type: ActivityType.selectAllUnread, title: title, identifier: "smartfeed.allUnread")
donate(selectingActivity!)
}
func selectingStarred() {
invalidateCurrentActivities()
let title = NSLocalizedString("See articles in “Starred”", comment: "Starred")
selectingActivity = makeSelectingActivity(type: ActivityType.selectStarred, title: title, identifier: "smartfeed.starred")
donate(selectingActivity!)
}
func selectingFolder(_ folder: Folder) {
invalidateCurrentActivities()
let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder")
let title = NSString.localizedStringWithFormat(localizedText as NSString, folder.nameForDisplay) as String
selectingActivity = makeSelectingActivity(type: ActivityType.selectFolder, title: title, identifier: ActivityManager.identifer(for: folder))
let userInfo = folder.deepLinkUserInfo
selectingActivity!.userInfo = userInfo
selectingActivity!.requiredUserInfoKeys = Set(userInfo.keys.map { $0 as! String })
donate(selectingActivity!)
}
func selectingFeed(_ feed: Feed) {
invalidateCurrentActivities()
let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Feed")
let title = NSString.localizedStringWithFormat(localizedText as NSString, feed.nameForDisplay) as String
selectingActivity = makeSelectingActivity(type: ActivityType.selectFeed, title: title, identifier: ActivityManager.identifer(for: feed))
let userInfo = feed.deepLinkUserInfo
selectingActivity!.userInfo = userInfo
selectingActivity!.requiredUserInfoKeys = Set(userInfo.keys.map { $0 as! String })
if let feed = fetcher as? Feed {
updateSelectingActivityFeedSearchAttributes(with: feed)
}
donate(selectingActivity!)
}
@ -95,8 +57,17 @@ class ActivityManager {
func selectingNextUnread() {
guard nextUnreadActivity == nil else { return }
let title = NSLocalizedString("See first unread article", comment: "First Unread")
nextUnreadActivity = makeSelectingActivity(type: ActivityType.nextUnread, title: title, identifier: "action.nextUnread")
nextUnreadActivity = NSUserActivity(activityType: ActivityType.nextUnread.rawValue)
nextUnreadActivity!.title = NSLocalizedString("See first unread article", comment: "First Unread")
#if os(iOS)
nextUnreadActivity!.suggestedInvocationPhrase = nextUnreadActivity!.title
nextUnreadActivity!.isEligibleForPrediction = true
nextUnreadActivity!.persistentIdentifier = "nextUnread:"
nextUnreadActivity!.contentAttributeSet?.relatedUniqueIdentifier = "nextUnread:"
#endif
donate(nextUnreadActivity!)
}
@ -105,12 +76,12 @@ class ActivityManager {
nextUnreadActivity = nil
}
func reading(_ article: Article?) {
func reading(fetcher: ArticleFetcher?, article: Article?) {
invalidateReading()
invalidateNextUnread()
guard let article = article else { return }
readingActivity = makeReadArticleActivity(article)
guard let fetcher = fetcher, let article = article else { return }
readingActivity = makeReadArticleActivity(fetcher: fetcher, article: article)
#if os(iOS)
updateReadArticleSearchAttributes(with: article)
@ -159,7 +130,7 @@ class ActivityManager {
#endif
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[DeepLinkKey.feedID.rawValue] as? String else {
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[ArticlePathKey.feedID] as? String else {
return
}
@ -180,28 +151,40 @@ class ActivityManager {
private extension ActivityManager {
func makeSelectingActivity(type: ActivityType, title: String, identifier: String) -> NSUserActivity {
let activity = NSUserActivity(activityType: type.rawValue)
func makeSelectFeedActivity(fetcher: ArticleFetcher) -> NSUserActivity {
let activity = NSUserActivity(activityType: ActivityType.selectFeed.rawValue)
let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder")
let displayName = (fetcher as? DisplayNameProvider)?.nameForDisplay ?? ""
let title = NSString.localizedStringWithFormat(localizedText as NSString, displayName) as String
activity.title = title
activity.keywords = Set(makeKeywords(title))
activity.isEligibleForSearch = true
let articleFetcherIdentifierUserInfo = fetcher.articleFetcherType?.userInfo ?? [AnyHashable: Any]()
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo]
activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String })
#if os(iOS)
activity.suggestedInvocationPhrase = title
activity.isEligibleForPrediction = true
activity.persistentIdentifier = identifier
activity.contentAttributeSet?.relatedUniqueIdentifier = identifier
activity.persistentIdentifier = fetcher.articleFetcherType?.description ?? ""
activity.contentAttributeSet?.relatedUniqueIdentifier = fetcher.articleFetcherType?.description ?? ""
#endif
return activity
}
func makeReadArticleActivity(_ article: Article) -> NSUserActivity {
func makeReadArticleActivity(fetcher: ArticleFetcher, article: Article) -> NSUserActivity {
let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue)
activity.title = ArticleStringFormatter.truncatedTitle(article)
let userInfo = article.deepLinkUserInfo
activity.userInfo = userInfo
activity.requiredUserInfoKeys = Set(userInfo.keys.map { $0 as! String })
let articleFetcherIdentifierUserInfo = fetcher.articleFetcherType?.userInfo ?? [AnyHashable: Any]()
let articlePathUserInfo = article.pathUserInfo
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo, UserInfoKey.articlePath: articlePathUserInfo]
activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String })
activity.isEligibleForHandoff = true
#if os(iOS)

View File

@ -9,12 +9,8 @@
import Foundation
enum ActivityType: String {
case selectToday = "com.ranchero.NetNewsWire.SelectToday"
case selectAllUnread = "com.ranchero.NetNewsWire.SelectAllUnread"
case selectStarred = "com.ranchero.NetNewsWire.SelectStarred"
case selectFolder = "com.ranchero.NetNewsWire.SelectFolder"
case selectFeed = "com.ranchero.NetNewsWire.SelectFeed"
case nextUnread = "com.ranchero.NetNewsWire.NextUnread"
case readArticle = "com.ranchero.NetNewsWire.ReadArticle"
case selectFeed = "SelectFeed"
case nextUnread = "NextUnread"
case readArticle = "ReadArticle"
case addFeedIntent = "AddFeedIntent"
}

View File

@ -18,18 +18,3 @@ extension Notification.Name {
static let WebInspectorEnabledDidChange = Notification.Name("WebInspectorEnabledDidChange")
#endif
}
typealias UserInfoDictionary = [AnyHashable: Any]
struct UserInfoKey {
static let view = "view"
static let article = "article"
static let articles = "articles"
static let navigationKeyPressed = "navigationKeyPressed"
static let objects = "objects"
static let feed = "feed"
static let url = "url"
static let author = "author"
}

View File

@ -94,16 +94,23 @@ extension Article {
}
}
// MARK: DeepLinkProvider
// MARK: Path
extension Article: DeepLinkProvider {
struct ArticlePathKey {
static let accountID = "accountID"
static let accountName = "accountName"
static let feedID = "feedID"
static let articleID = "articleID"
}
public var deepLinkUserInfo: [AnyHashable : Any] {
extension Article {
public var pathUserInfo: [AnyHashable : Any] {
return [
DeepLinkKey.accountID.rawValue: accountID,
DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "",
DeepLinkKey.feedID.rawValue: feedID,
DeepLinkKey.articleID.rawValue: articleID
ArticlePathKey.accountID: accountID,
ArticlePathKey.accountName: account?.nameForDisplay ?? "",
ArticlePathKey.feedID: feedID,
ArticlePathKey.articleID: articleID
]
}

View File

@ -13,6 +13,10 @@ import Articles
struct SearchFeedDelegate: SmartFeedDelegate {
var articleFetcherType: ArticleFetcherType? {
return ArticleFetcherType.smartFeed(String(describing: SearchFeedDelegate.self))
}
var nameForDisplay: String {
return nameForDisplayPrefix + searchString
}

View File

@ -13,6 +13,10 @@ import Articles
struct SearchTimelineFeedDelegate: SmartFeedDelegate {
var articleFetcherType: ArticleFetcherType? {
return ArticleFetcherType.smartFeed(String(describing: SearchTimelineFeedDelegate.self))
}
var nameForDisplay: String {
return nameForDisplayPrefix + searchString
}

View File

@ -72,6 +72,10 @@ final class SmartFeed: PseudoFeed {
extension SmartFeed: ArticleFetcher {
var articleFetcherType: ArticleFetcherType? {
delegate.articleFetcherType
}
func fetchArticles() -> Set<Article> {
return delegate.fetchArticles()
}

View File

@ -20,7 +20,20 @@ final class SmartFeedsController: DisplayNameProvider {
let starredFeed = SmartFeed(delegate: StarredFeedDelegate())
private init() {
self.smartFeeds = [todayFeed, unreadFeed, starredFeed]
}
func find(by identifier: String) -> PseudoFeed? {
switch identifier {
case String(describing: TodayFeedDelegate.self):
return todayFeed
case String(describing: UnreadFeed.self):
return unreadFeed
case String(describing: StarredFeedDelegate.self):
return starredFeed
default:
return nil
}
}
}

View File

@ -15,6 +15,10 @@ import Account
struct StarredFeedDelegate: SmartFeedDelegate {
var articleFetcherType: ArticleFetcherType? {
return ArticleFetcherType.smartFeed(String(describing: StarredFeedDelegate.self))
}
let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title")
let fetchType: FetchType = .starred
var smallIcon: IconImage? = AppAssets.starredFeedImage

View File

@ -13,6 +13,10 @@ import Account
struct TodayFeedDelegate: SmartFeedDelegate {
var articleFetcherType: ArticleFetcherType? {
return ArticleFetcherType.smartFeed(String(describing: TodayFeedDelegate.self))
}
let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title")
let fetchType = FetchType.today
var smallIcon: IconImage? = AppAssets.todayFeedImage

View File

@ -53,6 +53,10 @@ final class UnreadFeed: PseudoFeed {
extension UnreadFeed: ArticleFetcher {
var articleFetcherType: ArticleFetcherType? {
return ArticleFetcherType.smartFeed(String(describing: UnreadFeed.self))
}
func fetchArticles() -> Set<Article> {
return fetchUnreadArticles()
}

26
Shared/UserInfoKey.swift Normal file
View File

@ -0,0 +1,26 @@
//
// UserInfoKey.swift
// NetNewsWire
//
// Created by Maurice Parker on 11/14/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
typealias UserInfoDictionary = [AnyHashable: Any]
struct UserInfoKey {
static let view = "view"
static let article = "article"
static let articles = "articles"
static let navigationKeyPressed = "navigationKeyPressed"
static let objects = "objects"
static let feed = "feed"
static let url = "url"
static let author = "author"
static let articlePath = "articlePath"
static let feedIdentifier = "feedIdentifier"
}

View File

@ -53,7 +53,7 @@ private extension UserNotificationManager {
}
content.sound = UNNotificationSound.default
content.userInfo = article.deepLinkUserInfo
content.userInfo = [UserInfoKey.articlePath: article.pathUserInfo]
let request = UNNotificationRequest.init(identifier: "articleID:\(article.articleID)", content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)

View File

@ -61,13 +61,9 @@
<key>NSUserActivityTypes</key>
<array>
<string>AddFeedIntent</string>
<string>com.ranchero.NetNewsWire.NextUnread</string>
<string>com.ranchero.NetNewsWire.ReadArticle</string>
<string>com.ranchero.NetNewsWire.SelectAllUnread</string>
<string>com.ranchero.NetNewsWire.SelectFeed</string>
<string>com.ranchero.NetNewsWire.SelectFolder</string>
<string>com.ranchero.NetNewsWire.SelectStarred</string>
<string>com.ranchero.NetNewsWire.SelectToday</string>
<string>NextUnread</string>
<string>ReadArticle</string>
<string>SelectFeed</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>

View File

@ -332,14 +332,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
guard let activityType = ActivityType(rawValue: activity.activityType) else { return }
switch activityType {
case .selectToday:
handleSelectToday()
case .selectAllUnread:
handleSelectAllUnread()
case .selectStarred:
handleSelectStarred()
case .selectFolder:
handleSelectFolder(activity.userInfo)
case .selectFeed:
handleSelectFeed(activity.userInfo)
case .nextUnread:
@ -543,7 +535,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
if let ip = indexPath, let node = nodeFor(ip), let fetcher = node.representedObject as? ArticleFetcher {
timelineFetcher = fetcher
updateSelectingActivity(with: node)
activityManager.selecting(fetcher: fetcher)
installTimelineControllerIfNecessary(animated: animated)
} else {
timelineFetcher = nil
@ -590,7 +582,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
stopArticleExtractor()
currentArticle = article
activityManager.reading(currentArticle)
activityManager.reading(fetcher: timelineFetcher, article: article)
if article == nil {
if rootSplitViewController.isCollapsed {
@ -1630,108 +1622,84 @@ private extension SceneCoordinator {
// MARK: NSUserActivity
func updateSelectingActivity(with node: Node) {
switch true {
case node.representedObject === SmartFeedsController.shared.todayFeed:
activityManager.selectingToday()
case node.representedObject === SmartFeedsController.shared.unreadFeed:
activityManager.selectingAllUnread()
case node.representedObject === SmartFeedsController.shared.starredFeed:
activityManager.selectingStarred()
case node.representedObject is Folder:
activityManager.selectingFolder(node.representedObject as! Folder)
case node.representedObject is Feed:
activityManager.selectingFeed(node.representedObject as! Feed)
default:
func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) {
guard let userInfo = userInfo,
let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any],
let articleFetcherType = ArticleFetcherType(userInfo: feedIdentifierUserInfo) else {
return
}
switch articleFetcherType {
case .smartFeed(let identifier):
guard let smartFeed = SmartFeedsController.shared.find(by: identifier) else { return }
if let indexPath = indexPathFor(smartFeed) {
selectFeed(indexPath, animated: false)
}
case .script:
break
}
}
func handleSelectToday() {
if let indexPath = indexPathFor(SmartFeedsController.shared.todayFeed) {
selectFeed(indexPath, animated: false)
}
}
func handleSelectAllUnread() {
if let indexPath = indexPathFor(SmartFeedsController.shared.unreadFeed) {
selectFeed(indexPath, animated: false)
}
}
func handleSelectStarred() {
if let indexPath = indexPathFor(SmartFeedsController.shared.starredFeed) {
selectFeed(indexPath, animated: false)
}
}
func handleSelectFolder(_ userInfo: [AnyHashable : Any]?) {
guard let accountNode = findAccountNode(userInfo), let folderNode = findFolderNode(userInfo, beginningAt: accountNode) else {
case .folder(let accountID, let folderName):
guard let accountNode = findAccountNode(accountID: accountID), let folderNode = findFolderNode(folderName: folderName, beginningAt: accountNode) else {
return
}
if let indexPath = indexPathFor(folderNode) {
selectFeed(indexPath, animated: false)
}
}
func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) {
guard let accountNode = findAccountNode(userInfo), let feedNode = findFeedNode(userInfo, beginningAt: accountNode) else {
case .feed(let accountID, let feedID):
guard let accountNode = findAccountNode(accountID: accountID), let feedNode = findFeedNode(feedID: feedID, beginningAt: accountNode) else {
return
}
if let feed = feedNode.representedObject as? Feed {
discloseFeed(feed, animated: false)
}
}
}
func handleReadArticle(_ userInfo: [AnyHashable : Any]?) {
guard let accountNode = findAccountNode(userInfo), let feedNode = findFeedNode(userInfo, beginningAt: accountNode) else {
guard let userInfo = userInfo,
let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let accountName = articlePathUserInfo[ArticlePathKey.accountName] as? String,
let feedID = articlePathUserInfo[ArticlePathKey.feedID] as? String,
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
return
}
guard let accountNode = findAccountNode(accountID: accountID, accountName: accountName), let feedNode = findFeedNode(feedID: feedID, beginningAt: accountNode) else {
return
}
discloseFeed(feedNode.representedObject as! Feed, animated: false) {
guard let articleID = userInfo?[DeepLinkKey.articleID.rawValue] as? String else { return }
if let article = self.articles.first(where: { $0.articleID == articleID }) {
self.selectArticle(article)
}
}
}
func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? {
guard let accountID = userInfo?[DeepLinkKey.accountID.rawValue] as? String else {
return nil
}
func findAccountNode(accountID: String, accountName: String? = nil) -> Node? {
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
return node
}
guard let accountName = userInfo?[DeepLinkKey.accountName.rawValue] as? String else {
return nil
}
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.nameForDisplay == accountName }) {
if let accountName = accountName, let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.nameForDisplay == accountName }) {
return node
}
return nil
}
func findFolderNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? {
guard let folderName = userInfo?[DeepLinkKey.folderName.rawValue] as? String else {
return nil
}
func findFolderNode(folderName: String, beginningAt startingNode: Node) -> Node? {
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Folder)?.nameForDisplay == folderName }) {
return node
}
return nil
}
func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? {
guard let feedID = userInfo?[DeepLinkKey.feedID.rawValue] as? String else {
return nil
}
func findFeedNode(feedID: String, beginningAt startingNode: Node) -> Node? {
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) {
return node
}