u
This commit is contained in:
@@ -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"
|
||||
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"
|
||||
onClick={this.handleDownload}
|
||||
sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }}
|
||||
>
|
||||
<DownloadRoundedIcon sx={{ fontSize: 15 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<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>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</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 (
|
||||
<Tooltip title={titleText} arrow>
|
||||
<Box
|
||||
component="a"
|
||||
href={notFound ? undefined : vfsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={this.handleClick}
|
||||
<Box
|
||||
component="a"
|
||||
href={notFound ? undefined : vfsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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={{
|
||||
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)')),
|
||||
fontSize: '0.82rem',
|
||||
fontWeight: 500,
|
||||
color: 'inherit',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.25,
|
||||
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 ? (
|
||||
<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"
|
||||
{downloadName} - {effectiveType || 'VFS'}{notFound ? ' (404)' : ''}
|
||||
</Typography>
|
||||
{!notFound && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
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 && (
|
||||
<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={{
|
||||
fontSize: '0.82rem',
|
||||
fontWeight: 600,
|
||||
p: 0.4,
|
||||
color: 'inherit',
|
||||
display: 'block',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: 300,
|
||||
lineHeight: 1.25,
|
||||
textDecoration: notFound ? 'line-through' : 'none',
|
||||
opacity: 0.75,
|
||||
'&:hover': { opacity: 1, bgcolor: 'action.hover' },
|
||||
}}
|
||||
>
|
||||
{displayName}
|
||||
</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>
|
||||
<DownloadRoundedIcon sx={{ fontSize: 15 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
{!notFound && (
|
||||
<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>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user