Files
dashboard/frontend/src/components/chat/FileMessageContent.tsx
Matthias Hochmeister 5032e1593b new features
2026-03-23 13:08:19 +01:00

272 lines
9.3 KiB
TypeScript

import React, { useState } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import DownloadIcon from '@mui/icons-material/Download';
import CloseIcon from '@mui/icons-material/Close';
interface FileParam {
name: string;
size?: number;
mimetype?: string;
id: number | string;
path?: string;
previewAvailable?: string;
}
interface FileMessageContentProps {
messageParameters: Record<string, any>;
isOwnMessage: boolean;
}
function formatFileSize(bytes?: number): string {
if (!bytes) return '';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function extractFileParams(messageParameters: Record<string, any>): FileParam[] {
const files: FileParam[] = [];
for (const key of Object.keys(messageParameters)) {
if (key === 'file' || key.startsWith('file')) {
const val = messageParameters[key];
if (val && typeof val === 'object' && val.id) {
files.push(val as FileParam);
}
}
}
return files;
}
type OverlayMode = 'image' | 'pdf' | 'video';
interface ContentOverlayProps {
open: boolean;
onClose: () => void;
mode: OverlayMode;
contentUrl: string;
downloadUrl: string;
name: string;
}
const ContentOverlay: React.FC<ContentOverlayProps> = ({ open, onClose, mode, contentUrl, downloadUrl, name }) => (
<Dialog
open={open}
onClose={onClose}
maxWidth="lg"
fullWidth
PaperProps={{ sx: { bgcolor: mode === 'image' ? 'black' : 'background.paper', m: 1 } }}
>
<DialogContent sx={{ p: 0, position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
{mode === 'image' && (
<Box
component="img"
src={contentUrl}
alt={name}
sx={{ maxWidth: '100%', maxHeight: '85vh', display: 'block', objectFit: 'contain' }}
/>
)}
{mode === 'pdf' && (
<Box component="iframe" src={contentUrl} sx={{ width: '100%', height: '80vh', border: 'none', display: 'block' }} title={name} />
)}
{mode === 'video' && (
<Box
component="video"
controls
src={contentUrl}
sx={{ maxWidth: '100%', maxHeight: '85vh', display: 'block' }}
/>
)}
<Tooltip title="Schlie\u00DFen">
<IconButton
onClick={onClose}
size="small"
sx={{ position: 'absolute', top: 8, right: 8, bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.75)' } }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Herunterladen">
<IconButton
component="a"
href={downloadUrl}
download={name}
target="_blank"
rel="noopener noreferrer"
size="small"
sx={{ position: 'absolute', top: 8, right: 44, bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.75)' } }}
>
<DownloadIcon fontSize="small" />
</IconButton>
</Tooltip>
<Typography
variant="caption"
sx={{ position: 'absolute', bottom: 8, left: 0, right: 0, textAlign: 'center', color: 'rgba(255,255,255,0.7)', px: 2 }}
noWrap
>
{name}
</Typography>
</DialogContent>
</Dialog>
);
const FileMessageContent: React.FC<FileMessageContentProps> = ({ messageParameters, isOwnMessage }) => {
const [overlayFile, setOverlayFile] = useState<{ file: FileParam; mode: OverlayMode } | null>(null);
const files = extractFileParams(messageParameters);
if (files.length === 0) return null;
const getOverlayMode = (file: FileParam): OverlayMode | null => {
const mime = file.mimetype ?? '';
if (mime.startsWith('image/') && file.previewAvailable === 'yes') return 'image';
if (mime === 'application/pdf') return 'pdf';
if (mime.startsWith('video/')) return 'video';
return null;
};
return (
<>
{files.map((file, idx) => {
const downloadUrl = `/api/nextcloud/talk/files/${file.id}/download${file.path ? `?path=${encodeURIComponent(file.path)}` : ''}`;
const thumbUrl = `/api/nextcloud/talk/files/${file.id}/preview?w=400&h=400`;
const mime = file.mimetype ?? '';
const isImage = mime.startsWith('image/') && file.previewAvailable === 'yes';
const isAudio = mime.startsWith('audio/');
const overlayMode = getOverlayMode(file);
if (isImage) {
return (
<Box key={idx} sx={{ mt: 0.5 }}>
<Box
component="img"
src={thumbUrl}
alt={file.name}
onClick={() => setOverlayFile({ file, mode: 'image' })}
sx={{
maxWidth: '100%',
maxHeight: 200,
borderRadius: 1,
display: 'block',
cursor: 'zoom-in',
'&:hover': { opacity: 0.9 },
}}
/>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mt: 0.25 }}>
<Typography variant="caption" sx={{ opacity: 0.7, fontSize: '0.7rem' }} noWrap>
{file.name}
</Typography>
<Tooltip title="Herunterladen">
<IconButton
size="small"
component="a"
href={downloadUrl}
download={file.name}
target="_blank"
rel="noopener noreferrer"
sx={{ color: 'inherit', p: 0.25, flexShrink: 0 }}
>
<DownloadIcon sx={{ fontSize: '0.9rem' }} />
</IconButton>
</Tooltip>
</Box>
</Box>
);
}
if (isAudio) {
return (
<Box key={idx} sx={{ mt: 0.5 }}>
<Box
component="audio"
controls
src={downloadUrl}
sx={{ width: '100%', display: 'block' }}
/>
<Typography variant="caption" sx={{ opacity: 0.7, fontSize: '0.7rem', display: 'block', mt: 0.25 }} noWrap>
{file.name}
</Typography>
</Box>
);
}
return (
<Box
key={idx}
onClick={() => {
if (overlayMode) {
setOverlayFile({ file, mode: overlayMode });
} else {
window.open(downloadUrl, '_blank', 'noopener,noreferrer');
}
}}
sx={{
mt: 0.5,
display: 'flex',
alignItems: 'center',
gap: 1,
p: 0.75,
borderRadius: 1,
cursor: 'pointer',
bgcolor: isOwnMessage
? 'rgba(255,255,255,0.15)'
: (theme: any) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)',
'&:hover': {
bgcolor: isOwnMessage
? 'rgba(255,255,255,0.25)'
: (theme: any) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
},
}}
>
<InsertDriveFileIcon fontSize="small" sx={{ opacity: 0.8, flexShrink: 0 }} />
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="body2" noWrap title={file.name} sx={{ fontWeight: 500, lineHeight: 1.2 }}>
{file.name}
</Typography>
{file.size && (
<Typography variant="caption" sx={{ opacity: 0.7 }}>
{formatFileSize(file.size)}
</Typography>
)}
</Box>
<Tooltip title="Herunterladen">
<IconButton
size="small"
component="a"
href={downloadUrl}
download={file.name}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
sx={{ flexShrink: 0, color: 'inherit' }}
>
<DownloadIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
);
})}
{overlayFile && (
<ContentOverlay
open
onClose={() => setOverlayFile(null)}
mode={overlayFile.mode}
contentUrl={
overlayFile.mode === 'image'
? `/api/nextcloud/talk/files/${overlayFile.file.id}/preview?w=1200&h=1200`
: `/api/nextcloud/talk/files/${overlayFile.file.id}/download${overlayFile.file.path ? `?path=${encodeURIComponent(overlayFile.file.path)}` : ''}`
}
downloadUrl={`/api/nextcloud/talk/files/${overlayFile.file.id}/download${overlayFile.file.path ? `?path=${encodeURIComponent(overlayFile.file.path)}` : ''}`}
name={overlayFile.file.name}
/>
)}
</>
);
};
export default FileMessageContent;