增加换肤功能
This commit is contained in:
806
TUIKit/TUIRoomKit/Source/View/ViewModel/BottomViewModel.swift
Normal file
806
TUIKit/TUIRoomKit/Source/View/ViewModel/BottomViewModel.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
53
TUIKit/TUIRoomKit/Source/View/ViewModel/PopUpViewModel.swift
Normal file
53
TUIKit/TUIRoomKit/Source/View/ViewModel/PopUpViewModel.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
163
TUIKit/TUIRoomKit/Source/View/ViewModel/RoomInfoViewModel.swift
Normal file
163
TUIKit/TUIRoomKit/Source/View/ViewModel/RoomInfoViewModel.swift
Normal 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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
212
TUIKit/TUIRoomKit/Source/View/ViewModel/TopViewModel.swift
Normal file
212
TUIKit/TUIRoomKit/Source/View/ViewModel/TopViewModel.swift
Normal 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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
355
TUIKit/TUIRoomKit/Source/View/ViewModel/UserListViewModel.swift
Normal file
355
TUIKit/TUIRoomKit/Source/View/ViewModel/UserListViewModel.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user