ThreadViewModel.swift 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. //
  2. // ThreadViewModel.swift
  3. // Mastodon
  4. //
  5. // Created by MainasuK Cirno on 2021-4-12.
  6. //
  7. import os.log
  8. import UIKit
  9. import Combine
  10. import CoreData
  11. import CoreDataStack
  12. import GameplayKit
  13. import MastodonSDK
  14. import MastodonMeta
  15. import MastodonAsset
  16. import MastodonCore
  17. import MastodonLocalization
  18. class ThreadViewModel {
  19. let logger = Logger(subsystem: "ThreadViewModel", category: "ViewModel")
  20. var disposeBag = Set<AnyCancellable>()
  21. var rootItemObserver: AnyCancellable?
  22. // input
  23. let context: AppContext
  24. let authContext: AuthContext
  25. let mastodonStatusThreadViewModel: MastodonStatusThreadViewModel
  26. // output
  27. var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
  28. @Published var root: StatusItem.Thread?
  29. @Published var threadContext: ThreadContext?
  30. private(set) lazy var loadThreadStateMachine: GKStateMachine = {
  31. let stateMachine = GKStateMachine(states: [
  32. LoadThreadState.Initial(viewModel: self),
  33. LoadThreadState.Loading(viewModel: self),
  34. LoadThreadState.Fail(viewModel: self),
  35. LoadThreadState.NoMore(viewModel: self),
  36. ])
  37. stateMachine.enter(LoadThreadState.Initial.self)
  38. return stateMachine
  39. }()
  40. @Published var navigationBarTitle: MastodonMetaContent?
  41. init(
  42. context: AppContext,
  43. authContext: AuthContext,
  44. optionalRoot: StatusItem.Thread?
  45. ) {
  46. self.context = context
  47. self.authContext = authContext
  48. self.root = optionalRoot
  49. self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context)
  50. // end init
  51. ManagedObjectObserver.observe(context: context.managedObjectContext)
  52. .sink(receiveCompletion: { completion in
  53. // do nohting
  54. }, receiveValue: { [weak self] changes in
  55. guard let self = self else { return }
  56. let objectIDs: [NSManagedObjectID] = changes.changeTypes.compactMap { changeType in
  57. guard case let .delete(object) = changeType else { return nil }
  58. return object.objectID
  59. }
  60. self.delete(objectIDs: objectIDs)
  61. })
  62. .store(in: &disposeBag)
  63. $root
  64. .receive(on: DispatchQueue.main)
  65. .sink { [weak self] root in
  66. guard let self = self else { return }
  67. guard case let .root(threadContext) = root else { return }
  68. guard let status = threadContext.status.object(in: self.context.managedObjectContext) else { return }
  69. // bind threadContext
  70. self.threadContext = .init(
  71. domain: status.domain,
  72. statusID: status.id,
  73. replyToID: status.inReplyToID
  74. )
  75. // bind titleView
  76. self.navigationBarTitle = {
  77. let title = L10n.Scene.Thread.title(status.author.displayNameWithFallback)
  78. let content = MastodonContent(content: title, emojis: status.author.emojis.asDictionary)
  79. return try? MastodonMetaContent.convert(document: content)
  80. }()
  81. }
  82. .store(in: &disposeBag)
  83. }
  84. deinit {
  85. os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
  86. }
  87. }
  88. extension ThreadViewModel {
  89. struct ThreadContext {
  90. let domain: String
  91. let statusID: Mastodon.Entity.Status.ID
  92. let replyToID: Mastodon.Entity.Status.ID?
  93. }
  94. }
  95. extension ThreadViewModel {
  96. func delete(objectIDs: [NSManagedObjectID]) {
  97. if let root = self.root,
  98. case let .root(threadContext) = root,
  99. objectIDs.contains(threadContext.status.objectID)
  100. {
  101. self.root = nil
  102. }
  103. self.mastodonStatusThreadViewModel.delete(objectIDs: objectIDs)
  104. }
  105. }