AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/Calling/CallingViewModel.swift (310 lines of code) (raw):

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // import Combine import Foundation // swiftlint:disable type_body_length internal class CallingViewModel: ObservableObject { @Published var isParticipantGridDisplayed: Bool @Published var isVideoGridViewAccessibilityAvailable = false @Published var appState: AppStatus = .foreground @Published var isInPip = false @Published var allowLocalCameraPreview = false @Published var captionsStarted = false private let compositeViewModelFactory: CompositeViewModelFactoryProtocol private let store: Store<AppState, Action> private let localizationProvider: LocalizationProviderProtocol private let accessibilityProvider: AccessibilityProviderProtocol private let callType: CompositeCallType private let captionsOptions: CaptionsOptions private let callScreenOptions: CallScreenOptions private var cancellables = Set<AnyCancellable>() private var callHasConnected = false private var callClientRequested = false let localVideoViewModel: LocalVideoViewModel let participantGridsViewModel: ParticipantGridViewModel let bannerViewModel: BannerViewModel let lobbyOverlayViewModel: LobbyOverlayViewModel let loadingOverlayViewModel: LoadingOverlayViewModel let leaveCallConfirmationViewModel: LeaveCallConfirmationViewModel let participantListViewModel: ParticipantsListViewModel let participantActionViewModel: ParticipantMenuViewModel var onHoldOverlayViewModel: OnHoldOverlayViewModel! let isRightToLeft: Bool var controlBarViewModel: ControlBarViewModel! var infoHeaderViewModel: InfoHeaderViewModel! var lobbyWaitingHeaderViewModel: LobbyWaitingHeaderViewModel! var lobbyActionErrorViewModel: LobbyErrorHeaderViewModel! var errorInfoViewModel: ErrorInfoViewModel! var callDiagnosticsViewModel: CallDiagnosticsViewModel! var bottomToastViewModel: BottomToastViewModel! var supportFormViewModel: SupportFormViewModel! var captionsLanguageListViewModel: CaptionsLanguageListViewModel! var captionsRttListViewModel: CaptionsRttListViewModel! var moreCallOptionsListViewModel: MoreCallOptionsListViewModel! var audioDeviceListViewModel: AudioDevicesListViewModel! var captionsInfoViewModel: CaptionsRttInfoViewModel! var capabilitiesManager: CapabilitiesManager! var captionsErrorViewModel: CaptionsErrorViewModel! // swiftlint:disable function_body_length init(compositeViewModelFactory: CompositeViewModelFactoryProtocol, store: Store<AppState, Action>, localizationProvider: LocalizationProviderProtocol, accessibilityProvider: AccessibilityProviderProtocol, isIpadInterface: Bool, allowLocalCameraPreview: Bool, callType: CompositeCallType, captionsOptions: CaptionsOptions, capabilitiesManager: CapabilitiesManager, callScreenOptions: CallScreenOptions, rendererViewManager: RendererViewManager ) { self.store = store self.compositeViewModelFactory = compositeViewModelFactory self.localizationProvider = localizationProvider self.isRightToLeft = localizationProvider.isRightToLeft self.accessibilityProvider = accessibilityProvider self.allowLocalCameraPreview = allowLocalCameraPreview self.capabilitiesManager = capabilitiesManager self.callType = callType self.captionsOptions = captionsOptions self.callScreenOptions = callScreenOptions let actionDispatch: ActionDispatch = store.dispatch audioDeviceListViewModel = compositeViewModelFactory.makeAudioDevicesListViewModel( dispatchAction: actionDispatch, localUserState: store.state.localUserState) captionsLanguageListViewModel = compositeViewModelFactory.makeCaptionsLanguageListViewModel( dispatchAction: actionDispatch, state: store.state ) captionsInfoViewModel = compositeViewModelFactory.makeCaptionsRttInfoViewModel( state: store.state, captionsOptions: captionsOptions) captionsErrorViewModel = compositeViewModelFactory.makeCaptionsErrorViewModel(dispatchAction: actionDispatch) supportFormViewModel = compositeViewModelFactory.makeSupportFormViewModel() localVideoViewModel = compositeViewModelFactory.makeLocalVideoViewModel(dispatchAction: actionDispatch) participantGridsViewModel = compositeViewModelFactory.makeParticipantGridsViewModel( isIpadInterface: isIpadInterface, rendererViewManager: rendererViewManager) bannerViewModel = compositeViewModelFactory.makeBannerViewModel(dispatchAction: store.dispatch) lobbyOverlayViewModel = compositeViewModelFactory.makeLobbyOverlayViewModel() loadingOverlayViewModel = compositeViewModelFactory.makeLoadingOverlayViewModel() infoHeaderViewModel = compositeViewModelFactory .makeInfoHeaderViewModel(dispatchAction: actionDispatch, localUserState: store.state.localUserState, callScreenInfoHeaderState: store.state.callScreenInfoHeaderState, buttonViewDataState: store.state.buttonViewDataState, controlHeaderViewData: callScreenOptions.headerViewData ) lobbyWaitingHeaderViewModel = compositeViewModelFactory .makeLobbyWaitingHeaderViewModel(localUserState: store.state.localUserState, dispatchAction: actionDispatch) lobbyActionErrorViewModel = compositeViewModelFactory .makeLobbyActionErrorViewModel(localUserState: store.state.localUserState, dispatchAction: actionDispatch) let isCallConnected = store.state.callingState.status == .connected let callingStatus = store.state.callingState.status let isOutgoingCall = CallingViewModel.isOutgoingCallDialingInProgress(callType: callType, callingStatus: callingStatus) let isRemoteHold = store.state.callingState.status == .remoteHold isParticipantGridDisplayed = (isCallConnected || isOutgoingCall || isRemoteHold) && CallingViewModel.hasRemoteParticipants(store.state.remoteParticipantsState.participantInfoList) leaveCallConfirmationViewModel = compositeViewModelFactory.makeLeaveCallConfirmationViewModel( endCall: { store.dispatch(action: .callingAction(.callEndRequested)) }, dismissConfirmation: { store.dispatch(action: .hideDrawer) } ) participantListViewModel = compositeViewModelFactory .makeParticipantsListViewModel( localUserState: store.state.localUserState, isDisplayed: store.state.navigationState.participantsVisible, dispatchAction: store.dispatch) participantActionViewModel = compositeViewModelFactory .makeParticipantMenuViewModel( localUserState: store.state.localUserState, isDisplayed: store.state.navigationState.participantActionsVisible, dispatchAction: store.dispatch) controlBarViewModel = compositeViewModelFactory .makeControlBarViewModel(dispatchAction: actionDispatch, onEndCallTapped: { [weak self] in guard let self = self else { return } if callScreenOptions.controlBarOptions?.leaveCallConfirmationMode != .alwaysDisabled { store.dispatch(action: .showEndCallConfirmation) } else { self.endCall() } }, localUserState: store.state.localUserState, capabilitiesManager: capabilitiesManager, buttonViewDataState: store.state.buttonViewDataState) onHoldOverlayViewModel = compositeViewModelFactory.makeOnHoldOverlayViewModel(resumeAction: { [weak self] in guard let self = self else { return } self.resumeOnHold() }) store.$state .receive(on: DispatchQueue.main) .sink { [weak self] state in self?.receive(state) }.store(in: &cancellables) updateIsLocalCameraOn(with: store.state) errorInfoViewModel = compositeViewModelFactory.makeErrorInfoViewModel(title: "", subtitle: "") callDiagnosticsViewModel = compositeViewModelFactory .makeCallDiagnosticsViewModel(dispatchAction: store.dispatch) bottomToastViewModel = compositeViewModelFactory.makeBottomToastViewModel( toastNotificationState: store.state.toastNotificationState, dispatchAction: store.dispatch) let buttonActions = ButtonActions( showSharingViewAction: { store.dispatch(action: .showSupportShare) }, showSupportFormAction: { store.dispatch(action: .showSupportForm) }, showCaptionsViewAction: { store.dispatch(action: .showCaptionsRttListView) } ) moreCallOptionsListViewModel = compositeViewModelFactory.makeMoreCallOptionsListViewModel( isCaptionsAvailable: true, buttonActions: buttonActions, controlBarOptions: callScreenOptions.controlBarOptions, buttonViewDataState: store.state.buttonViewDataState, dispatchAction: store.dispatch ) let captionsButtonActions = ButtonActions( showSpokenLanguage: { store.dispatch(action: .showSpokenLanguageView) }, showCaptionsLanguage: { store.dispatch(action: .showCaptionsLanguageView) }, showRttView: { store.dispatch(action: .rttAction(.updateMaximized(isMaximized: true))) store.dispatch(action: .rttAction(.turnOnRtt)) store.dispatch(action: .hideDrawer) } ) captionsRttListViewModel = compositeViewModelFactory.makeCaptionsRttListViewModel( state: store.state, captionsOptions: captionsOptions, dispatchAction: store.dispatch, buttonActions: captionsButtonActions, isDisplayed: store.state.navigationState.captionsRttViewVisible) } // swiftlint:enable function_body_length func endCall() { store.dispatch(action: .callingAction(.callEndRequested)) } func resumeOnHold() { store.dispatch(action: .callingAction(.resumeRequested)) } func dismissDrawer() { store.dispatch(action: .hideDrawer) } func receive(_ state: AppState) { if appState != state.lifeCycleState.currentStatus { appState = state.lifeCycleState.currentStatus } guard state.visibilityState.currentStatus != .hidden else { return } participantListViewModel.update(localUserState: state.localUserState, remoteParticipantsState: state.remoteParticipantsState, isDisplayed: state.navigationState.participantsVisible) participantActionViewModel.update(localUserState: state.localUserState, isDisplayed: state.navigationState.participantActionsVisible, participantInfoModel: state.navigationState.selectedParticipant) audioDeviceListViewModel.update( audioDeviceStatus: state.localUserState.audioState.device, navigationState: state.navigationState, visibilityState: state.visibilityState) leaveCallConfirmationViewModel.update(state: state) supportFormViewModel.update(state: state) captionsRttListViewModel.update(state: state) captionsInfoViewModel.update(state: state) captionsLanguageListViewModel.update(state: state) captionsErrorViewModel.update(captionsState: state.captionsState, callingState: state.callingState) controlBarViewModel.update(localUserState: state.localUserState, permissionState: state.permissionState, callingState: state.callingState, visibilityState: state.visibilityState, navigationState: state.navigationState, buttonViewDataState: state.buttonViewDataState) infoHeaderViewModel.update(localUserState: state.localUserState, remoteParticipantsState: state.remoteParticipantsState, callingState: state.callingState, visibilityState: state.visibilityState, callScreenInfoHeaderState: state.callScreenInfoHeaderState , buttonViewDataState: state.buttonViewDataState ) localVideoViewModel.update(localUserState: state.localUserState, visibilityState: state.visibilityState) lobbyWaitingHeaderViewModel.update(localUserState: state.localUserState, remoteParticipantsState: state.remoteParticipantsState, callingState: state.callingState, visibilityState: state.visibilityState) lobbyActionErrorViewModel.update(localUserState: state.localUserState, remoteParticipantsState: state.remoteParticipantsState, callingState: state.callingState) participantGridsViewModel.update(callingState: state.callingState, captionsState: state.captionsState, rttState: state.rttState, remoteParticipantsState: state.remoteParticipantsState, visibilityState: state.visibilityState, lifeCycleState: state.lifeCycleState) bannerViewModel.update(callingState: state.callingState, visibilityState: state.visibilityState) lobbyOverlayViewModel.update(callingStatus: state.callingState.status) onHoldOverlayViewModel.update(callingStatus: state.callingState.status, audioSessionStatus: state.audioSessionState.status) moreCallOptionsListViewModel.update(navigationState: state.navigationState, rttState: state.rttState, visibilityState: state.visibilityState, buttonViewDataState: state.buttonViewDataState) receiveExtension(state) } private func receiveExtension(_ state: AppState) { let newIsCallConnected = state.callingState.status == .connected let isOutgoingCall = CallingViewModel.isOutgoingCallDialingInProgress(callType: callType, callingStatus: state.callingState.status) let isRemoteHold = store.state.callingState.status == .remoteHold let shouldParticipantGridDisplayed = (newIsCallConnected || isOutgoingCall || isRemoteHold) && CallingViewModel.hasRemoteParticipants(state.remoteParticipantsState.participantInfoList) if shouldParticipantGridDisplayed != isParticipantGridDisplayed { isParticipantGridDisplayed = shouldParticipantGridDisplayed } if callHasConnected != newIsCallConnected && newIsCallConnected { DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { [weak self] in guard let self = self else { return } self.accessibilityProvider.postQueuedAnnouncement( self.localizationProvider.getLocalizedString(.joinedCallAccessibilityLabel)) } callHasConnected = newIsCallConnected } updateIsLocalCameraOn(with: state) errorInfoViewModel.update(errorState: state.errorState) isInPip = state.visibilityState.currentStatus == .pipModeEntered callDiagnosticsViewModel.update(diagnosticsState: state.diagnosticsState) bottomToastViewModel.update(toastNotificationState: state.toastNotificationState) } private static func hasRemoteParticipants(_ participants: [ParticipantInfoModel]) -> Bool { return participants.filter({ participant in participant.status != .inLobby && participant.status != .disconnected }).count > 0 } private func updateIsLocalCameraOn(with state: AppState) { let isLocalCameraOn = state.localUserState.cameraState.operation == .on let displayName = state.localUserState.displayName ?? "" let isLocalUserInfoNotEmpty = isLocalCameraOn || !displayName.isEmpty isVideoGridViewAccessibilityAvailable = !lobbyOverlayViewModel.isDisplayed && !onHoldOverlayViewModel.isDisplayed && (isLocalUserInfoNotEmpty || isParticipantGridDisplayed) } private static func isOutgoingCallDialingInProgress(callType: CompositeCallType, callingStatus: CallingStatus?) -> Bool { let isOutgoingCall = (callType == .oneToNOutgoing && (callingStatus == nil || callingStatus == .connecting || callingStatus == .ringing || callingStatus == .earlyMedia)) return isOutgoingCall } } // swiftlint:enable type_body_length