@azure/communication-react
Version:
React library for building modern communication user experiences utilizing Azure Communication Services
290 lines • 22.7 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';
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';
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;
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* () {
const constrain = getQueryOptions({
role
});
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,
// 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((_c = (_b = (_a = props.options) === null || _a === void 0 ? void 0 : _a.galleryOptions) === null || _b === void 0 ? void 0 : _b.layout) !== null && _c !== void 0 ? _c : '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: (_d = props.options) === null || _d === void 0 ? void 0 : _d.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, capabilitiesChangedNotificationBarProps: capabilitiesChangedNotificationBarProps, logo: (_f = (_e = props.options) === null || _e === void 0 ? void 0 : _e.branding) === null || _f === void 0 ? void 0 : _f.logo, backgroundImage: (_h = (_g = props.options) === null || _g === void 0 ? void 0 : _g.branding) === null || _h === void 0 ? void 0 : _h.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: (_j = locale.strings.call.leavingCallTitle) !== null && _j !== void 0 ? _j : '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 (!((_l = (_k = props.options) === null || _k === void 0 ? void 0 : _k.surveyOptions) === null || _l === void 0 ? void 0 : _l.disableSurvey)) {
pageElement = React.createElement(SurveyPage, { dataUiId: 'left-call-page', surveyOptions: (_m = props.options) === null || _m === void 0 ? void 0 : _m.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: (_o = props.options) === null || _o === void 0 ? void 0 : _o.disableAutoShowDtmfDialer, notificationOptions: (_p = props.options) === null || _p === void 0 ? void 0 : _p.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: (_q = props.options) === null || _q === void 0 ? void 0 : _q.disableAutoShowDtmfDialer, notificationOptions: (_r = props.options) === null || _r === void 0 ? void 0 : _r.notificationOptions });
}
useEndedCallConsoleErrors(endedCall);
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