// // UserListManagerViewModel.swift // TUIRoomKit // // Created by janejntang on 2023/2/10. // Copyright © 2023 Tencent. All rights reserved. // import Foundation import RTCRoomEngine protocol UserListManagerViewEventResponder: AnyObject { func makeToast(text: String) func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) func setUserListManagerViewHidden(isHidden: Bool) func dismissView() func updateUI(item: ButtonItemData) func updateStackView(items:[ButtonItemData]) func addStackView(item: ButtonItemData, index: Int?) func removeStackView(itemType: ButtonItemData.ButtonType) } class UserListManagerViewModel: NSObject { var selectUserId: String = "" let timeoutNumber: Double = 60 var userListManagerItems: [ButtonItemData] = [] weak var viewResponder: UserListManagerViewEventResponder? var engineManager: EngineManager { EngineManager.shared } var roomInfo: TUIRoomInfo { engineManager.store.roomInfo } var currentUser: UserEntity { engineManager.store.currentUser } var attendeeList: [UserEntity] { engineManager.store.attendeeList } private var hasOpenCameraInvite = false private var hasOpenMicrophoneInvite = false var selectUserInfo: UserEntity? { attendeeList.first(where: { $0.userId == selectUserId } ) } init(selectUserId: String) { self.selectUserId = selectUserId super.init() createManagerItems() subscribeEngine() } deinit { unsubscribeEngine() debugPrint("self:\(self)") } private func subscribeEngine() { EngineEventCenter.shared.subscribeEngine(event: .onUserVideoStateChanged, observer: self) EngineEventCenter.shared.subscribeEngine(event: .onUserAudioStateChanged, observer: self) EngineEventCenter.shared.subscribeEngine(event: .onSendMessageForUserDisableChanged, observer: self) EngineEventCenter.shared.subscribeEngine(event: .onSeatListChanged, observer: self) EngineEventCenter.shared.subscribeEngine(event: .onUserRoleChanged, observer: self) } private func unsubscribeEngine() { EngineEventCenter.shared.unsubscribeEngine(event: .onUserVideoStateChanged, observer: self) EngineEventCenter.shared.unsubscribeEngine(event: .onUserAudioStateChanged, observer: self) EngineEventCenter.shared.unsubscribeEngine(event: .onSendMessageForUserDisableChanged, observer: self) EngineEventCenter.shared.unsubscribeEngine(event: .onSeatListChanged, observer: self) EngineEventCenter.shared.unsubscribeEngine(event: .onUserRoleChanged, observer: self) } private func createManagerItems() { if checkMediaShownState() { userListManagerItems.append(muteAudioItem) userListManagerItems.append(muteVideoItem) } if checkInviteSeatItemShownState() { userListManagerItems.append(inviteSeatItem) } if checkChangeHostItemShownState() { userListManagerItems.append(changeHostItem) } if checkSetAdministratorItemShownState() { userListManagerItems.append(setAdministratorItem) } if checkMuteMessageItemShownState() { userListManagerItems.append(muteMessageItem) } if checkKickOutItemShownState() { userListManagerItems.append(kickOutItem) } } private lazy var muteAudioItem: ButtonItemData = { let item = ButtonItemData() item.normalTitle = .muteText item.normalIcon = "room_unMute_audio" item.selectedTitle = .requestOpenAudioText item.selectedIcon = "room_mute_audio" item.resourceBundle = tuiRoomKitBundle() item.buttonType = .muteAudioItemType item.isSelect = !(selectUserInfo?.hasAudioStream ?? true) item.hasLineView = true item.action = { [weak self] sender in guard let self = self, let button = sender as? UIButton else { return } self.muteAudioAction(sender: button) } return item }() private lazy var muteVideoItem: ButtonItemData = { let item = ButtonItemData() item.normalTitle = .closeVideoText item.normalIcon = "room_unMute_video" item.selectedTitle = .requestOpenVideoText item.selectedIcon = "room_mute_video" item.resourceBundle = tuiRoomKitBundle() item.buttonType = .muteVideoItemType item.isSelect = !(selectUserInfo?.hasVideoStream ?? true) item.hasLineView = true item.action = { [weak self] sender in guard let self = self, let button = sender as? UIButton else { return } self.muteVideoAction(sender: button) } return item }() private lazy var inviteSeatItem: ButtonItemData = { let item = ButtonItemData() item.normalTitle = .inviteSeatText item.normalIcon = "room_invite_seat" item.selectedTitle = .stepDownSeatText item.selectedIcon = "room_step_down_seat" item.resourceBundle = tuiRoomKitBundle() item.isSelect = selectUserInfo?.isOnSeat ?? false item.hasLineView = true item.action = { [weak self] sender in guard let self = self, let button = sender as? UIButton else { return } self.inviteSeatAction(sender: button) } return item }() private lazy var changeHostItem: ButtonItemData = { let item = ButtonItemData() item.normalTitle = .changeHostText item.normalIcon = "room_change_host" item.resourceBundle = tuiRoomKitBundle() item.hasLineView = true item.action = { [weak self] sender in guard let self = self, let button = sender as? UIButton else { return } self.changeHostAction(sender: button) } return item }() private lazy var muteMessageItem: ButtonItemData = { let item = ButtonItemData() item.normalTitle = .muteMessageText item.normalIcon = "room_mute_message" item.selectedTitle = .unMuteMessageText item.selectedIcon = "room_unMute_message" item.resourceBundle = tuiRoomKitBundle() item.isSelect = selectUserInfo?.disableSendingMessage ?? false item.hasLineView = true item.action = { [weak self] sender in guard let self = self, let button = sender as? UIButton else { return } self.muteMessageAction(sender: button) } return item }() private lazy var kickOutItem: ButtonItemData = { let item = ButtonItemData() item.normalTitle = .kickOutRoomText item.normalIcon = "room_kickOut_room" item.resourceBundle = tuiRoomKitBundle() item.hasLineView = true item.action = { [weak self] sender in guard let self = self, let button = sender as? UIButton else { return } self.kickOutAction(sender: button) } return item }() private lazy var setAdministratorItem: ButtonItemData = { let item = ButtonItemData() item.normalTitle = .setAsAdministratorText item.selectedTitle = .undoAdministratorText item.normalIcon = "room_set_administrator" item.selectedIcon = "room_undo_administrator" item.resourceBundle = tuiRoomKitBundle() item.isSelect = selectUserInfo?.userRole == .administrator item.hasLineView = true item.action = { [weak self] sender in guard let self = self, let button = sender as? UIButton else { return } self.setAdministratorAction(sender: button) } return item }() func backBlockAction(sender: UIView) { sender.isHidden = true } private func checkMediaShownState() -> Bool { guard let selectUserInfo = selectUserInfo else { return false } if !roomInfo.isSeatEnabled { return true } else if selectUserInfo.isOnSeat { return true } return false } private func checkInviteSeatItemShownState() -> Bool { return roomInfo.isSeatEnabled } private func checkChangeHostItemShownState() -> Bool { return currentUser.userRole == .roomOwner } private func checkMuteMessageItemShownState() -> Bool { return true } private func checkKickOutItemShownState() -> Bool { return currentUser.userRole == .roomOwner } private func checkSetAdministratorItemShownState() -> Bool { return currentUser.userRole == .roomOwner } } extension UserListManagerViewModel { private func addViewItem(buttonItem: ButtonItemData, index: Int) { guard !isContainedViewItem(buttonType: buttonItem.buttonType) else { return } if userListManagerItems.count > index + 1 { userListManagerItems.insert(buttonItem, at: index) viewResponder?.addStackView(item: buttonItem, index: index) } else { userListManagerItems.append(buttonItem) viewResponder?.addStackView(item: buttonItem, index: nil) } } private func removeViewItem(buttonType: ButtonItemData.ButtonType) { userListManagerItems.removeAll(where: { $0.buttonType == buttonType }) viewResponder?.removeStackView(itemType: buttonType) } private func isContainedViewItem(buttonType: ButtonItemData.ButtonType) -> Bool { return userListManagerItems.contains(where: { $0.buttonType == buttonType }) } private func muteAudioAction(sender: UIButton) { guard let userInfo = attendeeList.first(where: { $0.userId == selectUserId }) else { return } let mute = userInfo.hasAudioStream if mute { engineManager.closeRemoteDeviceByAdmin(userId: selectUserId, device: .microphone) { sender.isSelected = !sender.isSelected } onError: { [weak self] _, _ in guard let self = self else { return } self.viewResponder?.makeToast(text: localizedReplace(.muteAudioErrorToastText, replace: userInfo.userName)) } } else { viewResponder?.makeToast(text: .invitedOpenAudioText) guard !hasOpenMicrophoneInvite else { viewResponder?.dismissView() return } engineManager.openRemoteDeviceByAdmin(userId: selectUserId, device: .microphone, onAccepted: { [weak self] _, _ in guard let self = self else { return } sender.isSelected = !sender.isSelected self.hasOpenMicrophoneInvite = false }, onRejected: { [weak self] _, _, _ in guard let self = self else { return } self.hasOpenMicrophoneInvite = false self.viewResponder?.makeToast(text: userInfo.userName + localizedReplace(.muteAudioRejectToastText, replace: userInfo.userName)) }, onCancelled: { [weak self] _, _ in guard let self = self else { return } self.hasOpenMicrophoneInvite = false }, onTimeout: { [weak self] _, _ in guard let self = self else { return } self.hasOpenMicrophoneInvite = false self.viewResponder?.makeToast(text: .openAudioInvitationTimeoutText) }) { [weak self] _, _, _, _ in guard let self = self else { return } self.hasOpenMicrophoneInvite = false } hasOpenMicrophoneInvite = true } viewResponder?.dismissView() } private func muteVideoAction(sender: UIButton) { guard let userInfo = selectUserInfo else { return } let mute = userInfo.hasVideoStream if mute { engineManager.closeRemoteDeviceByAdmin(userId: selectUserId, device: .camera) { [weak self] in guard let _ = self else { return } sender.isSelected = !sender.isSelected } onError: { [weak self] _, _ in guard let self = self else { return } self.viewResponder?.makeToast(text: localizedReplace(.muteVideoErrorToastText, replace: userInfo.userName)) } } else { viewResponder?.makeToast(text: .invitedOpenVideoText) guard !hasOpenCameraInvite else { viewResponder?.dismissView() return } engineManager.openRemoteDeviceByAdmin(userId: selectUserId, device: .camera, onAccepted: { [weak self] _, _ in guard let self = self else { return } sender.isSelected = !sender.isSelected self.hasOpenCameraInvite = false }, onRejected: { [weak self] _, _, _ in guard let self = self else { return } self.hasOpenCameraInvite = false self.viewResponder?.makeToast(text: userInfo.userName + localizedReplace(.muteVideoRejectToastText, replace: userInfo.userName)) }, onCancelled: { [weak self] _, _ in guard let self = self else { return } self.hasOpenCameraInvite = false }, onTimeout: { [weak self] _, _ in guard let self = self else { return } self.hasOpenCameraInvite = false self.viewResponder?.makeToast(text: .openVideoInvitationTimeoutText) }) { [weak self] _, _, _, _ in guard let self = self else { return } self.hasOpenCameraInvite = false } hasOpenCameraInvite = true } viewResponder?.dismissView() } func inviteSeatAction(sender: UIButton) { guard let userInfo = selectUserInfo else { return } sender.isSelected = !sender.isSelected if sender.isSelected { guard engineManager.store.seatList.count < roomInfo.maxSeatCount else { RoomRouter.makeToastInCenter(toast: .theStageIsFullText, duration: 0.5) viewResponder?.dismissView() return } engineManager.takeUserOnSeatByAdmin(userId: selectUserId, timeout: timeoutNumber) { _,_ in let text: String = localizedReplace(.onStageText, replace: userInfo.userName) RoomRouter.makeToastInCenter(toast: text, duration: 0.5) } onRejected: { requestId, userId, message in let text: String = localizedReplace(.refusedTakeSeatInvitationText, replace: userInfo.userName) RoomRouter.makeToastInCenter(toast: text, duration: 0.5) } onCancelled: { _, _ in } onTimeout: { _, _ in let text: String = localizedReplace(.takeSeatInvitationTimeoutText, replace: userInfo.userName) RoomRouter.makeToastInCenter(toast: text, duration: 0.5) } onError: { _, _, code, message in let text: String = code == .requestIdRepeat ? .receivedSameRequestText : message RoomRouter.makeToastInCenter(toast: text, duration: 0.5) } viewResponder?.makeToast(text: .invitedTakeSeatText) } else { engineManager.kickUserOffSeatByAdmin(userId: selectUserId) } viewResponder?.dismissView() } private func changeHostAction(sender: UIButton) { guard let userInfo = attendeeList.first(where: { $0.userId == selectUserId }) else { return } let title = localizedReplace(.transferHostTitle, replace: userInfo.userName) viewResponder?.showAlert(title: title, message: .transferHostMessage, sureTitle: .transferHostsureText, declineTitle: .cancelText, sureBlock: { [weak self] in guard let self = self else { return } self.engineManager.changeUserRole(userId: self.selectUserId, role: .roomOwner) { [weak self] in guard let self = self else { return } let text = localizedReplace(.haveTransferredMasterText, replace: userInfo.userName) self.viewResponder?.makeToast(text: text) self.viewResponder?.dismissView() } onError: { [weak self] code, message in guard let self = self else { return } self.viewResponder?.dismissView() } }, declineBlock: nil) } private func muteMessageAction(sender: UIButton) { guard let userInfo = attendeeList.first(where: { $0.userId == selectUserId }) else { return } engineManager.disableSendingMessageByAdmin(userId: selectUserId, isDisable: !userInfo.disableSendingMessage) viewResponder?.dismissView() } func kickOutAction(sender: UIButton) { let kickOutTitle = localizedReplace(.kickOutText, replace: selectUserInfo?.userName ?? "") viewResponder?.showAlert(title: kickOutTitle, message: nil, sureTitle: .alertOkText, declineTitle: .cancelText, sureBlock: { [weak self] in guard let self = self else { return } self.engineManager.kickRemoteUserOutOfRoom(userId: self.selectUserId) self.viewResponder?.dismissView() }, declineBlock: nil) } private func setAdministratorAction(sender: UIButton) { guard let userInfo = selectUserInfo else { return } let role: TUIRole = userInfo.userRole == .administrator ? .generalUser : .administrator engineManager.changeUserRole(userId: selectUserId, role: role) { [weak self] in guard let self = self else { return } let setAdministratorText = localizedReplace(.setUpAdministratorText, replace: userInfo.userName) let removeAdministratorText = localizedReplace(.removedAdministratorText, replace: userInfo.userName) let text: String = role == .administrator ? setAdministratorText : removeAdministratorText self.viewResponder?.makeToast(text: text) self.viewResponder?.dismissView() } onError: { [weak self] code, message in guard let self = self else { return } self.viewResponder?.dismissView() debugPrint("changeUserRole,code:\(code),message:\(message)") } } } extension UserListManagerViewModel: RoomEngineEventResponder { func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) { switch name { case .onUserAudioStateChanged: guard let userId = param?["userId"] as? String else { return } guard let hasAudio = param?["hasAudio"] as? Bool else { return } guard userId == selectUserId else { return } muteAudioItem.isSelect = !hasAudio viewResponder?.updateUI(item: muteAudioItem) case .onUserVideoStateChanged: guard let userId = param?["userId"] as? String else { return } guard let hasVideo = param?["hasVideo"] as? Bool else { return } guard let streamType = param?["streamType"] as? TUIVideoStreamType else { return } guard userId == selectUserId, streamType == .cameraStream else { return } muteVideoItem.isSelect = !hasVideo viewResponder?.updateUI(item: muteVideoItem) case .onSendMessageForUserDisableChanged: guard let userId = param?["userId"] as? String else { return } guard let muted = param?["muted"] as? Bool else { return } guard userId == selectUserId else { return } muteMessageItem.isSelect = muted viewResponder?.updateUI(item: muteMessageItem) case .onSeatListChanged: if let seated = param?["seated"] as? [TUISeatInfo], seated.first(where: { $0.userId == selectUserId }) != nil { inviteSeatItem.isSelect = true viewResponder?.updateUI(item: inviteSeatItem) addViewItem(buttonItem: muteAudioItem, index: 0) addViewItem(buttonItem: muteVideoItem, index: 1) } if let left = param?["left"] as? [TUISeatInfo], left.first(where: { $0.userId == selectUserId }) != nil { inviteSeatItem.isSelect = false viewResponder?.updateUI(item: inviteSeatItem) removeViewItem(buttonType: .muteAudioItemType) removeViewItem(buttonType: .muteVideoItemType) } case .onUserRoleChanged: guard let userId = param?["userId"] as? String else { return } guard let userRole = param?["userRole"] as? TUIRole else { return } guard userId == currentUser.userId || userId == selectUserId else { return } guard currentUser.userId != selectUserId else { return } guard let selectUserInfo = selectUserInfo else { return } userListManagerItems.removeAll() if currentUser.userRole == .roomOwner { createManagerItems() viewResponder?.updateStackView(items: userListManagerItems) } else if currentUser.userRole == .administrator, selectUserInfo.userRole == .generalUser { createManagerItems() viewResponder?.updateStackView(items: userListManagerItems) } else { viewResponder?.dismissView() } default: break } } } private extension String { static var muteAudioErrorToastText: String { localized("Failed to mute.") } static var muteAudioRejectToastText: String { localized(" rejected to the microphone access request.") } static var muteVideoErrorToastText: String { localized("Failed to disable video.") } static var muteVideoRejectToastText: String { localized(" rejected to the camera access request.") } static var muteText: String { localized("Mute") } static var requestOpenVideoText: String { localized("Ask to start video") } static var requestOpenAudioText: String { localized("Ask to unmute") } static var closeVideoText: String { localized("Stop video") } static var changeHostText: String { localized("Make host") } static var muteMessageText: String { localized("Disable chat") } static var unMuteMessageText: String { localized("Enable chat") } static var kickOutRoomText: String { localized("Remove") } static var stepDownSeatText: String { localized("Leave the stage") } static var inviteSeatText: String { localized("Invite to stage") } static var invitedTakeSeatText: String { localized("The audience has been invited to the stage") } static var refusedTakeSeatInvitationText: String { localized("xx refused to go on stage") } static var takeSeatInvitationTimeoutText: String { localized("The invitation to xx to go on stage has timed out") } static var openVideoInvitationTimeoutText: String { localized("The invitation to start the video has timed out") } static var openAudioInvitationTimeoutText: String { localized("The invitation to start the audio has timed out") } static var invitedOpenAudioText: String { localized("The audience has been invited to open the audio") } static var invitedOpenVideoText: String { localized("The audience has been invited to open the video") } static var kickOutText: String { localized("Do you want to move xx out of the conference?") } static var setAsAdministratorText: String { localized("Set as administrator") } static var undoAdministratorText: String { localized("Undo administrator") } static var haveTransferredMasterText: String { localized("The host has been transferred to xx") } static var setUpAdministratorText: String { localized("xx has been set as conference admin") } static var removedAdministratorText: String { localized("The conference admin status of xx has been withdrawn") } static var alertOkText: String { localized("OK") } static var cancelText: String { localized("Cancel") } static var transferHostTitle: String { localized("Transfer the host to xx") } static var transferHostMessage: String { localized("After transfer the host, you will become a general user") } static var transferHostsureText: String { localized("Confirm transfer") } static var receivedSameRequestText: String { localized("This member has already received the same request, please try again later") } static var onStageText: String { localized("xx is on stage") } static var theStageIsFullText: String { localized("The stage is full") } }