增加换肤功能

This commit is contained in:
启星
2025-08-14 10:07:49 +08:00
parent f6964c1e89
commit 4f9318d98e
8789 changed files with 978530 additions and 2 deletions

View File

@@ -0,0 +1,806 @@
//
// BottomViewModel.swift
// Alamofire
//
// Created by aby on 2022/12/22.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
import TUICore
import Factory
import Combine
protocol BottomViewModelResponder: AnyObject {
func updateButtonView(item: ButtonItemData)
func makeToast(text: String)
func updataBottomView(isUp:Bool)
func updateStackView(items: [ButtonItemData])
func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?)
}
class BottomViewModel: NSObject {
private(set) var viewItems: [ButtonItemData] = []
weak var viewResponder: BottomViewModelResponder?
var engineManager: EngineManager {
EngineManager.shared
}
var engineEventCenter: EngineEventCenter {
EngineEventCenter.shared
}
var roomInfo: TUIRoomInfo {
engineManager.store.roomInfo
}
var currentUser: UserEntity {
engineManager.store.currentUser
}
var attendeeList: [UserEntity] {
engineManager.store.attendeeList
}
var inviteSeatList: [RequestEntity] {
engineManager.store.inviteSeatList
}
var isCalledFromShareScreen = false
var cancellableSet = Set<AnyCancellable>()
private lazy var memberItem: ButtonItemData = {
let memberItem = ButtonItemData()
memberItem.normalTitle = String(format: .memberText, attendeeList.count)
memberItem.normalIcon = "room_member"
memberItem.resourceBundle = tuiRoomKitBundle()
memberItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.memberAction(sender: button)
}
return memberItem
}()
private lazy var muteAudioItem: ButtonItemData = {
let muteAudioItem = ButtonItemData()
muteAudioItem.normalTitle = .muteAudioText
muteAudioItem.selectedTitle = .unMuteAudioText
muteAudioItem.normalIcon = "room_unMute_audio"
muteAudioItem.selectedIcon = "room_mic_off"
muteAudioItem.resourceBundle = tuiRoomKitBundle()
muteAudioItem.buttonType = .muteAudioItemType
muteAudioItem.isSelect = !currentUser.hasAudioStream
muteAudioItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.muteAudioAction(sender: button)
}
return muteAudioItem
}()
private lazy var muteVideoItem: ButtonItemData = {
let muteVideoItem = ButtonItemData()
muteVideoItem.normalTitle = .muteVideoText
muteVideoItem.selectedTitle = .unMuteVideoText
muteVideoItem.normalIcon = "room_camera_on"
muteVideoItem.selectedIcon = "room_camera_off"
muteVideoItem.resourceBundle = tuiRoomKitBundle()
muteVideoItem.buttonType = .muteVideoItemType
muteVideoItem.isSelect = !currentUser.hasVideoStream
muteVideoItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.muteVideoAction(sender: button)
}
return muteVideoItem
}()
private lazy var shareScreenItem: ButtonItemData = {
let shareScreenItem = ButtonItemData()
shareScreenItem.normalTitle = .shareScreenOnText
shareScreenItem.selectedTitle = .shareScreenOffText
shareScreenItem.normalIcon = "room_shareScreen_on"
shareScreenItem.selectedIcon = "room_shareScreen_off"
shareScreenItem.resourceBundle = tuiRoomKitBundle()
shareScreenItem.buttonType = .shareScreenItemType
shareScreenItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.shareScreenAction(sender: button)
}
return shareScreenItem
}()
private lazy var chatItem: ButtonItemData = {
let chatItem = ButtonItemData()
chatItem.normalIcon = "room_chat"
chatItem.normalTitle = .chatText
chatItem.resourceBundle = tuiRoomKitBundle()
chatItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.chatAction(sender: button)
}
return chatItem
}()
private lazy var moreItem: ButtonItemData = {
let moreItem = ButtonItemData()
moreItem.normalTitle = .unfoldText
moreItem.normalIcon = "room_more"
moreItem.selectedTitle = .dropText
moreItem.selectedIcon = "room_drop"
moreItem.resourceBundle = tuiRoomKitBundle()
moreItem.buttonType = .moreItemType
moreItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.moreAction(sender: button)
}
return moreItem
}()
private lazy var floatItem: ButtonItemData = {
let floatItem = ButtonItemData()
floatItem.normalTitle = .floatText
floatItem.normalIcon = "room_float"
floatItem.resourceBundle = tuiRoomKitBundle()
floatItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.floatAction(sender: button)
}
return floatItem
}()
private lazy var setupItem: ButtonItemData = {
let setupItem = ButtonItemData()
setupItem.normalTitle = .setupText
setupItem.normalIcon = "room_setting"
setupItem.resourceBundle = tuiRoomKitBundle()
setupItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.setupAction(sender: button)
}
return setupItem
}()
private lazy var inviteItem: ButtonItemData = {
let inviteItem = ButtonItemData()
inviteItem.normalTitle = .inviteText
inviteItem.normalIcon = "room_invite"
inviteItem.resourceBundle = tuiRoomKitBundle()
inviteItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.inviteAction(sender: button)
}
return inviteItem
}()
private lazy var raiseHandApplyItem: ButtonItemData = {
let item = ButtonItemData()
item.normalTitle = .stageManagementText
item.normalIcon = "room_hand_raise_list"
item.resourceBundle = tuiRoomKitBundle()
item.buttonType = .raiseHandApplyItemType
item.noticeText = String(inviteSeatList.count)
item.hasNotice = inviteSeatList.count > 0
item.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.raiseHandApplyAction(sender: button)
}
return item
}()
private lazy var raiseHandItem: ButtonItemData = {
let item = ButtonItemData()
item.normalTitle = currentUser.userRole == .generalUser ? .applyJoinStageText : .joinStageText
item.normalIcon = "room_apply_join_stage"
item.selectedIcon = "room_cancel_request"
item.selectedTitle = .cancelStageText
item.resourceBundle = tuiRoomKitBundle()
item.buttonType = .raiseHandItemType
item.isSelect = engineManager.store.selfTakeSeatRequestId != nil
item.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.raiseHandAction(sender: button)
}
return item
}()
private lazy var leaveSeatHandItem: ButtonItemData = {
let item = ButtonItemData()
item.normalIcon = "room_leave_seat"
item.selectedIcon = "room_apply_join_stage"
item.normalTitle = .leaveSeatText
item.selectedTitle = .applyJoinStageText
item.buttonType = .leaveSeatItemType
item.resourceBundle = tuiRoomKitBundle()
item.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.leaveSeatAction(sender: button)
}
return item
}()
private lazy var invitationPopupPublisher = {
conferenceStore.select(ViewSelectors.getShowinvitationPopupView)
}()
override init() {
super.init()
createBottomData()
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_UserOnSeatChanged, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasAudioStream, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasVideoStream, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_SomeoneSharing, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewUserList, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
EngineEventCenter.shared.subscribeEngine(event: .onAllUserCameraDisableChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onAllUserMicrophoneDisableChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onInitialSelfUserInfo, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRemoteUserEnterRoom, observer: self)
NotificationCenter.default.addObserver(self,
selector: #selector(onUserScreenCaptureStarted),
name: UIScreen.capturedDidChangeNotification, object: nil)
invitationPopupPublisher
.receive(on: DispatchQueue.mainQueue)
.sink { [weak self] showInvitationPopupView in
guard let self = self else { return }
if showInvitationPopupView {
showMemberSelectViewAction()
}
}
.store(in: &cancellableSet)
}
func createBottomData() {
creatBaseBottomData()
createMoreBottomData()
}
func creatBaseBottomData(){
viewItems.append(memberItem)
viewItems.append(muteAudioItem)
viewItems.append(muteVideoItem)
if roomInfo.isSeatEnabled {
if currentUser.userRole == .roomOwner {
viewItems.append(raiseHandApplyItem)
} else {
if currentUser.isOnSeat {
viewItems.append(leaveSeatHandItem)
} else {
viewItems.append(raiseHandItem)
}
}
if currentUser.userRole == .administrator {
viewItems.append(raiseHandApplyItem)
}
}
viewItems.append(shareScreenItem)
if hasTUIChatItem() {
viewItems.append(chatItem)
}
viewItems.append(moreItem)
updateAudioItem()
updateVideoItem()
}
func createMoreBottomData(){
viewItems.append(inviteItem)
viewItems.append(floatItem)
viewItems.append(setupItem)
reorderTheMoreItem()
}
func memberAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .userListViewType, height: 720.scale375Height(), backgroundColor: UIColor(0x17181F))
}
func muteAudioAction(sender: UIButton) {
engineEventCenter.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": true])
if currentUser.hasAudioStream {
engineManager.muteLocalAudio()
return
}
//If all hosts are muted, ordinary members of the room cannot turn on their microphones.
if self.roomInfo.isMicrophoneDisableForAllUser && currentUser.userRole == .generalUser {
viewResponder?.makeToast(text: .muteAudioRoomReasonText)
return
}
//If you are speaking in a room with your hand raised and you are not on the microphone, you cannot turn on the microphone.
if roomInfo.isSeatEnabled, !currentUser.isOnSeat {
viewResponder?.makeToast(text: .muteSeatReasonText)
return
}
engineManager.unmuteLocalAudio()
guard !engineManager.store.audioSetting.isMicOpened else { return }
engineManager.openLocalMicrophone()
}
func muteVideoAction(sender: UIButton) {
engineEventCenter.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": true])
if currentUser.hasVideoStream {
engineManager.closeLocalCamera()
return
}
//If the entire host bans paintings, ordinary members of the room cannot turn on the camera.
if self.roomInfo.isCameraDisableForAllUser && self.currentUser.userRole == .generalUser {
viewResponder?.makeToast(text: .muteVideoRoomReasonText)
return
}
//If you are speaking in a room with your hands raised and you are not on the mic, you cannot turn on the camera.
if roomInfo.isSeatEnabled, !currentUser.isOnSeat {
viewResponder?.makeToast(text: .muteSeatReasonText)
return
}
engineManager.setLocalVideoView(streamType: .cameraStream, view: nil)
engineManager.openLocalCamera()
}
func raiseHandApplyAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .raiseHandApplicationListViewType, height: 720.scale375Height(), backgroundColor: UIColor(0x22262E))
}
func raiseHandAction(sender: UIButton) {
sender.isSelected = !sender.isSelected
engineEventCenter.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": true])
if sender.isSelected {
handleRaiseHandAction()
} else {
handleCancelRaiseHandAction()
}
}
func handleRaiseHandAction() {
_ = engineManager.takeSeat() { [weak self] _,_ in
guard let self = self else { return }
self.viewResponder?.makeToast(text: .takenSeatText)
} onRejected: { [weak self] _, _, _ in
guard let self = self else { return }
self.viewResponder?.makeToast(text: .rejectedTakeSeatText)
self.changeItemSelectState(type: .raiseHandItemType, isSelected: false)
} onTimeout: { [weak self] requestId, userId in
guard let self = self else { return }
self.viewResponder?.makeToast(text: .joinStageApplicationTimedOutText)
self.changeItemSelectState(type: .raiseHandItemType, isSelected: false)
} onError: { [weak self] _, _, code, message in
guard let self = self else { return }
self.changeItemSelectState(type: .raiseHandItemType, isSelected: false)
}
changeItemSelectState(type: .raiseHandItemType)
guard currentUser.userRole == .generalUser else { return }
viewResponder?.makeToast(text: .applicationHasSentText)
}
func handleCancelRaiseHandAction() {
engineManager.cancelTakeSeatRequest()
changeItemSelectState(type: .raiseHandItemType)
viewResponder?.makeToast(text: .joinStageApplicationCancelledText)
}
func leaveSeatAction(sender: UIButton) {
engineEventCenter.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": true])
if currentUser.userRole == .administrator {
engineManager.leaveSeat()
} else {
viewResponder?.showAlert(title: .leaveSeatTitle, message: .leaveSeatMessage, sureTitle: .leaveSeatText, declineTitle: .toastCancelText, sureBlock: { [weak self] in
guard let self = self else { return }
self.engineManager.leaveSeat()
}, declineBlock: nil)
}
}
func shareScreenAction(sender: UIButton) {
if #available(iOS 12.0, *) {
guard let item = viewItems.first(where: { $0.buttonType == .shareScreenItemType })
else { return }
if !item.isSelect {
//If someone else is screen sharing, you can no longer screen share yourself
guard engineManager.store.attendeeList.first(where: {$0.hasScreenStream}) == nil else {
viewResponder?.makeToast(text: .othersScreenSharingText)
return
}
//If you are in a room where you are raising your hand to speak, and you are not on the mic, you cannot share your screen.
guard !(roomInfo.isSeatEnabled && !currentUser.isOnSeat) else {
viewResponder?.makeToast(text: .muteSeatReasonText)
return
}
if TUICore.callService(TUICore_PrivacyService,
method: TUICore_PrivacyService_ScreenShareAntifraudReminderMethod,
param: nil, resultCallback: { [weak self] code, message, param in
guard let self = self else { return }
if code == TUICore_PrivacyService_EnableScreenShareAntifraudReminderMethod_Continue {
self.isCalledFromShareScreen = true
BroadcastLauncher.launch()
}
}) == nil {
isCalledFromShareScreen = true
BroadcastLauncher.launch()
}
} else {
viewResponder?.showAlert(title: .toastTitleText, message: .toastMessageText, sureTitle: .toastStopText, declineTitle: .toastCancelText, sureBlock: { [weak self] in
guard let self = self else { return }
self.engineManager.stopScreenCapture()
}, declineBlock: nil)
}
} else {
viewResponder?.makeToast(text: .versionLowToastText)
}
}
func chatAction(sender: UIButton) {
let user = engineManager.store.currentUser
let roomInfo = engineManager.store.roomInfo
RoomRouter.shared.pushToChatController(user: user, roomInfo: roomInfo)
}
func moreAction(sender: UIButton) {
sender.isSelected = !sender.isSelected
engineEventCenter.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": !sender.isSelected])
viewResponder?.updataBottomView(isUp: sender.isSelected)
changeItemSelectState(type: .moreItemType)
}
func inviteAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .inviteViewType, height: 158.scale375Height())
}
func floatAction(sender: UIButton) {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ShowRoomVideoFloatView, param: [:])
}
func setupAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .mediaSettingViewType, height: 709.scale375Height())
}
func showMemberSelectViewAction() {
conferenceStore.dispatch(action: InvitationViewActions.resetPopupViewFlag())
let inRoomUsers = attendeeList.map{ UserInfo(userEntity: $0).convertToUser() }
// TODO: @jeremiawang Use ConferenceRouter to push ContactVC
let participants = ConferenceParticipants(unSelectableList: inRoomUsers)
guard let vc = Container.shared.contactViewController(participants) as? (ContactViewProtocol & UIViewController) else {
return
}
vc.delegate = self
RoomRouter.shared.push(viewController: vc)
}
@objc func onUserScreenCaptureStarted(notification:Notification)
{
guard let screen = notification.object as? UIScreen else {return}
if screen.isCaptured,isCalledFromShareScreen {
engineManager.startScreenCapture()
}
}
deinit {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_UserOnSeatChanged, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasAudioStream, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasVideoStream, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_SomeoneSharing, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewUserList, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onAllUserCameraDisableChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onAllUserMicrophoneDisableChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onInitialSelfUserInfo, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRemoteUserEnterRoom, observer: self)
NotificationCenter.default.removeObserver(self, name: UIScreen.capturedDidChangeNotification, object: nil)
debugPrint("deinit \(self)")
}
@Injected(\.conferenceStore) private var conferenceStore: ConferenceStore
}
// MARK: - Private
extension BottomViewModel {
private func hasTUIChatItem() -> Bool {
return TUICore.getService(TUICore_TUIChatService) != nil
}
private func changeItemSelectState(type: ButtonItemData.ButtonType, isSelected: Bool? = nil) {
guard let item = viewItems.first(where: { $0.buttonType == type })
else { return }
if let isSelected = isSelected {
item.isSelect = isSelected
} else {
item.isSelect = !item.isSelect
}
viewResponder?.updateButtonView(item: item)
}
private func updateRaiseHandItem() {
guard roomInfo.isSeatEnabled else { return }
raiseHandItem.normalTitle = currentUser.userRole == .generalUser ? .applyJoinStageText : .joinStageText
leaveSeatHandItem.isSelect = false
raiseHandItem.isSelect = false
if currentUser.userRole == .roomOwner {
guard let index = viewItems.firstIndex(where:{ $0.buttonType == .leaveSeatItemType || $0.buttonType == .raiseHandItemType }) else { return }
viewItems.remove(at: index)
} else if let index = viewItems.firstIndex(where:{ $0.buttonType == .leaveSeatItemType || $0.buttonType == .raiseHandItemType }) {
if currentUser.isOnSeat {
viewItems[index] = leaveSeatHandItem
} else {
viewItems[index] = raiseHandItem
}
} else {
if currentUser.isOnSeat {
addViewItem(buttonItem: leaveSeatHandItem, index: 3)
} else {
addViewItem(buttonItem: raiseHandItem, index: 1)
}
}
}
private func reorderTheMoreItem() {
guard viewItems.count > 6 else { return }
guard let index = viewItems.firstIndex(where: { $0.buttonType == .moreItemType }), index != 5 else { return }
viewItems.remove(at: index)
viewItems.insert(moreItem, at: 5)
}
private func removeViewItem(buttonType: ButtonItemData.ButtonType) {
viewItems.removeAll(where: { $0.buttonType == buttonType })
}
private func addViewItem(buttonItem: ButtonItemData, index: Int) {
guard !isContainedViewItem(buttonType: buttonItem.buttonType) else { return }
if viewItems.count > index + 1 {
viewItems.insert(buttonItem, at: index)
} else {
viewItems.append(buttonItem)
}
}
private func isContainedViewItem(buttonType: ButtonItemData.ButtonType) -> Bool {
return viewItems.contains(where: { $0.buttonType == buttonType })
}
private func updateAudioItem() {
if roomInfo.isSeatEnabled, currentUser.userRole == .generalUser, !currentUser.isOnSeat {
//If the audience in the room who raises their hand to speak is not on the microphone, the microphone button will not be displayed.
removeViewItem(buttonType: .muteAudioItemType)
} else if !isContainedViewItem(buttonType: .muteAudioItemType) {
addViewItem(buttonItem: muteAudioItem, index: 1)
}
muteAudioItem.isSelect = !currentUser.hasAudioStream
muteAudioItem.alpha = checkMicAuthority() || currentUser.hasAudioStream ? 1 : 0.5
}
private func updateVideoItem() {
if roomInfo.isSeatEnabled, currentUser.userRole == .generalUser, !currentUser.isOnSeat {
removeViewItem(buttonType: .muteVideoItemType)
} else if !isContainedViewItem(buttonType: .muteVideoItemType) {
addViewItem(buttonItem: muteVideoItem, index: 2)
}
muteVideoItem.isSelect = !currentUser.hasVideoStream
muteVideoItem.alpha = checkCameraAuthority() || currentUser.hasVideoStream ? 1 : 0.5
}
private func checkMicAuthority() -> Bool {
if self.roomInfo.isMicrophoneDisableForAllUser && currentUser.userRole == .generalUser {
return false
}
if roomInfo.isSeatEnabled, !currentUser.isOnSeat {
return false
}
return true
}
private func checkCameraAuthority() -> Bool {
if self.roomInfo.isCameraDisableForAllUser && currentUser.userRole == .generalUser {
return false
}
if roomInfo.isSeatEnabled, !currentUser.isOnSeat {
return false
}
return true
}
private func updateRaiseHandApplyItem() {
guard roomInfo.isSeatEnabled else { return }
raiseHandItem.normalTitle = currentUser.userRole == .generalUser ? .applyJoinStageText : .joinStageText
if currentUser.userRole == .roomOwner {
addViewItem(buttonItem: raiseHandApplyItem, index: 3)
} else if currentUser.userRole == .administrator {
addViewItem(buttonItem: raiseHandApplyItem, index: 4)
} else {
removeViewItem(buttonType: .raiseHandApplyItemType)
}
}
}
extension BottomViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_UserOnSeatChanged:
guard roomInfo.isSeatEnabled else { return }
updateRaiseHandItem()
updateAudioItem()
updateVideoItem()
reorderTheMoreItem()
viewResponder?.updateStackView(items: viewItems)
case .TUIRoomKitService_CurrentUserRoleChanged:
updateAudioItem()
updateVideoItem()
updateRaiseHandApplyItem()
updateRaiseHandItem()
reorderTheMoreItem()
viewResponder?.updateStackView(items: viewItems)
case .TUIRoomKitService_CurrentUserHasAudioStream:
guard let hasAudio = info?["hasAudio"] as? Bool else { return }
guard let reason = info?["reason"] as? TUIChangeReason else { return }
if !hasAudio, reason == .byAdmin, !roomInfo.isMicrophoneDisableForAllUser {
if !roomInfo.isSeatEnabled {
viewResponder?.makeToast(text: .noticeMicrophoneOffTitleText)
} else if currentUser.isOnSeat {
viewResponder?.makeToast(text: .noticeMicrophoneOffTitleText)
}
}
updateAudioItem()
viewResponder?.updateButtonView(item: muteAudioItem)
case .TUIRoomKitService_CurrentUserHasVideoStream:
guard let hasVideo = info?["hasVideo"] as? Bool else { return }
guard let reason = info?["reason"] as? TUIChangeReason else { return }
if !hasVideo, reason == .byAdmin, !roomInfo.isCameraDisableForAllUser {
if !roomInfo.isSeatEnabled {
viewResponder?.makeToast(text: .noticeCameraOffTitleText)
} else if currentUser.isOnSeat {
viewResponder?.makeToast(text: .noticeCameraOffTitleText)
}
}
updateVideoItem()
viewResponder?.updateButtonView(item: muteVideoItem)
case .TUIRoomKitService_SomeoneSharing:
guard let userId = info?["userId"] as? String else { return }
guard let hasVideo = info?["hasVideo"] as? Bool else { return }
guard userId == currentUser.userId else { return }
changeItemSelectState(type: .shareScreenItemType, isSelected: hasVideo)
if !hasVideo {
isCalledFromShareScreen = false
}
case .TUIRoomKitService_RenewUserList:
memberItem.normalTitle = String(format: .memberText, attendeeList.count)
viewResponder?.updateButtonView(item: memberItem)
case .TUIRoomKitService_RenewSeatList:
raiseHandApplyItem.noticeText = String(inviteSeatList.count)
raiseHandApplyItem.hasNotice = inviteSeatList.count > 0
viewResponder?.updateButtonView(item: raiseHandApplyItem)
default: break
}
}
}
extension BottomViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onAllUserCameraDisableChanged:
updateVideoItem()
viewResponder?.updateButtonView(item: muteVideoItem)
case .onAllUserMicrophoneDisableChanged:
updateAudioItem()
viewResponder?.updateButtonView(item: muteAudioItem)
case .onInitialSelfUserInfo:
updateAudioItem()
updateVideoItem()
updateRaiseHandApplyItem()
updateRaiseHandItem()
reorderTheMoreItem()
viewResponder?.updateStackView(items: viewItems)
case .onRemoteUserEnterRoom:
guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
conferenceStore.dispatch(action: ConferenceInvitationActions.removeInvitation(payload: userInfo.userId))
default: break
}
}
}
extension BottomViewModel: ContactViewSelectDelegate {
func onMemberSelected(_ viewController: any ContactViewProtocol, invitees: [User]) {
let userIdList = invitees.map{ $0.userId }
self.conferenceStore.dispatch(action: ConferenceInvitationActions.inviteUsers(payload: (roomInfo.roomId, userIdList)))
RoomRouter.shared.pop()
if !invitees.isEmpty{
viewResponder?.makeToast(text: .inviteEnterRoomSuccesstext)
}
}
}
private extension String {
static var memberText: String {
localized("Users(%lld)")
}
static var muteAudioText: String {
localized("Mute")
}
static var unMuteAudioText: String {
localized("Unmute")
}
static var muteVideoText: String {
localized("Stop video")
}
static var unMuteVideoText: String {
localized("Start video")
}
static var stageManagementText: String {
localized("Applies")
}
static var cancelStageText: String {
localized("Cancel")
}
static var applyJoinStageText: String {
localized("Join stage")
}
static var leaveSeatText: String {
localized("Step down")
}
static var muteSeatReasonText: String {
localized("Can be turned on after taking the stage")
}
static var muteAudioRoomReasonText: String {
localized("All on mute audio, unable to turn on microphone")
}
static var muteVideoRoomReasonText: String {
localized("All on mute video, unable to turn on camera")
}
static var noticeCameraOffTitleText: String {
localized("The conference owner disabled your video.")
}
static var noticeMicrophoneOffTitleText: String {
localized("You were muted by the host.")
}
static var shareScreenOnText: String {
localized("Share")
}
static var shareScreenOffText: String {
localized("Stop")
}
static var versionLowToastText: String {
localized("Your system version is below 12.0. Please update.")
}
static var chatText: String {
localized("Chat")
}
static var unfoldText: String {
localized("More")
}
static var inviteText: String {
localized("Invite")
}
static var floatText: String {
localized("Floating")
}
static var setupText: String {
localized("Settings")
}
static var dropText: String {
localized("Drop")
}
static var rejectedTakeSeatText: String {
localized("Application to go on stage was rejected")
}
static var takenSeatText: String {
localized("Succeed on stage")
}
static var othersScreenSharingText: String {
localized("An existing member is sharing. Please try again later")
}
static var toastTitleText: String {
localized("Share Screen")
}
static var toastMessageText: String {
localized("Stop TUIRoom screen sharing screen live?")
}
static var toastCancelText: String {
localized("Cancel")
}
static var toastStopText: String {
localized("Stop")
}
static var applicationHasSentText: String {
localized("Application has been sent, please wait for the owner/administrator to approve")
}
static var joinStageText: String {
localized("Join stage")
}
static var leaveSeatTitle: String {
localized("Are you sure you want to step down?")
}
static var leaveSeatMessage: String {
localized("To get on stage again, you need to resend the application and wait for the owner/administrator to approve it.")
}
static var joinStageApplicationCancelledText: String {
localized("Application for stage has been cancelled")
}
static var joinStageApplicationTimedOutText: String {
localized("The request to go on stage has timed out")
}
static var inviteEnterRoomSuccesstext: String {
localized("Invitation has been sent, waiting for users to join")
}
}

View File

@@ -0,0 +1,653 @@
//
// ConferenceMainViewModel.swift
// TUIRoomKit
//
// Created by aby on 2022/12/27.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import TUICore
import RTCRoomEngine
import Factory
protocol ConferenceMainViewResponder: AnyObject {
func makeToast(text: String)
func changeToolBarHiddenState()
func setToolBarDelayHidden(isDelay: Bool)
func showExitRoomView()
func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?)
func showAlertWithAutoConfirm(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?, autoConfirmSeconds: Int?)
func showRaiseHandNoticeView()
func updateRoomInfo(roomInfo: TUIRoomInfo)
func showPasswordView(roomId: String)
func hidePasswordView()
func showRepeatJoinRoomAlert()
}
class ConferenceMainViewModel: NSObject {
weak var viewResponder: ConferenceMainViewResponder? = nil
var engineManager: EngineManager {
EngineManager.shared
}
var store: RoomStore {
engineManager.store
}
var roomInfo: TUIRoomInfo {
store.roomInfo
}
var currentUser: UserEntity {
store.currentUser
}
let roomRouter: RoomRouter = RoomRouter.shared
private var isShownOpenCameraInviteAlert = false
private var isShownOpenMicrophoneInviteAlert = false
private var isShownTakeSeatInviteAlert = false
private weak var localAudioViewModel: LocalAudioViewModel?
private var selfRole: TUIRole?
var joinConferenceParams: JoinConferenceParams?
var startConferenceParams: StartConferenceParams?
var isShownWaterMark: Bool = ConferenceSession.sharedInstance.implementation.isEnableWaterMark;
override init() {
super.init()
selfRole = currentUser.userRole
subscribeEngine()
subLogoutNotification()
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onRoomDismissed, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOutOfRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRequestReceived, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onAllUserMicrophoneDisableChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onAllUserCameraDisableChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOffSeat, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOffLine, observer: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserMuteMessage, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ChangeToolBarHiddenState, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ShowExitRoomView, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
EngineEventCenter.shared.subscribeEngine(event: .onStartedRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onJoinedRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onGetUserListFinished, observer: self)
}
private func subLogoutNotification() {
NotificationCenter.default.addObserver(self,
selector: #selector(dismissConferenceViewForLogout),
name: NSNotification.Name.TUILogoutSuccess, object: nil)
}
private func unsubLogoutNotification() {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.TUILogoutSuccess, object: nil)
}
private func unsubscribeEngine() {
EngineEventCenter.shared.unsubscribeEngine(event: .onRoomDismissed, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOutOfRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRequestReceived, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onAllUserMicrophoneDisableChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onAllUserCameraDisableChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOffSeat, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOffLine, observer: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserMuteMessage, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ChangeToolBarHiddenState, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ShowExitRoomView, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onStartedRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onJoinedRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onGetUserListFinished, observer: self)
}
func hideLocalAudioView() {
localAudioViewModel?.hideLocalAudioView()
}
func showLocalAudioView() {
localAudioViewModel?.showLocalAudioView()
}
func onViewDidLoadAction() {
if store.isEnteredRoom {
let roomId = startConferenceParams?.roomId ?? joinConferenceParams?.roomId
if let roomId = roomId, store.roomInfo.roomId != roomId {
viewResponder?.showRepeatJoinRoomAlert()
}
return
}
if startConferenceParams != nil {
quickStartConference()
return
}
if joinConferenceParams != nil {
joinConference()
}
}
func quickStartConference() {
guard let startParams = startConferenceParams, !startParams.roomId.isEmpty else {
return
}
ConferenceOptions.quickStart(startConferenceParams: startParams) { [weak self] roomInfo in
guard let self = self else { return }
guard !self.viewStore.isInternalCreation else { return }
self.notifySuccess(roomInfo: roomInfo, event: .onStartedRoom)
} onError: { [weak self] code, message in
guard let self = self else { return }
self.handleOperateConferenceFailedResult(roomId: startParams.roomId, event: .onStartedRoom, error: code, message: message)
}
}
func joinConference() {
guard let joinParams = joinConferenceParams, !joinParams.roomId.isEmpty else {
return
}
ConferenceOptions.join(joinConferenParams: joinParams) { [weak self] roomInfo in
guard let self = self else { return }
self.viewResponder?.hidePasswordView()
self.notifySuccess(roomInfo: roomInfo, event: .onJoinedRoom)
} onError: { [weak self] code, message in
guard let self = self else { return }
if code == .needPassword {
self.viewResponder?.showPasswordView(roomId: joinParams.roomId)
} else if code == .wrongPassword {
self.viewResponder?.makeToast(text: .wrongPasswordText)
} else {
self.handleOperateConferenceFailedResult(roomId: joinParams.roomId, event: .onJoinedRoom, error: code, message: message)
}
}
}
func notifySuccess(roomInfo: TUIRoomInfo?,
event: EngineEventCenter.RoomEngineEvent) {
let param = [
"roomInfo" : roomInfo ?? TUIRoomInfo(),
"error" : TUIError.success,
"mesasge" : ""
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: event, param: param)
}
func notifyError(roomId: String,
event: EngineEventCenter.RoomEngineEvent,
error: TUIError,
message: String) {
let roomInfo = TUIRoomInfo()
roomInfo.roomId = roomId
let param = [
"roomInfo" : roomInfo,
"error" : error,
"mesasge" : message
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: event, param: param)
}
func setJoinConferenceParams(params: JoinConferenceParams) {
joinConferenceParams = params
store.setCameraOpened(params.isOpenCamera)
store.setSoundOnSpeaker(params.isOpenSpeaker)
}
func setStartConferenceParams(params: StartConferenceParams) {
startConferenceParams = params
store.setCameraOpened(params.isOpenCamera)
store.setSoundOnSpeaker(params.isOpenSpeaker)
}
@objc func dismissConferenceViewForLogout() {
viewResponder?.showAlertWithAutoConfirm(title: .logoutText, message: nil, sureTitle: .alertOkText, declineTitle: nil, sureBlock: {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}, declineBlock: nil, autoConfirmSeconds: 5)
}
func handleWrongPasswordFault(roomId: String) {
handleOperateConferenceFailedResult(roomId: roomId, event: .onJoinedRoom, error: .wrongPassword, message: "password is wrong")
}
private func handleOperateConferenceFailedResult(roomId: String, event: EngineEventCenter.RoomEngineEvent, error: TUIError, message: String) {
if viewStore.isInternalCreation {
roomRouter.pop()
let errorText = "Error: " + String(describing: error) + ", Message: " + message
conferenceStore.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: errorText)))
} else {
notifyError(roomId: roomId, event: event, error: error, message: message)
}
}
deinit {
unsubscribeEngine()
unsubLogoutNotification()
debugPrint("deinit \(self)")
}
@Injected(\.conferenceStore) var conferenceStore: ConferenceStore
@Injected(\.conferenceMainViewStore) var viewStore: ConferenceMainViewStore
}
extension ConferenceMainViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onRoomDismissed:
handleRoomDismissed()
case .onKickedOutOfRoom:
handleKickedOutOfRoom()
case .onAllUserMicrophoneDisableChanged:
guard let isDisable = param?["isDisable"] as? Bool else { return }
handleAllUserMicrophoneDisableChanged(isDisable: isDisable)
case .onAllUserCameraDisableChanged:
guard let isDisable = param?["isDisable"] as? Bool else { return }
handleAllUserCameraDisableChanged(isDisable: isDisable)
case .onKickedOffSeat:
viewResponder?.makeToast(text: .kickedOffSeat)
case .onRequestReceived:
guard let request = param?["request"] as? TUIRequest else { return }
handleReceivedRequest(request: request)
case .onKickedOffLine:
handleKickedOffLine()
case .onStartedRoom:
guard let roomInfo = param?["roomInfo"] as? TUIRoomInfo else { return }
guard let error = param?["error"] as? TUIError else { return }
if error == .success {
handleStartRoom(roomInfo: roomInfo)
conferenceStore.dispatch(action: RoomActions.updateRoomState(payload: RoomInfo(with: roomInfo)))
}
case .onJoinedRoom:
guard let roomInfo = param?["roomInfo"] as? TUIRoomInfo else { return }
guard let error = param?["error"] as? TUIError else { return }
if error == .success {
handleJoinRoom(roomInfo: roomInfo)
conferenceStore.dispatch(action: RoomActions.updateRoomState(payload: RoomInfo(with: roomInfo)))
}
case .onGetUserListFinished:
let allUsers = self.store.attendeeList.map{ UserInfo(userEntity: $0) }
conferenceStore.dispatch(action: UserActions.updateAllUsers(payload: allUsers))
conferenceStore.dispatch(action: ConferenceInvitationActions.getInvitationList(payload: (store.roomInfo.roomId, "", [])))
default: break
}
}
private func handleRoomDismissed() {
#if RTCube_APPSTORE
if currentUser.userRole == .roomOwner {
let selector = NSSelectorFromString("showAlertUserLiveTimeOut")
if UIViewController.responds(to: selector) {
UIViewController.perform(selector)
}
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
engineManager.destroyEngineManager()
return
}
#endif
engineManager.destroyEngineManager()
viewResponder?.showAlertWithAutoConfirm(title: .destroyAlertText, message: nil, sureTitle: .alertOkText, declineTitle: nil, sureBlock: {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}, declineBlock: nil, autoConfirmSeconds: 5)
}
private func handleKickedOutOfRoom() {
engineManager.destroyEngineManager()
viewResponder?.showAlertWithAutoConfirm(title: .kickOffTitleText, message: nil, sureTitle: .alertOkText, declineTitle: nil , sureBlock: {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}, declineBlock: nil, autoConfirmSeconds: 5)
}
private func handleAllUserMicrophoneDisableChanged(isDisable: Bool) {
if isDisable {
RoomRouter.makeToastInCenter(toast: .allMuteAudioText, duration: 1.5)
} else {
RoomRouter.makeToastInCenter(toast: .allUnMuteAudioText, duration: 1.5)
}
}
private func handleAllUserCameraDisableChanged(isDisable: Bool) {
if isDisable {
RoomRouter.makeToastInCenter(toast: .allMuteVideoText, duration: 1.5)
} else {
RoomRouter.makeToastInCenter(toast: .allUnMuteVideoText, duration: 1.5)
}
}
private func handleReceivedRequest(request: TUIRequest) {
switch request.requestAction {
case .openRemoteCamera:
handleOpenCameraRequest(request: request)
case .openRemoteMicrophone:
handleOpenMicrophoneRequest(request: request)
case .invalidAction:
break
case .remoteUserOnSeat:
handleOnSeatRequest(request: request)
default: break
}
}
private func handleOpenCameraRequest(request: TUIRequest) {
guard !isShownOpenCameraInviteAlert else { return }
guard let userInfo = store.attendeeList.first(where: { $0.userId == request.userId }) else { return }
let nameText: String = userInfo.userRole == .roomOwner ? .hostText : .administratorText
let title = localizedReplace(.inviteTurnOnVideoText, replace: nameText)
viewResponder?.showAlert(title: title, message: nil, sureTitle: .agreeText, declineTitle: .declineText, sureBlock: { [weak self] in
guard let self = self else { return }
self.isShownOpenCameraInviteAlert = false
self.agreeOpenLocalCamera(request: request)
}, declineBlock: { [weak self] in
guard let self = self else { return }
self.isShownOpenCameraInviteAlert = false
self.engineManager.responseRemoteRequest(request.requestId, agree: false)
})
isShownOpenCameraInviteAlert = true
}
private func agreeOpenLocalCamera(request: TUIRequest) {
engineManager.setLocalVideoView(streamType: .cameraStream, view: nil)
if RoomCommon.checkAuthorCamaraStatusIsDenied() {
engineManager.responseRemoteRequest(request.requestId, agree: true)
} else {
RoomCommon.cameraStateActionWithPopCompletion { [weak self] granted in
guard let self = self else { return }
self.engineManager.responseRemoteRequest(request.requestId, agree: granted)
}
}
}
private func handleOpenMicrophoneRequest(request: TUIRequest) {
guard !isShownOpenMicrophoneInviteAlert else { return }
guard let userInfo = store.attendeeList.first(where: { $0.userId == request.userId }) else { return }
let nameText: String = userInfo.userRole == .roomOwner ? .hostText : .administratorText
let title = localizedReplace(.inviteTurnOnAudioText, replace: nameText)
viewResponder?.showAlert(title: title, message: nil, sureTitle: .agreeText, declineTitle: .declineText, sureBlock: { [weak self] in
guard let self = self else { return }
self.isShownOpenMicrophoneInviteAlert = false
self.agreeOpenLocalMic(request: request)
}, declineBlock: { [weak self] in
guard let self = self else { return }
self.isShownOpenMicrophoneInviteAlert = false
self.engineManager.responseRemoteRequest(request.requestId, agree: false)
})
isShownOpenMicrophoneInviteAlert = true
}
private func agreeOpenLocalMic(request: TUIRequest) {
if RoomCommon.checkAuthorMicStatusIsDenied() {
self.engineManager.responseRemoteRequest(request.requestId, agree: true)
} else {
RoomCommon.micStateActionWithPopCompletion { [weak self] granted in
guard let self = self else { return }
self.engineManager.responseRemoteRequest(request.requestId, agree: granted)
}
}
}
private func handleOnSeatRequest(request: TUIRequest) {
guard roomInfo.isSeatEnabled && !isShownTakeSeatInviteAlert else { return }
guard let userInfo = store.attendeeList.first(where: { $0.userId == request.userId }) else { return }
let nameText: String = userInfo.userRole == .roomOwner ? .hostText : .administratorText
let title = localizedReplace(.inviteSpeakOnStageTitle, replace: nameText)
viewResponder?.showAlert(title: title, message: .inviteSpeakOnStageMessage, sureTitle: .agreeSeatText, declineTitle: .declineText, sureBlock: { [weak self] in
guard let self = self else { return }
self.isShownTakeSeatInviteAlert = false
self.agreeOnSeatRequest(requestId: request.requestId)
}, declineBlock: { [weak self] in
guard let self = self else { return }
self.isShownTakeSeatInviteAlert = false
self.disagreeOnSeatRequest(requestId: request.requestId)
})
isShownTakeSeatInviteAlert = true
}
private func agreeOnSeatRequest(requestId: String) {
engineManager.responseRemoteRequest(requestId, agree: true) {
} onError: { [weak self] code, message in
guard let self = self else { return }
switch code {
case .failed:
self.viewResponder?.makeToast(text: .goOnStageTimedOutText)
case .allSeatOccupied:
self.viewResponder?.makeToast(text: .onStageNumberReachedLimitText)
default: break
}
}
}
private func disagreeOnSeatRequest(requestId: String) {
engineManager.responseRemoteRequest(requestId, agree: false) {
} onError: { code, message in
debugPrint("responseRemoteRequest:code:\(code),message:\(message)")
}
}
private func handleKickedOffLine() {
viewResponder?.showAlertWithAutoConfirm(title: .kieckedOffLineText, message: nil, sureTitle: .alertOkText, declineTitle: nil, sureBlock: {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}, declineBlock: nil, autoConfirmSeconds: 5)
}
private func handleStartRoom(roomInfo: TUIRoomInfo) {
viewResponder?.updateRoomInfo(roomInfo: roomInfo)
}
private func handleJoinRoom(roomInfo: TUIRoomInfo) {
if roomInfo.isSeatEnabled, store.isShownRaiseHandNotice {
viewResponder?.showRaiseHandNoticeView()
}
viewResponder?.updateRoomInfo(roomInfo: roomInfo)
}
}
extension ConferenceMainViewModel: ConferenceMainViewFactory {
func makeConferencePasswordView() -> ConferencePasswordView {
let passwordView = ConferencePasswordView()
passwordView.isHidden = true
passwordView.viewModel = self
return passwordView
}
func makeTopView() -> TopView {
let viewModel = TopViewModel()
let topView = TopView(viewModel: viewModel)
topView.backgroundColor = UIColor(0x0F1014)
return topView
}
func makeBottomView() -> BottomView {
let viewModel = BottomViewModel()
let bottomView = BottomView(viewModel: viewModel)
return bottomView
}
func makeVideoSeatView() -> UIView {
let viewModel = TUIVideoSeatViewModel()
let videoSeatView = TUIVideoSeatView(viewModel: viewModel)
videoSeatView.backgroundColor = UIColor(0x0F1014)
return videoSeatView
}
func makeRaiseHandNoticeView() -> UIView {
let raiseHandNoticeView = RaiseHandNoticeView()
raiseHandNoticeView.isHidden = true
return raiseHandNoticeView
}
func makeLocalAudioView() -> UIView {
let localAudioViewModel = LocalAudioViewModel()
localAudioViewModel.hideLocalAudioView()
let view = LocalAudioView(viewModel: localAudioViewModel)
self.localAudioViewModel = localAudioViewModel
return view
}
func makeWaterMarkLayer() -> WaterMarkLayer {
let layer = WaterMarkLayer()
layer.backgroundColor = UIColor.clear.cgColor
layer.anchorPoint = CGPointZero
layer.text = getWaterMarkText()
layer.lineStyle = .multiLine
layer.cornerRadius = 16
return layer
}
func makeFloatChatButton() -> FloatChatButton {
let floatchatButton = FloatChatButton()
floatchatButton.isHidden = !store.shouldShowFloatChatView
if store.isEnteredRoom {
floatchatButton.updateRoomId(roomId: store.roomInfo.roomId)
}
return floatchatButton
}
func makeFloatChatDisplayView() -> FloatChatDisplayView {
let view = FloatChatDisplayView()
view.isHidden = !store.shouldShowFloatChatView
return view
}
func makeRaiseHandApplicationNotificationView() -> RaiseHandApplicationNotificationView {
let viewModel = RaiseHandApplicationNotificationViewModel()
let notificationView = RaiseHandApplicationNotificationView(viewModel: viewModel)
return notificationView
}
private func getWaterMarkText() -> String {
let customizeText = ConferenceSession.sharedInstance.implementation.waterMarkText
if !customizeText.isEmpty {
return customizeText
}
let userId = TUILogin.getUserID() ?? currentUser.userId
let userName = TUILogin.getNickName() ?? currentUser.userName
var defaultText = userId
if !userName.isEmpty {
defaultText = defaultText + "(\(userName))"
}
return defaultText
}
}
extension ConferenceMainViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key{
case .TUIRoomKitService_CurrentUserRoleChanged:
guard let userRole = info?["userRole"] as? TUIRole else { return }
handleSelfRoleChanged(userRole: userRole)
case .TUIRoomKitService_CurrentUserMuteMessage:
guard let isMute = info?["isMute"] as? Bool else { return }
viewResponder?.makeToast(text: isMute ? .messageTurnedOffText : .messageTurnedOnText)
case .TUIRoomKitService_ChangeToolBarHiddenState:
viewResponder?.changeToolBarHiddenState()
case .TUIRoomKitService_SetToolBarDelayHidden:
guard let isDelay = info?["isDelay"] as? Bool else { return }
viewResponder?.setToolBarDelayHidden(isDelay: isDelay)
case .TUIRoomKitService_ShowExitRoomView:
viewResponder?.showExitRoomView()
case .TUIRoomKitService_DismissConferenceViewController:
conferenceStore.dispatch(action: ConferenceInvitationActions.clearInvitationList())
conferenceStore.dispatch(action: RoomActions.clearRoomState())
default: break
}
}
private func handleSelfRoleChanged(userRole: TUIRole) {
switch userRole {
case .roomOwner:
viewResponder?.makeToast(text: .haveBecomeMasterText)
case .administrator:
viewResponder?.makeToast(text: .haveBecomeAdministratorText)
case .generalUser:
if selfRole == .administrator {
viewResponder?.makeToast(text: .revokedAdministratorText)
}
default: break
}
selfRole = userRole
}
}
private extension String {
static var kickOffTitleText: String {
localized("You were removed by the host.")
}
static var destroyAlertText: String {
localized("The conference was closed.")
}
static var inviteTurnOnAudioText: String {
localized("xx invites you to turn on the microphone")
}
static var inviteTurnOnVideoText: String {
localized("xx invites you to turn on the camera")
}
static var inviteSpeakOnStageTitle: String {
localized("xx invites you to speak on stage")
}
static var inviteSpeakOnStageMessage: String {
localized("You can turn on the camera and unmute it once you are on stage")
}
static var messageTurnedOffText: String {
localized("You were muted message by the host.")
}
static var messageTurnedOnText: String {
localized("You were unmuted message by the host.")
}
static var haveBecomeMasterText: String {
localized("You are now a host")
}
static var haveBecomeAdministratorText: String {
localized("You have become a conference admin")
}
static var kickedOffLineText: String {
localized("You are already logged in elsewhere")
}
static var alertOkText: String {
localized("OK")
}
static var declineText: String {
localized("Decline")
}
static var agreeText: String {
localized("Agree")
}
static var agreeSeatText: String {
localized("Approve")
}
static var allMuteAudioText: String {
localized("All audios disabled")
}
static var allMuteVideoText: String {
localized("All videos disabled")
}
static var allUnMuteAudioText: String {
localized("All audios enabled")
}
static var allUnMuteVideoText: String {
localized("All videos enabled")
}
static var kickedOffSeat: String {
localized("You have been asked to leave stage")
}
static var hostText: String {
localized("Host")
}
static var administratorText: String {
localized("Administrator")
}
static var revokedAdministratorText: String {
localized("Your conference admin status has been revoked")
}
static var onStageNumberReachedLimitText: String {
localized("The stage is full, please contact the host")
}
static var goOnStageTimedOutText: String {
localized("Failed to go on stage, invitation has timed out")
}
static var kieckedOffLineText: String {
localized("You are already logged in elsewhere")
}
static var logoutText: String {
localized("You are logged out")
}
static let wrongPasswordText = localized("Wrong password, please re-enter")
}

View File

@@ -0,0 +1,99 @@
//
// ExitRoomViewModel.swift
// TUIRoomKit
//
// Created by krabyu on 2023/8/23.
//
import Foundation
protocol ExitRoomViewModelResponder: AnyObject {
func makeToast(message: String)
func dismissView()
}
class ExitRoomViewModel {
var engineManager: EngineManager {
EngineManager.shared
}
var isRoomOwner: Bool {
engineManager.store.currentUser.userId == engineManager.store.roomInfo.ownerId
}
weak var viewResponder: ExitRoomViewModelResponder?
func isShownLeaveRoomButton() -> Bool {
if isRoomOwner {
return getFilterRoomOwnerNumber() > 0
} else {
return true
}
}
func isShownDestroyRoomButton() -> Bool {
return isRoomOwner
}
func leaveRoomAction() {
if isRoomOwner {
if getFilterRoomOwnerNumber() == 1, let userInfo = getNextRoomOwner() {
appointMasterAndExitRoom(userId: userInfo.userId)
} else if getFilterRoomOwnerNumber() > 1 {
viewResponder?.dismissView()
RoomRouter.shared.presentPopUpViewController(viewType: .transferMasterViewType, height: 720.scale375Height())
} else {
destroyRoom()
}
} else {
exitRoom()
}
}
func exitRoom() {
engineManager.exitRoom { [weak self] in
guard let self = self else { return }
self.viewResponder?.dismissView()
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
} onError: { [weak self] code, message in
guard let self = self else { return }
self.viewResponder?.makeToast(message: message)
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}
}
func destroyRoom() {
engineManager.destroyRoom { [weak self] in
guard let self = self else { return }
self.viewResponder?.dismissView()
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
} onError: { [weak self] code, message in
guard let self = self else { return }
self.viewResponder?.makeToast(message: message)
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}
}
private func getNextRoomOwner() -> UserEntity? {
let userInfoArray = engineManager.store.attendeeList.filter({ $0.userId != engineManager.store.roomInfo.ownerId })
return userInfoArray.first
}
private func getFilterRoomOwnerNumber() -> Int {
let array = engineManager.store.attendeeList.filter({ $0.userId != engineManager.store.roomInfo.ownerId })
return array.count
}
private func appointMasterAndExitRoom(userId: String) {
engineManager.changeUserRole(userId: userId, role: .roomOwner) { [weak self] in
guard let self = self else { return }
self.exitRoom()
} onError: { [weak self] code, message in
guard let self = self else { return }
self.viewResponder?.makeToast(message: message)
}
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,117 @@
//
// LocalAudioViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2024/1/5.
//
import Foundation
import RTCRoomEngine
protocol LocalAudioViewModelResponder: AnyObject {
func updateMuteAudioButton(isSelected: Bool)
func makeToast(text: String)
func show()
func hide()
}
class LocalAudioViewModel: NSObject {
weak var viewResponder: LocalAudioViewModelResponder?
var engineManager: EngineManager {
EngineManager.shared
}
var roomInfo: TUIRoomInfo {
engineManager.store.roomInfo
}
var currentUser: UserEntity {
engineManager.store.currentUser
}
var ableDisplay: Bool = false
override init() {
super.init()
subscribeUIEvent()
}
deinit {
unsubscribeUIEvent()
}
private func subscribeUIEvent() {
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasAudioStream, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_UserOnSeatChanged, responder: self)
}
private func unsubscribeUIEvent() {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasAudioStream, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_UserOnSeatChanged, responder: self)
}
func showLocalAudioView() {
ableDisplay = true
guard !checkMuteAudioHiddenState() else { return }
viewResponder?.show()
}
func hideLocalAudioView() {
ableDisplay = false
viewResponder?.hide()
}
func muteAudioAction() {
if currentUser.hasAudioStream {
engineManager.muteLocalAudio()
return
}
//If all hosts are muted, room members cannot turn on their microphones
if self.roomInfo.isMicrophoneDisableForAllUser && self.currentUser.userId != roomInfo.ownerId {
viewResponder?.makeToast(text: .muteAudioRoomReasonText)
return
}
//If you are speaking in a room with your hand raised and you are not on the microphone, you cannot turn on the microphone.
if roomInfo.isSeatEnabled, !currentUser.isOnSeat {
viewResponder?.makeToast(text: .muteSeatReasonText)
return
}
engineManager.unmuteLocalAudio()
guard !engineManager.store.audioSetting.isMicOpened else { return }
engineManager.openLocalMicrophone()
}
func checkMuteAudioHiddenState() -> Bool {
return roomInfo.isSeatEnabled && currentUser.userRole == .generalUser &&
!currentUser.isOnSeat
}
func checkMuteAudioSelectedState() -> Bool {
return !currentUser.hasAudioStream
}
}
extension LocalAudioViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_CurrentUserHasAudioStream:
viewResponder?.updateMuteAudioButton(isSelected: checkMuteAudioSelectedState())
case .TUIRoomKitService_CurrentUserRoleChanged, .TUIRoomKitService_UserOnSeatChanged:
if ableDisplay, !checkMuteAudioHiddenState() {
viewResponder?.show()
} else {
viewResponder?.hide()
}
default: break
}
}
}
private extension String {
static var muteAudioRoomReasonText: String {
localized("All on mute audio, unable to turn on microphone")
}
static var muteSeatReasonText: String {
localized("Can be turned on after taking the stage")
}
}

View File

@@ -0,0 +1,302 @@
//
// MediaSettingViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/16.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
protocol MediaSettingViewEventResponder: AnyObject {
func showResolutionAlert()
func showFrameRateAlert()
func showQualityView()
func updateStackView(item: ListCellItemData)
func makeToast(text: String)
}
class MediaSettingViewModel {
private(set) var videoItems: [ListCellItemData] = []
private(set) var audioItems: [ListCellItemData] = []
private(set) var otherItems: [ListCellItemData] = []
weak var viewResponder: MediaSettingViewEventResponder? = nil
var engineManager: EngineManager {
EngineManager.shared
}
var store: RoomStore {
engineManager.store
}
var videoSetting: VideoModel {
engineManager.store.videoSetting
}
var audioSetting: AudioModel {
engineManager.store.audioSetting
}
let resolutionNameItems: [String] = [.smoothResolutionText, .standardResolutionText, .highResolutionText, .superResolutionText]
private let resolutionItems: [TUIVideoQuality] = [.quality360P, .quality540P, .quality720P, .quality1080P]
private let bitrateArray = [550, 850, 1_200, 2_000]
let topItems: [String] = [.videoText, .audioText, .otherText]
let frameRateArray = ["15", "20"]
init() {
createVideoItem()
createAudioItem()
createOtherItem()
}
private func createVideoItem() {
let resolutionItem = ListCellItemData()
resolutionItem.titleText = .resolutionText
resolutionItem.hasOverAllAction = true
resolutionItem.type = .resolutionType
resolutionItem.hasDownLineView = true
resolutionItem.hasRightButton = true
let buttonData = ButtonItemData()
if let resolutionName = getResolutionName(videoQuality: videoSetting.videoQuality) {
buttonData.normalTitle = resolutionName
}
buttonData.orientation = .right
buttonData.normalIcon = "room_down_arrow1"
buttonData.resourceBundle = tuiRoomKitBundle()
buttonData.titleFont = UIFont.systemFont(ofSize: 16, weight: .regular)
buttonData.titleColor = UIColor(0xD1D9EC)
buttonData.size = CGSize(width: 80, height: 30)
buttonData.isEnabled = false
resolutionItem.buttonData = buttonData
resolutionItem.action = { [weak self] sender in
guard let self = self else { return }
self.resolutionAction()
}
videoItems.append(resolutionItem)
let frameRateItem = ListCellItemData()
frameRateItem.titleText = .frameRateText
frameRateItem.hasOverAllAction = true
frameRateItem.type = .frameRateType
frameRateItem.hasRightButton = true
let frameRateButtonData = ButtonItemData()
frameRateButtonData.orientation = .right
frameRateButtonData.normalIcon = "room_down_arrow1"
frameRateButtonData.normalTitle = String(videoSetting.videoFps)
frameRateButtonData.resourceBundle = tuiRoomKitBundle()
frameRateButtonData.titleFont = UIFont.systemFont(ofSize: 16, weight: .regular)
frameRateButtonData.titleColor = UIColor(0xD1D9EC)
frameRateButtonData.size = CGSize(width: 80, height: 30)
frameRateButtonData.isEnabled = false
frameRateItem.buttonData = frameRateButtonData
frameRateItem.action = { [weak self] sender in
guard let self = self else { return }
self.frameRateAction()
}
videoItems.append(frameRateItem)
}
private func createAudioItem() {
let captureVolumeItem = ListCellItemData()
captureVolumeItem.titleText = .captureVolumeText
captureVolumeItem.hasSlider = true
captureVolumeItem.hasSliderLabel = true
captureVolumeItem.minimumValue = 0
captureVolumeItem.maximumValue = 100
captureVolumeItem.sliderStep = 1
captureVolumeItem.sliderDefault = Float(audioSetting.captureVolume)
captureVolumeItem.hasDownLineView = true
captureVolumeItem.action = { [weak self] sender in
guard let self = self, let view = sender as? UISlider else { return }
self.captureVolumeAction(sender: view)
}
audioItems.append(captureVolumeItem)
let playingVolumeItem = ListCellItemData()
playingVolumeItem.titleText = .playVolumeText
playingVolumeItem.hasSlider = true
playingVolumeItem.hasSliderLabel = true
playingVolumeItem.minimumValue = 0
playingVolumeItem.maximumValue = 100
playingVolumeItem.sliderStep = 1
playingVolumeItem.sliderDefault = Float(audioSetting.playVolume)
playingVolumeItem.hasDownLineView = true
playingVolumeItem.action = { [weak self] sender in
guard let self = self, let view = sender as? UISlider else { return }
self.playingVolumeAction(sender: view)
}
audioItems.append(playingVolumeItem)
let volumePromptItem = ListCellItemData()
volumePromptItem.titleText = .volumePromptText
volumePromptItem.hasSwitch = true
volumePromptItem.isSwitchOn = audioSetting.volumePrompt
volumePromptItem.action = { [weak self] sender in
guard let self = self, let view = sender as? UISwitch else { return }
self.volumePromptAction(sender: view)
}
audioItems.append(volumePromptItem)
}
private func resolutionAction() {
viewResponder?.showResolutionAlert()
}
private func createOtherItem() {
let qualityItem = ListCellItemData()
qualityItem.titleText = .qualityInspectionText
qualityItem.hasOverAllAction = true
qualityItem.hasRightButton = true
let buttonData = ButtonItemData()
buttonData.orientation = .right
buttonData.normalIcon = "room_right_arrow1"
buttonData.resourceBundle = tuiRoomKitBundle()
buttonData.titleFont = UIFont.systemFont(ofSize: 16, weight: .regular)
buttonData.titleColor = UIColor(0xD1D9EC)
buttonData.size = CGSize(width: 80, height: 30)
buttonData.isEnabled = false
qualityItem.buttonData = buttonData
qualityItem.action = { [weak self] sender in
guard let self = self else { return }
self.showQualityAction()
}
otherItems.append(qualityItem)
let floatChatItem = ListCellItemData()
floatChatItem.titleText = .floatChatText
floatChatItem.hasSwitch = true
floatChatItem.isSwitchOn = store.shouldShowFloatChatView
floatChatItem.action = { [weak self] sender in
guard let self = self, let view = sender as? UISwitch else { return }
self.floatChatShowAction(shouldShow: view.isOn)
}
otherItems.append(floatChatItem)
}
private func showQualityAction() {
viewResponder?.showQualityView()
}
private func floatChatShowAction(shouldShow: Bool) {
store.updateFloatChatShowState(shouldShow: shouldShow)
}
private func frameRateAction() {
viewResponder?.showFrameRateAlert()
}
private func captureVolumeAction(sender: UISlider) {
engineManager.setAudioCaptureVolume(Int(sender.value))
}
private func playingVolumeAction(sender: UISlider) {
engineManager.setAudioPlayoutVolume(Int(sender.value))
}
private func volumePromptAction(sender: UISwitch) {
engineManager.enableAudioVolumeEvaluation(isVolumePrompt: sender.isOn)
}
func changeResolutionAction(index: Int) {
guard let videoItem = videoItems.first(where: { $0.type == .resolutionType }) else { return }
guard let quality = resolutionItems[safe: index] else { return }
guard let resolutionName = getResolutionName(videoQuality: quality) else { return }
videoItem.buttonData?.normalTitle = resolutionName
viewResponder?.updateStackView(item: videoItem)
engineManager.setVideoEncoder(videoQuality: quality, bitrate: getBitrate(videoQuality: quality))
}
func changeFrameRateAction(index: Int) {
guard let videoItem = videoItems.first(where: { $0.type == .frameRateType }) else { return }
guard let frameRate = frameRateArray[safe: index] else { return }
videoItem.buttonData?.normalTitle = frameRate
viewResponder?.updateStackView(item: videoItem)
engineManager.setVideoEncoder(fps: Int(frameRate))
}
func getCurrentResolutionIndex() -> Int {
guard let index = resolutionItems.firstIndex(where: { $0 == videoSetting.videoQuality }) else { return 0 }
return index
}
func getCurrentFrameRateIndex() -> Int {
let frameRateString = String(videoSetting.videoFps)
guard let index = frameRateArray.firstIndex(where: { $0 == frameRateString }) else { return 0 }
return index
}
private func getResolutionName(videoQuality: TUIVideoQuality) -> String? {
guard let index = resolutionItems.firstIndex(of: videoQuality) else { return nil }
guard let resolutionName = resolutionNameItems[safe: index] else { return nil }
return resolutionName
}
private func getBitrate(videoQuality: TUIVideoQuality) -> Int? {
guard let index = resolutionItems.firstIndex(of: videoQuality) else { return nil }
guard let bitrate = bitrateArray[safe: index] else { return nil }
return bitrate
}
deinit {
debugPrint("deinit \(self)")
}
}
private extension String {
static var videoText: String {
localized("Video Settings")
}
static var audioText: String {
localized("Audio Settings")
}
static var otherText: String {
localized("Other Settings")
}
static var versionLowToastText: String {
localized("Your system version is below 12.0. Please update.")
}
static var resolutionText: String {
localized("Resolution")
}
static var frameRateText: String {
localized("Frame Rate")
}
static var bitrateText: String {
localized("Bitrate")
}
static var localMirrorText: String {
localized("Local Mirror")
}
static var captureVolumeText: String {
localized("Capture Volume")
}
static var playVolumeText: String {
localized("Playback Volume")
}
static var volumePromptText: String {
localized("Volume Reminder")
}
static var audioRecordingText: String {
localized("Audio Recording")
}
static var smoothResolutionText: String {
localized("Smooth")
}
static var standardResolutionText: String {
localized("Standard Definition")
}
static var highResolutionText: String {
localized("High Definition")
}
static var superResolutionText: String {
localized("Full High Definition")
}
static var qualityInspectionText: String {
localized("Network Quality Monitoring")
}
static var floatChatText: String {
localized("Floating Chat")
}
}

View File

@@ -0,0 +1,116 @@
//
// MemberInviteViewModel.swift
// TUIRoomKit
//
// Created by krabyu on 2023/8/21.
//
import Foundation
import RTCRoomEngine
protocol MemberInviteResponder : NSObjectProtocol {
func showCopyToast(copyType: CopyType?)
}
class MemberInviteViewModel {
var title: String = .inviteMembersToJoin
private(set) lazy var messageItems: [ListCellItemData] = {
return generateListData()
}()
private lazy var roomInfo = {
EngineManager.shared.store.roomInfo
}()
private lazy var conferenceInfoDetails = {
title
}()
private var roomLink: String? {
guard let bundleId = Bundle.main.bundleIdentifier else { return nil }
if bundleId == "com.tencent.tuiroom.apiexample" || bundleId == "com.tencent.fx.rtmpdemo" {
return "https://web.sdk.qcloud.com/trtc/webrtc/test/tuiroom-inner/index.html#/" + "room?roomId=" + roomInfo.roomId
} else if bundleId == "com.tencent.mrtc" {
return "https://web.sdk.qcloud.com/component/tuiroom/index.html#/" + "room?roomId=" + roomInfo.roomId
} else {
return nil
}
}
weak var viewResponder: MemberInviteResponder?
func createListCellItemData(titleText: String, messageText: String,
hasButton: Bool, copyType: CopyType?) -> ListCellItemData {
let item = ListCellItemData()
item.titleText = titleText
item.messageText = messageText
item.hasRightButton = hasButton
if hasButton {
let buttonData = ButtonItemData()
buttonData.normalIcon = "room_copy"
buttonData.normalTitle = .copyText
buttonData.cornerRadius = 4
buttonData.titleFont = UIFont(name: "PingFangSC-Regular", size: 12)
buttonData.titleColor = UIColor(0xB2BBD1)
buttonData.backgroundColor = UIColor(0x6B758A).withAlphaComponent(0.7)
buttonData.resourceBundle = tuiRoomKitBundle()
buttonData.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.copyAction(sender: button, text: item.messageText,copyType: copyType)
}
conferenceInfoDetails = conferenceInfoDetails + "\n\(titleText) : \(messageText)"
item.buttonData = buttonData
}
return item
}
func generateListData() -> [ListCellItemData] {
var array: [ListCellItemData] = []
let roomNametem = createListCellItemData(titleText: .roomName, messageText: roomInfo.name, hasButton: false, copyType: nil)
array.append(roomNametem)
let roomTypeItem = createListCellItemData(titleText: .roomType, messageText: roomInfo.isSeatEnabled ? .onStageSpeechRoom : .freeSpeechRoom, hasButton: false, copyType: nil)
array.append(roomTypeItem)
let roomIdItem = createListCellItemData(titleText: .roomIdText, messageText: roomInfo.roomId, hasButton: true, copyType: .copyRoomIdType)
array.append(roomIdItem)
if roomInfo.password.count > 0 {
let roomPasswordItem = createListCellItemData(titleText: .roomPassword, messageText: roomInfo.password, hasButton: true, copyType: .copyRoomPassword)
array.append(roomPasswordItem)
}
if let roomLink = roomLink {
let roomLinkItem = createListCellItemData(titleText: .roomLinkText, messageText: roomLink, hasButton: true, copyType: .copyRoomLinkType)
array.append(roomLinkItem)
}
return array
}
func copyAction(sender: UIButton, text: String, copyType: CopyType?) {
UIPasteboard.general.string = text
viewResponder?.showCopyToast(copyType: copyType)
}
func copyAction() {
UIPasteboard.general.string = conferenceInfoDetails
}
deinit {
debugPrint("deinit \(self)")
}
}
private extension String {
static var roomIdText: String {
localized("ConferenceID")
}
static var roomLinkText: String {
localized("Link")
}
static var copyText: String {
localized("Copy")
}
static var inviteMemberText: String {
localized("Invite member")
}
static let roomPassword = localized("Conference password")
static let conferencePasswordSuccess = localized("Conference password copied successfully.")
static let roomName = localized("Room name")
static let roomType = localized("Room type")
static let freeSpeechRoom = localized("Free Speech Room")
static let onStageSpeechRoom = localized("On-stage Speech Room")
static let inviteMembersToJoin = localized("Invite Others")
}

View File

@@ -0,0 +1,77 @@
//
// MoreFunctionViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/12.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import TUICore
class MoreFunctionViewModel {
private(set) var viewItems: [ButtonItemData] = []
var engineManager: EngineManager {
return EngineManager.shared
}
var engineEventCenter: EngineEventCenter {
return EngineEventCenter.shared
}
init() {
createBottomData()
}
func createBottomData() {
if hasTUIChatItem() {
let chatItem = ButtonItemData()
chatItem.normalIcon = "room_chat"
chatItem.normalTitle = .chatText
chatItem.resourceBundle = tuiRoomKitBundle()
chatItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.chatAction(sender: button)
}
viewItems.append(chatItem)
}
let settingItem = ButtonItemData()
settingItem.normalIcon = "room_setting"
settingItem.normalTitle = .settingText
settingItem.resourceBundle = tuiRoomKitBundle()
settingItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.settingAction(sender: button)
}
viewItems.append(settingItem)
}
private func hasTUIChatItem() -> Bool {
return TUICore.getService(TUICore_TUIChatService) != nil
}
func settingAction(sender: UIButton) {
sender.isSelected = !sender.isSelected
RoomRouter.shared.presentPopUpViewController(viewType: .mediaSettingViewType, height: 300.scale375())
}
func chatAction(sender: UIButton) {
RoomRouter.shared.dismissPopupViewController()
let user = engineManager.store.currentUser
let roomInfo = engineManager.store.roomInfo
RoomRouter.shared.pushToChatController(user: user, roomInfo: roomInfo)
}
deinit {
debugPrint("deinit \(self)")
}
}
private extension String {
static var settingText: String {
localized("Settings")
}
static var chatText: String {
localized("Chat")
}
}

View File

@@ -0,0 +1,53 @@
//
// PopUpViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/12.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import TUICore
enum PopUpViewType {
case roomInfoViewType
case moreViewType
case mediaSettingViewType
case userListViewType
case raiseHandApplicationListViewType
case transferMasterViewType
case QRCodeViewType
case chatViewType
case inviteViewType
case inviteMemberViewType
}
protocol PopUpViewModelResponder: AnyObject {
func searchControllerChangeActive(isActive: Bool)
func updateViewOrientation(isLandscape: Bool)
}
class PopUpViewModel {
let viewType: PopUpViewType
let height: CGFloat
var backgroundColor: UIColor?
weak var viewResponder: PopUpViewModelResponder?
init(viewType: PopUpViewType, height: CGFloat) {
self.viewType = viewType
self.height = height
}
func panelControlAction() {
changeSearchControllerActive()
RoomRouter.shared.dismissPopupViewController()
}
func changeSearchControllerActive() {
viewResponder?.searchControllerChangeActive(isActive: false)
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,69 @@
//
// QRCodeViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/11.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import PhotosUI
class QRCodeViewModel {
let urlString: String
var store: RoomStore {
EngineManager.shared.store
}
init(urlString: String) {
self.urlString = urlString
}
func copyAction(sender: UIButton, text: String) {
UIPasteboard.general.string = text
}
func saveIntoAlbumAction(sender: UIButton, image: UIImage) {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
func backAction() {
RoomRouter.shared.dismissPopupViewController()
}
func createQRCodeImageView(url: String, imageView: UIImageView) {
if url.count == 0 { return }
guard let filter = CIFilter(name: "CIQRCodeGenerator") else {return}
filter.setDefaults()
// Set filter input data
let data = url.data(using: String.Encoding.utf8)
filter.setValue(data, forKey: "inputMessage")
// Set the error correction rate of QR code
filter.setValue("M", forKey: "inputCorrectionLevel")
guard var image = filter.outputImage else { return }
let transform = CGAffineTransform(scaleX: 20, y: 20)
image = image.transformed(by: transform)
let resultImage = UIImage(ciImage: image)
guard let center = UIImage(named: "AppIcon.png") else { return }
guard let resultImage = getClearImage(sourceImage: resultImage, center: center) else { return }
imageView.image = resultImage
}
private func getClearImage(sourceImage: UIImage, center: UIImage) -> UIImage? {
let size = sourceImage.size
UIGraphicsBeginImageContext(size)
sourceImage.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
let width: CGFloat = 80
let height: CGFloat = 80
let x: CGFloat = (size.width - width) * 0.5
let y: CGFloat = (size.height - height) * 0.5
center.draw(in: CGRect(x: x, y: y, width: width, height: height))
guard let resultImage = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return resultImage
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,190 @@
//
// QualityInfoViewModel.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/4/19.
// Copyright © 2024 Tencent. All rights reserved.
//
import Foundation
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
protocol QualityViewResponder: AnyObject {
func reloadData()
}
class QualityCellModel: NSObject {
enum CellType {
case upDown
case normal
}
var titleText: String = ""
var type: CellType = .upDown
var uplinkString: String = ""
var downlinkString: String = ""
var normalString: String = ""
}
class QualitySectionModel: NSObject {
var titleText: String = ""
var items: [QualityCellModel] = []
}
class QualityInfoViewModel: NSObject {
static let timeSuffix = "ms"
static let lossSuffix = "%"
static let bitrateSuffix = "kbps"
static let framerateSuffix = "FPS"
var sections: [QualitySectionModel] = []
weak var viewResponder: QualityViewResponder? = nil
private var rttCellModel: QualityCellModel = {
var rttCellModel = QualityCellModel()
rttCellModel.titleText = .rttString
rttCellModel.normalString = "0" + timeSuffix
rttCellModel.type = .normal
return rttCellModel
}()
private var lossCellModel: QualityCellModel = {
var lossCellModel = QualityCellModel()
lossCellModel.titleText = .lossString
lossCellModel.uplinkString = "0" + lossSuffix
lossCellModel.downlinkString = "0" + lossSuffix
return lossCellModel
}()
private var audioBitrateCellModel: QualityCellModel = {
var bitrateCellModel = QualityCellModel()
bitrateCellModel.titleText = .bitrateString
bitrateCellModel.uplinkString = "0" + bitrateSuffix
bitrateCellModel.downlinkString = "0" + bitrateSuffix
return bitrateCellModel
}()
private var videoResCellModel: QualityCellModel = {
var resCellModel = QualityCellModel()
resCellModel.titleText = .resolutionString
resCellModel.uplinkString = "0x0"
resCellModel.downlinkString = "0x0"
return resCellModel
}()
private var videoFrameRateCellModel: QualityCellModel = {
var frameCellModel = QualityCellModel()
frameCellModel.titleText = .frameRateString
frameCellModel.uplinkString = "0" + framerateSuffix
frameCellModel.downlinkString = "0" + framerateSuffix
return frameCellModel
}()
private var videoBitrateCellModel: QualityCellModel = {
var bitrateCellModel = QualityCellModel()
bitrateCellModel.titleText = .bitrateString
bitrateCellModel.uplinkString = "0" + bitrateSuffix
bitrateCellModel.downlinkString = "0" + bitrateSuffix
return bitrateCellModel
}()
override init() {
super.init()
self.addSection(with: [self.rttCellModel, self.lossCellModel], title: .networkString)
self.addSection(with: [self.audioBitrateCellModel], title: .audioString)
self.addSection(with: [self.videoResCellModel, self.videoFrameRateCellModel, self.videoBitrateCellModel], title: .videoString)
subscribeEngine()
}
private func addSection(with items: [QualityCellModel], title: String) {
let section = QualitySectionModel()
section.titleText = title
section.items = items
self.sections.append(section)
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onStatistics, observer: self)
}
private func unsubscribeEngine() {
EngineEventCenter.shared.unsubscribeEngine(event: .onStatistics, observer: self)
}
deinit {
unsubscribeEngine()
}
}
extension QualityInfoViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onStatistics:
guard let data = param?["statistics"] as? TRTCStatistics else { return }
handleStatistics(data: data)
default:
break
}
}
private func handleStatistics(data: TRTCStatistics) {
let localStatistics = data.localStatistics.first(where: { $0.streamType == .big })
let remoteStatistics = data.remoteStatistics ?? []
self.rttCellModel.normalString = String(data.rtt) + QualityInfoViewModel.timeSuffix
// assemble uplink data
self.lossCellModel.uplinkString = String(data.upLoss) + QualityInfoViewModel.lossSuffix
self.audioBitrateCellModel.uplinkString = String(localStatistics?.audioBitrate ?? 0) + QualityInfoViewModel.bitrateSuffix
self.videoResCellModel.uplinkString = String(localStatistics?.width ?? 0) + "x" + String(localStatistics?.height ?? 0)
self.videoFrameRateCellModel.uplinkString = String(localStatistics?.frameRate ?? 0) + QualityInfoViewModel.framerateSuffix
self.videoBitrateCellModel.uplinkString = String(localStatistics?.videoBitrate ?? 0) + QualityInfoViewModel.bitrateSuffix
// assemble downlink data
let remoteSumAudioBitrate = remoteStatistics.reduce(0) { sum, stream in
return sum + stream.audioBitrate
}
let remoteMaxFramerate = remoteStatistics.max(by: {$0.frameRate < $1.frameRate})
let remoteMaxVideoRes = remoteStatistics.max(by: { $0.width * $0.height < $1.width * $1.height })
let remoteSumVideoBitrate = remoteStatistics.reduce(0) { sum, stream in
return sum + stream.videoBitrate
}
self.lossCellModel.downlinkString = String(data.downLoss) + QualityInfoViewModel.lossSuffix
self.audioBitrateCellModel.downlinkString = String(remoteSumAudioBitrate) + QualityInfoViewModel.bitrateSuffix
self.videoResCellModel.downlinkString = String(remoteMaxVideoRes?.width ?? 0) + "x" + String(remoteMaxVideoRes?.height ?? 0)
self.videoFrameRateCellModel.downlinkString = String(remoteMaxFramerate?.frameRate ?? 0) + QualityInfoViewModel.framerateSuffix
self.videoBitrateCellModel.downlinkString = String(remoteSumVideoBitrate) + QualityInfoViewModel.bitrateSuffix
self.viewResponder?.reloadData()
}
}
private extension String {
static var networkString: String {
localized("Network")
}
static var audioString: String {
localized("Audio")
}
static var videoString: String {
localized("Video")
}
static var rttString: String {
localized("Latency")
}
static var lossString: String {
localized("Packet Loss Rate")
}
static var bitrateString: String {
localized("Bitrate")
}
static var resolutionString: String {
localized("Resolution")
}
static var frameRateString: String {
localized("Frame Rate")
}
}

View File

@@ -0,0 +1,96 @@
//
// RaiseHandApplicationListViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/13.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
protocol RaiseHandApplicationListViewResponder: NSObject {
func reloadApplyListView()
func makeToast(text: String)
func updatePlaceholderViewState(isShown: Bool)
func updateApplyButtonState(isEnabled: Bool)
}
class RaiseHandApplicationListViewModel: NSObject {
weak var viewResponder: RaiseHandApplicationListViewResponder? = nil
var engineManager: EngineManager {
EngineManager.shared
}
var roomInfo: TUIRoomInfo {
engineManager.store.roomInfo
}
var inviteSeatList: [RequestEntity] = []
var isPlaceholderViewShown: Bool {
return inviteSeatList.isEmpty
}
var isApplyButtonEnabled: Bool {
return !inviteSeatList.isEmpty
}
override init() {
super.init()
inviteSeatList = engineManager.store.inviteSeatList
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
}
func respondAllRequest(isAgree: Bool) {
var isShownStageFullToast = false
for requestEntity in engineManager.store.inviteSeatList {
engineManager.responseRemoteRequest(requestEntity.requestId, agree: isAgree) { [weak self] in
guard let self = self else { return }
self.engineManager.store.deleteTakeSeatRequest(requestId: requestEntity.requestId)
} onError: { [weak self] code, message in
guard let self = self else { return }
guard code == .allSeatOccupied, !isShownStageFullToast else { return }
self.viewResponder?.makeToast(text: .onStageNumberReachedLimitText)
isShownStageFullToast = true
}
}
}
func respondRequest(isAgree: Bool, request: RequestEntity) {
engineManager.responseRemoteRequest(request.requestId, agree: isAgree) { [weak self] in
guard let self = self else { return }
self.engineManager.store.deleteTakeSeatRequest(requestId: request.requestId)
self.reloadApplyListView()
} onError: { [weak self] code, message in
guard let self = self else { return }
guard code == .allSeatOccupied else { return }
self.viewResponder?.makeToast(text: .onStageNumberReachedLimitText)
}
}
func reloadApplyListView() {
inviteSeatList = engineManager.store.inviteSeatList
viewResponder?.updatePlaceholderViewState(isShown: isPlaceholderViewShown)
viewResponder?.updateApplyButtonState(isEnabled: isApplyButtonEnabled)
viewResponder?.reloadApplyListView()
}
deinit {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
debugPrint("deinit \(self)")
}
}
extension RaiseHandApplicationListViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
if key == .TUIRoomKitService_RenewSeatList {
self.reloadApplyListView()
}
}
}
private extension String {
static var onStageNumberReachedLimitText: String {
localized("The stage is full")
}
}

View File

@@ -0,0 +1,104 @@
//
// RaiseHandApplicationNotificationViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2024/5/16.
//
import Foundation
import RTCRoomEngine
protocol RaiseHandApplicationNotificationViewModelResponder: AnyObject {
func showRaiseHandApplicationNotificationView(userId: String, userName: String, count: Int)
func hideRaiseHandApplicationNotificationView()
}
class RaiseHandApplicationNotificationViewModel: NSObject {
var delayDisappearanceTime = 5.0
lazy var userId: String? = {
return inviteSeatList.last?.userId
}()
lazy var userName: String? = {
return inviteSeatList.last?.userName
}()
lazy var applicationCount: Int? = {
return inviteSeatList.count
}()
weak var responder: RaiseHandApplicationNotificationViewModelResponder?
var inviteSeatList: [RequestEntity] {
EngineManager.shared.store.inviteSeatList
}
lazy var isShownRaiseHandApplicationNotificationView: Bool = {
return getShownRequestEntity() != nil
}()
override init() {
super.init()
subscribeEngine()
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onRequestReceived, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onDeletedTakeSeatRequest, observer: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
}
private func unsubscribeEngine() {
EngineEventCenter.shared.unsubscribeEngine(event: .onRequestReceived, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onDeletedTakeSeatRequest, observer: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
}
private func getShownRequestEntity() -> RequestEntity? {
let currentTime = Date().timeIntervalSince1970
guard let lastItem = inviteSeatList.last else { return nil }
if delayDisappearanceTime > 0, currentTime - lastItem.timestamp > delayDisappearanceTime {
return nil
} else {
return lastItem
}
}
func checkRaiseHandApplicationAction() {
RoomRouter.shared.presentPopUpViewController(viewType: .raiseHandApplicationListViewType, height: 720.scale375Height(), backgroundColor: UIColor(0x22262E))
}
deinit {
unsubscribeEngine()
}
}
extension RaiseHandApplicationNotificationViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onRequestReceived:
guard let request = param?["request"] as? TUIRequest else { return }
guard request.requestAction == .takeSeat else { return }
self.userId = request.userId
self.userName = request.userName
responder?.showRaiseHandApplicationNotificationView(userId: request.userId, userName: request.userName, count: inviteSeatList.count)
case .onDeletedTakeSeatRequest:
guard let userId = param?["userId"] as? String else { return }
guard userId == self.userId else { return }
let requestItem = getShownRequestEntity()
self.userId = requestItem?.userId
self.userName = requestItem?.userName
if let requestItem = requestItem {
responder?.showRaiseHandApplicationNotificationView(userId: requestItem.userId, userName: requestItem.userName, count: inviteSeatList.count)
} else {
responder?.hideRaiseHandApplicationNotificationView()
}
default: break
}
}
}
extension RaiseHandApplicationNotificationViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
if key == .TUIRoomKitService_RenewSeatList {
guard let requestItem = getShownRequestEntity() else { return }
responder?.showRaiseHandApplicationNotificationView(userId: requestItem.userId, userName: requestItem.userName, count: inviteSeatList.count)
}
}
}

View File

@@ -0,0 +1,163 @@
//
// RoomInfoViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/3.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
enum CopyType {
case copyRoomIdType
case copyRoomLinkType
case copyRoomPassword
}
protocol RoomInfoResponder : NSObjectProtocol {
func showCopyToast(copyType: CopyType?)
func updateNameLabel(_ text: String)
}
class RoomInfoViewModel: NSObject {
private(set) var messageItems: [ListCellItemData] = []
var store: RoomStore {
EngineManager.shared.store
}
var roomInfo: TUIRoomInfo {
store.roomInfo
}
lazy var title = {
roomInfo.name
}()
weak var viewResponder: RoomInfoResponder?
var roomLink: String? {
guard let bundleId = Bundle.main.bundleIdentifier else { return nil }
if bundleId == "com.tencent.tuiroom.apiexample" || bundleId == "com.tencent.fx.rtmpdemo" {
return "https://web.sdk.qcloud.com/trtc/webrtc/test/tuiroom-inner/index.html#/" + "room?roomId=" + roomInfo.roomId
} else if bundleId == "com.tencent.mrtc" {
return "https://web.sdk.qcloud.com/component/tuiroom/index.html#/" + "room?roomId=" + roomInfo.roomId
} else {
return nil
}
}
private lazy var conferenceDetails = {
title
}()
override init() {
super.init()
subscribeEngine()
createSourceData()
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onConferenceInfoChanged, observer: self)
}
private func unsubscribeUIEvent() {
EngineEventCenter.shared.unsubscribeEngine(event: .onConferenceInfoChanged, observer: self)
}
func createListCellItemData(titleText: String, messageText: String,
hasButton: Bool, copyType: CopyType?) -> ListCellItemData {
let item = ListCellItemData()
item.titleText = titleText
item.messageText = messageText
item.hasRightButton = hasButton
if item.hasRightButton {
let buttonData = ButtonItemData()
buttonData.normalIcon = "room_copy"
buttonData.normalTitle = .copyText
buttonData.cornerRadius = 4
buttonData.titleFont = UIFont(name: "PingFangSC-Regular", size: 12)
buttonData.titleColor = UIColor(0xB2BBD1)
buttonData.backgroundColor = UIColor(0x6B758A).withAlphaComponent(0.7)
buttonData.resourceBundle = tuiRoomKitBundle()
buttonData.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.copyAction(sender: button, text: item.messageText,copyType: copyType)
}
item.buttonData = buttonData
}
conferenceDetails = conferenceDetails + "\n\(titleText) : \(messageText)"
return item
}
func createSourceData() {
var userName = roomInfo.ownerId
if let userModel = store.attendeeList.first(where: { $0.userId == roomInfo.ownerId}) {
userName = userModel.userName
}
let roomHostItem = createListCellItemData(titleText: .roomHostText, messageText: userName, hasButton: false, copyType: nil)
messageItems.append(roomHostItem)
let roomTypeItem = createListCellItemData(titleText: .roomTypeText, messageText: roomInfo.isSeatEnabled ? .raiseHandSpeakText: .freedomSpeakText, hasButton: false, copyType: nil)
messageItems.append(roomTypeItem)
let roomIdItem = createListCellItemData(titleText: .roomIdText, messageText: roomInfo.roomId, hasButton: true, copyType: .copyRoomIdType)
messageItems.append(roomIdItem)
if roomInfo.password.count > 0 {
let passwordItem = createListCellItemData(titleText: .conferencePasswordText, messageText: roomInfo.password, hasButton: true, copyType: .copyRoomPassword)
messageItems.append(passwordItem)
}
if let roomLink = roomLink {
let roomLinkItem = createListCellItemData(titleText: .roomLinkText, messageText: roomLink, hasButton: true, copyType: .copyRoomLinkType)
messageItems.append(roomLinkItem)
}
}
func copyAction(sender: UIButton, text: String, copyType: CopyType?){
UIPasteboard.general.string = text
viewResponder?.showCopyToast(copyType: copyType)
}
func codeAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .QRCodeViewType, height: 720.scale375Height())
}
func copyConferenceDetails() {
UIPasteboard.general.string = conferenceDetails
}
deinit {
unsubscribeUIEvent()
debugPrint("deinit \(self)")
}
}
extension RoomInfoViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onConferenceInfoChanged:
guard let conferenceInfo = param?["conferenceInfo"] as? TUIConferenceInfo else { return }
guard let modifyFlag = param?["modifyFlag"] as? TUIConferenceModifyFlag else { return }
guard modifyFlag.contains(.roomName) else { return }
viewResponder?.updateNameLabel(conferenceInfo.basicRoomInfo.name)
default: break
}
}
}
private extension String {
static var freedomSpeakText: String {
localized("Free Speech Conference")
}
static var raiseHandSpeakText: String {
localized("On-stage Speaking Conference")
}
static var roomHostText: String {
localized("Host")
}
static var roomTypeText: String {
localized("Conference Type")
}
static var roomIdText: String {
localized("ConferenceID")
}
static var roomLinkText: String {
localized("Link")
}
static var copyText: String {
localized("Copy")
}
static let conferencePasswordText = localized("Conference password")
}

View File

@@ -0,0 +1,212 @@
//
// RoomVideoFloatViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/7/11.
//
import Foundation
import RTCRoomEngine
protocol RoomVideoFloatViewResponder: NSObject {
func updateUserStatus(user: UserEntity)
func updateUserAudioVolume(hasAudio: Bool, volume: Int)
func makeToast(text: String)
func showAvatarImageView(isShow: Bool)
}
class RoomVideoFloatViewModel: NSObject {
var userId: String = ""
var streamType: TUIVideoStreamType = .cameraStream
weak var renderView: UIView?
weak var viewResponder: RoomVideoFloatViewResponder?
var engineManager: EngineManager {
EngineManager.shared
}
var roomInfo: TUIRoomInfo {
engineManager.store.roomInfo
}
var currentUser: UserEntity {
engineManager.store.currentUser
}
override init() {
super.init()
subscribeEngine()
subLogoutNotification()
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onUserVideoStateChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRoomDismissed, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOutOfRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserAudioStateChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOffLine, observer: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RoomOwnerChanged, responder: self)
}
private func unsubscribeEngine() {
EngineEventCenter.shared.unsubscribeEngine(event: .onUserVideoStateChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRoomDismissed, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOutOfRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserAudioStateChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOffLine, observer: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RoomOwnerChanged, responder: self)
}
private func subLogoutNotification() {
NotificationCenter.default.addObserver(self,
selector: #selector(dismissFloatViewForLogout),
name: NSNotification.Name.TUILogoutSuccess, object: nil)
}
private func unsubLogoutNotification() {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.TUILogoutSuccess, object: nil)
}
func showRoomMainView() {
if engineManager.store.isEnteredRoom {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ShowRoomMainView, param: [:])
}
}
func showFloatWindowViewVideo(renderView: UIView?) {
self.renderView = renderView
if let userModel = getScreenUserModel() { //If someone is screen sharing, show the screen share first
showScreenStream(userModel: userModel)
} else { //Show host without screen sharing
showCameraStream()
}
}
func getUserEntity(userId: String) -> UserEntity? {
return engineManager.store.attendeeList.first(where: { $0.userId == userId })
}
@objc private func dismissFloatViewForLogout() {
RoomVideoFloatView.dismiss()
}
deinit {
unsubscribeEngine()
unsubLogoutNotification()
debugPrint("deinit \(self)")
}
}
extension RoomVideoFloatViewModel {
private func getScreenUserModel() -> UserEntity? {
return engineManager.store.attendeeList.first(where: { $0.hasScreenStream == true })
}
private func showScreenStream(userModel: UserEntity) {
let streamType: TUIVideoStreamType = userModel.userId == currentUser.userId ? .cameraStream : .screenStream
startPlayVideo(userId: userModel.userId, streamType: streamType)
changePlayingState(userId: userModel.userId, streamType: streamType)
viewResponder?.updateUserStatus(user: userModel)
viewResponder?.showAvatarImageView(isShow: false)
}
private func showCameraStream() {
guard let userModel = getUserEntity(userId: roomInfo.ownerId) ?? getUserEntity(userId: currentUser.userId) else { return }
changePlayingState(userId: userModel.userId, streamType: .cameraStream)
viewResponder?.updateUserStatus(user: userModel)
if userModel.hasVideoStream {
startPlayVideo(userId: userModel.userId, streamType: .cameraStream)
} else {
viewResponder?.showAvatarImageView(isShow: true)
}
}
private func startPlayVideo(userId: String, streamType: TUIVideoStreamType) {
if userId == currentUser.userId {
engineManager.setLocalVideoView(streamType: streamType, view: renderView)
} else {
engineManager.setRemoteVideoView(userId: userId, streamType: streamType, view: renderView)
engineManager.startPlayRemoteVideo(userId: userId, streamType: streamType)
}
viewResponder?.showAvatarImageView(isShow: false)
}
private func stopPlayVideo(userId: String, streamType: TUIVideoStreamType) {
if userId == currentUser.userId {
engineManager.setLocalVideoView(streamType: streamType, view: nil)
return
}
engineManager.setRemoteVideoView(userId: userId, streamType: streamType, view: nil)
guard let userItem = getUserEntity(userId: userId) else { return }
if streamType == .screenStream, userItem.hasScreenStream {
engineManager.stopPlayRemoteVideo(userId: userId, streamType: .screenStream)
} else if streamType == .cameraStream, userItem.hasVideoStream {
engineManager.stopPlayRemoteVideo(userId: userId, streamType: .cameraStream)
}
}
private func changePlayingState(userId: String, streamType: TUIVideoStreamType) {
self.userId = userId
self.streamType = streamType
}
}
extension RoomVideoFloatViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onKickedOutOfRoom, .onRoomDismissed:
engineManager.destroyEngineManager()
RoomVideoFloatView.dismiss()
case .onUserVideoStateChanged:
guard let userId = param?["userId"] as? String else { return }
guard let streamType = param?["streamType"] as? TUIVideoStreamType else { return }
guard let hasVideo = param?["hasVideo"] as? Bool else { return }
if streamType == .screenStream {
if hasVideo {
stopPlayVideo(userId: roomInfo.ownerId, streamType: .cameraStream)
guard let userModel = getUserEntity(userId: userId) else { return }
showScreenStream(userModel: userModel)
} else {
stopPlayVideo(userId: self.userId, streamType: .screenStream)
showCameraStream()
}
return
}
guard getScreenUserModel() == nil else { return } //If someone is screen sharing, don't show the host screen
guard userId == roomInfo.ownerId else { return }
if hasVideo {
startPlayVideo(userId: userId, streamType: streamType)
} else {
viewResponder?.showAvatarImageView(isShow: true)
}
case .onUserAudioStateChanged:
guard let userId = param?["userId"] as? String else { return }
guard let hasAudio = param?["hasAudio"] as? Bool else { return }
guard userId == self.userId else { return }
var volume = 0
if let userModel = getUserEntity(userId: self.userId) {
volume = userModel.userVoiceVolume
}
viewResponder?.updateUserAudioVolume(hasAudio: hasAudio, volume: volume)
case .onUserVoiceVolumeChanged:
guard let volumeNumber = param?[self.userId] as? NSNumber else { return }
guard let userModel = getUserEntity(userId: self.userId) else { return }
viewResponder?.updateUserAudioVolume(hasAudio: userModel.hasAudioStream, volume: volumeNumber.intValue)
case .onKickedOffLine:
RoomVideoFloatView.dismiss()
default: break
}
}
}
extension RoomVideoFloatViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_RoomOwnerChanged:
guard getScreenUserModel() == nil else { return } //If someone is screen sharing, don't show the host screen
stopPlayVideo(userId: self.userId, streamType: .cameraStream)
showCameraStream()
default: break
}
}
}

View File

@@ -0,0 +1,586 @@
//
// TUIVideoSeatPresenter.swift
// TUIVideoSeat
//
// Created by WesleyLei on 2022/9/28.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
protocol TUIVideoSeatViewModelResponder: AnyObject {
func reloadData()
func insertItems(at indexPaths: [IndexPath])
func deleteItems(at indexPaths: [IndexPath])
func getVideoVisibleCell(_ item: VideoSeatItem) -> VideoSeatCell?
func getMoveMiniscreen() -> TUIVideoSeatDragCell
func updateMiniscreen(_ item: VideoSeatItem?)
func updateMiniscreenVolume(_ item: VideoSeatItem)
func updateVideoSeatCellUI(_ item: VideoSeatItem)
func updateSeatVolume(_ item: VideoSeatItem)
func showScreenCaptureMaskView(isShow: Bool)
func destroyVideoSeatResponder()
}
enum TUIVideoSeatViewType {
case unknown
case singleType
case pureAudioType
case largeSmallWindowType
case speechType
case equallyDividedType
}
class TUIVideoSeatViewModel: NSObject {
private var videoSeatItems: [VideoSeatItem] = []
private var shareItem: VideoSeatItem?
private var speakerItem: VideoSeatItem?
private var isSwitchPosition: Bool = false
private var speakerUpdateTimer: Int = 0
private let speakerUpdateTimeInterval = 5
private var itemStreamType: TUIVideoStreamType {
if listSeatItem.filter({ $0.hasVideoStream }).count > 5 {
return .cameraStreamLow
} else {
return .cameraStream
}
}
var listSeatItem: [VideoSeatItem] = []
private var isHasVideoStream: Bool {
return videoSeatItems.firstIndex(where: { $0.isHasVideoStream }) != nil
}
private var isHasScreenStream: Bool {
return shareItem != nil
}
weak var viewResponder: TUIVideoSeatViewModelResponder?
var videoSeatViewType: TUIVideoSeatViewType = .unknown
var engineManager: EngineManager {
EngineManager.shared
}
var store: RoomStore {
engineManager.store
}
var roomInfo: TUIRoomInfo {
store.roomInfo
}
var currentUserId: String {
store.currentUser.userId
}
override init() {
super.init()
initVideoSeatItems()
subscribeUIEvent()
}
private func initVideoSeatItems() {
videoSeatItems = []
let videoItems = store.roomInfo.isSeatEnabled ? store.seatList : store.attendeeList
guard videoItems.count > 0 else { return }
videoItems.forEach { userInfo in
let userItem = VideoSeatItem()
userItem.update(userInfo: userInfo)
videoSeatItems.append(userItem)
}
if let shareInfo = videoItems.first(where: { $0.hasScreenStream }) {
updateShareItem(userInfo: shareInfo)
}
sortSeatItems()
reloadSeatItems()
}
private func subscribeUIEvent() {
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, responder: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserAudioStateChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserVideoStateChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserScreenCaptureStopped, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRemoteUserEnterRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRemoteUserLeaveRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserRoleChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onSeatListChanged, observer: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
}
private func unsubscribeUIEvent() {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, responder: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserAudioStateChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserVideoStateChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserScreenCaptureStopped, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRemoteUserEnterRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRemoteUserLeaveRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserRoleChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onSeatListChanged, observer: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
}
func updateShareItem(userInfo: UserEntity) {
guard userInfo.hasScreenStream, shareItem == nil else { return }
let item = VideoSeatItem()
item.update(userInfo: userInfo)
item.videoStreamType = .screenStream
shareItem = item
}
deinit {
unsubscribeUIEvent()
debugPrint("deinit:\(self)")
}
}
extension TUIVideoSeatViewModel {
private func startPlayVideo(item: VideoSeatItem, renderView: UIView?) {
guard let renderView = renderView else { return }
if item.userId == currentUserId {
engineManager.setLocalVideoView(streamType: item.videoStreamType, view: renderView)
} else {
item.videoStreamType = item.videoStreamType == .screenStream ? .screenStream : itemStreamType
engineManager.setRemoteVideoView(userId: item.userId, streamType: item.videoStreamType, view: renderView)
engineManager.startPlayRemoteVideo(userId: item.userId, streamType: item.videoStreamType)
}
guard let seatCell = viewResponder?.getVideoVisibleCell(item) else { return }
seatCell.updateUI(item: item)
}
private func stopPlayVideo(item: VideoSeatItem) {
if item.userId == currentUserId {
engineManager.setLocalVideoView(streamType: item.videoStreamType, view: nil)
} else {
engineManager.setRemoteVideoView(userId: item.userId, streamType: item.videoStreamType, view: nil)
engineManager.stopPlayRemoteVideo(userId: item.userId, streamType: item.videoStreamType)
}
guard let seatCell = viewResponder?.getVideoVisibleCell(item) else { return }
seatCell.updateUI(item: item)
}
private func addUserInfo(_ userId: String) {
guard !videoSeatItems.contains(where: { $0.userId == userId }) else { return }
guard let userInfo = getUserInfo(userId: userId) else { return }
let seatItem = VideoSeatItem()
seatItem.update(userInfo: userInfo)
videoSeatItems.append(seatItem)
if checkNeededSort() {
refreshListSeatItem()
viewResponder?.reloadData()
resetMiniscreen()
} else {
reloadSeatItems()
}
}
private func getUserInfo(userId: String) -> UserEntity? {
return store.attendeeList.first(where: { $0.userId == userId })
}
private func removeSeatItem(_ userId: String) {
if shareItem?.userId == userId, let seatItem = shareItem {
stopPlayVideo(item: seatItem)
}
if speakerItem?.userId == userId, let seatItem = speakerItem {
stopPlayVideo(item: seatItem)
}
if let seatItem = videoSeatItems.first(where: { $0.userId == userId }) {
stopPlayVideo(item: seatItem)
}
videoSeatItems.removeAll(where: { $0.userId == userId })
var deleteIndex: [IndexPath] = []
if let index = listSeatItem.firstIndex(where: { $0.userId == userId && $0.videoStreamType != .screenStream }) {
deleteIndex.append(IndexPath(item: index, section: 0))
}
refreshListSeatItem()
if videoSeatViewType == .largeSmallWindowType {
viewResponder?.reloadData()
} else {
viewResponder?.deleteItems(at: deleteIndex)
}
resetMiniscreen()
}
private func changeUserRole(userId: String, userRole: TUIRole) {
if let item = getSeatItem(userId) {
item.userRole = userRole
viewResponder?.updateVideoSeatCellUI(item)
}
if let shareItem = shareItem, shareItem.userId == userId {
shareItem.userRole = userRole
viewResponder?.updateVideoSeatCellUI(shareItem)
}
if let speakerItem = speakerItem, speakerItem.userId == userId {
speakerItem.userRole = userRole
viewResponder?.updateVideoSeatCellUI(speakerItem)
}
guard userRole == .roomOwner else { return }
refreshListSeatItem()
viewResponder?.reloadData()
resetMiniscreen()
}
private func getSeatItem(_ userId: String) -> VideoSeatItem? {
return videoSeatItems.first(where: { $0.userId == userId })
}
private func sortSeatItems() {
guard checkNeededSort() else { return }
// I'm second
if let currentItemIndex = videoSeatItems.firstIndex(where: { $0.userId == self.currentUserId }) {
let currentItem = videoSeatItems.remove(at: currentItemIndex)
videoSeatItems.insert(currentItem, at: 0)
}
// Homeowners always come first
if let roomOwnerItemIndex = videoSeatItems.firstIndex(where: { $0.userId == roomInfo.ownerId }) {
let roomOwnerItem = videoSeatItems.remove(at: roomOwnerItemIndex)
videoSeatItems.insert(roomOwnerItem, at: 0)
}
}
private func checkNeededSort() -> Bool {
var isSort = false
if let roomOwnerItemIndex = videoSeatItems.firstIndex(where: { $0.userId == roomInfo.ownerId }) {
isSort = roomOwnerItemIndex != 0
}
if let currentItemIndex = videoSeatItems.firstIndex(where: { $0.userId == self.currentUserId }) {
if currentUserId == roomInfo.ownerId {
isSort = isSort || (currentItemIndex != 0)
} else {
isSort = isSort || (currentItemIndex != 1)
}
}
return isSort
}
private func findCurrentSpeaker(list: [VideoSeatItem]) -> VideoSeatItem? {
if let shareItem = shareItem {
return list.first(where: { $0.hasAudioStream && $0.userVoiceVolume > 10 && $0.userId != shareItem.userId })
} else if let speech = listSeatItem.first {
return list.first(where:{ $0.hasAudioStream && $0.userVoiceVolume > 10 && $0.userId != speech.userId })
}
return nil
}
private func refreshListSeatItem() {
sortSeatItems()
listSeatItem = Array(videoSeatItems)
if videoSeatItems.count == 1 {
videoSeatViewType = .singleType
if isHasScreenStream {
refreshMultiVideo()
}
} else if videoSeatItems.count == 2, isHasVideoStream, !isHasScreenStream {
videoSeatViewType = .largeSmallWindowType
if isSwitchPosition {
let first = listSeatItem[0]
listSeatItem[0] = listSeatItem[1]
listSeatItem[1] = first
}
} else if videoSeatItems.count >= 2, !isHasVideoStream, !isHasScreenStream {
videoSeatViewType = .pureAudioType
} else {
refreshMultiVideo()
}
}
private func refreshMultiVideo() {
let videoResult = videoSeatItems.filter({ $0.hasVideoStream })
var speechItem: VideoSeatItem?
if let item = shareItem {
speechItem = item
} else if videoResult.count == 1, let item = videoResult.first {
speechItem = item
}
if let item = speechItem, let seatItemIndex = videoSeatItems.firstIndex(where: { $0.userId == item.userId }) {
videoSeatViewType = .speechType
if item.videoStreamType == .screenStream, item.userId != currentUserId {
listSeatItem.insert(item, at: 0)
} else {
listSeatItem.remove(at: seatItemIndex)
listSeatItem.insert(item, at: 0)
if item.userId == speakerItem?.userId {
speakerItem = nil
}
}
if let currentSpeakerItem = findCurrentSpeaker(list: listSeatItem) {
speakerItem = currentSpeakerItem
} else {
if let item = speakerItem, videoSeatItems.firstIndex(where: { $0.userId == item.userId }) == nil {
speakerItem = nil
}
}
} else {
videoSeatViewType = .equallyDividedType
}
}
private func reloadSeatItems() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let type = self.videoSeatViewType
let lastListSeatItem = Array(self.listSeatItem)
self.refreshListSeatItem()
self.updateCollectionView(type, lastListSeatItem)
self.resetMiniscreen()
}
}
private func resetMiniscreen() {
if self.videoSeatViewType == .speechType {
self.viewResponder?.updateMiniscreen(self.speakerItem)
} else {
self.speakerItem = nil
self.viewResponder?.updateMiniscreen(nil)
}
}
private func updateSeatVolume(item: VideoSeatItem) {
viewResponder?.updateSeatVolume(item)
if let shareItem = shareItem, shareItem.userId == item.userId {
shareItem.hasAudioStream = item.hasAudioStream
shareItem.userVoiceVolume = item.userVoiceVolume
viewResponder?.updateSeatVolume(shareItem)
}
if let speakerItem = speakerItem, speakerItem.userId == item.userId {
speakerItem.hasAudioStream = item.hasAudioStream
speakerItem.userVoiceVolume = item.userVoiceVolume
viewResponder?.updateMiniscreenVolume(speakerItem)
}
}
private func updateCollectionView(_ type: TUIVideoSeatViewType, _ lastList: [VideoSeatItem]) {
if type != videoSeatViewType {
viewResponder?.reloadData()
} else {
let count = lastList.count
let diffItem = listSeatItem.count - count
var indexPaths: [IndexPath] = []
if diffItem > 0 {
for i in count ... (count + diffItem - 1) {
indexPaths.append(IndexPath(item: i, section: 0))
}
viewResponder?.insertItems(at: indexPaths)
}
for i in 0 ... min(max(count - 1, 0), max(listSeatItem.count - 1, 0)) {
guard lastList.count > i && listSeatItem.count > i && lastList[i] != listSeatItem[i] else { continue }
guard let item = listSeatItem[safe: i] else { continue }
viewResponder?.updateVideoSeatCellUI(item)
guard let cell = viewResponder?.getVideoVisibleCell(item) else { continue }
if item.hasVideoStream {
startPlayVideo(item: item, renderView: cell.renderView)
} else {
stopPlayVideo(item: item)
}
}
}
}
private func isConformedSpeakerTimeInterval() -> Bool {
let currentTime: TimeInterval = Date().timeIntervalSince1970
let timeStamp = Int(currentTime)
let totalTime: UInt = UInt(labs(timeStamp - speakerUpdateTimer))
return totalTime > speakerUpdateTimeInterval
}
}
extension TUIVideoSeatViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_RenewVideoSeatView:
initVideoSeatItems()
case .TUIRoomKitService_DismissConferenceViewController:
viewResponder?.destroyVideoSeatResponder()
viewResponder = nil
default: break
}
}
}
extension TUIVideoSeatViewModel: 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 let seatItem = getSeatItem(userId) else { return }
seatItem.hasAudioStream = hasAudio
updateSeatVolume(item: seatItem)
case .onUserVoiceVolumeChanged:
guard let volumeMap = param as? [String: NSNumber] else { return }
userVoiceVolumeChanged(volumeMap: volumeMap)
case .onUserVideoStateChanged:
guard let userId = param?["userId"] as? String else { return }
guard let streamType = param?["streamType"] as? TUIVideoStreamType else { return }
guard let hasVideo = param?["hasVideo"] as? Bool else { return }
userVideoStateChanged(userId: userId, streamType: streamType, hasVideo: hasVideo)
case .onUserScreenCaptureStopped:
userScreenCaptureStopped()
case .onRemoteUserEnterRoom:
guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
guard !roomInfo.isSeatEnabled else { return }
addUserInfo(userInfo.userId)
case .onRemoteUserLeaveRoom:
guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
removeSeatItem(userInfo.userId)
case .onUserRoleChanged:
guard let userId = param?["userId"] as? String else { return }
guard let userRole = param?["userRole"] as? TUIRole else { return }
engineManager.fetchRoomInfo(roomId: roomInfo.roomId) { [weak self] _ in
guard let self = self else { return }
self.changeUserRole(userId: userId, userRole: userRole)
}
case .onSeatListChanged:
guard let left = param?["left"] as? [TUISeatInfo] else { return }
guard let seated = param?["seated"] as? [TUISeatInfo] else { return }
seatListChanged(seated: seated, left: left)
default: break
}
}
}
extension TUIVideoSeatViewModel: TUIRoomObserver {
private func userVoiceVolumeChanged(volumeMap: [String: NSNumber]) {
if volumeMap.count <= 0 {
return
}
for (userId, volume) in volumeMap {
guard let seatItem = getSeatItem(userId) else { continue }
seatItem.userVoiceVolume = volume.intValue
updateSeatVolume(item: seatItem)
}
guard videoSeatViewType == .speechType else { return }
guard let currentSpeakerItem = findCurrentSpeaker(list: listSeatItem) else { return }
if viewResponder?.getMoveMiniscreen().seatItem != nil, speakerItem?.userId == currentSpeakerItem.userId {
viewResponder?.updateMiniscreenVolume(currentSpeakerItem)
} else {
guard isConformedSpeakerTimeInterval() else { return }
viewResponder?.updateMiniscreen(currentSpeakerItem)
speakerUpdateTimer = Int(Date().timeIntervalSince1970)
}
speakerItem = currentSpeakerItem
}
private func userVideoStateChanged(userId: String, streamType: TUIVideoStreamType, hasVideo: Bool) {
if streamType == .screenStream, userId == currentUserId {
viewResponder?.showScreenCaptureMaskView(isShow: hasVideo)
return
}
guard let seatItem = getSeatItem(userId) else { return }
if streamType == .cameraStream || streamType == .cameraStreamLow {
seatItem.hasVideoStream = hasVideo
if hasVideo {
setRemoteRenderParams(userId: userId, streamType: streamType)
startPlayVideo(item: seatItem, renderView: viewResponder?.getVideoVisibleCell(seatItem)?.renderView)
} else {
stopPlayVideo(item: seatItem)
}
reloadSeatItems()
} else {
updateScreenStreamView(seatItem: seatItem, hasVideo: hasVideo)
}
}
private func updateScreenStreamView(seatItem: VideoSeatItem, hasVideo: Bool) {
let screenIndexPath = IndexPath(item: 0, section: 0)
if hasVideo {
guard let shareUserInfo = getUserInfo(userId: seatItem.userId) else { return }
updateShareItem(userInfo: shareUserInfo)
refreshListSeatItem()
viewResponder?.insertItems(at: [screenIndexPath])
} else {
shareItem = nil
refreshListSeatItem()
if videoSeatViewType == .largeSmallWindowType {
viewResponder?.reloadData()
} else {
viewResponder?.deleteItems(at: [screenIndexPath])
}
}
speakerItem = nil
viewResponder?.updateMiniscreen(nil)
}
private func setRemoteRenderParams(userId: String, streamType: TUIVideoStreamType) {
let renderParams = TRTCRenderParams()
renderParams.fillMode = (streamType == .screenStream) ? .fit : .fill
let trtcStreamType: TRTCVideoStreamType = (streamType == .screenStream) ? .sub : .big
engineManager.setRemoteRenderParams(userId: userId, streamType: trtcStreamType, params: renderParams)
}
private func seatListChanged(seated: [TUISeatInfo], left: [TUISeatInfo]) {
for leftSeat in left {
if let userId = leftSeat.userId {
removeSeatItem(userId)
}
}
for seatInfo in seated {
if let userId = seatInfo.userId {
addUserInfo(userId)
}
}
}
private func userScreenCaptureStopped() {
viewResponder?.showScreenCaptureMaskView(isShow: false)
if shareItem?.userId == currentUserId {
shareItem = nil
}
reloadSeatItems()
}
}
extension TUIVideoSeatViewModel: TUIVideoSeatViewResponder {
func switchPosition() {
guard videoSeatViewType == .largeSmallWindowType else { return }
isSwitchPosition = !isSwitchPosition
refreshListSeatItem()
viewResponder?.reloadData()
resetMiniscreen()
}
func clickVideoSeat() {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ChangeToolBarHiddenState, param: [:])
guard RoomRouter.shared.hasChatWindow() else { return }
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_HiddenChatWindow, param: [:])
}
func startPlayVideoStream(item: VideoSeatItem, renderView: UIView?) {
startPlayVideo(item: item, renderView: renderView)
}
func stopPlayVideoStream(item: VideoSeatItem) {
stopPlayVideo(item: item)
}
func updateSpeakerPlayVideoState(currentPageIndex: Int) {
guard videoSeatViewType != .speechType else { return }
if currentPageIndex == 0 {
viewResponder?.updateMiniscreen(speakerItem)
} else if let item = videoSeatItems.first(where: { $0.userId == speakerItem?.userId }),
let renderView = viewResponder?.getVideoVisibleCell(item)?.renderView {
startPlayVideo(item: item, renderView: renderView)
}
}
func stopScreenCapture() {
EngineEventCenter.shared.notifyEngineEvent(event: .onUserScreenCaptureStopped, param: [:])
engineManager.stopScreenCapture()
}
}

View File

@@ -0,0 +1,212 @@
//
// TopViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2022/12/30.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
protocol TopViewModelResponder: AnyObject {
func updateTimerLabel(text: String)
func updateStackView(item: ButtonItemData)
func updateMeetingNameLabel(_ text: String)
#if RTCube_APPSTORE
func showReportView()
#endif
}
class TopViewModel: NSObject {
private var topMenuTimer: DispatchSourceTimer?
private(set) var viewItems: [ButtonItemData] = []
var engineManager: EngineManager {
return EngineManager.shared
}
var store: RoomStore {
return engineManager.store
}
weak var viewResponder: TopViewModelResponder?
var roomInfo: TUIRoomInfo {
engineManager.store.roomInfo
}
var currentUser: UserEntity {
engineManager.store.currentUser
}
override init() {
super.init()
createBottomData()
initialStatus()
subscribeUIEvent()
updateTimerLabelText()
}
private func createBottomData() {
let micItem = ButtonItemData()
micItem.normalIcon = "room_earpiece"
micItem.selectedIcon = "room_speakerphone"
micItem.backgroundColor = UIColor(0xA3AEC7)
micItem.resourceBundle = tuiRoomKitBundle()
micItem.isSelect = engineManager.store.audioSetting.isSoundOnSpeaker
micItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.switchMicItemAction(sender: button)
}
viewItems.append(micItem)
let cameraItem = ButtonItemData()
cameraItem.normalIcon = "room_switch_camera"
cameraItem.backgroundColor = UIColor(0xA3AEC7)
cameraItem.resourceBundle = tuiRoomKitBundle()
cameraItem.buttonType = .switchCamaraItemType
cameraItem.isHidden = !currentUser.hasVideoStream
cameraItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.switchCameraItemAction(sender: button)
}
viewItems.append(cameraItem)
#if RTCube_APPSTORE
injectReport()
#endif
}
private func initialStatus() {
if engineManager.store.audioSetting.isSoundOnSpeaker {
engineManager.setAudioRoute(isSoundOnSpeaker: true)
} else {
engineManager.setAudioRoute(isSoundOnSpeaker: false)
}
}
private func subscribeUIEvent() {
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasVideoStream, responder: self)
EngineEventCenter.shared.subscribeEngine(event: .onConferenceInfoChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onStartedRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onJoinedRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onInitialRoomInfo, observer: self)
}
private func unsubscribeUIEvent() {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasVideoStream, responder: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onConferenceInfoChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onStartedRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onJoinedRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onInitialRoomInfo, observer: self)
}
private func switchMicItemAction(sender: UIButton) {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": true])
sender.isSelected = !sender.isSelected
if sender.isSelected {
engineManager.setAudioRoute(isSoundOnSpeaker: true)
} else {
engineManager.setAudioRoute(isSoundOnSpeaker: false)
}
}
private func switchCameraItemAction(sender: UIButton) {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": true])
engineManager.switchCamera()
}
private func updateTimer(totalSeconds: UInt) {
let second: UInt = totalSeconds % 60
let minute: UInt = (totalSeconds / 60) % 60
let hour: UInt = totalSeconds / 3_600
var timerText: String
if hour > 0 {
timerText = String(format: "%.2d:%.2d:%.2d", hour, minute, second)
} else {
timerText = String(format: "%.2d:%.2d", minute, second)
}
self.viewResponder?.updateTimerLabel(text: timerText)
}
func dropDownAction(sender: UIView) {
RoomRouter.shared.presentPopUpViewController(viewType: .roomInfoViewType, height: 290.scale375Height())
}
func exitAction(sender: UIView) {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ShowExitRoomView, param: [:])
}
func updateTimerLabelText() {
let timeInterval: TimeInterval = Date().timeIntervalSince1970
let timeStamp = Int(timeInterval)
var totalSeconds: UInt = UInt(labs(timeStamp - store.timeStampOnEnterRoom))
guard topMenuTimer == nil, store.isEnteredRoom else { return }
updateTimer(totalSeconds: totalSeconds)
topMenuTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
topMenuTimer?.schedule(deadline: .now(), repeating: .seconds(1))
topMenuTimer?.setEventHandler { [weak self] in
guard let self = self else { return }
totalSeconds += 1
self.updateTimer(totalSeconds: totalSeconds)
}
topMenuTimer?.resume()
}
deinit {
unsubscribeUIEvent()
topMenuTimer?.cancel()
topMenuTimer = nil
debugPrint("deinit \(self)")
}
}
extension TopViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_CurrentUserHasVideoStream:
guard let hasVideo = info?["hasVideo"] as? Bool else { return }
guard let item = viewItems.first(where: { $0.buttonType == .switchCamaraItemType }) else { return }
item.isHidden = !hasVideo
viewResponder?.updateStackView(item: item)
default: break
}
}
}
extension TopViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onConferenceInfoChanged:
guard let conferenceInfo = param?["conferenceInfo"] as? TUIConferenceInfo else { return }
guard let modifyFlag = param?["modifyFlag"] as? TUIConferenceModifyFlag else { return }
guard modifyFlag.contains(.roomName) else { return }
viewResponder?.updateMeetingNameLabel(conferenceInfo.basicRoomInfo.name)
case .onStartedRoom, .onJoinedRoom:
updateTimerLabelText()
case .onInitialRoomInfo:
guard let roomInfo = param?["roomInfo"] as? TUIRoomInfo else { return }
viewResponder?.updateMeetingNameLabel(roomInfo.name)
default: break
}
}
}
#if RTCube_APPSTORE
extension TopViewModel {
private func injectReport() {
if currentUser.userId == roomInfo.roomId {
return
}
let reportItem = ButtonItemData()
reportItem.normalIcon = "room_report"
reportItem.backgroundColor = UIColor(0xA3AEC7)
reportItem.resourceBundle = tuiRoomKitBundle()
reportItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.reportItemAction(sender: button)
}
viewItems.append(reportItem)
}
private func reportItemAction(sender: UIButton) {
viewResponder?.showReportView()
}
}
#endif

View File

@@ -0,0 +1,83 @@
//
// TransferMasterViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/2/20.
//
import Foundation
protocol TransferMasterViewResponder: NSObject {
func reloadTransferMasterTableView()
func searchControllerChangeActive(isActive: Bool)
func makeToast(message: String)
}
class TransferMasterViewModel: NSObject {
var attendeeList: [UserEntity] = []
var userId: String = ""
weak var viewResponder: TransferMasterViewResponder? = nil
var engineManager: EngineManager {
EngineManager.shared
}
let roomRouter: RoomRouter = RoomRouter.shared
override init() {
super.init()
attendeeList = self.engineManager.store.attendeeList.filter({ [weak self] userModel in
guard let self = self else { return true }
return userModel.userId != self.engineManager.store.currentUser.userId
})
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewUserList, responder: self)
}
func appointMasterAction(sender: UIButton) {
guard userId != "" else { return }
engineManager.changeUserRole(userId: userId, role: .roomOwner) { [weak self] in
guard let self = self else { return }
self.engineManager.exitRoom {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
} onError: { [weak self] code, message in
guard let self = self else { return }
self.viewResponder?.makeToast(message: message)
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}
} onError: { [weak self] code, message in
guard let self = self else { return }
self.viewResponder?.makeToast(message: message)
}
}
deinit {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewUserList, responder: self)
debugPrint("deinit \(self)")
}
}
extension TransferMasterViewModel: PopUpViewModelResponder {
func updateViewOrientation(isLandscape: Bool) {
viewResponder?.searchControllerChangeActive(isActive: false)
attendeeList = engineManager.store.attendeeList.filter({ [weak self] userModel in
guard let self = self else { return true }
return userModel.userId != self.engineManager.store.currentUser.userId
})
viewResponder?.reloadTransferMasterTableView()
}
func searchControllerChangeActive(isActive: Bool) {
viewResponder?.searchControllerChangeActive(isActive: isActive)
}
}
extension TransferMasterViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_RenewUserList:
attendeeList = engineManager.store.attendeeList.filter({ [weak self] userModel in
guard let self = self else { return true }
return userModel.userId != self.engineManager.store.currentUser.userId
})
viewResponder?.reloadTransferMasterTableView()
default: break
}
}
}

View File

@@ -0,0 +1,583 @@
//
// 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")
}
}

View File

@@ -0,0 +1,355 @@
//
// UserListViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/4.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
import Factory
protocol UserListViewResponder: NSObject {
func updateBottomControlView(isHidden: Bool)
func reloadUserListView()
func makeToast(text: String)
func updateUserManagerViewDisplayStatus(isHidden: Bool)
func updateMuteAllAudioButtonState(isSelect: Bool)
func updateMuteAllVideoButtonState(isSelect: Bool)
func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?)
func updateListStateView()
func updateMemberLabel(count: Int)
func updateUserListTableView()
}
class UserListViewModel: NSObject {
var userId: String = ""
var userName: String = ""
var attendeeList: [UserEntity] = []
var invitationList: [TUIInvitation] = []
var engineManager: EngineManager {
EngineManager.shared
}
var store: RoomStore {
engineManager.store
}
var currentUser: UserEntity {
store.currentUser
}
var roomInfo: TUIRoomInfo {
store.roomInfo
}
let timeoutNumber: Double = 60
weak var viewResponder: UserListViewResponder?
lazy var userListType: UserListType = isSeatEnabled ? .onStageUsers: .allUsers
var onStageCount: Int {
store.seatList.count
}
var offStageCount: Int {
store.offSeatList.count
}
var allUserCount: Int {
store.attendeeList.count
}
var isShownBottomControlView: Bool {
return store.currentUser.userRole != .generalUser
}
lazy var isSeatEnabled: Bool = {
return roomInfo.isSeatEnabled
}()
lazy var isShownNotificationView: Bool = {
return false
}()
lazy var isSearching: Bool = {
return false
}()
lazy var searchText: String = {
return ""
}()
var invitationUserList: [TUIInvitation] {
return conferenceStore.selectCurrent(ConferenceInvitationSelectors.getInvitationList)
}
@Injected(\.conferenceStore) var conferenceStore
override init() {
super.init()
updateAttendeeList()
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewUserList, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RoomOwnerChanged, responder: self)
EngineEventCenter.shared.subscribeEngine(event: .onAllUserMicrophoneDisableChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onAllUserCameraDisableChanged, observer: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
}
deinit {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewUserList, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RoomOwnerChanged, responder: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onAllUserMicrophoneDisableChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onAllUserCameraDisableChanged, observer: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
debugPrint("deinit \(self)")
}
func updateAttendeeList() {
var userList: [UserEntity] = []
switch userListType {
case .allUsers:
userList = store.attendeeList
case .onStageUsers:
userList = store.seatList
case .offStageUsers:
userList = store.offSeatList
case .notInRoomUsers:
userList = invitationList.map{ UserEntity(invitation: $0) }
}
if isSearching, searchText.count > 0 {
let searchArray = userList.filter({ model -> Bool in
return (model.userName.contains(searchText))
})
attendeeList = searchArray
} else {
attendeeList = userList
}
}
func muteAllAudioAction(sender: UIButton, view: UserListView) {
let isSelected = sender.isSelected
viewResponder?.showAlert(title: sender.isSelected ? .allUnmuteTitle : .allMuteTitle,
message: sender.isSelected ? .allUnmuteMessage : .allMuteMessage,
sureTitle: sender.isSelected ? .confirmReleaseText : .allMuteActionText,
declineTitle: .cancelText, sureBlock: { [weak self] in
guard let self = self else { return }
if self.roomInfo.isMicrophoneDisableForAllUser != !isSelected {
self.engineManager.muteAllAudioAction(isMute: !isSelected) {
} onError: { [weak self] _, message in
guard let self = self else { return }
self.viewResponder?.makeToast(text:message)
}
} else {
let text: String = isSelected ? .allUnMuteAudioText : .allMuteAudioText
self.viewResponder?.makeToast(text: text)
}
}, declineBlock: nil)
}
func muteAllVideoAction(sender: UIButton, view: UserListView) {
let isSelected = sender.isSelected
viewResponder?.showAlert(title: sender.isSelected ? .allUnmuteVideoTitle : .allMuteVideoTitle,
message: sender.isSelected ? .allUnmuteVideoMessage : .allMuteVideoMessage,
sureTitle: sender.isSelected ? .confirmReleaseText : .allMuteVideoActionText,
declineTitle: .cancelText, sureBlock: { [weak self] in
guard let self = self else { return }
if self.roomInfo.isCameraDisableForAllUser != !isSelected {
self.engineManager.muteAllVideoAction(isMute: !isSelected) {
} onError: { [weak self] _, message in
guard let self = self else { return }
self.viewResponder?.makeToast(text:message)
}
} else {
let text: String = isSelected ? .allUnMuteVideoText : .allMuteVideoText
self.viewResponder?.makeToast(text: text)
}
}, declineBlock: nil)
}
func showUserManageViewAction(userId: String, userName: String) {
self.userId = userId
self.userName = userName
guard checkShowManagerView() else { return }
viewResponder?.updateUserManagerViewDisplayStatus(isHidden: false)
}
private func checkShowManagerView() -> Bool {
guard let userInfo = engineManager.store.attendeeList.first(where: { $0.userId == userId }) else { return false }
if currentUser.userRole == .roomOwner, userId != currentUser.userId {
return true
} else if currentUser.userRole == .administrator, userId != currentUser.userId, userInfo.userRole == .generalUser {
return true
} else {
return false
}
}
func inviteSeatAction(sender: UIButton) {
guard let userInfo = attendeeList.first(where: { $0.userId == userId }) else { return }
guard store.seatList.count < roomInfo.maxSeatCount else {
RoomRouter.makeToastInCenter(toast: .theStageIsFullText, duration: 0.5)
return
}
engineManager.takeUserOnSeatByAdmin(userId: userId, 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)
}
func checkSelfInviteAbility(invitee: UserEntity) -> Bool {
if currentUser.userRole == .roomOwner {
return true
} else if currentUser.userRole == .administrator, invitee.userRole == .generalUser {
return true
} else {
return false
}
}
func changeListState(type: UserListType) {
userListType = type
updateAttendeeList()
viewResponder?.reloadUserListView()
}
func compareLists(oldList: [TUIInvitation], newList: [TUIInvitation]) -> (added: [TUIInvitation], removed: [TUIInvitation], changed: [TUIInvitation]) {
var added = [TUIInvitation]()
var removed = [TUIInvitation]()
var changed = [TUIInvitation]()
let oldDict = Dictionary(uniqueKeysWithValues: oldList.map { ($0.invitee.userId, $0) })
let newDict = Dictionary(uniqueKeysWithValues: newList.map { ($0.invitee.userId, $0) })
for newInvitation in newList {
if oldDict[newInvitation.invitee.userId] == nil {
added.append(newInvitation)
}
}
for oldInvitation in oldList {
if newDict[oldInvitation.invitee.userId] == nil {
removed.append(oldInvitation)
}
}
for newInvitation in newList {
if let oldInvitation = oldDict[newInvitation.invitee.userId], oldInvitation != newInvitation {
changed.append(newInvitation)
}
}
return (added, removed, changed)
}
}
extension UserListViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_RenewUserList, .TUIRoomKitService_RenewSeatList:
viewResponder?.updateMemberLabel(count: allUserCount)
updateAttendeeList()
viewResponder?.reloadUserListView()
viewResponder?.updateListStateView()
case .TUIRoomKitService_CurrentUserRoleChanged:
guard let userRole = info?["userRole"] as? TUIRole else { return }
viewResponder?.updateBottomControlView(isHidden: userRole == .generalUser)
case .TUIRoomKitService_RoomOwnerChanged:
viewResponder?.reloadUserListView()
default: break
}
}
}
extension UserListViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onAllUserCameraDisableChanged:
guard let isDisable = param?["isDisable"] as? Bool else { return }
viewResponder?.updateMuteAllVideoButtonState(isSelect: isDisable)
case .onAllUserMicrophoneDisableChanged:
guard let isDisable = param?["isDisable"] as? Bool else { return }
viewResponder?.updateMuteAllAudioButtonState(isSelect: isDisable)
default: break
}
}
}
extension UserListViewModel: RaiseHandApplicationNotificationViewListener {
func onShown() {
isShownNotificationView = true
viewResponder?.updateUserListTableView()
}
func onHidden() {
isShownNotificationView = false
viewResponder?.updateUserListTableView()
}
}
private extension String {
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 allMuteTitle: String {
localized("All current and incoming members will be muted")
}
static var allMuteVideoTitle: String {
localized("All current and incoming members will be restricted from video")
}
static var allMuteMessage: String {
localized("Members will unable to turn on the microphone")
}
static var allMuteVideoMessage: String {
localized("Members will unable to turn on video")
}
static var allUnmuteMessage: String {
localized("Members will be able to turn on the microphone")
}
static var allUnmuteVideoMessage: String {
localized("Members will be able to turn on video")
}
static var allUnmuteTitle: String {
localized("All members will be unmuted")
}
static var allUnmuteVideoTitle: String {
localized("All members will not be restricted from video")
}
static var cancelText: String {
localized("Cancel")
}
static var allMuteActionText: String {
localized("Mute All")
}
static var allMuteVideoActionText: String {
localized("Stop all video")
}
static var confirmReleaseText: String {
localized("Confirm release")
}
static var allMuteAudioText: String {
localized("All audios disabled")
}
static var allUnMuteAudioText: String {
localized("All audios enabled")
}
static var allMuteVideoText: String {
localized("All videos disabled")
}
static var allUnMuteVideoText: String {
localized("All videos enabled")
}
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")
}
}