Fix many build errors.

This commit is contained in:
Brent Simmons 2024-03-18 21:08:37 -07:00
parent 2a44e1ccf1
commit e58f8ada42
20 changed files with 624 additions and 253 deletions

View File

@ -321,12 +321,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
feedMetadataFile.load() feedMetadataFile.load()
opmlFile.load() opmlFile.load()
Task { Task { @MainActor in
try? await self.database.cleanupDatabaseAtStartup(subscribedToFeedIDs: self.flattenedFeeds().feedIDs()) try? await self.database.cleanupDatabaseAtStartup(subscribedToFeedIDs: self.flattenedFeeds().feedIDs())
self.fetchAllUnreadCounts()
Task { @MainActor in
self.fetchAllUnreadCounts()
}
} }
self.delegate.accountDidInitialize(self) self.delegate.accountDidInitialize(self)
@ -645,6 +642,41 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
fetchUnreadCounts(for: feeds, completion: completion) fetchUnreadCounts(for: feeds, completion: completion)
} }
public func articles(for fetchType: FetchType) async throws -> Set<Article> {
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) { public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) {
switch fetchType { switch fetchType {
case .starred(let limit): case .starred(let limit):
@ -670,6 +702,43 @@ 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)
return articles
}
public func articles(articleIDs: Set<String>) async throws -> Set<Article> {
try await database.articles(articleIDs: articleIDs)
}
public func unreadArticles(feed: Feed) async throws -> Set<Article> {
try await database.unreadArticles(feedIDs: Set([feed.feedID]))
}
public func unreadArticles(feeds: Set<Feed>) async throws -> Set<Article> {
if feeds.isEmpty {
return Set<Article>()
}
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<Article> {
let feeds = folder.flattenedFeeds()
return try await unreadArticles(feeds: feeds)
}
public func fetchUnreadCountForToday(_ completion: @escaping SingleUnreadCountCompletionBlock) { public func fetchUnreadCountForToday(_ completion: @escaping SingleUnreadCountCompletionBlock) {
database.fetchUnreadCountForToday(for: flattenedFeeds().feedIDs(), completion: completion) database.fetchUnreadCountForToday(for: flattenedFeeds().feedIDs(), completion: completion)
} }
@ -911,7 +980,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
#if DEBUG #if DEBUG
Task { Task {
let t1 = Date() let t1 = Date()
let articles = try! await fetchArticlesMatching("Brent NetNewsWire") let articles = try! await articlesMatching(searchString: "Brent NetNewsWire")
let t2 = Date() let t2 = Date()
print(t2.timeIntervalSince(t1)) print(t2.timeIntervalSince(t1))
print(articles.count) print(articles.count)
@ -999,23 +1068,48 @@ extension Account: FeedMetadataDelegate {
private extension Account { private extension Account {
func starredArticles(limit: Int? = nil) async throws -> Set<Article> {
try await database.starredArticles(feedIDs: allFeedIDs(), limit: limit)
}
func fetchStarredArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { 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<Article> {
try await unreadArticles(container: self)
} }
func fetchUnreadArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { func fetchUnreadArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
fetchUnreadArticlesAsync(forContainer: self, limit: limit, completion) fetchUnreadArticlesAsync(forContainer: self, limit: limit, completion)
} }
func todayArticles(limit: Int? = nil) async throws -> Set<Article> {
try await database.todayArticles(feedIDs: allFeedIDs(), limit: limit)
}
func fetchTodayArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { 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<Article> {
try await articles(container: folder)
} }
func fetchArticlesAsync(folder: Folder, _ completion: @escaping ArticleSetResultBlock) { func fetchArticlesAsync(folder: Folder, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync(forContainer: folder, completion) fetchArticlesAsync(forContainer: folder, completion)
} }
func fetchUnreadArticlesAsync(folder: Folder, _ completion: @escaping ArticleSetResultBlock) { func fetchUnreadArticlesAsync(folder: Folder, _ completion: @escaping ArticleSetResultBlock) {
fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion) fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion)
} }
@ -1031,24 +1125,41 @@ private extension Account {
} }
} }
func fetchArticlesMatching(_ searchString: String) async throws -> Set<Article> { func articlesMatching(searchString: String) async throws -> Set<Article> {
let feedIDs = flattenedFeeds().feedIDs() try await database.articlesMatching(searchString: searchString, feedIDs: allFeedIDs())
return try await database.articlesMatching(searchString: searchString, feedIDs: feedIDs)
} }
func fetchArticlesMatchingAsync(_ searchString: String, _ completion: @escaping ArticleSetResultBlock) { func fetchArticlesMatchingAsync(_ searchString: String, _ completion: @escaping ArticleSetResultBlock) {
database.fetchArticlesMatchingAsync(searchString, flattenedFeeds().feedIDs(), completion) database.fetchArticlesMatchingAsync(searchString, flattenedFeeds().feedIDs(), completion)
} }
func articlesMatching(searchString: String, articleIDs: Set<String>) async throws -> Set<Article> {
try await database.articlesMatching(searchString: searchString, articleIDs: articleIDs)
}
func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) { func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
database.fetchArticlesMatchingWithArticleIDsAsync(searchString, articleIDs, completion) database.fetchArticlesMatchingWithArticleIDsAsync(searchString, articleIDs, completion)
} }
func fetchArticlesAsync(articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) { func fetchArticlesAsync(articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
return database.fetchArticlesAsync(articleIDs: articleIDs, completion) return database.fetchArticlesAsync(articleIDs: articleIDs, completion)
} }
func articles(container: Container) async throws -> Set<Article> {
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) { func fetchArticlesAsync(forContainer container: Container, _ completion: @escaping ArticleSetResultBlock) {
let feeds = container.flattenedFeeds() let feeds = container.flattenedFeeds()
database.fetchArticlesAsync(feeds.feedIDs()) { [weak self] (articleSetResult) in 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<Article> {
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) { func fetchUnreadArticlesAsync(forContainer container: Container, limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
let feeds = container.flattenedFeeds() let feeds = container.flattenedFeeds()
database.fetchUnreadArticlesAsync(feeds.feedIDs(), limit) { [weak self] (articleSetResult) in database.fetchUnreadArticlesAsync(feeds.feedIDs(), limit) { [weak self] (articleSetResult) in
@ -1091,7 +1217,8 @@ private extension Account {
for article in articles where !article.status.read { for article in articles where !article.status.read {
unreadCountStorage[article.feedID, default: 0] += 1 unreadCountStorage[article.feedID, default: 0] += 1
} }
feeds.forEach { (feed) in
for feed in feeds {
let unreadCount = unreadCountStorage[feed.feedID, default: 0] let unreadCount = unreadCountStorage[feed.feedID, default: 0]
feed.unreadCount = unreadCount feed.unreadCount = unreadCount
} }
@ -1138,6 +1265,12 @@ private extension Account {
flattenedFeedsNeedUpdate = false flattenedFeedsNeedUpdate = false
} }
/// feedIDs for all feeds in the account, not just top level.
func allFeedIDs() -> Set<String> {
flattenedFeeds().feedIDs()
}
func rebuildFeedDictionaries() { func rebuildFeedDictionaries() {
var idDictionary = [String: Feed]() var idDictionary = [String: Feed]()
var externalIDDictionary = [String: Feed]() var externalIDDictionary = [String: Feed]()

View File

@ -344,6 +344,21 @@ public final class AccountManager: UnreadCountProvider {
// These fetch articles from active accounts and return a merged Set<Article>. // These fetch articles from active accounts and return a merged Set<Article>.
@MainActor public func fetchArticles(fetchType: FetchType) async throws -> Set<Article> {
guard activeAccounts.count > 0 else {
return Set<Article>()
}
var allFetchedArticles = Set<Article>()
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) { public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) {
precondition(Thread.isMainThread) precondition(Thread.isMainThread)

View File

@ -12,12 +12,25 @@ import ArticlesDatabase
public protocol ArticleFetcher { public protocol ArticleFetcher {
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) @MainActor func fetchArticles() async throws -> Set<Article>
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) @MainActor func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
@MainActor func fetchUnreadArticles() async throws -> Set<Article>
@MainActor func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
} }
extension Feed: ArticleFetcher { extension Feed: ArticleFetcher {
public func fetchArticles() async throws -> Set<Article> {
guard let account else {
assertionFailure("Expected feed.account, but got nil.")
return Set<Article>()
}
return try await account.articles(feed: self)
}
public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
guard let account = account else { guard let account = account else {
assertionFailure("Expected feed.account, but got nil.") assertionFailure("Expected feed.account, but got nil.")
@ -27,6 +40,16 @@ extension Feed: ArticleFetcher {
account.fetchArticlesAsync(.feed(self), completion) account.fetchArticlesAsync(.feed(self), completion)
} }
public func fetchUnreadArticles() async throws -> Set<Article> {
guard let account else {
assertionFailure("Expected feed.account, but got nil.")
return Set<Article>()
}
return try await account.unreadArticles(feed: self)
}
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
guard let account = account else { guard let account = account else {
assertionFailure("Expected feed.account, but got nil.") assertionFailure("Expected feed.account, but got nil.")
@ -45,22 +68,49 @@ extension Feed: ArticleFetcher {
} }
extension Folder: ArticleFetcher { extension Folder: ArticleFetcher {
public func fetchArticles() async throws -> Set<Articles.Article> {
try await articles(unreadOnly: false)
}
public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
guard let account = account else {
guard let account else {
assertionFailure("Expected folder.account, but got nil.") assertionFailure("Expected folder.account, but got nil.")
completion(.success(Set<Article>())) completion(.success(Set<Article>()))
return return
} }
account.fetchArticlesAsync(.folder(self, false), completion) account.fetchArticlesAsync(.folder(self, false), completion)
} }
public func fetchUnreadArticles() async throws -> Set<Article> {
try await articles(unreadOnly: true)
}
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
guard let account = account else {
guard let account else {
assertionFailure("Expected folder.account, but got nil.") assertionFailure("Expected folder.account, but got nil.")
completion(.success(Set<Article>())) completion(.success(Set<Article>()))
return return
} }
account.fetchArticlesAsync(.folder(self, true), completion) account.fetchArticlesAsync(.folder(self, true), completion)
} }
} }
private extension Folder {
func articles(unreadOnly: Bool = false) async throws -> Set<Article> {
guard let account else {
assertionFailure("Expected folder.account, but got nil.")
return Set<Article>()
}
return try await account.articles(for: .folder(self, unreadOnly))
}
}

View File

@ -10,22 +10,38 @@ import Foundation
import Articles import Articles
import ArticlesDatabase import ArticlesDatabase
public struct SingleArticleFetcher: ArticleFetcher { public struct SingleArticleFetcher {
private let account: Account private let account: Account
private let articleID: String private let articleID: String
public init(account: Account, articleID: String) { public init(account: Account, articleID: String) {
self.account = account self.account = account
self.articleID = articleID self.articleID = articleID
} }
}
public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion) extension SingleArticleFetcher: ArticleFetcher {
}
public func fetchArticles() async throws -> Set<Article> {
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion) try await account.articles(articleIDs: Set([articleID]))
} }
public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion)
}
// Doesnt actually fetch unread articles. Fetches whatever articleID it is asked to fetch.
public func fetchUnreadArticles() async throws -> Set<Article> {
try await fetchArticles()
}
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion)
}
} }

View File

@ -75,7 +75,7 @@ public extension Set where Element == Article {
return Set<String>(map { $0.articleID }) return Set<String>(map { $0.articleID })
} }
func unreadArticles() -> Set<Article> { @MainActor func unreadArticles() -> Set<Article> {
let articles = self.filter { !$0.status.read } let articles = self.filter { !$0.status.read }
return Set(articles) return Set(articles)
} }

View File

@ -38,13 +38,6 @@ public final class ArticleStatus: Hashable, @unchecked Sendable {
return _read return _read
} }
set {
Self.lock.lock()
defer {
Self.lock.unlock()
}
_read = newValue
}
} }
public var starred: Bool { public var starred: Bool {
@ -56,13 +49,6 @@ public final class ArticleStatus: Hashable, @unchecked Sendable {
return _starred return _starred
} }
set {
Self.lock.lock()
defer {
Self.lock.unlock()
}
_starred = newValue
}
} }
private var _read = false private var _read = false
@ -89,11 +75,17 @@ public final class ArticleStatus: Hashable, @unchecked Sendable {
} }
public func setBoolStatus(_ status: Bool, forKey key: ArticleStatus.Key) { public func setBoolStatus(_ status: Bool, forKey key: ArticleStatus.Key) {
Self.lock.lock()
defer {
Self.lock.unlock()
}
switch key { switch key {
case .read: case .read:
read = status _read = status
case .starred: case .starred:
starred = status _starred = status
} }
} }

View File

@ -97,7 +97,7 @@ public actor ArticlesDatabase {
return articlesTable.articles(articleIDs: articleIDs, database: database) return articlesTable.articles(articleIDs: articleIDs, database: database)
} }
public func unreadArticles(feedIDs: Set<String>, limit: Int?) throws -> Set<Article> { public func unreadArticles(feedIDs: Set<String>, limit: Int? = nil) throws -> Set<Article> {
guard let database else { guard let database else {
throw DatabaseError.suspended throw DatabaseError.suspended
@ -105,7 +105,7 @@ public actor ArticlesDatabase {
return articlesTable.unreadArticles(feedIDs: feedIDs, limit: limit, database: database) return articlesTable.unreadArticles(feedIDs: feedIDs, limit: limit, database: database)
} }
public func todayArticles(feedIDs: Set<String>, limit: Int?) throws -> Set<Article> { public func todayArticles(feedIDs: Set<String>, limit: Int? = nil) throws -> Set<Article> {
guard let database else { guard let database else {
throw DatabaseError.suspended throw DatabaseError.suspended
@ -113,7 +113,7 @@ public actor ArticlesDatabase {
return articlesTable.todayArticles(feedIDs: feedIDs, cutoffDate: todayCutoffDate(), limit: limit, database: database) return articlesTable.todayArticles(feedIDs: feedIDs, cutoffDate: todayCutoffDate(), limit: limit, database: database)
} }
public func starredArticles(feedIDs: Set<String>, limit: Int?) throws -> Set<Article> { public func starredArticles(feedIDs: Set<String>, limit: Int? = nil) throws -> Set<Article> {
guard let database else { guard let database else {
throw DatabaseError.suspended throw DatabaseError.suspended

View File

@ -42,9 +42,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articles = try await articles(feedID: feedID) let articles = try await articles(feedID: feedID)
completion(.success(articles)) callArticleSetCompletion(completion, .success(articles))
} catch { } catch {
completion(.failure(.suspended)) callArticleSetCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -54,9 +54,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articles = try await articles(feedIDs: feedIDs) let articles = try await articles(feedIDs: feedIDs)
completion(.success(articles)) callArticleSetCompletion(completion, .success(articles))
} catch { } catch {
completion(.failure(.suspended)) callArticleSetCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -66,9 +66,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articles = try await articles(articleIDs: articleIDs) let articles = try await articles(articleIDs: articleIDs)
completion(.success(articles)) callArticleSetCompletion(completion, .success(articles))
} catch { } catch {
completion(.failure(.suspended)) callArticleSetCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -78,9 +78,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articles = try await unreadArticles(feedIDs: feedIDs, limit: limit) let articles = try await unreadArticles(feedIDs: feedIDs, limit: limit)
completion(.success(articles)) callArticleSetCompletion(completion, .success(articles))
} catch { } catch {
completion(.failure(.suspended)) callArticleSetCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -90,9 +90,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articles = try await todayArticles(feedIDs: feedIDs, limit: limit) let articles = try await todayArticles(feedIDs: feedIDs, limit: limit)
completion(.success(articles)) callArticleSetCompletion(completion, .success(articles))
} catch { } catch {
completion(.failure(.suspended)) callArticleSetCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -102,9 +102,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articles = try await starredArticles(feedIDs: feedIDs, limit: limit) let articles = try await starredArticles(feedIDs: feedIDs, limit: limit)
completion(.success(articles)) callArticleSetCompletion(completion, .success(articles))
} catch { } catch {
completion(.failure(.suspended)) callArticleSetCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -114,9 +114,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articles = try await articlesMatching(searchString: searchString, feedIDs: feedIDs) let articles = try await articlesMatching(searchString: searchString, feedIDs: feedIDs)
completion(.success(articles)) callArticleSetCompletion(completion, .success(articles))
} catch { } catch {
completion(.failure(.suspended)) callArticleSetCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -126,9 +126,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articles = try await articlesMatching(searchString: searchString, articleIDs: articleIDs) let articles = try await articlesMatching(searchString: searchString, articleIDs: articleIDs)
completion(.success(articles)) callArticleSetCompletion(completion, .success(articles))
} catch { } catch {
completion(.failure(.suspended)) callArticleSetCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -141,9 +141,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let unreadCountDictionary = try await allUnreadCounts() let unreadCountDictionary = try await allUnreadCounts()
completion(.success(unreadCountDictionary)) callUnreadCountDictionaryCompletion(completion, .success(unreadCountDictionary))
} catch { } catch {
completion(.failure(.suspended)) callUnreadCountDictionaryCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -154,9 +154,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let unreadCount = try await unreadCount(feedID: feedID) ?? 0 let unreadCount = try await unreadCount(feedID: feedID) ?? 0
completion(.success(unreadCount)) callSingleUnreadCountCompletion(completion, .success(unreadCount))
} catch { } catch {
completion(.failure(.suspended)) callSingleUnreadCountCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -167,9 +167,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let unreadCountDictionary = try await unreadCounts(feedIDs: feedIDs) let unreadCountDictionary = try await unreadCounts(feedIDs: feedIDs)
completion(.success(unreadCountDictionary)) callUnreadCountDictionaryCompletion(completion, .success(unreadCountDictionary))
} catch { } catch {
completion(.failure(.suspended)) callUnreadCountDictionaryCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -179,9 +179,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let unreadCount = try await unreadCountForToday(feedIDs: feedIDs)! let unreadCount = try await unreadCountForToday(feedIDs: feedIDs)!
completion(.success(unreadCount)) callSingleUnreadCountCompletion(completion, .success(unreadCount))
} catch { } catch {
completion(.failure(.suspended)) callSingleUnreadCountCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -191,9 +191,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let unreadCount = try await unreadCount(feedIDs: feedIDs, since: since)! let unreadCount = try await unreadCount(feedIDs: feedIDs, since: since)!
completion(.success(unreadCount)) callSingleUnreadCountCompletion(completion, .success(unreadCount))
} catch { } catch {
completion(.failure(.suspended)) callSingleUnreadCountCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -203,9 +203,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let unreadCount = try await starredAndUnreadCount(feedIDs: feedIDs)! let unreadCount = try await starredAndUnreadCount(feedIDs: feedIDs)!
completion(.success(unreadCount)) callSingleUnreadCountCompletion(completion, .success(unreadCount))
} catch { } catch {
completion(.failure(.suspended)) callSingleUnreadCountCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -218,9 +218,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articleChanges = try await update(parsedItems: parsedItems, feedID: feedID, deleteOlder: deleteOlder) let articleChanges = try await update(parsedItems: parsedItems, feedID: feedID, deleteOlder: deleteOlder)
completion(.success(articleChanges)) callUpdateArticlesCompletion(completion, .success(articleChanges))
} catch { } catch {
completion(.failure(.suspended)) callUpdateArticlesCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -231,9 +231,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articleChanges = try await update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) let articleChanges = try await update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead)
completion(.success(articleChanges)) callUpdateArticlesCompletion(completion, .success(articleChanges))
} catch { } catch {
completion(.failure(.suspended)) callUpdateArticlesCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -244,9 +244,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
try await delete(articleIDs: articleIDs) try await delete(articleIDs: articleIDs)
completion?(nil) callDatabaseCompletion(completion)
} catch { } catch {
completion?(.suspended) callDatabaseCompletion(completion, .suspended)
} }
} }
} }
@ -259,9 +259,9 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articleIDs = try await unreadArticleIDs()! let articleIDs = try await unreadArticleIDs()!
completion(.success(articleIDs)) callArticleIDsCompletion(completion, .success(articleIDs))
} catch { } catch {
completion(.failure(.suspended)) callArticleIDsCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -272,46 +272,46 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
let articleIDs = try await starredArticleIDs()! let articleIDs = try await starredArticleIDs()!
completion(.success(articleIDs)) callArticleIDsCompletion(completion, .success(articleIDs))
} catch { } catch {
completion(.failure(.suspended)) callArticleIDsCompletion(completion, .failure(.suspended))
} }
} }
} }
/// Fetch articleIDs for articles that we should have, but dont. These articles are either (starred) or (newer than the article cutoff date). /// Fetch articleIDs for articles that we should have, but dont. These articles are either (starred) or (newer than the article cutoff date).
nonisolated func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) { nonisolated func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) {
Task { Task {
do { do {
let articleIDs = try await articleIDsForStatusesWithoutArticlesNewerThanCutoffDate()! let articleIDs = try await articleIDsForStatusesWithoutArticlesNewerThanCutoffDate()!
completion(.success(articleIDs)) callArticleIDsCompletion(completion, .success(articleIDs))
} catch { } catch {
completion(.failure(.suspended)) callArticleIDsCompletion(completion, .failure(.suspended))
} }
} }
} }
nonisolated func mark(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping ArticleStatusesResultBlock) { nonisolated func mark(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping ArticleStatusesResultBlock) {
Task { Task {
do { do {
let statuses = try await mark(articles: articles, statusKey: statusKey, flag: flag)! let statuses = try await mark(articles: articles, statusKey: statusKey, flag: flag)!
completion(.success(statuses)) callArticleStatusesCompletion(completion, .success(statuses))
} catch { } catch {
completion(.failure(.suspended)) callArticleStatusesCompletion(completion, .failure(.suspended))
} }
} }
} }
nonisolated func markAndFetchNew(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping ArticleIDsCompletionBlock) { nonisolated func markAndFetchNew(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping ArticleIDsCompletionBlock) {
Task { Task {
do { do {
let statuses = try await markAndFetchNew(articleIDs: articleIDs, statusKey: statusKey, flag: flag) let statuses = try await markAndFetchNew(articleIDs: articleIDs, statusKey: statusKey, flag: flag)
completion(.success(statuses)) callArticleIDsCompletion(completion, .success(statuses))
} catch { } catch {
completion(.failure(.suspended)) callArticleIDsCompletion(completion, .failure(.suspended))
} }
} }
} }
@ -323,10 +323,63 @@ public extension ArticlesDatabase {
Task { Task {
do { do {
try await createStatusesIfNeeded(articleIDs: articleIDs) try await createStatusesIfNeeded(articleIDs: articleIDs)
completion(nil) callDatabaseCompletion(completion)
} catch { } 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)
}
}
} }

View File

@ -968,42 +968,68 @@ extension AppDelegate: NSWindowRestoration {
private extension AppDelegate { private extension AppDelegate {
func handleMarkAsRead(userInfo: [AnyHashable: Any]) { struct ArticlePathInfo {
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let accountID: String
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { let articleID: String
return
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.") os_log(.debug, "No account found from notification.")
return return
} }
let article = try? account!.fetchArticles(.articleIDs([articleID])) let articleID = articlePathInfo.articleID
guard article != nil else {
os_log(.debug, "No article found from search using %@", articleID) Task {
return 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]) { func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else {
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { return
return
} }
let account = AccountManager.shared.existingAccount(with: accountID) guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else {
guard account != nil else {
os_log(.debug, "No account found from notification.") os_log(.debug, "No account found from notification.")
return return
} }
let article = try? account!.fetchArticles(.articleIDs([articleID])) let articleID = articlePathInfo.articleID
guard article != nil else {
os_log(.debug, "No article found from search using %@", articleID) Task {
return
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 }
} }
} }

View File

@ -69,11 +69,13 @@ extension SidebarViewController {
return return
} }
let articles = unreadArticles(for: objects) Task {
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else { let articles = await unreadArticles(for: objects)
return 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?) { @objc func deleteFromContextualMenu(_ sender: Any?) {
@ -349,12 +351,12 @@ private extension SidebarViewController {
return item return item
} }
func unreadArticles(for objects: [Any]) -> Set<Article> { @MainActor func unreadArticles(for objects: [Any]) async -> Set<Article> {
var articles = Set<Article>() var articles = Set<Article>()
for object in objects { for object in objects {
if let articleFetcher = object as? ArticleFetcher { if let articleFetcher = object as? ArticleFetcher {
if let unreadArticles = try? articleFetcher.fetchUnreadArticles() { if let unreadArticles = try? await articleFetcher.fetchUnreadArticles() {
articles.formUnion(unreadArticles) articles.formUnion(unreadArticles)
} }
} }

View File

@ -253,18 +253,33 @@ protocol SidebarDelegate: AnyObject {
} }
} }
@IBAction func doubleClickedSidebar(_ sender: Any?) { @MainActor @IBAction func doubleClickedSidebar(_ sender: Any?) {
guard outlineView.clickedRow == outlineView.selectedRow else { guard outlineView.clickedRow == outlineView.selectedRow else {
return return
} }
if AppDefaults.shared.feedDoubleClickMarkAsRead, let articles = try? singleSelectedFeed?.fetchUnreadArticles() {
if let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) { if AppDefaults.shared.feedDoubleClickMarkAsRead, let feed = singleSelectedFeed {
runCommand(markReadCommand) Task { @MainActor in
await markArticlesInFeedAsRead(feed: feed)
} }
} }
openInBrowser(sender) 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?) { @IBAction func openInBrowser(_ sender: Any?) {
guard let feed = singleSelectedFeed, let homePageURL = feed.homePageURL else { guard let feed = singleSelectedFeed, let homePageURL = feed.homePageURL else {
return return

View File

@ -258,11 +258,8 @@ private extension TimelineViewController {
} }
func markAllAsReadMenuItem(_ feed: Feed) -> NSMenuItem? { func markAllAsReadMenuItem(_ feed: Feed) -> NSMenuItem? {
guard let articlesSet = try? feed.fetchArticles() else {
return nil guard feed.unreadCount > 0 else {
}
let articles = Array(articlesSet)
guard articles.canMarkAllAsRead() else {
return nil return nil
} }

View File

@ -63,14 +63,16 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
var representedObjects: [AnyObject]? { var representedObjects: [AnyObject]? {
didSet { didSet {
if !representedObjectArraysAreEqual(oldValue, representedObjects) { guard !representedObjectArraysAreEqual(oldValue, representedObjects) else {
unreadCount = 0 return
}
selectionDidChange(nil) unreadCount = 0
if showsSearchResults { selectionDidChange(nil)
fetchAndReplaceArticlesAsync()
} else { Task {
fetchAndReplaceArticlesSync() await fetchAndReplaceArticles()
if !showsSearchResults {
if articles.count > 0 { if articles.count > 0 {
tableView.scrollRowToVisible(0) tableView.scrollRowToVisible(0)
} }
@ -290,29 +292,31 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
if let articlePathUserInfo = state[UserInfoKey.articlePath] as? [AnyHashable : Any], if let articlePathUserInfo = state[UserInfoKey.articlePath] as? [AnyHashable : Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let account = AccountManager.shared.existingAccount(with: accountID), let account = AccountManager.shared.existingAccount(with: accountID),
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String { let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String {
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID) exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
fetchAndReplaceArticlesSync()
Task {
if let selectedIndex = articles.firstIndex(where: { $0.articleID == articleID }) { await fetchAndReplaceArticles()
tableView.selectRow(selectedIndex)
DispatchQueue.main.async { Task { @MainActor in
self.tableView.scrollTo(row: selectedIndex) if let selectedIndex = articles.firstIndex(where: { $0.articleID == articleID }) {
tableView.selectRow(selectedIndex)
self.tableView.scrollTo(row: selectedIndex)
focus()
}
} }
focus()
} }
} else { } else {
Task {
fetchAndReplaceArticlesSync() await fetchAndReplaceArticles()
}
} }
} }
// MARK: - Actions // MARK: - Actions
@objc func openArticleInBrowser(_ sender: Any?) { @objc func openArticleInBrowser(_ sender: Any?) {
@ -534,19 +538,21 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
func goToDeepLink(for userInfo: [AnyHashable : Any]) { func goToDeepLink(for userInfo: [AnyHashable : Any]) {
guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return } guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return }
if isReadFiltered ?? false { Task {
if let accountName = userInfo[ArticlePathKey.accountName] as? String, if isReadFiltered ?? false {
let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) { if let accountName = userInfo[ArticlePathKey.accountName] as? String,
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID) let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) {
fetchAndReplaceArticlesSync() exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
await fetchAndReplaceArticles()
}
} }
}
guard let ix = articles.firstIndex(where: { $0.articleID == articleID }) else { return } guard let ix = articles.firstIndex(where: { $0.articleID == articleID }) else { return }
NSCursor.setHiddenUntilMouseMoves(true) NSCursor.setHiddenUntilMouseMoves(true)
tableView.selectRow(ix) tableView.selectRow(ix)
tableView.scrollTo(row: ix) tableView.scrollTo(row: ix)
}
} }
func goToNextUnread(wrappingToTop wrapping: Bool = false) { func goToNextUnread(wrappingToTop wrapping: Bool = false) {
@ -645,19 +651,25 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
@objc func accountStateDidChange(_ note: Notification) { @objc func accountStateDidChange(_ note: Notification) {
if representedObjectsContainsAnyPseudoFeed() { if representedObjectsContainsAnyPseudoFeed() {
fetchAndReplaceArticlesAsync() Task {
await fetchAndReplaceArticles()
}
} }
} }
@objc func accountsDidChange(_ note: Notification) { @objc func accountsDidChange(_ note: Notification) {
if representedObjectsContainsAnyPseudoFeed() { if representedObjectsContainsAnyPseudoFeed() {
fetchAndReplaceArticlesAsync() Task {
await fetchAndReplaceArticles()
}
} }
} }
@objc func containerChildrenDidChange(_ note: Notification) { @objc func containerChildrenDidChange(_ note: Notification) {
if representedObjectsContainsAnyPseudoFeed() || representedObjectsContainAnyFolder() { if representedObjectsContainsAnyPseudoFeed() || representedObjectsContainAnyFolder() {
fetchAndReplaceArticlesAsync() Task {
await fetchAndReplaceArticles()
}
} }
} }
@ -949,8 +961,10 @@ private extension TimelineViewController {
if let article = oneSelectedArticle, let account = article.account { if let article = oneSelectedArticle, let account = article.account {
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: article.articleID) exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: article.articleID)
} }
performBlockAndRestoreSelection { Task {
fetchAndReplaceArticlesSync() await performAsyncBlockAndRestoreSelection {
await fetchAndReplaceArticles()
}
} }
} }
@ -1033,6 +1047,12 @@ private extension TimelineViewController {
restoreSelection(savedSelection) restoreSelection(savedSelection)
} }
func performAsyncBlockAndRestoreSelection(_ block: (() async -> Void)) async {
let savedSelection = selectedArticleIDs()
await block()
restoreSelection(savedSelection)
}
func rows(for articleID: String) -> [Int]? { func rows(for articleID: String) -> [Int]? {
updateArticleRowMapIfNeeded() updateArticleRowMapIfNeeded()
return articleRowMap[articleID] return articleRowMap[articleID]
@ -1091,44 +1111,43 @@ private extension TimelineViewController {
// MARK: - Fetching Articles // MARK: - Fetching Articles
func fetchAndReplaceArticlesSync() { func fetchAndReplaceArticles() async {
// To be called when the user has made a change of selection in the sidebar.
// It blocks the main thread, so that theres no async delay,
// so that the entire display refreshes at once.
// Its a better user experience this way.
cancelPendingAsyncFetches() cancelPendingAsyncFetches()
guard var representedObjects = representedObjects else { guard var representedObjects = representedObjects else {
emptyTheTimeline() emptyTheTimeline()
return return
} }
if exceptionArticleFetcher != nil { if let exceptionArticleFetcher {
representedObjects.append(exceptionArticleFetcher as AnyObject) representedObjects.append(exceptionArticleFetcher as AnyObject)
exceptionArticleFetcher = nil self.exceptionArticleFetcher = nil
} }
let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects) let fetchedArticles = await unsortedArticles(for: representedObjects)
replaceArticles(with: fetchedArticles) replaceArticles(with: fetchedArticles)
} }
func fetchAndReplaceArticlesAsync() { // func fetchAndReplaceArticlesSync() {
// To be called when we need to do an entire fetch, but an async delay is okay. // // To be called when the user has made a change of selection in the sidebar.
// Example: we have the Today feed selected, and the calendar day just changed. // // It blocks the main thread, so that theres no async delay,
cancelPendingAsyncFetches() // // so that the entire display refreshes at once.
guard var representedObjects = representedObjects else { // // Its a better user experience this way.
emptyTheTimeline() // cancelPendingAsyncFetches()
return // guard var representedObjects = representedObjects else {
} // emptyTheTimeline()
// return
if exceptionArticleFetcher != nil { // }
representedObjects.append(exceptionArticleFetcher as AnyObject) //
exceptionArticleFetcher = nil // if exceptionArticleFetcher != nil {
} // representedObjects.append(exceptionArticleFetcher as AnyObject)
// exceptionArticleFetcher = nil
fetchUnsortedArticlesAsync(for: representedObjects) { [weak self] (articles) in // }
self?.replaceArticles(with: articles) //
} // let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects)
} // replaceArticles(with: fetchedArticles)
// }
func cancelPendingAsyncFetches() { func cancelPendingAsyncFetches() {
fetchSerialNumber += 1 fetchSerialNumber += 1
@ -1139,27 +1158,27 @@ private extension TimelineViewController {
articles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed) articles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed)
} }
func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<Article> { // func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<Article> {
cancelPendingAsyncFetches() // cancelPendingAsyncFetches()
let fetchers = representedObjects.compactMap{ $0 as? ArticleFetcher } // let fetchers = representedObjects.compactMap{ $0 as? ArticleFetcher }
if fetchers.isEmpty { // if fetchers.isEmpty {
return Set<Article>() // return Set<Article>()
} // }
//
var fetchedArticles = Set<Article>() // var fetchedArticles = Set<Article>()
for fetchers in fetchers { // for fetchers in fetchers {
if (fetchers as? SidebarItem)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true { // if (fetchers as? SidebarItem)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true {
if let articles = try? fetchers.fetchUnreadArticles() { // if let articles = try? fetchers.fetchUnreadArticles() {
fetchedArticles.formUnion(articles) // fetchedArticles.formUnion(articles)
} // }
} else { // } else {
if let articles = try? fetchers.fetchArticles() { // if let articles = try? fetchers.fetchArticles() {
fetchedArticles.formUnion(articles) // fetchedArticles.formUnion(articles)
} // }
} // }
} // }
return fetchedArticles // return fetchedArticles
} // }
func fetchUnsortedArticlesAsync(for representedObjects: [Any], completion: @escaping ArticleSetBlock) { func fetchUnsortedArticlesAsync(for representedObjects: [Any], completion: @escaping ArticleSetBlock) {
// The callback will *not* be called if the fetch is no longer relevant that is, // 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) fetchRequestQueue.add(fetchOperation)
} }
func unsortedArticles(for representedObjects: [Any]) async -> Set<Article> {
let fetchers = representedObjects.compactMap{ $0 as? ArticleFetcher }
if fetchers.isEmpty {
return Set<Article>()
}
var fetchedArticles = Set<Article>()
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]) { func selectArticles(_ articleIDs: [String]) {
let indexesToSelect = indexesForArticleIDs(Set(articleIDs)) let indexesToSelect = indexesForArticleIDs(Set(articleIDs))
if indexesToSelect.isEmpty { if indexesToSelect.isEmpty {

View File

@ -12,7 +12,7 @@ import Account
import Articles import Articles
@objc(ScriptableFeed) @objc(ScriptableFeed)
class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { @objcMembers class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let feed:Feed let feed:Feed
let container:ScriptingObjectContainer let container:ScriptingObjectContainer
@ -163,21 +163,21 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
return ScriptableAuthor(author, container:self) return ScriptableAuthor(author, container:self)
} }
@objc(articles) // @objc(articles)
var articles:NSArray { // var articles:NSArray {
let feedArticles = (try? feed.fetchArticles()) ?? Set<Article>() // let feedArticles = (try? feed.fetchArticles()) ?? Set<Article>()
// the articles are a set, use the sorting algorithm from the viewer // // the articles are a set, use the sorting algorithm from the viewer
let sortedArticles = feedArticles.sorted(by:{ // let sortedArticles = feedArticles.sorted(by:{
return $0.logicalDatePublished > $1.logicalDatePublished // return $0.logicalDatePublished > $1.logicalDatePublished
}) // })
return sortedArticles.map { ScriptableArticle($0, container:self) } as NSArray // return sortedArticles.map { ScriptableArticle($0, container:self) } as NSArray
} // }
@objc(valueInArticlesWithUniqueID:) // @objc(valueInArticlesWithUniqueID:)
func valueInArticles(withUniqueID id:String) -> ScriptableArticle? { // func valueInArticles(withUniqueID id:String) -> ScriptableArticle? {
let articles = (try? feed.fetchArticles()) ?? Set<Article>() // let articles = (try? feed.fetchArticles()) ?? Set<Article>()
guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil } // guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil }
return ScriptableArticle(article, container:self) // return ScriptableArticle(article, container:self)
} // }
} }

View File

@ -286,10 +286,12 @@ private extension ActivityManager {
return "account_\(article.accountID)_feed_\(article.feedID)_article_\(article.articleID)" 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]() var ids = [String]()
ids.append(identifier(for: feed)) ids.append(identifier(for: feed))
if let articles = try? feed.fetchArticles() {
if let articles = try? await feed.fetchArticles() {
for article in articles { for article in articles {
ids.append(identifier(for: article)) ids.append(identifier(for: article))
} }

View File

@ -84,11 +84,23 @@ final class SmartFeed: PseudoFeed {
extension SmartFeed: ArticleFetcher { extension SmartFeed: ArticleFetcher {
func fetchArticles() async throws -> Set<Article> {
try await delegate.fetchArticles()
}
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
delegate.fetchArticlesAsync(completion) delegate.fetchArticlesAsync(completion)
} }
func fetchUnreadArticles() async throws -> Set<Article> {
try await delegate.fetchUnreadArticles()
}
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
delegate.fetchUnreadArticlesAsync(completion) delegate.fetchUnreadArticlesAsync(completion)
} }
} }

View File

@ -14,25 +14,30 @@ import RSCore
import Database import Database
protocol SmartFeedDelegate: SidebarItemIdentifiable, DisplayNameProvider, ArticleFetcher, SmallIconProvider { protocol SmartFeedDelegate: SidebarItemIdentifiable, DisplayNameProvider, ArticleFetcher, SmallIconProvider {
var fetchType: FetchType { get } var fetchType: FetchType { get }
func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock) func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock)
} }
extension SmartFeedDelegate { extension SmartFeedDelegate {
func fetchArticles() throws -> Set<Article> { @MainActor func fetchArticles() async throws -> Set<Article> {
return try AccountManager.shared.fetchArticles(fetchType)
try await AccountManager.shared.fetchArticles(fetchType: fetchType)
} }
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { @MainActor func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
AccountManager.shared.fetchArticlesAsync(fetchType, completion) AccountManager.shared.fetchArticlesAsync(fetchType, completion)
} }
func fetchUnreadArticles() throws -> Set<Article> { @MainActor func fetchUnreadArticles() async throws -> Set<Article> {
return try fetchArticles().unreadArticles()
try await AccountManager.shared.fetchArticles(fetchType: fetchType)
} }
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { @MainActor func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync{ articleSetResult in fetchArticlesAsync{ articleSetResult in
switch articleSetResult { switch articleSetResult {
case .success(let articles): case .success(let articles):

View File

@ -66,11 +66,24 @@ final class UnreadFeed: PseudoFeed {
extension UnreadFeed: ArticleFetcher { extension UnreadFeed: ArticleFetcher {
// Always fetches unread articles
func fetchArticles() async throws -> Set<Article> {
try await fetchUnreadArticles()
}
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
fetchUnreadArticlesAsync(completion) fetchUnreadArticlesAsync(completion)
} }
func fetchUnreadArticles() async throws -> Set<Article> {
try await AccountManager.shared.fetchArticles(fetchType: fetchType)
}
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
AccountManager.shared.fetchArticlesAsync(fetchType, completion) AccountManager.shared.fetchArticlesAsync(fetchType, completion)
} }
} }

View File

@ -34,7 +34,7 @@ final class FetchRequestOperation {
self.resultBlock = resultBlock self.resultBlock = resultBlock
} }
func run(_ completion: @escaping (FetchRequestOperation) -> Void) { @MainActor func run(_ completion: @escaping (FetchRequestOperation) -> Void) {
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
precondition(!isFinished) precondition(!isFinished)

View File

@ -8,9 +8,7 @@
import Foundation import Foundation
// Main thread only. @MainActor final class FetchRequestQueue {
final class FetchRequestQueue {
private var pendingRequests = [FetchRequestOperation]() private var pendingRequests = [FetchRequestOperation]()
private var currentRequest: FetchRequestOperation? = nil private var currentRequest: FetchRequestOperation? = nil