UNPKG

communication-react-19

Version:

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

288 lines 17.8 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { mergeStyles, Stack } from '@fluentui/react'; import { ChatMyMessage } from '@fluentui-contrib/react-chat'; import { mergeClasses } from '@fluentui/react-components'; import { useTheme } from '../../../theming'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; /* @conditional-compile-remove(file-sharing-acs) */ import { useReducer } from 'react'; import { editBoxWidthStyles, richTextEditBoxActionButtonIcon } from '../../styles/EditBox.styles'; import { InputBoxButton } from '../../InputBoxButton'; import { useChatMyMessageStyles } from '../../styles/MessageThread.styles'; import { _AttachmentUploadCards } from '../../Attachment/AttachmentUploadCards'; import { useChatMessageRichTextEditContainerStyles } from '../../styles/ChatMessageComponent.styles'; import { modifyInlineImagesInContentString } from '../../utils/SendBoxUtils'; /* @conditional-compile-remove(rich-text-editor-image-upload) */ import { hasIncompleteAttachmentUploads, removeBrokenImageContentAndClearImageSizeStyles, getContentWithUpdatedInlineImagesInfo, isMessageTooLong, inlineImageIds } from '../../utils/SendBoxUtils'; import { getMessageState, onRenderCancelIcon, onRenderSubmitIcon, getTextValidationErrorMessage } from '../../utils/ChatMessageComponentAsEditBoxUtils'; /* @conditional-compile-remove(file-sharing-acs) */ import { attachmentMetadataReducer, getMessageWithAttachmentMetadata, doesMessageContainMultipleAttachments } from '../../utils/ChatMessageComponentAsEditBoxUtils'; import { RichTextInputBoxComponent } from '../../RichTextEditor/RichTextInputBoxComponent'; import { editBoxRichTextEditorStyle, richTextActionButtonsStyle } from '../../styles/RichTextEditor.styles'; import { RichTextSendBoxErrors } from '../../RichTextEditor/RichTextSendBoxErrors'; import { useLocale } from '../../../localization'; /* @conditional-compile-remove(file-sharing-acs) */ import { FluentV9ThemeProvider } from '../../../theming/FluentV9ThemeProvider'; /* @conditional-compile-remove(file-sharing-acs) */ import { attachmentUploadCardsStyles } from '../../styles/SendBox.styles'; /* @conditional-compile-remove(rich-text-editor-image-upload) */ import { SendBoxErrorBarType } from '../../SendBoxErrorBar'; /* @conditional-compile-remove(rich-text-editor-image-upload) */ import { BROKEN_IMAGE_SVG_DATA } from '../../styles/Common.style'; /* @conditional-compile-remove(rich-text-editor-image-upload) */ import { getPreviousInlineImages } from '../../utils/RichTextEditorUtils'; /** * @private */ export const ChatMessageComponentAsRichTextEditBox = (props) => { var _a; const { onCancel, onSubmit, strings, message, /* @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; /* @conditional-compile-remove(rich-text-editor-image-upload) */ const [initialInlineImages, setInitialInlineImages] = useState([]); const initialContent = useMemo(() => { /* @conditional-compile-remove(rich-text-editor-image-upload) */ const content = message.content; /* @conditional-compile-remove(rich-text-editor-image-upload) */ setInitialInlineImages(getPreviousInlineImages(content)); /* @conditional-compile-remove(rich-text-editor-image-upload) */ const document = new DOMParser().parseFromString(content !== null && content !== void 0 ? content : '', 'text/html'); // The broken image element is a div element with all the attributes of the original image element. // We need to convert it to a img element so the Rooster knows how to render it. // And we need to copy over all the attributes such as id, width, etc. // which is needed for sending the message with the images correctly. /* @conditional-compile-remove(rich-text-editor-image-upload) */ document.querySelectorAll('.broken-image-wrapper').forEach((brokenImage) => { var _a; const imageElement = document.createElement('img'); const attributes = brokenImage.attributes; for (const attribute of attributes) { imageElement.setAttribute(attribute.name, attribute.value); } imageElement.src = BROKEN_IMAGE_SVG_DATA; imageElement.style.width = '3rem'; imageElement.style.height = '3rem'; (_a = brokenImage.parentElement) === null || _a === void 0 ? void 0 : _a.replaceChild(imageElement, brokenImage); }); /* @conditional-compile-remove(rich-text-editor-image-upload) */ return document.body.innerHTML; return message.content; }, [message]); const [contentValue, setContentValue] = useState(initialContent || ''); /* @conditional-compile-remove(rich-text-editor-image-upload) */ const [contentValueWithInlineImagesOverflow, setContentValueWithInlineImagesOverflow] = useState(false); /* @conditional-compile-remove(file-sharing-acs) */ const [attachmentMetadata, handleAttachmentAction] = useReducer(attachmentMetadataReducer, (_a = getMessageWithAttachmentMetadata(message)) !== null && _a !== void 0 ? _a : []); /* @conditional-compile-remove(rich-text-editor-image-upload) */ const [attachmentUploadsPendingError, setAttachmentUploadsPendingError] = useState(undefined); const editTextFieldRef = React.useRef(null); const theme = useTheme(); const messageState = useMemo(() => { var _a, _b; // 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 = (_b = (_a = editTextFieldRef.current) === null || _a === void 0 ? void 0 : _a.getPlainContent()) !== null && _b !== void 0 ? _b : contentValue; let messageLengthState = getMessageState(plainTextContent, /* @conditional-compile-remove(file-sharing-acs) */ attachmentMetadata !== null && attachmentMetadata !== void 0 ? attachmentMetadata : []); /* @conditional-compile-remove(rich-text-editor-image-upload) */ const imageIds = inlineImageIds(contentValue); /* @conditional-compile-remove(rich-text-editor-image-upload) */ if (messageLengthState !== 'OK' && imageIds.length > 0) { // Check if too long if (isMessageTooLong(contentValue.length)) { messageLengthState = 'too long'; } else { messageLengthState = 'OK'; } } return messageLengthState; }, [/* @conditional-compile-remove(file-sharing-acs) */ attachmentMetadata, contentValue]); /* @conditional-compile-remove(rich-text-editor-image-upload) */ const imageUploadErrorMessage = useMemo(() => { var _a, _b; return (_b = (_a = inlineImagesWithProgress === null || inlineImagesWithProgress === void 0 ? void 0 : inlineImagesWithProgress.filter((image) => image.error).pop()) === null || _a === void 0 ? void 0 : _a.error) === null || _b === void 0 ? void 0 : _b.message; }, [inlineImagesWithProgress]); const editContainerStyles = useChatMessageRichTextEditContainerStyles(); const chatMyMessageStyles = useChatMyMessageStyles(); const locale = useLocale().strings; const setContent = useCallback((newValue) => { setContentValue(newValue !== null && newValue !== void 0 ? newValue : ''); }, []); useEffect(() => { var _a; (_a = editTextFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []); const textValidationErrorMessage = useMemo(() => { return getTextValidationErrorMessage(messageState, strings.editBoxTextLimit, strings.editBoxEmptyWarningText, /* @conditional-compile-remove(rich-text-editor-image-upload) */ contentValueWithInlineImagesOverflow); }, [ messageState, strings.editBoxTextLimit, strings.editBoxEmptyWarningText, /* @conditional-compile-remove(rich-text-editor-image-upload) */ contentValueWithInlineImagesOverflow ]); const iconClassName = useCallback((isHover) => { const color = isHover ? theme.palette.accent : theme.palette.neutralSecondary; return mergeStyles(richTextEditBoxActionButtonIcon, { color }); }, [theme.palette.accent, theme.palette.neutralSecondary]); const onRenderThemedCancelIcon = useCallback((isHover) => { return onRenderCancelIcon(iconClassName(isHover)); }, [iconClassName]); const onRenderThemedSubmitIcon = useCallback((isHover) => { return onRenderSubmitIcon(iconClassName(isHover)); }, [iconClassName]); /* @conditional-compile-remove(file-sharing-acs) */ const hasMultipleAttachments = useMemo(() => { return doesMessageContainMultipleAttachments(message); }, [message]); const onSubmitHandler = useCallback(() => { if (messageState !== 'OK') { return; } /* @conditional-compile-remove(rich-text-editor-image-upload) */ if (inlineImagesWithProgress && inlineImagesWithProgress.length > 0) { const contentWithUpdatedInlineImagesInfo = getContentWithUpdatedInlineImagesInfo(contentValue, inlineImagesWithProgress); const messageTooLong = isMessageTooLong(contentWithUpdatedInlineImagesInfo.length); // Set contentValueWithInlineImagesOverflow state to display the error bar setContentValueWithInlineImagesOverflow(messageTooLong); // The change from the setContentValueOverflow in the previous line will not kick in yet. // We need to rely on the local value of messageTooLong to return early if the message is too long. if (messageTooLong) { return; } } // Don't send message until all attachments have been uploaded successfully /* @conditional-compile-remove(rich-text-editor-image-upload) */ setAttachmentUploadsPendingError(undefined); /* @conditional-compile-remove(rich-text-editor-image-upload) */ if (hasIncompleteAttachmentUploads(inlineImagesWithProgress)) { setAttachmentUploadsPendingError({ message: strings.imageUploadsPendingError, timestamp: Date.now(), errorBarType: SendBoxErrorBarType.info }); return; } let content = contentValue; /* @conditional-compile-remove(rich-text-editor-image-upload) */ content = removeBrokenImageContentAndClearImageSizeStyles(content); let initInlineImages = []; /* @conditional-compile-remove(rich-text-editor-image-upload) */ initInlineImages = initialInlineImages !== null && initialInlineImages !== void 0 ? initialInlineImages : []; modifyInlineImagesInContentString(content, initInlineImages, (content) => { // it's very important to pass an empty attachment here // so when user removes all attachments, UI can reflect it instantly // if you set it to undefined, the attachments pre-edited would still be there // until edit message event is received onSubmit(content, /* @conditional-compile-remove(file-sharing-acs) */ attachmentMetadata || []); }); }, [ messageState, /* @conditional-compile-remove(rich-text-editor-image-upload) */ inlineImagesWithProgress, /* @conditional-compile-remove(rich-text-editor-image-upload) */ initialInlineImages, contentValue, /* @conditional-compile-remove(rich-text-editor-image-upload) */ strings.imageUploadsPendingError, onSubmit, /* @conditional-compile-remove(file-sharing-acs) */ attachmentMetadata ]); const actionButtons = useMemo(() => { return (React.createElement(Stack, { horizontal: true }, React.createElement(InputBoxButton, { className: richTextActionButtonsStyle, ariaLabel: strings.editBoxCancelButton, tooltipContent: strings.editBoxCancelButton, onRenderIcon: onRenderThemedCancelIcon, onClick: () => { onCancel && onCancel(message.messageId); }, id: 'dismissIconWrapper', "data-testId": 'chat-message-rich-text-edit-box-cancel-button' }), React.createElement(InputBoxButton, { className: richTextActionButtonsStyle, ariaLabel: strings.editBoxSubmitButton, tooltipContent: strings.editBoxSubmitButton, onRenderIcon: onRenderThemedSubmitIcon, onClick: (e) => { onSubmitHandler(); e.stopPropagation(); }, id: 'submitIconWrapper', "data-testId": 'chat-message-rich-text-edit-box-submit-button' }))); }, [ message.messageId, onCancel, onRenderThemedCancelIcon, onRenderThemedSubmitIcon, strings.editBoxCancelButton, strings.editBoxSubmitButton, onSubmitHandler ]); const richTextLocaleStrings = useMemo(() => { /* @conditional-compile-remove(rich-text-editor) */ return Object.assign(Object.assign({}, locale.richTextSendBox), strings); return locale.sendBox; }, [ /* @conditional-compile-remove(rich-text-editor) */ locale.richTextSendBox, /* @conditional-compile-remove(rich-text-editor) */ strings, locale.sendBox ]); /* @conditional-compile-remove(file-sharing-acs) */ const onCancelAttachmentUpload = useCallback((attachmentId) => { // edit box only capable of removing attachments // we need to expand attachment actions // if we want to support more actions e.g. add handleAttachmentAction({ type: 'remove', id: attachmentId }); }, []); /* @conditional-compile-remove(file-sharing-acs) */ const onRenderAttachmentUploads = useCallback(() => { return (React.createElement(Stack, { className: attachmentUploadCardsStyles }, React.createElement(FluentV9ThemeProvider, { v8Theme: theme }, React.createElement(_AttachmentUploadCards, { attachments: attachmentMetadata, onCancelAttachmentUpload: onCancelAttachmentUpload })))); }, [attachmentMetadata, onCancelAttachmentUpload, theme]); const onChangeHandler = useCallback((content, /* @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, message.messageId); }); setContent(content); }, [ setContent, /* @conditional-compile-remove(rich-text-editor-image-upload) */ onRemoveInlineImage, /* @conditional-compile-remove(rich-text-editor-image-upload) */ message.messageId ]); const getContent = () => { return (React.createElement(Stack, { className: mergeStyles(editBoxWidthStyles) }, React.createElement(RichTextSendBoxErrors, { textValidationErrorMessage: textValidationErrorMessage, systemMessage: message.failureReason, /* @conditional-compile-remove(rich-text-editor-image-upload) */ attachmentUploadsPendingError: attachmentUploadsPendingError, /* @conditional-compile-remove(rich-text-editor-image-upload) */ attachmentProgressError: imageUploadErrorMessage ? { message: imageUploadErrorMessage, timestamp: Date.now(), errorBarType: SendBoxErrorBarType.error } : undefined }), React.createElement(RichTextInputBoxComponent, { placeholderText: strings.editBoxPlaceholderText, onChange: onChangeHandler, onEnterKeyDown: onSubmitHandler, editorComponentRef: editTextFieldRef, initialContent: initialContent, strings: richTextLocaleStrings, disabled: false, actionComponents: actionButtons, richTextEditorStyleProps: editBoxRichTextEditorStyle, isHorizontalLayoutDisabled: true, /* @conditional-compile-remove(file-sharing-acs) */ onRenderAttachmentUploads: onRenderAttachmentUploads, /* @conditional-compile-remove(rich-text-editor-image-upload) */ onPaste: onPaste, /* @conditional-compile-remove(rich-text-editor-image-upload) */ onInsertInlineImage: onInsertInlineImage ? (imageAttributes) => { onInsertInlineImage(imageAttributes, message.messageId); } : undefined }))); }; const attached = message.attached === true ? 'center' : message.attached === 'bottom' ? 'bottom' : 'top'; return (React.createElement(ChatMyMessage, { attached: attached, root: { className: mergeClasses(chatMyMessageStyles.root, /* @conditional-compile-remove(file-sharing-acs) */ hasMultipleAttachments ? chatMyMessageStyles.multipleAttachmentsInEditing : undefined) }, body: { className: mergeClasses(editContainerStyles.body, attached !== 'top' ? editContainerStyles.bodyAttached : undefined) } }, getContent())); }; export default ChatMessageComponentAsRichTextEditBox; //# sourceMappingURL=ChatMessageComponentAsRichTextEditBox.js.map