UNPKG

@azure/communication-react

Version:

React library for building modern communication user experiences utilizing Azure Communication Services

290 lines • 22.7 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 { 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