From f901436211bfbb0c4100c1db3569b380c9f94a11 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Sun, 3 May 2020 21:33:57 +0800 Subject: [PATCH 01/19] mailto links now open on iOS fixes #2036 Extends `URL` with an email address `var` for `mailto` schemes and adds a decisionHandler for `mailto` schemes on `WebViewController`. If the device cannot send mail, an alert is displayed. --- NetNewsWire.xcodeproj/project.pbxproj | 8 ++++++++ Shared/Extensions/URL-Extensions.swift | 17 +++++++++++++++++ iOS/Article/WebViewController.swift | 25 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 Shared/Extensions/URL-Extensions.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 2ea40713b..42ec49302 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -674,6 +674,9 @@ D5F4EDB720074D6500B9E363 /* WebFeed+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB620074D6500B9E363 /* WebFeed+Scriptability.swift */; }; D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */; }; DD82AB0A231003F6002269DF /* SharingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82AB09231003F6002269DF /* SharingTests.swift */; }; + DF41F3AE245EFCD7004EFB01 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF41F3AD245EFCD7004EFB01 /* URL-Extensions.swift */; }; + DF41F3C8245EFD45004EFB01 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF41F3AD245EFCD7004EFB01 /* URL-Extensions.swift */; }; + DF41F3C9245EFD46004EFB01 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF41F3AD245EFCD7004EFB01 /* URL-Extensions.swift */; }; FF3ABF13232599810074C542 /* ArticleSorterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF09232599450074C542 /* ArticleSorterTests.swift */; }; FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; @@ -1635,6 +1638,7 @@ D5F4EDB620074D6500B9E363 /* WebFeed+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebFeed+Scriptability.swift"; sourceTree = ""; }; D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = ""; }; DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.swift; sourceTree = ""; }; + DF41F3AD245EFCD7004EFB01 /* URL-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL-Extensions.swift"; sourceTree = ""; }; FF3ABF09232599450074C542 /* ArticleSorterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorterTests.swift; sourceTree = ""; }; FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorter.swift; sourceTree = ""; }; FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAsReadAlertController.swift; sourceTree = ""; }; @@ -2405,6 +2409,7 @@ 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */, 516AE9DE2372269A007DEEAA /* IconImage.swift */, B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */, + DF41F3AD245EFCD7004EFB01 /* URL-Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -3852,6 +3857,7 @@ 65ED401C235DEF6C0081F399 /* FaviconGenerator.swift in Sources */, 65ED401D235DEF6C0081F399 /* RefreshInterval.swift in Sources */, 65ED401E235DEF6C0081F399 /* TimelineCellData.swift in Sources */, + DF41F3C9245EFD46004EFB01 /* URL-Extensions.swift in Sources */, 65ED401F235DEF6C0081F399 /* BuiltinSmartFeedInspectorViewController.swift in Sources */, 65ED4020235DEF6C0081F399 /* AppDelegate+Scriptability.swift in Sources */, 65ED4021235DEF6C0081F399 /* NNW3Document.swift in Sources */, @@ -4038,6 +4044,7 @@ 51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */, 5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */, 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */, + DF41F3C8245EFD45004EFB01 /* URL-Extensions.swift in Sources */, FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */, 51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */, 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, @@ -4156,6 +4163,7 @@ 51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */, 849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */, 8405DDA522168C62008CE1BF /* TimelineContainerViewController.swift in Sources */, + DF41F3AE245EFCD7004EFB01 /* URL-Extensions.swift in Sources */, 844B5B671FEA18E300C7C76A /* MainWIndowKeyboardHandler.swift in Sources */, 848D578E21543519005FFAD5 /* PasteboardWebFeed.swift in Sources */, 5144EA2F2279FAB600D19003 /* AccountsDetailViewController.swift in Sources */, diff --git a/Shared/Extensions/URL-Extensions.swift b/Shared/Extensions/URL-Extensions.swift new file mode 100644 index 000000000..be121c334 --- /dev/null +++ b/Shared/Extensions/URL-Extensions.swift @@ -0,0 +1,17 @@ +// +// URL-Extensions.swift +// NetNewsWire +// +// Created by Stuart Breckenridge on 03/05/2020. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation + +extension URL { + + /// Extracts email address from a `URL` with a `mailto` scheme, otherwise `nil`. + var emailAddress: String? { + scheme == "mailto" ? URLComponents(url: self, resolvingAgainstBaseURL: false)?.path : nil + } +} diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index d8e59ff05..132dc48d7 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -12,6 +12,7 @@ import RSCore import Account import Articles import SafariServices +import MessageUI protocol WebViewControllerDelegate: class { func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState) @@ -310,6 +311,23 @@ extension WebViewController: WKNavigationDelegate { let vc = SFSafariViewController(url: url) self.present(vc, animated: true) } + } else if components?.scheme == "mailto" { + decisionHandler(.cancel) + + guard let emailAddress = components?.url?.emailAddress else { + return + } + + if MFMailComposeViewController.canSendMail() { + let mailComposeViewController = MFMailComposeViewController() + mailComposeViewController.setToRecipients([emailAddress]) + mailComposeViewController.mailComposeDelegate = self + self.present(mailComposeViewController, animated: true, completion: {}) + } else { + let alert = UIAlertController(title: "Error", message: "This device cannot send emails.", preferredStyle: .alert) + alert.addAction(.init(title: "Dismiss", style: .cancel, handler: nil)) + self.present(alert, animated: true, completion: nil) + } } else { decisionHandler(.allow) } @@ -666,3 +684,10 @@ private extension WebViewController { } } + +// MARK: MFMailComposeViewControllerDelegate +extension WebViewController: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + self.dismiss(animated: true, completion: nil) + } +} From e825a5d5161a231cb2d56b378b3a855514234663 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Sun, 3 May 2020 22:37:01 +0800 Subject: [PATCH 02/19] Adds handling of `tel` url schemes --- Shared/Extensions/URL-Extensions.swift | 5 +++++ iOS/Article/WebViewController.swift | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Shared/Extensions/URL-Extensions.swift b/Shared/Extensions/URL-Extensions.swift index be121c334..1cac0dc6a 100644 --- a/Shared/Extensions/URL-Extensions.swift +++ b/Shared/Extensions/URL-Extensions.swift @@ -14,4 +14,9 @@ extension URL { var emailAddress: String? { scheme == "mailto" ? URLComponents(url: self, resolvingAgainstBaseURL: false)?.path : nil } + + /// Extracts telephone number from a `URL` with a `tel` scheme, otherwise `nil`. + var telNumber: String? { + scheme == "tel" ? URLComponents(url: self, resolvingAgainstBaseURL: false)?.path : nil + } } diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 132dc48d7..c25d4b29b 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -314,7 +314,7 @@ extension WebViewController: WKNavigationDelegate { } else if components?.scheme == "mailto" { decisionHandler(.cancel) - guard let emailAddress = components?.url?.emailAddress else { + guard let emailAddress = url.emailAddress else { return } @@ -328,6 +328,13 @@ extension WebViewController: WKNavigationDelegate { alert.addAction(.init(title: "Dismiss", style: .cancel, handler: nil)) self.present(alert, animated: true, completion: nil) } + } else if components?.scheme == "tel" { + decisionHandler(.cancel) + + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil) + } + } else { decisionHandler(.allow) } From f58ac4d1608b785d489bc0dad9cc272c489a7497 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Mon, 4 May 2020 13:24:35 +0800 Subject: [PATCH 03/19] Wraps Alert strings in `NSLocalizedString` Also removes `tel` scheme handling. --- Shared/Extensions/URL-Extensions.swift | 4 ---- iOS/Article/WebViewController.swift | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Shared/Extensions/URL-Extensions.swift b/Shared/Extensions/URL-Extensions.swift index 1cac0dc6a..4fb9b8d7f 100644 --- a/Shared/Extensions/URL-Extensions.swift +++ b/Shared/Extensions/URL-Extensions.swift @@ -15,8 +15,4 @@ extension URL { scheme == "mailto" ? URLComponents(url: self, resolvingAgainstBaseURL: false)?.path : nil } - /// Extracts telephone number from a `URL` with a `tel` scheme, otherwise `nil`. - var telNumber: String? { - scheme == "tel" ? URLComponents(url: self, resolvingAgainstBaseURL: false)?.path : nil - } } diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index c25d4b29b..9e6d62555 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -324,8 +324,8 @@ extension WebViewController: WKNavigationDelegate { mailComposeViewController.mailComposeDelegate = self self.present(mailComposeViewController, animated: true, completion: {}) } else { - let alert = UIAlertController(title: "Error", message: "This device cannot send emails.", preferredStyle: .alert) - alert.addAction(.init(title: "Dismiss", style: .cancel, handler: nil)) + let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), message: NSLocalizedString("This device cannot send emails.", comment: "This device cannot send emails."), preferredStyle: .alert) + alert.addAction(.init(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)) self.present(alert, animated: true, completion: nil) } } else if components?.scheme == "tel" { From e6486231831260d63fa7ce02eeaebb5a2d8e9e67 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Thu, 7 May 2020 11:27:57 -0700 Subject: [PATCH 04/19] Bump build to 46. --- xcconfig/common/NetNewsWire_ios_target_common.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig index be0a9ff23..eb3906f45 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.1 -CURRENT_PROJECT_VERSION = 45 +CURRENT_PROJECT_VERSION = 46 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon From 58575331dd66a4d8b498e34c6809cb9c95ef3193 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 13 May 2020 12:33:51 +0800 Subject: [PATCH 05/19] Fixes #2018 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a new `MarkAsReadAlertControllerSourceType` protocol to which `CGRect`, `UIView`, and `UIBarButtonItem` conform to. The `MarkAsReadAlertController` now presents an action sheet for mark as read, mark above as read, and mark below as read. The above is used for the `popoverPresentationController`’s `barButtonItem`, `sourceRect`, or `sourceView` as needed. --- iOS/MasterFeed/MasterFeedViewController.swift | 15 ++--- .../MarkAsReadAlertController.swift | 42 +++++++++++--- .../MasterTimelineViewController.swift | 56 ++++++++++--------- 3 files changed, 72 insertions(+), 41 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index d8f99402f..accf4b67e 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -937,7 +937,7 @@ private extension MasterFeedViewController { guard let node = dataSource.itemIdentifier(for: indexPath), coordinator.unreadCountFor(node) > 0, let feed = node.representedObject as? WebFeed, - let articles = try? feed.fetchArticles() else { + let articles = try? feed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } @@ -947,8 +947,9 @@ private extension MasterFeedViewController { completion(true) } + let action = UIAlertAction(title: title, style: .default) { [weak self] action in - MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in self?.coordinator.markAllAsRead(Array(articles)) completion(true) } @@ -1026,7 +1027,7 @@ private extension MasterFeedViewController { } let articles = Array(fetchedArticles) - return markAllAsReadAction(articles: articles, nameForDisplay: articleFetcher.nameForDisplay) + return markAllAsReadAction(articles: articles, nameForDisplay: articleFetcher.nameForDisplay, indexPath: indexPath) } func markAllAsReadAction(account: Account) -> UIAction? { @@ -1038,16 +1039,16 @@ private extension MasterFeedViewController { return markAllAsReadAction(articles: articles, nameForDisplay: account.nameForDisplay) } - func markAllAsReadAction(articles: [Article], nameForDisplay: String) -> UIAction? { - guard articles.canMarkAllAsRead() else { + func markAllAsReadAction(articles: [Article], nameForDisplay: String, indexPath: IndexPath? = nil) -> UIAction? { + guard articles.canMarkAllAsRead(), let indexPath = indexPath, let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, nameForDisplay) as String - + let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in - MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in self?.coordinator.markAllAsRead(articles) } } diff --git a/iOS/MasterTimeline/MarkAsReadAlertController.swift b/iOS/MasterTimeline/MarkAsReadAlertController.swift index 670b2c637..241b734c2 100644 --- a/iOS/MasterTimeline/MarkAsReadAlertController.swift +++ b/iOS/MasterTimeline/MarkAsReadAlertController.swift @@ -9,13 +9,20 @@ import Foundation import UIKit +protocol MarkAsReadAlertControllerSourceType {} +extension CGRect: MarkAsReadAlertControllerSourceType {} +extension UIView: MarkAsReadAlertControllerSourceType {} +extension UIBarButtonItem: MarkAsReadAlertControllerSourceType {} + + struct MarkAsReadAlertController { - static func confirm(_ controller: UIViewController?, - coordinator: SceneCoordinator?, - confirmTitle: String, - cancelCompletion: (() -> Void)? = nil, - completion: @escaping () -> Void) { + static func confirm(_ controller: UIViewController?, + coordinator: SceneCoordinator?, + confirmTitle: String, + sourceType: T, + cancelCompletion: (() -> Void)? = nil, + completion: @escaping () -> Void) where T: MarkAsReadAlertControllerSourceType { guard let controller = controller, let coordinator = coordinator else { completion() @@ -23,7 +30,7 @@ struct MarkAsReadAlertController { } if AppDefaults.confirmMarkAllAsRead { - let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion) { _ in + let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion, sourceType: sourceType) { _ in completion() } controller.present(alertController, animated: true) @@ -32,10 +39,12 @@ struct MarkAsReadAlertController { } } - private static func alert(coordinator: SceneCoordinator, + private static func alert(coordinator: SceneCoordinator, confirmTitle: String, cancelCompletion: (() -> Void)?, - completion: @escaping (UIAlertAction) -> Void) -> UIAlertController { + sourceType: T, + completion: @escaping (UIAlertAction) -> Void) -> UIAlertController where T: MarkAsReadAlertControllerSourceType { + let title = NSLocalizedString("Mark As Read", comment: "Mark As Read") let message = NSLocalizedString("You can turn this confirmation off in settings.", @@ -43,7 +52,7 @@ struct MarkAsReadAlertController { let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings") - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in cancelCompletion?() } @@ -55,6 +64,21 @@ struct MarkAsReadAlertController { alertController.addAction(markAction) alertController.addAction(settingsAction) alertController.addAction(cancelAction) + + if let barButtonItem = sourceType as? UIBarButtonItem { + alertController.popoverPresentationController?.barButtonItem = barButtonItem + return alertController + } + + if let rect = sourceType as? CGRect { + alertController.popoverPresentationController?.sourceRect = rect + return alertController + } + + if let view = sourceType as? UIView { + alertController.popoverPresentationController?.sourceView = view + return alertController + } return alertController } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index fdd148c81..54f1f8bb9 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -116,7 +116,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner @IBAction func markAllAsRead(_ sender: Any) { let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read") - MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title) { [weak self] in + + guard let barButtonItem = sender as? UIBarButtonItem else { + return + } + + MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: barButtonItem) { [weak self] in self?.coordinator.markAllAsReadInTimeline() } } @@ -260,11 +265,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner popoverController.sourceRect = CGRect(x: view.frame.size.width/2, y: view.frame.size.height/2, width: 1, height: 1) } - if let action = self.markAboveAsReadAlertAction(article, completion: completion) { + if let action = self.markAboveAsReadAlertAction(article, indexPath: indexPath, completion: completion) { alert.addAction(action) } - if let action = self.markBelowAsReadAlertAction(article, completion: completion) { + if let action = self.markBelowAsReadAlertAction(article, indexPath: indexPath, completion: completion) { alert.addAction(action) } @@ -272,7 +277,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner alert.addAction(action) } - if let action = self.markAllInFeedAsReadAlertAction(article, completion: completion) { + if let action = self.markAllInFeedAsReadAlertAction(article, indexPath: indexPath, completion: completion) { alert.addAction(action) } @@ -317,11 +322,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner actions.append(self.toggleArticleStarStatusAction(article)) - if let action = self.markAboveAsReadAction(article) { + if let action = self.markAboveAsReadAction(article, indexPath: indexPath) { actions.append(action) } - if let action = self.markBelowAsReadAction(article) { + if let action = self.markBelowAsReadAction(article, indexPath: indexPath) { actions.append(action) } @@ -329,7 +334,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner actions.append(action) } - if let action = self.markAllInFeedAsReadAction(article) { + if let action = self.markAllInFeedAsReadAction(article, indexPath: indexPath) { actions.append(action) } @@ -696,38 +701,38 @@ private extension MasterTimelineViewController { return action } - func markAboveAsReadAction(_ article: Article) -> UIAction? { - guard coordinator.canMarkAboveAsRead(for: article) else { + func markAboveAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? { + guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read") let image = AppAssets.markAboveAsReadImage let action = UIAction(title: title, image: image) { [weak self] action in - MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in self?.coordinator.markAboveAsRead(article) } } return action } - func markBelowAsReadAction(_ article: Article) -> UIAction? { - guard coordinator.canMarkBelowAsRead(for: article) else { + func markBelowAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? { + guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read") let image = AppAssets.markBelowAsReadImage let action = UIAction(title: title, image: image) { [weak self] action in - MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in self?.coordinator.markBelowAsRead(article) } } return action } - func markAboveAsReadAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? { - guard coordinator.canMarkAboveAsRead(for: article) else { + func markAboveAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { + guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } @@ -737,7 +742,7 @@ private extension MasterTimelineViewController { } let action = UIAlertAction(title: title, style: .default) { [weak self] action in - MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in self?.coordinator.markAboveAsRead(article) completion(true) } @@ -745,8 +750,8 @@ private extension MasterTimelineViewController { return action } - func markBelowAsReadAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? { - guard coordinator.canMarkBelowAsRead(for: article) else { + func markBelowAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { + guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } @@ -756,7 +761,7 @@ private extension MasterTimelineViewController { } let action = UIAlertAction(title: title, style: .default) { [weak self] action in - MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in self?.coordinator.markBelowAsRead(article) completion(true) } @@ -787,36 +792,37 @@ private extension MasterTimelineViewController { return action } - func markAllInFeedAsReadAction(_ article: Article) -> UIAction? { + func markAllInFeedAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? { guard let webFeed = article.webFeed else { return nil } guard let fetchedArticles = try? webFeed.fetchArticles() else { return nil } let articles = Array(fetchedArticles) - guard articles.canMarkAllAsRead() else { + guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } + let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in - MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in self?.coordinator.markAllAsRead(articles) } } return action } - func markAllInFeedAsReadAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? { + func markAllInFeedAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { guard let webFeed = article.webFeed else { return nil } guard let fetchedArticles = try? webFeed.fetchArticles() else { return nil } let articles = Array(fetchedArticles) - guard articles.canMarkAllAsRead() else { + guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } @@ -827,7 +833,7 @@ private extension MasterTimelineViewController { } let action = UIAlertAction(title: title, style: .default) { [weak self] action in - MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in self?.coordinator.markAllAsRead(articles) completion(true) } From f153643273068020c1c2ba62da159b8133c0037c Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 13 May 2020 13:03:51 +0800 Subject: [PATCH 06/19] Removes return statements that are not needed. --- iOS/MasterTimeline/MarkAsReadAlertController.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/iOS/MasterTimeline/MarkAsReadAlertController.swift b/iOS/MasterTimeline/MarkAsReadAlertController.swift index 241b734c2..23e064ce5 100644 --- a/iOS/MasterTimeline/MarkAsReadAlertController.swift +++ b/iOS/MasterTimeline/MarkAsReadAlertController.swift @@ -67,17 +67,14 @@ struct MarkAsReadAlertController { if let barButtonItem = sourceType as? UIBarButtonItem { alertController.popoverPresentationController?.barButtonItem = barButtonItem - return alertController } if let rect = sourceType as? CGRect { alertController.popoverPresentationController?.sourceRect = rect - return alertController } if let view = sourceType as? UIView { alertController.popoverPresentationController?.sourceView = view - return alertController } return alertController From a9aefd252ebc87c398faeac701e6c5a43d44438a Mon Sep 17 00:00:00 2001 From: Rizwan Mohamed Ibrahim Date: Wed, 13 May 2020 17:29:59 +0530 Subject: [PATCH 07/19] adds keyboard shortcuts - toggle side bar on timeline - toggle readers view on article - go to settings on global --- iOS/Article/ArticleViewController.swift | 4 ++++ iOS/KeyboardManager.swift | 9 +++++++++ iOS/MasterTimeline/MasterTimelineViewController.swift | 6 +++++- iOS/RootSplitViewController.swift | 6 +++++- iOS/SceneCoordinator.swift | 5 ++++- 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index bb839e372..9bd609774 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -250,6 +250,10 @@ class ArticleViewController: UIViewController { @IBAction func showActivityDialog(_ sender: Any) { currentWebViewController?.showActivityDialog(popOverBarButtonItem: actionBarButtonItem) } + + @objc func toggleReaderView(_ sender: Any?) { + currentWebViewController?.toggleArticleExtractor() + } // MARK: Keyboard Shortcuts @objc func navigateToTimeline(_ sender: Any?) { diff --git a/iOS/KeyboardManager.swift b/iOS/KeyboardManager.swift index a13f44ceb..c463fcde2 100644 --- a/iOS/KeyboardManager.swift +++ b/iOS/KeyboardManager.swift @@ -138,6 +138,9 @@ private extension KeyboardManager { let goToStarredTitle = NSLocalizedString("Go To Starred", comment: "Go To Starred") keys.append(KeyboardManager.createKeyCommand(title: goToStarredTitle, action: "goToStarred:", input: "3", modifiers: [.command])) + let gotoSettings = NSLocalizedString("Go To Settings", comment: "Go To Settings") + keys.append(KeyboardManager.createKeyCommand(title: gotoSettings, action: "goToSettings:", input: ",", modifiers: [.command])) + let articleSearchTitle = NSLocalizedString("Article Search", comment: "Article Search") keys.append(KeyboardManager.createKeyCommand(title: articleSearchTitle, action: "articleSearch:", input: "f", modifiers: [.command, .alternate])) @@ -186,6 +189,12 @@ private extension KeyboardManager { let toggleStarredTitle = NSLocalizedString("Toggle Starred Status", comment: "Toggle Starred Status") keys.append(KeyboardManager.createKeyCommand(title: toggleStarredTitle, action: "toggleStarred:", input: "l", modifiers: [.command, .shift])) + let toggleSidebar = NSLocalizedString("Toggle Sidebar", comment: "Toggle Sidebar") + keys.append(KeyboardManager.createKeyCommand(title: toggleSidebar, action: "toggleSidebar:", input: "s", modifiers: [.command, .control])) + + let toggleReaderView = NSLocalizedString("Toggle Reader View", comment: "Toggle Reader View") + keys.append(KeyboardManager.createKeyCommand(title: toggleReaderView, action: "toggleReaderView:", input: "r", modifiers: [.command, .shift])) + return keys } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index fdd148c81..5663c06fe 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -156,7 +156,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner @objc func showFeedInspector(_ sender: UITapGestureRecognizer) { coordinator.showFeedInspector() } - + + @objc func toggleSidebar(_ sender: Any?) { + coordinator.toggleSidebar() + } + // MARK: API func restoreSelectionIfNecessary(adjustScroll: Bool) { diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index 852c863d1..22f483876 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -121,7 +121,11 @@ class RootSplitViewController: UISplitViewController { @objc func goToStarred(_ sender: Any?) { coordinator.selectStarredFeed() } - + + @objc func goToSettings(_ sender: Any?) { + coordinator.showSettings() + } + @objc func toggleRead(_ sender: Any?) { coordinator.toggleReadForCurrentArticle() } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 25b732781..64abb8d3d 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1199,7 +1199,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func navigateToDetail() { articleViewController?.focus() } - + + func toggleSidebar() { + rootSplitViewController.preferredDisplayMode = rootSplitViewController.displayMode == .allVisible ? .primaryHidden : .allVisible + } } // MARK: UISplitViewControllerDelegate From 32c8c038ee6ee9d7a386642bb0ffc08c3dc47cf5 Mon Sep 17 00:00:00 2001 From: Rizwan Mohamed Ibrahim Date: Wed, 13 May 2020 21:57:02 +0530 Subject: [PATCH 08/19] move toggle side bar to RootSplitViewController --- iOS/MasterTimeline/MasterTimelineViewController.swift | 4 ---- iOS/RootSplitViewController.swift | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 5663c06fe..270458f72 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -157,10 +157,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner coordinator.showFeedInspector() } - @objc func toggleSidebar(_ sender: Any?) { - coordinator.toggleSidebar() - } - // MARK: API func restoreSelectionIfNecessary(adjustScroll: Bool) { diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index 22f483876..33bd740c1 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -133,5 +133,8 @@ class RootSplitViewController: UISplitViewController { @objc func toggleStarred(_ sender: Any?) { coordinator.toggleStarredForCurrentArticle() } - + + @objc func toggleSidebar(_ sender: Any?) { + coordinator.toggleSidebar() + } } From a9e8cec90e2e9b372ded29f9256ab7ef346ff2f8 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 14 May 2020 17:10:55 +0800 Subject: [PATCH 09/19] Key commands work with action sheets --- .../MasterTimelineViewController.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 54f1f8bb9..be746ed92 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -117,12 +117,20 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner @IBAction func markAllAsRead(_ sender: Any) { let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read") - guard let barButtonItem = sender as? UIBarButtonItem else { - return + if let source = sender as? UIBarButtonItem { + MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: source) { [weak self] in + self?.coordinator.markAllAsReadInTimeline() + } } - MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: barButtonItem) { [weak self] in - self?.coordinator.markAllAsReadInTimeline() + if let _ = sender as? UIKeyCommand { + guard let indexPath = tableView.indexPathForSelectedRow, let contentView = tableView.cellForRow(at: indexPath)?.contentView else { + return + } + + MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in + self?.coordinator.markAllAsReadInTimeline() + } } } From bfef02e18f2c4ba72a2cc77691c3d40d779f70d8 Mon Sep 17 00:00:00 2001 From: Rizwan Mohamed Ibrahim Date: Thu, 14 May 2020 17:17:16 +0530 Subject: [PATCH 10/19] adds text activation rule for share extension --- iOS/ShareExtension/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iOS/ShareExtension/Info.plist b/iOS/ShareExtension/Info.plist index fc1cd201d..260decfa3 100644 --- a/iOS/ShareExtension/Info.plist +++ b/iOS/ShareExtension/Info.plist @@ -32,6 +32,8 @@ NSExtensionActivationSupportsWebURLWithMaxCount 1 + NSExtensionActivationSupportsText + 1 NSExtensionJavaScriptPreprocessingFile SafariExt From f7f7f4dddb26352dd9d10261d6a68d3901148d02 Mon Sep 17 00:00:00 2001 From: Rizwan Mohamed Ibrahim Date: Thu, 14 May 2020 17:58:38 +0530 Subject: [PATCH 11/19] adds keyboard shortcuts for open in Safari --- Shared/Resources/GlobalKeyboardShortcuts.plist | 4 ++-- iOS/KeyboardManager.swift | 3 +++ iOS/MasterFeed/MasterFeedViewController.swift | 4 ++++ iOS/RootSplitViewController.swift | 4 ++++ iOS/SceneCoordinator.swift | 16 ++++++++++++++++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Shared/Resources/GlobalKeyboardShortcuts.plist b/Shared/Resources/GlobalKeyboardShortcuts.plist index 4c6f3662c..4471354f7 100644 --- a/Shared/Resources/GlobalKeyboardShortcuts.plist +++ b/Shared/Resources/GlobalKeyboardShortcuts.plist @@ -106,11 +106,11 @@ title - Open in Browser + Open in Safari key [return] action - openInBrowser: + openInSafari: key diff --git a/iOS/KeyboardManager.swift b/iOS/KeyboardManager.swift index c463fcde2..bf0a4c465 100644 --- a/iOS/KeyboardManager.swift +++ b/iOS/KeyboardManager.swift @@ -177,6 +177,9 @@ private extension KeyboardManager { let openInBrowserTitle = NSLocalizedString("Open In Browser", comment: "Open In Browser") keys.append(KeyboardManager.createKeyCommand(title: openInBrowserTitle, action: "openInBrowser:", input: UIKeyCommand.inputRightArrow, modifiers: [.command])) + let openInSafariTitle = NSLocalizedString("Open In Safari", comment: "Open In Safari") + keys.append(KeyboardManager.createKeyCommand(title: openInSafariTitle, action: "openInSafari:", input: "\r", modifiers: [])) + let toggleReadTitle = NSLocalizedString("Toggle Read Status", comment: "Toggle Read Status") keys.append(KeyboardManager.createKeyCommand(title: toggleReadTitle, action: "toggleRead:", input: "u", modifiers: [.command, .shift])) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index accf4b67e..1ca161c31 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -420,6 +420,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { @objc func openInBrowser(_ sender: Any?) { coordinator.showBrowserForCurrentFeed() } + + @objc func openInSafari(_ sender: Any?) { + coordinator.showSafariForCurrentFeed() + } @objc override func delete(_ sender: Any?) { if let indexPath = coordinator.currentFeedIndexPath { diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index 33bd740c1..1089dbe26 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -81,6 +81,10 @@ class RootSplitViewController: UISplitViewController { @objc func openInBrowser(_ sender: Any?) { coordinator.showBrowserForCurrentArticle() } + + @objc func openInSafari(_ sender: Any?) { + coordinator.showSafariForCurrentArticle() + } @objc func articleSearch(_ sender: Any?) { coordinator.showSearch() diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 64abb8d3d..93a73fd5c 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -12,6 +12,7 @@ import Account import Articles import RSCore import RSTree +import SafariServices enum PanelMode { case unset @@ -1183,6 +1184,21 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } UIApplication.shared.open(url, options: [:]) } + + func showSafariForCurrentFeed() { + if let ip = currentFeedIndexPath, let url = homePageURLForFeed(ip) { + let vc = SFSafariViewController(url: url) + rootSplitViewController.viewControllers.last?.present(vc, animated: true) + } + } + + func showSafariForCurrentArticle() { + guard let preferredLink = currentArticle?.preferredLink, let url = URL(string: preferredLink) else { + return + } + let vc = SFSafariViewController(url: url) + rootSplitViewController.viewControllers.last?.present(vc, animated: true) + } func navigateToFeeds() { masterFeedViewController?.focus() From 04458adee5ddb06b234dba5d7c68d90daa803883 Mon Sep 17 00:00:00 2001 From: Rizwan Mohamed Ibrahim Date: Thu, 14 May 2020 18:13:41 +0530 Subject: [PATCH 12/19] adds "Mark All Read" shortcut for feeds list --- iOS/MasterFeed/MasterFeedViewController.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index accf4b67e..651afc6c2 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -458,7 +458,18 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { self.reloadAllVisibleCells() } } - + + @objc func markAllAsRead(_ sender: Any) { + guard let indexPath = tableView.indexPathForSelectedRow, let contentView = tableView.cellForRow(at: indexPath)?.contentView else { + return + } + + let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read") + MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in + self?.coordinator.markAllAsReadInTimeline() + } + } + // MARK: API func restoreSelectionIfNecessary(adjustScroll: Bool) { From c9ebb6bd752d37c497e862029773465664e1edae Mon Sep 17 00:00:00 2001 From: Rizwan Mohamed Ibrahim Date: Fri, 15 May 2020 12:39:33 +0530 Subject: [PATCH 13/19] adds "Open In App Browser" shortcut --- Shared/Resources/GlobalKeyboardShortcuts.plist | 4 ++-- iOS/Article/ArticleViewController.swift | 6 +++++- iOS/Article/WebViewController.swift | 10 +++++++++- iOS/KeyboardManager.swift | 4 ++-- iOS/MasterFeed/MasterFeedViewController.swift | 14 +++++++++----- iOS/RootSplitViewController.swift | 8 ++++++-- iOS/SceneCoordinator.swift | 15 ++++----------- 7 files changed, 37 insertions(+), 24 deletions(-) diff --git a/Shared/Resources/GlobalKeyboardShortcuts.plist b/Shared/Resources/GlobalKeyboardShortcuts.plist index 4471354f7..88e4eac90 100644 --- a/Shared/Resources/GlobalKeyboardShortcuts.plist +++ b/Shared/Resources/GlobalKeyboardShortcuts.plist @@ -106,11 +106,11 @@ title - Open in Safari + Open in App Browser key [return] action - openInSafari: + openFeedInAppBrowser: key diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 9bd609774..b43783b64 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -256,6 +256,7 @@ class ArticleViewController: UIViewController { } // MARK: Keyboard Shortcuts + @objc func navigateToTimeline(_ sender: Any?) { coordinator.navigateToTimeline() } @@ -277,7 +278,10 @@ class ArticleViewController: UIViewController { func stopArticleExtractorIfProcessing() { currentWebViewController?.stopArticleExtractorIfProcessing() } - + + func openInAppBrowser() { + currentWebViewController?.openInAppBrowser() + } } // MARK: WebViewControllerDelegate diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 3483c2079..c08c38ecf 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -226,7 +226,15 @@ class WebViewController: UIViewController { activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem present(activityViewController, animated: true) } - + + func openInAppBrowser() { + guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else { + return + } + + let vc = SFSafariViewController(url: url) + present(vc, animated: true) + } } // MARK: ArticleExtractorDelegate diff --git a/iOS/KeyboardManager.swift b/iOS/KeyboardManager.swift index bf0a4c465..9129a4032 100644 --- a/iOS/KeyboardManager.swift +++ b/iOS/KeyboardManager.swift @@ -177,8 +177,8 @@ private extension KeyboardManager { let openInBrowserTitle = NSLocalizedString("Open In Browser", comment: "Open In Browser") keys.append(KeyboardManager.createKeyCommand(title: openInBrowserTitle, action: "openInBrowser:", input: UIKeyCommand.inputRightArrow, modifiers: [.command])) - let openInSafariTitle = NSLocalizedString("Open In Safari", comment: "Open In Safari") - keys.append(KeyboardManager.createKeyCommand(title: openInSafariTitle, action: "openInSafari:", input: "\r", modifiers: [])) + let openInAppBrowserTitle = NSLocalizedString("Open In App Browser", comment: "Open In App Browser") + keys.append(KeyboardManager.createKeyCommand(title: openInAppBrowserTitle, action: "openInAppBrowser:", input: "\r", modifiers: [])) let toggleReadTitle = NSLocalizedString("Toggle Read Status", comment: "Toggle Read Status") keys.append(KeyboardManager.createKeyCommand(title: toggleReadTitle, action: "toggleRead:", input: "u", modifiers: [.command, .shift])) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 1ca161c31..27aa0c9cc 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -11,6 +11,7 @@ import Account import Articles import RSCore import RSTree +import SafariServices class MasterFeedViewController: UITableViewController, UndoableCommandRunner { @@ -420,10 +421,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { @objc func openInBrowser(_ sender: Any?) { coordinator.showBrowserForCurrentFeed() } - - @objc func openInSafari(_ sender: Any?) { - coordinator.showSafariForCurrentFeed() - } @objc override func delete(_ sender: Any?) { if let indexPath = coordinator.currentFeedIndexPath { @@ -507,7 +504,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { func focus() { becomeFirstResponder() } - + + func openInAppBrowser() { + if let indexPath = coordinator.currentFeedIndexPath, + let url = coordinator.homePageURLForFeed(indexPath) { + let vc = SFSafariViewController(url: url) + present(vc, animated: true) + } + } } // MARK: UIContextMenuInteractionDelegate diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index 1089dbe26..4117a0ee7 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -82,8 +82,12 @@ class RootSplitViewController: UISplitViewController { coordinator.showBrowserForCurrentArticle() } - @objc func openInSafari(_ sender: Any?) { - coordinator.showSafariForCurrentArticle() + @objc func openInAppBrowser(_ sender: Any?) { + coordinator.showInAppBrowserForCurrentArticle() + } + + @objc func openFeedInAppBrowser(_ sender: Any?) { + coordinator.showInAppBrowserForCurrentFeed() } @objc func articleSearch(_ sender: Any?) { diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 93a73fd5c..22d1f30a3 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1185,19 +1185,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { UIApplication.shared.open(url, options: [:]) } - func showSafariForCurrentFeed() { - if let ip = currentFeedIndexPath, let url = homePageURLForFeed(ip) { - let vc = SFSafariViewController(url: url) - rootSplitViewController.viewControllers.last?.present(vc, animated: true) - } + func showInAppBrowserForCurrentArticle() { + articleViewController?.openInAppBrowser() } - func showSafariForCurrentArticle() { - guard let preferredLink = currentArticle?.preferredLink, let url = URL(string: preferredLink) else { - return - } - let vc = SFSafariViewController(url: url) - rootSplitViewController.viewControllers.last?.present(vc, animated: true) + func showInAppBrowserForCurrentFeed() { + masterFeedViewController.openInAppBrowser() } func navigateToFeeds() { From 23ee6761ca589ebcff70de0da4a9e2ed4d19d5ae Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Fri, 15 May 2020 19:53:44 +0800 Subject: [PATCH 14/19] adds grouping and summaries to notifications --- .../UserNotifications/UserNotificationManager.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index 2f37a2438..aa345bf4a 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -47,15 +47,21 @@ private extension UserNotificationManager { let content = UNMutableNotificationContent() content.title = webFeed.nameForDisplay - content.body = ArticleStringFormatter.truncatedTitle(article) - if content.body.isEmpty { - content.body = ArticleStringFormatter.truncatedSummary(article) + + if !ArticleStringFormatter.truncatedTitle(article).isEmpty { + content.subtitle = ArticleStringFormatter.truncatedTitle(article) } + + content.body = ArticleStringFormatter.truncatedSummary(article) content.sound = UNNotificationSound.default content.userInfo = [UserInfoKey.articlePath: article.pathUserInfo] + content.threadIdentifier = webFeed.webFeedID + content.summaryArgument = "\(webFeed.nameForDisplay)" + content.summaryArgumentCount = 1 let request = UNNotificationRequest.init(identifier: "articleID:\(article.articleID)", content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) } From fa51fa47b469decb7e46c671761881d19f85c31a Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Fri, 15 May 2020 20:32:33 +0800 Subject: [PATCH 15/19] reorged the send notification code --- Shared/UserNotifications/UserNotificationManager.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index aa345bf4a..6ddbad025 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -53,15 +53,15 @@ private extension UserNotificationManager { } content.body = ArticleStringFormatter.truncatedSummary(article) - - content.sound = UNNotificationSound.default - content.userInfo = [UserInfoKey.articlePath: article.pathUserInfo] + content.threadIdentifier = webFeed.webFeedID content.summaryArgument = "\(webFeed.nameForDisplay)" content.summaryArgumentCount = 1 - - let request = UNNotificationRequest.init(identifier: "articleID:\(article.articleID)", content: content, trigger: nil) + content.sound = UNNotificationSound.default + content.userInfo = [UserInfoKey.articlePath: article.pathUserInfo] + + let request = UNNotificationRequest.init(identifier: "articleID:\(article.articleID)", content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } From 8ff2776d88e342f58649c6630084b7860f686f67 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 15 May 2020 10:45:05 -0500 Subject: [PATCH 16/19] Make sure we have only one web view in the view hierarchy after navigation. Issue #2075 --- iOS/Article/WebViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index dacd94ced..e7270b919 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -60,7 +60,7 @@ class WebViewController: UIViewController { private(set) var article: Article? - let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 1.0) + let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3) var windowScrollY = 0 override func viewDidLoad() { @@ -291,7 +291,7 @@ extension WebViewController: UIContextMenuInteractionDelegate { extension WebViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - if view.subviews.count > 1 { + while view.subviews.count > 1 { view.subviews.last?.removeFromSuperview() } } From c2b749a74cd9c7849d697fa3c968d71a9ccbde62 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 15 May 2020 17:06:49 -0500 Subject: [PATCH 17/19] Rebuild backing stores immediately when deselecting a Feed. Issue #2079 --- iOS/SceneCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 22d1f30a3..a877756ee 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -750,7 +750,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { setTimelineFeed(nil, animated: false) { if self.isReadFeedsFiltered { - self.queueRebuildBackingStores() + self.rebuildBackingStores() } self.activityManager.invalidateSelecting() if self.rootSplitViewController.isCollapsed && self.navControllerForTimeline().viewControllers.last is MasterTimelineViewController { From 6224dfad031e2cb34e1b148aac3c39f3d3ab39f1 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Mon, 18 May 2020 08:39:22 +0800 Subject: [PATCH 18/19] Notification Permission Requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2057 • On app launch, the app checks if notification permissions are granted and registers with APNS if that is the case. It will not request permissions as part of the app launch. • When a user requests to be notified of new articles, the authorizationStatus is checked: - if `notDetermined` or `provisional`, an authorization request is made, and if successful, the Notify of New Articles status is updated (otherwise it is reverted) - if `denied`, an alert is thrown asking the user to enable in settings (and the change to notify of new articles is reverted) - if `authorized` the update is made. `WebFeedInspectorViewController` also monitors for the app entering the foreground so that it can get the latest notification auth settings. --- iOS/AppDelegate.swift | 6 +- .../WebFeedInspectorViewController.swift | 65 ++++++++++++++++++- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 0dd99bad3..b5c7244b8 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -90,9 +90,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD DispatchQueue.main.async { self.unreadCount = AccountManager.shared.unreadCount } - - UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in - if granted { + + UNUserNotificationCenter.current().getNotificationSettings { (settings) in + if settings.authorizationStatus == .authorized { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } diff --git a/iOS/Inspector/WebFeedInspectorViewController.swift b/iOS/Inspector/WebFeedInspectorViewController.swift index 9a39d6a78..a518a8777 100644 --- a/iOS/Inspector/WebFeedInspectorViewController.swift +++ b/iOS/Inspector/WebFeedInspectorViewController.swift @@ -9,6 +9,7 @@ import UIKit import Account import SafariServices +import UserNotifications class WebFeedInspectorViewController: UITableViewController { @@ -38,6 +39,8 @@ class WebFeedInspectorViewController: UITableViewController { return webFeed.homePageURL == nil } + private var userNotificationSettings: UNNotificationSettings? + override func viewDidLoad() { tableView.register(InspectorIconHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") @@ -51,6 +54,14 @@ class WebFeedInspectorViewController: UITableViewController { feedURLLabel.text = webFeed.url NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil) + + NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main, using: { _ in + self.updateNotificationSettings() + }) + } + + override func viewDidAppear(_ animated: Bool) { + updateNotificationSettings() } override func viewDidDisappear(_ animated: Bool) { @@ -67,7 +78,30 @@ class WebFeedInspectorViewController: UITableViewController { } @IBAction func notifyAboutNewArticlesChanged(_ sender: Any) { - webFeed.isNotifyAboutNewArticles = notifyAboutNewArticlesSwitch.isOn + guard let settings = userNotificationSettings else { + notifyAboutNewArticlesSwitch.isOn = !notifyAboutNewArticlesSwitch.isOn + return + } + if settings.authorizationStatus == .denied { + notifyAboutNewArticlesSwitch.isOn = !notifyAboutNewArticlesSwitch.isOn + present(notificationUpdateErrorAlert(), animated: true, completion: nil) + } else if settings.authorizationStatus == .authorized { + webFeed.isNotifyAboutNewArticles = notifyAboutNewArticlesSwitch.isOn + } else { + UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in + self.updateNotificationSettings() + if granted { + DispatchQueue.main.async { + self.webFeed.isNotifyAboutNewArticles = self.notifyAboutNewArticlesSwitch.isOn + UIApplication.shared.registerForRemoteNotifications() + } + } else { + DispatchQueue.main.async { + self.notifyAboutNewArticlesSwitch.isOn = !self.notifyAboutNewArticlesSwitch.isOn + } + } + } + } } @IBAction func alwaysShowReaderViewChanged(_ sender: Any) { @@ -158,3 +192,32 @@ extension WebFeedInspectorViewController: UITextFieldDelegate { } } + +// MARK: UNUserNotificationCenter + +extension WebFeedInspectorViewController { + + func updateNotificationSettings() { + UNUserNotificationCenter.current().getNotificationSettings { (settings) in + DispatchQueue.main.async { + self.userNotificationSettings = settings + if settings.authorizationStatus == .authorized { + UIApplication.shared.registerForRemoteNotifications() + } + } + } + } + + func notificationUpdateErrorAlert() -> UIAlertController { + let alert = UIAlertController(title: NSLocalizedString("Enable Notifications", comment: "Notifications"), + message: NSLocalizedString("Notifications need to be enabled in the Settings app.", comment: "Notifications need to be enabled in the Settings app."), preferredStyle: .alert) + let openSettings = UIAlertAction(title: NSLocalizedString("Open Settings", comment: "Open Settings"), style: .default) { (action) in + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil) + } + let dismiss = UIAlertAction(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil) + alert.addAction(openSettings) + alert.addAction(dismiss) + return alert + } + +} From 2297d218c03aad19892b7ac7034377dfa474f7e5 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Mon, 18 May 2020 09:23:42 +0800 Subject: [PATCH 19/19] Uses selector syntax. --- iOS/Inspector/WebFeedInspectorViewController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iOS/Inspector/WebFeedInspectorViewController.swift b/iOS/Inspector/WebFeedInspectorViewController.swift index a518a8777..bb3522edf 100644 --- a/iOS/Inspector/WebFeedInspectorViewController.swift +++ b/iOS/Inspector/WebFeedInspectorViewController.swift @@ -55,9 +55,8 @@ class WebFeedInspectorViewController: UITableViewController { NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil) - NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main, using: { _ in - self.updateNotificationSettings() - }) + NotificationCenter.default.addObserver(self, selector: #selector(updateNotificationSettings), name: UIApplication.willEnterForegroundNotification, object: nil) + } override func viewDidAppear(_ animated: Bool) { @@ -197,6 +196,7 @@ extension WebFeedInspectorViewController: UITextFieldDelegate { extension WebFeedInspectorViewController { + @objc func updateNotificationSettings() { UNUserNotificationCenter.current().getNotificationSettings { (settings) in DispatchQueue.main.async {