forked from zelo72/mastodon-ios
167 lines
7.1 KiB
Swift
167 lines
7.1 KiB
Swift
//
|
|
// 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<AnyCancellable>()
|
|
|
|
// input
|
|
let context: AppContext
|
|
weak var tableView: UITableView?
|
|
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
|
|
|
let viewDidLoad = PassthroughSubject<Void, Never>()
|
|
let selectedIndex = CurrentValueSubject<NotificationSegment, Never>(.EveryThing)
|
|
let noMoreNotification = CurrentValueSubject<Bool, Never>(false)
|
|
|
|
let activeMastodonAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
|
let fetchedResultsController: NSFetchedResultsController<MastodonNotification>!
|
|
let notificationPredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
|
|
let cellFrameCache = NSCache<NSNumber, NSValue>()
|
|
|
|
var needsScrollToTopAfterDataSourceUpdate = false
|
|
let dataSourceDidUpdated = PassthroughSubject<Void, Never>()
|
|
let isFetchingLatestNotification = CurrentValueSubject<Bool, Never>(false)
|
|
|
|
// output
|
|
var diffableDataSource: UITableViewDiffableDataSource<NotificationSection, NotificationItem>!
|
|
|
|
// 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<LoadLatestState?, Never>(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<LoadOldestState?, Never>(nil)
|
|
|
|
init(context: AppContext) {
|
|
self.context = context
|
|
self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
|
self.fetchedResultsController = {
|
|
let fetchRequest = MastodonNotification.sortedFetchRequest
|
|
fetchRequest.returnsObjectsAsFaults = false
|
|
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
|
|
}
|
|
}
|