NetNewsWire/Shared/Activity/ActivityManager.swift

286 lines
8.8 KiB
Swift
Raw Normal View History

//
// ActivityManager.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 8/23/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import CoreSpotlight
import CoreServices
import RSCore
import Account
import Articles
2019-08-26 00:04:15 +02:00
import Intents
class ActivityManager {
private var nextUnreadActivity: NSUserActivity?
private var selectingActivity: NSUserActivity?
private var readingActivity: NSUserActivity?
private var readingArticle: Article?
2019-08-26 00:04:15 +02:00
var stateRestorationActivity: NSUserActivity? {
if readingActivity != nil {
return readingActivity
}
return selectingActivity
}
init() {
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
}
func invalidateCurrentActivities() {
invalidateReading()
invalidateSelecting()
invalidateNextUnread()
}
func selecting(fetcher: ArticleFetcher) {
invalidateCurrentActivities()
selectingActivity = makeSelectFeedActivity(fetcher: fetcher)
if let feed = fetcher as? Feed {
updateSelectingActivityFeedSearchAttributes(with: feed)
}
2019-10-18 20:01:28 +02:00
donate(selectingActivity!)
2019-08-26 00:04:15 +02:00
}
func invalidateSelecting() {
selectingActivity?.invalidate()
selectingActivity = nil
}
func selectingNextUnread() {
guard nextUnreadActivity == nil else { return }
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
2019-10-18 20:01:28 +02:00
donate(nextUnreadActivity!)
}
func invalidateNextUnread() {
nextUnreadActivity?.invalidate()
nextUnreadActivity = nil
}
func reading(fetcher: ArticleFetcher?, article: Article?) {
invalidateReading()
invalidateNextUnread()
guard let fetcher = fetcher, let article = article else { return }
readingActivity = makeReadArticleActivity(fetcher: fetcher, article: article)
#if os(iOS)
updateReadArticleSearchAttributes(with: article)
#endif
2019-10-18 20:01:28 +02:00
donate(readingActivity!)
2019-08-26 00:04:15 +02:00
}
func invalidateReading() {
readingActivity?.invalidate()
readingActivity = nil
readingArticle = nil
}
#if os(iOS)
static func cleanUp(_ account: Account) {
var ids = [String]()
if let folders = account.folders {
for folder in folders {
ids.append(identifer(for: folder))
}
}
for feed in account.flattenedFeeds() {
ids.append(contentsOf: identifers(for: feed))
}
2019-10-18 20:01:28 +02:00
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
}
static func cleanUp(_ folder: Folder) {
var ids = [String]()
ids.append(identifer(for: folder))
for feed in folder.flattenedFeeds() {
ids.append(contentsOf: identifers(for: feed))
}
2019-10-18 20:01:28 +02:00
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
}
static func cleanUp(_ feed: Feed) {
2019-10-18 20:01:28 +02:00
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifers(for: feed))
}
#endif
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[ArticlePathKey.feedID] as? String else {
return
}
#if os(iOS)
if let article = readingArticle, activityFeedId == article.feedID {
updateReadArticleSearchAttributes(with: article)
}
#endif
if activityFeedId == feed.feedID {
updateSelectingActivityFeedSearchAttributes(with: feed)
}
}
2019-08-26 00:04:15 +02:00
}
// MARK: Private
private extension ActivityManager {
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
2019-08-26 00:04:15 +02:00
activity.title = title
2019-08-26 00:04:15 +02:00
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 = fetcher.articleFetcherType?.description ?? ""
activity.contentAttributeSet?.relatedUniqueIdentifier = fetcher.articleFetcherType?.description ?? ""
#endif
2019-08-26 00:04:15 +02:00
return activity
}
func makeReadArticleActivity(fetcher: ArticleFetcher, article: Article) -> NSUserActivity {
let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue)
activity.title = ArticleStringFormatter.truncatedTitle(article)
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)
activity.keywords = Set(makeKeywords(article))
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = false
activity.persistentIdentifier = ActivityManager.identifer(for: article)
updateReadArticleSearchAttributes(with: article)
#endif
readingArticle = article
return activity
}
#if os(iOS)
func updateReadArticleSearchAttributes(with article: Article) {
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeCompositeContent as String)
attributeSet.title = ArticleStringFormatter.truncatedTitle(article)
attributeSet.contentDescription = article.summary
attributeSet.keywords = makeKeywords(article)
2019-10-18 20:01:28 +02:00
attributeSet.relatedUniqueIdentifier = ActivityManager.identifer(for: article)
if let iconImage = article.iconImage() {
attributeSet.thumbnailData = iconImage.image.pngData()
}
readingActivity?.contentAttributeSet = attributeSet
readingActivity?.needsSave = true
}
#endif
func makeKeywords(_ article: Article) -> [String] {
let feedNameKeywords = makeKeywords(article.feed?.nameForDisplay)
let articleTitleKeywords = makeKeywords(ArticleStringFormatter.truncatedTitle(article))
return feedNameKeywords + articleTitleKeywords
}
2019-08-26 00:04:15 +02:00
func makeKeywords(_ value: String?) -> [String] {
return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? []
}
func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
attributeSet.title = feed.nameForDisplay
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
2019-10-18 20:01:28 +02:00
attributeSet.relatedUniqueIdentifier = ActivityManager.identifer(for: feed)
if let iconImage = appDelegate.feedIconDownloader.icon(for: feed) {
attributeSet.thumbnailData = iconImage.image.dataRepresentation()
} else if let iconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
attributeSet.thumbnailData = iconImage.image.dataRepresentation()
}
selectingActivity!.contentAttributeSet = attributeSet
selectingActivity!.needsSave = true
}
2019-10-18 20:01:28 +02:00
func donate(_ activity: NSUserActivity) {
// You have to put the search item in the index or the activity won't index
// itself because the relatedUniqueIdentifier on the activity attributeset is populated.
if let attributeSet = activity.contentAttributeSet {
let identifier = attributeSet.relatedUniqueIdentifier
let tempAttributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
let searchableItem = CSSearchableItem(uniqueIdentifier: identifier, domainIdentifier: nil, attributeSet: tempAttributeSet)
CSSearchableIndex.default().indexSearchableItems([searchableItem])
}
activity.becomeCurrent()
}
static func identifer(for folder: Folder) -> String {
return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)"
}
static func identifer(for feed: Feed) -> String {
return "account_\(feed.account!.accountID)_feed_\(feed.feedID)"
}
static func identifer(for article: Article) -> String {
return "account_\(article.accountID)_feed_\(article.feedID)_article_\(article.articleID)"
}
static func identifers(for feed: Feed) -> [String] {
var ids = [String]()
ids.append(identifer(for: feed))
for article in feed.fetchArticles() {
ids.append(identifer(for: article))
}
return ids
}
}