Add basic timeline fetching

This commit is contained in:
Maurice Parker 2020-07-01 12:30:55 -05:00
parent c1eb9ab927
commit 74f34b1c78
6 changed files with 102 additions and 20 deletions

@ -20,10 +20,6 @@ final class SceneModel: ObservableObject {
extension SceneModel: SidebarModelDelegate { extension SceneModel: SidebarModelDelegate {
func sidebarSelectionDidChange(_: SidebarModel, feeds: [Feed]?) {
print("**** sidebar selection changed ***")
}
func unreadCount(for feed: Feed) -> Int { func unreadCount(for feed: Feed) -> Int {
// TODO: Get the count from the timeline if Feed is the current timeline // TODO: Get the count from the timeline if Feed is the current timeline
return feed.unreadCount return feed.unreadCount

@ -11,7 +11,6 @@ import RSCore
import Account import Account
protocol SidebarModelDelegate: class { protocol SidebarModelDelegate: class {
func sidebarSelectionDidChange(_: SidebarModel, feeds: [Feed]?)
func unreadCount(for: Feed) -> Int func unreadCount(for: Feed) -> Int
} }

@ -30,10 +30,3 @@ struct TimelineContainerView: View {
} }
} }
struct TimelineContainerView_Previews: PreviewProvider {
static var previews: some View {
TimelineContainerView()
.environmentObject(SceneModel())
}
}

@ -13,7 +13,11 @@ struct TimelineItemView: View {
var timelineItem: TimelineItem var timelineItem: TimelineItem
var body: some View { var body: some View {
Text(verbatim: timelineItem.article.title ?? "N/A") VStack {
Text(verbatim: timelineItem.article.title ?? "N/A")
.frame(maxWidth: .infinity, alignment: .leading)
Divider()
}
} }
} }

@ -9,6 +9,7 @@
import Foundation import Foundation
import RSCore import RSCore
import Account import Account
import Articles
protocol TimelineModelDelegate: class { protocol TimelineModelDelegate: class {
func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed) func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed)
@ -21,6 +22,28 @@ class TimelineModel: ObservableObject {
@Published var timelineItems = [TimelineItem]() @Published var timelineItems = [TimelineItem]()
private var feeds = [Feed]() private var feeds = [Feed]()
private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue()
private var exceptionArticleFetcher: ArticleFetcher?
private var isReadFiltered = false
private var articles = [Article]()
private var sortDirection = AppDefaults.timelineSortDirection {
didSet {
if sortDirection != oldValue {
sortParametersDidChange()
}
}
}
private var groupByFeed = AppDefaults.timelineGroupByFeed {
didSet {
if groupByFeed != oldValue {
sortParametersDidChange()
}
}
}
init() { init() {
} }
@ -29,12 +52,72 @@ class TimelineModel: ObservableObject {
func rebuildTimelineItems(_ feed: Feed) { func rebuildTimelineItems(_ feed: Feed) {
feeds = [feed] feeds = [feed]
fetchAndReplaceArticlesAsync()
} }
} }
// MARK: Private // MARK: Private
private extension TimelineModel { private extension TimelineModel {
func sortParametersDidChange() {
performBlockAndRestoreSelection {
let unsortedArticles = Set(articles)
replaceArticles(with: unsortedArticles)
}
}
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
// let savedSelection = selectedArticleIDs()
block()
// restoreSelection(savedSelection)
}
// MARK: Article Fetching
func fetchAndReplaceArticlesAsync() {
cancelPendingAsyncFetches()
var fetchers = feeds as [ArticleFetcher]
if let fetcher = exceptionArticleFetcher {
fetchers.append(fetcher)
exceptionArticleFetcher = nil
}
fetchUnsortedArticlesAsync(for: fetchers) { [weak self] (articles) in
self?.replaceArticles(with: articles)
}
}
func cancelPendingAsyncFetches() {
fetchSerialNumber += 1
fetchRequestQueue.cancelAllRequests()
}
func fetchUnsortedArticlesAsync(for representedObjects: [Any], completion: @escaping ArticleSetBlock) {
// The callback will *not* be called if the fetch is no longer relevant that is,
// if its been superseded by a newer fetch, or the timeline was emptied, etc., it wont get called.
precondition(Thread.isMainThread)
cancelPendingAsyncFetches()
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: isReadFiltered ?? true, representedObjects: representedObjects) { [weak self] (articles, operation) in
precondition(Thread.isMainThread)
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
return
}
completion(articles)
}
fetchRequestQueue.add(fetchOperation)
}
func replaceArticles(with unsortedArticles: Set<Article>) {
articles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed)
timelineItems = articles.map { TimelineItem(article: $0) }
// TODO: Update unread counts and other item done in didSet on AppKit
}
// MARK: - Notifications
} }

@ -13,12 +13,19 @@ struct TimelineView: View {
@EnvironmentObject private var timelineModel: TimelineModel @EnvironmentObject private var timelineModel: TimelineModel
var body: some View { var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) ScrollView {
} LazyVStack() {
} ForEach(timelineModel.timelineItems) { timelineItem in
TimelineItemView(timelineItem: timelineItem)
struct TimelineView_Previews: PreviewProvider { }
static var previews: some View { }
TimelineView() }
} }
// var body: some View {
// List(timelineModel.timelineItems) { timelineItem in
// TimelineItemView(timelineItem: timelineItem)
// }
// }
} }