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