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
|
// 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 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) {
|
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)
|
// MARK: - Disk (Private)
|
||||||
|
|
||||||
private extension Account {
|
private extension Account {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 */,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = {
|
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() {
|
||||||
|
|
|
@ -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 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 {
|
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 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]) {
|
func selectArticles(_ articleIDs: [String]) {
|
||||||
|
|
||||||
let indexesToSelect = indexesForArticleIDs(Set(articleIDs))
|
let indexesToSelect = indexesForArticleIDs(Set(articleIDs))
|
||||||
|
|
|
@ -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 */,
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue