diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 5ef2b4958..039a2d5cd 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -847,6 +847,8 @@ D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */; }; DD82AB0A231003F6002269DF /* SharingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82AB09231003F6002269DF /* SharingTests.swift */; }; DFFB8FC2279B75E300AC21D7 /* Account in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4A24D343A500E90810 /* Account */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + DFFC199827A0D0D7004B7AEF /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFC199727A0D0D7004B7AEF /* NotificationsViewController.swift */; }; + DFFC199A27A0D32A004B7AEF /* NotificationsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFC199927A0D32A004B7AEF /* NotificationsTableViewCell.swift */; }; FF3ABF13232599810074C542 /* ArticleSorterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF09232599450074C542 /* ArticleSorterTests.swift */; }; FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; @@ -1583,6 +1585,8 @@ D5F4EDB620074D6500B9E363 /* WebFeed+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebFeed+Scriptability.swift"; sourceTree = ""; }; D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = ""; }; DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.swift; sourceTree = ""; }; + DFFC199727A0D0D7004B7AEF /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = ""; }; + DFFC199927A0D32A004B7AEF /* NotificationsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewCell.swift; sourceTree = ""; }; FF3ABF09232599450074C542 /* ArticleSorterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorterTests.swift; sourceTree = ""; }; FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorter.swift; sourceTree = ""; }; FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAsReadAlertController.swift; sourceTree = ""; }; @@ -1979,6 +1983,8 @@ 51A16993235E10D600EB091F /* SettingsViewController.swift */, 5108F6D12375EED2001ABC45 /* TimelineCustomizerViewController.swift */, 5108F6D32375EEEF001ABC45 /* TimelinePreviewTableViewController.swift */, + DFFC199727A0D0D7004B7AEF /* NotificationsViewController.swift */, + DFFC199927A0D32A004B7AEF /* NotificationsTableViewCell.swift */, ); path = Settings; sourceTree = ""; @@ -4031,6 +4037,7 @@ 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */, B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */, 518ED21D23D0F26000E0A862 /* UIViewController-Extensions.swift in Sources */, + DFFC199827A0D0D7004B7AEF /* NotificationsViewController.swift in Sources */, 51A9A5F52380F6A60033AADF /* ModalNavigationController.swift in Sources */, 51EAED96231363EF00A9EEE3 /* NonIntrinsicButton.swift in Sources */, 51C4527B2265091600C03939 /* MasterUnreadIndicatorView.swift in Sources */, @@ -4042,6 +4049,7 @@ 51C45291226509C800C03939 /* SmartFeed.swift in Sources */, 51C452A722650A3D00C03939 /* RSImage-Extensions.swift in Sources */, 511B9807237DCAC90028BCAA /* UserInfoKey.swift in Sources */, + DFFC199A27A0D32A004B7AEF /* NotificationsTableViewCell.swift in Sources */, 51C45269226508F600C03939 /* MasterFeedTableViewCell.swift in Sources */, 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */, 517630232336657E00E15FFF /* WebViewProvider.swift in Sources */, diff --git a/iOS/Settings/NotificationsTableViewCell.swift b/iOS/Settings/NotificationsTableViewCell.swift new file mode 100644 index 000000000..1058687ec --- /dev/null +++ b/iOS/Settings/NotificationsTableViewCell.swift @@ -0,0 +1,74 @@ +// +// NotificationsTableViewCell.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 26/01/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import UIKit +import Account +import UserNotifications + +extension Notification.Name { + static let NotificationPreferencesDidUpdate = Notification.Name("NotificationPreferencesDidUpdate") +} + +class NotificationsTableViewCell: UITableViewCell { + + @IBOutlet weak var notificationsSwitch: UISwitch! + @IBOutlet weak var notificationsLabel: UILabel! + @IBOutlet weak var notificationsImageView: UIImageView! + var feed: WebFeed? + + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + func configure(_ webFeed: WebFeed, _ status: UNAuthorizationStatus) { + self.feed = webFeed + var isOn = false + if webFeed.isNotifyAboutNewArticles == nil { + isOn = false + } else { + isOn = webFeed.isNotifyAboutNewArticles! + } + notificationsSwitch.isOn = isOn + notificationsSwitch.addTarget(self, action: #selector(toggleWebFeedNotification(_:)), for: .touchUpInside) + if status == .denied { notificationsSwitch.isEnabled = false } + notificationsLabel.text = webFeed.nameForDisplay + notificationsImageView.image = webFeed.smallIcon?.image + notificationsImageView.layer.cornerRadius = 4 + } + + @objc + private func toggleWebFeedNotification(_ sender: Any) { + guard let feed = feed else { + return + } + if feed.isNotifyAboutNewArticles == nil { + feed.isNotifyAboutNewArticles = true + } + else { + feed.isNotifyAboutNewArticles!.toggle() + } + } + + @objc + private func requestNotificationPermissions(_ sender: Any) { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in + NotificationCenter.default.post(name: .NotificationPreferencesDidUpdate, object: nil) + } + } + + + +} diff --git a/iOS/Settings/NotificationsViewController.swift b/iOS/Settings/NotificationsViewController.swift new file mode 100644 index 000000000..f72e76567 --- /dev/null +++ b/iOS/Settings/NotificationsViewController.swift @@ -0,0 +1,103 @@ +// +// NotificationsViewController.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 26/01/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import UIKit +import Account +import UserNotifications + +class NotificationsViewController: UIViewController { + + @IBOutlet weak var notificationsTableView: UITableView! + private var status: UNAuthorizationStatus = .notDetermined + + + override func viewDidLoad() { + super.viewDidLoad() + self.title = NSLocalizedString("New Article Notifications", comment: "Notifications") + notificationsTableView.sectionHeaderTopPadding = 25 + + NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: .FaviconDidBecomeAvailable, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: .NotificationPreferencesDidUpdate, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: UIScene.willEnterForegroundNotification, object: nil) + + reloadNotificationTableView() + } + + @objc + private func reloadNotificationTableView(_ sender: Any? = nil) { + UNUserNotificationCenter.current().getNotificationSettings { settings in + DispatchQueue.main.async { + self.status = settings.authorizationStatus + self.notificationsTableView.reloadData() + } + } + } + + private func sortedWebFeedsForAccount(_ account: Account) -> [WebFeed] { + return Array(account.flattenedWebFeeds()).sorted(by: { $0.nameForDisplay.caseInsensitiveCompare($1.nameForDisplay) == .orderedAscending }) + } + +} + +// MARK: UITableViewDataSource +extension NotificationsViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + if status == .denied { return 1 } + return 1 + AccountManager.shared.activeAccounts.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0 { + if status == .denied { return 1 } + return 0 + } + return AccountManager.shared.sortedActiveAccounts[section - 1].flattenedWebFeeds().count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + + if indexPath.section == 0 { + let openSettingsCell = tableView.dequeueReusableCell(withIdentifier: "OpenSettingsCell") as! VibrantBasicTableViewCell + return openSettingsCell + } else { + let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell + let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1] + let feed = sortedWebFeedsForAccount(account)[indexPath.row] + cell.configure(feed, status) + return cell + } + } + + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + if section == 0 { return nil } + return AccountManager.shared.sortedActiveAccounts[section - 1].nameForDisplay + } + + func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + if section == 0 { + if status == .denied { + return NSLocalizedString("Notification permissions are currently denied. Enable notifications in the Settings app.", comment: "Notifications denied.") + } + } + return nil + } +} + +extension NotificationsViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!) + } + + +} diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index e36af8d35..385a825db 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -1,9 +1,8 @@ - + - - + @@ -37,19 +36,36 @@ + + + + + + + + + + + - + - +