2019-08-24 21:57:51 +02:00
|
|
|
//
|
2019-08-25 21:43:11 +02:00
|
|
|
// ActivityManager.swift
|
2019-08-24 21:57:51 +02:00
|
|
|
// NetNewsWire-iOS
|
|
|
|
//
|
|
|
|
// Created by Maurice Parker on 8/23/19.
|
|
|
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import CoreServices
|
2019-08-27 21:20:34 +02:00
|
|
|
import Account
|
2019-08-24 21:57:51 +02:00
|
|
|
import Articles
|
2019-08-26 00:04:15 +02:00
|
|
|
import Intents
|
2024-02-26 03:42:26 +01:00
|
|
|
import UniformTypeIdentifiers
|
2024-04-16 07:21:17 +02:00
|
|
|
import Images
|
2019-08-24 21:57:51 +02:00
|
|
|
|
2024-03-19 18:15:30 +01:00
|
|
|
#if os(iOS)
|
|
|
|
@preconcurrency import CoreSpotlight
|
|
|
|
#else
|
|
|
|
import CoreSpotlight
|
|
|
|
#endif
|
|
|
|
|
2024-03-20 07:05:30 +01:00
|
|
|
@MainActor final class ActivityManager {
|
|
|
|
|
2019-09-03 22:52:59 +02:00
|
|
|
private var nextUnreadActivity: NSUserActivity?
|
|
|
|
private var selectingActivity: NSUserActivity?
|
|
|
|
private var readingActivity: NSUserActivity?
|
2019-10-04 02:05:54 +02:00
|
|
|
private var readingArticle: Article?
|
2019-08-26 00:04:15 +02:00
|
|
|
|
2019-11-26 23:33:11 +01:00
|
|
|
var stateRestorationActivity: NSUserActivity {
|
|
|
|
if let activity = readingActivity {
|
|
|
|
return activity
|
2019-09-01 02:30:21 +02:00
|
|
|
}
|
2019-11-26 23:33:11 +01:00
|
|
|
|
|
|
|
if let activity = selectingActivity {
|
|
|
|
return activity
|
|
|
|
}
|
|
|
|
|
|
|
|
let activity = NSUserActivity(activityType: ActivityType.restoration.rawValue)
|
2019-11-27 00:00:13 +01:00
|
|
|
#if os(iOS)
|
2019-11-26 23:33:11 +01:00
|
|
|
activity.persistentIdentifier = UUID().uuidString
|
2019-11-27 00:00:13 +01:00
|
|
|
#endif
|
2019-11-26 23:33:11 +01:00
|
|
|
activity.becomeCurrent()
|
|
|
|
return activity
|
2019-09-01 02:30:21 +02:00
|
|
|
}
|
|
|
|
|
2019-08-28 18:44:54 +02:00
|
|
|
init() {
|
2024-02-26 08:12:21 +01:00
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
2019-08-28 18:44:54 +02:00
|
|
|
}
|
|
|
|
|
2019-09-01 22:31:11 +02:00
|
|
|
func invalidateCurrentActivities() {
|
2019-09-03 22:52:59 +02:00
|
|
|
invalidateReading()
|
|
|
|
invalidateSelecting()
|
|
|
|
invalidateNextUnread()
|
2019-09-01 22:31:11 +02:00
|
|
|
}
|
|
|
|
|
2024-02-26 06:14:10 +01:00
|
|
|
func selecting(feed: SidebarItem) {
|
2019-09-03 22:52:59 +02:00
|
|
|
invalidateCurrentActivities()
|
|
|
|
|
2019-11-15 13:19:14 +01:00
|
|
|
selectingActivity = makeSelectFeedActivity(feed: feed)
|
2019-09-03 22:52:59 +02:00
|
|
|
|
2024-02-26 08:12:21 +01:00
|
|
|
if let feed = feed as? Feed {
|
|
|
|
updateSelectingActivityFeedSearchAttributes(with: feed)
|
2019-11-14 22:06:32 +01:00
|
|
|
}
|
2019-08-28 00:43:15 +02:00
|
|
|
|
2019-10-18 20:01:28 +02:00
|
|
|
donate(selectingActivity!)
|
2019-08-26 00:04:15 +02:00
|
|
|
}
|
|
|
|
|
2019-09-03 22:52:59 +02:00
|
|
|
func invalidateSelecting() {
|
|
|
|
selectingActivity?.invalidate()
|
|
|
|
selectingActivity = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func selectingNextUnread() {
|
|
|
|
guard nextUnreadActivity == nil else { return }
|
2019-11-14 22:06:32 +01:00
|
|
|
|
|
|
|
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!)
|
2019-09-03 22:52:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func invalidateNextUnread() {
|
|
|
|
nextUnreadActivity?.invalidate()
|
|
|
|
nextUnreadActivity = nil
|
|
|
|
}
|
|
|
|
|
2024-02-26 06:14:10 +01:00
|
|
|
func reading(feed: SidebarItem?, article: Article?) {
|
2019-09-03 22:52:59 +02:00
|
|
|
invalidateReading()
|
|
|
|
invalidateNextUnread()
|
2019-10-04 02:05:54 +02:00
|
|
|
|
2019-11-14 22:35:19 +01:00
|
|
|
guard let article = article else { return }
|
2019-11-15 13:19:14 +01:00
|
|
|
readingActivity = makeReadArticleActivity(feed: feed, article: article)
|
2019-10-04 02:05:54 +02:00
|
|
|
|
|
|
|
#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
|
|
|
}
|
|
|
|
|
2019-09-03 22:52:59 +02:00
|
|
|
func invalidateReading() {
|
2019-09-24 14:01:22 +02:00
|
|
|
readingActivity?.invalidate()
|
|
|
|
readingActivity = nil
|
2019-10-04 02:05:54 +02:00
|
|
|
readingArticle = nil
|
2019-09-03 22:52:59 +02:00
|
|
|
}
|
|
|
|
|
2019-10-03 22:49:27 +02:00
|
|
|
#if os(iOS)
|
2024-03-20 04:33:54 +01:00
|
|
|
@MainActor static func cleanUp(_ account: Account) {
|
2024-03-19 18:15:30 +01:00
|
|
|
|
|
|
|
Task { @MainActor in
|
|
|
|
var ids = [String]()
|
|
|
|
|
|
|
|
if let folders = account.folders {
|
|
|
|
for folder in folders {
|
|
|
|
ids.append(identifier(for: folder))
|
|
|
|
}
|
2019-08-28 18:30:40 +02:00
|
|
|
}
|
2024-03-19 18:15:30 +01:00
|
|
|
|
|
|
|
for feed in account.flattenedFeeds() {
|
|
|
|
let feedIdentifiers = await identifiers(for: feed)
|
|
|
|
ids.append(contentsOf: feedIdentifiers)
|
|
|
|
}
|
|
|
|
|
|
|
|
try? await CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
|
2019-08-28 18:30:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-20 04:33:54 +01:00
|
|
|
@MainActor static func cleanUp(_ folder: Folder) {
|
2024-03-19 18:15:30 +01:00
|
|
|
|
|
|
|
Task { @MainActor in
|
2024-03-20 04:33:54 +01:00
|
|
|
|
|
|
|
var ids: [String] = [String]()
|
2024-03-19 18:15:30 +01:00
|
|
|
ids.append(identifier(for: folder))
|
|
|
|
|
2024-03-20 04:33:54 +01:00
|
|
|
let feeds = folder.flattenedFeeds()
|
|
|
|
for feed in feeds {
|
2024-03-19 18:15:30 +01:00
|
|
|
let feedIdentifiers = await identifiers(for: feed)
|
|
|
|
ids.append(contentsOf: feedIdentifiers)
|
|
|
|
}
|
|
|
|
|
|
|
|
try? await CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
|
2019-08-28 18:30:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-20 04:33:54 +01:00
|
|
|
@MainActor static func cleanUp(_ feed: Feed) {
|
|
|
|
|
2024-03-19 18:15:30 +01:00
|
|
|
Task { @MainActor in
|
|
|
|
let feedIdentifiers = await identifiers(for: feed)
|
|
|
|
try? await CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: feedIdentifiers)
|
|
|
|
}
|
2019-08-28 18:30:40 +02:00
|
|
|
}
|
2019-10-03 22:49:27 +02:00
|
|
|
#endif
|
|
|
|
|
2024-02-26 08:12:21 +01:00
|
|
|
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
2024-04-16 07:21:17 +02:00
|
|
|
guard let feed = note.userInfo?[FeedIconDownloader.feedKey] as? Feed, let activityFeedID = selectingActivity?.userInfo?[ArticlePathKey.feedID] as? String else {
|
2019-08-28 18:44:54 +02:00
|
|
|
return
|
|
|
|
}
|
2019-10-04 02:05:54 +02:00
|
|
|
|
2019-10-04 02:37:04 +02:00
|
|
|
#if os(iOS)
|
2024-04-08 01:09:23 +02:00
|
|
|
if let article = readingArticle, activityFeedID == article.feedID {
|
2019-10-04 02:05:54 +02:00
|
|
|
updateReadArticleSearchAttributes(with: article)
|
|
|
|
}
|
2019-10-04 02:37:04 +02:00
|
|
|
#endif
|
2019-10-04 02:05:54 +02:00
|
|
|
|
2024-04-08 01:09:23 +02:00
|
|
|
if activityFeedID == feed.feedID {
|
2024-02-26 08:12:21 +01:00
|
|
|
updateSelectingActivityFeedSearchAttributes(with: feed)
|
2019-08-28 18:44:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-26 00:04:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Private
|
|
|
|
|
|
|
|
private extension ActivityManager {
|
|
|
|
|
2024-02-26 06:14:10 +01:00
|
|
|
func makeSelectFeedActivity(feed: SidebarItem) -> NSUserActivity {
|
2019-11-14 22:06:32 +01:00
|
|
|
let activity = NSUserActivity(activityType: ActivityType.selectFeed.rawValue)
|
|
|
|
|
|
|
|
let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder")
|
2019-11-15 13:19:14 +01:00
|
|
|
let title = NSString.localizedStringWithFormat(localizedText as NSString, feed.nameForDisplay) as String
|
2019-08-26 00:04:15 +02:00
|
|
|
activity.title = title
|
2019-11-14 22:06:32 +01:00
|
|
|
|
2019-08-26 00:04:15 +02:00
|
|
|
activity.keywords = Set(makeKeywords(title))
|
|
|
|
activity.isEligibleForSearch = true
|
2019-11-14 22:06:32 +01:00
|
|
|
|
2024-02-26 06:34:22 +01:00
|
|
|
let articleFetcherIdentifierUserInfo = feed.sidebarItemID?.userInfo ?? [AnyHashable: Any]()
|
2019-11-14 22:06:32 +01:00
|
|
|
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo]
|
|
|
|
activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String })
|
2019-10-03 22:49:27 +02:00
|
|
|
|
2024-02-26 06:34:22 +01:00
|
|
|
activity.persistentIdentifier = feed.sidebarItemID?.description ?? ""
|
2021-09-20 21:31:56 +02:00
|
|
|
|
2019-10-03 22:49:27 +02:00
|
|
|
#if os(iOS)
|
|
|
|
activity.suggestedInvocationPhrase = title
|
|
|
|
activity.isEligibleForPrediction = true
|
2024-02-26 06:34:22 +01:00
|
|
|
activity.contentAttributeSet?.relatedUniqueIdentifier = feed.sidebarItemID?.description ?? ""
|
2019-10-03 22:49:27 +02:00
|
|
|
#endif
|
2021-09-20 21:31:56 +02:00
|
|
|
|
2019-08-26 00:04:15 +02:00
|
|
|
return activity
|
|
|
|
}
|
|
|
|
|
2024-02-26 06:14:10 +01:00
|
|
|
func makeReadArticleActivity(feed: SidebarItem?, article: Article) -> NSUserActivity {
|
2019-08-25 02:31:29 +02:00
|
|
|
let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue)
|
2019-10-20 09:33:28 +02:00
|
|
|
activity.title = ArticleStringFormatter.truncatedTitle(article)
|
2019-11-14 22:06:32 +01:00
|
|
|
|
2019-11-15 13:19:14 +01:00
|
|
|
if let feed = feed {
|
2024-02-26 06:34:22 +01:00
|
|
|
let articleFetcherIdentifierUserInfo = feed.sidebarItemID?.userInfo ?? [AnyHashable: Any]()
|
2019-11-14 22:35:19 +01:00
|
|
|
let articlePathUserInfo = article.pathUserInfo
|
|
|
|
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo, UserInfoKey.articlePath: articlePathUserInfo]
|
|
|
|
} else {
|
|
|
|
activity.userInfo = [UserInfoKey.articlePath: article.pathUserInfo]
|
|
|
|
}
|
2019-11-14 22:06:32 +01:00
|
|
|
activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String })
|
|
|
|
|
2019-10-03 22:49:27 +02:00
|
|
|
activity.isEligibleForHandoff = true
|
|
|
|
|
2022-01-04 23:25:20 +01:00
|
|
|
activity.persistentIdentifier = ActivityManager.identifier(for: article)
|
2021-09-20 21:31:56 +02:00
|
|
|
|
2019-10-03 22:49:27 +02:00
|
|
|
#if os(iOS)
|
2019-10-04 02:05:54 +02:00
|
|
|
activity.keywords = Set(makeKeywords(article))
|
2019-08-24 21:57:51 +02:00
|
|
|
activity.isEligibleForSearch = true
|
|
|
|
activity.isEligibleForPrediction = false
|
2019-10-04 02:05:54 +02:00
|
|
|
updateReadArticleSearchAttributes(with: article)
|
|
|
|
#endif
|
|
|
|
|
|
|
|
readingArticle = article
|
|
|
|
|
|
|
|
return activity
|
|
|
|
}
|
|
|
|
|
2019-10-04 02:37:04 +02:00
|
|
|
#if os(iOS)
|
2019-10-04 02:05:54 +02:00
|
|
|
func updateReadArticleSearchAttributes(with article: Article) {
|
2019-08-24 21:57:51 +02:00
|
|
|
|
2024-02-26 03:42:26 +01:00
|
|
|
let attributeSet = CSSearchableItemAttributeSet(itemContentType: UTType.compositeContent.identifier)
|
2019-10-20 09:33:28 +02:00
|
|
|
attributeSet.title = ArticleStringFormatter.truncatedTitle(article)
|
2019-08-24 21:57:51 +02:00
|
|
|
attributeSet.contentDescription = article.summary
|
2019-10-04 02:05:54 +02:00
|
|
|
attributeSet.keywords = makeKeywords(article)
|
2022-01-04 23:25:20 +01:00
|
|
|
attributeSet.relatedUniqueIdentifier = ActivityManager.identifier(for: article)
|
2019-10-18 20:01:28 +02:00
|
|
|
|
2019-11-06 01:05:57 +01:00
|
|
|
if let iconImage = article.iconImage() {
|
|
|
|
attributeSet.thumbnailData = iconImage.image.pngData()
|
2019-08-24 21:57:51 +02:00
|
|
|
}
|
|
|
|
|
2019-10-04 02:05:54 +02:00
|
|
|
readingActivity?.contentAttributeSet = attributeSet
|
|
|
|
readingActivity?.needsSave = true
|
|
|
|
|
|
|
|
}
|
2019-10-04 02:37:04 +02:00
|
|
|
#endif
|
2019-10-04 02:05:54 +02:00
|
|
|
|
|
|
|
func makeKeywords(_ article: Article) -> [String] {
|
2024-02-26 08:12:21 +01:00
|
|
|
let feedNameKeywords = makeKeywords(article.feed?.nameForDisplay)
|
2019-10-20 09:33:28 +02:00
|
|
|
let articleTitleKeywords = makeKeywords(ArticleStringFormatter.truncatedTitle(article))
|
2019-10-04 02:05:54 +02:00
|
|
|
return feedNameKeywords + articleTitleKeywords
|
2019-08-24 21:57:51 +02:00
|
|
|
}
|
|
|
|
|
2019-08-26 00:04:15 +02:00
|
|
|
func makeKeywords(_ value: String?) -> [String] {
|
|
|
|
return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? []
|
|
|
|
}
|
|
|
|
|
2024-03-20 07:05:30 +01:00
|
|
|
@MainActor func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
|
|
|
|
|
2024-02-25 04:03:20 +01:00
|
|
|
let attributeSet = CSSearchableItemAttributeSet(contentType: UTType.item)
|
2019-09-01 02:30:21 +02:00
|
|
|
attributeSet.title = feed.nameForDisplay
|
|
|
|
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
|
2022-01-04 23:25:20 +01:00
|
|
|
attributeSet.relatedUniqueIdentifier = ActivityManager.identifier(for: feed)
|
2021-05-08 21:42:44 +02:00
|
|
|
|
|
|
|
if let iconImage = IconImageCache.shared.imageForFeed(feed) {
|
2019-11-06 01:05:57 +01:00
|
|
|
attributeSet.thumbnailData = iconImage.image.dataRepresentation()
|
2019-09-01 02:30:21 +02:00
|
|
|
}
|
2021-05-08 21:42:44 +02:00
|
|
|
|
2019-09-01 02:30:21 +02:00
|
|
|
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
|
2024-02-25 04:03:20 +01:00
|
|
|
let tempAttributeSet = CSSearchableItemAttributeSet(contentType: UTType.item)
|
2019-10-18 20:01:28 +02:00
|
|
|
let searchableItem = CSSearchableItem(uniqueIdentifier: identifier, domainIdentifier: nil, attributeSet: tempAttributeSet)
|
|
|
|
CSSearchableIndex.default().indexSearchableItems([searchableItem])
|
|
|
|
}
|
|
|
|
|
|
|
|
activity.becomeCurrent()
|
|
|
|
}
|
|
|
|
|
2022-01-04 23:25:20 +01:00
|
|
|
static func identifier(for folder: Folder) -> String {
|
2019-08-28 18:30:40 +02:00
|
|
|
return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)"
|
|
|
|
}
|
|
|
|
|
2024-02-26 06:41:18 +01:00
|
|
|
static func identifier(for feed: Feed) -> String {
|
2024-02-26 08:12:21 +01:00
|
|
|
return "account_\(feed.account!.accountID)_feed_\(feed.feedID)"
|
2019-08-28 18:30:40 +02:00
|
|
|
}
|
|
|
|
|
2022-01-04 23:25:20 +01:00
|
|
|
static func identifier(for article: Article) -> String {
|
2024-02-26 08:12:21 +01:00
|
|
|
return "account_\(article.accountID)_feed_\(article.feedID)_article_\(article.articleID)"
|
2019-08-28 18:30:40 +02:00
|
|
|
}
|
|
|
|
|
2024-03-19 05:08:37 +01:00
|
|
|
@MainActor static func identifiers(for feed: Feed) async -> [String] {
|
|
|
|
|
2019-08-28 18:30:40 +02:00
|
|
|
var ids = [String]()
|
2022-01-04 23:25:20 +01:00
|
|
|
ids.append(identifier(for: feed))
|
2024-03-19 05:08:37 +01:00
|
|
|
|
|
|
|
if let articles = try? await feed.fetchArticles() {
|
2019-12-17 07:45:59 +01:00
|
|
|
for article in articles {
|
2022-01-04 23:25:20 +01:00
|
|
|
ids.append(identifier(for: article))
|
2019-12-17 07:45:59 +01:00
|
|
|
}
|
2019-08-28 18:30:40 +02:00
|
|
|
}
|
2019-12-17 07:45:59 +01:00
|
|
|
|
2019-08-28 18:30:40 +02:00
|
|
|
return ids
|
|
|
|
}
|
2019-08-24 21:57:51 +02:00
|
|
|
}
|