diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift
index bbc6cd424..d5808dcc5 100644
--- a/iOS/AppAssets.swift
+++ b/iOS/AppAssets.swift
@@ -54,6 +54,10 @@ struct AppAssets {
static var accountTheOldReaderImage: UIImage = {
return UIImage(named: "accountTheOldReader")!
}()
+
+ static var appBadgeImage: UIImage = {
+ return UIImage(systemName: "app.badge")!
+ }()
static var articleExtractorError: UIImage = {
return UIImage(named: "articleExtractorError")!
@@ -181,6 +185,10 @@ struct AppAssets {
return UIImage(systemName: "ellipsis.circle")!
}()
+ static var moreImageFill: UIImage = {
+ return UIImage(systemName: "ellipsis.circle.fill")!
+ }()
+
static var nextArticleImage: UIImage = {
return UIImage(systemName: "chevron.down")!
}()
diff --git a/iOS/Settings/NotificationsTableViewCell.swift b/iOS/Settings/NotificationsTableViewCell.swift
index 1058687ec..cf0c66beb 100644
--- a/iOS/Settings/NotificationsTableViewCell.swift
+++ b/iOS/Settings/NotificationsTableViewCell.swift
@@ -10,11 +10,8 @@ import UIKit
import Account
import UserNotifications
-extension Notification.Name {
- static let NotificationPreferencesDidUpdate = Notification.Name("NotificationPreferencesDidUpdate")
-}
-class NotificationsTableViewCell: UITableViewCell {
+class NotificationsTableViewCell: VibrantBasicTableViewCell {
@IBOutlet weak var notificationsSwitch: UISwitch!
@IBOutlet weak var notificationsLabel: UILabel!
@@ -33,7 +30,7 @@ class NotificationsTableViewCell: UITableViewCell {
// Configure the view for the selected state
}
- func configure(_ webFeed: WebFeed, _ status: UNAuthorizationStatus) {
+ func configure(_ webFeed: WebFeed) {
self.feed = webFeed
var isOn = false
if webFeed.isNotifyAboutNewArticles == nil {
@@ -43,9 +40,8 @@ class NotificationsTableViewCell: UITableViewCell {
}
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.image = IconImageCache.shared.imageFor(webFeed.feedID!)?.image
notificationsImageView.layer.cornerRadius = 4
}
@@ -61,14 +57,5 @@ class NotificationsTableViewCell: UITableViewCell {
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
index f72e76567..61b4ed398 100644
--- a/iOS/Settings/NotificationsViewController.swift
+++ b/iOS/Settings/NotificationsViewController.swift
@@ -11,22 +11,47 @@ import Account
import UserNotifications
class NotificationsViewController: UIViewController {
-
+
@IBOutlet weak var notificationsTableView: UITableView!
+
+ private lazy var searchController: UISearchController = {
+ let searchController = UISearchController(searchResultsController: nil)
+ searchController.searchBar.placeholder = NSLocalizedString("Find a feed", comment: "Find a feed")
+ searchController.searchBar.searchBarStyle = .minimal
+ searchController.delegate = self
+ searchController.searchBar.delegate = self
+ searchController.searchBar.sizeToFit()
+ searchController.obscuresBackgroundDuringPresentation = false
+ searchController.hidesNavigationBarDuringPresentation = false
+ self.definesPresentationContext = true
+ return searchController
+ }()
private var status: UNAuthorizationStatus = .notDetermined
+ private var newArticleNotificationFilter: Bool = false {
+ didSet {
+ filterButton.menu = notificationFilterMenu()
+ }
+ }
+ private var filterButton: UIBarButtonItem!
-
- override func viewDidLoad() {
- super.viewDidLoad()
- self.title = NSLocalizedString("New Article Notifications", comment: "Notifications")
- notificationsTableView.sectionHeaderTopPadding = 25
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ title = NSLocalizedString("New Article Notifications", comment: "Notifications")
- 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)
+ notificationsTableView.prefetchDataSource = self
+ navigationItem.searchController = searchController
+
+ filterButton = UIBarButtonItem(
+ title: nil,
+ image: AppAssets.moreImage,
+ primaryAction: nil,
+ menu: notificationFilterMenu())
+
+ navigationItem.rightBarButtonItem = filterButton
reloadNotificationTableView()
+
+ NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: UIScene.willEnterForegroundNotification, object: nil)
}
@objc
@@ -34,18 +59,114 @@ class NotificationsViewController: UIViewController {
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
self.status = settings.authorizationStatus
+ if self.status != .authorized {
+ self.filterButton.isEnabled = false
+ self.newArticleNotificationFilter = false
+ }
self.notificationsTableView.reloadData()
}
}
}
+ private func notificationFilterMenu() -> UIMenu {
+
+ if filterButton != nil {
+ if newArticleNotificationFilter {
+ filterButton.image = AppAssets.moreImageFill
+ } else {
+ filterButton.image = AppAssets.moreImage
+ }
+ }
+
+
+ let filterMenu = UIMenu(title: "",
+ image: nil,
+ identifier: nil,
+ options: [.displayInline],
+ children: [
+ UIAction(
+ title: NSLocalizedString("Show Feeds with Notifications Enabled", comment: "Feeds with Notifications"),
+ image: nil,
+ identifier: nil,
+ discoverabilityTitle: nil,
+ attributes: [],
+ state: newArticleNotificationFilter ? .on : .off,
+ handler: { [weak self] _ in
+ self?.newArticleNotificationFilter.toggle()
+ self?.notificationsTableView.reloadData()
+ })])
+
+
+ var menus = [UIMenuElement]()
+ menus.append(filterMenu)
+
+ for account in AccountManager.shared.sortedActiveAccounts {
+ let accountMenu = UIMenu(title: account.nameForDisplay, image: nil, identifier: nil, options: .singleSelection, children: [enableAllAction(for: account), disableAllAction(for: account)])
+ menus.append(accountMenu)
+ }
+
+ let combinedMenu = UIMenu(title: "",
+ image: nil,
+ identifier: nil,
+ options: .displayInline,
+ children: menus)
+
+ return combinedMenu
+ }
+
+ private func enableAllAction(for account: Account) -> UIAction {
+ let action = UIAction(title: NSLocalizedString("Enable All Notifications", comment: "Enable All"),
+ image: nil,
+ identifier: nil,
+ discoverabilityTitle: nil,
+ attributes: [],
+ state: .off) { [weak self] _ in
+ for feed in account.flattenedWebFeeds() {
+ feed.isNotifyAboutNewArticles = true
+ }
+ self?.notificationsTableView.reloadData()
+ self?.filterButton.menu = self?.notificationFilterMenu()
+ }
+ return action
+ }
+
+ private func disableAllAction(for account: Account) -> UIAction {
+ let action = UIAction(title: NSLocalizedString("Disable All Notifications", comment: "Disable All"),
+ image: nil,
+ identifier: nil,
+ discoverabilityTitle: nil,
+ attributes: [],
+ state: .off) { [weak self] _ in
+ for feed in account.flattenedWebFeeds() {
+ feed.isNotifyAboutNewArticles = false
+ }
+ self?.notificationsTableView.reloadData()
+ self?.filterButton.menu = self?.notificationFilterMenu()
+ }
+ return action
+ }
+
+ // MARK: - Feed Filtering
+
private func sortedWebFeedsForAccount(_ account: Account) -> [WebFeed] {
return Array(account.flattenedWebFeeds()).sorted(by: { $0.nameForDisplay.caseInsensitiveCompare($1.nameForDisplay) == .orderedAscending })
}
-
+
+ private func filteredWebFeeds(_ searchText: String? = "", account: Account) -> [WebFeed] {
+ sortedWebFeedsForAccount(account).filter { feed in
+ return feed.nameForDisplay.lowercased().contains(searchText!.lowercased())
+ }
+ }
+
+ private func feedsWithNotificationsEnabled(_ account: Account) -> [WebFeed] {
+ sortedWebFeedsForAccount(account).filter { feed in
+ return feed.isNotifyAboutNewArticles == true
+ }
+ }
+
}
-// MARK: UITableViewDataSource
+// MARK: - UITableViewDataSource
extension NotificationsViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
@@ -58,25 +179,40 @@ extension NotificationsViewController: UITableViewDataSource {
if status == .denied { return 1 }
return 0
}
- return AccountManager.shared.sortedActiveAccounts[section - 1].flattenedWebFeeds().count
+ if searchController.isActive {
+ return filteredWebFeeds(searchController.searchBar.text, account: AccountManager.shared.sortedActiveAccounts[section - 1]).count
+ } else if newArticleNotificationFilter == true {
+ return feedsWithNotificationsEnabled(AccountManager.shared.sortedActiveAccounts[section - 1]).count
+ } else {
+ 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
+ if searchController.isActive {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
+ let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
+ cell.configure(filteredWebFeeds(searchController.searchBar.text, account: account)[indexPath.row])
+ return cell
+ } else if newArticleNotificationFilter == true {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
+ let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
+ cell.configure(feedsWithNotificationsEnabled(account)[indexPath.row])
+ return cell
+ } else {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
+ let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
+ cell.configure(sortedWebFeedsForAccount(account)[indexPath.row])
+ return cell
+ }
}
}
-
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section == 0 { return nil }
return AccountManager.shared.sortedActiveAccounts[section - 1].nameForDisplay
@@ -92,12 +228,55 @@ extension NotificationsViewController: UITableViewDataSource {
}
}
+
+// MARK: - UITableViewDelegate
extension NotificationsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
- UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
+ if indexPath.section == 0 {
+ UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
+ }
}
+}
+
+
+extension NotificationsViewController: UITableViewDataSourcePrefetching {
+
+ func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
+ for path in indexPaths {
+ let account = AccountManager.shared.sortedActiveAccounts[path.section - 1]
+ let feed = sortedWebFeedsForAccount(account)[path.row]
+ let _ = IconImageCache.shared.imageFor(feed.feedID!)
+ }
+ }
+
+}
+
+
+// MARK: - UISearchControllerDelegate
+extension NotificationsViewController: UISearchControllerDelegate {
+
+ func didDismissSearchController(_ searchController: UISearchController) {
+ print(#function)
+ searchController.isActive = false
+ notificationsTableView.reloadData()
+ }
+
+}
+
+// MARK: - UISearchBarDelegate
+extension NotificationsViewController: UISearchBarDelegate {
+
+ func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
+ searchController.isActive = true
+ newArticleNotificationFilter = false
+ notificationsTableView.reloadData()
+ }
+
+ func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
+ notificationsTableView.reloadData()
+ }
}
diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard
index d90a425b8..b6cf12d35 100644
--- a/iOS/Settings/Settings.storyboard
+++ b/iOS/Settings/Settings.storyboard
@@ -43,8 +43,8 @@
-
-
+
-
+
+
@@ -1329,6 +1330,9 @@
+
+
+
diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift
index 2131e8939..6bd34618e 100644
--- a/iOS/Settings/SettingsViewController.swift
+++ b/iOS/Settings/SettingsViewController.swift
@@ -47,6 +47,8 @@ class SettingsViewController: UITableViewController {
tableView.register(UINib(nibName: "SettingsComboTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsComboTableViewCell")
tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell")
+ refreshNotificationStatus()
+
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 44
}
@@ -72,7 +74,6 @@ class SettingsViewController: UITableViewController {
refreshClearsReadArticlesSwitch.isOn = false
}
-
articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name
if AppDefaults.shared.confirmMarkAllAsRead {
@@ -106,7 +107,6 @@ class SettingsViewController: UITableViewController {
tableView.scrollToRow(at: IndexPath(row: 0, section: 4), at: .top, animated: true)
scrollToArticlesSection = false
}
- refreshNotificationStatus()
}
@objc