Files
NetNewsWire/Multiplatform/Shared/SceneModel.swift
Stuart Breckenridge 673f0ce718 Handles single and multiple sync failures
If a single sync failure is encountered a sheet is presented which allows the user to update their credentials.

If multiple sync failures are encountered an alert is shown listing the accounts which encountered errors. On iOS, this alert can take the user into Settings, but there is no obvious way to programatically pesent macOS preferences.
2020-07-25 16:40:04 +08:00

211 lines
5.8 KiB
Swift

//
// SceneModel.swift
// NetNewsWire
//
// Created by Maurice Parker on 6/28/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Combine
import Account
import Articles
import RSCore
final class SceneModel: ObservableObject {
@Published var markAllAsReadButtonState: Bool?
@Published var nextUnreadButtonState: Bool?
@Published var readButtonState: Bool?
@Published var starButtonState: Bool?
@Published var extractorButtonState: ArticleExtractorButtonState?
@Published var openInBrowserButtonState: Bool?
@Published var shareButtonState: Bool?
@Published var accountErrorMessage = ""
@Published var accountSyncErrors: [AccountSyncError] = []
var selectedArticles: [Article] {
timelineModel.selectedArticles
}
private var refreshProgressModel: RefreshProgressModel? = nil
private var articleIconSchemeHandler: ArticleIconSchemeHandler? = nil
private(set) var webViewProvider: WebViewProvider? = nil
private(set) var sidebarModel = SidebarModel()
private(set) var timelineModel = TimelineModel()
private var cancellables = Set<AnyCancellable>()
// MARK: Initialization API
/// Prepares the SceneModel to be used in the views
func startup() {
sidebarModel.delegate = self
timelineModel.delegate = self
timelineModel.startup()
self.articleIconSchemeHandler = ArticleIconSchemeHandler(sceneModel: self)
self.webViewProvider = WebViewProvider(articleIconSchemeHandler: self.articleIconSchemeHandler!)
subscribeToAccountSyncErrors()
subscribeToToolbarChangeEvents()
}
// MARK: Navigation API
/// Goes to the next unread item found in Sidebar and Timeline order, top to bottom
func goToNextUnread() {
if !timelineModel.goToNextUnread() {
timelineModel.isSelectNextUnread = true
sidebarModel.selectNextUnread.send(true)
}
}
// MARK: Article Management API
/// Marks all the articles in the Timeline as read
func markAllAsRead() {
timelineModel.markAllAsRead()
}
/// Toggles the read status for the selected articles
func toggleReadStatusForSelectedArticles() {
timelineModel.toggleReadStatusForSelectedArticles()
}
/// Toggles the star status for the selected articles
func toggleStarredStatusForSelectedArticles() {
timelineModel.toggleStarredStatusForSelectedArticles()
}
/// Opens the selected article in an external browser
func openSelectedArticleInBrowser() {
timelineModel.openSelectedArticleInBrowser()
}
/// Retrieves the article before the given article in the Timeline
func findPrevArticle(_ article: Article) -> Article? {
return timelineModel.findPrevArticle(article)
}
/// Retrieves the article after the given article in the Timeline
func findNextArticle(_ article: Article) -> Article? {
return timelineModel.findNextArticle(article)
}
/// Returns the article with the given articleID
func articleFor(_ articleID: String) -> Article? {
return timelineModel.articleFor(articleID)
}
}
// MARK: SidebarModelDelegate
extension SceneModel: SidebarModelDelegate {
func unreadCount(for feed: Feed) -> Int {
// TODO: Get the count from the timeline if Feed is the current timeline
return feed.unreadCount
}
}
// MARK: TimelineModelDelegate
extension SceneModel: TimelineModelDelegate {
var selectedFeeds: Published<[Feed]>.Publisher {
return sidebarModel.$selectedFeeds
}
func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed) {
}
}
// MARK: Private
private extension SceneModel {
// MARK: Subscriptions
func subscribeToToolbarChangeEvents() {
NotificationCenter.default.publisher(for: .UnreadCountDidChange)
.compactMap { $0.object as? AccountManager }
.sink { [weak self] accountManager in
self?.updateNextUnreadButtonState(accountManager: accountManager)
}.store(in: &cancellables)
let blankNotification = Notification(name: .StatusesDidChange)
let statusesDidChangePublisher = NotificationCenter.default.publisher(for: .StatusesDidChange).prepend(blankNotification)
let combinedPublisher = timelineModel.$articles.combineLatest(timelineModel.$selectedArticles, statusesDidChangePublisher)
combinedPublisher.sink { [weak self] (articles, selectedArticles, _) in
self?.updateMarkAllAsReadButtonsState(articles: articles)
self?.updateArticleButtonsState(selectedArticles: selectedArticles)
}.store(in: &cancellables)
}
func subscribeToAccountSyncErrors() {
NotificationCenter.default.publisher(for: .AccountsDidFailToSyncWithErrors)
.sink { [weak self] notification in
guard let errors = notification.object as? [AccountSyncError] else {
return
}
self?.accountSyncErrors = errors
}.store(in: &cancellables)
}
// MARK: Button State Updates
func updateNextUnreadButtonState(accountManager: AccountManager) {
if accountManager.unreadCount > 0 {
self.nextUnreadButtonState = false
} else {
self.nextUnreadButtonState = nil
}
}
func updateMarkAllAsReadButtonsState(articles: [Article]) {
if articles.canMarkAllAsRead() {
markAllAsReadButtonState = false
} else {
markAllAsReadButtonState = nil
}
}
func updateArticleButtonsState(selectedArticles: [Article]) {
guard !selectedArticles.isEmpty else {
readButtonState = nil
starButtonState = nil
openInBrowserButtonState = nil
shareButtonState = nil
return
}
if selectedArticles.anyArticleIsUnread() {
readButtonState = true
} else if selectedArticles.anyArticleIsReadAndCanMarkUnread() {
readButtonState = false
} else {
readButtonState = nil
}
if selectedArticles.anyArticleIsUnstarred() {
starButtonState = false
} else {
starButtonState = true
}
if selectedArticles.count == 1, selectedArticles.first?.preferredLink != nil {
openInBrowserButtonState = true
shareButtonState = true
} else {
openInBrowserButtonState = nil
shareButtonState = nil
}
}
}