diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 3dc1aad09..a81cf063b 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -84,6 +84,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public var isDeleted = false + public var containerID: ContainerIdentifier? { + return ContainerIdentifier.account(accountID) + } + public var account: Account? { return self } diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index f5c99af54..c6089eb13 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; }; 51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; }; 51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; }; + 51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */; }; 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; }; 51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; }; 51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; }; @@ -235,6 +236,7 @@ 518B2EA52351306200400001 /* Account_project_test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_test.xcconfig; sourceTree = ""; }; 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = ""; }; 51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; + 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerIdentifier.swift; sourceTree = ""; }; 51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = ""; }; 51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = ""; }; 51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = ""; }; @@ -535,6 +537,7 @@ 84F73CF0202788D80000BCEF /* ArticleFetcher.swift */, 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */, 8419740D1F6DD25F006346C4 /* Container.swift */, + 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */, 84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */, 84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */, 51BC8FCB237EC055004F8B56 /* Feed.swift */, @@ -1039,6 +1042,7 @@ 515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */, 844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */, 9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */, + 51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */, 9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */, 9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */, 84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */, diff --git a/Frameworks/Account/Container.swift b/Frameworks/Account/Container.swift index 4c7d36e85..f9dbaacb1 100644 --- a/Frameworks/Account/Container.swift +++ b/Frameworks/Account/Container.swift @@ -16,7 +16,7 @@ extension Notification.Name { public static let ChildrenDidChange = Notification.Name("ChildrenDidChange") } -public protocol Container: class { +public protocol Container: class, ContainerIdentifiable { var account: Account? { get } var topLevelWebFeeds: Set { get set } diff --git a/Frameworks/Account/ContainerIdentifier.swift b/Frameworks/Account/ContainerIdentifier.swift new file mode 100644 index 000000000..9745a80db --- /dev/null +++ b/Frameworks/Account/ContainerIdentifier.swift @@ -0,0 +1,19 @@ +// +// ContainerIdentifier.swift +// Account +// +// Created by Maurice Parker on 11/24/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public protocol ContainerIdentifiable { + var containerID: ContainerIdentifier? { get } +} + +public enum ContainerIdentifier: Hashable { + case smartFeedController + case account(String) // accountID + case folder(String, String) // accountID, folderName +} diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index 61dfe1aca..211e58b79 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -16,6 +16,14 @@ public final class Folder: Feed, Renamable, Container, Hashable { return .read } + public var containerID: ContainerIdentifier? { + guard let accountID = account?.accountID else { + assertionFailure("Expected feed.account, but got nil.") + return nil + } + return ContainerIdentifier.folder(accountID, nameForDisplay) + } + public var feedID: FeedIdentifier? { guard let accountID = account?.accountID else { assertionFailure("Expected feed.account, but got nil.") diff --git a/Shared/SmartFeeds/SmartFeedsController.swift b/Shared/SmartFeeds/SmartFeedsController.swift index b9163a0c6..75288e496 100644 --- a/Shared/SmartFeeds/SmartFeedsController.swift +++ b/Shared/SmartFeeds/SmartFeedsController.swift @@ -10,7 +10,11 @@ import Foundation import RSCore import Account -final class SmartFeedsController: DisplayNameProvider { +final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable { + + var containerID: ContainerIdentifier? { + return ContainerIdentifier.smartFeedController + } public static let shared = SmartFeedsController() let nameForDisplay = NSLocalizedString("Smart Feeds", comment: "Smart Feeds group title") diff --git a/Shared/Tree/WebFeedTreeControllerDelegate.swift b/Shared/Tree/WebFeedTreeControllerDelegate.swift index 2eb149037..6f572581d 100644 --- a/Shared/Tree/WebFeedTreeControllerDelegate.swift +++ b/Shared/Tree/WebFeedTreeControllerDelegate.swift @@ -40,7 +40,6 @@ private extension WebFeedTreeControllerDelegate { let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared) smartFeedsNode.canHaveChildNodes = true smartFeedsNode.isGroupItem = true - smartFeedsNode.isExpanded = true topLevelNodes.append(smartFeedsNode) } @@ -137,7 +136,6 @@ private extension WebFeedTreeControllerDelegate { let accountNode = parent.existingOrNewChildNode(with: account) accountNode.canHaveChildNodes = true accountNode.isGroupItem = true - accountNode.isExpanded = true return accountNode } return nodes diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 5b64d2e38..3e42727cf 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -191,7 +191,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } headerView.tag = section - headerView.disclosureExpanded = sectionNode.isExpanded + headerView.disclosureExpanded = coordinator.isExpanded(sectionNode) if section == tableView.numberOfSections - 1 { headerView.isLastSection = true @@ -334,7 +334,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } // If this is a folder and isn't expanded or doesn't have any entries, let the users drop on it - if destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !destNode.isExpanded) { + if destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !coordinator.isExpanded(destNode)) { return proposedDestinationIndexPath } @@ -403,7 +403,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { return } - if sectionNode.isExpanded { + if coordinator.isExpanded(sectionNode) { headerView.disclosureExpanded = false coordinator.collapse(sectionNode) self.applyChanges(animated: true) @@ -520,7 +520,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { return } - if !sectionNode.isExpanded { + if !coordinator.isExpanded(sectionNode) { coordinator.expand(sectionNode) self.applyChanges(animated: true) { completion?() @@ -687,7 +687,7 @@ private extension MasterFeedViewController { } else { cell.indentationLevel = 1 } - cell.setDisclosure(isExpanded: node.isExpanded, animated: false) + cell.setDisclosure(isExpanded: coordinator.isExpanded(node), animated: false) cell.isDisclosureAvailable = node.canHaveChildNodes cell.name = nameFor(node) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 9df775bd4..17e82d3c4 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -66,6 +66,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { private let fetchRequestQueue = FetchRequestQueue() private var animatingChanges = false + private var expandedTable = Set() private var shadowTable = [[Node]]() private var lastSearchString = "" private var lastSearchScope: SearchScope? = nil @@ -269,7 +270,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { super.init() - for _ in treeController.rootNode.childNodes { + for sectionNode in treeController.rootNode.childNodes { + markExpanded(sectionNode) shadowTable.append([Node]()) } @@ -393,15 +395,59 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } @objc func accountStateDidChange(_ note: Notification) { - updateForAccountChanges() + if timelineFetcherContainsAnyPseudoFeed() { + fetchAndReplaceArticlesAsync(animated: true) { + self.masterTimelineViewController?.reinitializeArticles() + self.rebuildBackingStores() + } + } else { + rebuildBackingStores() + } + } @objc func userDidAddAccount(_ note: Notification) { - updateForAccountChanges() + let expandNewAccount = { + if let account = note.userInfo?[Account.UserInfoKey.account] as? Account, + let node = self.treeController.rootNode.childNodeRepresentingObject(account) { + self.markExpanded(node) + } + } + + if timelineFetcherContainsAnyPseudoFeed() { + fetchAndReplaceArticlesAsync(animated: true) { + self.masterTimelineViewController?.reinitializeArticles() + self.rebuildBackingStores() { + expandNewAccount() + } + } + } else { + rebuildBackingStores() { + expandNewAccount() + } + } } @objc func userDidDeleteAccount(_ note: Notification) { - updateForAccountChanges() + let cleanupAccount = { + if let account = note.userInfo?[Account.UserInfoKey.account] as? Account, + let node = self.treeController.rootNode.childNodeRepresentingObject(account) { + self.unmarkExpanded(node) + } + } + + if timelineFetcherContainsAnyPseudoFeed() { + fetchAndReplaceArticlesAsync(animated: true) { + self.masterTimelineViewController?.reinitializeArticles() + self.rebuildBackingStores() { + cleanupAccount() + } + } + } else { + rebuildBackingStores() { + cleanupAccount() + } + } } @objc func userDefaultsDidChange(_ note: Notification) { @@ -469,9 +515,28 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { articleReadFilterType = .read refreshTimeline() } + + func markExpanded(_ node: Node) { + if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID { + expandedTable.insert(containerID) + } + } + + func unmarkExpanded(_ node: Node) { + if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID { + expandedTable.remove(containerID) + } + } + + func isExpanded(_ node: Node) -> Bool { + if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID { + return expandedTable.contains(containerID) + } + return false + } func expand(_ node: Node) { - node.isExpanded = true + markExpanded(node) animatingChanges = true rebuildShadowTable() animatingChanges = false @@ -479,10 +544,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func expandAllSectionsAndFolders() { for sectionNode in treeController.rootNode.childNodes { - sectionNode.isExpanded = true + markExpanded(sectionNode) for topLevelNode in sectionNode.childNodes { if topLevelNode.representedObject is Folder { - topLevelNode.isExpanded = true + markExpanded(topLevelNode) } } } @@ -492,7 +557,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func collapse(_ node: Node) { - node.isExpanded = false + unmarkExpanded(node) animatingChanges = true rebuildShadowTable() animatingChanges = false @@ -500,10 +565,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func collapseAllFolders() { for sectionNode in treeController.rootNode.childNodes { - sectionNode.isExpanded = true + unmarkExpanded(sectionNode) for topLevelNode in sectionNode.childNodes { if topLevelNode.representedObject is Folder { - topLevelNode.isExpanded = true + unmarkExpanded(topLevelNode) } } } @@ -1073,17 +1138,6 @@ private extension SceneCoordinator { unreadCount = count } - func updateForAccountChanges() { - if timelineFetcherContainsAnyPseudoFeed() { - fetchAndReplaceArticlesAsync(animated: true) { - self.masterTimelineViewController?.reinitializeArticles() - self.rebuildBackingStores() - } - } else { - rebuildBackingStores() - } - } - func rebuildBackingStores(_ updateExpandedNodes: (() -> Void)? = nil) { if !animatingChanges && !BatchUpdate.shared.isPerforming { treeController.rebuild() @@ -1101,10 +1155,10 @@ private extension SceneCoordinator { var result = [Node]() let sectionNode = treeController.rootNode.childAtIndex(i)! - if sectionNode.isExpanded { + if isExpanded(sectionNode) { for node in sectionNode.childNodes { result.append(node) - if node.isExpanded { + if isExpanded(node) { for child in node.childNodes { result.append(child) } @@ -1263,7 +1317,7 @@ private extension SceneCoordinator { return true } - if node.isExpanded { + if isExpanded(node) { continue } @@ -1374,7 +1428,7 @@ private extension SceneCoordinator { return } - if node.isExpanded { + if isExpanded(node) { continue } diff --git a/submodules/RSTree b/submodules/RSTree index a041d4fc0..2fc9b9cff 160000 --- a/submodules/RSTree +++ b/submodules/RSTree @@ -1 +1 @@ -Subproject commit a041d4fc0e45077d28386e7efe4fca3a175584ad +Subproject commit 2fc9b9cff60032a272303ff6d6df5b39ec297179