@azure/communication-react
Version:
React library for building modern communication user experiences utilizing Azure Communication Services
129 lines • 9.34 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