StatusSection.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. //
  2. // TimelineSection.swift
  3. // Mastodon
  4. //
  5. // Created by sxiaojian on 2021/1/27.
  6. //
  7. import Combine
  8. import CoreData
  9. import CoreDataStack
  10. import os.log
  11. import UIKit
  12. import AVKit
  13. import AlamofireImage
  14. import MastodonMeta
  15. import MastodonSDK
  16. import NaturalLanguage
  17. import MastodonCore
  18. import MastodonUI
  19. enum StatusSection: Equatable, Hashable {
  20. case main
  21. }
  22. extension StatusSection {
  23. static let logger = Logger(subsystem: "StatusSection", category: "logic")
  24. struct Configuration {
  25. let authContext: AuthContext
  26. weak var statusTableViewCellDelegate: StatusTableViewCellDelegate?
  27. weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
  28. let filterContext: Mastodon.Entity.Filter.Context?
  29. let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher?
  30. }
  31. static func diffableDataSource(
  32. tableView: UITableView,
  33. context: AppContext,
  34. configuration: Configuration
  35. ) -> UITableViewDiffableDataSource<StatusSection, StatusItem> {
  36. tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
  37. tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self))
  38. tableView.register(StatusThreadRootTableViewCell.self, forCellReuseIdentifier: String(describing: StatusThreadRootTableViewCell.self))
  39. tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
  40. return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
  41. switch item {
  42. case .feed(let record):
  43. let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
  44. context.managedObjectContext.performAndWait {
  45. guard let feed = record.object(in: context.managedObjectContext) else { return }
  46. configure(
  47. context: context,
  48. tableView: tableView,
  49. cell: cell,
  50. viewModel: StatusTableViewCell.ViewModel(value: .feed(feed)),
  51. configuration: configuration
  52. )
  53. }
  54. return cell
  55. case .feedLoader(let record):
  56. let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
  57. context.managedObjectContext.performAndWait {
  58. guard let feed = record.object(in: context.managedObjectContext) else { return }
  59. configure(
  60. cell: cell,
  61. feed: feed,
  62. configuration: configuration
  63. )
  64. }
  65. return cell
  66. case .status(let record):
  67. let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
  68. context.managedObjectContext.performAndWait {
  69. guard let status = record.object(in: context.managedObjectContext) else { return }
  70. configure(
  71. context: context,
  72. tableView: tableView,
  73. cell: cell,
  74. viewModel: StatusTableViewCell.ViewModel(value: .status(status)),
  75. configuration: configuration
  76. )
  77. }
  78. return cell
  79. case .thread(let thread):
  80. let cell = dequeueConfiguredReusableCell(
  81. context: context,
  82. tableView: tableView,
  83. indexPath: indexPath,
  84. configuration: ThreadCellRegistrationConfiguration(
  85. thread: thread,
  86. configuration: configuration
  87. )
  88. )
  89. return cell
  90. case .topLoader:
  91. let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
  92. cell.activityIndicatorView.startAnimating()
  93. return cell
  94. case .bottomLoader:
  95. let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
  96. cell.activityIndicatorView.startAnimating()
  97. return cell
  98. }
  99. }
  100. } // end func
  101. }
  102. extension StatusSection {
  103. struct ThreadCellRegistrationConfiguration {
  104. let thread: StatusItem.Thread
  105. let configuration: Configuration
  106. }
  107. static func dequeueConfiguredReusableCell(
  108. context: AppContext,
  109. tableView: UITableView,
  110. indexPath: IndexPath,
  111. configuration: ThreadCellRegistrationConfiguration
  112. ) -> UITableViewCell {
  113. let managedObjectContext = context.managedObjectContext
  114. switch configuration.thread {
  115. case .root(let threadContext):
  116. let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusThreadRootTableViewCell.self), for: indexPath) as! StatusThreadRootTableViewCell
  117. managedObjectContext.performAndWait {
  118. guard let status = threadContext.status.object(in: managedObjectContext) else { return }
  119. StatusSection.configure(
  120. context: context,
  121. tableView: tableView,
  122. cell: cell,
  123. viewModel: StatusThreadRootTableViewCell.ViewModel(value: .status(status)),
  124. configuration: configuration.configuration
  125. )
  126. }
  127. return cell
  128. case .reply(let threadContext),
  129. .leaf(let threadContext):
  130. let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
  131. managedObjectContext.performAndWait {
  132. guard let status = threadContext.status.object(in: managedObjectContext) else { return }
  133. StatusSection.configure(
  134. context: context,
  135. tableView: tableView,
  136. cell: cell,
  137. viewModel: StatusTableViewCell.ViewModel(value: .status(status)),
  138. configuration: configuration.configuration
  139. )
  140. }
  141. return cell
  142. }
  143. }
  144. }
  145. extension StatusSection {
  146. public static func setupStatusPollDataSource(
  147. context: AppContext,
  148. authContext: AuthContext,
  149. statusView: StatusView
  150. ) {
  151. let managedObjectContext = context.managedObjectContext
  152. statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource<PollSection, PollItem>(tableView: statusView.pollTableView) { tableView, indexPath, item in
  153. switch item {
  154. case .option(let record):
  155. // Fix cell reuse animation issue
  156. let cell: PollOptionTableViewCell = {
  157. let _cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self) + "@\(indexPath.row)#\(indexPath.section)") as? PollOptionTableViewCell
  158. _cell?.prepareForReuse()
  159. return _cell ?? PollOptionTableViewCell()
  160. }()
  161. cell.pollOptionView.viewModel.authContext = authContext
  162. managedObjectContext.performAndWait {
  163. guard let option = record.object(in: managedObjectContext) else {
  164. assertionFailure()
  165. return
  166. }
  167. cell.pollOptionView.configure(pollOption: option)
  168. // trigger update if needs
  169. let needsUpdatePoll: Bool = {
  170. // check first option in poll to trigger update poll only once
  171. guard option.index == 0 else { return false }
  172. let poll = option.poll
  173. guard !poll.expired else {
  174. logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): poll expired. Skip update poll \(poll.id)")
  175. return false
  176. }
  177. let now = Date()
  178. let timeIntervalSinceUpdate = now.timeIntervalSince(poll.updatedAt)
  179. #if DEBUG
  180. let autoRefreshTimeInterval: TimeInterval = 3 // speedup testing
  181. #else
  182. let autoRefreshTimeInterval: TimeInterval = 30
  183. #endif
  184. guard timeIntervalSinceUpdate > autoRefreshTimeInterval else {
  185. logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): skip update poll \(poll.id) due to recent updated")
  186. return false
  187. }
  188. logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update poll \(poll.id)…")
  189. return true
  190. }()
  191. if needsUpdatePoll {
  192. let pollRecord: ManagedObjectRecord<Poll> = .init(objectID: option.poll.objectID)
  193. Task { [weak context] in
  194. guard let context = context else { return }
  195. _ = try await context.apiService.poll(
  196. poll: pollRecord,
  197. authenticationBox: authContext.mastodonAuthenticationBox
  198. )
  199. }
  200. }
  201. } // end managedObjectContext.performAndWait
  202. return cell
  203. }
  204. }
  205. var _snapshot = NSDiffableDataSourceSnapshot<PollSection, PollItem>()
  206. _snapshot.appendSections([.main])
  207. if #available(iOS 15.0, *) {
  208. statusView.pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot)
  209. } else {
  210. statusView.pollTableViewDiffableDataSource?.apply(_snapshot, animatingDifferences: false)
  211. }
  212. }
  213. }
  214. extension StatusSection {
  215. static func configure(
  216. context: AppContext,
  217. tableView: UITableView,
  218. cell: StatusTableViewCell,
  219. viewModel: StatusTableViewCell.ViewModel,
  220. configuration: Configuration
  221. ) {
  222. setupStatusPollDataSource(
  223. context: context,
  224. authContext: configuration.authContext,
  225. statusView: cell.statusView
  226. )
  227. cell.statusView.viewModel.authContext = configuration.authContext
  228. cell.configure(
  229. tableView: tableView,
  230. viewModel: viewModel,
  231. delegate: configuration.statusTableViewCellDelegate
  232. )
  233. cell.statusView.viewModel.filterContext = configuration.filterContext
  234. configuration.activeFilters?
  235. .assign(to: \.activeFilters, on: cell.statusView.viewModel)
  236. .store(in: &cell.disposeBag)
  237. }
  238. static func configure(
  239. context: AppContext,
  240. tableView: UITableView,
  241. cell: StatusThreadRootTableViewCell,
  242. viewModel: StatusThreadRootTableViewCell.ViewModel,
  243. configuration: Configuration
  244. ) {
  245. setupStatusPollDataSource(
  246. context: context,
  247. authContext: configuration.authContext,
  248. statusView: cell.statusView
  249. )
  250. cell.statusView.viewModel.authContext = configuration.authContext
  251. cell.configure(
  252. tableView: tableView,
  253. viewModel: viewModel,
  254. delegate: configuration.statusTableViewCellDelegate
  255. )
  256. cell.statusView.viewModel.filterContext = configuration.filterContext
  257. configuration.activeFilters?
  258. .assign(to: \.activeFilters, on: cell.statusView.viewModel)
  259. .store(in: &cell.disposeBag)
  260. }
  261. static func configure(
  262. cell: TimelineMiddleLoaderTableViewCell,
  263. feed: Feed,
  264. configuration: Configuration
  265. ) {
  266. cell.configure(
  267. feed: feed,
  268. delegate: configuration.timelineMiddleLoaderTableViewCellDelegate
  269. )
  270. }
  271. }