Mark many things as MainActor and deal with the fallout.

This commit is contained in:
Brent Simmons 2024-03-25 21:10:37 -07:00
parent 87db1e3d5f
commit 27d27cbf1a
34 changed files with 693 additions and 625 deletions

View File

@ -62,7 +62,7 @@ public enum FetchType {
case searchWithArticleIDs(String, Set<String>) case searchWithArticleIDs(String, Set<String>)
} }
public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable { @MainActor public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable {
public struct UserInfoKey { public struct UserInfoKey {
public static let account = "account" // UserDidAddAccount, UserDidDeleteAccount public static let account = "account" // UserDidAddAccount, UserDidDeleteAccount
@ -894,6 +894,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return return
} }
database.createStatusesIfNeeded(articleIDs: articleIDs) { error in database.createStatusesIfNeeded(articleIDs: articleIDs) { error in
MainActor.assumeIsolated {
if let error = error { if let error = error {
completion?(error) completion?(error)
return return
@ -902,6 +904,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
completion?(nil) completion?(nil)
} }
} }
}
/// Mark articleIDs statuses based on statusKey and flag. /// Mark articleIDs statuses based on statusKey and flag.
/// Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification. /// Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
@ -912,6 +915,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return return
} }
database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in
MainActor.assumeIsolated {
if let error { if let error {
completion?(error) completion?(error)
} else { } else {
@ -920,6 +924,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
} }
} }
} }
}
/// Mark articleIDs as read. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification. /// Mark articleIDs as read. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
/// Returns a set of new article statuses. /// Returns a set of new article statuses.

View File

@ -11,7 +11,7 @@ import Articles
import RSWeb import RSWeb
import Secrets import Secrets
protocol AccountDelegate { @MainActor protocol AccountDelegate {
var behaviors: AccountBehaviors { get } var behaviors: AccountBehaviors { get }

View File

@ -14,18 +14,22 @@ public enum AccountError: LocalizedError {
case createErrorNotFound case createErrorNotFound
case createErrorAlreadySubscribed case createErrorAlreadySubscribed
case opmlImportInProgress case opmlImportInProgress
case wrappedError(error: Error, account: Account) case wrappedError(error: Error, accountID: String, accountName: String)
public var account: Account? { @MainActor public var account: Account? {
if case .wrappedError(_, let account) = self { if case .wrappedError(_, let accountID, _) = self {
return account return AccountManager.shared.existingAccount(with: accountID)
} else { } else {
return nil return nil
} }
} }
public var isCredentialsError: Bool { @MainActor public static func wrappedError(error: Error, account: Account) -> AccountError {
if case .wrappedError(let error, _) = self { wrappedError(error: error, accountID: account.accountID, accountName: account.nameForDisplay)
}
@MainActor public var isCredentialsError: Bool {
if case .wrappedError(let error, _, _) = self {
if case TransportError.httpError(let status) = error { if case TransportError.httpError(let status) = error {
return isCredentialsError(status: status) return isCredentialsError(status: status)
} }
@ -41,17 +45,17 @@ public enum AccountError: LocalizedError {
return NSLocalizedString("You are already subscribed to this feed and cant add it again.", comment: "Already subscribed") return NSLocalizedString("You are already subscribed to this feed and cant add it again.", comment: "Already subscribed")
case .opmlImportInProgress: case .opmlImportInProgress:
return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running") return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running")
case .wrappedError(let error, let account): case .wrappedError(let error, _, let accountName):
switch error { switch error {
case TransportError.httpError(let status): case TransportError.httpError(let status):
if isCredentialsError(status: status) { if isCredentialsError(status: status) {
let localizedText = NSLocalizedString("Your “%@” credentials are invalid or expired.", comment: "Invalid or expired") let localizedText = NSLocalizedString("Your “%@” credentials are invalid or expired.", comment: "Invalid or expired")
return NSString.localizedStringWithFormat(localizedText as NSString, account.nameForDisplay) as String return NSString.localizedStringWithFormat(localizedText as NSString, accountName) as String
} else { } else {
return unknownError(error, account) return unknownError(error, accountName)
} }
default: default:
return unknownError(error, account) return unknownError(error, accountName)
} }
} }
} }
@ -62,7 +66,7 @@ public enum AccountError: LocalizedError {
return nil return nil
case .createErrorAlreadySubscribed: case .createErrorAlreadySubscribed:
return nil return nil
case .wrappedError(let error, _): case .wrappedError(let error, _, _):
switch error { switch error {
case TransportError.httpError(let status): case TransportError.httpError(let status):
if isCredentialsError(status: status) { if isCredentialsError(status: status) {
@ -84,9 +88,9 @@ public enum AccountError: LocalizedError {
private extension AccountError { private extension AccountError {
func unknownError(_ error: Error, _ account: Account) -> String { func unknownError(_ error: Error, _ accountName: String) -> String {
let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error") let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error")
return NSString.localizedStringWithFormat(localizedText as NSString, account.nameForDisplay, error.localizedDescription) as String return NSString.localizedStringWithFormat(localizedText as NSString, accountName, error.localizedDescription) as String
} }
func isCredentialsError(status: Int) -> Bool { func isCredentialsError(status: Int) -> Bool {

View File

@ -15,7 +15,7 @@ import Secrets
// Main thread only. // Main thread only.
public final class AccountManager: UnreadCountProvider { @MainActor public final class AccountManager: UnreadCountProvider {
@MainActor public static var shared: AccountManager! @MainActor public static var shared: AccountManager!
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml" public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"

View File

@ -28,7 +28,7 @@ enum CloudKitAccountDelegateError: LocalizedError {
} }
} }
final class CloudKitAccountDelegate: AccountDelegate { @MainActor final class CloudKitAccountDelegate: AccountDelegate {
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
@ -416,12 +416,14 @@ final class CloudKitAccountDelegate: AccountDelegate {
self.database.insertStatuses(syncStatuses) { _ in self.database.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in self.database.selectPendingCount { result in
MainActor.assumeIsolated {
if let count = try? result.get(), count > 100 { if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account, showProgress: false) { _ in } self.sendArticleStatus(for: account, showProgress: false) { _ in }
} }
completion(.success(())) completion(.success(()))
} }
} }
}
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
@ -648,6 +650,7 @@ private extension CloudKitAccountDelegate {
if let parsedFeed = parsedFeed { if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed) { result in account.update(feed, with: parsedFeed) { result in
MainActor.assumeIsolated {
switch result { switch result {
case .success: case .success:
@ -676,7 +679,7 @@ private extension CloudKitAccountDelegate {
self.refreshProgress.completeTasks(3) self.refreshProgress.completeTasks(3)
completion(.failure(error)) completion(.failure(error))
} }
}
} }
} else { } else {
self.refreshProgress.completeTasks(3) self.refreshProgress.completeTasks(3)

View File

@ -20,7 +20,8 @@ enum CloudKitAccountZoneError: LocalizedError {
return NSLocalizedString("An unexpected CloudKit error occurred.", comment: "An unexpected CloudKit error occurred.") return NSLocalizedString("An unexpected CloudKit error occurred.", comment: "An unexpected CloudKit error occurred.")
} }
} }
final class CloudKitAccountZone: CloudKitZone {
@MainActor final class CloudKitAccountZone: CloudKitZone {
var zoneID: CKRecordZone.ID var zoneID: CKRecordZone.ID

View File

@ -13,7 +13,7 @@ import CloudKit
import Articles import Articles
import CloudKitExtras import CloudKitExtras
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { @MainActor final class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
private typealias UnclaimedFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String) private typealias UnclaimedFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String)
private var newUnclaimedFeeds = [String: [UnclaimedFeed]]() private var newUnclaimedFeeds = [String: [UnclaimedFeed]]()
@ -135,7 +135,6 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
account?.removeFolder(folder) account?.removeFolder(folder)
} }
} }
} }
private extension CloudKitAcountZoneDelegate { private extension CloudKitAcountZoneDelegate {

View File

@ -108,7 +108,7 @@ private extension CloudKitSendStatusOperation {
} }
} }
func processStatuses(_ syncStatuses: [SyncStatus], completion: @escaping (Bool) -> Void) { @MainActor func processStatuses(_ syncStatuses: [SyncStatus], completion: @escaping (Bool) -> Void) {
guard let account = account, let articlesZone = articlesZone else { guard let account = account, let articlesZone = articlesZone else {
completion(true) completion(true)
return return
@ -156,6 +156,7 @@ private extension CloudKitSendStatusOperation {
} }
case .failure(let error): case .failure(let error):
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
MainActor.assumeIsolated {
self.processAccountError(account, error) self.processAccountError(account, error)
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription) os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
completion(true) completion(true)
@ -165,6 +166,7 @@ private extension CloudKitSendStatusOperation {
} }
} }
} }
}
switch result { switch result {
case .success(let articles): case .success(let articles):
@ -178,7 +180,7 @@ private extension CloudKitSendStatusOperation {
} }
} }
func processAccountError(_ account: Account, _ error: Error) { @MainActor func processAccountError(_ account: Account, _ error: Error) {
if case CloudKitZoneError.userDeletedZone = error { if case CloudKitZoneError.userDeletedZone = error {
account.removeFeeds(account.topLevelFeeds) account.removeFeeds(account.topLevelFeeds)
for folder in account.folders ?? Set<Folder>() { for folder in account.folders ?? Set<Folder>() {

View File

@ -15,7 +15,7 @@ extension Notification.Name {
public static let ChildrenDidChange = Notification.Name("ChildrenDidChange") public static let ChildrenDidChange = Notification.Name("ChildrenDidChange")
} }
public protocol Container: AnyObject, ContainerIdentifiable { @MainActor public protocol Container: AnyObject, ContainerIdentifiable {
var account: Account? { get } var account: Account? { get }
var topLevelFeeds: Set<Feed> { get set } var topLevelFeeds: Set<Feed> { get set }

View File

@ -12,7 +12,7 @@ import Foundation
// Mainly used with deleting objects and undo/redo. // Mainly used with deleting objects and undo/redo.
// Especially redo. The idea is to put something back in the right place. // Especially redo. The idea is to put something back in the right place.
public struct ContainerPath { @MainActor public struct ContainerPath {
private weak var account: Account? private weak var account: Account?
private let names: [String] // empty if top-level of account private let names: [String] // empty if top-level of account

View File

@ -11,7 +11,7 @@ import RSWeb
import Articles import Articles
import Core import Core
public final class Feed: Renamable, DisplayNameProvider, UnreadCountProvider, Hashable { @MainActor public final class Feed: Renamable, DisplayNameProvider, UnreadCountProvider, Hashable {
public weak var account: Account? public weak var account: Account?
public let url: String public let url: String
@ -294,11 +294,11 @@ extension Feed: OPMLRepresentable {
extension Set where Element == Feed { extension Set where Element == Feed {
func feedIDs() -> Set<String> { @MainActor func feedIDs() -> Set<String> {
return Set<String>(map { $0.feedID }) return Set<String>(map { $0.feedID })
} }
func sorted() -> Array<Feed> { @MainActor func sorted() -> Array<Feed> {
return sorted(by: { (feed1, feed2) -> Bool in return sorted(by: { (feed1, feed2) -> Bool in
if feed1.nameForDisplay.localizedStandardCompare(feed2.nameForDisplay) == .orderedSame { if feed1.nameForDisplay.localizedStandardCompare(feed2.nameForDisplay) == .orderedSame {
return feed1.url < feed2.url return feed1.url < feed2.url

View File

@ -136,7 +136,8 @@ final class FeedbinAccountDelegate: AccountDelegate {
database.selectForProcessing { result in database.selectForProcessing { result in
func processStatuses(_ syncStatuses: [SyncStatus]) { MainActor.assumeIsolated {
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true } let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true } let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true }
@ -195,6 +196,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
} }
} }
} }
}
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) { func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
@ -564,12 +566,14 @@ final class FeedbinAccountDelegate: AccountDelegate {
self.database.insertStatuses(syncStatuses) { _ in self.database.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in self.database.selectPendingCount { result in
MainActor.assumeIsolated {
if let count = try? result.get(), count > 100 { if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in } self.sendArticleStatus(for: account) { _ in }
} }
completion(.success(())) completion(.success(()))
} }
} }
}
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
@ -1082,6 +1086,8 @@ private extension FeedbinAccountDelegate {
case .success(let (entries, page)): case .success(let (entries, page)):
self.processEntries(account: account, entries: entries) { error in self.processEntries(account: account, entries: entries) { error in
MainActor.assumeIsolated {
if let error = error { if let error = error {
completion(.failure(error)) completion(.failure(error))
return return
@ -1122,6 +1128,7 @@ private extension FeedbinAccountDelegate {
} }
} }
} }
}
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
@ -1146,6 +1153,8 @@ private extension FeedbinAccountDelegate {
self.processEntries(account: account, entries: entries) { error in self.processEntries(account: account, entries: entries) { error in
MainActor.assumeIsolated {
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
if let error = error { if let error = error {
@ -1163,6 +1172,7 @@ private extension FeedbinAccountDelegate {
} }
} }
} }
}
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
@ -1250,6 +1260,7 @@ private extension FeedbinAccountDelegate {
case .success(let (entries, nextPage)): case .success(let (entries, nextPage)):
self.processEntries(account: account, entries: entries) { error in self.processEntries(account: account, entries: entries) { error in
MainActor.assumeIsolated {
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
if let error = error { if let error = error {
@ -1259,6 +1270,7 @@ private extension FeedbinAccountDelegate {
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion) self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
} }
}
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
@ -1294,12 +1306,15 @@ private extension FeedbinAccountDelegate {
database.selectPendingReadStatusArticleIDs() { result in database.selectPendingReadStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) { MainActor.assumeIsolated {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } ) let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs) let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
account.fetchUnreadArticleIDs { articleIDsResult in account.fetchUnreadArticleIDs { articleIDsResult in
MainActor.assumeIsolated {
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else { guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return return
} }
@ -1323,7 +1338,7 @@ private extension FeedbinAccountDelegate {
group.notify(queue: DispatchQueue.main) { group.notify(queue: DispatchQueue.main) {
completion() completion()
} }
}
} }
} }
@ -1334,7 +1349,7 @@ private extension FeedbinAccountDelegate {
case .failure(let error): case .failure(let error):
os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription) os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription)
} }
}
} }
} }
@ -1347,12 +1362,15 @@ private extension FeedbinAccountDelegate {
database.selectPendingStarredStatusArticleIDs() { result in database.selectPendingStarredStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) { MainActor.assumeIsolated {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } ) let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs) let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs)
account.fetchStarredArticleIDs { articleIDsResult in account.fetchStarredArticleIDs { articleIDsResult in
MainActor.assumeIsolated {
guard let currentStarredArticleIDs = try? articleIDsResult.get() else { guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return return
} }
@ -1377,6 +1395,7 @@ private extension FeedbinAccountDelegate {
completion() completion()
} }
} }
}
} }
@ -1386,7 +1405,7 @@ private extension FeedbinAccountDelegate {
case .failure(let error): case .failure(let error):
os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription) os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription)
} }
}
} }
} }

View File

@ -408,7 +408,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let folder = container as? Folder, let collectionId = folder.externalID else { guard let folder = container as? Folder, let collectionId = folder.externalID else {
return DispatchQueue.main.async { return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed))) completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed.nameForDisplay)))
} }
} }
@ -442,7 +442,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
completion(.success(())) completion(.success(()))
case .failure: case .failure:
from.addFeed(feed) from.addFeed(feed)
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to))) completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay)))
} }
} }
case .failure(let error): case .failure(let error):

View File

@ -14,11 +14,11 @@ enum FeedlyAccountDelegateError: LocalizedError {
case unableToAddFolder(String) case unableToAddFolder(String)
case unableToRenameFolder(String, String) case unableToRenameFolder(String, String)
case unableToRemoveFolder(String) case unableToRemoveFolder(String)
case unableToMoveFeedBetweenFolders(Feed, Folder, Folder) case unableToMoveFeedBetweenFolders(String, String, String)
case addFeedChooseFolder case addFeedChooseFolder
case addFeedInvalidFolder(Folder) case addFeedInvalidFolder(String)
case unableToRenameFeed(String, String) case unableToRenameFeed(String, String)
case unableToRemoveFeed(Feed) case unableToRemoveFeed(String)
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
@ -41,24 +41,24 @@ enum FeedlyAccountDelegateError: LocalizedError {
let template = NSLocalizedString("Could not remove the folder named “%@”.", comment: "Feedly Could not remove a folder/collection.") let template = NSLocalizedString("Could not remove the folder named “%@”.", comment: "Feedly Could not remove a folder/collection.")
return String(format: template, name) return String(format: template, name)
case .unableToMoveFeedBetweenFolders(let feed, _, let to): case .unableToMoveFeedBetweenFolders(let feedName, _, let destinationFolderName):
let template = NSLocalizedString("Could not move “%@” to “%@”.", comment: "Feedly Could not move a feed between folders/collections.") let template = NSLocalizedString("Could not move “%@” to “%@”.", comment: "Feedly Could not move a feed between folders/collections.")
return String(format: template, feed.nameForDisplay, to.nameForDisplay) return String(format: template, feedName, destinationFolderName)
case .addFeedChooseFolder: case .addFeedChooseFolder:
return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly Feed can only be added to folders.") return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly Feed can only be added to folders.")
case .addFeedInvalidFolder(let invalidFolder): case .addFeedInvalidFolder(let folderName):
let template = NSLocalizedString("Feeds cannot be added to the “%@” folder.", comment: "Feedly Feed can only be added to folders.") let template = NSLocalizedString("Feeds cannot be added to the “%@” folder.", comment: "Feedly Feed can only be added to folders.")
return String(format: template, invalidFolder.nameForDisplay) return String(format: template, folderName)
case .unableToRenameFeed(let from, let to): case .unableToRenameFeed(let from, let to):
let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly Could not rename a feed.") let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly Could not rename a feed.")
return String(format: template, from, to) return String(format: template, from, to)
case .unableToRemoveFeed(let feed): case .unableToRemoveFeed(let feedName):
let template = NSLocalizedString("Could not remove “%@”.", comment: "Feedly Could not remove a feed.") let template = NSLocalizedString("Could not remove “%@”.", comment: "Feedly Could not remove a feed.")
return String(format: template, feed.nameForDisplay) return String(format: template, feedName)
} }
} }
@ -80,9 +80,9 @@ enum FeedlyAccountDelegateError: LocalizedError {
case .unableToRemoveFolder: case .unableToRemoveFolder:
return nil return nil
case .unableToMoveFeedBetweenFolders(let feed, let from, let to): case .unableToMoveFeedBetweenFolders(let feedName, let sourceFolderName, let destinationFolderName):
let template = NSLocalizedString("“%@” may be in both “%@” and “%@”.", comment: "Feedly Could not move a feed between folders/collections.") let template = NSLocalizedString("“%@” may be in both “%@” and “%@”.", comment: "Feedly Could not move a feed between folders/collections.")
return String(format: template, feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay) return String(format: template, feedName, sourceFolderName, destinationFolderName)
case .addFeedChooseFolder: case .addFeedChooseFolder:
return nil return nil

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
struct FeedlyFeedContainerValidator { @MainActor struct FeedlyFeedContainerValidator {
var container: Container var container: Container
func getValidContainer() throws -> (Folder, String) { func getValidContainer() throws -> (Folder, String) {
@ -17,7 +17,7 @@ struct FeedlyFeedContainerValidator {
} }
guard let collectionId = folder.externalID else { guard let collectionId = folder.externalID else {
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder) throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder.nameForDisplay)
} }
return (folder, collectionId) return (folder, collectionId)

View File

@ -23,7 +23,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
return NSLocalizedString("There is already a Feedly account with that username created.", comment: "Duplicate Error") return NSLocalizedString("There is already a Feedly account with that username created.", comment: "Duplicate Error")
} }
} }
@objc public final class OAuthAccountAuthorizationOperation: NSObject, MainThreadOperation, ASWebAuthenticationPresentationContextProviding { @MainActor @objc public final class OAuthAccountAuthorizationOperation: NSObject, MainThreadOperation, ASWebAuthenticationPresentationContextProviding {
public var isCanceled: Bool = false { public var isCanceled: Bool = false {
didSet { didSet {

View File

@ -10,7 +10,7 @@ import Foundation
import Articles import Articles
import Core import Core
public final class Folder: Renamable, Container, DisplayNameProvider, UnreadCountProvider, Hashable { @MainActor public final class Folder: Renamable, Container, DisplayNameProvider, UnreadCountProvider, Hashable {
public var containerID: ContainerIdentifier? { public var containerID: ContainerIdentifier? {
guard let accountID = account?.accountID else { guard let accountID = account?.accountID else {
@ -198,7 +198,7 @@ extension Folder: OPMLRepresentable {
extension Set where Element == Folder { extension Set where Element == Folder {
func sorted() -> Array<Folder> { @MainActor func sorted() -> Array<Folder> {
return sorted(by: { (folder1, folder2) -> Bool in return sorted(by: { (folder1, folder2) -> Bool in
return folder1.nameForDisplay.localizedStandardCompare(folder2.nameForDisplay) == .orderedAscending return folder1.nameForDisplay.localizedStandardCompare(folder2.nameForDisplay) == .orderedAscending
}) })

View File

@ -52,7 +52,7 @@ final class LocalAccountRefresher {
extension LocalAccountRefresher: DownloadSessionDelegate { extension LocalAccountRefresher: DownloadSessionDelegate {
func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? { @MainActor func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
guard let feed = representedObject as? Feed else { guard let feed = representedObject as? Feed else {
return nil return nil
} }
@ -68,7 +68,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
return request return request
} }
func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) { @MainActor func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
let feed = representedObject as! Feed let feed = representedObject as! Feed
guard !data.isEmpty, !isSuspended else { guard !data.isEmpty, !isSuspended else {
@ -101,6 +101,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
} }
account.update(feed, with: parsedFeed) { result in account.update(feed, with: parsedFeed) { result in
MainActor.assumeIsolated {
if case .success(let articleChanges) = result { if case .success(let articleChanges) = result {
if let httpResponse = response as? HTTPURLResponse { if let httpResponse = response as? HTTPURLResponse {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse) feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
@ -115,6 +116,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed) self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
} }
} }
}
} }
} }

View File

@ -330,12 +330,14 @@ extension NewsBlurAccountDelegate {
} }
database.selectPendingReadStatusArticleIDs() { result in database.selectPendingReadStatusArticleIDs() { result in
func process(_ pendingStoryHashes: Set<String>) { MainActor.assumeIsolated {
@MainActor func process(_ pendingStoryHashes: Set<String>) {
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } ) let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes) let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes)
account.fetchUnreadArticleIDs { articleIDsResult in account.fetchUnreadArticleIDs { articleIDsResult in
MainActor.assumeIsolated {
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else { guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return return
} }
@ -361,6 +363,7 @@ extension NewsBlurAccountDelegate {
} }
} }
} }
}
switch result { switch result {
case .success(let pendingArticleIDs): case .success(let pendingArticleIDs):
@ -370,6 +373,7 @@ extension NewsBlurAccountDelegate {
} }
} }
} }
}
func syncStoryStarredState(account: Account, hashes: [NewsBlurStoryHash]?, completion: @escaping (() -> Void)) { func syncStoryStarredState(account: Account, hashes: [NewsBlurStoryHash]?, completion: @escaping (() -> Void)) {
guard let hashes = hashes else { guard let hashes = hashes else {
@ -378,12 +382,14 @@ extension NewsBlurAccountDelegate {
} }
database.selectPendingStarredStatusArticleIDs() { result in database.selectPendingStarredStatusArticleIDs() { result in
func process(_ pendingStoryHashes: Set<String>) { MainActor.assumeIsolated {
@MainActor func process(_ pendingStoryHashes: Set<String>) {
let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } ) let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes) let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes)
account.fetchStarredArticleIDs { articleIDsResult in account.fetchStarredArticleIDs { articleIDsResult in
MainActor.assumeIsolated {
guard let currentStarredArticleIDs = try? articleIDsResult.get() else { guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return return
} }
@ -409,6 +415,7 @@ extension NewsBlurAccountDelegate {
} }
} }
} }
}
switch result { switch result {
case .success(let pendingArticleIDs): case .success(let pendingArticleIDs):
@ -418,6 +425,7 @@ extension NewsBlurAccountDelegate {
} }
} }
} }
}
func createFeed(account: Account, feed: NewsBlurFeed?, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) { func createFeed(account: Account, feed: NewsBlurFeed?, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
guard let feed = feed else { guard let feed = feed else {

View File

@ -8,7 +8,7 @@
import Articles import Articles
import Database import Database
import RSParser @preconcurrency import RSParser
import RSWeb import RSWeb
import SyncDatabase import SyncDatabase
import os.log import os.log
@ -136,7 +136,9 @@ final class NewsBlurAccountDelegate: AccountDelegate {
database.selectForProcessing { result in database.selectForProcessing { result in
func processStatuses(_ syncStatuses: [SyncStatus]) { MainActor.assumeIsolated {
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
let createUnreadStatuses = syncStatuses.filter { let createUnreadStatuses = syncStatuses.filter {
$0.key == SyncStatus.Key.read && $0.flag == false $0.key == SyncStatus.Key.read && $0.flag == false
} }
@ -203,6 +205,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
} }
} }
} }
}
func refreshArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) { func refreshArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
os_log(.debug, log: log, "Refreshing story statuses...") os_log(.debug, log: log, "Refreshing story statuses...")
@ -272,7 +275,8 @@ final class NewsBlurAccountDelegate: AccountDelegate {
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
func process(_ fetchedHashes: Set<String>) { MainActor.assumeIsolated {
@MainActor func process(_ fetchedHashes: Set<String>) {
let group = DispatchGroup() let group = DispatchGroup()
var errorOccurred = false var errorOccurred = false
@ -321,6 +325,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
} }
} }
} }
}
func processStories(account: Account, stories: [NewsBlurStory]?, since: Date? = nil, completion: @escaping (Result<Bool, DatabaseError>) -> Void) { func processStories(account: Account, stories: [NewsBlurStory]?, since: Date? = nil, completion: @escaping (Result<Bool, DatabaseError>) -> Void) {
let parsedItems = mapStoriesToParsedItems(stories: stories).filter { let parsedItems = mapStoriesToParsedItems(stories: stories).filter {
@ -591,12 +596,14 @@ final class NewsBlurAccountDelegate: AccountDelegate {
self.database.insertStatuses(syncStatuses) { _ in self.database.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in self.database.selectPendingCount { result in
MainActor.assumeIsolated {
if let count = try? result.get(), count > 100 { if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in } self.sendArticleStatus(for: account) { _ in }
} }
completion(.success(())) completion(.success(()))
} }
} }
}
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }

View File

@ -118,6 +118,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
switch result { switch result {
case .success(let articleIDs): case .success(let articleIDs):
account.markAsRead(Set(articleIDs)) { _ in account.markAsRead(Set(articleIDs)) { _ in
MainActor.assumeIsolated {
self.refreshArticleStatus(for: account) { _ in self.refreshArticleStatus(for: account) { _ in
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
self.refreshMissingArticles(account) { self.refreshMissingArticles(account) {
@ -128,6 +129,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
} }
} }
} }
}
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
@ -201,7 +203,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
database.selectForProcessing { result in database.selectForProcessing { result in
func processStatuses(_ syncStatuses: [SyncStatus]) { MainActor.assumeIsolated {
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true } let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true } let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true }
@ -243,6 +247,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
} }
} }
} }
}
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) { func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
os_log(.debug, log: log, "Refreshing article statuses...") os_log(.debug, log: log, "Refreshing article statuses...")
@ -617,12 +622,14 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
self.database.insertStatuses(syncStatuses) { _ in self.database.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in self.database.selectPendingCount { result in
MainActor.assumeIsolated {
if let count = try? result.get(), count > 100 { if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in } self.sendArticleStatus(for: account) { _ in }
} }
completion(.success(())) completion(.success(()))
} }
} }
}
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
@ -970,6 +977,7 @@ private extension ReaderAPIAccountDelegate {
switch result { switch result {
case .success(let articleIDs): case .success(let articleIDs):
account.markAsRead(Set(articleIDs)) { _ in account.markAsRead(Set(articleIDs)) { _ in
MainActor.assumeIsolated {
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
self.refreshArticleStatus(for: account) { _ in self.refreshArticleStatus(for: account) { _ in
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
@ -981,7 +989,7 @@ private extension ReaderAPIAccountDelegate {
} }
} }
}
} }
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
@ -994,7 +1002,8 @@ private extension ReaderAPIAccountDelegate {
func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) { func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) {
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in
func process(_ fetchedArticleIDs: Set<String>) { MainActor.assumeIsolated {
@MainActor func process(_ fetchedArticleIDs: Set<String>) {
guard !fetchedArticleIDs.isEmpty else { guard !fetchedArticleIDs.isEmpty else {
completion() completion()
return return
@ -1042,6 +1051,7 @@ private extension ReaderAPIAccountDelegate {
} }
} }
} }
}
func processEntries(account: Account, entries: [ReaderAPIEntry]?, completion: @escaping VoidCompletionBlock) { func processEntries(account: Account, entries: [ReaderAPIEntry]?, completion: @escaping VoidCompletionBlock) {
let parsedItems = mapEntriesToParsedItems(account: account, entries: entries) let parsedItems = mapEntriesToParsedItems(account: account, entries: entries)
@ -1100,10 +1110,13 @@ private extension ReaderAPIAccountDelegate {
database.selectPendingReadStatusArticleIDs() { result in database.selectPendingReadStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) { MainActor.assumeIsolated {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs) let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
account.fetchUnreadArticleIDs { articleIDsResult in account.fetchUnreadArticleIDs { articleIDsResult in
MainActor.assumeIsolated {
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else { guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return return
} }
@ -1129,6 +1142,7 @@ private extension ReaderAPIAccountDelegate {
} }
} }
} }
}
switch result { switch result {
case .success(let pendingArticleIDs): case .success(let pendingArticleIDs):
@ -1136,7 +1150,7 @@ private extension ReaderAPIAccountDelegate {
case .failure(let error): case .failure(let error):
os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription) os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription)
} }
}
} }
} }
@ -1149,10 +1163,13 @@ private extension ReaderAPIAccountDelegate {
database.selectPendingStarredStatusArticleIDs() { result in database.selectPendingStarredStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) { MainActor.assumeIsolated {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs) let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
account.fetchStarredArticleIDs { articleIDsResult in account.fetchStarredArticleIDs { articleIDsResult in
MainActor.assumeIsolated {
guard let currentStarredArticleIDs = try? articleIDsResult.get() else { guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return return
} }
@ -1178,6 +1195,7 @@ private extension ReaderAPIAccountDelegate {
} }
} }
} }
}
switch result { switch result {
case .success(let pendingArticleIDs): case .success(let pendingArticleIDs):
@ -1185,7 +1203,7 @@ private extension ReaderAPIAccountDelegate {
case .failure(let error): case .failure(let error):
os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription) os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription)
} }
}
} }
} }

View File

@ -257,7 +257,7 @@ final class ReaderAPICaller: NSObject {
} }
} }
func deleteTag(folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) { @MainActor func deleteTag(folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
guard let baseURL = apiBaseURL else { guard let baseURL = apiBaseURL else {
completion(.failure(CredentialsError.incompleteCredentials)) completion(.failure(CredentialsError.incompleteCredentials))
return return

View File

@ -17,12 +17,12 @@ extension Notification.Name {
public protocol DisplayNameProvider { public protocol DisplayNameProvider {
var nameForDisplay: String { get } @MainActor var nameForDisplay: String { get }
} }
public extension DisplayNameProvider { public extension DisplayNameProvider {
func postDisplayNameDidChangeNotification() { @MainActor func postDisplayNameDidChangeNotification() {
NotificationCenter.default.post(name: .DisplayNameDidChange, object: self, userInfo: nil) NotificationCenter.default.post(name: .DisplayNameDidChange, object: self, userInfo: nil)
} }

View File

@ -39,7 +39,7 @@ public protocol MainThreadOperation: AnyObject {
/// ///
/// The completionBlock is always called on the main thread. /// The completionBlock is always called on the main thread.
/// The queue will clear the completionBlock after calling it. /// The queue will clear the completionBlock after calling it.
var completionBlock: MainThreadOperationCompletionBlock? { get set } @MainActor var completionBlock: MainThreadOperationCompletionBlock? { get set }
/// Do the thing this operation does. /// Do the thing this operation does.
/// ///

View File

@ -153,7 +153,7 @@ extension Feed: PasteboardWriterOwner {
} }
} }
@objc final class FeedPasteboardWriter: NSObject, NSPasteboardWriting { @MainActor @objc final class FeedPasteboardWriter: NSObject, NSPasteboardWriting {
private let feed: Feed private let feed: Feed
static let feedUTI = "com.ranchero.feed" static let feedUTI = "com.ranchero.feed"

View File

@ -12,7 +12,7 @@ import AppKitExtras
typealias PasteboardFolderDictionary = [String: String] typealias PasteboardFolderDictionary = [String: String]
struct PasteboardFolder: Hashable { @MainActor struct PasteboardFolder: Hashable {
private struct Key { private struct Key {
static let name = "name" static let name = "name"
@ -91,7 +91,7 @@ extension Folder: PasteboardWriterOwner {
} }
} }
@objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting { @MainActor @objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting {
private let folder: Folder private let folder: Folder
static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder" static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder"

View File

@ -12,7 +12,7 @@ import Articles
import Core import Core
@objc(ScriptableAccount) @objc(ScriptableAccount)
class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { @MainActor class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let account:Account let account:Account
init (_ account:Account) { init (_ account:Account) {

View File

@ -44,7 +44,7 @@ import Articles
// I am not sure if account should prefer to be specified by name or by ID // I am not sure if account should prefer to be specified by name or by ID
// but in either case it seems like the accountID would be used as the keydata, so I chose ID // but in either case it seems like the accountID would be used as the keydata, so I chose ID
@objc(uniqueId) @objc(uniqueId)
var scriptingUniqueId:Any { @MainActor var scriptingUniqueId:Any {
return feed.feedID return feed.feedID
} }
@ -71,7 +71,7 @@ import Articles
return url 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) let scriptableAccount = ScriptableAccount(account)
if let folder = folder { if let folder = folder {
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount) let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
@ -120,27 +120,27 @@ import Articles
// MARK: --- Scriptable properties --- // MARK: --- Scriptable properties ---
@objc(url) @objc(url)
var url:String { @MainActor var url:String {
return self.feed.url return self.feed.url
} }
@objc(name) @objc(name)
var name:String { @MainActor var name:String {
return self.feed.name ?? "" return self.feed.name ?? ""
} }
@objc(homePageURL) @objc(homePageURL)
var homePageURL:String { @MainActor var homePageURL:String {
return self.feed.homePageURL ?? "" return self.feed.homePageURL ?? ""
} }
@objc(iconURL) @objc(iconURL)
var iconURL:String { @MainActor var iconURL:String {
return self.feed.iconURL ?? "" return self.feed.iconURL ?? ""
} }
@objc(faviconURL) @objc(faviconURL)
var faviconURL:String { @MainActor var faviconURL:String {
return self.feed.faviconURL ?? "" return self.feed.faviconURL ?? ""
} }
@ -152,13 +152,13 @@ import Articles
// MARK: --- scriptable elements --- // MARK: --- scriptable elements ---
@objc(authors) @objc(authors)
var authors:NSArray { @MainActor var authors:NSArray {
let feedAuthors = feed.authors ?? [] let feedAuthors = feed.authors ?? []
return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray
} }
@objc(valueInAuthorsWithUniqueID:) @objc(valueInAuthorsWithUniqueID:)
func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? { @MainActor func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? {
guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil } guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil }
return ScriptableAuthor(author, container:self) return ScriptableAuthor(author, container:self)
} }

View File

@ -12,7 +12,7 @@ import Articles
import Core import Core
@objc(ScriptableFolder) @objc(ScriptableFolder)
class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { @MainActor class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let folder:Folder let folder:Folder
let container:ScriptingObjectContainer let container:ScriptingObjectContainer

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import Account import Account
struct AddFeedDefaultContainer { @MainActor struct AddFeedDefaultContainer {
@MainActor static var defaultContainer: Container? { @MainActor static var defaultContainer: Container? {

View File

@ -71,7 +71,7 @@ final class FaviconDownloader {
cache = [Feed: IconImage]() cache = [Feed: IconImage]()
} }
func favicon(for feed: Feed) -> IconImage? { @MainActor func favicon(for feed: Feed) -> IconImage? {
assert(Thread.isMainThread) assert(Thread.isMainThread)
@ -93,7 +93,7 @@ final class FaviconDownloader {
return nil return nil
} }
func faviconAsIcon(for feed: Feed) -> IconImage? { @MainActor func faviconAsIcon(for feed: Feed) -> IconImage? {
if let image = cache[feed] { if let image = cache[feed] {
return image return image

View File

@ -37,7 +37,7 @@ struct ExtensionContainers: Codable {
} }
struct ExtensionAccount: ExtensionContainer { @MainActor struct ExtensionAccount: ExtensionContainer {
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case name case name
@ -70,7 +70,7 @@ struct ExtensionAccount: ExtensionContainer {
} }
struct ExtensionFolder: ExtensionContainer { @MainActor struct ExtensionFolder: ExtensionContainer {
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case accountName case accountName

View File

@ -9,7 +9,7 @@
import AppKit import AppKit
import Account import Account
@objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting { @MainActor @objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting {
private let smartFeed: PseudoFeed private let smartFeed: PseudoFeed

View File

@ -124,7 +124,7 @@ public extension SyncDatabase {
nonisolated func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) { nonisolated func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) {
Task { Task { @MainActor in
do { do {
try await self.insertStatuses(statuses) try await self.insertStatuses(statuses)
completion(nil) completion(nil)
@ -136,7 +136,7 @@ public extension SyncDatabase {
nonisolated func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) { nonisolated func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) {
Task { Task { @MainActor in
do { do {
if let syncStatuses = try await self.selectForProcessing(limit: limit) { if let syncStatuses = try await self.selectForProcessing(limit: limit) {
completion(.success(Array(syncStatuses))) completion(.success(Array(syncStatuses)))
@ -151,7 +151,7 @@ public extension SyncDatabase {
nonisolated func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) { nonisolated func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) {
Task { Task { @MainActor in
do { do {
if let count = try await self.selectPendingCount() { if let count = try await self.selectPendingCount() {
completion(.success(count)) completion(.success(count))
@ -167,7 +167,7 @@ public extension SyncDatabase {
nonisolated func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { nonisolated func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
Task { Task { @MainActor in
do { do {
if let articleIDs = try await self.selectPendingReadStatusArticleIDs() { if let articleIDs = try await self.selectPendingReadStatusArticleIDs() {
completion(.success(articleIDs)) completion(.success(articleIDs))
@ -182,7 +182,7 @@ public extension SyncDatabase {
nonisolated func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { nonisolated func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
Task { Task { @MainActor in
do { do {
if let articleIDs = try await self.selectPendingStarredStatusArticleIDs() { if let articleIDs = try await self.selectPendingStarredStatusArticleIDs() {
completion(.success(articleIDs)) completion(.success(articleIDs))
@ -197,7 +197,7 @@ public extension SyncDatabase {
nonisolated func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) { nonisolated func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) {
Task { Task { @MainActor in
do { do {
try await self.resetAllSelectedForProcessing() try await self.resetAllSelectedForProcessing()
completion?(nil) completion?(nil)
@ -209,7 +209,7 @@ public extension SyncDatabase {
nonisolated func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { nonisolated func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) {
Task { Task { @MainActor in
do { do {
try await self.resetSelectedForProcessing(articleIDs) try await self.resetSelectedForProcessing(articleIDs)
completion?(nil) completion?(nil)