Files
cinny/src/app/components/upload-card/CompactUploadCardRenderer.tsx
T
jaredandClaude Opus 5 6df160a7bf fix(upload): plain-language upload failure text instead of the raw MatrixError (#213)
The upload card printed the SDK's toString — 'MatrixError: [413] nope
(http://<hs>/_matrix/media/v3/upload?filename=…)'. describeUploadError() maps
the common cases to one sentence: 413/M_TOO_LARGE → 'This file is larger than
the server allows (limit N)' using m.upload.size when known, 429 → 'Slow down —
try again in a moment.', 401/403 → 'The server refused this upload: <server
text>', 5xx/transport after the retry loop → 'Couldn't reach the server. Check
your connection and retry.', other 4xx → the server's own sentence, URL
stripped. Both card renderers use it; the raw error is still console.warn-ed
by uploadContent for debugging. Unit-tested; verified headless with routed
413/403/503 responses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-19 12:31:26 -04:00

112 lines
3.5 KiB
TypeScript

import React, { useEffect } from 'react';
import { Chip, Icon, IconButton, Icons, Text, color } from 'folds';
import { UploadCard, UploadCardError, CompactUploadCardProgress } from './UploadCard';
import { TUploadAtom, UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { TUploadContent } from '../../utils/matrix';
import { bytesToSize, getFileTypeIcon } from '../../utils/common';
import { useMediaConfig } from '../../hooks/useMediaConfig';
import { describeUploadError } from '../../utils/uploadError';
type CompactUploadCardRendererProps = {
isEncrypted?: boolean;
uploadAtom: TUploadAtom;
onRemove: (file: TUploadContent) => void;
onComplete?: (upload: UploadSuccess) => void;
};
export function CompactUploadCardRenderer({
isEncrypted,
uploadAtom,
onRemove,
onComplete,
}: CompactUploadCardRendererProps) {
const mx = useMatrixClient();
const mediaConfig = useMediaConfig();
const allowSize = mediaConfig['m.upload.size'] || Infinity;
const { upload, startUpload, cancelUpload } = useBindUploadAtom(mx, uploadAtom, isEncrypted);
const { file } = upload;
const fileSizeExceeded = file.size >= allowSize;
if (upload.status === UploadStatus.Idle && !fileSizeExceeded) {
startUpload();
}
const removeUpload = () => {
cancelUpload();
onRemove(file);
};
useEffect(() => {
if (upload.status === UploadStatus.Success) {
onComplete?.(upload);
}
}, [upload, onComplete]);
return (
<UploadCard
compact
outlined
radii="300"
before={<Icon src={getFileTypeIcon(Icons, file.type)} />}
after={
<>
{upload.status === UploadStatus.Error && (
<Chip
as="button"
onClick={startUpload}
aria-label="Retry Upload"
variant="Critical"
radii="Pill"
outlined
>
<Text size="B300">Retry</Text>
</Chip>
)}
<IconButton
onClick={removeUpload}
aria-label="Cancel Upload"
variant="SurfaceVariant"
radii="Pill"
size="300"
>
<Icon src={Icons.Cross} size="200" />
</IconButton>
</>
}
>
{upload.status === UploadStatus.Success ? (
<>
<Text size="H6" truncate>
{(file as File).name}
</Text>
<Icon style={{ color: color.Success.Main }} src={Icons.Check} size="100" />
</>
) : (
<>
{upload.status === UploadStatus.Idle && !fileSizeExceeded && (
<CompactUploadCardProgress sentBytes={0} totalBytes={file.size} />
)}
{upload.status === UploadStatus.Loading && (
<CompactUploadCardProgress sentBytes={upload.progress.loaded} totalBytes={file.size} />
)}
{upload.status === UploadStatus.Error && (
<UploadCardError>
<Text size="T200">{describeUploadError(upload.error, allowSize)}</Text>
</UploadCardError>
)}
{upload.status === UploadStatus.Idle && fileSizeExceeded && (
<UploadCardError>
<Text size="T200">
The file size exceeds the limit. Maximum allowed size is{' '}
<b>{bytesToSize(allowSize)}</b>, but the uploaded file is{' '}
<b>{bytesToSize(file.size)}</b>.
</Text>
</UploadCardError>
)}
</>
)}
</UploadCard>
);
}