mastodon-ios/Mastodon/Scene/Notification/NotificationViewModel.swift

168 lines
7.1 KiB
Swift
Raw Normal View History

2021-04-12 10:31:53 +02:00
//
// NotificationViewModel.swift
// Mastodon
//
// Created by sxiaojian on 2021/4/12.
//
import Combine
import CoreData
import CoreDataStack
2021-04-15 04:16:30 +02:00
import Foundation
2021-04-12 10:31:53 +02:00
import GameplayKit
2021-04-13 15:31:49 +02:00
import MastodonSDK
2021-04-15 04:16:30 +02:00
import UIKit
2021-04-27 10:54:23 +02:00
import OSLog
2021-04-12 10:31:53 +02:00
2021-04-15 04:16:30 +02:00
final class NotificationViewModel: NSObject {
2021-04-12 10:31:53 +02:00
var disposeBag = Set<AnyCancellable>()
// input
let context: AppContext
2021-04-13 15:31:49 +02:00
weak var tableView: UITableView?
2021-04-12 10:31:53 +02:00
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
2021-04-13 15:31:49 +02:00
let viewDidLoad = PassthroughSubject<Void, Never>()
let selectedIndex = CurrentValueSubject<NotificationSegment, Never>(.everyThing)
2021-04-15 04:16:30 +02:00
let noMoreNotification = CurrentValueSubject<Bool, Never>(false)
2021-04-12 10:31:53 +02:00
let activeMastodonAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never>
2021-04-12 10:31:53 +02:00
let fetchedResultsController: NSFetchedResultsController<MastodonNotification>!
let notificationPredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
let cellFrameCache = NSCache<NSNumber, NSValue>()
2021-04-12 10:31:53 +02:00
var needsScrollToTopAfterDataSourceUpdate = false
let dataSourceDidUpdated = PassthroughSubject<Void, Never>()
2021-04-12 10:31:53 +02:00
let isFetchingLatestNotification = CurrentValueSubject<Bool, Never>(false)
2021-04-15 04:16:30 +02:00
// output
2021-04-12 10:31:53 +02:00
var diffableDataSource: UITableViewDiffableDataSource<NotificationSection, NotificationItem>!
2021-04-12 10:31:53 +02:00
// 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)
2021-04-14 13:04:11 +02:00
// bottom loader
2021-07-23 13:10:27 +02:00
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
2021-04-14 13:04:11 +02:00
// 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
}()
2021-04-15 04:16:30 +02:00
2021-04-14 13:04:11 +02:00
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<LoadOldestState?, Never>(nil)
init(context: AppContext) {
2021-04-12 10:31:53 +02:00
self.context = context
self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
self.fetchedResultsController = {
let fetchRequest = MastodonNotification.sortedFetchRequest
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.fetchBatchSize = 10
2021-04-15 04:16:30 +02:00
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(MastodonNotification.status), #keyPath(MastodonNotification.account)]
2021-04-12 10:31:53 +02:00
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: context.managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil
)
return controller
}()
super.init()
2021-04-15 04:16:30 +02:00
fetchedResultsController.delegate = self
2021-04-12 10:31:53 +02:00
context.authenticationService.activeMastodonAuthenticationBox
2021-04-13 15:31:49 +02:00
.sink(receiveValue: { [weak self] box in
guard let self = self else { return }
self.activeMastodonAuthenticationBox.value = box
2021-04-16 07:45:54 +02:00
if let domain = box?.domain, let userID = box?.userID {
self.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID)
2021-04-13 15:31:49 +02:00
}
})
2021-04-12 10:31:53 +02:00
.store(in: &disposeBag)
notificationPredicate
2021-04-15 04:16:30 +02:00
.compactMap { $0 }
2021-04-12 10:31:53 +02:00
.sink { [weak self] predicate in
guard let self = self else { return }
self.fetchedResultsController.fetchRequest.predicate = predicate
do {
2021-04-14 09:00:48 +02:00
self.diffableDataSource?.defaultRowAnimation = .fade
2021-04-12 10:31:53 +02:00
try self.fetchedResultsController.performFetch()
2021-04-14 09:00:48 +02:00
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let self = self else { return }
self.diffableDataSource?.defaultRowAnimation = .automatic
}
2021-04-12 10:31:53 +02:00
} catch {
assertionFailure(error.localizedDescription)
}
}
.store(in: &disposeBag)
2021-04-13 15:31:49 +02:00
2021-04-15 04:16:30 +02:00
viewDidLoad
2021-04-13 15:31:49 +02:00
.sink { [weak self] in
2021-04-16 07:45:54 +02:00
guard let domain = self?.activeMastodonAuthenticationBox.value?.domain, let userID = self?.activeMastodonAuthenticationBox.value?.userID else { return }
self?.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID)
2021-04-13 15:31:49 +02:00
}
.store(in: &disposeBag)
2021-04-12 10:31:53 +02:00
}
2021-04-27 10:54:23 +02:00
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 }
2021-04-27 13:41:55 +02:00
context.apiService.rejectFollowRequest(mastodonUserID: notification.account.id, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
2021-04-27 10:54:23 +02:00
.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)
}
2021-04-12 10:31:53 +02:00
}
2021-04-16 07:45:54 +02:00
extension NotificationViewModel {
enum NotificationSegment: Int {
case everyThing
case mentions
2021-04-16 07:45:54 +02:00
}
}