NetNewsWire/Multiplatform/Shared/Timeline/TimelineModel.swift

201 lines
5.4 KiB
Swift
Raw Normal View History

2020-06-30 18:03:33 +02:00
//
// TimelineModel.swift
// NetNewsWire
//
// Created by Maurice Parker on 6/30/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import RSCore
import Account
2020-07-01 19:30:55 +02:00
import Articles
2020-06-30 18:03:33 +02:00
protocol TimelineModelDelegate: class {
func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed)
}
class TimelineModel: ObservableObject {
weak var delegate: TimelineModelDelegate?
@Published var nameForDisplay = ""
2020-06-30 18:03:33 +02:00
@Published var timelineItems = [TimelineItem]()
2020-07-01 19:30:55 +02:00
private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue()
private var exceptionArticleFetcher: ArticleFetcher?
private var isReadFiltered = false
private var articles = [Article]() {
didSet {
articleDictionaryNeedsUpdate = true
}
}
2020-07-01 19:30:55 +02:00
private var articleDictionaryNeedsUpdate = true
private var _idToArticleDictionary = [String: Article]()
private var idToArticleDictionary: [String: Article] {
if articleDictionaryNeedsUpdate {
rebuildArticleDictionaries()
}
return _idToArticleDictionary
}
private var sortDirection = AppDefaults.shared.timelineSortDirection {
2020-07-01 19:30:55 +02:00
didSet {
if sortDirection != oldValue {
sortParametersDidChange()
}
}
}
private var groupByFeed = AppDefaults.shared.timelineGroupByFeed {
2020-07-01 19:30:55 +02:00
didSet {
if groupByFeed != oldValue {
sortParametersDidChange()
}
}
}
2020-07-01 18:13:11 +02:00
2020-06-30 18:03:33 +02:00
init() {
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
2020-06-30 18:03:33 +02:00
}
// MARK: API
func rebuildTimelineItems(feeds: [Feed]) {
if feeds.count == 1 {
nameForDisplay = feeds.first!.nameForDisplay
} else {
nameForDisplay = NSLocalizedString("Multiple", comment: "Multiple Feeds")
}
fetchAndReplaceArticlesAsync(feeds: feeds)
2020-06-30 18:03:33 +02:00
}
// TODO: Replace this with ScrollViewReader if we have to keep it
func loadMoreTimelineItemsIfNecessary(_ timelineItem: TimelineItem) {
let thresholdIndex = timelineItems.index(timelineItems.endIndex, offsetBy: -10)
if timelineItems.firstIndex(where: { $0.id == timelineItem.id }) == thresholdIndex {
nextBatch()
}
}
func articleFor(_ articleID: String) -> Article? {
return idToArticleDictionary[articleID]
}
func findPrevArticle(_ article: Article) -> Article? {
guard let index = articles.firstIndex(of: article), index > 0 else {
return nil
}
return articles[index - 1]
}
func findNextArticle(_ article: Article) -> Article? {
guard let index = articles.firstIndex(of: article), index + 1 != articles.count else {
return nil
}
return articles[index + 1]
}
func selectArticle(_ article: Article) {
// TODO: Implement me!
}
2020-06-30 18:03:33 +02:00
}
// MARK: Private
2020-07-01 19:30:55 +02:00
2020-06-30 18:03:33 +02:00
private extension TimelineModel {
// MARK: Notifications
@objc func statusesDidChange(_ note: Notification) {
guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String> else {
return
}
for i in 0..<timelineItems.count {
if articleIDs.contains(timelineItems[i].article.articleID) {
timelineItems[i].updateStatus()
}
}
}
// MARK:
2020-07-01 19:30:55 +02:00
func sortParametersDidChange() {
performBlockAndRestoreSelection {
let unsortedArticles = Set(articles)
replaceArticles(with: unsortedArticles)
}
}
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
// let savedSelection = selectedArticleIDs()
block()
// restoreSelection(savedSelection)
}
func rebuildArticleDictionaries() {
var idDictionary = [String: Article]()
articles.forEach { article in
idDictionary[article.articleID] = article
}
_idToArticleDictionary = idDictionary
articleDictionaryNeedsUpdate = false
}
2020-07-01 19:30:55 +02:00
// MARK: Article Fetching
func fetchAndReplaceArticlesAsync(feeds: [Feed]) {
2020-07-01 19:30:55 +02:00
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, representedObjects: representedObjects) { [weak self] (articles, operation) in
2020-07-01 19:30:55 +02:00
precondition(Thread.isMainThread)
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
return
}
completion(articles)
}
fetchRequestQueue.add(fetchOperation)
}
2020-06-30 18:03:33 +02:00
2020-07-01 19:30:55 +02:00
func replaceArticles(with unsortedArticles: Set<Article>) {
articles = Array(unsortedArticles).sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupByFeed)
timelineItems = [TimelineItem]()
nextBatch()
2020-07-01 19:30:55 +02:00
// TODO: Update unread counts and other item done in didSet on AppKit
}
func nextBatch() {
let rangeEndIndex = timelineItems.endIndex + 50 > articles.endIndex ? articles.endIndex : timelineItems.endIndex + 50
let range = timelineItems.endIndex..<rangeEndIndex
for i in range {
timelineItems.append(TimelineItem(article: articles[i]))
}
}
2020-07-01 19:30:55 +02:00
// MARK: - Notifications
2020-06-30 18:03:33 +02:00
}