Add initial support for per feed notifications

This commit is contained in:
Maurice Parker 2019-10-02 19:42:16 -05:00
parent aba0d15cb6
commit cc187875d9
9 changed files with 161 additions and 19 deletions

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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) {

View File

@ -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
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="cfG-Pn-VJS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="cfG-Pn-VJS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15400"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15504"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -34,11 +34,11 @@
<objects>
<viewController title="Feed" storyboardIdentifier="Feed" showSeguePresentationStyle="single" id="sfH-oR-GXm" customClass="FeedInspectorViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="ecA-UY-KEd">
<rect key="frame" x="0.0" y="0.0" width="256" height="298"/>
<rect key="frame" x="0.0" y="0.0" width="256" height="332"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="H9X-OG-K0p">
<rect key="frame" x="104" y="230" width="48" height="48"/>
<rect key="frame" x="104" y="264" width="48" height="48"/>
<constraints>
<constraint firstAttribute="width" constant="48" id="1Cy-0w-dBg"/>
<constraint firstAttribute="height" constant="48" id="edb-lw-Ict"/>
@ -46,7 +46,7 @@
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSNetwork" id="MZ2-89-Bje"/>
</imageView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="IWu-80-XC5">
<rect key="frame" x="20" y="166" width="216" height="56"/>
<rect key="frame" x="20" y="200" width="216" height="56"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="56" id="zV3-AX-gyC"/>
</constraints>
@ -104,13 +104,24 @@ Field</string>
<action selector="isReaderViewAlwaysOnChanged:" target="sfH-oR-GXm" id="rsD-0e-ksP"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZBX-E8-k9c">
<rect key="frame" x="18" y="164" width="179" height="18"/>
<buttonCell key="cell" type="check" title="Notify About New Articles" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="Bw5-c7-yDX">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="isNotifyAboutNewArticlesChanged:" target="sfH-oR-GXm" id="Vx9-pQ-RnP"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="nH2-ab-KJ5" firstAttribute="top" secondItem="IWu-80-XC5" secondAttribute="bottom" constant="20" id="2OX-H3-BJ0"/>
<constraint firstItem="zm0-15-BFy" firstAttribute="top" secondItem="2WO-Iu-p5e" secondAttribute="bottom" constant="4" id="2fb-QO-XIm"/>
<constraint firstItem="IWu-80-XC5" firstAttribute="top" secondItem="H9X-OG-K0p" secondAttribute="bottom" constant="8" symbolic="YES" id="4WB-WJ-3Z4"/>
<constraint firstItem="ZBX-E8-k9c" firstAttribute="top" secondItem="IWu-80-XC5" secondAttribute="bottom" constant="20" id="5L7-aZ-vdg"/>
<constraint firstItem="nH2-ab-KJ5" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="8pK-lW-xQk"/>
<constraint firstItem="H9X-OG-K0p" firstAttribute="centerX" secondItem="ecA-UY-KEd" secondAttribute="centerX" id="9CA-KA-HEg"/>
<constraint firstItem="nH2-ab-KJ5" firstAttribute="top" secondItem="ZBX-E8-k9c" secondAttribute="bottom" constant="20" id="CpA-X9-EbP"/>
<constraint firstAttribute="bottom" secondItem="Vvk-KG-JlG" secondAttribute="bottom" constant="20" id="IxJ-5N-NhL"/>
<constraint firstAttribute="trailing" secondItem="ju6-Zo-8X4" secondAttribute="trailing" constant="20" symbolic="YES" id="Jzi-tP-TIw"/>
<constraint firstAttribute="trailing" secondItem="Vvk-KG-JlG" secondAttribute="trailing" constant="20" symbolic="YES" id="KAS-A7-TxB"/>
@ -120,6 +131,7 @@ Field</string>
<constraint firstAttribute="trailing" secondItem="IWu-80-XC5" secondAttribute="trailing" constant="20" symbolic="YES" id="WW6-xR-Zue"/>
<constraint firstItem="H9X-OG-K0p" firstAttribute="top" secondItem="ecA-UY-KEd" secondAttribute="top" constant="20" symbolic="YES" id="Z6q-PN-wOC"/>
<constraint firstItem="zm0-15-BFy" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="aho-BJ-kmB"/>
<constraint firstItem="ZBX-E8-k9c" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="cjR-0i-YNG"/>
<constraint firstAttribute="trailing" secondItem="2WO-Iu-p5e" secondAttribute="trailing" constant="20" symbolic="YES" id="dLU-a6-nfx"/>
<constraint firstAttribute="trailing" secondItem="zm0-15-BFy" secondAttribute="trailing" constant="20" symbolic="YES" id="js6-b2-FIR"/>
<constraint firstItem="IWu-80-XC5" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="r6h-Z0-g7b"/>
@ -131,6 +143,7 @@ Field</string>
<connections>
<outlet property="homePageURLTextField" destination="zm0-15-BFy" id="0Jh-yy-mnF"/>
<outlet property="imageView" destination="H9X-OG-K0p" id="Rm6-X6-csH"/>
<outlet property="isNotifyAboutNewArticlesCheckBox" destination="ZBX-E8-k9c" id="FWc-Ds-LUy"/>
<outlet property="isReaderViewAlwaysOnCheckBox" destination="nH2-ab-KJ5" id="xPg-P5-3cr"/>
<outlet property="nameTextField" destination="IWu-80-XC5" id="zg4-5h-hoP"/>
<outlet property="urlTextField" destination="Vvk-KG-JlG" id="bcl-fq-3nQ"/>
@ -138,7 +151,7 @@ Field</string>
</viewController>
<customObject id="1ho-ZO-Gkb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="67" y="46"/>
<point key="canvasLocation" x="67" y="69.5"/>
</scene>
<!--Folder-->
<scene sceneID="8By-fa-WDQ">
@ -189,7 +202,7 @@ Field</string>
</viewController>
<customObject id="4SD-ni-Scy" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="67" y="329"/>
<point key="canvasLocation" x="67" y="389"/>
</scene>
<!--Builtin Smart Feed-->
<scene sceneID="dFq-3d-JKW">
@ -231,7 +244,7 @@ Field</string>
</viewController>
<customObject id="3Xn-vX-2s9" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="67" y="553"/>
<point key="canvasLocation" x="67" y="613"/>
</scene>
<!--Nothing to inspect-->
<scene sceneID="lUc-e1-dN7">

View File

@ -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 = "<group>"; };
51FD40BD2341555600880194 /* UIImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage-Extensions.swift"; sourceTree = "<group>"; };
51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineUnreadCountView.swift; sourceTree = "<group>"; };
51FE10022345529D0056195D /* UserNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationManager.swift; sourceTree = "<group>"; };
557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsReaderAPIAccountView.swift; sourceTree = "<group>"; };
55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsReaderAPI.xib; sourceTree = "<group>"; };
55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = "<group>"; };
@ -1456,6 +1459,14 @@
path = "Article Extractor";
sourceTree = "<group>";
};
51FE0FF9234552490056195D /* UserNotifications */ = {
isa = PBXGroup;
children = (
51FE10022345529D0056195D /* UserNotificationManager.swift */,
);
path = UserNotifications;
sourceTree = "<group>";
};
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 */,

View File

@ -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<Article> 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)
}
}

View File

@ -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

View File

@ -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