Continue fixing concurrency warnings.

This commit is contained in:
Brent Simmons 2024-03-19 23:05:30 -07:00
parent 6ab10e871c
commit d0760f3d12
64 changed files with 444 additions and 459 deletions

View File

@ -709,7 +709,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public func articles(feed: Feed) async throws -> Set<Article> {
let articles = try await database.articles(feedID: feed.feedID)
validateUnreadCount(feed, articles)
await validateUnreadCount(feed, articles)
return articles
}
@ -801,12 +801,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
precondition(type == .onMyMac || type == .cloudKit)
database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder) { updateArticlesResult in
switch updateArticlesResult {
case .success(let articleChanges):
self.sendNotificationAbout(articleChanges)
completion(.success(articleChanges))
case .failure(let databaseError):
completion(.failure(databaseError))
MainActor.assumeIsolated {
switch updateArticlesResult {
case .success(let articleChanges):
self.sendNotificationAbout(articleChanges)
completion(.success(articleChanges))
case .failure(let databaseError):
completion(.failure(databaseError))
}
}
}
}
@ -821,12 +824,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { updateArticlesResult in
switch updateArticlesResult {
case .success(let newAndUpdatedArticles):
self.sendNotificationAbout(newAndUpdatedArticles)
completion(nil)
case .failure(let databaseError):
completion(databaseError)
MainActor.assumeIsolated {
switch updateArticlesResult {
case .success(let newAndUpdatedArticles):
self.sendNotificationAbout(newAndUpdatedArticles)
completion(nil)
case .failure(let databaseError):
completion(databaseError)
}
}
}
}
@ -839,14 +845,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
database.mark(articles, statusKey: statusKey, flag: flag) { result in
switch result {
case .success(let updatedStatuses):
let updatedArticleIDs = updatedStatuses.articleIDs()
let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) })
self.noteStatusesForArticlesDidChange(updatedArticles)
completion(.success(updatedArticles))
case .failure(let error):
completion(.failure(error))
MainActor.assumeIsolated {
switch result {
case .success(let updatedStatuses):
let updatedArticleIDs = updatedStatuses.articleIDs()
let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) })
self.noteStatusesForArticlesDidChange(updatedArticles)
completion(.success(updatedArticles))
case .failure(let error):
completion(.failure(error))
}
}
}
}
@ -1118,12 +1127,15 @@ private extension Account {
func fetchArticlesAsync(feed: Feed, _ completion: @escaping ArticleSetResultBlock) {
database.fetchArticlesAsync(feed.feedID) { [weak self] articleSetResult in
switch articleSetResult {
case .success(let articles):
self?.validateUnreadCount(feed, articles)
completion(.success(articles))
case .failure(let databaseError):
completion(.failure(databaseError))
MainActor.assumeIsolated {
switch articleSetResult {
case .success(let articles):
self?.validateUnreadCount(feed, articles)
completion(.success(articles))
case .failure(let databaseError):
completion(.failure(databaseError))
}
}
}
}
@ -1227,7 +1239,7 @@ private extension Account {
}
}
func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
@MainActor func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
// articles must contain all the unread articles for the feed.
// The unread number should match the feeds unread count.
@ -1301,7 +1313,7 @@ private extension Account {
unreadCount = updatedUnreadCount
}
func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
@MainActor func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
let feeds = Set(articles.compactMap { $0.feed })
let statuses = Set(articles.map { $0.status })
let articleIDs = Set(articles.map { $0.articleID })
@ -1389,7 +1401,7 @@ private extension Account {
}
}
func sendNotificationAbout(_ articleChanges: ArticleChanges) {
@MainActor func sendNotificationAbout(_ articleChanges: ArticleChanges) {
var feeds = Set<Feed>()
if let newArticles = articleChanges.newArticles {

View File

@ -18,7 +18,7 @@ import Secrets
public final class AccountManager: UnreadCountProvider {
public static var shared: AccountManager!
@MainActor public static var shared: AccountManager!
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"
private static let jsonNetNewsWireNewsURL = "https://netnewswire.blog/feed.json"
@ -79,7 +79,7 @@ public final class AccountManager: UnreadCountProvider {
return lastArticleFetchEndTime
}
public func existingActiveAccount(forDisplayName displayName: String) -> Account? {
@MainActor public func existingActiveAccount(forDisplayName displayName: String) -> Account? {
return AccountManager.shared.activeAccounts.first(where: { $0.nameForDisplay == displayName })
}

View File

@ -86,7 +86,7 @@ final class CloudKitArticlesZone: CloudKitZone {
}
}
func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
@MainActor func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !articles.isEmpty else {
completion(.success(()))
return
@ -112,7 +112,7 @@ final class CloudKitArticlesZone: CloudKitZone {
delete(ckQuery: ckQuery, completion: completion)
}
func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
@MainActor func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !statusUpdates.isEmpty else {
completion(.success(()))
return
@ -164,7 +164,7 @@ final class CloudKitArticlesZone: CloudKitZone {
private extension CloudKitArticlesZone {
func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
@MainActor func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in
switch result {
@ -187,7 +187,7 @@ private extension CloudKitArticlesZone {
return "a|\(id)"
}
func makeStatusRecord(_ article: Article) -> CKRecord {
@MainActor func makeStatusRecord(_ article: Article) -> CKRecord {
let recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: zoneID)
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
if let feedExternalID = article.feed?.externalID {
@ -198,7 +198,7 @@ private extension CloudKitArticlesZone {
return record
}
func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
@MainActor func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: zoneID)
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
@ -212,7 +212,7 @@ private extension CloudKitArticlesZone {
return record
}
func makeArticleRecord(_ article: Article) -> CKRecord {
@MainActor func makeArticleRecord(_ article: Article) -> CKRecord {
let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: zoneID)
let record = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID)

View File

@ -139,22 +139,24 @@ private extension CloudKitSendStatusOperation {
return
}
} else {
articlesZone.modifyArticles(statusUpdates) { result in
switch result {
case .success:
self.database.deleteSelectedForProcessing(statusUpdates.map({ $0.articleID })) { _ in
done(false)
}
case .failure(let error):
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
self.processAccountError(account, error)
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
completion(true)
Task { @MainActor in
articlesZone.modifyArticles(statusUpdates) { result in
switch result {
case .success:
self.database.deleteSelectedForProcessing(statusUpdates.map({ $0.articleID })) { _ in
done(false)
}
case .failure(let error):
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
self.processAccountError(account, error)
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
completion(true)
}
}
}
}
}
}
switch result {

View File

@ -48,7 +48,7 @@ extension Feed {
public extension Article {
var account: Account? {
@MainActor var account: Account? {
// The force unwrapped shared instance was crashing Account.framework unit tests.
guard let manager = AccountManager.shared else {
return nil
@ -56,7 +56,7 @@ public extension Article {
return manager.existingAccount(with: accountID)
}
var feed: Feed? {
@MainActor var feed: Feed? {
return account?.existingFeed(withFeedID: feedID)
}
}

View File

@ -133,7 +133,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
return anchor
}
private func didEndRequestingAccessToken(_ result: Result<OAuthAuthorizationGrant, Error>) {
@MainActor private func didEndRequestingAccessToken(_ result: Result<OAuthAuthorizationGrant, Error>) {
guard !isCanceled else {
didFinish()
return
@ -147,7 +147,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
}
}
private func saveAccount(for grant: OAuthAuthorizationGrant) {
@MainActor private func saveAccount(for grant: OAuthAuthorizationGrant) {
guard !AccountManager.shared.duplicateServiceAccount(type: .feedly, username: grant.accessToken.username) else {
didFinish(OAuthAccountAuthorizationOperationError.duplicateAccount)
return

View File

@ -12,226 +12,144 @@ import Account
struct AppAssets {
static var accountBazQux: RSImage! = {
return RSImage(named: "accountBazQux")
}()
static let accountBazQux = RSImage(named: "accountBazQux")
static var accountCloudKit: RSImage! = {
return RSImage(named: "accountCloudKit")
}()
static let accountCloudKit = RSImage(named: "accountCloudKit")
static var accountFeedbin: RSImage! = {
return RSImage(named: "accountFeedbin")
}()
static let accountFeedbin = RSImage(named: "accountFeedbin")
static let accountFeedly = RSImage(named: "accountFeedly")
static var accountFeedly: RSImage! = {
return RSImage(named: "accountFeedly")
}()
static var accountFreshRSS: RSImage! = {
return RSImage(named: "accountFreshRSS")
}()
static let accountFreshRSS = RSImage(named: "accountFreshRSS")
static var accountInoreader: RSImage! = {
return RSImage(named: "accountInoreader")
}()
static let accountInoreader = RSImage(named: "accountInoreader")
static var accountLocal: RSImage! = {
return RSImage(named: "accountLocal")
}()
static let accountLocal = RSImage(named: "accountLocal")
static var accountNewsBlur: RSImage! = {
return RSImage(named: "accountNewsBlur")
}()
static var accountTheOldReader: RSImage! = {
return RSImage(named: "accountTheOldReader")
}()
static let accountNewsBlur = RSImage(named: "accountNewsBlur")
static var addNewSidebarItemImage: RSImage = {
return NSImage(systemSymbolName: "plus", accessibilityDescription: nil)!
}()
static let accountTheOldReader = RSImage(named: "accountTheOldReader")
static var articleExtractorError: RSImage = {
return RSImage(named: "articleExtractorError")!
}()
static let addNewSidebarItemImage = NSImage(systemSymbolName: "plus", accessibilityDescription: nil)!
static var articleExtractorOff: RSImage = {
return RSImage(named: "articleExtractorOff")!
}()
static let articleExtractorError = RSImage(named: "articleExtractorError")!
static var articleExtractorOn: RSImage = {
return RSImage(named: "articleExtractorOn")!
}()
static let articleExtractorOff = RSImage(named: "articleExtractorOff")!
static var articleTheme: RSImage = {
return NSImage(systemSymbolName: "doc.richtext", accessibilityDescription: nil)!
}()
static let articleExtractorOn = RSImage(named: "articleExtractorOn")!
static var cleanUpImage: RSImage = {
return NSImage(systemSymbolName: "wind", accessibilityDescription: nil)!
}()
static let articleTheme = NSImage(systemSymbolName: "doc.richtext", accessibilityDescription: nil)!
static var marsEditIcon: RSImage = {
return RSImage(named: "MarsEditIcon")!
}()
static var microblogIcon: RSImage = {
return RSImage(named: "MicroblogIcon")!
}()
static var faviconTemplateImage: RSImage = {
return RSImage(named: "faviconTemplateImage")!
}()
static let cleanUpImage = NSImage(systemSymbolName: "wind", accessibilityDescription: nil)!
static var filterActive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle.fill", accessibilityDescription: nil)!
static let marsEditIcon = RSImage(named: "MarsEditIcon")!
static var filterInactive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle", accessibilityDescription: nil)!
static let microblogIcon = RSImage(named: "MicroblogIcon")!
static var iconLightBackgroundColor: NSColor = {
return NSColor(named: NSColor.Name("iconLightBackgroundColor"))!
}()
static let faviconTemplateImage = RSImage(named: "faviconTemplateImage")!
static var iconDarkBackgroundColor: NSColor = {
return NSColor(named: NSColor.Name("iconDarkBackgroundColor"))!
}()
static var legacyArticleExtractor: RSImage! = {
return RSImage(named: "legacyArticleExtractor")
}()
static var legacyArticleExtractorError: RSImage! = {
return RSImage(named: "legacyArticleExtractorError")
}()
static var legacyArticleExtractorInactiveDark: RSImage! = {
return RSImage(named: "legacyArticleExtractorInactiveDark")
}()
static var legacyArticleExtractorInactiveLight: RSImage! = {
return RSImage(named: "legacyArticleExtractorInactiveLight")
}()
static var legacyArticleExtractorProgress1: RSImage! = {
return RSImage(named: "legacyArticleExtractorProgress1")
}()
static var legacyArticleExtractorProgress2: RSImage! = {
return RSImage(named: "legacyArticleExtractorProgress2")
}()
static var legacyArticleExtractorProgress3: RSImage! = {
return RSImage(named: "legacyArticleExtractorProgress3")
}()
static var legacyArticleExtractorProgress4: RSImage! = {
return RSImage(named: "legacyArticleExtractorProgress4")
}()
static var folderImage: IconImage = {
static let filterActive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle.fill", accessibilityDescription: nil)!
static let filterInactive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle", accessibilityDescription: nil)!
static let iconLightBackgroundColor = NSColor(named: NSColor.Name("iconLightBackgroundColor"))!
static let iconDarkBackgroundColor = NSColor(named: NSColor.Name("iconDarkBackgroundColor"))!
static let legacyArticleExtractor = RSImage(named: "legacyArticleExtractor")!
static let legacyArticleExtractorError = RSImage(named: "legacyArticleExtractorError")!
static let legacyArticleExtractorInactiveDark = RSImage(named: "legacyArticleExtractorInactiveDark")!
static let legacyArticleExtractorInactiveLight = RSImage(named: "legacyArticleExtractorInactiveLight")!
static let legacyArticleExtractorProgress1 = RSImage(named: "legacyArticleExtractorProgress1")
static let legacyArticleExtractorProgress2 = RSImage(named: "legacyArticleExtractorProgress2")
static let legacyArticleExtractorProgress3 = RSImage(named: "legacyArticleExtractorProgress3")
static let legacyArticleExtractorProgress4 = RSImage(named: "legacyArticleExtractorProgress4")
static let folderImage: IconImage = {
let image = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)!
let preferredColor = NSColor(named: "AccentColor")!
let coloredImage = image.tinted(with: preferredColor)
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
}()
static var markAllAsReadImage: RSImage = {
return RSImage(named: "markAllAsRead")!
}()
static let markAllAsReadImage = RSImage(named: "markAllAsRead")!
static var nextUnreadImage: RSImage = {
return NSImage(systemSymbolName: "chevron.down.circle", accessibilityDescription: nil)!
}()
static let nextUnreadImage = NSImage(systemSymbolName: "chevron.down.circle", accessibilityDescription: nil)!
static var openInBrowserImage: RSImage = {
return NSImage(systemSymbolName: "safari", accessibilityDescription: nil)!
}()
static let openInBrowserImage = NSImage(systemSymbolName: "safari", accessibilityDescription: nil)!
static var preferencesToolbarAccountsImage = NSImage(systemSymbolName: "at", accessibilityDescription: nil)!
static let preferencesToolbarAccountsImage = NSImage(systemSymbolName: "at", accessibilityDescription: nil)!
static var preferencesToolbarGeneralImage = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)!
static let preferencesToolbarGeneralImage = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)!
static var preferencesToolbarAdvancedImage = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)!
static let preferencesToolbarAdvancedImage = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)!
static var readClosedImage = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
static let readClosedImage = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
static var readOpenImage: RSImage = {
return NSImage(systemSymbolName: "circle", accessibilityDescription: nil)!
}()
static let readOpenImage = NSImage(systemSymbolName: "circle", accessibilityDescription: nil)!
static var refreshImage: RSImage = {
return NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil)!
}()
static var searchFeedImage: IconImage = {
static let refreshImage = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil)!
static let searchFeedImage: IconImage = {
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!, isSymbol: true, isBackgroundSupressed: true)
}()
static var shareImage: RSImage = {
return NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: nil)!
}()
static let shareImage = NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: nil)!
static var sidebarToggleImage: RSImage = {
return NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)!
}()
static var starClosedImage: RSImage = {
return NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
}()
static let sidebarToggleImage = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)!
static var starOpenImage: RSImage = {
return NSImage(systemSymbolName: "star", accessibilityDescription: nil)!
}()
static var starredFeedImage: IconImage = {
static let starClosedImage = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
static let starOpenImage = NSImage(systemSymbolName: "star", accessibilityDescription: nil)!
static let starredFeedImage: IconImage = {
let image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
let preferredColor = NSColor(named: "StarColor")!
let coloredImage = image.tinted(with: preferredColor)
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
}()
static var timelineSeparatorColor: NSColor = {
return NSColor(named: "timelineSeparatorColor")!
}()
static var timelineStarSelected: RSImage! = {
return RSImage(named: "timelineStar")?.tinted(with: .white)
}()
static let timelineSeparatorColor = NSColor(named: "timelineSeparatorColor")!
static var timelineStarUnselected: RSImage! = {
return RSImage(named: "timelineStar")?.tinted(with: starColor)
}()
static let timelineStarSelected = RSImage(named: "timelineStar")?.tinted(with: .white)
static var todayFeedImage: IconImage = {
static let timelineStarUnselected = RSImage(named: "timelineStar")?.tinted(with: starColor)
static let todayFeedImage: IconImage = {
let image = NSImage(systemSymbolName: "sun.max.fill", accessibilityDescription: nil)!
let preferredColor = NSColor.orange
let coloredImage = image.tinted(with: preferredColor)
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
}()
static var unreadFeedImage: IconImage = {
static let unreadFeedImage: IconImage = {
let image = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
let preferredColor = NSColor(named: "AccentColor")!
let coloredImage = image.tinted(with: preferredColor)
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
}()
static var swipeMarkReadImage = RSImage(systemSymbolName: "circle", accessibilityDescription: "Mark Read")!
static let swipeMarkReadImage = RSImage(systemSymbolName: "circle", accessibilityDescription: "Mark Read")!
.withSymbolConfiguration(.init(scale: .large))
static var swipeMarkUnreadImage = RSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: "Mark Unread")!
static let swipeMarkUnreadImage = RSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: "Mark Unread")!
.withSymbolConfiguration(.init(scale: .large))
static var swipeMarkStarredImage = RSImage(systemSymbolName: "star.fill", accessibilityDescription: "Star")!
static let swipeMarkStarredImage = RSImage(systemSymbolName: "star.fill", accessibilityDescription: "Star")!
.withSymbolConfiguration(.init(scale: .large))
static var swipeMarkUnstarredImage = RSImage(systemSymbolName: "star", accessibilityDescription: "Unstar")!
static let swipeMarkUnstarredImage = RSImage(systemSymbolName: "star", accessibilityDescription: "Unstar")!
.withSymbolConfiguration(.init(scale: .large))!
static var starColor: NSColor = {
return NSColor(named: NSColor.Name("StarColor"))!
}()
static let starColor = NSColor(named: NSColor.Name("StarColor"))!
static func image(for accountType: AccountType) -> NSImage? {
switch accountType {
case .onMyMac:
@ -254,5 +172,4 @@ struct AppAssets {
return AppAssets.accountTheOldReader
}
}
}

View File

@ -15,8 +15,8 @@ enum FontSize: Int {
case veryLarge = 3
}
final class AppDefaults {
final class AppDefaults: Sendable {
static let defaultThemeName = "Default"
static let shared = AppDefaults()
@ -66,7 +66,7 @@ final class AppDefaults {
return false
}()
var isFirstRun: Bool = {
let isFirstRun: Bool = {
if let _ = UserDefaults.standard.object(forKey: Key.firstRunDate) as? Date {
return false
}

View File

@ -27,10 +27,10 @@ protocol SPUUpdaterDelegate {}
import Sparkle
#endif
var appDelegate: AppDelegate!
@MainActor var appDelegate: AppDelegate!
@NSApplicationMain
final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate {
@MainActor final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate {
private struct WindowRestorationIdentifiers {
static let mainWindow = "mainWindow"
@ -109,9 +109,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
private var themeImportPath: String?
private let secretsProvider = Secrets()
private let accountManager: AccountManager
private let articleThemesManager: ArticleThemesManager
@MainActor override init() {
override init() {
NSWindow.allowsAutomaticWindowTabbing = false
self.accountManager = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!, secretsProvider: secretsProvider)
AccountManager.shared = self.accountManager
self.articleThemesManager = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
ArticleThemesManager.shared = self.articleThemesManager
super.init()
#if !MAC_APP_STORE
@ -120,9 +130,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
crashReporter.enable()
#endif
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!, secretsProvider: secretsProvider)
ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil)
@ -199,9 +206,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
if isFirstRun {
os_log(.debug, "Is first run.")
}
let localAccount = AccountManager.shared.defaultAccount
let localAccount = accountManager.defaultAccount
if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
if isFirstRun && !accountManager.anyAccountHasAtLeastOneFeed() {
// Import feeds. Either old NNW 3 feeds or the default feeds.
if !NNW3ImportController.importSubscriptionsIfFileExists(account: localAccount) {
DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
@ -223,8 +230,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
DispatchQueue.main.async {
self.unreadCount = AccountManager.shared.unreadCount
Task { @MainActor in
self.unreadCount = self.accountManager.unreadCount
}
if InspectorWindowController.shouldOpenAtStartup {
@ -241,7 +248,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
if settings.authorizationStatus == .authorized {
DispatchQueue.main.async {
Task { @MainActor in
NSApplication.shared.registerForRemoteNotifications()
}
}
@ -258,7 +265,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
refreshTimer!.update()
syncTimer!.update()
} else {
DispatchQueue.main.async {
Task { @MainActor in
self.refreshTimer!.timedRefresh(nil)
self.syncTimer!.timedRefresh(nil)
}
@ -279,7 +286,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
}
#if !MAC_APP_STORE
DispatchQueue.main.async {
Task { @MainActor in
CrashReporter.check(crashReporter: self.crashReporter)
}
#endif
@ -318,7 +325,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
}
func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) {
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo)
accountManager.receiveRemoteNotification(userInfo: userInfo)
}
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
@ -333,7 +340,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
ArticleThemeDownloader.shared.cleanUp()
AccountManager.shared.sendArticleStatusAll() {
accountManager.sendArticleStatusAll() {
self.isShutDownSyncDone = true
}
@ -344,7 +351,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
// MARK: Notifications
@objc func unreadCountDidChange(_ note: Notification) {
if note.object is AccountManager {
unreadCount = AccountManager.shared.unreadCount
unreadCount = accountManager.unreadCount
}
}
@ -385,7 +392,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
let url = userInfo["url"] as? URL else {
return
}
DispatchQueue.main.async {
Task { @MainActor in
self.importTheme(filename: url.path)
}
}
@ -444,15 +451,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
let isDisplayingSheet = mainWindowController?.isDisplayingSheet ?? false
if item.action == #selector(refreshAll(_:)) {
return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty
return !accountManager.refreshInProgress && !accountManager.activeAccounts.isEmpty
}
if item.action == #selector(importOPMLFromFile(_:)) {
return AccountManager.shared.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) })
return accountManager.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) })
}
if item.action == #selector(addAppNews(_:)) {
return !isDisplayingSheet && !AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() && !AccountManager.shared.activeAccounts.isEmpty
return !isDisplayingSheet && !accountManager.anyAccountHasNetNewsWireNewsSubscription() && !accountManager.activeAccounts.isEmpty
}
if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) {
@ -460,7 +467,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
}
if item.action == #selector(showAddFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) {
return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty
return !isDisplayingSheet && !accountManager.activeAccounts.isEmpty
}
#if !MAC_APP_STORE
@ -474,23 +481,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
// MARK: UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .badge, .sound])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
switch response.actionIdentifier {
case "MARK_AS_READ":
handleMarkAsRead(userInfo: userInfo)
case "MARK_AS_STARRED":
handleMarkAsStarred(userInfo: userInfo)
default:
mainWindowController?.handle(response)
guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else {
completionHandler()
return
}
let actionIdentifier = response.actionIdentifier
Task { @MainActor in
switch actionIdentifier {
case "MARK_AS_READ":
handleMarkAsRead(articlePathInfo: articlePathInfo)
case "MARK_AS_STARRED":
handleMarkAsStarred(articlePathInfo: articlePathInfo)
default:
mainWindowController?.handle(articlePathInfo: articlePathInfo)
}
completionHandler()
}
completionHandler()
}
// MARK: Add Feed
@ -529,7 +544,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
}
@IBAction func refreshAll(_ sender: Any?) {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present)
accountManager.refreshAll(errorHandler: ErrorHandler.present)
}
@IBAction func showAddFeedWindow(_ sender: Any?) {
@ -603,7 +618,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
}
@IBAction func addAppNews(_ sender: Any?) {
if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() {
if accountManager.anyAccountHasNetNewsWireNewsSubscription() {
return
}
addFeed(AccountManager.netNewsWireNewsURL, name: "NetNewsWire News")
@ -700,12 +715,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
extension AppDelegate {
@IBAction func debugSearch(_ sender: Any?) {
AccountManager.shared.defaultAccount.debugRunSearch()
accountManager.defaultAccount.debugRunSearch()
}
@IBAction func debugDropConditionalGetInfo(_ sender: Any?) {
#if DEBUG
AccountManager.shared.activeAccounts.forEach{ $0.debugDropConditionalGetInfo() }
accountManager.activeAccounts.forEach{ $0.debugDropConditionalGetInfo() }
#endif
}
@ -817,7 +832,7 @@ internal extension AppDelegate {
func importTheme() {
do {
try ArticleThemesManager.shared.importTheme(filename: filename)
try articleThemesManager.importTheme(filename: filename)
confirmImportSuccess(themeName: theme.name)
} catch {
NSApplication.shared.presentError(error)
@ -827,7 +842,7 @@ internal extension AppDelegate {
alert.beginSheetModal(for: window) { result in
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
if ArticleThemesManager.shared.themeExists(filename: filename) {
if self.articleThemesManager.themeExists(filename: filename) {
let alert = NSAlert()
alert.alertStyle = .warning
@ -901,14 +916,14 @@ internal extension AppDelegate {
informativeText = error.localizedDescription
}
DispatchQueue.main.async {
Task { @MainActor in
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error")
alert.informativeText = informativeText
alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder"))
alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
let button = alert.buttons.first
button?.target = self
button?.action = #selector(self.openThemesFolder(_:))
@ -920,7 +935,7 @@ internal extension AppDelegate {
@objc func openThemesFolder(_ sender: Any) {
if themeImportPath == nil {
let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath)
let url = URL(fileURLWithPath: articleThemesManager.folderPath)
NSWorkspace.shared.open(url)
} else {
let url = URL(fileURLWithPath: themeImportPath!)
@ -968,16 +983,15 @@ extension AppDelegate: NSWindowRestoration {
private extension AppDelegate {
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
func handleMarkAsRead(articlePathInfo: ArticlePathInfo) {
guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else {
return
}
guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else {
guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else {
os_log(.debug, "No account found from notification.")
return
}
let articleID = articlePathInfo.articleID
guard let articleID = articlePathInfo.articleID else {
return
}
Task {
guard let articles = try? await account.articles(for: .articleIDs([articleID])) else {
@ -989,16 +1003,15 @@ private extension AppDelegate {
}
}
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
func handleMarkAsStarred(articlePathInfo: ArticlePathInfo) {
guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else {
return
}
guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else {
guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else {
os_log(.debug, "No account found from notification.")
return
}
let articleID = articlePathInfo.articleID
guard let articleID = articlePathInfo.articleID else {
return
}
Task {

View File

@ -16,7 +16,7 @@ import CrashReporter
// At some point this code should probably move into RSCore, so Rainier and any other
// future apps can use it.
struct CrashReporter {
@MainActor struct CrashReporter {
struct DefaultsKey {
static let sendCrashLogsAutomaticallyKey = "SendCrashLogsAutomatically"

View File

@ -12,14 +12,14 @@ import os.log
struct ErrorHandler {
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account")
public static func present(_ error: Error) {
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account")
@MainActor public static func present(_ error: Error) {
NSApplication.shared.presentError(error)
}
public static func log(_ error: Error) {
os_log(.error, log: self.log, "%@", error.localizedDescription)
os_log(.error, log: log, "%@", error.localizedDescription)
}
}

View File

@ -8,7 +8,7 @@
import AppKit
final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector {
@MainActor final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector {
@IBOutlet var nameTextField: NSTextField?
@IBOutlet weak var smartFeedImageView: NSImageView!

View File

@ -11,7 +11,7 @@ import Articles
import Account
import UserNotifications
final class FeedInspectorViewController: NSViewController, Inspector {
@MainActor final class FeedInspectorViewController: NSViewController, Inspector {
@IBOutlet weak var iconView: IconView!
@IBOutlet weak var nameTextField: NSTextField?

View File

@ -10,7 +10,7 @@ import AppKit
import Account
import RSCore
final class FolderInspectorViewController: NSViewController, Inspector {
@MainActor final class FolderInspectorViewController: NSViewController, Inspector {
@IBOutlet var nameTextField: NSTextField?
@IBOutlet weak var folderImageView: NSImageView!

View File

@ -10,11 +10,11 @@ import AppKit
protocol Inspector: AnyObject {
var objects: [Any]? { get set }
var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects.
var windowTitle: String { get }
@MainActor var objects: [Any]? { get set }
@MainActor var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects.
@MainActor var windowTitle: String { get }
func canInspect(_ objects: [Any]) -> Bool
@MainActor func canInspect(_ objects: [Any]) -> Bool
}
typealias InspectorViewController = Inspector & NSViewController

View File

@ -8,7 +8,7 @@
import AppKit
final class NothingInspectorViewController: NSViewController, Inspector {
@MainActor final class NothingInspectorViewController: NSViewController, Inspector {
@IBOutlet var nothingTextField: NSTextField?
@IBOutlet var multipleTextField: NSTextField?

View File

@ -22,7 +22,7 @@ import RSParser
// Else,
// display error sheet.
class AddFeedController: AddFeedWindowControllerDelegate {
@MainActor final class AddFeedController: AddFeedWindowControllerDelegate {
private let hostWindow: NSWindow
private var addFeedWindowController: AddFeedWindowController?

View File

@ -11,7 +11,7 @@ import RSCore
import RSTree
import Account
class FolderTreeMenu {
@MainActor final class FolderTreeMenu {
static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu {

View File

@ -16,31 +16,32 @@ class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
return
}
Task { @MainActor in
let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48))
iconView.iconImage = iconImage
let renderedImage = iconView.asImage()
guard let data = renderedImage.dataRepresentation() else {
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
return
}
let headerFields = ["Cache-Control": "no-cache"]
if let response = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: headerFields) {
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
}
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
return
}
let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48))
iconView.iconImage = iconImage
let renderedImage = iconView.asImage()
guard let data = renderedImage.dataRepresentation() else {
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
return
}
let headerFields = ["Cache-Control": "no-cache"]
if let response = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: headerFields) {
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
}
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
urlSchemeTask.didFailWithError(URLError(.unknown))
}
}

View File

@ -216,13 +216,26 @@ final class DetailWebViewController: NSViewController {
extension DetailWebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
nonisolated func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == MessageName.windowDidScroll {
windowScrollY = message.body as? CGFloat
let updatedWindowScrollY = message.body as? CGFloat
Task { @MainActor in
windowScrollY = updatedWindowScrollY
}
} else if message.name == MessageName.mouseDidEnter, let link = message.body as? String {
delegate?.mouseDidEnter(self, link: link)
Task { @MainActor in
delegate?.mouseDidEnter(self, link: link)
}
} else if message.name == MessageName.mouseDidExit {
delegate?.mouseDidExit(self)
Task { @MainActor in
delegate?.mouseDidExit(self)
}
}
}
}
@ -239,10 +252,13 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
// WKNavigationDelegate
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
nonisolated public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
if let url = navigationAction.request.url {
self.openInBrowser(url, flags: navigationAction.modifierFlags)
let flags = navigationAction.modifierFlags
Task { @MainActor in
self.openInBrowser(url, flags: flags)
}
}
decisionHandler(.cancel)
return
@ -251,35 +267,42 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
decisionHandler(.allow)
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// See note in viewDidLoad()
if waitingForFirstReload {
assert(webView.isHidden)
waitingForFirstReload = false
reloadHTML()
nonisolated public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Waiting for the first navigation to complete isn't long enough to avoid the flash of white.
// A hard coded value is awful, but 5/100th of a second seems to be enough.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
webView.isHidden = false
}
} else {
if let windowScrollY = windowScrollY {
webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));")
self.windowScrollY = nil
Task { @MainActor in
// See note in viewDidLoad()
if waitingForFirstReload {
assert(webView.isHidden)
waitingForFirstReload = false
reloadHTML()
// Waiting for the first navigation to complete isn't long enough to avoid the flash of white.
// A hard coded value is awful, but 5/100th of a second seems to be enough.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
webView.isHidden = false
}
} else {
if let windowScrollY = windowScrollY {
_ = try? await webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));")
self.windowScrollY = nil
}
}
}
}
// WKUIDelegate
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
nonisolated func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
// This method is reached when WebKit handles a JavaScript based window.open() invocation, for example. One
// example where this is used is in YouTube's embedded video player when a user clicks on the video's title
// or on the "Watch in YouTube" button. For our purposes we'll handle such window.open calls the same way we
// handle clicks on a URL.
if let url = navigationAction.request.url {
self.openInBrowser(url, flags: navigationAction.modifierFlags)
let flags = navigationAction.modifierFlags
Task { @MainActor in
self.openInBrowser(url, flags: flags)
}
}
return nil

View File

@ -115,16 +115,17 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
return sidebarViewController?.selectedObjects
}
func handle(_ response: UNNotificationResponse) {
let userInfo = response.notification.request.content.userInfo
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
func handle(articlePathInfo: ArticlePathInfo) {
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathInfo)
currentTimelineViewController?.goToDeepLink(for: articlePathInfo)
}
func handle(_ activity: NSUserActivity) {
guard let userInfo = activity.userInfo else { return }
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
guard let userInfo = activity.userInfo, let articlePathUserInfo = ArticlePathInfo(userInfo: userInfo) else {
return
}
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
}

View File

@ -10,7 +10,7 @@ import AppKit
import Account
import UniformTypeIdentifiers
struct NNW3ImportController {
@MainActor struct NNW3ImportController {
/// Import NNW3 subscriptions if they exist.
/// Return true if Subscriptions.plist was found and subscriptions were imported.

View File

@ -11,7 +11,7 @@ import RSCore
// image - title - unreadCount
struct SidebarCellLayout {
@MainActor struct SidebarCellLayout {
let faviconRect: CGRect
let titleRect: CGRect

View File

@ -10,7 +10,7 @@ import AppKit
import RSTree
import Account
enum SidebarDeleteItemsAlert {
@MainActor struct SidebarDeleteItemsAlert {
/// Builds a delete confirmation dialog for the supplied nodes
static func build(_ nodes: [Node]) -> NSAlert {

View File

@ -12,7 +12,7 @@ import Articles
import RSCore
import Account
@objc final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource {
@objc @MainActor final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource {
let treeController: TreeController
static let dragOperationNone = NSDragOperation(rawValue: 0)

View File

@ -17,13 +17,13 @@ extension Notification.Name {
}
protocol SidebarDelegate: AnyObject {
func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
func unreadCount(for: AnyObject) -> Int
func sidebarInvalidatedRestorationState(_: SidebarViewController)
@MainActor func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
@MainActor func unreadCount(for: AnyObject) -> Int
@MainActor func sidebarInvalidatedRestorationState(_: SidebarViewController)
}
@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
@objc @MainActor class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
@IBOutlet weak var outlineView: NSOutlineView!
weak var delegate: SidebarDelegate?
@ -483,9 +483,9 @@ protocol SidebarDelegate: AnyObject {
revealAndSelectRepresentedObject(feed as AnyObject)
}
func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) {
guard let accountNode = findAccountNode(userInfo),
let feedNode = findFeedNode(userInfo, beginningAt: accountNode),
func deepLinkRevealAndSelect(for articlePathInfo: ArticlePathInfo) {
guard let accountNode = findAccountNode(articlePathInfo),
let feedNode = findFeedNode(articlePathInfo, beginningAt: accountNode),
let feed = feedNode.representedObject as? SidebarItem else {
return
}
@ -738,16 +738,17 @@ private extension SidebarViewController {
return nil
}
func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? {
guard let accountID = userInfo?[ArticlePathKey.accountID] as? String else {
func findAccountNode(_ articlePathInfo: ArticlePathInfo) -> Node? {
guard let accountID = articlePathInfo.accountID else {
return nil
}
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
return node
}
guard let accountName = userInfo?[ArticlePathKey.accountName] as? String else {
guard let accountName = articlePathInfo.accountName else {
return nil
}
@ -758,8 +759,8 @@ private extension SidebarViewController {
return nil
}
func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? {
guard let feedID = userInfo?[ArticlePathKey.feedID] as? String else {
func findFeedNode(_ articlePathInfo: ArticlePathInfo, beginningAt startingNode: Node) -> Node? {
guard let feedID = articlePathInfo.feedID else {
return nil
}
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) {

View File

@ -8,7 +8,7 @@
import AppKit
class UnreadCountView : NSView {
@MainActor final class UnreadCountView : NSView {
struct Appearance {
static let padding = NSEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0)

View File

@ -11,12 +11,13 @@ import Articles
import RSCore
extension Article: PasteboardWriterOwner {
public var pasteboardWriter: NSPasteboardWriting {
@MainActor public var pasteboardWriter: NSPasteboardWriting {
return ArticlePasteboardWriter(article: self)
}
}
@objc final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting {
@objc @MainActor final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting {
let article: Article
static let articleUTI = "com.ranchero.article"

View File

@ -26,7 +26,7 @@ struct TextFieldSizeInfo {
let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then.
}
final class MultilineTextFieldSizer {
@MainActor final class MultilineTextFieldSizer {
private let numberOfLines: Int
private let font: NSFont
@ -35,7 +35,7 @@ final class MultilineTextFieldSizer {
private let doubleLineHeightEstimate: Int
private var cache = [String: WidthHeightCache]() // Each string has a cache.
private var attributedCache = [NSAttributedString: WidthHeightCache]()
private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
@MainActor private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
private init(numberOfLines: Int, font: NSFont) {

View File

@ -12,7 +12,7 @@ import AppKit
// Uses a cache.
// Main thready only.
final class SingleLineTextFieldSizer {
@MainActor final class SingleLineTextFieldSizer {
let font: NSFont
private let textField: NSTextField

View File

@ -9,7 +9,7 @@
import AppKit
import Articles
struct TimelineCellData {
@MainActor struct TimelineCellData {
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")

View File

@ -9,7 +9,7 @@
import AppKit
import RSCore
struct TimelineCellLayout {
@MainActor struct TimelineCellLayout {
let width: CGFloat
let height: CGFloat

View File

@ -11,10 +11,10 @@ import Account
import Articles
protocol TimelineContainerViewControllerDelegate: AnyObject {
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed)
func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
@MainActor func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
@MainActor func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed)
@MainActor func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
}
final class TimelineContainerViewController: NSViewController {

View File

@ -13,9 +13,10 @@ import Account
import os.log
protocol TimelineDelegate: AnyObject {
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
func timelineInvalidatedRestorationState(_: TimelineViewController)
@MainActor func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
@MainActor func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
@MainActor func timelineInvalidatedRestorationState(_: TimelineViewController)
}
enum TimelineShowFeedName {
@ -535,12 +536,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
// MARK: - Navigation
func goToDeepLink(for userInfo: [AnyHashable : Any]) {
guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return }
func goToDeepLink(for articlePathInfo: ArticlePathInfo) {
guard let articleID = articlePathInfo.articleID else {
return
}
Task {
if isReadFiltered ?? false {
if let accountName = userInfo[ArticlePathKey.accountName] as? String,
if let accountName = articlePathInfo.accountName,
let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) {
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
await fetchAndReplaceArticles()

View File

@ -161,7 +161,7 @@ struct AddAccountsView: View {
}
var icloudAccount: some View {
@MainActor var icloudAccount: some View {
VStack(alignment: .leading) {
Text("iCloud")
.font(.headline)
@ -260,7 +260,7 @@ struct AddAccountsView: View {
}
}
private func isCloudInUse() -> Bool {
@MainActor private func isCloudInUse() -> Bool {
AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit })
}

View File

@ -92,7 +92,7 @@ extension AppDelegate : AppDelegateAppleEvents {
}
class NetNewsWireCreateElementCommand : NSCreateCommand {
override func performDefaultImplementation() -> Any? {
@MainActor override func performDefaultImplementation() -> Any? {
let classDescription = self.createClassDescription
if (classDescription.className == "feed") {
return ScriptableFeed.handleCreateElement(command:self)

View File

@ -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)
}
}
}
@ -142,7 +146,7 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
}
@objc(feed)
var feed: ScriptableFeed? {
@MainActor var feed: ScriptableFeed? {
guard let parentFeed = self.article.feed,
let account = parentFeed.account
else { return nil }

View File

@ -81,7 +81,7 @@ import Articles
}
}
class func handleCreateElement(command:NSCreateCommand) -> Any? {
@MainActor class func handleCreateElement(command:NSCreateCommand) -> Any? {
guard command.isCreateCommand(forClass:"Feed") else { return nil }
guard let arguments = command.arguments else {return nil}
let titleFromArgs = command.property(forKey:"name") as? String

View File

@ -65,7 +65,7 @@ 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? {
@MainActor class func handleCreateElement(command:NSCreateCommand) -> Any? {
guard command.isCreateCommand(forClass:"fold") else { return nil }
let name = command.property(forKey:"name") as? String ?? ""

View File

@ -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

View File

@ -20,8 +20,8 @@ import UniformTypeIdentifiers
import CoreSpotlight
#endif
class ActivityManager {
@MainActor final class ActivityManager {
private var nextUnreadActivity: NSUserActivity?
private var selectingActivity: NSUserActivity?
private var readingActivity: NSUserActivity?
@ -264,8 +264,8 @@ private extension ActivityManager {
return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? []
}
func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
@MainActor func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
let attributeSet = CSSearchableItemAttributeSet(contentType: UTType.item)
attributeSet.title = feed.nameForDisplay
attributeSet.keywords = makeKeywords(feed.nameForDisplay)

View File

@ -19,11 +19,12 @@ public enum ArticleExtractorState {
}
protocol ArticleExtractorDelegate {
func articleExtractionDidFail(with: Error)
func articleExtractionDidComplete(extractedArticle: ExtractedArticle)
@MainActor func articleExtractionDidFail(with: Error)
@MainActor func articleExtractionDidComplete(extractedArticle: ExtractedArticle)
}
class ArticleExtractor {
final class ArticleExtractor {
private var dataTask: URLSessionDataTask? = nil

View File

@ -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)
@ -30,10 +30,10 @@ struct ArticleRenderer {
}
}
static var imageIconScheme = "nnwImageIcon"
static var blank = Page(name: "blank")
static var page = Page(name: "page")
static let imageIconScheme = "nnwImageIcon"
static let blank = Page(name: "blank")
static let page = Page(name: "page")
private let article: Article?
private let extractedArticle: ExtractedArticle?
@ -328,7 +328,7 @@ private extension ArticleRenderer {
// MARK: - Article extension
private extension Article {
@MainActor private extension Article {
var baseURL: URL? {
var s = link

View File

@ -10,22 +10,20 @@ import Foundation
struct ArticlePathInfo {
let accountID: String
let articleID: String
let accountID: String?
let accountName: String?
let articleID: String?
let feedID: String?
init?(userInfo: [AnyHashable: Any]) {
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [String: String] else {
return nil
}
guard let accountID = articlePathUserInfo[ArticlePathKey.accountID] else {
return nil
}
guard let articleID = articlePathUserInfo[ArticlePathKey.articleID] else {
return nil
}
self.accountID = accountID
self.articleID = articleID
self.accountID = articlePathUserInfo[ArticlePathKey.accountID]
self.accountName = articlePathUserInfo[ArticlePathKey.accountName]
self.articleID = articlePathUserInfo[ArticlePathKey.articleID]
self.feedID = articlePathUserInfo[ArticlePathKey.feedID]
}
}

View File

@ -12,7 +12,7 @@ import Articles
// Mark articles read/unread, starred/unstarred, deleted/undeleted.
final class MarkStatusCommand: UndoableCommand {
@MainActor final class MarkStatusCommand: UndoableCommand {
let undoActionName: String
let redoActionName: String
@ -50,12 +50,12 @@ final class MarkStatusCommand: UndoableCommand {
self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, undoManager: undoManager, completion: completion)
}
func perform() {
@MainActor func perform() {
mark(statusKey, flag)
registerUndo()
}
func undo() {
@MainActor func undo() {
mark(statusKey, !flag)
registerRedo()
}
@ -63,7 +63,7 @@ final class MarkStatusCommand: UndoableCommand {
private extension MarkStatusCommand {
func mark(_ statusKey: ArticleStatus.Key, _ flag: Bool) {
@MainActor func mark(_ statusKey: ArticleStatus.Key, _ flag: Bool) {
markArticles(articles, statusKey: statusKey, flag: flag, completion: completion)
completion = nil
}
@ -83,7 +83,7 @@ private extension MarkStatusCommand {
}
}
static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] {
@MainActor static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] {
return articles.filter{ article in
guard article.status.boolStatus(forKey: statusKey) != flag else { return false }

View File

@ -21,7 +21,7 @@ final class SendToMarsEditCommand: SendToCommand {
appToUse() != nil
}
func sendObject(_ object: Any?, selectedText: String?) {
@MainActor func sendObject(_ object: Any?, selectedText: String?) {
guard canSendObject(object, selectedText: selectedText) else {
return
@ -39,7 +39,7 @@ final class SendToMarsEditCommand: SendToCommand {
private extension SendToMarsEditCommand {
func send(_ article: Article, to app: UserApp) {
@MainActor func send(_ article: Article, to app: UserApp) {
// App has already been launched.

View File

@ -29,7 +29,7 @@ final class SendToMicroBlogCommand: SendToCommand {
return true
}
func sendObject(_ object: Any?, selectedText: String?) {
@MainActor func sendObject(_ object: Any?, selectedText: String?) {
guard canSendObject(object, selectedText: selectedText) else {
return
@ -60,7 +60,7 @@ final class SendToMicroBlogCommand: SendToCommand {
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.

View File

@ -11,7 +11,7 @@ import Account
struct AddFeedDefaultContainer {
static var defaultContainer: Container? {
@MainActor static var defaultContainer: Container? {
if let accountID = AppDefaults.shared.addFeedAccountID, let account = AccountManager.shared.activeAccounts.first(where: { $0.accountID == accountID }) {
if let folderName = AppDefaults.shared.addFeedFolderName, let folder = account.existingFolder(withDisplayName: folderName) {

View File

@ -10,7 +10,7 @@ import Foundation
import Articles
import RSParser
struct ArticleStringFormatter {
@MainActor struct ArticleStringFormatter {
private static var feedNameCache = [String: String]()
private static var titleCache = [String: String]()

View File

@ -13,7 +13,7 @@ import Account
// These handle multiple accounts.
func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
@MainActor func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
let d: [String: Set<Article>] = accountAndArticlesDictionary(articles)
@ -42,7 +42,7 @@ private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String:
extension Article {
var feed: Feed? {
@MainActor var feed: Feed? {
return account?.existingFeed(withFeedID: feedID)
}
@ -98,7 +98,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):
@ -117,11 +117,11 @@ extension Article {
}
}
func iconImage() -> IconImage? {
@MainActor func iconImage() -> IconImage? {
return IconImageCache.shared.imageForArticle(self)
}
func iconImageUrl(feed: Feed) -> URL? {
@MainActor func iconImageUrl(feed: Feed) -> URL? {
if let image = iconImage() {
let fm = FileManager.default
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0]
@ -138,7 +138,7 @@ extension Article {
}
}
func byline() -> String {
@MainActor func byline() -> String {
guard let authors = authors ?? feed?.authors, !authors.isEmpty else {
return ""
}
@ -199,7 +199,7 @@ struct ArticlePathKey {
extension Article {
public var pathUserInfo: [AnyHashable : Any] {
@MainActor public var pathUserInfo: [AnyHashable : Any] {
return [
ArticlePathKey.accountID: accountID,
ArticlePathKey.accountName: account?.nameForDisplay ?? "",
@ -214,7 +214,7 @@ extension Article {
extension Article: SortableArticle {
var sortableName: String {
@MainActor var sortableName: String {
return feed?.name ?? ""
}

View File

@ -27,7 +27,7 @@ extension Account: SmallIconProvider {
extension Feed: SmallIconProvider {
var smallIcon: IconImage? {
@MainActor var smallIcon: IconImage? {
if let iconImage = appDelegate.faviconDownloader.favicon(for: self) {
return iconImage
}

View File

@ -10,7 +10,7 @@ import Foundation
import RSCore
import Account
final class FaviconGenerator {
@MainActor final class FaviconGenerator {
private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage

View File

@ -10,7 +10,7 @@ import Foundation
import Account
import Articles
final class IconImageCache {
@MainActor final class IconImageCache {
static let shared = IconImageCache()

View File

@ -10,7 +10,7 @@ import Foundation
import Account
import RSCore
struct DefaultFeedsImporter {
@MainActor struct DefaultFeedsImporter {
static func importDefaultFeeds(account: Account) {
let defaultFeedsURL = Bundle.main.url(forResource: "DefaultFeeds", withExtension: "opml")!

View File

@ -14,9 +14,9 @@ import Account
final class ExtensionContainersFile {
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile")
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile")
private static var filePath: String = {
private static let filePath: String = {
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
return containerURL!.appendingPathComponent("extension_containers.plist").path

View File

@ -10,8 +10,8 @@ import Foundation
import os.log
import Account
final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter, Sendable {
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
private static let filePath: String = {
@ -30,7 +30,7 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
return operationQueue
}
override init() {
@MainActor override init() {
operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
@ -41,14 +41,16 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
}
func presentedItemDidChange() {
DispatchQueue.main.async {
Task { @MainActor in
self.process()
}
}
func resume() {
NSFileCoordinator.addFilePresenter(self)
process()
Task { @MainActor in
process()
}
}
func suspend() {
@ -95,8 +97,8 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
private extension ExtensionFeedAddRequestFile {
func process() {
@MainActor func process() {
let decoder = PropertyListDecoder()
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
@ -130,7 +132,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):

View File

@ -62,7 +62,7 @@ final class SmartFeed: PseudoFeed {
}
}
@objc func fetchUnreadCounts() {
@objc @MainActor func fetchUnreadCounts() {
let activeAccounts = AccountManager.shared.activeAccounts
// Remove any accounts that are no longer active or have been deleted
@ -113,15 +113,18 @@ private extension SmartFeed {
func fetchUnreadCount(for account: Account) {
delegate.fetchUnreadCount(for: account) { singleUnreadCountResult in
guard let accountUnreadCount = try? singleUnreadCountResult.get() else {
return
MainActor.assumeIsolated {
guard let accountUnreadCount = try? singleUnreadCountResult.get() else {
return
}
self.unreadCounts[account.accountID] = accountUnreadCount
self.updateUnreadCount()
}
self.unreadCounts[account.accountID] = accountUnreadCount
self.updateUnreadCount()
}
}
func updateUnreadCount() {
@MainActor func updateUnreadCount() {
unreadCount = AccountManager.shared.activeAccounts.reduce(0) { (result, account) -> Int in
if let oneUnreadCount = unreadCounts[account.accountID] {
return result + oneUnreadCount

View File

@ -51,13 +51,13 @@ final class UnreadFeed: PseudoFeed {
}
#endif
init() {
@MainActor init() {
self.unreadCount = appDelegate.unreadCount
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: appDelegate)
}
@objc func unreadCountDidChange(_ note: Notification) {
@objc @MainActor func unreadCountDidChange(_ note: Notification) {
assert(note.object is AppDelegate)
unreadCount = appDelegate.unreadCount

View File

@ -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

View File

@ -9,7 +9,7 @@
import Foundation
import Account
class AccountRefreshTimer {
@MainActor final class AccountRefreshTimer {
var shuttingDown = false
@ -64,7 +64,7 @@ class AccountRefreshTimer {
}
@objc func timedRefresh(_ sender: Timer?) {
@objc @MainActor func timedRefresh(_ sender: Timer?) {
guard !shuttingDown else {
return

View File

@ -9,8 +9,8 @@
import Foundation
import Account
class ArticleStatusSyncTimer {
@MainActor final class ArticleStatusSyncTimer {
private static let intervalSeconds = Double(120)
var shuttingDown = false

View File

@ -24,7 +24,7 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate {
filterExceptions = Set<SidebarItemIdentifier>()
}
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
@MainActor func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
if node.isRoot {
return childNodesForRootNode(node)
}
@ -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)
@ -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

View File

@ -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,8 +22,8 @@ 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)
accountNode.canHaveChildNodes = true

View File

@ -25,9 +25,11 @@ final class UserNotificationManager: NSObject {
return
}
for article in articles {
if !article.status.read, let feed = article.feed, feed.isNotifyAboutNewArticles ?? false {
sendNotification(feed: feed, article: article)
Task { @MainActor in
for article in articles {
if !article.status.read, let feed = article.feed, feed.isNotifyAboutNewArticles ?? false {
sendNotification(feed: feed, article: article)
}
}
}
}
@ -53,7 +55,7 @@ final class UserNotificationManager: NSObject {
private extension UserNotificationManager {
func sendNotification(feed: Feed, article: Article) {
@MainActor func sendNotification(feed: Feed, article: Article) {
let content = UNMutableNotificationContent()
content.title = feed.nameForDisplay
@ -79,7 +81,7 @@ private extension UserNotificationManager {
/// - feed: `Feed`
/// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil.
/// - Warning: In certain scenarios, this will return the `faviconTemplateImage`.
func thumbnailAttachment(for article: Article, feed: Feed) -> UNNotificationAttachment? {
@MainActor func thumbnailAttachment(for article: Article, feed: Feed) -> UNNotificationAttachment? {
if let imageURL = article.iconImageUrl(feed: feed) {
let thumbnail = try? UNNotificationAttachment(identifier: feed.feedID, url: imageURL, options: nil)
return thumbnail