mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-01-31 03:05:00 +01:00
Port async timeline fetching from desktop NNW
This commit is contained in:
parent
0c455c086d
commit
2719772701
@ -50,7 +50,8 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
|
private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
|
||||||
|
private var fetchSerialNumber = 0
|
||||||
|
private let fetchRequestQueue = FetchRequestQueue()
|
||||||
private var articleRowMap = [String: Int]() // articleID: rowIndex
|
private var articleRowMap = [String: Int]() // articleID: rowIndex
|
||||||
|
|
||||||
private var animatingChanges = false
|
private var animatingChanges = false
|
||||||
@ -107,14 +108,13 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||||||
|
|
||||||
var timelineFetcher: ArticleFetcher? {
|
var timelineFetcher: ArticleFetcher? {
|
||||||
didSet {
|
didSet {
|
||||||
unreadCount = 0
|
|
||||||
currentArticleIndexPath = nil
|
currentArticleIndexPath = nil
|
||||||
if timelineFetcher is Feed {
|
if timelineFetcher is Feed {
|
||||||
showFeedNames = false
|
showFeedNames = false
|
||||||
} else {
|
} else {
|
||||||
showFeedNames = true
|
showFeedNames = true
|
||||||
}
|
}
|
||||||
fetchArticles()
|
fetchAndReplaceArticlesSync()
|
||||||
NotificationCenter.default.post(name: .ArticlesReinitialized, object: self, userInfo: nil)
|
NotificationCenter.default.post(name: .ArticlesReinitialized, object: self, userInfo: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -269,10 +269,16 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func accountStateDidChange(_ note: Notification) {
|
@objc func accountStateDidChange(_ note: Notification) {
|
||||||
|
if timelineFetcherContainsAnyPseudoFeed() {
|
||||||
|
fetchAndReplaceArticlesSync()
|
||||||
|
}
|
||||||
rebuildBackingStores()
|
rebuildBackingStores()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func accountsDidChange(_ note: Notification) {
|
@objc func accountsDidChange(_ note: Notification) {
|
||||||
|
if timelineFetcherContainsAnyPseudoFeed() {
|
||||||
|
fetchAndReplaceArticlesSync()
|
||||||
|
}
|
||||||
rebuildBackingStores()
|
rebuildBackingStores()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -872,18 +878,6 @@ private extension AppCoordinator {
|
|||||||
|
|
||||||
// MARK: Fetching Articles
|
// MARK: Fetching Articles
|
||||||
|
|
||||||
func fetchArticles() {
|
|
||||||
|
|
||||||
guard let timelineFetcher = timelineFetcher else {
|
|
||||||
articles = ArticleArray()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let fetchedArticles = timelineFetcher.fetchArticles()
|
|
||||||
updateArticles(with: fetchedArticles)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func emptyTheTimeline() {
|
func emptyTheTimeline() {
|
||||||
if !articles.isEmpty {
|
if !articles.isEmpty {
|
||||||
articles = [Article]()
|
articles = [Article]()
|
||||||
@ -891,10 +885,10 @@ private extension AppCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sortDirectionDidChange() {
|
func sortDirectionDidChange() {
|
||||||
updateArticles(with: Set(articles))
|
replaceArticles(with: Set(articles))
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateArticles(with unsortedArticles: Set<Article>) {
|
func replaceArticles(with unsortedArticles: Set<Article>) {
|
||||||
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection)
|
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection)
|
||||||
if articles != sortedArticles {
|
if articles != sortedArticles {
|
||||||
articles = sortedArticles
|
articles = sortedArticles
|
||||||
@ -922,6 +916,8 @@ private extension AppCoordinator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Fetching Articles
|
||||||
|
|
||||||
func queueFetchAndMergeArticles() {
|
func queueFetchAndMergeArticles() {
|
||||||
fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles))
|
fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles))
|
||||||
}
|
}
|
||||||
@ -932,20 +928,72 @@ private extension AppCoordinator {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var unsortedArticles = timelineFetcher.fetchArticles()
|
fetchUnsortedArticlesAsync(for: [timelineFetcher]) { [weak self] (unsortedArticles) in
|
||||||
|
// 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 {
|
||||||
let unsortedArticleIDs = unsortedArticles.articleIDs()
|
return
|
||||||
for article in articles {
|
|
||||||
if !unsortedArticleIDs.contains(article.articleID) {
|
|
||||||
unsortedArticles.insert(article)
|
|
||||||
}
|
}
|
||||||
|
let unsortedArticleIDs = unsortedArticles.articleIDs()
|
||||||
|
var updatedArticles = unsortedArticles
|
||||||
|
for article in strongSelf.articles {
|
||||||
|
if !unsortedArticleIDs.contains(article.articleID) {
|
||||||
|
updatedArticles.insert(article)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.replaceArticles(with: updatedArticles)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateArticles(with: unsortedArticles)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 timelineFetcher = timelineFetcher else {
|
||||||
|
emptyTheTimeline()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let fetchedArticles = fetchUnsortedArticlesSync(for: [timelineFetcher])
|
||||||
|
replaceArticles(with: fetchedArticles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelPendingAsyncFetches() {
|
||||||
|
fetchSerialNumber += 1
|
||||||
|
fetchRequestQueue.cancelAllRequests()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 timelineFetcherContainsAnyPseudoFeed() -> Bool {
|
func timelineFetcherContainsAnyPseudoFeed() -> Bool {
|
||||||
if timelineFetcher is PseudoFeed {
|
if timelineFetcher is PseudoFeed {
|
||||||
return true
|
return true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user