ConferenceListView.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. //
  2. // ConferenceListView.swift
  3. // TUIRoomKit
  4. //
  5. // Created by CY zhao on 2024/6/3.
  6. //
  7. import UIKit
  8. import Combine
  9. import Factory
  10. import TUICore
  11. import RTCRoomEngine
  12. struct ConferenceSection {
  13. let date: Date
  14. let conferences: [ConferenceInfo]
  15. }
  16. @objcMembers public class ConferenceListView: UIView {
  17. // MARK: - Intailizer
  18. public init(viewController: UIViewController) {
  19. super.init(frame: .zero)
  20. let viewRoute = ConferenceRoute.init(viewController: viewController)
  21. navigation.initializeRoute(viewController: viewController, rootRoute: viewRoute)
  22. }
  23. @available(*, unavailable, message: "Use init(viewController:) instead")
  24. required init?(coder: NSCoder) {
  25. fatalError("init(coder:) has not been implemented")
  26. }
  27. @available(*, unavailable, message: "Use init(viewController:) instead")
  28. override init(frame: CGRect) {
  29. fatalError("init(frame:) has not implement ")
  30. }
  31. // MARK: - Public Methods
  32. public func reloadList() {
  33. store.dispatch(action: ScheduleViewActions.refreshConferenceList())
  34. }
  35. // MARK: Private Properties
  36. private let conferencesPerFetch = 10
  37. private lazy var conferenceListPublisher = {
  38. self.store.select(ConferenceListSelectors.getConferenceList)
  39. }()
  40. private lazy var cursorPublisher = {
  41. self.store.select(ConferenceListSelectors.getConferenceListCursor)
  42. }()
  43. private lazy var needRefreshPublisher = {
  44. self.store.select(ViewSelectors.getRefreshListFlag)
  45. }()
  46. private var fetchListCursor = ""
  47. private var sections: [ConferenceSection] = []
  48. var cancellableSet = Set<AnyCancellable>()
  49. private let historyRooms: UIButton = {
  50. let button = UIButton(type: .custom)
  51. button.setTitle(.historyConferenceText, for: .normal)
  52. button.setTitleColor(UIColor.tui_color(withHex: "1C66E5"), for: .normal)
  53. button.titleLabel?.font = UIFont(name: "PingFangSC-Medium", size: 14)
  54. let normalIcon = UIImage(named: "room_right_blue_arrow", in: tuiRoomKitBundle(), compatibleWith: nil)
  55. button.setImage(normalIcon, for: .normal)
  56. button.sizeToFit()
  57. var imageWidth = button.imageView?.bounds.size.width ?? 0
  58. var titleWidth = button.titleLabel?.bounds.size.width ?? 0
  59. button.titleEdgeInsets = UIEdgeInsets(top: 0,
  60. left: -imageWidth,
  61. bottom: 0,
  62. right: imageWidth);
  63. button.imageEdgeInsets = UIEdgeInsets(top: 0,
  64. left: titleWidth,
  65. bottom: 0,
  66. right: -titleWidth)
  67. return button
  68. }()
  69. private lazy var tableview: UITableView = {
  70. let tableView = UITableView(frame: .zero, style: .plain)
  71. tableView.separatorStyle = .none
  72. tableView.delegate = self
  73. tableView.dataSource = self
  74. tableView.backgroundColor = .clear
  75. tableView.register(ConferenceListCell.self, forCellReuseIdentifier: ConferenceListCell.reusedIdentifier)
  76. if #available(iOS 15.0, *) {
  77. tableView.sectionHeaderTopPadding = 0
  78. }
  79. return tableView
  80. }()
  81. private let noScheduleTipLabel: UILabel = {
  82. let tip = UILabel()
  83. tip.textAlignment = .center
  84. tip.font = UIFont.systemFont(ofSize: 14)
  85. tip.textColor = UIColor.tui_color(withHex: "8F9AB2")
  86. tip.text = .noScheduleText
  87. tip.adjustsFontSizeToFitWidth = true
  88. return tip
  89. }()
  90. private let noScheduleImageView: UIImageView = {
  91. let image = UIImage(named: "room_no_schedule", in: tuiRoomKitBundle(), compatibleWith: nil)
  92. let imageView = UIImageView(image: image)
  93. return imageView
  94. }()
  95. private lazy var dateFormater: DateFormatter = {
  96. let dateFormatter = DateFormatter()
  97. dateFormatter.locale = Locale.current
  98. dateFormatter.dateStyle = .medium
  99. dateFormatter.timeStyle = .none
  100. dateFormatter.timeZone = .current
  101. return dateFormatter
  102. }()
  103. // MARK: - view layout
  104. private var isViewReady: Bool = false
  105. public override func didMoveToWindow() {
  106. super.didMoveToWindow()
  107. guard !isViewReady else { return }
  108. backgroundColor = .white
  109. constructViewHierarchy()
  110. activateConstraints()
  111. bindInteraction()
  112. isViewReady = true
  113. }
  114. // MARK: Private Methods
  115. private func constructViewHierarchy() {
  116. addSubview(noScheduleImageView)
  117. addSubview(noScheduleTipLabel)
  118. addSubview(tableview)
  119. }
  120. private func activateConstraints() {
  121. tableview.snp.makeConstraints { make in
  122. make.leading.bottom.trailing.top.equalToSuperview()
  123. }
  124. noScheduleImageView.snp.makeConstraints { make in
  125. make.centerX.equalToSuperview()
  126. make.top.equalToSuperview().offset(160)
  127. make.width.equalTo(120.scale375())
  128. make.height.equalTo(79.scale375())
  129. }
  130. noScheduleTipLabel.snp.makeConstraints { make in
  131. make.top.equalTo(noScheduleImageView.snp.bottom).offset(20)
  132. make.centerX.equalToSuperview()
  133. }
  134. }
  135. private func bindInteraction() {
  136. subscribeToast()
  137. subscribeScheduleSubject()
  138. subscribeRoomSubject()
  139. store.dispatch(action: ConferenceListActions.fetchConferenceList(payload: (fetchListCursor, conferencesPerFetch)))
  140. conferenceListPublisher
  141. .receive(on: DispatchQueue.global(qos: .default))
  142. .map { [weak self] newInfos -> (Int, [ConferenceSection]) in
  143. guard let self = self else { return (0, []) }
  144. let newSections = self.groupAndSortInfos(newInfos)
  145. return (newInfos.count, newSections)
  146. }
  147. .receive(on: DispatchQueue.mainQueue)
  148. .sink { [weak self] (conferenceCount, newSections) in
  149. guard let self = self else { return }
  150. self.sections = newSections
  151. self.tableview.reloadData()
  152. if conferenceCount > 0 {
  153. self.noScheduleImageView.isHidden = true
  154. self.noScheduleTipLabel.isHidden = true
  155. } else {
  156. self.noScheduleImageView.isHidden = false
  157. self.noScheduleTipLabel.isHidden = false
  158. }
  159. }
  160. .store(in: &cancellableSet)
  161. cursorPublisher
  162. .receive(on: DispatchQueue.mainQueue)
  163. .sink { [weak self] cursor in
  164. guard let self = self else { return }
  165. self.fetchListCursor = cursor
  166. }
  167. .store(in: &cancellableSet)
  168. needRefreshPublisher
  169. .receive(on: DispatchQueue.main)
  170. .removeDuplicates()
  171. .sink { [weak self] needRefresh in
  172. guard let self = self else { return }
  173. if needRefresh {
  174. store.dispatch(action: ConferenceListActions.resetConferenceList())
  175. store.dispatch(action: ConferenceListActions.fetchConferenceList(payload: ("", conferencesPerFetch)))
  176. store.dispatch(action: ScheduleViewActions.stopRefreshList())
  177. }
  178. }
  179. .store(in: &cancellableSet)
  180. }
  181. private func groupAndSortInfos(_ infos: [ConferenceInfo]) -> [ConferenceSection] {
  182. var groupedInfos: [Date: [ConferenceInfo]] = [:]
  183. let calendar = Calendar.current
  184. for info in infos {
  185. let date = calendar.startOfDay(for: Date(timeIntervalSince1970: TimeInterval(info.scheduleStartTime)))
  186. groupedInfos[date, default: []].append(info)
  187. }
  188. var retData: [ConferenceSection] = groupedInfos.map { (date, infos) in
  189. print("")
  190. return ConferenceSection(date: date, conferences: infos.sorted { (confercence1, conference2) -> Bool in
  191. if confercence1.scheduleStartTime == conference2.scheduleStartTime {
  192. return confercence1.basicInfo.createTime < conference2.basicInfo.createTime
  193. } else {
  194. return confercence1.scheduleStartTime < conference2.scheduleStartTime
  195. }
  196. })
  197. }
  198. retData.sort(by: { $0.date < $1.date })
  199. return retData
  200. }
  201. deinit {
  202. debugPrint("deinit \(self)")
  203. }
  204. // MARK: - private property.
  205. @Injected(\.conferenceStore) var store: ConferenceStore
  206. @Injected(\.navigation) var navigation: Route
  207. }
  208. extension ConferenceListView: UITableViewDataSource {
  209. public func numberOfSections(in tableView: UITableView) -> Int {
  210. return sections.count
  211. }
  212. public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  213. return sections[section].conferences.count
  214. }
  215. public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  216. let cell = tableView.dequeueReusableCell(withIdentifier: ConferenceListCell.reusedIdentifier, for: indexPath)
  217. if let cell = cell as? ConferenceListCell, indexPath.row < sections[indexPath.section].conferences.count {
  218. let info = sections[indexPath.section].conferences[indexPath.row]
  219. cell.updateCell(with: info)
  220. }
  221. return cell
  222. }
  223. public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  224. let conferenceInfo = sections[indexPath.section].conferences[indexPath.row]
  225. navigation.pushTo(route: .scheduleDetails(conferenceInfo: conferenceInfo))
  226. }
  227. }
  228. extension ConferenceListView: UITableViewDelegate {
  229. public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  230. return 68.0
  231. }
  232. public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  233. let headerView = UIView()
  234. headerView.backgroundColor = UIColor.white
  235. let calendarImage = UIImage(named: "room_calendar", in: tuiRoomKitBundle(), compatibleWith: nil)
  236. let imageView = UIImageView(image: calendarImage)
  237. headerView.addSubview(imageView)
  238. let headerLabel = UILabel()
  239. headerLabel.font = UIFont(name: "PingFangSC-Regular", size: 14)
  240. headerLabel.textColor = UIColor.tui_color(withHex: "969EB4")
  241. headerLabel.text = self.dateFormater.string(from: sections[section].date)
  242. headerView.addSubview(headerLabel)
  243. imageView.snp.makeConstraints { make in
  244. make.leading.equalToSuperview()
  245. make.centerY.equalTo(headerLabel)
  246. make.height.width.equalTo(16)
  247. }
  248. headerLabel.snp.makeConstraints { make in
  249. make.leading.equalTo(imageView.snp.trailing).offset(4)
  250. make.top.equalToSuperview()
  251. }
  252. return headerView
  253. }
  254. public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
  255. return 40
  256. }
  257. public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  258. let offsetY = scrollView.contentOffset.y
  259. let contentHeight = scrollView.contentSize.height
  260. let height = scrollView.frame.size.height
  261. if offsetY > contentHeight - height {
  262. if !fetchListCursor.isEmpty {
  263. store.dispatch(action: ConferenceListActions.fetchConferenceList(payload: (fetchListCursor, conferencesPerFetch)))
  264. }
  265. }
  266. }
  267. }
  268. extension ConferenceListView {
  269. private func subscribeScheduleSubject() {
  270. store.scheduleActionSubject
  271. .receive(on: RunLoop.main)
  272. .filter { $0.id == ScheduleResponseActions.onScheduleSuccess.id }
  273. .sink { [weak self] action in
  274. guard let self = self else { return }
  275. if let action = action as? AnonymousAction<TUIConferenceInfo> {
  276. let view = InviteEnterRoomView(conferenceInfo: ConferenceInfo(with: action.payload), style: .inviteWhenSuccess)
  277. self.navigation.present(route: .popup(view: view))
  278. }
  279. }
  280. .store(in: &cancellableSet)
  281. }
  282. private func subscribeToast() {
  283. store.toastSubject
  284. .receive(on: DispatchQueue.main)
  285. .sink { [weak self] toast in
  286. guard let self = self else { return }
  287. var position = TUICSToastPositionBottom
  288. switch toast.position {
  289. case .center:
  290. position = TUICSToastPositionCenter
  291. default:
  292. break
  293. }
  294. if self.isPresenting() {
  295. self.makeToast(toast.message, duration: toast.duration, position: position)
  296. }
  297. }
  298. .store(in: &cancellableSet)
  299. }
  300. private func subscribeRoomSubject() {
  301. store.roomActionSubject
  302. .receive(on: RunLoop.main)
  303. .filter { $0.id == RoomResponseActions.onExitSuccess.id }
  304. .sink { [weak self] action in
  305. guard let self = self else { return }
  306. self.store.dispatch(action: ScheduleViewActions.refreshConferenceList())
  307. }
  308. .store(in: &cancellableSet)
  309. }
  310. }
  311. private extension String {
  312. static var noScheduleText: String {
  313. localized("No Room Scheduled")
  314. }
  315. static var historyConferenceText: String {
  316. localized("History Room")
  317. }
  318. }
  319. extension UIView {
  320. func isPresenting() -> Bool {
  321. guard let viewController = self.parentViewController else { return false }
  322. return viewController.presentedViewController == nil
  323. }
  324. var parentViewController: UIViewController? {
  325. var parentResponder: UIResponder? = self
  326. while parentResponder != nil {
  327. parentResponder = parentResponder?.next
  328. if let viewController = parentResponder as? UIViewController {
  329. return viewController
  330. }
  331. }
  332. return nil
  333. }
  334. }