From 7c947b935d9c06d1bc4b467b2ed8db9f4e714f3f Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 20 Nov 2019 16:41:13 -0600 Subject: [PATCH 01/50] Rewrite three panel mode so that background screenshooting will work. --- NetNewsWire.xcodeproj/project.pbxproj | 4 - iOS/RootSplitViewController.swift | 4 +- iOS/SceneCoordinator.swift | 169 ++++++++++-------- iOS/SceneDelegate.swift | 2 +- .../UISplitViewController-Extensions.swift | 20 --- 5 files changed, 100 insertions(+), 99 deletions(-) delete mode 100644 iOS/UIKit Extensions/UISplitViewController-Extensions.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 92322aad5..0f1f6dde2 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -35,7 +35,6 @@ 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; }; 512E08E72268801200BDCFDD /* WebFeedTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97611ED9EB96007D329B /* WebFeedTreeControllerDelegate.swift */; }; 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */; }; - 512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512E092B2268B25500BDCFDD /* UISplitViewController-Extensions.swift */; }; 512E094D2268B8AB00BDCFDD /* DeleteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C9C1FAE83C600ECDEDB /* DeleteCommand.swift */; }; 5131463E235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 51314668235A7E4600387FDC /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51314666235A7E4600387FDC /* IntentHandler.swift */; }; @@ -1233,7 +1232,6 @@ 512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageHeaderView.swift; sourceTree = ""; }; 512AF9DC236F05230066F8BE /* InteractiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveLabel.swift; sourceTree = ""; }; 512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewSectionHeader.swift; sourceTree = ""; }; - 512E092B2268B25500BDCFDD /* UISplitViewController-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISplitViewController-Extensions.swift"; sourceTree = ""; }; 51314617235A797400387FDC /* NetNewsWire_iOSintentextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSintentextension_target.xcconfig; sourceTree = ""; }; 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire iOS Intents Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 51314665235A7E4600387FDC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1863,7 +1861,6 @@ 5108F6D723763094001ABC45 /* TickMarkSlider.swift */, 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */, 51F85BF622749FA100C787DC /* UIFont-Extensions.swift */, - 512E092B2268B25500BDCFDD /* UISplitViewController-Extensions.swift */, 51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */, 51FFF0C3235EE8E5002762AA /* VibrantButton.swift */, 5186A634235EF3A800C97195 /* VibrantLabel.swift */, @@ -3951,7 +3948,6 @@ 51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */, 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, 5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */, - 512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */, 51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */, 51C452A022650A1900C03939 /* WebFeedIconDownloader.swift in Sources */, 51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */, diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index 007425424..2e2ca5e68 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -22,9 +22,7 @@ class RootSplitViewController: UISplitViewController { } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - if UIApplication.shared.applicationState != .background { - self.coordinator.configureThreePanelMode(for: size) - } + self.coordinator.configurePanelMode(for: size) super.viewWillTransition(to: size, with: coordinator) } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 71bd4dc91..9a18ae7e5 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -13,6 +13,11 @@ import Articles import RSCore import RSTree +enum PanelMode { + case unset + case three + case standard +} enum SearchScope: Int { case timeline = 0 case global = 1 @@ -25,6 +30,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return rootSplitViewController.undoManager } + private var panelMode: PanelMode = .unset + private var activityManager = ActivityManager() private var isShowingExtractedArticle = false @@ -34,10 +41,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { private var masterNavigationController: UINavigationController! private var masterFeedViewController: MasterFeedViewController! private var masterTimelineViewController: MasterTimelineViewController? - - private var subSplitViewController: UISplitViewController? { - return rootSplitViewController.children.last as? UISplitViewController - } + private var subSplitViewController: UISplitViewController? private var articleViewController: ArticleViewController? { if let detail = masterNavigationController.viewControllers.last as? ArticleViewController { @@ -55,6 +59,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return nil } + private var wasRootSplitViewControllerCollapsed = false + private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) private var fetchSerialNumber = 0 private let fetchRequestQueue = FetchRequestQueue() @@ -103,7 +109,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } var isThreePanelMode: Bool { - return subSplitViewController != nil + return panelMode == .three } var rootNode: Node { @@ -297,7 +303,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { let detailNavigationController = addNavControllerIfNecessary(articleViewController, showButton: true) rootSplitViewController.showDetailViewController(detailNavigationController, sender: self) - configureThreePanelMode(for: size) + configurePanelMode(for: size) return rootSplitViewController } @@ -325,19 +331,24 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { handleReadArticle(userInfo) } - func configureThreePanelMode(for size: CGSize) { - guard rootSplitViewController.traitCollection.userInterfaceIdiom == .pad && !rootSplitViewController.isCollapsed else { + func configurePanelMode(for size: CGSize) { + guard rootSplitViewController.traitCollection.userInterfaceIdiom == .pad else { return } + if (size.width / size.height) > 1.2 { - if !isThreePanelMode { - transitionToThreePanelMode() + if panelMode == .unset || panelMode == .standard { + panelMode = .three + configureThreePanelMode() } } else { - if isThreePanelMode { - transitionFromThreePanelMode() + if panelMode == .unset || panelMode == .three { + panelMode = .standard + configureStandardPanelMode() } } + + wasRootSplitViewControllerCollapsed = rootSplitViewController.isCollapsed } func selectFirstUnreadInAllUnread() { @@ -608,9 +619,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { let currentArticleViewController: ArticleViewController if articleViewController == nil { - currentArticleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) - currentArticleViewController.coordinator = self - installArticleController(currentArticleViewController, animated: animated) + currentArticleViewController = installArticleController(animated: animated) } else { currentArticleViewController = articleViewController! } @@ -963,15 +972,36 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { extension SceneCoordinator: UISplitViewControllerDelegate { func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool { + guard !isThreePanelMode else { + return true + } + + if let articleViewController = (secondaryViewController as? UINavigationController)?.topViewController as? ArticleViewController { + masterNavigationController.pushViewController(articleViewController, animated: false) + return false + } + return currentArticle == nil } func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? { - if currentArticle == nil { + guard !isThreePanelMode else { + return subSplitViewController + } + + guard currentArticle != nil else { let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) articleViewController.coordinator = self - return articleViewController + let controller = addNavControllerIfNecessary(articleViewController, showButton: true) + return controller } + + if let articleViewController = masterNavigationController.viewControllers.last as? ArticleViewController { + masterNavigationController.popViewController(animated: false) + let controller = addNavControllerIfNecessary(articleViewController, showButton: true) + return controller + } + return nil } @@ -986,7 +1016,6 @@ extension SceneCoordinator: UINavigationControllerDelegate { if UIApplication.shared.applicationState == .background { return } - // If we are showing the Feeds and only the feeds start clearing stuff if viewController === masterFeedViewController && !isThreePanelMode && !isTimelineViewControllerPending { @@ -1518,26 +1547,39 @@ private extension SceneCoordinator { } } - func installArticleController(_ articleController: UIViewController, animated: Bool) { + @discardableResult + func installArticleController(_ recycledArticleController: ArticleViewController? = nil, animated: Bool) -> ArticleViewController { isArticleViewControllerPending = true + let articleController: ArticleViewController = { + if let controller = recycledArticleController { + return controller + } else { + let controller = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) + controller.coordinator = self + return controller + } + }() + if let subSplit = subSplitViewController { let controller = addNavControllerIfNecessary(articleController, showButton: false) subSplit.showDetailViewController(controller, sender: self) - } else if rootSplitViewController.isCollapsed { - let controller = addNavControllerIfNecessary(articleController, showButton: false) - masterNavigationController.pushViewController(controller, animated: animated) + } else if rootSplitViewController.isCollapsed || wasRootSplitViewControllerCollapsed { + masterNavigationController.pushViewController(articleController, animated: animated) } else { let controller = addNavControllerIfNecessary(articleController, showButton: true) rootSplitViewController.showDetailViewController(controller, sender: self) } + return articleController + } func addNavControllerIfNecessary(_ controller: UIViewController, showButton: Bool) -> UIViewController { - if rootSplitViewController.traitCollection.horizontalSizeClass == .compact { + // You will sometimes get a compact horizontal size class while in three panel mode. Dunno why it lies. + if rootSplitViewController.traitCollection.horizontalSizeClass == .compact && !isThreePanelMode { return controller @@ -1560,14 +1602,16 @@ private extension SceneCoordinator { } - func configureDoubleSplit() { + func installSubSplit() { rootSplitViewController.preferredPrimaryColumnWidthFraction = 0.30 - let subSplit = UISplitViewController.template() - subSplit.preferredDisplayMode = .allVisible - subSplit.preferredPrimaryColumnWidthFraction = 0.4285 + subSplitViewController = UISplitViewController() + subSplitViewController!.preferredDisplayMode = .allVisible + subSplitViewController!.viewControllers = [InteractiveNavigationController.template()] + subSplitViewController!.preferredPrimaryColumnWidthFraction = 0.4285 - rootSplitViewController.showDetailViewController(subSplit, sender: self) + rootSplitViewController.showDetailViewController(subSplitViewController!, sender: self) + rootSplitViewController.setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: .regular), forChild: subSplitViewController!) } func navControllerForTimeline() -> UINavigationController { @@ -1578,67 +1622,50 @@ private extension SceneCoordinator { } } - @discardableResult - func transitionToThreePanelMode() -> UIViewController { - + func configureThreePanelMode() { + let recycledArticleController = articleViewController defer { masterNavigationController.viewControllers = [masterFeedViewController] } - let controller: UIViewController = { - if let result = articleViewController { - return result - } else { - let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) - articleViewController.coordinator = self - return articleViewController - } - }() - configureDoubleSplit() + if rootSplitViewController.viewControllers.last is InteractiveNavigationController { + _ = rootSplitViewController.viewControllers.popLast() + } + + installSubSplit() installTimelineControllerIfNecessary(animated: false) masterTimelineViewController?.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem masterTimelineViewController?.navigationItem.leftItemsSupplementBackButton = true - // Create the new sub split controller and add the timeline in the primary position - let masterTimelineNavController = subSplitViewController!.viewControllers.first as! UINavigationController - masterTimelineNavController.viewControllers = [masterTimelineViewController!] - - // Put the detail or no selection controller in the secondary (or detail) position of the sub split - let navController = addNavControllerIfNecessary(controller, showButton: false) - subSplitViewController!.showDetailViewController(navController, sender: self) + installArticleController(recycledArticleController, animated: false) masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true) masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: false) - - // We made sure this was there above when we called configureDoubleSplit - return subSplitViewController! - } - func transitionFromThreePanelMode() { - + func configureStandardPanelMode() { + let recycledArticleController = articleViewController rootSplitViewController.preferredPrimaryColumnWidthFraction = UISplitViewController.automaticDimension - if let subSplit = rootSplitViewController.viewControllers.last as? UISplitViewController { - - // Push a new timeline on to the master navigation controller. For some reason recycling the timeline can freak - // the system out and throw it into an infinite loop. - if currentFeedIndexPath != nil { - masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) - masterTimelineViewController!.coordinator = self - masterNavigationController.pushViewController(masterTimelineViewController!, animated: false) - } - - // Pull the detail or no selection controller out of the sub split second position and move it to the root split controller - // secondary (detail) position. - if let detailNav = subSplit.viewControllers.last as? UINavigationController, let topController = detailNav.topViewController { - let newNav = addNavControllerIfNecessary(topController, showButton: true) - rootSplitViewController.showDetailViewController(newNav, sender: self) - } + // Set the is Pending flags early to prevent the navigation controller delegate from thinking that we + // swiping around in the user interface + isTimelineViewControllerPending = true + isArticleViewControllerPending = true + masterNavigationController.viewControllers = [masterFeedViewController] + if rootSplitViewController.viewControllers.last is UISplitViewController { + subSplitViewController = nil + _ = rootSplitViewController.viewControllers.popLast() } - + + if currentFeedIndexPath != nil { + masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) + masterTimelineViewController!.coordinator = self + masterNavigationController.pushViewController(masterTimelineViewController!, animated: false) + } + + installArticleController(recycledArticleController, animated: false) } // MARK: NSUserActivity diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 05df42d02..0a8868d1a 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -58,7 +58,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneWillEnterForeground(_ scene: UIScene) { appDelegate.prepareAccountsForForeground() - self.coordinator.configureThreePanelMode(for: window!.frame.size) + self.coordinator.configurePanelMode(for: window!.frame.size) } func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { diff --git a/iOS/UIKit Extensions/UISplitViewController-Extensions.swift b/iOS/UIKit Extensions/UISplitViewController-Extensions.swift deleted file mode 100644 index 63e9acc18..000000000 --- a/iOS/UIKit Extensions/UISplitViewController-Extensions.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// UISplitViewController-Extensions.swift -// NetNewsWire -// -// Created by Maurice Parker on 4/18/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import UIKit - -extension UISplitViewController { - - static func template() -> UISplitViewController { - let splitViewController = UISplitViewController() - splitViewController.preferredDisplayMode = .automatic - splitViewController.viewControllers = [InteractiveNavigationController.template()] - return splitViewController - } - -} From 56d43f8f2d16bf0ca1179b22f3059e466d25704d Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 20 Nov 2019 18:16:54 -0600 Subject: [PATCH 02/50] Do a full reload of the article when rearranging the UI to accommodate for split screen color changes. --- Mac/MainWindow/Detail/DetailWebViewController.swift | 2 +- Shared/Article Rendering/main.js | 4 ++-- iOS/Article/ArticleViewController.swift | 13 ++++++++++++- iOS/SceneCoordinator.swift | 4 ++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index 9ae240bfc..1ef693fbe 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -221,7 +221,7 @@ private extension DetailWebViewController { var render = "error();" if let data = try? encoder.encode(templateData) { let json = String(data: data, encoding: .utf8)! - render = "render(\(json));" + render = "render(\(json), 0);" } webView.evaluateJavaScript(render) diff --git a/Shared/Article Rendering/main.js b/Shared/Article Rendering/main.js index 22a7a8442..4ecf8eb5f 100644 --- a/Shared/Article Rendering/main.js +++ b/Shared/Article Rendering/main.js @@ -30,11 +30,11 @@ function error() { document.body.innerHTML = "error"; } -function render(data) { +function render(data, scrollY) { document.getElementsByTagName("style")[0].innerHTML = data.style; document.body.innerHTML = data.body; - window.scrollTo(0, 0); + window.scrollTo(0, scrollY); wrapFrames() stripStyles() diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 3f8689d9c..5f67abd26 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -59,6 +59,8 @@ class ArticleViewController: UIViewController { } } + var restoreOffset = 0 + var currentArticle: Article? { switch state { case .article(let article): @@ -190,9 +192,11 @@ class ArticleViewController: UIViewController { var render = "error();" if let data = try? encoder.encode(templateData) { let json = String(data: data, encoding: .utf8)! - render = "render(\(json));" + render = "render(\(json), \(restoreOffset));" } + restoreOffset = 0 + ArticleViewControllerWebViewProvider.shared.articleIconSchemeHandler.currentArticle = currentArticle webView?.scrollView.setZoomScale(1.0, animated: false) webView?.evaluateJavaScript(render) @@ -319,6 +323,13 @@ class ArticleViewController: UIViewController { webView?.evaluateJavaScript("showClickedImage();") } + func fullReload() { + if let offset = webView?.scrollView.contentOffset.y { + restoreOffset = Int(offset) + webView?.reload() + } + } + } // MARK: WKNavigationDelegate diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 9a18ae7e5..7e89ea43f 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1572,6 +1572,10 @@ private extension SceneCoordinator { rootSplitViewController.showDetailViewController(controller, sender: self) } + // We have to do a full reload when installing an article controller. We may have changed color contexts + // and need to update the article colors. An example is in dark mode. Split screen doesn't use true black + // like darkmode usually does. + articleController.fullReload() return articleController } From f818a1618f402e444ba0edaec7314089ae3bd66c Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 20 Nov 2019 20:28:24 -0600 Subject: [PATCH 03/50] Implement drag and drop feed arrangement. --- NetNewsWire.xcodeproj/project.pbxproj | 16 +- iOS/MasterFeed/MasterFeedDataSource.swift | 140 +----------------- .../MasterFeedViewController+Drag.swift | 33 +++++ iOS/MasterFeed/MasterFeedViewController.swift | 9 +- 4 files changed, 51 insertions(+), 147 deletions(-) create mode 100644 iOS/MasterFeed/MasterFeedViewController+Drag.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 0f1f6dde2..e19fa382a 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -90,6 +90,9 @@ 515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; }; 515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; }; 515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; }; + 51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */; }; + 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */; }; + 51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */; }; 516A093723609A3600EAE89B /* SettingsAccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */; }; 516A09392360A2AE00EAE89B /* SettingsAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */; }; 516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */; }; @@ -212,7 +215,6 @@ 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 */; }; 51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; }; 51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; }; 51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; }; @@ -1266,6 +1268,9 @@ 515D4FCB2325815A00EE1167 /* SafariExt.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = SafariExt.js; sourceTree = ""; }; 515D4FCD2325909200EE1167 /* NetNewsWire_iOS_ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_ShareExtension.entitlements; sourceTree = ""; }; 515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSshareextension_target.xcconfig; sourceTree = ""; }; + 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drag.swift"; sourceTree = ""; }; + 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drop.swift"; sourceTree = ""; }; + 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = ""; }; 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsAccountTableViewCell.xib; sourceTree = ""; }; 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountTableViewCell.swift; sourceTree = ""; }; 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsTableViewCell.xib; sourceTree = ""; }; @@ -1326,7 +1331,6 @@ 51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = ""; }; 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 = ""; }; - 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = ""; }; 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = ""; }; 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = ""; }; @@ -1875,7 +1879,9 @@ isa = PBXGroup; children = ( 51C45264226508F600C03939 /* MasterFeedViewController.swift */, - 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */, + 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */, + 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */, + 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */, 51CE1C0A23622006005548FC /* RefreshProgressView.swift */, 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */, 51C45260226508F600C03939 /* Cell */, @@ -3976,6 +3982,7 @@ 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, + 51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */, 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */, 5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */, 5108F6D42375EEEF001ABC45 /* TimelinePreviewTableViewController.swift in Sources */, @@ -4001,6 +4008,7 @@ 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */, 5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */, 518651DA235621840078E021 /* ImageTransition.swift in Sources */, + 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */, 514219372352510100E07E2C /* ImageScrollView.swift in Sources */, 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, @@ -4012,12 +4020,12 @@ 51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */, 5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */, 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */, - 51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */, FFD43E412340F488009E5CA3 /* UndoAvailableAlertController.swift in Sources */, 51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */, 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, 51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */, 5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */, + 51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */, 51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */, 513228FC233037630033D4ED /* Reachability.swift in Sources */, 51C45259226508D300C03939 /* AppDefaults.swift in Sources */, diff --git a/iOS/MasterFeed/MasterFeedDataSource.swift b/iOS/MasterFeed/MasterFeedDataSource.swift index b2353fe0f..9d6511354 100644 --- a/iOS/MasterFeed/MasterFeedDataSource.swift +++ b/iOS/MasterFeed/MasterFeedDataSource.swift @@ -14,12 +14,10 @@ import Account class MasterFeedDataSource: UITableViewDiffableDataSource { private var coordinator: SceneCoordinator! - private var errorHandler: ((Error) -> ())! - init(coordinator: SceneCoordinator, errorHandler: @escaping (Error) -> (), tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource.CellProvider) { + init(coordinator: SceneCoordinator, tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource.CellProvider) { super.init(tableView: tableView, cellProvider: cellProvider) self.coordinator = coordinator - self.errorHandler = errorHandler self.defaultRowAnimation = .middle } @@ -30,140 +28,4 @@ class MasterFeedDataSource: UITableViewDiffableDataSource { return true } - override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { - guard let node = itemIdentifier(for: indexPath) else { - return false - } - return node.representedObject is WebFeed - } - - override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { - - guard let sourceNode = itemIdentifier(for: sourceIndexPath), let webFeed = sourceNode.representedObject as? WebFeed 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 itemIdentifier(for: 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 Web Feed - guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else { - return - } - - if sameAccount(sourceNode, destParentNode!) { - moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination) - } else { - moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination) - } - - } - - private func sameAccount(_ node: Node, _ parentNode: Node) -> Bool { - if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) { - if accountID == parentAccountID { - return true - } - } - return false - } - - private func nodeAccount(_ node: Node) -> Account? { - if let account = node.representedObject as? Account { - return account - } else if let folder = node.representedObject as? Folder { - return folder.account - } else if let webFeed = node.representedObject as? WebFeed { - return webFeed.account - } else { - return nil - } - - } - - private func nodeAccountID(_ node: Node) -> String? { - return nodeAccount(node)?.accountID - } - - func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { - BatchUpdate.shared.start() - sourceContainer.account?.moveWebFeed(feed, from: sourceContainer, to: destinationContainer) { result in - BatchUpdate.shared.end() - switch result { - case .success: - break - case .failure(let error): - self.errorHandler(error) - } - } - } - - func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { - - if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) { - - BatchUpdate.shared.start() - destinationContainer.account?.addWebFeed(existingFeed, to: destinationContainer) { result in - switch result { - case .success: - sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in - BatchUpdate.shared.end() - switch result { - case .success: - break - case .failure(let error): - self.errorHandler(error) - } - } - case .failure(let error): - BatchUpdate.shared.end() - self.errorHandler(error) - } - } - - } else { - - BatchUpdate.shared.start() - destinationContainer.account?.createWebFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in - switch result { - case .success: - sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in - BatchUpdate.shared.end() - switch result { - case .success: - break - case .failure(let error): - self.errorHandler(error) - } - } - case .failure(let error): - BatchUpdate.shared.end() - self.errorHandler(error) - } - } - - } - } - } diff --git a/iOS/MasterFeed/MasterFeedViewController+Drag.swift b/iOS/MasterFeed/MasterFeedViewController+Drag.swift new file mode 100644 index 000000000..333b89814 --- /dev/null +++ b/iOS/MasterFeed/MasterFeedViewController+Drag.swift @@ -0,0 +1,33 @@ +// +// MasterFeedViewController+Drag.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 11/20/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit +import MobileCoreServices +import Account + +extension MasterFeedViewController: UITableViewDragDelegate { + + func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + guard let node = dataSource.itemIdentifier(for: indexPath), let webFeed = node.representedObject as? WebFeed else { + return [UIDragItem]() + } + + let data = webFeed.url.data(using: .utf8) + let itemProvider = NSItemProvider() + + itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeURL as String, visibility: .all) { completion in + completion(data, nil) + return nil + } + + let dragItem = UIDragItem(itemProvider: itemProvider) + dragItem.localObject = node + return [dragItem] + } + +} diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index f31a16525..cc3ade3d5 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -17,7 +17,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { private var refreshProgressView: RefreshProgressView? private var addNewItemButton: UIBarButtonItem! - private lazy var dataSource = makeDataSource() + lazy var dataSource = makeDataSource() var undoableCommands = [UndoableCommand]() weak var coordinator: SceneCoordinator! @@ -38,8 +38,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { navigationController?.navigationBar.prefersLargeTitles = true } - navigationItem.rightBarButtonItem = editButtonItem - // Set the bar button item so that it doesn't show on the timeline view navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) @@ -51,6 +49,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { tableView.register(MasterFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.dataSource = dataSource + tableView.dragDelegate = self + tableView.dropDelegate = self + tableView.dragInteractionEnabled = true resetEstimatedRowHeight() tableView.separatorStyle = .none @@ -630,7 +631,7 @@ private extension MasterFeedViewController { } func makeDataSource() -> UITableViewDiffableDataSource { - return MasterFeedDataSource(coordinator: coordinator, errorHandler: ErrorHandler.present(self), tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in + return MasterFeedDataSource(coordinator: coordinator, 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 From 7243e0e07b7280398c34053e39ce753e4166a822 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 20 Nov 2019 20:28:50 -0600 Subject: [PATCH 04/50] Implement drag and drop feed arrangement --- .../MasterFeedViewController+Drop.swift | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 iOS/MasterFeed/MasterFeedViewController+Drop.swift diff --git a/iOS/MasterFeed/MasterFeedViewController+Drop.swift b/iOS/MasterFeed/MasterFeedViewController+Drop.swift new file mode 100644 index 000000000..a64c0c89b --- /dev/null +++ b/iOS/MasterFeed/MasterFeedViewController+Drop.swift @@ -0,0 +1,167 @@ +// +// MasterFeedViewController+Drop.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 11/20/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit +import RSCore +import Account +import RSTree + +extension MasterFeedViewController: UITableViewDropDelegate { + + func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { + return session.localDragSession != nil //&& session.canLoadObjects(ofClass: URL.self) + } + + func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { + if tableView.hasActiveDrag { + if let destIndexPath = destinationIndexPath, let destNode = dataSource.itemIdentifier(for: destIndexPath) { + if destNode.representedObject is Folder { + return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) + } else { + return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) + } + } + } + return UITableViewDropProposal(operation: .forbidden) + } + + func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) { + guard let dragItem = dropCoordinator.items.first?.dragItem, + let sourceNode = dragItem.localObject as? Node, + let sourceIndexPath = dataSource.indexPath(for: sourceNode), + let webFeed = sourceNode.representedObject as? WebFeed, + let destinationIndexPath = dropCoordinator.destinationIndexPath 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 dataSource.itemIdentifier(for: 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 Web Feed + guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else { + return + } + + if sameAccount(sourceNode, destParentNode!) { + moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination) + } else { + moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination) + } + + + } + + private func sameAccount(_ node: Node, _ parentNode: Node) -> Bool { + if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) { + if accountID == parentAccountID { + return true + } + } + return false + } + + private func nodeAccount(_ node: Node) -> Account? { + if let account = node.representedObject as? Account { + return account + } else if let folder = node.representedObject as? Folder { + return folder.account + } else if let webFeed = node.representedObject as? WebFeed { + return webFeed.account + } else { + return nil + } + + } + + private func nodeAccountID(_ node: Node) -> String? { + return nodeAccount(node)?.accountID + } + + func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { + BatchUpdate.shared.start() + sourceContainer.account?.moveWebFeed(feed, from: sourceContainer, to: destinationContainer) { result in + BatchUpdate.shared.end() + switch result { + case .success: + break + case .failure(let error): + self.presentError(error) + } + } + } + + func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { + + if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) { + + BatchUpdate.shared.start() + destinationContainer.account?.addWebFeed(existingFeed, to: destinationContainer) { result in + switch result { + case .success: + sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in + BatchUpdate.shared.end() + switch result { + case .success: + break + case .failure(let error): + self.presentError(error) + } + } + case .failure(let error): + BatchUpdate.shared.end() + self.presentError(error) + } + } + + } else { + + BatchUpdate.shared.start() + destinationContainer.account?.createWebFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in + switch result { + case .success: + sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in + BatchUpdate.shared.end() + switch result { + case .success: + break + case .failure(let error): + self.presentError(error) + } + } + case .failure(let error): + BatchUpdate.shared.end() + self.presentError(error) + } + } + + } + } + + +} From 150e50082ceeeb9f016e60dfea43c75d7abd0ce3 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 21 Nov 2019 13:22:33 -0600 Subject: [PATCH 05/50] Fix drag and drop target bugs --- .../MasterFeedViewController+Drop.swift | 66 ++++++++++++------- iOS/MasterFeed/MasterFeedViewController.swift | 6 +- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController+Drop.swift b/iOS/MasterFeed/MasterFeedViewController+Drop.swift index a64c0c89b..d77014ea7 100644 --- a/iOS/MasterFeed/MasterFeedViewController+Drop.swift +++ b/iOS/MasterFeed/MasterFeedViewController+Drop.swift @@ -18,45 +18,67 @@ extension MasterFeedViewController: UITableViewDropDelegate { } func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { - if tableView.hasActiveDrag { - if let destIndexPath = destinationIndexPath, let destNode = dataSource.itemIdentifier(for: destIndexPath) { - if destNode.representedObject is Folder { - return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) - } else { - return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) - } - } + guard let destIndexPath = destinationIndexPath, + destIndexPath.section > 0, + tableView.hasActiveDrag, + let destNode = dataSource.itemIdentifier(for: destIndexPath), + let destCell = tableView.cellForRow(at: destIndexPath) else { + return UITableViewDropProposal(operation: .forbidden) } - return UITableViewDropProposal(operation: .forbidden) + + if destNode.representedObject is Folder && session.location(in: destCell).y >= 0 { + return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) + } else { + return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) + } + } func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) { guard let dragItem = dropCoordinator.items.first?.dragItem, let sourceNode = dragItem.localObject as? Node, - let sourceIndexPath = dataSource.indexPath(for: sourceNode), let webFeed = sourceNode.representedObject as? WebFeed, - let destinationIndexPath = dropCoordinator.destinationIndexPath else { + let destIndexPath = dropCoordinator.destinationIndexPath 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 dataSource.itemIdentifier(for: adjustedDestIndexPath)! + let isFolderDrop: Bool = { + if let propDestNode = dataSource.itemIdentifier(for: destIndexPath), let propCell = tableView.cellForRow(at: destIndexPath) { + return propDestNode.representedObject is Folder && dropCoordinator.session.location(in: propCell).y >= 0 } + return false + }() + + // Based on the drop we have to determine a node to start looking for a parent container. + let destNode: Node? = { + + if destIndexPath.row == 0 { + + return coordinator.rootNode.childAtIndex(destIndexPath.section)! + + } else { + + if isFolderDrop { + return dataSource.itemIdentifier(for: destIndexPath) + } else { + if destIndexPath.row > 0 { + return dataSource.itemIdentifier(for: IndexPath(row: destIndexPath.row - 1, section: destIndexPath.section)) + } else { + return nil + } + } + + } + }() // Now we start looking for the parent container let destParentNode: Node? = { - if destNode.representedObject is Container { + if destNode?.representedObject is Container { return destNode } else { - if destNode.parent?.representedObject is Container { - return destNode.parent! + if destNode?.parent?.representedObject is Container { + return destNode!.parent! } else { return nil } diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index cc3ade3d5..2afb785b7 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -353,13 +353,11 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { sortedNodes.remove(at: index) - let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0 - let adjustedIndex = index - movementAdjustment - if adjustedIndex >= sortedNodes.count { + if index >= sortedNodes.count { let lastSortedIndexPath = dataSource.indexPath(for: sortedNodes[sortedNodes.count - 1])! return IndexPath(row: lastSortedIndexPath.row + 1, section: lastSortedIndexPath.section) } else { - return dataSource.indexPath(for: sortedNodes[adjustedIndex])! + return dataSource.indexPath(for: sortedNodes[index])! } } From eed6333368508a09cc184800d8e2f0d75bd679aa Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 21 Nov 2019 13:49:05 -0600 Subject: [PATCH 06/50] Restrict drag and drop to the same process (for now) --- iOS/MasterFeed/MasterFeedViewController+Drag.swift | 2 +- iOS/MasterFeed/MasterFeedViewController+Drop.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController+Drag.swift b/iOS/MasterFeed/MasterFeedViewController+Drag.swift index 333b89814..65bd16610 100644 --- a/iOS/MasterFeed/MasterFeedViewController+Drag.swift +++ b/iOS/MasterFeed/MasterFeedViewController+Drag.swift @@ -20,7 +20,7 @@ extension MasterFeedViewController: UITableViewDragDelegate { let data = webFeed.url.data(using: .utf8) let itemProvider = NSItemProvider() - itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeURL as String, visibility: .all) { completion in + itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeURL as String, visibility: .ownProcess) { completion in completion(data, nil) return nil } diff --git a/iOS/MasterFeed/MasterFeedViewController+Drop.swift b/iOS/MasterFeed/MasterFeedViewController+Drop.swift index d77014ea7..afaf62a16 100644 --- a/iOS/MasterFeed/MasterFeedViewController+Drop.swift +++ b/iOS/MasterFeed/MasterFeedViewController+Drop.swift @@ -14,7 +14,7 @@ import RSTree extension MasterFeedViewController: UITableViewDropDelegate { func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { - return session.localDragSession != nil //&& session.canLoadObjects(ofClass: URL.self) + return session.localDragSession != nil } func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { From 89e9a7b80e409633b503c76a44375f101a53015f Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 21 Nov 2019 15:55:50 -0600 Subject: [PATCH 07/50] Add filter button show/hide unread feeds. Issue #1311 --- Shared/SmartFeeds/SmartFeedsController.swift | 3 +- .../Tree/WebFeedTreeControllerDelegate.swift | 56 ++++++++++++------- iOS/AppAssets.swift | 8 +++ iOS/Base.lproj/Main.storyboard | 9 +++ iOS/MasterFeed/MasterFeedViewController.swift | 11 ++++ iOS/SceneCoordinator.swift | 14 +++++ 6 files changed, 81 insertions(+), 20 deletions(-) diff --git a/Shared/SmartFeeds/SmartFeedsController.swift b/Shared/SmartFeeds/SmartFeedsController.swift index 37a424a17..683495606 100644 --- a/Shared/SmartFeeds/SmartFeedsController.swift +++ b/Shared/SmartFeeds/SmartFeedsController.swift @@ -8,13 +8,14 @@ import Foundation import RSCore +import Account final class SmartFeedsController: DisplayNameProvider { public static let shared = SmartFeedsController() let nameForDisplay = NSLocalizedString("Smart Feeds", comment: "Smart Feeds group title") - var smartFeeds = [AnyObject]() + var smartFeeds = [Feed]() let todayFeed = SmartFeed(delegate: TodayFeedDelegate()) let unreadFeed = UnreadFeed() let starredFeed = SmartFeed(delegate: StarredFeedDelegate()) diff --git a/Shared/Tree/WebFeedTreeControllerDelegate.swift b/Shared/Tree/WebFeedTreeControllerDelegate.swift index 62409cd14..6fa6fbc8c 100644 --- a/Shared/Tree/WebFeedTreeControllerDelegate.swift +++ b/Shared/Tree/WebFeedTreeControllerDelegate.swift @@ -13,8 +13,9 @@ import Account final class WebFeedTreeControllerDelegate: TreeControllerDelegate { + var isUnreadFiltered = false + func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { - if node.isRoot { return childNodesForRootNode(node) } @@ -32,29 +33,47 @@ final class WebFeedTreeControllerDelegate: TreeControllerDelegate { private extension WebFeedTreeControllerDelegate { func childNodesForRootNode(_ rootNode: Node) -> [Node]? { + var topLevelNodes = [Node]() - // The top-level nodes are Smart Feeds and accounts. + // Check to see if we should show the SmartFeeds top level by checking the unreadFeed + if !(isUnreadFiltered && SmartFeedsController.shared.unreadFeed.unreadCount == 0) { + let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared) + smartFeedsNode.canHaveChildNodes = true + smartFeedsNode.isGroupItem = true + topLevelNodes.append(smartFeedsNode) + } - let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared) - smartFeedsNode.canHaveChildNodes = true - smartFeedsNode.isGroupItem = true - - return [smartFeedsNode] + sortedAccountNodes(rootNode) + topLevelNodes.append(contentsOf: sortedAccountNodes(rootNode)) + + return topLevelNodes } func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] { - - return SmartFeedsController.shared.smartFeeds.map { parentNode.existingOrNewChildNode(with: $0) } + return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in + if isUnreadFiltered && feed.unreadCount == 0 { + return nil + } + return parentNode.existingOrNewChildNode(with: feed as AnyObject) + } } func childNodesForContainerNode(_ containerNode: Node) -> [Node]? { - let container = containerNode.representedObject as! Container var children = [AnyObject]() - children.append(contentsOf: Array(container.topLevelWebFeeds)) + + for webFeed in container.topLevelWebFeeds { + if !(isUnreadFiltered && webFeed.unreadCount == 0) { + children.append(webFeed) + } + } + if let folders = container.folders { - children.append(contentsOf: Array(folders)) + for folder in folders { + if !(isUnreadFiltered && folder.unreadCount == 0) { + children.append(folder) + } + } } var updatedChildNodes = [Node]() @@ -77,13 +96,14 @@ private extension WebFeedTreeControllerDelegate { } func createNode(representedObject: Any, parent: Node) -> Node? { - if let webFeed = representedObject as? WebFeed { return createNode(webFeed: webFeed, parent: parent) } + if let folder = representedObject as? Folder { return createNode(folder: folder, parent: parent) } + if let account = representedObject as? Account { return createNode(account: account, parent: parent) } @@ -92,19 +112,16 @@ private extension WebFeedTreeControllerDelegate { } func createNode(webFeed: WebFeed, parent: Node) -> Node { - return parent.createChildNode(webFeed) } func createNode(folder: Folder, parent: Node) -> Node { - let node = parent.createChildNode(folder) node.canHaveChildNodes = true return node } func createNode(account: Account, parent: Node) -> Node { - let node = parent.createChildNode(account) node.canHaveChildNodes = true node.isGroupItem = true @@ -112,8 +129,10 @@ private extension WebFeedTreeControllerDelegate { } func sortedAccountNodes(_ parent: Node) -> [Node] { - - let nodes = AccountManager.shared.sortedActiveAccounts.map { (account) -> Node in + let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in + if isUnreadFiltered && account.unreadCount == 0 { + return nil + } let accountNode = parent.existingOrNewChildNode(with: account) accountNode.canHaveChildNodes = true accountNode.isGroupItem = true @@ -123,7 +142,6 @@ private extension WebFeedTreeControllerDelegate { } func nodeInArrayRepresentingObject(_ nodes: [Node], _ representedObject: AnyObject) -> Node? { - for oneNode in nodes { if oneNode.representedObject === representedObject { return oneNode diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 20993af6f..081725a9a 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -89,6 +89,14 @@ struct AppAssets { return RSImage(named: "faviconTemplateImage")! }() + static var filterInactiveImage: UIImage = { + UIImage(systemName: "line.horizontal.3.decrease.circle")! + }() + + static var filterActiveImage: UIImage = { + UIImage(systemName: "line.horizontal.3.decrease.circle.fill")! + }() + static var fullScreenBackgroundColor: UIColor = { return UIColor(named: "fullScreenBackgroundColor")! }() diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index dab0d6343..194e60b7b 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -214,9 +214,17 @@ + + + + + + + + @@ -289,6 +297,7 @@ + diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 2afb785b7..89315194b 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -14,6 +14,7 @@ import RSTree class MasterFeedViewController: UITableViewController, UndoableCommandRunner { + @IBOutlet weak var filterButton: UIBarButtonItem! private var refreshProgressView: RefreshProgressView? private var addNewItemButton: UIBarButtonItem! @@ -370,6 +371,16 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { coordinator.showSettings() } + @IBAction func toggleFilter(_ sender: Any) { + if coordinator.isUnreadFeedsFiltered { + filterButton.image = AppAssets.filterInactiveImage + coordinator.showAllFeeds() + } else { + filterButton.image = AppAssets.filterActiveImage + coordinator.hideUnreadFeeds() + } + } + @IBAction func add(_ sender: UIBarButtonItem) { coordinator.showAdd(.feed) } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 7e89ea43f..78d02adf3 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -112,6 +112,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return panelMode == .three } + var isUnreadFeedsFiltered: Bool { + return treeControllerDelegate.isUnreadFiltered + } + var rootNode: Node { return treeController.rootNode } @@ -484,6 +488,16 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } return 0 } + + func showAllFeeds() { + treeControllerDelegate.isUnreadFiltered = false + rebuildBackingStores() + } + + func hideUnreadFeeds() { + treeControllerDelegate.isUnreadFiltered = true + rebuildBackingStores() + } func expand(_ node: Node) { node.isExpanded = true From 64c1a615b098c006b982072da6e9e5c709ba4d50 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 21 Nov 2019 16:25:00 -0600 Subject: [PATCH 08/50] Make sure top level nodes are always expanded. --- .../Tree/WebFeedTreeControllerDelegate.swift | 2 + iOS/SceneCoordinator.swift | 37 ++----------------- 2 files changed, 6 insertions(+), 33 deletions(-) diff --git a/Shared/Tree/WebFeedTreeControllerDelegate.swift b/Shared/Tree/WebFeedTreeControllerDelegate.swift index 6fa6fbc8c..fd4d4f754 100644 --- a/Shared/Tree/WebFeedTreeControllerDelegate.swift +++ b/Shared/Tree/WebFeedTreeControllerDelegate.swift @@ -40,6 +40,7 @@ private extension WebFeedTreeControllerDelegate { let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared) smartFeedsNode.canHaveChildNodes = true smartFeedsNode.isGroupItem = true + smartFeedsNode.isExpanded = true topLevelNodes.append(smartFeedsNode) } @@ -136,6 +137,7 @@ 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/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 78d02adf3..91c7fcf9f 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -268,7 +268,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { super.init() for section in treeController.rootNode.childNodes { - section.isExpanded = true shadowTable.append([Node]()) } @@ -392,51 +391,23 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } @objc func accountStateDidChange(_ note: Notification) { - - let rebuildAndExpand = { - guard let account = note.userInfo?[Account.UserInfoKey.account] as? Account else { - assertionFailure() - return - } - - self.rebuildBackingStores() { - // If we are activating an account, then automatically expand it - if account.isActive, let node = self.treeController.rootNode.childNodeRepresentingObject(account) { - node.isExpanded = true - } - } - } - if timelineFetcherContainsAnyPseudoFeed() { fetchAndReplaceArticlesAsync { - rebuildAndExpand() + self.rebuildBackingStores() } } else { - rebuildAndExpand() + rebuildBackingStores() } - } @objc func userDidAddAccount(_ note: Notification) { - - let rebuildAndExpand = { - self.rebuildBackingStores() { - // Automatically expand any new accounts - if let account = note.userInfo?[Account.UserInfoKey.account] as? Account, - let node = self.treeController.rootNode.childNodeRepresentingObject(account) { - node.isExpanded = true - } - } - } - if timelineFetcherContainsAnyPseudoFeed() { fetchAndReplaceArticlesAsync { - rebuildAndExpand() + self.rebuildBackingStores() } } else { - rebuildAndExpand() + rebuildBackingStores() } - } @objc func userDidDeleteAccount(_ note: Notification) { From e8826130a45a9aea0ab3c1e58a6e67249f609bdf Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 21 Nov 2019 18:22:43 -0600 Subject: [PATCH 09/50] Add timeline filter button --- Frameworks/Account/Feed.swift | 8 ++++++ Frameworks/Account/Folder.swift | 4 +++ Frameworks/Account/WebFeed.swift | 4 +++ Shared/SmartFeeds/SmartFeed.swift | 4 +++ Shared/SmartFeeds/UnreadFeed.swift | 4 +++ iOS/Base.lproj/Main.storyboard | 9 ++++++- .../MasterTimelineViewController.swift | 26 +++++++++++++++++-- iOS/SceneCoordinator.swift | 11 ++++++++ 8 files changed, 67 insertions(+), 3 deletions(-) diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index 3fcf1e3d2..32ba79313 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -9,6 +9,14 @@ import Foundation import RSCore +public enum ReadFilter { + case read + case all + case none +} + public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider { + var defaultReadFilter: ReadFilter { get } + } diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index cf6f1df57..fd3ab44ec 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -12,6 +12,10 @@ import RSCore public final class Folder: Feed, Renamable, Container, Hashable { + public var defaultReadFilter: ReadFilter { + return .read + } + public var feedID: FeedIdentifier? { guard let accountID = account?.accountID else { assertionFailure("Expected feed.account, but got nil.") diff --git a/Frameworks/Account/WebFeed.swift b/Frameworks/Account/WebFeed.swift index 929701123..2bcc1dc5e 100644 --- a/Frameworks/Account/WebFeed.swift +++ b/Frameworks/Account/WebFeed.swift @@ -13,6 +13,10 @@ import Articles public final class WebFeed: Feed, Renamable, Hashable { + public var defaultReadFilter: ReadFilter { + return .all + } + public var feedID: FeedIdentifier? { guard let accountID = account?.accountID else { assertionFailure("Expected feed.account, but got nil.") diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index 0419c47a1..b3e4a59fb 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -13,6 +13,10 @@ import Account final class SmartFeed: PseudoFeed { + public var defaultReadFilter: ReadFilter { + return .all + } + var feedID: FeedIdentifier? { delegate.feedID } diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index f41793988..8238425a2 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -19,6 +19,10 @@ import Articles final class UnreadFeed: PseudoFeed { + public var defaultReadFilter: ReadFilter { + return .none + } + var feedID: FeedIdentifier? { return FeedIdentifier.smartFeed(String(describing: UnreadFeed.self)) } diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index 194e60b7b..61184ea2b 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -167,10 +167,17 @@ - + + + + + + + + diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index e1ec464b8..762b065ff 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -17,6 +17,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner private var numberOfTextLines = 0 private var iconSize = IconSize.medium + @IBOutlet weak var filterButton: UIBarButtonItem! @IBOutlet weak var markAllAsReadButton: UIBarButtonItem! @IBOutlet weak var firstUnreadButton: UIBarButtonItem! @@ -87,7 +88,19 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } // MARK: Actions - + @IBAction func toggleFilter(_ sender: Any) { + switch coordinator.articleReadFilter { + case .all: + filterButton.image = AppAssets.filterActiveImage + coordinator.hideUnreadArticles() + case .read: + filterButton.image = AppAssets.filterInactiveImage + coordinator.showAllArticles() + default: + break + } + } + @IBAction func markAllAsRead(_ sender: Any) { if coordinator.displayUndoAvailableTip { let alertController = UndoAvailableAlertController.alert { [weak self] _ in @@ -466,7 +479,6 @@ extension MasterTimelineViewController: UISearchBarDelegate { private extension MasterTimelineViewController { func resetUI() { - title = coordinator.timelineFeed?.nameForDisplay if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView { self.titleView = titleView @@ -484,6 +496,16 @@ private extension MasterTimelineViewController { navigationItem.titleView = titleView } + switch coordinator.articleReadFilter { + case .all: + filterButton.isHidden = false + filterButton.image = AppAssets.filterInactiveImage + case .read: + filterButton.isHidden = false + filterButton.image = AppAssets.filterActiveImage + default: + filterButton.isHidden = true + } tableView.selectRow(at: nil, animated: false, scrollPosition: .top) if dataSource.snapshot().itemIdentifiers(inSection: 0).count > 0 { diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 91c7fcf9f..a82a121e1 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -116,6 +116,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return treeControllerDelegate.isUnreadFiltered } + var articleReadFilter: ReadFilter = .all + var rootNode: Node { return treeController.rootNode } @@ -469,6 +471,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { treeControllerDelegate.isUnreadFiltered = true rebuildBackingStores() } + + func showAllArticles() { + articleReadFilter = .all + } + + func hideUnreadArticles() { + articleReadFilter = .read + } func expand(_ node: Node) { node.isExpanded = true @@ -1129,6 +1139,7 @@ private extension SceneCoordinator { func setTimelineFeed(_ feed: Feed?, completion: (() -> Void)? = nil) { timelineFeed = feed timelineMiddleIndexPath = nil + articleReadFilter = feed?.defaultReadFilter ?? .all fetchAndReplaceArticlesAsync { self.masterTimelineViewController?.reinitializeArticles() From adbb5b6392c6724cb90041389b91985d6e20ac32 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 21 Nov 2019 19:08:40 -0600 Subject: [PATCH 10/50] Remove callback that shouldn't have been added. --- iOS/SceneCoordinator.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index a82a121e1..322c7ba50 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1483,7 +1483,6 @@ private extension SceneCoordinator { let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, representedObjects: representedObjects) { [weak self] (articles, operation) in precondition(Thread.isMainThread) guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { - callback(Set
()) return } callback(articles) From 6d8fca01ea6fc30071ce2097d56e65f1f90eee0f Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 21 Nov 2019 19:54:35 -0600 Subject: [PATCH 11/50] Filter async requests based on ReadFilter. --- Frameworks/Account/Feed.swift | 2 +- Frameworks/Account/WebFeed.swift | 2 +- .../Timeline/TimelineViewController.swift | 2 +- Shared/SmartFeeds/SmartFeed.swift | 2 +- Shared/SmartFeeds/UnreadFeed.swift | 2 +- Shared/Timeline/FetchRequestOperation.swift | 47 ++++++++++++------- .../MasterTimelineViewController.swift | 6 +-- iOS/SceneCoordinator.swift | 14 ++++-- 8 files changed, 49 insertions(+), 28 deletions(-) diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index 32ba79313..5a05fdeb5 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -11,8 +11,8 @@ import RSCore public enum ReadFilter { case read - case all case none + case unavailable } public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider { diff --git a/Frameworks/Account/WebFeed.swift b/Frameworks/Account/WebFeed.swift index 2bcc1dc5e..49857d9ca 100644 --- a/Frameworks/Account/WebFeed.swift +++ b/Frameworks/Account/WebFeed.swift @@ -14,7 +14,7 @@ import Articles public final class WebFeed: Feed, Renamable, Hashable { public var defaultReadFilter: ReadFilter { - return .all + return .none } public var feedID: FeedIdentifier? { diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 54ff1e9e9..2d0000446 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -1000,7 +1000,7 @@ private extension TimelineViewController { // if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called. precondition(Thread.isMainThread) cancelPendingAsyncFetches() - let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, representedObjects: representedObjects) { [weak self] (articles, operation) in + let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: .none, representedObjects: representedObjects) { [weak self] (articles, operation) in precondition(Thread.isMainThread) guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { return diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index b3e4a59fb..b231be418 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -14,7 +14,7 @@ import Account final class SmartFeed: PseudoFeed { public var defaultReadFilter: ReadFilter { - return .all + return .none } var feedID: FeedIdentifier? { diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index 8238425a2..4b5407a3f 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -20,7 +20,7 @@ import Articles final class UnreadFeed: PseudoFeed { public var defaultReadFilter: ReadFilter { - return .none + return .unavailable } var feedID: FeedIdentifier? { diff --git a/Shared/Timeline/FetchRequestOperation.swift b/Shared/Timeline/FetchRequestOperation.swift index ff9e0664b..411f7f020 100644 --- a/Shared/Timeline/FetchRequestOperation.swift +++ b/Shared/Timeline/FetchRequestOperation.swift @@ -19,14 +19,16 @@ typealias FetchRequestOperationResultBlock = (Set
, FetchRequestOperatio final class FetchRequestOperation { let id: Int + let readFilter: ReadFilter let resultBlock: FetchRequestOperationResultBlock var isCanceled = false var isFinished = false private let representedObjects: [Any] - init(id: Int, representedObjects: [Any], resultBlock: @escaping FetchRequestOperationResultBlock) { + init(id: Int, readFilter: ReadFilter, representedObjects: [Any], resultBlock: @escaping FetchRequestOperationResultBlock) { precondition(Thread.isMainThread) self.id = id + self.readFilter = readFilter self.representedObjects = representedObjects self.resultBlock = resultBlock } @@ -60,25 +62,38 @@ final class FetchRequestOperation { let numberOfFetchers = articleFetchers.count var fetchersReturned = 0 var fetchedArticles = Set
() - for articleFetcher in articleFetchers { - articleFetcher.fetchArticlesAsync { (articles) in - precondition(Thread.isMainThread) - guard !self.isCanceled else { - callCompletionIfNeeded() - return - } - - assert(!self.isFinished) + + func process(articles: Set
) { + precondition(Thread.isMainThread) + guard !self.isCanceled else { + callCompletionIfNeeded() + return + } + + assert(!self.isFinished) - fetchedArticles.formUnion(articles) - fetchersReturned += 1 - if fetchersReturned == numberOfFetchers { - self.isFinished = true - self.resultBlock(fetchedArticles, self) - callCompletionIfNeeded() + fetchedArticles.formUnion(articles) + fetchersReturned += 1 + if fetchersReturned == numberOfFetchers { + self.isFinished = true + self.resultBlock(fetchedArticles, self) + callCompletionIfNeeded() + } + } + + for articleFetcher in articleFetchers { + if readFilter == .read { + articleFetcher.fetchUnreadArticlesAsync { (articles) in + process(articles: articles) + } + } else { + articleFetcher.fetchArticlesAsync { (articles) in + process(articles: articles) } } } + } + } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 762b065ff..2bb1af496 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -90,7 +90,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner // MARK: Actions @IBAction func toggleFilter(_ sender: Any) { switch coordinator.articleReadFilter { - case .all: + case .none: filterButton.image = AppAssets.filterActiveImage coordinator.hideUnreadArticles() case .read: @@ -497,7 +497,7 @@ private extension MasterTimelineViewController { } switch coordinator.articleReadFilter { - case .all: + case .none: filterButton.isHidden = false filterButton.image = AppAssets.filterInactiveImage case .read: @@ -548,7 +548,7 @@ private extension MasterTimelineViewController { self?.configure(cell, article: article) return cell }) - dataSource.defaultRowAnimation = .left + dataSource.defaultRowAnimation = .middle return dataSource } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 322c7ba50..29e2732a2 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -116,7 +116,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return treeControllerDelegate.isUnreadFiltered } - var articleReadFilter: ReadFilter = .all + var articleReadFilter: ReadFilter = .none var rootNode: Node { return treeController.rootNode @@ -473,11 +473,17 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func showAllArticles() { - articleReadFilter = .all + articleReadFilter = .none + fetchAndReplaceArticlesAsync { + self.rebuildBackingStores() + } } func hideUnreadArticles() { articleReadFilter = .read + fetchAndReplaceArticlesAsync { + self.rebuildBackingStores() + } } func expand(_ node: Node) { @@ -1139,7 +1145,7 @@ private extension SceneCoordinator { func setTimelineFeed(_ feed: Feed?, completion: (() -> Void)? = nil) { timelineFeed = feed timelineMiddleIndexPath = nil - articleReadFilter = feed?.defaultReadFilter ?? .all + articleReadFilter = feed?.defaultReadFilter ?? .none fetchAndReplaceArticlesAsync { self.masterTimelineViewController?.reinitializeArticles() @@ -1480,7 +1486,7 @@ private extension SceneCoordinator { precondition(Thread.isMainThread) cancelPendingAsyncFetches() - let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, representedObjects: representedObjects) { [weak self] (articles, operation) in + let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: articleReadFilter, representedObjects: representedObjects) { [weak self] (articles, operation) in precondition(Thread.isMainThread) guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { return From 2d210a3f17b3dcf91e5363cc875e8500bc2ff37c Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 21 Nov 2019 20:12:31 -0600 Subject: [PATCH 12/50] Delete dead code --- iOS/MasterTimeline/MasterTimelineViewController.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 2bb1af496..6296e4ee6 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -142,10 +142,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner // MARK: API - func restoreTimelinePosition() { - - } - func restoreSelectionIfNecessary(adjustScroll: Bool) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { if adjustScroll { From 654f40a98e6eb4a3c7cad230c5525cc41486c432 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 21 Nov 2019 20:31:58 -0600 Subject: [PATCH 13/50] Add always on unread "filter" to Unread. --- .../MasterTimelineViewController.swift | 11 ++++------- iOS/SceneCoordinator.swift | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 6296e4ee6..40fc72822 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -96,8 +96,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner case .read: filterButton.image = AppAssets.filterInactiveImage coordinator.showAllArticles() - default: - break + case .unavailable: + filterButton.image = AppAssets.filterActiveImage + coordinator.refreshTimeline() } } @@ -494,13 +495,9 @@ private extension MasterTimelineViewController { switch coordinator.articleReadFilter { case .none: - filterButton.isHidden = false filterButton.image = AppAssets.filterInactiveImage - case .read: - filterButton.isHidden = false - filterButton.image = AppAssets.filterActiveImage default: - filterButton.isHidden = true + filterButton.image = AppAssets.filterActiveImage } tableView.selectRow(at: nil, animated: false, scrollPosition: .top) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 29e2732a2..a6f098e56 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -379,7 +379,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { @objc func containerChildrenDidChange(_ note: Notification) { if timelineFetcherContainsAnyPseudoFeed() || timelineFetcherContainsAnyFolder() { - fetchAndReplaceArticlesAsync() {} + refreshTimeline() } rebuildBackingStores() } @@ -395,6 +395,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { @objc func accountStateDidChange(_ note: Notification) { if timelineFetcherContainsAnyPseudoFeed() { fetchAndReplaceArticlesAsync { + self.masterTimelineViewController?.reinitializeArticles() self.rebuildBackingStores() } } else { @@ -405,6 +406,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { @objc func userDidAddAccount(_ note: Notification) { if timelineFetcherContainsAnyPseudoFeed() { fetchAndReplaceArticlesAsync { + self.masterTimelineViewController?.reinitializeArticles() self.rebuildBackingStores() } } else { @@ -415,6 +417,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { @objc func userDidDeleteAccount(_ note: Notification) { if timelineFetcherContainsAnyPseudoFeed() { fetchAndReplaceArticlesAsync { + self.masterTimelineViewController?.reinitializeArticles() self.rebuildBackingStores() } } else { @@ -462,6 +465,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return 0 } + func refreshTimeline() { + fetchAndReplaceArticlesAsync() { + self.masterTimelineViewController?.reinitializeArticles() + } + } + func showAllFeeds() { treeControllerDelegate.isUnreadFiltered = false rebuildBackingStores() @@ -474,16 +483,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func showAllArticles() { articleReadFilter = .none - fetchAndReplaceArticlesAsync { - self.rebuildBackingStores() - } + refreshTimeline() } func hideUnreadArticles() { articleReadFilter = .read - fetchAndReplaceArticlesAsync { - self.rebuildBackingStores() - } + refreshTimeline() } func expand(_ node: Node) { From a5b4d570af7c314fdd9ecb32159b42a4aeac625e Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 09:32:27 -0600 Subject: [PATCH 14/50] Change ReadFilter case unavailable to alwaysRead --- Frameworks/Account/Feed.swift | 2 +- Shared/SmartFeeds/UnreadFeed.swift | 2 +- iOS/MasterTimeline/MasterTimelineViewController.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index 5a05fdeb5..13f3d3613 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -12,7 +12,7 @@ import RSCore public enum ReadFilter { case read case none - case unavailable + case alwaysRead } public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider { diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index 4b5407a3f..1ec4163ed 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -20,7 +20,7 @@ import Articles final class UnreadFeed: PseudoFeed { public var defaultReadFilter: ReadFilter { - return .unavailable + return .alwaysRead } var feedID: FeedIdentifier? { diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 40fc72822..a08f1b387 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -96,7 +96,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner case .read: filterButton.image = AppAssets.filterInactiveImage coordinator.showAllArticles() - case .unavailable: + case .alwaysRead: filterButton.image = AppAssets.filterActiveImage coordinator.refreshTimeline() } From eea5d6f327a67960ed2e4d2d98de798374b5ef95 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 09:40:39 -0600 Subject: [PATCH 15/50] Change ReadFilter to ReadFilterType and differentiate the ReadFilter from the query filter. --- Frameworks/Account/Feed.swift | 4 ++-- Frameworks/Account/Folder.swift | 2 +- Frameworks/Account/WebFeed.swift | 2 +- Shared/SmartFeeds/SmartFeed.swift | 2 +- Shared/SmartFeeds/UnreadFeed.swift | 2 +- Shared/Timeline/FetchRequestOperation.swift | 6 +++--- iOS/MasterTimeline/MasterTimelineViewController.swift | 4 ++-- iOS/SceneCoordinator.swift | 11 ++++++----- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index 13f3d3613..4a14a43af 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -9,7 +9,7 @@ import Foundation import RSCore -public enum ReadFilter { +public enum ReadFilterType { case read case none case alwaysRead @@ -17,6 +17,6 @@ public enum ReadFilter { public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider { - var defaultReadFilter: ReadFilter { get } + var defaultReadFilterType: ReadFilterType { get } } diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index fd3ab44ec..61dfe1aca 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -12,7 +12,7 @@ import RSCore public final class Folder: Feed, Renamable, Container, Hashable { - public var defaultReadFilter: ReadFilter { + public var defaultReadFilterType: ReadFilterType { return .read } diff --git a/Frameworks/Account/WebFeed.swift b/Frameworks/Account/WebFeed.swift index 49857d9ca..757d31e6e 100644 --- a/Frameworks/Account/WebFeed.swift +++ b/Frameworks/Account/WebFeed.swift @@ -13,7 +13,7 @@ import Articles public final class WebFeed: Feed, Renamable, Hashable { - public var defaultReadFilter: ReadFilter { + public var defaultReadFilterType: ReadFilterType { return .none } diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index b231be418..b6302ad44 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -13,7 +13,7 @@ import Account final class SmartFeed: PseudoFeed { - public var defaultReadFilter: ReadFilter { + public var defaultReadFilterType: ReadFilterType { return .none } diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index 1ec4163ed..053390c4c 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -19,7 +19,7 @@ import Articles final class UnreadFeed: PseudoFeed { - public var defaultReadFilter: ReadFilter { + public var defaultReadFilterType: ReadFilterType { return .alwaysRead } diff --git a/Shared/Timeline/FetchRequestOperation.swift b/Shared/Timeline/FetchRequestOperation.swift index 411f7f020..08f9cddc0 100644 --- a/Shared/Timeline/FetchRequestOperation.swift +++ b/Shared/Timeline/FetchRequestOperation.swift @@ -19,13 +19,13 @@ typealias FetchRequestOperationResultBlock = (Set
, FetchRequestOperatio final class FetchRequestOperation { let id: Int - let readFilter: ReadFilter + let readFilter: Bool let resultBlock: FetchRequestOperationResultBlock var isCanceled = false var isFinished = false private let representedObjects: [Any] - init(id: Int, readFilter: ReadFilter, representedObjects: [Any], resultBlock: @escaping FetchRequestOperationResultBlock) { + init(id: Int, readFilter: Bool, representedObjects: [Any], resultBlock: @escaping FetchRequestOperationResultBlock) { precondition(Thread.isMainThread) self.id = id self.readFilter = readFilter @@ -82,7 +82,7 @@ final class FetchRequestOperation { } for articleFetcher in articleFetchers { - if readFilter == .read { + if readFilter { articleFetcher.fetchUnreadArticlesAsync { (articles) in process(articles: articles) } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index a08f1b387..69d5a5700 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -89,7 +89,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner // MARK: Actions @IBAction func toggleFilter(_ sender: Any) { - switch coordinator.articleReadFilter { + switch coordinator.articleReadFilterType { case .none: filterButton.image = AppAssets.filterActiveImage coordinator.hideUnreadArticles() @@ -493,7 +493,7 @@ private extension MasterTimelineViewController { navigationItem.titleView = titleView } - switch coordinator.articleReadFilter { + switch coordinator.articleReadFilterType { case .none: filterButton.image = AppAssets.filterInactiveImage default: diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index a6f098e56..3a4a9aece 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -116,7 +116,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return treeControllerDelegate.isUnreadFiltered } - var articleReadFilter: ReadFilter = .none + var articleReadFilterType: ReadFilterType = .none var rootNode: Node { return treeController.rootNode @@ -482,12 +482,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func showAllArticles() { - articleReadFilter = .none + articleReadFilterType = .none refreshTimeline() } func hideUnreadArticles() { - articleReadFilter = .read + articleReadFilterType = .read refreshTimeline() } @@ -1150,7 +1150,7 @@ private extension SceneCoordinator { func setTimelineFeed(_ feed: Feed?, completion: (() -> Void)? = nil) { timelineFeed = feed timelineMiddleIndexPath = nil - articleReadFilter = feed?.defaultReadFilter ?? .none + articleReadFilterType = feed?.defaultReadFilterType ?? .none fetchAndReplaceArticlesAsync { self.masterTimelineViewController?.reinitializeArticles() @@ -1491,7 +1491,8 @@ private extension SceneCoordinator { precondition(Thread.isMainThread) cancelPendingAsyncFetches() - let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: articleReadFilter, representedObjects: representedObjects) { [weak self] (articles, operation) in + let readFilter = articleReadFilterType != .none + let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: readFilter, representedObjects: representedObjects) { [weak self] (articles, operation) in precondition(Thread.isMainThread) guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { return From 387b867d711695be30e56954644c91723e57453b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 09:43:42 -0600 Subject: [PATCH 16/50] Shim Mac interface to FeedRequestOperation until article filtering is enabled on the Mac. --- Mac/MainWindow/Timeline/TimelineViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 2d0000446..4eb1cae11 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -1000,7 +1000,7 @@ private extension TimelineViewController { // if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called. precondition(Thread.isMainThread) cancelPendingAsyncFetches() - let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: .none, representedObjects: representedObjects) { [weak self] (articles, operation) in + let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: false, representedObjects: representedObjects) { [weak self] (articles, operation) in precondition(Thread.isMainThread) guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { return From 43744ec1281f1e29d60fe261655549d0e60f8c7b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 10:21:30 -0600 Subject: [PATCH 17/50] Add folder read fetch query. --- Frameworks/Account/Account.swift | 43 ++++++++++++++++--- Frameworks/Account/ArticleFetcher.swift | 17 ++++++-- .../ArticlesDatabase/ArticlesDatabase.swift | 12 +++++- .../ArticlesDatabase/ArticlesTable.swift | 19 ++++++++ 4 files changed, 79 insertions(+), 12 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 065d90135..3dc1aad09 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -47,7 +47,7 @@ public enum FetchType { case starred case unread case today - case unreadForFolder(Folder) + case folder(Folder, Bool) case webFeed(WebFeed) case articleIDs(Set) case search(String) @@ -589,8 +589,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return fetchUnreadArticles() case .today: return fetchTodayArticles() - case .unreadForFolder(let folder): - return fetchArticles(folder: folder) + case .folder(let folder, let readFilter): + if readFilter { + return fetchUnreadArticles(folder: folder) + } else { + return fetchArticles(folder: folder) + } case .webFeed(let webFeed): return fetchArticles(webFeed: webFeed) case .articleIDs(let articleIDs): @@ -610,8 +614,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, fetchUnreadArticlesAsync(callback) case .today: fetchTodayArticlesAsync(callback) - case .unreadForFolder(let folder): - fetchArticlesAsync(folder: folder, callback) + case .folder(let folder, let readFilter): + if readFilter { + return fetchUnreadArticlesAsync(folder: folder, callback) + } else { + return fetchArticlesAsync(folder: folder, callback) + } case .webFeed(let webFeed): fetchArticlesAsync(webFeed: webFeed, callback) case .articleIDs(let articleIDs): @@ -887,10 +895,18 @@ private extension Account { } func fetchArticles(folder: Folder) -> Set
{ - return fetchUnreadArticles(forContainer: folder) + return fetchArticles(forContainer: folder) } func fetchArticlesAsync(folder: Folder, _ callback: @escaping ArticleSetBlock) { + fetchArticlesAsync(forContainer: folder, callback) + } + + func fetchUnreadArticles(folder: Folder) -> Set
{ + return fetchUnreadArticles(forContainer: folder) + } + + func fetchUnreadArticlesAsync(folder: Folder, _ callback: @escaping ArticleSetBlock) { fetchUnreadArticlesAsync(forContainer: folder, callback) } @@ -945,6 +961,21 @@ private extension Account { } + func fetchArticles(forContainer container: Container) -> Set
{ + let feeds = container.flattenedWebFeeds() + let articles = database.fetchArticles(feeds.webFeedIDs()) + validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + return articles + } + + func fetchArticlesAsync(forContainer container: Container, _ callback: @escaping ArticleSetBlock) { + let webFeeds = container.flattenedWebFeeds() + database.fetchArticlesAsync(webFeeds.webFeedIDs()) { [weak self] (articles) in + self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles) + callback(articles) + } + } + func fetchUnreadArticles(forContainer container: Container) -> Set
{ let feeds = container.flattenedWebFeeds() let articles = database.fetchUnreadArticles(feeds.webFeedIDs()) diff --git a/Frameworks/Account/ArticleFetcher.swift b/Frameworks/Account/ArticleFetcher.swift index 4bf3f647e..bf4b08dac 100644 --- a/Frameworks/Account/ArticleFetcher.swift +++ b/Frameworks/Account/ArticleFetcher.swift @@ -49,11 +49,20 @@ extension WebFeed: ArticleFetcher { extension Folder: ArticleFetcher { public func fetchArticles() -> Set
{ - return fetchUnreadArticles() + guard let account = account else { + assertionFailure("Expected folder.account, but got nil.") + return Set
() + } + return account.fetchArticles(.folder(self, false)) } public func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) { - fetchUnreadArticlesAsync(callback) + guard let account = account else { + assertionFailure("Expected folder.account, but got nil.") + callback(Set
()) + return + } + account.fetchArticlesAsync(.folder(self, false), callback) } public func fetchUnreadArticles() -> Set
{ @@ -61,7 +70,7 @@ extension Folder: ArticleFetcher { assertionFailure("Expected folder.account, but got nil.") return Set
() } - return account.fetchArticles(.unreadForFolder(self)) + return account.fetchArticles(.folder(self, true)) } public func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) { @@ -70,6 +79,6 @@ extension Folder: ArticleFetcher { callback(Set
()) return } - account.fetchArticlesAsync(.unreadForFolder(self), callback) + account.fetchArticlesAsync(.folder(self, true), callback) } } diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index cdc116511..d80eae09f 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -48,12 +48,16 @@ public final class ArticlesDatabase { return articlesTable.fetchArticles(webFeedID) } + public func fetchArticles(_ webFeedIDs: Set) -> Set
{ + return articlesTable.fetchArticles(webFeedIDs) + } + public func fetchArticles(articleIDs: Set) -> Set
{ return articlesTable.fetchArticles(articleIDs: articleIDs) } - public func fetchUnreadArticles(_ webFeedID: Set) -> Set
{ - return articlesTable.fetchUnreadArticles(webFeedID) + public func fetchUnreadArticles(_ webFeedIDs: Set) -> Set
{ + return articlesTable.fetchUnreadArticles(webFeedIDs) } public func fetchTodayArticles(_ webFeedIDs: Set) -> Set
{ @@ -78,6 +82,10 @@ public final class ArticlesDatabase { articlesTable.fetchArticlesAsync(webFeedID, callback) } + public func fetchArticlesAsync(_ webFeedIDs: Set, _ callback: @escaping ArticleSetBlock) { + articlesTable.fetchArticlesAsync(webFeedIDs, callback) + } + public func fetchArticlesAsync(articleIDs: Set, _ callback: @escaping ArticleSetBlock) { articlesTable.fetchArticlesAsync(articleIDs: articleIDs, callback) } diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 02629bff6..153a74368 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -60,6 +60,25 @@ final class ArticlesTable: DatabaseTable { return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject], withLimits: withLimits) } + func fetchArticles(_ webFeedIDs: Set) -> Set
{ + return fetchArticles{ self.fetchArticles(webFeedIDs, $0) } + } + + func fetchArticlesAsync(_ webFeedIDs: Set, _ callback: @escaping ArticleSetBlock) { + fetchArticlesAsync({ self.fetchArticles(webFeedIDs, $0) }, callback) + } + + private func fetchArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ + // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 + if webFeedIDs.isEmpty { + return Set
() + } + let parameters = webFeedIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! + let whereClause = "feedID in \(placeholders)" + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true) + } + // MARK: - Fetching Articles by articleID func fetchArticles(articleIDs: Set) -> Set
{ From 7667dbf60ea5d35c0d1f00bceb6c1ea2aee950ef Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 10:55:54 -0600 Subject: [PATCH 18/50] Add hide read feeds menu option --- Mac/Base.lproj/Main.storyboard | 10 ++++++++-- Mac/MainWindow/MainWindowController.swift | 8 ++++++++ Mac/MainWindow/Sidebar/SidebarViewController.swift | 14 +++++++++++++- Shared/Tree/WebFeedTreeControllerDelegate.swift | 12 ++++++------ iOS/SceneCoordinator.swift | 6 +++--- 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard index a077b7153..27675b309 100644 --- a/Mac/Base.lproj/Main.storyboard +++ b/Mac/Base.lproj/Main.storyboard @@ -1,7 +1,7 @@ - + - + @@ -362,6 +362,12 @@ + + + + + + diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index ff050a501..74b988dd5 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -237,6 +237,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { return currentSearchField != nil } + if item.action == #selector(toggleReadFilter(_:)) { + (item as! NSMenuItem).state = sidebarViewController?.isReadFiltered ?? false ? .on : .off + } + if item.action == #selector(toggleSidebar(_:)) { guard let splitViewItem = sidebarSplitViewItem else { return false @@ -438,6 +442,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } window?.makeFirstResponder(searchField) } + + @IBAction func toggleReadFilter(_ sender: Any?) { + sidebarViewController?.toggleReadFilter() + } } // MARK: - SidebarDelegate diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index ba74c57be..54851c8ae 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -30,6 +30,9 @@ protocol SidebarDelegate: class { lazy var dataSource: SidebarOutlineDataSource = { return SidebarOutlineDataSource(treeController: treeController) }() + var isReadFiltered: Bool { + return treeControllerDelegate.isReadFiltered + } var undoableCommands = [UndoableCommand]() private var animatingChanges = false @@ -333,7 +336,16 @@ protocol SidebarDelegate: class { } revealAndSelectRepresentedObject(feedNode.representedObject) } - + + func toggleReadFilter() { + if treeControllerDelegate.isReadFiltered { + treeControllerDelegate.isReadFiltered = false + } else { + treeControllerDelegate.isReadFiltered = true + } + rebuildTreeAndReloadDataIfNeeded() + } + } // MARK: - NSUserInterfaceValidations diff --git a/Shared/Tree/WebFeedTreeControllerDelegate.swift b/Shared/Tree/WebFeedTreeControllerDelegate.swift index fd4d4f754..2eb149037 100644 --- a/Shared/Tree/WebFeedTreeControllerDelegate.swift +++ b/Shared/Tree/WebFeedTreeControllerDelegate.swift @@ -13,7 +13,7 @@ import Account final class WebFeedTreeControllerDelegate: TreeControllerDelegate { - var isUnreadFiltered = false + var isReadFiltered = false func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { if node.isRoot { @@ -36,7 +36,7 @@ private extension WebFeedTreeControllerDelegate { var topLevelNodes = [Node]() // Check to see if we should show the SmartFeeds top level by checking the unreadFeed - if !(isUnreadFiltered && SmartFeedsController.shared.unreadFeed.unreadCount == 0) { + if !(isReadFiltered && SmartFeedsController.shared.unreadFeed.unreadCount == 0) { let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared) smartFeedsNode.canHaveChildNodes = true smartFeedsNode.isGroupItem = true @@ -51,7 +51,7 @@ private extension WebFeedTreeControllerDelegate { func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] { return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in - if isUnreadFiltered && feed.unreadCount == 0 { + if isReadFiltered && feed.unreadCount == 0 { return nil } return parentNode.existingOrNewChildNode(with: feed as AnyObject) @@ -64,14 +64,14 @@ private extension WebFeedTreeControllerDelegate { var children = [AnyObject]() for webFeed in container.topLevelWebFeeds { - if !(isUnreadFiltered && webFeed.unreadCount == 0) { + if !(isReadFiltered && webFeed.unreadCount == 0) { children.append(webFeed) } } if let folders = container.folders { for folder in folders { - if !(isUnreadFiltered && folder.unreadCount == 0) { + if !(isReadFiltered && folder.unreadCount == 0) { children.append(folder) } } @@ -131,7 +131,7 @@ private extension WebFeedTreeControllerDelegate { func sortedAccountNodes(_ parent: Node) -> [Node] { let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in - if isUnreadFiltered && account.unreadCount == 0 { + if isReadFiltered && account.unreadCount == 0 { return nil } let accountNode = parent.existingOrNewChildNode(with: account) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 3a4a9aece..a50bc53ba 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -113,7 +113,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } var isUnreadFeedsFiltered: Bool { - return treeControllerDelegate.isUnreadFiltered + return treeControllerDelegate.isReadFiltered } var articleReadFilterType: ReadFilterType = .none @@ -472,12 +472,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func showAllFeeds() { - treeControllerDelegate.isUnreadFiltered = false + treeControllerDelegate.isReadFiltered = false rebuildBackingStores() } func hideUnreadFeeds() { - treeControllerDelegate.isUnreadFiltered = true + treeControllerDelegate.isReadFiltered = true rebuildBackingStores() } From 5ac14fb91081d3a0a9507828108ee290068c9cd8 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 11:47:03 -0600 Subject: [PATCH 19/50] Add read filter toggle for articles. Issue #130. --- Mac/Base.lproj/Main.storyboard | 8 ++++- Mac/MainWindow/MainWindowController.swift | 18 ++++++++-- .../TimelineContainerViewController.swift | 9 +++++ .../Timeline/TimelineViewController.swift | 36 +++++++++++++++++-- 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard index 27675b309..19fbdd9f3 100644 --- a/Mac/Base.lproj/Main.storyboard +++ b/Mac/Base.lproj/Main.storyboard @@ -336,6 +336,12 @@ + + + + + + @@ -365,7 +371,7 @@ - + diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 74b988dd5..39b56f8d3 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -237,10 +237,19 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { return currentSearchField != nil } - if item.action == #selector(toggleReadFilter(_:)) { + if item.action == #selector(toggleReadFeedsFilter(_:)) { (item as! NSMenuItem).state = sidebarViewController?.isReadFiltered ?? false ? .on : .off } + if item.action == #selector(toggleReadArticlesFilter(_:)) { + if let timelineContainer = timelineContainerViewController { + (item as! NSMenuItem).isEnabled = true + (item as! NSMenuItem).state = timelineContainer.isReadFiltered ? .on : .off + } else { + (item as! NSMenuItem).isEnabled = false + } + } + if item.action == #selector(toggleSidebar(_:)) { guard let splitViewItem = sidebarSplitViewItem else { return false @@ -443,9 +452,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { window?.makeFirstResponder(searchField) } - @IBAction func toggleReadFilter(_ sender: Any?) { + @IBAction func toggleReadFeedsFilter(_ sender: Any?) { sidebarViewController?.toggleReadFilter() } + + @IBAction func toggleReadArticlesFilter(_ sender: Any?) { + timelineContainerViewController?.toggleReadFilter() + } + } // MARK: - SidebarDelegate diff --git a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift index b4f3b3a27..cdeaec45d 100644 --- a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift @@ -30,6 +30,10 @@ final class TimelineContainerViewController: NSViewController { weak var delegate: TimelineContainerViewControllerDelegate? + var isReadFiltered: Bool { + return regularTimelineViewController.isReadFiltered + } + lazy var regularTimelineViewController = { return TimelineViewController(delegate: self) }() @@ -79,6 +83,11 @@ final class TimelineContainerViewController: NSViewController { } return true } + + func toggleReadFilter() { + regularTimelineViewController.toggleReadFilter() + } + } extension TimelineContainerViewController: TimelineDelegate { diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 4eb1cae11..60f558dec 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -20,6 +20,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr @IBOutlet var tableView: TimelineTableView! + private var articleReadFilterType: ReadFilterType? + var isReadFiltered: Bool { + return articleReadFilterType ?? .read != .none + } + var representedObjects: [AnyObject]? { didSet { if !representedObjectArraysAreEqual(oldValue, representedObjects) { @@ -36,6 +41,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr showFeedNames = false } + determineReadFilterType() selectionDidChange(nil) if showsSearchResults { fetchAndReplaceArticlesAsync() @@ -213,6 +219,19 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr return representedObjects.first! === object } + func toggleReadFilter() { + guard let filterType = articleReadFilterType else { return } + switch filterType { + case .alwaysRead: + break + case .read: + articleReadFilterType = ReadFilterType.none + case .none: + articleReadFilterType = ReadFilterType.read + } + fetchAndReplaceArticlesAsync() + } + // MARK: - Actions @objc func openArticleInBrowser(_ sender: Any?) { @@ -944,6 +963,14 @@ private extension TimelineViewController { } // MARK: - Fetching Articles + + func determineReadFilterType() { + if representedObjects?.count ?? 0 == 1, let feed = representedObjects?.first as? Feed { + articleReadFilterType = feed.defaultReadFilterType + } else { + articleReadFilterType = .read + } + } func fetchAndReplaceArticlesSync() { // To be called when the user has made a change of selection in the sidebar. @@ -990,7 +1017,11 @@ private extension TimelineViewController { var fetchedArticles = Set
() for articleFetcher in articleFetchers { - fetchedArticles.formUnion(articleFetcher.fetchArticles()) + if articleReadFilterType != ReadFilterType.none { + fetchedArticles.formUnion(articleFetcher.fetchUnreadArticles()) + } else { + fetchedArticles.formUnion(articleFetcher.fetchArticles()) + } } return fetchedArticles } @@ -1000,7 +1031,8 @@ private extension TimelineViewController { // if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called. precondition(Thread.isMainThread) cancelPendingAsyncFetches() - let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: false, representedObjects: representedObjects) { [weak self] (articles, operation) in + let readFilter = articleReadFilterType != ReadFilterType.none + let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: readFilter, representedObjects: representedObjects) { [weak self] (articles, operation) in precondition(Thread.isMainThread) guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { return From 21648a498f85c5c8d53d8599a0e282a060f4fc9f Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 11:57:06 -0600 Subject: [PATCH 20/50] Make selection restore when toggling read feeds. --- Mac/MainWindow/Sidebar/SidebarViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index 54851c8ae..c03adc08c 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -343,7 +343,7 @@ protocol SidebarDelegate: class { } else { treeControllerDelegate.isReadFiltered = true } - rebuildTreeAndReloadDataIfNeeded() + rebuildTreeAndRestoreSelection() } } From fea48e7ab9ab72e0cc65ccac5c77f364c5192162 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 15:11:15 -0600 Subject: [PATCH 21/50] Increase inspector header padding. Issue #1324 --- iOS/Account/FeedbinAccountViewController.swift | 2 +- iOS/Account/LocalAccountViewController.swift | 2 +- iOS/Inspector/AccountInspectorViewController.swift | 2 +- iOS/Inspector/WebFeedInspectorViewController.swift | 2 +- iOS/UIKit Extensions/ImageHeaderView.swift | 2 ++ 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/iOS/Account/FeedbinAccountViewController.swift b/iOS/Account/FeedbinAccountViewController.swift index 71f4fce38..45fcb8237 100644 --- a/iOS/Account/FeedbinAccountViewController.swift +++ b/iOS/Account/FeedbinAccountViewController.swift @@ -44,7 +44,7 @@ class FeedbinAccountViewController: UITableViewController { } override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? 64.0 : super.tableView(tableView, heightForHeaderInSection: section) + return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) } override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/iOS/Account/LocalAccountViewController.swift b/iOS/Account/LocalAccountViewController.swift index 6aed1eddf..be0c1596d 100644 --- a/iOS/Account/LocalAccountViewController.swift +++ b/iOS/Account/LocalAccountViewController.swift @@ -36,7 +36,7 @@ class LocalAccountViewController: UITableViewController { } override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? 64.0 : super.tableView(tableView, heightForHeaderInSection: section) + return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) } override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/iOS/Inspector/AccountInspectorViewController.swift b/iOS/Inspector/AccountInspectorViewController.swift index cc0095643..5f56a3605 100644 --- a/iOS/Inspector/AccountInspectorViewController.swift +++ b/iOS/Inspector/AccountInspectorViewController.swift @@ -128,7 +128,7 @@ extension AccountInspectorViewController { } override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? 64.0 : super.tableView(tableView, heightForHeaderInSection: section) + return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) } override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/iOS/Inspector/WebFeedInspectorViewController.swift b/iOS/Inspector/WebFeedInspectorViewController.swift index ff790e84f..00066f4de 100644 --- a/iOS/Inspector/WebFeedInspectorViewController.swift +++ b/iOS/Inspector/WebFeedInspectorViewController.swift @@ -78,7 +78,7 @@ class WebFeedInspectorViewController: UITableViewController { extension WebFeedInspectorViewController { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? 64.0 : super.tableView(tableView, heightForHeaderInSection: section) + return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) } override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/iOS/UIKit Extensions/ImageHeaderView.swift b/iOS/UIKit Extensions/ImageHeaderView.swift index 3c26eda89..2e8a25420 100644 --- a/iOS/UIKit Extensions/ImageHeaderView.swift +++ b/iOS/UIKit Extensions/ImageHeaderView.swift @@ -10,6 +10,8 @@ import UIKit class ImageHeaderView: UITableViewHeaderFooterView { + static let rowHeight = CGFloat(integerLiteral: 88) + var imageView = UIImageView() override init(reuseIdentifier: String?) { From e22c3831369a11feee150c6936e5a5d2c947b5e3 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 15:15:48 -0600 Subject: [PATCH 22/50] Update launch storyboards to match the new Feeds layout. --- iOS/Base.lproj/LaunchScreenPad.storyboard | 7 ++++--- iOS/Base.lproj/LaunchScreenPhone.storyboard | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/iOS/Base.lproj/LaunchScreenPad.storyboard b/iOS/Base.lproj/LaunchScreenPad.storyboard index 070aca5aa..edb676e06 100644 --- a/iOS/Base.lproj/LaunchScreenPad.storyboard +++ b/iOS/Base.lproj/LaunchScreenPad.storyboard @@ -1,8 +1,8 @@ - + - + @@ -70,7 +70,7 @@ - + @@ -114,5 +114,6 @@ + diff --git a/iOS/Base.lproj/LaunchScreenPhone.storyboard b/iOS/Base.lproj/LaunchScreenPhone.storyboard index a61b6f5d9..6ac462068 100644 --- a/iOS/Base.lproj/LaunchScreenPhone.storyboard +++ b/iOS/Base.lproj/LaunchScreenPhone.storyboard @@ -1,8 +1,8 @@ - + - + @@ -70,7 +70,7 @@ - + @@ -114,5 +114,6 @@ + From dd7431d5cb83eeaa8498fbecf6c74775eaf64fac Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 15:23:21 -0600 Subject: [PATCH 23/50] Remove obsolete code. --- iOS/MasterFeed/MasterFeedDataSource.swift | 8 -------- iOS/MasterFeed/MasterFeedViewController.swift | 4 +++- iOS/MasterTimeline/MasterTimelineDataSource.swift | 7 ------- iOS/MasterTimeline/MasterTimelineViewController.swift | 2 +- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedDataSource.swift b/iOS/MasterFeed/MasterFeedDataSource.swift index 9d6511354..feec33e2e 100644 --- a/iOS/MasterFeed/MasterFeedDataSource.swift +++ b/iOS/MasterFeed/MasterFeedDataSource.swift @@ -13,14 +13,6 @@ import Account class MasterFeedDataSource: UITableViewDiffableDataSource { - private var coordinator: SceneCoordinator! - - init(coordinator: SceneCoordinator, tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource.CellProvider) { - super.init(tableView: tableView, cellProvider: cellProvider) - self.coordinator = coordinator - self.defaultRowAnimation = .middle - } - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { guard let node = itemIdentifier(for: indexPath), !(node.representedObject is PseudoFeed) else { return false diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 89315194b..649825a4f 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -640,11 +640,13 @@ private extension MasterFeedViewController { } func makeDataSource() -> UITableViewDiffableDataSource { - return MasterFeedDataSource(coordinator: coordinator, tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in + let dataSource = MasterFeedDataSource(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 }) + dataSource.defaultRowAnimation = .middle + return dataSource } func resetEstimatedRowHeight() { diff --git a/iOS/MasterTimeline/MasterTimelineDataSource.swift b/iOS/MasterTimeline/MasterTimelineDataSource.swift index ad160e8af..3647a93ae 100644 --- a/iOS/MasterTimeline/MasterTimelineDataSource.swift +++ b/iOS/MasterTimeline/MasterTimelineDataSource.swift @@ -9,13 +9,6 @@ import UIKit class MasterTimelineDataSource: UITableViewDiffableDataSource where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable { - - private var coordinator: SceneCoordinator! - - init(coordinator: SceneCoordinator, tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource.CellProvider) { - super.init(tableView: tableView, cellProvider: cellProvider) - self.coordinator = coordinator - } override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 69d5a5700..8d65ccc34 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -536,7 +536,7 @@ private extension MasterTimelineViewController { func makeDataSource() -> UITableViewDiffableDataSource { let dataSource: UITableViewDiffableDataSource = - MasterTimelineDataSource(coordinator: coordinator, tableView: tableView, cellProvider: { [weak self] tableView, indexPath, article in + MasterTimelineDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, article in let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell self?.configure(cell, article: article) return cell From c8cfcae8e3d3159c044fc3afc1c9226c8bbd721f Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 22 Nov 2019 19:59:25 -0600 Subject: [PATCH 24/50] Fix drag and drop targeting bugs. --- iOS/MasterFeed/MasterFeedViewController+Drop.swift | 8 ++++++-- iOS/MasterFeed/MasterFeedViewController.swift | 11 ++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController+Drop.swift b/iOS/MasterFeed/MasterFeedViewController+Drop.swift index afaf62a16..92eca2791 100644 --- a/iOS/MasterFeed/MasterFeedViewController+Drop.swift +++ b/iOS/MasterFeed/MasterFeedViewController+Drop.swift @@ -26,8 +26,12 @@ extension MasterFeedViewController: UITableViewDropDelegate { return UITableViewDropProposal(operation: .forbidden) } - if destNode.representedObject is Folder && session.location(in: destCell).y >= 0 { - return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) + if destNode.representedObject is Folder { + if session.location(in: destCell).y >= 0 { + return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) + } else { + return UITableViewDropProposal(operation: .move, intent: .unspecified) + } } else { return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) } diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 649825a4f..1dbc79ffb 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -327,8 +327,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) { - let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0 - return IndexPath(row: destIndexPath.row + movementAdjustment, section: destIndexPath.section) + return proposedDestinationIndexPath } // If we are dragging around in the same container, just return the original source @@ -353,12 +352,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } else { sortedNodes.remove(at: index) - + if index >= sortedNodes.count { let lastSortedIndexPath = dataSource.indexPath(for: sortedNodes[sortedNodes.count - 1])! - return IndexPath(row: lastSortedIndexPath.row + 1, section: lastSortedIndexPath.section) + let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0 + return IndexPath(row: lastSortedIndexPath.row + movementAdjustment, section: lastSortedIndexPath.section) } else { - return dataSource.indexPath(for: sortedNodes[index])! + let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0 + return dataSource.indexPath(for: sortedNodes[index - movementAdjustment])! } } From e26f20449c714522a05b527335ed38b459ad99a8 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 23 Nov 2019 11:07:40 -0600 Subject: [PATCH 25/50] Adjusted so that tap zones only appear for fullscreen. Issue #1331 --- iOS/Article/ArticleViewController.swift | 6 ++++++ iOS/Base.lproj/Main.storyboard | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 5f67abd26..c9f3827f4 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -36,6 +36,8 @@ class ArticleViewController: UIViewController { @IBOutlet private weak var webViewContainer: UIView! @IBOutlet private weak var showNavigationView: UIView! @IBOutlet private weak var showToolbarView: UIView! + @IBOutlet private weak var showNavigationViewConstraint: NSLayoutConstraint! + @IBOutlet private weak var showToolbarViewConstraint: NSLayoutConstraint! private var articleExtractorButton: ArticleExtractorButton = { let button = ArticleExtractorButton(type: .system) @@ -478,6 +480,8 @@ private extension ArticleViewController { func showBars() { if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed { coordinator.showStatusBar() + showNavigationViewConstraint.constant = 0 + showToolbarViewConstraint.constant = 0 navigationController?.setNavigationBarHidden(false, animated: true) navigationController?.setToolbarHidden(false, animated: true) } @@ -486,6 +490,8 @@ private extension ArticleViewController { func hideBars() { if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed { coordinator.hideStatusBar() + showNavigationViewConstraint.constant = 44.0 + showToolbarViewConstraint.constant = 44.0 navigationController?.setNavigationBarHidden(true, animated: true) navigationController?.setToolbarHidden(true, animated: true) } diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index 61184ea2b..276aee563 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -21,14 +21,14 @@ - + - + @@ -37,14 +37,14 @@ - + - + @@ -121,7 +121,9 @@ + + From 01f86d8c1b8c2668e27b6c2a9b02da11e01d04e2 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 23 Nov 2019 11:20:36 -0600 Subject: [PATCH 26/50] Vertically center small fonts and favicons when we hit the row minimum. Issue #1329 --- iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift | 8 +++++++- .../Cell/MasterFeedTableViewSectionHeaderLayout.swift | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift index 3ba4d5239..4ff894978 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift @@ -98,7 +98,7 @@ struct MasterFeedTableViewCellLayout { } } - let rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height) + var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height) // Determine cell height let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MasterFeedTableViewCellLayout.verticalPadding) @@ -117,6 +117,12 @@ struct MasterFeedTableViewCellLayout { rDisclosure = MasterFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds) } + // Small fonts and the Favicon need centered if we hit the minimum row height + if cellHeight == MasterFeedTableViewCellLayout.minRowHeight { + rLabel = MasterFeedTableViewCellLayout.centerVertically(rLabel, newBounds) + rFavicon = MasterFeedTableViewCellLayout.centerVertically(rFavicon, newBounds) + } + // Separator Insets let separatorInset = MasterFeedTableViewCellLayout.disclosureButtonSize.width separatorRect = CGRect(x: separatorInset, y: cellHeight - 0.5, width: cellWidth - separatorInset, height: 0.5) diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeaderLayout.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeaderLayout.swift index 5b487f837..1421e16f0 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeaderLayout.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeaderLayout.swift @@ -57,7 +57,7 @@ struct MasterFeedTableViewSectionHeaderLayout { labelWidth = cellWidth - (rLabelx + MasterFeedTableViewSectionHeaderLayout.labelMarginRight + maxUnreadCountSize.width + MasterFeedTableViewSectionHeaderLayout.unreadCountMarginRight) let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth))) - let rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height) + var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height) // Determine cell height let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MasterFeedTableViewSectionHeaderLayout.verticalPadding) @@ -74,6 +74,11 @@ struct MasterFeedTableViewSectionHeaderLayout { } rDisclosure = MasterFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds) + // Small fonts need centered if we hit the minimum row height + if cellHeight == MasterFeedTableViewSectionHeaderLayout.minRowHeight { + rLabel = MasterFeedTableViewCellLayout.centerVertically(rLabel, newBounds) + } + // Assign the properties self.height = cellHeight self.unreadCountRect = rUnread From 6d18cfec7c9151bdc67fbb7389dbd4906fe6f2c2 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 23 Nov 2019 12:30:18 -0600 Subject: [PATCH 27/50] Refactor SmartFeedController to find SmartFeeds by FeedIdentifier instead of the string identifier. --- Shared/SmartFeeds/SmartFeedsController.swift | 19 ++++++++++++------- iOS/SceneCoordinator.swift | 16 ++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Shared/SmartFeeds/SmartFeedsController.swift b/Shared/SmartFeeds/SmartFeedsController.swift index 683495606..b9163a0c6 100644 --- a/Shared/SmartFeeds/SmartFeedsController.swift +++ b/Shared/SmartFeeds/SmartFeedsController.swift @@ -24,14 +24,19 @@ final class SmartFeedsController: DisplayNameProvider { self.smartFeeds = [todayFeed, unreadFeed, starredFeed] } - func find(by identifier: String) -> PseudoFeed? { + func find(by identifier: FeedIdentifier) -> PseudoFeed? { switch identifier { - case String(describing: TodayFeedDelegate.self): - return todayFeed - case String(describing: UnreadFeed.self): - return unreadFeed - case String(describing: StarredFeedDelegate.self): - return starredFeed + case .smartFeed(let stringIdentifer): + switch stringIdentifer { + case String(describing: TodayFeedDelegate.self): + return todayFeed + case String(describing: UnreadFeed.self): + return unreadFeed + case String(describing: StarredFeedDelegate.self): + return starredFeed + default: + return nil + } default: return nil } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index a50bc53ba..87d49eb90 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1684,14 +1684,14 @@ private extension SceneCoordinator { func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) { guard let userInfo = userInfo, let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any], - let articleFetcherType = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { + let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { return } - switch articleFetcherType { + switch feedIdentifier { - case .smartFeed(let identifier): - guard let smartFeed = SmartFeedsController.shared.find(by: identifier) else { return } + case .smartFeed: + guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return } if let indexPath = indexPathFor(smartFeed) { selectFeed(indexPath, animated: false) } @@ -1744,14 +1744,14 @@ private extension SceneCoordinator { func restoreFeed(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool { guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any], - let articleFetcherType = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { + let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { return false } - switch articleFetcherType { + switch feedIdentifier { - case .smartFeed(let identifier): - guard let smartFeed = SmartFeedsController.shared.find(by: identifier) else { return false } + case .smartFeed: + guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return false } if smartFeed.fetchArticles().contains(accountID: accountID, articleID: articleID) { if let indexPath = indexPathFor(smartFeed) { selectFeed(indexPath, animated: false) { From d7c3f1ee1958f6c9e52ca3d89d004511c030c469 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 23 Nov 2019 16:38:07 -0600 Subject: [PATCH 28/50] Don't attempt to move a feed if the drop target is the same as the source target. --- iOS/MasterFeed/MasterFeedViewController+Drop.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iOS/MasterFeed/MasterFeedViewController+Drop.swift b/iOS/MasterFeed/MasterFeedViewController+Drop.swift index 92eca2791..bd759e710 100644 --- a/iOS/MasterFeed/MasterFeedViewController+Drop.swift +++ b/iOS/MasterFeed/MasterFeedViewController+Drop.swift @@ -130,6 +130,8 @@ extension MasterFeedViewController: UITableViewDropDelegate { } func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { + guard sourceContainer !== destinationContainer else { return } + BatchUpdate.shared.start() sourceContainer.account?.moveWebFeed(feed, from: sourceContainer, to: destinationContainer) { result in BatchUpdate.shared.end() From d8b324e3da27c98a1e678160565ee3861f767bfa Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 23 Nov 2019 18:00:51 -0600 Subject: [PATCH 29/50] Change nav bar so that if it is hidden and animating itself showing, you don't see the navbar items moving into place. --- .../MasterTimelineViewController.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 8d65ccc34..27a9020e1 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -82,9 +82,22 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } + override func viewWillAppear(_ animated: Bool) { + // If the nav bar is hidden, fade it in to avoid it showing stuff as it is getting laid out + if navigationController?.navigationBar.isHidden ?? false { + navigationController?.navigationBar.alpha = 0 + } + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(true) coordinator.isTimelineViewControllerPending = false + + if navigationController?.navigationBar.alpha == 0 { + UIView.animate(withDuration: 0.5) { + self.navigationController?.navigationBar.alpha = 1 + } + } } // MARK: Actions From 9f60984ba261312a012c314857ca97864cafa191 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 23 Nov 2019 18:22:58 -0600 Subject: [PATCH 30/50] Increase the number of characters available for the summary in the timeline. Issue #1333 --- Shared/Data/ArticleStringFormatter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/Data/ArticleStringFormatter.swift b/Shared/Data/ArticleStringFormatter.swift index 7fa6f808a..6bd00baf3 100644 --- a/Shared/Data/ArticleStringFormatter.swift +++ b/Shared/Data/ArticleStringFormatter.swift @@ -89,7 +89,7 @@ struct ArticleStringFormatter { return cachedBody } var s = body.rsparser_stringByDecodingHTMLEntities() - s = s.rs_string(byStrippingHTML: 150) + s = s.rs_string(byStrippingHTML: 250) s = s.rs_stringByTrimmingWhitespace() s = s.rs_stringWithCollapsedWhitespace() if s == "Comments" { // Hacker News. From 4296c243ff561530d80beb9f03d5dc4c65891848 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 23 Nov 2019 22:15:29 -0600 Subject: [PATCH 31/50] Implement custom previews for context menus to crop cell separators. Issue #1221 --- NetNewsWire.xcodeproj/project.pbxproj | 10 ++++-- iOS/MasterFeed/MasterFeedViewController.swift | 35 +++++++++++++++---- .../MasterTimelineViewController.swift | 11 +++++- .../CroppingTargetedPreview.swift | 20 +++++++++++ submodules/RSTree | 2 +- 5 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 iOS/UIKit Extensions/CroppingTargetedPreview.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index e19fa382a..f2f16e50a 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ 51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */; }; 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */; }; 51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */; }; + 51627A93238A3836007B3B4B /* CroppingTargetedPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A92238A3836007B3B4B /* CroppingTargetedPreview.swift */; }; 516A093723609A3600EAE89B /* SettingsAccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */; }; 516A09392360A2AE00EAE89B /* SettingsAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */; }; 516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */; }; @@ -1271,6 +1272,7 @@ 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drag.swift"; sourceTree = ""; }; 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drop.swift"; sourceTree = ""; }; 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = ""; }; + 51627A92238A3836007B3B4B /* CroppingTargetedPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CroppingTargetedPreview.swift; sourceTree = ""; }; 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsAccountTableViewCell.xib; sourceTree = ""; }; 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountTableViewCell.swift; sourceTree = ""; }; 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsTableViewCell.xib; sourceTree = ""; }; @@ -1854,14 +1856,17 @@ children = ( 51F85BFA2275D85000C787DC /* Array-Extensions.swift */, 51F85BF42273625800C787DC /* Bundle-Extensions.swift */, + 51627A92238A3836007B3B4B /* CroppingTargetedPreview.swift */, 512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */, 512AF9DC236F05230066F8BE /* InteractiveLabel.swift */, + 51934CC1230F5963006127BE /* InteractiveNavigationController.swift */, + 51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */, 51EAED95231363EF00A9EEE3 /* NonIntrinsicButton.swift */, 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */, 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */, + 51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */, 512363372369155100951F16 /* RoundedProgressView.swift */, 51C45250226506F400C03939 /* String-Extensions.swift */, - 51934CC1230F5963006127BE /* InteractiveNavigationController.swift */, 5108F6D723763094001ABC45 /* TickMarkSlider.swift */, 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */, 51F85BF622749FA100C787DC /* UIFont-Extensions.swift */, @@ -1869,8 +1874,6 @@ 51FFF0C3235EE8E5002762AA /* VibrantButton.swift */, 5186A634235EF3A800C97195 /* VibrantLabel.swift */, 5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */, - 51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */, - 51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */, ); path = "UIKit Extensions"; sourceTree = ""; @@ -3962,6 +3965,7 @@ 51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */, 514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */, 5141E7562374A2890013FF27 /* ArticleIconSchemeHandler.swift in Sources */, + 51627A93238A3836007B3B4B /* CroppingTargetedPreview.swift in Sources */, 512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */, 51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */, 5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */, diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 1dbc79ffb..113c746e3 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -294,11 +294,22 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { return nil } if node.representedObject is WebFeed { - return makeFeedContextMenu(indexPath: indexPath, includeDeleteRename: true) + return makeFeedContextMenu(node: node, indexPath: indexPath, includeDeleteRename: true) } else { - return makeFolderContextMenu(indexPath: indexPath) + return makeFolderContextMenu(node: node, indexPath: indexPath) } } + + override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + guard let nodeUniqueId = configuration.identifier as? Int, + let node = coordinator.rootNode.descendantNode(where: { $0.uniqueID == nodeUniqueId }), + let indexPath = dataSource.indexPath(for: node), + let cell = tableView.cellForRow(at: indexPath) else { + return nil + } + + return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell)) + } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { becomeFirstResponder() @@ -572,12 +583,22 @@ extension MasterFeedViewController: UIContextMenuInteractionDelegate { return nil } - return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions in + return UIContextMenuConfiguration(identifier: sectionIndex as NSCopying, previewProvider: nil) { suggestedActions in let accountInfoAction = self.getAccountInfoAction(account: account) let deactivateAction = self.deactivateAccountAction(account: account) return UIMenu(title: "", children: [accountInfoAction, deactivateAction]) } } + + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + + guard let sectionIndex = configuration.identifier as? Int, + let cell = tableView.headerView(forSection: sectionIndex) else { + return nil + } + + return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell)) + } } // MARK: MasterTableViewCellDelegate @@ -785,8 +806,8 @@ private extension MasterFeedViewController { } } - func makeFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration { - return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in + func makeFeedContextMenu(node: Node, indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration { + return UIContextMenuConfiguration(identifier: node.uniqueID as NSCopying, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in guard let self = self else { return nil } @@ -819,8 +840,8 @@ private extension MasterFeedViewController { } - func makeFolderContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration { - return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [weak self] suggestedActions in + func makeFolderContextMenu(node: Node, indexPath: IndexPath) -> UIContextMenuConfiguration { + return UIContextMenuConfiguration(identifier: node.uniqueID as NSCopying, previewProvider: nil, actionProvider: { [weak self] suggestedActions in guard let self = self else { return nil } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 27a9020e1..ac428fe8d 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -286,7 +286,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil } - return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [weak self] suggestedActions in + return UIContextMenuConfiguration(identifier: indexPath.row as NSCopying, previewProvider: nil, actionProvider: { [weak self] suggestedActions in guard let self = self else { return nil } @@ -317,6 +317,15 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } + override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + guard let row = configuration.identifier as? Int, + let cell = tableView.cellForRow(at: IndexPath(row: row, section: 0)) else { + return nil + } + + return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell)) + } + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { becomeFirstResponder() let article = dataSource.itemIdentifier(for: indexPath) diff --git a/iOS/UIKit Extensions/CroppingTargetedPreview.swift b/iOS/UIKit Extensions/CroppingTargetedPreview.swift new file mode 100644 index 000000000..58a19dfbf --- /dev/null +++ b/iOS/UIKit Extensions/CroppingTargetedPreview.swift @@ -0,0 +1,20 @@ +// +// CroppingPreviewParameters.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 11/23/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +class CroppingPreviewParameters: UIPreviewParameters { + + init(view: UIView) { + super.init() + let newBounds = CGRect(x: 1, y: 1, width: view.bounds.width - 2, height: view.bounds.height - 2) + let visiblePath = UIBezierPath(roundedRect: newBounds, cornerRadius: 10) + self.visiblePath = visiblePath + } + +} diff --git a/submodules/RSTree b/submodules/RSTree index 3dc1c288b..a041d4fc0 160000 --- a/submodules/RSTree +++ b/submodules/RSTree @@ -1 +1 @@ -Subproject commit 3dc1c288bb4e15fedf17fa8fc43c1d5cec36af5e +Subproject commit a041d4fc0e45077d28386e7efe4fca3a175584ad From ab9e8c09cef657595324212e328cb04bccab0f69 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 23 Nov 2019 22:18:41 -0600 Subject: [PATCH 32/50] Correct file name. --- NetNewsWire.xcodeproj/project.pbxproj | 8 ++++---- ...getedPreview.swift => CroppingPreviewParameters.swift} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename iOS/UIKit Extensions/{CroppingTargetedPreview.swift => CroppingPreviewParameters.swift} (100%) diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index f2f16e50a..0c0408a50 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -93,7 +93,7 @@ 51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */; }; 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */; }; 51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */; }; - 51627A93238A3836007B3B4B /* CroppingTargetedPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A92238A3836007B3B4B /* CroppingTargetedPreview.swift */; }; + 51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */; }; 516A093723609A3600EAE89B /* SettingsAccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */; }; 516A09392360A2AE00EAE89B /* SettingsAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */; }; 516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */; }; @@ -1272,7 +1272,7 @@ 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drag.swift"; sourceTree = ""; }; 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drop.swift"; sourceTree = ""; }; 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = ""; }; - 51627A92238A3836007B3B4B /* CroppingTargetedPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CroppingTargetedPreview.swift; sourceTree = ""; }; + 51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CroppingPreviewParameters.swift; sourceTree = ""; }; 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsAccountTableViewCell.xib; sourceTree = ""; }; 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountTableViewCell.swift; sourceTree = ""; }; 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsTableViewCell.xib; sourceTree = ""; }; @@ -1856,7 +1856,7 @@ children = ( 51F85BFA2275D85000C787DC /* Array-Extensions.swift */, 51F85BF42273625800C787DC /* Bundle-Extensions.swift */, - 51627A92238A3836007B3B4B /* CroppingTargetedPreview.swift */, + 51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */, 512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */, 512AF9DC236F05230066F8BE /* InteractiveLabel.swift */, 51934CC1230F5963006127BE /* InteractiveNavigationController.swift */, @@ -3965,7 +3965,7 @@ 51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */, 514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */, 5141E7562374A2890013FF27 /* ArticleIconSchemeHandler.swift in Sources */, - 51627A93238A3836007B3B4B /* CroppingTargetedPreview.swift in Sources */, + 51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */, 512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */, 51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */, 5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */, diff --git a/iOS/UIKit Extensions/CroppingTargetedPreview.swift b/iOS/UIKit Extensions/CroppingPreviewParameters.swift similarity index 100% rename from iOS/UIKit Extensions/CroppingTargetedPreview.swift rename to iOS/UIKit Extensions/CroppingPreviewParameters.swift From 70338797046d5d1db6d51724acd40158a5fb55d0 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 03:42:38 -0600 Subject: [PATCH 33/50] Animate safe area inset changes. Issue #1341 --- iOS/Article/ArticleViewController.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index c9f3827f4..eb8c3a47b 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -140,6 +140,12 @@ class ArticleViewController: UIViewController { coordinator.isArticleViewControllerPending = false } + override func viewSafeAreaInsetsDidChange() { + UIView.animate(withDuration: 1) { + self.view.layoutIfNeeded() + } + } + func updateUI() { guard let article = currentArticle else { From 00094858fa3914420ef17822386a7b5665c508c1 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 03:47:29 -0600 Subject: [PATCH 34/50] Remove filter button from All Unread timeline. --- iOS/MasterTimeline/MasterTimelineViewController.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index ac428fe8d..0db8fc13c 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -110,8 +110,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner filterButton.image = AppAssets.filterInactiveImage coordinator.showAllArticles() case .alwaysRead: - filterButton.image = AppAssets.filterActiveImage - coordinator.refreshTimeline() + break } } @@ -517,9 +516,13 @@ private extension MasterTimelineViewController { switch coordinator.articleReadFilterType { case .none: + filterButton.isHidden = false filterButton.image = AppAssets.filterInactiveImage - default: + case .read: + filterButton.isHidden = false filterButton.image = AppAssets.filterActiveImage + case .alwaysRead: + filterButton.isHidden = true } tableView.selectRow(at: nil, animated: false, scrollPosition: .top) From e05fdc99ddd9bfbbf339f3e5de9f7c1171b01d59 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 04:29:15 -0600 Subject: [PATCH 35/50] Change to use show/hide verbiage in menu items instead of check marks --- Mac/MainWindow/MainWindowController.swift | 33 +++++++++++++++---- .../TimelineContainerViewController.swift | 3 +- .../Timeline/TimelineViewController.swift | 5 +-- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 39b56f8d3..f77524c1c 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -238,16 +238,11 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } if item.action == #selector(toggleReadFeedsFilter(_:)) { - (item as! NSMenuItem).state = sidebarViewController?.isReadFiltered ?? false ? .on : .off + return validateToggleReadFeeds(item) } if item.action == #selector(toggleReadArticlesFilter(_:)) { - if let timelineContainer = timelineContainerViewController { - (item as! NSMenuItem).isEnabled = true - (item as! NSMenuItem).state = timelineContainer.isReadFiltered ? .on : .off - } else { - (item as! NSMenuItem).isEnabled = false - } + return validateToggleReadArticles(item) } if item.action == #selector(toggleSidebar(_:)) { @@ -832,6 +827,30 @@ private extension MainWindowController { return result } + + func validateToggleReadFeeds(_ item: NSValidatedUserInterfaceItem) -> Bool { + guard let menuItem = item as? NSMenuItem else { return false } + + let showCommand = NSLocalizedString("Show Read Feeds", comment: "Command") + let hideCommand = NSLocalizedString("Hide Read Feeds", comment: "Command") + menuItem.title = sidebarViewController?.isReadFiltered ?? false ? showCommand : hideCommand + return true + } + + func validateToggleReadArticles(_ item: NSValidatedUserInterfaceItem) -> Bool { + guard let menuItem = item as? NSMenuItem else { return false } + + let showCommand = NSLocalizedString("Show Read Articles", comment: "Command") + let hideCommand = NSLocalizedString("Hide Read Articles", comment: "Command") + + if let isReadFiltered = timelineContainerViewController?.isReadFiltered { + menuItem.title = isReadFiltered ? showCommand : hideCommand + return true + } else { + menuItem.title = hideCommand + return false + } + } // MARK: - Misc. diff --git a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift index cdeaec45d..0130fc535 100644 --- a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift @@ -30,7 +30,8 @@ final class TimelineContainerViewController: NSViewController { weak var delegate: TimelineContainerViewControllerDelegate? - var isReadFiltered: Bool { + var isReadFiltered: Bool? { + guard let currentTimelineViewController = currentTimelineViewController, mode(for: currentTimelineViewController) == .regular else { return nil } return regularTimelineViewController.isReadFiltered } diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 60f558dec..a6f88080d 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -21,8 +21,9 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr @IBOutlet var tableView: TimelineTableView! private var articleReadFilterType: ReadFilterType? - var isReadFiltered: Bool { - return articleReadFilterType ?? .read != .none + var isReadFiltered: Bool? { + guard let articleReadFilterType = articleReadFilterType, articleReadFilterType != .alwaysRead else { return nil} + return articleReadFilterType != .none } var representedObjects: [AnyObject]? { From 88707517e8f92c3071095923c91090ad811fe51f Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 09:41:50 -0600 Subject: [PATCH 36/50] Make sliders should be quantized. Issue #1342 --- iOS/Article/ArticleViewController.swift | 48 +++++++++++++++---- .../CroppingPreviewParameters.swift | 7 +++ iOS/UIKit Extensions/TickMarkSlider.swift | 11 ++++- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index eb8c3a47b..0888355f3 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -123,6 +123,8 @@ class ArticleViewController: UIViewController { webView.navigationDelegate = self webView.uiDelegate = self + webView.addInteraction(UIContextMenuInteraction(delegate: self)) + webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) @@ -340,6 +342,44 @@ class ArticleViewController: UIViewController { } +// MARK: InteractiveNavigationControllerTappable + +extension ArticleViewController: InteractiveNavigationControllerTappable { + func didTapNavigationBar() { + hideBars() + } +} + +// MARK: UIContextMenuInteractionDelegate + +extension ArticleViewController: UIContextMenuInteractionDelegate { + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { + + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions in + let action1 = UIAction(title: "Action 1", image: AppAssets.infoImage) { [weak self] action in + } + let action2 = UIAction(title: "Action 2", image: AppAssets.infoImage) { [weak self] action in + } + let action3 = UIAction(title: "Action 3", image: AppAssets.infoImage) { [weak self] action in + } + let action4 = UIAction(title: "Action 4", image: AppAssets.infoImage) { [weak self] action in + } + let action5 = UIAction(title: "Action 5", image: AppAssets.infoImage) { [weak self] action in + } + let action6 = UIAction(title: "Action 6", image: AppAssets.infoImage) { [weak self] action in + } + let action7 = UIAction(title: "Action 7", image: AppAssets.infoImage) { [weak self] action in + } + return UIMenu(title: "", children: [action1, action2, action3, action4, action5, action6, action7]) + } + } + + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + return UITargetedPreview(view: webView, parameters: CroppingPreviewParameters(view: webView, size: CGSize(width: webView.bounds.width, height: 200))) + } + +} + // MARK: WKNavigationDelegate extension ArticleViewController: WKNavigationDelegate { @@ -376,14 +416,6 @@ extension ArticleViewController: WKNavigationDelegate { } -// MARK: InteractiveNavigationControllerTappable - -extension ArticleViewController: InteractiveNavigationControllerTappable { - func didTapNavigationBar() { - hideBars() - } -} - // MARK: WKUIDelegate extension ArticleViewController: WKUIDelegate { diff --git a/iOS/UIKit Extensions/CroppingPreviewParameters.swift b/iOS/UIKit Extensions/CroppingPreviewParameters.swift index 58a19dfbf..c99f6e404 100644 --- a/iOS/UIKit Extensions/CroppingPreviewParameters.swift +++ b/iOS/UIKit Extensions/CroppingPreviewParameters.swift @@ -17,4 +17,11 @@ class CroppingPreviewParameters: UIPreviewParameters { self.visiblePath = visiblePath } + init(view: UIView, size: CGSize) { + super.init() + let newBounds = CGRect(x: 0, y: 0, width: size.width, height: size.height) + let visiblePath = UIBezierPath(roundedRect: newBounds, cornerRadius: 10) + self.visiblePath = visiblePath + } + } diff --git a/iOS/UIKit Extensions/TickMarkSlider.swift b/iOS/UIKit Extensions/TickMarkSlider.swift index 9c111c738..a6b8f7b2c 100644 --- a/iOS/UIKit Extensions/TickMarkSlider.swift +++ b/iOS/UIKit Extensions/TickMarkSlider.swift @@ -13,9 +13,12 @@ class TickMarkSlider: UISlider { private var enableFeedback = false private let feedbackGenerator = UISelectionFeedbackGenerator() + private var roundedValue: Float? override var value: Float { didSet { - if enableFeedback && value.truncatingRemainder(dividingBy: 1) == 0 { + let testValue = value.rounded() + if testValue != roundedValue && enableFeedback && value.truncatingRemainder(dividingBy: 1) == 0 { + roundedValue = testValue feedbackGenerator.selectionChanged() } } @@ -66,6 +69,12 @@ class TickMarkSlider: UISlider { } } + + override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + let result = super.continueTracking(touch, with: event) + value = value.rounded() + return result + } override func endTracking(_ touch: UITouch?, with event: UIEvent?) { value = value.rounded() From 06d3c3520650b4abce8fd6ed9c60d5751b74ae40 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 10:01:47 -0600 Subject: [PATCH 37/50] Rollback POC code that shouldn't have gotten added --- iOS/Article/ArticleViewController.swift | 32 ------------------- .../CroppingPreviewParameters.swift | 7 ---- 2 files changed, 39 deletions(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 0888355f3..585178f01 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -123,8 +123,6 @@ class ArticleViewController: UIViewController { webView.navigationDelegate = self webView.uiDelegate = self - webView.addInteraction(UIContextMenuInteraction(delegate: self)) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) @@ -350,36 +348,6 @@ extension ArticleViewController: InteractiveNavigationControllerTappable { } } -// MARK: UIContextMenuInteractionDelegate - -extension ArticleViewController: UIContextMenuInteractionDelegate { - func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { - - return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions in - let action1 = UIAction(title: "Action 1", image: AppAssets.infoImage) { [weak self] action in - } - let action2 = UIAction(title: "Action 2", image: AppAssets.infoImage) { [weak self] action in - } - let action3 = UIAction(title: "Action 3", image: AppAssets.infoImage) { [weak self] action in - } - let action4 = UIAction(title: "Action 4", image: AppAssets.infoImage) { [weak self] action in - } - let action5 = UIAction(title: "Action 5", image: AppAssets.infoImage) { [weak self] action in - } - let action6 = UIAction(title: "Action 6", image: AppAssets.infoImage) { [weak self] action in - } - let action7 = UIAction(title: "Action 7", image: AppAssets.infoImage) { [weak self] action in - } - return UIMenu(title: "", children: [action1, action2, action3, action4, action5, action6, action7]) - } - } - - func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - return UITargetedPreview(view: webView, parameters: CroppingPreviewParameters(view: webView, size: CGSize(width: webView.bounds.width, height: 200))) - } - -} - // MARK: WKNavigationDelegate extension ArticleViewController: WKNavigationDelegate { diff --git a/iOS/UIKit Extensions/CroppingPreviewParameters.swift b/iOS/UIKit Extensions/CroppingPreviewParameters.swift index c99f6e404..58a19dfbf 100644 --- a/iOS/UIKit Extensions/CroppingPreviewParameters.swift +++ b/iOS/UIKit Extensions/CroppingPreviewParameters.swift @@ -17,11 +17,4 @@ class CroppingPreviewParameters: UIPreviewParameters { self.visiblePath = visiblePath } - init(view: UIView, size: CGSize) { - super.init() - let newBounds = CGRect(x: 0, y: 0, width: size.width, height: size.height) - let visiblePath = UIBezierPath(roundedRect: newBounds, cornerRadius: 10) - self.visiblePath = visiblePath - } - } From 57e8a98b57df63158a66201becdaee27f9e0a979 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 10:27:02 -0600 Subject: [PATCH 38/50] Stop animating the initial timeline load. Issue #1334 --- iOS/SceneCoordinator.swift | 82 ++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 48 deletions(-) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 87d49eb90..227da9855 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -393,36 +393,15 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } @objc func accountStateDidChange(_ note: Notification) { - if timelineFetcherContainsAnyPseudoFeed() { - fetchAndReplaceArticlesAsync { - self.masterTimelineViewController?.reinitializeArticles() - self.rebuildBackingStores() - } - } else { - rebuildBackingStores() - } + updateForAccountChanges() } @objc func userDidAddAccount(_ note: Notification) { - if timelineFetcherContainsAnyPseudoFeed() { - fetchAndReplaceArticlesAsync { - self.masterTimelineViewController?.reinitializeArticles() - self.rebuildBackingStores() - } - } else { - rebuildBackingStores() - } + updateForAccountChanges() } @objc func userDidDeleteAccount(_ note: Notification) { - if timelineFetcherContainsAnyPseudoFeed() { - fetchAndReplaceArticlesAsync { - self.masterTimelineViewController?.reinitializeArticles() - self.rebuildBackingStores() - } - } else { - rebuildBackingStores() - } + updateForAccountChanges() } @objc func userDefaultsDidChange(_ note: Notification) { @@ -466,7 +445,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func refreshTimeline() { - fetchAndReplaceArticlesAsync() { + fetchAndReplaceArticlesAsync(animated: true) { self.masterTimelineViewController?.reinitializeArticles() } } @@ -556,13 +535,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { self.activityManager.selecting(feed: feed) self.installTimelineControllerIfNecessary(animated: animated) - setTimelineFeed(feed) { + setTimelineFeed(feed, animated: false) { completion?() } } else { - setTimelineFeed(nil) { + setTimelineFeed(nil, animated: false) { self.activityManager.invalidateSelecting() if self.rootSplitViewController.isCollapsed && self.navControllerForTimeline().viewControllers.last is MasterTimelineViewController { self.navControllerForTimeline().popViewController(animated: animated) @@ -647,7 +626,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { isSearching = true savedSearchArticles = articles savedSearchArticleIds = Set(articles.map { $0.articleID }) - setTimelineFeed(nil) + setTimelineFeed(nil, animated: true) selectArticle(nil) } @@ -655,9 +634,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { if let ip = currentFeedIndexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed { timelineFeed = feed masterTimelineViewController?.reinitializeArticles() - replaceArticles(with: savedSearchArticles!, animate: true) + replaceArticles(with: savedSearchArticles!, animated: true) } else { - setTimelineFeed(nil) + setTimelineFeed(nil, animated: true) } lastSearchString = "" @@ -673,7 +652,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { guard isSearching else { return } if searchString.count < 3 { - setTimelineFeed(nil) + setTimelineFeed(nil, animated: true) return } @@ -681,9 +660,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { switch searchScope { case .global: - setTimelineFeed(SmartFeed(delegate: SearchFeedDelegate(searchString: searchString))) + setTimelineFeed(SmartFeed(delegate: SearchFeedDelegate(searchString: searchString)), animated: true) case .timeline: - setTimelineFeed(SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: savedSearchArticleIds!))) + setTimelineFeed(SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: savedSearchArticleIds!)), animated: true) } lastSearchString = searchString @@ -1090,6 +1069,17 @@ 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 { @@ -1147,12 +1137,12 @@ private extension SceneCoordinator { return indexPathFor(node) } - func setTimelineFeed(_ feed: Feed?, completion: (() -> Void)? = nil) { + func setTimelineFeed(_ feed: Feed?, animated: Bool, completion: (() -> Void)? = nil) { timelineFeed = feed timelineMiddleIndexPath = nil articleReadFilterType = feed?.defaultReadFilterType ?? .none - fetchAndReplaceArticlesAsync { + fetchAndReplaceArticlesAsync(animated: animated) { self.masterTimelineViewController?.reinitializeArticles() completion?() } @@ -1413,25 +1403,25 @@ private extension SceneCoordinator { func emptyTheTimeline() { if !articles.isEmpty { - replaceArticles(with: Set
(), animate: false) + replaceArticles(with: Set
(), animated: false) } } func sortParametersDidChange() { - replaceArticles(with: Set(articles), animate: true) + replaceArticles(with: Set(articles), animated: true) } - func replaceArticles(with unsortedArticles: Set
, animate: Bool) { + func replaceArticles(with unsortedArticles: Set
, animated: Bool) { let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed) - replaceArticles(with: sortedArticles, animate: animate) + replaceArticles(with: sortedArticles, animated: animated) } - func replaceArticles(with sortedArticles: ArticleArray, animate: Bool) { + func replaceArticles(with sortedArticles: ArticleArray, animated: Bool) { if articles != sortedArticles { articles = sortedArticles updateShowNamesAndIcons() updateUnreadCount() - masterTimelineViewController?.reloadArticles(animated: animate) + masterTimelineViewController?.reloadArticles(animated: animated) } } @@ -1458,7 +1448,7 @@ private extension SceneCoordinator { } } - strongSelf.replaceArticles(with: updatedArticles, animate: true) + strongSelf.replaceArticles(with: updatedArticles, animated: true) } } @@ -1468,7 +1458,7 @@ private extension SceneCoordinator { fetchRequestQueue.cancelAllRequests() } - func fetchAndReplaceArticlesAsync(completion: @escaping () -> Void) { + func fetchAndReplaceArticlesAsync(animated: Bool, completion: @escaping () -> Void) { // To be called when we need to do an entire fetch, but an async delay is okay. // Example: we have the Today feed selected, and the calendar day just changed. cancelPendingAsyncFetches() @@ -1479,7 +1469,7 @@ private extension SceneCoordinator { } fetchUnsortedArticlesAsync(for: [timelineFetcher]) { [weak self] (articles) in - self?.replaceArticles(with: articles, animate: true) + self?.replaceArticles(with: articles, animated: animated) completion() } @@ -1543,14 +1533,10 @@ private extension SceneCoordinator { func installTimelineControllerIfNecessary(animated: Bool) { if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 { - isTimelineViewControllerPending = true - masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) masterTimelineViewController!.coordinator = self navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: animated) - - masterTimelineViewController?.reloadArticles(animated: false) } } From b5525e1a9e44fdd9605441f029f55559d4833b81 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 10:33:13 -0600 Subject: [PATCH 39/50] Restore back button items. Issue #1337 --- iOS/MasterFeed/MasterFeedViewController.swift | 3 --- iOS/MasterTimeline/MasterTimelineViewController.swift | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 113c746e3..5b64d2e38 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -39,9 +39,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { navigationController?.navigationBar.prefersLargeTitles = true } - // Set the bar button item so that it doesn't show on the timeline view - navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) - // If you don't have an empty table header, UIKit tries to help out by putting one in for you // that makes a gap between the first section header and the navigation bar var frame = CGRect.zero diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 0db8fc13c..32a4bb73c 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -72,8 +72,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner resetUI() applyChanges(animated: false) - // Set the bar button item so that it doesn't show on the article view - navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) + title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline" // Restore the scroll position if we have one stored if let restoreIndexPath = coordinator.timelineMiddleIndexPath { From a052bbe74e4ad7fa6a66bbf20b8dd300f51f6e29 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 10:47:09 -0600 Subject: [PATCH 40/50] Fix title flashing in after navigation bar is shown. Issue #1336 --- .../MasterTimelineViewController.swift | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 32a4bb73c..1c8145aa5 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -13,9 +13,9 @@ import Articles class MasterTimelineViewController: UITableViewController, UndoableCommandRunner { - private var titleView: MasterTimelineTitleView? private var numberOfTextLines = 0 private var iconSize = IconSize.medium + private lazy var feedTapGestureRecognizer = UITapGestureRecognizer(target: self, action:#selector(showFeedInspector(_:))) @IBOutlet weak var filterButton: UIBarButtonItem! @IBOutlet weak var markAllAsReadButton: UIBarButtonItem! @@ -69,6 +69,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner iconSize = AppDefaults.timelineIconSize resetEstimatedRowHeight() + if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView { + navigationItem.titleView = titleView + } + resetUI() applyChanges(animated: false) @@ -358,7 +362,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } @objc func webFeedIconDidBecomeAvailable(_ note: Notification) { - titleView?.iconView.iconImage = coordinator.timelineIconImage + + if let titleView = navigationItem.titleView as? MasterTimelineTitleView { + titleView.iconView.iconImage = coordinator.timelineIconImage + } + guard let feed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else { return } @@ -389,7 +397,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } @objc func faviconDidBecomeAvailable(_ note: Notification) { - titleView?.iconView.iconImage = coordinator.timelineIconImage + if let titleView = navigationItem.titleView as? MasterTimelineTitleView { + titleView.iconView.iconImage = coordinator.timelineIconImage + } if coordinator.showIcons { queueReloadAvailableCells() } @@ -409,7 +419,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } @objc func displayNameDidChange(_ note: Notification) { - titleView?.label.text = coordinator.timelineFeed?.nameForDisplay + if let titleView = navigationItem.titleView as? MasterTimelineTitleView { + titleView.label.text = coordinator.timelineFeed?.nameForDisplay + } } @objc func scrollPositionDidChange() { @@ -497,17 +509,16 @@ private extension MasterTimelineViewController { func resetUI() { - if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView { - self.titleView = titleView - + if let titleView = navigationItem.titleView as? MasterTimelineTitleView { titleView.iconView.iconImage = coordinator.timelineIconImage titleView.label.text = coordinator.timelineFeed?.nameForDisplay updateTitleUnreadCount() if coordinator.timelineFeed is WebFeed { titleView.heightAnchor.constraint(equalToConstant: 44.0).isActive = true - let tap = UITapGestureRecognizer(target: self, action:#selector(showFeedInspector(_:))) - titleView.addGestureRecognizer(tap) + titleView.addGestureRecognizer(feedTapGestureRecognizer) + } else { + titleView.removeGestureRecognizer(feedTapGestureRecognizer) } navigationItem.titleView = titleView @@ -544,7 +555,9 @@ private extension MasterTimelineViewController { } func updateTitleUnreadCount() { - self.titleView?.unreadCountView.unreadCount = coordinator.unreadCount + if let titleView = navigationItem.titleView as? MasterTimelineTitleView { + titleView.unreadCountView.unreadCount = coordinator.unreadCount + } } func applyChanges(animated: Bool, completion: (() -> Void)? = nil) { From 4048d79e136255365bc2580222886e3eaa1c716b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 11:21:00 -0600 Subject: [PATCH 41/50] Specify header font again. Issue #1335 --- iOS/Resources/styleSheet.css | 1 + 1 file changed, 1 insertion(+) diff --git a/iOS/Resources/styleSheet.css b/iOS/Resources/styleSheet.css index 1b44588d3..a95e21bc3 100644 --- a/iOS/Resources/styleSheet.css +++ b/iOS/Resources/styleSheet.css @@ -67,6 +67,7 @@ body .headerTable { border-bottom: 1px solid var(--header-table-border-color); } body .header { + font: -apple-system-body; color: var(--header-color); } body .header a:link, body .header a:visited { From a3abef2b620781096678538f32ee428ec6226a6b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 11:24:00 -0600 Subject: [PATCH 42/50] Change Customize Layout setting to Timeline Layout setting. Issue #1338 --- iOS/Settings/Settings.storyboard | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index bcb9ff294..605b69281 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -184,7 +184,7 @@ - + + + + + + + + + + + diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index 024d08d59..3c249e19b 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -159,8 +159,8 @@ class SettingsViewController: UITableViewController { case 4: switch indexPath.row { case 0: - let timeline = UIStoryboard.settings.instantiateController(ofType: AboutViewController.self) - self.navigationController?.pushViewController(timeline, animated: true) + openURL("https://ranchero.com/netnewswire/help/ios/5.0/en/") + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) case 1: openURL("https://ranchero.com/netnewswire/") tableView.selectRow(at: nil, animated: true, scrollPosition: .none) @@ -176,6 +176,9 @@ class SettingsViewController: UITableViewController { case 5: openURL("https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes") tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + case 6: + let timeline = UIStoryboard.settings.instantiateController(ofType: AboutViewController.self) + self.navigationController?.pushViewController(timeline, animated: true) default: break } From 5c4cd072ce2f3c55faac8f0d1e167b06ccf4d349 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 13:37:56 -0600 Subject: [PATCH 45/50] Make next unread button work with new async feed functionality. --- iOS/Article/ArticleViewController.swift | 3 +- .../MasterTimelineViewController.swift | 4 +- iOS/SceneCoordinator.swift | 42 ++++++++++++------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 585178f01..0515e7cac 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -141,7 +141,8 @@ class ArticleViewController: UIViewController { } override func viewSafeAreaInsetsDidChange() { - UIView.animate(withDuration: 1) { + // When the bars are hiding, the bar hiding duration is used. In all other cases, execute immediately. + UIView.animate(withDuration: 0.0) { self.view.layoutIfNeeded() } } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 1c8145aa5..7ce7a0123 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -76,8 +76,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner resetUI() applyChanges(animated: false) - title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline" - // Restore the scroll position if we have one stored if let restoreIndexPath = coordinator.timelineMiddleIndexPath { tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false) @@ -509,6 +507,8 @@ private extension MasterTimelineViewController { func resetUI() { + title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline" + if let titleView = navigationItem.titleView as? MasterTimelineTitleView { titleView.iconView.iconImage = coordinator.timelineIconImage titleView.label.text = coordinator.timelineFeed?.nameForDisplay diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 227da9855..9bfc22697 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -269,7 +269,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { super.init() - for section in treeController.rootNode.childNodes { + for _ in treeController.rootNode.childNodes { shadowTable.append([Node]()) } @@ -519,7 +519,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return indexPathFor(node) } - func selectFeed(_ indexPath: IndexPath?, animated: Bool, completion: (() -> Void)? = nil) { + func selectFeed(_ indexPath: IndexPath?, animated: Bool, deselectArticle: Bool = true, completion: (() -> Void)? = nil) { guard indexPath != currentFeedIndexPath else { completion?() return @@ -529,7 +529,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { masterFeedViewController.updateFeedSelection(animated: animated) emptyTheTimeline() - selectArticle(nil) + if deselectArticle { + selectArticle(nil) + } if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed { @@ -718,9 +720,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return } - selectNextUnreadFeedFetcher() - if selectNextUnreadArticleInTimeline() { - activityManager.selectingNextUnread() + selectNextUnreadFeed() { + if self.selectNextUnreadArticleInTimeline() { + self.activityManager.selectingNextUnread() + } } } @@ -1315,7 +1318,7 @@ private extension SceneCoordinator { } - func selectNextUnreadFeedFetcher() { + func selectNextUnreadFeed(completion: @escaping () -> Void) { let indexPath: IndexPath = { if currentFeedIndexPath == nil { @@ -1338,15 +1341,19 @@ private extension SceneCoordinator { } }() - if selectNextUnreadFeedFetcher(startingWith: nextIndexPath) { - return + selectNextUnreadFeed(startingWith: nextIndexPath) { found in + if !found { + self.selectNextUnreadFeed(startingWith: IndexPath(row: 0, section: 0)) { _ in + completion() + } + } else { + completion() + } } - selectNextUnreadFeedFetcher(startingWith: IndexPath(row: 0, section: 0)) } - @discardableResult - func selectNextUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool { + func selectNextUnreadFeed(startingWith indexPath: IndexPath, completion: @escaping (Bool) -> Void) { for i in indexPath.section.. 0 { - selectFeed(nextIndexPath, animated: true) - return true + selectFeed(nextIndexPath, animated: false, deselectArticle: false) { + completion(true) + } + return } } } - return false + completion(false) } From 2c7ec880870bb91bc8c8ea6ea34f570e6d77f05b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 13:41:32 -0600 Subject: [PATCH 46/50] Removed dead code. --- iOS/Article/ArticleViewController.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 0515e7cac..47eabd5a8 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -141,10 +141,8 @@ class ArticleViewController: UIViewController { } override func viewSafeAreaInsetsDidChange() { - // When the bars are hiding, the bar hiding duration is used. In all other cases, execute immediately. - UIView.animate(withDuration: 0.0) { - self.view.layoutIfNeeded() - } + // This will animate if the show/hide bars animation is happening. + view.layoutIfNeeded() } func updateUI() { From 69aeacd98d41888930d4337f8f2e43903012c256 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 14:18:58 -0600 Subject: [PATCH 47/50] Add fullscreen article setting/functionality. Issue #1343 --- iOS/AppDefaults.swift | 11 ++++ iOS/Article/ArticleViewController.swift | 9 ++++ iOS/Settings/Settings.storyboard | 51 +++++++++++++++--- iOS/Settings/SettingsViewController.swift | 66 ++++++++++++++++------- 4 files changed, 111 insertions(+), 26 deletions(-) diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index 0942df915..f29b7d191 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -23,6 +23,7 @@ struct AppDefaults { static let timelineNumberOfLines = "timelineNumberOfLines" static let timelineIconSize = "timelineIconSize" static let timelineSortDirection = "timelineSortDirection" + static let articleFullscreenEnabled = "articleFullscreenEnabled" static let displayUndoAvailableTip = "displayUndoAvailableTip" static let lastRefresh = "lastRefresh" static let addWebFeedAccountID = "addWebFeedAccountID" @@ -92,6 +93,15 @@ struct AppDefaults { } } + static var articleFullscreenEnabled: Bool { + get { + return bool(for: Key.articleFullscreenEnabled) + } + set { + setBool(for: Key.articleFullscreenEnabled, newValue) + } + } + static var displayUndoAvailableTip: Bool { get { return bool(for: Key.displayUndoAvailableTip) @@ -135,6 +145,7 @@ struct AppDefaults { Key.timelineNumberOfLines: 2, Key.timelineIconSize: IconSize.medium.rawValue, Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, + Key.articleFullscreenEnabled: false, Key.displayUndoAvailableTip: true] AppDefaults.shared.register(defaults: defaults) } diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 47eabd5a8..fd400d9a2 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -135,6 +135,13 @@ class ArticleViewController: UIViewController { } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if AppDefaults.articleFullscreenEnabled { + hideBars() + } + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(true) coordinator.isArticleViewControllerPending = false @@ -484,6 +491,7 @@ private extension ArticleViewController { func showBars() { if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed { + AppDefaults.articleFullscreenEnabled = false coordinator.showStatusBar() showNavigationViewConstraint.constant = 0 showToolbarViewConstraint.constant = 0 @@ -494,6 +502,7 @@ private extension ArticleViewController { func hideBars() { if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed { + AppDefaults.articleFullscreenEnabled = true coordinator.hideStatusBar() showNavigationViewConstraint.constant = 44.0 showToolbarViewConstraint.constant = 44.0 diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index 24d8aef6f..c81c9ed12 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -19,7 +19,7 @@ - + @@ -196,10 +196,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -216,7 +250,7 @@ - + @@ -233,7 +267,7 @@ - + @@ -250,7 +284,7 @@ - + @@ -267,7 +301,7 @@ - + @@ -284,7 +318,7 @@ - + @@ -301,7 +335,7 @@ - + @@ -334,6 +368,7 @@ + diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index 3c249e19b..cde10c3a2 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -17,6 +17,7 @@ class SettingsViewController: UITableViewController { @IBOutlet weak var timelineSortOrderSwitch: UISwitch! @IBOutlet weak var groupByFeedSwitch: UISwitch! + @IBOutlet weak var showFullscreenArticlesSwitch: UISwitch! weak var presentingParentController: UIViewController? @@ -50,6 +51,12 @@ class SettingsViewController: UITableViewController { groupByFeedSwitch.isOn = false } + if AppDefaults.articleFullscreenEnabled { + showFullscreenArticlesSwitch.isOn = true + } else { + showFullscreenArticlesSwitch.isOn = false + } + let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 20.0, y: 0.0, width: 0.0, height: 0.0)) buildLabel.font = UIFont.systemFont(ofSize: 11.0) buildLabel.textColor = UIColor.gray @@ -71,24 +78,42 @@ class SettingsViewController: UITableViewController { // MARK: UITableView + override func numberOfSections(in tableView: UITableView) -> Int { + var sections = super.numberOfSections(in: tableView) + if traitCollection.userInterfaceIdiom != .phone { + sections = sections - 1 + } + return sections + } + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch section { + var adjustedSection = section + if traitCollection.userInterfaceIdiom != .phone && section > 3 { + adjustedSection = adjustedSection + 1 + } + + switch adjustedSection { case 1: return AccountManager.shared.accounts.count + 1 case 2: - let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section) + let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: adjustedSection) if AccountManager.shared.activeAccounts.isEmpty || AccountManager.shared.anyAccountHasFeedWithURL(appNewsURLString) { return defaultNumberOfRows - 1 } return defaultNumberOfRows default: - return super.tableView(tableView, numberOfRowsInSection: section) + return super.tableView(tableView, numberOfRowsInSection: adjustedSection) } } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + var adjustedSection = indexPath.section + if traitCollection.userInterfaceIdiom != .phone && adjustedSection > 3 { + adjustedSection = adjustedSection + 1 + } + let cell: UITableViewCell - switch indexPath.section { + switch adjustedSection { case 1: let sortedAccounts = AccountManager.shared.sortedAccounts @@ -105,8 +130,8 @@ class SettingsViewController: UITableViewController { } default: - - cell = super.tableView(tableView, cellForRowAt: indexPath) + let adjustedIndexPath = IndexPath(row: indexPath.row, section: adjustedSection) + cell = super.tableView(tableView, cellForRowAt: adjustedIndexPath) } @@ -114,7 +139,12 @@ class SettingsViewController: UITableViewController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - switch indexPath.section { + var adjustedSection = indexPath.section + if traitCollection.userInterfaceIdiom != .phone && adjustedSection > 3 { + adjustedSection = adjustedSection + 1 + } + + switch adjustedSection { case 0: UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!) tableView.selectRow(at: nil, animated: true, scrollPosition: .none) @@ -156,7 +186,7 @@ class SettingsViewController: UITableViewController { default: break } - case 4: + case 5: switch indexPath.row { case 0: openURL("https://ranchero.com/netnewswire/help/ios/5.0/en/") @@ -200,19 +230,11 @@ class SettingsViewController: UITableViewController { } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - if indexPath.section == 1 { - return super.tableView(tableView, heightForRowAt: IndexPath(row: 0, section: 1)) - } else { - return super.tableView(tableView, heightForRowAt: indexPath) - } + return super.tableView(tableView, heightForRowAt: IndexPath(row: 0, section: 1)) } override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int { - if indexPath.section == 1 { - return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1)) - } else { - return super.tableView(tableView, indentationLevelForRowAt: indexPath) - } + return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1)) } // MARK: Actions @@ -237,6 +259,14 @@ class SettingsViewController: UITableViewController { } } + @IBAction func switchFullscreenArticles(_ sender: Any) { + if showFullscreenArticlesSwitch.isOn { + AppDefaults.articleFullscreenEnabled = true + } else { + AppDefaults.articleFullscreenEnabled = false + } + } + // MARK: Notifications @objc func contentSizeCategoryDidChange() { From 74297944e9fc5ab6f00fe2efb4eb9ad2fd6ae8f1 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 14:36:17 -0600 Subject: [PATCH 48/50] Clear current article so that wrapping occurs --- iOS/SceneCoordinator.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 9bfc22697..9df775bd4 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1370,7 +1370,7 @@ private extension SceneCoordinator { let nextIndexPath = IndexPath(row: j, section: i) guard let node = nodeFor(nextIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else { assertionFailure() - completion(true) + completion(false) return } @@ -1380,6 +1380,7 @@ private extension SceneCoordinator { if unreadCountProvider.unreadCount > 0 { selectFeed(nextIndexPath, animated: false, deselectArticle: false) { + self.currentArticle = nil completion(true) } return From cd493730b1d1c222be9d9079382e50b438cd61cc Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 14:49:44 -0600 Subject: [PATCH 49/50] Hide bars when returning to the foreground so that they don't come back. --- iOS/Article/ArticleViewController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index fd400d9a2..ffbdb48e7 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -249,7 +249,10 @@ class ArticleViewController: UIViewController { } @objc func willEnterForeground(_ note: Notification) { - showBars() + // The toolbar will come back on you if you don't hide it again + if AppDefaults.articleFullscreenEnabled { + hideBars() + } } // MARK: Actions From 6a56936850906a7b63503ddf4007ab021e597bea Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 24 Nov 2019 18:29:00 -0600 Subject: [PATCH 50/50] Moved expanded state away from Node so that it won't get lost on rebuilds. Issue #1346 --- Frameworks/Account/Account.swift | 4 + .../Account/Account.xcodeproj/project.pbxproj | 4 + Frameworks/Account/Container.swift | 2 +- Frameworks/Account/ContainerIdentifier.swift | 19 ++++ Frameworks/Account/Folder.swift | 8 ++ Shared/SmartFeeds/SmartFeedsController.swift | 6 +- .../Tree/WebFeedTreeControllerDelegate.swift | 2 - iOS/MasterFeed/MasterFeedViewController.swift | 10 +- iOS/SceneCoordinator.swift | 104 +++++++++++++----- submodules/RSTree | 2 +- 10 files changed, 126 insertions(+), 35 deletions(-) create mode 100644 Frameworks/Account/ContainerIdentifier.swift 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