diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 5b8fcf5ae..1dd5ff8e1 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -321,12 +321,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, feedMetadataFile.load() opmlFile.load() - Task { + Task { @MainActor in try? await self.database.cleanupDatabaseAtStartup(subscribedToFeedIDs: self.flattenedFeeds().feedIDs()) - - Task { @MainActor in - self.fetchAllUnreadCounts() - } + self.fetchAllUnreadCounts() } self.delegate.accountDidInitialize(self) @@ -645,6 +642,41 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, fetchUnreadCounts(for: feeds, completion: completion) } + public func articles(for fetchType: FetchType) async throws -> Set
{ + + switch fetchType { + + case .starred(let limit): + return try await starredArticles(limit: limit) + + case .unread(let limit): + return try await unreadArticles(limit: limit) + + case .today(let limit): + return try await todayArticles(limit: limit) + + case .folder(let folder, let readFilter): + if readFilter { + return try await unreadArticles(folder: folder) + } else { + return try await articles(folder: folder) + } + + case .feed(let feed): + return try await articles(feed: feed) + + case .articleIDs(let articleIDs): + return try await articles(articleIDs: articleIDs) + + case .search(let searchString): + return try await articlesMatching(searchString: searchString) + + case .searchWithArticleIDs(let searchString, let articleIDs): + return try await articlesMatching(searchString: searchString, articleIDs: articleIDs) + } + + } + public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) { switch fetchType { case .starred(let limit): @@ -670,6 +702,43 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } + public func articles(feed: Feed) async throws -> Set
{ + + let articles = try await database.articles(feedID: feed.feedID) + validateUnreadCount(feed, articles) + return articles + } + + public func articles(articleIDs: Set) async throws -> Set
{ + + try await database.articles(articleIDs: articleIDs) + } + + public func unreadArticles(feed: Feed) async throws -> Set
{ + + try await database.unreadArticles(feedIDs: Set([feed.feedID])) + } + + public func unreadArticles(feeds: Set) async throws -> Set
{ + + if feeds.isEmpty { + return Set
() + } + + let feedIDs = feeds.feedIDs() + let articles = try await database.unreadArticles(feedIDs: feedIDs) + + validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + + return articles + } + + public func unreadArticles(folder: Folder) async throws -> Set
{ + + let feeds = folder.flattenedFeeds() + return try await unreadArticles(feeds: feeds) + } + public func fetchUnreadCountForToday(_ completion: @escaping SingleUnreadCountCompletionBlock) { database.fetchUnreadCountForToday(for: flattenedFeeds().feedIDs(), completion: completion) } @@ -911,7 +980,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, #if DEBUG Task { let t1 = Date() - let articles = try! await fetchArticlesMatching("Brent NetNewsWire") + let articles = try! await articlesMatching(searchString: "Brent NetNewsWire") let t2 = Date() print(t2.timeIntervalSince(t1)) print(articles.count) @@ -999,23 +1068,48 @@ extension Account: FeedMetadataDelegate { private extension Account { + func starredArticles(limit: Int? = nil) async throws -> Set
{ + + try await database.starredArticles(feedIDs: allFeedIDs(), limit: limit) + } + func fetchStarredArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { - database.fetchedStarredArticlesAsync(flattenedFeeds().feedIDs(), limit, completion) + + database.fetchedStarredArticlesAsync(allFeedIDs(), limit, completion) + } + + func unreadArticles(limit: Int? = nil) async throws -> Set
{ + + try await unreadArticles(container: self) } func fetchUnreadArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + fetchUnreadArticlesAsync(forContainer: self, limit: limit, completion) } + func todayArticles(limit: Int? = nil) async throws -> Set
{ + + try await database.todayArticles(feedIDs: allFeedIDs(), limit: limit) + } + func fetchTodayArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { - database.fetchTodayArticlesAsync(flattenedFeeds().feedIDs(), limit, completion) + + database.fetchTodayArticlesAsync(allFeedIDs(), limit, completion) + } + + func articles(folder: Folder) async throws -> Set
{ + + try await articles(container: folder) } func fetchArticlesAsync(folder: Folder, _ completion: @escaping ArticleSetResultBlock) { + fetchArticlesAsync(forContainer: folder, completion) } func fetchUnreadArticlesAsync(folder: Folder, _ completion: @escaping ArticleSetResultBlock) { + fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion) } @@ -1031,24 +1125,41 @@ private extension Account { } } - func fetchArticlesMatching(_ searchString: String) async throws -> Set
{ + func articlesMatching(searchString: String) async throws -> Set
{ - let feedIDs = flattenedFeeds().feedIDs() - return try await database.articlesMatching(searchString: searchString, feedIDs: feedIDs) + try await database.articlesMatching(searchString: searchString, feedIDs: allFeedIDs()) } func fetchArticlesMatchingAsync(_ searchString: String, _ completion: @escaping ArticleSetResultBlock) { + database.fetchArticlesMatchingAsync(searchString, flattenedFeeds().feedIDs(), completion) } + func articlesMatching(searchString: String, articleIDs: Set) async throws -> Set
{ + + try await database.articlesMatching(searchString: searchString, articleIDs: articleIDs) + } + func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set, _ completion: @escaping ArticleSetResultBlock) { + database.fetchArticlesMatchingWithArticleIDsAsync(searchString, articleIDs, completion) } func fetchArticlesAsync(articleIDs: Set, _ completion: @escaping ArticleSetResultBlock) { + return database.fetchArticlesAsync(articleIDs: articleIDs, completion) } + func articles(container: Container) async throws -> Set
{ + + let feeds = container.flattenedFeeds() + let articles = try await database.articles(feedIDs: allFeedIDs()) + + validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + + return articles + } + func fetchArticlesAsync(forContainer container: Container, _ completion: @escaping ArticleSetResultBlock) { let feeds = container.flattenedFeeds() database.fetchArticlesAsync(feeds.feedIDs()) { [weak self] (articleSetResult) in @@ -1062,6 +1173,21 @@ private extension Account { } } + func unreadArticles(container: Container, limit: Int? = nil) async throws -> Set
{ + + let feeds = container.flattenedFeeds() + let feedIDs = feeds.feedIDs() + let articles = try await database.unreadArticles(feedIDs: feedIDs, limit: limit) + + // We don't validate limit queries because they, by definition, won't correctly match the + // complete unread state for the given container. + if limit == nil { + validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + } + + return articles + } + func fetchUnreadArticlesAsync(forContainer container: Container, limit: Int?, _ completion: @escaping ArticleSetResultBlock) { let feeds = container.flattenedFeeds() database.fetchUnreadArticlesAsync(feeds.feedIDs(), limit) { [weak self] (articleSetResult) in @@ -1091,7 +1217,8 @@ private extension Account { for article in articles where !article.status.read { unreadCountStorage[article.feedID, default: 0] += 1 } - feeds.forEach { (feed) in + + for feed in feeds { let unreadCount = unreadCountStorage[feed.feedID, default: 0] feed.unreadCount = unreadCount } @@ -1138,6 +1265,12 @@ private extension Account { flattenedFeedsNeedUpdate = false } + /// feedIDs for all feeds in the account, not just top level. + func allFeedIDs() -> Set { + + flattenedFeeds().feedIDs() + } + func rebuildFeedDictionaries() { var idDictionary = [String: Feed]() var externalIDDictionary = [String: Feed]() diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index e4cf12d5a..37803307c 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -344,6 +344,21 @@ public final class AccountManager: UnreadCountProvider { // These fetch articles from active accounts and return a merged Set
. + @MainActor public func fetchArticles(fetchType: FetchType) async throws -> Set
{ + + guard activeAccounts.count > 0 else { + return Set
() + } + + var allFetchedArticles = Set
() + for account in activeAccounts { + let articles = try await account.articles(for: fetchType) + allFetchedArticles.formUnion(articles) + } + + return allFetchedArticles + } + public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) { precondition(Thread.isMainThread) diff --git a/Account/Sources/Account/ArticleFetcher.swift b/Account/Sources/Account/ArticleFetcher.swift index 4a74ff5b7..8d8e343ce 100644 --- a/Account/Sources/Account/ArticleFetcher.swift +++ b/Account/Sources/Account/ArticleFetcher.swift @@ -12,12 +12,25 @@ import ArticlesDatabase public protocol ArticleFetcher { - func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) - func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) + @MainActor func fetchArticles() async throws -> Set
+ @MainActor func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) + + @MainActor func fetchUnreadArticles() async throws -> Set
+ @MainActor func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) } extension Feed: ArticleFetcher { + public func fetchArticles() async throws -> Set
{ + + guard let account else { + assertionFailure("Expected feed.account, but got nil.") + return Set
() + } + + return try await account.articles(feed: self) + } + public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { guard let account = account else { assertionFailure("Expected feed.account, but got nil.") @@ -27,6 +40,16 @@ extension Feed: ArticleFetcher { account.fetchArticlesAsync(.feed(self), completion) } + public func fetchUnreadArticles() async throws -> Set
{ + + guard let account else { + assertionFailure("Expected feed.account, but got nil.") + return Set
() + } + + return try await account.unreadArticles(feed: self) + } + public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { guard let account = account else { assertionFailure("Expected feed.account, but got nil.") @@ -45,22 +68,49 @@ extension Feed: ArticleFetcher { } extension Folder: ArticleFetcher { + + public func fetchArticles() async throws -> Set { + + try await articles(unreadOnly: false) + } public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - guard let account = account else { + + guard let account else { assertionFailure("Expected folder.account, but got nil.") completion(.success(Set
())) return } + account.fetchArticlesAsync(.folder(self, false), completion) } + public func fetchUnreadArticles() async throws -> Set
{ + + try await articles(unreadOnly: true) + } + public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - guard let account = account else { + + guard let account else { assertionFailure("Expected folder.account, but got nil.") completion(.success(Set
())) return } + account.fetchArticlesAsync(.folder(self, true), completion) } } + +private extension Folder { + + func articles(unreadOnly: Bool = false) async throws -> Set
{ + + guard let account else { + assertionFailure("Expected folder.account, but got nil.") + return Set
() + } + + return try await account.articles(for: .folder(self, unreadOnly)) + } +} diff --git a/Account/Sources/Account/SingleArticleFetcher.swift b/Account/Sources/Account/SingleArticleFetcher.swift index 8718c2bb4..dbc552b50 100644 --- a/Account/Sources/Account/SingleArticleFetcher.swift +++ b/Account/Sources/Account/SingleArticleFetcher.swift @@ -10,22 +10,38 @@ import Foundation import Articles import ArticlesDatabase -public struct SingleArticleFetcher: ArticleFetcher { - +public struct SingleArticleFetcher { + private let account: Account private let articleID: String - + public init(account: Account, articleID: String) { self.account = account self.articleID = articleID } - - public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion) - } - - public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion) - } - +} + +extension SingleArticleFetcher: ArticleFetcher { + + public func fetchArticles() async throws -> Set
{ + + try await account.articles(articleIDs: Set([articleID])) + } + + public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { + + return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion) + } + + // Doesn’t actually fetch unread articles. Fetches whatever articleID it is asked to fetch. + + public func fetchUnreadArticles() async throws -> Set
{ + + try await fetchArticles() + } + + public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { + + return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion) + } } diff --git a/Articles/Sources/Articles/Article.swift b/Articles/Sources/Articles/Article.swift index 3d363e509..a78343b32 100644 --- a/Articles/Sources/Articles/Article.swift +++ b/Articles/Sources/Articles/Article.swift @@ -75,7 +75,7 @@ public extension Set where Element == Article { return Set(map { $0.articleID }) } - func unreadArticles() -> Set
{ + @MainActor func unreadArticles() -> Set
{ let articles = self.filter { !$0.status.read } return Set(articles) } diff --git a/Articles/Sources/Articles/ArticleStatus.swift b/Articles/Sources/Articles/ArticleStatus.swift index 7cc0954a9..a0a8bd696 100644 --- a/Articles/Sources/Articles/ArticleStatus.swift +++ b/Articles/Sources/Articles/ArticleStatus.swift @@ -38,13 +38,6 @@ public final class ArticleStatus: Hashable, @unchecked Sendable { return _read } - set { - Self.lock.lock() - defer { - Self.lock.unlock() - } - _read = newValue - } } public var starred: Bool { @@ -56,13 +49,6 @@ public final class ArticleStatus: Hashable, @unchecked Sendable { return _starred } - set { - Self.lock.lock() - defer { - Self.lock.unlock() - } - _starred = newValue - } } private var _read = false @@ -89,11 +75,17 @@ public final class ArticleStatus: Hashable, @unchecked Sendable { } public func setBoolStatus(_ status: Bool, forKey key: ArticleStatus.Key) { + + Self.lock.lock() + defer { + Self.lock.unlock() + } + switch key { case .read: - read = status + _read = status case .starred: - starred = status + _starred = status } } diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift index 817b0b4a5..d47a4ff13 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift @@ -97,7 +97,7 @@ public actor ArticlesDatabase { return articlesTable.articles(articleIDs: articleIDs, database: database) } - public func unreadArticles(feedIDs: Set, limit: Int?) throws -> Set
{ + public func unreadArticles(feedIDs: Set, limit: Int? = nil) throws -> Set
{ guard let database else { throw DatabaseError.suspended @@ -105,7 +105,7 @@ public actor ArticlesDatabase { return articlesTable.unreadArticles(feedIDs: feedIDs, limit: limit, database: database) } - public func todayArticles(feedIDs: Set, limit: Int?) throws -> Set
{ + public func todayArticles(feedIDs: Set, limit: Int? = nil) throws -> Set
{ guard let database else { throw DatabaseError.suspended @@ -113,7 +113,7 @@ public actor ArticlesDatabase { return articlesTable.todayArticles(feedIDs: feedIDs, cutoffDate: todayCutoffDate(), limit: limit, database: database) } - public func starredArticles(feedIDs: Set, limit: Int?) throws -> Set
{ + public func starredArticles(feedIDs: Set, limit: Int? = nil) throws -> Set
{ guard let database else { throw DatabaseError.suspended diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabaseCompatibility.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabaseCompatibility.swift index ba79ead30..8aad09da9 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabaseCompatibility.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabaseCompatibility.swift @@ -42,9 +42,9 @@ public extension ArticlesDatabase { Task { do { let articles = try await articles(feedID: feedID) - completion(.success(articles)) + callArticleSetCompletion(completion, .success(articles)) } catch { - completion(.failure(.suspended)) + callArticleSetCompletion(completion, .failure(.suspended)) } } } @@ -54,9 +54,9 @@ public extension ArticlesDatabase { Task { do { let articles = try await articles(feedIDs: feedIDs) - completion(.success(articles)) + callArticleSetCompletion(completion, .success(articles)) } catch { - completion(.failure(.suspended)) + callArticleSetCompletion(completion, .failure(.suspended)) } } } @@ -66,9 +66,9 @@ public extension ArticlesDatabase { Task { do { let articles = try await articles(articleIDs: articleIDs) - completion(.success(articles)) + callArticleSetCompletion(completion, .success(articles)) } catch { - completion(.failure(.suspended)) + callArticleSetCompletion(completion, .failure(.suspended)) } } } @@ -78,9 +78,9 @@ public extension ArticlesDatabase { Task { do { let articles = try await unreadArticles(feedIDs: feedIDs, limit: limit) - completion(.success(articles)) + callArticleSetCompletion(completion, .success(articles)) } catch { - completion(.failure(.suspended)) + callArticleSetCompletion(completion, .failure(.suspended)) } } } @@ -90,9 +90,9 @@ public extension ArticlesDatabase { Task { do { let articles = try await todayArticles(feedIDs: feedIDs, limit: limit) - completion(.success(articles)) + callArticleSetCompletion(completion, .success(articles)) } catch { - completion(.failure(.suspended)) + callArticleSetCompletion(completion, .failure(.suspended)) } } } @@ -102,9 +102,9 @@ public extension ArticlesDatabase { Task { do { let articles = try await starredArticles(feedIDs: feedIDs, limit: limit) - completion(.success(articles)) + callArticleSetCompletion(completion, .success(articles)) } catch { - completion(.failure(.suspended)) + callArticleSetCompletion(completion, .failure(.suspended)) } } } @@ -114,9 +114,9 @@ public extension ArticlesDatabase { Task { do { let articles = try await articlesMatching(searchString: searchString, feedIDs: feedIDs) - completion(.success(articles)) + callArticleSetCompletion(completion, .success(articles)) } catch { - completion(.failure(.suspended)) + callArticleSetCompletion(completion, .failure(.suspended)) } } } @@ -126,9 +126,9 @@ public extension ArticlesDatabase { Task { do { let articles = try await articlesMatching(searchString: searchString, articleIDs: articleIDs) - completion(.success(articles)) + callArticleSetCompletion(completion, .success(articles)) } catch { - completion(.failure(.suspended)) + callArticleSetCompletion(completion, .failure(.suspended)) } } } @@ -141,9 +141,9 @@ public extension ArticlesDatabase { Task { do { let unreadCountDictionary = try await allUnreadCounts() - completion(.success(unreadCountDictionary)) + callUnreadCountDictionaryCompletion(completion, .success(unreadCountDictionary)) } catch { - completion(.failure(.suspended)) + callUnreadCountDictionaryCompletion(completion, .failure(.suspended)) } } } @@ -154,9 +154,9 @@ public extension ArticlesDatabase { Task { do { let unreadCount = try await unreadCount(feedID: feedID) ?? 0 - completion(.success(unreadCount)) + callSingleUnreadCountCompletion(completion, .success(unreadCount)) } catch { - completion(.failure(.suspended)) + callSingleUnreadCountCompletion(completion, .failure(.suspended)) } } } @@ -167,9 +167,9 @@ public extension ArticlesDatabase { Task { do { let unreadCountDictionary = try await unreadCounts(feedIDs: feedIDs) - completion(.success(unreadCountDictionary)) + callUnreadCountDictionaryCompletion(completion, .success(unreadCountDictionary)) } catch { - completion(.failure(.suspended)) + callUnreadCountDictionaryCompletion(completion, .failure(.suspended)) } } } @@ -179,9 +179,9 @@ public extension ArticlesDatabase { Task { do { let unreadCount = try await unreadCountForToday(feedIDs: feedIDs)! - completion(.success(unreadCount)) + callSingleUnreadCountCompletion(completion, .success(unreadCount)) } catch { - completion(.failure(.suspended)) + callSingleUnreadCountCompletion(completion, .failure(.suspended)) } } } @@ -191,9 +191,9 @@ public extension ArticlesDatabase { Task { do { let unreadCount = try await unreadCount(feedIDs: feedIDs, since: since)! - completion(.success(unreadCount)) + callSingleUnreadCountCompletion(completion, .success(unreadCount)) } catch { - completion(.failure(.suspended)) + callSingleUnreadCountCompletion(completion, .failure(.suspended)) } } } @@ -203,9 +203,9 @@ public extension ArticlesDatabase { Task { do { let unreadCount = try await starredAndUnreadCount(feedIDs: feedIDs)! - completion(.success(unreadCount)) + callSingleUnreadCountCompletion(completion, .success(unreadCount)) } catch { - completion(.failure(.suspended)) + callSingleUnreadCountCompletion(completion, .failure(.suspended)) } } } @@ -218,9 +218,9 @@ public extension ArticlesDatabase { Task { do { let articleChanges = try await update(parsedItems: parsedItems, feedID: feedID, deleteOlder: deleteOlder) - completion(.success(articleChanges)) + callUpdateArticlesCompletion(completion, .success(articleChanges)) } catch { - completion(.failure(.suspended)) + callUpdateArticlesCompletion(completion, .failure(.suspended)) } } } @@ -231,9 +231,9 @@ public extension ArticlesDatabase { Task { do { let articleChanges = try await update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) - completion(.success(articleChanges)) + callUpdateArticlesCompletion(completion, .success(articleChanges)) } catch { - completion(.failure(.suspended)) + callUpdateArticlesCompletion(completion, .failure(.suspended)) } } } @@ -244,9 +244,9 @@ public extension ArticlesDatabase { Task { do { try await delete(articleIDs: articleIDs) - completion?(nil) + callDatabaseCompletion(completion) } catch { - completion?(.suspended) + callDatabaseCompletion(completion, .suspended) } } } @@ -259,9 +259,9 @@ public extension ArticlesDatabase { Task { do { let articleIDs = try await unreadArticleIDs()! - completion(.success(articleIDs)) + callArticleIDsCompletion(completion, .success(articleIDs)) } catch { - completion(.failure(.suspended)) + callArticleIDsCompletion(completion, .failure(.suspended)) } } } @@ -272,46 +272,46 @@ public extension ArticlesDatabase { Task { do { let articleIDs = try await starredArticleIDs()! - completion(.success(articleIDs)) + callArticleIDsCompletion(completion, .success(articleIDs)) } catch { - completion(.failure(.suspended)) + callArticleIDsCompletion(completion, .failure(.suspended)) } } } /// Fetch articleIDs for articles that we should have, but don’t. These articles are either (starred) or (newer than the article cutoff date). nonisolated func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) { - + Task { do { let articleIDs = try await articleIDsForStatusesWithoutArticlesNewerThanCutoffDate()! - completion(.success(articleIDs)) + callArticleIDsCompletion(completion, .success(articleIDs)) } catch { - completion(.failure(.suspended)) + callArticleIDsCompletion(completion, .failure(.suspended)) } } } nonisolated func mark(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping ArticleStatusesResultBlock) { - + Task { do { let statuses = try await mark(articles: articles, statusKey: statusKey, flag: flag)! - completion(.success(statuses)) + callArticleStatusesCompletion(completion, .success(statuses)) } catch { - completion(.failure(.suspended)) + callArticleStatusesCompletion(completion, .failure(.suspended)) } } } - + nonisolated func markAndFetchNew(articleIDs: Set, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping ArticleIDsCompletionBlock) { Task { do { let statuses = try await markAndFetchNew(articleIDs: articleIDs, statusKey: statusKey, flag: flag) - completion(.success(statuses)) + callArticleIDsCompletion(completion, .success(statuses)) } catch { - completion(.failure(.suspended)) + callArticleIDsCompletion(completion, .failure(.suspended)) } } } @@ -323,10 +323,63 @@ public extension ArticlesDatabase { Task { do { try await createStatusesIfNeeded(articleIDs: articleIDs) - completion(nil) + callDatabaseCompletion(completion) } catch { - completion(.suspended) + callDatabaseCompletion(completion, .suspended) } } } + + nonisolated private func callUnreadCountDictionaryCompletion(_ completion: @escaping UnreadCountDictionaryCompletionBlock, _ result: UnreadCountDictionaryCompletionResult) { + + Task { @MainActor in + completion(result) + } + } + + nonisolated private func callSingleUnreadCountCompletion(_ completion: @escaping SingleUnreadCountCompletionBlock, _ result: SingleUnreadCountResult) { + + Task { @MainActor in + completion(result) + } + } + + nonisolated private func callUpdateArticlesCompletion(_ completion: @escaping UpdateArticlesCompletionBlock, _ result: UpdateArticlesResult) { + + Task { @MainActor in + completion(result) + } + } + + nonisolated private func callArticleSetCompletion(_ completion: @escaping ArticleSetResultBlock, _ result: ArticleSetResult) { + + Task { @MainActor in + completion(result) + } + } + + nonisolated private func callArticleStatusesCompletion(_ completion: @escaping ArticleStatusesResultBlock, _ result: ArticleStatusesResult) { + + Task { @MainActor in + completion(result) + } + } + + nonisolated private func callArticleIDsCompletion(_ completion: @escaping ArticleIDsCompletionBlock, _ result: ArticleIDsResult) { + + Task { @MainActor in + completion(result) + } + } + + nonisolated private func callDatabaseCompletion(_ completion: DatabaseCompletionBlock?, _ error: DatabaseError? = nil) { + + guard let completion else { + return + } + + Task { @MainActor in + completion(error) + } + } } diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 50e98dc8a..92446ad74 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -968,42 +968,68 @@ extension AppDelegate: NSWindowRestoration { private extension AppDelegate { - func handleMarkAsRead(userInfo: [AnyHashable: Any]) { - guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], - let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, - let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { - return + struct ArticlePathInfo { + + let accountID: String + let articleID: 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 } - - let account = AccountManager.shared.existingAccount(with: accountID) - guard account != nil else { + } + + func handleMarkAsRead(userInfo: [AnyHashable: Any]) { + + guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else { + return + } + guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else { os_log(.debug, "No account found from notification.") return } - let article = try? account!.fetchArticles(.articleIDs([articleID])) - guard article != nil else { - os_log(.debug, "No article found from search using %@", articleID) - return + let articleID = articlePathInfo.articleID + + Task { + guard let articles = try? await account.articles(for: .articleIDs([articleID])) else { + os_log(.debug, "No article found from search using %@", articleID) + return + } + + account.markArticles(articles, statusKey: .read, flag: true) { _ in } } - account!.markArticles(article!, statusKey: .read, flag: true) { _ in } } func handleMarkAsStarred(userInfo: [AnyHashable: Any]) { - guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], - let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, - let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { - return + + guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else { + return } - let account = AccountManager.shared.existingAccount(with: accountID) - guard account != nil else { + guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else { os_log(.debug, "No account found from notification.") return } - let article = try? account!.fetchArticles(.articleIDs([articleID])) - guard article != nil else { - os_log(.debug, "No article found from search using %@", articleID) - return + let articleID = articlePathInfo.articleID + + Task { + + guard let articles = try? await account.articles(for: .articleIDs([articleID])) else { + os_log(.debug, "No article found from search using %@", articleID) + return + } + + account.markArticles(articles, statusKey: .starred, flag: true) { _ in } } - account!.markArticles(article!, statusKey: .starred, flag: true) { _ in } } } diff --git a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift index 161c7be43..6bc267609 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift @@ -69,11 +69,13 @@ extension SidebarViewController { return } - let articles = unreadArticles(for: objects) - guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else { - return + Task { + let articles = await unreadArticles(for: objects) + guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else { + return + } + runCommand(markReadCommand) } - runCommand(markReadCommand) } @objc func deleteFromContextualMenu(_ sender: Any?) { @@ -349,12 +351,12 @@ private extension SidebarViewController { return item } - func unreadArticles(for objects: [Any]) -> Set
{ + @MainActor func unreadArticles(for objects: [Any]) async -> Set
{ var articles = Set
() for object in objects { if let articleFetcher = object as? ArticleFetcher { - if let unreadArticles = try? articleFetcher.fetchUnreadArticles() { + if let unreadArticles = try? await articleFetcher.fetchUnreadArticles() { articles.formUnion(unreadArticles) } } diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index 5ab7cf65b..2e5cb8226 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -253,18 +253,33 @@ protocol SidebarDelegate: AnyObject { } } - @IBAction func doubleClickedSidebar(_ sender: Any?) { + @MainActor @IBAction func doubleClickedSidebar(_ sender: Any?) { + guard outlineView.clickedRow == outlineView.selectedRow else { return } - if AppDefaults.shared.feedDoubleClickMarkAsRead, let articles = try? singleSelectedFeed?.fetchUnreadArticles() { - if let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) { - runCommand(markReadCommand) + + if AppDefaults.shared.feedDoubleClickMarkAsRead, let feed = singleSelectedFeed { + Task { @MainActor in + await markArticlesInFeedAsRead(feed: feed) } } + openInBrowser(sender) } + @MainActor private func markArticlesInFeedAsRead(feed: Feed) async { + + guard let articles = try? await feed.fetchUnreadArticles() else { + return + } + guard let undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else { + return + } + + runCommand(markReadCommand) + } + @IBAction func openInBrowser(_ sender: Any?) { guard let feed = singleSelectedFeed, let homePageURL = feed.homePageURL else { return diff --git a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift index b7f30b01c..fe88a3bce 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift @@ -258,11 +258,8 @@ private extension TimelineViewController { } func markAllAsReadMenuItem(_ feed: Feed) -> NSMenuItem? { - guard let articlesSet = try? feed.fetchArticles() else { - return nil - } - let articles = Array(articlesSet) - guard articles.canMarkAllAsRead() else { + + guard feed.unreadCount > 0 else { return nil } diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 25d373132..517ffb461 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -63,14 +63,16 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr var representedObjects: [AnyObject]? { didSet { - if !representedObjectArraysAreEqual(oldValue, representedObjects) { - unreadCount = 0 + guard !representedObjectArraysAreEqual(oldValue, representedObjects) else { + return + } - selectionDidChange(nil) - if showsSearchResults { - fetchAndReplaceArticlesAsync() - } else { - fetchAndReplaceArticlesSync() + unreadCount = 0 + selectionDidChange(nil) + + Task { + await fetchAndReplaceArticles() + if !showsSearchResults { if articles.count > 0 { tableView.scrollRowToVisible(0) } @@ -290,29 +292,31 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr } if let articlePathUserInfo = state[UserInfoKey.articlePath] as? [AnyHashable : Any], - let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, - let account = AccountManager.shared.existingAccount(with: accountID), - let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String { - + let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, + let account = AccountManager.shared.existingAccount(with: accountID), + let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String { + exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID) - fetchAndReplaceArticlesSync() - - if let selectedIndex = articles.firstIndex(where: { $0.articleID == articleID }) { - tableView.selectRow(selectedIndex) - DispatchQueue.main.async { - self.tableView.scrollTo(row: selectedIndex) + + Task { + await fetchAndReplaceArticles() + + Task { @MainActor in + if let selectedIndex = articles.firstIndex(where: { $0.articleID == articleID }) { + + tableView.selectRow(selectedIndex) + self.tableView.scrollTo(row: selectedIndex) + focus() + } } - focus() } - } else { - - fetchAndReplaceArticlesSync() - + Task { + await fetchAndReplaceArticles() + } } - } - + // MARK: - Actions @objc func openArticleInBrowser(_ sender: Any?) { @@ -534,19 +538,21 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr func goToDeepLink(for userInfo: [AnyHashable : Any]) { guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return } - if isReadFiltered ?? false { - if let accountName = userInfo[ArticlePathKey.accountName] as? String, - let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) { - exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID) - fetchAndReplaceArticlesSync() + Task { + if isReadFiltered ?? false { + if let accountName = userInfo[ArticlePathKey.accountName] as? String, + let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) { + exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID) + await fetchAndReplaceArticles() + } } - } - guard let ix = articles.firstIndex(where: { $0.articleID == articleID }) else { return } - - NSCursor.setHiddenUntilMouseMoves(true) - tableView.selectRow(ix) - tableView.scrollTo(row: ix) + guard let ix = articles.firstIndex(where: { $0.articleID == articleID }) else { return } + + NSCursor.setHiddenUntilMouseMoves(true) + tableView.selectRow(ix) + tableView.scrollTo(row: ix) + } } func goToNextUnread(wrappingToTop wrapping: Bool = false) { @@ -645,19 +651,25 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr @objc func accountStateDidChange(_ note: Notification) { if representedObjectsContainsAnyPseudoFeed() { - fetchAndReplaceArticlesAsync() + Task { + await fetchAndReplaceArticles() + } } } @objc func accountsDidChange(_ note: Notification) { if representedObjectsContainsAnyPseudoFeed() { - fetchAndReplaceArticlesAsync() + Task { + await fetchAndReplaceArticles() + } } } @objc func containerChildrenDidChange(_ note: Notification) { if representedObjectsContainsAnyPseudoFeed() || representedObjectsContainAnyFolder() { - fetchAndReplaceArticlesAsync() + Task { + await fetchAndReplaceArticles() + } } } @@ -949,8 +961,10 @@ private extension TimelineViewController { if let article = oneSelectedArticle, let account = article.account { exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: article.articleID) } - performBlockAndRestoreSelection { - fetchAndReplaceArticlesSync() + Task { + await performAsyncBlockAndRestoreSelection { + await fetchAndReplaceArticles() + } } } @@ -1033,6 +1047,12 @@ private extension TimelineViewController { restoreSelection(savedSelection) } + func performAsyncBlockAndRestoreSelection(_ block: (() async -> Void)) async { + let savedSelection = selectedArticleIDs() + await block() + restoreSelection(savedSelection) + } + func rows(for articleID: String) -> [Int]? { updateArticleRowMapIfNeeded() return articleRowMap[articleID] @@ -1091,44 +1111,43 @@ private extension TimelineViewController { // MARK: - Fetching Articles - func fetchAndReplaceArticlesSync() { - // To be called when the user has made a change of selection in the sidebar. - // It blocks the main thread, so that there’s no async delay, - // so that the entire display refreshes at once. - // It’s a better user experience this way. + func fetchAndReplaceArticles() async { + cancelPendingAsyncFetches() + guard var representedObjects = representedObjects else { emptyTheTimeline() return } - - if exceptionArticleFetcher != nil { + + if let exceptionArticleFetcher { representedObjects.append(exceptionArticleFetcher as AnyObject) - exceptionArticleFetcher = nil + self.exceptionArticleFetcher = nil } - - let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects) + + let fetchedArticles = await unsortedArticles(for: representedObjects) replaceArticles(with: fetchedArticles) } - func fetchAndReplaceArticlesAsync() { - // To be called when we need to do an entire fetch, but an async delay is okay. - // Example: we have the Today feed selected, and the calendar day just changed. - cancelPendingAsyncFetches() - guard var representedObjects = representedObjects else { - emptyTheTimeline() - return - } - - if exceptionArticleFetcher != nil { - representedObjects.append(exceptionArticleFetcher as AnyObject) - exceptionArticleFetcher = nil - } - - fetchUnsortedArticlesAsync(for: representedObjects) { [weak self] (articles) in - self?.replaceArticles(with: articles) - } - } +// func fetchAndReplaceArticlesSync() { +// // To be called when the user has made a change of selection in the sidebar. +// // It blocks the main thread, so that there’s no async delay, +// // so that the entire display refreshes at once. +// // It’s a better user experience this way. +// cancelPendingAsyncFetches() +// guard var representedObjects = representedObjects else { +// emptyTheTimeline() +// return +// } +// +// if exceptionArticleFetcher != nil { +// representedObjects.append(exceptionArticleFetcher as AnyObject) +// exceptionArticleFetcher = nil +// } +// +// let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects) +// replaceArticles(with: fetchedArticles) +// } func cancelPendingAsyncFetches() { fetchSerialNumber += 1 @@ -1139,27 +1158,27 @@ private extension TimelineViewController { articles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed) } - func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set
{ - cancelPendingAsyncFetches() - let fetchers = representedObjects.compactMap{ $0 as? ArticleFetcher } - if fetchers.isEmpty { - return Set
() - } - - var fetchedArticles = Set
() - for fetchers in fetchers { - if (fetchers as? SidebarItem)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true { - if let articles = try? fetchers.fetchUnreadArticles() { - fetchedArticles.formUnion(articles) - } - } else { - if let articles = try? fetchers.fetchArticles() { - fetchedArticles.formUnion(articles) - } - } - } - return fetchedArticles - } +// func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set
{ +// cancelPendingAsyncFetches() +// let fetchers = representedObjects.compactMap{ $0 as? ArticleFetcher } +// if fetchers.isEmpty { +// return Set
() +// } +// +// var fetchedArticles = Set
() +// for fetchers in fetchers { +// if (fetchers as? SidebarItem)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true { +// if let articles = try? fetchers.fetchUnreadArticles() { +// fetchedArticles.formUnion(articles) +// } +// } else { +// if let articles = try? fetchers.fetchArticles() { +// fetchedArticles.formUnion(articles) +// } +// } +// } +// return fetchedArticles +// } func fetchUnsortedArticlesAsync(for representedObjects: [Any], completion: @escaping ArticleSetBlock) { // The callback will *not* be called if the fetch is no longer relevant — that is, @@ -1177,6 +1196,29 @@ private extension TimelineViewController { fetchRequestQueue.add(fetchOperation) } + func unsortedArticles(for representedObjects: [Any]) async -> Set
{ + + let fetchers = representedObjects.compactMap{ $0 as? ArticleFetcher } + if fetchers.isEmpty { + return Set
() + } + + var fetchedArticles = Set
() + for fetcher in fetchers { + if (fetcher as? SidebarItem)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true { + if let articles = try? await fetcher.fetchUnreadArticles() { + fetchedArticles.formUnion(articles) + } + } else { + if let articles = try? await fetcher.fetchArticles() { + fetchedArticles.formUnion(articles) + } + } + } + + return fetchedArticles + } + func selectArticles(_ articleIDs: [String]) { let indexesToSelect = indexesForArticleIDs(Set(articleIDs)) if indexesToSelect.isEmpty { diff --git a/Mac/Scriptability/Feed+Scriptability.swift b/Mac/Scriptability/Feed+Scriptability.swift index baba90d41..efecfd265 100644 --- a/Mac/Scriptability/Feed+Scriptability.swift +++ b/Mac/Scriptability/Feed+Scriptability.swift @@ -12,7 +12,7 @@ import Account import Articles @objc(ScriptableFeed) -class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { +@objcMembers class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { let feed:Feed let container:ScriptingObjectContainer @@ -163,21 +163,21 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine return ScriptableAuthor(author, container:self) } - @objc(articles) - var articles:NSArray { - let feedArticles = (try? feed.fetchArticles()) ?? Set
() - // the articles are a set, use the sorting algorithm from the viewer - let sortedArticles = feedArticles.sorted(by:{ - return $0.logicalDatePublished > $1.logicalDatePublished - }) - return sortedArticles.map { ScriptableArticle($0, container:self) } as NSArray - } +// @objc(articles) +// var articles:NSArray { +// let feedArticles = (try? feed.fetchArticles()) ?? Set
() +// // the articles are a set, use the sorting algorithm from the viewer +// let sortedArticles = feedArticles.sorted(by:{ +// return $0.logicalDatePublished > $1.logicalDatePublished +// }) +// return sortedArticles.map { ScriptableArticle($0, container:self) } as NSArray +// } - @objc(valueInArticlesWithUniqueID:) - func valueInArticles(withUniqueID id:String) -> ScriptableArticle? { - let articles = (try? feed.fetchArticles()) ?? Set
() - guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil } - return ScriptableArticle(article, container:self) - } +// @objc(valueInArticlesWithUniqueID:) +// func valueInArticles(withUniqueID id:String) -> ScriptableArticle? { +// let articles = (try? feed.fetchArticles()) ?? Set
() +// guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil } +// return ScriptableArticle(article, container:self) +// } } diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index ae0a138c3..de78967ef 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -286,10 +286,12 @@ private extension ActivityManager { return "account_\(article.accountID)_feed_\(article.feedID)_article_\(article.articleID)" } - static func identifiers(for feed: Feed) -> [String] { + @MainActor static func identifiers(for feed: Feed) async -> [String] { + var ids = [String]() ids.append(identifier(for: feed)) - if let articles = try? feed.fetchArticles() { + + if let articles = try? await feed.fetchArticles() { for article in articles { ids.append(identifier(for: article)) } diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index 69e1f1787..33f73b697 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -84,11 +84,23 @@ final class SmartFeed: PseudoFeed { extension SmartFeed: ArticleFetcher { + func fetchArticles() async throws -> Set
{ + + try await delegate.fetchArticles() + } + func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { + delegate.fetchArticlesAsync(completion) } + func fetchUnreadArticles() async throws -> Set
{ + + try await delegate.fetchUnreadArticles() + } + func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { + delegate.fetchUnreadArticlesAsync(completion) } } diff --git a/Shared/SmartFeeds/SmartFeedDelegate.swift b/Shared/SmartFeeds/SmartFeedDelegate.swift index d634df341..062786998 100644 --- a/Shared/SmartFeeds/SmartFeedDelegate.swift +++ b/Shared/SmartFeeds/SmartFeedDelegate.swift @@ -14,25 +14,30 @@ import RSCore import Database protocol SmartFeedDelegate: SidebarItemIdentifiable, DisplayNameProvider, ArticleFetcher, SmallIconProvider { + var fetchType: FetchType { get } + func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock) } extension SmartFeedDelegate { - func fetchArticles() throws -> Set
{ - return try AccountManager.shared.fetchArticles(fetchType) + @MainActor func fetchArticles() async throws -> Set
{ + + try await AccountManager.shared.fetchArticles(fetchType: fetchType) } - func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { + @MainActor func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { AccountManager.shared.fetchArticlesAsync(fetchType, completion) } - func fetchUnreadArticles() throws -> Set
{ - return try fetchArticles().unreadArticles() + @MainActor func fetchUnreadArticles() async throws -> Set
{ + + try await AccountManager.shared.fetchArticles(fetchType: fetchType) } - func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { + @MainActor func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { + fetchArticlesAsync{ articleSetResult in switch articleSetResult { case .success(let articles): diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index bc993785c..c1aa25e28 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -66,11 +66,24 @@ final class UnreadFeed: PseudoFeed { extension UnreadFeed: ArticleFetcher { + // Always fetches unread articles + func fetchArticles() async throws -> Set
{ + + try await fetchUnreadArticles() + } + func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { + fetchUnreadArticlesAsync(completion) } + func fetchUnreadArticles() async throws -> Set
{ + + try await AccountManager.shared.fetchArticles(fetchType: fetchType) + } + func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { + AccountManager.shared.fetchArticlesAsync(fetchType, completion) } } diff --git a/Shared/Timeline/FetchRequestOperation.swift b/Shared/Timeline/FetchRequestOperation.swift index 8195755c9..86f636e0b 100644 --- a/Shared/Timeline/FetchRequestOperation.swift +++ b/Shared/Timeline/FetchRequestOperation.swift @@ -34,7 +34,7 @@ final class FetchRequestOperation { self.resultBlock = resultBlock } - func run(_ completion: @escaping (FetchRequestOperation) -> Void) { + @MainActor func run(_ completion: @escaping (FetchRequestOperation) -> Void) { precondition(Thread.isMainThread) precondition(!isFinished) diff --git a/Shared/Timeline/FetchRequestQueue.swift b/Shared/Timeline/FetchRequestQueue.swift index 4fd9ff093..a35c39e05 100644 --- a/Shared/Timeline/FetchRequestQueue.swift +++ b/Shared/Timeline/FetchRequestQueue.swift @@ -8,9 +8,7 @@ import Foundation -// Main thread only. - -final class FetchRequestQueue { +@MainActor final class FetchRequestQueue { private var pendingRequests = [FetchRequestOperation]() private var currentRequest: FetchRequestOperation? = nil