UNPKG

communication-react-19

Version:

React library for building modern communication user experiences utilizing Azure Communication Services (React 19 compatible fork)

253 lines 19.2 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import React, { useCallback, useMemo, useRef, useState } from 'react'; /* @conditional-compile-remove(call-readiness) */ import { useEffect } from 'react'; import { useAdaptedSelector } from '../hooks/useAdaptedSelector'; import { useHandlers } from '../hooks/useHandlers'; import { LocalDeviceSettings } from '../components/LocalDeviceSettings'; import { StartCallButton } from '../components/StartCallButton'; import { devicePermissionSelector } from '../selectors/devicePermissionSelector'; import { useSelector } from '../hooks/useSelector'; import { CameraButton, DevicesButton, ErrorBar, _useContainerWidth, useTheme } from "../../../../../react-components/src"; import { getCallingSelector } from "../../../../../calling-component-bindings/src"; import { Image, mergeStyles, Panel, PanelType, Stack } from '@fluentui/react'; import { callDetailsContainerStyles, configurationCenteredContent, configurationSectionStyle, deviceConfigurationStackTokens, fillWidth, logoStyles, panelFocusProps, panelStyles, startCallButtonStyleDesktop } from '../styles/CallConfiguration.styles'; import { LocalPreview } from '../components/LocalPreview'; import { callDetailsStyleDesktop, callDetailsStyleMobile, configurationStackTokensDesktop, configurationStackTokensMobile, configurationContainerStyle, selectionContainerStyle, startCallButtonContainerStyleDesktop, startCallButtonContainerStyleMobile, startCallButtonStyleMobile, titleContainerStyleDesktop, titleContainerStyleMobile } from '../styles/CallConfiguration.styles'; import { useLocale } from '../../localization'; import { bannerNotificationStyles } from '../styles/CallPage.styles'; import { usePropsFor } from '../hooks/usePropsFor'; import { ConfigurationPageErrorBar } from '../components/ConfigurationPageErrorBar'; import { _isSafari } from '../utils'; /* @conditional-compile-remove(call-readiness) */ import { getDevicePermissionState } from '../utils'; /* @conditional-compile-remove(call-readiness) */ import { CallReadinessModal, CallReadinessModalFallBack } from '../components/CallReadinessModal'; import { VIDEO_EFFECTS_SIDE_PANE_WIDTH_REM, useVideoEffectsPane } from '../components/SidePane/useVideoEffectsPane'; import { SidePane } from '../components/SidePane/SidePane'; import { useIsParticularSidePaneOpen } from '../components/SidePane/SidePaneProvider'; import { localVideoSelector } from '../../CallComposite/selectors/localVideoStreamSelector'; import { SvgWithWordWrapping } from '../components/SvgWithWordWrapping'; import { getMicrophones, getRole } from '../selectors/baseSelectors'; import { getEnvironmentInfo } from '../selectors/baseSelectors'; /* @conditional-compile-remove(call-readiness) */ import { getCameras } from '../selectors/baseSelectors'; /** * @private */ export const ConfigurationPage = (props) => { var _a; const { startCallHandler, mobileView, modalLayerHostId, /* @conditional-compile-remove(call-readiness) */ deviceChecks, /* @conditional-compile-remove(call-readiness) */ onPermissionsTroubleshootingClick, /* @conditional-compile-remove(call-readiness) */ onNetworkingTroubleShootingClick, joinCallOptions = { microphoneCheck: 'requireMicrophoneAvailable' } } = props; const theme = useTheme(); const options = useAdaptedSelector(getCallingSelector(DevicesButton)); const localDeviceSettingsHandlers = useHandlers(LocalDeviceSettings); const { video: cameraPermissionGranted, audio: microphonePermissionGranted } = useSelector(devicePermissionSelector); const environmentInfo = useSelector(getEnvironmentInfo); /* @conditional-compile-remove(call-readiness) */ // use permission API to get video and audio permission state const [videoState, setVideoState] = useState(undefined); /* @conditional-compile-remove(call-readiness) */ const [audioState, setAudioState] = useState(undefined); /* @conditional-compile-remove(call-readiness) */ getDevicePermissionState(environmentInfo, setVideoState, setAudioState, videoState, audioState); const configContainerRef = useRef(null); const configWidth = _useContainerWidth(configContainerRef); /** * We want to stack the two sections (preview and devices) when the container is less than 450 wide. * We lose size calculation when the container is less than 450 wide, so we stack the two sections. */ const stackConfig = !!configWidth && configWidth < 450; const errorBarProps = usePropsFor(ErrorBar); const microphones = useSelector(getMicrophones); let disableStartCallButton = (!microphonePermissionGranted || (microphones === null || microphones === void 0 ? void 0 : microphones.length) === 0) && joinCallOptions.microphoneCheck === 'requireMicrophoneAvailable'; const role = useSelector(getRole); const isCameraOn = useSelector(localVideoSelector).isAvailable; const [cameraLoading, setCameraLoading] = useState(false); const switchCamera = useCallback((device, options) => __awaiter(void 0, void 0, void 0, function* () { // Only set camera to be loading if we are switching source while the camera is on setCameraLoading(isCameraOn); try { yield localDeviceSettingsHandlers.onSelectCamera(device, options); } finally { setCameraLoading(false); } }), [localDeviceSettingsHandlers, isCameraOn]); const { onToggleCamera } = usePropsFor(CameraButton); const toggleCamera = useCallback((options) => __awaiter(void 0, void 0, void 0, function* () { // Only set camera to loading if we are turning on the camera (i.e. the camera was off) setCameraLoading(!isCameraOn); try { yield onToggleCamera(options); } finally { setCameraLoading(false); } }), [isCameraOn, onToggleCamera]); let filteredLatestErrors = props.latestErrors; // TODO: move this logic to the error bar selector once role is plumbed from the headless SDK if (role !== 'Consumer') { filteredLatestErrors = filteredLatestErrors.filter((e) => e.type !== 'callCameraAccessDenied' && e.type !== 'callCameraAccessDeniedSafari'); } if ((useIsParticularSidePaneOpen('videoeffects') || !isCameraOn) && errorBarProps) { filteredLatestErrors = filteredLatestErrors.filter((e) => e.type !== 'unableToStartVideoEffect'); } if (role === 'Consumer') { // If user's role permissions do not allow access to the microphone button then DO NOT disable the start call button // because microphone device permission is not needed for the user's role disableStartCallButton = false; } /* @conditional-compile-remove(call-readiness) */ const cameras = useSelector(getCameras); /* @conditional-compile-remove(call-readiness) */ // Overrides role permissions if CallCompositeOptions deviceChecks are set if (deviceChecks) { if (['doNotPrompt', 'optional'].includes(deviceChecks.camera) && ['doNotPrompt', 'optional'].includes(deviceChecks.microphone)) { disableStartCallButton = false; } else if (deviceChecks.camera === 'required') { disableStartCallButton = !cameraPermissionGranted || (cameras === null || cameras === void 0 ? void 0 : cameras.length) === 0; } } const locale = useLocale(); const title = locale.strings.call.configurationPageTitle.length > 0 ? (React.createElement(Stack.Item, { className: mobileView ? titleContainerStyleMobile(theme) : titleContainerStyleDesktop(theme) }, React.createElement(SvgWithWordWrapping, { width: mobileView ? 325 : 445, lineHeightPx: 16 * 1.5, bufferHeightPx: 16, text: locale.strings.call.configurationPageTitle, role: "heading" }))) : (React.createElement(React.Fragment, null)); const callDescription = locale.strings.call.configurationPageCallDetails && (React.createElement(Stack.Item, { className: mobileView ? callDetailsStyleMobile(theme) : callDetailsStyleDesktop(theme) }, locale.strings.call.configurationPageCallDetails)); const mobileWithPreview = mobileView && role !== 'Consumer'; /* @conditional-compile-remove(call-readiness) */ const permissionsState = useMemo(() => ({ // fall back to using cameraPermissionGranted and microphonePermissionGranted if permission API is not supported camera: videoState && videoState !== 'unsupported' ? cameraPermissionGranted !== false ? videoState : 'denied' : cameraPermissionGranted !== false ? cameraPermissionGranted ? 'granted' : 'prompt' : 'denied', microphone: audioState && audioState !== 'unsupported' ? microphonePermissionGranted !== false ? audioState : 'denied' : microphonePermissionGranted !== false ? microphonePermissionGranted ? 'granted' : 'prompt' : 'denied' }), [videoState, audioState, cameraPermissionGranted, microphonePermissionGranted]); /* @conditional-compile-remove(call-readiness) */ const networkErrors = errorBarProps.activeErrorMessages.filter((message) => message.type === 'callNetworkQualityLow'); /* @conditional-compile-remove(call-readiness) */ const [isPermissionsModalDismissed, setIsPermissionsModalDismissed] = useState(true); // When permission API is not available, we want to show screen saying checking for access (disappears on its own) // then based on permission setting, we show permission denied or nothing /* @conditional-compile-remove(call-readiness) */ const [minimumFallbackTimerElapsed, setMinimumFallbackTimerElapsed] = useState(false); /* @conditional-compile-remove(call-readiness) */ useEffect(() => { setTimeout(() => { setMinimumFallbackTimerElapsed(true); }, 2000); }, []); /* @conditional-compile-remove(call-readiness) */ const forceShowingCheckPermissions = !minimumFallbackTimerElapsed; const { toggleVideoEffectsPane, closeVideoEffectsPane, isVideoEffectsPaneOpen } = useVideoEffectsPane(props.updateSidePaneRenderer, mobileView, props.latestErrors, props.onDismissError); const startCall = useCallback(() => __awaiter(void 0, void 0, void 0, function* () { closeVideoEffectsPane(); startCallHandler(); }), [startCallHandler, closeVideoEffectsPane]); const panelLayerProps = useMemo(() => ({ hostId: modalLayerHostId }), [modalLayerHostId]); const filteredErrorBarProps = useMemo(() => (Object.assign(Object.assign({}, errorBarProps), { activeErrorMessages: filteredLatestErrors })), [errorBarProps, filteredLatestErrors]); const containerStyles = useMemo(() => { var _a; return configurationContainerStyle(!mobileView, (_a = props.backgroundImage) === null || _a === void 0 ? void 0 : _a.url); }, [mobileView, (_a = props.backgroundImage) === null || _a === void 0 ? void 0 : _a.url]); return (React.createElement("div", { ref: configContainerRef, className: mergeStyles(containerStyles) }, React.createElement(Stack, { styles: bannerNotificationStyles }, React.createElement(ConfigurationPageErrorBar /* @conditional-compile-remove(call-readiness) */ // show trouble shooting error bar when encountering network error/ permission error , { /* @conditional-compile-remove(call-readiness) */ // show trouble shooting error bar when encountering network error/ permission error showTroubleShootingErrorBar: !cameraPermissionGranted || !microphonePermissionGranted || networkErrors.length > 0, /* @conditional-compile-remove(call-readiness) */ permissionsState: permissionsState, /* @conditional-compile-remove(call-readiness) */ onNetworkingTroubleShootingClick: onNetworkingTroubleShootingClick, /* @conditional-compile-remove(call-readiness) */ onPermissionsTroubleshootingClick: onPermissionsTroubleshootingClick, errorBarProps: filteredErrorBarProps, onDismissError: props.onDismissError })), /* @conditional-compile-remove(call-readiness) */ // show the following screen if permission API is availible (not unsupported) and videoState, audioState is assigned values videoState && videoState !== 'unsupported' && audioState && audioState !== 'unsupported' && (React.createElement(CallReadinessModal, { modalLayerHostId: modalLayerHostId, mobileView: mobileView, /* @conditional-compile-remove(unsupported-browser) */ environmentInfo: environmentInfo, permissionsState: permissionsState, isPermissionsModalDismissed: isPermissionsModalDismissed, setIsPermissionsModalDismissed: setIsPermissionsModalDismissed, onPermissionsTroubleshootingClick: onPermissionsTroubleshootingClick, doNotPromptCamera: (deviceChecks === null || deviceChecks === void 0 ? void 0 : deviceChecks.camera) === 'doNotPrompt' })), /* @conditional-compile-remove(call-readiness) */ // show the following screen if permission API is not availible (unsupported) and videoState, audioState is assigned values videoState && audioState && (videoState === 'unsupported' || audioState === 'unsupported') && (React.createElement(CallReadinessModalFallBack /* @conditional-compile-remove(call-readiness) */ , { /* @conditional-compile-remove(call-readiness) */ modalLayerHostId: modalLayerHostId, mobileView: mobileView, checkPermissionModalShowing: forceShowingCheckPermissions, permissionsState: permissionsState, isPermissionsModalDismissed: isPermissionsModalDismissed, /* @conditional-compile-remove(unsupported-browser) */ environmentInfo: environmentInfo, setIsPermissionsModalDismissed: setIsPermissionsModalDismissed, onPermissionsTroubleshootingClick: onPermissionsTroubleshootingClick })), React.createElement(Stack, { verticalFill: true, grow: true, horizontal: true, className: fillWidth }, React.createElement(Stack, { className: configurationCenteredContent(mobileWithPreview, !!props.logo), verticalAlign: stackConfig && !mobileView ? undefined : 'center', verticalFill: mobileWithPreview, tokens: mobileWithPreview ? configurationStackTokensMobile : configurationStackTokensDesktop }, React.createElement(Stack.Item, { styles: callDetailsContainerStyles }, React.createElement(Logo, { logo: props.logo }), title, callDescription), React.createElement(Stack, { horizontal: configHorizontal(mobileWithPreview, mobileView, stackConfig), horizontalAlign: mobileWithPreview ? 'stretch' : 'center', verticalFill: mobileWithPreview, tokens: deviceConfigurationStackTokens }, role !== 'Consumer' && (React.createElement(LocalPreview, { mobileView: mobileWithPreview, showDevicesButton: mobileView, onToggleCamera: toggleCamera, cameraLoading: cameraLoading && !isCameraOn })), React.createElement(Stack, { styles: mobileView ? undefined : configurationSectionStyle }, !mobileWithPreview && (React.createElement(Stack, { className: mobileView ? undefined : selectionContainerStyle(theme, _isSafari(environmentInfo)) }, React.createElement(LocalDeviceSettings, Object.assign({}, options, localDeviceSettingsHandlers, { onSelectCamera: switchCamera, cameraPermissionGranted: cameraPermissionGrantedTrampoline(cameraPermissionGranted, /* @conditional-compile-remove(call-readiness) */ videoState), microphonePermissionGranted: micPermissionGrantedTrampoline(microphonePermissionGranted, /* @conditional-compile-remove(call-readiness) */ audioState), /* @conditional-compile-remove(call-readiness) */ onClickEnableDevicePermission: () => { setIsPermissionsModalDismissed(true); }, onClickVideoEffects: toggleVideoEffectsPane })))), React.createElement(Stack, { styles: mobileWithPreview ? startCallButtonContainerStyleMobile : startCallButtonContainerStyleDesktop, horizontalAlign: mobileWithPreview ? 'stretch' : 'end' }, React.createElement(StartCallButton, { className: mobileWithPreview ? startCallButtonStyleMobile : startCallButtonStyleDesktop, onClick: startCall, disabled: disableStartCallButton, hideIcon: true }))))), React.createElement(Panel, { isOpen: isVideoEffectsPaneOpen, hasCloseButton: false, isBlocking: false, isHiddenOnDismiss: false, styles: panelStyles, focusTrapZoneProps: panelFocusProps, layerProps: panelLayerProps, type: PanelType.custom, customWidth: `${VIDEO_EFFECTS_SIDE_PANE_WIDTH_REM}rem` }, React.createElement(SidePane, { ariaLabel: isVideoEffectsPaneOpen ? locale.strings.call.videoEffectsPaneAriaLabel : undefined, mobileView: props.mobileView, updateSidePaneRenderer: props.updateSidePaneRenderer, maxWidth: `${VIDEO_EFFECTS_SIDE_PANE_WIDTH_REM}rem`, minWidth: `${VIDEO_EFFECTS_SIDE_PANE_WIDTH_REM}rem` }))))); }; const cameraPermissionGrantedTrampoline = (cameraPermissionGranted, videoState) => { /* @conditional-compile-remove(call-readiness) */ return videoState && videoState !== 'unsupported' ? videoState === 'granted' : cameraPermissionGranted; return cameraPermissionGranted; }; const micPermissionGrantedTrampoline = (microphonePermissionGranted, audioState) => { /* @conditional-compile-remove(call-readiness) */ return audioState && audioState !== 'unsupported' ? audioState === 'granted' : microphonePermissionGranted; return microphonePermissionGranted; }; const Logo = (props) => { if (!props.logo) { return React.createElement(React.Fragment, null); } return React.createElement(Image, { styles: logoStyles(props.logo.shape), src: props.logo.url, alt: props.logo.alt }); }; const configHorizontal = (mobileWithPreview, isMobile, configTooNarrow) => { if (configTooNarrow && !isMobile) { return false; } return !mobileWithPreview; }; //# sourceMappingURL=ConfigurationPage.js.map