Merge pull request #2686 from stuartbreckenridge/richer-notifications
Richer notifications
This commit is contained in:
commit
00ab05c95a
|
@ -455,7 +455,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
|||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
mainWindowController?.handle(response)
|
||||
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
||||
switch response.actionIdentifier {
|
||||
case "MARK_AS_READ":
|
||||
handleMarkAsRead(userInfo: userInfo)
|
||||
case "MARK_AS_STARRED":
|
||||
handleMarkAsStarred(userInfo: userInfo)
|
||||
default:
|
||||
mainWindowController?.handle(response)
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
|
@ -791,3 +801,47 @@ extension AppDelegate: NSWindowRestoration {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// Handle Notification Actions
|
||||
|
||||
private extension AppDelegate {
|
||||
|
||||
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
os_log(.debug, "No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
os_log(.debug, "No article found from search using %@", articleID)
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .read, flag: true)
|
||||
}
|
||||
|
||||
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
}
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
os_log(.debug, "No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
os_log(.debug, "No article found from search using %@", articleID)
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .starred, flag: true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1293,11 +1293,15 @@
|
|||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
17E0081625936DFF000C23F0 /* Secrets in Embed Frameworks */,
|
||||
17A1598924E3DEDD005DA32A /* RSParser in Embed Frameworks */,
|
||||
17A1598624E3DEDD005DA32A /* RSDatabase in Embed Frameworks */,
|
||||
17A1598324E3DEDD005DA32A /* RSWeb in Embed Frameworks */,
|
||||
17E0081925936DFF000C23F0 /* SyncDatabase in Embed Frameworks */,
|
||||
17E0081025936DF6000C23F0 /* Articles in Embed Frameworks */,
|
||||
17A1597D24E3DEDD005DA32A /* RSCore in Embed Frameworks */,
|
||||
17A1598024E3DEDD005DA32A /* RSTree in Embed Frameworks */,
|
||||
17E0081325936DF6000C23F0 /* ArticlesDatabase in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -2046,7 +2050,6 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
51BC2F4B24D343A500E90810 /* Account in Frameworks */,
|
||||
513F32882593EF8F0003048F /* RSCore in Frameworks */,
|
||||
51BC2F4D24D343AB00E90810 /* RSTree in Frameworks */,
|
||||
);
|
||||
|
@ -2079,7 +2082,11 @@
|
|||
17A1597C24E3DEDD005DA32A /* RSCore in Frameworks */,
|
||||
516B695D24D2F28E00B5702F /* Account in Frameworks */,
|
||||
17A1598524E3DEDD005DA32A /* RSDatabase in Frameworks */,
|
||||
17E0080F25936DF6000C23F0 /* Articles in Frameworks */,
|
||||
17E0081525936DFF000C23F0 /* Secrets in Frameworks */,
|
||||
51E4989724A8065700B667CB /* CloudKit.framework in Frameworks */,
|
||||
17E0081225936DF6000C23F0 /* ArticlesDatabase in Frameworks */,
|
||||
17E0081825936DFF000C23F0 /* SyncDatabase in Frameworks */,
|
||||
51E4989924A8067000B667CB /* WebKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -3799,6 +3806,10 @@
|
|||
17A1598124E3DEDD005DA32A /* RSWeb */,
|
||||
17A1598424E3DEDD005DA32A /* RSDatabase */,
|
||||
17A1598724E3DEDD005DA32A /* RSParser */,
|
||||
17E0080E25936DF6000C23F0 /* Articles */,
|
||||
17E0081125936DF6000C23F0 /* ArticlesDatabase */,
|
||||
17E0081425936DFF000C23F0 /* Secrets */,
|
||||
17E0081725936DFF000C23F0 /* SyncDatabase */,
|
||||
);
|
||||
productName = iOS;
|
||||
productReference = 51C0513D24A77DF800194D5E /* NetNewsWire.app */;
|
||||
|
@ -6162,6 +6173,48 @@
|
|||
package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */;
|
||||
productName = RSParser;
|
||||
};
|
||||
17E007F925936D7B000C23F0 /* RSCore */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */;
|
||||
productName = RSCore;
|
||||
};
|
||||
17E0080125936D89000C23F0 /* RSCore */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */;
|
||||
productName = RSCore;
|
||||
};
|
||||
17E0080E25936DF6000C23F0 /* Articles */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Articles;
|
||||
};
|
||||
17E0081125936DF6000C23F0 /* ArticlesDatabase */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = ArticlesDatabase;
|
||||
};
|
||||
17E0081425936DFF000C23F0 /* Secrets */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Secrets;
|
||||
};
|
||||
17E0081725936DFF000C23F0 /* SyncDatabase */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SyncDatabase;
|
||||
};
|
||||
17E0081F25936E31000C23F0 /* Articles */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Articles;
|
||||
};
|
||||
17E0082225936E31000C23F0 /* ArticlesDatabase */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = ArticlesDatabase;
|
||||
};
|
||||
17E0082525936E31000C23F0 /* Secrets */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Secrets;
|
||||
};
|
||||
17E0082825936E31000C23F0 /* SyncDatabase */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SyncDatabase;
|
||||
};
|
||||
5102AE6824D17F7C0050839C /* RSCore */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */;
|
||||
|
|
|
@ -103,6 +103,22 @@ extension Article {
|
|||
return FaviconGenerator.favicon(webFeed)
|
||||
}
|
||||
|
||||
func iconImageUrl(webFeed: WebFeed) -> URL? {
|
||||
if let image = iconImage() {
|
||||
let fm = FileManager.default
|
||||
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
#if os(macOS)
|
||||
path.appendPathComponent(webFeed.webFeedID + "_smallIcon.tiff")
|
||||
#else
|
||||
path.appendPathComponent(webFeed.webFeedID + "_smallIcon.png")
|
||||
#endif
|
||||
fm.createFile(atPath: path.path, contents: image.image.dataRepresentation()!, attributes: nil)
|
||||
return path
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func byline() -> String {
|
||||
guard let authors = authors ?? webFeed?.authors, !authors.isEmpty else {
|
||||
return ""
|
||||
|
|
|
@ -17,6 +17,7 @@ final class UserNotificationManager: NSObject {
|
|||
super.init()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
||||
registerCategoriesAndActions()
|
||||
}
|
||||
|
||||
@objc func accountDidDownloadArticles(_ note: Notification) {
|
||||
|
@ -43,26 +44,55 @@ final class UserNotificationManager: NSObject {
|
|||
|
||||
private extension UserNotificationManager {
|
||||
|
||||
private func sendNotification(webFeed: WebFeed, article: Article) {
|
||||
func sendNotification(webFeed: WebFeed, article: Article) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = webFeed.nameForDisplay
|
||||
|
||||
if !ArticleStringFormatter.truncatedTitle(article).isEmpty {
|
||||
content.subtitle = ArticleStringFormatter.truncatedTitle(article)
|
||||
}
|
||||
|
||||
content.body = ArticleStringFormatter.truncatedSummary(article)
|
||||
|
||||
content.threadIdentifier = webFeed.webFeedID
|
||||
content.summaryArgument = "\(webFeed.nameForDisplay)"
|
||||
content.summaryArgumentCount = 1
|
||||
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = [UserInfoKey.articlePath: article.pathUserInfo]
|
||||
content.categoryIdentifier = "NEW_ARTICLE_NOTIFICATION_CATEGORY"
|
||||
if let attachment = thumbnailAttachment(for: article, webFeed: webFeed) {
|
||||
content.attachments.append(attachment)
|
||||
}
|
||||
|
||||
let request = UNNotificationRequest.init(identifier: "articleID:\(article.articleID)", content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
|
||||
/// Determine if there is an available icon for the article. This will then move it to the caches directory and make it avialble for the notification.
|
||||
/// - Parameters:
|
||||
/// - article: `Article`
|
||||
/// - webFeed: `WebFeed`
|
||||
/// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil.
|
||||
/// - Warning: In certain scenarios, this will return the `faviconTemplateImage`.
|
||||
func thumbnailAttachment(for article: Article, webFeed: WebFeed) -> UNNotificationAttachment? {
|
||||
if let imageURL = article.iconImageUrl(webFeed: webFeed) {
|
||||
let thumbnail = try? UNNotificationAttachment(identifier: webFeed.webFeedID, url: imageURL, options: nil)
|
||||
return thumbnail
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerCategoriesAndActions() {
|
||||
let readAction = UNNotificationAction(identifier: "MARK_AS_READ", title: NSLocalizedString("Mark as Read", comment: "Mark as Read"), options: [])
|
||||
let starredAction = UNNotificationAction(identifier: "MARK_AS_STARRED", title: NSLocalizedString("Mark as Starred", comment: "Mark as Starred"), options: [])
|
||||
let openAction = UNNotificationAction(identifier: "OPEN_ARTICLE", title: NSLocalizedString("Open", comment: "Open"), options: [.foreground])
|
||||
|
||||
let newArticleCategory =
|
||||
UNNotificationCategory(identifier: "NEW_ARTICLE_NOTIFICATION_CATEGORY",
|
||||
actions: [openAction, readAction, starredAction],
|
||||
intentIdentifiers: [],
|
||||
hiddenPreviewsBodyPlaceholder: "",
|
||||
options: .customDismissAction)
|
||||
|
||||
UNUserNotificationCenter.current().setNotificationCategories([newArticleCategory])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ struct StarredWidgetView : View {
|
|||
else {
|
||||
GeometryReader { metrics in
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: -4) {
|
||||
starredImage
|
||||
Spacer()
|
||||
Text(L10n.localizedCount(entry.widgetData.currentStarredCount)).bold().font(.callout).minimumScaleFactor(0.5).lineLimit(1)
|
||||
|
|
|
@ -24,7 +24,7 @@ struct TodayWidgetView : View {
|
|||
else {
|
||||
GeometryReader { metrics in
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: -4) {
|
||||
todayImage
|
||||
Spacer()
|
||||
Text(L10n.localizedCount(entry.widgetData.currentTodayCount)).bold().font(.callout).minimumScaleFactor(0.5).lineLimit(1)
|
||||
|
|
|
@ -24,7 +24,7 @@ struct UnreadWidgetView : View {
|
|||
else {
|
||||
GeometryReader { metrics in
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: -4) {
|
||||
unreadImage
|
||||
Spacer()
|
||||
Text(L10n.localizedCount(entry.widgetData.currentUnreadCount)).bold().font(.callout).minimumScaleFactor(0.5).lineLimit(1)
|
||||
|
|
|
@ -191,10 +191,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
defer { completionHandler() }
|
||||
|
||||
if let sceneDelegate = response.targetScene?.delegate as? SceneDelegate {
|
||||
sceneDelegate.handle(response)
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
||||
switch response.actionIdentifier {
|
||||
case "MARK_AS_READ":
|
||||
handleMarkAsRead(userInfo: userInfo)
|
||||
case "MARK_AS_STARRED":
|
||||
handleMarkAsStarred(userInfo: userInfo)
|
||||
default:
|
||||
if let sceneDelegate = response.targetScene?.delegate as? SceneDelegate {
|
||||
sceneDelegate.handle(response)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -397,3 +406,67 @@ private extension AppDelegate {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// Handle Notification Actions
|
||||
|
||||
private extension AppDelegate {
|
||||
|
||||
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
}
|
||||
resumeDatabaseProcessingIfNecessary()
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
os_log(.debug, "No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
os_log(.debug, "No article found from search using %@", articleID)
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .read, flag: true)
|
||||
self.prepareAccountsForBackground()
|
||||
account!.syncArticleStatus(completion: { [weak self] _ in
|
||||
if !AccountManager.shared.isSuspended {
|
||||
if #available(iOS 14, *) {
|
||||
try? WidgetDataEncoder.shared.encodeWidgetData()
|
||||
}
|
||||
self?.prepareAccountsForBackground()
|
||||
self?.suspendApplication()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
}
|
||||
resumeDatabaseProcessingIfNecessary()
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
os_log(.debug, "No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
os_log(.debug, "No article found from search using %@", articleID)
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .starred, flag: true)
|
||||
account!.syncArticleStatus(completion: { [weak self] _ in
|
||||
if !AccountManager.shared.isSuspended {
|
||||
if #available(iOS 14, *) {
|
||||
try? WidgetDataEncoder.shared.encodeWidgetData()
|
||||
}
|
||||
self?.prepareAccountsForBackground()
|
||||
self?.suspendApplication()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue