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? { public var isArticleExtractorAlwaysOn: Bool? {
get { get {
return metadata.isArticleExtractorAlwaysOn return metadata.isArticleExtractorAlwaysOn

View File

@ -24,6 +24,7 @@ final class FeedMetadata: Codable {
case editedName case editedName
case authors case authors
case contentHash case contentHash
case isNotifyAboutNewArticles
case isArticleExtractorAlwaysOn case isArticleExtractorAlwaysOn
case conditionalGetInfo case conditionalGetInfo
case subscriptionID case subscriptionID
@ -78,10 +79,18 @@ final class FeedMetadata: Codable {
} }
} }
var isNotifyAboutNewArticles: Bool? {
didSet {
if isNotifyAboutNewArticles != oldValue {
valueDidChange(.isNotifyAboutNewArticles)
}
}
}
var isArticleExtractorAlwaysOn: Bool? { var isArticleExtractorAlwaysOn: Bool? {
didSet { didSet {
if isArticleExtractorAlwaysOn != oldValue { if isArticleExtractorAlwaysOn != oldValue {
valueDidChange(.contentHash) valueDidChange(.isArticleExtractorAlwaysOn)
} }
} }
} }

View File

@ -7,6 +7,7 @@
// //
import AppKit import AppKit
import UserNotifications
import Articles import Articles
import RSTree import RSTree
import RSWeb import RSWeb
@ -16,8 +17,9 @@ import RSCore
var appDelegate: AppDelegate! var appDelegate: AppDelegate!
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UnreadCountProvider { class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider {
var userNotificationManager: UserNotificationManager!
var faviconDownloader: FaviconDownloader! var faviconDownloader: FaviconDownloader!
var imageDownloader: ImageDownloader! var imageDownloader: ImageDownloader!
var authorAvatarDownloader: AuthorAvatarDownloader! var authorAvatarDownloader: AuthorAvatarDownloader!
@ -130,7 +132,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
} }
let localAccount = AccountManager.shared.defaultAccount let localAccount = AccountManager.shared.defaultAccount
DefaultFeedsImporter.importIfNeeded(isFirstRun, account: localAccount) DefaultFeedsImporter.importIfNeeded(isFirstRun, account: localAccount)
let tempDirectory = NSTemporaryDirectory() let tempDirectory = NSTemporaryDirectory()
let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String) let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String)
let cacheFolder = (tempDirectory as NSString).appendingPathComponent(bundleIdentifier) let cacheFolder = (tempDirectory as NSString).appendingPathComponent(bundleIdentifier)
@ -179,6 +181,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
refreshTimer = AccountRefreshTimer() refreshTimer = AccountRefreshTimer()
syncTimer = ArticleStatusSyncTimer() 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 #if RELEASE
debugMenuItem.menu?.removeItem(debugMenuItem) debugMenuItem.menu?.removeItem(debugMenuItem)
DispatchQueue.main.async { DispatchQueue.main.async {
@ -322,6 +335,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
return true return true
} }
// MARK: UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .badge, .sound])
}
// MARK: Add Feed // MARK: Add Feed
func addFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) { 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 { final class FeedInspectorViewController: NSViewController, Inspector {
@IBOutlet var imageView: NSImageView? @IBOutlet weak var imageView: NSImageView?
@IBOutlet var nameTextField: NSTextField? @IBOutlet weak var nameTextField: NSTextField?
@IBOutlet var homePageURLTextField: NSTextField? @IBOutlet weak var homePageURLTextField: NSTextField?
@IBOutlet var urlTextField: NSTextField? @IBOutlet weak var urlTextField: NSTextField?
@IBOutlet weak var isNotifyAboutNewArticlesCheckBox: NSButton!
@IBOutlet weak var isReaderViewAlwaysOnCheckBox: NSButton? @IBOutlet weak var isReaderViewAlwaysOnCheckBox: NSButton?
private var feed: Feed? { private var feed: Feed? {
@ -51,6 +52,10 @@ final class FeedInspectorViewController: NSViewController, Inspector {
} }
// MARK: Actions // MARK: Actions
@IBAction func isNotifyAboutNewArticlesChanged(_ sender: Any) {
feed?.isNotifyAboutNewArticles = (isNotifyAboutNewArticlesCheckBox?.state ?? .off) == .on ? true : false
}
@IBAction func isReaderViewAlwaysOnChanged(_ sender: Any) { @IBAction func isReaderViewAlwaysOnChanged(_ sender: Any) {
feed?.isArticleExtractorAlwaysOn = (isReaderViewAlwaysOnCheckBox?.state ?? .off) == .on ? true : false feed?.isArticleExtractorAlwaysOn = (isReaderViewAlwaysOnCheckBox?.state ?? .off) == .on ? true : false
} }
@ -89,6 +94,7 @@ private extension FeedInspectorViewController {
updateName() updateName()
updateHomePageURL() updateHomePageURL()
updateFeedURL() updateFeedURL()
updateNotifyAboutNewArticles()
updateIsReaderViewAlwaysOn() updateIsReaderViewAlwaysOn()
view.needsLayout = true view.needsLayout = true
@ -135,6 +141,10 @@ private extension FeedInspectorViewController {
urlTextField?.stringValue = feed?.url ?? "" urlTextField?.stringValue = feed?.url ?? ""
} }
func updateNotifyAboutNewArticles() {
isNotifyAboutNewArticlesCheckBox?.state = (feed?.isNotifyAboutNewArticles ?? false) ? .on : .off
}
func updateIsReaderViewAlwaysOn() { func updateIsReaderViewAlwaysOn() {
isReaderViewAlwaysOnCheckBox?.state = (feed?.isArticleExtractorAlwaysOn ?? false) ? .on : .off isReaderViewAlwaysOnCheckBox?.state = (feed?.isArticleExtractorAlwaysOn ?? false) ? .on : .off
} }

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-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> <dependencies>
<deployment identifier="macosx"/> <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"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@ -34,11 +34,11 @@
<objects> <objects>
<viewController title="Feed" storyboardIdentifier="Feed" showSeguePresentationStyle="single" id="sfH-oR-GXm" customClass="FeedInspectorViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController"> <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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="H9X-OG-K0p"> <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> <constraints>
<constraint firstAttribute="width" constant="48" id="1Cy-0w-dBg"/> <constraint firstAttribute="width" constant="48" id="1Cy-0w-dBg"/>
<constraint firstAttribute="height" constant="48" id="edb-lw-Ict"/> <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"/> <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSNetwork" id="MZ2-89-Bje"/>
</imageView> </imageView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="IWu-80-XC5"> <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> <constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="56" id="zV3-AX-gyC"/> <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="56" id="zV3-AX-gyC"/>
</constraints> </constraints>
@ -104,13 +104,24 @@ Field</string>
<action selector="isReaderViewAlwaysOnChanged:" target="sfH-oR-GXm" id="rsD-0e-ksP"/> <action selector="isReaderViewAlwaysOnChanged:" target="sfH-oR-GXm" id="rsD-0e-ksP"/>
</connections> </connections>
</button> </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> </subviews>
<constraints> <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="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="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="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="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="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="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"/> <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 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="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="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="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 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"/> <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> <connections>
<outlet property="homePageURLTextField" destination="zm0-15-BFy" id="0Jh-yy-mnF"/> <outlet property="homePageURLTextField" destination="zm0-15-BFy" id="0Jh-yy-mnF"/>
<outlet property="imageView" destination="H9X-OG-K0p" id="Rm6-X6-csH"/> <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="isReaderViewAlwaysOnCheckBox" destination="nH2-ab-KJ5" id="xPg-P5-3cr"/>
<outlet property="nameTextField" destination="IWu-80-XC5" id="zg4-5h-hoP"/> <outlet property="nameTextField" destination="IWu-80-XC5" id="zg4-5h-hoP"/>
<outlet property="urlTextField" destination="Vvk-KG-JlG" id="bcl-fq-3nQ"/> <outlet property="urlTextField" destination="Vvk-KG-JlG" id="bcl-fq-3nQ"/>
@ -138,7 +151,7 @@ Field</string>
</viewController> </viewController>
<customObject id="1ho-ZO-Gkb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="1ho-ZO-Gkb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="67" y="46"/> <point key="canvasLocation" x="67" y="69.5"/>
</scene> </scene>
<!--Folder--> <!--Folder-->
<scene sceneID="8By-fa-WDQ"> <scene sceneID="8By-fa-WDQ">
@ -189,7 +202,7 @@ Field</string>
</viewController> </viewController>
<customObject id="4SD-ni-Scy" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="4SD-ni-Scy" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="67" y="329"/> <point key="canvasLocation" x="67" y="389"/>
</scene> </scene>
<!--Builtin Smart Feed--> <!--Builtin Smart Feed-->
<scene sceneID="dFq-3d-JKW"> <scene sceneID="dFq-3d-JKW">
@ -231,7 +244,7 @@ Field</string>
</viewController> </viewController>
<customObject id="3Xn-vX-2s9" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="3Xn-vX-2s9" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="67" y="553"/> <point key="canvasLocation" x="67" y="613"/>
</scene> </scene>
<!--Nothing to inspect--> <!--Nothing to inspect-->
<scene sceneID="lUc-e1-dN7"> <scene sceneID="lUc-e1-dN7">

View File

@ -218,6 +218,8 @@
51FA73B72332D5F70090D516 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */; }; 51FA73B72332D5F70090D516 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */; };
51FD40C72341555A00880194 /* UIImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD40BD2341555600880194 /* UIImage-Extensions.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 */; }; 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 */; }; 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; };
55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; };
5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = "<group>"; };
@ -1456,6 +1459,14 @@
path = "Article Extractor"; path = "Article Extractor";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
51FE0FF9234552490056195D /* UserNotifications */ = {
isa = PBXGroup;
children = (
51FE10022345529D0056195D /* UserNotificationManager.swift */,
);
path = UserNotifications;
sourceTree = "<group>";
};
6581C73620CED60100F4AD34 /* SafariExtension */ = { 6581C73620CED60100F4AD34 /* SafariExtension */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1879,6 +1890,7 @@
849A97861ED9ECEF007D329B /* Article Styles */, 849A97861ED9ECEF007D329B /* Article Styles */,
84DAEE201F86CAE00058304B /* Importers */, 84DAEE201F86CAE00058304B /* Importers */,
8444C9011FED81880051386C /* Exporters */, 8444C9011FED81880051386C /* Exporters */,
51FE0FF9234552490056195D /* UserNotifications */,
84F2D5341FC22FCB00998D64 /* SmartFeeds */, 84F2D5341FC22FCB00998D64 /* SmartFeeds */,
848F6AE31FC29CFA002D422E /* Favicons */, 848F6AE31FC29CFA002D422E /* Favicons */,
845213211FCA5B10003B6E93 /* Images */, 845213211FCA5B10003B6E93 /* Images */,
@ -2830,6 +2842,7 @@
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */, 5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */,
512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */, 512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */,
51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */,
51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */, 51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */,
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */, 51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */, 51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */,
@ -2953,6 +2966,7 @@
5144EA43227A380F00D19003 /* ExportOPMLWindowController.swift in Sources */, 5144EA43227A380F00D19003 /* ExportOPMLWindowController.swift in Sources */,
842611A21FCB769D0086A189 /* RSHTMLMetadata+Extension.swift in Sources */, 842611A21FCB769D0086A189 /* RSHTMLMetadata+Extension.swift in Sources */,
84A1500520048DDF0046AD9A /* SendToMarsEditCommand.swift in Sources */, 84A1500520048DDF0046AD9A /* SendToMarsEditCommand.swift in Sources */,
51FE10032345529D0056195D /* UserNotificationManager.swift in Sources */,
D5907DB22004BB37005947E5 /* ScriptingObjectContainer.swift in Sources */, D5907DB22004BB37005947E5 /* ScriptingObjectContainer.swift in Sources */,
849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */, 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */,
8405DD8A2213E0E3008CE1BF /* DetailContainerView.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 RSCore
import RSWeb import RSWeb
import Account import Account
import UserNotifications
import BackgroundTasks import BackgroundTasks
import os.log import os.log
var appDelegate: AppDelegate! var appDelegate: AppDelegate!
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate, UnreadCountProvider { class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider {
private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
@ -34,6 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "application") var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "application")
var userNotificationManager: UserNotificationManager!
var faviconDownloader: FaviconDownloader! var faviconDownloader: FaviconDownloader!
var imageDownloader: ImageDownloader! var imageDownloader: ImageDownloader!
var authorAvatarDownloader: AuthorAvatarDownloader! var authorAvatarDownloader: AuthorAvatarDownloader!
@ -90,7 +90,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
} }
} }
} }
UNUserNotificationCenter.current().delegate = self
userNotificationManager = UserNotificationManager()
syncTimer = ArticleStatusSyncTimer() syncTimer = ArticleStatusSyncTimer()
#if DEBUG #if DEBUG
@ -171,6 +174,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
logMessage(message, type: .debug) logMessage(message, type: .debug)
} }
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .badge, .sound])
}
} }
// MARK: App Initialization // MARK: App Initialization

View File

@ -52,6 +52,9 @@ struct FeedInspectorView : View {
Spacer() Spacer()
}) { }) {
TextField("Feed Name", text: $viewModel.name) TextField("Feed Name", text: $viewModel.name)
Toggle(isOn: $viewModel.isNotifyAboutNewArticles) {
Text("Notify About New Articles")
}
Toggle(isOn: $viewModel.isArticleExtractorAlwaysOn) { Toggle(isOn: $viewModel.isArticleExtractorAlwaysOn) {
Text("Always Show Reader View") 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 { var isArticleExtractorAlwaysOn: Bool {
get { get {
return feed.isArticleExtractorAlwaysOn ?? false return feed.isArticleExtractorAlwaysOn ?? false