communication-react-19
Version:
React library for building modern communication user experiences utilizing Azure Communication Services (React 19 compatible fork)
322 lines • 24.9 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 { ErrorBar, useTheme } from "../../../../react-components/src";
import { NotificationStack } from "../../../../react-components/src";
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { BaseProvider } from '../common/BaseComposite';
import { useLocale } from '../localization';
import { CallAdapterProvider, useAdapter } from './adapter/CallAdapterProvider';
import { CallPage } from './pages/CallPage';
import { ConfigurationPage } from './pages/ConfigurationPage';
import { NoticePage } from './pages/NoticePage';
import { useSelector } from './hooks/useSelector';
import { getAlternateCallerId, getEndedCall, getPage, getRole, getTargetCallees } from './selectors/baseSelectors';
/* @conditional-compile-remove(unsupported-browser) */
import { getEnvironmentInfo } from './selectors/baseSelectors';
import { LobbyPage } from './pages/LobbyPage';
import { TransferPage } from './pages/TransferPage';
import { leavingPageStyle, mainScreenContainerStyleDesktop, mainScreenContainerStyleMobile } from './styles/CallComposite.styles';
import { LayerHost, mergeStyles } from '@fluentui/react';
import { modalLayerHostStyle } from '../common/styles/ModalLocalAndRemotePIP.styles';
import { useId } from '@fluentui/react-hooks';
import { HoldPage } from './pages/HoldPage';
/* @conditional-compile-remove(unsupported-browser) */
import { UnsupportedBrowserPage } from './pages/UnsupportedBrowser';
import { SidePaneProvider } from './components/SidePane/SidePaneProvider';
import { filterLatestNotifications, getEndedCallPageProps, trackNotificationAsDismissed, updateTrackedNotificationsWithActiveNotifications } from './utils';
import { computeComplianceNotification } from './utils';
import { usePropsFor } from './hooks/usePropsFor';
import { deviceCountSelector } from './selectors/deviceCountSelector';
import { capabilitiesChangedInfoAndRoleSelector } from './selectors/capabilitiesChangedInfoAndRoleSelector';
import { useTrackedCapabilityChangedNotifications } from './utils/TrackCapabilityChangedNotifications';
import { useEndedCallConsoleErrors } from './utils/useConsoleErrors';
import { SurveyPage } from './pages/SurveyPage';
import { useAudio } from '../common/AudioProvider';
import { complianceBannerSelector } from './selectors/complianceBannerSelector';
import { devicePermissionSelector } from './selectors/devicePermissionSelector';
const isShowing = (overrideSidePane) => {
return !!(overrideSidePane === null || overrideSidePane === void 0 ? void 0 : overrideSidePane.isActive);
};
const MainScreen = (props) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
const adapter = useAdapter();
const { camerasCount, microphonesCount } = useSelector(deviceCountSelector);
const hasCameras = camerasCount > 0;
const hasMicrophones = microphonesCount > 0;
const role = useSelector(getRole);
const { video: cameraHasPermission, audio: micHasPermission } = useSelector(devicePermissionSelector);
useEffect(() => {
(() => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c, _d;
const constrain = getQueryOptions({ role });
/* @conditional-compile-remove(call-readiness) */
{
constrain.audio = ((_b = (_a = props.options) === null || _a === void 0 ? void 0 : _a.deviceChecks) === null || _b === void 0 ? void 0 : _b.microphone) === 'doNotPrompt' ? false : constrain.audio;
constrain.video = ((_d = (_c = props.options) === null || _c === void 0 ? void 0 : _c.deviceChecks) === null || _d === void 0 ? void 0 : _d.camera) === 'doNotPrompt' ? false : constrain.video;
}
const permissionsResult = yield adapter.askDevicePermission(constrain);
if (permissionsResult === null || permissionsResult === void 0 ? void 0 : permissionsResult.audio) {
adapter.queryMicrophones();
}
if (permissionsResult === null || permissionsResult === void 0 ? void 0 : permissionsResult.video) {
adapter.queryCameras();
}
adapter.querySpeakers();
}))();
}, [
adapter,
role,
/* @conditional-compile-remove(call-readiness) */
(_a = props.options) === null || _a === void 0 ? void 0 : _a.deviceChecks,
// Ensure we re-ask for permissions if the number of devices goes from 0 -> n during a call
// as we cannot request permissions when there are no devices.
hasCameras,
hasMicrophones,
// Ensure we re-query for devices when permission for the device is granted.
cameraHasPermission,
micHasPermission
]);
const { callInvitationUrl, onFetchAvatarPersonaData, onFetchParticipantMenuItems } = props;
const page = useSelector(getPage);
const endedCall = useSelector(getEndedCall);
const [sidePaneRenderer, setSidePaneRenderer] = React.useState();
const [injectedSidePaneProps, setInjectedSidePaneProps] = React.useState();
const [userSetGalleryLayout, setUserSetGalleryLayout] = useState((_d = (_c = (_b = props.options) === null || _b === void 0 ? void 0 : _b.galleryOptions) === null || _c === void 0 ? void 0 : _c.layout) !== null && _d !== void 0 ? _d : 'floatingLocalVideo');
const [userSetOverflowGalleryPosition, setUserSetOverflowGalleryPosition] = useState('Responsive');
const overridePropsRef = useRef(props.overrideSidePane);
useEffect(() => {
setInjectedSidePaneProps(props.overrideSidePane);
// When the injected side pane is opened, clear the previous side pane active state.
// this ensures when the injected side pane is "closed", the previous side pane is not "re-opened".
if (!isShowing(overridePropsRef.current) && isShowing(props.overrideSidePane)) {
setSidePaneRenderer(undefined);
}
overridePropsRef.current = props.overrideSidePane;
}, [props.overrideSidePane]);
const onSidePaneIdChange = props.onSidePaneIdChange;
useEffect(() => {
onSidePaneIdChange === null || onSidePaneIdChange === void 0 ? void 0 : onSidePaneIdChange(sidePaneRenderer === null || sidePaneRenderer === void 0 ? void 0 : sidePaneRenderer.id);
}, [sidePaneRenderer === null || sidePaneRenderer === void 0 ? void 0 : sidePaneRenderer.id, onSidePaneIdChange]);
// When the call ends ensure the side pane is set to closed to prevent the side pane being open if the call is re-joined.
useEffect(() => {
const closeSidePane = () => {
setSidePaneRenderer(undefined);
};
const resetUserGalleryLayout = () => {
if (userSetGalleryLayout === 'togetherMode') {
setUserSetGalleryLayout('floatingLocalVideo');
}
};
const handleCallEnded = () => {
closeSidePane();
resetUserGalleryLayout();
};
adapter.on('callEnded', handleCallEnded);
return () => {
adapter.off('callEnded', handleCallEnded);
};
}, [adapter, userSetGalleryLayout]);
const compositeAudioContext = useAudio();
const capabilitiesChangedInfoAndRole = useSelector(capabilitiesChangedInfoAndRoleSelector);
const capabilitiesChangedNotificationBarProps = useTrackedCapabilityChangedNotifications(capabilitiesChangedInfoAndRole);
// Track the last dismissed errors of any error kind to prevent errors from re-appearing on subsequent page navigation
// This works by tracking the most recent timestamp of any active error type.
// And then tracking when that error type was last dismissed.
const activeErrors = usePropsFor(ErrorBar).activeErrorMessages;
const activeInCallErrors = usePropsFor(NotificationStack).activeErrorMessages;
const activeNotifications = usePropsFor(NotificationStack).activeNotifications;
const complianceProps = useSelector(complianceBannerSelector);
const cachedProps = useRef({
latestBooleanState: {
callTranscribeState: false,
callRecordState: false
},
latestStringState: {
callTranscribeState: 'off',
callRecordState: 'off'
},
lastUpdated: Date.now()
});
const complianceNotification = useMemo(() => {
return computeComplianceNotification(complianceProps, cachedProps);
}, [complianceProps, cachedProps]);
useEffect(() => {
if (complianceNotification) {
for (let i = activeNotifications.length - 1; i >= 0; i--) {
const notification = activeNotifications[i];
if (notification &&
[
'recordingStarted',
'transcriptionStarted',
'recordingStopped',
'transcriptionStopped',
'recordingAndTranscriptionStarted',
'recordingAndTranscriptionStopped',
'recordingStoppedStillTranscribing',
'transcriptionStoppedStillRecording'
].includes(notification.type)) {
activeNotifications.splice(i, 1);
}
}
activeNotifications.push(complianceNotification);
}
setTrackedNotifications((prev) => updateTrackedNotificationsWithActiveNotifications(prev, activeNotifications));
}, [complianceNotification, activeNotifications]);
const [trackedErrors, setTrackedErrors] = useState({});
const [trackedInCallErrors, setTrackedInCallErrors] = useState({});
const [trackedNotifications, setTrackedNotifications] = useState({});
useEffect(() => {
setTrackedErrors((prev) => updateTrackedNotificationsWithActiveNotifications(prev, activeErrors));
setTrackedInCallErrors((prev) => updateTrackedNotificationsWithActiveNotifications(prev, activeInCallErrors));
}, [activeErrors, activeInCallErrors]);
const onDismissError = useCallback((error) => {
setTrackedErrors((prev) => trackNotificationAsDismissed(error.type, prev));
setTrackedInCallErrors((prev) => trackNotificationAsDismissed(error.type, prev));
}, []);
const onDismissNotification = useCallback((notification) => {
setTrackedNotifications((prev) => trackNotificationAsDismissed(notification.type, prev));
}, []);
const latestErrors = useMemo(() => filterLatestNotifications(activeErrors, trackedErrors), [activeErrors, trackedErrors]);
const latestInCallErrors = useMemo(() => filterLatestNotifications(activeInCallErrors, trackedInCallErrors), [activeInCallErrors, trackedInCallErrors]);
const latestNotifications = useMemo(() => {
const result = filterLatestNotifications(activeNotifications, trackedNotifications);
// sort notifications by timestamp from earliest to latest
result.sort((a, b) => { var _a, _b; return ((_a = a.timestamp) !== null && _a !== void 0 ? _a : new Date(Date.now())).getTime() - ((_b = b.timestamp) !== null && _b !== void 0 ? _b : new Date(Date.now())).getTime(); });
return result;
}, [activeNotifications, trackedNotifications]);
const callees = useSelector(getTargetCallees);
const locale = useLocale();
const palette = useTheme().palette;
const alternateCallerId = useSelector(getAlternateCallerId);
const leavePageStyle = useMemo(() => leavingPageStyle(palette), [palette]);
let pageElement;
const [pinnedParticipants, setPinnedParticipants] = useState([]);
switch (page) {
case 'configuration':
pageElement = (React.createElement(ConfigurationPage, { joinCallOptions: (_e = props.options) === null || _e === void 0 ? void 0 : _e.joinCallOptions, mobileView: props.mobileView, startCallHandler: () => {
if (callees) {
adapter.startCall(callees, alternateCallerId ? { alternateCallerId: { phoneNumber: alternateCallerId } } : {});
}
else {
adapter.joinCall({
microphoneOn: 'keep',
cameraOn: 'keep'
});
}
}, updateSidePaneRenderer: setSidePaneRenderer, latestErrors: latestErrors, onDismissError: onDismissError, modalLayerHostId: props.modalLayerHostId,
/* @conditional-compile-remove(call-readiness) */
deviceChecks: (_f = props.options) === null || _f === void 0 ? void 0 : _f.deviceChecks,
/* @conditional-compile-remove(call-readiness) */
onPermissionsTroubleshootingClick: (_g = props.options) === null || _g === void 0 ? void 0 : _g.onPermissionsTroubleshootingClick,
/* @conditional-compile-remove(call-readiness) */
onNetworkingTroubleShootingClick: (_h = props.options) === null || _h === void 0 ? void 0 : _h.onNetworkingTroubleShootingClick, capabilitiesChangedNotificationBarProps: capabilitiesChangedNotificationBarProps, logo: (_k = (_j = props.options) === null || _j === void 0 ? void 0 : _j.branding) === null || _k === void 0 ? void 0 : _k.logo, backgroundImage: (_m = (_l = props.options) === null || _l === void 0 ? void 0 : _l.branding) === null || _m === void 0 ? void 0 : _m.backgroundImage }));
break;
case 'accessDeniedTeamsMeeting':
pageElement = (React.createElement(NoticePage, { iconName: "NoticePageAccessDeniedTeamsMeeting", title: locale.strings.call.failedToJoinTeamsMeetingReasonAccessDeniedTitle, moreDetails: locale.strings.call.failedToJoinTeamsMeetingReasonAccessDeniedMoreDetails, dataUiId: 'access-denied-teams-meeting-page' }));
break;
case 'removedFromCall':
pageElement = (React.createElement(NoticePage, { iconName: "NoticePageRemovedFromCall", title: locale.strings.call.removedFromCallTitle, moreDetails: locale.strings.call.removedFromCallMoreDetails, dataUiId: 'removed-from-call-page' }));
break;
case 'joinCallFailedDueToNoNetwork':
pageElement = (React.createElement(NoticePage, { iconName: "NoticePageJoinCallFailedDueToNoNetwork", title: locale.strings.call.failedToJoinCallDueToNoNetworkTitle, moreDetails: locale.strings.call.failedToJoinCallDueToNoNetworkMoreDetails, dataUiId: 'join-call-failed-due-to-no-network-page' }));
break;
case 'leaving':
pageElement = (React.createElement(NoticePage, { title: (_o = locale.strings.call.leavingCallTitle) !== null && _o !== void 0 ? _o : 'Leaving...', dataUiId: 'leaving-page', pageStyle: leavePageStyle, disableStartCallButton: true }));
break;
case 'badRequest': {
const { title, moreDetails, disableStartCallButton, iconName } = getEndedCallPageProps(locale, endedCall);
pageElement = (React.createElement(NoticePage, { iconName: iconName, title: title, moreDetails: callees ? '' : moreDetails, dataUiId: 'left-call-page', disableStartCallButton: disableStartCallButton }));
break;
}
case 'leftCall': {
const { title, moreDetails, disableStartCallButton, iconName } = getEndedCallPageProps(locale, endedCall);
if (!((_q = (_p = props.options) === null || _p === void 0 ? void 0 : _p.surveyOptions) === null || _q === void 0 ? void 0 : _q.disableSurvey)) {
pageElement = (React.createElement(SurveyPage, { dataUiId: 'left-call-page', surveyOptions: (_r = props.options) === null || _r === void 0 ? void 0 : _r.surveyOptions, iconName: iconName, title: title, moreDetails: moreDetails, disableStartCallButton: disableStartCallButton, mobileView: props.mobileView }));
break;
}
pageElement = (React.createElement(NoticePage, { iconName: iconName, title: title, moreDetails: callees ? '' : moreDetails, dataUiId: 'left-call-page', disableStartCallButton: disableStartCallButton }));
break;
}
case 'lobby':
pageElement = (React.createElement(LobbyPage, { mobileView: props.mobileView, modalLayerHostId: props.modalLayerHostId, options: props.options, updateSidePaneRenderer: setSidePaneRenderer, mobileChatTabHeader: props.mobileChatTabHeader, latestErrors: latestInCallErrors, latestNotifications: latestNotifications, onDismissError: onDismissError, onDismissNotification: onDismissNotification, capabilitiesChangedNotificationBarProps: capabilitiesChangedNotificationBarProps }));
break;
case 'transferring':
pageElement = (React.createElement(TransferPage, { mobileView: props.mobileView, modalLayerHostId: props.modalLayerHostId, options: props.options, updateSidePaneRenderer: setSidePaneRenderer, mobileChatTabHeader: props.mobileChatTabHeader, onFetchAvatarPersonaData: onFetchAvatarPersonaData, latestErrors: latestInCallErrors, latestNotifications: latestNotifications, onDismissError: onDismissError, onDismissNotification: onDismissNotification, capabilitiesChangedNotificationBarProps: capabilitiesChangedNotificationBarProps }));
break;
case 'call':
pageElement = (React.createElement(CallPage, { callInvitationURL: callInvitationUrl, onFetchAvatarPersonaData: onFetchAvatarPersonaData, onFetchParticipantMenuItems: onFetchParticipantMenuItems, mobileView: props.mobileView, modalLayerHostId: props.modalLayerHostId, options: props.options, updateSidePaneRenderer: setSidePaneRenderer, mobileChatTabHeader: props.mobileChatTabHeader, onCloseChatPane: props.onCloseChatPane, latestErrors: latestInCallErrors, latestNotifications: latestNotifications, onDismissError: onDismissError, onDismissNotification: onDismissNotification, galleryLayout: userSetGalleryLayout, onUserSetGalleryLayoutChange: setUserSetGalleryLayout, onSetUserSetOverflowGalleryPosition: setUserSetOverflowGalleryPosition, userSetOverflowGalleryPosition: userSetOverflowGalleryPosition, capabilitiesChangedNotificationBarProps: capabilitiesChangedNotificationBarProps, pinnedParticipants: pinnedParticipants, setPinnedParticipants: setPinnedParticipants, compositeAudioContext: compositeAudioContext, disableAutoShowDtmfDialer: (_s = props.options) === null || _s === void 0 ? void 0 : _s.disableAutoShowDtmfDialer, notificationOptions: (_t = props.options) === null || _t === void 0 ? void 0 : _t.notificationOptions }));
break;
case 'hold':
pageElement = (React.createElement(React.Fragment, null, React.createElement(HoldPage, { mobileView: props.mobileView, modalLayerHostId: props.modalLayerHostId, options: props.options, updateSidePaneRenderer: setSidePaneRenderer, mobileChatTabHeader: props.mobileChatTabHeader, latestErrors: latestInCallErrors, latestNotifications: latestNotifications, onDismissError: onDismissError, onDismissNotification: onDismissNotification, capabilitiesChangedNotificationBarProps: capabilitiesChangedNotificationBarProps })));
break;
}
if (page === 'returningFromBreakoutRoom') {
pageElement = (React.createElement(CallPage, { callInvitationURL: callInvitationUrl, onFetchAvatarPersonaData: onFetchAvatarPersonaData, onFetchParticipantMenuItems: onFetchParticipantMenuItems, mobileView: props.mobileView, modalLayerHostId: props.modalLayerHostId, options: props.options, updateSidePaneRenderer: setSidePaneRenderer, mobileChatTabHeader: props.mobileChatTabHeader, onCloseChatPane: props.onCloseChatPane, latestErrors: latestInCallErrors, latestNotifications: latestNotifications, onDismissError: onDismissError, onDismissNotification: onDismissNotification, galleryLayout: userSetGalleryLayout, onUserSetGalleryLayoutChange: setUserSetGalleryLayout, onSetUserSetOverflowGalleryPosition: setUserSetOverflowGalleryPosition, userSetOverflowGalleryPosition: userSetOverflowGalleryPosition, capabilitiesChangedNotificationBarProps: capabilitiesChangedNotificationBarProps, pinnedParticipants: pinnedParticipants, setPinnedParticipants: setPinnedParticipants, compositeAudioContext: compositeAudioContext, disableAutoShowDtmfDialer: (_u = props.options) === null || _u === void 0 ? void 0 : _u.disableAutoShowDtmfDialer, notificationOptions: (_v = props.options) === null || _v === void 0 ? void 0 : _v.notificationOptions }));
}
useEndedCallConsoleErrors(endedCall);
/* @conditional-compile-remove(unsupported-browser) */
const environmentInfo = useSelector(getEnvironmentInfo);
/* @conditional-compile-remove(unsupported-browser) */
switch (page) {
case 'unsupportedEnvironment':
pageElement = (React.createElement(React.Fragment, null,
/* @conditional-compile-remove(unsupported-browser) */
React.createElement(UnsupportedBrowserPage, { onTroubleshootingClick: (_w = props.options) === null || _w === void 0 ? void 0 : _w.onEnvironmentInfoTroubleshootingClick, environmentInfo: environmentInfo })));
break;
}
if (!pageElement) {
throw new Error('Invalid call composite page');
}
return (React.createElement(SidePaneProvider, { sidePaneRenderer: sidePaneRenderer, overrideSidePane: injectedSidePaneProps }, pageElement));
};
/**
* A customizable UI composite for calling experience.
*
* @remarks Call composite min width/height are as follow:
* - mobile: 17.5rem x 21rem (280px x 336px, with default rem at 16px)
* - desktop: 30rem x 22rem (480px x 352px, with default rem at 16px)
*
* @public
*/
export const CallComposite = (props) => React.createElement(CallCompositeInner, Object.assign({}, props));
/** @private */
export const CallCompositeInner = (props) => {
const { adapter, callInvitationUrl, onFetchAvatarPersonaData, onFetchParticipantMenuItems, options, formFactor = 'desktop' } = props;
const mobileView = formFactor === 'mobile';
const modalLayerHostId = useId('modalLayerhost');
const mainScreenContainerClassName = useMemo(() => {
return mobileView ? mainScreenContainerStyleMobile : mainScreenContainerStyleDesktop;
}, [mobileView]);
return (React.createElement("div", { className: mainScreenContainerClassName },
React.createElement(BaseProvider, Object.assign({}, props),
React.createElement(CallAdapterProvider, { adapter: adapter },
React.createElement(MainScreen, { callInvitationUrl: callInvitationUrl, onFetchAvatarPersonaData: onFetchAvatarPersonaData, onFetchParticipantMenuItems: onFetchParticipantMenuItems, mobileView: mobileView, modalLayerHostId: modalLayerHostId, options: options, onSidePaneIdChange: props.onSidePaneIdChange, overrideSidePane: props.overrideSidePane, mobileChatTabHeader: props.mobileChatTabHeader, onCloseChatPane: props.onCloseChatPane }),
// This layer host is for ModalLocalAndRemotePIP in SidePane. This LayerHost cannot be inside the SidePane
// because when the SidePane is hidden, ie. style property display is 'none', it takes up no space. This causes problems when dragging
// the Modal because the draggable bounds thinks it has no space and will always return to its initial position after dragging.
// Additionally, this layer host cannot be in the Call Arrangement as it needs to be rendered before useMinMaxDragPosition() in
// common/utils useRef is called.
// Warning: this is fragile and works because the call arrangement page is only rendered after the call has connected and thus this
// LayerHost will be guaranteed to have rendered (and subsequently mounted in the DOM). This ensures the DOM element will be available
// before the call to `document.getElementById(modalLayerHostId)` is made.
React.createElement(LayerHost, { id: modalLayerHostId, className: mergeStyles(modalLayerHostStyle) })))));
};
const getQueryOptions = (options) => {
if (options.role === 'Consumer') {
return {
video: false,
audio: true
};
}
return { video: true, audio: true };
};
//# sourceMappingURL=CallComposite.js.map