diff --git a/Mac/AppAssets.swift b/Mac/AppAssets.swift index 2e6103762..21c22c35a 100644 --- a/Mac/AppAssets.swift +++ b/Mac/AppAssets.swift @@ -295,4 +295,10 @@ struct AppAssets { } } + static var notificationSoundBlipFileName: String = { + // https://freesound.org/people/cabled_mess/sounds/350862/ + return "notificationSoundBlip.mp3" + }() + + } diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 111585f6d..c1b45c983 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -244,7 +244,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, refreshTimer = AccountRefreshTimer() syncTimer = ArticleStatusSyncTimer() - UNUserNotificationCenter.current().requestAuthorization(options:[.badge]) { (granted, error) in } + UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .alert, .badge]) { (granted, error) in } UNUserNotificationCenter.current().getNotificationSettings { (settings) in if settings.authorizationStatus == .authorized { diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 925f0464a..5c93d428e 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -848,6 +848,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 */; }; + DDF9E1D728EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */; }; + DDF9E1D828EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */; }; + DDF9E1D928EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */; }; DF5AD10128D6922200CA3BF7 /* SmartFeedSummaryWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1768144D2564BCE000D98635 /* SmartFeedSummaryWidget.swift */; }; DF790D6028E9769300455FC7 /* Thanks.md in Resources */ = {isa = PBXBuildFile; fileRef = DF790D5F28E9769300455FC7 /* Thanks.md */; }; DF790D6228E990A900455FC7 /* AboutData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF790D6128E990A900455FC7 /* AboutData.swift */; }; @@ -1596,6 +1599,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 = ""; }; + DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = notificationSoundBlip.mp3; sourceTree = ""; }; DF790D5F28E9769300455FC7 /* Thanks.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Thanks.md; sourceTree = ""; }; DF790D6128E990A900455FC7 /* AboutData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutData.swift; sourceTree = ""; }; DFC14F0E28EA55BD00F6EE86 /* AboutWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutWindowController.swift; sourceTree = ""; }; @@ -1866,8 +1870,7 @@ 511D43CE231FA51100FB1562 /* Resources */ = { isa = PBXGroup; children = ( - DFFC4E7528E95F78006B82AF /* About.plist */, - DF790D5F28E9769300455FC7 /* Thanks.md */, + DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */, DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */, 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */, 51077C5727A86D16000C71DB /* Hyperlegible.nnwtheme */, @@ -2743,6 +2746,8 @@ 84C9FC9A2262A1A900D921D6 /* Resources */ = { isa = PBXGroup; children = ( + DFFC4E7528E95F78006B82AF /* About.plist */, + DF790D5F28E9769300455FC7 /* Thanks.md */, 5103A9B324216A4200410853 /* blank.html */, 51BB7C302335ACDE008E8144 /* page.html */, 514219572353C28900E07E2C /* main_ios.js */, @@ -3407,6 +3412,7 @@ 65ED405D235DEF6C0081F399 /* SidebarKeyboardShortcuts.plist in Resources */, 514A89A3244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */, 51D0214726ED617100FF2E0F /* core.css in Resources */, + DDF9E1D828EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */, 5103A9F5242258C600410853 /* AccountsAddCloudKit.xib in Resources */, 65ED405E235DEF6C0081F399 /* DefaultFeeds.opml in Resources */, 51333D3C2468615D00EB5C91 /* AddRedditFeedSheet.xib in Resources */, @@ -3463,6 +3469,7 @@ 516A093723609A3600EAE89B /* SettingsComboTableViewCell.xib in Resources */, 51077C5A27A86D16000C71DB /* Hyperlegible.nnwtheme in Resources */, 516A09422361248000EAE89B /* Inspector.storyboard in Resources */, + DDF9E1D928EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */, 51DEE81A26FBFF84006DAA56 /* Promenade.nnwtheme in Resources */, 1768140B2564BB8300D98635 /* NetNewsWire_iOSwidgetextension_target.xcconfig in Resources */, 5103A9B424216A4200410853 /* blank.html in Resources */, @@ -3512,6 +3519,7 @@ B27EEBF9244D15F3000932E6 /* stylesheet.css in Resources */, 5144EA3B227A379E00D19003 /* ImportOPMLSheet.xib in Resources */, 844B5B691FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist in Resources */, + DDF9E1D728EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */, 5103A9F4242258C600410853 /* AccountsAddCloudKit.xib in Resources */, 51077C5827A86D16000C71DB /* Hyperlegible.nnwtheme in Resources */, 84A3EE5F223B667F00557320 /* DefaultFeeds.opml in Resources */, diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d6f3f5287..2921a00c2 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -60,8 +60,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/RSCore.git", "state": { "branch": null, - "revision": "4425a29db97b97c44e9ebee16e6090b116b10055", - "version": "1.0.14" + "revision": "a2f711d64af8f1baefdf0092f57a7f0df7f0e5e8", + "version": "1.0.15" } }, { diff --git a/Shared/ArticleStyles/ArticleThemesManager.swift b/Shared/ArticleStyles/ArticleThemesManager.swift index ab66608a9..352d1a360 100644 --- a/Shared/ArticleStyles/ArticleThemesManager.swift +++ b/Shared/ArticleStyles/ArticleThemesManager.swift @@ -31,19 +31,18 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { set { if newValue != currentThemeName { AppDefaults.shared.currentThemeName = newValue - updateThemeNames() - updateCurrentTheme() + currentTheme = articleThemeWithThemeName(newValue) } } } - var currentTheme: ArticleTheme { + lazy var currentTheme = { articleThemeWithThemeName(currentThemeName) }() { didSet { NotificationCenter.default.post(name: .CurrentArticleThemeDidChangeNotification, object: self) } } - var themeNames = [AppDefaults.defaultThemeName] { + lazy var themeNames = { buildThemeNames() }() { didSet { NotificationCenter.default.post(name: .ArticleThemeNamesDidChangeNotification, object: self) } @@ -51,7 +50,6 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { init(folderPath: String) { self.folderPath = folderPath - self.currentTheme = ArticleTheme.defaultTheme super.init() @@ -63,15 +61,12 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { abort() } - updateThemeNames() - updateCurrentTheme() - NSFileCoordinator.addFilePresenter(self) } func presentedSubitemDidChange(at url: URL) { - updateThemeNames() - updateCurrentTheme() + themeNames = buildThemeNames() + currentTheme = articleThemeWithThemeName(currentThemeName) } // MARK: API @@ -93,7 +88,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { try FileManager.default.copyItem(atPath: filename, toPath: toFilename) } - func articleThemeWithThemeName(_ themeName: String) -> ArticleTheme? { + func articleThemeWithThemeName(_ themeName: String) -> ArticleTheme { if themeName == AppDefaults.defaultThemeName { return ArticleTheme.defaultTheme } @@ -107,7 +102,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { path = installedPath isAppTheme = false } else { - return nil + return ArticleTheme.defaultTheme } do { @@ -115,7 +110,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { } catch { NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) logger.error("Failed to import theme: \(error.localizedDescription, privacy: .public)") - return nil + return ArticleTheme.defaultTheme } } @@ -132,7 +127,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { private extension ArticleThemesManager { - func updateThemeNames() { + func buildThemeNames() -> [String] { let appThemeFilenames = Bundle.main.paths(forResourcesOfType: ArticleTheme.nnwThemeSuffix, inDirectory: nil) let appThemeNames = Set(appThemeFilenames.map { ArticleTheme.themeNameForPath($0) }) @@ -140,32 +135,7 @@ private extension ArticleThemesManager { let allThemeNames = appThemeNames.union(installedThemeNames) - let sortedThemeNames = allThemeNames.sorted(by: { $0.compare($1, options: .caseInsensitive) == .orderedAscending }) - if sortedThemeNames != themeNames { - themeNames = sortedThemeNames - } - } - - func defaultArticleTheme() -> ArticleTheme { - return articleThemeWithThemeName(AppDefaults.defaultThemeName)! - } - - func updateCurrentTheme() { - var themeName = currentThemeName - if !themeNames.contains(themeName) { - themeName = AppDefaults.defaultThemeName - currentThemeName = AppDefaults.defaultThemeName - } - - var articleTheme = articleThemeWithThemeName(themeName) - if articleTheme == nil { - articleTheme = defaultArticleTheme() - currentThemeName = AppDefaults.defaultThemeName - } - - if let articleTheme = articleTheme, articleTheme != currentTheme { - currentTheme = articleTheme - } + return allThemeNames.sorted(by: { $0.compare($1, options: .caseInsensitive) == .orderedAscending }) } func allThemePaths(_ folder: String) -> [String] { diff --git a/Shared/Resources/notificationSoundBlip.mp3 b/Shared/Resources/notificationSoundBlip.mp3 new file mode 100644 index 000000000..f22ba4752 Binary files /dev/null and b/Shared/Resources/notificationSoundBlip.mp3 differ diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index 35523f595..2a20f693b 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -62,7 +62,7 @@ private extension UserNotificationManager { } content.body = ArticleStringFormatter.truncatedSummary(article) content.threadIdentifier = webFeed.webFeedID - content.sound = UNNotificationSound.default + content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: AppAssets.notificationSoundBlipFileName)) content.userInfo = [UserInfoKey.articlePath: article.pathUserInfo] content.categoryIdentifier = "NEW_ARTICLE_NOTIFICATION_CATEGORY" if let attachment = thumbnailAttachment(for: article, webFeed: webFeed) { diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 23e3a4d91..9c7faca3d 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -325,4 +325,9 @@ struct AppAssets { } } + static var notificationSoundBlipFileName: String = { + // https://freesound.org/people/cabled_mess/sounds/350862/ + return "notificationSoundBlip.mp3" + }() + } diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 12ce2aa90..a12370ba6 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -87,6 +87,8 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { return keyboardManager.keyCommands } + private var lastKnownDisplayMode: UISplitViewController.DisplayMode? + var currentUnreadCount: Int = 0 { didSet { updateUnreadCountIndicator() @@ -102,7 +104,6 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(reloadDueToThemeChange(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(configureAppearanceMenu(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(updateUnreadCountIndicator(_:)), name: UIDevice.orientationDidChangeNotification, object: nil) articleExtractorButton.addTarget(self, action: #selector(toggleArticleExtractor(_:)), for: .touchUpInside) toolbarItems?.insert(UIBarButtonItem(customView: articleExtractorButton), at: 6) @@ -343,42 +344,6 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { configureAppearanceMenu() } - - /// Updates the indicator count in the navigation bar. - /// For iPhone, this indicator is visible if the unread count is > 0. - /// For iPad, this indicator is visible if it is in `portrait` or `unknown` - /// orientation, **and** the unread count is > 0. - /// - Parameter sender: `Any` (optional) - @objc - public func updateUnreadCountIndicator(_ sender: Any? = nil) { - if UIDevice.current.userInterfaceIdiom == .phone { - if currentUnreadCount > 0 { - let unreadCountView = MasterTimelineUnreadCountView(frame: .zero) - unreadCountView.unreadCount = currentUnreadCount - unreadCountView.setFrameIfNotEqual(CGRect(x: 0, y: 0, width: unreadCountView.intrinsicContentSize.width, height: unreadCountView.intrinsicContentSize.height)) - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: unreadCountView) - } else { - navigationItem.leftBarButtonItem = nil - } - } else { - - if UIDevice.current.orientation.isPortrait || !UIDevice.current.orientation.isValidInterfaceOrientation { - if currentUnreadCount > 0 { - let unreadCountView = MasterTimelineUnreadCountView(frame: .zero) - unreadCountView.unreadCount = currentUnreadCount - unreadCountView.setFrameIfNotEqual(CGRect(x: 0, y: 0, width: unreadCountView.intrinsicContentSize.width, height: unreadCountView.intrinsicContentSize.height)) - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: unreadCountView) - } else { - navigationItem.leftBarButtonItem = nil - } - } else { - navigationItem.leftBarButtonItem = nil - } - } - } - - - // MARK: Notifications @objc dynamic func unreadCountDidChange(_ notification: Notification) { @@ -486,6 +451,12 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) { currentWebViewController?.setScrollPosition(isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) } + + public func splitViewControllerWillChangeTo(displayMode: UISplitViewController.DisplayMode) { + lastKnownDisplayMode = displayMode + updateUnreadCountIndicator() + } + } // MARK: Find in Article @@ -644,4 +615,15 @@ private extension ArticleViewController { return controller } + func updateUnreadCountIndicator() { + if currentUnreadCount > 0 && (traitCollection.userInterfaceIdiom == .phone || lastKnownDisplayMode == .secondaryOnly) { + let unreadCountView = MasterTimelineUnreadCountView(frame: .zero) + unreadCountView.unreadCount = currentUnreadCount + unreadCountView.setFrameIfNotEqual(CGRect(x: 0, y: 0, width: unreadCountView.intrinsicContentSize.width, height: unreadCountView.intrinsicContentSize.height)) + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: unreadCountView) + } else { + navigationItem.leftBarButtonItem = nil + } + } + } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 79fe5be0f..103200a22 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -289,15 +289,24 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { self.masterFeedViewController = rootSplitViewController.viewController(for: .primary) as? MasterFeedViewController self.masterFeedViewController.coordinator = self - self.masterFeedViewController?.navigationController?.delegate = self - + if let navController = self.masterFeedViewController?.navigationController { + navController.delegate = self + configureNavigationController(navController) + } + self.masterTimelineViewController = rootSplitViewController.viewController(for: .supplementary) as? MasterTimelineViewController self.masterTimelineViewController?.coordinator = self - self.masterTimelineViewController?.navigationController?.delegate = self + if let navController = self.masterTimelineViewController?.navigationController { + navController.delegate = self + configureNavigationController(navController) + } self.articleViewController = rootSplitViewController.viewController(for: .secondary) as? ArticleViewController self.articleViewController?.coordinator = self - + if let navController = self.articleViewController?.navigationController { + configureNavigationController(navController) + } + for sectionNode in treeController.rootNode.childNodes { markExpanded(sectionNode) shadowTable.append((sectionID: "", feedNodes: [FeedNode]())) @@ -1314,6 +1323,10 @@ extension SceneCoordinator: UISplitViewControllerDelegate { } } + func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) { + articleViewController?.splitViewControllerWillChangeTo(displayMode: displayMode) + } + } // MARK: UINavigationControllerDelegate @@ -1370,6 +1383,31 @@ extension SceneCoordinator: UINavigationControllerDelegate { private extension SceneCoordinator { + func configureNavigationController(_ navController: UINavigationController) { + + let scrollEdge = UINavigationBarAppearance() + scrollEdge.configureWithOpaqueBackground() + scrollEdge.shadowColor = nil + scrollEdge.shadowImage = UIImage() + + let standard = UINavigationBarAppearance() + standard.shadowColor = .opaqueSeparator + standard.shadowImage = UIImage() + + navController.navigationBar.standardAppearance = standard + navController.navigationBar.compactAppearance = standard + navController.navigationBar.scrollEdgeAppearance = scrollEdge + navController.navigationBar.compactScrollEdgeAppearance = scrollEdge + + navController.navigationBar.tintColor = AppAssets.primaryAccentColor + + let toolbarAppearance = UIToolbarAppearance() + navController.toolbar.standardAppearance = toolbarAppearance + navController.toolbar.compactAppearance = toolbarAppearance + navController.toolbar.scrollEdgeAppearance = toolbarAppearance + navController.toolbar.tintColor = AppAssets.primaryAccentColor + } + func markArticlesWithUndo(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) { guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager, completion: completion) else { diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 402f822ef..0eb98e50a 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -28,6 +28,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging { coordinator = SceneCoordinator(rootSplitViewController: rootViewController) rootViewController.coordinator = coordinator rootViewController.delegate = coordinator + rootViewController.showsSecondaryOnlyButton = true coordinator.restoreWindowState(session.stateRestorationActivity) diff --git a/iOS/Settings/ArticleThemesTableViewController.swift b/iOS/Settings/ArticleThemesTableViewController.swift index 66d118caf..cc2bcc465 100644 --- a/iOS/Settings/ArticleThemesTableViewController.swift +++ b/iOS/Settings/ArticleThemesTableViewController.swift @@ -72,9 +72,9 @@ class ArticleThemesTableViewController: UITableViewController, Logging { override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { guard let cell = tableView.cellForRow(at: indexPath), - let themeName = cell.textLabel?.text, - let theme = ArticleThemesManager.shared.articleThemeWithThemeName(themeName), - !theme.isAppTheme else { return nil } + let themeName = cell.textLabel?.text else { return nil } + + guard !ArticleThemesManager.shared.articleThemeWithThemeName(themeName).isAppTheme else { return nil } let deleteTitle = NSLocalizedString("Delete", comment: "Delete") let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completion) in