UNPKG

communication-react-19

Version:

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

271 lines 16.3 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react'; import { mergeStyles, Stack } from '@fluentui/react'; import { Spinner, SpinnerSize } from '@fluentui/react'; import { callCompositeContainerStyles, compositeOuterContainerStyles } from './styles/CallWithChatCompositeStyles'; import { chatSpinnerContainerStyles } from './styles/CallWithChatCompositeStyles'; import { CallWithChatBackedCallAdapter } from './adapter/CallWithChatBackedCallAdapter'; import { CallWithChatBackedChatAdapter } from './adapter/CallWithChatBackedChatAdapter'; import { ChatComposite } from '../ChatComposite'; import { BaseProvider } from '../common/BaseComposite'; import { useTheme } from "../../../../react-components/src"; import { useId } from '@fluentui/react-hooks'; import { containerDivStyles } from '../common/ContainerRectProps'; import { useCallWithChatCompositeStrings } from './hooks/useCallWithChatCompositeStrings'; import { CallCompositeInner } from '../CallComposite/CallComposite'; import { ChatButtonWithUnreadMessagesBadge } from './ChatButton/ChatButtonWithUnreadMessagesBadge'; import { getDesktopCommonButtonStyles } from '../common/ControlBar/CommonCallControlBar'; import { isDisabled } from '../CallComposite/utils'; import { SidePaneHeader } from '../common/SidePaneHeader'; import { useUnreadMessagesTracker } from './ChatButton/useUnreadMessagesTracker'; import { useLocale } from '../localization'; const CallWithChatScreen = (props) => { var _a, _b; const { callWithChatAdapter, fluentTheme, formFactor = 'desktop' } = props; const { surveyOptions } = props; const mobileView = formFactor === 'mobile'; if (!callWithChatAdapter) { throw new Error('CallWithChatAdapter is undefined'); } const callAdapter = useMemo(() => new CallWithChatBackedCallAdapter(callWithChatAdapter), [callWithChatAdapter]); const [currentCallState, setCurrentCallState] = useState(); const [isChatInitialized, setIsChatInitialized] = useState(false); const [currentPage, setCurrentPage] = useState(); const [isChatOpen, setIsChatOpen] = useState(false); const containerRef = useRef(null); useEffect(() => { const updateCallWithChatPage = (newState) => { var _a; setCurrentPage(newState.page); setCurrentCallState((_a = newState.call) === null || _a === void 0 ? void 0 : _a.state); setIsChatInitialized(newState.chat ? true : false); }; updateCallWithChatPage(callWithChatAdapter.getState()); callWithChatAdapter.onStateChange(updateCallWithChatPage); return () => { callWithChatAdapter.offStateChange(updateCallWithChatPage); }; }, [callWithChatAdapter]); const chatAdapter = useMemo(() => { return new CallWithChatBackedChatAdapter(callWithChatAdapter); }, [callWithChatAdapter]); /** Constant setting of id for the parent stack of the composite */ const compositeParentDivId = useId('callWithChatCompositeParentDiv-internal'); const closeChat = useCallback(() => { setIsChatOpen(false); }, []); const openChat = useCallback(() => { setIsChatOpen(true); // timeout is required to give the window time to render the sendbox so we have something to send focus to. // TODO: Selecting elements in the DOM via attributes is not stable. We should expose an API from ChatComposite to be able to focus on the sendbox. const chatFocusTimeout = setInterval(() => { const callWithChatCompositeRootDiv = document.querySelector(`[id="${compositeParentDivId}"]`); const sendbox = callWithChatCompositeRootDiv === null || callWithChatCompositeRootDiv === void 0 ? void 0 : callWithChatCompositeRootDiv.querySelector(`[id="sendbox"]`); if (sendbox !== null) { sendbox.focus(); clearInterval(chatFocusTimeout); } }, 3); setTimeout(() => { clearInterval(chatFocusTimeout); }, 300); }, [compositeParentDivId]); const isOnHold = currentPage === 'hold'; useEffect(() => { if (isOnHold) { closeChat(); } }, [closeChat, isOnHold]); const hasJoinedCall = !!(currentPage && hasJoinedCallFn(currentPage, currentCallState !== null && currentCallState !== void 0 ? currentCallState : 'None')); const toggleChat = useCallback(() => { if (isChatOpen || !hasJoinedCall) { closeChat(); } else { openChat(); } }, [closeChat, hasJoinedCall, isChatOpen, openChat]); const callWithChatStrings = useCallWithChatCompositeStrings(); const chatButtonStrings = useMemo(() => ({ label: callWithChatStrings.chatButtonLabel, tooltipOffContent: callWithChatStrings.chatButtonTooltipOpen, tooltipOnContent: callWithChatStrings.chatButtonTooltipClose }), [callWithChatStrings]); const theme = useTheme(); const commonButtonStyles = useMemo(() => (!mobileView ? getDesktopCommonButtonStyles(theme) : undefined), [mobileView, theme]); const showChatButton = checkShowChatButton(props.callControls); const chatButtonDisabled = showChatButton && (checkChatButtonIsDisabled(props.callControls) || !hasJoinedCall || isOnHold); const chatTabHeaderProps = useMemo(() => mobileView && showChatButton ? { onClick: toggleChat, disabled: chatButtonDisabled } : undefined, [chatButtonDisabled, mobileView, toggleChat, showChatButton]); const unreadChatMessagesCount = useUnreadMessagesTracker(chatAdapter, isChatOpen, isChatInitialized); const customChatButton = useCallback((args) => ({ placement: mobileView ? 'primary' : 'secondary', onRenderButton: () => (React.createElement(ChatButtonWithUnreadMessagesBadge, { checked: isChatOpen, showLabel: args.displayType !== 'compact', onClick: toggleChat, disabled: chatButtonDisabled, strings: chatButtonStrings, styles: commonButtonStyles, newMessageLabel: callWithChatStrings.chatButtonNewMessageNotificationLabel, unreadChatMessagesCount: unreadChatMessagesCount, // As chat is disabled when on hold, we don't want to show the unread badge when on hold hideUnreadChatMessagesBadge: isOnHold, disableTooltip: mobileView })) }), [ callWithChatStrings.chatButtonNewMessageNotificationLabel, chatButtonStrings, commonButtonStyles, isChatOpen, chatButtonDisabled, mobileView, toggleChat, unreadChatMessagesCount, isOnHold ]); const callControlOptionsFromProps = useMemo(() => (Object.assign({}, (typeof props.callControls === 'object' ? props.callControls : {}))), [props.callControls]); const injectedCustomButtonsFromProps = useMemo(() => { var _a; return [...((_a = callControlOptionsFromProps.onFetchCustomButtonProps) !== null && _a !== void 0 ? _a : [])]; }, [callControlOptionsFromProps]); const callCompositeOptions = useMemo(() => ({ callControls: props.callControls === false ? false : Object.assign(Object.assign({}, callControlOptionsFromProps), { onFetchCustomButtonProps: [ ...(showChatButton ? [customChatButton] : []), ...injectedCustomButtonsFromProps ], legacyControlBarExperience: false }), /* @conditional-compile-remove(call-readiness) */ deviceChecks: props.deviceChecks, /* @conditional-compile-remove(call-readiness) */ onNetworkingTroubleShootingClick: props.onNetworkingTroubleShootingClick, /* @conditional-compile-remove(call-readiness) */ onPermissionsTroubleshootingClick: props.onPermissionsTroubleshootingClick, /* @conditional-compile-remove(unsupported-browser) */ onEnvironmentInfoTroubleshootingClick: props.onEnvironmentInfoTroubleshootingClick, remoteVideoTileMenuOptions: props.remoteVideoTileMenuOptions, galleryOptions: props.galleryOptions, localVideoTile: props.localVideoTile, surveyOptions: surveyOptions, branding: { logo: props.logo, backgroundImage: props.backgroundImage }, spotlight: props.spotlight, joinCallOptions: props.joinCallOptions }), [ props.callControls, callControlOptionsFromProps, showChatButton, customChatButton, injectedCustomButtonsFromProps, /* @conditional-compile-remove(call-readiness) */ props.deviceChecks, /* @conditional-compile-remove(unsupported-browser) */ props.onEnvironmentInfoTroubleshootingClick, /* @conditional-compile-remove(call-readiness) */ props.onNetworkingTroubleShootingClick, /* @conditional-compile-remove(call-readiness) */ props.onPermissionsTroubleshootingClick, props.galleryOptions, props.localVideoTile, props.remoteVideoTileMenuOptions, surveyOptions, props.logo, props.backgroundImage, props.spotlight, props.joinCallOptions ]); const chatCompositeOptions = useMemo(() => ({ topic: false, /* @conditional-compile-remove(chat-composite-participant-pane) */ participantPane: false, /* @conditional-compile-remove(file-sharing-acs) */ attachmentOptions: props.attachmentOptions, /* @conditional-compile-remove(rich-text-editor-composite-support) */ richTextEditor: props.richTextEditor }), [ /* @conditional-compile-remove(file-sharing-acs) */ props.attachmentOptions, /* @conditional-compile-remove(rich-text-editor-composite-support) */ props.richTextEditor ]); const chatSpinnerLabel = useLocale().strings.callWithChat.chatContentSpinnerLabel; const onRenderChatContent = useCallback(() => { if (!isChatInitialized) { return (React.createElement(Stack, { styles: chatSpinnerContainerStyles }, React.createElement(Spinner, { label: chatSpinnerLabel, size: SpinnerSize.large }))); } return (React.createElement(ChatComposite, { adapter: chatAdapter, fluentTheme: theme, options: chatCompositeOptions, onFetchAvatarPersonaData: props.onFetchAvatarPersonaData })); }, [chatAdapter, props.onFetchAvatarPersonaData, chatCompositeOptions, theme, isChatInitialized, chatSpinnerLabel]); let chatPaneTitle = callWithChatStrings.chatPaneTitle; // If breakout room settings are defined then we know we are in a breakout room so we should // use the breakout room chat pane title. if ((_b = (_a = callAdapter.getState().call) === null || _a === void 0 ? void 0 : _a.breakoutRooms) === null || _b === void 0 ? void 0 : _b.breakoutRoomSettings) { chatPaneTitle = callWithChatStrings.breakoutRoomChatPaneTitle; } const sidePaneHeaderRenderer = useCallback(() => { var _a; return (React.createElement(SidePaneHeader, { headingText: chatPaneTitle, onClose: closeChat, dismissSidePaneButtonAriaLabel: (_a = callWithChatStrings.dismissSidePaneButtonLabel) !== null && _a !== void 0 ? _a : '', mobileView: mobileView, chatButtonPresent: showChatButton })); }, [chatPaneTitle, closeChat, callWithChatStrings.dismissSidePaneButtonLabel, mobileView, showChatButton]); const sidePaneContentRenderer = useMemo(() => (hasJoinedCall ? onRenderChatContent : undefined), [hasJoinedCall, onRenderChatContent]); const sidePaneRenderer = useMemo(() => ({ contentRenderer: sidePaneContentRenderer, headerRenderer: sidePaneHeaderRenderer, id: 'chat' }), [sidePaneContentRenderer, sidePaneHeaderRenderer]); const overrideSidePaneProps = useMemo(() => ({ renderer: sidePaneRenderer, isActive: isChatOpen, persistRenderingWhenClosed: true }), [isChatOpen, sidePaneRenderer]); const onSidePaneIdChange = useCallback((sidePaneId) => { // If the pane is switched to something other than chat, removing rendering chat. if (sidePaneId && sidePaneId !== 'chat') { closeChat(); } }, [closeChat]); // 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(() => { callAdapter.on('callEnded', closeChat); return () => { callAdapter.off('callEnded', closeChat); }; }, [callAdapter, closeChat]); return (React.createElement("div", { ref: containerRef, className: mergeStyles(containerDivStyles) }, React.createElement(Stack, { verticalFill: true, grow: true, styles: compositeOuterContainerStyles, id: compositeParentDivId }, React.createElement(Stack, { horizontal: true, grow: true }, React.createElement(Stack.Item, { grow: true, styles: callCompositeContainerStyles(mobileView) }, React.createElement(CallCompositeInner, Object.assign({}, props, { formFactor: formFactor, options: callCompositeOptions, adapter: callAdapter, fluentTheme: fluentTheme, callInvitationUrl: props.joinInvitationURL, overrideSidePane: overrideSidePaneProps, onSidePaneIdChange: onSidePaneIdChange, mobileChatTabHeader: chatTabHeaderProps, onCloseChatPane: closeChat }))))))); }; /** * CallWithChatComposite brings together key components to provide a full call with chat experience out of the box. * * @public */ export const CallWithChatComposite = (props) => { var _a, _b; const { adapter, fluentTheme, rtl, formFactor, joinInvitationURL, options } = props; return (React.createElement(BaseProvider, { fluentTheme: fluentTheme, rtl: rtl, locale: props.locale, icons: props.icons, formFactor: props.formFactor }, React.createElement(CallWithChatScreen, Object.assign({}, props, { /* @conditional-compile-remove(call-readiness) */ deviceChecks: options === null || options === void 0 ? void 0 : options.deviceChecks, callWithChatAdapter: adapter, formFactor: formFactor, callControls: options === null || options === void 0 ? void 0 : options.callControls, joinInvitationURL: joinInvitationURL, fluentTheme: fluentTheme, remoteVideoTileMenuOptions: options === null || options === void 0 ? void 0 : options.remoteVideoTileMenuOptions, /* @conditional-compile-remove(file-sharing-acs) */ attachmentOptions: options === null || options === void 0 ? void 0 : options.attachmentOptions, localVideoTile: options === null || options === void 0 ? void 0 : options.localVideoTile, galleryOptions: options === null || options === void 0 ? void 0 : options.galleryOptions, logo: (_a = options === null || options === void 0 ? void 0 : options.branding) === null || _a === void 0 ? void 0 : _a.logo, backgroundImage: (_b = options === null || options === void 0 ? void 0 : options.branding) === null || _b === void 0 ? void 0 : _b.backgroundImage, surveyOptions: options === null || options === void 0 ? void 0 : options.surveyOptions, spotlight: options === null || options === void 0 ? void 0 : options.spotlight, /* @conditional-compile-remove(rich-text-editor-composite-support) */ richTextEditor: options === null || options === void 0 ? void 0 : options.richTextEditor, joinCallOptions: options === null || options === void 0 ? void 0 : options.joinCallOptions })))); }; const hasJoinedCallFn = (page, callStatus) => { return ((page === 'call' && (callStatus === 'Connected' || callStatus === 'RemoteHold' || callStatus === 'Disconnecting')) || (page === 'hold' && (callStatus === 'LocalHold' || callStatus === 'Disconnecting'))); }; const checkShowChatButton = (callControls) => { if (callControls === undefined || callControls === true) { return true; } if (callControls === false) { return false; } return callControls.chatButton !== false; }; const checkChatButtonIsDisabled = (callControls) => { return typeof callControls === 'object' && isDisabled(callControls === null || callControls === void 0 ? void 0 : callControls.chatButton); }; //# sourceMappingURL=CallWithChatComposite.js.map