snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
369
react/features/file-sharing/components/web/FileItem.tsx
Normal file
369
react/features/file-sharing/components/web/FileItem.tsx
Normal file
@@ -0,0 +1,369 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconDownload, IconTrash } from '../../../base/icons/svg';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.web';
|
||||
import {
|
||||
formatFileSize,
|
||||
formatTimestamp,
|
||||
getFileIcon
|
||||
} from '../../functions.any';
|
||||
import { IFileMetadata } from '../../types';
|
||||
|
||||
/**
|
||||
* Props for the FileItem component.
|
||||
*/
|
||||
interface IFileItemProps {
|
||||
|
||||
/**
|
||||
* Whether to show action buttons (download, remove).
|
||||
*/
|
||||
actionsVisible?: boolean;
|
||||
|
||||
/**
|
||||
* Additional CSS class name.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The file metadata to display.
|
||||
*/
|
||||
file: IFileMetadata;
|
||||
|
||||
/**
|
||||
* Size of the file icon in pixels (default: 64).
|
||||
*/
|
||||
iconSize?: number;
|
||||
|
||||
/**
|
||||
* Callback function when download button is clicked.
|
||||
*/
|
||||
onDownload?: (fileId: string) => void;
|
||||
|
||||
/**
|
||||
* Callback function when remove button is clicked.
|
||||
*/
|
||||
onRemove?: (fileId: string) => void;
|
||||
|
||||
/**
|
||||
* Whether to show the author/uploader information.
|
||||
*/
|
||||
showAuthor?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to show the remove button.
|
||||
*/
|
||||
showRemoveButton?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to show the timestamp.
|
||||
*/
|
||||
showTimestamp?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
buttonContainer: {
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
gap: theme.spacing(2),
|
||||
position: 'absolute',
|
||||
right: theme.spacing(3),
|
||||
top: 0
|
||||
},
|
||||
|
||||
fileIconContainer: {
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
margin: 'auto'
|
||||
},
|
||||
|
||||
fileItem: {
|
||||
backgroundColor: theme.palette.fileSharingItemBackground,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing(3),
|
||||
justifyContent: 'space-between',
|
||||
maxWidth: '100%',
|
||||
minWidth: 0,
|
||||
padding: theme.spacing(3),
|
||||
position: 'relative',
|
||||
|
||||
'& .actionIconVisibility': {
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s'
|
||||
},
|
||||
|
||||
'& .timestampVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.fileSharingItemBorder,
|
||||
|
||||
'& .actionIconVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'& .timestampVisibility': {
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
|
||||
'&.focused .actionIconVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'&.focused .timestampVisibility': {
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
|
||||
fileItemDetails: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
gap: theme.spacing(1),
|
||||
justifyContent: 'center',
|
||||
minWidth: 0,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
fileName: {
|
||||
...theme.typography.labelBold,
|
||||
gap: theme.spacing(1),
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
|
||||
fileAuthorParticipant: {
|
||||
alignItems: 'center',
|
||||
display: 'inline-flex',
|
||||
gap: theme.spacing(1)
|
||||
},
|
||||
|
||||
fileAuthorParticipantName: {
|
||||
...theme.typography.labelBold,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
|
||||
fileSize: {
|
||||
...theme.typography.labelRegular
|
||||
},
|
||||
|
||||
fileTimestamp: {
|
||||
...theme.typography.labelRegular,
|
||||
display: 'flex',
|
||||
lineHeight: '1.2rem',
|
||||
marginTop: theme.spacing(1),
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
iconButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '8px',
|
||||
padding: 0,
|
||||
|
||||
'&:focus-visible': {
|
||||
outline: `2px solid ${theme.palette.action01}`,
|
||||
borderRadius: '4px'
|
||||
}
|
||||
},
|
||||
|
||||
progressBar: {
|
||||
backgroundColor: theme.palette.fileSharingItemBorder,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: 4,
|
||||
overflow: 'hidden',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
progressFill: {
|
||||
backgroundColor: theme.palette.action01,
|
||||
height: '100%',
|
||||
transition: 'width 0.3s ease'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component for displaying file information in a consistent way across the application.
|
||||
*
|
||||
* @param {IFileItemProps} props - The component props.
|
||||
* @returns {JSX.Element} The FileItem component.
|
||||
*/
|
||||
const FileItem = ({
|
||||
actionsVisible = true,
|
||||
className = '',
|
||||
file,
|
||||
iconSize = 64,
|
||||
onDownload,
|
||||
onRemove,
|
||||
showAuthor = true,
|
||||
showRemoveButton = false,
|
||||
showTimestamp = true
|
||||
}: IFileItemProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const [ isFocused, setIsFocused ] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const isUploading = (file.progress ?? 100) < 100;
|
||||
|
||||
/**
|
||||
* Handles the download button click.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleDownload = useCallback(() => {
|
||||
onDownload?.(file.fileId);
|
||||
}, [ onDownload, file.fileId ]);
|
||||
|
||||
/**
|
||||
* Handles the remove button click.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleRemove = useCallback(() => {
|
||||
onRemove?.(file.fileId);
|
||||
}, [ onRemove, file.fileId ]);
|
||||
|
||||
/**
|
||||
* Handles blur event to remove focus state.
|
||||
*
|
||||
* @param {React.FocusEvent} e - The blur event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleBlur = useCallback((e: React.FocusEvent) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||
setIsFocused(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handles focus event to set focus state.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleFocus = useCallback(() => {
|
||||
setIsFocused(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { cx(classes.fileItem, isFocused && 'focused', className) }
|
||||
key = { file.fileId }
|
||||
onBlur = { handleBlur }
|
||||
onFocus = { handleFocus }
|
||||
tabIndex = { -1 }
|
||||
title = { file.fileName }>
|
||||
{
|
||||
!isUploading && (
|
||||
<>
|
||||
<div className = { classes.fileIconContainer }>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.fileSharingText }
|
||||
size = { iconSize }
|
||||
src = { getFileIcon(file.fileType) } />
|
||||
</div>
|
||||
<div className = { classes.fileItemDetails }>
|
||||
<div className = { cx(classes.fileName, 'fileName') }>
|
||||
{ file.fileName }
|
||||
</div>
|
||||
<div className = { cx(classes.fileSize, 'fileSize') }>
|
||||
{ formatFileSize(file.fileSize) }
|
||||
</div>
|
||||
{
|
||||
showAuthor && (
|
||||
<div className = { classes.fileAuthorParticipant }>
|
||||
<Avatar
|
||||
displayName = { file.authorParticipantName }
|
||||
participantId = { file.authorParticipantId }
|
||||
size = { 16 } />
|
||||
<div className = { classes.fileAuthorParticipantName }>
|
||||
{ file.authorParticipantName }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
showTimestamp && (
|
||||
<div className = { `${classes.fileTimestamp} timestampVisibility` }>
|
||||
<pre>
|
||||
{ formatTimestamp(file.timestamp) }
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
actionsVisible && (
|
||||
<div className = { `${classes.buttonContainer} actionIconVisibility` }>
|
||||
{
|
||||
onDownload && (
|
||||
<button
|
||||
aria-label = { `${t('fileSharing.downloadFile')} ${file.fileName}` }
|
||||
className = { classes.iconButton }
|
||||
onClick = { handleDownload }
|
||||
type = 'button'>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.fileSharingText }
|
||||
size = { 24 }
|
||||
src = { IconDownload } />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
showRemoveButton && onRemove && (
|
||||
<button
|
||||
aria-label = { `${t('fileSharing.removeFile')} ${file.fileName}` }
|
||||
className = { classes.iconButton }
|
||||
onClick = { handleRemove }
|
||||
type = 'button'>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.fileSharingText }
|
||||
size = { 24 }
|
||||
src = { IconTrash } />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
isUploading && (
|
||||
<div
|
||||
aria-label = { t('fileSharing.fileUploadProgress') }
|
||||
aria-valuemax = { 100 }
|
||||
aria-valuemin = { 0 }
|
||||
aria-valuenow = { file.progress }
|
||||
className = { classes.progressBar }
|
||||
role = 'progressbar'>
|
||||
<div
|
||||
className = { classes.progressFill }
|
||||
style = {{ width: `${file.progress}%` }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileItem;
|
||||
249
react/features/file-sharing/components/web/FileSharing.tsx
Normal file
249
react/features/file-sharing/components/web/FileSharing.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector, useStore } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCloudUpload } from '../../../base/icons/svg';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.web';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
import { downloadFile, removeFile } from '../../actions';
|
||||
import {
|
||||
isFileUploadingEnabled,
|
||||
processFiles
|
||||
} from '../../functions.any';
|
||||
|
||||
import FileItem from './FileItem';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
margin: '0 auto',
|
||||
maxWidth: '600px',
|
||||
padding: theme.spacing(3),
|
||||
position: 'relative',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
dropZone: {
|
||||
backgroundColor: theme.palette.fileSharingItemBackground,
|
||||
border: `2px dashed ${theme.palette.fileSharingItemBorder}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
opacity: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
zIndex: 0,
|
||||
|
||||
'&.dragging': {
|
||||
backgroundColor: theme.palette.fileSharingItemBorder,
|
||||
borderColor: theme.palette.action01,
|
||||
opacity: 0.8,
|
||||
zIndex: 2
|
||||
}
|
||||
},
|
||||
|
||||
fileList: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
listStyleType: 'none',
|
||||
marginBottom: theme.spacing(8),
|
||||
marginTop: 0,
|
||||
overflowY: 'auto',
|
||||
padding: 0,
|
||||
zIndex: 1,
|
||||
|
||||
'& > li': {
|
||||
listStyleType: 'none',
|
||||
margin: 0,
|
||||
padding: 0
|
||||
}
|
||||
},
|
||||
|
||||
hiddenInput: {
|
||||
visibility: 'hidden'
|
||||
},
|
||||
|
||||
noFilesContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '88%',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
noFilesText: {
|
||||
...theme.typography.bodyLongBold,
|
||||
color: theme.palette.fileSharingEmptyText,
|
||||
padding: '0 24px',
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
uploadButton: {
|
||||
bottom: theme.spacing(4),
|
||||
cursor: 'pointer',
|
||||
left: '50%',
|
||||
position: 'absolute',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '85%',
|
||||
zIndex: 1
|
||||
},
|
||||
|
||||
uploadIcon: {
|
||||
margin: '0 auto'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const FileSharing = () => {
|
||||
const { classes } = useStyles();
|
||||
const [ isDragging, setIsDragging ] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const uploadButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const store = useStore();
|
||||
const { files } = useSelector((state: IReduxState) => state['features/file-sharing']);
|
||||
const sortedFiles = Array.from(files.values()).sort((a, b) => a.fileName.localeCompare(b.fileName));
|
||||
const isUploadEnabled = useSelector(isFileUploadingEnabled);
|
||||
|
||||
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
processFiles(e.target.files as FileList, store);
|
||||
e.target.value = ''; // Reset the input value to allow re-uploading the same file
|
||||
uploadButtonRef.current?.focus();
|
||||
}
|
||||
}, [ processFiles ]);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
if (e.dataTransfer.files?.length > 0) {
|
||||
processFiles(e.dataTransfer.files as FileList, store);
|
||||
}
|
||||
}, [ processFiles ]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
fileInputRef.current?.click();
|
||||
}, []);
|
||||
|
||||
const handleKeyPress = useCallback((e: React.KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{
|
||||
isUploadEnabled && (
|
||||
<>
|
||||
<div
|
||||
className = { `${classes.dropZone} ${
|
||||
isDragging ? 'dragging' : ''
|
||||
}` }
|
||||
onDragEnter = { handleDragEnter }
|
||||
onDragLeave = { handleDragLeave }
|
||||
onDragOver = { handleDragOver }
|
||||
onDrop = { handleDrop } />
|
||||
{
|
||||
sortedFiles.length === 0 && (
|
||||
<div
|
||||
className = { classes.noFilesContainer }
|
||||
onClick = { handleClick }
|
||||
onKeyUp = { handleKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
<Icon
|
||||
className = { classes.uploadIcon }
|
||||
color = { BaseTheme.palette.fileSharingEmptyIcon }
|
||||
size = { 160 }
|
||||
src = { IconCloudUpload } />
|
||||
<span className = { classes.noFilesText }>
|
||||
{ t('fileSharing.dragAndDrop') }
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<input
|
||||
className = { classes.hiddenInput }
|
||||
multiple = { true }
|
||||
onChange = { handleFileSelect }
|
||||
ref = { fileInputRef }
|
||||
tabIndex = { -1 }
|
||||
type = 'file' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
sortedFiles.length > 0 && (
|
||||
<ul className = { classes.fileList }>
|
||||
{
|
||||
sortedFiles.map(file => (
|
||||
<li key = { file.fileId }>
|
||||
<FileItem
|
||||
file = { file }
|
||||
onDownload = { fileId => dispatch(downloadFile(fileId)) }
|
||||
onRemove = { fileId => dispatch(removeFile(fileId)) }
|
||||
showRemoveButton = { isUploadEnabled } />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
{
|
||||
<Tooltip
|
||||
containerClassName = { classes.uploadButton }
|
||||
content = { isUploadEnabled ? t('fileSharing.uploadFile') : t('fileSharing.uploadDisabled') }
|
||||
position = 'top'>
|
||||
<Button
|
||||
accessibilityLabel = { t('fileSharing.uploadFile') }
|
||||
className = { classes.uploadButton }
|
||||
disabled = { !isUploadEnabled }
|
||||
labelKey = { 'fileSharing.uploadFile' }
|
||||
onClick = { handleClick }
|
||||
onKeyPress = { handleKeyPress }
|
||||
ref = { uploadButtonRef }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSharing;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconShareDoc } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { openFileSharingPanel } from '../../../chat/actions.any';
|
||||
import { isFileSharingEnabled } from '../../functions.any';
|
||||
|
||||
/**
|
||||
* Component that renders a button to open the file sharing panel.
|
||||
*
|
||||
* @augments AbstractButton
|
||||
*/
|
||||
class FileSharingButton extends AbstractButton<AbstractButtonProps> {
|
||||
override icon = IconShareDoc;
|
||||
override label = 'toolbar.fileSharing';
|
||||
override tooltip = 'toolbar.fileSharing';
|
||||
|
||||
/**
|
||||
* Handles clicking the button to open the file sharing panel.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(openFileSharingPanel());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {Object} - Mapped props.
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
visible: isFileSharingEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(FileSharingButton));
|
||||
Reference in New Issue
Block a user