From 81c4756f9787d251884d80304afcbdb447374338 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 27 Nov 2019 11:43:36 -0600 Subject: [PATCH] Persist article read filters across application launches. Issue #1349 --- Frameworks/Account/ContainerIdentifier.swift | 4 +- Frameworks/Account/FeedIdentifier.swift | 6 +-- Shared/UserInfoKey.swift | 2 +- .../MasterTimelineViewController.swift | 25 +++++---- iOS/SceneCoordinator.swift | 51 ++++++++++++++----- 5 files changed, 56 insertions(+), 32 deletions(-) diff --git a/Frameworks/Account/ContainerIdentifier.swift b/Frameworks/Account/ContainerIdentifier.swift index 47d87e4b6..1797787c1 100644 --- a/Frameworks/Account/ContainerIdentifier.swift +++ b/Frameworks/Account/ContainerIdentifier.swift @@ -17,7 +17,7 @@ public enum ContainerIdentifier: Hashable { case account(String) // accountID case folder(String, String) // accountID, folderName - public var userInfo: [AnyHashable: Any] { + public var userInfo: [AnyHashable: AnyHashable] { switch self { case .smartFeedController: return [ @@ -37,7 +37,7 @@ public enum ContainerIdentifier: Hashable { } } - public init?(userInfo: [AnyHashable: Any]) { + public init?(userInfo: [AnyHashable: AnyHashable]) { guard let type = userInfo["type"] as? String else { return nil } switch type { diff --git a/Frameworks/Account/FeedIdentifier.swift b/Frameworks/Account/FeedIdentifier.swift index 6b462b99f..1bd3230d7 100644 --- a/Frameworks/Account/FeedIdentifier.swift +++ b/Frameworks/Account/FeedIdentifier.swift @@ -12,7 +12,7 @@ public protocol FeedIdentifiable { var feedID: FeedIdentifier? { get } } -public enum FeedIdentifier: CustomStringConvertible { +public enum FeedIdentifier: CustomStringConvertible, Hashable { case smartFeed(String) // String is a unique identifier case script(String) // String is a unique identifier @@ -32,7 +32,7 @@ public enum FeedIdentifier: CustomStringConvertible { } } - public var userInfo: [AnyHashable: Any] { + public var userInfo: [AnyHashable: AnyHashable] { switch self { case .smartFeed(let id): return [ @@ -59,7 +59,7 @@ public enum FeedIdentifier: CustomStringConvertible { } } - public init?(userInfo: [AnyHashable: Any]) { + public init?(userInfo: [AnyHashable: AnyHashable]) { guard let type = userInfo["type"] as? String else { return nil } switch type { diff --git a/Shared/UserInfoKey.swift b/Shared/UserInfoKey.swift index 1823643c7..d095dd5c0 100644 --- a/Shared/UserInfoKey.swift +++ b/Shared/UserInfoKey.swift @@ -26,5 +26,5 @@ struct UserInfoKey { static let windowState = "windowState" static let containerExpandedWindowState = "containerExpandedWindowState" static let readFeedsFilterState = "readFeedsFilterState" - + static let readArticlesFilterState = "readArticlesFilterState" } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 7ce7a0123..d79add2cd 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -103,15 +103,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner // MARK: Actions @IBAction func toggleFilter(_ sender: Any) { - switch coordinator.articleReadFilterType { - case .none: - filterButton.image = AppAssets.filterActiveImage - coordinator.hideUnreadArticles() - case .read: + if coordinator.isReadArticlesFiltered { filterButton.image = AppAssets.filterInactiveImage coordinator.showAllArticles() - case .alwaysRead: - break + } else { + filterButton.image = AppAssets.filterActiveImage + coordinator.hideReadArticles() } } @@ -524,17 +521,19 @@ private extension MasterTimelineViewController { navigationItem.titleView = titleView } - switch coordinator.articleReadFilterType { - case .none: + switch coordinator.timelineDefaultReadFilterType { + case .none, .read: filterButton.isHidden = false - filterButton.image = AppAssets.filterInactiveImage - case .read: - filterButton.isHidden = false - filterButton.image = AppAssets.filterActiveImage case .alwaysRead: filterButton.isHidden = true } + if coordinator.isReadArticlesFiltered { + filterButton.image = AppAssets.filterActiveImage + } else { + filterButton.image = AppAssets.filterInactiveImage + } + tableView.selectRow(at: nil, animated: false, scrollPosition: .top) if dataSource.snapshot().itemIdentifiers(inSection: 0).count > 0 { tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 9ae4c4ae7..c16755a5a 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -67,6 +67,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { private var animatingChanges = false private var expandedTable = Set() + private var readFilterEnabledTable = [FeedIdentifier: Bool]() private var shadowTable = [[Node]]() private var lastSearchString = "" private var lastSearchScope: SearchScope? = nil @@ -121,7 +122,17 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return treeControllerDelegate.isReadFiltered } - var articleReadFilterType: ReadFilterType = .none + var isReadArticlesFiltered: Bool { + if let feedID = timelineFeed?.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] { + return readFilterEnabled + } else { + return timelineDefaultReadFilterType != .none + } + } + + var timelineDefaultReadFilterType: ReadFilterType { + return timelineFeed?.defaultReadFilterType ?? .none + } var rootNode: Node { return treeController.rootNode @@ -537,12 +548,16 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func showAllArticles() { - articleReadFilterType = .none + if let feedID = timelineFeed?.feedID { + readFilterEnabledTable[feedID] = false + } refreshTimeline() } - func hideUnreadArticles() { - articleReadFilterType = .read + func hideReadArticles() { + if let feedID = timelineFeed?.feedID { + readFilterEnabledTable[feedID] = true + } refreshTimeline() } @@ -1215,7 +1230,6 @@ private extension SceneCoordinator { func setTimelineFeed(_ feed: Feed?, animated: Bool, completion: (() -> Void)? = nil) { timelineFeed = feed timelineMiddleIndexPath = nil - articleReadFilterType = feed?.defaultReadFilterType ?? .none fetchAndReplaceArticlesAsync(animated: animated) { self.masterTimelineViewController?.reinitializeArticles() @@ -1576,8 +1590,7 @@ private extension SceneCoordinator { precondition(Thread.isMainThread) cancelPendingAsyncFetches() - let readFilter = articleReadFilterType != .none - let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: readFilter, representedObjects: representedObjects) { [weak self] (articles, operation) in + let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: isReadArticlesFiltered, representedObjects: representedObjects) { [weak self] (articles, operation) in precondition(Thread.isMainThread) guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { return @@ -1763,10 +1776,15 @@ private extension SceneCoordinator { // MARK: NSUserActivity func windowState() -> [AnyHashable: Any] { - let containerIdentifierUserInfos = expandedTable.map( { $0.userInfo }) + let containerExpandedWindowState = expandedTable.map( { $0.userInfo }) + var readArticlesFilterState = [[AnyHashable: AnyHashable]: Bool]() + for key in readFilterEnabledTable.keys { + readArticlesFilterState[key.userInfo] = readFilterEnabledTable[key] + } return [ UserInfoKey.readFeedsFilterState: isReadFeedsFiltered, - UserInfoKey.containerExpandedWindowState: containerIdentifierUserInfos + UserInfoKey.containerExpandedWindowState: containerExpandedWindowState, + UserInfoKey.readArticlesFilterState: readArticlesFilterState ] } @@ -1774,15 +1792,22 @@ private extension SceneCoordinator { if let readFeedsFilterState = windowState[UserInfoKey.readFeedsFilterState] as? Bool { treeControllerDelegate.isReadFiltered = readFeedsFilterState } - if let containerIdentifierUserInfos = windowState[UserInfoKey.containerExpandedWindowState] as? [[AnyHashable: Any]] { - let containerIdentifers = containerIdentifierUserInfos.compactMap( { ContainerIdentifier(userInfo: $0) }) + if let containerExpandedWindowState = windowState[UserInfoKey.containerExpandedWindowState] as? [[AnyHashable: AnyHashable]] { + let containerIdentifers = containerExpandedWindowState.compactMap( { ContainerIdentifier(userInfo: $0) }) expandedTable = Set(containerIdentifers) } + if let readArticlesFilterState = windowState[UserInfoKey.readArticlesFilterState] as? [[AnyHashable: AnyHashable]: Bool] { + for key in readArticlesFilterState.keys { + if let feedIdentifier = FeedIdentifier(userInfo: key) { + readFilterEnabledTable[feedIdentifier] = readArticlesFilterState[key] + } + } + } } func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) { guard let userInfo = userInfo, - let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any], + let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable], let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { return } @@ -1842,7 +1867,7 @@ private extension SceneCoordinator { } func restoreFeed(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool { - guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any], + guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable], let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { return false }