From 3a99e6430f51237a9c4ff528e4b30211d77bd169 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 27 Jan 2020 12:58:32 -0700 Subject: [PATCH] Make the article icon code specify the desired article so that it can't pull the wrong one by mistake. Issue #1707 --- .../Detail/DetailIconSchemeHandler.swift | 4 +- .../Detail/DetailWebViewController.swift | 8 +-- NetNewsWire.xcodeproj/project.pbxproj | 18 +++--- .../Article Rendering/ArticleRenderer.swift | 2 +- iOS/AppDelegate.swift | 2 - iOS/Article/ArticleIconSchemeHandler.swift | 58 +++++++++++++++++++ iOS/Article/WebViewController.swift | 5 +- iOS/Article/WebViewProvider.swift | 15 +++-- iOS/SceneCoordinator.swift | 29 +++++++++- 9 files changed, 112 insertions(+), 29 deletions(-) rename Shared/Article Rendering/ArticleIconSchemeHandler.swift => Mac/MainWindow/Detail/DetailIconSchemeHandler.swift (91%) create mode 100644 iOS/Article/ArticleIconSchemeHandler.swift diff --git a/Shared/Article Rendering/ArticleIconSchemeHandler.swift b/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift similarity index 91% rename from Shared/Article Rendering/ArticleIconSchemeHandler.swift rename to Mac/MainWindow/Detail/DetailIconSchemeHandler.swift index 20b0f32bc..4aee30c11 100644 --- a/Shared/Article Rendering/ArticleIconSchemeHandler.swift +++ b/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift @@ -1,5 +1,5 @@ // -// AccountViewControllerSchemeHandler.swift +// DetailIconSchemeHandler.swift // NetNewsWire-iOS // // Created by Maurice Parker on 11/7/19. @@ -10,7 +10,7 @@ import Foundation import WebKit import Articles -class ArticleIconSchemeHandler: NSObject, WKURLSchemeHandler { +class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler { var currentArticle: Article? diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index 3a0474aba..a291908dc 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -39,7 +39,7 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { } #endif - private let articleIconSchemeHandler = ArticleIconSchemeHandler() + private let detailIconSchemeHandler = DetailIconSchemeHandler() private var waitingForFirstReload = false private let keyboardDelegate = DetailKeyboardDelegate() @@ -66,7 +66,7 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { let configuration = WKWebViewConfiguration() configuration.preferences = preferences - configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) + configuration.setURLSchemeHandler(detailIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) let userContentController = WKUserContentController() userContentController.add(self, name: MessageName.mouseDidEnter) @@ -208,10 +208,10 @@ private extension DetailWebViewController { case .loading: rendering = ArticleRenderer.loadingHTML(style: style) case .article(let article): - articleIconSchemeHandler.currentArticle = article + detailIconSchemeHandler.currentArticle = article rendering = ArticleRenderer.articleHTML(article: article, style: style) case .extracted(let article, let extractedArticle): - articleIconSchemeHandler.currentArticle = article + detailIconSchemeHandler.currentArticle = article rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style) } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 5f42a18bd..97e9c47e1 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -72,7 +72,6 @@ 513C5D0C232574DA003D4054 /* RSTree.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9520DD8CFE00CA8CF5 /* RSTree.framework */; }; 513C5D0E232574E4003D4054 /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; }; 5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */; }; - 5141E7562374A2890013FF27 /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */; }; 5142192A23522B5500E07E2C /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5142192923522B5500E07E2C /* ImageViewController.swift */; }; 514219372352510100E07E2C /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514219362352510100E07E2C /* ImageScrollView.swift */; }; 5142194B2353C1CF00E07E2C /* main_mac.js in Resources */ = {isa = PBXBuildFile; fileRef = 5142194A2353C1CF00E07E2C /* main_mac.js */; }; @@ -121,8 +120,8 @@ 518651DA235621840078E021 /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518651D9235621840078E021 /* ImageTransition.swift */; }; 5186A635235EF3A800C97195 /* VibrantLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5186A634235EF3A800C97195 /* VibrantLabel.swift */; }; 518B2EE82351B45600400001 /* NetNewsWire_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */; }; - 518C3193237B00D9004D740F /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */; }; - 518C3194237B00DA004D740F /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */; }; + 518C3193237B00D9004D740F /* DetailIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* DetailIconSchemeHandler.swift */; }; + 518C3194237B00DA004D740F /* DetailIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* DetailIconSchemeHandler.swift */; }; 518ED21D23D0F26000E0A862 /* UIViewController-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518ED21C23D0F26000E0A862 /* UIViewController-Extensions.swift */; }; 51934CCB230F599B006127BE /* InteractiveNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CC1230F5963006127BE /* InteractiveNavigationController.swift */; }; 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; }; @@ -258,6 +257,7 @@ 51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */; }; 51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFA2275D85000C787DC /* Array-Extensions.swift */; }; 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */; }; + 51F9F3F723DF6DB200A314FD /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F3F623DF6DB200A314FD /* ArticleIconSchemeHandler.swift */; }; 51FA73A42332BE110090D516 /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; }; 51FA73A52332BE110090D516 /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; }; 51FA73A72332BE880090D516 /* ExtractedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A62332BE880090D516 /* ExtractedArticle.swift */; }; @@ -1265,7 +1265,7 @@ 513C5CEB232571C2003D4054 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; 513C5CED232571C2003D4054 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedInspectorViewController.swift; sourceTree = ""; }; - 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleIconSchemeHandler.swift; sourceTree = ""; }; + 5141E7552374A2890013FF27 /* DetailIconSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailIconSchemeHandler.swift; sourceTree = ""; }; 5142192923522B5500E07E2C /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = ""; }; 514219362352510100E07E2C /* ImageScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageScrollView.swift; sourceTree = ""; }; 5142194A2353C1CF00E07E2C /* main_mac.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main_mac.js; sourceTree = ""; }; @@ -1382,6 +1382,7 @@ 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem-Extensions.swift"; sourceTree = ""; }; 51F85BFA2275D85000C787DC /* Array-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array-Extensions.swift"; sourceTree = ""; }; 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineUILabelSizer.swift; sourceTree = ""; }; + 51F9F3F623DF6DB200A314FD /* ArticleIconSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleIconSchemeHandler.swift; sourceTree = ""; }; 51FA73A32332BE110090D516 /* ArticleExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractor.swift; sourceTree = ""; }; 51FA73A62332BE880090D516 /* ExtractedArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtractedArticle.swift; sourceTree = ""; }; 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = ""; }; @@ -1970,6 +1971,7 @@ 512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */, 51AB8AB223B7F4C6008F147D /* WebViewController.swift */, 517630222336657E00E15FFF /* WebViewProvider.swift */, + 51F9F3F623DF6DB200A314FD /* ArticleIconSchemeHandler.swift */, ); path = Article; sourceTree = ""; @@ -1993,7 +1995,6 @@ 51C452A822650DA100C03939 /* Article Rendering */ = { isa = PBXGroup; children = ( - 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */, 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */, 517630032336215100E15FFF /* main.js */, 49F40DEF2335B71000552BF4 /* newsfoot.js */, @@ -2318,6 +2319,7 @@ 84216D0222128B9D0049B9B9 /* DetailWebViewController.swift */, 84E8E0EA202F693600562D8F /* DetailWebView.swift */, 84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */, + 5141E7552374A2890013FF27 /* DetailIconSchemeHandler.swift */, B528F81D23333C7E00E735DD /* page.html */, 5142194A2353C1CF00E07E2C /* main_mac.js */, 848362FC2262A30800DA1D35 /* styleSheet.css */, @@ -3726,7 +3728,7 @@ 65ED3FE9235DEF6C0081F399 /* FaviconURLFinder.swift in Sources */, 65ED3FEA235DEF6C0081F399 /* SidebarViewController+ContextualMenus.swift in Sources */, 65ED3FEC235DEF6C0081F399 /* RSHTMLMetadata+Extension.swift in Sources */, - 518C3194237B00DA004D740F /* ArticleIconSchemeHandler.swift in Sources */, + 518C3194237B00DA004D740F /* DetailIconSchemeHandler.swift in Sources */, 65ED3FED235DEF6C0081F399 /* SendToMarsEditCommand.swift in Sources */, 65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */, 65ED3FEF235DEF6C0081F399 /* ScriptingObjectContainer.swift in Sources */, @@ -3889,7 +3891,6 @@ 51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */, 51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */, 514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */, - 5141E7562374A2890013FF27 /* ArticleIconSchemeHandler.swift in Sources */, 51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */, 512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */, 51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */, @@ -3919,6 +3920,7 @@ 84CAFCA522BC8C08007694F0 /* FetchRequestQueue.swift in Sources */, 51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */, 51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, + 51F9F3F723DF6DB200A314FD /* ArticleIconSchemeHandler.swift in Sources */, 512AF9C2236ED52C0066F8BE /* ImageHeaderView.swift in Sources */, 51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */, 51C45290226509C100C03939 /* PseudoFeed.swift in Sources */, @@ -4074,7 +4076,7 @@ 848D578E21543519005FFAD5 /* PasteboardWebFeed.swift in Sources */, 5144EA2F2279FAB600D19003 /* AccountsDetailViewController.swift in Sources */, 849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */, - 518C3193237B00D9004D740F /* ArticleIconSchemeHandler.swift in Sources */, + 518C3193237B00D9004D740F /* DetailIconSchemeHandler.swift in Sources */, 84C9FC6722629B9000D921D6 /* AppDelegate.swift in Sources */, 84C9FC7A22629E1200D921D6 /* AccountsTableViewBackgroundView.swift in Sources */, 84CAFCAF22BC8C35007694F0 /* FetchRequestOperation.swift in Sources */, diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index be8f5daef..e19adec0a 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -140,7 +140,7 @@ private extension ArticleRenderer { d["title"] = title d["body"] = body - d["avatars"] = ""; + d["avatars"] = ""; var feedLink = "" if let feedTitle = article.webFeed?.nameForDisplay { diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index a79c70147..a4784955b 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -58,8 +58,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD super.init() appDelegate = self - // Force lazy initialization of the web view provider so that it can warm up the queue of prepared web views - let _ = WebViewProvider.shared AccountManager.shared = AccountManager() NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) diff --git a/iOS/Article/ArticleIconSchemeHandler.swift b/iOS/Article/ArticleIconSchemeHandler.swift new file mode 100644 index 000000000..498986199 --- /dev/null +++ b/iOS/Article/ArticleIconSchemeHandler.swift @@ -0,0 +1,58 @@ +// +// ArticleIconSchemeHandler.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 1/27/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation +import WebKit +import Articles + +class ArticleIconSchemeHandler: NSObject, WKURLSchemeHandler { + + let coordinator: SceneCoordinator + + init(coordinator: SceneCoordinator) { + self.coordinator = coordinator + } + + func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + + guard let url = urlSchemeTask.request.url else { + urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) + return + } + + let articleID = url.absoluteString.stripping(prefix: "\(ArticleRenderer.imageIconScheme)://") + + guard let iconImage = coordinator.articleFor(articleID)?.iconImage() else { + urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) + return + } + + let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) + iconView.iconImage = iconImage + let renderedImage = iconView.asImage() + + guard let data = renderedImage.dataRepresentation() else { + urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) + return + } + + let headerFields = ["Cache-Control": "no-cache"] + if let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: headerFields) { + urlSchemeTask.didReceive(response) + urlSchemeTask.didReceive(data) + urlSchemeTask.didFinish() + } + + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { + urlSchemeTask.didFailWithError(URLError(.unknown)) + } + +} + diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 40038d000..5dcfca2f6 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -83,7 +83,7 @@ class WebViewController: UIViewController { webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasClicked) webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasShown) webView.removeFromSuperview() - WebViewProvider.shared.enqueueWebView(webView) + coordinator.webViewProvider.enqueueWebView(webView) webView = nil } } @@ -96,7 +96,7 @@ class WebViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil) - WebViewProvider.shared.dequeueWebView() { webView in + coordinator.webViewProvider.dequeueWebView() { webView in // Add the webview self.webView = webView @@ -486,7 +486,6 @@ private extension WebViewController { restoreWindowScrollY = 0 - WebViewProvider.shared.articleIconSchemeHandler.currentArticle = article webView.scrollView.setZoomScale(1.0, animated: false) webView.evaluateJavaScript(render) diff --git a/iOS/Article/WebViewProvider.swift b/iOS/Article/WebViewProvider.swift index 49068b5e3..c9010baaf 100644 --- a/iOS/Article/WebViewProvider.swift +++ b/iOS/Article/WebViewProvider.swift @@ -13,9 +13,7 @@ import WebKit /// Keep a queue of WebViews where we've already done a trivial load so that by the time we need them in the UI, they're past the flash-to-shite part of their lifecycle. class WebViewProvider: NSObject, WKNavigationDelegate { - static let shared = WebViewProvider() - - let articleIconSchemeHandler = ArticleIconSchemeHandler() + let articleIconSchemeHandler: ArticleIconSchemeHandler private let minimumQueueDepth = 3 private let maximumQueueDepth = 6 @@ -24,6 +22,12 @@ class WebViewProvider: NSObject, WKNavigationDelegate { private var waitingForFirstLoad = true private var waitingCompletionHandler: ((WKWebView) -> ())? + init(coordinator: SceneCoordinator) { + articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator) + super.init() + replenishQueueIfNeeded() + } + func dequeueWebView(completion: @escaping (WKWebView) -> ()) { if waitingForFirstLoad { waitingCompletionHandler = completion @@ -58,11 +62,6 @@ class WebViewProvider: NSObject, WKNavigationDelegate { // MARK: Private - private override init() { - super.init() - replenishQueueIfNeeded() - } - private func replenishQueueIfNeeded() { while queue.count < minimumQueueDepth { let preferences = WKPreferences() diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index a630ce626..2510c8974 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -30,6 +30,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return rootSplitViewController.undoManager } + lazy var webViewProvider = WebViewProvider(coordinator: self) + private var panelMode: PanelMode = .unset private var activityManager = ActivityManager() @@ -255,9 +257,19 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { private(set) var articles = ArticleArray() { didSet { timelineMiddleIndexPath = nil + articleDictionaryNeedsUpdate = true } } - + + private var articleDictionaryNeedsUpdate = true + private var _idToArticleDictionary = [String: Article]() + private var idToAticleDictionary: [String: Article] { + if articleDictionaryNeedsUpdate { + rebuildArticleDictionaries() + } + return _idToArticleDictionary + } + private var currentArticleRow: Int? { guard let article = currentArticle else { return nil } return articles.firstIndex(of: article) @@ -572,6 +584,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return shadowTable[section] } + func articleFor(_ articleID: String) -> Article? { + return idToAticleDictionary[articleID] + } + func cappedIndexPath(_ indexPath: IndexPath) -> IndexPath { guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else { return IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1) @@ -1222,6 +1238,17 @@ private extension SceneCoordinator { unreadCount = count } + func rebuildArticleDictionaries() { + var idDictionary = [String: Article]() + + articles.forEach { article in + idDictionary[article.articleID] = article + } + + _idToArticleDictionary = idDictionary + articleDictionaryNeedsUpdate = false + } + func rebuildBackingStores(initialLoad: Bool = false, updateExpandedNodes: (() -> Void)? = nil) { if !animatingChanges && !BatchUpdate.shared.isPerforming { treeController.rebuild()