Files
jitsi-meet/react/features/chat/components/web/FileMessage.tsx
Hristo Terezov a62fa3f833 feat(chat): Display file uploads as inline chat messages
Integrates file sharing into the chat interface so uploaded files appear as messages in the chat timeline alongside text
messages.

Changes:
- Created FileMessage component for inline file display in chat
- Extracted FileItem component for reusable file UI across chat and file sharing tab
- Show "A file was deleted" placeholder instead of removing message when file deleted
- Hide message menu (3-dot) when no actions are available for file messages
- Add button backgrounds in chat context to hide text on hover
- Fix timing: local participant only sees file message after upload completes (progress: 100%)

Technical implementation:
- Added fileMetadata field to IMessage interface
- Added isDeleted flag to IFileMetadata for soft-delete state
- Middleware dispatches addMessage when files uploaded (ADD_FILE action)
- Middleware uses editMessage when files deleted to preserve chat history
- Minimal state retention (only isDeleted flag) for deleted files

This provides a unified messaging experience where file sharing is part of the conversation flow.
2025-10-14 08:45:51 -05:00

144 lines
4.3 KiB
TypeScript

import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { downloadFile, removeFile } from '../../../file-sharing/actions';
import FileItem from '../../../file-sharing/components/web/FileItem';
import { isFileUploadingEnabled } from '../../../file-sharing/functions.any';
import { IMessage } from '../../types';
/**
* Props for the FileMessage component.
*/
interface IFileMessageProps {
/**
* Additional CSS class name.
*/
className?: string;
/**
* The message containing file metadata.
*/
message: IMessage;
/**
* Screen reader help text for accessibility.
*/
screenReaderHelpText?: string;
}
const useStyles = makeStyles()(theme => {
return {
fileMessageContainer: {
margin: `${theme.spacing(1)} 0`,
maxWidth: '100%',
minWidth: 0,
padding: 0,
display: 'flex',
flexDirection: 'column',
// Override FileItem styles for compact chat display
'& .fileItem': {
padding: theme.spacing(2), // Reduced from 3 (24px → 16px)
gap: theme.spacing(2), // Reduced from 3
// Add background to button container to hide text underneath in chat context
'& > div:last-child': {
backgroundColor: theme.palette.ui02,
paddingLeft: theme.spacing(2)
},
'&:hover > div:last-child': {
backgroundColor: theme.palette.ui03
}
},
'& .fileName': {
...theme.typography.bodyShortRegular // Match message text font
},
'& .fileSize': {
...theme.typography.labelRegular // Keep smaller for metadata
}
},
deletedFileMessage: {
...theme.typography.bodyShortRegular,
fontStyle: 'italic',
color: theme.palette.text02,
padding: theme.spacing(1, 0)
}
};
});
/**
* Component for displaying file messages in chat.
*
* @param {IFileMessageProps} props - The component props.
* @returns {JSX.Element | null} The FileMessage component or null if no file metadata.
*/
const FileMessage = ({ className = '', message, screenReaderHelpText }: IFileMessageProps) => {
const { classes, cx } = useStyles();
const dispatch = useDispatch();
const { t } = useTranslation();
const isUploadEnabled = useSelector(isFileUploadingEnabled);
/**
* Handles the download action for a file.
*
* @param {string} fileId - The ID of the file to download.
* @returns {void}
*/
const handleDownload = useCallback((fileId: string) => {
dispatch(downloadFile(fileId));
}, [ dispatch ]);
/**
* Handles the remove action for a file.
*
* @param {string} fileId - The ID of the file to remove.
* @returns {void}
*/
const handleRemove = useCallback((fileId: string) => {
dispatch(removeFile(fileId));
}, [ dispatch ]);
if (!message.fileMetadata) {
return null;
}
// If the file has been deleted, show a deletion message instead of the file item.
if (message.fileMetadata.isDeleted) {
return (
<div className = { cx(classes.fileMessageContainer, className) }>
<div className = { classes.deletedFileMessage }>
{t('chat.fileDeleted')}
</div>
</div>
);
}
return (
<div className = { cx(classes.fileMessageContainer, className) }>
{screenReaderHelpText && (
<span className = 'sr-only'>
{screenReaderHelpText}
</span>
)}
<FileItem
actionsVisible = { true }
className = 'fileItem'
file = { message.fileMetadata }
iconSize = { 40 }
onDownload = { handleDownload }
onRemove = { handleRemove }
showAuthor = { false }
showRemoveButton = { isUploadEnabled }
showTimestamp = { false } />
</div>
);
};
export default FileMessage;