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