From 21cf8415d2045bc98bdb0bfa921fb30b5013dffe Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 11 Jan 2020 11:30:16 -0700 Subject: [PATCH] Add confirmation for Mark As Read actions. Issue #1603 --- NetNewsWire.xcodeproj/project.pbxproj | 8 +-- iOS/AppDefaults.swift | 10 +-- iOS/MasterFeed/MasterFeedViewController.swift | 31 +++++++++- .../MarkAsReadAlertController.swift | 62 +++++++++++++++++++ .../MasterTimelineViewController.swift | 53 ++++++++++------ .../UndoAvailableAlertController.swift | 31 ---------- iOS/SceneCoordinator.swift | 8 +-- iOS/Settings/Settings.storyboard | 61 +++++++++++++----- iOS/Settings/SettingsViewController.swift | 26 +++++++- 9 files changed, 208 insertions(+), 82 deletions(-) create mode 100644 iOS/MasterTimeline/MarkAsReadAlertController.swift delete mode 100644 iOS/MasterTimeline/UndoAvailableAlertController.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index baec952ba..87fd1ee4a 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -651,7 +651,7 @@ 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 */; }; - FFD43E412340F488009E5CA3 /* UndoAvailableAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD43E372340F320009E5CA3 /* UndoAvailableAlertController.swift */; }; + FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1592,7 +1592,7 @@ DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.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 /* UndoAvailableAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoAvailableAlertController.swift; sourceTree = ""; }; + FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAsReadAlertController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1929,7 +1929,7 @@ 5148F4542336DB7000F8CD8B /* MasterTimelineTitleView.swift */, 5148F44A2336DB4700F8CD8B /* MasterTimelineTitleView.xib */, 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */, - FFD43E372340F320009E5CA3 /* UndoAvailableAlertController.swift */, + FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */, 51C4526F2265091600C03939 /* Cell */, ); path = MasterTimeline; @@ -3941,7 +3941,7 @@ 51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */, 5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */, 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */, - FFD43E412340F488009E5CA3 /* UndoAvailableAlertController.swift in Sources */, + FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */, 51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */, 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, 51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */, diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index ddf3789c5..21e636d05 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -25,7 +25,7 @@ struct AppDefaults { static let timelineIconSize = "timelineIconSize" static let timelineSortDirection = "timelineSortDirection" static let articleFullscreenEnabled = "articleFullscreenEnabled" - static let displayUndoAvailableTip = "displayUndoAvailableTip" + static let confirmMarkAllAsRead = "confirmMarkAllAsRead" static let lastRefresh = "lastRefresh" static let addWebFeedAccountID = "addWebFeedAccountID" static let addWebFeedFolderName = "addWebFeedFolderName" @@ -112,12 +112,12 @@ struct AppDefaults { } } - static var displayUndoAvailableTip: Bool { + static var confirmMarkAllAsRead: Bool { get { - return bool(for: Key.displayUndoAvailableTip) + return bool(for: Key.confirmMarkAllAsRead) } set { - setBool(for: Key.displayUndoAvailableTip, newValue) + setBool(for: Key.confirmMarkAllAsRead, newValue) } } @@ -157,7 +157,7 @@ struct AppDefaults { Key.timelineIconSize: IconSize.medium.rawValue, Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, Key.articleFullscreenEnabled: false, - Key.displayUndoAvailableTip: true] + Key.confirmMarkAllAsRead: true] AppDefaults.shared.register(defaults: defaults) } diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index ce32cb10a..93637ca39 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -281,6 +281,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { alert.addAction(action) } + if let action = self.markAllAsReadAlertAction(indexPath: indexPath, completion: completion) { + alert.addAction(action) + } + let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel) { _ in completion(true) @@ -1017,6 +1021,29 @@ private extension MasterFeedViewController { return action } + func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { + guard let node = dataSource.itemIdentifier(for: indexPath), + coordinator.unreadCountFor(node) > 0, + let feed = node.representedObject as? WebFeed, + let articles = try? feed.fetchArticles() else { + return nil + } + + let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") + let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String + let cancel = { + completion(true) + } + + let action = UIAlertAction(title: title, style: .default) { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in + self?.coordinator.markAllAsRead(Array(articles)) + completion(true) + } + } + return action + } + func deleteAction(indexPath: IndexPath) -> UIAction { let title = NSLocalizedString("Delete", comment: "Delete") @@ -1108,7 +1135,9 @@ private extension MasterFeedViewController { let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, nameForDisplay) as String let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in - self?.coordinator.markAllAsRead(articles) + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in + self?.coordinator.markAllAsRead(articles) + } } return action diff --git a/iOS/MasterTimeline/MarkAsReadAlertController.swift b/iOS/MasterTimeline/MarkAsReadAlertController.swift new file mode 100644 index 000000000..670b2c637 --- /dev/null +++ b/iOS/MasterTimeline/MarkAsReadAlertController.swift @@ -0,0 +1,62 @@ +// +// UndoAvailableAlertController.swift +// NetNewsWire +// +// Created by Phil Viso on 9/29/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation +import UIKit + +struct MarkAsReadAlertController { + + static func confirm(_ controller: UIViewController?, + coordinator: SceneCoordinator?, + confirmTitle: String, + cancelCompletion: (() -> Void)? = nil, + completion: @escaping () -> Void) { + + guard let controller = controller, let coordinator = coordinator else { + completion() + return + } + + if AppDefaults.confirmMarkAllAsRead { + let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion) { _ in + completion() + } + controller.present(alertController, animated: true) + } else { + completion() + } + } + + private static func alert(coordinator: SceneCoordinator, + confirmTitle: String, + cancelCompletion: (() -> Void)?, + completion: @escaping (UIAlertAction) -> Void) -> UIAlertController { + + let title = NSLocalizedString("Mark As Read", comment: "Mark As Read") + let message = NSLocalizedString("You can turn this confirmation off in settings.", + comment: "You can turn this confirmation off in settings.") + let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") + let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in + cancelCompletion?() + } + let settingsAction = UIAlertAction(title: settingsTitle, style: .default) { _ in + coordinator.showSettings(scrollToArticlesSection: true) + } + let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: completion) + + alertController.addAction(markAction) + alertController.addAction(settingsAction) + alertController.addAction(cancelAction) + + return alertController + } + +} diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 7d4dca9c5..5c65b8d1a 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -121,15 +121,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } @IBAction func markAllAsRead(_ sender: Any) { - if coordinator.displayUndoAvailableTip { - let alertController = UndoAvailableAlertController.alert { [weak self] _ in - self?.coordinator.displayUndoAvailableTip = false - self?.coordinator.markAllAsReadInTimeline() - } - - present(alertController, animated: true) - } else { - coordinator.markAllAsReadInTimeline() + let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read") + MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title) { [weak self] in + self?.coordinator.markAllAsReadInTimeline() } } @@ -710,7 +704,9 @@ private extension MasterTimelineViewController { let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read") let image = AppAssets.markAboveAsReadImage let action = UIAction(title: title, image: image) { [weak self] action in - self?.coordinator.markAboveAsRead(article) + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in + self?.coordinator.markAboveAsRead(article) + } } return action } @@ -723,7 +719,9 @@ private extension MasterTimelineViewController { let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read") let image = AppAssets.markBelowAsReadImage let action = UIAction(title: title, image: image) { [weak self] action in - self?.coordinator.markBelowAsRead(article) + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in + self?.coordinator.markBelowAsRead(article) + } } return action } @@ -734,10 +732,16 @@ private extension MasterTimelineViewController { } let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read") - let action = UIAlertAction(title: title, style: .default) { [weak self] action in - self?.coordinator.markAboveAsRead(article) + let cancel = { completion(true) } + + let action = UIAlertAction(title: title, style: .default) { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in + self?.coordinator.markAboveAsRead(article) + completion(true) + } + } return action } @@ -747,10 +751,16 @@ private extension MasterTimelineViewController { } let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read") - let action = UIAlertAction(title: title, style: .default) { [weak self] action in - self?.coordinator.markBelowAsRead(article) + let cancel = { completion(true) } + + let action = UIAlertAction(title: title, style: .default) { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in + self?.coordinator.markBelowAsRead(article) + completion(true) + } + } return action } @@ -790,7 +800,9 @@ private extension MasterTimelineViewController { let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in - self?.coordinator.markAllAsRead(articles) + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in + self?.coordinator.markAllAsRead(articles) + } } return action } @@ -808,10 +820,15 @@ private extension MasterTimelineViewController { let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Mark All as Read in Feed") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String + let cancel = { + completion(true) + } let action = UIAlertAction(title: title, style: .default) { [weak self] action in - self?.coordinator.markAllAsRead(articles) - completion(true) + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in + self?.coordinator.markAllAsRead(articles) + completion(true) + } } return action } diff --git a/iOS/MasterTimeline/UndoAvailableAlertController.swift b/iOS/MasterTimeline/UndoAvailableAlertController.swift deleted file mode 100644 index 0d9ea4767..000000000 --- a/iOS/MasterTimeline/UndoAvailableAlertController.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// UndoAvailableAlertController.swift -// NetNewsWire -// -// Created by Phil Viso on 9/29/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import Foundation -import UIKit - -struct UndoAvailableAlertController { - - static func alert(handler: @escaping (UIAlertAction) -> Void) -> UIAlertController { - let title = NSLocalizedString("Undo Available", comment: "Undo Available") - let message = NSLocalizedString("You can undo this and other actions with a three finger swipe to the left.", - comment: "Mark all articles") - let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") - let confirmTitle = NSLocalizedString("Got It", comment: "Got It") - - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) - let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: handler) - - alertController.addAction(cancelAction) - alertController.addAction(markAction) - - return alertController - } - -} diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 07ea13baa..8158c7a0f 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -92,11 +92,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { var prefersStatusBarHidden = false - var displayUndoAvailableTip: Bool { - get { AppDefaults.displayUndoAvailableTip } - set { AppDefaults.displayUndoAvailableTip = newValue } - } - private let treeControllerDelegate = WebFeedTreeControllerDelegate() private let treeController: TreeController @@ -964,9 +959,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } } - func showSettings() { + func showSettings(scrollToArticlesSection: Bool = false) { let settingsNavController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController let settingsViewController = settingsNavController.topViewController as! SettingsViewController + settingsViewController.scrollToArticlesSection = scrollToArticlesSection settingsNavController.modalPresentationStyle = .formSheet settingsViewController.presentingParentController = rootSplitViewController rootSplitViewController.present(settingsNavController, animated: true) diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index 6555971dc..09a0ad8bc 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -207,14 +207,14 @@ - + - + - - - + + @@ -231,9 +231,39 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -266,7 +296,7 @@ - + @@ -283,7 +313,7 @@ - + @@ -300,7 +330,7 @@ - + @@ -317,14 +347,14 @@ - + - + @@ -351,7 +381,7 @@ - + @@ -368,7 +398,7 @@ - + @@ -385,7 +415,7 @@ - + @@ -417,6 +447,7 @@ + diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index 2bfaa9819..2c1739d32 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -18,8 +18,10 @@ class SettingsViewController: UITableViewController { @IBOutlet weak var timelineSortOrderSwitch: UISwitch! @IBOutlet weak var groupByFeedSwitch: UISwitch! @IBOutlet weak var refreshClearsReadArticlesSwitch: UISwitch! + @IBOutlet weak var confirmMarkAllAsReadSwitch: UISwitch! @IBOutlet weak var showFullscreenArticlesSwitch: UISwitch! + var scrollToArticlesSection = false weak var presentingParentController: UIViewController? override func viewDidLoad() { @@ -34,7 +36,7 @@ class SettingsViewController: UITableViewController { tableView.register(UINib(nibName: "SettingsAccountTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsAccountTableViewCell") tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell") - + } override func viewWillAppear(_ animated: Bool) { @@ -58,6 +60,12 @@ class SettingsViewController: UITableViewController { refreshClearsReadArticlesSwitch.isOn = false } + if AppDefaults.confirmMarkAllAsRead { + confirmMarkAllAsReadSwitch.isOn = true + } else { + confirmMarkAllAsReadSwitch.isOn = false + } + if AppDefaults.articleFullscreenEnabled { showFullscreenArticlesSwitch.isOn = true } else { @@ -81,6 +89,12 @@ class SettingsViewController: UITableViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + + if scrollToArticlesSection { + tableView.scrollToRow(at: IndexPath(row: 0, section: 4), at: .top, animated: true) + scrollToArticlesSection = false + } + } // MARK: UITableView @@ -195,7 +209,7 @@ class SettingsViewController: UITableViewController { } case 3: switch indexPath.row { - case 3: + case 4: let timeline = UIStoryboard.settings.instantiateController(ofType: TimelineCustomizerViewController.self) self.navigationController?.pushViewController(timeline, animated: true) default: @@ -285,6 +299,14 @@ class SettingsViewController: UITableViewController { } } + @IBAction func switchConfirmMarkAllAsRead(_ sender: Any) { + if confirmMarkAllAsReadSwitch.isOn { + AppDefaults.confirmMarkAllAsRead = true + } else { + AppDefaults.confirmMarkAllAsRead = false + } + } + @IBAction func switchFullscreenArticles(_ sender: Any) { if showFullscreenArticlesSwitch.isOn { AppDefaults.articleFullscreenEnabled = true