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

View File

@ -18,7 +18,7 @@ import Secrets
public final class AccountManager: UnreadCountProvider { 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" public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"
private static let jsonNetNewsWireNewsURL = "https://netnewswire.blog/feed.json" private static let jsonNetNewsWireNewsURL = "https://netnewswire.blog/feed.json"
@ -79,7 +79,7 @@ public final class AccountManager: UnreadCountProvider {
return lastArticleFetchEndTime 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 }) 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 { guard !articles.isEmpty else {
completion(.success(())) completion(.success(()))
return return
@ -112,7 +112,7 @@ final class CloudKitArticlesZone: CloudKitZone {
delete(ckQuery: ckQuery, completion: completion) 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 { guard !statusUpdates.isEmpty else {
completion(.success(())) completion(.success(()))
return return
@ -164,7 +164,7 @@ final class CloudKitArticlesZone: CloudKitZone {
private extension CloudKitArticlesZone { 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 { if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in self.createZoneRecord() { result in
switch result { switch result {
@ -187,7 +187,7 @@ private extension CloudKitArticlesZone {
return "a|\(id)" 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 recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: zoneID)
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID) let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
if let feedExternalID = article.feed?.externalID { if let feedExternalID = article.feed?.externalID {
@ -198,7 +198,7 @@ private extension CloudKitArticlesZone {
return record return record
} }
func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord { @MainActor func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: zoneID) let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: zoneID)
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID) let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
@ -212,7 +212,7 @@ private extension CloudKitArticlesZone {
return record return record
} }
func makeArticleRecord(_ article: Article) -> CKRecord { @MainActor func makeArticleRecord(_ article: Article) -> CKRecord {
let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: zoneID) let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: zoneID)
let record = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID) let record = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,14 +12,14 @@ import os.log
struct ErrorHandler { struct ErrorHandler {
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account") private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account")
public static func present(_ error: Error) { @MainActor public static func present(_ error: Error) {
NSApplication.shared.presentError(error) NSApplication.shared.presentError(error)
} }
public static func log(_ error: 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 import AppKit
final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector { @MainActor final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector {
@IBOutlet var nameTextField: NSTextField? @IBOutlet var nameTextField: NSTextField?
@IBOutlet weak var smartFeedImageView: NSImageView! @IBOutlet weak var smartFeedImageView: NSImageView!

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import RSCore
import RSTree import RSTree
import Account import Account
class FolderTreeMenu { @MainActor final class FolderTreeMenu {
static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu { 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) { func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else { Task { @MainActor in
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
return
}
let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
iconView.iconImage = iconImage urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
let renderedImage = iconView.asImage() return
}
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()
}
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) { func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
urlSchemeTask.didFailWithError(URLError(.unknown)) urlSchemeTask.didFailWithError(URLError(.unknown))
} }
} }

View File

@ -216,13 +216,26 @@ final class DetailWebViewController: NSViewController {
extension DetailWebViewController: WKScriptMessageHandler { extension DetailWebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { nonisolated func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == MessageName.windowDidScroll { 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 { } 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 { } else if message.name == MessageName.mouseDidExit {
delegate?.mouseDidExit(self)
Task { @MainActor in
delegate?.mouseDidExit(self)
}
} }
} }
} }
@ -239,10 +252,13 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
// WKNavigationDelegate // 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 navigationAction.navigationType == .linkActivated {
if let url = navigationAction.request.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)
}
} }
decisionHandler(.cancel) decisionHandler(.cancel)
return return
@ -251,35 +267,42 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
decisionHandler(.allow) decisionHandler(.allow)
} }
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { nonisolated public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 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. Task { @MainActor in
// A hard coded value is awful, but 5/100th of a second seems to be enough. // See note in viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { if waitingForFirstReload {
webView.isHidden = false assert(webView.isHidden)
} waitingForFirstReload = false
} else { reloadHTML()
if let windowScrollY = windowScrollY {
webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));") // Waiting for the first navigation to complete isn't long enough to avoid the flash of white.
self.windowScrollY = nil // 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 // 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 // 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 // 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 // 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. // handle clicks on a URL.
if let url = navigationAction.request.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 return nil

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
import AppKit import AppKit
class UnreadCountView : NSView { @MainActor final class UnreadCountView : NSView {
struct Appearance { struct Appearance {
static let padding = NSEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0) 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 import RSCore
extension Article: PasteboardWriterOwner { extension Article: PasteboardWriterOwner {
public var pasteboardWriter: NSPasteboardWriting {
@MainActor public var pasteboardWriter: NSPasteboardWriting {
return ArticlePasteboardWriter(article: self) return ArticlePasteboardWriter(article: self)
} }
} }
@objc final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting { @objc @MainActor final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting {
let article: Article let article: Article
static let articleUTI = "com.ranchero.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. 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 numberOfLines: Int
private let font: NSFont private let font: NSFont
@ -35,7 +35,7 @@ final class MultilineTextFieldSizer {
private let doubleLineHeightEstimate: Int private let doubleLineHeightEstimate: Int
private var cache = [String: WidthHeightCache]() // Each string has a cache. private var cache = [String: WidthHeightCache]() // Each string has a cache.
private var attributedCache = [NSAttributedString: WidthHeightCache]() private var attributedCache = [NSAttributedString: WidthHeightCache]()
private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]() @MainActor private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
private init(numberOfLines: Int, font: NSFont) { private init(numberOfLines: Int, font: NSFont) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -161,7 +161,7 @@ struct AddAccountsView: View {
} }
var icloudAccount: some View { @MainActor var icloudAccount: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("iCloud") Text("iCloud")
.font(.headline) .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 }) AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit })
} }

View File

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

View File

@ -111,7 +111,9 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
return article.status.boolStatus(forKey:.read) return article.status.boolStatus(forKey:.read)
} }
set { 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) return article.status.boolStatus(forKey:.starred)
} }
set { 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) @objc(feed)
var feed: ScriptableFeed? { @MainActor var feed: ScriptableFeed? {
guard let parentFeed = self.article.feed, guard let parentFeed = self.article.feed,
let account = parentFeed.account let account = parentFeed.account
else { return nil } 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 command.isCreateCommand(forClass:"Feed") else { return nil }
guard let arguments = command.arguments else {return nil} guard let arguments = command.arguments else {return nil}
let titleFromArgs = command.property(forKey:"name") as? String let titleFromArgs = command.property(forKey:"name") as? String

View File

@ -65,7 +65,7 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
or or
tell account X to make new folder at end with properties {name:"new folder name"} 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 } guard command.isCreateCommand(forClass:"fold") else { return nil }
let name = command.property(forKey:"name") as? String ?? "" let name = command.property(forKey:"name") as? String ?? ""

View File

@ -26,7 +26,7 @@ extension NSScriptCommand {
return true return true
} }
func accountAndFolderForNewChild() -> (Account, Folder?) { @MainActor func accountAndFolderForNewChild() -> (Account, Folder?) {
let appleEvent = self.appleEvent let appleEvent = self.appleEvent
var account = AccountManager.shared.defaultAccount var account = AccountManager.shared.defaultAccount
var folder:Folder? = nil var folder:Folder? = nil

View File

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

View File

@ -19,11 +19,12 @@ public enum ArticleExtractorState {
} }
protocol ArticleExtractorDelegate { 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 private var dataTask: URLSessionDataTask? = nil

View File

@ -14,7 +14,7 @@ import RSCore
import Articles import Articles
import Account import Account
struct ArticleRenderer { @MainActor struct ArticleRenderer {
typealias Rendering = (style: String, html: String, title: String, baseURL: String) typealias Rendering = (style: String, html: String, title: String, baseURL: String)
@ -30,10 +30,10 @@ struct ArticleRenderer {
} }
} }
static var imageIconScheme = "nnwImageIcon" static let imageIconScheme = "nnwImageIcon"
static var blank = Page(name: "blank") static let blank = Page(name: "blank")
static var page = Page(name: "page") static let page = Page(name: "page")
private let article: Article? private let article: Article?
private let extractedArticle: ExtractedArticle? private let extractedArticle: ExtractedArticle?
@ -328,7 +328,7 @@ private extension ArticleRenderer {
// MARK: - Article extension // MARK: - Article extension
private extension Article { @MainActor private extension Article {
var baseURL: URL? { var baseURL: URL? {
var s = link var s = link

View File

@ -10,22 +10,20 @@ import Foundation
struct ArticlePathInfo { struct ArticlePathInfo {
let accountID: String let accountID: String?
let articleID: String let accountName: String?
let articleID: String?
let feedID: String?
init?(userInfo: [AnyHashable: Any]) { init?(userInfo: [AnyHashable: Any]) {
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [String: String] else { guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [String: String] else {
return nil return nil
} }
guard let accountID = articlePathUserInfo[ArticlePathKey.accountID] else {
return nil
}
guard let articleID = articlePathUserInfo[ArticlePathKey.articleID] else {
return nil
}
self.accountID = accountID self.accountID = articlePathUserInfo[ArticlePathKey.accountID]
self.articleID = articleID 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. // Mark articles read/unread, starred/unstarred, deleted/undeleted.
final class MarkStatusCommand: UndoableCommand { @MainActor final class MarkStatusCommand: UndoableCommand {
let undoActionName: String let undoActionName: String
let redoActionName: String let redoActionName: String
@ -50,12 +50,12 @@ final class MarkStatusCommand: UndoableCommand {
self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, undoManager: undoManager, completion: completion) self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, undoManager: undoManager, completion: completion)
} }
func perform() { @MainActor func perform() {
mark(statusKey, flag) mark(statusKey, flag)
registerUndo() registerUndo()
} }
func undo() { @MainActor func undo() {
mark(statusKey, !flag) mark(statusKey, !flag)
registerRedo() registerRedo()
} }
@ -63,7 +63,7 @@ final class MarkStatusCommand: UndoableCommand {
private extension MarkStatusCommand { 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) markArticles(articles, statusKey: statusKey, flag: flag, completion: completion)
completion = nil 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 return articles.filter{ article in
guard article.status.boolStatus(forKey: statusKey) != flag else { return false } guard article.status.boolStatus(forKey: statusKey) != flag else { return false }

View File

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

View File

@ -29,7 +29,7 @@ final class SendToMicroBlogCommand: SendToCommand {
return true return true
} }
func sendObject(_ object: Any?, selectedText: String?) { @MainActor func sendObject(_ object: Any?, selectedText: String?) {
guard canSendObject(object, selectedText: selectedText) else { guard canSendObject(object, selectedText: selectedText) else {
return return
@ -60,7 +60,7 @@ final class SendToMicroBlogCommand: SendToCommand {
private extension Article { private extension Article {
var attributionString: String { @MainActor var attributionString: String {
// Feed name, or feed name + author name (if author is specified per-article). // Feed name, or feed name + author name (if author is specified per-article).
// Includes trailing space. // Includes trailing space.

View File

@ -11,7 +11,7 @@ import Account
struct AddFeedDefaultContainer { 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 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) { if let folderName = AppDefaults.shared.addFeedFolderName, let folder = account.existingFolder(withDisplayName: folderName) {

View File

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

View File

@ -13,7 +13,7 @@ import Account
// These handle multiple accounts. // 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) let d: [String: Set<Article>] = accountAndArticlesDictionary(articles)
@ -42,7 +42,7 @@ private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String:
extension Article { extension Article {
var feed: Feed? { @MainActor var feed: Feed? {
return account?.existingFeed(withFeedID: feedID) return account?.existingFeed(withFeedID: feedID)
} }
@ -98,7 +98,7 @@ extension Article {
return datePublished ?? dateModified ?? status.dateArrived return datePublished ?? dateModified ?? status.dateArrived
} }
var isAvailableToMarkUnread: Bool { @MainActor var isAvailableToMarkUnread: Bool {
guard let markUnreadWindow = account?.behaviors.compactMap( { behavior -> Int? in guard let markUnreadWindow = account?.behaviors.compactMap( { behavior -> Int? in
switch behavior { switch behavior {
case .disallowMarkAsUnreadAfterPeriod(let days): case .disallowMarkAsUnreadAfterPeriod(let days):
@ -117,11 +117,11 @@ extension Article {
} }
} }
func iconImage() -> IconImage? { @MainActor func iconImage() -> IconImage? {
return IconImageCache.shared.imageForArticle(self) return IconImageCache.shared.imageForArticle(self)
} }
func iconImageUrl(feed: Feed) -> URL? { @MainActor func iconImageUrl(feed: Feed) -> URL? {
if let image = iconImage() { if let image = iconImage() {
let fm = FileManager.default let fm = FileManager.default
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0] 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 { guard let authors = authors ?? feed?.authors, !authors.isEmpty else {
return "" return ""
} }
@ -199,7 +199,7 @@ struct ArticlePathKey {
extension Article { extension Article {
public var pathUserInfo: [AnyHashable : Any] { @MainActor public var pathUserInfo: [AnyHashable : Any] {
return [ return [
ArticlePathKey.accountID: accountID, ArticlePathKey.accountID: accountID,
ArticlePathKey.accountName: account?.nameForDisplay ?? "", ArticlePathKey.accountName: account?.nameForDisplay ?? "",
@ -214,7 +214,7 @@ extension Article {
extension Article: SortableArticle { extension Article: SortableArticle {
var sortableName: String { @MainActor var sortableName: String {
return feed?.name ?? "" return feed?.name ?? ""
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -14,9 +14,9 @@ import Account
final class ExtensionContainersFile { 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 appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
return containerURL!.appendingPathComponent("extension_containers.plist").path return containerURL!.appendingPathComponent("extension_containers.plist").path

View File

@ -10,8 +10,8 @@ import Foundation
import os.log import os.log
import Account 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 log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
private static let filePath: String = { private static let filePath: String = {
@ -30,7 +30,7 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
return operationQueue return operationQueue
} }
override init() { @MainActor override init() {
operationQueue = OperationQueue() operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1 operationQueue.maxConcurrentOperationCount = 1
@ -41,14 +41,16 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
} }
func presentedItemDidChange() { func presentedItemDidChange() {
DispatchQueue.main.async { Task { @MainActor in
self.process() self.process()
} }
} }
func resume() { func resume() {
NSFileCoordinator.addFilePresenter(self) NSFileCoordinator.addFilePresenter(self)
process() Task { @MainActor in
process()
}
} }
func suspend() { func suspend() {
@ -95,8 +97,8 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
private extension ExtensionFeedAddRequestFile { private extension ExtensionFeedAddRequestFile {
func process() { @MainActor func process() {
let decoder = PropertyListDecoder() let decoder = PropertyListDecoder()
let encoder = PropertyListEncoder() let encoder = PropertyListEncoder()
encoder.outputFormat = .binary encoder.outputFormat = .binary
@ -130,7 +132,7 @@ private extension ExtensionFeedAddRequestFile {
requests?.forEach { processRequest($0) } requests?.forEach { processRequest($0) }
} }
func processRequest(_ request: ExtensionFeedAddRequest) { @MainActor func processRequest(_ request: ExtensionFeedAddRequest) {
var destinationAccountID: String? = nil var destinationAccountID: String? = nil
switch request.destinationContainerID { switch request.destinationContainerID {
case .account(let accountID): 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 let activeAccounts = AccountManager.shared.activeAccounts
// Remove any accounts that are no longer active or have been deleted // Remove any accounts that are no longer active or have been deleted
@ -113,15 +113,18 @@ private extension SmartFeed {
func fetchUnreadCount(for account: Account) { func fetchUnreadCount(for account: Account) {
delegate.fetchUnreadCount(for: account) { singleUnreadCountResult in 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 unreadCount = AccountManager.shared.activeAccounts.reduce(0) { (result, account) -> Int in
if let oneUnreadCount = unreadCounts[account.accountID] { if let oneUnreadCount = unreadCounts[account.accountID] {
return result + oneUnreadCount return result + oneUnreadCount

View File

@ -51,13 +51,13 @@ final class UnreadFeed: PseudoFeed {
} }
#endif #endif
init() { @MainActor init() {
self.unreadCount = appDelegate.unreadCount self.unreadCount = appDelegate.unreadCount
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: appDelegate) 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) assert(note.object is AppDelegate)
unreadCount = appDelegate.unreadCount unreadCount = appDelegate.unreadCount

View File

@ -67,7 +67,7 @@ extension Array where Element == Article {
return false return false
} }
func anyArticleIsReadAndCanMarkUnread() -> Bool { @MainActor func anyArticleIsReadAndCanMarkUnread() -> Bool {
return anyArticlePassesTest { $0.status.read && $0.isAvailableToMarkUnread } return anyArticlePassesTest { $0.status.read && $0.isAvailableToMarkUnread }
} }
@ -95,7 +95,7 @@ extension Array where Element == Article {
var i = 0 var i = 0
for article in self { for article in self {
let otherArticle = otherArticles[i] let otherArticle = otherArticles[i]
if article.account != otherArticle.account || article.articleID != otherArticle.articleID { if article.accountID != otherArticle.accountID || article.articleID != otherArticle.articleID {
return false return false
} }
i += 1 i += 1

View File

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

View File

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

View File

@ -24,7 +24,7 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate {
filterExceptions = Set<SidebarItemIdentifier>() filterExceptions = Set<SidebarItemIdentifier>()
} }
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { @MainActor func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
if node.isRoot { if node.isRoot {
return childNodesForRootNode(node) return childNodesForRootNode(node)
} }
@ -41,7 +41,7 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate {
private extension FeedTreeControllerDelegate { private extension FeedTreeControllerDelegate {
func childNodesForRootNode(_ rootNode: Node) -> [Node]? { @MainActor func childNodesForRootNode(_ rootNode: Node) -> [Node]? {
var topLevelNodes = [Node]() var topLevelNodes = [Node]()
let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared) let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared)
@ -132,7 +132,7 @@ private extension FeedTreeControllerDelegate {
return node return node
} }
func sortedAccountNodes(_ parent: Node) -> [Node] { @MainActor func sortedAccountNodes(_ parent: Node) -> [Node] {
let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in
let accountNode = parent.existingOrNewChildNode(with: account) let accountNode = parent.existingOrNewChildNode(with: account)
accountNode.canHaveChildNodes = true accountNode.canHaveChildNodes = true

View File

@ -14,7 +14,7 @@ import Account
final class FolderTreeControllerDelegate: TreeControllerDelegate { 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) return node.isRoot ? childNodesForRootNode(node) : childNodes(node)
} }
@ -22,8 +22,8 @@ final class FolderTreeControllerDelegate: TreeControllerDelegate {
private extension FolderTreeControllerDelegate { private extension FolderTreeControllerDelegate {
func childNodesForRootNode(_ node: Node) -> [Node]? { @MainActor func childNodesForRootNode(_ node: Node) -> [Node]? {
let accountNodes: [Node] = AccountManager.shared.sortedActiveAccounts.map { account in let accountNodes: [Node] = AccountManager.shared.sortedActiveAccounts.map { account in
let accountNode = Node(representedObject: account, parent: node) let accountNode = Node(representedObject: account, parent: node)
accountNode.canHaveChildNodes = true accountNode.canHaveChildNodes = true

View File

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