UNPKG

communication-react-19

Version:

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

134 lines 9.43 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { Stack, FocusZone, Spinner, useTheme } from '@fluentui/react'; import { TextField } from '@fluentui/react'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import { useMemo } from 'react'; import { _Caption } from './Caption'; import { captionContainerClassName, captionsBannerClassName, captionsBannerFullHeightClassName, captionsContainerClassName, loadingBannerFullHeightStyles, loadingBannerStyles } from './styles/Captions.style'; import { rttDisclosureBannerClassName } from './styles/Captions.style'; import { useLocale } from '../localization'; import { RealTimeText } from './RealTimeText'; import { _RTTDisclosureBanner } from './RTTDisclosureBanner'; import { sortCaptionsAndRealTimeTexts } from './utils/sortCaptionsAndRealTimeTexts'; import { expandIconClassName, bannerTitleContainerClassName, realTimeTextInputBoxStyles } from './styles/Captions.style'; import { titleClassName } from './styles/CaptionsSettingsModal.styles'; import { Text, IconButton } from '@fluentui/react'; import { _CaptionsAndRTTAnnouncer } from './CaptionsAndRTTAnnouncer'; const SCROLL_OFFSET_ALLOWANCE = 20; /** * @public * A component for displaying a CaptionsBanner with user icon, displayName and captions text. */ export const CaptionsBanner = (props) => { var _a, _b, _c, _d, _e; const { captions, realTimeTexts, isCaptionsOn, startCaptionsInProgress, onRenderAvatar, formFactor = 'default', captionsOptions, isRealTimeTextOn, onSendRealTimeText, latestLocalRealTimeText } = props; const localeStrings = useLocale().strings.captionsBanner; const strings = Object.assign(Object.assign({}, localeStrings), props.strings); const captionsScrollDivRef = useRef(null); const [isAtBottomOfScroll, setIsAtBottomOfScroll] = useState(true); const theme = useTheme(); const [expandBannerHeight, setExpandBannerHeight] = useState(false); const getTitle = () => { var _a, _b, _c; if (isCaptionsOn && isRealTimeTextOn) { return (_a = strings.captionsAndRealTimeTextContainerTitle) !== null && _a !== void 0 ? _a : ''; } else if (isCaptionsOn) { return (_b = strings.captionsOnlyContainerTitle) !== null && _b !== void 0 ? _b : ''; } else if (isRealTimeTextOn) { return (_c = strings.realTimeTextOnlyContainerTitle) !== null && _c !== void 0 ? _c : ''; } return ''; }; // merge realtimetexts and captions into one array based on timestamp // Combine captions and realTimeTexts into one list const combinedList = useMemo(() => { var _a; return sortCaptionsAndRealTimeTexts(captions, (_a = realTimeTexts === null || realTimeTexts === void 0 ? void 0 : realTimeTexts.completedMessages) !== null && _a !== void 0 ? _a : []); }, [captions, realTimeTexts === null || realTimeTexts === void 0 ? void 0 : realTimeTexts.completedMessages]); const mergedCaptions = useMemo(() => { var _a; return [...combinedList, ...((_a = realTimeTexts === null || realTimeTexts === void 0 ? void 0 : realTimeTexts.currentInProgress) !== null && _a !== void 0 ? _a : []), realTimeTexts === null || realTimeTexts === void 0 ? void 0 : realTimeTexts.myInProgress].slice(-50); }, [combinedList, realTimeTexts]); const scrollToBottom = () => { if (captionsScrollDivRef.current) { captionsScrollDivRef.current.scrollTop = captionsScrollDivRef.current.scrollHeight; } }; const handleScrollToTheBottom = useCallback(() => { if (!captionsScrollDivRef.current) { return; } const atBottom = Math.ceil(captionsScrollDivRef.current.scrollTop) >= captionsScrollDivRef.current.scrollHeight - captionsScrollDivRef.current.clientHeight - SCROLL_OFFSET_ALLOWANCE; setIsAtBottomOfScroll(atBottom); }, []); useEffect(() => { const captionsScrollDiv = captionsScrollDivRef.current; captionsScrollDiv === null || captionsScrollDiv === void 0 ? void 0 : captionsScrollDiv.addEventListener('scroll', handleScrollToTheBottom); return () => { captionsScrollDiv === null || captionsScrollDiv === void 0 ? void 0 : captionsScrollDiv.removeEventListener('scroll', handleScrollToTheBottom); }; }, [handleScrollToTheBottom, isCaptionsOn, isRealTimeTextOn]); useEffect(() => { // only auto scroll to bottom is already is at bottom of scroll before new caption comes in if (isAtBottomOfScroll) { scrollToBottom(); } }, [captions, realTimeTexts, isAtBottomOfScroll]); const [textFieldValue, setTextFieldValue] = useState(''); useEffect(() => { // if the latest real time text sent by myself is final, clear the text field if (latestLocalRealTimeText && !latestLocalRealTimeText.isTyping) { setTextFieldValue(''); } }, [latestLocalRealTimeText]); const handleKeyDown = (event) => { if (event.key === 'Enter') { event.preventDefault(); if (textFieldValue && onSendRealTimeText) { onSendRealTimeText(textFieldValue, true); setTextFieldValue(''); } } }; const realTimeTextDisclosureBannerStrings = { bannerTitle: (_a = strings.realTimeTextBannerTitle) !== null && _a !== void 0 ? _a : '', bannerContent: (_b = strings.realTimeTextBannerContent) !== null && _b !== void 0 ? _b : '', bannerLinkLabel: (_c = strings.realTimeTextBannerLinkLabel) !== null && _c !== void 0 ? _c : '' }; const captionsAndRealTimeText = () => { return (React.createElement(React.Fragment, null, mergedCaptions .filter((caption) => caption) .map((caption) => { if ('message' in caption) { return (React.createElement("div", { key: `RealTimeText - ${caption.id}`, className: captionContainerClassName, "data-is-focusable": true }, React.createElement(RealTimeText, Object.assign({}, caption)))); } return (React.createElement("div", { key: `Captions - ${caption.id}`, className: captionContainerClassName, "data-is-focusable": true }, React.createElement(_Caption, Object.assign({}, caption, { onRenderAvatar: onRenderAvatar })))); }))); }; return (React.createElement(React.Fragment, null, (startCaptionsInProgress || isCaptionsOn || isRealTimeTextOn) && (React.createElement(FocusZone, { shouldFocusOnMount: true, className: captionsContainerClassName, "data-ui-id": "captions-banner" }, React.createElement(_CaptionsAndRTTAnnouncer, { captions: captions, realTimeTexts: realTimeTexts, realTimeTextTitle: (_d = strings.realTimeTextBannerTitle) !== null && _d !== void 0 ? _d : '', captionsTitle: (_e = strings.captionsOnlyContainerTitle) !== null && _e !== void 0 ? _e : '' }), (isCaptionsOn || isRealTimeTextOn) && formFactor === 'compact' && (React.createElement(Stack, { horizontal: true, horizontalAlign: "space-between", verticalAlign: "center", className: bannerTitleContainerClassName }, React.createElement(Text, { className: titleClassName }, getTitle()), React.createElement(IconButton, { "data-ui-id": "captions-banner-expand-icon", iconProps: { iconName: expandBannerHeight ? 'MinimizeIcon' : 'ExpandIcon' }, ariaLabel: expandBannerHeight ? strings.minimizeButtonAriaLabel : strings.expandButtonAriaLabel, onClick: () => setExpandBannerHeight(!expandBannerHeight), styles: expandIconClassName(theme) }))), (isCaptionsOn || isRealTimeTextOn) && (React.createElement("div", { ref: captionsScrollDivRef, className: (captionsOptions === null || captionsOptions === void 0 ? void 0 : captionsOptions.height) === 'full' ? captionsBannerFullHeightClassName(theme) : captionsBannerClassName(formFactor, expandBannerHeight), "data-ui-id": "captions-banner-inner", "data-is-focusable": true }, isRealTimeTextOn && (React.createElement(Stack, { className: rttDisclosureBannerClassName() }, React.createElement(_RTTDisclosureBanner, { strings: realTimeTextDisclosureBannerStrings }))), captionsAndRealTimeText())), isRealTimeTextOn && onSendRealTimeText && (React.createElement(TextField, { styles: realTimeTextInputBoxStyles(theme), placeholder: strings.realTimeTextInputBoxDefaultText, value: textFieldValue, onKeyDown: handleKeyDown, onChange: (_, newValue) => { setTextFieldValue(newValue || ''); onSendRealTimeText(newValue || '', false); }, maxLength: 2000, errorMessage: textFieldValue.length >= 2000 ? strings.realTimeTextInputErrorMessage : undefined })), !isCaptionsOn && !isRealTimeTextOn && (React.createElement(Stack, { verticalAlign: "center", styles: (captionsOptions === null || captionsOptions === void 0 ? void 0 : captionsOptions.height) === 'full' ? loadingBannerFullHeightStyles(theme) : loadingBannerStyles(formFactor), "data-is-focusable": true }, React.createElement(Spinner, { label: strings === null || strings === void 0 ? void 0 : strings.captionsBannerSpinnerText, ariaLive: "assertive", labelPosition: "right" }))))))); }; //# sourceMappingURL=CaptionsBanner.js.map