diff --git a/Frameworks/Account/AccountError.swift b/Frameworks/Account/AccountError.swift index 090cb268d..480871f2b 100644 --- a/Frameworks/Account/AccountError.swift +++ b/Frameworks/Account/AccountError.swift @@ -48,8 +48,8 @@ public enum AccountError: LocalizedError { case .wrappedError(let error, _): switch error { case TransportError.httpError(let status): - if status == 401 { - return NSLocalizedString("Please update your credentials for this account.", comment: "Try later") + if status == 401 || status == 403 { + return NSLocalizedString("Please update your credentials for this account, or ensure that your account with this service is still valid.", comment: "Expired credentials") } else { return NSLocalizedString("Please try again later.", comment: "Try later") } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 25245494f..4fa40a99d 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -122,6 +122,7 @@ 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; 51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; }; 51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; }; + 51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */; }; 51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; }; @@ -733,6 +734,7 @@ 51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = "<group>"; }; 51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; }; + 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = "<group>"; }; 51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = "<group>"; }; 51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; }; 51E3EB3C229AB08300645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; }; @@ -1134,6 +1136,7 @@ isa = PBXGroup; children = ( 51C45264226508F600C03939 /* MasterFeedViewController.swift */, + 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */, 51C45260226508F600C03939 /* Cell */, ); path = MasterFeed; @@ -2469,6 +2472,7 @@ 51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */, 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */, 51E595AD228E1C2100FCC42B /* AddAccountViewController.swift in Sources */, + 51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */, 51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */, 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, 51934CD023108953006127BE /* ActivityID.swift in Sources */, diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index 2c4bd27c6..9a51a1e7d 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -20,6 +20,10 @@ class ActivityManager { private var selectingActivity: NSUserActivity? = nil private var readingActivity: NSUserActivity? = nil + init() { + NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil) + } + func selectingToday() { let title = NSLocalizedString("See articles for Today", comment: "Today") selectingActivity = makeSelectingActivity(type: ActivityType.selectToday, title: title, identifier: "smartfeed.today") @@ -41,7 +45,7 @@ class ActivityManager { func selectingFolder(_ folder: Folder) { let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder") let title = NSString.localizedStringWithFormat(localizedText as NSString, folder.nameForDisplay) as String - selectingActivity = makeSelectingActivity(type: ActivityType.selectFolder, title: title, identifier: "folder.\(folder.nameForDisplay)") + selectingActivity = makeSelectingActivity(type: ActivityType.selectFolder, title: title, identifier: identifer(for: folder)) selectingActivity!.userInfo = [ ActivityID.accountID.rawValue: folder.account?.accountID ?? "", @@ -55,14 +59,15 @@ class ActivityManager { func selectingFeed(_ feed: Feed) { let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Feed") let title = NSString.localizedStringWithFormat(localizedText as NSString, feed.nameForDisplay) as String - selectingActivity = makeSelectingActivity(type: ActivityType.selectFeed, title: title, identifier: feed.url) + selectingActivity = makeSelectingActivity(type: ActivityType.selectFeed, title: title, identifier: identifer(for: feed)) selectingActivity!.userInfo = [ ActivityID.accountID.rawValue: feed.account?.accountID ?? "", ActivityID.accountName.rawValue: feed.account?.name ?? "", ActivityID.feedID.rawValue: feed.feedID ] - + updateSelectingActivityFeedSearchAttributes(with: feed) + selectingActivity!.becomeCurrent() } @@ -74,6 +79,46 @@ class ActivityManager { readingActivity?.becomeCurrent() } + func cleanUp(_ account: Account) { + var ids = [String]() + + if let folders = account.folders { + for folder in folders { + ids.append(identifer(for: folder)) + } + } + + for feed in account.flattenedFeeds() { + ids.append(contentsOf: identifers(for: feed)) + } + + NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: ids) {} + } + + func cleanUp(_ folder: Folder) { + var ids = [String]() + ids.append(identifer(for: folder)) + + for feed in folder.flattenedFeeds() { + ids.append(contentsOf: identifers(for: feed)) + } + + NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: ids) {} + } + + func cleanUp(_ feed: Feed) { + NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: identifers(for: feed)) {} + } + + @objc func feedIconDidBecomeAvailable(_ note: Notification) { + guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[ActivityID.feedID.rawValue] as? String else { + return + } + if activityFeedId == feed.feedID { + updateSelectingActivityFeedSearchAttributes(with: feed) + } + } + } // MARK: Private @@ -110,13 +155,13 @@ private extension ActivityManager { activity.isEligibleForSearch = true activity.isEligibleForPrediction = false activity.isEligibleForHandoff = true + activity.persistentIdentifier = identifer(for: article) // CoreSpotlight let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeCompositeContent as String) attributeSet.title = article.title attributeSet.contentDescription = article.summary attributeSet.keywords = keywords - attributeSet.relatedUniqueIdentifier = article.url if let image = article.avatarImage() { attributeSet.thumbnailData = image.pngData() @@ -131,4 +176,43 @@ private extension ActivityManager { return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? [] } + func identifer(for folder: Folder) -> String { + return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)" + } + + func identifer(for feed: Feed) -> String { + return "account_\(feed.account!.accountID)_feed_\(feed.feedID)" + } + + func identifer(for article: Article) -> String { + return "account_\(article.accountID)_feed_\(article.feedID)_article_\(article.articleID)" + } + + func identifers(for feed: Feed) -> [String] { + var ids = [String]() + ids.append(identifer(for: feed)) + + for article in feed.fetchArticles() { + ids.append(identifer(for: article)) + } + + return ids + } + + func updateSelectingActivityFeedSearchAttributes(with feed: Feed) { + + let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String) + attributeSet.title = feed.nameForDisplay + attributeSet.keywords = makeKeywords(feed.nameForDisplay) + if let image = appDelegate.feedIconDownloader.icon(for: feed) { + attributeSet.thumbnailData = image.pngData() + } else if let image = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) { + attributeSet.thumbnailData = image.pngData() + } + + selectingActivity!.contentAttributeSet = attributeSet + selectingActivity!.needsSave = true + + } + } diff --git a/Shared/Commands/DeleteCommand.swift b/Shared/Commands/DeleteCommand.swift index 6d1bda0e3..6c62e0554 100644 --- a/Shared/Commands/DeleteCommand.swift +++ b/Shared/Commands/DeleteCommand.swift @@ -54,24 +54,6 @@ final class DeleteCommand: UndoableCommand { registerUndo() } - func perform(completion: @escaping () -> Void) { - - let group = DispatchGroup() - group.enter() - itemSpecifiers.forEach { - $0.delete() { - group.leave() - } - } - treeController.rebuild() - - group.notify(queue: DispatchQueue.main) { - self.registerUndo() - completion() - } - - } - func undo() { BatchUpdate.shared.perform { diff --git a/iOS/AppCoordinator.swift b/iOS/AppCoordinator.swift index 7df057dad..e87596f8c 100644 --- a/iOS/AppCoordinator.swift +++ b/iOS/AppCoordinator.swift @@ -76,8 +76,12 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return treeController.rootNode } - var numberOfSections: Int { - return shadowTable.count + var allSections: [Int] { + var sections = [Int]() + for (index, _) in shadowTable.enumerated() { + sections.append(index) + } + return sections } private(set) var currentMasterIndexPath: IndexPath? { @@ -313,14 +317,6 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { // MARK: API - func beginUpdates() { - animatingChanges = true - } - - func endUpdates() { - animatingChanges = false - } - func rowsInSection(_ section: Int) -> Int { return shadowTable[section].count } @@ -361,6 +357,10 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return shadowTable[indexPath.section][indexPath.row] } + func nodesFor(section: Int) -> [Node] { + return shadowTable[section] + } + func indexPathFor(_ node: Node) -> IndexPath? { for i in 0..<shadowTable.count { if let row = shadowTable[i].firstIndex(of: node) { @@ -388,8 +388,7 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return 0 } - func expand(section: Int, completion: ([IndexPath]) -> ()) { - + func expand(section: Int) { guard let expandNode = treeController.rootNode.childAtIndex(section) else { return } @@ -397,11 +396,9 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { animatingChanges = true - var indexPathsToInsert = [IndexPath]() var i = 0 func addNode(_ node: Node) { - indexPathsToInsert.append(IndexPath(row: i, section: section)) shadowTable[section].insert(node, at: i) i = i + 1 } @@ -415,36 +412,26 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } } - completion(indexPathsToInsert) - animatingChanges = false - } - func expand(_ indexPath: IndexPath, completion: ([IndexPath]) -> ()) { - + func expand(_ indexPath: IndexPath) { let expandNode = shadowTable[indexPath.section][indexPath.row] expandedNodes.append(expandNode) animatingChanges = true - var indexPathsToInsert = [IndexPath]() for i in 0..<expandNode.childNodes.count { if let child = expandNode.childAtIndex(i) { let nextIndex = indexPath.row + i + 1 - indexPathsToInsert.append(IndexPath(row: nextIndex, section: indexPath.section)) shadowTable[indexPath.section].insert(child, at: nextIndex) } } - completion(indexPathsToInsert) - animatingChanges = false - } - func collapse(section: Int, completion: ([IndexPath]) -> ()) { - + func collapse(section: Int) { animatingChanges = true guard let collapseNode = treeController.rootNode.childAtIndex(section) else { @@ -455,20 +442,12 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { expandedNodes.remove(at: removeNode) } - var indexPathsToRemove = [IndexPath]() - for i in 0..<shadowTable[section].count { - indexPathsToRemove.append(IndexPath(row: i, section: section)) - } shadowTable[section] = [Node]() - completion(indexPathsToRemove) - animatingChanges = false - } - func collapse(_ indexPath: IndexPath, completion: ([IndexPath]) -> ()) { - + func collapse(_ indexPath: IndexPath) { animatingChanges = true let collapseNode = shadowTable[indexPath.section][indexPath.row] @@ -476,24 +455,13 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { expandedNodes.remove(at: removeNode) } - var indexPathsToRemove = [IndexPath]() - - for child in collapseNode.childNodes { - if let index = shadowTable[indexPath.section].firstIndex(of: child) { - indexPathsToRemove.append(IndexPath(row: index, section: indexPath.section)) - } - } - for child in collapseNode.childNodes { if let index = shadowTable[indexPath.section].firstIndex(of: child) { shadowTable[indexPath.section].remove(at: index) } } - completion(indexPathsToRemove) - animatingChanges = false - } func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet { diff --git a/iOS/MasterFeed/MasterFeedDataSource.swift b/iOS/MasterFeed/MasterFeedDataSource.swift new file mode 100644 index 000000000..fe7807721 --- /dev/null +++ b/iOS/MasterFeed/MasterFeedDataSource.swift @@ -0,0 +1,87 @@ +// +// MasterFeedDataSource.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 8/28/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit +import RSCore +import RSTree +import Account + +class MasterFeedDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable { + + private var coordinator: AppCoordinator! + private var errorHandler: ((Error) -> ())! + + init(coordinator: AppCoordinator, errorHandler: @escaping (Error) -> (), tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider) { + super.init(tableView: tableView, cellProvider: cellProvider) + self.coordinator = coordinator + self.errorHandler = errorHandler + } + + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + guard let node = coordinator.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else { + return false + } + return true + } + + override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { + guard let node = coordinator.nodeFor(indexPath) else { + return false + } + return node.representedObject is Feed + } + + override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { + + guard let sourceNode = coordinator.nodeFor(sourceIndexPath), let feed = sourceNode.representedObject as? Feed else { + return + } + + // Based on the drop we have to determine a node to start looking for a parent container. + let destNode: Node = { + if destinationIndexPath.row == 0 { + return coordinator.rootNode.childAtIndex(destinationIndexPath.section)! + } else { + let movementAdjustment = sourceIndexPath > destinationIndexPath ? 1 : 0 + let adjustedDestIndexPath = IndexPath(row: destinationIndexPath.row - movementAdjustment, section: destinationIndexPath.section) + return coordinator.nodeFor(adjustedDestIndexPath)! + } + }() + + // Now we start looking for the parent container + let destParentNode: Node? = { + if destNode.representedObject is Container { + return destNode + } else { + if destNode.parent?.representedObject is Container { + return destNode.parent! + } else { + return nil + } + } + }() + + // Move the Feed + guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else { + return + } + + BatchUpdate.shared.start() + source.account?.moveFeed(feed, from: source, to: destination) { result in + switch result { + case .success: + BatchUpdate.shared.end() + case .failure(let error): + self.errorHandler(error) + } + } + + } + + +} diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 14bd25023..72bac2de0 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -18,9 +18,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { @IBOutlet private weak var markAllAsReadButton: UIBarButtonItem! @IBOutlet private weak var addNewItemButton: UIBarButtonItem! + private lazy var dataSource = makeDataSource() var undoableCommands = [UndoableCommand]() - weak var coordinator: AppCoordinator! + override var canBecomeFirstResponder: Bool { return true } @@ -36,6 +37,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { navigationItem.rightBarButtonItem = editButtonItem tableView.register(MasterFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") + tableView.dataSource = dataSource NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) @@ -48,6 +50,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged) updateUI() + applyChanges(animate: false) } @@ -71,38 +74,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { // MARK: Notifications @objc func unreadCountDidChange(_ note: Notification) { - updateUI() - - guard let representedObject = note.object else { - return - } - - if let account = representedObject as? Account { - if let node = coordinator.rootNode.childNodeRepresentingObject(account) { - let sectionIndex = coordinator.rootNode.indexOfChild(node)! - if let headerView = tableView.headerView(forSection: sectionIndex) as? MasterFeedTableViewSectionHeader { - headerView.unreadCount = account.unreadCount - } - } - return - } - - var node: Node? = nil - if let coordinator = representedObject as? AppCoordinator, let fetcher = coordinator.timelineFetcher { - node = coordinator.rootNode.descendantNodeRepresentingObject(fetcher as AnyObject) - } else { - node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject) - } - - guard let unwrappedNode = node, let indexPath = coordinator.indexPathFor(unwrappedNode) else { - return - } - - performBlockAndRestoreSelection { - tableView.reloadRows(at: [indexPath], with: .none) - } - + applyChanges(animate: false) } @objc func faviconDidBecomeAvailable(_ note: Notification) { @@ -110,15 +83,12 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } @objc func feedSettingDidChange(_ note: Notification) { - guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else { return } - if key == Feed.FeedSettingKey.homePageURL || key == Feed.FeedSettingKey.faviconURL { configureCellsForRepresentedObject(feed) } - } @objc func userDidAddFeed(_ notification: Notification) { @@ -133,19 +103,11 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } @objc func contentSizeCategoryDidChange(_ note: Notification) { - tableView.reloadData() + applyChanges(animate: false) } // MARK: Table View - override func numberOfSections(in tableView: UITableView) -> Int { - return coordinator.numberOfSections - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return coordinator.rowsInSection(section) - } - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { guard let nameProvider = coordinator.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else { @@ -197,26 +159,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { return UIView(frame: CGRect.zero) } - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell - - guard let node = coordinator.nodeFor(indexPath) else { - return cell - } - - configure(cell, node) - return cell - - } - - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - guard let node = coordinator.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else { - return false - } - return true - } - override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { var actions = [UIContextualAction]() @@ -296,13 +238,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { coordinator.selectFeed(indexPath) } - override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { - guard let node = coordinator.nodeFor(indexPath) else { - return false - } - return node.representedObject is Feed - } - override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { // Adjust the index path so that it will never be in the smart feeds area @@ -360,53 +295,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } - override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { - - guard let sourceNode = coordinator.nodeFor(sourceIndexPath), let feed = sourceNode.representedObject as? Feed else { - return - } - - // Based on the drop we have to determine a node to start looking for a parent container. - let destNode: Node = { - if destinationIndexPath.row == 0 { - return coordinator.rootNode.childAtIndex(destinationIndexPath.section)! - } else { - let movementAdjustment = sourceIndexPath > destinationIndexPath ? 1 : 0 - let adjustedDestIndexPath = IndexPath(row: destinationIndexPath.row - movementAdjustment, section: destinationIndexPath.section) - return coordinator.nodeFor(adjustedDestIndexPath)! - } - }() - - // Now we start looking for the parent container - let destParentNode: Node? = { - if destNode.representedObject is Container { - return destNode - } else { - if destNode.parent?.representedObject is Container { - return destNode.parent! - } else { - return nil - } - } - }() - - // Move the Feed - guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else { - return - } - - BatchUpdate.shared.start() - source.account?.moveFeed(feed, from: source, to: destination) { result in - switch result { - case .success: - BatchUpdate.shared.end() - case .failure(let error): - self.presentError(error) - } - } - - } - // MARK: Actions @IBAction func settings(_ sender: UIBarButtonItem) { @@ -449,18 +337,12 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { if coordinator.isExpanded(sectionNode) { headerView.disclosureExpanded = false - coordinator.collapse(section: sectionIndex) { [weak self] indexPaths in - self?.tableView.beginUpdates() - self?.tableView.deleteRows(at: indexPaths, with: .automatic) - self?.tableView.endUpdates() - } + coordinator.collapse(section: sectionIndex) + self.applyChanges(animate: true) } else { headerView.disclosureExpanded = true - coordinator.expand(section: sectionIndex) { [weak self] indexPaths in - self?.tableView.beginUpdates() - self?.tableView.insertRows(at: indexPaths, with: .automatic) - self?.tableView.endUpdates() - } + coordinator.expand(section: sectionIndex) + self.applyChanges(animate: true) } } @@ -486,7 +368,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { func reloadFeeds() { updateUI() - tableView.reloadData() + applyChanges(animate: true) } func discloseFeed(_ feed: Feed) { @@ -506,16 +388,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { return } - coordinator.expand(indexPath) { [weak self] indexPaths in - self?.tableView.beginUpdates() - tableView.reloadRows(at: [indexPath], with: .automatic) - self?.tableView.insertRows(at: indexPaths, with: .automatic) - self?.tableView.endUpdates() - } - - if let indexPath = coordinator.indexPathFor(node) { - tableView.scrollToRow(at: indexPath, at: .middle, animated: true) - coordinator.selectFeed(indexPath) + coordinator.expand(indexPath) + reloadNode(parent) + + self.applyChanges(animate: true) { [weak self] in + if let indexPath = self?.coordinator.indexPathFor(node) { + self?.tableView.scrollToRow(at: indexPath, at: .middle, animated: true) + self?.coordinator.selectFeed(indexPath) + } } } @@ -544,7 +424,49 @@ private extension MasterFeedViewController { markAllAsReadButton.isEnabled = coordinator.isAnyUnreadAvailable addNewItemButton.isEnabled = !AccountManager.shared.activeAccounts.isEmpty } + + func reloadNode(_ node: Node) { + var snapshot = dataSource.snapshot() + snapshot.reloadItems([node]) + dataSource.apply(snapshot) + } + + func applyChanges(animate: Bool, completion: (() -> Void)? = nil) { + + let selectedNode: Node? = { + if let selectedIndexPath = tableView.indexPathForSelectedRow { + return coordinator.nodeFor(selectedIndexPath) + } else { + return nil + } + }() + + var snapshot = NSDiffableDataSourceSnapshot<Int, Node>() + + let sections = coordinator.allSections + snapshot.appendSections(sections) + for section in sections { + snapshot.appendItems(coordinator.nodesFor(section: section), toSection: section) + } + + dataSource.apply(snapshot, animatingDifferences: animate) { [weak self] in + if let nodeToSelect = selectedNode, let selectingIndexPath = self?.coordinator.indexPathFor(nodeToSelect) { + self?.tableView.selectRow(at: selectingIndexPath, animated: false, scrollPosition: .none) + } + completion?() + } + + } + + func makeDataSource() -> UITableViewDiffableDataSource<Int, Node> { + return MasterFeedDataSource(coordinator: coordinator, errorHandler: ErrorHandler.present(self), tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell + self?.configure(cell, node) + return cell + }) + } + func configure(_ cell: MasterFeedTableViewCell, _ node: Node) { cell.delegate = self @@ -619,32 +541,18 @@ private extension MasterFeedViewController { guard let indexPath = tableView.indexPath(for: cell) else { return } - coordinator.expand(indexPath) { [weak self] indexPaths in - self?.tableView.beginUpdates() - self?.tableView.insertRows(at: indexPaths, with: .automatic) - self?.tableView.endUpdates() - } + coordinator.expand(indexPath) + self.applyChanges(animate: true) } func collapse(_ cell: MasterFeedTableViewCell) { guard let indexPath = tableView.indexPath(for: cell) else { return } - coordinator.collapse(indexPath) { [weak self] indexPaths in - self?.tableView.beginUpdates() - self?.tableView.deleteRows(at: indexPaths, with: .automatic) - self?.tableView.endUpdates() - } + coordinator.collapse(indexPath) + self.applyChanges(animate: true) } - func performBlockAndRestoreSelection(_ block: (() -> Void)) { - let indexPaths = tableView.indexPathsForSelectedRows - block() - indexPaths?.forEach { [weak self] indexPath in - self?.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) - } - } - func makeFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration { return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in @@ -820,7 +728,7 @@ private extension MasterFeedViewController { feed.rename(to: name) { result in switch result { case .success: - break + self?.reloadNode(node) case .failure(let error): self?.presentError(error) } @@ -829,7 +737,7 @@ private extension MasterFeedViewController { folder.rename(to: name) { result in switch result { case .success: - break + self?.reloadNode(node) case .failure(let error): self?.presentError(error) } @@ -851,7 +759,6 @@ private extension MasterFeedViewController { } func delete(indexPath: IndexPath) { - guard let undoManager = undoManager, let deleteNode = coordinator.nodeFor(indexPath), let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], treeController: coordinator.treeController, undoManager: undoManager, errorHandler: ErrorHandler.present(self)) @@ -859,23 +766,14 @@ private extension MasterFeedViewController { return } - var deleteIndexPaths = [indexPath] - if coordinator.isExpanded(deleteNode) { - for i in 0..<deleteNode.numberOfChildNodes { - deleteIndexPaths.append(IndexPath(row: indexPath.row + 1 + i, section: indexPath.section)) - } + if let folder = deleteNode.representedObject as? Folder { + ActivityManager.shared.cleanUp(folder) + } else if let feed = deleteNode.representedObject as? Feed { + ActivityManager.shared.cleanUp(feed) } pushUndoableCommand(deleteCommand) - - coordinator.beginUpdates() - deleteCommand.perform { - self.coordinator.treeController.rebuild() - self.coordinator.rebuildShadowTable() - self.tableView.deleteRows(at: deleteIndexPaths, with: .automatic) - self.coordinator.endUpdates() - } - + deleteCommand.perform() } } diff --git a/iOS/Settings/SettingsDetailAccountView.swift b/iOS/Settings/SettingsDetailAccountView.swift index 606a6f487..8bc4d5edf 100644 --- a/iOS/Settings/SettingsDetailAccountView.swift +++ b/iOS/Settings/SettingsDetailAccountView.swift @@ -114,6 +114,7 @@ struct SettingsDetailAccountView : View { func delete() { AccountManager.shared.deleteAccount(account) + ActivityManager.shared.cleanUp(account) } } } diff --git a/iOS/Settings/UIKit/DetailAccountViewController.swift b/iOS/Settings/UIKit/DetailAccountViewController.swift index 43b281ada..ca01d1db9 100644 --- a/iOS/Settings/UIKit/DetailAccountViewController.swift +++ b/iOS/Settings/UIKit/DetailAccountViewController.swift @@ -121,6 +121,7 @@ private extension DetailAccountViewController { let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in guard let account = self?.account else { return } AccountManager.shared.deleteAccount(account) + ActivityManager.shared.cleanUp(account) self?.navigationController?.popViewController(animated: true) } alertController.addAction(markAction)