HashtagTimelineViewController.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. //
  2. // HashtagTimelineViewController.swift
  3. // Mastodon
  4. //
  5. // Created by BradGao on 2021/3/30.
  6. //
  7. import os.log
  8. import UIKit
  9. import AVKit
  10. import Combine
  11. import GameplayKit
  12. import CoreData
  13. import MastodonAsset
  14. import MastodonCore
  15. import MastodonUI
  16. import MastodonLocalization
  17. final class HashtagTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
  18. let logger = Logger(subsystem: "HashtagTimelineViewController", category: "ViewController")
  19. weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
  20. weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
  21. let mediaPreviewTransitionController = MediaPreviewTransitionController()
  22. var disposeBag = Set<AnyCancellable>()
  23. var viewModel: HashtagTimelineViewModel!
  24. let composeBarButtonItem: UIBarButtonItem = {
  25. let barButtonItem = UIBarButtonItem()
  26. barButtonItem.image = Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate)
  27. return barButtonItem
  28. }()
  29. let titleView = DoubleTitleLabelNavigationBarTitleView()
  30. let tableView: UITableView = {
  31. let tableView = ControlContainableTableView()
  32. tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
  33. tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self))
  34. tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
  35. tableView.rowHeight = UITableView.automaticDimension
  36. tableView.separatorStyle = .none
  37. tableView.backgroundColor = .clear
  38. return tableView
  39. }()
  40. let refreshControl = RefreshControl()
  41. deinit {
  42. os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
  43. }
  44. }
  45. extension HashtagTimelineViewController {
  46. override func viewDidLoad() {
  47. super.viewDidLoad()
  48. let _title = "#\(viewModel.hashtag)"
  49. title = _title
  50. titleView.update(title: _title, subtitle: nil)
  51. navigationItem.titleView = titleView
  52. view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
  53. ThemeService.shared.currentTheme
  54. .receive(on: RunLoop.main)
  55. .sink { [weak self] theme in
  56. guard let self = self else { return }
  57. self.view.backgroundColor = theme.secondarySystemBackgroundColor
  58. }
  59. .store(in: &disposeBag)
  60. navigationItem.rightBarButtonItem = composeBarButtonItem
  61. composeBarButtonItem.target = self
  62. composeBarButtonItem.action = #selector(HashtagTimelineViewController.composeBarButtonItemPressed(_:))
  63. tableView.translatesAutoresizingMaskIntoConstraints = false
  64. view.addSubview(tableView)
  65. NSLayoutConstraint.activate([
  66. tableView.topAnchor.constraint(equalTo: view.topAnchor),
  67. tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  68. tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  69. tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  70. ])
  71. tableView.delegate = self
  72. viewModel.setupDiffableDataSource(
  73. tableView: tableView,
  74. statusTableViewCellDelegate: self
  75. )
  76. tableView.refreshControl = refreshControl
  77. refreshControl.addTarget(self, action: #selector(HashtagTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
  78. viewModel.didLoadLatest
  79. .receive(on: DispatchQueue.main)
  80. .sink { [weak self] _ in
  81. guard let self = self else { return }
  82. self.refreshControl.endRefreshing()
  83. }
  84. .store(in: &disposeBag)
  85. // setup batch fetch
  86. viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
  87. viewModel.listBatchFetchViewModel.shouldFetch
  88. .receive(on: DispatchQueue.main)
  89. .sink { [weak self] _ in
  90. guard let self = self else { return }
  91. self.viewModel.stateMachine.enter(HashtagTimelineViewModel.State.Loading.self)
  92. }
  93. .store(in: &disposeBag)
  94. viewModel.hashtagEntity
  95. .receive(on: DispatchQueue.main)
  96. .sink { [weak self] tag in
  97. self?.updatePromptTitle()
  98. }
  99. .store(in: &disposeBag)
  100. }
  101. override func viewWillAppear(_ animated: Bool) {
  102. super.viewWillAppear(animated)
  103. tableView.deselectRow(with: transitionCoordinator, animated: animated)
  104. }
  105. }
  106. extension HashtagTimelineViewController {
  107. private func updatePromptTitle() {
  108. var subtitle: String?
  109. defer {
  110. titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle)
  111. }
  112. guard let histories = viewModel.hashtagEntity.value?.history else {
  113. return
  114. }
  115. if histories.isEmpty {
  116. // No tag history, remove the prompt title
  117. return
  118. } else {
  119. let sortedHistory = histories.sorted { (h1, h2) -> Bool in
  120. return h1.day > h2.day
  121. }
  122. let peopleTalkingNumber = sortedHistory
  123. .prefix(2)
  124. .compactMap({ Int($0.accounts) })
  125. .reduce(0, +)
  126. subtitle = L10n.Plural.peopleTalking(peopleTalkingNumber)
  127. }
  128. }
  129. }
  130. extension HashtagTimelineViewController {
  131. @objc private func refreshControlValueChanged(_ sender: RefreshControl) {
  132. guard viewModel.stateMachine.enter(HashtagTimelineViewModel.State.Reloading.self) else {
  133. sender.endRefreshing()
  134. return
  135. }
  136. }
  137. @objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
  138. os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
  139. let composeViewModel = ComposeViewModel(
  140. context: context,
  141. authContext: viewModel.authContext,
  142. kind: .hashtag(hashtag: viewModel.hashtag)
  143. )
  144. _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
  145. }
  146. }
  147. // MARK: - AuthContextProvider
  148. extension HashtagTimelineViewController: AuthContextProvider {
  149. var authContext: AuthContext { viewModel.authContext }
  150. }
  151. // MARK: - UITableViewDelegate
  152. extension HashtagTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
  153. // sourcery:inline:HashtagTimelineViewController.AutoGenerateTableViewDelegate
  154. // Generated using Sourcery
  155. // DO NOT EDIT
  156. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  157. aspectTableView(tableView, didSelectRowAt: indexPath)
  158. }
  159. func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
  160. return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point)
  161. }
  162. func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
  163. return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
  164. }
  165. func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
  166. return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration)
  167. }
  168. func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
  169. aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
  170. }
  171. // sourcery:end
  172. }
  173. // MARK: - StatusTableViewCellDelegate
  174. extension HashtagTimelineViewController: StatusTableViewCellDelegate { }
  175. extension HashtagTimelineViewController {
  176. override var keyCommands: [UIKeyCommand]? {
  177. return navigationKeyCommands + statusNavigationKeyCommands
  178. }
  179. }
  180. // MARK: - StatusTableViewControllerNavigateable
  181. extension HashtagTimelineViewController: StatusTableViewControllerNavigateable {
  182. @objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
  183. navigateKeyCommandHandler(sender)
  184. }
  185. @objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
  186. statusKeyCommandHandler(sender)
  187. }
  188. }