From 74f34b1c7865aeb0b8616882b26af781378103e9 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 12:30:55 -0500 Subject: [PATCH] Add basic timeline fetching --- Multiplatform/Shared/SceneModel.swift | 4 - .../Shared/Sidebar/SidebarModel.swift | 1 - .../Timeline/TimelineContainerView.swift | 7 -- .../Shared/Timeline/TimelineItemView.swift | 6 +- .../Shared/Timeline/TimelineModel.swift | 83 +++++++++++++++++++ .../Shared/Timeline/TimelineView.swift | 21 +++-- 6 files changed, 102 insertions(+), 20 deletions(-) diff --git a/Multiplatform/Shared/SceneModel.swift b/Multiplatform/Shared/SceneModel.swift index 033b56148..f09b48f6d 100644 --- a/Multiplatform/Shared/SceneModel.swift +++ b/Multiplatform/Shared/SceneModel.swift @@ -20,10 +20,6 @@ final class SceneModel: ObservableObject { extension SceneModel: SidebarModelDelegate { - func sidebarSelectionDidChange(_: SidebarModel, feeds: [Feed]?) { - print("**** sidebar selection changed ***") - } - func unreadCount(for feed: Feed) -> Int { // TODO: Get the count from the timeline if Feed is the current timeline return feed.unreadCount diff --git a/Multiplatform/Shared/Sidebar/SidebarModel.swift b/Multiplatform/Shared/Sidebar/SidebarModel.swift index 38aff59a1..335d6b260 100644 --- a/Multiplatform/Shared/Sidebar/SidebarModel.swift +++ b/Multiplatform/Shared/Sidebar/SidebarModel.swift @@ -11,7 +11,6 @@ import RSCore import Account protocol SidebarModelDelegate: class { - func sidebarSelectionDidChange(_: SidebarModel, feeds: [Feed]?) func unreadCount(for: Feed) -> Int } diff --git a/Multiplatform/Shared/Timeline/TimelineContainerView.swift b/Multiplatform/Shared/Timeline/TimelineContainerView.swift index 010731817..008e28d42 100644 --- a/Multiplatform/Shared/Timeline/TimelineContainerView.swift +++ b/Multiplatform/Shared/Timeline/TimelineContainerView.swift @@ -30,10 +30,3 @@ struct TimelineContainerView: View { } } - -struct TimelineContainerView_Previews: PreviewProvider { - static var previews: some View { - TimelineContainerView() - .environmentObject(SceneModel()) - } -} diff --git a/Multiplatform/Shared/Timeline/TimelineItemView.swift b/Multiplatform/Shared/Timeline/TimelineItemView.swift index d98943a2a..021c7cb5c 100644 --- a/Multiplatform/Shared/Timeline/TimelineItemView.swift +++ b/Multiplatform/Shared/Timeline/TimelineItemView.swift @@ -13,7 +13,11 @@ struct TimelineItemView: View { var timelineItem: TimelineItem 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() + } } } diff --git a/Multiplatform/Shared/Timeline/TimelineModel.swift b/Multiplatform/Shared/Timeline/TimelineModel.swift index b21da157e..6f06fa7ff 100644 --- a/Multiplatform/Shared/Timeline/TimelineModel.swift +++ b/Multiplatform/Shared/Timeline/TimelineModel.swift @@ -9,6 +9,7 @@ import Foundation import RSCore import Account +import Articles protocol TimelineModelDelegate: class { func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed) @@ -21,6 +22,28 @@ class TimelineModel: ObservableObject { @Published var timelineItems = [TimelineItem]() 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() { } @@ -29,12 +52,72 @@ class TimelineModel: ObservableObject { func rebuildTimelineItems(_ feed: Feed) { feeds = [feed] + fetchAndReplaceArticlesAsync() } } // MARK: Private + 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 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, 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
) { + 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 + } diff --git a/Multiplatform/Shared/Timeline/TimelineView.swift b/Multiplatform/Shared/Timeline/TimelineView.swift index 321a5cd2e..1989e5ea5 100644 --- a/Multiplatform/Shared/Timeline/TimelineView.swift +++ b/Multiplatform/Shared/Timeline/TimelineView.swift @@ -13,12 +13,19 @@ struct TimelineView: View { @EnvironmentObject private var timelineModel: TimelineModel var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} - -struct TimelineView_Previews: PreviewProvider { - static var previews: some View { - TimelineView() + ScrollView { + LazyVStack() { + ForEach(timelineModel.timelineItems) { timelineItem in + TimelineItemView(timelineItem: timelineItem) + } + } + } } + +// var body: some View { +// List(timelineModel.timelineItems) { timelineItem in +// TimelineItemView(timelineItem: timelineItem) +// } +// } + }