mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-02-02 20:16:54 +01:00
Fix many build errors.
This commit is contained in:
parent
2a44e1ccf1
commit
e58f8ada42
@ -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]()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
// Doesn’t 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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -42,9 +42,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articles = try await articles(feedID: feedID)
|
||||
completion(.success(articles))
|
||||
callArticleSetCompletion(completion, .success(articles))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleSetCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -54,9 +54,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articles = try await articles(feedIDs: feedIDs)
|
||||
completion(.success(articles))
|
||||
callArticleSetCompletion(completion, .success(articles))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleSetCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,9 +66,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articles = try await articles(articleIDs: articleIDs)
|
||||
completion(.success(articles))
|
||||
callArticleSetCompletion(completion, .success(articles))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleSetCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,9 +78,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articles = try await unreadArticles(feedIDs: feedIDs, limit: limit)
|
||||
completion(.success(articles))
|
||||
callArticleSetCompletion(completion, .success(articles))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleSetCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -90,9 +90,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articles = try await todayArticles(feedIDs: feedIDs, limit: limit)
|
||||
completion(.success(articles))
|
||||
callArticleSetCompletion(completion, .success(articles))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleSetCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,9 +102,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articles = try await starredArticles(feedIDs: feedIDs, limit: limit)
|
||||
completion(.success(articles))
|
||||
callArticleSetCompletion(completion, .success(articles))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleSetCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -114,9 +114,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articles = try await articlesMatching(searchString: searchString, feedIDs: feedIDs)
|
||||
completion(.success(articles))
|
||||
callArticleSetCompletion(completion, .success(articles))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleSetCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,9 +126,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articles = try await articlesMatching(searchString: searchString, articleIDs: articleIDs)
|
||||
completion(.success(articles))
|
||||
callArticleSetCompletion(completion, .success(articles))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleSetCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,9 +141,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let unreadCountDictionary = try await allUnreadCounts()
|
||||
completion(.success(unreadCountDictionary))
|
||||
callUnreadCountDictionaryCompletion(completion, .success(unreadCountDictionary))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callUnreadCountDictionaryCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,9 +154,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let unreadCount = try await unreadCount(feedID: feedID) ?? 0
|
||||
completion(.success(unreadCount))
|
||||
callSingleUnreadCountCompletion(completion, .success(unreadCount))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callSingleUnreadCountCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -167,9 +167,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let unreadCountDictionary = try await unreadCounts(feedIDs: feedIDs)
|
||||
completion(.success(unreadCountDictionary))
|
||||
callUnreadCountDictionaryCompletion(completion, .success(unreadCountDictionary))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callUnreadCountDictionaryCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -179,9 +179,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let unreadCount = try await unreadCountForToday(feedIDs: feedIDs)!
|
||||
completion(.success(unreadCount))
|
||||
callSingleUnreadCountCompletion(completion, .success(unreadCount))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callSingleUnreadCountCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -191,9 +191,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let unreadCount = try await unreadCount(feedIDs: feedIDs, since: since)!
|
||||
completion(.success(unreadCount))
|
||||
callSingleUnreadCountCompletion(completion, .success(unreadCount))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callSingleUnreadCountCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -203,9 +203,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let unreadCount = try await starredAndUnreadCount(feedIDs: feedIDs)!
|
||||
completion(.success(unreadCount))
|
||||
callSingleUnreadCountCompletion(completion, .success(unreadCount))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callSingleUnreadCountCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -218,9 +218,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articleChanges = try await update(parsedItems: parsedItems, feedID: feedID, deleteOlder: deleteOlder)
|
||||
completion(.success(articleChanges))
|
||||
callUpdateArticlesCompletion(completion, .success(articleChanges))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callUpdateArticlesCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -231,9 +231,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articleChanges = try await update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead)
|
||||
completion(.success(articleChanges))
|
||||
callUpdateArticlesCompletion(completion, .success(articleChanges))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callUpdateArticlesCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -244,9 +244,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
try await delete(articleIDs: articleIDs)
|
||||
completion?(nil)
|
||||
callDatabaseCompletion(completion)
|
||||
} catch {
|
||||
completion?(.suspended)
|
||||
callDatabaseCompletion(completion, .suspended)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -259,9 +259,9 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articleIDs = try await unreadArticleIDs()!
|
||||
completion(.success(articleIDs))
|
||||
callArticleIDsCompletion(completion, .success(articleIDs))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleIDsCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -272,46 +272,46 @@ public extension ArticlesDatabase {
|
||||
Task {
|
||||
do {
|
||||
let articleIDs = try await starredArticleIDs()!
|
||||
completion(.success(articleIDs))
|
||||
callArticleIDsCompletion(completion, .success(articleIDs))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleIDsCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch articleIDs for articles that we should have, but don’t. These articles are either (starred) or (newer than the article cutoff date).
|
||||
nonisolated func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) {
|
||||
|
||||
|
||||
Task {
|
||||
do {
|
||||
let articleIDs = try await articleIDsForStatusesWithoutArticlesNewerThanCutoffDate()!
|
||||
completion(.success(articleIDs))
|
||||
callArticleIDsCompletion(completion, .success(articleIDs))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
callArticleIDsCompletion(completion, .failure(.suspended))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func mark(_ articles: Set<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -63,14 +63,16 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
var representedObjects: [AnyObject]? {
|
||||
didSet {
|
||||
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
|
||||
unreadCount = 0
|
||||
guard !representedObjectArraysAreEqual(oldValue, representedObjects) else {
|
||||
return
|
||||
}
|
||||
|
||||
selectionDidChange(nil)
|
||||
if showsSearchResults {
|
||||
fetchAndReplaceArticlesAsync()
|
||||
} else {
|
||||
fetchAndReplaceArticlesSync()
|
||||
unreadCount = 0
|
||||
selectionDidChange(nil)
|
||||
|
||||
Task {
|
||||
await fetchAndReplaceArticles()
|
||||
if !showsSearchResults {
|
||||
if articles.count > 0 {
|
||||
tableView.scrollRowToVisible(0)
|
||||
}
|
||||
@ -290,29 +292,31 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
if let articlePathUserInfo = state[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let account = AccountManager.shared.existingAccount(with: accountID),
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String {
|
||||
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let account = AccountManager.shared.existingAccount(with: accountID),
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String {
|
||||
|
||||
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
|
||||
fetchAndReplaceArticlesSync()
|
||||
|
||||
if let selectedIndex = articles.firstIndex(where: { $0.articleID == articleID }) {
|
||||
tableView.selectRow(selectedIndex)
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.scrollTo(row: selectedIndex)
|
||||
|
||||
Task {
|
||||
await fetchAndReplaceArticles()
|
||||
|
||||
Task { @MainActor in
|
||||
if let selectedIndex = articles.firstIndex(where: { $0.articleID == articleID }) {
|
||||
|
||||
tableView.selectRow(selectedIndex)
|
||||
self.tableView.scrollTo(row: selectedIndex)
|
||||
focus()
|
||||
}
|
||||
}
|
||||
focus()
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
fetchAndReplaceArticlesSync()
|
||||
|
||||
Task {
|
||||
await fetchAndReplaceArticles()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc func openArticleInBrowser(_ sender: Any?) {
|
||||
@ -534,19 +538,21 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
func goToDeepLink(for userInfo: [AnyHashable : Any]) {
|
||||
guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return }
|
||||
|
||||
if isReadFiltered ?? false {
|
||||
if let accountName = userInfo[ArticlePathKey.accountName] as? String,
|
||||
let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) {
|
||||
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
|
||||
fetchAndReplaceArticlesSync()
|
||||
Task {
|
||||
if isReadFiltered ?? false {
|
||||
if let accountName = userInfo[ArticlePathKey.accountName] as? String,
|
||||
let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) {
|
||||
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
|
||||
await fetchAndReplaceArticles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let ix = articles.firstIndex(where: { $0.articleID == articleID }) else { return }
|
||||
|
||||
NSCursor.setHiddenUntilMouseMoves(true)
|
||||
tableView.selectRow(ix)
|
||||
tableView.scrollTo(row: ix)
|
||||
guard let ix = articles.firstIndex(where: { $0.articleID == articleID }) else { return }
|
||||
|
||||
NSCursor.setHiddenUntilMouseMoves(true)
|
||||
tableView.selectRow(ix)
|
||||
tableView.scrollTo(row: ix)
|
||||
}
|
||||
}
|
||||
|
||||
func goToNextUnread(wrappingToTop wrapping: Bool = false) {
|
||||
@ -645,19 +651,25 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
@objc func accountStateDidChange(_ note: Notification) {
|
||||
if representedObjectsContainsAnyPseudoFeed() {
|
||||
fetchAndReplaceArticlesAsync()
|
||||
Task {
|
||||
await fetchAndReplaceArticles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func accountsDidChange(_ note: Notification) {
|
||||
if representedObjectsContainsAnyPseudoFeed() {
|
||||
fetchAndReplaceArticlesAsync()
|
||||
Task {
|
||||
await fetchAndReplaceArticles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func containerChildrenDidChange(_ note: Notification) {
|
||||
if representedObjectsContainsAnyPseudoFeed() || representedObjectsContainAnyFolder() {
|
||||
fetchAndReplaceArticlesAsync()
|
||||
Task {
|
||||
await fetchAndReplaceArticles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -949,8 +961,10 @@ private extension TimelineViewController {
|
||||
if let article = oneSelectedArticle, let account = article.account {
|
||||
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: article.articleID)
|
||||
}
|
||||
performBlockAndRestoreSelection {
|
||||
fetchAndReplaceArticlesSync()
|
||||
Task {
|
||||
await performAsyncBlockAndRestoreSelection {
|
||||
await fetchAndReplaceArticles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1033,6 +1047,12 @@ private extension TimelineViewController {
|
||||
restoreSelection(savedSelection)
|
||||
}
|
||||
|
||||
func performAsyncBlockAndRestoreSelection(_ block: (() async -> Void)) async {
|
||||
let savedSelection = selectedArticleIDs()
|
||||
await block()
|
||||
restoreSelection(savedSelection)
|
||||
}
|
||||
|
||||
func rows(for articleID: String) -> [Int]? {
|
||||
updateArticleRowMapIfNeeded()
|
||||
return articleRowMap[articleID]
|
||||
@ -1091,44 +1111,43 @@ private extension TimelineViewController {
|
||||
|
||||
// MARK: - Fetching Articles
|
||||
|
||||
func fetchAndReplaceArticlesSync() {
|
||||
// To be called when the user has made a change of selection in the sidebar.
|
||||
// It blocks the main thread, so that there’s no async delay,
|
||||
// so that the entire display refreshes at once.
|
||||
// It’s a better user experience this way.
|
||||
func fetchAndReplaceArticles() async {
|
||||
|
||||
cancelPendingAsyncFetches()
|
||||
|
||||
guard var representedObjects = representedObjects else {
|
||||
emptyTheTimeline()
|
||||
return
|
||||
}
|
||||
|
||||
if exceptionArticleFetcher != nil {
|
||||
|
||||
if let exceptionArticleFetcher {
|
||||
representedObjects.append(exceptionArticleFetcher as AnyObject)
|
||||
exceptionArticleFetcher = nil
|
||||
self.exceptionArticleFetcher = nil
|
||||
}
|
||||
|
||||
let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects)
|
||||
|
||||
let fetchedArticles = await unsortedArticles(for: representedObjects)
|
||||
replaceArticles(with: fetchedArticles)
|
||||
}
|
||||
|
||||
func fetchAndReplaceArticlesAsync() {
|
||||
// To be called when we need to do an entire fetch, but an async delay is okay.
|
||||
// Example: we have the Today feed selected, and the calendar day just changed.
|
||||
cancelPendingAsyncFetches()
|
||||
guard var representedObjects = representedObjects else {
|
||||
emptyTheTimeline()
|
||||
return
|
||||
}
|
||||
|
||||
if exceptionArticleFetcher != nil {
|
||||
representedObjects.append(exceptionArticleFetcher as AnyObject)
|
||||
exceptionArticleFetcher = nil
|
||||
}
|
||||
|
||||
fetchUnsortedArticlesAsync(for: representedObjects) { [weak self] (articles) in
|
||||
self?.replaceArticles(with: articles)
|
||||
}
|
||||
}
|
||||
// func fetchAndReplaceArticlesSync() {
|
||||
// // To be called when the user has made a change of selection in the sidebar.
|
||||
// // It blocks the main thread, so that there’s no async delay,
|
||||
// // so that the entire display refreshes at once.
|
||||
// // It’s a better user experience this way.
|
||||
// cancelPendingAsyncFetches()
|
||||
// guard var representedObjects = representedObjects else {
|
||||
// emptyTheTimeline()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if exceptionArticleFetcher != nil {
|
||||
// representedObjects.append(exceptionArticleFetcher as AnyObject)
|
||||
// exceptionArticleFetcher = nil
|
||||
// }
|
||||
//
|
||||
// let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects)
|
||||
// replaceArticles(with: fetchedArticles)
|
||||
// }
|
||||
|
||||
func cancelPendingAsyncFetches() {
|
||||
fetchSerialNumber += 1
|
||||
@ -1139,27 +1158,27 @@ private extension TimelineViewController {
|
||||
articles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed)
|
||||
}
|
||||
|
||||
func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<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 {
|
||||
|
@ -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)
|
||||
// }
|
||||
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user