mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-14 23:47:47 +00:00
The display name is used in messages when messages are coming from visitors or from the history. The display name is used only when the participant is not available in the meeting to get its name.
448 lines
16 KiB
TypeScript
448 lines
16 KiB
TypeScript
import { Theme } from '@mui/material';
|
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
import { connect } from 'react-redux';
|
|
import { makeStyles } from 'tss-react/mui';
|
|
|
|
import { IReduxState } from '../../../app/types';
|
|
import { translate } from '../../../base/i18n/functions';
|
|
import { getParticipantById, getParticipantDisplayName, isPrivateChatEnabled } from '../../../base/participants/functions';
|
|
import Popover from '../../../base/popover/components/Popover.web';
|
|
import Message from '../../../base/react/components/web/Message';
|
|
import { MESSAGE_TYPE_LOCAL } from '../../constants';
|
|
import { getDisplayNameSuffix, getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
|
|
import { IChatMessageProps } from '../../types';
|
|
|
|
import MessageMenu from './MessageMenu';
|
|
import ReactButton from './ReactButton';
|
|
|
|
interface IProps extends IChatMessageProps {
|
|
className?: string;
|
|
enablePrivateChat?: boolean;
|
|
shouldDisplayMenuOnRight?: boolean;
|
|
state?: IReduxState;
|
|
}
|
|
|
|
const useStyles = makeStyles()((theme: Theme) => {
|
|
return {
|
|
chatMessageFooter: {
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginTop: theme.spacing(1)
|
|
},
|
|
chatMessageFooterLeft: {
|
|
display: 'flex',
|
|
flexGrow: 1,
|
|
overflow: 'hidden'
|
|
},
|
|
chatMessageWrapper: {
|
|
maxWidth: '100%'
|
|
},
|
|
chatMessage: {
|
|
display: 'inline-flex',
|
|
padding: '12px',
|
|
backgroundColor: theme.palette.ui02,
|
|
borderRadius: '4px 12px 12px 12px',
|
|
maxWidth: '100%',
|
|
marginTop: '4px',
|
|
boxSizing: 'border-box' as const,
|
|
|
|
'&.privatemessage': {
|
|
backgroundColor: theme.palette.support05
|
|
},
|
|
'&.local': {
|
|
backgroundColor: theme.palette.ui04,
|
|
borderRadius: '12px 4px 12px 12px',
|
|
|
|
'&.privatemessage': {
|
|
backgroundColor: theme.palette.support05
|
|
},
|
|
'&.local': {
|
|
backgroundColor: theme.palette.ui04,
|
|
borderRadius: '12px 4px 12px 12px',
|
|
|
|
'&.privatemessage': {
|
|
backgroundColor: theme.palette.support05
|
|
}
|
|
},
|
|
|
|
'&.error': {
|
|
backgroundColor: theme.palette.actionDanger,
|
|
borderRadius: 0,
|
|
fontWeight: 100
|
|
},
|
|
|
|
'&.lobbymessage': {
|
|
backgroundColor: theme.palette.support05
|
|
}
|
|
},
|
|
'&.error': {
|
|
backgroundColor: theme.palette.actionDanger,
|
|
borderRadius: 0,
|
|
fontWeight: 100
|
|
},
|
|
'&.lobbymessage': {
|
|
backgroundColor: theme.palette.support05
|
|
}
|
|
},
|
|
sideBySideContainer: {
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
justifyContent: 'left',
|
|
alignItems: 'center',
|
|
marginLeft: theme.spacing(1)
|
|
},
|
|
reactionBox: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: theme.spacing(1),
|
|
backgroundColor: theme.palette.grey[800],
|
|
borderRadius: theme.shape.borderRadius,
|
|
padding: theme.spacing(0, 1),
|
|
cursor: 'pointer'
|
|
},
|
|
reactionCount: {
|
|
fontSize: '0.8rem',
|
|
color: theme.palette.grey[400]
|
|
},
|
|
replyButton: {
|
|
padding: '2px'
|
|
},
|
|
replyWrapper: {
|
|
display: 'flex',
|
|
flexDirection: 'row' as const,
|
|
alignItems: 'center',
|
|
maxWidth: '100%'
|
|
},
|
|
messageContent: {
|
|
maxWidth: '100%',
|
|
overflow: 'hidden',
|
|
flex: 1
|
|
},
|
|
optionsButtonContainer: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
gap: theme.spacing(1),
|
|
minWidth: '32px',
|
|
minHeight: '32px'
|
|
},
|
|
displayName: {
|
|
...theme.typography.labelBold,
|
|
color: theme.palette.text02,
|
|
whiteSpace: 'nowrap',
|
|
textOverflow: 'ellipsis',
|
|
overflow: 'hidden',
|
|
marginBottom: theme.spacing(1),
|
|
maxWidth: '130px'
|
|
},
|
|
userMessage: {
|
|
...theme.typography.bodyShortRegular,
|
|
color: theme.palette.text01,
|
|
whiteSpace: 'pre-wrap',
|
|
wordBreak: 'break-word'
|
|
},
|
|
privateMessageNotice: {
|
|
...theme.typography.labelRegular,
|
|
color: theme.palette.text02,
|
|
marginTop: theme.spacing(1)
|
|
},
|
|
timestamp: {
|
|
...theme.typography.labelRegular,
|
|
color: theme.palette.text03,
|
|
marginTop: theme.spacing(1),
|
|
marginLeft: theme.spacing(1),
|
|
whiteSpace: 'nowrap',
|
|
flexShrink: 0
|
|
},
|
|
reactionsPopover: {
|
|
padding: theme.spacing(2),
|
|
backgroundColor: theme.palette.ui03,
|
|
borderRadius: theme.shape.borderRadius,
|
|
maxWidth: '150px',
|
|
maxHeight: '400px',
|
|
overflowY: 'auto',
|
|
color: theme.palette.text01
|
|
},
|
|
reactionItem: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing(1),
|
|
gap: theme.spacing(1),
|
|
borderBottom: `1px solid ${theme.palette.common.white}`,
|
|
paddingBottom: theme.spacing(1),
|
|
'&:last-child': {
|
|
borderBottom: 'none',
|
|
paddingBottom: 0
|
|
}
|
|
},
|
|
participantList: {
|
|
marginLeft: theme.spacing(1),
|
|
fontSize: '0.8rem',
|
|
maxWidth: '120px'
|
|
},
|
|
participant: {
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap'
|
|
}
|
|
};
|
|
});
|
|
|
|
const ChatMessage = ({
|
|
className = '',
|
|
message,
|
|
state,
|
|
showDisplayName,
|
|
shouldDisplayMenuOnRight,
|
|
enablePrivateChat,
|
|
knocking,
|
|
t
|
|
}: IProps) => {
|
|
const { classes, cx } = useStyles();
|
|
const [ isHovered, setIsHovered ] = useState(false);
|
|
const [ isReactionsOpen, setIsReactionsOpen ] = useState(false);
|
|
|
|
const handleMouseEnter = useCallback(() => {
|
|
setIsHovered(true);
|
|
}, []);
|
|
|
|
const handleMouseLeave = useCallback(() => {
|
|
setIsHovered(false);
|
|
}, []);
|
|
|
|
const handleReactionsOpen = useCallback(() => {
|
|
setIsReactionsOpen(true);
|
|
}, []);
|
|
|
|
const handleReactionsClose = useCallback(() => {
|
|
setIsReactionsOpen(false);
|
|
}, []);
|
|
|
|
/**
|
|
* Renders the display name of the sender.
|
|
*
|
|
* @returns {React$Element<*>}
|
|
*/
|
|
function _renderDisplayName() {
|
|
const { displayName } = message;
|
|
|
|
return (
|
|
<div
|
|
aria-hidden = { true }
|
|
className = { cx('display-name', classes.displayName) }>
|
|
{`${displayName}${getDisplayNameSuffix(message)}`}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the message privacy notice.
|
|
*
|
|
* @returns {React$Element<*>}
|
|
*/
|
|
function _renderPrivateNotice() {
|
|
return (
|
|
<div className = { classes.privateMessageNotice }>
|
|
{getPrivateNoticeMessage(message)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the time at which the message was sent.
|
|
*
|
|
* @returns {React$Element<*>}
|
|
*/
|
|
function _renderTimestamp() {
|
|
return (
|
|
<div className = { cx('timestamp', classes.timestamp) }>
|
|
<p>
|
|
{getFormattedTimestamp(message)}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the reactions for the message.
|
|
*
|
|
* @returns {React$Element<*>}
|
|
*/
|
|
const renderReactions = useMemo(() => {
|
|
if (!message.reactions || message.reactions.size === 0) {
|
|
return null;
|
|
}
|
|
|
|
const reactionsArray = Array.from(message.reactions.entries())
|
|
.map(([ reaction, participants ]) => {
|
|
return { reaction,
|
|
participants };
|
|
})
|
|
.sort((a, b) => b.participants.size - a.participants.size);
|
|
|
|
const totalReactions = reactionsArray.reduce((sum, { participants }) => sum + participants.size, 0);
|
|
const numReactionsDisplayed = 3;
|
|
|
|
const reactionsContent = (
|
|
<div className = { classes.reactionsPopover }>
|
|
{reactionsArray.map(({ reaction, participants }) => (
|
|
<div
|
|
className = { classes.reactionItem }
|
|
key = { reaction }>
|
|
<p>
|
|
<span>{reaction}</span>
|
|
<span>{participants.size}</span>
|
|
</p>
|
|
<div className = { classes.participantList }>
|
|
{Array.from(participants).map(participantId => (
|
|
<p
|
|
className = { classes.participant }
|
|
key = { participantId }>
|
|
{state && getParticipantDisplayName(state, participantId)}
|
|
</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<Popover
|
|
content = { reactionsContent }
|
|
onPopoverClose = { handleReactionsClose }
|
|
onPopoverOpen = { handleReactionsOpen }
|
|
position = 'top'
|
|
trigger = 'hover'
|
|
visible = { isReactionsOpen }>
|
|
<div className = { classes.reactionBox }>
|
|
{reactionsArray.slice(0, numReactionsDisplayed).map(({ reaction }, index) =>
|
|
<p key = { index }>{reaction}</p>
|
|
)}
|
|
{reactionsArray.length > numReactionsDisplayed && (
|
|
<p className = { classes.reactionCount }>
|
|
+{totalReactions - numReactionsDisplayed}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</Popover>
|
|
);
|
|
}, [ message?.reactions, isHovered, isReactionsOpen ]);
|
|
|
|
return (
|
|
<div
|
|
className = { cx(classes.chatMessageWrapper, className) }
|
|
id = { message.messageId }
|
|
onMouseEnter = { handleMouseEnter }
|
|
onMouseLeave = { handleMouseLeave }
|
|
tabIndex = { -1 }>
|
|
<div className = { classes.sideBySideContainer }>
|
|
{!shouldDisplayMenuOnRight && (
|
|
<div className = { classes.optionsButtonContainer }>
|
|
{isHovered && <MessageMenu
|
|
displayName = { message.displayName }
|
|
enablePrivateChat = { Boolean(enablePrivateChat) }
|
|
isFromVisitor = { message.isFromVisitor }
|
|
isLobbyMessage = { message.lobbyChat }
|
|
message = { message.message }
|
|
participantId = { message.participantId } />}
|
|
</div>
|
|
)}
|
|
<div
|
|
className = { cx(
|
|
'chatmessage',
|
|
classes.chatMessage,
|
|
className,
|
|
message.privateMessage && 'privatemessage',
|
|
message.lobbyChat && !knocking && 'lobbymessage'
|
|
) }>
|
|
<div className = { classes.replyWrapper }>
|
|
<div className = { cx('messagecontent', classes.messageContent) }>
|
|
{showDisplayName && _renderDisplayName()}
|
|
<div className = { cx('usermessage', classes.userMessage) }>
|
|
<Message
|
|
screenReaderHelpText = { message.displayName === message.recipient
|
|
? t<string>('chat.messageAccessibleTitleMe')
|
|
: t<string>('chat.messageAccessibleTitle', {
|
|
user: message.displayName
|
|
}) }
|
|
text = { getMessageText(message) } />
|
|
{(message.privateMessage || (message.lobbyChat && !knocking))
|
|
&& _renderPrivateNotice()}
|
|
<div className = { classes.chatMessageFooter }>
|
|
<div className = { classes.chatMessageFooterLeft }>
|
|
{message.reactions && message.reactions.size > 0 && (
|
|
<>
|
|
{renderReactions}
|
|
</>
|
|
)}
|
|
</div>
|
|
{_renderTimestamp()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{shouldDisplayMenuOnRight && (
|
|
<div className = { classes.sideBySideContainer }>
|
|
{!message.privateMessage && !message.lobbyChat && <div>
|
|
<div className = { classes.optionsButtonContainer }>
|
|
{isHovered && <ReactButton
|
|
messageId = { message.messageId }
|
|
receiverId = { '' } />}
|
|
</div>
|
|
</div>}
|
|
<div>
|
|
<div className = { classes.optionsButtonContainer }>
|
|
{isHovered && <MessageMenu
|
|
displayName = { message.displayName }
|
|
enablePrivateChat = { Boolean(enablePrivateChat) }
|
|
isFromVisitor = { message.isFromVisitor }
|
|
isLobbyMessage = { message.lobbyChat }
|
|
message = { message.message }
|
|
participantId = { message.participantId } />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Maps part of the Redux store to the props of this component.
|
|
*
|
|
* @param {Object} state - The Redux state.
|
|
* @returns {IProps}
|
|
*/
|
|
function _mapStateToProps(state: IReduxState, { message }: IProps) {
|
|
const { knocking } = state['features/lobby'];
|
|
|
|
const participant = getParticipantById(state, message.participantId);
|
|
|
|
// For visitor private messages, participant will be undefined but we should still allow private chat
|
|
// Create a visitor participant object for visitor messages to pass to isPrivateChatEnabled
|
|
const participantForCheck = message.isFromVisitor
|
|
? { id: message.participantId, name: message.displayName, isVisitor: true as const }
|
|
: participant;
|
|
|
|
const enablePrivateChat = (!message.isFromVisitor || message.privateMessage)
|
|
&& isPrivateChatEnabled(participantForCheck, state);
|
|
|
|
// Only the local messages appear on the right side of the chat therefore only for them the menu has to be on the
|
|
// left side.
|
|
const shouldDisplayMenuOnRight = message.messageType !== MESSAGE_TYPE_LOCAL;
|
|
|
|
return {
|
|
shouldDisplayMenuOnRight,
|
|
enablePrivateChat,
|
|
knocking,
|
|
state
|
|
};
|
|
}
|
|
|
|
export default translate(connect(_mapStateToProps)(ChatMessage));
|