Files
jitsi-meet/react/features/chat/components/web/MessageMenu.tsx
Hristo Terezov fc816aa149 fix(ChatMessage): context menu position
Before the chat message context menu was appearing on the left if the private chat message was disabled. The fix makes the context menu appear on the left only for messages from the local partcipant which are the only messages rendered to the right (therefore the context menu have to appear on the left side). For all other messages the context menu should appear on the right side because the message is positioned on the left side.
2025-08-02 10:19:16 -05:00

180 lines
6.1 KiB
TypeScript

import React, { useCallback, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { IconDotsHorizontal } from '../../../base/icons/svg';
import { getParticipantById } from '../../../base/participants/functions';
import Popover from '../../../base/popover/components/Popover.web';
import Button from '../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import { copyText } from '../../../base/util/copyText.web';
import { handleLobbyChatInitialized, openChat } from '../../actions.web';
export interface IProps {
className?: string;
displayName?: string;
enablePrivateChat: boolean;
isFromVisitor?: boolean;
isLobbyMessage: boolean;
message: string;
participantId: string;
}
const useStyles = makeStyles()(theme => {
return {
messageMenuButton: {
padding: '2px'
},
menuItem: {
padding: '8px 16px',
cursor: 'pointer',
color: 'white',
'&:hover': {
backgroundColor: theme.palette.action03
}
},
menuPanel: {
backgroundColor: theme.palette.ui03,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[3],
overflow: 'hidden'
},
copiedMessage: {
position: 'fixed',
backgroundColor: theme.palette.ui03,
color: 'white',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
zIndex: 1000,
opacity: 0,
transition: 'opacity 0.3s ease-in-out',
pointerEvents: 'none'
},
showCopiedMessage: {
opacity: 1
}
};
});
const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, enablePrivateChat, displayName }: IProps) => {
const dispatch = useDispatch();
const { classes, cx } = useStyles();
const { t } = useTranslation();
const [ isPopoverOpen, setIsPopoverOpen ] = useState(false);
const [ showCopiedMessage, setShowCopiedMessage ] = useState(false);
const [ popupPosition, setPopupPosition ] = useState({ top: 0,
left: 0 });
const buttonRef = useRef<HTMLDivElement>(null);
const participant = useSelector((state: IReduxState) => getParticipantById(state, participantId));
const handleMenuClick = useCallback(() => {
setIsPopoverOpen(true);
}, []);
const handleClose = useCallback(() => {
setIsPopoverOpen(false);
}, []);
const handlePrivateClick = useCallback(() => {
if (isLobbyMessage) {
dispatch(handleLobbyChatInitialized(participantId));
} else {
// For visitor messages, participant will be undefined but we can still open chat
// using the participantId which contains the visitor's original JID
if (isFromVisitor) {
// Handle visitor participant that doesn't exist in main participant list
const visitorParticipant = {
id: participantId,
name: displayName,
isVisitor: true
};
dispatch(openChat(visitorParticipant));
} else {
dispatch(openChat(participant));
}
}
handleClose();
}, [ dispatch, isLobbyMessage, participant, participantId, displayName ]);
const handleCopyClick = useCallback(() => {
copyText(message)
.then(success => {
if (success) {
if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setPopupPosition({
top: rect.top - 30,
left: rect.left
});
}
setShowCopiedMessage(true);
setTimeout(() => {
setShowCopiedMessage(false);
}, 2000);
} else {
console.error('Failed to copy text');
}
})
.catch(error => {
console.error('Error copying text:', error);
});
handleClose();
}, [ message ]);
const popoverContent = (
<div className = { classes.menuPanel }>
{enablePrivateChat && (
<div
className = { classes.menuItem }
onClick = { handlePrivateClick }>
{t('Private Message')}
</div>
)}
<div
className = { classes.menuItem }
onClick = { handleCopyClick }>
{t('Copy')}
</div>
</div>
);
return (
<div>
<div ref = { buttonRef }>
<Popover
content = { popoverContent }
onPopoverClose = { handleClose }
position = 'top'
trigger = 'click'
visible = { isPopoverOpen }>
<Button
accessibilityLabel = { t('toolbar.accessibilityLabel.moreOptions') }
className = { classes.messageMenuButton }
icon = { IconDotsHorizontal }
onClick = { handleMenuClick }
type = { BUTTON_TYPES.TERTIARY } />
</Popover>
</div>
{showCopiedMessage && ReactDOM.createPortal(
<div
className = { cx(classes.copiedMessage, { [classes.showCopiedMessage]: showCopiedMessage }) }
style = {{ top: `${popupPosition.top}px`,
left: `${popupPosition.left}px` }}>
{t('Message Copied')}
</div>,
document.body
)}
</div>
);
};
export default MessageMenu;