123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- //
- // MastodonStatusThreadViewModel.swift
- // MastodonStatusThreadViewModel
- //
- // Created by Cirno MainasuK on 2021-9-6.
- // Copyright © 2021 Twidere. All rights reserved.
- //
- import os.log
- import Foundation
- import Combine
- import CoreData
- import CoreDataStack
- import MastodonSDK
- import MastodonCore
- import MastodonMeta
- final class MastodonStatusThreadViewModel {
-
- var disposeBag = Set<AnyCancellable>()
-
- // input
- let context: AppContext
- @Published private(set) var deletedObjectIDs: Set<NSManagedObjectID> = Set()
- // output
- @Published var __ancestors: [StatusItem] = []
- @Published var ancestors: [StatusItem] = []
-
- @Published var __descendants: [StatusItem] = []
- @Published var descendants: [StatusItem] = []
-
- init(context: AppContext) {
- self.context = context
-
- Publishers.CombineLatest(
- $__ancestors,
- $deletedObjectIDs
- )
- .sink { [weak self] items, deletedObjectIDs in
- guard let self = self else { return }
- let newItems = items.filter { item in
- switch item {
- case .thread(let thread):
- return !deletedObjectIDs.contains(thread.record.objectID)
- default:
- assertionFailure()
- return false
- }
- }
- self.ancestors = newItems
- }
- .store(in: &disposeBag)
-
- Publishers.CombineLatest(
- $__descendants,
- $deletedObjectIDs
- )
- .sink { [weak self] items, deletedObjectIDs in
- guard let self = self else { return }
- let newItems = items.filter { item in
- switch item {
- case .thread(let thread):
- return !deletedObjectIDs.contains(thread.record.objectID)
- default:
- assertionFailure()
- return false
- }
- }
- self.descendants = newItems
- }
- .store(in: &disposeBag)
- }
-
- deinit {
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- }
-
- }
- extension MastodonStatusThreadViewModel {
-
- func appendAncestor(
- domain: String,
- nodes: [Node]
- ) {
- let ids = nodes.map { $0.statusID }
- var dictionary: [Status.ID: Status] = [:]
- do {
- let request = Status.sortedFetchRequest
- request.predicate = Status.predicate(domain: domain, ids: ids)
- let statuses = try self.context.managedObjectContext.fetch(request)
- for status in statuses {
- dictionary[status.id] = status
- }
- } catch {
- os_log("%{public}s[%{public}ld], %{public}s: fetch conversation fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
- return
- }
-
- var newItems: [StatusItem] = []
- for (i, node) in nodes.enumerated() {
- guard let status = dictionary[node.statusID] else { continue }
- let isLast = i == nodes.count - 1
-
- let record = ManagedObjectRecord<Status>(objectID: status.objectID)
- let context = StatusItem.Thread.Context(
- status: record,
- displayUpperConversationLink: !isLast,
- displayBottomConversationLink: true
- )
- let item = StatusItem.thread(.leaf(context: context))
- newItems.append(item)
- }
-
- let items = self.__ancestors + newItems
- self.__ancestors = items
- }
-
- func appendDescendant(
- domain: String,
- nodes: [Node]
- ) {
- let childrenIDs = nodes
- .map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } }
- .flatMap { $0 }
- var dictionary: [Status.ID: Status] = [:]
- do {
- let request = Status.sortedFetchRequest
- request.predicate = Status.predicate(domain: domain, ids: childrenIDs)
- let statuses = try self.context.managedObjectContext.fetch(request)
- for status in statuses {
- dictionary[status.id] = status
- }
- } catch {
- os_log("%{public}s[%{public}ld], %{public}s: fetch conversation fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
- return
- }
-
- var newItems: [StatusItem] = []
- for node in nodes {
- guard let status = dictionary[node.statusID] else { continue }
- // first tier
- let record = ManagedObjectRecord<Status>(objectID: status.objectID)
- let context = StatusItem.Thread.Context(
- status: record
- )
- let item = StatusItem.thread(.leaf(context: context))
- newItems.append(item)
-
- // second tier
- if let child = node.children.first {
- guard let secondaryStatus = dictionary[child.statusID] else { continue }
- let secondaryRecord = ManagedObjectRecord<Status>(objectID: secondaryStatus.objectID)
- let secondaryContext = StatusItem.Thread.Context(
- status: secondaryRecord,
- displayUpperConversationLink: true
- )
- let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext))
- newItems.append(secondaryItem)
-
- // update first tier context
- context.displayBottomConversationLink = true
- }
- }
-
- var items = self.__descendants
- for item in newItems {
- guard !items.contains(item) else { continue }
- items.append(item)
- }
- self.__descendants = items
- }
-
- }
- extension MastodonStatusThreadViewModel {
- class Node {
- typealias ID = String
-
- let statusID: ID
- let children: [Node]
-
- init(
- statusID: ID,
- children: [MastodonStatusThreadViewModel.Node]
- ) {
- self.statusID = statusID
- self.children = children
- }
- }
- }
- extension MastodonStatusThreadViewModel.Node {
- static func replyToThread(
- for replyToID: Mastodon.Entity.Status.ID?,
- from statuses: [Mastodon.Entity.Status]
- ) -> [MastodonStatusThreadViewModel.Node] {
- guard let replyToID = replyToID else {
- return []
- }
-
- var dict: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:]
- for status in statuses {
- dict[status.id] = status
- }
-
- var nextID: Mastodon.Entity.Status.ID? = replyToID
- var nodes: [MastodonStatusThreadViewModel.Node] = []
- while let _nextID = nextID {
- guard let status = dict[_nextID] else { break }
- nodes.append(MastodonStatusThreadViewModel.Node(
- statusID: _nextID,
- children: []
- ))
- nextID = status.inReplyToID
- }
-
- return nodes
- }
- }
- extension MastodonStatusThreadViewModel.Node {
- static func children(
- of statusID: ID,
- from statuses: [Mastodon.Entity.Status]
- ) -> [MastodonStatusThreadViewModel.Node] {
- var dictionary: [ID: Mastodon.Entity.Status] = [:]
- var mapping: [ID: Set<ID>] = [:]
-
- for status in statuses {
- dictionary[status.id] = status
- guard let replyToID = status.inReplyToID else { continue }
- if var set = mapping[replyToID] {
- set.insert(status.id)
- mapping[replyToID] = set
- } else {
- mapping[replyToID] = Set([status.id])
- }
- }
-
- var children: [MastodonStatusThreadViewModel.Node] = []
- let replies = Array(mapping[statusID] ?? Set())
- .compactMap { dictionary[$0] }
- .sorted(by: { $0.createdAt > $1.createdAt })
- for reply in replies {
- let child = child(of: reply.id, dictionary: dictionary, mapping: mapping)
- children.append(child)
- }
- return children
- }
-
- static func child(
- of statusID: ID,
- dictionary: [ID: Mastodon.Entity.Status],
- mapping: [ID: Set<ID>]
- ) -> MastodonStatusThreadViewModel.Node {
- let childrenIDs = mapping[statusID] ?? []
- let children = Array(childrenIDs)
- .compactMap { dictionary[$0] }
- .sorted(by: { $0.createdAt > $1.createdAt })
- .map { status in child(of: status.id, dictionary: dictionary, mapping: mapping) }
- return MastodonStatusThreadViewModel.Node(
- statusID: statusID,
- children: children
- )
- }
-
- }
- extension MastodonStatusThreadViewModel {
- func delete(objectIDs: [NSManagedObjectID]) {
- var set = deletedObjectIDs
- for objectID in objectIDs {
- set.insert(objectID)
- }
- self.deletedObjectIDs = set
- }
- }
|