Run database fetches async, in the timeline, when appropriate — for instance, when All Unread is selected and new articles come in.

This commit is contained in:
Brent Simmons 2019-07-05 20:06:31 -07:00
parent 6f16a2715e
commit 7a204ad6ed
20 changed files with 715 additions and 378 deletions

View File

@ -36,6 +36,16 @@ public enum AccountType: Int {
// TODO: more // TODO: more
} }
public enum FetchType {
case starred
case unread
case today
case unreadForFolder(Folder)
case feed(Feed)
case articleIDs(Set<String>)
case search(String)
}
public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable { public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable {
public struct UserInfoKey { public struct UserInfoKey {
@ -471,85 +481,44 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
} }
} }
public func fetchArticles(forArticleIDs articleIDs: Set<String>) -> Set<Article> { public func fetchArticles(_ fetchType: FetchType) -> Set<Article> {
return database.fetchArticles(forArticleIDs: articleIDs) switch fetchType {
} case .starred:
return fetchStarredArticles()
public func fetchArticles(for feed: Feed) -> Set<Article> { case .unread:
return fetchUnreadArticles()
let articles = database.fetchArticles(for: feed.feedID) case .today:
validateUnreadCount(feed, articles) return fetchTodayArticles()
return articles case .unreadForFolder(let folder):
} return fetchArticles(folder: folder)
case .feed(let feed):
public func fetchUnreadArticles(for feed: Feed) -> Set<Article> { return fetchArticles(feed: feed)
case .articleIDs(let articleIDs):
let articles = database.fetchUnreadArticles(for: Set([feed.feedID])) return fetchArticles(articleIDs: articleIDs)
validateUnreadCount(feed, articles) case .search(let searchString):
return articles return fetchArticlesMatching(searchString)
}
public func fetchUnreadArticles() -> Set<Article> {
return fetchUnreadArticles(forContainer: self)
}
public func fetchArticles(folder: Folder) -> Set<Article> {
return fetchUnreadArticles(forContainer: folder)
}
public func fetchUnreadArticles(forContainer container: Container) -> Set<Article> {
let feeds = container.flattenedFeeds()
let articles = database.fetchUnreadArticles(for: feeds.feedIDs())
// Validate unread counts. This was the site of a performance slowdown:
// it was calling going through the entire list of articles once per feed:
// feeds.forEach { validateUnreadCount($0, articles) }
// Now we loop through articles exactly once. This makes a huge difference.
var unreadCountStorage = [String: Int]() // [FeedID: Int]
articles.forEach { (article) in
precondition(!article.status.read)
unreadCountStorage[article.feedID, default: 0] += 1
} }
feeds.forEach { (feed) in }
let unreadCount = unreadCountStorage[feed.feedID, default: 0]
feed.unreadCount = unreadCount public func fetchArticlesAsync(_ fetchType: FetchType, _ callback: @escaping ArticleSetBlock) {
switch fetchType {
case .starred:
fetchStarredArticlesAsync(callback)
case .unread:
fetchUnreadArticlesAsync(callback)
case .today:
fetchTodayArticlesAsync(callback)
case .unreadForFolder(let folder):
fetchArticlesAsync(folder: folder, callback)
case .feed(let feed):
fetchArticlesAsync(feed: feed, callback)
case .articleIDs(let articleIDs):
fetchArticlesAsync(articleIDs: articleIDs, callback)
case .search(let searchString):
fetchArticlesMatchingAsync(searchString, callback)
} }
return articles
} }
public func fetchTodayArticles() -> Set<Article> {
return database.fetchTodayArticles(for: flattenedFeeds().feedIDs())
}
public func fetchStarredArticles() -> Set<Article> {
return database.fetchStarredArticles(for: flattenedFeeds().feedIDs())
}
public func fetchArticlesMatching(_ searchString: String) -> Set<Article> {
return database.fetchArticlesMatching(searchString, for: flattenedFeeds().feedIDs())
}
private func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
// articles must contain all the unread articles for the feed.
// The unread number should match the feeds unread count.
let feedUnreadCount = articles.reduce(0) { (result, article) -> Int in
if article.feed == feed && !article.status.read {
return result + 1
}
return result
}
feed.unreadCount = feedUnreadCount
}
public func fetchUnreadCountForToday(_ callback: @escaping (Int) -> Void) { public func fetchUnreadCountForToday(_ callback: @escaping (Int) -> Void) {
@ -816,6 +785,133 @@ extension Account: FeedMetadataDelegate {
} }
} }
// MARK: - Fetching (Private)
private extension Account {
func fetchStarredArticles() -> Set<Article> {
return database.fetchStarredArticles(flattenedFeeds().feedIDs())
}
func fetchStarredArticlesAsync(_ callback: @escaping ArticleSetBlock) {
database.fetchedStarredArticlesAsync(flattenedFeeds().feedIDs(), callback)
}
func fetchUnreadArticles() -> Set<Article> {
return fetchUnreadArticles(forContainer: self)
}
func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
fetchUnreadArticlesAsync(forContainer: self, callback)
}
func fetchTodayArticles() -> Set<Article> {
return database.fetchTodayArticles(flattenedFeeds().feedIDs())
}
func fetchTodayArticlesAsync(_ callback: @escaping ArticleSetBlock) {
database.fetchTodayArticlesAsync(flattenedFeeds().feedIDs(), callback)
}
func fetchArticles(folder: Folder) -> Set<Article> {
return fetchUnreadArticles(forContainer: folder)
}
func fetchArticlesAsync(folder: Folder, _ callback: @escaping ArticleSetBlock) {
fetchUnreadArticlesAsync(forContainer: folder, callback)
}
func fetchArticles(feed: Feed) -> Set<Article> {
let articles = database.fetchArticles(feed.feedID)
validateUnreadCount(feed, articles)
return articles
}
func fetchArticlesAsync(feed: Feed, _ callback: @escaping ArticleSetBlock) {
database.fetchArticlesAsync(feed.feedID) { [weak self] (articles) in
self?.validateUnreadCount(feed, articles)
callback(articles)
}
}
func fetchArticlesMatching(_ searchString: String) -> Set<Article> {
return database.fetchArticlesMatching(searchString, flattenedFeeds().feedIDs())
}
func fetchArticlesMatchingAsync(_ searchString: String, _ callback: @escaping ArticleSetBlock) {
database.fetchArticlesMatchingAsync(searchString, flattenedFeeds().feedIDs(), callback)
}
func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
return database.fetchArticles(articleIDs: articleIDs)
}
func fetchArticlesAsync(articleIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
return database.fetchArticlesAsync(articleIDs: articleIDs, callback)
}
func fetchUnreadArticles(feed: Feed) -> Set<Article> {
let articles = database.fetchUnreadArticles(Set([feed.feedID]))
validateUnreadCount(feed, articles)
return articles
}
func fetchUnreadArticlesAsync(for feed: Feed, callback: @escaping (Set<Article>) -> Void) {
// database.fetchUnreadArticlesAsync(for: Set([feed.feedID])) { [weak self] (articles) in
// self?.validateUnreadCount(feed, articles)
// callback(articles)
// }
}
func fetchUnreadArticles(forContainer container: Container) -> Set<Article> {
let feeds = container.flattenedFeeds()
let articles = database.fetchUnreadArticles(feeds.feedIDs())
validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles)
return articles
}
func fetchUnreadArticlesAsync(forContainer container: Container, _ callback: @escaping ArticleSetBlock) {
let feeds = container.flattenedFeeds()
database.fetchUnreadArticlesAsync(feeds.feedIDs()) { [weak self] (articles) in
self?.validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles)
callback(articles)
}
}
func validateUnreadCountsAfterFetchingUnreadArticles(_ feeds: Set<Feed>, _ articles: Set<Article>) {
// Validate unread counts. This was the site of a performance slowdown:
// it was calling going through the entire list of articles once per feed:
// feeds.forEach { validateUnreadCount($0, articles) }
// Now we loop through articles exactly once. This makes a huge difference.
var unreadCountStorage = [String: Int]() // [FeedID: Int]
articles.forEach { (article) in
precondition(!article.status.read)
unreadCountStorage[article.feedID, default: 0] += 1
}
feeds.forEach { (feed) in
let unreadCount = unreadCountStorage[feed.feedID, default: 0]
feed.unreadCount = unreadCount
}
}
func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
// articles must contain all the unread articles for the feed.
// The unread number should match the feeds unread count.
let feedUnreadCount = articles.reduce(0) { (result, article) -> Int in
if article.feed == feed && !article.status.read {
return result + 1
}
return result
}
feed.unreadCount = feedUnreadCount
}
}
// MARK: - Disk (Private) // MARK: - Disk (Private)
private extension Account { private extension Account {

View File

@ -202,7 +202,39 @@ public final class AccountManager: UnreadCountProvider {
unreadCount = calculateUnreadCount(activeAccounts) unreadCount = calculateUnreadCount(activeAccounts)
} }
// MARK: - Fetching Articles
// These fetch articles from active accounts and return a merged Set<Article>.
public func fetchArticles(_ fetchType: FetchType) -> Set<Article> {
precondition(Thread.isMainThread)
var articles = Set<Article>()
for account in activeAccounts {
articles.formUnion(account.fetchArticles(fetchType))
}
return articles
}
public func fetchArticlesAsync(_ fetchType: FetchType, _ callback: @escaping ArticleSetBlock) {
precondition(Thread.isMainThread)
var allFetchedArticles = Set<Article>()
let numberOfAccounts = activeAccounts.count
var accountsReporting = 0
for account in activeAccounts {
account.fetchArticlesAsync(fetchType) { (articles) in
allFetchedArticles.formUnion(articles)
accountsReporting += 1
if accountsReporting == numberOfAccounts {
callback(allFetchedArticles)
}
}
}
}
// MARK: Notifications // MARK: Notifications
@objc dynamic func unreadCountDidChange(_ notification: Notification) { @objc dynamic func unreadCountDidChange(_ notification: Notification) {

View File

@ -12,44 +12,59 @@ import Articles
public protocol ArticleFetcher { public protocol ArticleFetcher {
func fetchArticles() -> Set<Article> func fetchArticles() -> Set<Article>
func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock)
func fetchUnreadArticles() -> Set<Article> func fetchUnreadArticles() -> Set<Article>
func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock)
} }
extension Feed: ArticleFetcher { extension Feed: ArticleFetcher {
public func fetchArticles() -> Set<Article> { public func fetchArticles() -> Set<Article> {
return account?.fetchArticles(.feed(self)) ?? Set<Article>()
}
public func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) {
guard let account = account else { guard let account = account else {
assertionFailure("Expected feed.account, but got nil.") assertionFailure("Expected feed.account, but got nil.")
return Set<Article>() callback(Set<Article>())
return
} }
return account.fetchArticles(for: self) account.fetchArticlesAsync(.feed(self), callback)
} }
public func fetchUnreadArticles() -> Set<Article> { public func fetchUnreadArticles() -> Set<Article> {
preconditionFailure("feed.fetchUnreadArticles is unused.")
}
guard let account = account else { public func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
assertionFailure("Expected feed.account, but got nil.") preconditionFailure("feed.fetchUnreadArticlesAsync is unused.")
return Set<Article>()
}
return account.fetchUnreadArticles(for: self)
} }
} }
extension Folder: ArticleFetcher { extension Folder: ArticleFetcher {
public func fetchArticles() -> Set<Article> { public func fetchArticles() -> Set<Article> {
return fetchUnreadArticles() return fetchUnreadArticles()
} }
public func fetchUnreadArticles() -> Set<Article> { public func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) {
fetchUnreadArticlesAsync(callback)
}
public func fetchUnreadArticles() -> Set<Article> {
guard let account = account else { guard let account = account else {
assertionFailure("Expected folder.account, but got nil.") assertionFailure("Expected folder.account, but got nil.")
return Set<Article>() return Set<Article>()
} }
return account.fetchArticles(.unreadForFolder(self))
}
return account.fetchArticles(folder: self) public func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
guard let account = account else {
assertionFailure("Expected folder.account, but got nil.")
callback(Set<Article>())
return
}
account.fetchArticlesAsync(.unreadForFolder(self), callback)
} }
} }

View File

@ -1131,7 +1131,7 @@ private extension FeedbinAccountDelegate {
// Mark articles as unread // Mark articles as unread
let deltaUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs) let deltaUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
let markUnreadArticles = account.fetchArticles(forArticleIDs: deltaUnreadArticleIDs) let markUnreadArticles = account.fetchArticles(.articleIDs(deltaUnreadArticleIDs))
DispatchQueue.main.async { DispatchQueue.main.async {
_ = account.update(markUnreadArticles, statusKey: .read, flag: false) _ = account.update(markUnreadArticles, statusKey: .read, flag: false)
} }
@ -1147,7 +1147,7 @@ private extension FeedbinAccountDelegate {
// Mark articles as read // Mark articles as read
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(feedbinUnreadArticleIDs) let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(feedbinUnreadArticleIDs)
let markReadArticles = account.fetchArticles(forArticleIDs: deltaReadArticleIDs) let markReadArticles = account.fetchArticles(.articleIDs(deltaReadArticleIDs))
DispatchQueue.main.async { DispatchQueue.main.async {
_ = account.update(markReadArticles, statusKey: .read, flag: true) _ = account.update(markReadArticles, statusKey: .read, flag: true)
} }
@ -1174,7 +1174,7 @@ private extension FeedbinAccountDelegate {
// Mark articles as starred // Mark articles as starred
let deltaStarredArticleIDs = feedbinStarredArticleIDs.subtracting(currentStarredArticleIDs) let deltaStarredArticleIDs = feedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
let markStarredArticles = account.fetchArticles(forArticleIDs: deltaStarredArticleIDs) let markStarredArticles = account.fetchArticles(.articleIDs(deltaStarredArticleIDs))
DispatchQueue.main.async { DispatchQueue.main.async {
_ = account.update(markStarredArticles, statusKey: .starred, flag: true) _ = account.update(markStarredArticles, statusKey: .starred, flag: true)
} }
@ -1190,7 +1190,7 @@ private extension FeedbinAccountDelegate {
// Mark articles as unstarred // Mark articles as unstarred
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(feedbinStarredArticleIDs) let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(feedbinStarredArticleIDs)
let markUnstarredArticles = account.fetchArticles(forArticleIDs: deltaUnstarredArticleIDs) let markUnstarredArticles = account.fetchArticles(.articleIDs(deltaUnstarredArticleIDs))
DispatchQueue.main.async { DispatchQueue.main.async {
_ = account.update(markUnstarredArticles, statusKey: .starred, flag: false) _ = account.update(markUnstarredArticles, statusKey: .starred, flag: false)
} }

View File

@ -8,6 +8,8 @@
import Foundation import Foundation
public typealias ArticleSetBlock = (Set<Article>) -> Void
public struct Article: Hashable { public struct Article: Hashable {
public let articleID: String // Unique database ID (possibly sync service ID) public let articleID: String // Unique database ID (possibly sync service ID)

View File

@ -12,10 +12,12 @@ import RSDatabase
import RSParser import RSParser
import Articles import Articles
// This file and UnreadCountDictionary are the entirety of the public API for Database.framework. // This file is the entirety of the public API for ArticlesDatabase.framework.
// Everything else is implementation. // Everything else is implementation.
public typealias ArticleResultBlock = (Set<Article>) -> Void // Main thread only.
public typealias UnreadCountDictionary = [String: Int] // feedID: unreadCount
public typealias UnreadCountCompletionBlock = (UnreadCountDictionary) -> Void public typealias UnreadCountCompletionBlock = (UnreadCountDictionary) -> Void
public typealias UpdateArticlesWithFeedCompletionBlock = (Set<Article>?, Set<Article>?) -> Void //newArticles, updatedArticles public typealias UpdateArticlesWithFeedCompletionBlock = (Set<Article>?, Set<Article>?) -> Void //newArticles, updatedArticles
@ -46,38 +48,60 @@ public final class ArticlesDatabase {
// MARK: - Fetching Articles // MARK: - Fetching Articles
public func fetchArticles(for feedID: String) -> Set<Article> { public func fetchArticles(_ feedID: String) -> Set<Article> {
return articlesTable.fetchArticles(feedID) return articlesTable.fetchArticles(feedID)
} }
public func fetchArticles(forArticleIDs articleIDs: Set<String>) -> Set<Article> { public func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchArticles(forArticleIDs: articleIDs) return articlesTable.fetchArticles(articleIDs: articleIDs)
} }
public func fetchArticlesAsync(for feedID: String, _ resultBlock: @escaping ArticleResultBlock) { public func fetchUnreadArticles(_ feedIDs: Set<String>) -> Set<Article> {
articlesTable.fetchArticlesAsync(feedID, withLimits: true, resultBlock) return articlesTable.fetchUnreadArticles(feedIDs)
} }
public func fetchUnreadArticles(for feedIDs: Set<String>) -> Set<Article> { public func fetchTodayArticles(_ feedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchUnreadArticles(for: feedIDs) return articlesTable.fetchTodayArticles(feedIDs)
} }
public func fetchTodayArticles(for feedIDs: Set<String>) -> Set<Article> { public func fetchStarredArticles(_ feedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchTodayArticles(for: feedIDs) return articlesTable.fetchStarredArticles(feedIDs)
} }
public func fetchStarredArticles(for feedIDs: Set<String>) -> Set<Article> { public func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchStarredArticles(for: feedIDs) return articlesTable.fetchArticlesMatching(searchString, feedIDs)
} }
public func fetchArticlesMatching(_ searchString: String, for feedIDs: Set<String>) -> Set<Article> { // MARK: - Fetching Articles Async
return articlesTable.fetchArticlesMatching(searchString, for: feedIDs)
public func fetchArticlesAsync(_ feedID: String, _ callback: @escaping ArticleSetBlock) {
articlesTable.fetchArticlesAsync(feedID, callback)
}
public func fetchArticlesAsync(articleIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
articlesTable.fetchArticlesAsync(articleIDs: articleIDs, callback)
}
public func fetchUnreadArticlesAsync(_ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
articlesTable.fetchUnreadArticlesAsync(feedIDs, callback)
}
public func fetchTodayArticlesAsync(_ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
articlesTable.fetchTodayArticlesAsync(feedIDs, callback)
}
public func fetchedStarredArticlesAsync(_ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
articlesTable.fetchStarredArticlesAsync(feedIDs, callback)
}
public func fetchArticlesMatchingAsync(_ searchString: String, _ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
articlesTable.fetchArticlesMatchingAsync(searchString, feedIDs, callback)
} }
// MARK: - Unread Counts // MARK: - Unread Counts
public func fetchUnreadCounts(for feedIDs: Set<String>, _ completion: @escaping UnreadCountCompletionBlock) { public func fetchUnreadCounts(for feedIDs: Set<String>, _ callback: @escaping UnreadCountCompletionBlock) {
articlesTable.fetchUnreadCounts(feedIDs, completion) articlesTable.fetchUnreadCounts(feedIDs, callback)
} }
public func fetchUnreadCount(for feedIDs: Set<String>, since: Date, callback: @escaping (Int) -> Void) { public func fetchUnreadCount(for feedIDs: Set<String>, since: Date, callback: @escaping (Int) -> Void) {
@ -88,8 +112,8 @@ public final class ArticlesDatabase {
articlesTable.fetchStarredAndUnreadCount(feedIDs, callback) articlesTable.fetchStarredAndUnreadCount(feedIDs, callback)
} }
public func fetchAllNonZeroUnreadCounts(_ completion: @escaping UnreadCountCompletionBlock) { public func fetchAllNonZeroUnreadCounts(_ callback: @escaping UnreadCountCompletionBlock) {
articlesTable.fetchAllUnreadCounts(completion) articlesTable.fetchAllUnreadCounts(callback)
} }
// MARK: - Saving and Updating Articles // MARK: - Saving and Updating Articles

View File

@ -23,7 +23,6 @@
8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */; }; 8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */; };
8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */; }; 8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */; };
8477ACBC2221E76F00DF7F37 /* SearchTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBB2221E76F00DF7F37 /* SearchTable.swift */; }; 8477ACBC2221E76F00DF7F37 /* SearchTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBB2221E76F00DF7F37 /* SearchTable.swift */; };
848AD2961F58A91E004FB0EC /* UnreadCountDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848AD2951F58A91E004FB0EC /* UnreadCountDictionary.swift */; };
848E3EB920FBCFD20004B7ED /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EB820FBCFD20004B7ED /* RSCore.framework */; }; 848E3EB920FBCFD20004B7ED /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EB820FBCFD20004B7ED /* RSCore.framework */; };
848E3EBD20FBCFDE0004B7ED /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EBC20FBCFDE0004B7ED /* RSDatabase.framework */; }; 848E3EBD20FBCFDE0004B7ED /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EBC20FBCFDE0004B7ED /* RSDatabase.framework */; };
84E156EA1F0AB80500F8CC05 /* ArticlesDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156E91F0AB80500F8CC05 /* ArticlesDatabase.swift */; }; 84E156EA1F0AB80500F8CC05 /* ArticlesDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156E91F0AB80500F8CC05 /* ArticlesDatabase.swift */; };
@ -131,7 +130,6 @@
8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Attachment+Database.swift"; path = "Extensions/Attachment+Database.swift"; sourceTree = "<group>"; }; 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Attachment+Database.swift"; path = "Extensions/Attachment+Database.swift"; sourceTree = "<group>"; };
8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; }; 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
8477ACBB2221E76F00DF7F37 /* SearchTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTable.swift; sourceTree = "<group>"; }; 8477ACBB2221E76F00DF7F37 /* SearchTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTable.swift; sourceTree = "<group>"; };
848AD2951F58A91E004FB0EC /* UnreadCountDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadCountDictionary.swift; sourceTree = "<group>"; };
848E3EB820FBCFD20004B7ED /* RSCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 848E3EB820FBCFD20004B7ED /* RSCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
848E3EBA20FBCFD80004B7ED /* RSParser.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSParser.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 848E3EBA20FBCFD80004B7ED /* RSParser.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSParser.framework; sourceTree = BUILT_PRODUCTS_DIR; };
848E3EBC20FBCFDE0004B7ED /* RSDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 848E3EBC20FBCFDE0004B7ED /* RSDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -178,7 +176,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
84E156E91F0AB80500F8CC05 /* ArticlesDatabase.swift */, 84E156E91F0AB80500F8CC05 /* ArticlesDatabase.swift */,
848AD2951F58A91E004FB0EC /* UnreadCountDictionary.swift */,
845580661F0AEBCD003CCFA1 /* Constants.swift */, 845580661F0AEBCD003CCFA1 /* Constants.swift */,
84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */, 84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */,
8477ACBB2221E76F00DF7F37 /* SearchTable.swift */, 8477ACBB2221E76F00DF7F37 /* SearchTable.swift */,
@ -356,13 +353,13 @@
TargetAttributes = { TargetAttributes = {
844BEE361F0AB3AA004AB7CD = { 844BEE361F0AB3AA004AB7CD = {
CreatedOnToolsVersion = 8.3.2; CreatedOnToolsVersion = 8.3.2;
DevelopmentTeam = SHJK2V3AJG; DevelopmentTeam = 9C84TZ7Q6Z;
LastSwiftMigration = 0830; LastSwiftMigration = 0830;
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
}; };
844BEE3F1F0AB3AB004AB7CD = { 844BEE3F1F0AB3AB004AB7CD = {
CreatedOnToolsVersion = 8.3.2; CreatedOnToolsVersion = 8.3.2;
DevelopmentTeam = SHJK2V3AJG; DevelopmentTeam = 9C84TZ7Q6Z;
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
}; };
}; };
@ -501,7 +498,6 @@
files = ( files = (
845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */, 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */,
843CB9961F34174100EE6581 /* Author+Database.swift in Sources */, 843CB9961F34174100EE6581 /* Author+Database.swift in Sources */,
848AD2961F58A91E004FB0EC /* UnreadCountDictionary.swift in Sources */,
845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */, 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */,
8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */, 8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */,
8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */, 8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */,

View File

@ -29,6 +29,8 @@ final class ArticlesTable: DatabaseTable {
private var articleCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! private var articleCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)!
private var maximumArticleCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 4 * 31)! private var maximumArticleCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 4 * 31)!
private typealias ArticlesFetchMethod = (FMDatabase) -> Set<Article>
init(name: String, accountID: String, queue: RSDatabaseQueue) { init(name: String, accountID: String, queue: RSDatabaseQueue) {
self.name = name self.name = name
@ -43,52 +45,109 @@ final class ArticlesTable: DatabaseTable {
self.attachmentsLookupTable = DatabaseLookupTable(name: DatabaseTableName.attachmentsLookup, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.attachmentID, relatedTable: attachmentsTable, relationshipName: RelationshipName.attachments) self.attachmentsLookupTable = DatabaseLookupTable(name: DatabaseTableName.attachmentsLookup, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.attachmentID, relatedTable: attachmentsTable, relationshipName: RelationshipName.attachments)
} }
// MARK: Fetching // MARK: - Fetch Articles for Feed
func fetchArticles(_ feedID: String) -> Set<Article> { func fetchArticles(_ feedID: String) -> Set<Article> {
return fetchArticles{ self.fetchArticlesForFeedID(feedID, withLimits: true, $0) }
var articles = Set<Article>() }
queue.fetchSync { (database) in func fetchArticlesAsync(_ feedID: String, _ callback: @escaping ArticleSetBlock) {
articles = self.fetchArticlesForFeedID(feedID, withLimits: true, database: database) fetchArticlesAsync({ self.fetchArticlesForFeedID(feedID, withLimits: true, $0) }, callback)
}
private func fetchArticlesForFeedID(_ feedID: String, withLimits: Bool, _ database: FMDatabase) -> Set<Article> {
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject], withLimits: withLimits)
}
// MARK: - Fetch Articles by articleID
func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
return fetchArticles{ self.fetchArticles(articleIDs: articleIDs, $0) }
}
func fetchArticlesAsync(articleIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
return fetchArticlesAsync({ self.fetchArticles(articleIDs: articleIDs, $0) }, callback)
}
private func fetchArticles(articleIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
if articleIDs.isEmpty {
return Set<Article>()
}
let parameters = articleIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
let whereClause = "articleID in \(placeholders)"
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
}
// MARK: - Fetch Unread Articles
func fetchUnreadArticles(_ feedIDs: Set<String>) -> Set<Article> {
return fetchArticles{ self.fetchUnreadArticles(feedIDs, $0) }
}
func fetchUnreadArticlesAsync(_ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync({ self.fetchUnreadArticles(feedIDs, $0) }, callback)
}
private func fetchUnreadArticles(_ feedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0
if feedIDs.isEmpty {
return Set<Article>()
}
let parameters = feedIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
let whereClause = "feedID in \(placeholders) and read=0"
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
}
// MARK: - Fetch Today Articles
func fetchTodayArticles(_ feedIDs: Set<String>) -> Set<Article> {
return fetchArticles{ self.fetchTodayArticles(feedIDs, $0) }
}
func fetchTodayArticlesAsync(_ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync({ self.fetchTodayArticles(feedIDs, $0) }, callback)
}
private func fetchTodayArticles(_ feedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?)
//
// datePublished may be nil, so we fall back to dateArrived.
if feedIDs.isEmpty {
return Set<Article>()
}
let startOfToday = NSCalendar.startOfToday()
let parameters = feedIDs.map { $0 as AnyObject } + [startOfToday as AnyObject, startOfToday as AnyObject]
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and userDeleted = 0"
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
}
// MARK: - Fetch Starred Articles
func fetchStarredArticles(_ feedIDs: Set<String>) -> Set<Article> {
return fetchArticles{ self.fetchStarredArticles(feedIDs, $0) }
}
func fetchStarredArticlesAsync(_ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync({ self.fetchStarredArticles(feedIDs, $0) }, callback)
}
private func fetchStarredArticles(_ feedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred = 1 and userDeleted = 0;
if feedIDs.isEmpty {
return Set<Article>()
}
let parameters = feedIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
let whereClause = "feedID in \(placeholders) and starred = 1 and userDeleted = 0"
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
} }
return articles // MARK: - Fetch Search Articles
}
public func fetchArticles(forArticleIDs articleIDs: Set<String>) -> Set<Article> { func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>) -> Set<Article> {
return fetchArticlesForIDs(articleIDs)
}
func fetchArticlesAsync(_ feedID: String, withLimits: Bool, _ resultBlock: @escaping ArticleResultBlock) {
queue.fetch { (database) in
let articles = self.fetchArticlesForFeedID(feedID, withLimits: withLimits, database: database)
DispatchQueue.main.async {
resultBlock(articles)
}
}
}
func fetchUnreadArticles(for feedIDs: Set<String>) -> Set<Article> {
return fetchUnreadArticles(feedIDs)
}
public func fetchTodayArticles(for feedIDs: Set<String>) -> Set<Article> {
return fetchTodayArticles(feedIDs)
}
public func fetchStarredArticles(for feedIDs: Set<String>) -> Set<Article> {
return fetchStarredArticles(feedIDs)
}
func fetchArticlesMatching(_ searchString: String, for feedIDs: Set<String>) -> Set<Article> {
var articles: Set<Article> = Set<Article>() var articles: Set<Article> = Set<Article>()
queue.fetchSync { (database) in queue.fetchSync { (database) in
articles = self.fetchArticlesMatching(searchString, database) articles = self.fetchArticlesMatching(searchString, database)
@ -97,6 +156,32 @@ final class ArticlesTable: DatabaseTable {
return articles return articles
} }
func fetchArticlesMatchingAsync(_ searchString: String, _ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync({ self.fetchArticlesMatching(searchString, feedIDs, $0) }, callback)
}
private func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
let sql = "select rowid from search where search match ?;"
let sqlSearchString = sqliteSearchString(with: searchString)
let searchStringParameters = [sqlSearchString]
guard let resultSet = database.executeQuery(sql, withArgumentsIn: searchStringParameters) else {
return Set<Article>()
}
let searchRowIDs = resultSet.mapToSet { $0.longLongInt(forColumnIndex: 0) }
if searchRowIDs.isEmpty {
return Set<Article>()
}
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(searchRowIDs.count))!
let whereClause = "searchRowID in \(placeholders)"
let parameters: [AnyObject] = Array(searchRowIDs) as [AnyObject]
let articles = fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
// TODO: include the feedIDs in the SQL rather than filtering here.
return articles.filter{ feedIDs.contains($0.feedID) }
}
// MARK: - Fetch Articles for Indexer
func fetchArticleSearchInfos(_ articleIDs: Set<String>, in database: FMDatabase) -> Set<ArticleSearchInfo>? { func fetchArticleSearchInfos(_ articleIDs: Set<String>, in database: FMDatabase) -> Set<ArticleSearchInfo>? {
let parameters = articleIDs.map { $0 as AnyObject } let parameters = articleIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
@ -159,7 +244,7 @@ final class ArticlesTable: DatabaseTable {
return return
} }
let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: false, database: database) //4 let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: false, database) //4
let fetchedArticlesDictionary = fetchedArticles.dictionary() let fetchedArticlesDictionary = fetchedArticles.dictionary()
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5 let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
@ -346,6 +431,23 @@ private extension ArticlesTable {
// MARK: Fetching // MARK: Fetching
private func fetchArticles(_ fetchMethod: @escaping ArticlesFetchMethod) -> Set<Article> {
var articles = Set<Article>()
queue.fetchSync { (database) in
articles = fetchMethod(database)
}
return articles
}
private func fetchArticlesAsync(_ fetchMethod: @escaping ArticlesFetchMethod, _ callback: @escaping ArticleSetBlock) {
queue.fetch { (database) in
let articles = fetchMethod(database)
DispatchQueue.main.async {
callback(articles)
}
}
}
func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set<Article> { func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set<Article> {
// 1. Create DatabaseArticles without related objects. // 1. Create DatabaseArticles without related objects.
@ -453,97 +555,6 @@ private extension ArticlesTable {
return numberWithSQLAndParameters(sql, [feedID, articleCutoffDate], in: database) return numberWithSQLAndParameters(sql, [feedID, articleCutoffDate], in: database)
} }
func fetchArticlesForFeedID(_ feedID: String, withLimits: Bool, database: FMDatabase) -> Set<Article> {
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject], withLimits: withLimits)
}
func fetchArticlesForIDs(_ articleIDs: Set<String>) -> Set<Article> {
if articleIDs.isEmpty {
return Set<Article>()
}
var articles = Set<Article>()
queue.fetchSync { (database) in
let parameters = articleIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
let whereClause = "articleID in \(placeholders)"
articles = self.fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
}
return articles
}
func fetchUnreadArticles(_ feedIDs: Set<String>) -> Set<Article> {
if feedIDs.isEmpty {
return Set<Article>()
}
var articles = Set<Article>()
queue.fetchSync { (database) in
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0
let parameters = feedIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
let whereClause = "feedID in \(placeholders) and read=0"
articles = self.fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
}
return articles
}
func fetchTodayArticles(_ feedIDs: Set<String>) -> Set<Article> {
if feedIDs.isEmpty {
return Set<Article>()
}
var articles = Set<Article>()
queue.fetchSync { (database) in
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?)
//
// datePublished may be nil, so we fall back to dateArrived.
let startOfToday = NSCalendar.startOfToday()
let parameters = feedIDs.map { $0 as AnyObject } + [startOfToday as AnyObject, startOfToday as AnyObject]
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and userDeleted = 0"
// let whereClause = "feedID in \(placeholders) and datePublished > ? and userDeleted = 0"
articles = self.fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
}
return articles
}
func fetchStarredArticles(_ feedIDs: Set<String>) -> Set<Article> {
if feedIDs.isEmpty {
return Set<Article>()
}
var articles = Set<Article>()
queue.fetchSync { (database) in
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred = 1 and userDeleted = 0;
let parameters = feedIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
let whereClause = "feedID in \(placeholders) and starred = 1 and userDeleted = 0"
articles = self.fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
}
return articles
}
func fetchArticlesMatching(_ searchString: String, _ database: FMDatabase) -> Set<Article> { func fetchArticlesMatching(_ searchString: String, _ database: FMDatabase) -> Set<Article> {
let sql = "select rowid from search where search match ?;" let sql = "select rowid from search where search match ?;"
let sqlSearchString = sqliteSearchString(with: searchString) let sqlSearchString = sqliteSearchString(with: searchString)

View File

@ -1,28 +0,0 @@
//
// UnreadCountDictionary.swift
// Database
//
// Created by Brent Simmons on 8/31/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import Articles
public struct UnreadCountDictionary {
private var dictionary = [String: Int]()
public var isEmpty: Bool {
return dictionary.count < 1
}
public subscript(_ feedID: String) -> Int? {
get {
return dictionary[feedID]
}
set {
dictionary[feedID] = newValue
}
}
}

View File

@ -0,0 +1,72 @@
//
// FetchRequestOperation.swift
// NetNewsWire
//
// Created by Brent Simmons on 6/20/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import RSCore
import Account
import Articles
// Main thread only.
// Runs an asynchronous fetch.
typealias FetchRequestOperationResultBlock = (Set<Article>, FetchRequestOperation) -> Void
class FetchRequestOperation {
let id: Int
let resultBlock: FetchRequestOperationResultBlock
var isCanceled = false
var isFinished = false
private let representedObjects: [Any]
init(id: Int, representedObjects: [Any], resultBlock: @escaping FetchRequestOperationResultBlock) {
precondition(Thread.isMainThread)
self.id = id
self.representedObjects = representedObjects
self.resultBlock = resultBlock
}
func run(_ completion: @escaping (FetchRequestOperation) -> Void) {
precondition(Thread.isMainThread)
precondition(!isFinished)
if isCanceled {
completion(self)
return
}
let articleFetchers = representedObjects.compactMap{ $0 as? ArticleFetcher }
if articleFetchers.isEmpty {
isFinished = true
resultBlock(Set<Article>(), self)
completion(self)
return
}
let numberOfFetchers = articleFetchers.count
var fetchersReturned = 0
var fetchedArticles = Set<Article>()
for articleFetcher in articleFetchers {
articleFetcher.fetchArticlesAsync { (articles) in
precondition(Thread.isMainThread)
if self.isCanceled {
completion(self)
return
}
fetchedArticles.formUnion(articles)
fetchersReturned += 1
if fetchersReturned == numberOfFetchers {
self.isFinished = true
self.resultBlock(fetchedArticles, self)
completion(self)
}
}
}
}
}

View File

@ -0,0 +1,54 @@
//
// FetchRequestQueue.swift
// NetNewsWire
//
// Created by Brent Simmons on 6/20/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
// Main thread only.
class FetchRequestQueue {
private var pendingRequests = [FetchRequestOperation]()
private var currentRequest: FetchRequestOperation? = nil
func cancelAllRequests() {
precondition(Thread.isMainThread)
pendingRequests.forEach { $0.isCanceled = true }
currentRequest?.isCanceled = true
pendingRequests = [FetchRequestOperation]()
}
func add(_ fetchRequestOperation: FetchRequestOperation) {
precondition(Thread.isMainThread)
pendingRequests.append(fetchRequestOperation)
runNextRequestIfNeeded()
}
}
private extension FetchRequestQueue {
func runNextRequestIfNeeded() {
precondition(Thread.isMainThread)
removeCanceledAndFinishedRequests()
guard currentRequest == nil, let requestToRun = pendingRequests.first else {
return
}
currentRequest = requestToRun
pendingRequests.removeFirst()
requestToRun.run { (fetchRequestOperation) in
precondition(fetchRequestOperation === self.currentRequest)
precondition(fetchRequestOperation === requestToRun)
self.currentRequest = nil
self.runNextRequestIfNeeded()
}
}
func removeCanceledAndFinishedRequests() {
pendingRequests = pendingRequests.filter{ !$0.isCanceled && !$0.isFinished }
}
}

View File

@ -33,8 +33,10 @@ final class TimelineContainerViewController: NSViewController {
private lazy var regularTimelineViewController = { private lazy var regularTimelineViewController = {
return TimelineViewController(delegate: self) return TimelineViewController(delegate: self)
}() }()
private lazy var searchTimelineViewController = { private lazy var searchTimelineViewController: TimelineViewController = {
return TimelineViewController(delegate: self) let viewController = TimelineViewController(delegate: self)
viewController.showsSearchResults = true
return viewController
}() }()
override func viewDidLoad() { override func viewDidLoad() {

View File

@ -40,9 +40,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
} }
selectionDidChange(nil) selectionDidChange(nil)
fetchArticles() if showsSearchResults {
if articles.count > 0 { fetchAndReplaceArticlesAsync()
tableView.scrollRowToVisible(0) }
else {
fetchAndReplaceArticlesSync()
if articles.count > 0 {
tableView.scrollRowToVisible(0)
}
} }
} }
} }
@ -50,7 +55,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
private weak var delegate: TimelineDelegate? private weak var delegate: TimelineDelegate?
var sharingServiceDelegate: NSSharingServiceDelegate? var sharingServiceDelegate: NSSharingServiceDelegate?
var showsSearchResults = false
var selectedArticles: [Article] { var selectedArticles: [Article] {
return Array(articles.articlesForIndexes(tableView.selectedRowIndexes)) return Array(articles.articlesForIndexes(tableView.selectedRowIndexes))
} }
@ -79,6 +85,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
} }
var undoableCommands = [UndoableCommand]() var undoableCommands = [UndoableCommand]()
private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue()
private var articleRowMap = [String: Int]() // articleID: rowIndex private var articleRowMap = [String: Int]() // articleID: rowIndex
private var cellAppearance: TimelineCellAppearance! private var cellAppearance: TimelineCellAppearance!
private var cellAppearanceWithAvatar: TimelineCellAppearance! private var cellAppearanceWithAvatar: TimelineCellAppearance!
@ -100,7 +108,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
} }
private var didRegisterForNotifications = false private var didRegisterForNotifications = false
static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 2.0, maxInterval: 5.0) static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5, maxInterval: 2.0)
private var sortDirection = AppDefaults.timelineSortDirection { private var sortDirection = AppDefaults.timelineSortDirection {
didSet { didSet {
@ -502,13 +510,13 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
@objc func accountStateDidChange(_ note: Notification) { @objc func accountStateDidChange(_ note: Notification) {
if representedObjectsContainsAnyPseudoFeed() { if representedObjectsContainsAnyPseudoFeed() {
fetchArticles() fetchAndReplaceArticlesAsync()
} }
} }
@objc func accountsDidChange(_ note: Notification) { @objc func accountsDidChange(_ note: Notification) {
if representedObjectsContainsAnyPseudoFeed() { if representedObjectsContainsAnyPseudoFeed() {
fetchArticles() fetchAndReplaceArticlesAsync()
} }
} }
@ -521,7 +529,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
@objc func calendarDayChanged(_ note: Notification) { @objc func calendarDayChanged(_ note: Notification) {
if representedObjectsContainsTodayFeed() { if representedObjectsContainsTodayFeed() {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.fetchArticles() self?.fetchAndReplaceArticlesAsync()
} }
} }
} }
@ -606,24 +614,25 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
} }
@objc func fetchAndMergeArticles() { @objc func fetchAndMergeArticles() {
guard let representedObjects = representedObjects else { guard let representedObjects = representedObjects else {
return return
} }
performBlockAndRestoreSelection { fetchUnsortedArticlesAsync(for: representedObjects) { [weak self] (unsortedArticles) in
var unsortedArticles = fetchUnsortedArticles(for: representedObjects)
// Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles. // Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles.
guard let strongSelf = self else {
return
}
let unsortedArticleIDs = unsortedArticles.articleIDs() let unsortedArticleIDs = unsortedArticles.articleIDs()
for article in articles { var updatedArticles = unsortedArticles
for article in strongSelf.articles {
if !unsortedArticleIDs.contains(article.articleID) { if !unsortedArticleIDs.contains(article.articleID) {
unsortedArticles.insert(article) updatedArticles.insert(article)
} }
} }
strongSelf.performBlockAndRestoreSelection {
updateArticles(with: unsortedArticles) strongSelf.replaceArticles(with: updatedArticles)
}
} }
} }
} }
@ -842,7 +851,6 @@ private extension TimelineViewController {
} }
func emptyTheTimeline() { func emptyTheTimeline() {
if !articles.isEmpty { if !articles.isEmpty {
articles = [Article]() articles = [Article]()
} }
@ -852,7 +860,7 @@ private extension TimelineViewController {
performBlockAndRestoreSelection { performBlockAndRestoreSelection {
let unsortedArticles = Set(articles) let unsortedArticles = Set(articles)
updateArticles(with: unsortedArticles) replaceArticles(with: unsortedArticles)
} }
} }
@ -919,18 +927,39 @@ private extension TimelineViewController {
// MARK: Fetching Articles // MARK: Fetching Articles
func fetchArticles() { 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 let representedObjects = representedObjects else { guard let representedObjects = representedObjects else {
emptyTheTimeline() emptyTheTimeline()
return return
} }
let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects)
let fetchedArticles = fetchUnsortedArticles(for: representedObjects) replaceArticles(with: fetchedArticles)
updateArticles(with: fetchedArticles)
} }
func updateArticles(with unsortedArticles: Set<Article>) { 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 let representedObjects = representedObjects else {
emptyTheTimeline()
return
}
fetchUnsortedArticlesAsync(for: representedObjects) { [weak self] (articles) in
self?.replaceArticles(with: articles)
}
}
func cancelPendingAsyncFetches() {
fetchSerialNumber += 1
fetchRequestQueue.cancelAllRequests()
}
func replaceArticles(with unsortedArticles: Set<Article>) {
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection) let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection)
if articles != sortedArticles { if articles != sortedArticles {
@ -938,20 +967,35 @@ private extension TimelineViewController {
} }
} }
func fetchUnsortedArticles(for representedObjects: [Any]) -> Set<Article> { func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<Article> {
cancelPendingAsyncFetches()
var fetchedArticles = Set<Article>() let articleFetchers = representedObjects.compactMap{ $0 as? ArticleFetcher }
if articleFetchers.isEmpty {
for object in representedObjects { return Set<Article>()
if let articleFetcher = object as? ArticleFetcher {
fetchedArticles.formUnion(articleFetcher.fetchArticles())
}
} }
var fetchedArticles = Set<Article>()
for articleFetcher in articleFetchers {
fetchedArticles.formUnion(articleFetcher.fetchArticles())
}
return fetchedArticles return fetchedArticles
} }
func fetchUnsortedArticlesAsync(for representedObjects: [Any], callback: @escaping ArticleSetBlock) {
// The callback will *not* be called if the fetch is no longer relevant that is,
// if its been superseded by a newer fetch, or the timeline was emptied, etc., it wont get called.
precondition(Thread.isMainThread)
cancelPendingAsyncFetches()
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, representedObjects: representedObjects) { [weak self] (articles, operation) in
precondition(Thread.isMainThread)
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
return
}
callback(articles)
}
fetchRequestQueue.add(fetchOperation)
}
func selectArticles(_ articleIDs: [String]) { func selectArticles(_ articleIDs: [String]) {
let indexesToSelect = indexesForArticleIDs(Set(articleIDs)) let indexesToSelect = indexesForArticleIDs(Set(articleIDs))

View File

@ -267,8 +267,14 @@
84C9FC9D2262A1A900D921D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC9B2262A1A900D921D6 /* Assets.xcassets */; }; 84C9FC9D2262A1A900D921D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC9B2262A1A900D921D6 /* Assets.xcassets */; };
84C9FCA12262A1B300D921D6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC9F2262A1B300D921D6 /* Main.storyboard */; }; 84C9FCA12262A1B300D921D6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC9F2262A1B300D921D6 /* Main.storyboard */; };
84C9FCA42262A1B800D921D6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FCA22262A1B800D921D6 /* LaunchScreen.storyboard */; }; 84C9FCA42262A1B800D921D6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FCA22262A1B800D921D6 /* LaunchScreen.storyboard */; };
84CAFCA422BC8C08007694F0 /* FetchRequestQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */; };
84CAFCA522BC8C08007694F0 /* FetchRequestQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */; };
84CAFCAF22BC8C35007694F0 /* FetchRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */; };
84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */; };
84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; }; 84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; };
84D52E951FE588BB00D14F5B /* DetailStatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */; }; 84D52E951FE588BB00D14F5B /* DetailStatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */; };
84DEE56522C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DEE56422C32CA4005FC42C /* SmartFeedDelegate.swift */; };
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DEE56422C32CA4005FC42C /* SmartFeedDelegate.swift */; };
84E185B3203B74E500F69BFA /* SingleLineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */; }; 84E185B3203B74E500F69BFA /* SingleLineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */; };
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */; }; 84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */; };
84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */; }; 84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */; };
@ -871,9 +877,12 @@
84C9FC9C2262A1A900D921D6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 84C9FC9C2262A1A900D921D6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
84C9FCA02262A1B300D921D6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; 84C9FCA02262A1B300D921D6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
84C9FCA32262A1B800D921D6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 84C9FCA32262A1B800D921D6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchRequestQueue.swift; sourceTree = "<group>"; };
84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchRequestOperation.swift; sourceTree = "<group>"; };
84CBDDAE1FD3674C005A61AA /* Technotes */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Technotes; sourceTree = "<group>"; }; 84CBDDAE1FD3674C005A61AA /* Technotes */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Technotes; sourceTree = "<group>"; };
84CC88171FE59CBF00644329 /* SmartFeedsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedsController.swift; sourceTree = "<group>"; }; 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedsController.swift; sourceTree = "<group>"; };
84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailStatusBarView.swift; sourceTree = "<group>"; }; 84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailStatusBarView.swift; sourceTree = "<group>"; };
84DEE56422C32CA4005FC42C /* SmartFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedDelegate.swift; sourceTree = "<group>"; };
84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleLineTextFieldSizer.swift; sourceTree = "<group>"; }; 84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleLineTextFieldSizer.swift; sourceTree = "<group>"; };
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextFieldSizer.swift; sourceTree = "<group>"; }; 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextFieldSizer.swift; sourceTree = "<group>"; };
84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = "<group>"; }; 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = "<group>"; };
@ -1388,6 +1397,8 @@
84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */, 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */,
849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */, 849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */,
849A976A1ED9EBC8007D329B /* TimelineTableView.swift */, 849A976A1ED9EBC8007D329B /* TimelineTableView.swift */,
84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */,
84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */,
844B5B6C1FEA282400C7C76A /* Keyboard */, 844B5B6C1FEA282400C7C76A /* Keyboard */,
84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */, 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */,
849A976F1ED9EC04007D329B /* Cell */, 849A976F1ED9EC04007D329B /* Cell */,
@ -1724,6 +1735,7 @@
84F2D5351FC22FCB00998D64 /* PseudoFeed.swift */, 84F2D5351FC22FCB00998D64 /* PseudoFeed.swift */,
84F2D5391FC2308B00998D64 /* UnreadFeed.swift */, 84F2D5391FC2308B00998D64 /* UnreadFeed.swift */,
845EE7C01FC2488C00854A1F /* SmartFeed.swift */, 845EE7C01FC2488C00854A1F /* SmartFeed.swift */,
84DEE56422C32CA4005FC42C /* SmartFeedDelegate.swift */,
84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */, 84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */,
845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */, 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */,
8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */, 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */,
@ -1942,12 +1954,12 @@
ORGANIZATIONNAME = "Ranchero Software"; ORGANIZATIONNAME = "Ranchero Software";
TargetAttributes = { TargetAttributes = {
6581C73220CED60000F4AD34 = { 6581C73220CED60000F4AD34 = {
DevelopmentTeam = SHJK2V3AJG; DevelopmentTeam = M8L2WTLA8W;
ProvisioningStyle = Manual; ProvisioningStyle = Manual;
}; };
840D617B2029031C009BC708 = { 840D617B2029031C009BC708 = {
CreatedOnToolsVersion = 9.3; CreatedOnToolsVersion = 9.3;
DevelopmentTeam = SHJK2V3AJG; DevelopmentTeam = M8L2WTLA8W;
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
SystemCapabilities = { SystemCapabilities = {
com.apple.BackgroundModes = { com.apple.BackgroundModes = {
@ -1963,7 +1975,7 @@
}; };
849C645F1ED37A5D003D8FC0 = { 849C645F1ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1; CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = SHJK2V3AJG; DevelopmentTeam = M8L2WTLA8W;
ProvisioningStyle = Manual; ProvisioningStyle = Manual;
SystemCapabilities = { SystemCapabilities = {
com.apple.HardenedRuntime = { com.apple.HardenedRuntime = {
@ -1973,7 +1985,7 @@
}; };
849C64701ED37A5D003D8FC0 = { 849C64701ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1; CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = SHJK2V3AJG; DevelopmentTeam = 9C84TZ7Q6Z;
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
TestTargetID = 849C645F1ED37A5D003D8FC0; TestTargetID = 849C645F1ED37A5D003D8FC0;
}; };
@ -2335,6 +2347,7 @@
51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */, 51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */,
51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */, 51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */,
5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */, 5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */,
84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */, 51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */,
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */, 51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
5183CCEF227125970010922C /* SettingsViewController.swift in Sources */, 5183CCEF227125970010922C /* SettingsViewController.swift in Sources */,
@ -2368,6 +2381,7 @@
515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */, 515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */,
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */,
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
84CAFCA522BC8C08007694F0 /* FetchRequestQueue.swift in Sources */,
51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */, 51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */,
51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, 51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */,
51C45290226509C100C03939 /* PseudoFeed.swift in Sources */, 51C45290226509C100C03939 /* PseudoFeed.swift in Sources */,
@ -2382,6 +2396,7 @@
5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */, 5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */,
51C452882265093600C03939 /* AddFeedViewController.swift in Sources */, 51C452882265093600C03939 /* AddFeedViewController.swift in Sources */,
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
5183CCE3226F314C0010922C /* ProgressTableViewController.swift in Sources */, 5183CCE3226F314C0010922C /* ProgressTableViewController.swift in Sources */,
512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */, 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */,
51C45268226508F600C03939 /* MasterFeedUnreadCountView.swift in Sources */, 51C45268226508F600C03939 /* MasterFeedUnreadCountView.swift in Sources */,
@ -2483,6 +2498,7 @@
849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */, 849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */,
84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */, 84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */,
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */, 841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */,
84DEE56522C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */, 845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */,
51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */, 51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */,
849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */, 849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */,
@ -2493,6 +2509,7 @@
849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */, 849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */,
84C9FC6722629B9000D921D6 /* AppDelegate.swift in Sources */, 84C9FC6722629B9000D921D6 /* AppDelegate.swift in Sources */,
84C9FC7A22629E1200D921D6 /* AccountsTableViewBackgroundView.swift in Sources */, 84C9FC7A22629E1200D921D6 /* AccountsTableViewBackgroundView.swift in Sources */,
84CAFCAF22BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */, 8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */,
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */, 849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */, 5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */,
@ -2514,6 +2531,7 @@
D5E4CC64202C1AC1009B4FFC /* MainWindowController+Scriptability.swift in Sources */, D5E4CC64202C1AC1009B4FFC /* MainWindowController+Scriptability.swift in Sources */,
84C9FC7922629E1200D921D6 /* PreferencesWindowController.swift in Sources */, 84C9FC7922629E1200D921D6 /* PreferencesWindowController.swift in Sources */,
84411E711FE5FBFA004B527F /* SmallIconProvider.swift in Sources */, 84411E711FE5FBFA004B527F /* SmallIconProvider.swift in Sources */,
84CAFCA422BC8C08007694F0 /* FetchRequestQueue.swift in Sources */,
844B5B591FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift in Sources */, 844B5B591FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift in Sources */,
84C9FC7C22629E1200D921D6 /* AccountsPreferencesViewController.swift in Sources */, 84C9FC7C22629E1200D921D6 /* AccountsPreferencesViewController.swift in Sources */,
51EC114C2149FE3300B296E3 /* FolderTreeMenu.swift in Sources */, 51EC114C2149FE3300B296E3 /* FolderTreeMenu.swift in Sources */,

View File

@ -18,9 +18,11 @@ struct SearchFeedDelegate: SmartFeedDelegate {
let nameForDisplayPrefix = NSLocalizedString("Search: ", comment: "Search smart feed title prefix") let nameForDisplayPrefix = NSLocalizedString("Search: ", comment: "Search smart feed title prefix")
let searchString: String let searchString: String
let fetchType: FetchType
init(searchString: String) { init(searchString: String) {
self.searchString = searchString self.searchString = searchString
self.fetchType = .search(searchString)
} }
func fetchUnreadCount(for: Account, callback: @escaping (Int) -> Void) { func fetchUnreadCount(for: Account, callback: @escaping (Int) -> Void) {
@ -28,19 +30,3 @@ struct SearchFeedDelegate: SmartFeedDelegate {
} }
} }
// MARK: - ArticleFetcher
extension SearchFeedDelegate: ArticleFetcher {
func fetchArticles() -> Set<Article> {
var articles = Set<Article>()
for account in AccountManager.shared.activeAccounts {
articles.formUnion(account.fetchArticlesMatching(searchString))
}
return articles
}
func fetchUnreadArticles() -> Set<Article> {
return fetchArticles().unreadArticles()
}
}

View File

@ -11,10 +11,6 @@ import RSCore
import Articles import Articles
import Account import Account
protocol SmartFeedDelegate: DisplayNameProvider, ArticleFetcher {
func fetchUnreadCount(for: Account, callback: @escaping (Int) -> Void)
}
final class SmartFeed: PseudoFeed { final class SmartFeed: PseudoFeed {
var nameForDisplay: String { var nameForDisplay: String {
@ -61,9 +57,17 @@ extension SmartFeed: ArticleFetcher {
return delegate.fetchArticles() return delegate.fetchArticles()
} }
func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) {
delegate.fetchArticlesAsync(callback)
}
func fetchUnreadArticles() -> Set<Article> { func fetchUnreadArticles() -> Set<Article> {
return delegate.fetchUnreadArticles() return delegate.fetchUnreadArticles()
} }
func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
delegate.fetchUnreadArticlesAsync(callback)
}
} }
private extension SmartFeed { private extension SmartFeed {

View File

@ -0,0 +1,38 @@
//
// SmartFeedDelegate.swift
// NetNewsWire
//
// Created by Brent Simmons on 6/25/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import Account
import Articles
import RSCore
protocol SmartFeedDelegate: DisplayNameProvider, ArticleFetcher {
var fetchType: FetchType { get }
func fetchUnreadCount(for: Account, callback: @escaping (Int) -> Void)
}
extension SmartFeedDelegate {
func fetchArticles() -> Set<Article> {
return AccountManager.shared.fetchArticles(fetchType)
}
func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) {
AccountManager.shared.fetchArticlesAsync(fetchType, callback)
}
func fetchUnreadArticles() -> Set<Article> {
return fetchArticles().unreadArticles()
}
func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync{ callback($0.unreadArticles()) }
}
}

View File

@ -10,30 +10,14 @@ import Foundation
import Articles import Articles
import Account import Account
// Main thread only.
struct StarredFeedDelegate: SmartFeedDelegate { struct StarredFeedDelegate: SmartFeedDelegate {
let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title") let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title")
let fetchType: FetchType = .starred
func fetchUnreadCount(for account: Account, callback: @escaping (Int) -> Void) { func fetchUnreadCount(for account: Account, callback: @escaping (Int) -> Void) {
account.fetchUnreadCountForStarredArticles(callback) account.fetchUnreadCountForStarredArticles(callback)
} }
// MARK: ArticleFetcher
func fetchArticles() -> Set<Article> {
var articles = Set<Article>()
for account in AccountManager.shared.activeAccounts {
articles.formUnion(account.fetchStarredArticles())
}
return articles
}
func fetchUnreadArticles() -> Set<Article> {
return fetchArticles().unreadArticles()
}
} }

View File

@ -13,26 +13,10 @@ import Account
struct TodayFeedDelegate: SmartFeedDelegate { struct TodayFeedDelegate: SmartFeedDelegate {
let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title") let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title")
let fetchType = FetchType.today
func fetchUnreadCount(for account: Account, callback: @escaping (Int) -> Void) { func fetchUnreadCount(for account: Account, callback: @escaping (Int) -> Void) {
account.fetchUnreadCountForToday(callback) account.fetchUnreadCountForToday(callback)
} }
// MARK: ArticleFetcher
func fetchArticles() -> Set<Article> {
var articles = Set<Article>()
for account in AccountManager.shared.activeAccounts {
articles.formUnion(account.fetchTodayArticles())
}
return articles
}
func fetchUnreadArticles() -> Set<Article> {
return fetchArticles().unreadArticles()
}
} }

View File

@ -19,7 +19,8 @@ import Articles
final class UnreadFeed: PseudoFeed { final class UnreadFeed: PseudoFeed {
let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title") let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title")
let fetchType = FetchType.unread
var unreadCount = 0 { var unreadCount = 0 {
didSet { didSet {
if unreadCount != oldValue { if unreadCount != oldValue {
@ -50,16 +51,18 @@ final class UnreadFeed: PseudoFeed {
extension UnreadFeed: ArticleFetcher { extension UnreadFeed: ArticleFetcher {
func fetchArticles() -> Set<Article> { func fetchArticles() -> Set<Article> {
return fetchUnreadArticles() return fetchUnreadArticles()
} }
func fetchUnreadArticles() -> Set<Article> { func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) {
fetchUnreadArticlesAsync(callback)
}
var articles = Set<Article>() func fetchUnreadArticles() -> Set<Article> {
for account in AccountManager.shared.activeAccounts { return AccountManager.shared.fetchArticles(fetchType)
articles.formUnion(account.fetchUnreadArticles()) }
}
return articles func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
AccountManager.shared.fetchArticlesAsync(fetchType, callback)
} }
} }