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