fix: image compression checkbox now shows for all raster image types
The checkbox was only shown for image/jpeg and image/png. Users
uploading WebP, GIF, AVIF, BMP, TIFF, HEIC (iPhone photos) or any
other raster format never saw the checkbox at all.
Fix: isCompressible now checks file.type.startsWith('image/') and
excludes only image/svg+xml (vector — would rasterise) and empty type
strings. compressImage signature widened to File | Blob so it matches
the TUploadContent type without unsafe casts.
The send-path guard in handleSendUpload was also widened from the
hardcoded jpeg/png check to use isCompressible(), keeping the two gates
in sync. The Blob-safe id attribute uses the .name fallback so it
doesn't break when originalFile is a Blob without a name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,7 @@ type CompressionCheckboxProps = {
|
||||
setMetadata: (fileItem: TUploadItem, metadata: TUploadMetadata) => void;
|
||||
};
|
||||
function CompressionCheckbox({ fileItem, metadata, setMetadata }: CompressionCheckboxProps) {
|
||||
const originalFile = fileItem.originalFile as File;
|
||||
const originalFile = fileItem.originalFile as File | Blob;
|
||||
const [compressing, setCompressing] = useState(false);
|
||||
const compressPromiseRef = useRef<Promise<void> | null>(null);
|
||||
|
||||
@@ -136,6 +136,7 @@ function CompressionCheckbox({ fileItem, metadata, setMetadata }: CompressionChe
|
||||
: null;
|
||||
|
||||
const originalSize = formatFileSize(originalFile.size);
|
||||
const fileId = `compress-${(originalFile as File).name ?? ''}-${originalFile.size}`;
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -152,14 +153,14 @@ function CompressionCheckbox({ fileItem, metadata, setMetadata }: CompressionChe
|
||||
{/* Checkbox row */}
|
||||
<Box alignItems="Center" gap="200">
|
||||
<input
|
||||
id={`compress-${originalFile.name}-${originalFile.size}`}
|
||||
id={fileId}
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
style={{ cursor: 'pointer', flexShrink: 0 }}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`compress-${originalFile.name}-${originalFile.size}`}
|
||||
htmlFor={fileId}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
mxcUrlToHttp,
|
||||
tryDeleteMxcContent,
|
||||
} from '../../utils/matrix';
|
||||
import { compressImage } from '../../utils/imageCompression';
|
||||
import { compressImage, isCompressible } from '../../utils/imageCompression';
|
||||
import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater';
|
||||
import { useFilePicker } from '../../hooks/useFilePicker';
|
||||
import { useFilePasteHandler } from '../../hooks/useFilePasteHandler';
|
||||
@@ -412,16 +412,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
// Resolve the MXC URL to use — may be overridden if compression is enabled
|
||||
let mxc = upload.mxc;
|
||||
|
||||
if (
|
||||
fileItem.metadata.compressImage &&
|
||||
fileItem.originalFile.type.startsWith('image') &&
|
||||
(fileItem.originalFile.type === 'image/jpeg' ||
|
||||
fileItem.originalFile.type === 'image/png')
|
||||
) {
|
||||
if (fileItem.metadata.compressImage && isCompressible(fileItem.originalFile)) {
|
||||
// Use the cached compression result if available, otherwise compute it now
|
||||
let compressionResult = fileItem.metadata.compressionResult;
|
||||
if (compressionResult === undefined) {
|
||||
compressionResult = await compressImage(fileItem.originalFile as File);
|
||||
compressionResult = await compressImage(fileItem.originalFile);
|
||||
}
|
||||
|
||||
if (compressionResult) {
|
||||
|
||||
@@ -6,19 +6,26 @@ export type CompressionResult = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
const COMPRESSIBLE_TYPES = ['image/jpeg', 'image/png'];
|
||||
// Any raster image the browser can render to canvas can be compressed to JPEG.
|
||||
// SVG is vector and must stay as-is; GIF animation is lost on canvas but static
|
||||
// frames compress fine. Empty type string (undetected MIME) is excluded.
|
||||
const isCompressibleType = (type: string): boolean =>
|
||||
type.startsWith('image/') && type !== 'image/svg+xml' && type !== '';
|
||||
|
||||
/** Returns true if this file type can be compressed (JPEG or PNG, any size). */
|
||||
export function isCompressible(file: File): boolean {
|
||||
return COMPRESSIBLE_TYPES.includes(file.type);
|
||||
/** Returns true if this file can be compressed via canvas (any raster image type). */
|
||||
export function isCompressible(file: File | Blob): boolean {
|
||||
return isCompressibleType(file.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress an image file via canvas.toBlob.
|
||||
* Returns null if the file type is not compressible (GIF, SVG, WebP, video, audio, etc.).
|
||||
* Compress an image file via canvas.toBlob → JPEG at the given quality.
|
||||
* Returns null if the browser cannot render the image (e.g. unsupported codec).
|
||||
*/
|
||||
export async function compressImage(file: File, quality = 0.82): Promise<CompressionResult | null> {
|
||||
if (!COMPRESSIBLE_TYPES.includes(file.type)) return null;
|
||||
export async function compressImage(
|
||||
file: File | Blob,
|
||||
quality = 0.82,
|
||||
): Promise<CompressionResult | null> {
|
||||
if (!isCompressibleType(file.type)) return null;
|
||||
|
||||
const img = await loadImage(file);
|
||||
const canvas = document.createElement('canvas');
|
||||
@@ -48,7 +55,7 @@ export async function compressImage(file: File, quality = 0.82): Promise<Compres
|
||||
});
|
||||
}
|
||||
|
||||
function loadImage(file: File): Promise<HTMLImageElement> {
|
||||
function loadImage(file: File | Blob): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
|
||||
Reference in New Issue
Block a user