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

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // import SwiftUI // swiftlint:disable type_body_length // swiftlint:disable file_length struct CallingView: View { enum InfoHeaderViewConstants { static let horizontalPadding: CGFloat = 8.0 static let maxWidth: CGFloat = 380.0 static let height: CGFloat = 46.0 } enum ErrorInfoConstants { static let controlBarHeight: CGFloat = 92 static let horizontalPadding: CGFloat = 8 } enum Constants { static let topAlertAreaViewTopPadding: CGFloat = 10.0 } enum DiagnosticToastInfoConstants { static let bottomPaddingPortrait: CGFloat = 5 static let bottomPaddingLandscape: CGFloat = 16 } enum CaptionsInfoConstants { static let maxHeight: CGFloat = 115.0 } @ObservedObject var viewModel: CallingViewModel @StateObject private var keyboard = KeyboardResponder() let avatarManager: AvatarViewManagerProtocol let viewManager: VideoViewManager @Environment(\.horizontalSizeClass) var widthSizeClass: UserInterfaceSizeClass? @Environment(\.verticalSizeClass) var heightSizeClass: UserInterfaceSizeClass? @State private var orientation: UIDeviceOrientation = UIDevice.current.orientation @State private var isAutoCommitted = false var safeAreaIgnoreArea: Edge.Set { return getSizeClass() != .iphoneLandscapeScreenSize ? [] : [/* .bottom */] } var isIpad: Bool { return getSizeClass() == .ipadScreenSize } var body: some View { ZStack { GeometryReader { geometry in ZStack { if getSizeClass() != .iphoneLandscapeScreenSize { portraitCallingView } }.frame(width: geometry.size.width, height: geometry.size.height) } .ignoresSafeArea(isIpad ? [] : .keyboard) if getSizeClass() == .iphoneLandscapeScreenSize { landscapeCallingView } errorInfoView } .modifier(PopupModalView(isPresented: viewModel.loadingOverlayViewModel.isDisplayed && !viewModel.lobbyOverlayViewModel.isDisplayed) { LoadingOverlayView(viewModel: viewModel.loadingOverlayViewModel) .accessibilityElement(children: .contain) .accessibilityHidden(!viewModel.loadingOverlayViewModel.isDisplayed) }) .modifier(PopupModalView(isPresented: viewModel.onHoldOverlayViewModel.isDisplayed) { OverlayView(viewModel: viewModel.onHoldOverlayViewModel) .accessibilityElement(children: .contain) .accessibilityHidden(!viewModel.onHoldOverlayViewModel.isDisplayed) }) .overlay(bottomDrawer) .environment(\.screenSizeClass, getSizeClass()) .environment(\.appPhase, viewModel.appState) .edgesIgnoringSafeArea(safeAreaIgnoreArea) .onRotate { newOrientation in updateChildViewIfNeededWith(newOrientation: newOrientation) }.onAppear { resetOrientation() } } var bottomDrawer: some View { ZStack { BottomDrawer(isPresented: viewModel.supportFormViewModel.isDisplayed, hideDrawer: viewModel.supportFormViewModel.hideForm) { reportErrorView .accessibilityElement(children: .contain) .accessibilityAddTraits(.isModal) } BottomDrawer(isPresented: viewModel.moreCallOptionsListViewModel.isDisplayed, hideDrawer: viewModel.dismissDrawer) { MoreCallOptionsListView(viewModel: viewModel.moreCallOptionsListViewModel, avatarManager: avatarManager) } BottomDrawer(isPresented: viewModel.audioDeviceListViewModel.isDisplayed, hideDrawer: viewModel.dismissDrawer) { AudioDevicesListView(viewModel: viewModel.audioDeviceListViewModel, avatarManager: avatarManager) } BottomDrawer(isPresented: viewModel.participantActionViewModel.isDisplayed, hideDrawer: viewModel.dismissDrawer) { ParticipantMenuView(viewModel: viewModel.participantActionViewModel, avatarManager: avatarManager) } BottomDrawer(isPresented: viewModel.participantListViewModel.isDisplayed, hideDrawer: viewModel.dismissDrawer) { ParticipantsListView(viewModel: viewModel.participantListViewModel, avatarManager: avatarManager) } BottomDrawer(isPresented: viewModel.captionsLanguageListViewModel.isDisplayed, hideDrawer: viewModel.dismissDrawer) { CaptionsLanguageListView(viewModel: viewModel.captionsLanguageListViewModel, avatarManager: avatarManager) } BottomDrawer(isPresented: viewModel.captionsRttListViewModel.isDisplayed, hideDrawer: viewModel.dismissDrawer, title: viewModel.captionsRttListViewModel.title, startIcon: CompositeIcon.leftArrow, startIconAction: viewModel.captionsRttListViewModel.backButtonAction) { CaptionsRttListView(viewModel: viewModel.captionsRttListViewModel, avatarManager: avatarManager) } BottomDrawer(isPresented: viewModel.leaveCallConfirmationViewModel.isDisplayed, hideDrawer: viewModel.dismissDrawer) { LeaveCallConfirmationView( viewModel: viewModel.leaveCallConfirmationViewModel, avatarManager: avatarManager) } } } var captionsAndRttDrawer: some View { return ExpandableDrawer( isPresented: Binding( get: { viewModel.captionsInfoViewModel.isDisplayed }, set: { newValue in if !newValue { viewModel.dismissDrawer() } } ), hideDrawer: viewModel.dismissDrawer, title: viewModel.captionsInfoViewModel?.title, endIcon: viewModel.captionsInfoViewModel?.endIcon, endIconAction: viewModel.captionsInfoViewModel?.endIconAction, endIconAccessibilityValue: viewModel.captionsInfoViewModel?.endIconAccessibilityValue, showTextBox: viewModel.captionsInfoViewModel?.isRttAvailable ?? false, shouldExpand: viewModel.captionsInfoViewModel?.shouldExpand ?? false, expandIconAccessibilityValue: viewModel.captionsInfoViewModel?.expandIconAccessibilityValue, collapseIconAccessibilityValue: viewModel.captionsInfoViewModel?.collapseIconAccessibilityValue, textBoxHint: viewModel.captionsInfoViewModel?.textBoxHint, isAutoCommitted: $isAutoCommitted, commitAction: { message, isFinal in viewModel.captionsInfoViewModel?.commitMessage(message, isFinal ?? false) }, updateHeightAction: { shouldMaximize in viewModel.captionsInfoViewModel.updateLayoutHelight(shouldMaximize) }, content: { CaptionsRttInfoView( viewModel: viewModel.captionsInfoViewModel!, avatarViewManager: avatarManager ) } ).onReceive(viewModel.captionsInfoViewModel.captionsManager.$isAutoCommit) { shouldClear in if shouldClear { isAutoCommitted = true } } } var captionsAndRttInfoViewPlaceholder: some View { Spacer() .frame(maxWidth: .infinity, maxHeight: DrawerConstants.collapsedHeight, alignment: .bottom) .zIndex(1) } var captionsAndRttIpadView: some View { return CaptionsAndRttLandscapeView( title: viewModel.captionsInfoViewModel?.title, endIcon: viewModel.captionsInfoViewModel?.endIcon, endIconAction: viewModel.captionsInfoViewModel?.endIconAction, showTextBox: viewModel.captionsInfoViewModel?.isRttAvailable ?? false, textBoxHint: viewModel.captionsInfoViewModel?.textBoxHint, isAutoCommitted: $isAutoCommitted, commitAction: { message, isFinal in viewModel.captionsInfoViewModel?.commitMessage(message, isFinal ?? false) }, content: { CaptionsRttInfoView( viewModel: viewModel.captionsInfoViewModel!, avatarViewManager: avatarManager ) } ).onReceive(viewModel.captionsInfoViewModel.captionsManager.$isAutoCommit) { shouldClear in if shouldClear { isAutoCommitted = true } } } var portraitCallingView: some View { VStack(alignment: .center, spacing: 0) { if isIpad { HStack { containerView ZStack { if !viewModel.isInPip && viewModel.captionsInfoViewModel.isDisplayed { captionsAndRttIpadView } bottomToastDiagnosticsView .accessibilityElement(children: .contain) captionsErrorView.accessibilityElement(children: .contain) } } if keyboard.keyboardHeight == 0 { ControlBarView(viewModel: viewModel.controlBarViewModel) } } else { ZStack { containerView.padding(2) if !viewModel.isInPip { captionsAndRttDrawer } bottomToastDiagnosticsView .accessibilityElement(children: .contain) captionsErrorView.accessibilityElement(children: .contain) } ControlBarView(viewModel: viewModel.controlBarViewModel) } } } var landscapeCallingView: some View { ZStack { HStack(alignment: .center, spacing: 0) { containerView if !viewModel.isInPip && viewModel.captionsInfoViewModel.isDisplayed { captionsAndRttIpadView.padding(.leading, 2) } if keyboard.keyboardHeight == 0 { ControlBarView(viewModel: viewModel.controlBarViewModel) } } } } var containerView: some View { Group { ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading) { GeometryReader { geometry in ZStack { videoGridView .accessibilityHidden(!viewModel.isVideoGridViewAccessibilityAvailable) if viewModel.isParticipantGridDisplayed && viewModel.allowLocalCameraPreview { Group { DraggableLocalVideoView(containerBounds: geometry.frame(in: .local), viewModel: viewModel, avatarManager: avatarManager, viewManager: viewManager, orientation: $orientation, screenSize: getSizeClass()) } .accessibilityElement(children: .contain) .accessibilityIdentifier( AccessibilityIdentifier.draggablePipViewAccessibilityID.rawValue) } }.zIndex(2) .ignoresSafeArea(isIpad ? [] : .keyboard) } if (viewModel.captionsInfoViewModel.isRttDisplayed || viewModel.captionsInfoViewModel.isCaptionsDisplayed) && !viewModel.isInPip && !isIpad && getSizeClass() != .iphoneLandscapeScreenSize { captionsAndRttInfoViewPlaceholder } } topAlertAreaView .accessibilityElement(children: .contain) .accessibilitySortPriority(1) .accessibilityHidden(viewModel.lobbyOverlayViewModel.isDisplayed || viewModel.onHoldOverlayViewModel.isDisplayed || viewModel.loadingOverlayViewModel.isDisplayed) } .contentShape(Rectangle()) .animation(.linear(duration: 0.167), value: true) .onTapGesture(perform: { viewModel.infoHeaderViewModel.toggleDisplayInfoHeaderIfNeeded() }) .accessibilityElement(children: .contain) .modifier(PopupModalView(isPresented: viewModel.lobbyOverlayViewModel.isDisplayed) { OverlayView(viewModel: viewModel.lobbyOverlayViewModel) .accessibilityElement(children: .contain) .accessibilityHidden(!viewModel.lobbyOverlayViewModel.isDisplayed) }) } } var topAlertAreaView: some View { GeometryReader { geometry in let geoWidth: CGFloat = geometry.size.width let isIpad = getSizeClass() == .ipadScreenSize let widthWithoutHorizontalPadding = geoWidth - 2 * InfoHeaderViewConstants.horizontalPadding let infoHeaderViewWidth = isIpad ? min(widthWithoutHorizontalPadding, InfoHeaderViewConstants.maxWidth) : widthWithoutHorizontalPadding VStack(spacing: 0) { bannerView HStack { if isIpad { Spacer() } else { EmptyView() } infoHeaderView .frame(width: infoHeaderViewWidth, alignment: .leading) .padding(.leading, InfoHeaderViewConstants.horizontalPadding) Spacer() }.accessibilityElement(children: .contain) HStack { if isIpad { Spacer() } else { EmptyView() } lobbyWaitingHeaderView .frame(width: infoHeaderViewWidth, alignment: .leading) .padding(.leading, InfoHeaderViewConstants.horizontalPadding) Spacer() } HStack { if isIpad { Spacer() } else { EmptyView() } lobbyActionErrorView .frame(width: infoHeaderViewWidth, alignment: .leading) .padding(.leading, InfoHeaderViewConstants.horizontalPadding) Spacer() } HStack { if isIpad { Spacer() } else { EmptyView() } topMessageBarDiagnosticsView .frame(width: infoHeaderViewWidth, alignment: .leading) .padding(.leading, InfoHeaderViewConstants.horizontalPadding) Spacer() } } .padding(.top, Constants.topAlertAreaViewTopPadding) .accessibilityElement(children: .contain) } } var infoHeaderView: some View { InfoHeaderView(viewModel: viewModel.infoHeaderViewModel, avatarViewManager: avatarManager) } var lobbyWaitingHeaderView: some View { LobbyWaitingHeaderView(viewModel: viewModel.lobbyWaitingHeaderViewModel, avatarViewManager: avatarManager) } var lobbyActionErrorView: some View { LobbyErrorHeaderView(viewModel: viewModel.lobbyActionErrorViewModel, avatarViewManager: avatarManager) } var bannerView: some View { BannerView(viewModel: viewModel.bannerViewModel) } var participantGridsView: some View { ParticipantGridView(viewModel: viewModel.participantGridsViewModel, avatarViewManager: avatarManager, screenSize: getSizeClass()) .edgesIgnoringSafeArea(safeAreaIgnoreArea) } var localVideoFullscreenView: some View { Group { LocalVideoView(viewModel: viewModel.localVideoViewModel, viewManager: viewManager, viewType: .localVideofull, avatarManager: avatarManager) .background(Color(StyleProvider.color.surface)) .edgesIgnoringSafeArea(safeAreaIgnoreArea) } } var videoGridView: some View { Group { if viewModel.isParticipantGridDisplayed { participantGridsView } else { localVideoFullscreenView } } } var errorInfoView: some View { return VStack { Spacer() ErrorInfoView(viewModel: viewModel.errorInfoViewModel) .padding(EdgeInsets(top: 0, leading: ErrorInfoConstants.horizontalPadding, bottom: ErrorInfoConstants.controlBarHeight, trailing: ErrorInfoConstants.horizontalPadding) ) .accessibilityElement(children: .contain) .accessibilityAddTraits(.isModal) } } var bottomToastDiagnosticsView: some View { VStack { Spacer() BottomToastView(viewModel: viewModel.bottomToastViewModel) .padding( EdgeInsets(top: 0, leading: 0, bottom: getSizeClass() == .iphoneLandscapeScreenSize ? DiagnosticToastInfoConstants.bottomPaddingLandscape : DiagnosticToastInfoConstants.bottomPaddingPortrait, trailing: 0) ) .accessibilityElement(children: .contain) .accessibilityAddTraits(.isStaticText) }.frame(maxWidth: .infinity, alignment: .center) } var captionsErrorView: some View { VStack { Spacer() CaptionsErrorView(viewModel: viewModel.captionsErrorViewModel) .padding( EdgeInsets(top: 0, leading: 0, bottom: getSizeClass() == .iphoneLandscapeScreenSize ? DiagnosticToastInfoConstants.bottomPaddingLandscape : DiagnosticToastInfoConstants.bottomPaddingPortrait, trailing: 0) ) .accessibilityElement(children: .contain) .accessibilityAddTraits(.isStaticText) }.frame(maxWidth: .infinity, alignment: .center) } var topMessageBarDiagnosticsView: some View { VStack { ForEach(viewModel.callDiagnosticsViewModel.messageBarStack) { diagnosticMessageBarViewModel in MessageBarDiagnosticView(viewModel: diagnosticMessageBarViewModel) .accessibilityElement(children: .contain) .accessibilityAddTraits(.isStaticText) } Spacer() } } var reportErrorView: some View { return Group { SupportFormView(viewModel: viewModel.supportFormViewModel) } } } // swiftlint:enable type_body_length extension CallingView { private func getSizeClass() -> ScreenSizeClassType { switch (widthSizeClass, heightSizeClass) { case (.compact, .regular): return .iphonePortraitScreenSize case (.compact, .compact), (.regular, .compact): return .iphoneLandscapeScreenSize default: return .ipadScreenSize } } private func updateChildViewIfNeededWith(newOrientation: UIDeviceOrientation) { let areAllOrientationsSupported = SupportedOrientationsPreferenceKey.defaultValue == .all if newOrientation != orientation && newOrientation != .unknown && newOrientation != .faceDown && newOrientation != .faceUp && (areAllOrientationsSupported || (!areAllOrientationsSupported && newOrientation != .portraitUpsideDown)) { orientation = newOrientation if UIDevice.current.userInterfaceIdiom == .phone { UIViewController.attemptRotationToDeviceOrientation() } } } private func resetOrientation() { UIDevice.current.setValue(UIDevice.current.orientation.rawValue, forKey: "orientation") UIViewController.attemptRotationToDeviceOrientation() } } // swiftlint:enable file_length