@azure/communication-react
Version:
React library for building modern communication user experiences utilizing Azure Communication Services
136 lines • 10.5 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';
/** @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, 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;
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]);
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;
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, onRenderAttachmentDownloads, actionsForAttachment),
getActionsMenu());
}, [actionsForAttachment, getActionsMenu, inlineImageOptions, 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, isBlockedMessage ? chatMessageCommonStyles.blocked : props.message.status === 'failed' ? chatMessageCommonStyles.failed : undefined, attached !== 'top' ? chatMyMessageStyles.bodyAttached : 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