import React, { ReactNode, useEffect, useRef, useState } from 'react';
import { Box, Chip, Icon, IconButton, Icons, Text, color, config, toRem } from 'folds';
import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { TUploadContent } from '../../utils/matrix';
import { bytesToSize, getFileTypeIcon } from '../../utils/common';
import {
roomUploadAtomFamily,
TUploadItem,
TUploadMetadata,
} from '../../state/room/roomInputDrafts';
import { useObjectURL } from '../../hooks/useObjectURL';
import { useMediaConfig } from '../../hooks/useMediaConfig';
import { compressImage, formatFileSize, isCompressible } from '../../utils/imageCompression';
import { tryDeleteMxcContent } from '../../utils/matrix';
type PreviewImageProps = {
fileItem: TUploadItem;
};
function PreviewImage({ fileItem }: PreviewImageProps) {
const { originalFile, metadata } = fileItem;
const fileUrl = useObjectURL(originalFile);
return (
);
}
type PreviewVideoProps = {
fileItem: TUploadItem;
};
function PreviewVideo({ fileItem }: PreviewVideoProps) {
const { originalFile, metadata } = fileItem;
const fileUrl = useObjectURL(originalFile);
return (
);
}
type MediaPreviewProps = {
fileItem: TUploadItem;
onSpoiler: (marked: boolean) => void;
children: ReactNode;
};
function MediaPreview({ fileItem, onSpoiler, children }: MediaPreviewProps) {
const { originalFile, metadata } = fileItem;
const fileUrl = useObjectURL(originalFile);
return fileUrl ? (
{children}
}
onClick={() => onSpoiler(!metadata.markedAsSpoiler)}
>
Spoiler
) : null;
}
type CompressionCheckboxProps = {
fileItem: TUploadItem;
metadata: TUploadMetadata;
setMetadata: (fileItem: TUploadItem, metadata: TUploadMetadata) => void;
};
function CompressionCheckbox({ fileItem, metadata, setMetadata }: CompressionCheckboxProps) {
const originalFile = fileItem.originalFile as File;
const [compressing, setCompressing] = useState(false);
const compressPromiseRef = useRef | null>(null);
if (!isCompressible(originalFile)) return null;
const handleChange = async (e: React.ChangeEvent) => {
const checked = e.target.checked;
if (!checked) {
setMetadata(fileItem, { ...metadata, compressImage: false, compressionResult: undefined });
return;
}
setMetadata(fileItem, { ...metadata, compressImage: true, compressionResult: undefined });
setCompressing(true);
const p = compressImage(originalFile).then((result) => {
setCompressing(false);
setMetadata(fileItem, { ...metadata, compressImage: true, compressionResult: result });
});
compressPromiseRef.current = p;
};
const checked = !!metadata.compressImage;
const result = metadata.compressionResult;
const savingPct =
result && result.originalSize > 0
? Math.round(((result.originalSize - result.compressedSize) / result.originalSize) * 100)
: null;
const originalSize = formatFileSize(originalFile.size);
return (
{/* Checkbox row */}
{compressing && (
compressing…
)}
{/* Before / after row — always show original; show compressed when ready */}
Original
{originalSize}
{checked && !compressing && result !== undefined && (
<>
→
5
? (color.Success?.Container ?? color.Primary.Container)
: color.Surface.Container,
border: `1px solid ${
savingPct !== null && savingPct > 5
? (color.Success?.ContainerLine ?? color.Primary.ContainerLine)
: color.Surface.ContainerLine
}`,
}}
>
Compressed
{result ? (
{formatFileSize(result.compressedSize)}
{savingPct !== null && savingPct > 0 && (
({savingPct}% smaller)
)}
) : (
unavailable
)}
>
)}
);
}
type UploadCardRendererProps = {
isEncrypted?: boolean;
fileItem: TUploadItem;
setMetadata: (fileItem: TUploadItem, metadata: TUploadMetadata) => void;
onRemove: (file: TUploadContent) => void;
onComplete?: (upload: UploadSuccess) => void;
};
export function UploadCardRenderer({
isEncrypted,
fileItem,
setMetadata,
onRemove,
onComplete,
}: UploadCardRendererProps) {
const mx = useMatrixClient();
const mediaConfig = useMediaConfig();
const allowSize = mediaConfig['m.upload.size'] || Infinity;
const uploadAtom = roomUploadAtomFamily(fileItem.file);
const { metadata } = fileItem;
const { upload, startUpload, cancelUpload } = useBindUploadAtom(mx, uploadAtom, isEncrypted);
const { file } = upload;
const fileSizeExceeded = file.size >= allowSize;
if (upload.status === UploadStatus.Idle && !fileSizeExceeded) {
startUpload();
}
const handleSpoiler = (marked: boolean) => {
setMetadata(fileItem, { ...metadata, markedAsSpoiler: marked });
};
const removeUpload = () => {
if (upload.status === UploadStatus.Success) {
// Upload already completed — delete the orphaned MXC from the server.
tryDeleteMxcContent(mx, upload.mxc);
}
cancelUpload();
onRemove(file);
};
useEffect(() => {
if (upload.status === UploadStatus.Success) {
onComplete?.(upload);
}
}, [upload, onComplete]);
return (
}
after={
<>
{upload.status === UploadStatus.Error && (
Retry
)}
>
}
bottom={
<>
{fileItem.originalFile.type.startsWith('image') && (
)}
{fileItem.originalFile.type.startsWith('video') && (
)}
{(fileItem.originalFile.type.startsWith('image') ||
fileItem.originalFile.type.startsWith('video')) && (
setMetadata(fileItem, { ...metadata, caption: e.target.value })}
data-caption-input
style={{
marginTop: '6px',
width: '100%',
background: 'var(--bg-surface-low)',
border: '1px solid var(--bg-surface-border)',
borderRadius: '6px',
padding: '5px 8px',
fontSize: '0.85rem',
color: 'var(--text-primary)',
outline: 'none',
boxSizing: 'border-box',
transition: 'border-color 0.15s, box-shadow 0.15s',
}}
/>
)}
{upload.status === UploadStatus.Idle && !fileSizeExceeded && (
)}
{upload.status === UploadStatus.Loading && (
)}
{upload.status === UploadStatus.Error && (
{upload.error.message}
)}
{upload.status === UploadStatus.Idle && fileSizeExceeded && (
The file size exceeds the limit. Maximum allowed size is{' '}
{bytesToSize(allowSize)}, but the uploaded file is{' '}
{bytesToSize(file.size)}.
)}
>
}
>
{(file as File).name}
{upload.status === UploadStatus.Success && (
)}
);
}