AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/Setup/SetupViewModel.swift (181 lines of code) (raw):

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // import Combine import Foundation class SetupViewModel: ObservableObject { private let logger: Logger private let store: Store<AppState, Action> private let localizationProvider: LocalizationProviderProtocol private let callType: CompositeCallType private var callingStatus: CallingStatus = .none let isRightToLeft: Bool let previewAreaViewModel: PreviewAreaViewModel var title: String var subTitle: String? var networkManager: NetworkManager var audioSessionManager: AudioSessionManagerProtocol var errorInfoViewModel: ErrorInfoViewModel var dismissButtonViewModel: IconButtonViewModel! var joinCallButtonViewModel: PrimaryButtonViewModel! var setupControlBarViewModel: SetupControlBarViewModel! var joiningCallActivityViewModel: JoiningCallActivityViewModel! var cancellables = Set<AnyCancellable>() var audioDeviceListViewModel: AudioDevicesListViewModel! @Published var isJoinRequested = false init(compositeViewModelFactory: CompositeViewModelFactoryProtocol, logger: Logger, store: Store<AppState, Action>, networkManager: NetworkManager, audioSessionManager: AudioSessionManagerProtocol, localizationProvider: LocalizationProviderProtocol, setupScreenViewData: SetupScreenViewData? = nil, callType: CompositeCallType) { let actionDispatch: ActionDispatch = store.dispatch self.store = store self.networkManager = networkManager self.networkManager.startMonitor() self.audioSessionManager = audioSessionManager self.localizationProvider = localizationProvider self.isRightToLeft = localizationProvider.isRightToLeft self.logger = logger self.callType = callType if let title = setupScreenViewData?.title, !title.isEmpty { // if title is not nil/empty, use given title and optional subtitle self.title = title self.subTitle = setupScreenViewData?.subtitle } else { // else if title is nil/empty, use default title self.title = self.localizationProvider.getLocalizedString(.setupTitle) self.subTitle = nil } previewAreaViewModel = compositeViewModelFactory.makePreviewAreaViewModel(dispatchAction: store.dispatch) var joiningButtonLocalization = LocalizationKey.joiningCall if self.callType == .oneToNOutgoing { joiningButtonLocalization = LocalizationKey.startingCall } joiningCallActivityViewModel = compositeViewModelFactory.makeJoiningCallActivityViewModel( title: self.localizationProvider.getLocalizedString(joiningButtonLocalization)) errorInfoViewModel = compositeViewModelFactory.makeErrorInfoViewModel(title: "", subtitle: "") audioDeviceListViewModel = compositeViewModelFactory.makeAudioDevicesListViewModel( dispatchAction: actionDispatch, localUserState: store.state.localUserState) var callButtonLocalization = LocalizationKey.joinCall if self.callType == .oneToNOutgoing { callButtonLocalization = LocalizationKey.startCall } joinCallButtonViewModel = compositeViewModelFactory.makePrimaryButtonViewModel( buttonStyle: .primaryFilled, buttonLabel: self.localizationProvider .getLocalizedString(callButtonLocalization), iconName: .meetNow, isDisabled: false) { [weak self] in guard let self = self else { return } self.joinCallButtonTapped() } updateAccessibilityLabel() dismissButtonViewModel = compositeViewModelFactory.makeIconButtonViewModel( iconName: .leftArrow, buttonType: .controlButton, isDisabled: false) { [weak self] in guard let self = self else { return } self.dismissButtonTapped() } dismissButtonViewModel.update( accessibilityLabel: self.localizationProvider.getLocalizedString(.dismissAccessibilityLabel)) setupControlBarViewModel = compositeViewModelFactory .makeSetupControlBarViewModel(dispatchAction: store.dispatch, localUserState: store.state.localUserState, buttonViewDataState: store.state.buttonViewDataState) store.$state .receive(on: DispatchQueue.main) .sink { [weak self] state in self?.receive(state) }.store(in: &cancellables) $isJoinRequested.sink { [weak self] value in self?.setupControlBarViewModel.update(isJoinRequested: value) }.store(in: &cancellables) } func updateAccessibilityLabel() { if joinCallButtonViewModel.isDisabled { // Update the accessibility label for the disabled state var key = LocalizationKey.joinCallDiableStateAccessibilityLabel if callType == .oneToNOutgoing { key = LocalizationKey.startCallDiableStateAccessibilityLabel } joinCallButtonViewModel.update(accessibilityLabel: self.localizationProvider.getLocalizedString(key)) } else { // Update the accessibility label for the normal state var key = LocalizationKey.joinCall if callType == .oneToNOutgoing { key = LocalizationKey.startCall } joinCallButtonViewModel.update(accessibilityLabel: self.localizationProvider.getLocalizedString(key)) } } deinit { networkManager.stopMonitor() } func joinCallButtonTapped() { guard networkManager.isConnected else { handleOffline() return } guard audioSessionManager.isAudioUsedByOther() else { handleMicUnavailableEvent() return } isJoinRequested = true store.dispatch(action: .callingAction(.callStartRequested)) } func dismissButtonTapped() { let isJoining = callingStatus != .none let action: Action = isJoining ? .callingAction(.callEndRequested) : .compositeExitAction store.dispatch(action: action) } func receive(_ state: AppState) { let newCallingStatus = state.callingState.status if callingStatus != newCallingStatus, newCallingStatus == .none { isJoinRequested = false } callingStatus = newCallingStatus let localUserState = state.localUserState let permissionState = state.permissionState let callingState = state.callingState previewAreaViewModel.update(localUserState: localUserState, permissionState: permissionState, visibilityState: state.visibilityState) setupControlBarViewModel.update(localUserState: localUserState, permissionState: permissionState, callingState: callingState, buttonViewDataState: state.buttonViewDataState) joinCallButtonViewModel.update(isDisabled: permissionState.audioPermission == .denied) // Disable the dismiss button for now to avoid crash /// Todo remove after calling native SDK fix dismissButtonViewModel.update(isDisabled: newCallingStatus == .connecting) updateAccessibilityLabel() errorInfoViewModel.update(errorState: state.errorState) audioDeviceListViewModel.update( audioDeviceStatus: state.localUserState.audioState.device, navigationState: state.navigationState, visibilityState: state.visibilityState ) objectWillChange.send() } func shouldShowSetupControlBarView() -> Bool { let cameraStatus = store.state.localUserState.cameraState.operation return cameraStatus == .off || !isJoinRequested } func dismissAudioDevicesDrawer() { store.dispatch(action: .hideDrawer) } private func handleOffline() { store.dispatch( action: .errorAction( .statusErrorAndCallReset(internalError: .callJoinConnectionFailed, error: nil))) // only show banner again when user taps on button explicitly // banner would not reappear when other events^1 send identical error state again // 1: camera on/off, audio on/off, switch to background/foreground, etc. errorInfoViewModel.show() } private func handleMicUnavailableEvent() { store.dispatch(action: .errorAction(.statusErrorAndCallReset(internalError: .micNotAvailable, error: nil))) // only show banner again when user taps on button explicitly // banner would not reappear when other events^1 send identical error state again // 1: camera on/off, audio on/off, switch to background/foreground, etc. errorInfoViewModel.show() } }