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 IconButton from '@mui/material/IconButton';
import CircularProgress from '@mui/material/CircularProgress';
import Tooltip from '@mui/material/Tooltip';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import PictureAsPdfRoundedIcon from '@mui/icons-material/PictureAsPdfRounded';
import TableChartRoundedIcon from '@mui/icons-material/TableChartRounded';
@@ -399,7 +398,6 @@ export class VfsAttachmentLink extends PureComponent {
target="_blank"
rel="noopener noreferrer"
onClick={this.handleClick}
title={notFound ? 'Nicht mehr vorhanden (404)' : `VFS Bild im neuen Tab öffnen: ${downloadName}`}
sx={{
display: 'block',
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 }}>
<InsertPhotoRoundedIcon sx={{ fontSize: 16, flexShrink: 0 }} />
<Typography
variant="caption"
sx={{
fontWeight: 600,
fontWeight: 500,
fontSize: '0.72rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{downloadName}
{downloadName} - {effectiveType || 'image'}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.2, flexShrink: 0 }}>
@@ -457,24 +454,22 @@ export class VfsAttachmentLink extends PureComponent {
)}
{!notFound && (
<>
<Tooltip title="In neuem Tab öffnen" arrow>
<IconButton
size="small"
aria-label="In neuem Tab öffnen"
onClick={this.handleOpenInNewTab}
sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }}
>
<OpenInNewRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Tooltip>
<Tooltip title="Herunterladen" arrow>
<IconButton
size="small"
aria-label="Herunterladen"
onClick={this.handleDownload}
sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }}
>
<DownloadRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Tooltip>
</>
)}
</Box>
@@ -483,18 +478,7 @@ 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 (
<Tooltip title={titleText} arrow>
<Box
component="a"
href={notFound ? undefined : vfsUrl}
@@ -536,45 +520,21 @@ export class VfsAttachmentLink extends PureComponent {
},
}}
>
{isDownloading || isOpening ? (
<CircularProgress size={16} color="inherit" thickness={5} />
) : notFound ? (
<ErrorOutlineRoundedIcon sx={{ fontSize: 20, color: isUser ? 'inherit' : 'error.main' }} />
) : (
getAttachmentIcon(effectiveType, downloadName, isUser)
)}
<Box sx={{ minWidth: 0 }}>
<Typography
component="span"
sx={{
fontSize: '0.82rem',
fontWeight: 600,
fontWeight: 500,
color: 'inherit',
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: 300,
lineHeight: 1.25,
textDecoration: notFound ? 'line-through' : 'none',
}}
>
{displayName}
{downloadName} - {effectiveType || 'VFS'}{notFound ? ' (404)' : ''}
</Typography>
<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>
{!notFound && (
<Box
sx={{
@@ -582,13 +542,17 @@ export class VfsAttachmentLink extends PureComponent {
alignItems: 'center',
gap: 0.2,
ml: 0.5,
flexShrink: 0,
}}
onClick={(e) => e.stopPropagation()}
>
{(isDownloading || isOpening) && (
<CircularProgress size={13} color="inherit" thickness={4} sx={{ mx: 0.4 }} />
)}
{canView && (
<Tooltip title="In neuem Tab öffnen" arrow>
<IconButton
size="small"
aria-label="In neuem Tab öffnen"
onClick={this.handleOpenInNewTab}
sx={{
p: 0.4,
@@ -599,11 +563,10 @@ export class VfsAttachmentLink extends PureComponent {
>
<OpenInNewRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Tooltip>
)}
<Tooltip title="Herunterladen" arrow>
<IconButton
size="small"
aria-label="Herunterladen"
onClick={this.handleDownload}
sx={{
p: 0.4,
@@ -614,11 +577,9 @@ export class VfsAttachmentLink extends PureComponent {
>
<DownloadRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Tooltip>
</Box>
)}
</Box>
</Tooltip>
);
}
}

View File

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