// // 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() var rootItemObserver: AnyCancellable? // input let context: AppContext let mastodonStatusThreadViewModel: MastodonStatusThreadViewModel // let cellFrameCache = NSCache() // let existStatusFetchedResultsController: StatusFetchedResultsController // weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? // weak var tableView: UITableView? // output var diffableDataSource: UITableViewDiffableDataSource? @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) } }