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