UNPKG

communication-react-19

Version:

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

294 lines 17.2 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import React, { useCallback, useMemo, useRef, useState } from 'react'; /* @conditional-compile-remove(file-sharing-acs) */ import { useEffect } from 'react'; import { RichTextInputBoxComponent } from './RichTextInputBoxComponent'; import { Icon, Stack } from '@fluentui/react'; import { useLocale } from '../../localization'; import { sendIconStyle } from '../styles/SendBox.styles'; /* @conditional-compile-remove(file-sharing-acs) */ import { useV9CustomStyles } from '../styles/SendBox.styles'; import { InputBoxButton } from '../InputBoxButton'; import { RichTextSendBoxErrors } from './RichTextSendBoxErrors'; import { isMessageTooLong, isSendBoxButtonDisabled, sanitizeText, modifyInlineImagesInContentString } from '../utils/SendBoxUtils'; /* @conditional-compile-remove(rich-text-editor-image-upload) */ import { hasInlineImageContent, getContentWithUpdatedInlineImagesInfo } from '../utils/SendBoxUtils'; import { useTheme } from '../../theming'; import { richTextActionButtonsStyle, sendBoxRichTextEditorStyle } from '../styles/RichTextEditor.styles'; /* @conditional-compile-remove(file-sharing-acs) */ import { _AttachmentUploadCards } from '../Attachment/AttachmentUploadCards'; /* @conditional-compile-remove(file-sharing-acs) */ import { isAttachmentUploadCompleted, hasIncompleteAttachmentUploads, toAttachmentMetadata } from '../utils/SendBoxUtils'; /* @conditional-compile-remove(rich-text-editor-image-upload) */ import { SendBoxErrorBarType } from '../SendBoxErrorBar'; /* @conditional-compile-remove(file-sharing-acs) */ import { attachmentUploadCardsStyles } from '../styles/SendBox.styles'; /* @conditional-compile-remove(file-sharing-acs) */ import { FluentV9ThemeProvider } from '../../theming/FluentV9ThemeProvider'; /* @conditional-compile-remove(rich-text-editor) */ import { richTextSendBoxIdentifier } from './RichTextSendBoxUtils'; /** * A component to render SendBox with Rich Text Editor support. * * @beta */ export const RichTextSendBox = (props) => { const { disabled = false, systemMessage, autoFocus, onSendMessage, onTyping, /* @conditional-compile-remove(file-sharing-acs) */ attachments, /* @conditional-compile-remove(file-sharing-acs) */ onCancelAttachmentUpload, /* @conditional-compile-remove(rich-text-editor-image-upload) */ onPaste, /* @conditional-compile-remove(rich-text-editor-image-upload) */ onInsertInlineImage, /* @conditional-compile-remove(rich-text-editor-image-upload) */ inlineImagesWithProgress, /* @conditional-compile-remove(rich-text-editor-image-upload) */ onRemoveInlineImage } = props; const theme = useTheme(); const locale = useLocale(); const localeStrings = useMemo(() => { /* @conditional-compile-remove(rich-text-editor) */ return locale.strings.richTextSendBox; return locale.strings.sendBox; }, [/* @conditional-compile-remove(rich-text-editor) */ locale.strings.richTextSendBox, locale.strings.sendBox]); const strings = useMemo(() => { return Object.assign(Object.assign({}, localeStrings), props.strings); }, [localeStrings, props.strings]); const [contentValue, setContentValue] = useState(''); const [contentValueOverflow, setContentValueOverflow] = useState(false); /* @conditional-compile-remove(file-sharing-acs) */ const [attachmentUploadsPendingError, setAttachmentUploadsPendingError] = useState(undefined); const editorComponentRef = useRef(null); /* @conditional-compile-remove(file-sharing-acs) */ const customV9Styles = useV9CustomStyles(); const contentTooLongMessage = useMemo(() => (contentValueOverflow ? strings.textTooLong : undefined), [contentValueOverflow, strings.textTooLong]); /* @conditional-compile-remove(file-sharing-acs) */ useEffect(() => { if (!hasIncompleteAttachmentUploads(attachments)) { setAttachmentUploadsPendingError(undefined); } }, [attachments]); const setContent = useCallback((newValue) => { if (newValue === undefined) { return; } setContentValueOverflow(isMessageTooLong(newValue.length)); setContentValue(newValue); }, []); const onChangeHandler = useCallback((newValue, /* @conditional-compile-remove(rich-text-editor-image-upload) */ removedInlineImages) => { /* @conditional-compile-remove(rich-text-editor-image-upload) */ removedInlineImages === null || removedInlineImages === void 0 ? void 0 : removedInlineImages.forEach((removedInlineImage) => onRemoveInlineImage && onRemoveInlineImage(removedInlineImage)); setContent(newValue); }, [setContent, /* @conditional-compile-remove(rich-text-editor-image-upload) */ onRemoveInlineImage]); const hasContent = useMemo(() => { var _a; // get plain text content from the editor to check if the message is empty // as the content may contain tags even when the content is empty const plainTextContent = (_a = editorComponentRef.current) === null || _a === void 0 ? void 0 : _a.getPlainContent(); const hasPlainText = sanitizeText(contentValue !== null && contentValue !== void 0 ? contentValue : '').length > 0 && sanitizeText(plainTextContent !== null && plainTextContent !== void 0 ? plainTextContent : '').length > 0; /* @conditional-compile-remove(rich-text-editor-image-upload) */ const hasInlineImages = hasInlineImageContent(contentValue); return hasPlainText || /* @conditional-compile-remove(rich-text-editor-image-upload) */ hasInlineImages; }, [contentValue]); const sendMessageOnClick = useCallback(() => { /* @conditional-compile-remove(rich-text-editor-image-upload) */ if (inlineImagesWithProgress && inlineImagesWithProgress.length > 0) { const contentWithUpdatedInlineImagesInfo = getContentWithUpdatedInlineImagesInfo(contentValue, inlineImagesWithProgress); const messageTooLong = isMessageTooLong(contentWithUpdatedInlineImagesInfo.length); // Set contentValueOverflow state to display the error bar setContentValueOverflow(messageTooLong); // The change from the setContentValueOverflow in the previous line will not kick in yet. // We need to relay on the local value of messageTooLong to return early if the message is too long. if (messageTooLong) { return; } } if (disabled || contentValueOverflow) { return; } // Don't send message until all attachments have been uploaded successfully /* @conditional-compile-remove(file-sharing-acs) */ setAttachmentUploadsPendingError(undefined); /* @conditional-compile-remove(rich-text-editor-image-upload) */ const hasIncompleteImageUploads = hasIncompleteAttachmentUploads(inlineImagesWithProgress); /* @conditional-compile-remove(file-sharing-acs) */ /* @conditional-compile-remove(rich-text-editor-image-upload) */ if ( /* @conditional-compile-remove(file-sharing-acs) */ hasIncompleteAttachmentUploads(attachments) || /* @conditional-compile-remove(rich-text-editor-image-upload) */ hasIncompleteImageUploads) { /* @conditional-compile-remove(file-sharing-acs) */ let errorMessage = strings.attachmentUploadsPendingError; /* @conditional-compile-remove(rich-text-editor-image-upload) */ if (hasIncompleteImageUploads) { errorMessage = strings.imageUploadsPendingError || ''; } setAttachmentUploadsPendingError({ message: errorMessage, timestamp: Date.now(), /* @conditional-compile-remove(rich-text-editor-image-upload) */ errorBarType: SendBoxErrorBarType.info }); return; } // we don't want to send empty messages including spaces, newlines, tabs // Message can be empty if there is a valid attachment upload if (hasContent || /* @conditional-compile-remove(file-sharing-acs) */ isAttachmentUploadCompleted(attachments)) { const sendMessage = (content) => { var _a, _b; onSendMessage(content, /* @conditional-compile-remove(file-sharing-acs) */ /* @conditional-compile-remove(rich-text-editor-composite-support) */ { /* @conditional-compile-remove(file-sharing-acs) */ attachments: toAttachmentMetadata(attachments), /* @conditional-compile-remove(rich-text-editor-composite-support) */ type: 'html' }); setContentValue(''); (_a = editorComponentRef.current) === null || _a === void 0 ? void 0 : _a.setEmptyContent(); (_b = editorComponentRef.current) === null || _b === void 0 ? void 0 : _b.focus(); }; modifyInlineImagesInContentString(contentValue, [], (content) => { sendMessage(content); }); } }, [ disabled, contentValueOverflow, /* @conditional-compile-remove(file-sharing-acs) */ attachments, /* @conditional-compile-remove(rich-text-editor-image-upload) */ inlineImagesWithProgress, contentValue, hasContent, /* @conditional-compile-remove(file-sharing-acs) */ strings.attachmentUploadsPendingError, /* @conditional-compile-remove(rich-text-editor-image-upload) */ strings.imageUploadsPendingError, onSendMessage ]); // ignore attachments error (errored attachment will not be added, shouldn't disable SendButton) const hasBlockingErrorMessages = useMemo(() => { var _a; return (!!systemMessage || !!contentTooLongMessage || /* @conditional-compile-remove(file-sharing-acs) */ !!attachmentUploadsPendingError || /* @conditional-compile-remove(rich-text-editor-image-upload) */ !!((_a = inlineImagesWithProgress === null || inlineImagesWithProgress === void 0 ? void 0 : inlineImagesWithProgress.filter((image) => image.error).pop()) === null || _a === void 0 ? void 0 : _a.error)); }, [ contentTooLongMessage, /* @conditional-compile-remove(file-sharing-acs) */ attachmentUploadsPendingError, systemMessage, /* @conditional-compile-remove(rich-text-editor-image-upload) */ inlineImagesWithProgress ]); const isSendBoxButtonDisabledValue = useMemo(() => { return isSendBoxButtonDisabled({ hasContent, /* @conditional-compile-remove(file-sharing-acs) */ hasCompletedAttachmentUploads: isAttachmentUploadCompleted(attachments), hasError: hasBlockingErrorMessages, disabled }); }, [/* @conditional-compile-remove(file-sharing-acs) */ attachments, disabled, hasContent, hasBlockingErrorMessages]); const onRenderSendIcon = useCallback((isHover) => { return (React.createElement(Icon, { iconName: isHover && hasContent ? 'SendBoxSendHovered' : 'SendBoxSend', className: sendIconStyle({ theme, isSendBoxButtonDisabled: isSendBoxButtonDisabledValue, defaultTextColor: theme.palette.neutralSecondary }) })); }, [theme, isSendBoxButtonDisabledValue, hasContent]); const sendBoxErrorsProps = useMemo(() => { var _a, _b, _c, _d; /* @conditional-compile-remove(file-sharing-acs) */ const uploadErrorMessage = (_b = (_a = attachments === null || attachments === void 0 ? void 0 : attachments.filter((attachmentUpload) => attachmentUpload.error).pop()) === null || _a === void 0 ? void 0 : _a.error) === null || _b === void 0 ? void 0 : _b.message; /* @conditional-compile-remove(rich-text-editor-image-upload) */ const imageUploadErrorMessage = (_d = (_c = inlineImagesWithProgress === null || inlineImagesWithProgress === void 0 ? void 0 : inlineImagesWithProgress.filter((image) => image.error).pop()) === null || _c === void 0 ? void 0 : _c.error) === null || _d === void 0 ? void 0 : _d.message; /* @conditional-compile-remove(file-sharing-acs) */ const errorMessage = uploadErrorMessage || /* @conditional-compile-remove(rich-text-editor-image-upload) */ imageUploadErrorMessage; /* @conditional-compile-remove(rich-text-editor-image-upload) */ let errorBarType = SendBoxErrorBarType.error; /* @conditional-compile-remove(file-sharing-acs) */ if (uploadErrorMessage) { errorBarType = SendBoxErrorBarType.warning; } return { /* @conditional-compile-remove(file-sharing-acs) */ attachmentUploadsPendingError: attachmentUploadsPendingError, /* @conditional-compile-remove(file-sharing-acs) */ attachmentProgressError: errorMessage ? { message: errorMessage, timestamp: Date.now(), /* @conditional-compile-remove(rich-text-editor-image-upload) */ errorBarType: errorBarType } : undefined, systemMessage: systemMessage, textValidationErrorMessage: contentTooLongMessage }; }, [ /* @conditional-compile-remove(file-sharing-acs) */ attachments, contentTooLongMessage, /* @conditional-compile-remove(file-sharing-acs) */ attachmentUploadsPendingError, /* @conditional-compile-remove(rich-text-editor-image-upload) */ inlineImagesWithProgress, systemMessage ]); /* @conditional-compile-remove(file-sharing-acs) */ const onRenderAttachmentUploads = useCallback(() => { return (React.createElement(Stack, { className: attachmentUploadCardsStyles }, React.createElement(FluentV9ThemeProvider, { v8Theme: theme, className: customV9Styles.clearBackground }, React.createElement(_AttachmentUploadCards, { attachments: attachments, onCancelAttachmentUpload: onCancelAttachmentUpload, strings: { removeAttachment: strings.removeAttachment, uploading: strings.uploading, uploadProgress: strings.uploadProgress, uploadCompleted: strings.uploadCompleted, attachmentMoreMenu: strings.attachmentMoreMenu }, disabled: disabled })))); }, [ theme, customV9Styles.clearBackground, attachments, onCancelAttachmentUpload, strings.removeAttachment, strings.uploading, strings.uploadProgress, strings.uploadCompleted, strings.attachmentMoreMenu, disabled ]); const sendButton = useMemo(() => { return (React.createElement(InputBoxButton, { onRenderIcon: onRenderSendIcon, onClick: (e) => { sendMessageOnClick(); e.stopPropagation(); // Prevents the click from bubbling up and triggering a focus event on the chat. }, className: richTextActionButtonsStyle, ariaLabel: localeStrings.sendButtonAriaLabel, tooltipContent: localeStrings.sendButtonAriaLabel, disabled: isSendBoxButtonDisabledValue })); }, [isSendBoxButtonDisabledValue, localeStrings.sendButtonAriaLabel, onRenderSendIcon, sendMessageOnClick]); /* @conditional-compile-remove(file-sharing-acs) */ const hasAttachmentUploads = useMemo(() => { return isAttachmentUploadCompleted(attachments) || hasIncompleteAttachmentUploads(attachments); }, [attachments]); return (React.createElement(Stack, null, React.createElement(RichTextSendBoxErrors, Object.assign({}, sendBoxErrorsProps)), React.createElement(RichTextInputBoxComponent, { placeholderText: strings.placeholderText, autoFocus: autoFocus, onChange: onChangeHandler, onEnterKeyDown: sendMessageOnClick, onTyping: onTyping, editorComponentRef: editorComponentRef, strings: strings, disabled: disabled, actionComponents: sendButton, richTextEditorStyleProps: sendBoxRichTextEditorStyle, /* @conditional-compile-remove(file-sharing-acs) */ onRenderAttachmentUploads: onRenderAttachmentUploads, /* @conditional-compile-remove(file-sharing-acs) */ hasAttachments: hasAttachmentUploads, /* @conditional-compile-remove(rich-text-editor-image-upload) */ onPaste: onPaste, /* @conditional-compile-remove(rich-text-editor-image-upload) */ onInsertInlineImage: onInsertInlineImage }))); }; /* @conditional-compile-remove(rich-text-editor) */ // Add component type check to assist in identification for usePropsFor // to avoid issue where production build does not have the component name // eslint-disable-next-line @typescript-eslint/no-explicit-any RichTextSendBox[richTextSendBoxIdentifier] = true; //# sourceMappingURL=RichTextSendBox.js.map