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:
parent
6f16a2715e
commit
7a204ad6ed
|
@ -36,6 +36,16 @@ public enum AccountType: Int {
|
|||
// 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 struct UserInfoKey {
|
||||
|
@ -471,85 +481,44 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
}
|
||||
}
|
||||
|
||||
public func fetchArticles(forArticleIDs articleIDs: Set<String>) -> Set<Article> {
|
||||
return database.fetchArticles(forArticleIDs: articleIDs)
|
||||
}
|
||||
|
||||
public func fetchArticles(for feed: Feed) -> Set<Article> {
|
||||
|
||||
let articles = database.fetchArticles(for: feed.feedID)
|
||||
validateUnreadCount(feed, articles)
|
||||
return articles
|
||||
}
|
||||
|
||||
public func fetchUnreadArticles(for feed: Feed) -> Set<Article> {
|
||||
|
||||
let articles = database.fetchUnreadArticles(for: Set([feed.feedID]))
|
||||
validateUnreadCount(feed, articles)
|
||||
return articles
|
||||
}
|
||||
|
||||
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
|
||||
public func fetchArticles(_ fetchType: FetchType) -> Set<Article> {
|
||||
switch fetchType {
|
||||
case .starred:
|
||||
return fetchStarredArticles()
|
||||
case .unread:
|
||||
return fetchUnreadArticles()
|
||||
case .today:
|
||||
return fetchTodayArticles()
|
||||
case .unreadForFolder(let folder):
|
||||
return fetchArticles(folder: folder)
|
||||
case .feed(let feed):
|
||||
return fetchArticles(feed: feed)
|
||||
case .articleIDs(let articleIDs):
|
||||
return fetchArticles(articleIDs: articleIDs)
|
||||
case .search(let searchString):
|
||||
return fetchArticlesMatching(searchString)
|
||||
}
|
||||
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 feed’s 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) {
|
||||
|
||||
|
@ -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 feed’s 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)
|
||||
|
||||
private extension Account {
|
||||
|
|
|
@ -202,7 +202,39 @@ public final class AccountManager: UnreadCountProvider {
|
|||
|
||||
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
|
||||
|
||||
@objc dynamic func unreadCountDidChange(_ notification: Notification) {
|
||||
|
|
|
@ -12,44 +12,59 @@ import Articles
|
|||
public protocol ArticleFetcher {
|
||||
|
||||
func fetchArticles() -> Set<Article>
|
||||
func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock)
|
||||
func fetchUnreadArticles() -> Set<Article>
|
||||
func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock)
|
||||
}
|
||||
|
||||
extension Feed: ArticleFetcher {
|
||||
|
||||
public func fetchArticles() -> Set<Article> {
|
||||
return account?.fetchArticles(.feed(self)) ?? Set<Article>()
|
||||
}
|
||||
|
||||
public func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) {
|
||||
guard let account = account else {
|
||||
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> {
|
||||
preconditionFailure("feed.fetchUnreadArticles is unused.")
|
||||
}
|
||||
|
||||
guard let account = account else {
|
||||
assertionFailure("Expected feed.account, but got nil.")
|
||||
return Set<Article>()
|
||||
}
|
||||
return account.fetchUnreadArticles(for: self)
|
||||
public func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
|
||||
preconditionFailure("feed.fetchUnreadArticlesAsync is unused.")
|
||||
}
|
||||
}
|
||||
|
||||
extension Folder: ArticleFetcher {
|
||||
|
||||
public func fetchArticles() -> Set<Article> {
|
||||
|
||||
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 {
|
||||
assertionFailure("Expected folder.account, but got nil.")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1131,7 +1131,7 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
|
||||
let markUnreadArticles = account.fetchArticles(forArticleIDs: deltaUnreadArticleIDs)
|
||||
let markUnreadArticles = account.fetchArticles(.articleIDs(deltaUnreadArticleIDs))
|
||||
DispatchQueue.main.async {
|
||||
_ = account.update(markUnreadArticles, statusKey: .read, flag: false)
|
||||
}
|
||||
|
@ -1147,7 +1147,7 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(feedbinUnreadArticleIDs)
|
||||
let markReadArticles = account.fetchArticles(forArticleIDs: deltaReadArticleIDs)
|
||||
let markReadArticles = account.fetchArticles(.articleIDs(deltaReadArticleIDs))
|
||||
DispatchQueue.main.async {
|
||||
_ = account.update(markReadArticles, statusKey: .read, flag: true)
|
||||
}
|
||||
|
@ -1174,7 +1174,7 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
// Mark articles as starred
|
||||
let deltaStarredArticleIDs = feedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
|
||||
let markStarredArticles = account.fetchArticles(forArticleIDs: deltaStarredArticleIDs)
|
||||
let markStarredArticles = account.fetchArticles(.articleIDs(deltaStarredArticleIDs))
|
||||
DispatchQueue.main.async {
|
||||
_ = account.update(markStarredArticles, statusKey: .starred, flag: true)
|
||||
}
|
||||
|
@ -1190,7 +1190,7 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
// Mark articles as unstarred
|
||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(feedbinStarredArticleIDs)
|
||||
let markUnstarredArticles = account.fetchArticles(forArticleIDs: deltaUnstarredArticleIDs)
|
||||
let markUnstarredArticles = account.fetchArticles(.articleIDs(deltaUnstarredArticleIDs))
|
||||
DispatchQueue.main.async {
|
||||
_ = account.update(markUnstarredArticles, statusKey: .starred, flag: false)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public typealias ArticleSetBlock = (Set<Article>) -> Void
|
||||
|
||||
public struct Article: Hashable {
|
||||
|
||||
public let articleID: String // Unique database ID (possibly sync service ID)
|
||||
|
|
|
@ -12,10 +12,12 @@ import RSDatabase
|
|||
import RSParser
|
||||
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.
|
||||
|
||||
public typealias ArticleResultBlock = (Set<Article>) -> Void
|
||||
// Main thread only.
|
||||
|
||||
public typealias UnreadCountDictionary = [String: Int] // feedID: unreadCount
|
||||
public typealias UnreadCountCompletionBlock = (UnreadCountDictionary) -> Void
|
||||
public typealias UpdateArticlesWithFeedCompletionBlock = (Set<Article>?, Set<Article>?) -> Void //newArticles, updatedArticles
|
||||
|
||||
|
@ -46,38 +48,60 @@ public final class ArticlesDatabase {
|
|||
|
||||
// MARK: - Fetching Articles
|
||||
|
||||
public func fetchArticles(for feedID: String) -> Set<Article> {
|
||||
public func fetchArticles(_ feedID: String) -> Set<Article> {
|
||||
return articlesTable.fetchArticles(feedID)
|
||||
}
|
||||
|
||||
public func fetchArticles(forArticleIDs articleIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchArticles(forArticleIDs: articleIDs)
|
||||
public func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchArticles(articleIDs: articleIDs)
|
||||
}
|
||||
|
||||
public func fetchArticlesAsync(for feedID: String, _ resultBlock: @escaping ArticleResultBlock) {
|
||||
articlesTable.fetchArticlesAsync(feedID, withLimits: true, resultBlock)
|
||||
public func fetchUnreadArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchUnreadArticles(feedIDs)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticles(for feedIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchUnreadArticles(for: feedIDs)
|
||||
public func fetchTodayArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchTodayArticles(feedIDs)
|
||||
}
|
||||
|
||||
public func fetchTodayArticles(for feedIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchTodayArticles(for: feedIDs)
|
||||
public func fetchStarredArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchStarredArticles(feedIDs)
|
||||
}
|
||||
|
||||
public func fetchStarredArticles(for feedIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchStarredArticles(for: feedIDs)
|
||||
public func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchArticlesMatching(searchString, feedIDs)
|
||||
}
|
||||
|
||||
public func fetchArticlesMatching(_ searchString: String, for feedIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchArticlesMatching(searchString, for: feedIDs)
|
||||
// MARK: - Fetching Articles Async
|
||||
|
||||
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
|
||||
|
||||
public func fetchUnreadCounts(for feedIDs: Set<String>, _ completion: @escaping UnreadCountCompletionBlock) {
|
||||
articlesTable.fetchUnreadCounts(feedIDs, completion)
|
||||
public func fetchUnreadCounts(for feedIDs: Set<String>, _ callback: @escaping UnreadCountCompletionBlock) {
|
||||
articlesTable.fetchUnreadCounts(feedIDs, callback)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
public func fetchAllNonZeroUnreadCounts(_ completion: @escaping UnreadCountCompletionBlock) {
|
||||
articlesTable.fetchAllUnreadCounts(completion)
|
||||
public func fetchAllNonZeroUnreadCounts(_ callback: @escaping UnreadCountCompletionBlock) {
|
||||
articlesTable.fetchAllUnreadCounts(callback)
|
||||
}
|
||||
|
||||
// MARK: - Saving and Updating Articles
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
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 */; };
|
||||
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 */; };
|
||||
848E3EBD20FBCFDE0004B7ED /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EBC20FBCFDE0004B7ED /* RSDatabase.framework */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
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; };
|
||||
|
@ -178,7 +176,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
84E156E91F0AB80500F8CC05 /* ArticlesDatabase.swift */,
|
||||
848AD2951F58A91E004FB0EC /* UnreadCountDictionary.swift */,
|
||||
845580661F0AEBCD003CCFA1 /* Constants.swift */,
|
||||
84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */,
|
||||
8477ACBB2221E76F00DF7F37 /* SearchTable.swift */,
|
||||
|
@ -356,13 +353,13 @@
|
|||
TargetAttributes = {
|
||||
844BEE361F0AB3AA004AB7CD = {
|
||||
CreatedOnToolsVersion = 8.3.2;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = 9C84TZ7Q6Z;
|
||||
LastSwiftMigration = 0830;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
844BEE3F1F0AB3AB004AB7CD = {
|
||||
CreatedOnToolsVersion = 8.3.2;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = 9C84TZ7Q6Z;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
|
@ -501,7 +498,6 @@
|
|||
files = (
|
||||
845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */,
|
||||
843CB9961F34174100EE6581 /* Author+Database.swift in Sources */,
|
||||
848AD2961F58A91E004FB0EC /* UnreadCountDictionary.swift in Sources */,
|
||||
845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */,
|
||||
8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */,
|
||||
8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */,
|
||||
|
|
|
@ -29,6 +29,8 @@ final class ArticlesTable: DatabaseTable {
|
|||
private var articleCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)!
|
||||
private var maximumArticleCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 4 * 31)!
|
||||
|
||||
private typealias ArticlesFetchMethod = (FMDatabase) -> Set<Article>
|
||||
|
||||
init(name: String, accountID: String, queue: RSDatabaseQueue) {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: Fetching
|
||||
// MARK: - Fetch Articles for Feed
|
||||
|
||||
func fetchArticles(_ feedID: String) -> Set<Article> {
|
||||
|
||||
var articles = Set<Article>()
|
||||
return fetchArticles{ self.fetchArticlesForFeedID(feedID, withLimits: true, $0) }
|
||||
}
|
||||
|
||||
queue.fetchSync { (database) in
|
||||
articles = self.fetchArticlesForFeedID(feedID, withLimits: true, database: database)
|
||||
func fetchArticlesAsync(_ feedID: String, _ callback: @escaping ArticleSetBlock) {
|
||||
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> {
|
||||
|
||||
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> {
|
||||
func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>) -> Set<Article> {
|
||||
var articles: Set<Article> = Set<Article>()
|
||||
queue.fetchSync { (database) in
|
||||
articles = self.fetchArticlesMatching(searchString, database)
|
||||
|
@ -97,6 +156,32 @@ final class ArticlesTable: DatabaseTable {
|
|||
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>? {
|
||||
let parameters = articleIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
|
||||
|
@ -159,7 +244,7 @@ final class ArticlesTable: DatabaseTable {
|
|||
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 newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
|
||||
|
@ -346,6 +431,23 @@ private extension ArticlesTable {
|
|||
|
||||
// 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> {
|
||||
|
||||
// 1. Create DatabaseArticles without related objects.
|
||||
|
@ -453,97 +555,6 @@ private extension ArticlesTable {
|
|||
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> {
|
||||
let sql = "select rowid from search where search match ?;"
|
||||
let sqlSearchString = sqliteSearchString(with: searchString)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -33,8 +33,10 @@ final class TimelineContainerViewController: NSViewController {
|
|||
private lazy var regularTimelineViewController = {
|
||||
return TimelineViewController(delegate: self)
|
||||
}()
|
||||
private lazy var searchTimelineViewController = {
|
||||
return TimelineViewController(delegate: self)
|
||||
private lazy var searchTimelineViewController: TimelineViewController = {
|
||||
let viewController = TimelineViewController(delegate: self)
|
||||
viewController.showsSearchResults = true
|
||||
return viewController
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
|
|
@ -40,9 +40,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
}
|
||||
|
||||
selectionDidChange(nil)
|
||||
fetchArticles()
|
||||
if articles.count > 0 {
|
||||
tableView.scrollRowToVisible(0)
|
||||
if showsSearchResults {
|
||||
fetchAndReplaceArticlesAsync()
|
||||
}
|
||||
else {
|
||||
fetchAndReplaceArticlesSync()
|
||||
if articles.count > 0 {
|
||||
tableView.scrollRowToVisible(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +55,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
|
||||
private weak var delegate: TimelineDelegate?
|
||||
var sharingServiceDelegate: NSSharingServiceDelegate?
|
||||
|
||||
|
||||
var showsSearchResults = false
|
||||
var selectedArticles: [Article] {
|
||||
return Array(articles.articlesForIndexes(tableView.selectedRowIndexes))
|
||||
}
|
||||
|
@ -79,6 +85,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
}
|
||||
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
private var fetchSerialNumber = 0
|
||||
private let fetchRequestQueue = FetchRequestQueue()
|
||||
private var articleRowMap = [String: Int]() // articleID: rowIndex
|
||||
private var cellAppearance: TimelineCellAppearance!
|
||||
private var cellAppearanceWithAvatar: TimelineCellAppearance!
|
||||
|
@ -100,7 +108,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
}
|
||||
|
||||
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 {
|
||||
didSet {
|
||||
|
@ -502,13 +510,13 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
|
||||
@objc func accountStateDidChange(_ note: Notification) {
|
||||
if representedObjectsContainsAnyPseudoFeed() {
|
||||
fetchArticles()
|
||||
fetchAndReplaceArticlesAsync()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func accountsDidChange(_ note: Notification) {
|
||||
if representedObjectsContainsAnyPseudoFeed() {
|
||||
fetchArticles()
|
||||
fetchAndReplaceArticlesAsync()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -521,7 +529,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
@objc func calendarDayChanged(_ note: Notification) {
|
||||
if representedObjectsContainsTodayFeed() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.fetchArticles()
|
||||
self?.fetchAndReplaceArticlesAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -606,24 +614,25 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
}
|
||||
|
||||
@objc func fetchAndMergeArticles() {
|
||||
|
||||
guard let representedObjects = representedObjects else {
|
||||
return
|
||||
}
|
||||
|
||||
performBlockAndRestoreSelection {
|
||||
|
||||
var unsortedArticles = fetchUnsortedArticles(for: representedObjects)
|
||||
|
||||
fetchUnsortedArticlesAsync(for: representedObjects) { [weak self] (unsortedArticles) in
|
||||
// Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles.
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let unsortedArticleIDs = unsortedArticles.articleIDs()
|
||||
for article in articles {
|
||||
var updatedArticles = unsortedArticles
|
||||
for article in strongSelf.articles {
|
||||
if !unsortedArticleIDs.contains(article.articleID) {
|
||||
unsortedArticles.insert(article)
|
||||
updatedArticles.insert(article)
|
||||
}
|
||||
}
|
||||
|
||||
updateArticles(with: unsortedArticles)
|
||||
strongSelf.performBlockAndRestoreSelection {
|
||||
strongSelf.replaceArticles(with: updatedArticles)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -842,7 +851,6 @@ private extension TimelineViewController {
|
|||
}
|
||||
|
||||
func emptyTheTimeline() {
|
||||
|
||||
if !articles.isEmpty {
|
||||
articles = [Article]()
|
||||
}
|
||||
|
@ -852,7 +860,7 @@ private extension TimelineViewController {
|
|||
|
||||
performBlockAndRestoreSelection {
|
||||
let unsortedArticles = Set(articles)
|
||||
updateArticles(with: unsortedArticles)
|
||||
replaceArticles(with: unsortedArticles)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -919,18 +927,39 @@ private extension TimelineViewController {
|
|||
|
||||
// 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 there’s no async delay,
|
||||
// so that the entire display refreshes at once.
|
||||
// It’s a better user experience this way.
|
||||
cancelPendingAsyncFetches()
|
||||
guard let representedObjects = representedObjects else {
|
||||
emptyTheTimeline()
|
||||
return
|
||||
}
|
||||
|
||||
let fetchedArticles = fetchUnsortedArticles(for: representedObjects)
|
||||
updateArticles(with: fetchedArticles)
|
||||
let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects)
|
||||
replaceArticles(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)
|
||||
if articles != sortedArticles {
|
||||
|
@ -938,20 +967,35 @@ private extension TimelineViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func fetchUnsortedArticles(for representedObjects: [Any]) -> Set<Article> {
|
||||
|
||||
var fetchedArticles = Set<Article>()
|
||||
|
||||
for object in representedObjects {
|
||||
|
||||
if let articleFetcher = object as? ArticleFetcher {
|
||||
fetchedArticles.formUnion(articleFetcher.fetchArticles())
|
||||
}
|
||||
func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<Article> {
|
||||
cancelPendingAsyncFetches()
|
||||
let articleFetchers = representedObjects.compactMap{ $0 as? ArticleFetcher }
|
||||
if articleFetchers.isEmpty {
|
||||
return Set<Article>()
|
||||
}
|
||||
|
||||
var fetchedArticles = Set<Article>()
|
||||
for articleFetcher in articleFetchers {
|
||||
fetchedArticles.formUnion(articleFetcher.fetchArticles())
|
||||
}
|
||||
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 it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t 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]) {
|
||||
|
||||
let indexesToSelect = indexesForArticleIDs(Set(articleIDs))
|
||||
|
|
|
@ -267,8 +267,14 @@
|
|||
84C9FC9D2262A1A900D921D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC9B2262A1A900D921D6 /* Assets.xcassets */; };
|
||||
84C9FCA12262A1B300D921D6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC9F2262A1B300D921D6 /* Main.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 */; };
|
||||
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 */; };
|
||||
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1388,6 +1397,8 @@
|
|||
84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */,
|
||||
849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */,
|
||||
849A976A1ED9EBC8007D329B /* TimelineTableView.swift */,
|
||||
84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */,
|
||||
84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */,
|
||||
844B5B6C1FEA282400C7C76A /* Keyboard */,
|
||||
84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */,
|
||||
849A976F1ED9EC04007D329B /* Cell */,
|
||||
|
@ -1724,6 +1735,7 @@
|
|||
84F2D5351FC22FCB00998D64 /* PseudoFeed.swift */,
|
||||
84F2D5391FC2308B00998D64 /* UnreadFeed.swift */,
|
||||
845EE7C01FC2488C00854A1F /* SmartFeed.swift */,
|
||||
84DEE56422C32CA4005FC42C /* SmartFeedDelegate.swift */,
|
||||
84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */,
|
||||
845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */,
|
||||
8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */,
|
||||
|
@ -1942,12 +1954,12 @@
|
|||
ORGANIZATIONNAME = "Ranchero Software";
|
||||
TargetAttributes = {
|
||||
6581C73220CED60000F4AD34 = {
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = M8L2WTLA8W;
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
840D617B2029031C009BC708 = {
|
||||
CreatedOnToolsVersion = 9.3;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = M8L2WTLA8W;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
|
@ -1963,7 +1975,7 @@
|
|||
};
|
||||
849C645F1ED37A5D003D8FC0 = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = M8L2WTLA8W;
|
||||
ProvisioningStyle = Manual;
|
||||
SystemCapabilities = {
|
||||
com.apple.HardenedRuntime = {
|
||||
|
@ -1973,7 +1985,7 @@
|
|||
};
|
||||
849C64701ED37A5D003D8FC0 = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = 9C84TZ7Q6Z;
|
||||
ProvisioningStyle = Automatic;
|
||||
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
||||
};
|
||||
|
@ -2335,6 +2347,7 @@
|
|||
51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */,
|
||||
51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */,
|
||||
5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */,
|
||||
84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
|
||||
51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */,
|
||||
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
|
||||
5183CCEF227125970010922C /* SettingsViewController.swift in Sources */,
|
||||
|
@ -2368,6 +2381,7 @@
|
|||
515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */,
|
||||
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */,
|
||||
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
|
||||
84CAFCA522BC8C08007694F0 /* FetchRequestQueue.swift in Sources */,
|
||||
51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */,
|
||||
51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */,
|
||||
51C45290226509C100C03939 /* PseudoFeed.swift in Sources */,
|
||||
|
@ -2382,6 +2396,7 @@
|
|||
5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */,
|
||||
51C452882265093600C03939 /* AddFeedViewController.swift in Sources */,
|
||||
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
|
||||
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
|
||||
5183CCE3226F314C0010922C /* ProgressTableViewController.swift in Sources */,
|
||||
512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */,
|
||||
51C45268226508F600C03939 /* MasterFeedUnreadCountView.swift in Sources */,
|
||||
|
@ -2483,6 +2498,7 @@
|
|||
849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */,
|
||||
84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */,
|
||||
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */,
|
||||
84DEE56522C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
|
||||
845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */,
|
||||
51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */,
|
||||
849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */,
|
||||
|
@ -2493,6 +2509,7 @@
|
|||
849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */,
|
||||
84C9FC6722629B9000D921D6 /* AppDelegate.swift in Sources */,
|
||||
84C9FC7A22629E1200D921D6 /* AccountsTableViewBackgroundView.swift in Sources */,
|
||||
84CAFCAF22BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
|
||||
8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */,
|
||||
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
|
||||
5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */,
|
||||
|
@ -2514,6 +2531,7 @@
|
|||
D5E4CC64202C1AC1009B4FFC /* MainWindowController+Scriptability.swift in Sources */,
|
||||
84C9FC7922629E1200D921D6 /* PreferencesWindowController.swift in Sources */,
|
||||
84411E711FE5FBFA004B527F /* SmallIconProvider.swift in Sources */,
|
||||
84CAFCA422BC8C08007694F0 /* FetchRequestQueue.swift in Sources */,
|
||||
844B5B591FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift in Sources */,
|
||||
84C9FC7C22629E1200D921D6 /* AccountsPreferencesViewController.swift in Sources */,
|
||||
51EC114C2149FE3300B296E3 /* FolderTreeMenu.swift in Sources */,
|
||||
|
|
|
@ -18,9 +18,11 @@ struct SearchFeedDelegate: SmartFeedDelegate {
|
|||
|
||||
let nameForDisplayPrefix = NSLocalizedString("Search: ", comment: "Search smart feed title prefix")
|
||||
let searchString: String
|
||||
let fetchType: FetchType
|
||||
|
||||
init(searchString: String) {
|
||||
self.searchString = searchString
|
||||
self.fetchType = .search(searchString)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,6 @@ import RSCore
|
|||
import Articles
|
||||
import Account
|
||||
|
||||
protocol SmartFeedDelegate: DisplayNameProvider, ArticleFetcher {
|
||||
func fetchUnreadCount(for: Account, callback: @escaping (Int) -> Void)
|
||||
}
|
||||
|
||||
final class SmartFeed: PseudoFeed {
|
||||
|
||||
var nameForDisplay: String {
|
||||
|
@ -61,9 +57,17 @@ extension SmartFeed: ArticleFetcher {
|
|||
return delegate.fetchArticles()
|
||||
}
|
||||
|
||||
func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) {
|
||||
delegate.fetchArticlesAsync(callback)
|
||||
}
|
||||
|
||||
func fetchUnreadArticles() -> Set<Article> {
|
||||
return delegate.fetchUnreadArticles()
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
|
||||
delegate.fetchUnreadArticlesAsync(callback)
|
||||
}
|
||||
}
|
||||
|
||||
private extension SmartFeed {
|
||||
|
|
|
@ -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()) }
|
||||
}
|
||||
}
|
|
@ -10,30 +10,14 @@ import Foundation
|
|||
import Articles
|
||||
import Account
|
||||
|
||||
// Main thread only.
|
||||
|
||||
struct StarredFeedDelegate: SmartFeedDelegate {
|
||||
|
||||
let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title")
|
||||
let fetchType: FetchType = .starred
|
||||
|
||||
func fetchUnreadCount(for account: Account, callback: @escaping (Int) -> Void) {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,26 +13,10 @@ import Account
|
|||
struct TodayFeedDelegate: SmartFeedDelegate {
|
||||
|
||||
let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title")
|
||||
let fetchType = FetchType.today
|
||||
|
||||
func fetchUnreadCount(for account: Account, callback: @escaping (Int) -> Void) {
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ import Articles
|
|||
final class UnreadFeed: PseudoFeed {
|
||||
|
||||
let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title")
|
||||
|
||||
let fetchType = FetchType.unread
|
||||
|
||||
var unreadCount = 0 {
|
||||
didSet {
|
||||
if unreadCount != oldValue {
|
||||
|
@ -50,16 +51,18 @@ final class UnreadFeed: PseudoFeed {
|
|||
extension UnreadFeed: ArticleFetcher {
|
||||
|
||||
func fetchArticles() -> Set<Article> {
|
||||
|
||||
return fetchUnreadArticles()
|
||||
}
|
||||
|
||||
func fetchUnreadArticles() -> Set<Article> {
|
||||
func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) {
|
||||
fetchUnreadArticlesAsync(callback)
|
||||
}
|
||||
|
||||
var articles = Set<Article>()
|
||||
for account in AccountManager.shared.activeAccounts {
|
||||
articles.formUnion(account.fetchUnreadArticles())
|
||||
}
|
||||
return articles
|
||||
func fetchUnreadArticles() -> Set<Article> {
|
||||
return AccountManager.shared.fetchArticles(fetchType)
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
|
||||
AccountManager.shared.fetchArticlesAsync(fetchType, callback)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue