diff --git a/Mac/MainWindow/Detail/Keyboard/DetailKeyboardDelegate.swift b/Mac/MainWindow/Detail/Keyboard/DetailKeyboardDelegate.swift index d2d3b49ea..50e878bbb 100644 --- a/Mac/MainWindow/Detail/Keyboard/DetailKeyboardDelegate.swift +++ b/Mac/MainWindow/Detail/Keyboard/DetailKeyboardDelegate.swift @@ -9,7 +9,7 @@ import AppKit import RSCore -@objc final class DetailKeyboardDelegate: NSObject, KeyboardDelegate { +@MainActor @objc final class DetailKeyboardDelegate: NSObject, KeyboardDelegate { let shortcuts: Set diff --git a/Mac/MainWindow/Keyboard/MainWIndowKeyboardHandler.swift b/Mac/MainWindow/Keyboard/MainWIndowKeyboardHandler.swift index 04483edca..d685acc57 100644 --- a/Mac/MainWindow/Keyboard/MainWIndowKeyboardHandler.swift +++ b/Mac/MainWindow/Keyboard/MainWIndowKeyboardHandler.swift @@ -9,7 +9,7 @@ import AppKit import RSCore -final class MainWindowKeyboardHandler: KeyboardDelegate { +@MainActor final class MainWindowKeyboardHandler: KeyboardDelegate { static let shared = MainWindowKeyboardHandler() let globalShortcuts: Set diff --git a/Mac/MainWindow/SharingServicePickerDelegate.swift b/Mac/MainWindow/SharingServicePickerDelegate.swift index 591062fc5..efc5222ac 100644 --- a/Mac/MainWindow/SharingServicePickerDelegate.swift +++ b/Mac/MainWindow/SharingServicePickerDelegate.swift @@ -12,8 +12,8 @@ import RSCore @objc final class SharingServicePickerDelegate: NSObject, NSSharingServicePickerDelegate { private let sharingServiceDelegate: SharingServiceDelegate - - init(_ window: NSWindow?) { + + @MainActor init(_ window: NSWindow?) { sharingServiceDelegate = SharingServiceDelegate(window) } @@ -27,14 +27,14 @@ import RSCore } static func customSharingServices(for items: [Any]) -> [NSSharingService] { + guard let object = items.first else { + return [NSSharingService]() + } + let customServices: [SendToCommand] = [SendToMarsEditCommand(), SendToMicroBlogCommand()] return customServices.compactMap { (sendToCommand) -> NSSharingService? in - guard let object = items.first else { - return nil - } - guard sendToCommand.canSendObject(object, selectedText: nil) else { return nil } diff --git a/Mac/MainWindow/Sidebar/Keyboard/SidebarKeyboardDelegate.swift b/Mac/MainWindow/Sidebar/Keyboard/SidebarKeyboardDelegate.swift index 26d5a299d..93b867662 100644 --- a/Mac/MainWindow/Sidebar/Keyboard/SidebarKeyboardDelegate.swift +++ b/Mac/MainWindow/Sidebar/Keyboard/SidebarKeyboardDelegate.swift @@ -9,7 +9,7 @@ import AppKit import RSCore -@objc final class SidebarKeyboardDelegate: NSObject, KeyboardDelegate { +@MainActor @objc final class SidebarKeyboardDelegate: NSObject, KeyboardDelegate { @IBOutlet weak var sidebarViewController: SidebarViewController? let shortcuts: Set diff --git a/Mac/MainWindow/Sidebar/PasteboardFeed.swift b/Mac/MainWindow/Sidebar/PasteboardFeed.swift index 7d2cca788..e3d07e235 100644 --- a/Mac/MainWindow/Sidebar/PasteboardFeed.swift +++ b/Mac/MainWindow/Sidebar/PasteboardFeed.swift @@ -181,7 +181,7 @@ extension Feed: PasteboardWriterOwner { return [FeedPasteboardWriter.feedUTIType, .URL, .string, FeedPasteboardWriter.feedUTIInternalType] } - func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { + @MainActor func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { let plist: Any? @@ -204,15 +204,15 @@ extension Feed: PasteboardWriterOwner { private extension FeedPasteboardWriter { - var pasteboardFeed: PasteboardFeed { + @MainActor var pasteboardFeed: PasteboardFeed { return PasteboardFeed(url: feed.url, feedID: feed.feedID, homePageURL: feed.homePageURL, name: feed.name, editedName: feed.editedName, accountID: feed.account?.accountID, accountType: feed.account?.type) } - var exportDictionary: PasteboardFeedDictionary { + @MainActor var exportDictionary: PasteboardFeedDictionary { return pasteboardFeed.exportDictionary() } - var internalDictionary: PasteboardFeedDictionary { + @MainActor var internalDictionary: PasteboardFeedDictionary { var dictionary = pasteboardFeed.internalDictionary() if dictionary[PasteboardFeed.Key.containerName] == nil, case let .folder(accountID, folderName) = containerID { diff --git a/Mac/MainWindow/Sidebar/PasteboardFolder.swift b/Mac/MainWindow/Sidebar/PasteboardFolder.swift index 2a7e14eb8..ba7a32656 100644 --- a/Mac/MainWindow/Sidebar/PasteboardFolder.swift +++ b/Mac/MainWindow/Sidebar/PasteboardFolder.swift @@ -109,7 +109,7 @@ extension Folder: PasteboardWriterOwner { return [.string, FolderPasteboardWriter.folderUTIInternalType] } - func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { + @MainActor func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { let plist: Any? @@ -127,11 +127,11 @@ extension Folder: PasteboardWriterOwner { } private extension FolderPasteboardWriter { - var pasteboardFolder: PasteboardFolder { + @MainActor var pasteboardFolder: PasteboardFolder { return PasteboardFolder(name: folder.name ?? "", folderID: String(folder.folderID), accountID: folder.account?.accountID) } - var internalDictionary: PasteboardFeedDictionary { + @MainActor var internalDictionary: PasteboardFeedDictionary { return pasteboardFolder.internalDictionary() } } diff --git a/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift b/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift index 80b58ff87..d0a26f28e 100644 --- a/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift +++ b/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift @@ -12,7 +12,7 @@ import Articles import RSCore import Account -@objc final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource { +@MainActor @objc final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource { let treeController: TreeController static let dragOperationNone = NSDragOperation(rawValue: 0) @@ -250,7 +250,7 @@ private extension SidebarOutlineDataSource { } } - func accountForNode(_ node: Node) -> Account? { + @MainActor func accountForNode(_ node: Node) -> Account? { if let account = node.representedObject as? Account { return account } @@ -263,7 +263,7 @@ private extension SidebarOutlineDataSource { return nil } - func commonAccountsFor(_ nodes: Set) -> Set { + @MainActor func commonAccountsFor(_ nodes: Set) -> Set { var accounts = Set() for node in nodes { guard let oneAccount = accountForNode(node) else { @@ -274,7 +274,7 @@ private extension SidebarOutlineDataSource { return accounts } - func accountHasFolderRepresentingAnyDraggedFolders(_ account: Account, _ draggedFolders: Set) -> Bool { + @MainActor func accountHasFolderRepresentingAnyDraggedFolders(_ account: Account, _ draggedFolders: Set) -> Bool { for draggedFolder in draggedFolders { if account.existingFolder(with: draggedFolder.name) != nil { return true @@ -283,7 +283,7 @@ private extension SidebarOutlineDataSource { return false } - func validateLocalFolderDrop(_ outlineView: NSOutlineView, _ draggedFolder: PasteboardFolder, _ parentNode: Node, _ index: Int) -> NSDragOperation { + @MainActor func validateLocalFolderDrop(_ outlineView: NSOutlineView, _ draggedFolder: PasteboardFolder, _ parentNode: Node, _ index: Int) -> NSDragOperation { guard let dropAccount = parentNode.representedObject as? Account, dropAccount.accountID != draggedFolder.accountID else { return SidebarOutlineDataSource.dragOperationNone } @@ -297,7 +297,7 @@ private extension SidebarOutlineDataSource { return .copy // different AccountIDs means can only copy } - func validateLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set, _ parentNode: Node, _ index: Int) -> NSDragOperation { + @MainActor func validateLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set, _ parentNode: Node, _ index: Int) -> NSDragOperation { guard let dropAccount = parentNode.representedObject as? Account else { return SidebarOutlineDataSource.dragOperationNone } @@ -315,7 +315,7 @@ private extension SidebarOutlineDataSource { return .copy // different AccountIDs means can only copy } - func copyFeedInAccount(_ feed: Feed, _ destination: Container ) { + @MainActor func copyFeedInAccount(_ feed: Feed, _ destination: Container ) { destination.account?.addFeed(feed, to: destination) { result in switch result { case .success: @@ -326,7 +326,7 @@ private extension SidebarOutlineDataSource { } } - func moveFeedInAccount(_ feed: Feed, _ source: Container, _ destination: Container) { + @MainActor func moveFeedInAccount(_ feed: Feed, _ source: Container, _ destination: Container) { BatchUpdate.shared.start() source.account?.moveFeed(feed, from: source, to: destination) { result in BatchUpdate.shared.end() @@ -339,7 +339,7 @@ private extension SidebarOutlineDataSource { } } - func copyFeedBetweenAccounts(_ feed: Feed, _ destinationContainer: Container) { + @MainActor func copyFeedBetweenAccounts(_ feed: Feed, _ destinationContainer: Container) { guard let destinationAccount = destinationContainer.account else { return } diff --git a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift index 427b2820d..150d26ebe 100644 --- a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift +++ b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift @@ -24,7 +24,7 @@ extension Article: PasteboardWriterOwner { static let articleUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.article" static let articleUTIInternalType = NSPasteboard.PasteboardType(rawValue: articleUTIInternal) - private lazy var renderedHTML: String = { + @MainActor private lazy var renderedHTML: String = { let rendering = ArticleRenderer.articleHTML(article: article, theme: ArticleThemesManager.shared.currentTheme) return rendering.html }() @@ -46,7 +46,7 @@ extension Article: PasteboardWriterOwner { return types } - func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { + @MainActor func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { let plist: Any? switch type { @@ -70,7 +70,7 @@ extension Article: PasteboardWriterOwner { private extension ArticlePasteboardWriter { - func plainText() -> String { + @MainActor func plainText() -> String { var s = "" if let title = article.title { @@ -137,7 +137,7 @@ private extension ArticlePasteboardWriter { static let accountID = "accountID" } - func exportDictionary() -> [String: Any] { + @MainActor func exportDictionary() -> [String: Any] { var d = [String: Any]() d[Key.articleID] = article.articleID @@ -163,7 +163,7 @@ private extension ArticlePasteboardWriter { return d } - func internalDictionary() -> [String: Any] { + @MainActor func internalDictionary() -> [String: Any] { var d = exportDictionary() d[Key.accountID] = article.accountID return d diff --git a/Mac/MainWindow/Timeline/Keyboard/TimelineKeyboardDelegate.swift b/Mac/MainWindow/Timeline/Keyboard/TimelineKeyboardDelegate.swift index fb228538b..b95f5bd1f 100644 --- a/Mac/MainWindow/Timeline/Keyboard/TimelineKeyboardDelegate.swift +++ b/Mac/MainWindow/Timeline/Keyboard/TimelineKeyboardDelegate.swift @@ -11,7 +11,7 @@ import RSCore // Doesn’t have any shortcuts of its own — they’re all in MainWindowKeyboardHandler. -@objc final class TimelineKeyboardDelegate: NSObject, KeyboardDelegate { +@MainActor @objc final class TimelineKeyboardDelegate: NSObject, KeyboardDelegate { @IBOutlet weak var timelineViewController: TimelineViewController? let shortcuts: Set diff --git a/Mac/Preferences/Accounts/AddAccountsView.swift b/Mac/Preferences/Accounts/AddAccountsView.swift index 068a25647..fb15b6543 100644 --- a/Mac/Preferences/Accounts/AddAccountsView.swift +++ b/Mac/Preferences/Accounts/AddAccountsView.swift @@ -162,7 +162,7 @@ struct AddAccountsView: View { } - var icloudAccount: some View { + @MainActor var icloudAccount: some View { VStack(alignment: .leading) { Text("label.text.cloudkit", comment: "iCloud") .font(.headline) @@ -261,7 +261,7 @@ struct AddAccountsView: View { } } - private func isCloudInUse() -> Bool { + @MainActor private func isCloudInUse() -> Bool { AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit }) } diff --git a/Mac/Scriptability/Account+Scriptability.swift b/Mac/Scriptability/Account+Scriptability.swift index 0c7c84380..093d2dd56 100644 --- a/Mac/Scriptability/Account+Scriptability.swift +++ b/Mac/Scriptability/Account+Scriptability.swift @@ -12,7 +12,7 @@ import Articles import RSCore @objc(ScriptableAccount) -class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { +@MainActor class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { let account:Account init (_ account:Account) { diff --git a/Mac/Scriptability/Article+Scriptability.swift b/Mac/Scriptability/Article+Scriptability.swift index 094a023a3..5a89428c9 100644 --- a/Mac/Scriptability/Article+Scriptability.swift +++ b/Mac/Scriptability/Article+Scriptability.swift @@ -11,7 +11,7 @@ import Account import Articles @objc(ScriptableArticle) -class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { +@MainActor class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { let article:Article let container:ScriptingObjectContainer @@ -111,7 +111,9 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta return article.status.boolStatus(forKey:.read) } set { - markArticles([self.article], statusKey: .read, flag: newValue) + Task { @MainActor in + markArticles([self.article], statusKey: .read, flag: newValue) + } } } @@ -121,7 +123,9 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta return article.status.boolStatus(forKey:.starred) } set { - markArticles([self.article], statusKey: .starred, flag: newValue) + Task { @MainActor in + markArticles([self.article], statusKey: .starred, flag: newValue) + } } } diff --git a/Mac/Scriptability/Folder+Scriptability.swift b/Mac/Scriptability/Folder+Scriptability.swift index 61925db59..6f88fd49d 100644 --- a/Mac/Scriptability/Folder+Scriptability.swift +++ b/Mac/Scriptability/Folder+Scriptability.swift @@ -50,7 +50,7 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai return self.classDescription as! NSScriptClassDescription } - func deleteElement(_ element:ScriptingObject) { + @MainActor func deleteElement(_ element:ScriptingObject) { if let scriptableFeed = element as? ScriptableFeed { BatchUpdate.shared.perform { folder.account?.removeFeed(scriptableFeed.feed, from: folder) { result in } @@ -65,38 +65,43 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai or tell account X to make new folder at end with properties {name:"new folder name"} */ - class func handleCreateElement(command:NSCreateCommand) -> Any? { + class func handleCreateElement(command:NSCreateCommand) -> Any? { guard command.isCreateCommand(forClass:"fold") else { return nil } let name = command.property(forKey:"name") as? String ?? "" - // some combination of the tell target and the location specifier ("in" or "at") - // identifies where the new folder should be created - let (account, folder) = command.accountAndFolderForNewChild() - guard folder == nil else { - print("support for folders within folders is NYI"); - return nil - } - command.suspendExecution() - - account.addFolder(name) { result in - switch result { - case .success(let folder): - let scriptableAccount = ScriptableAccount(account) - let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount) - command.resumeExecution(withResult:scriptableFolder.objectSpecifier) - case .failure: + + Task { @MainActor in + + // some combination of the tell target and the location specifier ("in" or "at") + // identifies where the new folder should be created + let (account, folder) = command.accountAndFolderForNewChild() + guard folder == nil else { + print("NetNewsWire does not support creating folders within folders."); command.resumeExecution(withResult:nil) + return + } + + + account.addFolder(name) { result in + switch result { + case .success(let folder): + let scriptableAccount = ScriptableAccount(account) + let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount) + command.resumeExecution(withResult:scriptableFolder.objectSpecifier) + case .failure: + command.resumeExecution(withResult:nil) + } } } - + return nil } // MARK: --- Scriptable elements --- @objc(feeds) - var feeds:NSArray { + @MainActor var feeds:NSArray { let feeds = Array(folder.topLevelFeeds) return feeds.map { ScriptableFeed($0, container:self) } as NSArray } diff --git a/Mac/Scriptability/NSScriptCommand+NetNewsWire.swift b/Mac/Scriptability/NSScriptCommand+NetNewsWire.swift index 1cc5e36e5..62311790b 100644 --- a/Mac/Scriptability/NSScriptCommand+NetNewsWire.swift +++ b/Mac/Scriptability/NSScriptCommand+NetNewsWire.swift @@ -26,7 +26,7 @@ extension NSScriptCommand { return true } - func accountAndFolderForNewChild() -> (Account, Folder?) { + @MainActor func accountAndFolderForNewChild() -> (Account, Folder?) { let appleEvent = self.appleEvent var account = AccountManager.shared.defaultAccount var folder:Folder? = nil diff --git a/Mac/Scriptability/WebFeed+Scriptability.swift b/Mac/Scriptability/WebFeed+Scriptability.swift index 3ed8d72ac..e3a169807 100644 --- a/Mac/Scriptability/WebFeed+Scriptability.swift +++ b/Mac/Scriptability/WebFeed+Scriptability.swift @@ -71,7 +71,7 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine return url } - class func scriptableFeed(_ feed: Feed, account: Account, folder: Folder?) -> ScriptableFeed { + @MainActor class func scriptableFeed(_ feed: Feed, account: Account, folder: Folder?) -> ScriptableFeed { let scriptableAccount = ScriptableAccount(account) if let folder = folder { let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount) @@ -85,33 +85,39 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine guard command.isCreateCommand(forClass:"Feed") else { return nil } guard let arguments = command.arguments else {return nil} let titleFromArgs = command.property(forKey:"name") as? String - let (account, folder) = command.accountAndFolderForNewChild() - guard let url = self.urlForNewFeed(arguments:arguments) else {return nil} - - if let existingFeed = account.existingFeed(withURL:url) { - return scriptableFeed(existingFeed, account:account, folder:folder).objectSpecifier - } - - let container: Container = folder != nil ? folder! : account - - // We need to download the feed and parse it. - // RSParser does the callback for the download on the main thread. - // Because we can't wait here (on the main thread) for the callback, we have to return from this function. - // Generally, returning from an AppleEvent handler function means that handling the Apple event is over, - // but we don’t yet have the result of the event yet, so we prevent the Apple event from returning by calling - // suspendExecution(). When we get the callback, we supply the event result and call resumeExecution(). - command.suspendExecution() - - account.createFeed(url: url, name: titleFromArgs, container: container, validateFeed: true) { result in - switch result { - case .success(let feed): - NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed]) - let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder) - command.resumeExecution(withResult:scriptableFeed.objectSpecifier) - case .failure: + + // We need to download the feed and parse it. + // RSParser does the callback for the download on the main thread. + // Because we can't wait here (on the main thread) for the callback, we have to return from this function. + // Generally, returning from an AppleEvent handler function means that handling the Apple event is over, + // but we don’t yet have the result of the event yet, so we prevent the Apple event from returning by calling + // suspendExecution(). When we get the callback, we supply the event result and call resumeExecution(). + command.suspendExecution() + + Task { @MainActor in + let (account, folder) = command.accountAndFolderForNewChild() + guard let url = self.urlForNewFeed(arguments:arguments) else { command.resumeExecution(withResult:nil) + return } + if let existingFeed = account.existingFeed(withURL:url) { + let scriptableFeed = scriptableFeed(existingFeed, account:account, folder:folder) + command.resumeExecution(withResult:scriptableFeed.objectSpecifier) + return + } + + let container: Container = folder != nil ? folder! : account + account.createFeed(url: url, name: titleFromArgs, container: container, validateFeed: true) { result in + switch result { + case .success(let feed): + NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed]) + let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder) + command.resumeExecution(withResult:scriptableFeed.objectSpecifier) + case .failure: + command.resumeExecution(withResult:nil) + } + } } return nil diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index f5c5fd6ba..cfe37e274 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -235,7 +235,7 @@ private extension ActivityManager { } #endif - func makeKeywords(_ article: Article) -> [String] { + @MainActor func makeKeywords(_ article: Article) -> [String] { let feedNameKeywords = makeKeywords(article.feed?.nameForDisplay) let articleTitleKeywords = makeKeywords(ArticleStringFormatter.truncatedTitle(article)) return feedNameKeywords + articleTitleKeywords @@ -274,11 +274,11 @@ private extension ActivityManager { activity.becomeCurrent() } - static func identifier(for folder: Folder) -> String { + @MainActor static func identifier(for folder: Folder) -> String { return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)" } - static func identifier(for feed: Feed) -> String { + @MainActor static func identifier(for feed: Feed) -> String { return "account_\(feed.account!.accountID)_feed_\(feed.feedID)" } diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index a207c1b14..197c439a1 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -14,7 +14,7 @@ import RSCore import Articles import Account -struct ArticleRenderer { +@MainActor struct ArticleRenderer { typealias Rendering = (style: String, html: String, title: String, baseURL: String) @@ -330,7 +330,7 @@ private extension ArticleRenderer { private extension Article { - var baseURL: URL? { + @MainActor var baseURL: URL? { var s = link if s == nil { s = feed?.homePageURL diff --git a/Shared/Commands/DeleteCommand.swift b/Shared/Commands/DeleteCommand.swift index f6b79d3c3..9aaf24386 100644 --- a/Shared/Commands/DeleteCommand.swift +++ b/Shared/Commands/DeleteCommand.swift @@ -12,7 +12,7 @@ import RSTree import Account import Articles -final class DeleteCommand: UndoableCommand { +@MainActor final class DeleteCommand: UndoableCommand { let treeController: TreeController? let undoManager: UndoManager @@ -112,7 +112,7 @@ private struct SidebarItemSpecifier { return nil } - init?(node: Node, errorHandler: @escaping (Error) -> ()) { + @MainActor init?(node: Node, errorHandler: @escaping (Error) -> ()) { var account: Account? @@ -142,7 +142,7 @@ private struct SidebarItemSpecifier { } - func delete(completion: @escaping () -> Void) { + @MainActor func delete(completion: @escaping () -> Void) { if let feed { @@ -170,7 +170,7 @@ private struct SidebarItemSpecifier { } } - func restore() { + @MainActor func restore() { if let _ = feed { restoreFeed() @@ -180,7 +180,7 @@ private struct SidebarItemSpecifier { } } - private func restoreFeed() { + @MainActor private func restoreFeed() { guard let account = account, let feed = feed, let container = path.resolveContainer() else { return @@ -194,7 +194,7 @@ private struct SidebarItemSpecifier { } - private func restoreFolder() { + @MainActor private func restoreFolder() { guard let account = account, let folder = folder else { return diff --git a/Shared/Commands/MarkStatusCommand.swift b/Shared/Commands/MarkStatusCommand.swift index 45502075a..da4755e7d 100644 --- a/Shared/Commands/MarkStatusCommand.swift +++ b/Shared/Commands/MarkStatusCommand.swift @@ -23,7 +23,7 @@ public extension Notification.Name { static let MarkStatusCommandDidUndoDirectMarking = Notification.Name("MarkStatusCommandDidUndoDirectMarking") } -final class MarkStatusCommand: UndoableCommand { +@MainActor final class MarkStatusCommand: UndoableCommand { let undoActionName: String let redoActionName: String diff --git a/Shared/ExtensionPoints/SendToMarsEditCommand.swift b/Shared/ExtensionPoints/SendToMarsEditCommand.swift index 89a867fff..71b778a89 100644 --- a/Shared/ExtensionPoints/SendToMarsEditCommand.swift +++ b/Shared/ExtensionPoints/SendToMarsEditCommand.swift @@ -23,23 +23,25 @@ final class SendToMarsEditCommand: SendToCommand { func sendObject(_ object: Any?, selectedText: String?) { - guard canSendObject(object, selectedText: selectedText) else { - return - } - guard let article = (object as? ArticlePasteboardWriter)?.article else { - return - } - guard let app = appToUse(), app.launchIfNeeded(), app.bringToFront() else { - return - } + Task { @MainActor in + guard canSendObject(object, selectedText: selectedText) else { + return + } + guard let article = (object as? ArticlePasteboardWriter)?.article else { + return + } + guard let app = appToUse(), app.launchIfNeeded(), app.bringToFront() else { + return + } - send(article, to: app) + send(article, to: app) + } } } private extension SendToMarsEditCommand { - func send(_ article: Article, to app: UserApp) { + @MainActor func send(_ article: Article, to app: UserApp) { // App has already been launched. diff --git a/Shared/ExtensionPoints/SendToMicroBlogCommand.swift b/Shared/ExtensionPoints/SendToMicroBlogCommand.swift index f458138d8..c33936c6c 100644 --- a/Shared/ExtensionPoints/SendToMicroBlogCommand.swift +++ b/Shared/ExtensionPoints/SendToMicroBlogCommand.swift @@ -31,36 +31,38 @@ final class SendToMicroBlogCommand: SendToCommand { func sendObject(_ object: Any?, selectedText: String?) { - guard canSendObject(object, selectedText: selectedText) else { - return - } - guard let article = (object as? ArticlePasteboardWriter)?.article else { - return - } - guard microBlogApp.launchIfNeeded(), microBlogApp.bringToFront() else { - return - } + Task { @MainActor in + guard canSendObject(object, selectedText: selectedText) else { + return + } + guard let article = (object as? ArticlePasteboardWriter)?.article else { + return + } + guard microBlogApp.launchIfNeeded(), microBlogApp.bringToFront() else { + return + } - // TODO: get text from contentHTML or contentText if no title and no selectedText. - // TODO: consider selectedText. + // TODO: get text from contentHTML or contentText if no title and no selectedText. + // TODO: consider selectedText. - let s = article.attributionString + article.linkString + let s = article.attributionString + article.linkString - let urlQueryDictionary = ["text": s] - guard let urlQueryString = urlQueryDictionary.urlQueryString else { - return + let urlQueryDictionary = ["text": s] + guard let urlQueryString = urlQueryDictionary.urlQueryString else { + return + } + guard let url = URL(string: "microblog://post?" + urlQueryString) else { + return + } + + NSWorkspace.shared.open(url) } - guard let url = URL(string: "microblog://post?" + urlQueryString) else { - return - } - - NSWorkspace.shared.open(url) } } private extension Article { - var attributionString: String { + @MainActor var attributionString: String { // Feed name, or feed name + author name (if author is specified per-article). // Includes trailing space. diff --git a/Shared/Extensions/AddFeedDefaultContainer.swift b/Shared/Extensions/AddFeedDefaultContainer.swift index 6bf7fa880..f53600168 100644 --- a/Shared/Extensions/AddFeedDefaultContainer.swift +++ b/Shared/Extensions/AddFeedDefaultContainer.swift @@ -9,7 +9,7 @@ import Foundation import Account -struct AddFeedDefaultContainer { +@MainActor struct AddFeedDefaultContainer { static var defaultContainer: Container? { diff --git a/Shared/Extensions/ArticleUtilities.swift b/Shared/Extensions/ArticleUtilities.swift index 8a1723a3b..ba7a66192 100644 --- a/Shared/Extensions/ArticleUtilities.swift +++ b/Shared/Extensions/ArticleUtilities.swift @@ -13,7 +13,7 @@ import Account // These handle multiple accounts. -func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) { +@MainActor func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) { let d: [String: Set
] = accountAndArticlesDictionary(articles) let group = DispatchGroup() @@ -93,7 +93,7 @@ extension Article { return datePublished ?? dateModified ?? status.dateArrived } - var isAvailableToMarkUnread: Bool { + @MainActor var isAvailableToMarkUnread: Bool { guard let markUnreadWindow = account?.behaviors.compactMap( { behavior -> Int? in switch behavior { case .disallowMarkAsUnreadAfterPeriod(let days): @@ -133,7 +133,7 @@ extension Article { } } - func byline() -> String { + @MainActor func byline() -> String { guard let authors = authors ?? feed?.authors, !authors.isEmpty else { return "" } @@ -194,7 +194,7 @@ struct ArticlePathKey { extension Article { - public var pathUserInfo: [AnyHashable : Any] { + @MainActor public var pathUserInfo: [AnyHashable : Any] { return [ ArticlePathKey.accountID: accountID, ArticlePathKey.accountName: account?.nameForDisplay ?? "", @@ -209,7 +209,7 @@ extension Article { extension Article: SortableArticle { - var sortableName: String { + @MainActor var sortableName: String { return feed?.name ?? "" } diff --git a/Shared/Favicons/FaviconDownloader.swift b/Shared/Favicons/FaviconDownloader.swift index fff2bbd9e..0c3f8d7d9 100644 --- a/Shared/Favicons/FaviconDownloader.swift +++ b/Shared/Favicons/FaviconDownloader.swift @@ -18,7 +18,7 @@ extension Notification.Name { static let FaviconDidBecomeAvailable = Notification.Name("FaviconDidBecomeAvailableNotification") // userInfo key: FaviconDownloader.UserInfoKey.faviconURL } -final class FaviconDownloader: Logging { +@MainActor final class FaviconDownloader: Logging { private static let saveQueue = CoalescingQueue(name: "Cache Save Queue", interval: 1.0) @@ -280,11 +280,15 @@ private extension FaviconDownloader { } func queueSaveHomePageToFaviconURLCacheIfNeeded() { - FaviconDownloader.saveQueue.add(self, #selector(saveHomePageToFaviconURLCacheIfNeeded)) + Task { @MainActor in + FaviconDownloader.saveQueue.add(self, #selector(saveHomePageToFaviconURLCacheIfNeeded)) + } } func queueSaveHomePageURLsWithNoFaviconURLCacheIfNeeded() { - FaviconDownloader.saveQueue.add(self, #selector(saveHomePageURLsWithNoFaviconURLCacheIfNeeded)) + Task { @MainActor in + FaviconDownloader.saveQueue.add(self, #selector(saveHomePageURLsWithNoFaviconURLCacheIfNeeded)) + } } func saveHomePageToFaviconURLCache() { diff --git a/Shared/Images/FeedIconDownloader.swift b/Shared/Images/FeedIconDownloader.swift index 911f75022..fcdfd4b92 100644 --- a/Shared/Images/FeedIconDownloader.swift +++ b/Shared/Images/FeedIconDownloader.swift @@ -20,7 +20,7 @@ extension Notification.Name { public final class FeedIconDownloader { - private static let saveQueue = CoalescingQueue(name: "Cache Save Queue", interval: 1.0) + @MainActor private static let saveQueue = CoalescingQueue(name: "Cache Save Queue", interval: 1.0) private let imageDownloader: ImageDownloader @@ -28,7 +28,9 @@ public final class FeedIconDownloader { private var feedURLToIconURLCachePath: String private var feedURLToIconURLCacheDirty = false { didSet { - queueSaveFeedURLToIconURLCacheIfNeeded() + Task { @MainActor in + queueSaveFeedURLToIconURLCacheIfNeeded() + } } } @@ -36,7 +38,9 @@ public final class FeedIconDownloader { private var homePageToIconURLCachePath: String private var homePageToIconURLCacheDirty = false { didSet { - queueSaveHomePageToIconURLCacheIfNeeded() + Task { @MainActor in + queueSaveHomePageToIconURLCacheIfNeeded() + } } } @@ -44,7 +48,9 @@ public final class FeedIconDownloader { private var homePagesWithNoIconURLCachePath: String private var homePagesWithNoIconURLCacheDirty = false { didSet { - queueHomePagesWithNoIconURLCacheIfNeeded() + Task { @MainActor in + queueHomePagesWithNoIconURLCacheIfNeeded() + } } } @@ -255,15 +261,15 @@ private extension FeedIconDownloader { homePagesWithNoIconURLCache = Set(decoded) } - func queueSaveFeedURLToIconURLCacheIfNeeded() { + @MainActor func queueSaveFeedURLToIconURLCacheIfNeeded() { FeedIconDownloader.saveQueue.add(self, #selector(saveFeedURLToIconURLCacheIfNeeded)) } - func queueSaveHomePageToIconURLCacheIfNeeded() { + @MainActor func queueSaveHomePageToIconURLCacheIfNeeded() { FeedIconDownloader.saveQueue.add(self, #selector(saveHomePageToIconURLCacheIfNeeded)) } - func queueHomePagesWithNoIconURLCacheIfNeeded() { + @MainActor func queueHomePagesWithNoIconURLCacheIfNeeded() { FeedIconDownloader.saveQueue.add(self, #selector(saveHomePagesWithNoIconURLCacheIfNeeded)) } diff --git a/Shared/ShareExtension/ExtensionContainers.swift b/Shared/ShareExtension/ExtensionContainers.swift index b283f177a..919654ccb 100644 --- a/Shared/ShareExtension/ExtensionContainers.swift +++ b/Shared/ShareExtension/ExtensionContainers.swift @@ -55,7 +55,7 @@ struct ExtensionAccount: ExtensionContainer { let containerID: ContainerIdentifier? let folders: [ExtensionFolder] - init(account: Account) { + @MainActor init(account: Account) { self.name = account.nameForDisplay self.accountID = account.accountID self.type = account.type @@ -84,7 +84,7 @@ struct ExtensionFolder: ExtensionContainer { let name: String let containerID: ContainerIdentifier? - init(folder: Folder) { + @MainActor init(folder: Folder) { self.accountName = folder.account?.nameForDisplay ?? "" self.accountID = folder.account?.accountID ?? "" self.name = folder.nameForDisplay diff --git a/Shared/ShareExtension/ExtensionContainersFile.swift b/Shared/ShareExtension/ExtensionContainersFile.swift index 6e85f989b..f8a58eda9 100644 --- a/Shared/ShareExtension/ExtensionContainersFile.swift +++ b/Shared/ShareExtension/ExtensionContainersFile.swift @@ -21,10 +21,12 @@ final class ExtensionContainersFile: Logging { private var isDirty = false { didSet { - queueSaveToDiskIfNeeded() + Task { @MainActor in + queueSaveToDiskIfNeeded() + } } } - private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5) + @MainActor private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5) init() { if !FileManager.default.fileExists(atPath: ExtensionContainersFile.filePath) { @@ -66,7 +68,7 @@ private extension ExtensionContainersFile { isDirty = true } - func queueSaveToDiskIfNeeded() { + @MainActor func queueSaveToDiskIfNeeded() { saveQueue.add(self, #selector(saveToDiskIfNeeded)) } diff --git a/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift b/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift index cd8ad978f..d5fd9d137 100644 --- a/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift +++ b/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift @@ -28,7 +28,7 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter, Logging { return operationQueue } - override init() { + @MainActor override init() { operationQueue = OperationQueue() operationQueue.maxConcurrentOperationCount = 1 @@ -46,7 +46,9 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter, Logging { func resume() { NSFileCoordinator.addFilePresenter(self) - process() + Task { @MainActor in + process() + } } func suspend() { @@ -93,7 +95,7 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter, Logging { private extension ExtensionFeedAddRequestFile { - func process() { + @MainActor func process() { let decoder = PropertyListDecoder() let encoder = PropertyListEncoder() @@ -128,7 +130,7 @@ private extension ExtensionFeedAddRequestFile { requests?.forEach { processRequest($0) } } - func processRequest(_ request: ExtensionFeedAddRequest) { + @MainActor func processRequest(_ request: ExtensionFeedAddRequest) { var destinationAccountID: String? = nil switch request.destinationContainerID { case .account(let accountID): diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index c3d6a3686..d72f9a375 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -107,7 +107,9 @@ extension SmartFeed: ArticleFetcher { private extension SmartFeed { func queueFetchUnreadCounts() { - CoalescingQueue.standard.add(self, #selector(fetchUnreadCounts)) + Task { @MainActor in + CoalescingQueue.standard.add(self, #selector(fetchUnreadCounts)) + } } @MainActor func fetchUnreadCount(for account: Account) { diff --git a/Shared/SmartFeeds/SmartFeedDelegate.swift b/Shared/SmartFeeds/SmartFeedDelegate.swift index d30b53f13..a119cb2f7 100644 --- a/Shared/SmartFeeds/SmartFeedDelegate.swift +++ b/Shared/SmartFeeds/SmartFeedDelegate.swift @@ -17,9 +17,9 @@ protocol SmartFeedDelegate: ItemIdentifiable, DisplayNameProvider, ArticleFetche func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock) } -extension SmartFeedDelegate { +@MainActor extension SmartFeedDelegate { - @MainActor func fetchArticles() throws -> Set
{ + func fetchArticles() throws -> Set
{ return try AccountManager.shared.fetchArticles(fetchType) } diff --git a/Shared/SmartFeeds/SmartFeedsController.swift b/Shared/SmartFeeds/SmartFeedsController.swift index afe330aab..4a8b54a0e 100644 --- a/Shared/SmartFeeds/SmartFeedsController.swift +++ b/Shared/SmartFeeds/SmartFeedsController.swift @@ -10,7 +10,7 @@ import Foundation import RSCore import Account -final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable { +@MainActor final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable { var containerID: ContainerIdentifier? { return ContainerIdentifier.smartFeedController diff --git a/Shared/Timeline/ArticleArray.swift b/Shared/Timeline/ArticleArray.swift index 7f99e8e6e..8c43c13a5 100644 --- a/Shared/Timeline/ArticleArray.swift +++ b/Shared/Timeline/ArticleArray.swift @@ -67,7 +67,7 @@ extension Array where Element == Article { return false } - func anyArticleIsReadAndCanMarkUnread() -> Bool { + @MainActor func anyArticleIsReadAndCanMarkUnread() -> Bool { return anyArticlePassesTest { $0.status.read && $0.isAvailableToMarkUnread } } @@ -95,7 +95,7 @@ extension Array where Element == Article { var i = 0 for article in self { let otherArticle = otherArticles[i] - if article.account != otherArticle.account || article.articleID != otherArticle.articleID { + if article.accountID != otherArticle.accountID || article.articleID != otherArticle.articleID { return false } i += 1 diff --git a/Shared/Timeline/FetchRequestOperation.swift b/Shared/Timeline/FetchRequestOperation.swift index 1abb52da9..64f7d6f83 100644 --- a/Shared/Timeline/FetchRequestOperation.swift +++ b/Shared/Timeline/FetchRequestOperation.swift @@ -33,7 +33,7 @@ final class FetchRequestOperation { self.resultBlock = resultBlock } - func run(_ completion: @escaping (FetchRequestOperation) -> Void) { + @MainActor func run(_ completion: @escaping (FetchRequestOperation) -> Void) { precondition(Thread.isMainThread) precondition(!isFinished) diff --git a/Shared/Timeline/FetchRequestQueue.swift b/Shared/Timeline/FetchRequestQueue.swift index 4fd9ff093..a895ddf6f 100644 --- a/Shared/Timeline/FetchRequestQueue.swift +++ b/Shared/Timeline/FetchRequestQueue.swift @@ -10,7 +10,7 @@ import Foundation // Main thread only. -final class FetchRequestQueue { +@MainActor final class FetchRequestQueue { private var pendingRequests = [FetchRequestOperation]() private var currentRequest: FetchRequestOperation? = nil diff --git a/Shared/Timer/AccountRefreshTimer.swift b/Shared/Timer/AccountRefreshTimer.swift index b0e81fb4e..9b5310fc1 100644 --- a/Shared/Timer/AccountRefreshTimer.swift +++ b/Shared/Timer/AccountRefreshTimer.swift @@ -9,7 +9,7 @@ import Foundation import Account -class AccountRefreshTimer { +@MainActor class AccountRefreshTimer { var shuttingDown = false @@ -75,5 +75,4 @@ class AccountRefreshTimer { AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log, completion: nil) } - } diff --git a/Shared/Timer/ArticleStatusSyncTimer.swift b/Shared/Timer/ArticleStatusSyncTimer.swift index 32e3cf604..5e0d8ffae 100644 --- a/Shared/Timer/ArticleStatusSyncTimer.swift +++ b/Shared/Timer/ArticleStatusSyncTimer.swift @@ -19,7 +19,7 @@ class ArticleStatusSyncTimer { private var lastTimedRefresh: Date? private let launchTime = Date() - func fireOldTimer() { + @MainActor func fireOldTimer() { if let timer = internalTimer { if timer.fireDate < Date() { timedRefresh(nil) @@ -59,7 +59,7 @@ class ArticleStatusSyncTimer { } - @objc func timedRefresh(_ sender: Timer?) { + @MainActor @objc func timedRefresh(_ sender: Timer?) { guard !shuttingDown else { return diff --git a/Shared/Tree/FeedTreeControllerDelegate.swift b/Shared/Tree/FeedTreeControllerDelegate.swift index 846d01077..523892385 100644 --- a/Shared/Tree/FeedTreeControllerDelegate.swift +++ b/Shared/Tree/FeedTreeControllerDelegate.swift @@ -41,7 +41,7 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate { private extension FeedTreeControllerDelegate { - func childNodesForRootNode(_ rootNode: Node) -> [Node]? { + @MainActor func childNodesForRootNode(_ rootNode: Node) -> [Node]? { var topLevelNodes = [Node]() let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared) @@ -54,7 +54,7 @@ private extension FeedTreeControllerDelegate { return topLevelNodes } - func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] { + @MainActor func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] { return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in // All Smart Feeds should remain visible despite the Hide Read Feeds setting return parentNode.existingOrNewChildNode(with: feed as AnyObject) @@ -132,7 +132,7 @@ private extension FeedTreeControllerDelegate { return node } - func sortedAccountNodes(_ parent: Node) -> [Node] { + @MainActor func sortedAccountNodes(_ parent: Node) -> [Node] { let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in let accountNode = parent.existingOrNewChildNode(with: account) accountNode.canHaveChildNodes = true diff --git a/Shared/Tree/FolderTreeControllerDelegate.swift b/Shared/Tree/FolderTreeControllerDelegate.swift index 2c1e60cf2..7ab644ed7 100644 --- a/Shared/Tree/FolderTreeControllerDelegate.swift +++ b/Shared/Tree/FolderTreeControllerDelegate.swift @@ -14,7 +14,7 @@ import Account final class FolderTreeControllerDelegate: TreeControllerDelegate { - func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { + @MainActor func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { return node.isRoot ? childNodesForRootNode(node) : childNodes(node) } @@ -22,7 +22,7 @@ final class FolderTreeControllerDelegate: TreeControllerDelegate { private extension FolderTreeControllerDelegate { - func childNodesForRootNode(_ node: Node) -> [Node]? { + @MainActor func childNodesForRootNode(_ node: Node) -> [Node]? { let accountNodes: [Node] = AccountManager.shared.sortedActiveAccounts.map { account in let accountNode = Node(representedObject: account, parent: node) @@ -33,7 +33,7 @@ private extension FolderTreeControllerDelegate { } - func childNodes(_ node: Node) -> [Node]? { + @MainActor func childNodes(_ node: Node) -> [Node]? { guard let account = node.representedObject as? Account, let folders = account.folders else { return nil