From 2a39ada5abcb0fcace17d87e4e18f52968cba611 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 02:41:33 -0500 Subject: [PATCH 01/25] Prevent label from overflowing into below cell when using editing controls --- .../Cell/MasterFeedTableViewCell.swift | 1 + .../Cell/MasterFeedTableViewCellLayout.swift | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift index 1e1427f95..9e1b7523b 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift @@ -87,6 +87,7 @@ class MasterFeedTableViewCell : VibrantTableViewCell { label.numberOfLines = 0 label.allowsDefaultTighteningForTruncation = false label.adjustsFontForContentSizeCategory = true + label.lineBreakMode = .byTruncatingTail label.font = .preferredFont(forTextStyle: .body) return label }() diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift index c4f83bf86..13cdc57c6 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift @@ -37,10 +37,6 @@ struct MasterFeedTableViewCellLayout { if indent { initialIndent += MasterFeedTableViewCellLayout.disclosureButtonSize.width } - if showingEditingControl { - initialIndent += MasterFeedTableViewCellLayout.editingControlIndent - } - let bounds = CGRect(x: initialIndent, y: 0.0, width: floor(cellWidth - initialIndent - insets.right), height: 0.0) // Disclosure Button @@ -68,18 +64,12 @@ struct MasterFeedTableViewCellLayout { if !unreadCountIsHidden { rUnread.size = unreadCountSize rUnread.origin.x = bounds.maxX - (MasterFeedTableViewCellLayout.unreadCountMarginRight + unreadCountSize.width) - if showingEditingControl { - rUnread.origin.x = rUnread.origin.x - MasterFeedTableViewCellLayout.editingControlIndent - } } // Title - - var rLabelx = CGFloat.zero - if shouldShowDisclosure { - rLabelx = bounds.minX + MasterFeedTableViewCellLayout.disclosureButtonSize.width - } else { - rLabelx = rFavicon.maxX + MasterFeedTableViewCellLayout.imageMarginRight + var rLabelx = insets.left + MasterFeedTableViewCellLayout.disclosureButtonSize.width + if !shouldShowDisclosure { + rLabelx = rLabelx + MasterFeedTableViewCellLayout.imageSize.width + MasterFeedTableViewCellLayout.imageMarginRight } let rLabely = UIFontMetrics.default.scaledValue(for: MasterFeedTableViewCellLayout.verticalPadding) @@ -91,7 +81,23 @@ struct MasterFeedTableViewCellLayout { } let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth))) - let rLabel = CGRect(x: rLabelx, y: rLabely, width: labelSizeInfo.size.width, height: labelSizeInfo.size.height) + + // Now that we've got everything (especially the label) computed without the editing controls, update for them. + // We do this because we don't want the row height to change when the editing controls are brought out. We will + // handle the missing space, but removing it from the label and truncating. + if showingEditingControl { + rDisclosure.origin.x += MasterFeedTableViewCellLayout.editingControlIndent + rFavicon.origin.x += MasterFeedTableViewCellLayout.editingControlIndent + rLabelx += MasterFeedTableViewCellLayout.editingControlIndent + if !unreadCountIsHidden { + rUnread.origin.x -= MasterFeedTableViewCellLayout.editingControlIndent + labelWidth = cellWidth - (rLabelx + MasterFeedTableViewCellLayout.labelMarginRight + (cellWidth - rUnread.minX)) + } else { + labelWidth = cellWidth - (rLabelx + MasterFeedTableViewCellLayout.labelMarginRight + MasterFeedTableViewCellLayout.editingControlIndent) + } + } + + let 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) From 42f433202344133df05e773cb407fbfa784ffdef Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 04:04:13 -0500 Subject: [PATCH 02/25] Fix crash that can happen if updateUI is called before the UI is fully setup --- iOS/MasterFeed/MasterFeedViewController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index e6f0723ff..20f92c253 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -14,7 +14,7 @@ import RSTree class MasterFeedViewController: UITableViewController, UndoableCommandRunner { - private var refreshProgressView: RefreshProgressView! + private var refreshProgressView: RefreshProgressView? private var addNewItemButton: UIBarButtonItem! private lazy var dataSource = makeDataSource() @@ -572,8 +572,8 @@ private extension MasterFeedViewController { } func updateUI() { - refreshProgressView.updateRefreshLabel() - addNewItemButton.isEnabled = !AccountManager.shared.activeAccounts.isEmpty + refreshProgressView?.updateRefreshLabel() + addNewItemButton?.isEnabled = !AccountManager.shared.activeAccounts.isEmpty } func reloadNode(_ node: Node) { From 9de38b00b91e5c5eb99c8dd20a15388119df7a92 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 04:52:49 -0500 Subject: [PATCH 03/25] Reset scrollview zoom level when reloading html. Issue #1216 --- iOS/Article/ArticleViewController.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index a479e693f..b74cc65c6 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -118,8 +118,6 @@ class ArticleViewController: UIViewController { // to work around this bug: http://www.openradar.me/22855188 let url = Bundle.main.url(forResource: "page", withExtension: "html")! webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) -// let request = URLRequest(url: url) -// webView.load(request) } @@ -184,6 +182,7 @@ class ArticleViewController: UIViewController { render = "render(\(json));" } + webView?.scrollView.setZoomScale(1.0, animated: false) webView?.evaluateJavaScript(render) } From fe7f6bb8df8af474c1d1fba399a36324e6c01cb9 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 11:06:55 -0500 Subject: [PATCH 04/25] Make image zoom fetch requests cancellable. Issue #1178 --- iOS/Article/ArticleViewController.swift | 5 ++-- iOS/Resources/main_ios.js | 37 +++++++++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index b74cc65c6..36d43bf72 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -85,6 +85,9 @@ class ArticleViewController: UIViewController { deinit { if webView != nil { + webView?.evaluateJavaScript("cancelImageLoad();") + webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasClicked) + webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasShown) webView.removeFromSuperview() ArticleViewControllerWebViewProvider.shared.enqueueWebView(webView) webView = nil @@ -109,8 +112,6 @@ class ArticleViewController: UIViewController { webView.navigationDelegate = self webView.uiDelegate = self - webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasClicked) - webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasShown) webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) diff --git a/iOS/Resources/main_ios.js b/iOS/Resources/main_ios.js index 2af2e5a81..7a87e51c1 100644 --- a/iOS/Resources/main_ios.js +++ b/iOS/Resources/main_ios.js @@ -1,23 +1,37 @@ -var imageIsLoading = false; +var controller = new AbortController() + +// Cancel any pending image loads (there might be none) and reset the controller +function cancelImageLoad() { + controller.abort(); + controller = new AbortController(); +} // Used to pop a resizable image view async function imageWasClicked(img) { - img.classList.add("nnwClicked"); + cancelImageLoad(); + showNetworkLoading(img); try { - showNetworkLoading(img); - const response = await fetch(img.src); + + const signal = controller.signal; + const response = await fetch(img.src, { signal: signal }); if (!response.ok) { throw new Error('Network response was not ok.'); } const imgBlob = await response.blob(); + if (signal.aborted) { + throw new Error('Network response was aborted.'); + } + hideNetworkLoading(img); - + var reader = new FileReader(); reader.readAsDataURL(imgBlob); reader.onloadend = function() { + img.classList.add("nnwClicked"); + const rect = img.getBoundingClientRect(); var message = { x: rect.x, @@ -29,8 +43,9 @@ async function imageWasClicked(img) { var jsonMessage = JSON.stringify(message); window.webkit.messageHandlers.imageWasClicked.postMessage(jsonMessage); - + } + } catch (error) { hideNetworkLoading(img); console.log('There has been a problem with your fetch operation: ', error.message); @@ -39,7 +54,6 @@ async function imageWasClicked(img) { } function showNetworkLoading(img) { - imageIsLoading = true; var wrapper = document.createElement("div"); wrapper.classList.add("activityIndicatorWrap"); @@ -64,8 +78,6 @@ function hideNetworkLoading(img) { var wrapperParent = wrapper.parentNode; wrapperParent.insertBefore(img, wrapper); wrapperParent.removeChild(wrapper); - - imageIsLoading = false; } // Used to animate the transition to a fullscreen image @@ -85,7 +97,7 @@ function showClickedImage() { // Add the click listener for images function imageClicks() { window.onclick = function(event) { - if (event.target.matches('img') && !imageIsLoading) { + if (event.target.matches('img')) { imageWasClicked(event.target); } } @@ -101,8 +113,9 @@ function inlineVideos() { } function postRenderProcessing() { - imageClicks() - inlineVideos() + cancelImageLoad(); + imageClicks(); + inlineVideos(); } const activityIndicator = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjAiIHdpZHRoPSI2NHB4IiBoZWlnaHQ9IjY0cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMwMDAwMDAiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDMwIDY0IDY0KSIvPjxwYXRoIGQ9Ik01OS42IDBoOHY0MGgtOFYweiIgZmlsbD0iI2NjY2NjYyIgdHJhbnNmb3JtPSJyb3RhdGUoNjAgNjQgNjQpIi8+PHBhdGggZD0iTTU5LjYgMGg4djQwaC04VjB6IiBmaWxsPSIjY2NjY2NjIiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDEyMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNiMmIyYjIiIHRyYW5zZm9ybT0icm90YXRlKDE1MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM5OTk5OTkiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM3ZjdmN2YiIHRyYW5zZm9ybT0icm90YXRlKDIxMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM2NjY2NjYiIHRyYW5zZm9ybT0icm90YXRlKDI0MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM0YzRjNGMiIHRyYW5zZm9ybT0icm90YXRlKDI3MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMzMzMzMzMiIHRyYW5zZm9ybT0icm90YXRlKDMwMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMxOTE5MTkiIHRyYW5zZm9ybT0icm90YXRlKDMzMCA2NCA2NCkiLz48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgdmFsdWVzPSIwIDY0IDY0OzMwIDY0IDY0OzYwIDY0IDY0OzkwIDY0IDY0OzEyMCA2NCA2NDsxNTAgNjQgNjQ7MTgwIDY0IDY0OzIxMCA2NCA2NDsyNDAgNjQgNjQ7MjcwIDY0IDY0OzMwMCA2NCA2NDszMzAgNjQgNjQiIGNhbGNNb2RlPSJkaXNjcmV0ZSIgZHVyPSIxMDgwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIj48L2FuaW1hdGVUcmFuc2Zvcm0+PC9nPjwvc3ZnPg=="; From 64446ec6093eede21866ce31cd2afca4f31d682c Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 11:51:01 -0500 Subject: [PATCH 05/25] Rename database to feeds in settings. Issue #1187 --- iOS/Settings/Settings.storyboard | 176 +++++++++++----------- iOS/Settings/SettingsViewController.swift | 10 +- 2 files changed, 93 insertions(+), 93 deletions(-) diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index c8497b2e5..a49be81f1 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -57,10 +57,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -90,7 +169,7 @@ - + @@ -120,7 +199,7 @@ - + @@ -151,72 +230,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -233,7 +250,7 @@ - + @@ -250,7 +267,7 @@ - + @@ -267,7 +284,7 @@ - + @@ -284,7 +301,7 @@ - + @@ -301,7 +318,7 @@ - + @@ -317,23 +334,6 @@ - - - - - - - - - - - diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index cab947682..cff7c1d82 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -81,7 +81,7 @@ class SettingsViewController: UITableViewController { switch section { case 1: return AccountManager.shared.accounts.count + 1 - case 4: + case 2: let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section) if AccountManager.shared.activeAccounts.isEmpty || AccountManager.shared.anyAccountHasFeedWithURL(appNewsURLString) { return defaultNumberOfRows - 1 @@ -135,7 +135,7 @@ class SettingsViewController: UITableViewController { controller.account = sortedAccounts[indexPath.row] self.navigationController?.pushViewController(controller, animated: true) } - case 3: + case 2: switch indexPath.row { case 0: let timeline = UIStoryboard.settings.instantiateController(ofType: RefreshIntervalViewController.self) @@ -152,6 +152,9 @@ class SettingsViewController: UITableViewController { let sourceRect = tableView.rectForRow(at: indexPath) exportOPML(sourceView: sourceView, sourceRect: sourceRect) } + case 3: + addFeed() + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) default: break } @@ -175,9 +178,6 @@ class SettingsViewController: UITableViewController { case 5: openURL("https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes") tableView.selectRow(at: nil, animated: true, scrollPosition: .none) - case 6: - addFeed() - tableView.selectRow(at: nil, animated: true, scrollPosition: .none) default: break } From 2962ccc24b39967b2a623fb889f3289939ca46fa Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 12:04:39 -0500 Subject: [PATCH 06/25] Added error handling for OPML import. --- iOS/Settings/SettingsViewController.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index cff7c1d82..ce66d6a56 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -264,7 +264,15 @@ extension SettingsViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { for url in urls { - opmlAccount?.importOPML(url) { result in} + opmlAccount?.importOPML(url) { result in + switch result { + case .success: + break + case .failure(let error): + let title = NSLocalizedString("Import Failed", comment: "Import Failed") + self.presentError(title: title, message: error.localizedDescription) + } + } } } From 58e30dc682c3f830644bde3abbc3792c70d263ef Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 12:07:49 -0500 Subject: [PATCH 07/25] Add titles to add sheet. Issue #1197 --- iOS/Add/AddContainerViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iOS/Add/AddContainerViewController.swift b/iOS/Add/AddContainerViewController.swift index 108a45565..262b348e8 100644 --- a/iOS/Add/AddContainerViewController.swift +++ b/iOS/Add/AddContainerViewController.swift @@ -109,6 +109,7 @@ private extension AddContainerViewController { return } + navigationItem.title = NSLocalizedString("Add Feed", comment: "Add Feed") resetUI() hideCurrentController() @@ -126,6 +127,7 @@ private extension AddContainerViewController { return } + navigationItem.title = NSLocalizedString("Add Folder", comment: "Add Folder") resetUI() hideCurrentController() displayContentController(UIStoryboard.add.instantiateController(ofType: AddFolderViewController.self)) From 2d70a6c1a53406ef64566acdf5c0af25ef8629e6 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 13:51:32 -0500 Subject: [PATCH 08/25] Prevent insertion point bug. Issue ##1204 --- iOS/Add/AddFeedViewController.swift | 1 - iOS/Add/AddFolderViewController.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/iOS/Add/AddFeedViewController.swift b/iOS/Add/AddFeedViewController.swift index 27c3d2e6c..49b1061db 100644 --- a/iOS/Add/AddFeedViewController.swift +++ b/iOS/Add/AddFeedViewController.swift @@ -44,7 +44,6 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh urlTextField.autocapitalizationType = .none urlTextField.text = initialFeed urlTextField.delegate = self - urlTextField.becomeFirstResponder() if initialFeed != nil { delegate?.readyToAdd(state: true) diff --git a/iOS/Add/AddFolderViewController.swift b/iOS/Add/AddFolderViewController.swift index 61f0f01f9..be70a3d9d 100644 --- a/iOS/Add/AddFolderViewController.swift +++ b/iOS/Add/AddFolderViewController.swift @@ -31,7 +31,6 @@ class AddFolderViewController: UITableViewController, AddContainerViewController accounts = AccountManager.shared.sortedActiveAccounts nameTextField.delegate = self - nameTextField.becomeFirstResponder() accountLabel.text = (accounts[0] as DisplayNameProvider).nameForDisplay From 8bbabbacdbdf3d43b918b24191f85a1b0802c740 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 14:38:39 -0500 Subject: [PATCH 09/25] Don't execute 3 panel mode changes in an animation block --- iOS/RootSplitViewController.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index cf7040c42..e4e7920ae 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -15,9 +15,7 @@ class RootSplitViewController: UISplitViewController { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - coordinator.animate(alongsideTransition: { [weak self] context in - self?.coordinator.configureThreePanelMode(for: size) - }) + self.coordinator.configureThreePanelMode(for: size) } // MARK: Keyboard Shortcuts From 8d85d01da4c8dbe3d741b4b353b683dad7b25118 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 15:35:45 -0500 Subject: [PATCH 10/25] Persist feed name changes correctly. --- iOS/Inspector/FeedInspectorView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iOS/Inspector/FeedInspectorView.swift b/iOS/Inspector/FeedInspectorView.swift index 363103263..1e66ed5d8 100644 --- a/iOS/Inspector/FeedInspectorView.swift +++ b/iOS/Inspector/FeedInspectorView.swift @@ -69,7 +69,6 @@ struct FeedInspectorView : View { .onDisappear { self.viewModel.save() } .navigationBarTitle(Text(verbatim: self.viewModel.nameForDisplay), displayMode: .inline) .navigationBarItems(leading: Button(action: { - self.viewModel.save() self.viewController?.dismiss(animated: true) }) { Text("Done") } ) } @@ -137,7 +136,8 @@ struct FeedInspectorView : View { func save() { if name != nameForDisplay { - feed.editedName = name.isEmpty ? nil : name + let newName = name.isEmpty ? (feed.name ?? NSLocalizedString("Untitled", comment: "Feed name")) : name + feed.rename(to: newName) { _ in } } } From 30da68218f44bd681ea4d54971100cedad4b0b84 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 15:46:20 -0500 Subject: [PATCH 11/25] Don't incorrectly assign an avatar background when there isn't an image --- iOS/AvatarView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/AvatarView.swift b/iOS/AvatarView.swift index 265ee8042..1e0cc956d 100644 --- a/iOS/AvatarView.swift +++ b/iOS/AvatarView.swift @@ -74,7 +74,7 @@ final class AvatarView: UIView { override func layoutSubviews() { imageView.setFrameIfNotEqual(rectForImageView()) - if (isVerticalBackgroundExposed && !isSymbolImage) || !isDisconcernable { + if (image != nil && isVerticalBackgroundExposed && !isSymbolImage) || !isDisconcernable { backgroundColor = AppAssets.avatarBackgroundColor } else { backgroundColor = nil From 51acc5972f04c378885b496193f82ef67fca2fec Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 16:08:03 -0500 Subject: [PATCH 12/25] Prevent the search bar from appearing on rotation. Issue #1171 --- iOS/MasterTimeline/MasterTimelineViewController.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 03b8d7ba0..61103ed05 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -57,7 +57,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner NSLocalizedString("Here", comment: "Here"), NSLocalizedString("All Articles", comment: "All Articles") ] - navigationItem.searchController = searchController definesPresentationContext = true // Configure the table @@ -74,6 +73,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner super.viewWillAppear(animated) } + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + // You have to assign the search controller here to avoid showing it by default + // https://stackoverflow.com/questions/57581557/how-to-initally-hide-searchbar-in-navigation-controller-on-ios-13 + navigationItem.searchController = searchController + } + // MARK: Actions @IBAction func markAllAsRead(_ sender: Any) { From 326b2886679a77e6fac39ed554d86204db2de515 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 18:02:57 -0500 Subject: [PATCH 13/25] Fix background transition bug for 3 column mode --- iOS/RootSplitViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index e4e7920ae..90246cc40 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -14,8 +14,8 @@ class RootSplitViewController: UISplitViewController { var coordinator: SceneCoordinator! override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) self.coordinator.configureThreePanelMode(for: size) + super.viewWillTransition(to: size, with: coordinator) } // MARK: Keyboard Shortcuts From 165a2863276600178e495a4a729c0821cd4bc185 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 19:47:28 -0500 Subject: [PATCH 14/25] Added some padding to the footers in settings. --- iOS/Settings/AboutViewController.swift | 5 ++++- iOS/Settings/SettingsViewController.swift | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/iOS/Settings/AboutViewController.swift b/iOS/Settings/AboutViewController.swift index a472efd63..59fc79207 100644 --- a/iOS/Settings/AboutViewController.swift +++ b/iOS/Settings/AboutViewController.swift @@ -33,8 +33,11 @@ class AboutViewController: UITableViewController { buildLabel.numberOfLines = 0 buildLabel.sizeToFit() buildLabel.translatesAutoresizingMaskIntoConstraints = false - tableView.tableFooterView = buildLabel + let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0)) + wrapperView.translatesAutoresizingMaskIntoConstraints = false + wrapperView.addSubview(buildLabel) + tableView.tableFooterView = wrapperView } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index ce66d6a56..b7c926536 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -66,7 +66,11 @@ class SettingsViewController: UITableViewController { buildLabel.text = "\(Bundle.main.appName) v \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))" buildLabel.sizeToFit() buildLabel.translatesAutoresizingMaskIntoConstraints = false - tableView.tableFooterView = buildLabel + + let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0)) + wrapperView.translatesAutoresizingMaskIntoConstraints = false + wrapperView.addSubview(buildLabel) + tableView.tableFooterView = wrapperView } From 280f754217772ba1ce6edfb06e6781d228f91cc7 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 30 Oct 2019 20:38:57 -0500 Subject: [PATCH 15/25] Reload nodes who's unread counts change as that could change the cell layout --- iOS/MasterFeed/MasterFeedViewController.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 20f92c253..d7232e61c 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -99,10 +99,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject) } - if let node = node, let indexPath = dataSource.indexPath(for: node), let unreadCountProvider = node.representedObject as? UnreadCountProvider { - if let cell = tableView.cellForRow(at: indexPath) as? MasterFeedTableViewCell { - cell.unreadCount = unreadCountProvider.unreadCount - } + if let node = node, dataSource.indexPath(for: node) != nil { + reloadNode(node) } } From 34d0142dbc55dad50c6383c6d48b25608f26e2db Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 31 Oct 2019 11:21:08 -0500 Subject: [PATCH 16/25] Don't transition to three panel mode when just getting screenshots for the background --- iOS/RootSplitViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index 90246cc40..48bf64b9f 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -14,7 +14,9 @@ class RootSplitViewController: UISplitViewController { var coordinator: SceneCoordinator! override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - self.coordinator.configureThreePanelMode(for: size) + if UIApplication.shared.applicationState != .background { + self.coordinator.configureThreePanelMode(for: size) + } super.viewWillTransition(to: size, with: coordinator) } From 1e7b71a48207ce7d6ee94f6074b7d43e860aab55 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 31 Oct 2019 12:22:37 -0500 Subject: [PATCH 17/25] Use correct queues for user interface elements --- Shared/Extensions/RSImage-Extensions.swift | 2 +- iOS/AvatarView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Shared/Extensions/RSImage-Extensions.swift b/Shared/Extensions/RSImage-Extensions.swift index 337f09fa6..bc2edd616 100644 --- a/Shared/Extensions/RSImage-Extensions.swift +++ b/Shared/Extensions/RSImage-Extensions.swift @@ -14,7 +14,7 @@ extension RSImage { static let avatarSize = 48 static func scaledForAvatar(_ data: Data, imageResultBlock: @escaping (RSImage?) -> Void) { - DispatchQueue.global().async { + DispatchQueue.global(qos: .userInteractive).async { let image = RSImage.scaledForAvatar(data) DispatchQueue.main.async { imageResultBlock(image) diff --git a/iOS/AvatarView.swift b/iOS/AvatarView.swift index 1e0cc956d..135fca250 100644 --- a/iOS/AvatarView.swift +++ b/iOS/AvatarView.swift @@ -16,7 +16,7 @@ final class AvatarView: UIView { imageView.image = image if self.traitCollection.userInterfaceStyle == .dark { - DispatchQueue.global(qos: .background).async { + DispatchQueue.global(qos: .userInteractive).async { if self.image?.isDark() ?? false { DispatchQueue.main.async { self.isDisconcernable = false From 5bcb5a982f697d85d12d08448f0f9069a3397f2d Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 31 Oct 2019 13:38:38 -0500 Subject: [PATCH 18/25] Cache home pages with no icon between launches --- Shared/Images/FeedIconDownloader.swift | 53 ++++++++++++++++++++++++-- iOS/AppDelegate.swift | 5 ++- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/Shared/Images/FeedIconDownloader.swift b/Shared/Images/FeedIconDownloader.swift index 2d924e584..e1edec0e3 100644 --- a/Shared/Images/FeedIconDownloader.swift +++ b/Shared/Images/FeedIconDownloader.swift @@ -32,7 +32,14 @@ public final class FeedIconDownloader { } } - private var homePagesWithNoIconURL = Set() + private var homePagesWithNoIconURLCache = Set() + private var homePagesWithNoIconURLCachePath: String + private var homePagesWithNoIconURLCacheDirty = false { + didSet { + queueHomePagesWithNoIconURLCacheIfNeeded() + } + } + private var urlsInProgress = Set() private var cache = [Feed: RSImage]() private var waitingForFeedURLs = [String: Feed]() @@ -40,7 +47,9 @@ public final class FeedIconDownloader { init(imageDownloader: ImageDownloader, folder: String) { self.imageDownloader = imageDownloader self.homePageToIconURLCachePath = (folder as NSString).appendingPathComponent("HomePageToIconURLCache.plist") + self.homePagesWithNoIconURLCachePath = (folder as NSString).appendingPathComponent("HomePagesWithNoIconURLCache.plist") loadHomePageToIconURLCache() + loadHomePagesWithNoIconURLCache() NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: imageDownloader) } @@ -99,13 +108,19 @@ public final class FeedIconDownloader { } } + @objc func saveHomePagesWithNoIconURLCacheIfNeeded() { + if homePagesWithNoIconURLCacheDirty { + saveHomePagesWithNoIconURLCache() + } + } + } private extension FeedIconDownloader { func icon(forHomePageURL homePageURL: String, feed: Feed, _ imageResultBlock: @escaping (RSImage?) -> Void) { - if homePagesWithNoIconURL.contains(homePageURL) { + if homePagesWithNoIconURLCache.contains(homePageURL) { imageResultBlock(nil) return } @@ -141,7 +156,8 @@ private extension FeedIconDownloader { } func cacheIconURL(for homePageURL: String, _ iconURL: String) { - homePagesWithNoIconURL.remove(homePageURL) + homePagesWithNoIconURLCache.remove(homePageURL) + homePagesWithNoIconURLCacheDirty = true homePageToIconURLCache[homePageURL] = iconURL homePageToIconURLCacheDirty = true } @@ -172,7 +188,8 @@ private extension FeedIconDownloader { return } - homePagesWithNoIconURL.insert(homePageURL) + homePagesWithNoIconURLCache.insert(homePageURL) + homePagesWithNoIconURLCacheDirty = true } func loadHomePageToIconURLCache() { @@ -184,10 +201,24 @@ private extension FeedIconDownloader { homePageToIconURLCache = (try? decoder.decode([String: String].self, from: data)) ?? [String: String]() } + func loadHomePagesWithNoIconURLCache() { + let url = URL(fileURLWithPath: homePagesWithNoIconURLCachePath) + guard let data = try? Data(contentsOf: url) else { + return + } + let decoder = PropertyListDecoder() + let decoded = (try? decoder.decode([String].self, from: data)) ?? [String]() + homePagesWithNoIconURLCache = Set(decoded) + } + func queueSaveHomePageToIconURLCacheIfNeeded() { FeedIconDownloader.saveQueue.add(self, #selector(saveHomePageToIconURLCacheIfNeeded)) } + func queueHomePagesWithNoIconURLCacheIfNeeded() { + FeedIconDownloader.saveQueue.add(self, #selector(saveHomePagesWithNoIconURLCacheIfNeeded)) + } + func saveHomePageToIconURLCache() { homePageToIconURLCacheDirty = false @@ -202,4 +233,18 @@ private extension FeedIconDownloader { } } + func saveHomePagesWithNoIconURLCache() { + homePagesWithNoIconURLCacheDirty = false + + let encoder = PropertyListEncoder() + encoder.outputFormat = .binary + let url = URL(fileURLWithPath: homePagesWithNoIconURLCachePath) + do { + let data = try encoder.encode(Array(homePagesWithNoIconURLCache)) + try data.write(to: url) + } catch { + assertionFailure(error.localizedDescription) + } + } + } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index f0fe5936f..17a1ef1bd 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -200,12 +200,13 @@ private extension AppDelegate { let faviconsFolderURL = tempDir.appendingPathComponent("Favicons") let imagesFolderURL = tempDir.appendingPathComponent("Images") let homePageToIconURL = tempDir.appendingPathComponent("HomePageToIconURLCache.plist") - + let homePagesWithNoIconURL = tempDir.appendingPathComponent("HomePagesWithNoIconURLCache.plist") + // If the image disk cache hasn't been flushed for 3 days and the network is available, delete it if let flushDate = AppDefaults.lastImageCacheFlushDate, flushDate.addingTimeInterval(3600*24*3) < Date() { if let reachability = try? Reachability(hostname: "apple.com") { if reachability.connection != .unavailable { - for tempItem in [faviconsFolderURL, imagesFolderURL, homePageToIconURL] { + for tempItem in [faviconsFolderURL, imagesFolderURL, homePageToIconURL, homePagesWithNoIconURL] { do { os_log(.info, log: self.log, "Removing cache file: %@", tempItem.absoluteString) try FileManager.default.removeItem(at: tempItem) From 8ba15c62349e93cac88a1958022b2572411966d1 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 31 Oct 2019 14:04:34 -0500 Subject: [PATCH 19/25] Cache favicon to homepage mappings --- Shared/Favicons/FaviconDownloader.swift | 98 ++++++++++++++++++++++++- iOS/AppDelegate.swift | 6 +- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/Shared/Favicons/FaviconDownloader.swift b/Shared/Favicons/FaviconDownloader.swift index 0cedd923c..8605c29cd 100644 --- a/Shared/Favicons/FaviconDownloader.swift +++ b/Shared/Favicons/FaviconDownloader.swift @@ -18,11 +18,28 @@ extension Notification.Name { final class FaviconDownloader { + private static let saveQueue = CoalescingQueue(name: "Cache Save Queue", interval: 1.0) + private let folder: String private let diskCache: BinaryDiskCache private var singleFaviconDownloaderCache = [String: SingleFaviconDownloader]() // faviconURL: SingleFaviconDownloader + private var homePageToFaviconURLCache = [String: String]() //homePageURL: faviconURL - private var homePageURLsWithNoFaviconURL = Set() + private var homePageToFaviconURLCachePath: String + private var homePageToFaviconURLCacheDirty = false { + didSet { + queueSaveHomePageToFaviconURLCacheIfNeeded() + } + } + + private var homePageURLsWithNoFaviconURLCache = Set() + private var homePageURLsWithNoFaviconURLCachePath: String + private var homePageURLsWithNoFaviconURLCacheDirty = false { + didSet { + queueSaveHomePageURLsWithNoFaviconURLCacheIfNeeded() + } + } + private let queue: DispatchQueue private var cache = [Feed: RSImage]() // faviconURL: RSImage @@ -36,6 +53,11 @@ final class FaviconDownloader { self.diskCache = BinaryDiskCache(folder: folder) self.queue = DispatchQueue(label: "FaviconDownloader serial queue - \(folder)") + self.homePageToFaviconURLCachePath = (folder as NSString).appendingPathComponent("HomePageToFaviconURLCache.plist") + self.homePageURLsWithNoFaviconURLCachePath = (folder as NSString).appendingPathComponent("HomePageURLsWithNoFaviconURLCache.plist") + loadHomePageToFaviconURLCache() + loadHomePageURLsWithNoFaviconURLCache() + NotificationCenter.default.addObserver(self, selector: #selector(didLoadFavicon(_:)), name: .DidLoadFavicon, object: nil) } @@ -92,7 +114,7 @@ final class FaviconDownloader { func favicon(withHomePageURL homePageURL: String) -> RSImage? { let url = homePageURL.rs_normalizedURL() - if homePageURLsWithNoFaviconURL.contains(url) { + if homePageURLsWithNoFaviconURLCache.contains(url) { return nil } @@ -103,10 +125,12 @@ final class FaviconDownloader { findFaviconURL(with: url) { (faviconURL) in if let faviconURL = faviconURL { self.homePageToFaviconURLCache[url] = faviconURL + self.homePageToFaviconURLCacheDirty = true let _ = self.favicon(with: faviconURL) } else { - self.homePageURLsWithNoFaviconURL.insert(url) + self.homePageURLsWithNoFaviconURLCache.insert(url) + self.homePageURLsWithNoFaviconURLCacheDirty = true } } @@ -126,6 +150,18 @@ final class FaviconDownloader { postFaviconDidBecomeAvailableNotification(singleFaviconDownloader.faviconURL) } + + @objc func saveHomePageToFaviconURLCacheIfNeeded() { + if homePageToFaviconURLCacheDirty { + saveHomePageToFaviconURLCache() + } + } + + @objc func saveHomePageURLsWithNoFaviconURLCacheIfNeeded() { + if homePageURLsWithNoFaviconURLCacheDirty { + saveHomePageURLsWithNoFaviconURLCache() + } + } } private extension FaviconDownloader { @@ -175,4 +211,60 @@ private extension FaviconDownloader { NotificationCenter.default.post(name: .FaviconDidBecomeAvailable, object: self, userInfo: userInfo) } } + + func loadHomePageToFaviconURLCache() { + let url = URL(fileURLWithPath: homePageToFaviconURLCachePath) + guard let data = try? Data(contentsOf: url) else { + return + } + let decoder = PropertyListDecoder() + homePageToFaviconURLCache = (try? decoder.decode([String: String].self, from: data)) ?? [String: String]() + } + + func loadHomePageURLsWithNoFaviconURLCache() { + let url = URL(fileURLWithPath: homePageURLsWithNoFaviconURLCachePath) + guard let data = try? Data(contentsOf: url) else { + return + } + let decoder = PropertyListDecoder() + let decoded = (try? decoder.decode([String].self, from: data)) ?? [String]() + homePageURLsWithNoFaviconURLCache = Set(decoded) + } + + func queueSaveHomePageToFaviconURLCacheIfNeeded() { + FaviconDownloader.saveQueue.add(self, #selector(saveHomePageToFaviconURLCacheIfNeeded)) + } + + func queueSaveHomePageURLsWithNoFaviconURLCacheIfNeeded() { + FaviconDownloader.saveQueue.add(self, #selector(saveHomePageURLsWithNoFaviconURLCacheIfNeeded)) + } + + func saveHomePageToFaviconURLCache() { + homePageToFaviconURLCacheDirty = false + + let encoder = PropertyListEncoder() + encoder.outputFormat = .binary + let url = URL(fileURLWithPath: homePageToFaviconURLCachePath) + do { + let data = try encoder.encode(homePageToFaviconURLCache) + try data.write(to: url) + } catch { + assertionFailure(error.localizedDescription) + } + } + + func saveHomePageURLsWithNoFaviconURLCache() { + homePageURLsWithNoFaviconURLCacheDirty = false + + let encoder = PropertyListEncoder() + encoder.outputFormat = .binary + let url = URL(fileURLWithPath: homePageURLsWithNoFaviconURLCachePath) + do { + let data = try encoder.encode(Array(homePageURLsWithNoFaviconURLCache)) + try data.write(to: url) + } catch { + assertionFailure(error.localizedDescription) + } + } + } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 17a1ef1bd..6d2e2f292 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -201,12 +201,14 @@ private extension AppDelegate { let imagesFolderURL = tempDir.appendingPathComponent("Images") let homePageToIconURL = tempDir.appendingPathComponent("HomePageToIconURLCache.plist") let homePagesWithNoIconURL = tempDir.appendingPathComponent("HomePagesWithNoIconURLCache.plist") - + let homePageToFaviconURL = tempDir.appendingPathComponent("HomePageToFaviconURLCache.plist") + let homePageURLsWithNoFaviconURL = tempDir.appendingPathComponent("HomePageURLsWithNoFaviconURLCache.plist") + // If the image disk cache hasn't been flushed for 3 days and the network is available, delete it if let flushDate = AppDefaults.lastImageCacheFlushDate, flushDate.addingTimeInterval(3600*24*3) < Date() { if let reachability = try? Reachability(hostname: "apple.com") { if reachability.connection != .unavailable { - for tempItem in [faviconsFolderURL, imagesFolderURL, homePageToIconURL, homePagesWithNoIconURL] { + for tempItem in [faviconsFolderURL, imagesFolderURL, homePageToIconURL, homePagesWithNoIconURL, homePageToFaviconURL, homePageURLsWithNoFaviconURL] { do { os_log(.info, log: self.log, "Removing cache file: %@", tempItem.absoluteString) try FileManager.default.removeItem(at: tempItem) From 8eb99b01c3dcb1119f72222d0b18acd939a452c3 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 31 Oct 2019 14:25:09 -0500 Subject: [PATCH 20/25] Increase default icon brightness --- Shared/Favicons/ColorHash.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Shared/Favicons/ColorHash.swift b/Shared/Favicons/ColorHash.swift index b275d0c59..5cd55d05a 100644 --- a/Shared/Favicons/ColorHash.swift +++ b/Shared/Favicons/ColorHash.swift @@ -18,7 +18,9 @@ import AppKit public class ColorHash { - public static let defaultLS = [CGFloat(0.45), CGFloat(0.6), CGFloat(0.75)] + public static let defaultSaturation = [CGFloat(0.35), CGFloat(0.5), CGFloat(0.65)] + public static let defaultBrightness = [CGFloat(0.5), CGFloat(0.65), CGFloat(0.80)] + let seed = CGFloat(131.0) let seed2 = CGFloat(137.0) let maxSafeInteger = 9007199254740991.0 / CGFloat(137.0) @@ -28,7 +30,7 @@ public class ColorHash { public private(set) var brightness: [CGFloat] public private(set) var saturation: [CGFloat] - public init(_ str: String, _ saturation: [CGFloat] = defaultLS, _ brightness: [CGFloat] = defaultLS) { + public init(_ str: String, _ saturation: [CGFloat] = defaultSaturation, _ brightness: [CGFloat] = defaultBrightness) { self.str = str self.saturation = saturation self.brightness = brightness From 0f5210d92f1e7299958e674c0f4a69f8bd50a1f5 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 31 Oct 2019 14:39:35 -0500 Subject: [PATCH 21/25] Change image processing queues to default quality of service --- Shared/Extensions/RSImage-Extensions.swift | 2 +- iOS/AvatarView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Shared/Extensions/RSImage-Extensions.swift b/Shared/Extensions/RSImage-Extensions.swift index bc2edd616..985fb7d36 100644 --- a/Shared/Extensions/RSImage-Extensions.swift +++ b/Shared/Extensions/RSImage-Extensions.swift @@ -14,7 +14,7 @@ extension RSImage { static let avatarSize = 48 static func scaledForAvatar(_ data: Data, imageResultBlock: @escaping (RSImage?) -> Void) { - DispatchQueue.global(qos: .userInteractive).async { + DispatchQueue.global(qos: .default).async { let image = RSImage.scaledForAvatar(data) DispatchQueue.main.async { imageResultBlock(image) diff --git a/iOS/AvatarView.swift b/iOS/AvatarView.swift index 135fca250..3bce3e986 100644 --- a/iOS/AvatarView.swift +++ b/iOS/AvatarView.swift @@ -16,7 +16,7 @@ final class AvatarView: UIView { imageView.image = image if self.traitCollection.userInterfaceStyle == .dark { - DispatchQueue.global(qos: .userInteractive).async { + DispatchQueue.global(qos: .default).async { if self.image?.isDark() ?? false { DispatchQueue.main.async { self.isDisconcernable = false From 3dd533ed0de1fa89098218c0d728ac4fb0f6b494 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 31 Oct 2019 14:44:03 -0500 Subject: [PATCH 22/25] Move dark image detection back to background queue --- iOS/AvatarView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/AvatarView.swift b/iOS/AvatarView.swift index 3bce3e986..1e0cc956d 100644 --- a/iOS/AvatarView.swift +++ b/iOS/AvatarView.swift @@ -16,7 +16,7 @@ final class AvatarView: UIView { imageView.image = image if self.traitCollection.userInterfaceStyle == .dark { - DispatchQueue.global(qos: .default).async { + DispatchQueue.global(qos: .background).async { if self.image?.isDark() ?? false { DispatchQueue.main.async { self.isDisconcernable = false From 0c32e8de1433366bf1740468ea64e38546fafc6c Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 31 Oct 2019 15:25:45 -0500 Subject: [PATCH 23/25] Don't try to scale the favicon for the master feed list. --- iOS/AvatarView.swift | 2 +- iOS/MasterFeed/MasterFeedViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/iOS/AvatarView.swift b/iOS/AvatarView.swift index 1e0cc956d..3bce3e986 100644 --- a/iOS/AvatarView.swift +++ b/iOS/AvatarView.swift @@ -16,7 +16,7 @@ final class AvatarView: UIView { imageView.image = image if self.traitCollection.userInterfaceStyle == .dark { - DispatchQueue.global(qos: .background).async { + DispatchQueue.global(qos: .default).async { if self.image?.isDark() ?? false { DispatchQueue.main.async { self.isDisconcernable = false diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index d7232e61c..7302a40f9 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -654,7 +654,7 @@ private extension MasterFeedViewController { return feedIconImage } - if let faviconImage = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) { + if let faviconImage = appDelegate.faviconDownloader.favicon(for: feed) { return faviconImage } From ebed17ed2f7d2949733fba2dc1e66e2e518154d7 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 31 Oct 2019 19:20:52 -0500 Subject: [PATCH 24/25] Tell iOS to wait while we are processing to allow us to try to finish --- iOS/AppDelegate.swift | 86 ++++++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 6d2e2f292..be4c90b19 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -18,6 +18,7 @@ var appDelegate: AppDelegate! @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider { + private var waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid var syncTimer: ArticleStatusSyncTimer? @@ -130,28 +131,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func prepareAccountsForBackground() { syncTimer?.invalidate() - - // Schedule background app refresh scheduleBackgroundFeedRefresh() - - // Sync article status - let completeProcessing = { [unowned self] in - UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask) - self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid - } - - DispatchQueue.global(qos: .background).async { - self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { - completeProcessing() - os_log("Accounts sync processing terminated for running too long.", log: self.log, type: .info) - } - - DispatchQueue.main.async { - AccountManager.shared.syncArticleStatusAll() { - completeProcessing() - } - } - } + waitForProgressToFinish() + syncArticleStatus() } func prepareAccountsForForeground() { @@ -256,6 +238,68 @@ private extension AppDelegate { } +// MARK: Go To Background +private extension AppDelegate { + + func waitForProgressToFinish() { + let completeProcessing = { [unowned self] in + AccountManager.shared.saveAll() + UIApplication.shared.endBackgroundTask(self.waitBackgroundUpdateTask) + self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid + } + + self.waitBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { + completeProcessing() + os_log("Accounts wait for progress terminated for running too long.", log: self.log, type: .info) + } + + DispatchQueue.main.async { [weak self] in + self?.waitToComplete() { + completeProcessing() + } + } + } + + func waitToComplete(completion: @escaping () -> Void) { + guard UIApplication.shared.applicationState != .active else { + os_log("App came back to forground, no longer waiting.", log: self.log, type: .info) + completion() + return + } + + if AccountManager.shared.refreshInProgress { + os_log("Waiting for refresh progress to finish...", log: self.log, type: .info) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in + self?.waitToComplete() { + completion() + } + } + } else { + os_log("Refresh progress complete.", log: self.log, type: .info) + completion() + } + } + + func syncArticleStatus() { + let completeProcessing = { [unowned self] in + UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask) + self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid + } + + self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { + completeProcessing() + os_log("Accounts sync processing terminated for running too long.", log: self.log, type: .info) + } + + DispatchQueue.main.async { + AccountManager.shared.syncArticleStatusAll() { + completeProcessing() + } + } + } + +} + // MARK: Background Tasks private extension AppDelegate { From b78b996e88eed32a0e49e6fdd4aa8cf1e5379b07 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 31 Oct 2019 20:55:08 -0500 Subject: [PATCH 25/25] Animate Select Feed context menu result. Issue #1220 --- iOS/MasterFeed/MasterFeedViewController.swift | 8 ++++---- .../MasterTimelineViewController.swift | 6 ++++-- iOS/SceneCoordinator.swift | 18 +++++++++++++----- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 7302a40f9..41994f17e 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -129,7 +129,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { guard let feed = notification.userInfo?[UserInfoKey.feed] as? Feed else { return } - discloseFeed(feed) + discloseFeed(feed, animated: true) } @objc func contentSizeCategoryDidChange(_ note: Notification) { @@ -481,7 +481,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } } - func discloseFeed(_ feed: Feed, completion: (() -> Void)? = nil) { + func discloseFeed(_ feed: Feed, animated: Bool, completion: (() -> Void)? = nil) { guard let node = coordinator.rootNode.descendantNodeRepresentingObject(feed as AnyObject) else { completion?() @@ -490,7 +490,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { if let indexPath = dataSource.indexPath(for: node) { tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true) - coordinator.selectFeed(indexPath) + coordinator.selectFeed(indexPath, animated: animated) completion?() return } @@ -505,7 +505,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { self.applyChanges(animate: true, adjustScroll: true) { [weak self] in if let indexPath = self?.dataSource.indexPath(for: node) { - self?.coordinator.selectFeed(indexPath) + self?.coordinator.selectFeed(indexPath, animated: animated) completion?() } } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 61103ed05..4c22bca01 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -586,7 +586,8 @@ private extension MasterTimelineViewController { let title = NSLocalizedString("Select Feed", comment: "Select Feed") let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in - self?.coordinator.discloseFeed(feed) + self?.coordinator.selectFeed(nil, animated: true) + self?.coordinator.discloseFeed(feed, animated: true) } return action } @@ -596,7 +597,8 @@ private extension MasterTimelineViewController { let title = NSLocalizedString("Select Feed", comment: "Select Feed") let action = UIAlertAction(title: title, style: .default) { [weak self] action in - self?.coordinator.discloseFeed(feed) + self?.coordinator.selectFeed(nil, animated: true) + self?.coordinator.discloseFeed(feed, animated: true) completionHandler(true) } return action diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 10153efe4..1be094103 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -65,6 +65,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { private var lastSearchScope: SearchScope? = nil private var isSearching: Bool = false private var searchArticleIds: Set? = nil + private var isTimelineViewControllerPending = false private var isArticleViewControllerPending = false private(set) var sortDirection = AppDefaults.timelineSortDirection { @@ -782,8 +783,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred) } - func discloseFeed(_ feed: Feed, completion: (() -> Void)? = nil) { - masterFeedViewController.discloseFeed(feed) { + func discloseFeed(_ feed: Feed, animated: Bool, completion: (() -> Void)? = nil) { + masterFeedViewController.discloseFeed(feed, animated: animated) { completion?() } } @@ -952,11 +953,15 @@ extension SceneCoordinator: UINavigationControllerDelegate { } // If we are showing the Feeds and only the feeds start clearing stuff - if viewController === masterFeedViewController && !isThreePanelMode { + if viewController === masterFeedViewController && !isThreePanelMode && !isTimelineViewControllerPending { activityManager.invalidateCurrentActivities() selectFeed(nil) return } + + if viewController is MasterTimelineViewController { + isTimelineViewControllerPending = false + } // If we are using a phone and navigate away from the detail, clear up the article resources (including activity). // Don't clear it if we have pushed an ArticleViewController, but don't yet see it on the navigation stack. @@ -1473,6 +1478,9 @@ private extension SceneCoordinator { // MARK: Double Split func installTimelineControllerIfNecessary(animated: Bool) { + + isTimelineViewControllerPending = true + if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 { masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) masterTimelineViewController!.coordinator = self @@ -1654,7 +1662,7 @@ private extension SceneCoordinator { return } if let feed = feedNode.representedObject as? Feed { - discloseFeed(feed) + discloseFeed(feed, animated: false) } } @@ -1663,7 +1671,7 @@ private extension SceneCoordinator { return } - discloseFeed(feedNode.representedObject as! Feed) { + discloseFeed(feedNode.representedObject as! Feed, animated: false) { guard let articleID = userInfo?[DeepLinkKey.articleID.rawValue] as? String else { return } if let article = self.articles.first(where: { $0.articleID == articleID }) {