Add the ability to handoff from iOS to Mac
This commit is contained in:
parent
d41da79c72
commit
0000e03083
|
@ -183,7 +183,7 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, De
|
||||||
public var deepLinkUserInfo: [AnyHashable : Any] {
|
public var deepLinkUserInfo: [AnyHashable : Any] {
|
||||||
return [
|
return [
|
||||||
DeepLinkKey.accountID.rawValue: account?.accountID ?? "",
|
DeepLinkKey.accountID.rawValue: account?.accountID ?? "",
|
||||||
DeepLinkKey.accountName.rawValue: account?.name ?? "",
|
DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "",
|
||||||
DeepLinkKey.feedID.rawValue: feedID
|
DeepLinkKey.feedID.rawValue: feedID
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun
|
||||||
public var deepLinkUserInfo: [AnyHashable : Any] {
|
public var deepLinkUserInfo: [AnyHashable : Any] {
|
||||||
return [
|
return [
|
||||||
DeepLinkKey.accountID.rawValue: account?.accountID ?? "",
|
DeepLinkKey.accountID.rawValue: account?.accountID ?? "",
|
||||||
DeepLinkKey.accountName.rawValue: account?.name ?? "",
|
DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "",
|
||||||
DeepLinkKey.folderName.rawValue: nameForDisplay
|
DeepLinkKey.folderName.rawValue: nameForDisplay
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,6 +211,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool {
|
||||||
|
guard let mainWindowController = mainWindowController else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mainWindowController.handle(userActivity)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||||
// https://github.com/brentsimmons/NetNewsWire/issues/522
|
// https://github.com/brentsimmons/NetNewsWire/issues/522
|
||||||
|
|
|
@ -18,6 +18,8 @@ enum TimelineSourceMode {
|
||||||
|
|
||||||
class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||||
|
|
||||||
|
private var activityManager = ActivityManager()
|
||||||
|
|
||||||
private var isShowingExtractedArticle = false
|
private var isShowingExtractedArticle = false
|
||||||
private var articleExtractor: ArticleExtractor? = nil
|
private var articleExtractor: ArticleExtractor? = nil
|
||||||
private var sharingServicePickerDelegate: NSSharingServicePickerDelegate?
|
private var sharingServicePickerDelegate: NSSharingServicePickerDelegate?
|
||||||
|
@ -119,6 +121,12 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||||
currentTimelineViewController?.goToDeepLink(for: userInfo)
|
currentTimelineViewController?.goToDeepLink(for: userInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handle(_ activity: NSUserActivity) {
|
||||||
|
guard let userInfo = activity.userInfo else { return }
|
||||||
|
sidebarViewController?.deepLinkRevealAndSelect(for: userInfo)
|
||||||
|
currentTimelineViewController?.goToDeepLink(for: userInfo)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Notifications
|
// MARK: - Notifications
|
||||||
|
|
||||||
// func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) {
|
// func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) {
|
||||||
|
@ -456,6 +464,8 @@ extension MainWindowController: SidebarDelegate {
|
||||||
extension MainWindowController: TimelineContainerViewControllerDelegate {
|
extension MainWindowController: TimelineContainerViewControllerDelegate {
|
||||||
|
|
||||||
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) {
|
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) {
|
||||||
|
activityManager.invalidateReading()
|
||||||
|
|
||||||
articleExtractor?.cancel()
|
articleExtractor?.cancel()
|
||||||
articleExtractor = nil
|
articleExtractor = nil
|
||||||
isShowingExtractedArticle = false
|
isShowingExtractedArticle = false
|
||||||
|
@ -464,6 +474,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(articles.first!)
|
||||||
if articles.first?.feed?.isArticleExtractorAlwaysOn ?? false {
|
if articles.first?.feed?.isArticleExtractorAlwaysOn ?? false {
|
||||||
detailState = .loading
|
detailState = .loading
|
||||||
startArticleExtractorForCurrentLink()
|
startArticleExtractorForCurrentLink()
|
||||||
|
|
|
@ -488,7 +488,7 @@ private extension SidebarViewController {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.name == accountName }) {
|
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.nameForDisplay == accountName }) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,10 @@
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>NSUserActivityTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>com.ranchero.NetNewsWire.ReadArticle</string>
|
||||||
|
</array>
|
||||||
<key>NSAppleEventsUsageDescription</key>
|
<key>NSAppleEventsUsageDescription</key>
|
||||||
<string>NetNewsWire communicates with other apps on your Mac when you choose to share an article.</string>
|
<string>NetNewsWire communicates with other apps on your Mac when you choose to share an article.</string>
|
||||||
<key>NSAppleScriptEnabled</key>
|
<key>NSAppleScriptEnabled</key>
|
||||||
|
|
|
@ -219,6 +219,8 @@
|
||||||
51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */; };
|
51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */; };
|
||||||
51FE10032345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; };
|
51FE10032345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; };
|
||||||
51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; };
|
51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; };
|
||||||
|
51FE10092346739D0056195D /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
|
||||||
|
51FE100A234673A00056195D /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; };
|
||||||
55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; };
|
55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; };
|
||||||
55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; };
|
55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; };
|
||||||
5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; };
|
5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; };
|
||||||
|
@ -2947,6 +2949,7 @@
|
||||||
51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */,
|
51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */,
|
||||||
849A97651ED9EB96007D329B /* FeedTreeControllerDelegate.swift in Sources */,
|
849A97651ED9EB96007D329B /* FeedTreeControllerDelegate.swift in Sources */,
|
||||||
849A97671ED9EB96007D329B /* UnreadCountView.swift in Sources */,
|
849A97671ED9EB96007D329B /* UnreadCountView.swift in Sources */,
|
||||||
|
51FE10092346739D0056195D /* ActivityType.swift in Sources */,
|
||||||
840BEE4121D70E64009BBAFA /* CrashReportWindowController.swift in Sources */,
|
840BEE4121D70E64009BBAFA /* CrashReportWindowController.swift in Sources */,
|
||||||
8426118A1FCB67AA0086A189 /* FeedIconDownloader.swift in Sources */,
|
8426118A1FCB67AA0086A189 /* FeedIconDownloader.swift in Sources */,
|
||||||
84C9FC7B22629E1200D921D6 /* AccountsControlsBackgroundView.swift in Sources */,
|
84C9FC7B22629E1200D921D6 /* AccountsControlsBackgroundView.swift in Sources */,
|
||||||
|
@ -2973,6 +2976,7 @@
|
||||||
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */,
|
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */,
|
||||||
8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */,
|
8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */,
|
||||||
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */,
|
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */,
|
||||||
|
51FE100A234673A00056195D /* ActivityManager.swift in Sources */,
|
||||||
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
|
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
|
||||||
55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */,
|
55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */,
|
||||||
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */,
|
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */,
|
||||||
|
|
|
@ -113,6 +113,7 @@ class ActivityManager {
|
||||||
readingActivity = nil
|
readingActivity = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
static func cleanUp(_ account: Account) {
|
static func cleanUp(_ account: Account) {
|
||||||
var ids = [String]()
|
var ids = [String]()
|
||||||
|
|
||||||
|
@ -143,7 +144,8 @@ class ActivityManager {
|
||||||
static func cleanUp(_ feed: Feed) {
|
static func cleanUp(_ feed: Feed) {
|
||||||
NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: identifers(for: feed)) {}
|
NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: identifers(for: feed)) {}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
@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?[DeepLinkKey.feedID.rawValue] as? String else {
|
||||||
return
|
return
|
||||||
|
@ -162,28 +164,33 @@ private extension ActivityManager {
|
||||||
func makeSelectingActivity(type: ActivityType, title: String, identifier: String) -> NSUserActivity {
|
func makeSelectingActivity(type: ActivityType, title: String, identifier: String) -> NSUserActivity {
|
||||||
let activity = NSUserActivity(activityType: type.rawValue)
|
let activity = NSUserActivity(activityType: type.rawValue)
|
||||||
activity.title = title
|
activity.title = title
|
||||||
activity.suggestedInvocationPhrase = title
|
|
||||||
activity.keywords = Set(makeKeywords(title))
|
activity.keywords = Set(makeKeywords(title))
|
||||||
activity.isEligibleForPrediction = true
|
|
||||||
activity.isEligibleForSearch = true
|
activity.isEligibleForSearch = true
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
activity.suggestedInvocationPhrase = title
|
||||||
|
activity.isEligibleForPrediction = true
|
||||||
activity.persistentIdentifier = identifier
|
activity.persistentIdentifier = identifier
|
||||||
|
#endif
|
||||||
|
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeReadArticleActivity(_ article: Article) -> NSUserActivity {
|
func makeReadArticleActivity(_ article: Article) -> NSUserActivity {
|
||||||
let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue)
|
let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue)
|
||||||
|
|
||||||
activity.title = article.title
|
activity.title = article.title
|
||||||
|
activity.userInfo = article.deepLinkUserInfo
|
||||||
|
activity.isEligibleForHandoff = true
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
|
||||||
let feedNameKeywords = makeKeywords(article.feed?.nameForDisplay)
|
let feedNameKeywords = makeKeywords(article.feed?.nameForDisplay)
|
||||||
let articleTitleKeywords = makeKeywords(article.title)
|
let articleTitleKeywords = makeKeywords(article.title)
|
||||||
let keywords = feedNameKeywords + articleTitleKeywords
|
let keywords = feedNameKeywords + articleTitleKeywords
|
||||||
activity.keywords = Set(keywords)
|
activity.keywords = Set(keywords)
|
||||||
|
|
||||||
activity.userInfo = article.deepLinkUserInfo
|
|
||||||
activity.isEligibleForSearch = true
|
activity.isEligibleForSearch = true
|
||||||
activity.isEligibleForPrediction = false
|
activity.isEligibleForPrediction = false
|
||||||
activity.isEligibleForHandoff = true
|
|
||||||
activity.persistentIdentifier = ActivityManager.identifer(for: article)
|
activity.persistentIdentifier = ActivityManager.identifer(for: article)
|
||||||
|
|
||||||
// CoreSpotlight
|
// CoreSpotlight
|
||||||
|
@ -197,7 +204,8 @@ private extension ActivityManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.contentAttributeSet = attributeSet
|
activity.contentAttributeSet = attributeSet
|
||||||
|
#endif
|
||||||
|
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,9 +219,17 @@ private extension ActivityManager {
|
||||||
attributeSet.title = feed.nameForDisplay
|
attributeSet.title = feed.nameForDisplay
|
||||||
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
|
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
|
||||||
if let image = appDelegate.feedIconDownloader.icon(for: feed) {
|
if let image = appDelegate.feedIconDownloader.icon(for: feed) {
|
||||||
|
#if os(iOS)
|
||||||
attributeSet.thumbnailData = image.pngData()
|
attributeSet.thumbnailData = image.pngData()
|
||||||
|
#else
|
||||||
|
attributeSet.thumbnailData = image.tiffRepresentation
|
||||||
|
#endif
|
||||||
} else if let image = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) {
|
} else if let image = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) {
|
||||||
|
#if os(iOS)
|
||||||
attributeSet.thumbnailData = image.pngData()
|
attributeSet.thumbnailData = image.pngData()
|
||||||
|
#else
|
||||||
|
attributeSet.thumbnailData = image.tiffRepresentation
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
selectingActivity!.contentAttributeSet = attributeSet
|
selectingActivity!.contentAttributeSet = attributeSet
|
||||||
|
|
|
@ -98,7 +98,7 @@ extension Article: DeepLinkProvider {
|
||||||
public var deepLinkUserInfo: [AnyHashable : Any] {
|
public var deepLinkUserInfo: [AnyHashable : Any] {
|
||||||
return [
|
return [
|
||||||
DeepLinkKey.accountID.rawValue: accountID,
|
DeepLinkKey.accountID.rawValue: accountID,
|
||||||
DeepLinkKey.accountName.rawValue: account?.name ?? "",
|
DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "",
|
||||||
DeepLinkKey.feedID.rawValue: feedID,
|
DeepLinkKey.feedID.rawValue: feedID,
|
||||||
DeepLinkKey.articleID.rawValue: articleID
|
DeepLinkKey.articleID.rawValue: articleID
|
||||||
]
|
]
|
||||||
|
|
|
@ -1658,7 +1658,7 @@ private extension SceneCoordinator {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.name == accountName }) {
|
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.nameForDisplay == accountName }) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue