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 @@
-
+
@@ -104,15 +112,15 @@
-
-
+
+
-
+
@@ -127,7 +135,7 @@
-
+
@@ -147,7 +155,7 @@
-
+
@@ -158,15 +166,15 @@
-
-
+
+
-
+
@@ -201,15 +209,15 @@
-
-
+
+
-
+
@@ -223,7 +231,7 @@
-
+
@@ -233,43 +241,62 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
@@ -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;