diff --git a/Frameworks/Account/AccountManager.swift b/Frameworks/Account/AccountManager.swift index d932e36f8..1a273deb8 100644 --- a/Frameworks/Account/AccountManager.swift +++ b/Frameworks/Account/AccountManager.swift @@ -272,6 +272,11 @@ public final class AccountManager: UnreadCountProvider { var allFetchedArticles = Set
() let numberOfAccounts = activeAccounts.count var accountsReporting = 0 + + guard numberOfAccounts > 0 else { + completion(.success(allFetchedArticles)) + return + } for account in activeAccounts { account.fetchArticlesAsync(fetchType) { (articleSetResult) in diff --git a/Shared/Article Rendering/main.js b/Shared/Article Rendering/main.js index c843c9a1c..ddfcf3e18 100644 --- a/Shared/Article Rendering/main.js +++ b/Shared/Article Rendering/main.js @@ -22,12 +22,12 @@ function stripStyles() { document.getElementsByTagName("body")[0].querySelectorAll("[style]").forEach(element => stripStylesFromElement(element, ["color", "background", "font"])); } -// Convert all Feedbin proxy images to be used as src, otherwise change image locations to be absolute +// Convert all Feedbin proxy images to be used as src, otherwise change image locations to be absolute if not already function convertImgSrc() { document.querySelectorAll("img").forEach(element => { if (element.hasAttribute("data-canonical-src")) { element.src = element.getAttribute("data-canonical-src") - } else { + } else if (!element.src.match(/^[a-z]+\:\/\//i)) { element.src = new URL(element.src, document.baseURI).href; } }); diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index ff64049bc..718060997 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -14,6 +14,11 @@ import SafariServices class ArticleViewController: UIViewController { + typealias State = (extractedArticle: ExtractedArticle?, + isShowingExtractedArticle: Bool, + articleExtractorButtonState: ArticleExtractorButtonState, + windowScrollY: Int) + @IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem! @IBOutlet private weak var prevArticleBarButtonItem: UIBarButtonItem! @IBOutlet private weak var nextArticleBarButtonItem: UIBarButtonItem! @@ -49,7 +54,16 @@ class ArticleViewController: UIViewController { updateUI() } } - var restoreWindowScrollY = 0 + + var currentState: State? { + guard let controller = currentWebViewController else { return nil} + return State(extractedArticle: controller.extractedArticle, + isShowingExtractedArticle: controller.isShowingExtractedArticle, + articleExtractorButtonState: controller.articleExtractorButtonState, + windowScrollY: controller.windowScrollY) + } + + var restoreState: State? private let keyboardManager = KeyboardManager(type: .detail) override var keyCommands: [UIKeyCommand]? { @@ -89,7 +103,12 @@ class ArticleViewController: UIViewController { ]) let controller = createWebViewController(article) - controller.restoreWindowScrollY = restoreWindowScrollY + if let state = restoreState { + controller.extractedArticle = state.extractedArticle + controller.isShowingExtractedArticle = state.isShowingExtractedArticle + controller.articleExtractorButtonState = state.articleExtractorButtonState + controller.windowScrollY = state.windowScrollY + } articleExtractorButton.buttonState = controller.articleExtractorButtonState pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil) @@ -239,18 +258,16 @@ class ArticleViewController: UIViewController { currentWebViewController?.fullReload() } + func stopArticleExtractorIfProcessing() { + currentWebViewController?.stopArticleExtractorIfProcessing() + } + } // MARK: WebViewControllerDelegate extension ArticleViewController: WebViewControllerDelegate { - func webViewController(_ webViewController: WebViewController, restoreWindowScrollYDidUpdate restoreWindowScrollY: Int) { - if webViewController === currentWebViewController { - self.restoreWindowScrollY = restoreWindowScrollY - } - } - func webViewController(_ webViewController: WebViewController, articleExtractorButtonStateDidUpdate buttonState: ArticleExtractorButtonState) { if webViewController === currentWebViewController { articleExtractorButton.buttonState = buttonState @@ -290,7 +307,7 @@ extension ArticleViewController: UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { guard finished, completed else { return } guard let article = currentWebViewController?.article else { return } - coordinator.selectArticle(article) + coordinator.selectArticle(article, animations: [.select, .scroll, .navigation]) articleExtractorButton.buttonState = currentWebViewController?.articleExtractorButtonState ?? .off } diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index c69ddfc2a..b85f1dd40 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -14,7 +14,6 @@ import Articles import SafariServices protocol WebViewControllerDelegate: class { - func webViewController(_: WebViewController, restoreWindowScrollYDidUpdate: Int) func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState) } @@ -39,8 +38,12 @@ class WebViewController: UIViewController { private var clickedImageCompletion: (() -> Void)? private var articleExtractor: ArticleExtractor? = nil - private var extractedArticle: ExtractedArticle? - private var isShowingExtractedArticle = false { + var extractedArticle: ExtractedArticle? { + didSet { + windowScrollY = 0 + } + } + var isShowingExtractedArticle = false { didSet { if isShowingExtractedArticle != oldValue { reloadHTML() @@ -64,18 +67,14 @@ class WebViewController: UIViewController { startArticleExtractor() } if article != oldValue { - restoreWindowScrollY = 0 + windowScrollY = 0 reloadHTML() } } } let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 1.0) - var restoreWindowScrollY = 0 { - didSet { - delegate?.webViewController(self, restoreWindowScrollYDidUpdate: restoreWindowScrollY) - } - } + var windowScrollY = 0 deinit { if webView != nil { @@ -122,8 +121,6 @@ class WebViewController: UIViewController { webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) - // Even though page.html should be loaded into this webview, we have to do it again - // to work around this bug: http://www.openradar.me/22855188 self.reloadHTML() self.view.setNeedsLayout() @@ -247,6 +244,12 @@ class WebViewController: UIViewController { } + func stopArticleExtractorIfProcessing() { + if articleExtractor?.state == .processing { + stopArticleExtractor() + } + } + func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) { guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else { return @@ -423,7 +426,7 @@ extension WebViewController: UIScrollViewDelegate { @objc func scrollPositionDidChange() { webView?.evaluateJavaScript("window.scrollY") { (scrollY, _) in - self.restoreWindowScrollY = scrollY as? Int ?? 0 + self.windowScrollY = scrollY as? Int ?? 0 } } @@ -481,10 +484,10 @@ private extension WebViewController { var render = "error();" if let data = try? encoder.encode(templateData) { let json = String(data: data, encoding: .utf8)! - render = "render(\(json), \(restoreWindowScrollY));" + render = "render(\(json), \(windowScrollY));" } - restoreWindowScrollY = 0 + windowScrollY = 0 webView.scrollView.setZoomScale(1.0, animated: false) webView.evaluateJavaScript(render) diff --git a/iOS/Article/WebViewProvider.swift b/iOS/Article/WebViewProvider.swift index c9010baaf..21ab0bf50 100644 --- a/iOS/Article/WebViewProvider.swift +++ b/iOS/Article/WebViewProvider.swift @@ -14,17 +14,20 @@ import WebKit class WebViewProvider: NSObject, WKNavigationDelegate { let articleIconSchemeHandler: ArticleIconSchemeHandler + let viewController: UIViewController private let minimumQueueDepth = 3 private let maximumQueueDepth = 6 - private var queue: [WKWebView] = [] + private var queue = UIView() private var waitingForFirstLoad = true private var waitingCompletionHandler: ((WKWebView) -> ())? - init(coordinator: SceneCoordinator) { + init(coordinator: SceneCoordinator, viewController: UIViewController) { articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator) + self.viewController = viewController super.init() + self.viewController.view.insertSubview(queue, at: 0) replenishQueueIfNeeded() } @@ -37,12 +40,12 @@ class WebViewProvider: NSObject, WKNavigationDelegate { } func enqueueWebView(_ webView: WKWebView) { - guard queue.count < maximumQueueDepth else { + guard queue.subviews.count < maximumQueueDepth else { return } webView.navigationDelegate = self - queue.insert(webView, at: 0) + queue.insertSubview(webView, at: 0) webView.loadFileURL(ArticleRenderer.page.url, allowingReadAccessTo: ArticleRenderer.page.baseURL) @@ -63,7 +66,7 @@ class WebViewProvider: NSObject, WKNavigationDelegate { // MARK: Private private func replenishQueueIfNeeded() { - while queue.count < minimumQueueDepth { + while queue.subviews.count < minimumQueueDepth { let preferences = WKPreferences() preferences.javaScriptCanOpenWindowsAutomatically = false preferences.javaScriptEnabled = true @@ -81,7 +84,8 @@ class WebViewProvider: NSObject, WKNavigationDelegate { } private func completeRequest(completion: @escaping (WKWebView) -> ()) { - if let webView = queue.popLast() { + if let webView = queue.subviews.last as? WKWebView { + webView.removeFromSuperview() webView.navigationDelegate = nil replenishQueueIfNeeded() completion(webView) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 911d2f108..4daa24072 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -30,7 +30,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return rootSplitViewController.undoManager } - lazy var webViewProvider = WebViewProvider(coordinator: self) + lazy var webViewProvider = WebViewProvider(coordinator: self, viewController: rootSplitViewController) private var panelMode: PanelMode = .unset @@ -133,6 +133,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return treeController.rootNode } + // At some point we should refactor the current Feed IndexPath out and only use the timeline feed private(set) var currentFeedIndexPath: IndexPath? var timelineIconImage: IconImage? { @@ -445,6 +446,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } @objc func unreadCountDidChange(_ note: Notification) { + // We will handle the filtering of unread feeds in unreadCountDidInitialize after they have all be calculated + guard AccountManager.shared.isUnreadCountsInitialized else { + return + } + // If we are filtering reads, the new unread count is greater than 1, and the feed isn't shown then continue guard let feed = note.object as? Feed, isReadFeedsFiltered, feed.unreadCount > 0, !shadowTableContains(feed) else { return @@ -805,6 +811,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func endSearching() { if let ip = currentFeedIndexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed { + emptyTheTimeline() timelineFeed = feed masterTimelineViewController?.reinitializeArticles(resetScroll: true) replaceArticles(with: savedSearchArticles!, animated: true) @@ -905,7 +912,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { activityManager.selectingNextUnread() return } - + + if self.isSearching { + self.masterTimelineViewController?.hideSearch() + } + selectNextUnreadFeed() { if self.selectNextUnreadArticleInTimeline() { self.activityManager.selectingNextUnread() @@ -1251,14 +1262,45 @@ private extension SceneCoordinator { func rebuildBackingStores(initialLoad: Bool = false, updateExpandedNodes: (() -> Void)? = nil) { if !animatingChanges && !BatchUpdate.shared.isPerforming { + + addCurrentFeedToFilterExeptionsIfNecessary() treeController.rebuild() + treeControllerDelegate.resetFilterExceptions() + updateExpandedNodes?() rebuildShadowTable() masterFeedViewController.reloadFeeds(initialLoad: initialLoad) - clearTimelineIfNoLongerAvailable() + } } + func addCurrentFeedToFilterExeptionsIfNecessary() { + if isReadFeedsFiltered, let feedID = timelineFeed?.feedID { + if timelineFeed is SmartFeed { + treeControllerDelegate.addFilterException(feedID) + } else if let folderFeed = timelineFeed as? Folder { + if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil { + treeControllerDelegate.addFilterException(feedID) + } + } else if let webFeed = timelineFeed as? WebFeed { + if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil { + treeControllerDelegate.addFilterException(feedID) + addParentFolderToFilterExceptions(webFeed) + } + } + } + } + + func addParentFolderToFilterExceptions(_ feed: Feed) { + guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject), + let folder = node.parent?.representedObject as? Folder, + let folderFeedID = folder.feedID else { + return + } + + treeControllerDelegate.addFilterException(folderFeedID) + } + func rebuildShadowTable() { shadowTable = [[Node]]() @@ -1281,6 +1323,11 @@ private extension SceneCoordinator { shadowTable.append(result) } + + // If we have a current Feed IndexPath it is no longer valid and needs reset. + if currentFeedIndexPath != nil { + currentFeedIndexPath = indexPathFor(timelineFeed as AnyObject) + } } func shadowTableContains(_ feed: Feed) -> Bool { @@ -1743,14 +1790,14 @@ private extension SceneCoordinator { } @discardableResult - func installArticleController(restoreWindowScrollY: Int = 0, animated: Bool) -> ArticleViewController { + func installArticleController(state: ArticleViewController.State? = nil, animated: Bool) -> ArticleViewController { isArticleViewControllerPending = true let articleController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) articleController.coordinator = self articleController.article = currentArticle - articleController.restoreWindowScrollY = restoreWindowScrollY + articleController.restoreState = state if let subSplit = subSplitViewController { let controller = addNavControllerIfNecessary(articleController, showButton: false) @@ -1813,12 +1860,12 @@ private extension SceneCoordinator { } func configureThreePanelMode() { - let articleRestoreWindowScrollY = articleViewController?.restoreWindowScrollY ?? 0 + articleViewController?.stopArticleExtractorIfProcessing() + let articleViewControllerState = articleViewController?.currentState defer { masterNavigationController.viewControllers = [masterFeedViewController] } - if rootSplitViewController.viewControllers.last is InteractiveNavigationController { _ = rootSplitViewController.viewControllers.popLast() } @@ -1828,14 +1875,15 @@ private extension SceneCoordinator { masterTimelineViewController?.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem masterTimelineViewController?.navigationItem.leftItemsSupplementBackButton = true - installArticleController(restoreWindowScrollY: articleRestoreWindowScrollY, animated: false) + installArticleController(state: articleViewControllerState, animated: false) masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true) masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: false) } func configureStandardPanelMode() { - let articleRestoreWindowScrollY = articleViewController?.restoreWindowScrollY ?? 0 + articleViewController?.stopArticleExtractorIfProcessing() + let articleViewControllerState = articleViewController?.currentState rootSplitViewController.preferredPrimaryColumnWidthFraction = UISplitViewController.automaticDimension // Set the is Pending flags early to prevent the navigation controller delegate from thinking that we @@ -1855,7 +1903,7 @@ private extension SceneCoordinator { masterNavigationController.pushViewController(masterTimelineViewController!, animated: false) } - installArticleController(restoreWindowScrollY: articleRestoreWindowScrollY, animated: false) + installArticleController(state: articleViewControllerState, animated: false) } // MARK: NSUserActivity diff --git a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig index 612abb1fc..25353e80e 100644 --- a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig +++ b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig @@ -1,7 +1,7 @@ // High Level Settings common to both the iOS application and any extensions we bundle with it MARKETING_VERSION = 5.0 -CURRENT_PROJECT_VERSION = 31 +CURRENT_PROJECT_VERSION = 33 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon