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
JavaScript
// 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