Continue fixing concurrency warnings.
This commit is contained in:
parent
6ab10e871c
commit
d0760f3d12
@ -709,7 +709,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
public func articles(feed: Feed) async throws -> Set<Article> {
|
||||
|
||||
let articles = try await database.articles(feedID: feed.feedID)
|
||||
validateUnreadCount(feed, articles)
|
||||
await validateUnreadCount(feed, articles)
|
||||
return articles
|
||||
}
|
||||
|
||||
@ -801,12 +801,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
precondition(type == .onMyMac || type == .cloudKit)
|
||||
|
||||
database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder) { updateArticlesResult in
|
||||
switch updateArticlesResult {
|
||||
case .success(let articleChanges):
|
||||
self.sendNotificationAbout(articleChanges)
|
||||
completion(.success(articleChanges))
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
switch updateArticlesResult {
|
||||
case .success(let articleChanges):
|
||||
self.sendNotificationAbout(articleChanges)
|
||||
completion(.success(articleChanges))
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -821,12 +824,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
}
|
||||
|
||||
database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { updateArticlesResult in
|
||||
switch updateArticlesResult {
|
||||
case .success(let newAndUpdatedArticles):
|
||||
self.sendNotificationAbout(newAndUpdatedArticles)
|
||||
completion(nil)
|
||||
case .failure(let databaseError):
|
||||
completion(databaseError)
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
switch updateArticlesResult {
|
||||
case .success(let newAndUpdatedArticles):
|
||||
self.sendNotificationAbout(newAndUpdatedArticles)
|
||||
completion(nil)
|
||||
case .failure(let databaseError):
|
||||
completion(databaseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -839,14 +845,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
}
|
||||
|
||||
database.mark(articles, statusKey: statusKey, flag: flag) { result in
|
||||
switch result {
|
||||
case .success(let updatedStatuses):
|
||||
let updatedArticleIDs = updatedStatuses.articleIDs()
|
||||
let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) })
|
||||
self.noteStatusesForArticlesDidChange(updatedArticles)
|
||||
completion(.success(updatedArticles))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
switch result {
|
||||
case .success(let updatedStatuses):
|
||||
let updatedArticleIDs = updatedStatuses.articleIDs()
|
||||
let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) })
|
||||
self.noteStatusesForArticlesDidChange(updatedArticles)
|
||||
completion(.success(updatedArticles))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1118,12 +1127,15 @@ private extension Account {
|
||||
|
||||
func fetchArticlesAsync(feed: Feed, _ completion: @escaping ArticleSetResultBlock) {
|
||||
database.fetchArticlesAsync(feed.feedID) { [weak self] articleSetResult in
|
||||
switch articleSetResult {
|
||||
case .success(let articles):
|
||||
self?.validateUnreadCount(feed, articles)
|
||||
completion(.success(articles))
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
switch articleSetResult {
|
||||
case .success(let articles):
|
||||
self?.validateUnreadCount(feed, articles)
|
||||
completion(.success(articles))
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1227,7 +1239,7 @@ private extension Account {
|
||||
}
|
||||
}
|
||||
|
||||
func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
|
||||
@MainActor func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
|
||||
// articles must contain all the unread articles for the feed.
|
||||
// The unread number should match the feed’s unread count.
|
||||
|
||||
@ -1301,7 +1313,7 @@ private extension Account {
|
||||
unreadCount = updatedUnreadCount
|
||||
}
|
||||
|
||||
func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
|
||||
@MainActor func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
|
||||
let feeds = Set(articles.compactMap { $0.feed })
|
||||
let statuses = Set(articles.map { $0.status })
|
||||
let articleIDs = Set(articles.map { $0.articleID })
|
||||
@ -1389,7 +1401,7 @@ private extension Account {
|
||||
}
|
||||
}
|
||||
|
||||
func sendNotificationAbout(_ articleChanges: ArticleChanges) {
|
||||
@MainActor func sendNotificationAbout(_ articleChanges: ArticleChanges) {
|
||||
var feeds = Set<Feed>()
|
||||
|
||||
if let newArticles = articleChanges.newArticles {
|
||||
|
@ -18,7 +18,7 @@ import Secrets
|
||||
|
||||
public final class AccountManager: UnreadCountProvider {
|
||||
|
||||
public static var shared: AccountManager!
|
||||
@MainActor public static var shared: AccountManager!
|
||||
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"
|
||||
private static let jsonNetNewsWireNewsURL = "https://netnewswire.blog/feed.json"
|
||||
|
||||
@ -79,7 +79,7 @@ public final class AccountManager: UnreadCountProvider {
|
||||
return lastArticleFetchEndTime
|
||||
}
|
||||
|
||||
public func existingActiveAccount(forDisplayName displayName: String) -> Account? {
|
||||
@MainActor public func existingActiveAccount(forDisplayName displayName: String) -> Account? {
|
||||
return AccountManager.shared.activeAccounts.first(where: { $0.nameForDisplay == displayName })
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,7 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
@MainActor func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
guard !articles.isEmpty else {
|
||||
completion(.success(()))
|
||||
return
|
||||
@ -112,7 +112,7 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
delete(ckQuery: ckQuery, completion: completion)
|
||||
}
|
||||
|
||||
func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
@MainActor func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
guard !statusUpdates.isEmpty else {
|
||||
completion(.success(()))
|
||||
return
|
||||
@ -164,7 +164,7 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
|
||||
private extension CloudKitArticlesZone {
|
||||
|
||||
func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
@MainActor func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
if case CloudKitZoneError.userDeletedZone = error {
|
||||
self.createZoneRecord() { result in
|
||||
switch result {
|
||||
@ -187,7 +187,7 @@ private extension CloudKitArticlesZone {
|
||||
return "a|\(id)"
|
||||
}
|
||||
|
||||
func makeStatusRecord(_ article: Article) -> CKRecord {
|
||||
@MainActor func makeStatusRecord(_ article: Article) -> CKRecord {
|
||||
let recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
|
||||
if let feedExternalID = article.feed?.externalID {
|
||||
@ -198,7 +198,7 @@ private extension CloudKitArticlesZone {
|
||||
return record
|
||||
}
|
||||
|
||||
func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
|
||||
@MainActor func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
|
||||
let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
|
||||
|
||||
@ -212,7 +212,7 @@ private extension CloudKitArticlesZone {
|
||||
return record
|
||||
}
|
||||
|
||||
func makeArticleRecord(_ article: Article) -> CKRecord {
|
||||
@MainActor func makeArticleRecord(_ article: Article) -> CKRecord {
|
||||
let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID)
|
||||
|
||||
|
@ -139,22 +139,24 @@ private extension CloudKitSendStatusOperation {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
articlesZone.modifyArticles(statusUpdates) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.database.deleteSelectedForProcessing(statusUpdates.map({ $0.articleID })) { _ in
|
||||
done(false)
|
||||
}
|
||||
case .failure(let error):
|
||||
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
||||
self.processAccountError(account, error)
|
||||
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
|
||||
completion(true)
|
||||
|
||||
Task { @MainActor in
|
||||
articlesZone.modifyArticles(statusUpdates) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.database.deleteSelectedForProcessing(statusUpdates.map({ $0.articleID })) { _ in
|
||||
done(false)
|
||||
}
|
||||
case .failure(let error):
|
||||
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
||||
self.processAccountError(account, error)
|
||||
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch result {
|
||||
|
@ -48,7 +48,7 @@ extension Feed {
|
||||
|
||||
public extension Article {
|
||||
|
||||
var account: Account? {
|
||||
@MainActor var account: Account? {
|
||||
// The force unwrapped shared instance was crashing Account.framework unit tests.
|
||||
guard let manager = AccountManager.shared else {
|
||||
return nil
|
||||
@ -56,7 +56,7 @@ public extension Article {
|
||||
return manager.existingAccount(with: accountID)
|
||||
}
|
||||
|
||||
var feed: Feed? {
|
||||
@MainActor var feed: Feed? {
|
||||
return account?.existingFeed(withFeedID: feedID)
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
|
||||
return anchor
|
||||
}
|
||||
|
||||
private func didEndRequestingAccessToken(_ result: Result<OAuthAuthorizationGrant, Error>) {
|
||||
@MainActor private func didEndRequestingAccessToken(_ result: Result<OAuthAuthorizationGrant, Error>) {
|
||||
guard !isCanceled else {
|
||||
didFinish()
|
||||
return
|
||||
@ -147,7 +147,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
private func saveAccount(for grant: OAuthAuthorizationGrant) {
|
||||
@MainActor private func saveAccount(for grant: OAuthAuthorizationGrant) {
|
||||
guard !AccountManager.shared.duplicateServiceAccount(type: .feedly, username: grant.accessToken.username) else {
|
||||
didFinish(OAuthAccountAuthorizationOperationError.duplicateAccount)
|
||||
return
|
||||
|
@ -12,226 +12,144 @@ import Account
|
||||
|
||||
struct AppAssets {
|
||||
|
||||
static var accountBazQux: RSImage! = {
|
||||
return RSImage(named: "accountBazQux")
|
||||
}()
|
||||
static let accountBazQux = RSImage(named: "accountBazQux")
|
||||
|
||||
static var accountCloudKit: RSImage! = {
|
||||
return RSImage(named: "accountCloudKit")
|
||||
}()
|
||||
static let accountCloudKit = RSImage(named: "accountCloudKit")
|
||||
|
||||
static var accountFeedbin: RSImage! = {
|
||||
return RSImage(named: "accountFeedbin")
|
||||
}()
|
||||
static let accountFeedbin = RSImage(named: "accountFeedbin")
|
||||
|
||||
static let accountFeedly = RSImage(named: "accountFeedly")
|
||||
|
||||
static var accountFeedly: RSImage! = {
|
||||
return RSImage(named: "accountFeedly")
|
||||
}()
|
||||
|
||||
static var accountFreshRSS: RSImage! = {
|
||||
return RSImage(named: "accountFreshRSS")
|
||||
}()
|
||||
static let accountFreshRSS = RSImage(named: "accountFreshRSS")
|
||||
|
||||
static var accountInoreader: RSImage! = {
|
||||
return RSImage(named: "accountInoreader")
|
||||
}()
|
||||
static let accountInoreader = RSImage(named: "accountInoreader")
|
||||
|
||||
static var accountLocal: RSImage! = {
|
||||
return RSImage(named: "accountLocal")
|
||||
}()
|
||||
static let accountLocal = RSImage(named: "accountLocal")
|
||||
|
||||
static var accountNewsBlur: RSImage! = {
|
||||
return RSImage(named: "accountNewsBlur")
|
||||
}()
|
||||
|
||||
static var accountTheOldReader: RSImage! = {
|
||||
return RSImage(named: "accountTheOldReader")
|
||||
}()
|
||||
static let accountNewsBlur = RSImage(named: "accountNewsBlur")
|
||||
|
||||
static var addNewSidebarItemImage: RSImage = {
|
||||
return NSImage(systemSymbolName: "plus", accessibilityDescription: nil)!
|
||||
}()
|
||||
static let accountTheOldReader = RSImage(named: "accountTheOldReader")
|
||||
|
||||
static var articleExtractorError: RSImage = {
|
||||
return RSImage(named: "articleExtractorError")!
|
||||
}()
|
||||
static let addNewSidebarItemImage = NSImage(systemSymbolName: "plus", accessibilityDescription: nil)!
|
||||
|
||||
static var articleExtractorOff: RSImage = {
|
||||
return RSImage(named: "articleExtractorOff")!
|
||||
}()
|
||||
static let articleExtractorError = RSImage(named: "articleExtractorError")!
|
||||
|
||||
static var articleExtractorOn: RSImage = {
|
||||
return RSImage(named: "articleExtractorOn")!
|
||||
}()
|
||||
static let articleExtractorOff = RSImage(named: "articleExtractorOff")!
|
||||
|
||||
static var articleTheme: RSImage = {
|
||||
return NSImage(systemSymbolName: "doc.richtext", accessibilityDescription: nil)!
|
||||
}()
|
||||
static let articleExtractorOn = RSImage(named: "articleExtractorOn")!
|
||||
|
||||
static var cleanUpImage: RSImage = {
|
||||
return NSImage(systemSymbolName: "wind", accessibilityDescription: nil)!
|
||||
}()
|
||||
static let articleTheme = NSImage(systemSymbolName: "doc.richtext", accessibilityDescription: nil)!
|
||||
|
||||
static var marsEditIcon: RSImage = {
|
||||
return RSImage(named: "MarsEditIcon")!
|
||||
}()
|
||||
|
||||
static var microblogIcon: RSImage = {
|
||||
return RSImage(named: "MicroblogIcon")!
|
||||
}()
|
||||
|
||||
static var faviconTemplateImage: RSImage = {
|
||||
return RSImage(named: "faviconTemplateImage")!
|
||||
}()
|
||||
static let cleanUpImage = NSImage(systemSymbolName: "wind", accessibilityDescription: nil)!
|
||||
|
||||
static var filterActive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle.fill", accessibilityDescription: nil)!
|
||||
static let marsEditIcon = RSImage(named: "MarsEditIcon")!
|
||||
|
||||
static var filterInactive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle", accessibilityDescription: nil)!
|
||||
static let microblogIcon = RSImage(named: "MicroblogIcon")!
|
||||
|
||||
static var iconLightBackgroundColor: NSColor = {
|
||||
return NSColor(named: NSColor.Name("iconLightBackgroundColor"))!
|
||||
}()
|
||||
static let faviconTemplateImage = RSImage(named: "faviconTemplateImage")!
|
||||
|
||||
static var iconDarkBackgroundColor: NSColor = {
|
||||
return NSColor(named: NSColor.Name("iconDarkBackgroundColor"))!
|
||||
}()
|
||||
|
||||
static var legacyArticleExtractor: RSImage! = {
|
||||
return RSImage(named: "legacyArticleExtractor")
|
||||
}()
|
||||
|
||||
static var legacyArticleExtractorError: RSImage! = {
|
||||
return RSImage(named: "legacyArticleExtractorError")
|
||||
}()
|
||||
|
||||
static var legacyArticleExtractorInactiveDark: RSImage! = {
|
||||
return RSImage(named: "legacyArticleExtractorInactiveDark")
|
||||
}()
|
||||
|
||||
static var legacyArticleExtractorInactiveLight: RSImage! = {
|
||||
return RSImage(named: "legacyArticleExtractorInactiveLight")
|
||||
}()
|
||||
|
||||
static var legacyArticleExtractorProgress1: RSImage! = {
|
||||
return RSImage(named: "legacyArticleExtractorProgress1")
|
||||
}()
|
||||
|
||||
static var legacyArticleExtractorProgress2: RSImage! = {
|
||||
return RSImage(named: "legacyArticleExtractorProgress2")
|
||||
}()
|
||||
|
||||
static var legacyArticleExtractorProgress3: RSImage! = {
|
||||
return RSImage(named: "legacyArticleExtractorProgress3")
|
||||
}()
|
||||
|
||||
static var legacyArticleExtractorProgress4: RSImage! = {
|
||||
return RSImage(named: "legacyArticleExtractorProgress4")
|
||||
}()
|
||||
|
||||
static var folderImage: IconImage = {
|
||||
static let filterActive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle.fill", accessibilityDescription: nil)!
|
||||
|
||||
static let filterInactive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle", accessibilityDescription: nil)!
|
||||
|
||||
static let iconLightBackgroundColor = NSColor(named: NSColor.Name("iconLightBackgroundColor"))!
|
||||
|
||||
static let iconDarkBackgroundColor = NSColor(named: NSColor.Name("iconDarkBackgroundColor"))!
|
||||
|
||||
static let legacyArticleExtractor = RSImage(named: "legacyArticleExtractor")!
|
||||
|
||||
static let legacyArticleExtractorError = RSImage(named: "legacyArticleExtractorError")!
|
||||
|
||||
static let legacyArticleExtractorInactiveDark = RSImage(named: "legacyArticleExtractorInactiveDark")!
|
||||
|
||||
static let legacyArticleExtractorInactiveLight = RSImage(named: "legacyArticleExtractorInactiveLight")!
|
||||
|
||||
static let legacyArticleExtractorProgress1 = RSImage(named: "legacyArticleExtractorProgress1")
|
||||
|
||||
static let legacyArticleExtractorProgress2 = RSImage(named: "legacyArticleExtractorProgress2")
|
||||
|
||||
static let legacyArticleExtractorProgress3 = RSImage(named: "legacyArticleExtractorProgress3")
|
||||
|
||||
static let legacyArticleExtractorProgress4 = RSImage(named: "legacyArticleExtractorProgress4")
|
||||
|
||||
static let folderImage: IconImage = {
|
||||
let image = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)!
|
||||
let preferredColor = NSColor(named: "AccentColor")!
|
||||
let coloredImage = image.tinted(with: preferredColor)
|
||||
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
||||
}()
|
||||
|
||||
static var markAllAsReadImage: RSImage = {
|
||||
return RSImage(named: "markAllAsRead")!
|
||||
}()
|
||||
static let markAllAsReadImage = RSImage(named: "markAllAsRead")!
|
||||
|
||||
static var nextUnreadImage: RSImage = {
|
||||
return NSImage(systemSymbolName: "chevron.down.circle", accessibilityDescription: nil)!
|
||||
}()
|
||||
static let nextUnreadImage = NSImage(systemSymbolName: "chevron.down.circle", accessibilityDescription: nil)!
|
||||
|
||||
static var openInBrowserImage: RSImage = {
|
||||
return NSImage(systemSymbolName: "safari", accessibilityDescription: nil)!
|
||||
}()
|
||||
static let openInBrowserImage = NSImage(systemSymbolName: "safari", accessibilityDescription: nil)!
|
||||
|
||||
static var preferencesToolbarAccountsImage = NSImage(systemSymbolName: "at", accessibilityDescription: nil)!
|
||||
static let preferencesToolbarAccountsImage = NSImage(systemSymbolName: "at", accessibilityDescription: nil)!
|
||||
|
||||
static var preferencesToolbarGeneralImage = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)!
|
||||
static let preferencesToolbarGeneralImage = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)!
|
||||
|
||||
static var preferencesToolbarAdvancedImage = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)!
|
||||
static let preferencesToolbarAdvancedImage = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)!
|
||||
|
||||
static var readClosedImage = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
|
||||
static let readClosedImage = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
|
||||
|
||||
static var readOpenImage: RSImage = {
|
||||
return NSImage(systemSymbolName: "circle", accessibilityDescription: nil)!
|
||||
}()
|
||||
static let readOpenImage = NSImage(systemSymbolName: "circle", accessibilityDescription: nil)!
|
||||
|
||||
static var refreshImage: RSImage = {
|
||||
return NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil)!
|
||||
}()
|
||||
|
||||
static var searchFeedImage: IconImage = {
|
||||
static let refreshImage = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil)!
|
||||
|
||||
static let searchFeedImage: IconImage = {
|
||||
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!, isSymbol: true, isBackgroundSupressed: true)
|
||||
}()
|
||||
|
||||
static var shareImage: RSImage = {
|
||||
return NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: nil)!
|
||||
}()
|
||||
static let shareImage = NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: nil)!
|
||||
|
||||
static var sidebarToggleImage: RSImage = {
|
||||
return NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)!
|
||||
}()
|
||||
|
||||
static var starClosedImage: RSImage = {
|
||||
return NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
|
||||
}()
|
||||
static let sidebarToggleImage = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)!
|
||||
|
||||
static var starOpenImage: RSImage = {
|
||||
return NSImage(systemSymbolName: "star", accessibilityDescription: nil)!
|
||||
}()
|
||||
|
||||
static var starredFeedImage: IconImage = {
|
||||
static let starClosedImage = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
|
||||
|
||||
static let starOpenImage = NSImage(systemSymbolName: "star", accessibilityDescription: nil)!
|
||||
|
||||
static let starredFeedImage: IconImage = {
|
||||
let image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
|
||||
let preferredColor = NSColor(named: "StarColor")!
|
||||
let coloredImage = image.tinted(with: preferredColor)
|
||||
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
||||
}()
|
||||
|
||||
static var timelineSeparatorColor: NSColor = {
|
||||
return NSColor(named: "timelineSeparatorColor")!
|
||||
}()
|
||||
|
||||
static var timelineStarSelected: RSImage! = {
|
||||
return RSImage(named: "timelineStar")?.tinted(with: .white)
|
||||
}()
|
||||
static let timelineSeparatorColor = NSColor(named: "timelineSeparatorColor")!
|
||||
|
||||
static var timelineStarUnselected: RSImage! = {
|
||||
return RSImage(named: "timelineStar")?.tinted(with: starColor)
|
||||
}()
|
||||
static let timelineStarSelected = RSImage(named: "timelineStar")?.tinted(with: .white)
|
||||
|
||||
static var todayFeedImage: IconImage = {
|
||||
static let timelineStarUnselected = RSImage(named: "timelineStar")?.tinted(with: starColor)
|
||||
|
||||
static let todayFeedImage: IconImage = {
|
||||
let image = NSImage(systemSymbolName: "sun.max.fill", accessibilityDescription: nil)!
|
||||
let preferredColor = NSColor.orange
|
||||
let coloredImage = image.tinted(with: preferredColor)
|
||||
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
||||
}()
|
||||
|
||||
static var unreadFeedImage: IconImage = {
|
||||
static let unreadFeedImage: IconImage = {
|
||||
let image = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
|
||||
let preferredColor = NSColor(named: "AccentColor")!
|
||||
let coloredImage = image.tinted(with: preferredColor)
|
||||
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
||||
}()
|
||||
|
||||
static var swipeMarkReadImage = RSImage(systemSymbolName: "circle", accessibilityDescription: "Mark Read")!
|
||||
static let swipeMarkReadImage = RSImage(systemSymbolName: "circle", accessibilityDescription: "Mark Read")!
|
||||
.withSymbolConfiguration(.init(scale: .large))
|
||||
|
||||
static var swipeMarkUnreadImage = RSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: "Mark Unread")!
|
||||
static let swipeMarkUnreadImage = RSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: "Mark Unread")!
|
||||
.withSymbolConfiguration(.init(scale: .large))
|
||||
|
||||
static var swipeMarkStarredImage = RSImage(systemSymbolName: "star.fill", accessibilityDescription: "Star")!
|
||||
static let swipeMarkStarredImage = RSImage(systemSymbolName: "star.fill", accessibilityDescription: "Star")!
|
||||
.withSymbolConfiguration(.init(scale: .large))
|
||||
|
||||
static var swipeMarkUnstarredImage = RSImage(systemSymbolName: "star", accessibilityDescription: "Unstar")!
|
||||
static let swipeMarkUnstarredImage = RSImage(systemSymbolName: "star", accessibilityDescription: "Unstar")!
|
||||
.withSymbolConfiguration(.init(scale: .large))!
|
||||
|
||||
static var starColor: NSColor = {
|
||||
return NSColor(named: NSColor.Name("StarColor"))!
|
||||
}()
|
||||
|
||||
static let starColor = NSColor(named: NSColor.Name("StarColor"))!
|
||||
|
||||
static func image(for accountType: AccountType) -> NSImage? {
|
||||
switch accountType {
|
||||
case .onMyMac:
|
||||
@ -254,5 +172,4 @@ struct AppAssets {
|
||||
return AppAssets.accountTheOldReader
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ enum FontSize: Int {
|
||||
case veryLarge = 3
|
||||
}
|
||||
|
||||
final class AppDefaults {
|
||||
|
||||
final class AppDefaults: Sendable {
|
||||
|
||||
static let defaultThemeName = "Default"
|
||||
|
||||
static let shared = AppDefaults()
|
||||
@ -66,7 +66,7 @@ final class AppDefaults {
|
||||
return false
|
||||
}()
|
||||
|
||||
var isFirstRun: Bool = {
|
||||
let isFirstRun: Bool = {
|
||||
if let _ = UserDefaults.standard.object(forKey: Key.firstRunDate) as? Date {
|
||||
return false
|
||||
}
|
||||
|
@ -27,10 +27,10 @@ protocol SPUUpdaterDelegate {}
|
||||
import Sparkle
|
||||
#endif
|
||||
|
||||
var appDelegate: AppDelegate!
|
||||
@MainActor var appDelegate: AppDelegate!
|
||||
|
||||
@NSApplicationMain
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate {
|
||||
@MainActor final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate {
|
||||
|
||||
private struct WindowRestorationIdentifiers {
|
||||
static let mainWindow = "mainWindow"
|
||||
@ -109,9 +109,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
|
||||
private var themeImportPath: String?
|
||||
private let secretsProvider = Secrets()
|
||||
private let accountManager: AccountManager
|
||||
private let articleThemesManager: ArticleThemesManager
|
||||
|
||||
@MainActor override init() {
|
||||
|
||||
override init() {
|
||||
NSWindow.allowsAutomaticWindowTabbing = false
|
||||
|
||||
self.accountManager = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!, secretsProvider: secretsProvider)
|
||||
AccountManager.shared = self.accountManager
|
||||
|
||||
self.articleThemesManager = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
|
||||
ArticleThemesManager.shared = self.articleThemesManager
|
||||
|
||||
super.init()
|
||||
|
||||
#if !MAC_APP_STORE
|
||||
@ -120,9 +130,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
crashReporter.enable()
|
||||
#endif
|
||||
|
||||
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!, secretsProvider: secretsProvider)
|
||||
ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil)
|
||||
@ -199,9 +206,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
if isFirstRun {
|
||||
os_log(.debug, "Is first run.")
|
||||
}
|
||||
let localAccount = AccountManager.shared.defaultAccount
|
||||
let localAccount = accountManager.defaultAccount
|
||||
|
||||
if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
|
||||
if isFirstRun && !accountManager.anyAccountHasAtLeastOneFeed() {
|
||||
// Import feeds. Either old NNW 3 feeds or the default feeds.
|
||||
if !NNW3ImportController.importSubscriptionsIfFileExists(account: localAccount) {
|
||||
DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
|
||||
@ -223,8 +230,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.unreadCount = AccountManager.shared.unreadCount
|
||||
Task { @MainActor in
|
||||
self.unreadCount = self.accountManager.unreadCount
|
||||
}
|
||||
|
||||
if InspectorWindowController.shouldOpenAtStartup {
|
||||
@ -241,7 +248,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
|
||||
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
||||
if settings.authorizationStatus == .authorized {
|
||||
DispatchQueue.main.async {
|
||||
Task { @MainActor in
|
||||
NSApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
@ -258,7 +265,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
refreshTimer!.update()
|
||||
syncTimer!.update()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
Task { @MainActor in
|
||||
self.refreshTimer!.timedRefresh(nil)
|
||||
self.syncTimer!.timedRefresh(nil)
|
||||
}
|
||||
@ -279,7 +286,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
}
|
||||
|
||||
#if !MAC_APP_STORE
|
||||
DispatchQueue.main.async {
|
||||
Task { @MainActor in
|
||||
CrashReporter.check(crashReporter: self.crashReporter)
|
||||
}
|
||||
#endif
|
||||
@ -318,7 +325,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
}
|
||||
|
||||
func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) {
|
||||
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo)
|
||||
accountManager.receiveRemoteNotification(userInfo: userInfo)
|
||||
}
|
||||
|
||||
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
|
||||
@ -333,7 +340,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
|
||||
ArticleThemeDownloader.shared.cleanUp()
|
||||
|
||||
AccountManager.shared.sendArticleStatusAll() {
|
||||
accountManager.sendArticleStatusAll() {
|
||||
self.isShutDownSyncDone = true
|
||||
}
|
||||
|
||||
@ -344,7 +351,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
// MARK: Notifications
|
||||
@objc func unreadCountDidChange(_ note: Notification) {
|
||||
if note.object is AccountManager {
|
||||
unreadCount = AccountManager.shared.unreadCount
|
||||
unreadCount = accountManager.unreadCount
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,7 +392,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
let url = userInfo["url"] as? URL else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
Task { @MainActor in
|
||||
self.importTheme(filename: url.path)
|
||||
}
|
||||
}
|
||||
@ -444,15 +451,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
let isDisplayingSheet = mainWindowController?.isDisplayingSheet ?? false
|
||||
|
||||
if item.action == #selector(refreshAll(_:)) {
|
||||
return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty
|
||||
return !accountManager.refreshInProgress && !accountManager.activeAccounts.isEmpty
|
||||
}
|
||||
|
||||
if item.action == #selector(importOPMLFromFile(_:)) {
|
||||
return AccountManager.shared.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) })
|
||||
return accountManager.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) })
|
||||
}
|
||||
|
||||
if item.action == #selector(addAppNews(_:)) {
|
||||
return !isDisplayingSheet && !AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() && !AccountManager.shared.activeAccounts.isEmpty
|
||||
return !isDisplayingSheet && !accountManager.anyAccountHasNetNewsWireNewsSubscription() && !accountManager.activeAccounts.isEmpty
|
||||
}
|
||||
|
||||
if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) {
|
||||
@ -460,7 +467,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
}
|
||||
|
||||
if item.action == #selector(showAddFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) {
|
||||
return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty
|
||||
return !isDisplayingSheet && !accountManager.activeAccounts.isEmpty
|
||||
}
|
||||
|
||||
#if !MAC_APP_STORE
|
||||
@ -474,23 +481,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
|
||||
// MARK: UNUserNotificationCenterDelegate
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
completionHandler([.banner, .badge, .sound])
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
||||
switch response.actionIdentifier {
|
||||
case "MARK_AS_READ":
|
||||
handleMarkAsRead(userInfo: userInfo)
|
||||
case "MARK_AS_STARRED":
|
||||
handleMarkAsStarred(userInfo: userInfo)
|
||||
default:
|
||||
mainWindowController?.handle(response)
|
||||
guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else {
|
||||
completionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
let actionIdentifier = response.actionIdentifier
|
||||
|
||||
Task { @MainActor in
|
||||
switch actionIdentifier {
|
||||
case "MARK_AS_READ":
|
||||
handleMarkAsRead(articlePathInfo: articlePathInfo)
|
||||
case "MARK_AS_STARRED":
|
||||
handleMarkAsStarred(articlePathInfo: articlePathInfo)
|
||||
default:
|
||||
mainWindowController?.handle(articlePathInfo: articlePathInfo)
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
// MARK: Add Feed
|
||||
@ -529,7 +544,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
}
|
||||
|
||||
@IBAction func refreshAll(_ sender: Any?) {
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present)
|
||||
accountManager.refreshAll(errorHandler: ErrorHandler.present)
|
||||
}
|
||||
|
||||
@IBAction func showAddFeedWindow(_ sender: Any?) {
|
||||
@ -603,7 +618,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
}
|
||||
|
||||
@IBAction func addAppNews(_ sender: Any?) {
|
||||
if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() {
|
||||
if accountManager.anyAccountHasNetNewsWireNewsSubscription() {
|
||||
return
|
||||
}
|
||||
addFeed(AccountManager.netNewsWireNewsURL, name: "NetNewsWire News")
|
||||
@ -700,12 +715,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
||||
extension AppDelegate {
|
||||
|
||||
@IBAction func debugSearch(_ sender: Any?) {
|
||||
AccountManager.shared.defaultAccount.debugRunSearch()
|
||||
accountManager.defaultAccount.debugRunSearch()
|
||||
}
|
||||
|
||||
@IBAction func debugDropConditionalGetInfo(_ sender: Any?) {
|
||||
#if DEBUG
|
||||
AccountManager.shared.activeAccounts.forEach{ $0.debugDropConditionalGetInfo() }
|
||||
accountManager.activeAccounts.forEach{ $0.debugDropConditionalGetInfo() }
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -817,7 +832,7 @@ internal extension AppDelegate {
|
||||
|
||||
func importTheme() {
|
||||
do {
|
||||
try ArticleThemesManager.shared.importTheme(filename: filename)
|
||||
try articleThemesManager.importTheme(filename: filename)
|
||||
confirmImportSuccess(themeName: theme.name)
|
||||
} catch {
|
||||
NSApplication.shared.presentError(error)
|
||||
@ -827,7 +842,7 @@ internal extension AppDelegate {
|
||||
alert.beginSheetModal(for: window) { result in
|
||||
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
|
||||
|
||||
if ArticleThemesManager.shared.themeExists(filename: filename) {
|
||||
if self.articleThemesManager.themeExists(filename: filename) {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
|
||||
@ -901,14 +916,14 @@ internal extension AppDelegate {
|
||||
informativeText = error.localizedDescription
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
Task { @MainActor in
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error")
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder"))
|
||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
|
||||
|
||||
|
||||
let button = alert.buttons.first
|
||||
button?.target = self
|
||||
button?.action = #selector(self.openThemesFolder(_:))
|
||||
@ -920,7 +935,7 @@ internal extension AppDelegate {
|
||||
|
||||
@objc func openThemesFolder(_ sender: Any) {
|
||||
if themeImportPath == nil {
|
||||
let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath)
|
||||
let url = URL(fileURLWithPath: articleThemesManager.folderPath)
|
||||
NSWorkspace.shared.open(url)
|
||||
} else {
|
||||
let url = URL(fileURLWithPath: themeImportPath!)
|
||||
@ -968,16 +983,15 @@ extension AppDelegate: NSWindowRestoration {
|
||||
|
||||
private extension AppDelegate {
|
||||
|
||||
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
|
||||
func handleMarkAsRead(articlePathInfo: ArticlePathInfo) {
|
||||
|
||||
guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else {
|
||||
return
|
||||
}
|
||||
guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else {
|
||||
guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else {
|
||||
os_log(.debug, "No account found from notification.")
|
||||
return
|
||||
}
|
||||
let articleID = articlePathInfo.articleID
|
||||
guard let articleID = articlePathInfo.articleID else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
guard let articles = try? await account.articles(for: .articleIDs([articleID])) else {
|
||||
@ -989,16 +1003,15 @@ private extension AppDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
|
||||
func handleMarkAsStarred(articlePathInfo: ArticlePathInfo) {
|
||||
|
||||
guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else {
|
||||
return
|
||||
}
|
||||
guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else {
|
||||
guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else {
|
||||
os_log(.debug, "No account found from notification.")
|
||||
return
|
||||
}
|
||||
let articleID = articlePathInfo.articleID
|
||||
guard let articleID = articlePathInfo.articleID else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
|
||||
|
@ -16,7 +16,7 @@ import CrashReporter
|
||||
// At some point this code should probably move into RSCore, so Rainier and any other
|
||||
// future apps can use it.
|
||||
|
||||
struct CrashReporter {
|
||||
@MainActor struct CrashReporter {
|
||||
|
||||
struct DefaultsKey {
|
||||
static let sendCrashLogsAutomaticallyKey = "SendCrashLogsAutomatically"
|
||||
|
@ -12,14 +12,14 @@ import os.log
|
||||
|
||||
struct ErrorHandler {
|
||||
|
||||
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account")
|
||||
|
||||
public static func present(_ error: Error) {
|
||||
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account")
|
||||
|
||||
@MainActor public static func present(_ error: Error) {
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
|
||||
public static func log(_ error: Error) {
|
||||
os_log(.error, log: self.log, "%@", error.localizedDescription)
|
||||
os_log(.error, log: log, "%@", error.localizedDescription)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector {
|
||||
@MainActor final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
@IBOutlet var nameTextField: NSTextField?
|
||||
@IBOutlet weak var smartFeedImageView: NSImageView!
|
||||
|
@ -11,7 +11,7 @@ import Articles
|
||||
import Account
|
||||
import UserNotifications
|
||||
|
||||
final class FeedInspectorViewController: NSViewController, Inspector {
|
||||
@MainActor final class FeedInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
@IBOutlet weak var iconView: IconView!
|
||||
@IBOutlet weak var nameTextField: NSTextField?
|
||||
|
@ -10,7 +10,7 @@ import AppKit
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
final class FolderInspectorViewController: NSViewController, Inspector {
|
||||
@MainActor final class FolderInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
@IBOutlet var nameTextField: NSTextField?
|
||||
@IBOutlet weak var folderImageView: NSImageView!
|
||||
|
@ -10,11 +10,11 @@ import AppKit
|
||||
|
||||
protocol Inspector: AnyObject {
|
||||
|
||||
var objects: [Any]? { get set }
|
||||
var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects.
|
||||
var windowTitle: String { get }
|
||||
@MainActor var objects: [Any]? { get set }
|
||||
@MainActor var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects.
|
||||
@MainActor var windowTitle: String { get }
|
||||
|
||||
func canInspect(_ objects: [Any]) -> Bool
|
||||
@MainActor func canInspect(_ objects: [Any]) -> Bool
|
||||
}
|
||||
|
||||
typealias InspectorViewController = Inspector & NSViewController
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
final class NothingInspectorViewController: NSViewController, Inspector {
|
||||
@MainActor final class NothingInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
@IBOutlet var nothingTextField: NSTextField?
|
||||
@IBOutlet var multipleTextField: NSTextField?
|
||||
|
@ -22,7 +22,7 @@ import RSParser
|
||||
// Else,
|
||||
// display error sheet.
|
||||
|
||||
class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
@MainActor final class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
|
||||
private let hostWindow: NSWindow
|
||||
private var addFeedWindowController: AddFeedWindowController?
|
||||
|
@ -11,7 +11,7 @@ import RSCore
|
||||
import RSTree
|
||||
import Account
|
||||
|
||||
class FolderTreeMenu {
|
||||
@MainActor final class FolderTreeMenu {
|
||||
|
||||
static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu {
|
||||
|
||||
|
@ -16,31 +16,32 @@ class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
|
||||
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
||||
|
||||
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
|
||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||
return
|
||||
}
|
||||
Task { @MainActor in
|
||||
|
||||
let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48))
|
||||
iconView.iconImage = iconImage
|
||||
let renderedImage = iconView.asImage()
|
||||
|
||||
guard let data = renderedImage.dataRepresentation() else {
|
||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||
return
|
||||
}
|
||||
|
||||
let headerFields = ["Cache-Control": "no-cache"]
|
||||
if let response = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: headerFields) {
|
||||
urlSchemeTask.didReceive(response)
|
||||
urlSchemeTask.didReceive(data)
|
||||
urlSchemeTask.didFinish()
|
||||
}
|
||||
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
|
||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||
return
|
||||
}
|
||||
|
||||
let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48))
|
||||
iconView.iconImage = iconImage
|
||||
let renderedImage = iconView.asImage()
|
||||
|
||||
guard let data = renderedImage.dataRepresentation() else {
|
||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||
return
|
||||
}
|
||||
|
||||
let headerFields = ["Cache-Control": "no-cache"]
|
||||
if let response = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: headerFields) {
|
||||
urlSchemeTask.didReceive(response)
|
||||
urlSchemeTask.didReceive(data)
|
||||
urlSchemeTask.didFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
|
||||
urlSchemeTask.didFailWithError(URLError(.unknown))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -216,13 +216,26 @@ final class DetailWebViewController: NSViewController {
|
||||
|
||||
extension DetailWebViewController: WKScriptMessageHandler {
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
nonisolated func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
|
||||
if message.name == MessageName.windowDidScroll {
|
||||
windowScrollY = message.body as? CGFloat
|
||||
|
||||
let updatedWindowScrollY = message.body as? CGFloat
|
||||
Task { @MainActor in
|
||||
windowScrollY = updatedWindowScrollY
|
||||
}
|
||||
|
||||
} else if message.name == MessageName.mouseDidEnter, let link = message.body as? String {
|
||||
delegate?.mouseDidEnter(self, link: link)
|
||||
|
||||
Task { @MainActor in
|
||||
delegate?.mouseDidEnter(self, link: link)
|
||||
}
|
||||
|
||||
} else if message.name == MessageName.mouseDidExit {
|
||||
delegate?.mouseDidExit(self)
|
||||
|
||||
Task { @MainActor in
|
||||
delegate?.mouseDidExit(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -239,10 +252,13 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
|
||||
|
||||
// WKNavigationDelegate
|
||||
|
||||
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
nonisolated public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
if navigationAction.navigationType == .linkActivated {
|
||||
if let url = navigationAction.request.url {
|
||||
self.openInBrowser(url, flags: navigationAction.modifierFlags)
|
||||
let flags = navigationAction.modifierFlags
|
||||
Task { @MainActor in
|
||||
self.openInBrowser(url, flags: flags)
|
||||
}
|
||||
}
|
||||
decisionHandler(.cancel)
|
||||
return
|
||||
@ -251,35 +267,42 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
// See note in viewDidLoad()
|
||||
if waitingForFirstReload {
|
||||
assert(webView.isHidden)
|
||||
waitingForFirstReload = false
|
||||
reloadHTML()
|
||||
nonisolated public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
|
||||
// Waiting for the first navigation to complete isn't long enough to avoid the flash of white.
|
||||
// A hard coded value is awful, but 5/100th of a second seems to be enough.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
webView.isHidden = false
|
||||
}
|
||||
} else {
|
||||
if let windowScrollY = windowScrollY {
|
||||
webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));")
|
||||
self.windowScrollY = nil
|
||||
Task { @MainActor in
|
||||
// See note in viewDidLoad()
|
||||
if waitingForFirstReload {
|
||||
assert(webView.isHidden)
|
||||
waitingForFirstReload = false
|
||||
reloadHTML()
|
||||
|
||||
// Waiting for the first navigation to complete isn't long enough to avoid the flash of white.
|
||||
// A hard coded value is awful, but 5/100th of a second seems to be enough.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
webView.isHidden = false
|
||||
}
|
||||
} else {
|
||||
if let windowScrollY = windowScrollY {
|
||||
_ = try? await webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));")
|
||||
self.windowScrollY = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WKUIDelegate
|
||||
|
||||
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||
nonisolated func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||
// This method is reached when WebKit handles a JavaScript based window.open() invocation, for example. One
|
||||
// example where this is used is in YouTube's embedded video player when a user clicks on the video's title
|
||||
// or on the "Watch in YouTube" button. For our purposes we'll handle such window.open calls the same way we
|
||||
// handle clicks on a URL.
|
||||
if let url = navigationAction.request.url {
|
||||
self.openInBrowser(url, flags: navigationAction.modifierFlags)
|
||||
let flags = navigationAction.modifierFlags
|
||||
|
||||
Task { @MainActor in
|
||||
self.openInBrowser(url, flags: flags)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -115,16 +115,17 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
return sidebarViewController?.selectedObjects
|
||||
}
|
||||
|
||||
func handle(_ response: UNNotificationResponse) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
||||
func handle(articlePathInfo: ArticlePathInfo) {
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: articlePathInfo)
|
||||
}
|
||||
|
||||
func handle(_ activity: NSUserActivity) {
|
||||
guard let userInfo = activity.userInfo else { return }
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
||||
|
||||
guard let userInfo = activity.userInfo, let articlePathUserInfo = ArticlePathInfo(userInfo: userInfo) else {
|
||||
return
|
||||
}
|
||||
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import AppKit
|
||||
import Account
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct NNW3ImportController {
|
||||
@MainActor struct NNW3ImportController {
|
||||
|
||||
/// Import NNW3 subscriptions if they exist.
|
||||
/// Return true if Subscriptions.plist was found and subscriptions were imported.
|
||||
|
@ -11,7 +11,7 @@ import RSCore
|
||||
|
||||
// image - title - unreadCount
|
||||
|
||||
struct SidebarCellLayout {
|
||||
@MainActor struct SidebarCellLayout {
|
||||
|
||||
let faviconRect: CGRect
|
||||
let titleRect: CGRect
|
||||
|
@ -10,7 +10,7 @@ import AppKit
|
||||
import RSTree
|
||||
import Account
|
||||
|
||||
enum SidebarDeleteItemsAlert {
|
||||
@MainActor struct SidebarDeleteItemsAlert {
|
||||
|
||||
/// Builds a delete confirmation dialog for the supplied nodes
|
||||
static func build(_ nodes: [Node]) -> NSAlert {
|
||||
|
@ -12,7 +12,7 @@ import Articles
|
||||
import RSCore
|
||||
import Account
|
||||
|
||||
@objc final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource {
|
||||
@objc @MainActor final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource {
|
||||
|
||||
let treeController: TreeController
|
||||
static let dragOperationNone = NSDragOperation(rawValue: 0)
|
||||
|
@ -17,13 +17,13 @@ extension Notification.Name {
|
||||
}
|
||||
|
||||
protocol SidebarDelegate: AnyObject {
|
||||
func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
|
||||
func unreadCount(for: AnyObject) -> Int
|
||||
func sidebarInvalidatedRestorationState(_: SidebarViewController)
|
||||
@MainActor func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
|
||||
@MainActor func unreadCount(for: AnyObject) -> Int
|
||||
@MainActor func sidebarInvalidatedRestorationState(_: SidebarViewController)
|
||||
}
|
||||
|
||||
@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
|
||||
|
||||
@objc @MainActor class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
|
||||
|
||||
@IBOutlet weak var outlineView: NSOutlineView!
|
||||
|
||||
weak var delegate: SidebarDelegate?
|
||||
@ -483,9 +483,9 @@ protocol SidebarDelegate: AnyObject {
|
||||
revealAndSelectRepresentedObject(feed as AnyObject)
|
||||
}
|
||||
|
||||
func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) {
|
||||
guard let accountNode = findAccountNode(userInfo),
|
||||
let feedNode = findFeedNode(userInfo, beginningAt: accountNode),
|
||||
func deepLinkRevealAndSelect(for articlePathInfo: ArticlePathInfo) {
|
||||
guard let accountNode = findAccountNode(articlePathInfo),
|
||||
let feedNode = findFeedNode(articlePathInfo, beginningAt: accountNode),
|
||||
let feed = feedNode.representedObject as? SidebarItem else {
|
||||
return
|
||||
}
|
||||
@ -738,16 +738,17 @@ private extension SidebarViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? {
|
||||
guard let accountID = userInfo?[ArticlePathKey.accountID] as? String else {
|
||||
func findAccountNode(_ articlePathInfo: ArticlePathInfo) -> Node? {
|
||||
|
||||
guard let accountID = articlePathInfo.accountID else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
|
||||
return node
|
||||
}
|
||||
|
||||
guard let accountName = userInfo?[ArticlePathKey.accountName] as? String else {
|
||||
guard let accountName = articlePathInfo.accountName else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -758,8 +759,8 @@ private extension SidebarViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? {
|
||||
guard let feedID = userInfo?[ArticlePathKey.feedID] as? String else {
|
||||
func findFeedNode(_ articlePathInfo: ArticlePathInfo, beginningAt startingNode: Node) -> Node? {
|
||||
guard let feedID = articlePathInfo.feedID else {
|
||||
return nil
|
||||
}
|
||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) {
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
class UnreadCountView : NSView {
|
||||
@MainActor final class UnreadCountView : NSView {
|
||||
|
||||
struct Appearance {
|
||||
static let padding = NSEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0)
|
||||
|
@ -11,12 +11,13 @@ import Articles
|
||||
import RSCore
|
||||
|
||||
extension Article: PasteboardWriterOwner {
|
||||
public var pasteboardWriter: NSPasteboardWriting {
|
||||
|
||||
@MainActor public var pasteboardWriter: NSPasteboardWriting {
|
||||
return ArticlePasteboardWriter(article: self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
@objc @MainActor final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
let article: Article
|
||||
static let articleUTI = "com.ranchero.article"
|
||||
|
@ -26,7 +26,7 @@ struct TextFieldSizeInfo {
|
||||
let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then.
|
||||
}
|
||||
|
||||
final class MultilineTextFieldSizer {
|
||||
@MainActor final class MultilineTextFieldSizer {
|
||||
|
||||
private let numberOfLines: Int
|
||||
private let font: NSFont
|
||||
@ -35,7 +35,7 @@ final class MultilineTextFieldSizer {
|
||||
private let doubleLineHeightEstimate: Int
|
||||
private var cache = [String: WidthHeightCache]() // Each string has a cache.
|
||||
private var attributedCache = [NSAttributedString: WidthHeightCache]()
|
||||
private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
||||
@MainActor private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
||||
|
||||
private init(numberOfLines: Int, font: NSFont) {
|
||||
|
||||
|
@ -12,7 +12,7 @@ import AppKit
|
||||
// Uses a cache.
|
||||
// Main thready only.
|
||||
|
||||
final class SingleLineTextFieldSizer {
|
||||
@MainActor final class SingleLineTextFieldSizer {
|
||||
|
||||
let font: NSFont
|
||||
private let textField: NSTextField
|
||||
|
@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import Articles
|
||||
|
||||
struct TimelineCellData {
|
||||
@MainActor struct TimelineCellData {
|
||||
|
||||
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import RSCore
|
||||
|
||||
struct TimelineCellLayout {
|
||||
@MainActor struct TimelineCellLayout {
|
||||
|
||||
let width: CGFloat
|
||||
let height: CGFloat
|
||||
|
@ -11,10 +11,10 @@ import Account
|
||||
import Articles
|
||||
|
||||
protocol TimelineContainerViewControllerDelegate: AnyObject {
|
||||
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
|
||||
func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed)
|
||||
func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
|
||||
|
||||
|
||||
@MainActor func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
|
||||
@MainActor func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed)
|
||||
@MainActor func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
|
||||
}
|
||||
|
||||
final class TimelineContainerViewController: NSViewController {
|
||||
|
@ -13,9 +13,10 @@ import Account
|
||||
import os.log
|
||||
|
||||
protocol TimelineDelegate: AnyObject {
|
||||
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
|
||||
func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
|
||||
func timelineInvalidatedRestorationState(_: TimelineViewController)
|
||||
|
||||
@MainActor func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
|
||||
@MainActor func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
|
||||
@MainActor func timelineInvalidatedRestorationState(_: TimelineViewController)
|
||||
}
|
||||
|
||||
enum TimelineShowFeedName {
|
||||
@ -535,12 +536,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
func goToDeepLink(for userInfo: [AnyHashable : Any]) {
|
||||
guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return }
|
||||
func goToDeepLink(for articlePathInfo: ArticlePathInfo) {
|
||||
|
||||
guard let articleID = articlePathInfo.articleID else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
if isReadFiltered ?? false {
|
||||
if let accountName = userInfo[ArticlePathKey.accountName] as? String,
|
||||
if let accountName = articlePathInfo.accountName,
|
||||
let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) {
|
||||
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
|
||||
await fetchAndReplaceArticles()
|
||||
|
@ -161,7 +161,7 @@ struct AddAccountsView: View {
|
||||
|
||||
}
|
||||
|
||||
var icloudAccount: some View {
|
||||
@MainActor var icloudAccount: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("iCloud")
|
||||
.font(.headline)
|
||||
@ -260,7 +260,7 @@ struct AddAccountsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func isCloudInUse() -> Bool {
|
||||
@MainActor private func isCloudInUse() -> Bool {
|
||||
AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit })
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ extension AppDelegate : AppDelegateAppleEvents {
|
||||
}
|
||||
|
||||
class NetNewsWireCreateElementCommand : NSCreateCommand {
|
||||
override func performDefaultImplementation() -> Any? {
|
||||
@MainActor override func performDefaultImplementation() -> Any? {
|
||||
let classDescription = self.createClassDescription
|
||||
if (classDescription.className == "feed") {
|
||||
return ScriptableFeed.handleCreateElement(command:self)
|
||||
|
@ -111,7 +111,9 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
return article.status.boolStatus(forKey:.read)
|
||||
}
|
||||
set {
|
||||
markArticles([self.article], statusKey: .read, flag: newValue)
|
||||
Task { @MainActor in
|
||||
markArticles([self.article], statusKey: .read, flag: newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +123,9 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
return article.status.boolStatus(forKey:.starred)
|
||||
}
|
||||
set {
|
||||
markArticles([self.article], statusKey: .starred, flag: newValue)
|
||||
Task { @MainActor in
|
||||
markArticles([self.article], statusKey: .starred, flag: newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,7 +146,7 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
}
|
||||
|
||||
@objc(feed)
|
||||
var feed: ScriptableFeed? {
|
||||
@MainActor var feed: ScriptableFeed? {
|
||||
guard let parentFeed = self.article.feed,
|
||||
let account = parentFeed.account
|
||||
else { return nil }
|
||||
|
@ -81,7 +81,7 @@ import Articles
|
||||
}
|
||||
}
|
||||
|
||||
class func handleCreateElement(command:NSCreateCommand) -> Any? {
|
||||
@MainActor class func handleCreateElement(command:NSCreateCommand) -> Any? {
|
||||
guard command.isCreateCommand(forClass:"Feed") else { return nil }
|
||||
guard let arguments = command.arguments else {return nil}
|
||||
let titleFromArgs = command.property(forKey:"name") as? String
|
||||
|
@ -65,7 +65,7 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
|
||||
or
|
||||
tell account X to make new folder at end with properties {name:"new folder name"}
|
||||
*/
|
||||
class func handleCreateElement(command:NSCreateCommand) -> Any? {
|
||||
@MainActor class func handleCreateElement(command:NSCreateCommand) -> Any? {
|
||||
guard command.isCreateCommand(forClass:"fold") else { return nil }
|
||||
let name = command.property(forKey:"name") as? String ?? ""
|
||||
|
||||
|
@ -26,7 +26,7 @@ extension NSScriptCommand {
|
||||
return true
|
||||
}
|
||||
|
||||
func accountAndFolderForNewChild() -> (Account, Folder?) {
|
||||
@MainActor func accountAndFolderForNewChild() -> (Account, Folder?) {
|
||||
let appleEvent = self.appleEvent
|
||||
var account = AccountManager.shared.defaultAccount
|
||||
var folder:Folder? = nil
|
||||
|
@ -20,8 +20,8 @@ import UniformTypeIdentifiers
|
||||
import CoreSpotlight
|
||||
#endif
|
||||
|
||||
class ActivityManager {
|
||||
|
||||
@MainActor final class ActivityManager {
|
||||
|
||||
private var nextUnreadActivity: NSUserActivity?
|
||||
private var selectingActivity: NSUserActivity?
|
||||
private var readingActivity: NSUserActivity?
|
||||
@ -264,8 +264,8 @@ private extension ActivityManager {
|
||||
return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? []
|
||||
}
|
||||
|
||||
func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
|
||||
|
||||
@MainActor func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
|
||||
|
||||
let attributeSet = CSSearchableItemAttributeSet(contentType: UTType.item)
|
||||
attributeSet.title = feed.nameForDisplay
|
||||
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
|
||||
|
@ -19,11 +19,12 @@ public enum ArticleExtractorState {
|
||||
}
|
||||
|
||||
protocol ArticleExtractorDelegate {
|
||||
func articleExtractionDidFail(with: Error)
|
||||
func articleExtractionDidComplete(extractedArticle: ExtractedArticle)
|
||||
|
||||
@MainActor func articleExtractionDidFail(with: Error)
|
||||
@MainActor func articleExtractionDidComplete(extractedArticle: ExtractedArticle)
|
||||
}
|
||||
|
||||
class ArticleExtractor {
|
||||
final class ArticleExtractor {
|
||||
|
||||
private var dataTask: URLSessionDataTask? = nil
|
||||
|
||||
|
@ -14,7 +14,7 @@ import RSCore
|
||||
import Articles
|
||||
import Account
|
||||
|
||||
struct ArticleRenderer {
|
||||
@MainActor struct ArticleRenderer {
|
||||
|
||||
typealias Rendering = (style: String, html: String, title: String, baseURL: String)
|
||||
|
||||
@ -30,10 +30,10 @@ struct ArticleRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
static var imageIconScheme = "nnwImageIcon"
|
||||
|
||||
static var blank = Page(name: "blank")
|
||||
static var page = Page(name: "page")
|
||||
static let imageIconScheme = "nnwImageIcon"
|
||||
|
||||
static let blank = Page(name: "blank")
|
||||
static let page = Page(name: "page")
|
||||
|
||||
private let article: Article?
|
||||
private let extractedArticle: ExtractedArticle?
|
||||
@ -328,7 +328,7 @@ private extension ArticleRenderer {
|
||||
|
||||
// MARK: - Article extension
|
||||
|
||||
private extension Article {
|
||||
@MainActor private extension Article {
|
||||
|
||||
var baseURL: URL? {
|
||||
var s = link
|
||||
|
@ -10,22 +10,20 @@ import Foundation
|
||||
|
||||
struct ArticlePathInfo {
|
||||
|
||||
let accountID: String
|
||||
let articleID: String
|
||||
let accountID: String?
|
||||
let accountName: String?
|
||||
let articleID: String?
|
||||
let feedID: String?
|
||||
|
||||
init?(userInfo: [AnyHashable: Any]) {
|
||||
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [String: String] else {
|
||||
return nil
|
||||
}
|
||||
guard let accountID = articlePathUserInfo[ArticlePathKey.accountID] else {
|
||||
return nil
|
||||
}
|
||||
guard let articleID = articlePathUserInfo[ArticlePathKey.articleID] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.accountID = accountID
|
||||
self.articleID = articleID
|
||||
self.accountID = articlePathUserInfo[ArticlePathKey.accountID]
|
||||
self.accountName = articlePathUserInfo[ArticlePathKey.accountName]
|
||||
self.articleID = articlePathUserInfo[ArticlePathKey.articleID]
|
||||
self.feedID = articlePathUserInfo[ArticlePathKey.feedID]
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import Articles
|
||||
|
||||
// Mark articles read/unread, starred/unstarred, deleted/undeleted.
|
||||
|
||||
final class MarkStatusCommand: UndoableCommand {
|
||||
@MainActor final class MarkStatusCommand: UndoableCommand {
|
||||
|
||||
let undoActionName: String
|
||||
let redoActionName: String
|
||||
@ -50,12 +50,12 @@ final class MarkStatusCommand: UndoableCommand {
|
||||
self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, undoManager: undoManager, completion: completion)
|
||||
}
|
||||
|
||||
func perform() {
|
||||
@MainActor func perform() {
|
||||
mark(statusKey, flag)
|
||||
registerUndo()
|
||||
}
|
||||
|
||||
func undo() {
|
||||
@MainActor func undo() {
|
||||
mark(statusKey, !flag)
|
||||
registerRedo()
|
||||
}
|
||||
@ -63,7 +63,7 @@ final class MarkStatusCommand: UndoableCommand {
|
||||
|
||||
private extension MarkStatusCommand {
|
||||
|
||||
func mark(_ statusKey: ArticleStatus.Key, _ flag: Bool) {
|
||||
@MainActor func mark(_ statusKey: ArticleStatus.Key, _ flag: Bool) {
|
||||
markArticles(articles, statusKey: statusKey, flag: flag, completion: completion)
|
||||
completion = nil
|
||||
}
|
||||
@ -83,7 +83,7 @@ private extension MarkStatusCommand {
|
||||
}
|
||||
}
|
||||
|
||||
static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] {
|
||||
@MainActor static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] {
|
||||
|
||||
return articles.filter{ article in
|
||||
guard article.status.boolStatus(forKey: statusKey) != flag else { return false }
|
||||
|
@ -21,7 +21,7 @@ final class SendToMarsEditCommand: SendToCommand {
|
||||
appToUse() != nil
|
||||
}
|
||||
|
||||
func sendObject(_ object: Any?, selectedText: String?) {
|
||||
@MainActor func sendObject(_ object: Any?, selectedText: String?) {
|
||||
|
||||
guard canSendObject(object, selectedText: selectedText) else {
|
||||
return
|
||||
@ -39,7 +39,7 @@ final class SendToMarsEditCommand: SendToCommand {
|
||||
|
||||
private extension SendToMarsEditCommand {
|
||||
|
||||
func send(_ article: Article, to app: UserApp) {
|
||||
@MainActor func send(_ article: Article, to app: UserApp) {
|
||||
|
||||
// App has already been launched.
|
||||
|
||||
|
@ -29,7 +29,7 @@ final class SendToMicroBlogCommand: SendToCommand {
|
||||
return true
|
||||
}
|
||||
|
||||
func sendObject(_ object: Any?, selectedText: String?) {
|
||||
@MainActor func sendObject(_ object: Any?, selectedText: String?) {
|
||||
|
||||
guard canSendObject(object, selectedText: selectedText) else {
|
||||
return
|
||||
@ -60,7 +60,7 @@ final class SendToMicroBlogCommand: SendToCommand {
|
||||
|
||||
private extension Article {
|
||||
|
||||
var attributionString: String {
|
||||
@MainActor var attributionString: String {
|
||||
|
||||
// Feed name, or feed name + author name (if author is specified per-article).
|
||||
// Includes trailing space.
|
||||
|
@ -11,7 +11,7 @@ import Account
|
||||
|
||||
struct AddFeedDefaultContainer {
|
||||
|
||||
static var defaultContainer: Container? {
|
||||
@MainActor static var defaultContainer: Container? {
|
||||
|
||||
if let accountID = AppDefaults.shared.addFeedAccountID, let account = AccountManager.shared.activeAccounts.first(where: { $0.accountID == accountID }) {
|
||||
if let folderName = AppDefaults.shared.addFeedFolderName, let folder = account.existingFolder(withDisplayName: folderName) {
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
||||
import Articles
|
||||
import RSParser
|
||||
|
||||
struct ArticleStringFormatter {
|
||||
@MainActor struct ArticleStringFormatter {
|
||||
|
||||
private static var feedNameCache = [String: String]()
|
||||
private static var titleCache = [String: String]()
|
||||
|
@ -13,7 +13,7 @@ import Account
|
||||
|
||||
// These handle multiple accounts.
|
||||
|
||||
func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
|
||||
@MainActor func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
|
||||
|
||||
let d: [String: Set<Article>] = accountAndArticlesDictionary(articles)
|
||||
|
||||
@ -42,7 +42,7 @@ private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String:
|
||||
|
||||
extension Article {
|
||||
|
||||
var feed: Feed? {
|
||||
@MainActor var feed: Feed? {
|
||||
return account?.existingFeed(withFeedID: feedID)
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ extension Article {
|
||||
return datePublished ?? dateModified ?? status.dateArrived
|
||||
}
|
||||
|
||||
var isAvailableToMarkUnread: Bool {
|
||||
@MainActor var isAvailableToMarkUnread: Bool {
|
||||
guard let markUnreadWindow = account?.behaviors.compactMap( { behavior -> Int? in
|
||||
switch behavior {
|
||||
case .disallowMarkAsUnreadAfterPeriod(let days):
|
||||
@ -117,11 +117,11 @@ extension Article {
|
||||
}
|
||||
}
|
||||
|
||||
func iconImage() -> IconImage? {
|
||||
@MainActor func iconImage() -> IconImage? {
|
||||
return IconImageCache.shared.imageForArticle(self)
|
||||
}
|
||||
|
||||
func iconImageUrl(feed: Feed) -> URL? {
|
||||
@MainActor func iconImageUrl(feed: Feed) -> URL? {
|
||||
if let image = iconImage() {
|
||||
let fm = FileManager.default
|
||||
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
@ -138,7 +138,7 @@ extension Article {
|
||||
}
|
||||
}
|
||||
|
||||
func byline() -> String {
|
||||
@MainActor func byline() -> String {
|
||||
guard let authors = authors ?? feed?.authors, !authors.isEmpty else {
|
||||
return ""
|
||||
}
|
||||
@ -199,7 +199,7 @@ struct ArticlePathKey {
|
||||
|
||||
extension Article {
|
||||
|
||||
public var pathUserInfo: [AnyHashable : Any] {
|
||||
@MainActor public var pathUserInfo: [AnyHashable : Any] {
|
||||
return [
|
||||
ArticlePathKey.accountID: accountID,
|
||||
ArticlePathKey.accountName: account?.nameForDisplay ?? "",
|
||||
@ -214,7 +214,7 @@ extension Article {
|
||||
|
||||
extension Article: SortableArticle {
|
||||
|
||||
var sortableName: String {
|
||||
@MainActor var sortableName: String {
|
||||
return feed?.name ?? ""
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ extension Account: SmallIconProvider {
|
||||
|
||||
extension Feed: SmallIconProvider {
|
||||
|
||||
var smallIcon: IconImage? {
|
||||
@MainActor var smallIcon: IconImage? {
|
||||
if let iconImage = appDelegate.faviconDownloader.favicon(for: self) {
|
||||
return iconImage
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
||||
import RSCore
|
||||
import Account
|
||||
|
||||
final class FaviconGenerator {
|
||||
@MainActor final class FaviconGenerator {
|
||||
|
||||
private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage
|
||||
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
||||
import Account
|
||||
import Articles
|
||||
|
||||
final class IconImageCache {
|
||||
@MainActor final class IconImageCache {
|
||||
|
||||
static let shared = IconImageCache()
|
||||
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
struct DefaultFeedsImporter {
|
||||
@MainActor struct DefaultFeedsImporter {
|
||||
|
||||
static func importDefaultFeeds(account: Account) {
|
||||
let defaultFeedsURL = Bundle.main.url(forResource: "DefaultFeeds", withExtension: "opml")!
|
||||
|
@ -14,9 +14,9 @@ import Account
|
||||
|
||||
final class ExtensionContainersFile {
|
||||
|
||||
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile")
|
||||
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile")
|
||||
|
||||
private static var filePath: String = {
|
||||
private static let filePath: String = {
|
||||
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
|
||||
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
||||
return containerURL!.appendingPathComponent("extension_containers.plist").path
|
||||
|
@ -10,8 +10,8 @@ import Foundation
|
||||
import os.log
|
||||
import Account
|
||||
|
||||
final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
|
||||
|
||||
final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter, Sendable {
|
||||
|
||||
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
|
||||
|
||||
private static let filePath: String = {
|
||||
@ -30,7 +30,7 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
|
||||
return operationQueue
|
||||
}
|
||||
|
||||
override init() {
|
||||
@MainActor override init() {
|
||||
operationQueue = OperationQueue()
|
||||
operationQueue.maxConcurrentOperationCount = 1
|
||||
|
||||
@ -41,14 +41,16 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
|
||||
}
|
||||
|
||||
func presentedItemDidChange() {
|
||||
DispatchQueue.main.async {
|
||||
Task { @MainActor in
|
||||
self.process()
|
||||
}
|
||||
}
|
||||
|
||||
func resume() {
|
||||
NSFileCoordinator.addFilePresenter(self)
|
||||
process()
|
||||
Task { @MainActor in
|
||||
process()
|
||||
}
|
||||
}
|
||||
|
||||
func suspend() {
|
||||
@ -95,8 +97,8 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
|
||||
|
||||
private extension ExtensionFeedAddRequestFile {
|
||||
|
||||
func process() {
|
||||
|
||||
@MainActor func process() {
|
||||
|
||||
let decoder = PropertyListDecoder()
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .binary
|
||||
@ -130,7 +132,7 @@ private extension ExtensionFeedAddRequestFile {
|
||||
requests?.forEach { processRequest($0) }
|
||||
}
|
||||
|
||||
func processRequest(_ request: ExtensionFeedAddRequest) {
|
||||
@MainActor func processRequest(_ request: ExtensionFeedAddRequest) {
|
||||
var destinationAccountID: String? = nil
|
||||
switch request.destinationContainerID {
|
||||
case .account(let accountID):
|
||||
|
@ -62,7 +62,7 @@ final class SmartFeed: PseudoFeed {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func fetchUnreadCounts() {
|
||||
@objc @MainActor func fetchUnreadCounts() {
|
||||
let activeAccounts = AccountManager.shared.activeAccounts
|
||||
|
||||
// Remove any accounts that are no longer active or have been deleted
|
||||
@ -113,15 +113,18 @@ private extension SmartFeed {
|
||||
|
||||
func fetchUnreadCount(for account: Account) {
|
||||
delegate.fetchUnreadCount(for: account) { singleUnreadCountResult in
|
||||
guard let accountUnreadCount = try? singleUnreadCountResult.get() else {
|
||||
return
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
guard let accountUnreadCount = try? singleUnreadCountResult.get() else {
|
||||
return
|
||||
}
|
||||
self.unreadCounts[account.accountID] = accountUnreadCount
|
||||
self.updateUnreadCount()
|
||||
}
|
||||
self.unreadCounts[account.accountID] = accountUnreadCount
|
||||
self.updateUnreadCount()
|
||||
}
|
||||
}
|
||||
|
||||
func updateUnreadCount() {
|
||||
@MainActor func updateUnreadCount() {
|
||||
unreadCount = AccountManager.shared.activeAccounts.reduce(0) { (result, account) -> Int in
|
||||
if let oneUnreadCount = unreadCounts[account.accountID] {
|
||||
return result + oneUnreadCount
|
||||
|
@ -51,13 +51,13 @@ final class UnreadFeed: PseudoFeed {
|
||||
}
|
||||
#endif
|
||||
|
||||
init() {
|
||||
@MainActor init() {
|
||||
|
||||
self.unreadCount = appDelegate.unreadCount
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: appDelegate)
|
||||
}
|
||||
|
||||
@objc func unreadCountDidChange(_ note: Notification) {
|
||||
@objc @MainActor func unreadCountDidChange(_ note: Notification) {
|
||||
|
||||
assert(note.object is AppDelegate)
|
||||
unreadCount = appDelegate.unreadCount
|
||||
|
@ -67,7 +67,7 @@ extension Array where Element == Article {
|
||||
return false
|
||||
}
|
||||
|
||||
func anyArticleIsReadAndCanMarkUnread() -> Bool {
|
||||
@MainActor func anyArticleIsReadAndCanMarkUnread() -> Bool {
|
||||
return anyArticlePassesTest { $0.status.read && $0.isAvailableToMarkUnread }
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ extension Array where Element == Article {
|
||||
var i = 0
|
||||
for article in self {
|
||||
let otherArticle = otherArticles[i]
|
||||
if article.account != otherArticle.account || article.articleID != otherArticle.articleID {
|
||||
if article.accountID != otherArticle.accountID || article.articleID != otherArticle.articleID {
|
||||
return false
|
||||
}
|
||||
i += 1
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import Account
|
||||
|
||||
class AccountRefreshTimer {
|
||||
@MainActor final class AccountRefreshTimer {
|
||||
|
||||
var shuttingDown = false
|
||||
|
||||
@ -64,7 +64,7 @@ class AccountRefreshTimer {
|
||||
|
||||
}
|
||||
|
||||
@objc func timedRefresh(_ sender: Timer?) {
|
||||
@objc @MainActor func timedRefresh(_ sender: Timer?) {
|
||||
|
||||
guard !shuttingDown else {
|
||||
return
|
||||
|
@ -9,8 +9,8 @@
|
||||
import Foundation
|
||||
import Account
|
||||
|
||||
class ArticleStatusSyncTimer {
|
||||
|
||||
@MainActor final class ArticleStatusSyncTimer {
|
||||
|
||||
private static let intervalSeconds = Double(120)
|
||||
|
||||
var shuttingDown = false
|
||||
|
@ -24,7 +24,7 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate {
|
||||
filterExceptions = Set<SidebarItemIdentifier>()
|
||||
}
|
||||
|
||||
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
||||
@MainActor func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
||||
if node.isRoot {
|
||||
return childNodesForRootNode(node)
|
||||
}
|
||||
@ -41,7 +41,7 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate {
|
||||
|
||||
private extension FeedTreeControllerDelegate {
|
||||
|
||||
func childNodesForRootNode(_ rootNode: Node) -> [Node]? {
|
||||
@MainActor func childNodesForRootNode(_ rootNode: Node) -> [Node]? {
|
||||
var topLevelNodes = [Node]()
|
||||
|
||||
let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared)
|
||||
@ -132,7 +132,7 @@ private extension FeedTreeControllerDelegate {
|
||||
return node
|
||||
}
|
||||
|
||||
func sortedAccountNodes(_ parent: Node) -> [Node] {
|
||||
@MainActor func sortedAccountNodes(_ parent: Node) -> [Node] {
|
||||
let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in
|
||||
let accountNode = parent.existingOrNewChildNode(with: account)
|
||||
accountNode.canHaveChildNodes = true
|
||||
|
@ -14,7 +14,7 @@ import Account
|
||||
|
||||
final class FolderTreeControllerDelegate: TreeControllerDelegate {
|
||||
|
||||
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
||||
@MainActor func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
||||
|
||||
return node.isRoot ? childNodesForRootNode(node) : childNodes(node)
|
||||
}
|
||||
@ -22,8 +22,8 @@ final class FolderTreeControllerDelegate: TreeControllerDelegate {
|
||||
|
||||
private extension FolderTreeControllerDelegate {
|
||||
|
||||
func childNodesForRootNode(_ node: Node) -> [Node]? {
|
||||
|
||||
@MainActor func childNodesForRootNode(_ node: Node) -> [Node]? {
|
||||
|
||||
let accountNodes: [Node] = AccountManager.shared.sortedActiveAccounts.map { account in
|
||||
let accountNode = Node(representedObject: account, parent: node)
|
||||
accountNode.canHaveChildNodes = true
|
||||
|
@ -25,9 +25,11 @@ final class UserNotificationManager: NSObject {
|
||||
return
|
||||
}
|
||||
|
||||
for article in articles {
|
||||
if !article.status.read, let feed = article.feed, feed.isNotifyAboutNewArticles ?? false {
|
||||
sendNotification(feed: feed, article: article)
|
||||
Task { @MainActor in
|
||||
for article in articles {
|
||||
if !article.status.read, let feed = article.feed, feed.isNotifyAboutNewArticles ?? false {
|
||||
sendNotification(feed: feed, article: article)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -53,7 +55,7 @@ final class UserNotificationManager: NSObject {
|
||||
|
||||
private extension UserNotificationManager {
|
||||
|
||||
func sendNotification(feed: Feed, article: Article) {
|
||||
@MainActor func sendNotification(feed: Feed, article: Article) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = feed.nameForDisplay
|
||||
@ -79,7 +81,7 @@ private extension UserNotificationManager {
|
||||
/// - feed: `Feed`
|
||||
/// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil.
|
||||
/// - Warning: In certain scenarios, this will return the `faviconTemplateImage`.
|
||||
func thumbnailAttachment(for article: Article, feed: Feed) -> UNNotificationAttachment? {
|
||||
@MainActor func thumbnailAttachment(for article: Article, feed: Feed) -> UNNotificationAttachment? {
|
||||
if let imageURL = article.iconImageUrl(feed: feed) {
|
||||
let thumbnail = try? UNNotificationAttachment(identifier: feed.feedID, url: imageURL, options: nil)
|
||||
return thumbnail
|
||||
|
Loading…
x
Reference in New Issue
Block a user