ThreadViewController.swift 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. //
  2. // ThreadViewController.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 AVKit
  12. import MastodonMeta
  13. import MastodonAsset
  14. import MastodonCore
  15. import MastodonUI
  16. import MastodonLocalization
  17. final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
  18. let logger = Logger(subsystem: "ThreadViewController", category: "ViewController")
  19. weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
  20. weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
  21. var disposeBag = Set<AnyCancellable>()
  22. var viewModel: ThreadViewModel!
  23. let mediaPreviewTransitionController = MediaPreviewTransitionController()
  24. let titleView = DoubleTitleLabelNavigationBarTitleView()
  25. let replyBarButtonItem = AdaptiveUserInterfaceStyleBarButtonItem(
  26. lightImage: UIImage(systemName: "arrowshape.turn.up.left")!,
  27. darkImage: UIImage(systemName: "arrowshape.turn.up.left.fill")!
  28. )
  29. let tableView: UITableView = {
  30. let tableView = ControlContainableTableView()
  31. tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
  32. tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self))
  33. tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
  34. tableView.register(ThreadReplyLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: ThreadReplyLoaderTableViewCell.self))
  35. tableView.rowHeight = UITableView.automaticDimension
  36. tableView.separatorStyle = .none
  37. tableView.backgroundColor = .clear
  38. return tableView
  39. }()
  40. deinit {
  41. os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
  42. }
  43. }
  44. extension ThreadViewController {
  45. override func viewDidLoad() {
  46. super.viewDidLoad()
  47. view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
  48. ThemeService.shared.currentTheme
  49. .receive(on: DispatchQueue.main)
  50. .sink { [weak self] theme in
  51. guard let self = self else { return }
  52. self.view.backgroundColor = theme.secondarySystemBackgroundColor
  53. }
  54. .store(in: &disposeBag)
  55. navigationItem.title = L10n.Scene.Thread.backTitle
  56. navigationItem.titleView = titleView
  57. navigationItem.rightBarButtonItem = replyBarButtonItem
  58. replyBarButtonItem.button.addTarget(self, action: #selector(ThreadViewController.replyBarButtonItemPressed(_:)), for: .touchUpInside)
  59. viewModel.$navigationBarTitle
  60. .receive(on: DispatchQueue.main)
  61. .sink { [weak self] title in
  62. guard let self = self else { return }
  63. guard let title = title else {
  64. self.titleView.update(title: "", subtitle: nil)
  65. return
  66. }
  67. self.titleView.update(titleMetaContent: title, subtitle: nil)
  68. }
  69. .store(in: &disposeBag)
  70. tableView.translatesAutoresizingMaskIntoConstraints = false
  71. view.addSubview(tableView)
  72. NSLayoutConstraint.activate([
  73. tableView.topAnchor.constraint(equalTo: view.topAnchor),
  74. tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  75. tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  76. tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  77. ])
  78. tableView.delegate = self
  79. viewModel.setupDiffableDataSource(
  80. tableView: tableView,
  81. statusTableViewCellDelegate: self
  82. )
  83. }
  84. override func viewWillAppear(_ animated: Bool) {
  85. super.viewWillAppear(animated)
  86. tableView.deselectRow(with: transitionCoordinator, animated: animated)
  87. }
  88. override func viewDidAppear(_ animated: Bool) {
  89. super.viewDidAppear(animated)
  90. UIAccessibility.post(notification: .screenChanged, argument: tableView)
  91. }
  92. }
  93. extension ThreadViewController {
  94. @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
  95. logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
  96. guard case let .root(threadContext) = viewModel.root else { return }
  97. let composeViewModel = ComposeViewModel(
  98. context: context,
  99. authContext: viewModel.authContext,
  100. kind: .reply(status: threadContext.status)
  101. )
  102. _ = coordinator.present(
  103. scene: .compose(viewModel: composeViewModel),
  104. from: self,
  105. transition: .modal(animated: true, completion: nil)
  106. )
  107. }
  108. }
  109. // MARK: - AuthContextProvider
  110. extension ThreadViewController: AuthContextProvider {
  111. var authContext: AuthContext { viewModel.authContext }
  112. }
  113. // MARK: - UITableViewDelegate
  114. extension ThreadViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
  115. // sourcery:inline:ThreadViewController.AutoGenerateTableViewDelegate
  116. // Generated using Sourcery
  117. // DO NOT EDIT
  118. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  119. aspectTableView(tableView, didSelectRowAt: indexPath)
  120. }
  121. func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
  122. return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point)
  123. }
  124. func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
  125. return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
  126. }
  127. func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
  128. return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration)
  129. }
  130. func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
  131. aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
  132. }
  133. // sourcery:end
  134. func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  135. guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
  136. guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil }
  137. switch item {
  138. case .thread(let thread):
  139. switch thread {
  140. case .root:
  141. return nil
  142. default:
  143. return indexPath
  144. }
  145. default:
  146. return indexPath
  147. }
  148. }
  149. }
  150. // MARK: - StatusTableViewCellDelegate
  151. extension ThreadViewController: StatusTableViewCellDelegate { }
  152. extension ThreadViewController {
  153. override var keyCommands: [UIKeyCommand]? {
  154. return navigationKeyCommands + statusNavigationKeyCommands
  155. }
  156. }
  157. // MARK: - StatusTableViewControllerNavigateable
  158. extension ThreadViewController: StatusTableViewControllerNavigateable {
  159. @objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
  160. navigateKeyCommandHandler(sender)
  161. }
  162. @objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
  163. statusKeyCommandHandler(sender)
  164. }
  165. }