This commit is contained in:
seb
2026-10-06 12:53:32 +02:00
parent b99b39f640
commit 1b5d8efbc3
2 changed files with 119 additions and 160 deletions

View File

@@ -9,7 +9,6 @@ import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import Tooltip from '@mui/material/Tooltip';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import PictureAsPdfRoundedIcon from '@mui/icons-material/PictureAsPdfRounded'; import PictureAsPdfRoundedIcon from '@mui/icons-material/PictureAsPdfRounded';
import TableChartRoundedIcon from '@mui/icons-material/TableChartRounded'; import TableChartRoundedIcon from '@mui/icons-material/TableChartRounded';
@@ -399,7 +398,6 @@ export class VfsAttachmentLink extends PureComponent {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={this.handleClick} onClick={this.handleClick}
title={notFound ? 'Nicht mehr vorhanden (404)' : `VFS Bild im neuen Tab öffnen: ${downloadName}`}
sx={{ sx={{
display: 'block', display: 'block',
cursor: notFound ? 'not-allowed' : 'pointer', cursor: notFound ? 'not-allowed' : 'pointer',
@@ -437,18 +435,17 @@ export class VfsAttachmentLink extends PureComponent {
}} }}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 0, flex: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 0, flex: 1 }}>
<InsertPhotoRoundedIcon sx={{ fontSize: 16, flexShrink: 0 }} />
<Typography <Typography
variant="caption" variant="caption"
sx={{ sx={{
fontWeight: 600, fontWeight: 500,
fontSize: '0.72rem', fontSize: '0.72rem',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
> >
{downloadName} {downloadName} - {effectiveType || 'image'}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.2, flexShrink: 0 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.2, flexShrink: 0 }}>
@@ -457,24 +454,22 @@ export class VfsAttachmentLink extends PureComponent {
)} )}
{!notFound && ( {!notFound && (
<> <>
<Tooltip title="In neuem Tab öffnen" arrow> <IconButton
<IconButton size="small"
size="small" aria-label="In neuem Tab öffnen"
onClick={this.handleOpenInNewTab} onClick={this.handleOpenInNewTab}
sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }} sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }}
> >
<OpenInNewRoundedIcon sx={{ fontSize: 15 }} /> <OpenInNewRoundedIcon sx={{ fontSize: 15 }} />
</IconButton> </IconButton>
</Tooltip> <IconButton
<Tooltip title="Herunterladen" arrow> size="small"
<IconButton aria-label="Herunterladen"
size="small" onClick={this.handleDownload}
onClick={this.handleDownload} sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }}
sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }} >
> <DownloadRoundedIcon sx={{ fontSize: 15 }} />
<DownloadRoundedIcon sx={{ fontSize: 15 }} /> </IconButton>
</IconButton>
</Tooltip>
</> </>
)} )}
</Box> </Box>
@@ -483,142 +478,108 @@ export class VfsAttachmentLink extends PureComponent {
); );
} }
const titleText = notFound
? `Datei nicht mehr im VFS vorhanden (404): ${downloadName}`
: isOpening
? `Wird im neuen Tab geöffnet…`
: isDownloading
? `Wird heruntergeladen…`
: canView
? `In neuem Tab öffnen (${downloadName})`
: `Herunterladen (${downloadName})`;
return ( return (
<Tooltip title={titleText} arrow> <Box
<Box component="a"
component="a" href={notFound ? undefined : vfsUrl}
href={notFound ? undefined : vfsUrl} target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" onClick={this.handleClick}
onClick={this.handleClick} sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.85,
my: 0.35,
mx: 0.2,
pl: 1.15,
pr: 0.85,
py: 0.5,
borderRadius: 2,
border: '1px solid',
borderColor: notFound
? (isUser ? 'rgba(255,255,255,0.2)' : 'error.light')
: (isUser ? 'rgba(255, 255, 255, 0.35)' : 'divider'),
bgcolor: notFound
? (isUser ? 'rgba(0,0,0,0.2)' : 'action.disabledBackground')
: (isUser
? 'rgba(255, 255, 255, 0.18)'
: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : 'rgba(0, 0, 0, 0.02)')),
textDecoration: notFound ? 'line-through' : 'none',
color: notFound
? (isUser ? 'rgba(255,255,255,0.6)' : 'text.disabled')
: (isUser ? 'inherit' : 'primary.main'),
opacity: notFound ? 0.7 : ((isDownloading || isOpening) ? 0.75 : 1),
cursor: notFound ? 'not-allowed' : 'pointer',
verticalAlign: 'middle',
maxWidth: '100%',
transition: 'all 150ms ease-in-out',
'&:hover': notFound ? {} : {
bgcolor: isUser ? 'rgba(255, 255, 255, 0.28)' : 'action.hover',
borderColor: isUser ? '#fff' : 'primary.main',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
},
}}
>
<Typography
component="span"
sx={{ sx={{
display: 'inline-flex', fontSize: '0.82rem',
alignItems: 'center', fontWeight: 500,
gap: 0.85, color: 'inherit',
my: 0.35, overflow: 'hidden',
mx: 0.2, textOverflow: 'ellipsis',
pl: 1.15, whiteSpace: 'nowrap',
pr: 0.85, lineHeight: 1.25,
py: 0.5,
borderRadius: 2,
border: '1px solid',
borderColor: notFound
? (isUser ? 'rgba(255,255,255,0.2)' : 'error.light')
: (isUser ? 'rgba(255, 255, 255, 0.35)' : 'divider'),
bgcolor: notFound
? (isUser ? 'rgba(0,0,0,0.2)' : 'action.disabledBackground')
: (isUser
? 'rgba(255, 255, 255, 0.18)'
: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : 'rgba(0, 0, 0, 0.02)')),
textDecoration: notFound ? 'line-through' : 'none', textDecoration: notFound ? 'line-through' : 'none',
color: notFound
? (isUser ? 'rgba(255,255,255,0.6)' : 'text.disabled')
: (isUser ? 'inherit' : 'primary.main'),
opacity: notFound ? 0.7 : ((isDownloading || isOpening) ? 0.75 : 1),
cursor: notFound ? 'not-allowed' : 'pointer',
verticalAlign: 'middle',
maxWidth: '100%',
transition: 'all 150ms ease-in-out',
'&:hover': notFound ? {} : {
bgcolor: isUser ? 'rgba(255, 255, 255, 0.28)' : 'action.hover',
borderColor: isUser ? '#fff' : 'primary.main',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
},
}} }}
> >
{isDownloading || isOpening ? ( {downloadName} - {effectiveType || 'VFS'}{notFound ? ' (404)' : ''}
<CircularProgress size={16} color="inherit" thickness={5} /> </Typography>
) : notFound ? ( {!notFound && (
<ErrorOutlineRoundedIcon sx={{ fontSize: 20, color: isUser ? 'inherit' : 'error.main' }} /> <Box
) : ( sx={{
getAttachmentIcon(effectiveType, downloadName, isUser) display: 'inline-flex',
)} alignItems: 'center',
<Box sx={{ minWidth: 0 }}> gap: 0.2,
<Typography ml: 0.5,
component="span" flexShrink: 0,
}}
onClick={(e) => e.stopPropagation()}
>
{(isDownloading || isOpening) && (
<CircularProgress size={13} color="inherit" thickness={4} sx={{ mx: 0.4 }} />
)}
{canView && (
<IconButton
size="small"
aria-label="In neuem Tab öffnen"
onClick={this.handleOpenInNewTab}
sx={{
p: 0.4,
color: 'inherit',
opacity: 0.75,
'&:hover': { opacity: 1, bgcolor: 'action.hover' },
}}
>
<OpenInNewRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
)}
<IconButton
size="small"
aria-label="Herunterladen"
onClick={this.handleDownload}
sx={{ sx={{
fontSize: '0.82rem', p: 0.4,
fontWeight: 600,
color: 'inherit', color: 'inherit',
display: 'block', opacity: 0.75,
overflow: 'hidden', '&:hover': { opacity: 1, bgcolor: 'action.hover' },
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: 300,
lineHeight: 1.25,
textDecoration: notFound ? 'line-through' : 'none',
}} }}
> >
{displayName} <DownloadRoundedIcon sx={{ fontSize: 15 }} />
</Typography> </IconButton>
<Typography
component="span"
sx={{
fontSize: '0.68rem',
opacity: 0.8,
display: 'block',
lineHeight: 1,
}}
>
{notFound
? 'nicht mehr vorhanden (404)'
: (label && label !== downloadName ? `${downloadName} · ${effectiveType || 'VFS'}` : (effectiveType || 'VFS Datei'))}
</Typography>
</Box> </Box>
{!notFound && ( )}
<Box </Box>
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.2,
ml: 0.5,
}}
onClick={(e) => e.stopPropagation()}
>
{canView && (
<Tooltip title="In neuem Tab öffnen" arrow>
<IconButton
size="small"
onClick={this.handleOpenInNewTab}
sx={{
p: 0.4,
color: 'inherit',
opacity: 0.75,
'&:hover': { opacity: 1, bgcolor: 'action.hover' },
}}
>
<OpenInNewRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Tooltip>
)}
<Tooltip title="Herunterladen" arrow>
<IconButton
size="small"
onClick={this.handleDownload}
sx={{
p: 0.4,
color: 'inherit',
opacity: 0.75,
'&:hover': { opacity: 1, bgcolor: 'action.hover' },
}}
>
<DownloadRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Tooltip>
</Box>
)}
</Box>
</Tooltip>
); );
} }
} }

View File

@@ -9,7 +9,7 @@ import {
TABLE_HEADER_PAIR, TABLE_HEADER_PAIR,
byMode, byMode,
} from '../../colors'; } from '../../colors';
import { VfsAttachmentLink, parseAttachmentTag, classifyLinkTarget } from './ChatAttachmentItems'; import { VfsAttachmentLink, parseAttachmentTag, classifyLinkTarget, resolveVfsUrl } from './ChatAttachmentItems';
/** /**
* Healing markdown renderer. * Healing markdown renderer.
@@ -43,16 +43,14 @@ function resolveImageUrl(rawUrl) {
const url = String(rawUrl ?? '').trim(); const url = String(rawUrl ?? '').trim();
if (!url) return null; if (!url) return null;
if (/^data:image\//i.test(url)) return url; if (/^data:image\//i.test(url)) return url;
if (/^https?:\/\//i.test(url) || url.startsWith('/')) return url; const classified = classifyLinkTarget(url);
if (classified.kind === 'web') return classified.url;
// Everything else is a VFS name: strip a leading `./`, encode each segment, keep subdirs. if (classified.kind === 'vfs') {
const segments = url if (classified.path.includes('..')) return null;
.replace(/^\.\//, '') return resolveVfsUrl(classified.path);
.split('/') }
.filter((s) => s && s !== '.') if (classified.kind === 'nav') return classified.url;
.map((s) => encodeURIComponent(s)); return null;
if (segments.length === 0 || segments.includes('..')) return null;
return `/api/vfs/${segments.join('/')}`;
} }
/** /**