mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-30 17:12:58 +08:00
fix(file-viewer): prevent stale media preview when switching files
Gate the rendered blob on a per-source key (projectId:path:kind) and clear the URL in the missing-projectId branch, so a previous file's blob can no longer flash under the new filename and the error state fully replaces an old preview (CodeRabbit: Functional Correctness, Minor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,9 +46,16 @@ export default function CodeEditorMediaPreview({
|
|||||||
const [url, setUrl] = useState<string | null>(null);
|
const [url, setUrl] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
// Identifies which file the current `url` was loaded for. Rendering is gated on
|
||||||
|
// this so a blob from a previously-opened file can never show under the new
|
||||||
|
// file (the editor reuses this component instance across files).
|
||||||
|
const [loadedKey, setLoadedKey] = useState<string | null>(null);
|
||||||
|
const sourceKey = `${projectId ?? ''}:${file.path}:${kind}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
|
setUrl(null);
|
||||||
|
setLoadedKey(null);
|
||||||
setError(labels.error);
|
setError(labels.error);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -97,6 +104,7 @@ export default function CodeEditorMediaPreview({
|
|||||||
const typed = outType && outType !== blob.type ? new Blob([blob], { type: outType }) : blob;
|
const typed = outType && outType !== blob.type ? new Blob([blob], { type: outType }) : blob;
|
||||||
objectUrl = URL.createObjectURL(typed);
|
objectUrl = URL.createObjectURL(typed);
|
||||||
setUrl(objectUrl);
|
setUrl(objectUrl);
|
||||||
|
setLoadedKey(sourceKey);
|
||||||
} catch (loadError: unknown) {
|
} catch (loadError: unknown) {
|
||||||
if (loadError instanceof Error && loadError.name === 'AbortError') {
|
if (loadError instanceof Error && loadError.name === 'AbortError') {
|
||||||
return;
|
return;
|
||||||
@@ -116,15 +124,19 @@ export default function CodeEditorMediaPreview({
|
|||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [file.path, projectId, kind, labels.error]);
|
}, [file.path, file.name, projectId, kind, sourceKey, labels.error]);
|
||||||
|
|
||||||
|
// Only expose the blob once it matches the file currently being shown, so a
|
||||||
|
// stale URL from the previous file is never rendered during a switch.
|
||||||
|
const currentUrl = url && loadedKey === sourceKey ? url : null;
|
||||||
|
|
||||||
const renderMedia = () => {
|
const renderMedia = () => {
|
||||||
if (!url) return null;
|
if (!currentUrl) return null;
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'image':
|
case 'image':
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={currentUrl}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
className="max-h-full max-w-full object-contain"
|
className="max-h-full max-w-full object-contain"
|
||||||
/>
|
/>
|
||||||
@@ -134,10 +146,10 @@ export default function CodeEditorMediaPreview({
|
|||||||
// load inside a sandboxed frame (any `sandbox` value yields a broken
|
// load inside a sandboxed frame (any `sandbox` value yields a broken
|
||||||
// viewer). Script execution is instead prevented upstream by validating
|
// viewer). Script execution is instead prevented upstream by validating
|
||||||
// the PDF magic bytes and pinning the blob's MIME type to application/pdf.
|
// the PDF magic bytes and pinning the blob's MIME type to application/pdf.
|
||||||
return <iframe src={url} title={file.name} className="h-full w-full border-0 bg-white" />;
|
return <iframe src={currentUrl} title={file.name} className="h-full w-full border-0 bg-white" />;
|
||||||
case 'video':
|
case 'video':
|
||||||
return (
|
return (
|
||||||
<video src={url} controls className="max-h-full max-w-full" autoPlay={false}>
|
<video src={currentUrl} controls className="max-h-full max-w-full" autoPlay={false}>
|
||||||
{labels.error}
|
{labels.error}
|
||||||
</video>
|
</video>
|
||||||
);
|
);
|
||||||
@@ -145,7 +157,7 @@ export default function CodeEditorMediaPreview({
|
|||||||
return (
|
return (
|
||||||
<div className="flex w-full max-w-xl flex-col items-center gap-4 px-6">
|
<div className="flex w-full max-w-xl flex-col items-center gap-4 px-6">
|
||||||
<p className="max-w-full truncate text-sm text-muted-foreground">{file.name}</p>
|
<p className="max-w-full truncate text-sm text-muted-foreground">{file.name}</p>
|
||||||
<audio src={url} controls className="w-full">
|
<audio src={currentUrl} controls className="w-full">
|
||||||
{labels.error}
|
{labels.error}
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,9 +173,9 @@ export default function CodeEditorMediaPreview({
|
|||||||
<div className="text-sm text-muted-foreground">{labels.loading}</div>
|
<div className="text-sm text-muted-foreground">{labels.loading}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && url && renderMedia()}
|
{!loading && currentUrl && renderMedia()}
|
||||||
|
|
||||||
{!loading && !url && (
|
{!loading && !currentUrl && (
|
||||||
<div className="flex flex-col items-center gap-3 p-8 text-center text-muted-foreground">
|
<div className="flex flex-col items-center gap-3 p-8 text-center text-muted-foreground">
|
||||||
<p className="text-sm">{error || labels.error}</p>
|
<p className="text-sm">{error || labels.error}</p>
|
||||||
<p className="break-all text-xs">{file.path}</p>
|
<p className="break-all text-xs">{file.path}</p>
|
||||||
@@ -174,9 +186,9 @@ export default function CodeEditorMediaPreview({
|
|||||||
|
|
||||||
const headerActions = (
|
const headerActions = (
|
||||||
<div className="flex shrink-0 items-center gap-0.5">
|
<div className="flex shrink-0 items-center gap-0.5">
|
||||||
{url && (
|
{currentUrl && (
|
||||||
<a
|
<a
|
||||||
href={url}
|
href={currentUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||||
|
|||||||
Reference in New Issue
Block a user