UNPKG

communication-react-19

Version:

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

181 lines 12.1 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { Text, mergeStyles } from '@fluentui/react'; import { ChatMyMessage } from '@fluentui-contrib/react-chat'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { chatMessageDateStyle, chatMessageFailedTagStyle, chatMessageDateFailedStyle } from '../../styles/ChatMessageComponent.styles'; import { useIdentifiers } from '../../../identifiers/IdentifierProvider'; import { useTheme } from '../../../theming'; import { ChatMessageActionFlyout } from '../ChatMessageActionsFlyout'; import { chatMessageActionMenuProps } from '../ChatMessageActionMenu'; import { useLocale } from '../../../localization'; import { createStyleFromV8Style } from '../../styles/v8StyleShim'; import { mergeClasses } from '@fluentui/react-components'; import { useChatMyMessageStyles, useChatMessageCommonStyles, chatMyMessageActionMenuClassName } from '../../styles/MessageThread.styles'; import { generateCustomizedTimestamp, generateDefaultTimestamp, getMessageBubbleContent, getMessageEditedDetails } from '../../utils/ChatMessageComponentUtils'; /* @conditional-compile-remove(file-sharing-acs) */ import { doesMessageContainMultipleAttachments } from '../../utils/ChatMessageComponentAsEditBoxUtils'; /** @private */ const MessageBubble = (props) => { var _a; const ids = useIdentifiers(); const theme = useTheme(); const locale = useLocale(); const { userId, message, onRemoveClick, onResendClick, disableEditing, showDate, messageContainerStyle, strings, onEditClick, remoteParticipantsCount = 0, onRenderAvatar, showMessageStatus, messageStatus, inlineImageOptions, /* @conditional-compile-remove(mention) */ mentionDisplayOptions, onDisplayDateTimeString, onRenderAttachmentDownloads, actionsForAttachment, shouldFocusFluentMessageBody } = props; const formattedTimestamp = useMemo(() => { const defaultTimeStamp = message.createdOn ? generateDefaultTimestamp(message.createdOn, showDate, strings) : undefined; const customTimestamp = message.createdOn ? generateCustomizedTimestamp(message.createdOn, locale, onDisplayDateTimeString) : ''; return customTimestamp || defaultTimeStamp; }, [locale, message.createdOn, onDisplayDateTimeString, showDate, strings]); // Track if the action menu was opened by touch - if so we increase the touch targets for the items const [wasInteractionByTouch, setWasInteractionByTouch] = useState(false); // `focused` state is used for show/hide actionMenu const [focused, setFocused] = React.useState(false); // The chat message action flyout should target the Chat.Message action menu if clicked, // or target the chat message if opened via touch press. // Undefined indicates the flyout menu should not be being shown. const messageRef = useRef(null); const messageActionButtonRef = useRef(null); const [chatMessageActionFlyoutTarget, setChatMessageActionFlyoutTarget] = useState(undefined); const chatActionsEnabled = !disableEditing && message.status !== 'sending' && !!message.mine && /* @conditional-compile-remove(data-loss-prevention) */ message.messageType !== 'blocked'; const [messageReadBy, setMessageReadBy] = useState([]); const actionMenuProps = chatMessageActionMenuProps({ ariaLabel: (_a = strings.actionMenuMoreOptions) !== null && _a !== void 0 ? _a : '', enabled: chatActionsEnabled, menuButtonRef: messageActionButtonRef, menuExpanded: chatMessageActionFlyoutTarget === messageActionButtonRef, onActionButtonClick: () => { if (message.messageType === 'chat') { props.onActionButtonClick(message, setMessageReadBy); setChatMessageActionFlyoutTarget(messageActionButtonRef); } }, theme }); useEffect(() => { if (shouldFocusFluentMessageBody) { // set focus in the next render cycle to avoid focus being stolen by other components setTimeout(() => { var _a; (_a = messageRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }); } }, [shouldFocusFluentMessageBody]); const onActionFlyoutDismiss = useCallback(() => { // When the flyout dismiss is called, since we control if the action flyout is visible // or not we need to set the target to undefined here to actually hide the action flyout setChatMessageActionFlyoutTarget(undefined); }, [setChatMessageActionFlyoutTarget]); /* @conditional-compile-remove(file-sharing-acs) */ const hasMultipleAttachments = useMemo(() => { return doesMessageContainMultipleAttachments(message); }, [message]); const getMessageDetails = useCallback(() => { if (messageStatus === 'failed') { return React.createElement("div", { className: chatMessageFailedTagStyle(theme) }, strings.failToSendTag); } else { return getMessageEditedDetails(message, theme, strings.editedTag); } }, [message, messageStatus, strings.editedTag, strings.failToSendTag, theme]); const isBlockedMessage = // eslint-disable-next-line no-constant-binary-expression false || /* @conditional-compile-remove(data-loss-prevention) */ message.messageType === 'blocked'; const chatMyMessageStyles = useChatMyMessageStyles(); const chatMessageCommonStyles = useChatMessageCommonStyles(); const attached = message.attached === true ? 'center' : message.attached === 'bottom' ? 'bottom' : 'top'; const getActionsMenu = useCallback(() => { return (React.createElement("div", { className: mergeClasses( // add the static class name to use it in useChatMyMessageStyles chatMyMessageActionMenuClassName, chatMyMessageStyles.menu, // Make actions menu visible when the message is focused or the flyout is shown focused || (chatMessageActionFlyoutTarget === null || chatMessageActionFlyoutTarget === void 0 ? void 0 : chatMessageActionFlyoutTarget.current) ? chatMyMessageStyles.menuVisible : chatMyMessageStyles.menuHidden) }, actionMenuProps === null || actionMenuProps === void 0 ? void 0 : actionMenuProps.children)); }, [ actionMenuProps === null || actionMenuProps === void 0 ? void 0 : actionMenuProps.children, chatMessageActionFlyoutTarget, chatMyMessageStyles.menu, chatMyMessageStyles.menuHidden, chatMyMessageStyles.menuVisible, focused ]); const getContent = useCallback(() => { return (React.createElement("div", null, getMessageBubbleContent(message, strings, userId, inlineImageOptions, /* @conditional-compile-remove(mention) */ mentionDisplayOptions, onRenderAttachmentDownloads, actionsForAttachment), getActionsMenu())); }, [ actionsForAttachment, getActionsMenu, inlineImageOptions, /* @conditional-compile-remove(mention) */ mentionDisplayOptions, message, onRenderAttachmentDownloads, strings, userId ]); const chatMessage = (React.createElement(React.Fragment, null, React.createElement("div", { key: props.message.messageId }, React.createElement(ChatMyMessage, { attached: attached, key: props.message.messageId, body: { // messageContainerStyle used in className and style prop as style prop can't handle CSS selectors className: mergeClasses(chatMessageCommonStyles.body, chatMyMessageStyles.body, /* @conditional-compile-remove(rich-text-editor-image-upload) */ chatMessageCommonStyles.bodyWithPlaceholderImage, /* @conditional-compile-remove(rich-text-editor-image-upload) */ chatMyMessageStyles.bodyWithPlaceholderImage, isBlockedMessage ? chatMessageCommonStyles.blocked : props.message.status === 'failed' ? chatMessageCommonStyles.failed : undefined, attached !== 'top' ? chatMyMessageStyles.bodyAttached : undefined, /* @conditional-compile-remove(file-sharing-acs) */ hasMultipleAttachments ? chatMyMessageStyles.multipleAttachmentsInViewing : undefined, mergeStyles(messageContainerStyle)), style: Object.assign({}, createStyleFromV8Style(messageContainerStyle)), ref: messageRef }, root: { className: chatMyMessageStyles.root, onBlur: (e) => { // `focused` controls is focused the whole `ChatMessage` or any of its children. When we're navigating // with keyboard the focused element will be changed and there is no way to use `:focus` selector if (chatMessageActionFlyoutTarget === null || chatMessageActionFlyoutTarget === void 0 ? void 0 : chatMessageActionFlyoutTarget.current) { // doesn't dismiss action button if flyout is open, otherwise, narrator's focus will stay on the closed action menu return; } const shouldPreserveFocusState = e.currentTarget.contains(e.relatedTarget); setFocused(shouldPreserveFocusState); }, onFocus: () => { // react onFocus is called even when nested component receives focus (i.e. it bubbles) // so when focus moves within actionMenu, the `focus` state in chatMessage remains true, and keeps actionMenu visible setFocused(true); } }, "data-testid": "chat-composite-message", author: React.createElement(Text, { className: chatMessageDateStyle(theme) }, message.senderDisplayName), timestamp: React.createElement(Text, { className: props.message.status === 'failed' ? chatMessageDateFailedStyle(theme) : chatMessageDateStyle(theme), "data-testid": ids.messageTimestamp }, formattedTimestamp), details: getMessageDetails(), onTouchStart: () => setWasInteractionByTouch(true), onPointerDown: () => setWasInteractionByTouch(false), onKeyDown: () => setWasInteractionByTouch(false), onClick: () => { if (!wasInteractionByTouch) { return; } // If the message was touched via touch we immediately open the menu // flyout (when using mouse the 3-dot menu that appears on hover // must be clicked to open the flyout). // In doing so here we set the target of the flyout to be the message and // not the 3-dot menu button to position the flyout correctly. setChatMessageActionFlyoutTarget(messageRef); if (message.messageType === 'chat') { props.onActionButtonClick(message, setMessageReadBy); } } }, getContent())), chatActionsEnabled && (React.createElement(ChatMessageActionFlyout, { hidden: !chatMessageActionFlyoutTarget, target: chatMessageActionFlyoutTarget, increaseFlyoutItemSize: wasInteractionByTouch, onDismiss: onActionFlyoutDismiss, onEditClick: onEditClick, onRemoveClick: onRemoveClick, onResendClick: onResendClick, strings: strings, messageReadBy: messageReadBy, messageStatus: messageStatus !== null && messageStatus !== void 0 ? messageStatus : 'failed', remoteParticipantsCount: remoteParticipantsCount, onRenderAvatar: onRenderAvatar, showMessageStatus: showMessageStatus })))); return chatMessage; }; /** @private */ export const ChatMyMessageComponentAsMessageBubble = React.memo(MessageBubble); //# sourceMappingURL=ChatMyMessageComponentAsMessageBubble.js.map