diff --git a/src/components/compose.css b/src/components/compose.css index f11ef5b0..0fda7e37 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -340,6 +340,21 @@ display: flex; gap: 8px; align-items: stretch; + + .media-error { + padding: 2px; + color: var(--orange-fg-color); + background-color: transparent; + border: 1.5px dashed transparent; + line-height: 1; + border-radius: 4px; + display: flex; + + &:is(:hover, :focus) { + background-color: var(--bg-color); + border-color: var(--orange-fg-color); + } + } } #compose-container .media-preview { flex-shrink: 0; diff --git a/src/components/compose.jsx b/src/components/compose.jsx index 78ae9300..3972e547 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -1967,6 +1967,26 @@ function CharCountMeter({ maxCharacters = 500, hidden }) { ); } +function prettyBytes(bytes) { + const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + let unitIndex = 0; + while (bytes >= 1024) { + bytes /= 1024; + unitIndex++; + } + return `${bytes.toFixed(0).toLocaleString()} ${units[unitIndex]}`; +} + +function scaleDimension(matrix, matrixLimit, width, height) { + // matrix = number of pixels + // matrixLimit = max number of pixels + // Calculate new width and height, downsize to within the limit, preserve aspect ratio, no decimals + const scalingFactor = Math.sqrt(matrixLimit / matrix); + const newWidth = Math.floor(width * scalingFactor); + const newHeight = Math.floor(height * scalingFactor); + return { newWidth, newHeight }; +} + function MediaAttachment({ attachment, disabled, @@ -1982,6 +2002,81 @@ function MediaAttachment({ [file, attachment.url], ); console.log({ attachment }); + + const checkMaxError = !!file?.size; + const configuration = checkMaxError ? getCurrentInstanceConfiguration() : {}; + const { + mediaAttachments: { + imageSizeLimit, + imageMatrixLimit, + videoSizeLimit, + videoMatrixLimit, + videoFrameRateLimit, + } = {}, + } = configuration || {}; + + const [maxError, setMaxError] = useState(() => { + if (!checkMaxError) return null; + if ( + type.startsWith('image') && + imageSizeLimit && + file.size > imageSizeLimit + ) { + return { + type: 'imageSizeLimit', + details: { + imageSize: file.size, + imageSizeLimit, + }, + }; + } else if ( + type.startsWith('video') && + videoSizeLimit && + file.size > videoSizeLimit + ) { + return { + type: 'videoSizeLimit', + details: { + videoSize: file.size, + videoSizeLimit, + }, + }; + } + return null; + }); + + const [imageMatrix, setImageMatrix] = useState({}); + useEffect(() => { + if (!checkMaxError || !imageMatrixLimit) return; + if (imageMatrix?.matrix > imageMatrixLimit) { + setMaxError({ + type: 'imageMatrixLimit', + details: { + imageMatrix: imageMatrix?.matrix, + imageMatrixLimit, + width: imageMatrix?.width, + height: imageMatrix?.height, + }, + }); + } + }, [imageMatrix, imageMatrixLimit, checkMaxError]); + + const [videoMatrix, setVideoMatrix] = useState({}); + useEffect(() => { + if (!checkMaxError || !videoMatrixLimit) return; + if (videoMatrix?.matrix > videoMatrixLimit) { + setMaxError({ + type: 'videoMatrixLimit', + details: { + videoMatrix: videoMatrix?.matrix, + videoMatrixLimit, + width: videoMatrix?.width, + height: videoMatrix?.height, + }, + }); + } + }, [videoMatrix, videoMatrixLimit, checkMaxError]); + const [description, setDescription] = useState(attachment.description); const [suffixType, subtype] = type.split('/'); const debouncedOnDescriptionChange = useDebouncedCallback( @@ -2053,6 +2148,50 @@ function MediaAttachment({ }; }, []); + const maxErrorToast = useRef(null); + + const maxErrorText = (err) => { + const { type, details } = err; + switch (type) { + case 'imageSizeLimit': { + const { imageSize, imageSizeLimit } = details; + return `File size too large. Uploading might encounter issues. Try reduce the file size from ${prettyBytes( + imageSize, + )} to ${prettyBytes(imageSizeLimit)} or lower.`; + } + case 'imageMatrixLimit': { + const { imageMatrix, imageMatrixLimit, width, height } = details; + const { newWidth, newHeight } = scaleDimension( + imageMatrix, + imageMatrixLimit, + width, + height, + ); + return `Dimension too large. Uploading might encounter issues. Try reduce dimension from ${width.toLocaleString()}×${height.toLocaleString()}px to ${newWidth.toLocaleString()}×${newHeight.toLocaleString()}px.`; + } + case 'videoSizeLimit': { + const { videoSize, videoSizeLimit } = details; + return `File size too large. Uploading might encounter issues. Try reduce the file size from ${prettyBytes( + videoSize, + )} to ${prettyBytes(videoSizeLimit)} or lower.`; + } + case 'videoMatrixLimit': { + const { videoMatrix, videoMatrixLimit, width, height } = details; + const { newWidth, newHeight } = scaleDimension( + videoMatrix, + videoMatrixLimit, + width, + height, + ); + return `Dimension too large. Uploading might encounter issues. Try reduce dimension from ${width.toLocaleString()}×${height.toLocaleString()}px to ${newWidth.toLocaleString()}×${newHeight.toLocaleString()}px.`; + } + case 'videoFrameRateLimit': { + // Not possible to detect this on client-side for now + return 'Frame rate too high. Uploading might encounter issues.'; + } + } + }; + return ( <>
{showModal && (