diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 8044c4679..5f793a3c2 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -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 = ""; }; 510BD110232C3801002692E4 /* AccountMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadataFile.swift; sourceTree = ""; }; 510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedMetadataFile.swift; sourceTree = ""; }; + 511B9803237CD4270028BCAA /* ArticleFetcherType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleFetcherType.swift; sourceTree = ""; }; 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinSyncTest.swift; sourceTree = ""; }; 513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = ""; }; 5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = ""; }; @@ -240,7 +241,6 @@ 51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = ""; }; 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = ""; }; 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = ""; }; - 51FE1007234635A20056195D /* DeepLinkProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkProvider.swift; sourceTree = ""; }; 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIEntry.swift; sourceTree = ""; }; 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPISubscription.swift; sourceTree = ""; }; 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITag.swift; sourceTree = ""; }; @@ -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 */, diff --git a/Frameworks/Account/ArticleFetcher.swift b/Frameworks/Account/ArticleFetcher.swift index 4a5987a56..a4bb9c015 100644 --- a/Frameworks/Account/ArticleFetcher.swift +++ b/Frameworks/Account/ArticleFetcher.swift @@ -11,6 +11,8 @@ import Articles public protocol ArticleFetcher { + var articleFetcherType: ArticleFetcherType? { get } + func fetchArticles() -> Set
func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) func fetchUnreadArticles() -> Set
@@ -18,7 +20,15 @@ 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
{ return account?.fetchArticles(.feed(self)) ?? Set
() } @@ -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
{ return fetchUnreadArticles() } diff --git a/Frameworks/Account/ArticleFetcherType.swift b/Frameworks/Account/ArticleFetcherType.swift new file mode 100644 index 000000000..b030c5da4 --- /dev/null +++ b/Frameworks/Account/ArticleFetcherType.swift @@ -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 + } + } + +} diff --git a/Frameworks/Account/DeepLinkProvider.swift b/Frameworks/Account/DeepLinkProvider.swift deleted file mode 100644 index 5b97d2244..000000000 --- a/Frameworks/Account/DeepLinkProvider.swift +++ /dev/null @@ -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 } -} diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index cd3a17ec1..c2855ef30 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -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 { diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index e23e7804a..28eeddd98 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -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 = Set() @@ -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 { diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 819da2fc1..d37b13a97 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -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 = ""; }; 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = ""; }; 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = ""; }; + 511B9805237DCAC90028BCAA /* UserInfoKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoKey.swift; sourceTree = ""; }; 511D43EE231FBDE800FB1562 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreenPad.storyboard; sourceTree = ""; }; 511D4410231FC02D00FB1562 /* KeyboardManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardManager.swift; sourceTree = ""; }; 512363372369155100951F16 /* RoundedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedProgressView.swift; sourceTree = ""; }; @@ -2193,8 +2196,8 @@ children = ( 849A97731ED9EC04007D329B /* ArticleStringFormatter.swift */, 849A97581ED9EB0D007D329B /* ArticleUtilities.swift */, - 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */, 5108F6B52375E612001ABC45 /* CacheCleaner.swift */, + 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */, ); path = Data; sourceTree = ""; @@ -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 */, diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index 2c871b48e..62ecd72a2 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -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!) - } - - func selectingAllUnread() { - invalidateCurrentActivities() + selectingActivity = makeSelectFeedActivity(fetcher: fetcher) - 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() + if let feed = fetcher as? Feed { + updateSelectingActivityFeedSearchAttributes(with: feed) + } - 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 }) - 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) diff --git a/Shared/Activity/ActivityType.swift b/Shared/Activity/ActivityType.swift index bce22877b..921bccda5 100644 --- a/Shared/Activity/ActivityType.swift +++ b/Shared/Activity/ActivityType.swift @@ -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" } diff --git a/Shared/AppNotifications.swift b/Shared/AppNotifications.swift index 6f2e8e4bb..d7a5be8be 100644 --- a/Shared/AppNotifications.swift +++ b/Shared/AppNotifications.swift @@ -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" -} - diff --git a/Shared/Data/ArticleUtilities.swift b/Shared/Data/ArticleUtilities.swift index 51c598115..72a5afe7e 100644 --- a/Shared/Data/ArticleUtilities.swift +++ b/Shared/Data/ArticleUtilities.swift @@ -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 ] } diff --git a/Shared/SmartFeeds/SearchFeedDelegate.swift b/Shared/SmartFeeds/SearchFeedDelegate.swift index 413d0bc31..6b1cdaca4 100644 --- a/Shared/SmartFeeds/SearchFeedDelegate.swift +++ b/Shared/SmartFeeds/SearchFeedDelegate.swift @@ -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 } diff --git a/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift b/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift index 84df490ac..3df0bc222 100644 --- a/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift +++ b/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift @@ -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 } diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index 4bba4d7a1..aef721a58 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -71,6 +71,10 @@ final class SmartFeed: PseudoFeed { } extension SmartFeed: ArticleFetcher { + + var articleFetcherType: ArticleFetcherType? { + delegate.articleFetcherType + } func fetchArticles() -> Set
{ return delegate.fetchArticles() diff --git a/Shared/SmartFeeds/SmartFeedsController.swift b/Shared/SmartFeeds/SmartFeedsController.swift index ad67c1120..37a424a17 100644 --- a/Shared/SmartFeeds/SmartFeedsController.swift +++ b/Shared/SmartFeeds/SmartFeedsController.swift @@ -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 + } + } + } diff --git a/Shared/SmartFeeds/StarredFeedDelegate.swift b/Shared/SmartFeeds/StarredFeedDelegate.swift index 796f13b7a..df4ad368b 100644 --- a/Shared/SmartFeeds/StarredFeedDelegate.swift +++ b/Shared/SmartFeeds/StarredFeedDelegate.swift @@ -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 diff --git a/Shared/SmartFeeds/TodayFeedDelegate.swift b/Shared/SmartFeeds/TodayFeedDelegate.swift index 2d350c577..37f67723f 100644 --- a/Shared/SmartFeeds/TodayFeedDelegate.swift +++ b/Shared/SmartFeeds/TodayFeedDelegate.swift @@ -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 diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index 20c3926df..8955597a2 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -52,6 +52,10 @@ final class UnreadFeed: PseudoFeed { } extension UnreadFeed: ArticleFetcher { + + var articleFetcherType: ArticleFetcherType? { + return ArticleFetcherType.smartFeed(String(describing: UnreadFeed.self)) + } func fetchArticles() -> Set
{ return fetchUnreadArticles() diff --git a/Shared/UserInfoKey.swift b/Shared/UserInfoKey.swift new file mode 100644 index 000000000..dfdce4e87 --- /dev/null +++ b/Shared/UserInfoKey.swift @@ -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" + +} diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index fbf3608f1..2b42228ec 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -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) diff --git a/iOS/Resources/Info.plist b/iOS/Resources/Info.plist index dfcacffa8..9eab84870 100644 --- a/iOS/Resources/Info.plist +++ b/iOS/Resources/Info.plist @@ -61,13 +61,9 @@ NSUserActivityTypes AddFeedIntent - com.ranchero.NetNewsWire.NextUnread - com.ranchero.NetNewsWire.ReadArticle - com.ranchero.NetNewsWire.SelectAllUnread - com.ranchero.NetNewsWire.SelectFeed - com.ranchero.NetNewsWire.SelectFolder - com.ranchero.NetNewsWire.SelectStarred - com.ranchero.NetNewsWire.SelectToday + NextUnread + ReadArticle + SelectFeed UIApplicationSceneManifest diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index d84c0636a..2d229b9a8 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -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: - 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 { - 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 { - 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 { - return + guard let userInfo = userInfo, + let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any], + let articleFetcherType = ArticleFetcherType(userInfo: feedIdentifierUserInfo) else { + return } + + switch articleFetcherType { - discloseFeed(feedNode.representedObject as! Feed, animated: false) { + case .smartFeed(let identifier): + guard let smartFeed = SmartFeedsController.shared.find(by: identifier) else { return } + if let indexPath = indexPathFor(smartFeed) { + selectFeed(indexPath, 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) + case .script: + break + + 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) + } + + 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 findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? { - guard let accountID = userInfo?[DeepLinkKey.accountID.rawValue] as? String else { - return nil + func handleReadArticle(_ userInfo: [AnyHashable : Any]?) { + 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) { + if let article = self.articles.first(where: { $0.articleID == articleID }) { + self.selectArticle(article) + } + } + } + + 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 }