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()
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<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) {
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<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) {
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<Article> {
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<Article> {
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<Article> {
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<Article> {
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<Article> {
func articlesMatching(searchString: String) async throws -> Set<Article> {
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<String>) async throws -> Set<Article> {
try await database.articlesMatching(searchString: searchString, articleIDs: articleIDs)
}
func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
database.fetchArticlesMatchingWithArticleIDsAsync(searchString, articleIDs, completion)
}
func fetchArticlesAsync(articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
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) {
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<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) {
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<String> {
flattenedFeeds().feedIDs()
}
func rebuildFeedDictionaries() {
var idDictionary = [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>.
@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) {
precondition(Thread.isMainThread)

View File

@ -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<Article>
@MainActor func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
@MainActor func fetchUnreadArticles() async throws -> Set<Article>
@MainActor func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
}
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) {
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<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) {
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<Articles.Article> {
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<Article>()))
return
}
account.fetchArticlesAsync(.folder(self, false), completion)
}
public func fetchUnreadArticles() async throws -> Set<Article> {
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<Article>()))
return
}
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 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<Article> {
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 })
}
func unreadArticles() -> Set<Article> {
@MainActor func unreadArticles() -> Set<Article> {
let articles = self.filter { !$0.status.read }
return Set(articles)
}

View File

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

View File

@ -97,7 +97,7 @@ public actor ArticlesDatabase {
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 {
throw DatabaseError.suspended
@ -105,7 +105,7 @@ public actor ArticlesDatabase {
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 {
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<String>, limit: Int?) throws -> Set<Article> {
public func starredArticles(feedIDs: Set<String>, limit: Int? = nil) throws -> Set<Article> {
guard let database else {
throw DatabaseError.suspended

View File

@ -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 dont. 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<Article>, 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<String>, 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)
}
}
}

View File

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

View File

@ -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<Article> {
@MainActor func unreadArticles(for objects: [Any]) async -> Set<Article> {
var articles = Set<Article>()
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)
}
}

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

View File

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

View File

@ -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 theres no async delay,
// so that the entire display refreshes at once.
// Its 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 theres no async delay,
// // so that the entire display refreshes at once.
// // Its 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<Article> {
cancelPendingAsyncFetches()
let fetchers = representedObjects.compactMap{ $0 as? ArticleFetcher }
if fetchers.isEmpty {
return Set<Article>()
}
var fetchedArticles = Set<Article>()
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<Article> {
// cancelPendingAsyncFetches()
// let fetchers = representedObjects.compactMap{ $0 as? ArticleFetcher }
// if fetchers.isEmpty {
// return Set<Article>()
// }
//
// var fetchedArticles = Set<Article>()
// 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<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]) {
let indexesToSelect = indexesForArticleIDs(Set(articleIDs))
if indexesToSelect.isEmpty {

View File

@ -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<Article>()
// 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<Article>()
// // 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<Article>()
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<Article>()
// guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil }
// return ScriptableArticle(article, container:self)
// }
}

View File

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

View File

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

View File

@ -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<Article> {
return try AccountManager.shared.fetchArticles(fetchType)
@MainActor func fetchArticles() async throws -> Set<Article> {
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<Article> {
return try fetchArticles().unreadArticles()
@MainActor func fetchUnreadArticles() async throws -> Set<Article> {
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):

View File

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

View File

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

View File

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