forked from zelo72/mastodon-ios
250 lines
10 KiB
Swift
250 lines
10 KiB
Swift
//
|
|
// ThreadViewModel.swift
|
|
// Mastodon
|
|
//
|
|
// Created by MainasuK Cirno on 2021-4-12.
|
|
//
|
|
|
|
import os.log
|
|
import UIKit
|
|
import Combine
|
|
import CoreData
|
|
import CoreDataStack
|
|
import GameplayKit
|
|
import MastodonSDK
|
|
import MastodonMeta
|
|
import MastodonAsset
|
|
import MastodonLocalization
|
|
|
|
class ThreadViewModel {
|
|
|
|
let logger = Logger(subsystem: "ThreadViewModel", category: "ViewModel")
|
|
|
|
var disposeBag = Set<AnyCancellable>()
|
|
var rootItemObserver: AnyCancellable?
|
|
|
|
// input
|
|
let context: AppContext
|
|
let mastodonStatusThreadViewModel: MastodonStatusThreadViewModel
|
|
|
|
// let cellFrameCache = NSCache<NSNumber, NSValue>()
|
|
// let existStatusFetchedResultsController: StatusFetchedResultsController
|
|
|
|
// weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
|
// weak var tableView: UITableView?
|
|
|
|
// output
|
|
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
|
|
@Published var root: StatusItem.Thread?
|
|
@Published var threadContext: ThreadContext?
|
|
|
|
private(set) lazy var loadThreadStateMachine: GKStateMachine = {
|
|
let stateMachine = GKStateMachine(states: [
|
|
LoadThreadState.Initial(viewModel: self),
|
|
LoadThreadState.Loading(viewModel: self),
|
|
LoadThreadState.Fail(viewModel: self),
|
|
LoadThreadState.NoMore(viewModel: self),
|
|
|
|
])
|
|
stateMachine.enter(LoadThreadState.Initial.self)
|
|
return stateMachine
|
|
}()
|
|
@Published var navigationBarTitle: MastodonMetaContent?
|
|
|
|
init(
|
|
context: AppContext,
|
|
optionalRoot: StatusItem.Thread?
|
|
) {
|
|
self.context = context
|
|
self.root = optionalRoot
|
|
self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context)
|
|
// self.rootNode = CurrentValueSubject(optionalStatus.flatMap { RootNode(domain: $0.domain, statusID: $0.id, replyToID: $0.inReplyToID) })
|
|
// self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) })
|
|
// self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil)
|
|
// self.navigationBarTitle = CurrentValueSubject(
|
|
// optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) })
|
|
// self.navigationBarTitleEmojiMeta = CurrentValueSubject(optionalStatus.flatMap { $0.author.emojis.asDictionary } ?? [:])
|
|
// end init
|
|
|
|
ManagedObjectObserver.observe(context: context.managedObjectContext)
|
|
.sink(receiveCompletion: { completion in
|
|
// do nohting
|
|
}, receiveValue: { [weak self] changes in
|
|
guard let self = self else { return }
|
|
|
|
let objectIDs: [NSManagedObjectID] = changes.changeTypes.compactMap { changeType in
|
|
guard case let .delete(object) = changeType else { return nil }
|
|
return object.objectID
|
|
}
|
|
|
|
self.delete(objectIDs: objectIDs)
|
|
})
|
|
.store(in: &disposeBag)
|
|
|
|
// // bind fetcher domain
|
|
// context.authenticationService.activeMastodonAuthenticationBox
|
|
// .receive(on: RunLoop.main)
|
|
// .sink { [weak self] box in
|
|
// guard let self = self else { return }
|
|
// self.existStatusFetchedResultsController.domain.value = box?.domain
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// rootNode
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] rootNode in
|
|
// guard let self = self else { return }
|
|
// guard rootNode != nil else { return }
|
|
// self.loadThreadStateMachine.enter(LoadThreadState.Loading.self)
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
|
|
$root
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] root in
|
|
guard let self = self else { return }
|
|
guard case let .root(threadContext) = root else { return }
|
|
guard let status = threadContext.status.object(in: self.context.managedObjectContext) else { return }
|
|
|
|
// bind threadContext
|
|
self.threadContext = .init(
|
|
domain: status.domain,
|
|
statusID: status.id,
|
|
replyToID: status.inReplyToID
|
|
)
|
|
|
|
// bind titleView
|
|
self.navigationBarTitle = {
|
|
let title = L10n.Scene.Thread.title(status.author.displayNameWithFallback)
|
|
let content = MastodonContent(content: title, emojis: status.author.emojis.asDictionary)
|
|
return try? MastodonMetaContent.convert(document: content)
|
|
}()
|
|
}
|
|
.store(in: &disposeBag)
|
|
|
|
// rootItem
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] rootItem in
|
|
// guard let self = self else { return }
|
|
// guard case let .root(objectID, _) = rootItem else { return }
|
|
// self.context.managedObjectContext.perform {
|
|
// guard let status = self.context.managedObjectContext.object(with: objectID) as? Status else {
|
|
// return
|
|
// }
|
|
// self.rootItemObserver = ManagedObjectObserver.observe(object: status)
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink(receiveCompletion: { _ in
|
|
// // do nothing
|
|
// }, receiveValue: { [weak self] change in
|
|
// guard let self = self else { return }
|
|
// switch change.changeType {
|
|
// case .delete:
|
|
// self.rootItem.value = nil
|
|
// default:
|
|
// break
|
|
// }
|
|
// })
|
|
// }
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// ancestorNodes
|
|
// .receive(on: DispatchQueue.main)
|
|
// .compactMap { [weak self] nodes -> [Item]? in
|
|
// guard let self = self else { return nil }
|
|
// guard !nodes.isEmpty else { return [] }
|
|
//
|
|
// guard let diffableDataSource = self.diffableDataSource else { return nil }
|
|
// let oldSnapshot = diffableDataSource.snapshot()
|
|
// var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
|
|
// for item in oldSnapshot.itemIdentifiers {
|
|
// switch item {
|
|
// case .reply(let objectID, let attribute):
|
|
// oldSnapshotAttributeDict[objectID] = attribute
|
|
// default:
|
|
// break
|
|
// }
|
|
// }
|
|
//
|
|
// var items: [Item] = []
|
|
// for node in nodes {
|
|
// let attribute = oldSnapshotAttributeDict[node.statusObjectID] ?? Item.StatusAttribute()
|
|
// items.append(Item.reply(statusObjectID: node.statusObjectID, attribute: attribute))
|
|
// }
|
|
//
|
|
// return items.reversed()
|
|
// }
|
|
// .assign(to: \.value, on: ancestorItems)
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// descendantNodes
|
|
// .receive(on: DispatchQueue.main)
|
|
// .compactMap { [weak self] nodes -> [Item]? in
|
|
// guard let self = self else { return nil }
|
|
// guard !nodes.isEmpty else { return [] }
|
|
//
|
|
// guard let diffableDataSource = self.diffableDataSource else { return nil }
|
|
// let oldSnapshot = diffableDataSource.snapshot()
|
|
// var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
|
|
// for item in oldSnapshot.itemIdentifiers {
|
|
// switch item {
|
|
// case .leaf(let objectID, let attribute):
|
|
// oldSnapshotAttributeDict[objectID] = attribute
|
|
// default:
|
|
// break
|
|
// }
|
|
// }
|
|
//
|
|
// var items: [Item] = []
|
|
//
|
|
// func buildThread(node: LeafNode) {
|
|
// let attribute = oldSnapshotAttributeDict[node.objectID] ?? Item.StatusAttribute()
|
|
// items.append(Item.leaf(statusObjectID: node.objectID, attribute: attribute))
|
|
// // only expand the first child
|
|
// if let firstChild = node.children.first {
|
|
// if !node.isChildrenExpanded {
|
|
// items.append(Item.leafBottomLoader(statusObjectID: node.objectID))
|
|
// } else {
|
|
// buildThread(node: firstChild)
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// for node in nodes {
|
|
// buildThread(node: node)
|
|
// }
|
|
// return items
|
|
// }
|
|
// .assign(to: \.value, on: descendantItems)
|
|
// .store(in: &disposeBag)
|
|
}
|
|
|
|
deinit {
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
}
|
|
|
|
}
|
|
|
|
extension ThreadViewModel {
|
|
|
|
struct ThreadContext {
|
|
let domain: String
|
|
let statusID: Mastodon.Entity.Status.ID
|
|
let replyToID: Mastodon.Entity.Status.ID?
|
|
}
|
|
|
|
}
|
|
|
|
extension ThreadViewModel {
|
|
func delete(objectIDs: [NSManagedObjectID]) {
|
|
if let root = self.root,
|
|
case let .root(threadContext) = root,
|
|
objectIDs.contains(threadContext.status.objectID)
|
|
{
|
|
self.root = nil
|
|
}
|
|
|
|
self.mastodonStatusThreadViewModel.delete(objectIDs: objectIDs)
|
|
}
|
|
}
|