// // NotificationViewModel.swift // Mastodon // // Created by sxiaojian on 2021/4/12. // import Combine import CoreData import CoreDataStack import Foundation import GameplayKit import MastodonSDK import UIKit import OSLog final class NotificationViewModel: NSObject { var disposeBag = Set() // input let context: AppContext weak var tableView: UITableView? weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? let viewDidLoad = PassthroughSubject() let selectedIndex = CurrentValueSubject(.EveryThing) let noMoreNotification = CurrentValueSubject(false) let activeMastodonAuthenticationBox: CurrentValueSubject let fetchedResultsController: NSFetchedResultsController! let notificationPredicate = CurrentValueSubject(nil) let cellFrameCache = NSCache() var needsScrollToTopAfterDataSourceUpdate = false let dataSourceDidUpdated = PassthroughSubject() let isFetchingLatestNotification = CurrentValueSubject(false) // output var diffableDataSource: UITableViewDiffableDataSource! // top loader private(set) lazy var loadLatestStateMachine: GKStateMachine = { // exclude timeline middle fetcher state let stateMachine = GKStateMachine(states: [ LoadLatestState.Initial(viewModel: self), LoadLatestState.Loading(viewModel: self), LoadLatestState.Fail(viewModel: self), LoadLatestState.Idle(viewModel: self), ]) stateMachine.enter(LoadLatestState.Initial.self) return stateMachine }() lazy var loadLatestStateMachinePublisher = CurrentValueSubject(nil) // bottom loader private(set) lazy var loadoldestStateMachine: GKStateMachine = { // exclude timeline middle fetcher state let stateMachine = GKStateMachine(states: [ LoadOldestState.Initial(viewModel: self), LoadOldestState.Loading(viewModel: self), LoadOldestState.Fail(viewModel: self), LoadOldestState.Idle(viewModel: self), LoadOldestState.NoMore(viewModel: self), ]) stateMachine.enter(LoadOldestState.Initial.self) return stateMachine }() lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) init(context: AppContext) { self.context = context self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) self.fetchedResultsController = { let fetchRequest = MastodonNotification.sortedFetchRequest fetchRequest.returnsObjectsAsFaults = false fetchRequest.fetchBatchSize = 10 fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(MastodonNotification.status), #keyPath(MastodonNotification.account)] let controller = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: context.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil ) return controller }() super.init() fetchedResultsController.delegate = self context.authenticationService.activeMastodonAuthenticationBox .sink(receiveValue: { [weak self] box in guard let self = self else { return } self.activeMastodonAuthenticationBox.value = box if let domain = box?.domain, let userID = box?.userID { self.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID) } }) .store(in: &disposeBag) notificationPredicate .compactMap { $0 } .sink { [weak self] predicate in guard let self = self else { return } self.fetchedResultsController.fetchRequest.predicate = predicate do { self.diffableDataSource?.defaultRowAnimation = .fade try self.fetchedResultsController.performFetch() DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in guard let self = self else { return } self.diffableDataSource?.defaultRowAnimation = .automatic } } catch { assertionFailure(error.localizedDescription) } } .store(in: &disposeBag) viewDidLoad .sink { [weak self] in guard let domain = self?.activeMastodonAuthenticationBox.value?.domain, let userID = self?.activeMastodonAuthenticationBox.value?.userID else { return } self?.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID) } .store(in: &disposeBag) } func acceptFollowRequest(notification: MastodonNotification) { guard let activeMastodonAuthenticationBox = self.activeMastodonAuthenticationBox.value else { return } context.apiService.acceptFollowRequest(mastodonUserID: notification.account.id, mastodonAuthenticationBox: activeMastodonAuthenticationBox) .sink { [weak self] completion in switch completion { case .failure(let error): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: accept FollowRequest fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) case .finished: self?.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) } } receiveValue: { _ in } .store(in: &disposeBag) } func rejectFollowRequest(notification: MastodonNotification) { guard let activeMastodonAuthenticationBox = self.activeMastodonAuthenticationBox.value else { return } context.apiService.rejectFollowRequest(mastodonUserID: notification.account.id, mastodonAuthenticationBox: activeMastodonAuthenticationBox) .sink { [weak self] completion in switch completion { case .failure(let error): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: reject FollowRequest fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) case .finished: self?.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) } } receiveValue: { _ in } .store(in: &disposeBag) } } extension NotificationViewModel { enum NotificationSegment: Int { case EveryThing case Mentions } }