diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index 0989691cc..a6a996c62 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -18,8 +18,8 @@ import RSDatabase public final class AccountManager: UnreadCountProvider { public static var shared: AccountManager! - public static let netNewsWireNewsURL = "https://nnw.ranchero.com/feed.xml" - private static let jsonNetNewsWireNewsURL = "https://nnw.ranchero.com/feed.json" + public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml" + private static let jsonNetNewsWireNewsURL = "https://netnewswire.blog/feed.json" public let defaultAccount: Account @@ -246,7 +246,7 @@ public final class AccountManager: UnreadCountProvider { } } - public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() -> Void)? = nil) { + public func refreshAll(errorHandler: @escaping @MainActor (Error) -> Void, completion: (() -> Void)? = nil) { guard let reachability = try? Reachability(hostname: "apple.com"), reachability.connection != .unavailable else { return } let group = DispatchGroup() @@ -259,7 +259,9 @@ public final class AccountManager: UnreadCountProvider { case .success: break case .failure(let error): - errorHandler(error) + Task { @MainActor in + errorHandler(error) + } } } } diff --git a/Appcasts/netnewswire-beta.xml b/Appcasts/netnewswire-beta.xml index c4699ebef..6db0df038 100755 --- a/Appcasts/netnewswire-beta.xml +++ b/Appcasts/netnewswire-beta.xml @@ -5,8 +5,26 @@ https://ranchero.com/downloads/netnewswire-beta.xml Most recent NetNewsWire changes with links to updates. en - + + NetNewsWire 6.1.2 + Twitter integration has been removed: Twitter has suspended NetNewsWire, and Twitter is removing free access to the Twitter API

+

Since Twitter does not provide RSS feeds, we’ve had to use the Twitter API. Without free access to that API, we can’t read feeds from Twitter.

+

We’ve left your Twitter feeds intact. If you have any starred items from those feeds, they will remain as long as you don’t delete those feeds.

+

You can still read whatever you have already downloaded. However, those feeds will no longer update.

+

Other changes…

+

Fixed a crashing bug that could happen in the sidebar

+

Fixed a bug that could prevent users from accessing BazQux if an article was missing a field

+

Fixed an issue that could prevent Feedly users from syncing if they tried to mark too many articles as read at the same time

+

Updated the Safari extension icon (credit to Louie Mantia for the new icon)

+ ]]>
+ Sat, 08 Apr 2023 10:30:00 -0800 + + 10.15.0 +
+ + NetNewsWire 6.1.1b1 Fixed a bug that could prevent users from accessing BazQux if an article was missing a field

@@ -17,172 +35,5 @@ 10.15.0
- - NetNewsWire 6.1 - Article themes. Several themes ship with the app, and you can create your own. You can change the theme in Preferences or by adding the theme switcher to the toolbar

-

Copy URLs using repaired, rather than raw, feed links

-

Restore article scroll position on relaunching app

-

Added Copy Article URL and Copy External URL commands to the Edit menu

-

Fixed a bug where using cmd-Q wouldn’t always quit the app as quickly as one might prefer

-

Disallow creation of iCloud account in the app if iCloud and iCloud Drive aren’t both enabled

-

Fixed bug showing quote tweets that only included an image

-

Added a hidden pref to suppress downloading/syncing on start: `defaults write com.ranchero.NetNewsWire-Evergreen DevroeSuppressSyncOnLaunch -bool true`

-

Video autoplay is now disallowed

-

Article view now supports RTL layout

-

Fixed a few font and sizing issues

-

Updated built-in feeds

-

Better alignment for items in General Preferences pane

- ]]>
- Thu, 07 Apr 2022 10:05:00 -0700 - - 10.15.0 -
- - - NetNewsWire 6.1b5 - Updated built-in feeds

-

Building on a new Apple Silicon Mac — testing to make sure all’s well

- ]]>
- Mon, 04 Apr 2022 22:10:00 -0700 - - 10.15.0 -
- - - NetNewsWire 6.1b4 - Fixed a few font and sizing issues.

- ]]>
- Sun, 27 Feb 2022 21:50:00 -0800 - - 10.15.0 -
- - - NetNewsWire 6.1b3 - Two new themes: Hyperlegible and NewsFax

-

Change in how built-in themes work: they’re part of the app bundle and they’re not copied into the Themes folder. When a built-in theme changes in a new app release, anyone using that new version gets the changes to the built-in theme

- ]]>
- Thu, 10 Feb 2022 21:35:00 -0800 - - 10.15.0 -
- - - NetNewsWire 6.1b2 - Article themes. Several themes ship with the app, and you can create your own. You can change the theme in Preferences or by adding the theme switcher to the toolbar.

-

Copy URLs using repaired, rather than raw, feed links.

-

Restore article scroll position on relaunching app.

-

Added Copy Article URL and Copy External URL commands to the Edit menu.

-

Fixed a bug where using cmd-Q wouldn’t always quit the app as quickly as one might prefer.

-

Disallow creation of iCloud account in the app if iCloud and iCloud Drive aren’t both enabled.

-

Fixed bug showing quote tweets that only included an image.

-

Added a hidden pref to suppress downloading/syncing on start: `defaults write com.ranchero.NetNewsWire-Evergreen DevroeSuppressSyncOnLaunch -bool true`

-

Video autoplay is now disallowed.

-

Article view now supports RTL layout.

- ]]>
- Mon, 17 Jan 2022 17:45:00 -0800 - - 10.15.0 -
- - - - NetNewsWire 6.0.3 - Same as 6.0.3b2 except for the version number.

- ]]>
- Sun, 05 Sep 2021 12:20:00 -0700 - - 10.15.0 -
- - - - NetNewsWire 6.0.3b2 - Feedly: preserve custom feed names with Feedly when moving them between folders

-

Preferences: use full-width row style in accounts and extensions panes

-

Fixed a crashing bug triggered by running some UI code outside of main thread

-

Fixed a crashing bug that could happen when the app tries to find a feed for a website

-

Fixed a crashing bug that could happen when rendering tweets

-

Changed how images are placed in Twitter articles so that you can better see who Tweeted the image

-

Fixed bug where iCloud syncing could stop prematurely when the sync database has records not in the local database

-

Fixed bug where favicons wouldn’t be found when a home page URL has non-ASCII characters

-

Fixed bug where external URLs in Feedbin feeds might be lost

-

Fixed bug where words prepended with $ wouldn’t appear in Twitter feeds

-

Fixed bug where newlines would be just a space in Twitter feeds

-

Fixed bug where BazQux-synced feeds might stop updating

- ]]>
- Sun, 29 Aug 2021 15:25:00 -0700 - - 10.15.0 -
- - - NetNewsWire 6.0.3b1 - Feedly: handle API change with deleting and don’t show a spurious error

-

NewsBlur: don’t fetch articles marked hidden by NewsBlur

-

FreshRSS: add API endpoint URL example in setup form

-

iCloud: fixed bug not retaining feeds in a folder where the folder hasn’t been synced yet

-

Feeds list: smart feeds remain visible despite Hide Read Feeds setting

-

Keyboard shortcuts: fixed regression where L key wouldn’t go to next unread when feed is all read

-

Twitter extension: fixed weird bug where an extra https:/ could appear in tweet text

- ]]>
- Thu, 20 May 2021 20:00:00 -0700 - - 10.15.0 -
- - - NetNewsWire 6.0.2 - Same as 6.0.2b1 — no changes other than version

- ]]>
- Tue, 20 Apr 2021 17:40:00 -0700 - - 10.15.0 -
- - - NetNewsWire 6.0.2b1 - Inoreader sync: fixed (hopefully) cause of rate limit errors — now doing background sync of statuses much less often - note that this fix needs to be rolled out across all NetNewsWire users in order for it to have full effect

-

Fixed regression with the L key — now works properly again

- ]]>
- Thu, 15 Apr 2021 19:15:00 -0700 - - 10.15.0 -
- - - NetNewsWire 6.0.1 - Adjusted layout of the add account sheet so that it fits on smaller monitors

-

Sidebar: properly scale the smart feed icons when sidebar is set to large size in System Preferences

- ]]>
- Thu, 01 Apr 2021 20:22:00 -0700 - - 10.15.0 -
- - - NetNewsWire 6.0.1b2 - Twitter: fixed a date parsing bug that could affect people in some locales, which would prevent Twitter feeds from working for them

-

Feeds list: fixed bug where newly added feed would be called Untitled past the time when the app actually knows its name

-

Fixed bug where next-unread command wouldn’t wrap around when you got to the bottom of the Feeds list

- ]]>
- Mon, 29 Mar 2021 20:55:00 -0700 - - 10.15.0 -
- - diff --git a/Appcasts/netnewswire-release.xml b/Appcasts/netnewswire-release.xml index 7c9cc240e..5475e3878 100755 --- a/Appcasts/netnewswire-release.xml +++ b/Appcasts/netnewswire-release.xml @@ -6,6 +6,24 @@ Most recent NetNewsWire releases (not test builds). en + + NetNewsWire 6.1.2 + Twitter integration has been removed: Twitter has suspended NetNewsWire, and Twitter is removing free access to the Twitter API

+

Since Twitter does not provide RSS feeds, we’ve had to use the Twitter API. Without free access to that API, we can’t read feeds from Twitter.

+

We’ve left your Twitter feeds intact. If you have any starred items from those feeds, they will remain as long as you don’t delete those feeds.

+

You can still read whatever you have already downloaded. However, those feeds will no longer update.

+

Other changes…

+

Fixed a crashing bug that could happen in the sidebar

+

Fixed a bug that could prevent users from accessing BazQux if an article was missing a field

+

Fixed an issue that could prevent Feedly users from syncing if they tried to mark too many articles as read at the same time

+

Updated the Safari extension icon (credit to Louie Mantia for the new icon)

+ ]]>
+ Sat, 08 Apr 2023 10:30:00 -0800 + + 10.15.0 +
+ NetNewsWire 6.1 String? { return UserDefaults.standard.string(forKey: key) } - + static func setString(for key: String, _ value: String?) { UserDefaults.standard.set(value, forKey: key) } - + static func bool(for key: String) -> Bool { return UserDefaults.standard.bool(forKey: key) } @@ -425,11 +434,11 @@ private extension AppDefaults { static func int(for key: String) -> Int { return UserDefaults.standard.integer(forKey: key) } - + static func setInt(for key: String, _ x: Int) { UserDefaults.standard.set(x, forKey: key) } - + static func date(for key: String) -> Date? { return UserDefaults.standard.object(forKey: key) as? Date } diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 63676411e..6cde701c4 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -32,11 +32,11 @@ var appDelegate: AppDelegate! @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate, Logging { - + private struct WindowRestorationIdentifiers { static let mainWindow = "mainWindow" } - + var userNotificationManager: UserNotificationManager! var faviconDownloader: FaviconDownloader! var imageDownloader: ImageDownloader! @@ -44,13 +44,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, var webFeedIconDownloader: WebFeedIconDownloader! var extensionContainersFile: ExtensionContainersFile! var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile! - + var appName: String! - + var refreshTimer: AccountRefreshTimer? var syncTimer: ArticleStatusSyncTimer? var lastRefreshInterval = AppDefaults.shared.refreshInterval - + var shuttingDown = false { didSet { if shuttingDown { @@ -61,9 +61,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } } - + var isShutDownSyncDone = false - + @IBOutlet var shareMenuItem: NSMenuItem! @IBOutlet var fileMenuItem: NSMenuItem! @IBOutlet var debugMenuItem: NSMenuItem! @@ -71,7 +71,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @IBOutlet var sortByNewestArticleOnTopMenuItem: NSMenuItem! @IBOutlet var groupArticlesByFeedMenuItem: NSMenuItem! @IBOutlet var checkForUpdatesMenuItem: NSMenuItem! - + var unreadCount = 0 { didSet { if unreadCount != oldValue { @@ -80,7 +80,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } } - + private var mainWindowController: MainWindowController? { var bestController: MainWindowController? for candidateController in mainWindowControllers { @@ -94,7 +94,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } return bestController } - + private var mainWindowControllers = [MainWindowController]() private var preferencesWindowController: NSWindowController? private var addFeedController: AddFeedController? @@ -109,50 +109,50 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, private var softwareUpdater: SPUUpdater! private var crashReporter: PLCrashReporter! #endif - - override init() { + + @MainActor override init() { NSWindow.allowsAutomaticWindowTabbing = false super.init() - + #if !MAC_APP_STORE let crashReporterConfig = PLCrashReporterConfig.defaultConfiguration() crashReporter = PLCrashReporter(configuration: crashReporterConfig) crashReporter.enable() #endif - + SecretsManager.provider = Secrets() AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!) ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!) FeedProviderManager.shared.delegate = ExtensionPointManager.shared - + NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil) NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil) - + appDelegate = self - + presentTwitterDeprecationAlertIfRequired() } - + // MARK: - API - func showAddFolderSheetOnWindow(_ window: NSWindow) { + @MainActor func showAddFolderSheetOnWindow(_ window: NSWindow) { addFolderWindowController = AddFolderWindowController() addFolderWindowController!.runSheetOnWindow(window) } - - func showAddWebFeedSheetOnWindow(_ window: NSWindow, urlString: String?, name: String?, account: Account?, folder: Folder?) { + + @MainActor func showAddWebFeedSheetOnWindow(_ window: NSWindow, urlString: String?, name: String?, account: Account?, folder: Folder?) { addFeedController = AddFeedController(hostWindow: window) addFeedController?.showAddFeedSheet(.webFeed, urlString, name, account, folder) } - + // MARK: - NSApplicationDelegate - - func applicationWillFinishLaunching(_ notification: Notification) { + + @MainActor func applicationWillFinishLaunching(_ notification: Notification) { installAppleEventHandlers() - + CacheCleaner.purgeIfNecessary() - + // Try to establish a cache in the Caches folder, but if it fails for some reason fall back to a temporary dir let cacheFolder: String if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path { @@ -162,25 +162,25 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String) cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier) } - + let faviconsFolder = (cacheFolder as NSString).appendingPathComponent("Favicons") let faviconsFolderURL = URL(fileURLWithPath: faviconsFolder) try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil) faviconDownloader = FaviconDownloader(folder: faviconsFolder) - + let imagesFolder = (cacheFolder as NSString).appendingPathComponent("Images") let imagesFolderURL = URL(fileURLWithPath: imagesFolder) try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil) imageDownloader = ImageDownloader(folder: imagesFolder) - + authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader) webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder) - + appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String) } - - func applicationDidFinishLaunching(_ note: Notification) { - + + @MainActor func applicationDidFinishLaunching(_ note: Notification) { + #if MAC_APP_STORE || TEST checkForUpdatesMenuItem.isHidden = true #else @@ -188,7 +188,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let hostBundle = Bundle.main let updateDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: self) self.softwareUpdater = SPUUpdater(hostBundle: hostBundle, applicationBundle: hostBundle, userDriver: updateDriver, delegate: self) - + do { try self.softwareUpdater.start() } @@ -196,55 +196,55 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, logger.error("Failed to start software updater with error: \(error.localizedDescription, privacy: .public)") } #endif - + AppDefaults.shared.registerDefaults() let isFirstRun = AppDefaults.shared.isFirstRun if isFirstRun { logger.debug("Is first run") } let localAccount = AccountManager.shared.defaultAccount - + if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() { // Import feeds. Either old NNW 3 feeds or the default feeds. if !NNW3ImportController.importSubscriptionsIfFileExists(account: localAccount) { DefaultFeedsImporter.importDefaultFeeds(account: localAccount) } } - + updateSortMenuItems() updateGroupByFeedMenuItem() - + if mainWindowController == nil { let mainWindowController = createAndShowMainWindow() mainWindowController.restoreStateFromUserDefaults() } - + fileMenuItem.submenu?.delegate = self shareMenuItem.submenu?.delegate = self - + if isFirstRun { mainWindowController?.window?.center() } - + NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) - + DispatchQueue.main.async { self.unreadCount = AccountManager.shared.unreadCount } - + if InspectorWindowController.shouldOpenAtStartup { self.toggleInspectorWindow(self) } - + extensionContainersFile = ExtensionContainersFile() extensionFeedAddRequestFile = ExtensionFeedAddRequestFile() - + refreshTimer = AccountRefreshTimer() syncTimer = ArticleStatusSyncTimer() - + UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .alert, .sound]) { (granted, error) in } - + UNUserNotificationCenter.current().getNotificationSettings { (settings) in if settings.authorizationStatus == .authorized { DispatchQueue.main.async { @@ -252,10 +252,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } } - + UNUserNotificationCenter.current().delegate = self userNotificationManager = UserNotificationManager() - + #if DEBUG refreshTimer!.update() syncTimer!.update() @@ -270,7 +270,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } #endif - + if AppDefaults.shared.showDebugMenu { // The Web Inspector uses SPI and can never appear in a MAC_APP_STORE build. #if MAC_APP_STORE @@ -283,24 +283,24 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } else { debugMenuItem.menu?.removeItem(debugMenuItem) } - + #if !MAC_APP_STORE DispatchQueue.main.async { CrashReporter.check(crashReporter: self.crashReporter) } #endif - + } - - func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool { + + @MainActor func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool { guard let mainWindowController = mainWindowController else { return false } mainWindowController.handle(userActivity) return true } - - func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + + @MainActor func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { // https://github.com/brentsimmons/NetNewsWire/issues/522 // I couldn’t reproduce the crashing bug, but it appears to happen on creating a main window // and its views and view controllers. The check below is so that the app does nothing @@ -313,43 +313,43 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, mainWindowController.showWindow(self) return false } - - func applicationDidBecomeActive(_ notification: Notification) { + + @MainActor func applicationDidBecomeActive(_ notification: Notification) { fireOldTimers() } - - func applicationDidResignActive(_ notification: Notification) { + + @MainActor func applicationDidResignActive(_ notification: Notification) { ArticleStringFormatter.emptyCaches() saveState() } - - func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { + + @MainActor func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) } - - func application(_ sender: NSApplication, openFile filename: String) -> Bool { + + @MainActor func application(_ sender: NSApplication, openFile filename: String) -> Bool { guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false } importTheme(filename: filename) return true } - - func applicationWillTerminate(_ notification: Notification) { + + @MainActor func applicationWillTerminate(_ notification: Notification) { shuttingDown = true saveState() - + ArticleThemeDownloader.shared.cleanUp() - + AccountManager.shared.sendArticleStatusAll() { self.isShutDownSyncDone = true } - + let timeout = Date().addingTimeInterval(2) while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: timeout) && timeout > Date() { } } - - func presentThemeImportError(_ error: Error) { + + @MainActor func presentThemeImportError(_ error: Error) { var informativeText: String = "" - + if let decodingError = error as? DecodingError { switch decodingError { case .typeMismatch(let type, _): @@ -369,14 +369,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } let localizedError = NSLocalizedString("alert.error.theme-data-corruption.%@", comment: "This theme cannot be used because of data corruption in the Info.plist: %@.") informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String - + default: informativeText = error.localizedDescription } } else { informativeText = error.localizedDescription } - + DispatchQueue.main.async { let alert = NSAlert() alert.alertStyle = .warning @@ -385,20 +385,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, alert.addButton(withTitle: NSLocalizedString("button.title.ok", comment: "OK")) alert.buttons[0].keyEquivalent = "\r" - - let response = alert.runModal() + + _ = alert.runModal() } } - + // MARK: Notifications - - @objc func unreadCountDidChange(_ note: Notification) { + + @MainActor @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { unreadCount = AccountManager.shared.unreadCount } } - - @objc func webFeedSettingDidChange(_ note: Notification) { + + @MainActor @objc func webFeedSettingDidChange(_ note: Notification) { guard let feed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else { return } @@ -406,31 +406,31 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let _ = faviconDownloader.favicon(for: feed) } } - - @objc func inspectableObjectsDidChange(_ note: Notification) { + + @MainActor @objc func inspectableObjectsDidChange(_ note: Notification) { guard let inspectorWindowController = inspectorWindowController, inspectorWindowController.isOpen else { return } inspectorWindowController.objects = objectsForInspector() } - - @objc func userDefaultsDidChange(_ note: Notification) { + + @MainActor @objc func userDefaultsDidChange(_ note: Notification) { updateSortMenuItems() updateGroupByFeedMenuItem() - + if lastRefreshInterval != AppDefaults.shared.refreshInterval { refreshTimer?.update() lastRefreshInterval = AppDefaults.shared.refreshInterval } - + updateDockBadge() } - - @objc func didWakeNotification(_ note: Notification) { + + @MainActor @objc func didWakeNotification(_ note: Notification) { fireOldTimers() } - - @objc func importDownloadedTheme(_ note: Notification) { + + @MainActor @objc func importDownloadedTheme(_ note: Notification) { guard let userInfo = note.userInfo, let url = userInfo["url"] as? URL else { return @@ -439,108 +439,108 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, self.importTheme(filename: url.path) } } - + // MARK: Main Window - - func createMainWindowController() -> MainWindowController { + + @MainActor func createMainWindowController() -> MainWindowController { let controller: MainWindowController controller = windowControllerWithName("MainWindow") as! MainWindowController - + if !(mainWindowController?.isOpen ?? false) { mainWindowControllers.removeAll() } mainWindowControllers.append(controller) return controller } - - func windowControllerWithName(_ storyboardName: String) -> NSWindowController { + + @MainActor func windowControllerWithName(_ storyboardName: String) -> NSWindowController { let storyboard = NSStoryboard(name: NSStoryboard.Name(storyboardName), bundle: nil) return storyboard.instantiateInitialController()! as! NSWindowController } - + @discardableResult - func createAndShowMainWindow() -> MainWindowController { + @MainActor func createAndShowMainWindow() -> MainWindowController { let controller = createMainWindowController() controller.showWindow(self) - + if let window = controller.window { window.restorationClass = Self.self window.identifier = NSUserInterfaceItemIdentifier(rawValue: WindowRestorationIdentifiers.mainWindow) } - + return controller } - - func createAndShowMainWindowIfNecessary() { + + @MainActor func createAndShowMainWindowIfNecessary() { if mainWindowController == nil { createAndShowMainWindow() } else { mainWindowController?.showWindow(self) } } - - func removeMainWindow(_ windowController: MainWindowController) { + + @MainActor func removeMainWindow(_ windowController: MainWindowController) { guard mainWindowControllers.count > 1 else { return } if let index = mainWindowControllers.firstIndex(of: windowController) { mainWindowControllers.remove(at: index) } } - + // MARK: NSUserInterfaceValidations - func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { + @MainActor func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { if shuttingDown { return false } - + let isDisplayingSheet = mainWindowController?.isDisplayingSheet ?? false let isSpecialAccountAvailable = AccountManager.shared.activeAccounts.contains(where: { $0.type == .onMyMac || $0.type == .cloudKit }) - + if item.action == #selector(refreshAll(_:)) { return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty } - + if item.action == #selector(importOPMLFromFile(_:)) { return AccountManager.shared.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) }) } - + if item.action == #selector(addAppNews(_:)) { return !isDisplayingSheet && !AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() && !AccountManager.shared.activeAccounts.isEmpty } - + if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) { return mainWindowController?.isOpen ?? false } - + if item.action == #selector(showAddWebFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) { return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty } - + if item.action == #selector(showAddRedditFeedWindow(_:)) { guard !isDisplayingSheet && isSpecialAccountAvailable && ExtensionPointManager.shared.isRedditEnabled else { return false } return ExtensionPointManager.shared.isRedditEnabled } - + #if !MAC_APP_STORE if item.action == #selector(toggleWebInspectorEnabled(_:)) { (item as! NSMenuItem).state = AppDefaults.shared.webInspectorEnabled ? .on : .off } #endif - + return true } // MARK: UNUserNotificationCenterDelegate - - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + + @MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .badge, .sound]) } - - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - + + @MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + let userInfo = response.notification.request.content.userInfo - + switch response.actionIdentifier { case "MARK_AS_READ": handleMarkAsRead(userInfo: userInfo) @@ -551,11 +551,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } completionHandler() } - + // MARK: Add Feed - func addWebFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) { + @MainActor func addWebFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) { createAndShowMainWindowIfNecessary() - + if mainWindowController!.isDisplayingSheet { return } @@ -564,13 +564,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: - Dock Badge - @objc func updateDockBadge() { + @MainActor @objc func updateDockBadge() { let label = unreadCount > 0 ? "\(unreadCount)" : "" NSApplication.shared.dockTile.badgeLabel = label } // MARK: - Actions - @IBAction func showPreferences(_ sender: Any?) { + @MainActor @IBAction func showPreferences(_ sender: Any?) { if preferencesWindowController == nil { preferencesWindowController = windowControllerWithName("Preferences") } @@ -578,35 +578,35 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, preferencesWindowController!.showWindow(self) } - @IBAction func newMainWindow(_ sender: Any?) { + @MainActor @IBAction func newMainWindow(_ sender: Any?) { createAndShowMainWindow() } - @IBAction func showMainWindow(_ sender: Any?) { + @MainActor @IBAction func showMainWindow(_ sender: Any?) { createAndShowMainWindowIfNecessary() mainWindowController?.window?.makeKey() } - @IBAction func refreshAll(_ sender: Any?) { + @MainActor @IBAction func refreshAll(_ sender: Any?) { AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present) } - @IBAction func showAddWebFeedWindow(_ sender: Any?) { + @MainActor @IBAction func showAddWebFeedWindow(_ sender: Any?) { addWebFeed(nil) } - @IBAction func showAddRedditFeedWindow(_ sender: Any?) { + @MainActor @IBAction func showAddRedditFeedWindow(_ sender: Any?) { createAndShowMainWindowIfNecessary() addFeedController = AddFeedController(hostWindow: mainWindowController!.window!) addFeedController?.showAddFeedSheet(.redditFeed) } - @IBAction func showAddFolderWindow(_ sender: Any?) { + @MainActor @IBAction func showAddFolderWindow(_ sender: Any?) { createAndShowMainWindowIfNecessary() showAddFolderSheetOnWindow(mainWindowController!.window!) } - @IBAction func showKeyboardShortcutsWindow(_ sender: Any?) { + @MainActor @IBAction func showKeyboardShortcutsWindow(_ sender: Any?) { if keyboardShortcutsWindowController == nil { keyboardShortcutsWindowController = WebViewWindowController(title: NSLocalizedString("window.title.keyboard-shortcuts", comment: "Keyboard Shortcuts")) @@ -619,13 +619,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let minSize = NSSize(width: 400, height: 400) window.setPointAndSizeAdjustingForScreen(point: point, size: size, minimumSize: minSize) } - + } keyboardShortcutsWindowController!.showWindow(self) } - @IBAction func toggleInspectorWindow(_ sender: Any?) { + @MainActor @IBAction func toggleInspectorWindow(_ sender: Any?) { if inspectorWindowController == nil { inspectorWindowController = (windowControllerWithName("Inspector") as! InspectorWindowController) } @@ -639,58 +639,58 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } - @IBAction func importOPMLFromFile(_ sender: Any?) { + @MainActor @IBAction func importOPMLFromFile(_ sender: Any?) { createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { return } - + importOPMLController = ImportOPMLWindowController() importOPMLController?.runSheetOnWindow(mainWindowController!.window!) } - - @IBAction func importNNW3FromFile(_ sender: Any?) { + + @MainActor @IBAction func importNNW3FromFile(_ sender: Any?) { createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { return } NNW3ImportController.askUserToImportNNW3Subscriptions(window: mainWindowController!.window!) } - - @IBAction func exportOPML(_ sender: Any?) { + + @MainActor @IBAction func exportOPML(_ sender: Any?) { createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { return } - + exportOPMLController = ExportOPMLWindowController() exportOPMLController?.runSheetOnWindow(mainWindowController!.window!) } - - @IBAction func addAppNews(_ sender: Any?) { + + @MainActor @IBAction func addAppNews(_ sender: Any?) { if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() { return } addWebFeed(AccountManager.netNewsWireNewsURL, name: "NetNewsWire News") } - @IBAction func openWebsite(_ sender: Any?) { + @MainActor @IBAction func openWebsite(_ sender: Any?) { Browser.open("https://netnewswire.com/", inBackground: false) } - - @IBAction func showHelp(_ sender: Any?) { + + @MainActor @IBAction func showHelp(_ sender: Any?) { Browser.open("https://netnewswire.com/help/mac/6.1/en/", inBackground: false) } - @IBAction func gotoToday(_ sender: Any?) { + @MainActor @IBAction func gotoToday(_ sender: Any?) { createAndShowMainWindowIfNecessary() mainWindowController!.gotoToday(sender) } - @IBAction func gotoAllUnread(_ sender: Any?) { + @MainActor @IBAction func gotoAllUnread(_ sender: Any?) { createAndShowMainWindowIfNecessary() mainWindowController!.gotoAllUnread(sender) @@ -702,27 +702,27 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, mainWindowController!.gotoStarred(sender) } - @IBAction func sortByOldestArticleOnTop(_ sender: Any?) { + @MainActor @IBAction func sortByOldestArticleOnTop(_ sender: Any?) { AppDefaults.shared.timelineSortDirection = .orderedAscending } - @IBAction func sortByNewestArticleOnTop(_ sender: Any?) { + @MainActor @IBAction func sortByNewestArticleOnTop(_ sender: Any?) { AppDefaults.shared.timelineSortDirection = .orderedDescending } - - @IBAction func groupByFeedToggled(_ sender: NSMenuItem) { + + @MainActor @IBAction func groupByFeedToggled(_ sender: NSMenuItem) { AppDefaults.shared.timelineGroupByFeed.toggle() } - @IBAction func checkForUpdates(_ sender: Any?) { + @MainActor @IBAction func checkForUpdates(_ sender: Any?) { #if !MAC_APP_STORE && !TEST self.softwareUpdater.checkForUpdates() #endif } - - @IBAction func showAbout(_ sender: Any?) { + + @MainActor @IBAction func showAbout(_ sender: Any?) { if #available(macOS 12, *) { for window in NSApplication.shared.windows { if window.identifier == .aboutNetNewsWire { @@ -741,26 +741,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, // MARK: - NSMenuDelegate -extension AppDelegate: NSMenuDelegate { +@MainActor extension AppDelegate: NSMenuDelegate { public func menuNeedsUpdate(_ menu: NSMenu) { let newShareMenu = mainWindowController?.shareMenu - + guard menu != fileMenuItem.submenu else { shareMenuItem.isEnabled = newShareMenu != nil return } - + menu.removeAllItems() if let newShareMenu = newShareMenu { menu.takeItems(from: newShareMenu) } } - + } // MARK: - Debug Menu -extension AppDelegate { +@MainActor extension AppDelegate { @IBAction func debugSearch(_ sender: Any?) { AccountManager.shared.defaultAccount.debugRunSearch() @@ -787,7 +787,7 @@ extension AppDelegate { let configuration = NSWorkspace.OpenConfiguration() configuration.createsNewApplicationInstance = true NSWorkspace.shared.openApplication(at: Bundle.main.bundleURL, configuration: configuration) - + NSApp.terminate(self) } } @@ -832,7 +832,7 @@ extension AppDelegate { } -internal extension AppDelegate { +@MainActor internal extension AppDelegate { func fireOldTimers() { // It’s possible there’s a refresh timer set to go off in the past. @@ -840,7 +840,7 @@ internal extension AppDelegate { refreshTimer?.fireOldTimer() syncTimer?.fireOldTimer() } - + func objectsForInspector() -> [Any]? { guard let window = NSApplication.shared.mainWindow, let windowController = window.windowController as? MainWindowController else { return nil @@ -858,15 +858,15 @@ internal extension AppDelegate { sortByNewestArticleOnTopMenuItem.state = sortByNewestOnTop ? .on : .off sortByOldestArticleOnTopMenuItem.state = sortByNewestOnTop ? .off : .on } - + func updateGroupByFeedMenuItem() { let groupByFeedEnabled = AppDefaults.shared.timelineGroupByFeed groupArticlesByFeedMenuItem.state = groupByFeedEnabled ? .on : .off } - + func importTheme(filename: String) { guard let window = mainWindowController?.window else { return } - + do { let theme = try ArticleTheme(path: filename, isAppTheme: false) let alert = NSAlert() @@ -874,11 +874,11 @@ internal extension AppDelegate { let localizedMessageText = NSLocalizedString("alert.title.install-theme.%@.%@", comment: "Install theme “%@” by %@? — the order of the variables is theme name, author name") alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name, theme.creatorName) as String - + var attrs = [NSAttributedString.Key : Any]() attrs[.font] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) attrs[.foregroundColor] = NSColor.textColor - + let titleParagraphStyle = NSMutableParagraphStyle() titleParagraphStyle.alignment = .center attrs[.paragraphStyle] = titleParagraphStyle @@ -912,7 +912,7 @@ internal extension AppDelegate { logger.error("Error importing theme: \(error.localizedDescription, privacy: .public)") } } - + alert.beginSheetModal(for: window) { result in if result == NSApplication.ModalResponse.alertFirstButtonReturn { @@ -940,10 +940,10 @@ internal extension AppDelegate { presentThemeImportError(error) } } - + func confirmImportSuccess(themeName: String) { guard let window = mainWindowController?.window else { return } - + let alert = NSAlert() alert.alertStyle = .informational alert.messageText = NSLocalizedString("alert.title.theme-installed", comment: "Theme installed") @@ -964,13 +964,13 @@ internal extension AppDelegate { if currentDate > expiryDate { return // If after August 9th, don't show } - + if AccountManager.shared.anyLocalOriCloudAccountHasAtLeastOneTwitterFeed() { showTwitterDeprecationAlert() } AppDefaults.shared.twitterDeprecationAlertShown = true } - + private func showTwitterDeprecationAlert() { DispatchQueue.main.async { let alert = NSAlert() @@ -982,13 +982,12 @@ internal extension AppDelegate { alert.runModal() } } - } /* the ScriptingAppDelegate protocol exposes a narrow set of accessors with internal visibility which are very similar to some private vars. - + These would be unnecessary if the similar accessors were marked internal rather than private, but for now, we'll keep the stratification of visibility */ @@ -1007,8 +1006,8 @@ extension AppDelegate : ScriptingAppDelegate { } } -extension AppDelegate: NSWindowRestoration { - +@MainActor extension AppDelegate: NSWindowRestoration { + @objc static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: @escaping (NSWindow?, Error?) -> Void) { var mainWindow: NSWindow? = nil if identifier.rawValue == WindowRestorationIdentifiers.mainWindow { @@ -1016,39 +1015,39 @@ extension AppDelegate: NSWindowRestoration { } completionHandler(mainWindow, nil) } - + } // Handle Notification Actions -private extension AppDelegate { - +@MainActor private extension AppDelegate { + func handleMarkAsRead(userInfo: [AnyHashable: Any]) { markArticle(userInfo: userInfo, statusKey: .read) } - + func handleMarkAsStarred(userInfo: [AnyHashable: Any]) { markArticle(userInfo: userInfo, statusKey: .starred) } - + func markArticle(userInfo: [AnyHashable: Any], statusKey: ArticleStatus.Key) { guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { return } - + guard let account = AccountManager.shared.existingAccount(with: accountID) else { logger.debug("No account found from notification.") return } - + guard let articles = try? account.fetchArticles(.articleIDs([articleID])), !articles.isEmpty else { logger.debug("No article found from search using: \(articleID, privacy: .public)") return } - + account.mark(articles: articles, statusKey: statusKey, flag: true) { _ in } } - + } diff --git a/Mac/Base.lproj/Preferences.storyboard b/Mac/Base.lproj/Preferences.storyboard index f7f2b1682..3c938cc9d 100644 --- a/Mac/Base.lproj/Preferences.storyboard +++ b/Mac/Base.lproj/Preferences.storyboard @@ -32,22 +32,30 @@ - + - - + + + + + + + + + + - + @@ -76,7 +84,7 @@ - + @@ -91,7 +99,7 @@ - + @@ -158,15 +166,15 @@ - - + + - + @@ -201,15 +209,15 @@ - - + + - - + + + + + + + + - - - - - - + + + + + - - - - - @@ -277,31 +304,32 @@ - + + - - + + - - - + + + + - + + - - @@ -427,35 +455,40 @@ - - - + + - + + + - + @@ -475,7 +508,6 @@ - @@ -642,7 +674,7 @@ - + @@ -809,7 +841,7 @@ - + diff --git a/Mac/Browser.swift b/Mac/Browser.swift index 6bf3bc982..68148ce8e 100644 --- a/Mac/Browser.swift +++ b/Mac/Browser.swift @@ -9,7 +9,7 @@ import Foundation import RSWeb -struct Browser { +@MainActor struct Browser { /// The user-specified default browser for opening web pages. /// diff --git a/Mac/CrashReporter/CrashReportWindowController.swift b/Mac/CrashReporter/CrashReportWindowController.swift index 7c7ce9ee7..5579e0ef3 100644 --- a/Mac/CrashReporter/CrashReportWindowController.swift +++ b/Mac/CrashReporter/CrashReportWindowController.swift @@ -8,7 +8,7 @@ import AppKit -final class CrashReportWindowController: NSWindowController { +@MainActor final class CrashReportWindowController: NSWindowController { @IBOutlet var textView: NSTextView! { didSet { diff --git a/Mac/CrashReporter/CrashReporter.swift b/Mac/CrashReporter/CrashReporter.swift index b37810a9d..09d1db06b 100644 --- a/Mac/CrashReporter/CrashReporter.swift +++ b/Mac/CrashReporter/CrashReporter.swift @@ -16,7 +16,7 @@ import CrashReporter // At some point this code should probably move into RSCore, so Rainier and any other // future apps can use it. -struct CrashReporter { +@MainActor struct CrashReporter { struct DefaultsKey { static let sendCrashLogsAutomaticallyKey = "SendCrashLogsAutomatically" diff --git a/Mac/ErrorHandler.swift b/Mac/ErrorHandler.swift index 4ec6f59be..1d332359c 100644 --- a/Mac/ErrorHandler.swift +++ b/Mac/ErrorHandler.swift @@ -10,16 +10,13 @@ import AppKit import Account import RSCore -struct ErrorHandler: Logging { +@MainActor struct ErrorHandler: Logging { - - public static func present(_ error: Error) { NSApplication.shared.presentError(error) } - + public static func log(_ error: Error) { ErrorHandler.logger.error("\(error.localizedDescription, privacy: .public)") } - } diff --git a/Mac/Inspector/BuiltinSmartFeedInspectorViewController.swift b/Mac/Inspector/BuiltinSmartFeedInspectorViewController.swift index 289c953a1..884aed07c 100644 --- a/Mac/Inspector/BuiltinSmartFeedInspectorViewController.swift +++ b/Mac/Inspector/BuiltinSmartFeedInspectorViewController.swift @@ -8,7 +8,7 @@ import AppKit -final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector { +@MainActor final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector { @IBOutlet var nameTextField: NSTextField? @IBOutlet weak var smartFeedImageView: NSImageView! diff --git a/Mac/Inspector/FolderInspectorViewController.swift b/Mac/Inspector/FolderInspectorViewController.swift index 421148f9e..8fa655396 100644 --- a/Mac/Inspector/FolderInspectorViewController.swift +++ b/Mac/Inspector/FolderInspectorViewController.swift @@ -10,7 +10,7 @@ import AppKit import Account import RSCore -final class FolderInspectorViewController: NSViewController, Inspector { +@MainActor final class FolderInspectorViewController: NSViewController, Inspector { @IBOutlet var nameTextField: NSTextField? @IBOutlet weak var folderImageView: NSImageView! diff --git a/Mac/Inspector/InspectorWindowController.swift b/Mac/Inspector/InspectorWindowController.swift index 2686f86c4..078de1439 100644 --- a/Mac/Inspector/InspectorWindowController.swift +++ b/Mac/Inspector/InspectorWindowController.swift @@ -8,7 +8,7 @@ import AppKit -protocol Inspector: AnyObject { +@MainActor protocol Inspector: AnyObject { var objects: [Any]? { get set } var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects. @@ -20,7 +20,7 @@ protocol Inspector: AnyObject { typealias InspectorViewController = Inspector & NSViewController -final class InspectorWindowController: NSWindowController { +@MainActor final class InspectorWindowController: NSWindowController { class var shouldOpenAtStartup: Bool { return UserDefaults.standard.bool(forKey: DefaultsKey.windowIsOpen) diff --git a/Mac/Inspector/NothingInspectorViewController.swift b/Mac/Inspector/NothingInspectorViewController.swift index f18782a2c..a4b6ae79c 100644 --- a/Mac/Inspector/NothingInspectorViewController.swift +++ b/Mac/Inspector/NothingInspectorViewController.swift @@ -8,7 +8,7 @@ import AppKit -final class NothingInspectorViewController: NSViewController, Inspector { +@MainActor final class NothingInspectorViewController: NSViewController, Inspector { @IBOutlet var nothingTextField: NSTextField? @IBOutlet var multipleTextField: NSTextField? diff --git a/Mac/Inspector/WebFeedInspectorViewController.swift b/Mac/Inspector/WebFeedInspectorViewController.swift index 7944c92b6..fb8ceecd0 100644 --- a/Mac/Inspector/WebFeedInspectorViewController.swift +++ b/Mac/Inspector/WebFeedInspectorViewController.swift @@ -11,7 +11,7 @@ import Articles import Account import UserNotifications -final class WebFeedInspectorViewController: NSViewController, Inspector { +@MainActor final class WebFeedInspectorViewController: NSViewController, Inspector { @IBOutlet weak var iconView: IconView! @IBOutlet weak var nameTextField: NSTextField? diff --git a/Mac/MainWindow/About/CreditsNetNewsWireView.swift b/Mac/MainWindow/About/CreditsNetNewsWireView.swift index 88a63fe75..f72318696 100644 --- a/Mac/MainWindow/About/CreditsNetNewsWireView.swift +++ b/Mac/MainWindow/About/CreditsNetNewsWireView.swift @@ -66,7 +66,9 @@ struct CreditsNetNewsWireView: View, LoadableAboutData { .onTapGesture { guard let url = appCredit.url else { return } if let _ = URL(string: url) { - Browser.open(url, inBackground: false) + Task { @MainActor in + Browser.open(url, inBackground: false) + } } } } diff --git a/Mac/MainWindow/AddFeed/AddFeedController.swift b/Mac/MainWindow/AddFeed/AddFeedController.swift index 226d1b7c8..824687187 100644 --- a/Mac/MainWindow/AddFeed/AddFeedController.swift +++ b/Mac/MainWindow/AddFeed/AddFeedController.swift @@ -22,7 +22,7 @@ import RSParser // Else, // display error sheet. -class AddFeedController: AddFeedWindowControllerDelegate { +@MainActor final class AddFeedController: AddFeedWindowControllerDelegate { private let hostWindow: NSWindow private var addFeedWindowController: AddFeedWindowController? diff --git a/Mac/MainWindow/AddFeed/AddFeedWIndowController.swift b/Mac/MainWindow/AddFeed/AddFeedWIndowController.swift index e574d7fa5..50ad03701 100644 --- a/Mac/MainWindow/AddFeed/AddFeedWIndowController.swift +++ b/Mac/MainWindow/AddFeed/AddFeedWIndowController.swift @@ -14,7 +14,7 @@ enum AddFeedWindowControllerType { case redditFeed } -protocol AddFeedWindowControllerDelegate: AnyObject { +@MainActor protocol AddFeedWindowControllerDelegate: AnyObject { // userEnteredURL will have already been validated and normalized. func addFeedWindowController(_: AddFeedWindowController, userEnteredURL: URL, userEnteredTitle: String?, container: Container) @@ -22,7 +22,7 @@ protocol AddFeedWindowControllerDelegate: AnyObject { } -protocol AddFeedWindowController { +@MainActor protocol AddFeedWindowController { var window: NSWindow? { get } func runSheetOnWindow(_ hostWindow: NSWindow) diff --git a/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift b/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift index 16c8a4d72..40b6f6301 100644 --- a/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift +++ b/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift @@ -12,7 +12,7 @@ import RSTree import Articles import Account -class AddWebFeedWindowController : NSWindowController, AddFeedWindowController { +@MainActor final class AddWebFeedWindowController : NSWindowController, AddFeedWindowController { @IBOutlet var urlTextField: NSTextField! @IBOutlet var nameTextField: NSTextField! diff --git a/Mac/MainWindow/AddFeed/FolderTreeMenu.swift b/Mac/MainWindow/AddFeed/FolderTreeMenu.swift index 87250dd6b..1c696f0dc 100644 --- a/Mac/MainWindow/AddFeed/FolderTreeMenu.swift +++ b/Mac/MainWindow/AddFeed/FolderTreeMenu.swift @@ -11,7 +11,7 @@ import RSCore import RSTree import Account -class FolderTreeMenu { +@MainActor final class FolderTreeMenu { static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu { diff --git a/Mac/MainWindow/AddFolder/AddFolderWindowController.swift b/Mac/MainWindow/AddFolder/AddFolderWindowController.swift index 84053ee20..08cc50310 100644 --- a/Mac/MainWindow/AddFolder/AddFolderWindowController.swift +++ b/Mac/MainWindow/AddFolder/AddFolderWindowController.swift @@ -10,7 +10,7 @@ import AppKit import Articles import Account -class AddFolderWindowController : NSWindowController { +@MainActor final class AddFolderWindowController : NSWindowController { @IBOutlet var folderNameTextField: NSTextField! @IBOutlet var accountPopupButton: NSPopUpButton! diff --git a/Mac/MainWindow/AddRedditFeedWindowController.swift b/Mac/MainWindow/AddRedditFeedWindowController.swift index d418dc94f..fc744d96f 100644 --- a/Mac/MainWindow/AddRedditFeedWindowController.swift +++ b/Mac/MainWindow/AddRedditFeedWindowController.swift @@ -12,7 +12,7 @@ import RSTree import Articles import Account -class AddRedditFeedWindowController : NSWindowController, AddFeedWindowController { +@MainActor final class AddRedditFeedWindowController : NSWindowController, AddFeedWindowController { @IBOutlet weak var typePopupButton: NSPopUpButton! @IBOutlet weak var typeDescriptionLabel: NSTextField! diff --git a/Mac/MainWindow/ArticleExtractorButton.swift b/Mac/MainWindow/ArticleExtractorButton.swift index 741a2dce1..dc7f5b158 100644 --- a/Mac/MainWindow/ArticleExtractorButton.swift +++ b/Mac/MainWindow/ArticleExtractorButton.swift @@ -15,7 +15,7 @@ enum ArticleExtractorButtonState { case off } -class ArticleExtractorButton: NSButton { +@MainActor final class ArticleExtractorButton: NSButton { public var rightClickAction: Selector? diff --git a/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift b/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift index 4aee30c11..334605cbe 100644 --- a/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift +++ b/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift @@ -10,7 +10,7 @@ import Foundation import WebKit import Articles -class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler { +final class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler { var currentArticle: Article? diff --git a/Mac/MainWindow/Detail/DetailStatusBarView.swift b/Mac/MainWindow/Detail/DetailStatusBarView.swift index eea5f72d2..119f5b112 100644 --- a/Mac/MainWindow/Detail/DetailStatusBarView.swift +++ b/Mac/MainWindow/Detail/DetailStatusBarView.swift @@ -9,7 +9,7 @@ import AppKit import Articles -final class DetailStatusBarView: NSView { +@MainActor final class DetailStatusBarView: NSView { @IBOutlet var urlLabel: NSTextField! diff --git a/Mac/MainWindow/Detail/DetailViewController.swift b/Mac/MainWindow/Detail/DetailViewController.swift index bcfd2533a..e4b099bf4 100644 --- a/Mac/MainWindow/Detail/DetailViewController.swift +++ b/Mac/MainWindow/Detail/DetailViewController.swift @@ -20,7 +20,7 @@ enum DetailState: Equatable { case extracted(Article, ExtractedArticle, CGFloat?) } -final class DetailViewController: NSViewController, WKUIDelegate { +@MainActor final class DetailViewController: NSViewController, WKUIDelegate { @IBOutlet var containerView: DetailContainerView! @IBOutlet var statusBarView: DetailStatusBarView! diff --git a/Mac/MainWindow/Detail/DetailWebView.swift b/Mac/MainWindow/Detail/DetailWebView.swift index f366e5ebe..dd793dd23 100644 --- a/Mac/MainWindow/Detail/DetailWebView.swift +++ b/Mac/MainWindow/Detail/DetailWebView.swift @@ -10,7 +10,7 @@ import AppKit import WebKit import RSCore -final class DetailWebView: WKWebView { +@MainActor final class DetailWebView: WKWebView { weak var keyboardDelegate: KeyboardDelegate? diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index 6d669cf5f..b861b9922 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -17,7 +17,7 @@ protocol DetailWebViewControllerDelegate: AnyObject { func mouseDidExit(_: DetailWebViewController) } -final class DetailWebViewController: NSViewController { +@MainActor final class DetailWebViewController: NSViewController { weak var delegate: DetailWebViewControllerDelegate? var webView: DetailWebView! @@ -90,6 +90,7 @@ final class DetailWebViewController: NSViewController { configuration.defaultWebpagePreferences = webpagePrefs configuration.preferences = preferences configuration.setURLSchemeHandler(detailIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) + configuration.mediaTypesRequiringUserActionForPlayback = .audio let userContentController = WKUserContentController() userContentController.add(self, name: MessageName.windowDidScroll) diff --git a/Mac/MainWindow/MainWindow.swift b/Mac/MainWindow/MainWindow.swift index 4fe147deb..18409f76a 100644 --- a/Mac/MainWindow/MainWindow.swift +++ b/Mac/MainWindow/MainWindow.swift @@ -8,7 +8,7 @@ import Foundation -class MainWindow: NSWindow { +@MainActor class MainWindow: NSWindow { override func sendEvent(_ event: NSEvent) { diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index cebdf3e50..5ed6e1a13 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -16,7 +16,7 @@ enum TimelineSourceMode { case regular, search } -class MainWindowController : NSWindowController, NSUserInterfaceValidations { +@MainActor final class MainWindowController : NSWindowController, NSUserInterfaceValidations { @IBOutlet weak var articleThemePopUpButton: NSPopUpButton? diff --git a/Mac/MainWindow/NNW3/NNW3ImportController.swift b/Mac/MainWindow/NNW3/NNW3ImportController.swift index 24cd71c43..1db4ed415 100644 --- a/Mac/MainWindow/NNW3/NNW3ImportController.swift +++ b/Mac/MainWindow/NNW3/NNW3ImportController.swift @@ -9,7 +9,7 @@ import AppKit import Account -struct NNW3ImportController { +@MainActor struct NNW3ImportController { /// Import NNW3 subscriptions if they exist. /// Return true if Subscriptions.plist was found and subscriptions were imported. diff --git a/Mac/MainWindow/NNW3/NNW3OpenPanelAccessoryViewController.swift b/Mac/MainWindow/NNW3/NNW3OpenPanelAccessoryViewController.swift index 110377b11..a8c2484c2 100644 --- a/Mac/MainWindow/NNW3/NNW3OpenPanelAccessoryViewController.swift +++ b/Mac/MainWindow/NNW3/NNW3OpenPanelAccessoryViewController.swift @@ -9,7 +9,7 @@ import AppKit import Account -final class NNW3OpenPanelAccessoryViewController: NSViewController { +@MainActor final class NNW3OpenPanelAccessoryViewController: NSViewController { @IBOutlet weak var accountPopUpButton: NSPopUpButton! diff --git a/Mac/MainWindow/OPML/ExportOPMLWindowController.swift b/Mac/MainWindow/OPML/ExportOPMLWindowController.swift index f642a9641..ad709cec5 100644 --- a/Mac/MainWindow/OPML/ExportOPMLWindowController.swift +++ b/Mac/MainWindow/OPML/ExportOPMLWindowController.swift @@ -9,7 +9,7 @@ import AppKit import Account -class ExportOPMLWindowController: NSWindowController { +@MainActor final class ExportOPMLWindowController: NSWindowController { @IBOutlet weak var accountPopUpButton: NSPopUpButton! private weak var hostWindow: NSWindow? diff --git a/Mac/MainWindow/OPML/ImportOPMLWindowController.swift b/Mac/MainWindow/OPML/ImportOPMLWindowController.swift index f0737fe93..3a53c3de3 100644 --- a/Mac/MainWindow/OPML/ImportOPMLWindowController.swift +++ b/Mac/MainWindow/OPML/ImportOPMLWindowController.swift @@ -9,7 +9,7 @@ import AppKit import Account -class ImportOPMLWindowController: NSWindowController { +@MainActor final class ImportOPMLWindowController: NSWindowController { @IBOutlet weak var accountPopUpButton: NSPopUpButton! private weak var hostWindow: NSWindow? diff --git a/Mac/MainWindow/Sidebar/Cell/SidebarCell.swift b/Mac/MainWindow/Sidebar/Cell/SidebarCell.swift index 0bd35cf22..5659c5e3d 100644 --- a/Mac/MainWindow/Sidebar/Cell/SidebarCell.swift +++ b/Mac/MainWindow/Sidebar/Cell/SidebarCell.swift @@ -11,7 +11,7 @@ import RSCore import Account import RSTree -class SidebarCell : NSTableCellView { +@MainActor final class SidebarCell : NSTableCellView { var iconImage: IconImage? { didSet { diff --git a/Mac/MainWindow/Sidebar/Cell/SidebarCellAppearance.swift b/Mac/MainWindow/Sidebar/Cell/SidebarCellAppearance.swift index c13996732..e5824ba6a 100644 --- a/Mac/MainWindow/Sidebar/Cell/SidebarCellAppearance.swift +++ b/Mac/MainWindow/Sidebar/Cell/SidebarCellAppearance.swift @@ -8,7 +8,7 @@ import AppKit -struct SidebarCellAppearance: Equatable { +@MainActor struct SidebarCellAppearance: Equatable { let imageSize: CGSize let imageMarginRight: CGFloat = 4.0 diff --git a/Mac/MainWindow/Sidebar/Cell/SidebarCellLayout.swift b/Mac/MainWindow/Sidebar/Cell/SidebarCellLayout.swift index e054b924a..ed9e91a94 100644 --- a/Mac/MainWindow/Sidebar/Cell/SidebarCellLayout.swift +++ b/Mac/MainWindow/Sidebar/Cell/SidebarCellLayout.swift @@ -11,7 +11,7 @@ import RSCore // image - title - unreadCount -struct SidebarCellLayout { +@MainActor struct SidebarCellLayout { let faviconRect: CGRect let titleRect: CGRect diff --git a/Mac/MainWindow/Sidebar/Renaming/RenameWindowController.swift b/Mac/MainWindow/Sidebar/Renaming/RenameWindowController.swift index dadf22ce0..94330508c 100644 --- a/Mac/MainWindow/Sidebar/Renaming/RenameWindowController.swift +++ b/Mac/MainWindow/Sidebar/Renaming/RenameWindowController.swift @@ -13,7 +13,7 @@ protocol RenameWindowControllerDelegate { func renameWindowController(_ windowController: RenameWindowController, didRenameObject: Any, withNewName: String) } -final class RenameWindowController: NSWindowController { +@MainActor final class RenameWindowController: NSWindowController { @IBOutlet var renamePrompt: NSTextField! @IBOutlet var newTitleTextField: NSTextField! diff --git a/Mac/MainWindow/Sidebar/SidebarDeleteItemsAlert.swift b/Mac/MainWindow/Sidebar/SidebarDeleteItemsAlert.swift index e11dbc055..fb2ad9e20 100644 --- a/Mac/MainWindow/Sidebar/SidebarDeleteItemsAlert.swift +++ b/Mac/MainWindow/Sidebar/SidebarDeleteItemsAlert.swift @@ -10,7 +10,7 @@ import AppKit import RSTree import Account -enum SidebarDeleteItemsAlert { +@MainActor enum SidebarDeleteItemsAlert { /// Builds a delete confirmation dialog for the supplied nodes static func build(_ nodes: [Node]) -> NSAlert { diff --git a/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift b/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift index 7e0478d5f..a4fb7b5e9 100644 --- a/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift +++ b/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift @@ -492,7 +492,7 @@ private extension SidebarOutlineDataSource { return true } - func acceptSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardWebFeed, _ parentNode: Node, _ index: Int) -> Bool { + @MainActor func acceptSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardWebFeed, _ parentNode: Node, _ index: Int) -> Bool { guard nodeIsDropTarget(parentNode), index == NSOutlineViewDropOnItemIndex else { return false } diff --git a/Mac/MainWindow/Sidebar/SidebarOutlineView.swift b/Mac/MainWindow/Sidebar/SidebarOutlineView.swift index 2be34e87c..eefc29a63 100644 --- a/Mac/MainWindow/Sidebar/SidebarOutlineView.swift +++ b/Mac/MainWindow/Sidebar/SidebarOutlineView.swift @@ -10,7 +10,7 @@ import AppKit import RSCore import RSTree -class SidebarOutlineView : NSOutlineView { +@MainActor class SidebarOutlineView : NSOutlineView { @IBOutlet var keyboardDelegate: KeyboardDelegate! diff --git a/Mac/MainWindow/Sidebar/SidebarStatusBarView.swift b/Mac/MainWindow/Sidebar/SidebarStatusBarView.swift index 315ea4ae3..6a85a6f9f 100644 --- a/Mac/MainWindow/Sidebar/SidebarStatusBarView.swift +++ b/Mac/MainWindow/Sidebar/SidebarStatusBarView.swift @@ -12,7 +12,7 @@ import Articles import RSWeb import Account -final class SidebarStatusBarView: NSView { +@MainActor final class SidebarStatusBarView: NSView { @IBOutlet var progressIndicator: NSProgressIndicator! @IBOutlet var progressLabel: NSTextField! diff --git a/Mac/MainWindow/Sidebar/UnreadCountView.swift b/Mac/MainWindow/Sidebar/UnreadCountView.swift index 25ee1c2a4..9cb9e59c8 100644 --- a/Mac/MainWindow/Sidebar/UnreadCountView.swift +++ b/Mac/MainWindow/Sidebar/UnreadCountView.swift @@ -27,7 +27,7 @@ class UnreadCountView : NSView { } } var unreadCountString: String { - return unreadCount < 1 ? "" : "\(unreadCount)" + return unreadCount < 1 ? "" : numberFormatter.string(from: NSNumber(value: unreadCount))! } private var intrinsicContentSizeIsValid = false @@ -92,5 +92,21 @@ class UnreadCountView : NSView { unreadCountString.draw(at: textRect().origin, withAttributes: Appearance.textAttributes) } } + + var numberFormatter: NumberFormatter! + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.frame = frameRect + + let formatter = NumberFormatter() + formatter.locale = Locale.current + formatter.numberStyle = .decimal + numberFormatter = formatter + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } } diff --git a/Mac/MainWindow/Timeline/TimelineContainerView.swift b/Mac/MainWindow/Timeline/TimelineContainerView.swift index e75475405..6a8b42298 100644 --- a/Mac/MainWindow/Timeline/TimelineContainerView.swift +++ b/Mac/MainWindow/Timeline/TimelineContainerView.swift @@ -8,7 +8,7 @@ import AppKit -final class TimelineContainerView: NSView { +@MainActor final class TimelineContainerView: NSView { private var contentViewConstraints: [NSLayoutConstraint]? diff --git a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift index c697a1a0c..81ec39b30 100644 --- a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift @@ -17,7 +17,7 @@ protocol TimelineContainerViewControllerDelegate: AnyObject { } -final class TimelineContainerViewController: NSViewController { +@MainActor final class TimelineContainerViewController: NSViewController { @IBOutlet weak var viewOptionsPopUpButton: NSPopUpButton! @IBOutlet weak var newestToOldestMenuItem: NSMenuItem! diff --git a/Mac/MainWindow/Timeline/TimelineTableRowView.swift b/Mac/MainWindow/Timeline/TimelineTableRowView.swift index 54d87479c..d4598ef1e 100644 --- a/Mac/MainWindow/Timeline/TimelineTableRowView.swift +++ b/Mac/MainWindow/Timeline/TimelineTableRowView.swift @@ -8,7 +8,7 @@ import AppKit -class TimelineTableRowView : NSTableRowView { +@MainActor final class TimelineTableRowView : NSTableRowView { private var separator: NSView? diff --git a/Mac/MainWindow/Timeline/TimelineTableView.swift b/Mac/MainWindow/Timeline/TimelineTableView.swift index 67c12fd70..8d997e229 100644 --- a/Mac/MainWindow/Timeline/TimelineTableView.swift +++ b/Mac/MainWindow/Timeline/TimelineTableView.swift @@ -9,7 +9,7 @@ import AppKit import RSCore -class TimelineTableView: NSTableView { +@MainActor final class TimelineTableView: NSTableView { weak var keyboardDelegate: KeyboardDelegate? diff --git a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift index 45d5a52cf..bdd1879a7 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift @@ -157,7 +157,7 @@ private extension TimelineViewController { func menu(for articles: [Article]) -> NSMenu? { let menu = NSMenu(title: "") - if articles.anyArticleIsUnread() { + if articles.anyArticleIsUnreadAndCanMarkRead() { menu.addItem(markReadMenuItem(articles)) } if articles.anyArticleIsReadAndCanMarkUnread() { @@ -169,10 +169,10 @@ private extension TimelineViewController { if articles.anyArticleIsStarred() { menu.addItem(markUnstarredMenuItem(articles)) } - if let first = articles.first, self.articles.articlesAbove(article: first).canMarkAllAsRead() { + if let first = articles.first, self.articles.articlesAbove(article: first).canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) { menu.addItem(markAboveReadMenuItem(articles)) } - if let last = articles.last, self.articles.articlesBelow(article: last).canMarkAllAsRead() { + if let last = articles.last, self.articles.articlesBelow(article: last).canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) { menu.addItem(markBelowReadMenuItem(articles)) } diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index c15a59a7b..ad650a52d 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -124,6 +124,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr } directlyMarkedAsUnreadArticles = Set
() + lastVerticalPosition = 0 articleRowMap = [String: [Int]]() tableView.reloadData() } @@ -194,6 +195,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr private let keyboardDelegate = TimelineKeyboardDelegate() private var timelineShowsSeparatorsObserver: NSKeyValueObservation? + private var markAsReadOnScrollWorkItem: DispatchWorkItem? + private var markAsReadOnScrollStart: Int? + private var markAsReadOnScrollEnd: Int? + private var lastVerticalPosition: CGFloat = 0 + convenience init(delegate: TimelineDelegate) { self.init(nibName: "TimelineTableView", bundle: nil) self.delegate = delegate @@ -224,6 +230,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidDirectMarking(_:)), name: .MarkStatusCommandDidDirectMarking, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidUndoDirectMarking(_:)), name: .MarkStatusCommandDidUndoDirectMarking, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidScroll), name: NSScrollView.didLiveScrollNotification, object: tableView.enclosingScrollView) didRegisterForNotifications = true } } @@ -235,6 +242,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr // MARK: - API func markAllAsRead(completion: (() -> Void)? = nil) { + markAllAsRead(articles, completion: completion) + } + + func markAllAsRead(_ articles: [Article], completion: (() -> Void)? = nil) { let markableArticles = Set(articles).subtracting(directlyMarkedAsUnreadArticles) guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: markableArticles, @@ -248,7 +259,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr } func canMarkAllAsRead() -> Bool { - return articles.canMarkAllAsRead() + return articles.canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) } func canMarkSelectedArticlesAsRead() -> Bool { @@ -329,6 +340,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr let urlStrings = selectedArticles.compactMap { $0.preferredLink } Browser.open(urlStrings, fromWindow: self.view.window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false) } + + @objc func scrollViewDidScroll(notification: Notification) { + markAsReadOnScroll() + } @IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) { guard !selectedArticles.isEmpty else { @@ -474,7 +489,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr func markReadCommandStatus() -> MarkCommandValidationStatus { let articles = selectedArticles - if articles.anyArticleIsUnread() { + if articles.anyArticleIsUnreadAndCanMarkRead() { return .canMark } @@ -499,12 +514,12 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr func canMarkAboveArticlesAsRead() -> Bool { guard let first = selectedArticles.first else { return false } - return articles.articlesAbove(article: first).canMarkAllAsRead() + return articles.articlesAbove(article: first).canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) } func canMarkBelowArticlesAsRead() -> Bool { guard let last = selectedArticles.last else { return false } - return articles.articlesBelow(article: last).canMarkAllAsRead() + return articles.articlesBelow(article: last).canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) } func markOlderArticlesRead(_ selectedArticles: [Article]) { @@ -1326,4 +1341,51 @@ private extension TimelineViewController { } return false } + + func markAsReadOnScroll() { + guard AppDefaults.shared.markArticlesAsReadOnScroll else { return } + + // Only try to mark if we are scrolling up + defer { + lastVerticalPosition = tableView.enclosingScrollView?.documentVisibleRect.origin.y ?? 0 + } + guard lastVerticalPosition < tableView.enclosingScrollView?.documentVisibleRect.origin.y ?? 0 else { + return + } + + // Make sure we are a little past the visible area so that marking isn't too touchy + let firstVisibleRowIndex = tableView.rows(in: tableView.visibleRect).location + guard let firstVisibleRowRect = tableView.rowView(atRow: firstVisibleRowIndex, makeIfNecessary: false)?.frame, + tableView.convert(firstVisibleRowRect, to: tableView.enclosingScrollView).origin.y < tableView.safeAreaInsets.top - 20 else { + return + } + + // We only mark immediately after scrolling stops, not during, to prevent scroll hitching + markAsReadOnScrollWorkItem?.cancel() + markAsReadOnScrollWorkItem = DispatchWorkItem { [weak self] in + defer { + self?.markAsReadOnScrollStart = nil + self?.markAsReadOnScrollEnd = nil + } + + guard let start: Int = self?.markAsReadOnScrollStart, + let end: Int = self?.markAsReadOnScrollEnd ?? self?.markAsReadOnScrollStart, + start <= end, + let self = self else { + return + } + + let articles = self.articles[start...end].filter({ $0.status.read == false }) + self.markAllAsRead(articles) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: markAsReadOnScrollWorkItem!) + + // Here we are creating a range of rows to attempt to mark later with the work item + guard markAsReadOnScrollStart != nil else { + markAsReadOnScrollStart = max(firstVisibleRowIndex - 5, 0) + return + } + markAsReadOnScrollEnd = max(markAsReadOnScrollEnd ?? 0, firstVisibleRowIndex) + } + } diff --git a/Mac/MainWindow/URLPasteboardWriter+NetNewsWire.swift b/Mac/MainWindow/URLPasteboardWriter+NetNewsWire.swift index a0c078089..0a70612f7 100644 --- a/Mac/MainWindow/URLPasteboardWriter+NetNewsWire.swift +++ b/Mac/MainWindow/URLPasteboardWriter+NetNewsWire.swift @@ -8,7 +8,7 @@ import RSCore -extension URLPasteboardWriter { +@MainActor extension URLPasteboardWriter { /// Copy URL strings, alerting the user the first time the array of URL strings contains `nil`. /// - Parameters: diff --git a/Mac/Preferences/Accounts/AccountCell.swift b/Mac/Preferences/Accounts/AccountCell.swift index 7a1dbecef..b6fb2eef3 100644 --- a/Mac/Preferences/Accounts/AccountCell.swift +++ b/Mac/Preferences/Accounts/AccountCell.swift @@ -8,7 +8,7 @@ import AppKit -class AccountCell: NSTableCellView { +@MainActor class AccountCell: NSTableCellView { private var originalImage: NSImage? diff --git a/Mac/Preferences/Accounts/AccountsAddCloudKitWindowController.swift b/Mac/Preferences/Accounts/AccountsAddCloudKitWindowController.swift index cf4aa0f10..1576ecfe4 100644 --- a/Mac/Preferences/Accounts/AccountsAddCloudKitWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsAddCloudKitWindowController.swift @@ -17,7 +17,7 @@ enum AccountsAddCloudKitWindowControllerError: LocalizedError { } } -class AccountsAddCloudKitWindowController: NSWindowController { +@MainActor class AccountsAddCloudKitWindowController: NSWindowController { @IBOutlet weak var limitationsAndSolutionsTextField: NSTextField! diff --git a/Mac/Preferences/Accounts/AccountsAddLocalWindowController.swift b/Mac/Preferences/Accounts/AccountsAddLocalWindowController.swift index 7482f309f..5ebeb07d0 100644 --- a/Mac/Preferences/Accounts/AccountsAddLocalWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsAddLocalWindowController.swift @@ -9,7 +9,7 @@ import AppKit import Account -class AccountsAddLocalWindowController: NSWindowController { +@MainActor class AccountsAddLocalWindowController: NSWindowController { @IBOutlet private weak var nameTextField: NSTextField! @IBOutlet private weak var localAccountNameTextField: NSTextField! diff --git a/Mac/Preferences/Accounts/AccountsDetailViewController.swift b/Mac/Preferences/Accounts/AccountsDetailViewController.swift index 3fe5487ee..6b395249e 100644 --- a/Mac/Preferences/Accounts/AccountsDetailViewController.swift +++ b/Mac/Preferences/Accounts/AccountsDetailViewController.swift @@ -9,7 +9,7 @@ import AppKit import Account -final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate { +@MainActor final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate { @IBOutlet weak var typeLabel: NSTextField! @IBOutlet weak var nameTextField: NSTextField! diff --git a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift index f223f50d4..3ca520f6a 100644 --- a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift @@ -12,7 +12,7 @@ import RSCore import RSWeb import Secrets -class AccountsFeedbinWindowController: NSWindowController, Logging { +@MainActor class AccountsFeedbinWindowController: NSWindowController, Logging { @IBOutlet weak var signInTextField: NSTextField! @IBOutlet weak var noAccountTextField: NSTextField! diff --git a/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift b/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift index 30825b14a..bdb0441d5 100644 --- a/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift @@ -12,7 +12,7 @@ import RSWeb import RSCore import Secrets -class AccountsNewsBlurWindowController: NSWindowController, Logging { +@MainActor class AccountsNewsBlurWindowController: NSWindowController, Logging { @IBOutlet weak var signInTextField: NSTextField! @IBOutlet weak var noAccountTextField: NSTextField! diff --git a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift index 4745a3d49..05aaa2dff 100644 --- a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift +++ b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift @@ -17,7 +17,7 @@ protocol AccountsPreferencesAddAccountDelegate { } // MARK: - AccountsPreferencesViewController -final class AccountsPreferencesViewController: NSViewController { +@MainActor final class AccountsPreferencesViewController: NSViewController { @IBOutlet weak var tableView: NSTableView! @IBOutlet weak var detailView: NSView! diff --git a/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift b/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift index 228395cc9..1e94b8e72 100644 --- a/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift @@ -12,7 +12,7 @@ import RSWeb import RSCore import Secrets -class AccountsReaderAPIWindowController: NSWindowController, Logging { +@MainActor class AccountsReaderAPIWindowController: NSWindowController, Logging { @IBOutlet weak var titleImageView: NSImageView! @IBOutlet weak var titleLabel: NSTextField! diff --git a/Mac/Preferences/Advanced/AdvancedPreferencesViewController.swift b/Mac/Preferences/Advanced/AdvancedPreferencesViewController.swift index a48e9fe91..53ccc3aee 100644 --- a/Mac/Preferences/Advanced/AdvancedPreferencesViewController.swift +++ b/Mac/Preferences/Advanced/AdvancedPreferencesViewController.swift @@ -8,11 +8,10 @@ import AppKit -final class AdvancedPreferencesViewController: NSViewController { +@MainActor final class AdvancedPreferencesViewController: NSViewController { @IBOutlet var releaseBuildsButton: NSButton! @IBOutlet var testBuildsButton: NSButton! - @IBOutlet weak var privacyPolicyTextField: NSTextField! let releaseBuildsURL = Bundle.main.infoDictionary!["SUFeedURL"]! as! String let testBuildsURL = Bundle.main.infoDictionary!["FeedURLForTestBuilds"]! as! String @@ -35,7 +34,6 @@ final class AdvancedPreferencesViewController: NSViewController { NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) didRegisterForNotification = true } - privacyPolicyTextField.attributedStringValue = AppAssets.privacyPolicyLink } @IBAction func updateTypeButtonClicked(_ sender: Any?) { diff --git a/Mac/Preferences/ExtensionPoints/ExtensionPointDetailViewController.swift b/Mac/Preferences/ExtensionPoints/ExtensionPointDetailViewController.swift index f94d0ae59..0a97d68ae 100644 --- a/Mac/Preferences/ExtensionPoints/ExtensionPointDetailViewController.swift +++ b/Mac/Preferences/ExtensionPoints/ExtensionPointDetailViewController.swift @@ -8,7 +8,7 @@ import Cocoa -class ExtensionPointDetailViewController: NSViewController { +@MainActor class ExtensionPointDetailViewController: NSViewController { @IBOutlet weak var imageView: NSImageView! @IBOutlet weak var titleLabel: NSTextField! diff --git a/Mac/Preferences/ExtensionPoints/ExtensionPointEnableWindowController.swift b/Mac/Preferences/ExtensionPoints/ExtensionPointEnableWindowController.swift index f8aed7ff5..1af42d40e 100644 --- a/Mac/Preferences/ExtensionPoints/ExtensionPointEnableWindowController.swift +++ b/Mac/Preferences/ExtensionPoints/ExtensionPointEnableWindowController.swift @@ -11,7 +11,7 @@ import AuthenticationServices import OAuthSwift import Secrets -class ExtensionPointEnableWindowController: NSWindowController { +@MainActor class ExtensionPointEnableWindowController: NSWindowController { @IBOutlet weak var imageView: NSImageView! @IBOutlet weak var titleLabel: NSTextField! diff --git a/Mac/Preferences/ExtensionPoints/ExtensionPointPreferencesViewController.swift b/Mac/Preferences/ExtensionPoints/ExtensionPointPreferencesViewController.swift index 6f29e6770..9c4d1f15b 100644 --- a/Mac/Preferences/ExtensionPoints/ExtensionPointPreferencesViewController.swift +++ b/Mac/Preferences/ExtensionPoints/ExtensionPointPreferencesViewController.swift @@ -16,7 +16,7 @@ protocol ExtensionPointPreferencesEnabler: AnyObject { func enable(_ extensionPointType: ExtensionPoint.Type) } -final class ExtensionPointPreferencesViewController: NSViewController { +@MainActor final class ExtensionPointPreferencesViewController: NSViewController { @IBOutlet weak var tableView: NSTableView! @IBOutlet weak var detailView: NSView! diff --git a/Mac/Preferences/General/GeneralPrefencesViewController.swift b/Mac/Preferences/General/GeneralPrefencesViewController.swift index db69f3863..581de2688 100644 --- a/Mac/Preferences/General/GeneralPrefencesViewController.swift +++ b/Mac/Preferences/General/GeneralPrefencesViewController.swift @@ -11,7 +11,7 @@ import RSCore import RSWeb import UserNotifications -final class GeneralPreferencesViewController: NSViewController { +@MainActor final class GeneralPreferencesViewController: NSViewController { private var userNotificationSettings: UNNotificationSettings? diff --git a/Mac/Preferences/PreferencesControlsBackgroundView.swift b/Mac/Preferences/PreferencesControlsBackgroundView.swift index 36353dd6b..307109c50 100644 --- a/Mac/Preferences/PreferencesControlsBackgroundView.swift +++ b/Mac/Preferences/PreferencesControlsBackgroundView.swift @@ -9,7 +9,7 @@ import AppKit import RSCore -final class PreferencesControlsBackgroundView: NSView { +@MainActor final class PreferencesControlsBackgroundView: NSView { private let lightModeFillColor = NSColor(white: 0.97, alpha: 1.0) private let darkModeFillColor = NSColor(red: 0.32, green: 0.34, blue: 0.35, alpha: 1.0) diff --git a/Mac/Preferences/PreferencesTableViewBackgroundView.swift b/Mac/Preferences/PreferencesTableViewBackgroundView.swift index 34548e429..df9fce2db 100644 --- a/Mac/Preferences/PreferencesTableViewBackgroundView.swift +++ b/Mac/Preferences/PreferencesTableViewBackgroundView.swift @@ -8,7 +8,7 @@ import AppKit -final class PreferencesTableViewBackgroundView: NSView { +@MainActor final class PreferencesTableViewBackgroundView: NSView { let lightBorderColor = NSColor(white: 0.71, alpha: 1.0) let darkBorderColor = NSColor(red: 0.41, green: 0.43, blue: 0.44, alpha: 1.0) diff --git a/Mac/Preferences/PreferencesWindowController.swift b/Mac/Preferences/PreferencesWindowController.swift index eb9ac233c..081edc724 100644 --- a/Mac/Preferences/PreferencesWindowController.swift +++ b/Mac/Preferences/PreferencesWindowController.swift @@ -28,7 +28,7 @@ private struct ToolbarItemIdentifier { static let Advanced = "Advanced" } -class PreferencesWindowController : NSWindowController, NSToolbarDelegate { +@MainActor class PreferencesWindowController : NSWindowController, NSToolbarDelegate { private let windowWidth = CGFloat(512.0) // Width is constant for all views; only the height changes private var viewControllers = [String: NSViewController]() diff --git a/Mac/Resources/Info.plist b/Mac/Resources/Info.plist index 90970f567..a5e21c04c 100644 --- a/Mac/Resources/Info.plist +++ b/Mac/Resources/Info.plist @@ -62,7 +62,7 @@ NSAppleScriptEnabled NSHumanReadableCopyright - Copyright © 2002-2022 Brent Simmons. All rights reserved. + Copyright © 2002-2023 Brent Simmons. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass diff --git a/Mac/Scriptability/AppDelegate+Scriptability.swift b/Mac/Scriptability/AppDelegate+Scriptability.swift index 07f3057c2..a16334eaf 100644 --- a/Mac/Scriptability/AppDelegate+Scriptability.swift +++ b/Mac/Scriptability/AppDelegate+Scriptability.swift @@ -39,7 +39,7 @@ extension AppDelegate : AppDelegateAppleEvents { NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(AppDelegate.getURL(_:_:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)) } - @objc func getURL(_ event: NSAppleEventDescriptor, _ withReplyEvent: NSAppleEventDescriptor) { + @MainActor @objc func getURL(_ event: NSAppleEventDescriptor, _ withReplyEvent: NSAppleEventDescriptor) { guard var urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else { return @@ -63,7 +63,9 @@ extension AppDelegate : AppDelegateAppleEvents { do { try ArticleThemeDownloader.shared.handleFile(at: location) } catch { - self.presentThemeImportError(error) + Task { @MainActor in + self.presentThemeImportError(error) + } } } task.resume() diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 374f61fe6..a319d30e9 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -225,7 +225,6 @@ 51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */; }; 517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; 517630052336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; - 517630232336657E00E15FFF /* WebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* WebViewProvider.swift */; }; 5177C21227B07C9E00643901 /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */; }; 5177C21327B07CFE00643901 /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */; }; 5177C21427B07D1E00643901 /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */; }; @@ -377,7 +376,6 @@ 51DC07992552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; 51DC079A2552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; 51DC07AC255209E200A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; - 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */; }; 51DEE81226FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; }; 51DEE81326FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; }; 51DEE81426FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; }; @@ -1268,7 +1266,6 @@ 516AE9DE2372269A007DEEAA /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = ""; }; 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = ""; }; 517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = ""; }; - 517630222336657E00E15FFF /* WebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProvider.swift; sourceTree = ""; }; 517A745A2443665000B553B9 /* UIPageViewController-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIPageViewController-Extensions.swift"; sourceTree = ""; }; 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = ""; }; 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = ""; }; @@ -1342,7 +1339,6 @@ 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = ""; }; 51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = ""; }; 51DC07972552083500A3F79F /* ArticleTextSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleTextSize.swift; sourceTree = ""; }; - 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = ""; }; 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Appanoose.nnwtheme; sourceTree = ""; }; 51DEE81726FBFF84006DAA56 /* Promenade.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Promenade.nnwtheme; sourceTree = ""; }; 51E36E70239D6610006F47A5 /* AddFeedSelectFolderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedSelectFolderTableViewCell.swift; sourceTree = ""; }; @@ -2184,9 +2180,7 @@ 518651D9235621840078E021 /* ImageTransition.swift */, 5142192923522B5500E07E2C /* ImageViewController.swift */, 512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */, - 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */, 51AB8AB223B7F4C6008F147D /* WebViewController.swift */, - 517630222336657E00E15FFF /* WebViewProvider.swift */, 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */, D3A398632465054F00F9A366 /* FindInArticleActivity.swift */, D3555BF324664539005E48C3 /* ArticleSearchBar.swift */, @@ -4196,7 +4190,6 @@ 511B9807237DCAC90028BCAA /* UserInfoKey.swift in Sources */, 51C45269226508F600C03939 /* MasterFeedTableViewCell.swift in Sources */, 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */, - 517630232336657E00E15FFF /* WebViewProvider.swift in Sources */, 51E43962238037C400015C31 /* AddFeedFolderViewController.swift in Sources */, 51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */, 51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */, @@ -4323,7 +4316,6 @@ 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */, 514219372352510100E07E2C /* ImageScrollView.swift in Sources */, 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, - 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */, D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */, DF28B44D294ED52700C4D8CA /* View+DismissOnExternalContext.swift in Sources */, 8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */, diff --git a/Shared/Article Extractor/ArticleExtractor.swift b/Shared/Article Extractor/ArticleExtractor.swift index 1fd4d151b..1b6cdd4f3 100644 --- a/Shared/Article Extractor/ArticleExtractor.swift +++ b/Shared/Article Extractor/ArticleExtractor.swift @@ -40,10 +40,10 @@ class ArticleExtractor: Logging { let clientURL = "https://extract.feedbin.com/parser" let username = SecretsManager.provider.mercuryClientId - let signiture = articleLink.hmacUsingSHA1(key: SecretsManager.provider.mercuryClientSecret) + let signature = articleLink.hmacUsingSHA1(key: SecretsManager.provider.mercuryClientSecret) if let base64URL = articleLink.data(using: .utf8)?.base64EncodedString() { - let fullURL = "\(clientURL)/\(username)/\(signiture)?base64_url=\(base64URL)" + let fullURL = "\(clientURL)/\(username)/\(signature)?base64_url=\(base64URL)" if let url = URL(string: fullURL) { self.url = url return diff --git a/Shared/Article Rendering/stylesheet.css b/Shared/Article Rendering/stylesheet.css index ae37b23ca..eabf7a21b 100644 --- a/Shared/Article Rendering/stylesheet.css +++ b/Shared/Article Rendering/stylesheet.css @@ -36,24 +36,26 @@ a:hover { :root { --header-table-border-color: rgba(0, 0, 0, 0.1); - --header-color: rgba(0, 0, 0, 0.3); - --body-code-color: #666; + --header-color: rgba(0, 0, 0, 0.66); + --body-code-color: #111; + --code-background-color: #eee; --system-message-color: #cbcbcb; --feedlink-color: rgba(255, 0, 0, 0.6); --article-title-color: #333; - --article-date-color: rgba(0, 0, 0, 0.3); + --article-date-color: rgba(0, 0, 0, 0.5); --table-cell-border-color: lightgray; } @media(prefers-color-scheme: dark) { :root { --header-color: rgba(94, 158, 244, 1); - --body-code-color: #b2b2b2; + --body-code-color: #dcdcdc; --system-message-color: #5f5f5f; --feedlink-color: rgba(94, 158, 244, 1); --article-title-color: #e0e0e0; --article-date-color: rgba(255, 255, 255, 0.5); --table-cell-border-color: dimgray; + --code-background-color: #333; } } @@ -106,6 +108,8 @@ body > .systemMessage { .articleDateline { margin-bottom: 5px; font-weight: bold; + font-variant-caps: all-small-caps; + letter-spacing: 0.025em; } .articleDateline a:link, .articleDateline a:visited { @@ -115,6 +119,7 @@ body > .systemMessage { .articleDatelineTitle { margin-bottom: 5px; font-weight: bold; + font-variant-caps: all-small-caps; } .articleDatelineTitle a:link, .articleDatelineTitle a:visited { @@ -122,19 +127,37 @@ body > .systemMessage { } .externalLink { - margin-bottom: 5px; + margin-top: 15px; + margin-bottom: 15px; +/* + font-variant-caps: all-small-caps; + letter-spacing: 0.025em; + */ + font-size: 0.875em; font-style: italic; + color: var(--article-date-color); width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.externalLink a { + font-family: "SF Mono", Menlo, Courier, monospace; + font-size: 0.85em; + font-variant-caps: normal; + letter-spacing: 0em; +} + .articleBody { margin-top: 20px; line-height: 1.6em; } +.articleBody a { + padding: 0px 1px; +} + h1 { line-height: 1.15em; font-weight: bold; @@ -149,6 +172,7 @@ pre { overflow-y: hidden; word-wrap: normal; word-break: normal; + border-radius: 3px; } pre { @@ -156,9 +180,15 @@ pre { } code, pre { - font-family: "SF Mono", Menlo, "Courier New", Courier, monospace; + font-family: "SF Mono", Menlo, Courier, monospace; font-size: 1em; -webkit-hyphens: none; + background: var(--code-background-color); +} + +code { + padding: 1px 2px; + border-radius: 2px; } pre code { @@ -219,10 +249,6 @@ img, figure, video, div, object { margin: 0 auto; } -video { - width: 100% !important; -} - iframe { max-width: 100%; margin: 0 auto; @@ -238,7 +264,6 @@ figure { } figcaption { - margin-top: 0.5em; font-size: 14px; line-height: 1.3em; } @@ -286,6 +311,30 @@ blockquote { border-top: 1px solid var(--header-table-border-color); } +/* Twitter */ + +.twitterAvatar { + vertical-align: middle; + border-radius: 4px; + height: 1.7em; + width: 1.7em; +} + +.twitterUsername { + line-height: 1.2; + margin-left: 4px; + display: inline-block; + vertical-align: middle; +} + +.twitterScreenName { + font-size: 66%; +} + +.twitterTimestamp { + font-size: 66%; +} + /* Newsfoot theme for light mode (default) */ .newsfoot-footnote-popover { background: #ccc; @@ -359,7 +408,6 @@ a.footnote:hover, padding-right: 20px; word-break: break-word; - -webkit-hyphens: auto; -webkit-text-size-adjust: none; } @@ -370,7 +418,8 @@ a.footnote:hover, font-size: [[font-size]]px; --primary-accent-color: #086AEE; --secondary-accent-color: #086AEE; - --block-quote-border-color: rgba(8, 106, 238, 0.75); + --block-quote-border-color: rgba(0, 0, 0, 0.25); + --ios-hover-color: lightgray; /* placeholder */ } @media(prefers-color-scheme: dark) { @@ -379,24 +428,44 @@ a.footnote:hover, --secondary-accent-color: #5E9EF4; --block-quote-border-color: rgba(94, 158, 244, 0.75); --header-table-border-color: rgba(255, 255, 255, 0.2); + --ios-hover-color: #444444; /* placeholder */ } } - + body a, body a:visited, body a * { color: var(--secondary-accent-color); } + .externalLink a { + font-size: inherit; + } + + .articleBody a:link, .articleBody a:visited { + text-decoration: none; + border-bottom: 1px solid var(--primary-accent-color); + color: var(--secondary-accent-color); + } + body .header { font: -apple-system-body; font-size: [[font-size]]px; } body .header a:link, body .header a:visited { - color: var(--primary-accent-color); + color: var(--secondary-accent-color); } + + @media (hover: hover) and (pointer: coarse) { + .articleBody a:hover { + background: var(--ios-hover-color); + } + } + pre { +/* border: 1px solid var(--secondary-accent-color); + */ padding: 5px; } @@ -439,15 +508,19 @@ a.footnote:hover, :root { color-scheme: light dark; - --accent-color: rgba(8, 106, 238, 1); - --block-quote-border-color: rgba(8, 106, 238, .50); + --accent-color: rgba( 8, 106, 238, 1); + --block-quote-border-color: rgba( 0, 0, 0, 0.25); + --hover-gradient-color-start: rgba(60, 146, 251, 1); + --hover-gradient-color-end: rgba(67, 149, 251, 1); } @media(prefers-color-scheme: dark) { :root { - --accent-color: rgba(94, 158, 244, 1); - --block-quote-border-color: rgba(94, 158, 244, .50); - --header-table-border-color: rgba(255, 255, 255, 0.1); + --accent-color: rgba( 94, 158, 244, 1); + --block-quote-border-color: rgba( 94, 158, 244, 0.50); + --header-table-border-color: rgba(255, 255, 255, 0.1); + --hover-gradient-color-start: rgba( 41, 121, 213, 1); + --hover-gradient-color-end: rgba( 42, 120, 212, 1); } } @@ -455,8 +528,26 @@ a.footnote:hover, color: var(--accent-color); } + .articleBody a:link: not(a > img, a > code), .articleBody a:visited: not(a > img, a > code) { + /* text-decoration: underline; */ + border-bottom: 1px solid var(--accent-color); + } + .articleBody a:hover { + border-radius: 2px; +/* + background: var(--accent-color); + */ + background: linear-gradient(0deg, var(--hover-gradient-color-start) 0%, var(--hover-gradient-color-end) 100%); + border-bottom: 1px solid var(--hover-gradient-color-end); + color: white; + text-decoration: none; + } + + pre { +/* border: 1px solid var(--accent-color); + */ padding: 10px; } diff --git a/Shared/ArticleStyles/ArticleThemesManager.swift b/Shared/ArticleStyles/ArticleThemesManager.swift index f97f0e7f0..71d24842d 100644 --- a/Shared/ArticleStyles/ArticleThemesManager.swift +++ b/Shared/ArticleStyles/ArticleThemesManager.swift @@ -6,9 +6,17 @@ // Copyright © 2015 Ranchero Software, LLC. All rights reserved. // -import Foundation +#if os(macOS) +import AppKit +#else +import UIKit +#endif + import RSCore import Combine +#if canImport(AppKit) +import AppKit +#endif public extension Notification.Name { static let ArticleThemeNamesDidChangeNotification = Notification.Name("ArticleThemeNamesDidChangeNotification") @@ -21,9 +29,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable public let folderPath: String lazy var presentedItemOperationQueue = OperationQueue.main - var presentedItemURL: URL? { - return URL(fileURLWithPath: folderPath) - } + var presentedItemURL: URL? var currentThemeName: String { get { @@ -35,6 +41,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable currentTheme = try articleThemeWithThemeName(newValue) AppDefaults.shared.currentThemeName = newValue objectWillChange.send() + updateFilePresenter() } catch { logger.error("Unable to set new theme: \(error.localizedDescription, privacy: .public)") } @@ -75,16 +82,22 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable assertionFailure("Could not create folder for Themes.") abort() } - - NSFileCoordinator.addFilePresenter(self) + + #if os(macOS) + NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: NSApplication.didBecomeActiveNotification, object: nil) + #else + NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil) + #endif + + updateFilePresenter() } func presentedSubitemDidChange(at url: URL) { - if url.lastPathComponent.localizedCaseInsensitiveContains("nnwtheme") { - themeNames = buildThemeNames() - do { - currentTheme = try articleThemeWithThemeName(currentThemeName) - } catch { + themeNames = buildThemeNames() + do { + currentTheme = try articleThemeWithThemeName(currentThemeName) + } catch { + Task { @MainActor in appDelegate.presentThemeImportError(error) } } @@ -108,6 +121,8 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable try FileManager.default.copyItem(atPath: filename, toPath: toFilename) objectWillChange.send() + + themeNames = buildThemeNames() } func articleThemeWithThemeName(_ themeName: String) throws -> ArticleTheme { @@ -129,15 +144,78 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable return try ArticleTheme(path: path, isAppTheme: isAppTheme) } + + func themesByDeveloper() -> (builtIn: [ArticleTheme], other: [ArticleTheme]) { + let installedProvidedThemes = themeNames.map({ try? articleThemeWithThemeName($0) }).compactMap({ $0 }).filter({ $0.isAppTheme }).sorted(by: { $0.name < $1.name }).filter({ $0.name != AppDefaults.defaultThemeName }) + + let installedOtherThemes = themeNames.map({ try? articleThemeWithThemeName($0) }).compactMap({ $0 }).filter({ !$0.isAppTheme }).sorted(by: { $0.name < $1.name }) + + return (installedProvidedThemes, installedOtherThemes) + } + + #if os(macOS) + func articleThemesMenu(for popUpButton: NSPopUpButton?) -> NSMenu { + let menu = NSMenu() + menu.autoenablesItems = false + menu.removeAllItems() + + let defaultMenuItem = NSMenuItem() + defaultMenuItem.title = ArticleTheme.defaultTheme.name + defaultMenuItem.action = #selector(updateThemeSelection(_:)) + defaultMenuItem.state = currentTheme.name == defaultMenuItem.title ? .on : .off + defaultMenuItem.target = self + menu.addItem(defaultMenuItem) + menu.addItem(NSMenuItem.separator()) + + let rancheroHeading = NSMenuItem(title: "Built-in Themes", action: nil, keyEquivalent: "") + rancheroHeading.attributedTitle = NSAttributedString(string: "Built-in Themes", attributes: [NSAttributedString.Key.foregroundColor : NSColor.secondaryLabelColor, NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 12)]) + rancheroHeading.isEnabled = false + menu.addItem(rancheroHeading) + + let installedThemes = ArticleThemesManager.shared.themesByDeveloper() + + for theme in installedThemes.0 { + let item = NSMenuItem() + item.title = theme.name + item.action = #selector(updateThemeSelection(_:)) + item.state = currentTheme.name == theme.name ? .on : .off + item.target = self + menu.addItem(item) + } + + menu.addItem(NSMenuItem.separator()) + + let thirdPartyHeading = NSMenuItem(title: "Other Themes", action: nil, keyEquivalent: "") + thirdPartyHeading.attributedTitle = NSAttributedString(string: "Other Themes", attributes: [NSAttributedString.Key.foregroundColor : NSColor.secondaryLabelColor, NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 12)]) + thirdPartyHeading.isEnabled = false + menu.addItem(thirdPartyHeading) + + for theme in installedThemes.1 { + let item = NSMenuItem() + item.title = theme.name + item.action = #selector(updateThemeSelection(_:)) + item.state = currentTheme.name == theme.name ? .on : .off + item.target = self + menu.addItem(item) + } + popUpButton?.selectItem(withTitle: ArticleThemesManager.shared.currentThemeName) + if popUpButton?.indexOfSelectedItem == -1 { + popUpButton?.selectItem(withTitle: ArticleTheme.defaultTheme.name) + } + return menu + } + + @objc + func updateThemeSelection(_ menuItem: NSMenuItem) { + currentThemeName = menuItem.title + } + + #endif func deleteTheme(themeName: String) { if let filename = pathForThemeName(themeName, folder: folderPath) { - do { - try FileManager.default.removeItem(atPath: filename) - } catch { - logger.error("\(error.localizedDescription)") - } - + try? FileManager.default.removeItem(atPath: filename) + themeNames = buildThemeNames() } } @@ -146,6 +224,19 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable // MARK : Private private extension ArticleThemesManager { + + @objc func applicationDidBecomeActive(_ note: Notification) { + themeNames = buildThemeNames() + } + + func updateFilePresenter() { + guard let currentThemePath = currentTheme.path else { + return + } + NSFileCoordinator.removeFilePresenter(self) + presentedItemURL = URL(fileURLWithPath: currentThemePath) + NSFileCoordinator.addFilePresenter(self) + } func buildThemeNames() -> [String] { let appThemeFilenames = Bundle.main.paths(forResourcesOfType: ArticleTheme.nnwThemeSuffix, inDirectory: nil) diff --git a/Shared/Commands/MarkStatusCommand.swift b/Shared/Commands/MarkStatusCommand.swift index dc7fe75f1..45502075a 100644 --- a/Shared/Commands/MarkStatusCommand.swift +++ b/Shared/Commands/MarkStatusCommand.swift @@ -56,7 +56,7 @@ final class MarkStatusCommand: UndoableCommand { } convenience init?(initialArticles: [Article], statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) { - self.init(initialArticles: Set(initialArticles), statusKey: .read, flag: flag, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion) + self.init(initialArticles: Set(initialArticles), statusKey: statusKey, flag: flag, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion) } convenience init?(initialArticles: Set
, markingRead: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) { diff --git a/Shared/Exporters/OPMLExporter.swift b/Shared/Exporters/OPMLExporter.swift index cba135a17..46d232e11 100644 --- a/Shared/Exporters/OPMLExporter.swift +++ b/Shared/Exporters/OPMLExporter.swift @@ -10,7 +10,7 @@ import Foundation import Account import RSCore -struct OPMLExporter { +@MainActor struct OPMLExporter { static func OPMLString(with account: Account, title: String) -> String { diff --git a/Shared/Extensions/CacheCleaner.swift b/Shared/Extensions/CacheCleaner.swift index b80896c5d..14046d523 100644 --- a/Shared/Extensions/CacheCleaner.swift +++ b/Shared/Extensions/CacheCleaner.swift @@ -10,7 +10,7 @@ import Foundation import RSWeb import RSCore -struct CacheCleaner: Logging { +@MainActor struct CacheCleaner: Logging { static func purgeIfNecessary() { diff --git a/Shared/Favicons/FaviconDownloader.swift b/Shared/Favicons/FaviconDownloader.swift index da9822650..170bd453a 100644 --- a/Shared/Favicons/FaviconDownloader.swift +++ b/Shared/Favicons/FaviconDownloader.swift @@ -122,7 +122,7 @@ final class FaviconDownloader: Logging { let url = homePageURL.normalizedURL if let url = URL(string: homePageURL) { - if url.host == "nnw.ranchero.com" { + if url.host == "nnw.ranchero.com" || url.host == "netnewswire.blog" { return IconImage.appIcon } } diff --git a/Shared/Images/WebFeedIconDownloader.swift b/Shared/Images/WebFeedIconDownloader.swift index 4604a1285..6bdb648ab 100644 --- a/Shared/Images/WebFeedIconDownloader.swift +++ b/Shared/Images/WebFeedIconDownloader.swift @@ -77,7 +77,7 @@ public final class WebFeedIconDownloader { return cachedImage } - if let hpURLString = feed.homePageURL, let hpURL = URL(string: hpURLString), hpURL.host == "nnw.ranchero.com" { + if let hpURLString = feed.homePageURL, let hpURL = URL(string: hpURLString), hpURL.host == "nnw.ranchero.com" || hpURL.host == "netnewswire.blog" { return IconImage.appIcon } diff --git a/Shared/Importers/DefaultFeeds.opml b/Shared/Importers/DefaultFeeds.opml index 9e809df40..64cb05c9a 100644 --- a/Shared/Importers/DefaultFeeds.opml +++ b/Shared/Importers/DefaultFeeds.opml @@ -4,35 +4,20 @@ Default Feeds -<<<<<<< HEAD - - - - - - - - - - - - - - - - - -======= - + + + + + - + + ->>>>>>> ios-release diff --git a/Shared/Importers/DefaultFeedsImporter.swift b/Shared/Importers/DefaultFeedsImporter.swift index 8ed67d3b9..d02b2cec3 100644 --- a/Shared/Importers/DefaultFeedsImporter.swift +++ b/Shared/Importers/DefaultFeedsImporter.swift @@ -10,7 +10,7 @@ import Foundation import Account import RSCore -struct DefaultFeedsImporter { +@MainActor struct DefaultFeedsImporter { static func importDefaultFeeds(account: Account) { let defaultFeedsURL = Bundle.main.url(forResource: "DefaultFeeds", withExtension: "opml")! diff --git a/Shared/Importers/OPMLDocument.swift b/Shared/Importers/OPMLDocument.swift index a304d9fa5..673a6dbde 100644 --- a/Shared/Importers/OPMLDocument.swift +++ b/Shared/Importers/OPMLDocument.swift @@ -30,7 +30,7 @@ public struct OPMLDocument: FileDocument { self.account = account } - public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + @MainActor public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces) let filename = "Subscriptions-\(accountName).opml" let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename) diff --git a/Shared/Secrets.swift.gyb b/Shared/Secrets.swift.gyb index 5b7814cb1..05b993a20 100644 --- a/Shared/Secrets.swift.gyb +++ b/Shared/Secrets.swift.gyb @@ -2,15 +2,7 @@ %{ import os -<<<<<<< HEAD -<<<<<<< HEAD -secrets = ['MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'TWITTER_CONSUMER_KEY', 'TWITTER_CONSUMER_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY'] -======= -secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY'] ->>>>>>> mac-release -======= -secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY'] ->>>>>>> ios-release +secrets = ['MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY'] def chunks(seq, size): return (seq[i:(i + size)] for i in range(0, len(seq), size)) diff --git a/Shared/Timeline/ArticleArray.swift b/Shared/Timeline/ArticleArray.swift index 2f8d69e06..7f99e8e6e 100644 --- a/Shared/Timeline/ArticleArray.swift +++ b/Shared/Timeline/ArticleArray.swift @@ -54,8 +54,8 @@ extension Array where Element == Article { return ArticleSorter.sortedByDate(articles: self, sortDirection: sortDirection, groupByFeed: groupByFeed) } - func canMarkAllAsRead() -> Bool { - return anyArticleIsUnread() + func canMarkAllAsRead(exemptArticles: Set
= .init()) -> Bool { + return anyArticleIsUnreadAndCanMarkRead(exemptArticles: exemptArticles) } func anyArticlePassesTest(_ test: ((Article) -> Bool)) -> Bool { @@ -71,8 +71,8 @@ extension Array where Element == Article { return anyArticlePassesTest { $0.status.read && $0.isAvailableToMarkUnread } } - func anyArticleIsUnread() -> Bool { - return anyArticlePassesTest { !$0.status.read } + func anyArticleIsUnreadAndCanMarkRead(exemptArticles: Set
= .init()) -> Bool { + return anyArticlePassesTest { !(exemptArticles.contains($0) || $0.status.read) } } func anyArticleIsStarred() -> Bool { diff --git a/Shared/Timer/AccountRefreshTimer.swift b/Shared/Timer/AccountRefreshTimer.swift index 33d07c32a..b0e81fb4e 100644 --- a/Shared/Timer/AccountRefreshTimer.swift +++ b/Shared/Timer/AccountRefreshTimer.swift @@ -73,7 +73,7 @@ class AccountRefreshTimer { lastTimedRefresh = Date() update() - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) + AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log, completion: nil) } } diff --git a/Technotes/HelpBook/5.0/en/netnewswire-news.markdown b/Technotes/HelpBook/5.0/en/netnewswire-news.markdown index 345972de0..1246b200a 100644 --- a/Technotes/HelpBook/5.0/en/netnewswire-news.markdown +++ b/Technotes/HelpBook/5.0/en/netnewswire-news.markdown @@ -11,6 +11,6 @@ Add the NetNewsWire News Feed The NetNewsWire News Feed is part of the default subscription list installed the first time you launched NetNewsWire. If it’s no longer in your list, it’s easy to add it back. -In the menu bar, select **Help → Add NetNewsWire News Feed**. NetNewsWire will open the [New Feed dialog](adding-feeds.html) pre-filled with the name and address (`https://nnw.ranchero.com/feed.json`). Click Add. +In the menu bar, select **Help → Add NetNewsWire News Feed**. NetNewsWire will open the [New Feed dialog](adding-feeds.html) pre-filled with the name and address (`https://netnewswire.blog/feed.json`). Click Add. -Now you’ll always be up to date with what’s happening with NetNewsWire. \ No newline at end of file +Now you’ll always be up to date with what’s happening with NetNewsWire. diff --git a/Technotes/ReleaseNotes-Mac.markdown b/Technotes/ReleaseNotes-Mac.markdown index c4c935dca..ea124531e 100644 --- a/Technotes/ReleaseNotes-Mac.markdown +++ b/Technotes/ReleaseNotes-Mac.markdown @@ -1,8 +1,29 @@ # Mac Release Notes +## 6.1.2 build 6114 8 Apr 2023 + +Update default feeds to remove feeds that don’t appear to be active anymore (sadly!). + +## 6.1.1 build 6112 13 Mar 2023 + +Revised Twitter removal warning to not mention any specific month. We’re holding this release until Twitter shuts down free access to its API. + +## 6.1.1 build 6111 9 Feb 2023 + +Same as 6.1.1b4 but with updated build and version number. + +### 6.1.1b4 build 6110 9 Feb 2023 + +Update the Twitter removal warning to say “later in February” instead of “February 9,” since Twitter postponed the removal date to the 13th and might postpone it further. + +### 6.1.1b3 build 6109 6 Feb 2023 + +Update Safari extension icon (credit to Louie Mantia for the new icon) + ### 6.1.1b2 build 6108 5 Feb 2023 Remove Twitter integration. On first launch, for people with Twitter feeds, display an alert explaining what happened + Fix a crashing bug that could happen in the sidebar ### 6.1.1b1 build 6107 3 Nov 2022 diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index 6f8758715..456b584ec 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -61,6 +61,7 @@ final class AppDefaults: ObservableObject { static let useSystemBrowser = "useSystemBrowser" static let currentThemeName = "currentThemeName" static let twitterDeprecationAlertShown = "twitterDeprecationAlertShown" + static let markArticlesAsReadOnScroll = "markArticlesAsReadOnScroll" } let isDeveloperBuild: Bool = { @@ -266,6 +267,16 @@ final class AppDefaults: ObservableObject { } } + var markArticlesAsReadOnScroll: Bool { + get { + return AppDefaults.bool(for: Key.markArticlesAsReadOnScroll) + } + set { + AppDefaults.setBool(for: Key.markArticlesAsReadOnScroll, newValue) + AppDefaults.shared.objectWillChange.send() + } + } + static func registerDefaults() { let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue, Key.timelineGroupByFeed: false, diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index a86bdac2e..4cca26407 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -57,7 +57,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var isSyncArticleStatusRunning = false var isWaitingForSyncTasks = false - override init() { + @MainActor override init() { super.init() appDelegate = self @@ -76,7 +76,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) } - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + @MainActor func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { AppDefaults.registerDefaults() let isFirstRun = AppDefaults.shared.isFirstRun @@ -127,7 +127,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } - func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + @MainActor func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { DispatchQueue.main.async { self.resumeDatabaseProcessingIfNecessary() AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) { @@ -137,17 +137,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } - func applicationWillTerminate(_ application: UIApplication) { + @MainActor func applicationWillTerminate(_ application: UIApplication) { shuttingDown = true } - func applicationDidEnterBackground(_ application: UIApplication) { + @MainActor func applicationDidEnterBackground(_ application: UIApplication) { IconImageCache.shared.emptyCache() } // MARK: Notifications - @objc func unreadCountDidChange(_ note: Notification) { + @MainActor @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { unreadCount = AccountManager.shared.unreadCount } @@ -155,21 +155,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - API - func manualRefresh(errorHandler: @escaping (Error) -> ()) { + @MainActor func manualRefresh(errorHandler: @escaping (Error) -> ()) { UIApplication.shared.connectedScenes.compactMap( { $0.delegate as? SceneDelegate } ).forEach { $0.cleanUp(conditional: true) } AccountManager.shared.refreshAll(errorHandler: errorHandler) } - func resumeDatabaseProcessingIfNecessary() { + @MainActor func resumeDatabaseProcessingIfNecessary() { if AccountManager.shared.isSuspended { AccountManager.shared.resumeAll() logger.info("Application processing resumed.") } } - func prepareAccountsForBackground() { + @MainActor func prepareAccountsForBackground() { extensionFeedAddRequestFile.suspend() syncTimer?.invalidate() scheduleBackgroundFeedRefresh() @@ -178,7 +178,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD waitForSyncTasksToFinish() } - func prepareAccountsForForeground() { + @MainActor func prepareAccountsForForeground() { extensionFeedAddRequestFile.resume() syncTimer?.update() @@ -218,7 +218,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } - func presentThemeImportError(_ error: Error) { + @MainActor func presentThemeImportError(_ error: Error) { let windowScene = { let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } return scenes.filter { $0.activationState == .foregroundActive }.first ?? scenes.first @@ -231,7 +231,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: App Initialization -private extension AppDelegate { +@MainActor private extension AppDelegate { private func initializeDownloaders() { let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! diff --git a/iOS/Article/ArticleExtractorButton.swift b/iOS/Article/ArticleExtractorButton.swift index 8580b4db7..b2c512385 100644 --- a/iOS/Article/ArticleExtractorButton.swift +++ b/iOS/Article/ArticleExtractorButton.swift @@ -15,7 +15,7 @@ enum ArticleExtractorButtonState { case off } -class ArticleExtractorButton: UIButton { +@MainActor class ArticleExtractorButton: UIButton { private var animatedLayer: CALayer? diff --git a/iOS/Article/ContextMenuPreviewViewController.swift b/iOS/Article/ContextMenuPreviewViewController.swift index 3b7e18219..2d6e3e961 100644 --- a/iOS/Article/ContextMenuPreviewViewController.swift +++ b/iOS/Article/ContextMenuPreviewViewController.swift @@ -9,7 +9,7 @@ import UIKit import Articles -class ContextMenuPreviewViewController: UIViewController { +@MainActor class ContextMenuPreviewViewController: UIViewController { @IBOutlet weak var blogNameLabel: UILabel! @IBOutlet weak var blogAuthorLabel: UILabel! diff --git a/iOS/Article/ImageTransition.swift b/iOS/Article/ImageTransition.swift index 25951b301..dc4a66d6d 100644 --- a/iOS/Article/ImageTransition.swift +++ b/iOS/Article/ImageTransition.swift @@ -8,7 +8,7 @@ import UIKit -class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning { +@MainActor final class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning { private weak var webViewController: WebViewController? private let duration = 0.4 diff --git a/iOS/Article/ImageViewController.swift b/iOS/Article/ImageViewController.swift index 21cdcfa09..e1b9f0616 100644 --- a/iOS/Article/ImageViewController.swift +++ b/iOS/Article/ImageViewController.swift @@ -25,6 +25,16 @@ class ImageViewController: UIViewController { return imageScrollView.zoomedFrame } + override var keyCommands: [UIKeyCommand]? { + return [ + UIKeyCommand( + title: NSLocalizedString("Close Image", comment: "Close Image"), + action: #selector(done(_:)), + input: " " + ) + ] + } + override func viewDidLoad() { super.viewDidLoad() diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 0ba555be4..73bc0cd0f 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -31,14 +31,15 @@ class WebViewController: UIViewController { private var topShowBarsViewConstraint: NSLayoutConstraint! private var bottomShowBarsViewConstraint: NSLayoutConstraint! - var webView: PreloadedWebView? { - return view.subviews[0] as? PreloadedWebView + var webView: WKWebView? { + return view.subviews[0] as? WKWebView } private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self) public var isFullScreenAvailable: Bool { return traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed } + private lazy var articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator); private lazy var transition = ImageTransition(controller: self) private var clickedImageCompletion: (() -> Void)? @@ -352,14 +353,6 @@ extension WebViewController: UIContextMenuInteractionDelegate { extension WebViewController: WKNavigationDelegate { - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - for (index, view) in view.subviews.enumerated() { - if index != 0, let oldWebView = view as? PreloadedWebView { - oldWebView.removeFromSuperview() - } - } - } - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if navigationAction.navigationType == .linkActivated { @@ -517,49 +510,57 @@ private extension WebViewController { return } - coordinator.webViewProvider.dequeueWebView() { webView in - - webView.ready { - - // Add the webview - webView.translatesAutoresizingMaskIntoConstraints = false - self.view.insertSubview(webView, at: 0) - NSLayoutConstraint.activate([ - self.view.leadingAnchor.constraint(equalTo: webView.leadingAnchor), - self.view.trailingAnchor.constraint(equalTo: webView.trailingAnchor), - self.view.topAnchor.constraint(equalTo: webView.topAnchor), - self.view.bottomAnchor.constraint(equalTo: webView.bottomAnchor) - ]) - - // UISplitViewController reports the wrong size to WKWebView which can cause horizontal - // rubberbanding on the iPad. This interferes with our UIPageViewController preventing - // us from easily swiping between WKWebViews. This hack fixes that. - webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: -1, bottom: 0, right: 0) - - webView.scrollView.setZoomScale(1.0, animated: false) - - self.view.setNeedsLayout() - self.view.layoutIfNeeded() - - // Configure the webview - webView.navigationDelegate = self - webView.uiDelegate = self - webView.scrollView.delegate = self - self.configureContextMenuInteraction() - - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.showFeedInspector) - - self.renderPage(webView) - - } - - } + let preferences = WKPreferences() + preferences.javaScriptCanOpenWindowsAutomatically = false + /// The defaults for `preferredContentMode` and `allowsContentJavaScript` are suitable + /// and don't need to be explicitly set. + /// `allowsContentJavaScript` replaces `WKPreferences.javascriptEnabled`. + let webpagePreferences = WKWebpagePreferences() + + let configuration = WKWebViewConfiguration() + configuration.defaultWebpagePreferences = webpagePreferences + configuration.preferences = preferences + configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs") + configuration.allowsInlineMediaPlayback = true + configuration.mediaTypesRequiringUserActionForPlayback = .audio + if #available(iOS 15.4, *) { + configuration.preferences.isElementFullscreenEnabled = true + } + configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) + + let webView = WKWebView(frame: self.view.bounds, configuration: configuration) + webView.isOpaque = false; + webView.backgroundColor = .clear; + + // Add the webview - using autolayout will cause fullscreen video to fail and lose the web view + webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.view.insertSubview(webView, at: 0) + + // UISplitViewController reports the wrong size to WKWebView which can cause horizontal + // rubberbanding on the iPad. This interferes with our UIPageViewController preventing + // us from easily swiping between WKWebViews. This hack fixes that. + webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: -1, bottom: 0, right: 0) + + webView.scrollView.setZoomScale(1.0, animated: false) + + self.view.setNeedsLayout() + self.view.layoutIfNeeded() + + // Configure the webview + webView.navigationDelegate = self + webView.uiDelegate = self + webView.scrollView.delegate = self + self.configureContextMenuInteraction() + + webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) + webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) + webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.showFeedInspector) + + self.renderPage(webView) } - func renderPage(_ webView: PreloadedWebView?) { + func renderPage(_ webView: WKWebView?) { guard let webView = webView else { return } let theme = ArticleThemesManager.shared.currentTheme diff --git a/iOS/Article/WebViewProvider.swift b/iOS/Article/WebViewProvider.swift deleted file mode 100644 index 4c49d3ed5..000000000 --- a/iOS/Article/WebViewProvider.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// WebViewProvider.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/21/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import Foundation -import RSCore -import WebKit - -/// WKWebView has an awful behavior of a flash to white on first load when in dark mode. -/// Keep a queue of WebViews where we've already done a trivial load so that by the time we need them in the UI, they're past the flash-to-shite part of their lifecycle. -class WebViewProvider: NSObject { - - private let articleIconSchemeHandler: ArticleIconSchemeHandler - private let operationQueue = MainThreadOperationQueue() - private var queue = NSMutableArray() - - init(coordinator: SceneCoordinator) { - articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator) - super.init() - replenishQueueIfNeeded() - } - - func replenishQueueIfNeeded() { - operationQueue.add(WebViewProviderReplenishQueueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler)) - } - - func dequeueWebView(completion: @escaping (PreloadedWebView) -> ()) { - operationQueue.add(WebViewProviderDequeueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler, completion: completion)) - operationQueue.add(WebViewProviderReplenishQueueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler)) - } - -} - -class WebViewProviderReplenishQueueOperation: MainThreadOperation { - - // MainThreadOperation - public var isCanceled = false - public var id: Int? - public weak var operationDelegate: MainThreadOperationDelegate? - public var name: String? = "WebViewProviderReplenishQueueOperation" - public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? - - private let minimumQueueDepth = 3 - - private var queue: NSMutableArray - private var articleIconSchemeHandler: ArticleIconSchemeHandler - - init(queue: NSMutableArray, articleIconSchemeHandler: ArticleIconSchemeHandler) { - self.queue = queue - self.articleIconSchemeHandler = articleIconSchemeHandler - } - - func run() { - while queue.count < minimumQueueDepth { - let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler) - webView.preload() - queue.insert(webView, at: 0) - } - self.operationDelegate?.operationDidComplete(self) - } - -} - -class WebViewProviderDequeueOperation: MainThreadOperation { - - // MainThreadOperation - public var isCanceled = false - public var id: Int? - public weak var operationDelegate: MainThreadOperationDelegate? - public var name: String? = "WebViewProviderFlushQueueOperation" - public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? - - private var queue: NSMutableArray - private var articleIconSchemeHandler: ArticleIconSchemeHandler - private var completion: (PreloadedWebView) -> () - - init(queue: NSMutableArray, articleIconSchemeHandler: ArticleIconSchemeHandler, completion: @escaping (PreloadedWebView) -> ()) { - self.queue = queue - self.articleIconSchemeHandler = articleIconSchemeHandler - self.completion = completion - } - - func run() { - if let webView = queue.lastObject as? PreloadedWebView { - self.completion(webView) - self.queue.remove(webView) - self.operationDelegate?.operationDidComplete(self) - return - } - - assertionFailure("Creating PreloadedWebView in \(#function); queue has run dry.") - - let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler) - webView.preload() - self.completion(webView) - self.operationDelegate?.operationDidComplete(self) - } - -} diff --git a/iOS/KeyboardManager.swift b/iOS/KeyboardManager.swift index 04cd279fb..780bf53d8 100644 --- a/iOS/KeyboardManager.swift +++ b/iOS/KeyboardManager.swift @@ -15,7 +15,7 @@ enum KeyboardType: String { case detail = "DetailKeyboardShortcuts" } -class KeyboardManager { +@MainActor final class KeyboardManager { private(set) var _keyCommands: [UIKeyCommand] var keyCommands: [UIKeyCommand] { diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift index a34551730..1161a41a2 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift @@ -15,7 +15,7 @@ protocol MasterFeedTableViewCellDelegate: AnyObject { func masterFeedTableViewCellDisclosureDidToggle(_ sender: MasterFeedTableViewCell, expanding: Bool) } -class MasterFeedTableViewCell : VibrantTableViewCell { +@MainActor final class MasterFeedTableViewCell : VibrantTableViewCell { weak var delegate: MasterFeedTableViewCellDelegate? diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift index d4c7338bb..1f1e266fb 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift @@ -9,7 +9,7 @@ import UIKit import RSCore -struct MasterFeedTableViewCellLayout { +@MainActor struct MasterFeedTableViewCellLayout { private static let indentWidth = CGFloat(integerLiteral: 15) private static let editingControlIndent = CGFloat(integerLiteral: 40) diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeader.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeader.swift index 4e10b0f09..a32c6a7ba 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeader.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeader.swift @@ -12,7 +12,7 @@ protocol MasterFeedTableViewSectionHeaderDelegate { func masterFeedTableViewSectionHeaderDisclosureDidToggle(_ sender: MasterFeedTableViewSectionHeader) } -class MasterFeedTableViewSectionHeader: UITableViewHeaderFooterView { +@MainActor class MasterFeedTableViewSectionHeader: UITableViewHeaderFooterView { var delegate: MasterFeedTableViewSectionHeaderDelegate? diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeaderLayout.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeaderLayout.swift index 73dce3f69..eeacff01f 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeaderLayout.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeaderLayout.swift @@ -9,7 +9,7 @@ import UIKit import RSCore -struct MasterFeedTableViewSectionHeaderLayout { +@MainActor struct MasterFeedTableViewSectionHeaderLayout { private static let labelMarginRight = CGFloat(integerLiteral: 8) private static let disclosureButtonSize = CGSize(width: 44, height: 44) diff --git a/iOS/MasterFeed/Cell/MasterFeedUnreadCountView.swift b/iOS/MasterFeed/Cell/MasterFeedUnreadCountView.swift index e084036b9..ce2b7d912 100644 --- a/iOS/MasterFeed/Cell/MasterFeedUnreadCountView.swift +++ b/iOS/MasterFeed/Cell/MasterFeedUnreadCountView.swift @@ -8,7 +8,7 @@ import UIKit -class MasterFeedUnreadCountView : UIView { +@MainActor class MasterFeedUnreadCountView : UIView { var padding: UIEdgeInsets { return UIEdgeInsets(top: 1.0, left: 9.0, bottom: 1.0, right: 9.0) @@ -35,8 +35,10 @@ class MasterFeedUnreadCountView : UIView { } var unreadCountString: String { - return unreadCount < 1 ? "" : "\(unreadCount)" + return unreadCount < 1 ? "" : numberFormatter.string(from: NSNumber(value: unreadCount))! } + + var numberFormatter: NumberFormatter! private var contentSizeIsValid = false private var _contentSize = CGSize.zero @@ -44,11 +46,21 @@ class MasterFeedUnreadCountView : UIView { override init(frame: CGRect) { super.init(frame: frame) self.isOpaque = false + + let formatter = NumberFormatter() + formatter.locale = Locale.current + formatter.numberStyle = .decimal + numberFormatter = formatter } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.isOpaque = false + + let formatter = NumberFormatter() + formatter.locale = Locale.current + formatter.numberStyle = .decimal + numberFormatter = formatter } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 7381bd7f5..9a1b033cb 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import WebKit import SwiftUI import Account import Articles @@ -37,7 +38,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma // If the first responder is the WKWebView (PreloadedWebView) we don't want to supply any keyboard // commands that the system is looking for by going up the responder chain. They will interfere with // the WKWebViews built in hardware keyboard shortcuts, specifically the up and down arrow keys. - guard let current = UIResponder.currentFirstResponder, !(current is PreloadedWebView) else { return nil } + guard let current = UIResponder.currentFirstResponder, !(current is WKWebView) else { return nil } return keyboardManager.keyCommands } @@ -564,6 +565,11 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma return } + if tableView.window == nil { + completion?() + return + } + tableView.performBatchUpdates { if let deletes = changes.deletes, !deletes.isEmpty { tableView.deleteSections(IndexSet(deletes), with: .middle) diff --git a/iOS/MasterFeed/RefreshProgressView.swift b/iOS/MasterFeed/RefreshProgressView.swift index ecec8fb61..05c030d2f 100644 --- a/iOS/MasterFeed/RefreshProgressView.swift +++ b/iOS/MasterFeed/RefreshProgressView.swift @@ -10,7 +10,7 @@ import SwiftUI import Account -struct RefreshProgressView: View { +@MainActor struct RefreshProgressView: View { static let width: CGFloat = 100 static let height: CGFloat = 5 diff --git a/iOS/MasterTimeline/Cell/MasterTimelineAccessibilityCellLayout.swift b/iOS/MasterTimeline/Cell/MasterTimelineAccessibilityCellLayout.swift index 7441e5ae8..6c5390ec3 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineAccessibilityCellLayout.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineAccessibilityCellLayout.swift @@ -9,7 +9,7 @@ import UIKit import RSCore -struct MasterTimelineAccessibilityCellLayout: MasterTimelineCellLayout { +@MainActor struct MasterTimelineAccessibilityCellLayout: MasterTimelineCellLayout { let height: CGFloat let unreadIndicatorRect: CGRect diff --git a/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift b/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift index 91cf05803..b5711e1e2 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift @@ -9,7 +9,7 @@ import UIKit import Articles -struct MasterTimelineCellData { +@MainActor struct MasterTimelineCellData { private static let noText = NSLocalizedString("label.text.no-text", comment: "(No Text)") diff --git a/iOS/MasterTimeline/Cell/MasterTimelineCellLayout.swift b/iOS/MasterTimeline/Cell/MasterTimelineCellLayout.swift index 6913a3989..44191a49d 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineCellLayout.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineCellLayout.swift @@ -21,7 +21,7 @@ protocol MasterTimelineCellLayout { } -extension MasterTimelineCellLayout { +@MainActor extension MasterTimelineCellLayout { static func rectForUnreadIndicator(_ point: CGPoint) -> CGRect { var r = CGRect.zero diff --git a/iOS/MasterTimeline/Cell/MasterTimelineDefaultCellLayout.swift b/iOS/MasterTimeline/Cell/MasterTimelineDefaultCellLayout.swift index 6b2ce58ad..85cf66048 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineDefaultCellLayout.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineDefaultCellLayout.swift @@ -9,7 +9,7 @@ import UIKit import RSCore -struct MasterTimelineDefaultCellLayout: MasterTimelineCellLayout { +@MainActor struct MasterTimelineDefaultCellLayout: MasterTimelineCellLayout { static let cellPadding = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 20) diff --git a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift index 394fe193e..23ddbbc72 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift @@ -9,7 +9,7 @@ import UIKit import RSCore -class MasterTimelineTableViewCell: VibrantTableViewCell { +@MainActor class MasterTimelineTableViewCell: VibrantTableViewCell { private let titleView = MasterTimelineTableViewCell.multiLineUILabel() private let summaryView = MasterTimelineTableViewCell.multiLineUILabel() diff --git a/iOS/MasterTimeline/Cell/MasterUnreadIndicatorView.swift b/iOS/MasterTimeline/Cell/MasterUnreadIndicatorView.swift index 32205f0f9..748bdb5cd 100644 --- a/iOS/MasterTimeline/Cell/MasterUnreadIndicatorView.swift +++ b/iOS/MasterTimeline/Cell/MasterUnreadIndicatorView.swift @@ -8,7 +8,7 @@ import UIKit -class MasterUnreadIndicatorView: UIView { +@MainActor class MasterUnreadIndicatorView: UIView { override func layoutSubviews() { super.layoutSubviews() diff --git a/iOS/MasterTimeline/Cell/MultilineUILabelSizer.swift b/iOS/MasterTimeline/Cell/MultilineUILabelSizer.swift index c5859ba42..4764629ff 100644 --- a/iOS/MasterTimeline/Cell/MultilineUILabelSizer.swift +++ b/iOS/MasterTimeline/Cell/MultilineUILabelSizer.swift @@ -26,7 +26,7 @@ struct TextFieldSizeInfo { let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then. } -final class MultilineUILabelSizer { +@MainActor final class MultilineUILabelSizer { private let numberOfLines: Int private let font: UIFont diff --git a/iOS/MasterTimeline/Cell/SingleLineUILabelSizer.swift b/iOS/MasterTimeline/Cell/SingleLineUILabelSizer.swift index 901df9a77..0af756044 100644 --- a/iOS/MasterTimeline/Cell/SingleLineUILabelSizer.swift +++ b/iOS/MasterTimeline/Cell/SingleLineUILabelSizer.swift @@ -12,7 +12,7 @@ import UIKit // Uses a cache. // Main thready only. -final class SingleLineUILabelSizer { +@MainActor final class SingleLineUILabelSizer { let font: UIFont private var cache = [String: CGSize]() diff --git a/iOS/MasterTimeline/MarkAsReadAlertController.swift b/iOS/MasterTimeline/MarkAsReadAlertController.swift index abee797e2..217a44c17 100644 --- a/iOS/MasterTimeline/MarkAsReadAlertController.swift +++ b/iOS/MasterTimeline/MarkAsReadAlertController.swift @@ -15,7 +15,7 @@ extension UIView: MarkAsReadAlertControllerSourceType {} extension UIBarButtonItem: MarkAsReadAlertControllerSourceType {} -struct MarkAsReadAlertController { +@MainActor struct MarkAsReadAlertController { static func confirm(_ controller: UIViewController?, coordinator: SceneCoordinator?, diff --git a/iOS/MasterTimeline/MasterTimelineDataSource.swift b/iOS/MasterTimeline/MasterTimelineDataSource.swift index 3647a93ae..156b04f28 100644 --- a/iOS/MasterTimeline/MasterTimelineDataSource.swift +++ b/iOS/MasterTimeline/MasterTimelineDataSource.swift @@ -8,7 +8,7 @@ import UIKit -class MasterTimelineDataSource: UITableViewDiffableDataSource where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable { +@MainActor final class MasterTimelineDataSource: UITableViewDiffableDataSource where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable { override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true diff --git a/iOS/MasterTimeline/MasterTimelineTitleView.swift b/iOS/MasterTimeline/MasterTimelineTitleView.swift index 5afec3d58..3eaecd70e 100644 --- a/iOS/MasterTimeline/MasterTimelineTitleView.swift +++ b/iOS/MasterTimeline/MasterTimelineTitleView.swift @@ -8,7 +8,7 @@ import UIKit -class MasterTimelineTitleView: UIView { +@MainActor final class MasterTimelineTitleView: UIView { @IBOutlet weak var iconView: IconView! @IBOutlet weak var label: UILabel! diff --git a/iOS/MasterTimeline/MasterTimelineUnreadCountView.swift b/iOS/MasterTimeline/MasterTimelineUnreadCountView.swift index 66a7a4b26..a89b37fd7 100644 --- a/iOS/MasterTimeline/MasterTimelineUnreadCountView.swift +++ b/iOS/MasterTimeline/MasterTimelineUnreadCountView.swift @@ -8,7 +8,7 @@ import UIKit -class MasterTimelineUnreadCountView: MasterFeedUnreadCountView { +@MainActor final class MasterTimelineUnreadCountView: MasterFeedUnreadCountView { override var padding: UIEdgeInsets { return UIEdgeInsets(top: 2.0, left: 9.0, bottom: 2.0, right: 9.0) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index b71cb92d1..63cd74021 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import WebKit import SwiftUI import RSCore import Account @@ -31,11 +32,15 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner private lazy var dataSource = makeDataSource() private let searchController = UISearchController(searchResultsController: nil) + private var markAsReadOnScrollWorkItem: DispatchWorkItem? + private var markAsReadOnScrollStart: Int? + private var markAsReadOnScrollEnd: Int? + private var lastVerticalPosition: CGFloat = 0 + var mainControllerIdentifier = MainControllerIdentifier.masterTimeline weak var coordinator: SceneCoordinator! var undoableCommands = [UndoableCommand]() - let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0) private let keyboardManager = KeyboardManager(type: .timeline) override var keyCommands: [UIKeyCommand]? { @@ -43,7 +48,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner // If the first responder is the WKWebView (PreloadedWebView) we don't want to supply any keyboard // commands that the system is looking for by going up the responder chain. They will interfere with // the WKWebViews built in hardware keyboard shortcuts, specifically the up and down arrow keys. - guard let current = UIResponder.currentFirstResponder, !(current is PreloadedWebView) else { return nil } + guard let current = UIResponder.currentFirstResponder, !(current is WKWebView) else { return nil } return keyboardManager.keyCommands } @@ -435,7 +440,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } override func scrollViewDidScroll(_ scrollView: UIScrollView) { - scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) + coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow() + markAsReadOnScroll() } // MARK: Notifications @@ -531,10 +537,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner updateUI() } - @objc func scrollPositionDidChange() { - coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow() - } - // MARK: Reloading func queueReloadAvailableCells() { @@ -679,7 +681,7 @@ private extension MasterTimelineViewController { func updateToolbar() { guard firstUnreadButton != nil else { return } - markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable + markAllAsReadButton.isEnabled = coordinator.canMarkAllAsRead() firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable if coordinator.isRootSplitCollapsed { @@ -696,6 +698,8 @@ private extension MasterTimelineViewController { } func applyChanges(animated: Bool, completion: (() -> Void)? = nil) { + lastVerticalPosition = 0 + if coordinator.articles.count == 0 { tableView.rowHeight = tableView.estimatedRowHeight } else { @@ -724,7 +728,6 @@ private extension MasterTimelineViewController { } func configure(_ cell: MasterTimelineTableViewCell, article: Article, indexPath: IndexPath) { - let iconImage = iconImageFor(article) let featuredImage = featuredImageFor(article) @@ -749,6 +752,54 @@ private extension MasterTimelineViewController { return nil } + func markAsReadOnScroll() { + // Only try to mark if we are scrolling up + defer { + lastVerticalPosition = tableView.contentOffset.y + } + guard lastVerticalPosition < tableView.contentOffset.y else { + return + } + + // Implement Mark As Read on Scroll where we mark after the leading edge goes a little beyond the safe area inset + guard AppDefaults.shared.markArticlesAsReadOnScroll, + lastVerticalPosition < tableView.contentOffset.y, + let firstVisibleIndexPath = tableView.indexPathsForVisibleRows?.first else { return } + + let firstVisibleRowRect = tableView.rectForRow(at: firstVisibleIndexPath) + guard tableView.convert(firstVisibleRowRect, to: nil).origin.y < tableView.safeAreaInsets.top - 20 else { return } + + // We only mark immediately after scrolling stops, not during, to prevent scroll hitching + markAsReadOnScrollWorkItem?.cancel() + markAsReadOnScrollWorkItem = DispatchWorkItem { [weak self] in + defer { + self?.markAsReadOnScrollStart = nil + self?.markAsReadOnScrollEnd = nil + } + + guard let start: Int = self?.markAsReadOnScrollStart, + let end: Int = self?.markAsReadOnScrollEnd ?? self?.markAsReadOnScrollStart, + start <= end, + let self = self else { + return + } + + let articles = Array(start...end) + .map({ IndexPath(row: $0, section: 0) }) + .compactMap({ self.dataSource.itemIdentifier(for: $0) }) + .filter({ $0.status.read == false }) + self.coordinator.markAllAsRead(articles) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: markAsReadOnScrollWorkItem!) + + // Here we are creating a range of rows to attempt to mark later with the work item + guard markAsReadOnScrollStart != nil else { + markAsReadOnScrollStart = max(firstVisibleIndexPath.row - 5, 0) + return + } + markAsReadOnScrollEnd = max(markAsReadOnScrollEnd ?? 0, firstVisibleIndexPath.row) + } + func toggleArticleReadStatusAction(_ article: Article) -> UIAction? { guard !article.status.read || article.isAvailableToMarkUnread else { return nil } @@ -876,7 +927,7 @@ private extension MasterTimelineViewController { } let articles = Array(fetchedArticles) - guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { + guard coordinator.canMarkAllAsRead(articles), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } @@ -899,7 +950,7 @@ private extension MasterTimelineViewController { } let articles = Array(fetchedArticles) - guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { + guard coordinator.canMarkAllAsRead(articles), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } diff --git a/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px 1.png b/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px 1.png deleted file mode 100644 index efdd38490..000000000 Binary files a/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px 1.png and /dev/null differ diff --git a/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px.png b/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px.png deleted file mode 100644 index efdd38490..000000000 Binary files a/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px.png and /dev/null differ diff --git a/iOS/Resources/Assets.xcassets/About.imageset/Contents.json b/iOS/Resources/Assets.xcassets/About.imageset/Contents.json deleted file mode 100644 index d3a718431..000000000 --- a/iOS/Resources/Assets.xcassets/About.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "AppIcon-1024px 1.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "AppIcon-1024px.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index 7b8c9d0c0..2c1075634 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -9,7 +9,7 @@ import UIKit import Account -class RootSplitViewController: UISplitViewController { +final class RootSplitViewController: UISplitViewController { var coordinator: SceneCoordinator! diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 52fca151b..0ecab2b4f 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -53,15 +53,13 @@ struct FeedNode: Hashable { } } -class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { +final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { var undoableCommands = [UndoableCommand]() var undoManager: UndoManager? { return rootSplitViewController.undoManager } - lazy var webViewProvider = WebViewProvider(coordinator: self) - private var activityManager = ActivityManager() private var rootSplitViewController: RootSplitViewController! @@ -111,7 +109,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } } - private var directlyMarkedAsUnreadArticles = Set
() + var directlyMarkedAsUnreadArticles = Set
() var prefersStatusBarHidden = false @@ -1043,10 +1041,18 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { completion?() } } + + func canMarkAllAsRead() -> Bool { + return articles.canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) + } + + func canMarkAllAsRead(_ articles: [Article]) -> Bool { + return articles.canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) + } func canMarkAboveAsRead(for article: Article) -> Bool { let articlesAboveArray = articles.articlesAbove(article: article) - return articlesAboveArray.canMarkAllAsRead() + return articlesAboveArray.canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) } func markAboveAsRead() { @@ -1064,7 +1070,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { func canMarkBelowAsRead(for article: Article) -> Bool { let articleBelowArray = articles.articlesBelow(article: article) - return articleBelowArray.canMarkAllAsRead() + return articleBelowArray.canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) } func markBelowAsRead() { diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 85974c41a..1cd4c50bb 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -12,7 +12,7 @@ import Account import Zip import RSCore -class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging { +@MainActor final class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging { var window: UIWindow? var coordinator: SceneCoordinator! @@ -195,7 +195,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging { do { try ArticleThemeDownloader.shared.handleFile(at: location) } catch { - self.presentError(error) + Task { @MainActor in + self.presentError(error) + } } } task.resume() diff --git a/iOS/Settings/AboutViewController.swift b/iOS/Settings/AboutViewController.swift new file mode 100644 index 000000000..d072aafca --- /dev/null +++ b/iOS/Settings/AboutViewController.swift @@ -0,0 +1,59 @@ +// +// AboutViewController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 4/25/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +class AboutViewController: UITableViewController { + + @IBOutlet weak var aboutTextView: UITextView! + @IBOutlet weak var creditsTextView: UITextView! + @IBOutlet weak var acknowledgmentsTextView: UITextView! + @IBOutlet weak var thanksTextView: UITextView! + @IBOutlet weak var dedicationTextView: UITextView! + + override func viewDidLoad() { + + super.viewDidLoad() + + configureCell(file: "About", textView: aboutTextView) + configureCell(file: "Credits", textView: creditsTextView) + configureCell(file: "Thanks", textView: thanksTextView) + configureCell(file: "Dedication", textView: dedicationTextView) + + let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0)) + buildLabel.font = UIFont.systemFont(ofSize: 11.0) + buildLabel.textColor = UIColor.gray + buildLabel.text = NSLocalizedString("Copyright © 2002-2023 Brent Simmons", comment: "Copyright") + buildLabel.numberOfLines = 0 + buildLabel.sizeToFit() + buildLabel.translatesAutoresizingMaskIntoConstraints = false + + 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 { + return UITableView.automaticDimension + } + +} + +private extension AboutViewController { + + func configureCell(file: String, textView: UITextView) { + let url = Bundle.main.url(forResource: file, withExtension: "rtf")! + let string = try! NSAttributedString(url: url, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) + textView.attributedText = string + textView.textColor = UIColor.label + textView.adjustsFontForContentSizeCategory = true + textView.font = .preferredFont(forTextStyle: .body) + } + +} diff --git a/iOS/Settings/Account and Extensions/Accounts/AccountsManagementView.swift b/iOS/Settings/Account and Extensions/Accounts/AccountsManagementView.swift index 19d0da441..5bf780cf7 100644 --- a/iOS/Settings/Account and Extensions/Accounts/AccountsManagementView.swift +++ b/iOS/Settings/Account and Extensions/Accounts/AccountsManagementView.swift @@ -22,21 +22,13 @@ public final class AccountManagementViewModel: ObservableObject { init() { refreshAccounts() - NotificationCenter.default.addObserver(forName: .AccountStateDidChange, object: nil, queue: .main) { [weak self] _ in - self?.refreshAccounts() - } + NotificationCenter.default.addObserver(self, selector: #selector(refreshAccounts(_:)), name: .AccountStateDidChange, object: nil) - NotificationCenter.default.addObserver(forName: .UserDidAddAccount, object: nil, queue: .main) { [weak self] _ in - self?.refreshAccounts() - } + NotificationCenter.default.addObserver(self, selector: #selector(refreshAccounts(_:)), name: .UserDidAddAccount, object: nil) - NotificationCenter.default.addObserver(forName: .UserDidDeleteAccount, object: nil, queue: .main) { [weak self] _ in - self?.refreshAccounts() - } + NotificationCenter.default.addObserver(self, selector: #selector(refreshAccounts(_:)), name: .UserDidDeleteAccount, object: nil) - NotificationCenter.default.addObserver(forName: .DisplayNameDidChange, object: nil, queue: .main) { [weak self] _ in - self?.refreshAccounts() - } + NotificationCenter.default.addObserver(self, selector: #selector(refreshAccounts(_:)), name: .DisplayNameDidChange, object: nil) } func temporarilyDeleteAccount(_ account: Account) { @@ -54,7 +46,8 @@ public final class AccountManagementViewModel: ObservableObject { self.refreshAccounts() } - private func refreshAccounts() { + @objc + private func refreshAccounts(_ sender: Any? = nil) { sortedActiveAccounts = AccountManager.shared.sortedActiveAccounts sortedInactiveAccounts = AccountManager.shared.sortedAccounts.filter({ $0.isActive == false }) } diff --git a/iOS/Settings/Account and Extensions/Extensions/AddExtensionListView.swift b/iOS/Settings/Account and Extensions/Extensions/AddExtensionListView.swift index f8a4dbb74..c7e81a742 100644 --- a/iOS/Settings/Account and Extensions/Extensions/AddExtensionListView.swift +++ b/iOS/Settings/Account and Extensions/Extensions/AddExtensionListView.swift @@ -50,9 +50,9 @@ struct AddExtensionListView: View { } } } - .onReceive(NotificationCenter.default.publisher(for: .ActiveExtensionPointsDidChange)) { _ in + .onReceive(NotificationCenter.default.publisher(for: .ActiveExtensionPointsDidChange), perform: { _ in dismiss() - } + }) } } diff --git a/iOS/Settings/Account and Extensions/Extensions/EnableExtensionPointView.swift b/iOS/Settings/Account and Extensions/Extensions/EnableExtensionPointView.swift index 20bea8a1e..767272762 100644 --- a/iOS/Settings/Account and Extensions/Extensions/EnableExtensionPointView.swift +++ b/iOS/Settings/Account and Extensions/Extensions/EnableExtensionPointView.swift @@ -32,9 +32,9 @@ struct EnableExtensionPointView: View { .navigationTitle(extensionPoint.title) .navigationBarTitleDisplayMode(.inline) .dismissOnExternalContextLaunch() - .onReceive(NotificationCenter.default.publisher(for: .ActiveExtensionPointsDidChange)) { _ in + .onReceive(NotificationCenter.default.publisher(for: .ActiveExtensionPointsDidChange), perform: { _ in dismiss() - } + }) .edgesIgnoringSafeArea(.bottom) } diff --git a/iOS/Settings/Account and Extensions/Extensions/ExtensionsManagementView.swift b/iOS/Settings/Account and Extensions/Extensions/ExtensionsManagementView.swift index a9f6a52ab..eb2fd676d 100644 --- a/iOS/Settings/Account and Extensions/Extensions/ExtensionsManagementView.swift +++ b/iOS/Settings/Account and Extensions/Extensions/ExtensionsManagementView.swift @@ -51,10 +51,9 @@ struct ExtensionsManagementView: View { } message: { Text("alert.message.cannot-undo-action", comment: "You can't undo this action.") } - .onReceive(NotificationCenter.default.publisher(for: .ActiveExtensionPointsDidChange)) { _ in + .onReceive(NotificationCenter.default.publisher(for: .ActiveExtensionPointsDidChange), perform: { _ in availableExtensionPointTypes = ExtensionPointManager.shared.availableExtensionPointTypes.sorted(by: { $0.title < $1.title }) - } - + }) } private var activeExtensionsSection: some View { diff --git a/iOS/Settings/Appearance/ArticleThemeManagerView.swift b/iOS/Settings/Appearance/ArticleThemeManagerView.swift index b7d7cbea5..cd2c177f0 100644 --- a/iOS/Settings/Appearance/ArticleThemeManagerView.swift +++ b/iOS/Settings/Appearance/ArticleThemeManagerView.swift @@ -16,8 +16,6 @@ struct ArticleThemeManagerView: View { @State private var showImportConfirmationAlert: (ArticleTheme?, Bool) = (nil, false) @State private var showImportErrorAlert: (Error?, Bool) = (nil, false) @State private var showImportSuccessAlert: Bool = false - @State private var installedFirstPartyThemes: [ArticleTheme] = [] - @State private var installedThirdPartyThemes: [ArticleTheme] = [] var body: some View { Form { @@ -36,9 +34,6 @@ struct ArticleThemeManagerView: View { } .navigationTitle(Text("navigation.title.article-themes", comment: "Article Themes")) - .task { - updateThemesArrays() - } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { @@ -137,9 +132,6 @@ struct ArticleThemeManagerView: View { actions: { }, message: { Text(verbatim: "\(showImportErrorAlert.0?.localizedDescription ?? "")") }) - .onReceive(themeManager.objectWillChange) { _ in - updateThemesArrays() - } } func articleThemeRow(_ theme: ArticleTheme) -> some View { @@ -176,12 +168,6 @@ struct ArticleThemeManagerView: View { } } } - - private func updateThemesArrays() { - installedFirstPartyThemes = themeManager.themeNames.map({ try? themeManager.articleThemeWithThemeName($0) }).compactMap({ $0 }).filter({ $0.isAppTheme }).sorted(by: { $0.name < $1.name }) - - installedThirdPartyThemes = themeManager.themeNames.map({ try? themeManager.articleThemeWithThemeName($0) }).compactMap({ $0 }).filter({ !$0.isAppTheme }).sorted(by: { $0.name < $1.name }) - } } struct ArticleThemeImporterView_Previews: PreviewProvider { diff --git a/iOS/Settings/Appearance/DisplayAndBehaviorsView.swift b/iOS/Settings/Appearance/DisplayAndBehaviorsView.swift index 42fa7b488..6f0fdaa94 100644 --- a/iOS/Settings/Appearance/DisplayAndBehaviorsView.swift +++ b/iOS/Settings/Appearance/DisplayAndBehaviorsView.swift @@ -22,14 +22,15 @@ struct DisplayAndBehaviorsView: View { Section(header: Text("label.text.timeline", comment: "Timeline")) { SettingsViewRows.sortOldestToNewest($appDefaults.timelineSortDirectionBool) SettingsViewRows.groupByFeed($appDefaults.timelineGroupByFeed) - SettingsViewRows.refreshToClearReadArticles($appDefaults.refreshClearsReadArticles) - SettingsViewRows.timelineLayout + SettingsRow.confirmMarkAllAsRead($appDefaults.confirmMarkAllAsRead) + SettingsRow.markAsReadOnScroll($appDefaults.markArticlesAsReadOnScroll) + SettingsRow.refreshToClearReadArticles($appDefaults.refreshClearsReadArticles) + SettingsRow.timelineLayout } Section(header: Text("label.text.articles", comment: "Articles")) { SettingsViewRows.themeSelection - SettingsViewRows.confirmMarkAllAsRead($appDefaults.confirmMarkAllAsRead) - SettingsViewRows.openLinksInNetNewsWire(Binding( + SettingsRow.openLinksInNetNewsWire(Binding( get: { !appDefaults.useSystemBrowser }, set: { appDefaults.useSystemBrowser = !$0 } )) diff --git a/iOS/Settings/General/SettingsRows.swift b/iOS/Settings/General/SettingsRows.swift index 2c7df5344..36d39528f 100644 --- a/iOS/Settings/General/SettingsRows.swift +++ b/iOS/Settings/General/SettingsRows.swift @@ -13,7 +13,7 @@ import UniformTypeIdentifiers // MARK: - Rows -struct SettingsViewRows { +struct SettingsRow { /// This row, when tapped, will open iOS System Settings. static var openSystemSettings: some View { @@ -23,7 +23,7 @@ struct SettingsViewRows { Image("system.settings") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 30.0, height: 30.0) + .frame(width: 25.0, height: 25.0) .clipShape(RoundedRectangle(cornerRadius: 6)) } .onTapGesture { @@ -40,7 +40,7 @@ struct SettingsViewRows { } icon: { Image("notifications.sounds") .resizable() - .frame(width: 30.0, height: 30.0) + .frame(width: 25.0, height: 25.0) .clipShape(RoundedRectangle(cornerRadius: 6)) } } @@ -55,12 +55,22 @@ struct SettingsViewRows { } icon: { Image("app.account") .resizable() - .frame(width: 30.0, height: 30.0) + .frame(width: 25.0, height: 25.0) .clipShape(RoundedRectangle(cornerRadius: 6)) } } } + + /// Toggle for determining if articles are marked as read when scrolling the timeline view. + /// - Parameter preference: `Binding` + /// - Returns: `some View` + static func markAsReadOnScroll(_ preference: Binding) -> some View { + Toggle(isOn: preference) { + Text("Mark As Read on Scroll", comment: "Mark As Read on Scroll") + } + } + /// This row, when tapped, will push the the Manage Extension screen /// in to view. static var manageExtensions: some View { @@ -70,7 +80,7 @@ struct SettingsViewRows { } icon: { Image("app.extension") .resizable() - .frame(width: 30.0, height: 30.0) + .frame(width: 25.0, height: 25.0) .clipShape(RoundedRectangle(cornerRadius: 6)) } } @@ -89,7 +99,7 @@ struct SettingsViewRows { } icon: { Image("app.import.opml") .resizable() - .frame(width: 30.0, height: 30.0) + .frame(width: 25.0, height: 25.0) .clipShape(RoundedRectangle(cornerRadius: 6)) } } @@ -108,7 +118,7 @@ struct SettingsViewRows { } icon: { Image("app.export.opml") .resizable() - .frame(width: 30.0, height: 30.0) + .frame(width: 25.0, height: 25.0) .clipShape(RoundedRectangle(cornerRadius: 6)) } } @@ -194,7 +204,7 @@ struct SettingsViewRows { } icon: { Image("app.appearance") .resizable() - .frame(width: 30.0, height: 30.0) + .frame(width: 25.0, height: 25.0) .clipShape(RoundedRectangle(cornerRadius: 6)) } } @@ -216,7 +226,7 @@ struct SettingsViewRows { .symbolRenderingMode(.hierarchical) .foregroundColor(Color(uiColor: AppAssets.primaryAccentColor)) .aspectRatio(contentMode: .fit) - .frame(width: 30.0, height: 30.0) + .frame(width: 25.0, height: 25.0) } .onTapGesture { selectedSheet.wrappedValue = sheet @@ -237,7 +247,7 @@ struct SettingsViewRows { .symbolRenderingMode(.hierarchical) .foregroundColor(Color(uiColor: AppAssets.primaryAccentColor)) .aspectRatio(contentMode: .fit) - .frame(width: 30.0, height: 30.0) + .frame(width: 25.0, height: 25.0) } } } diff --git a/iOS/Settings/General/SettingsView.swift b/iOS/Settings/General/SettingsView.swift index 18e74455a..dd478817c 100644 --- a/iOS/Settings/General/SettingsView.swift +++ b/iOS/Settings/General/SettingsView.swift @@ -14,6 +14,7 @@ import UserNotifications struct SettingsView: View { @Environment(\.dismiss) var dismiss + @Environment(\.scenePhase) var scenePhase @StateObject private var appDefaults = AppDefaults.shared @StateObject private var viewModel = SettingsViewModel() @@ -69,16 +70,16 @@ struct SettingsView: View { footer: Text("label.text.appearance-explainer", comment: "Manage the look, feel, and behavior of NetNewsWire.")) { SettingsViewRows.configureAppearance($isConfigureAppearanceShown) if viewModel.notificationPermissions == .authorized { - SettingsViewRows.configureNewArticleNotifications + SettingsRow.configureNewArticleNotifications } } // Help Section { ForEach(0.. [WebFeed] { diff --git a/iOS/TitleActivityItemSource.swift b/iOS/TitleActivityItemSource.swift index fff0609aa..ef5d9c3cc 100644 --- a/iOS/TitleActivityItemSource.swift +++ b/iOS/TitleActivityItemSource.swift @@ -29,8 +29,6 @@ class TitleActivityItemSource: NSObject, UIActivityItemSource { switch activityType.rawValue { case "com.omnigroup.OmniFocus3.iOS.QuickEntry", "com.culturedcode.ThingsiPhone.ShareExtension", - "com.tapbots.Tweetbot4.shareextension", - "com.tapbots.Tweetbot6.shareextension", "com.buffer.buffer.Buffer": return title default: diff --git a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig index 6cc3d75cc..c290664ef 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 = 6.1.1 -CURRENT_PROJECT_VERSION = 6114 +CURRENT_PROJECT_VERSION = 6115 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon diff --git a/xcconfig/common/NetNewsWire_mac_target_common.xcconfig b/xcconfig/common/NetNewsWire_mac_target_common.xcconfig index eb4b5b3a7..94e967287 100644 --- a/xcconfig/common/NetNewsWire_mac_target_common.xcconfig +++ b/xcconfig/common/NetNewsWire_mac_target_common.xcconfig @@ -1,6 +1,6 @@ // High Level Settings common to both the Mac application and any extensions we bundle with it -MARKETING_VERSION = 6.1.1b2 -CURRENT_PROJECT_VERSION = 6108 +MARKETING_VERSION = 6.2b1 +CURRENT_PROJECT_VERSION = 6115 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;