From cc187875d9c28ab75cf7de98e901a0e9439e0334 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 2 Oct 2019 19:42:16 -0500 Subject: [PATCH] Add initial support for per feed notifications --- Frameworks/Account/Feed.swift | 9 ++++ Frameworks/Account/FeedMetadata.swift | 11 ++++- Mac/AppDelegate.swift | 23 ++++++++- .../FeedInspectorViewController.swift | 18 +++++-- Mac/Inspector/Inspector.storyboard | 31 ++++++++---- NetNewsWire.xcodeproj/project.pbxproj | 14 ++++++ .../UserNotificationManager.swift | 48 +++++++++++++++++++ iOS/AppDelegate.swift | 13 +++-- iOS/Inspector/FeedInspectorView.swift | 13 +++++ 9 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 Shared/UserNotifications/UserNotificationManager.swift diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index 3ed63b50f..c2855ef30 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -123,6 +123,15 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha } } + public var isNotifyAboutNewArticles: Bool? { + get { + return metadata.isNotifyAboutNewArticles + } + set { + metadata.isNotifyAboutNewArticles = newValue + } + } + public var isArticleExtractorAlwaysOn: Bool? { get { return metadata.isArticleExtractorAlwaysOn diff --git a/Frameworks/Account/FeedMetadata.swift b/Frameworks/Account/FeedMetadata.swift index 7da7ac9c0..a85c259f2 100644 --- a/Frameworks/Account/FeedMetadata.swift +++ b/Frameworks/Account/FeedMetadata.swift @@ -24,6 +24,7 @@ final class FeedMetadata: Codable { case editedName case authors case contentHash + case isNotifyAboutNewArticles case isArticleExtractorAlwaysOn case conditionalGetInfo case subscriptionID @@ -78,10 +79,18 @@ final class FeedMetadata: Codable { } } + var isNotifyAboutNewArticles: Bool? { + didSet { + if isNotifyAboutNewArticles != oldValue { + valueDidChange(.isNotifyAboutNewArticles) + } + } + } + var isArticleExtractorAlwaysOn: Bool? { didSet { if isArticleExtractorAlwaysOn != oldValue { - valueDidChange(.contentHash) + valueDidChange(.isArticleExtractorAlwaysOn) } } } diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 9a56b3b1e..afb8c2ca3 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -7,6 +7,7 @@ // import AppKit +import UserNotifications import Articles import RSTree import RSWeb @@ -16,8 +17,9 @@ import RSCore var appDelegate: AppDelegate! @NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UnreadCountProvider { +class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider { + var userNotificationManager: UserNotificationManager! var faviconDownloader: FaviconDownloader! var imageDownloader: ImageDownloader! var authorAvatarDownloader: AuthorAvatarDownloader! @@ -130,7 +132,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } let localAccount = AccountManager.shared.defaultAccount DefaultFeedsImporter.importIfNeeded(isFirstRun, account: localAccount) - + let tempDirectory = NSTemporaryDirectory() let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String) let cacheFolder = (tempDirectory as NSString).appendingPathComponent(bundleIdentifier) @@ -179,6 +181,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, refreshTimer = AccountRefreshTimer() syncTimer = ArticleStatusSyncTimer() + UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in + if granted { + DispatchQueue.main.async { + NSApplication.shared.registerForRemoteNotifications() + } + } + } + + UNUserNotificationCenter.current().delegate = self + userNotificationManager = UserNotificationManager() + #if RELEASE debugMenuItem.menu?.removeItem(debugMenuItem) DispatchQueue.main.async { @@ -322,6 +335,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, return true } + // MARK: UNUserNotificationCenterDelegate + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.alert, .badge, .sound]) + } + // MARK: Add Feed func addFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) { diff --git a/Mac/Inspector/FeedInspectorViewController.swift b/Mac/Inspector/FeedInspectorViewController.swift index 42caa1418..0a3cef52d 100644 --- a/Mac/Inspector/FeedInspectorViewController.swift +++ b/Mac/Inspector/FeedInspectorViewController.swift @@ -12,10 +12,11 @@ import Account final class FeedInspectorViewController: NSViewController, Inspector { - @IBOutlet var imageView: NSImageView? - @IBOutlet var nameTextField: NSTextField? - @IBOutlet var homePageURLTextField: NSTextField? - @IBOutlet var urlTextField: NSTextField? + @IBOutlet weak var imageView: NSImageView? + @IBOutlet weak var nameTextField: NSTextField? + @IBOutlet weak var homePageURLTextField: NSTextField? + @IBOutlet weak var urlTextField: NSTextField? + @IBOutlet weak var isNotifyAboutNewArticlesCheckBox: NSButton! @IBOutlet weak var isReaderViewAlwaysOnCheckBox: NSButton? private var feed: Feed? { @@ -51,6 +52,10 @@ final class FeedInspectorViewController: NSViewController, Inspector { } // MARK: Actions + @IBAction func isNotifyAboutNewArticlesChanged(_ sender: Any) { + feed?.isNotifyAboutNewArticles = (isNotifyAboutNewArticlesCheckBox?.state ?? .off) == .on ? true : false + } + @IBAction func isReaderViewAlwaysOnChanged(_ sender: Any) { feed?.isArticleExtractorAlwaysOn = (isReaderViewAlwaysOnCheckBox?.state ?? .off) == .on ? true : false } @@ -89,6 +94,7 @@ private extension FeedInspectorViewController { updateName() updateHomePageURL() updateFeedURL() + updateNotifyAboutNewArticles() updateIsReaderViewAlwaysOn() view.needsLayout = true @@ -135,6 +141,10 @@ private extension FeedInspectorViewController { urlTextField?.stringValue = feed?.url ?? "" } + func updateNotifyAboutNewArticles() { + isNotifyAboutNewArticlesCheckBox?.state = (feed?.isNotifyAboutNewArticles ?? false) ? .on : .off + } + func updateIsReaderViewAlwaysOn() { isReaderViewAlwaysOnCheckBox?.state = (feed?.isArticleExtractorAlwaysOn ?? false) ? .on : .off } diff --git a/Mac/Inspector/Inspector.storyboard b/Mac/Inspector/Inspector.storyboard index 4f5f22bf5..eb460de87 100644 --- a/Mac/Inspector/Inspector.storyboard +++ b/Mac/Inspector/Inspector.storyboard @@ -1,8 +1,8 @@ - + - + @@ -34,11 +34,11 @@ - + - + @@ -46,7 +46,7 @@ - + @@ -104,13 +104,24 @@ Field + - + + @@ -120,6 +131,7 @@ Field + @@ -131,6 +143,7 @@ Field + @@ -138,7 +151,7 @@ Field - + @@ -189,7 +202,7 @@ Field - + @@ -231,7 +244,7 @@ Field - + diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 9b35dae91..21a1cdebc 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -218,6 +218,8 @@ 51FA73B72332D5F70090D516 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */; }; 51FD40C72341555A00880194 /* UIImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD40BD2341555600880194 /* UIImage-Extensions.swift */; }; 51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */; }; + 51FE10032345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; + 51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; }; 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; 5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; }; @@ -899,6 +901,7 @@ 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = ""; }; 51FD40BD2341555600880194 /* UIImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage-Extensions.swift"; sourceTree = ""; }; 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineUnreadCountView.swift; sourceTree = ""; }; + 51FE10022345529D0056195D /* UserNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationManager.swift; sourceTree = ""; }; 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsReaderAPIAccountView.swift; sourceTree = ""; }; 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsReaderAPI.xib; sourceTree = ""; }; 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = ""; }; @@ -1456,6 +1459,14 @@ path = "Article Extractor"; sourceTree = ""; }; + 51FE0FF9234552490056195D /* UserNotifications */ = { + isa = PBXGroup; + children = ( + 51FE10022345529D0056195D /* UserNotificationManager.swift */, + ); + path = UserNotifications; + sourceTree = ""; + }; 6581C73620CED60100F4AD34 /* SafariExtension */ = { isa = PBXGroup; children = ( @@ -1879,6 +1890,7 @@ 849A97861ED9ECEF007D329B /* Article Styles */, 84DAEE201F86CAE00058304B /* Importers */, 8444C9011FED81880051386C /* Exporters */, + 51FE0FF9234552490056195D /* UserNotifications */, 84F2D5341FC22FCB00998D64 /* SmartFeeds */, 848F6AE31FC29CFA002D422E /* Favicons */, 845213211FCA5B10003B6E93 /* Images */, @@ -2830,6 +2842,7 @@ 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, 5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */, 512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */, + 51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */, 51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */, 51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */, 51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */, @@ -2953,6 +2966,7 @@ 5144EA43227A380F00D19003 /* ExportOPMLWindowController.swift in Sources */, 842611A21FCB769D0086A189 /* RSHTMLMetadata+Extension.swift in Sources */, 84A1500520048DDF0046AD9A /* SendToMarsEditCommand.swift in Sources */, + 51FE10032345529D0056195D /* UserNotificationManager.swift in Sources */, D5907DB22004BB37005947E5 /* ScriptingObjectContainer.swift in Sources */, 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */, 8405DD8A2213E0E3008CE1BF /* DetailContainerView.swift in Sources */, diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift new file mode 100644 index 000000000..842b39778 --- /dev/null +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -0,0 +1,48 @@ +// +// NotificationManager.swift +// NetNewsWire +// +// Created by Maurice Parker on 10/2/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation +import Account +import Articles +import UserNotifications + +final class UserNotificationManager: NSObject { + + override init() { + super.init() + NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) + } + + @objc func accountDidDownloadArticles(_ note: Notification) { + guard let articles = note.userInfo?[Account.UserInfoKey.newArticles] as? Set
else { + return + } + + for article in articles { + if let feed = article.feed, feed.isNotifyAboutNewArticles ?? false { + sendNotification(feed: feed, article: article) + } + } + } + +} + +private extension UserNotificationManager { + + private func sendNotification(feed: Feed, article: Article) { + let content = UNMutableNotificationContent() + + content.title = feed.nameForDisplay + content.body = article.title ?? article.summary ?? "" + content.sound = UNNotificationSound.default + + let request = UNNotificationRequest.init(identifier: "articleID:\(article.articleID)", content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) + } + +} diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index b3141d7ca..ab37ce762 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -10,14 +10,13 @@ import UIKit import RSCore import RSWeb import Account -import UserNotifications import BackgroundTasks import os.log var appDelegate: AppDelegate! @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate, UnreadCountProvider { +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider { private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid @@ -34,6 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "application") + var userNotificationManager: UserNotificationManager! var faviconDownloader: FaviconDownloader! var imageDownloader: ImageDownloader! var authorAvatarDownloader: AuthorAvatarDownloader! @@ -90,7 +90,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele } } } - + + UNUserNotificationCenter.current().delegate = self + userNotificationManager = UserNotificationManager() + syncTimer = ArticleStatusSyncTimer() #if DEBUG @@ -171,6 +174,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele logMessage(message, type: .debug) } + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.alert, .badge, .sound]) + } + } // MARK: App Initialization diff --git a/iOS/Inspector/FeedInspectorView.swift b/iOS/Inspector/FeedInspectorView.swift index 95bc6b080..19edf5b23 100644 --- a/iOS/Inspector/FeedInspectorView.swift +++ b/iOS/Inspector/FeedInspectorView.swift @@ -52,6 +52,9 @@ struct FeedInspectorView : View { Spacer() }) { TextField("Feed Name", text: $viewModel.name) + Toggle(isOn: $viewModel.isNotifyAboutNewArticles) { + Text("Notify About New Articles") + } Toggle(isOn: $viewModel.isArticleExtractorAlwaysOn) { Text("Always Show Reader View") } @@ -108,6 +111,16 @@ struct FeedInspectorView : View { } } + var isNotifyAboutNewArticles: Bool { + get { + return feed.isNotifyAboutNewArticles ?? false + } + set { + objectWillChange.send() + feed.isNotifyAboutNewArticles = newValue + } + } + var isArticleExtractorAlwaysOn: Bool { get { return feed.isArticleExtractorAlwaysOn ?? false