ConferenceMainView.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. //
  2. // ConferenceMainView.swift
  3. // TUIRoomKit
  4. //
  5. // Created by aby on 2022/12/27.
  6. // Copyright © 2022 Tencent. All rights reserved.
  7. // The main conference interface is responsible for arranging and managing the top bar, bottom bar, video interface, etc.
  8. //
  9. import Foundation
  10. import RTCRoomEngine
  11. import Factory
  12. import Combine
  13. protocol ConferenceMainViewFactory {
  14. func makeBottomView() -> BottomView
  15. func makeTopView() -> TopView
  16. func makeVideoSeatView() -> UIView
  17. func makeRaiseHandNoticeView() -> UIView
  18. func makeLocalAudioView() -> LocalAudioView
  19. func makeWaterMarkLayer() -> WaterMarkLayer
  20. func makeFloatChatButton() -> FloatChatButton
  21. func makeFloatChatDisplayView() -> FloatChatDisplayView
  22. func makeRaiseHandApplicationNotificationView() -> RaiseHandApplicationNotificationView
  23. func makeConferencePasswordView() -> ConferencePasswordView
  24. }
  25. struct ConferenceMainViewLayout { //Layout changes when switching between horizontal and vertical screens
  26. let bottomViewLandscapeSpace: Float = 0
  27. let bottomViewPortraitSpace: Float = 34.0
  28. let topViewLandscapeHight: Float = 75.0
  29. let topViewPortraitHight: Float = 105.0
  30. let videoSeatViewPortraitSpace: Float = 73.0
  31. let videoSeatViewLandscapeSpace: Float = 82.0
  32. }
  33. class ConferenceMainView: UIView {
  34. let viewModel: ConferenceMainViewModel
  35. let viewFactory: ConferenceMainViewFactory
  36. let layout: ConferenceMainViewLayout = ConferenceMainViewLayout()
  37. init(viewModel: ConferenceMainViewModel,
  38. viewFactory: ConferenceMainViewFactory) {
  39. self.viewModel = viewModel
  40. self.viewFactory = viewFactory
  41. super.init(frame: .zero)
  42. viewModel.viewResponder = self
  43. subscribeUIEvent()
  44. }
  45. private var currentLandscape: Bool = isLandscape
  46. private let firstDelayDisappearanceTime = 6.0
  47. private let delayDisappearanceTime = 3.0
  48. private lazy var disableMessageUsersPublisher = {
  49. operation.select(UserSelectors.getDisableMessageUsers)
  50. }()
  51. private var cancellableSet = Set<AnyCancellable>()
  52. required init?(coder: NSCoder) {
  53. fatalError("init(coder:) has not been implemented")
  54. }
  55. lazy var topView: TopView = {
  56. return viewFactory.makeTopView()
  57. }()
  58. lazy var videoSeatView: UIView = {
  59. return viewFactory.makeVideoSeatView()
  60. }()
  61. lazy var bottomView: BottomView = {
  62. return viewFactory.makeBottomView()
  63. }()
  64. lazy var raiseHandNoticeView: UIView = {
  65. return viewFactory.makeRaiseHandNoticeView()
  66. }()
  67. lazy var localAudioView: LocalAudioView = {
  68. return viewFactory.makeLocalAudioView()
  69. }()
  70. lazy var waterMarkLayer: WaterMarkLayer = {
  71. return viewFactory.makeWaterMarkLayer()
  72. }()
  73. lazy var floatChatDisplayView: FloatChatDisplayView = {
  74. return viewFactory.makeFloatChatDisplayView()
  75. }()
  76. lazy var floatChatButton: FloatChatButton = {
  77. return viewFactory.makeFloatChatButton()
  78. }()
  79. lazy var raiseHandApplicationNotificationView: RaiseHandApplicationNotificationView = {
  80. let applicationNotificationView = viewFactory.makeRaiseHandApplicationNotificationView()
  81. return applicationNotificationView
  82. }()
  83. lazy var conferencePasswordView: ConferencePasswordView = {
  84. return viewFactory.makeConferencePasswordView()
  85. }()
  86. // MARK: - view layout
  87. private var isViewReady: Bool = false
  88. override func didMoveToWindow() {
  89. super.didMoveToWindow()
  90. guard !isViewReady else { return }
  91. backgroundColor = UIColor(0x0F1014)
  92. constructViewHierarchy()
  93. activateConstraints()
  94. bindInteraction()
  95. isViewReady = true
  96. }
  97. override func layoutSubviews() {
  98. super.layoutSubviews()
  99. guard currentLandscape != isLandscape else { return }
  100. setupRootViewOrientation(isLandscape: isLandscape)
  101. viewModel.setVideoResolutionMode(isLandscape: isLandscape)
  102. currentLandscape = isLandscape
  103. }
  104. func constructViewHierarchy() {
  105. addSubview(videoSeatView)
  106. if viewModel.isShownWaterMark {
  107. layer.addSublayer(waterMarkLayer)
  108. }
  109. addSubview(topView)
  110. addSubview(floatChatDisplayView)
  111. addSubview(floatChatButton)
  112. addSubview(bottomView)
  113. addSubview(localAudioView)
  114. addSubview(raiseHandNoticeView)
  115. addSubview(raiseHandApplicationNotificationView)
  116. addSubview(conferencePasswordView)
  117. }
  118. func activateConstraints() {
  119. setupRootViewOrientation(isLandscape: isLandscape)
  120. raiseHandNoticeView.snp.makeConstraints { make in
  121. make.bottom.equalTo(bottomView.snp.top).offset(-15)
  122. make.centerX.equalToSuperview()
  123. make.height.equalTo(40)
  124. make.width.equalTo(300)
  125. }
  126. localAudioView.snp.makeConstraints { make in
  127. make.centerX.equalToSuperview()
  128. make.width.height.equalTo(40.scale375())
  129. make.bottom.equalToSuperview().offset(-40.scale375Height())
  130. }
  131. floatChatButton.snp.makeConstraints { make in
  132. make.bottom.equalTo(localAudioView.snp.top).offset(-18)
  133. make.height.equalTo(30)
  134. make.leading.equalTo(videoSeatView.snp.leading)
  135. }
  136. floatChatDisplayView.snp.makeConstraints { make in
  137. make.bottom.equalTo(floatChatButton.snp.top).offset(-8)
  138. make.height.equalTo(128)
  139. make.leading.equalToSuperview().offset(5)
  140. make.width.equalTo(313)
  141. }
  142. raiseHandApplicationNotificationView.snp.makeConstraints { make in
  143. make.top.equalTo(topView.snp.bottom)
  144. make.width.equalTo(359.scale375())
  145. make.centerX.equalToSuperview()
  146. make.height.equalTo(40.scale375Height())
  147. }
  148. conferencePasswordView.snp.makeConstraints { make in
  149. make.edges.equalToSuperview()
  150. }
  151. }
  152. private func bindInteraction() {
  153. perform(#selector(hideToolBar),with: nil,afterDelay: firstDelayDisappearanceTime)
  154. subscribeSubject()
  155. }
  156. func setupRootViewOrientation(isLandscape: Bool) {
  157. videoSeatView.snp.remakeConstraints { make in
  158. if isLandscape {
  159. make.leading.equalTo(layout.videoSeatViewLandscapeSpace)
  160. make.trailing.equalTo(-layout.videoSeatViewLandscapeSpace)
  161. make.top.bottom.equalToSuperview()
  162. } else {
  163. make.leading.trailing.equalToSuperview()
  164. make.top.equalTo(layout.videoSeatViewPortraitSpace)
  165. make.bottom.equalTo(-layout.videoSeatViewPortraitSpace)
  166. }
  167. }
  168. topView.snp.remakeConstraints() { make in
  169. make.top.equalToSuperview()
  170. make.leading.equalTo(safeAreaLayoutGuide.snp.leading)
  171. make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing)
  172. if isLandscape {
  173. make.height.equalTo(layout.topViewLandscapeHight)
  174. } else {
  175. make.height.equalTo(layout.topViewPortraitHight)
  176. }
  177. }
  178. bottomView.snp.remakeConstraints { make in
  179. make.leading.equalTo(safeAreaLayoutGuide.snp.leading)
  180. make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing)
  181. make.height.equalTo(bottomView.isUnfold ? bottomView.unfoldHeight : bottomView.packUpHeight)
  182. if isLandscape {
  183. make.bottom.equalToSuperview().offset(-layout.bottomViewLandscapeSpace)
  184. } else {
  185. make.bottom.equalToSuperview().offset(-layout.bottomViewPortraitSpace)
  186. }
  187. }
  188. topView.updateRootViewOrientation(isLandscape: isLandscape)
  189. setupWaterMarkLayerOrientation(isLandscape: isLandscape)
  190. }
  191. private func setupWaterMarkLayerOrientation(isLandscape: Bool) {
  192. guard viewModel.isShownWaterMark else { return }
  193. let widthSpace = isLandscape ? CGFloat(layout.videoSeatViewLandscapeSpace) : 0
  194. let heightSpace = isLandscape ? 0 : CGFloat(layout.videoSeatViewPortraitSpace)
  195. waterMarkLayer.frame = CGRect(x: widthSpace, y: heightSpace, width: kScreenWidth - widthSpace * 2, height: kScreenHeight - heightSpace * 2)
  196. waterMarkLayer.setNeedsDisplay()
  197. }
  198. private func subscribeUIEvent() {
  199. EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ShowFloatChatView, responder: self)
  200. }
  201. private func unsubscribeEvent() {
  202. EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ShowFloatChatView, responder: self)
  203. }
  204. deinit {
  205. NSObject.cancelPreviousPerformRequests(withTarget: self)
  206. unsubscribeEvent()
  207. debugPrint("deinit \(self)")
  208. }
  209. // MARK: - private property.
  210. @Injected(\.conferenceStore) private var operation
  211. @Injected(\.navigation) private var route
  212. }
  213. extension ConferenceMainView: ConferenceMainViewResponder {
  214. func updateWaterMarkLayer(text: String) {
  215. waterMarkLayer.updateWaterMarkImage(text: text)
  216. }
  217. func hidePasswordView() {
  218. conferencePasswordView.hide()
  219. }
  220. func showPasswordView(roomId: String) {
  221. conferencePasswordView.show(roomId: roomId)
  222. }
  223. func showExitRoomView() {
  224. let view = ExitRoomView(viewModel: ExitRoomViewModel())
  225. view.show(rootView: self)
  226. }
  227. func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) {
  228. RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock)
  229. }
  230. func showAlertWithAutoConfirm(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?, autoConfirmSeconds: Int?) {
  231. RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock, autoConfirmSeconds: autoConfirmSeconds)
  232. }
  233. func makeToast(text: String) {
  234. RoomRouter.makeToastInCenter(toast: text, duration: 1)
  235. }
  236. func showRaiseHandNoticeView() {
  237. raiseHandNoticeView.isHidden = false
  238. }
  239. func updateRoomInfo(roomInfo: TUIRoomInfo) {
  240. floatChatButton.updateRoomId(roomId: roomInfo.roomId)
  241. }
  242. private func showToolBar() {
  243. topView.alpha = 1
  244. bottomView.alpha = 1
  245. topView.isHidden = false
  246. bottomView.isHidden = false
  247. localAudioView.hide()
  248. }
  249. @objc private func hideToolBar() {
  250. topView.alpha = 0
  251. bottomView.alpha = 0
  252. topView.isHidden = true
  253. bottomView.isHidden = true
  254. localAudioView.show()
  255. }
  256. func changeToolBarHiddenState() {
  257. NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideToolBar), object: nil)
  258. if topView.isHidden {
  259. showToolBar()
  260. perform(#selector(hideToolBar),with: nil,afterDelay: delayDisappearanceTime)
  261. } else if !bottomView.isUnfold {
  262. hideToolBar()
  263. }
  264. }
  265. func setToolBarDelayHidden(isDelay: Bool) {
  266. NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideToolBar), object: nil)
  267. guard !bottomView.isUnfold, isDelay else { return }
  268. perform(#selector(hideToolBar),with: nil,afterDelay: delayDisappearanceTime)
  269. }
  270. func showRepeatJoinRoomAlert() {
  271. let sureAction = UIAlertAction(title: .repeatJoinRoomSureText, style: .default) { _ in
  272. }
  273. let alertState = AlertState(title: .repeatJoinRoomTitle, message: .repeatJoinRoomMessage, sureAction: sureAction, declineAction: nil)
  274. route.present(route: .alert(state: alertState))
  275. }
  276. }
  277. extension ConferenceMainView: RoomKitUIEventResponder {
  278. func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
  279. switch key {
  280. case .TUIRoomKitService_ShowFloatChatView:
  281. guard let shouldShow = info?["shouldShow"] as? Bool else { return }
  282. floatChatButton.isHidden = !shouldShow
  283. floatChatDisplayView.isHidden = !shouldShow
  284. default: break
  285. }
  286. }
  287. }
  288. extension ConferenceMainView {
  289. private func subscribeSubject() {
  290. disableMessageUsersPublisher
  291. .receive(on: DispatchQueue.mainQueue)
  292. .sink(receiveValue: { [weak self] users in
  293. guard let self = self else { return }
  294. let isSelfDisableMessage = users.contains(self.viewModel.currentUser.userId)
  295. guard isSelfDisableMessage != self.viewModel.isSelfDisableMessage else { return }
  296. let text: String = isSelfDisableMessage ? .beenBannedFromTextChat : .allowedToTextChat
  297. self.operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: text)))
  298. self.viewModel.isSelfDisableMessage = isSelfDisableMessage
  299. })
  300. .store(in: &cancellableSet)
  301. }
  302. }
  303. private extension String {
  304. static let repeatJoinRoomTitle = localized("Currently in the room")
  305. static let repeatJoinRoomMessage = localized("Please exit before joining a new room")
  306. static let repeatJoinRoomSureText = localized("I see")
  307. static let beenBannedFromTextChat = localized("You have been banned from text chat")
  308. static let allowedToTextChat = localized("You are allowed to text chat")
  309. }