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