Make handling of programmatic feed selection when filtered or collapsed more consistent. Issues #1788 and #1792

This commit is contained in:
Maurice Parker 2020-02-17 17:40:40 -08:00
parent 6c5f0cc8b6
commit 03c1ed2625
5 changed files with 204 additions and 212 deletions

View File

@ -14,6 +14,13 @@ final class FetchRequestQueue {
private var pendingRequests = [FetchRequestOperation]() private var pendingRequests = [FetchRequestOperation]()
private var currentRequest: FetchRequestOperation? = nil private var currentRequest: FetchRequestOperation? = nil
var isAnyCurrentRequest: Bool {
if let currentRequest = currentRequest {
return !currentRequest.isCanceled
}
return false
}
func cancelAllRequests() { func cancelAllRequests() {
precondition(Thread.isMainThread) precondition(Thread.isMainThread)

View File

@ -58,7 +58,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
@ -149,13 +148,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
reloadAllVisibleCells() reloadAllVisibleCells()
} }
@objc func userDidAddFeed(_ notification: Notification) {
guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? WebFeed else {
return
}
discloseFeed(webFeed, animations: [.scroll, .navigation])
}
@objc func contentSizeCategoryDidChange(_ note: Notification) { @objc func contentSizeCategoryDidChange(_ note: Notification) {
resetEstimatedRowHeight() resetEstimatedRowHeight()
applyChanges(animated: false) applyChanges(animated: false)
@ -335,7 +327,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
becomeFirstResponder() becomeFirstResponder()
coordinator.selectFeed(indexPath, animations: [.navigation, .select, .scroll]) coordinator.selectFeed(indexPath: indexPath, animations: [.navigation, .select, .scroll])
} }
override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
@ -545,7 +537,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
} }
} }
func reloadFeeds(initialLoad: Bool) { func reloadFeeds(initialLoad: Bool, completion: (() -> Void)? = nil) {
updateUI() updateUI()
// We have to reload all the visible cells because if we got here by doing a table cell move, // We have to reload all the visible cells because if we got here by doing a table cell move,
@ -553,75 +545,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
// drops on a "folder" that should cause the dropped cell to disappear. // drops on a "folder" that should cause the dropped cell to disappear.
applyChanges(animated: !initialLoad) { [weak self] in applyChanges(animated: !initialLoad) { [weak self] in
if !initialLoad { if !initialLoad {
self?.reloadAllVisibleCells() self?.reloadAllVisibleCells(completion: completion)
} else {
completion?()
} }
} }
} }
func ensureSectionIsExpanded(_ sectionIndex: Int, completion: (() -> Void)? = nil) {
guard let sectionNode = coordinator.rootNode.childAtIndex(sectionIndex) else {
return
}
if !coordinator.isExpanded(sectionNode) {
coordinator.expand(sectionNode)
self.applyChanges(animated: true) {
completion?()
}
} else {
completion?()
}
}
func discloseFeed(_ webFeed: WebFeed, animations: Animations, completion: (() -> Void)? = nil) {
func discloseFeedInAccount() {
guard let node = coordinator.rootNode.descendantNodeRepresentingObject(webFeed as AnyObject) else {
completion?()
return
}
if let indexPath = dataSource.indexPath(for: node) {
coordinator.selectFeed(indexPath, animations: animations) {
completion?()
}
return
}
// It wasn't already visable, so expand its folder and try again
guard let parent = node.parent, parent.representedObject is Folder else {
completion?()
return
}
coordinator.expand(parent)
reloadNode(parent)
applyChanges(animated: true, adjustScroll: true) { [weak self] in
if let indexPath = self?.dataSource.indexPath(for: node) {
self?.coordinator.selectFeed(indexPath, animations: animations) {
completion?()
}
}
}
}
// If the account for the feed is collapsed, expand it
if let account = webFeed.account,
let accountNode = coordinator.rootNode.childNodeRepresentingObject(account as AnyObject),
!coordinator.isExpanded(accountNode) {
coordinator.expand(accountNode)
applyChanges(animated: true) {
discloseFeedInAccount()
}
} else {
discloseFeedInAccount()
}
}
func focus() { func focus() {
becomeFirstResponder() becomeFirstResponder()
} }
@ -836,16 +766,17 @@ private extension MasterFeedViewController {
} }
} }
private func reloadAllVisibleCells() { private func reloadAllVisibleCells(completion: (() -> Void)? = nil) {
let visibleNodes = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) } let visibleNodes = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) }
reloadCells(visibleNodes) reloadCells(visibleNodes, completion: completion)
} }
private func reloadCells(_ nodes: [Node]) { private func reloadCells(_ nodes: [Node], completion: (() -> Void)? = nil) {
var snapshot = dataSource.snapshot() var snapshot = dataSource.snapshot()
snapshot.reloadItems(nodes) snapshot.reloadItems(nodes)
dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
self?.restoreSelectionIfNecessary(adjustScroll: false) self?.restoreSelectionIfNecessary(adjustScroll: false)
completion?()
} }
} }
@ -1224,7 +1155,7 @@ private extension MasterFeedViewController {
deleteCommand.perform() deleteCommand.perform()
if indexPath == coordinator.currentFeedIndexPath { if indexPath == coordinator.currentFeedIndexPath {
coordinator.selectFeed(nil) coordinator.selectFeed(indexPath: nil)
} }
} }

View File

@ -774,7 +774,7 @@ private extension MasterTimelineViewController {
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed") let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in
self?.coordinator.discloseFeed(webFeed, animations: [.scroll, .navigation]) self?.coordinator.discloseWebFeed(webFeed, animations: [.scroll, .navigation])
} }
return action return action
} }
@ -785,7 +785,7 @@ private extension MasterTimelineViewController {
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed") let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] action in
self?.coordinator.discloseFeed(webFeed, animations: [.scroll, .navigation]) self?.coordinator.discloseWebFeed(webFeed, animations: [.scroll, .navigation])
completion(true) completion(true)
} }
return action return action

View File

@ -298,6 +298,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddAccount(_:)), name: .UserDidAddAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDidAddAccount(_:)), name: .UserDidAddAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDidDeleteAccount(_:)), name: .UserDidDeleteAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDidDeleteAccount(_:)), name: .UserDidDeleteAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(downloadArticlesDidUpdateUnreadCounts(_:)), name: .DownloadArticlesDidUpdateUnreadCounts, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(downloadArticlesDidUpdateUnreadCounts(_:)), name: .DownloadArticlesDidUpdateUnreadCounts, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
@ -360,27 +361,27 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
} }
func handle(_ activity: NSUserActivity, initialLoad: Bool) { func handle(_ activity: NSUserActivity) {
selectFeed(nil) { selectFeed(indexPath: nil) {
guard let activityType = ActivityType(rawValue: activity.activityType) else { return } guard let activityType = ActivityType(rawValue: activity.activityType) else { return }
switch activityType { switch activityType {
case .restoration: case .restoration:
break break
case .selectFeed: case .selectFeed:
self.handleSelectFeed(activity.userInfo, initialLoad: initialLoad) self.handleSelectFeed(activity.userInfo)
case .nextUnread: case .nextUnread:
self.selectFirstUnreadInAllUnread() self.selectFirstUnreadInAllUnread()
case .readArticle: case .readArticle:
self.handleReadArticle(activity.userInfo, initialLoad: initialLoad) self.handleReadArticle(activity.userInfo)
case .addFeedIntent: case .addFeedIntent:
self.showAdd(.feed) self.showAdd(.feed)
} }
} }
} }
func handle(_ response: UNNotificationResponse, initialLoad: Bool) { func handle(_ response: UNNotificationResponse) {
let userInfo = response.notification.request.content.userInfo let userInfo = response.notification.request.content.userInfo
handleReadArticle(userInfo, initialLoad: initialLoad) handleReadArticle(userInfo)
} }
func configurePanelMode(for size: CGSize) { func configurePanelMode(for size: CGSize) {
@ -404,15 +405,16 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
func selectFirstUnreadInAllUnread() { func selectFirstUnreadInAllUnread() {
masterFeedViewController.ensureSectionIsExpanded(0) { expand(SmartFeedsController.shared)
self.selectFeed(IndexPath(row: 1, section: 0)) { self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.unreadFeed) {
self.selectFeed(SmartFeedsController.shared.unreadFeed) {
self.selectFirstUnreadArticleInTimeline() self.selectFirstUnreadArticleInTimeline()
} }
} }
} }
func showSearch() { func showSearch() {
selectFeed(nil) { selectFeed(indexPath: nil) {
self.installTimelineControllerIfNecessary(animated: false) self.installTimelineControllerIfNecessary(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now()) { DispatchQueue.main.asyncAfter(deadline: .now()) {
self.masterTimelineViewController!.showSearchAll() self.masterTimelineViewController!.showSearchAll()
@ -443,14 +445,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return return
} }
for section in shadowTable { addShadowTableToFilterExceptions()
for node in section {
if let feed = node.representedObject as? Feed, let feedID = feed.feedID {
treeControllerDelegate.addFilterException(feedID)
}
}
}
rebuildBackingStores() rebuildBackingStores()
treeControllerDelegate.resetFilterExceptions() treeControllerDelegate.resetFilterExceptions()
} }
@ -490,14 +485,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
if timelineFetcherContainsAnyPseudoFeed() { if timelineFetcherContainsAnyPseudoFeed() {
fetchAndMergeArticlesAsync(animated: true) { fetchAndMergeArticlesAsync(animated: true) {
self.masterTimelineViewController?.reinitializeArticles(resetScroll: false) self.masterTimelineViewController?.reinitializeArticles(resetScroll: false)
self.rebuildBackingStores() { self.rebuildBackingStores(updateExpandedNodes: expandNewlyActivatedAccount)
expandNewlyActivatedAccount()
}
} }
} else { } else {
rebuildBackingStores() { self.rebuildBackingStores(updateExpandedNodes: expandNewlyActivatedAccount)
expandNewlyActivatedAccount()
}
} }
} }
@ -513,14 +504,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
if timelineFetcherContainsAnyPseudoFeed() { if timelineFetcherContainsAnyPseudoFeed() {
fetchAndMergeArticlesAsync(animated: true) { fetchAndMergeArticlesAsync(animated: true) {
self.masterTimelineViewController?.reinitializeArticles(resetScroll: false) self.masterTimelineViewController?.reinitializeArticles(resetScroll: false)
self.rebuildBackingStores() { self.rebuildBackingStores(updateExpandedNodes: expandNewAccount)
expandNewAccount()
}
} }
} else { } else {
rebuildBackingStores() { self.rebuildBackingStores(updateExpandedNodes: expandNewAccount)
expandNewAccount()
}
} }
} }
@ -535,17 +522,20 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
if timelineFetcherContainsAnyPseudoFeed() { if timelineFetcherContainsAnyPseudoFeed() {
fetchAndMergeArticlesAsync(animated: true) { fetchAndMergeArticlesAsync(animated: true) {
self.masterTimelineViewController?.reinitializeArticles(resetScroll: false) self.masterTimelineViewController?.reinitializeArticles(resetScroll: false)
self.rebuildBackingStores() { self.rebuildBackingStores(updateExpandedNodes: cleanupAccount)
cleanupAccount()
}
} }
} else { } else {
rebuildBackingStores() { self.rebuildBackingStores(updateExpandedNodes: cleanupAccount)
cleanupAccount()
}
} }
} }
@objc func userDidAddFeed(_ notification: Notification) {
guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? WebFeed else {
return
}
discloseWebFeed(webFeed, animations: [.scroll, .navigation])
}
@objc func userDefaultsDidChange(_ note: Notification) { @objc func userDefaultsDidChange(_ note: Notification) {
self.sortDirection = AppDefaults.timelineSortDirection self.sortDirection = AppDefaults.timelineSortDirection
self.groupByFeed = AppDefaults.timelineGroupByFeed self.groupByFeed = AppDefaults.timelineGroupByFeed
@ -567,7 +557,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
@objc func willEnterForeground(_ note: Notification) { @objc func willEnterForeground(_ note: Notification) {
queueFetchAndMergeArticles() // Don't interfere with any fetch requests that we may have initiated before the app was returned to the foreground.
// For example if you select Next Unread from the Home Screen Quick actions, you can start a request before we are
// in the foreground.
if !fetchRequestQueue.isAnyCurrentRequest {
queueFetchAndMergeArticles()
}
} }
// MARK: API // MARK: API
@ -637,20 +632,35 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
refreshTimeline(resetScroll: false) refreshTimeline(resetScroll: false)
} }
func isExpanded(_ node: Node) -> Bool { func isExpanded(_ containerIdentifiable: ContainerIdentifiable) -> Bool {
if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID { if let containerID = containerIdentifiable.containerID {
return expandedTable.contains(containerID) return expandedTable.contains(containerID)
} }
return false return false
} }
func expand(_ node: Node) { func isExpanded(_ node: Node) -> Bool {
markExpanded(node) if let containerIdentifiable = node.representedObject as? ContainerIdentifiable {
return isExpanded(containerIdentifiable)
}
return false
}
func expand(_ containerIdentifiable: ContainerIdentifiable) {
guard !isExpanded(containerIdentifiable) else { return }
markExpanded(containerIdentifiable)
animatingChanges = true animatingChanges = true
rebuildShadowTable() rebuildShadowTable()
animatingChanges = false animatingChanges = false
} }
func expand(_ node: Node) {
if let containerIdentifiable = node.representedObject as? ContainerIdentifiable {
expand(containerIdentifiable)
}
}
func expandAllSectionsAndFolders() { func expandAllSectionsAndFolders() {
for sectionNode in treeController.rootNode.childNodes { for sectionNode in treeController.rootNode.childNodes {
markExpanded(sectionNode) markExpanded(sectionNode)
@ -695,7 +705,18 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return indexPathFor(node) return indexPathFor(node)
} }
func selectFeed(_ indexPath: IndexPath?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) { func selectFeed(_ feed: Feed?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) {
let indexPath: IndexPath? = {
if let feed = feed, let indexPath = indexPathFor(feed as AnyObject) {
return indexPath
} else {
return nil
}
}()
selectFeed(indexPath: indexPath, animations: animations, deselectArticle: deselectArticle, completion: completion)
}
func selectFeed(indexPath: IndexPath?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) {
guard indexPath != currentFeedIndexPath else { guard indexPath != currentFeedIndexPath else {
completion?() completion?()
return return
@ -732,31 +753,34 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
func selectPrevFeed() { func selectPrevFeed() {
if let indexPath = prevFeedIndexPath { if let indexPath = prevFeedIndexPath {
selectFeed(indexPath, animations: [.navigation, .scroll]) selectFeed(indexPath: indexPath, animations: [.navigation, .scroll])
} }
} }
func selectNextFeed() { func selectNextFeed() {
if let indexPath = nextFeedIndexPath { if let indexPath = nextFeedIndexPath {
selectFeed(indexPath, animations: [.navigation, .scroll]) selectFeed(indexPath: indexPath, animations: [.navigation, .scroll])
} }
} }
func selectTodayFeed() { func selectTodayFeed() {
masterFeedViewController?.ensureSectionIsExpanded(0) { expand(SmartFeedsController.shared)
self.selectFeed(IndexPath(row: 0, section: 0), animations: [.navigation, .scroll]) self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.todayFeed) {
self.selectFeed(SmartFeedsController.shared.todayFeed, animations: [.navigation, .scroll])
} }
} }
func selectAllUnreadFeed() { func selectAllUnreadFeed() {
masterFeedViewController?.ensureSectionIsExpanded(0) { expand(SmartFeedsController.shared)
self.selectFeed(IndexPath(row: 1, section: 0), animations: [.navigation, .scroll]) self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.unreadFeed) {
self.selectFeed(SmartFeedsController.shared.unreadFeed, animations: [.navigation, .scroll])
} }
} }
func selectStarredFeed() { func selectStarredFeed() {
masterFeedViewController?.ensureSectionIsExpanded(0) { expand(SmartFeedsController.shared)
self.selectFeed(IndexPath(row: 2, section: 0), animations: [.navigation, .scroll]) self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.starredFeed) {
self.selectFeed(SmartFeedsController.shared.starredFeed, animations: [.navigation, .scroll])
} }
} }
@ -1009,14 +1033,35 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return timelineFeed == feed return timelineFeed == feed
} }
func discloseFeed(_ feed: WebFeed, animations: Animations = [], completion: (() -> Void)? = nil) { func discloseWebFeed(_ webFeed: WebFeed, animations: Animations = [], completion: (() -> Void)? = nil) {
if isSearching { if isSearching {
masterTimelineViewController?.hideSearch() masterTimelineViewController?.hideSearch()
} }
masterFeedViewController.discloseFeed(feed, animations: animations) { guard let account = webFeed.account else {
completion?() completion?()
return
} }
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(webFeed) })
expand(account)
if let parentFolder = parentFolder {
expand(parentFolder)
}
if let webFeedFeedID = webFeed.feedID {
self.treeControllerDelegate.addFilterException(webFeedFeedID)
}
if let parentFolderFeedID = parentFolder?.feedID {
self.treeControllerDelegate.addFilterException(parentFolderFeedID)
}
rebuildBackingStores() {
self.treeControllerDelegate.resetFilterExceptions()
self.selectFeed(webFeed, animations: animations, completion: completion)
}
} }
func showStatusBar() { func showStatusBar() {
@ -1258,30 +1303,26 @@ private extension SceneCoordinator {
_idToArticleDictionary = idDictionary _idToArticleDictionary = idDictionary
articleDictionaryNeedsUpdate = false articleDictionaryNeedsUpdate = false
} }
func rebuildBackingStores(initialLoad: Bool = false, updateExpandedNodes: (() -> Void)? = nil) { func ensureFeedIsAvailableToSelect(_ feed: Feed, completion: @escaping () -> Void) {
if !animatingChanges && !BatchUpdate.shared.isPerforming { addToFilterExeptionsIfNecessary(feed)
addShadowTableToFilterExceptions()
addCurrentFeedToFilterExeptionsIfNecessary()
treeController.rebuild() rebuildBackingStores() {
treeControllerDelegate.resetFilterExceptions() self.treeControllerDelegate.resetFilterExceptions()
completion()
updateExpandedNodes?()
rebuildShadowTable()
masterFeedViewController.reloadFeeds(initialLoad: initialLoad)
} }
} }
func addCurrentFeedToFilterExeptionsIfNecessary() { func addToFilterExeptionsIfNecessary(_ feed: Feed?) {
if isReadFeedsFiltered, let feedID = timelineFeed?.feedID { if isReadFeedsFiltered, let feedID = feed?.feedID {
if timelineFeed is SmartFeed { if feed is SmartFeed {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
} else if let folderFeed = timelineFeed as? Folder { } else if let folderFeed = feed as? Folder {
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil { if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
} }
} else if let webFeed = timelineFeed as? WebFeed { } else if let webFeed = feed as? WebFeed {
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil { if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
addParentFolderToFilterExceptions(webFeed) addParentFolderToFilterExceptions(webFeed)
@ -1299,7 +1340,31 @@ private extension SceneCoordinator {
treeControllerDelegate.addFilterException(folderFeedID) treeControllerDelegate.addFilterException(folderFeedID)
} }
func addShadowTableToFilterExceptions() {
for section in shadowTable {
for node in section {
if let feed = node.representedObject as? Feed, let feedID = feed.feedID {
treeControllerDelegate.addFilterException(feedID)
}
}
}
}
func rebuildBackingStores(initialLoad: Bool = false, updateExpandedNodes: (() -> Void)? = nil, completion: (() -> Void)? = nil) {
if !animatingChanges && !BatchUpdate.shared.isPerforming {
addToFilterExeptionsIfNecessary(timelineFeed)
treeController.rebuild()
treeControllerDelegate.resetFilterExceptions()
updateExpandedNodes?()
rebuildShadowTable()
masterFeedViewController.reloadFeeds(initialLoad: initialLoad, completion: completion)
}
}
func rebuildShadowTable() { func rebuildShadowTable() {
shadowTable = [[Node]]() shadowTable = [[Node]]()
@ -1405,18 +1470,30 @@ private extension SceneCoordinator {
self.showIcons = false self.showIcons = false
} }
func markExpanded(_ node: Node) { func markExpanded(_ containerIdentifiable: ContainerIdentifiable) {
if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID { if let containerID = containerIdentifiable.containerID {
expandedTable.insert(containerID) expandedTable.insert(containerID)
} }
} }
func unmarkExpanded(_ node: Node) { func markExpanded(_ node: Node) {
if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID { if let containerIdentifiable = node.representedObject as? ContainerIdentifiable {
markExpanded(containerIdentifiable)
}
}
func unmarkExpanded(_ containerIdentifiable: ContainerIdentifiable) {
if let containerID = containerIdentifiable.containerID {
expandedTable.remove(containerID) expandedTable.remove(containerID)
} }
} }
func unmarkExpanded(_ node: Node) {
if let containerIdentifiable = node.representedObject as? ContainerIdentifiable {
unmarkExpanded(containerIdentifiable)
}
}
// MARK: Select Prev Unread // MARK: Select Prev Unread
@discardableResult @discardableResult
@ -1507,7 +1584,7 @@ private extension SceneCoordinator {
} }
if unreadCountProvider.unreadCount > 0 { if unreadCountProvider.unreadCount > 0 {
selectFeed(prevIndexPath, animations: [.scroll, .navigation]) selectFeed(indexPath: prevIndexPath, animations: [.scroll, .navigation])
return true return true
} }
@ -1618,7 +1695,7 @@ private extension SceneCoordinator {
} }
if unreadCountProvider.unreadCount > 0 { if unreadCountProvider.unreadCount > 0 {
selectFeed(nextIndexPath, animations: [.scroll, .navigation], deselectArticle: false) { selectFeed(indexPath: nextIndexPath, animations: [.scroll, .navigation], deselectArticle: false) {
self.currentArticle = nil self.currentArticle = nil
completion(true) completion(true)
} }
@ -1660,7 +1737,7 @@ private extension SceneCoordinator {
} }
func queueFetchAndMergeArticles() { func queueFetchAndMergeArticles() {
fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticlesAsync)) fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticlesAsync))
} }
@objc func fetchAndMergeArticlesAsync() { @objc func fetchAndMergeArticlesAsync() {
@ -1920,7 +1997,7 @@ private extension SceneCoordinator {
] ]
} }
func handleSelectFeed(_ userInfo: [AnyHashable : Any]?, initialLoad: Bool) { func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) {
guard let userInfo = userInfo, guard let userInfo = userInfo,
let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable], let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
@ -1930,23 +2007,18 @@ private extension SceneCoordinator {
treeControllerDelegate.addFilterException(feedIdentifier) treeControllerDelegate.addFilterException(feedIdentifier)
masterFeedViewController.restoreSelection = true masterFeedViewController.restoreSelection = true
func rebuildIfNecessary() {
if !initialLoad && isReadFeedsFiltered {
rebuildBackingStores()
treeControllerDelegate.resetFilterExceptions()
}
}
switch feedIdentifier { switch feedIdentifier {
case .smartFeed: case .smartFeed:
guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return } guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return }
rebuildIfNecessary() expand(SmartFeedsController.shared)
rebuildBackingStores() {
if let indexPath = indexPathFor(smartFeed) { self.treeControllerDelegate.resetFilterExceptions()
selectFeed(indexPath) { if let indexPath = self.indexPathFor(smartFeed) {
self.masterFeedViewController.focus() self.selectFeed(indexPath: indexPath) {
self.masterFeedViewController.focus()
}
} }
} }
@ -1954,14 +2026,20 @@ private extension SceneCoordinator {
break break
case .folder(let accountID, let folderName): case .folder(let accountID, let folderName):
rebuildIfNecessary() guard let accountNode = self.findAccountNode(accountID: accountID),
let account = accountNode.representedObject as? Account else {
guard let accountNode = findAccountNode(accountID: accountID), let folderNode = findFolderNode(folderName: folderName, beginningAt: accountNode) else {
return return
} }
if let indexPath = indexPathFor(folderNode) {
selectFeed(indexPath) { expand(account)
self.masterFeedViewController.focus()
rebuildBackingStores() {
self.treeControllerDelegate.resetFilterExceptions()
if let folderNode = self.findFolderNode(folderName: folderName, beginningAt: accountNode), let indexPath = self.indexPathFor(folderNode) {
self.selectFeed(indexPath: indexPath) {
self.masterFeedViewController.focus()
}
} }
} }
@ -1972,23 +2050,13 @@ private extension SceneCoordinator {
return return
} }
for folder in account.sortedFolders ?? [Folder]() { self.discloseWebFeed(webFeed) {
if folder.objectIsChild(webFeed), let folderFeedID = folder.feedID {
treeControllerDelegate.addFilterException(folderFeedID)
break
}
}
rebuildIfNecessary()
discloseFeed(webFeed) {
self.masterFeedViewController.focus() self.masterFeedViewController.focus()
} }
} }
} }
func handleReadArticle(_ userInfo: [AnyHashable : Any]?, initialLoad: Bool) { func handleReadArticle(_ userInfo: [AnyHashable : Any]?) {
guard let userInfo = userInfo else { return } guard let userInfo = userInfo else { return }
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
@ -2007,25 +2075,11 @@ private extension SceneCoordinator {
return return
} }
guard let webFeed = account.existingWebFeed(withWebFeedID: webFeedID), guard let webFeed = account.existingWebFeed(withWebFeedID: webFeedID) else {
let webFeedFeedID = webFeed.feedID else { return
return
} }
treeControllerDelegate.addFilterException(webFeedFeedID) discloseWebFeed(webFeed) {
for folder in account.sortedFolders ?? [Folder]() {
if folder.objectIsChild(webFeed), let folderFeedID = folder.feedID {
treeControllerDelegate.addFilterException(folderFeedID)
break
}
}
if !initialLoad && isReadFeedsFiltered {
rebuildBackingStores()
treeControllerDelegate.resetFilterExceptions()
}
discloseFeed(webFeed) {
self.selectArticleInCurrentFeed(articleID) self.selectArticleInCurrentFeed(articleID)
} }
} }
@ -2043,7 +2097,7 @@ private extension SceneCoordinator {
case .smartFeed: case .smartFeed:
guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return false } guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return false }
if let indexPath = indexPathFor(smartFeed) { if let indexPath = indexPathFor(smartFeed) {
selectFeed(indexPath) { selectFeed(indexPath: indexPath) {
self.selectArticleInCurrentFeed(articleID) self.selectArticleInCurrentFeed(articleID)
} }
treeControllerDelegate.addFilterException(feedIdentifier) treeControllerDelegate.addFilterException(feedIdentifier)
@ -2110,7 +2164,7 @@ private extension SceneCoordinator {
func selectFeedAndArticle(feedNode: Node, articleID: String) -> Bool { func selectFeedAndArticle(feedNode: Node, articleID: String) -> Bool {
if let feedIndexPath = indexPathFor(feedNode) { if let feedIndexPath = indexPathFor(feedNode) {
selectFeed(feedIndexPath) { selectFeed(indexPath: feedIndexPath) {
self.selectArticleInCurrentFeed(articleID) self.selectArticleInCurrentFeed(articleID)
} }
return true return true

View File

@ -33,12 +33,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
if let notificationResponse = connectionOptions.notificationResponse { if let notificationResponse = connectionOptions.notificationResponse {
window!.makeKeyAndVisible() window!.makeKeyAndVisible()
coordinator.handle(notificationResponse, initialLoad: true) coordinator.handle(notificationResponse)
return return
} }
if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
coordinator.handle(userActivity, initialLoad: true) coordinator.handle(userActivity)
} }
window!.makeKeyAndVisible() window!.makeKeyAndVisible()
@ -52,7 +52,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
appDelegate.resumeDatabaseProcessingIfNecessary() appDelegate.resumeDatabaseProcessingIfNecessary()
coordinator.handle(userActivity, initialLoad: false) coordinator.handle(userActivity)
} }
func sceneDidEnterBackground(_ scene: UIScene) { func sceneDidEnterBackground(_ scene: UIScene) {
@ -74,7 +74,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func handle(_ response: UNNotificationResponse) { func handle(_ response: UNNotificationResponse) {
appDelegate.resumeDatabaseProcessingIfNecessary() appDelegate.resumeDatabaseProcessingIfNecessary()
coordinator.handle(response, initialLoad: false) coordinator.handle(response)
} }
func suspend() { func suspend() {