feat: backup/restore, user profile popover, image compress, video blur, wake lock
- backup/restore dialog + user profile popover components - image compression, video blur, wake lock utilities - message cache + conversation messages hook refinements - call context, active speakers, screen share dialog tweaks - audio + screen share settings persistence - refreshed app icons (smaller sizes) across all platforms
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// Client-side image compression before upload. Downscales huge photos
|
||||
// (phone cameras regularly emit 4000×3000+ at 5MB+) to a size that
|
||||
// actually makes sense for chat display. Preserves aspect ratio.
|
||||
//
|
||||
// Skips animated formats (gif/apng/webp) to keep motion, and skips
|
||||
// already-small files to avoid useless re-encode overhead.
|
||||
|
||||
const MAX_DIM = 2048;
|
||||
const TARGET_QUALITY = 0.85;
|
||||
const SKIP_BELOW_BYTES = 512 * 1024; // 512KB — not worth re-encoding
|
||||
const ANIMATED_MIME = /^image\/(gif|apng|webp)$/;
|
||||
|
||||
export async function compressImage(file: File): Promise<File> {
|
||||
if (!file.type.startsWith('image/')) return file;
|
||||
if (ANIMATED_MIME.test(file.type)) return file;
|
||||
if (file.size < SKIP_BELOW_BYTES) return file;
|
||||
if (typeof createImageBitmap !== 'function') return file;
|
||||
if (typeof OffscreenCanvas !== 'function') return file;
|
||||
|
||||
try {
|
||||
const bitmap = await createImageBitmap(file);
|
||||
const largest = Math.max(bitmap.width, bitmap.height);
|
||||
const scale = largest > MAX_DIM ? MAX_DIM / largest : 1;
|
||||
const w = Math.max(1, Math.round(bitmap.width * scale));
|
||||
const h = Math.max(1, Math.round(bitmap.height * scale));
|
||||
const canvas = new OffscreenCanvas(w, h);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
bitmap.close();
|
||||
return file;
|
||||
}
|
||||
ctx.drawImage(bitmap, 0, 0, w, h);
|
||||
bitmap.close();
|
||||
const blob = await canvas.convertToBlob({
|
||||
type: 'image/webp',
|
||||
quality: TARGET_QUALITY,
|
||||
});
|
||||
// If the re-encoded blob is actually larger (small PNGs can expand as
|
||||
// WebP), keep the original.
|
||||
if (blob.size >= file.size) return file;
|
||||
const name = renameToWebp(file.name);
|
||||
return new File([blob], name, { type: 'image/webp', lastModified: file.lastModified });
|
||||
} catch (err: unknown) {
|
||||
console.warn('compressImage failed — keeping original', err);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
function renameToWebp(original: string): string {
|
||||
const dot = original.lastIndexOf('.');
|
||||
const base = dot > 0 ? original.slice(0, dot) : original;
|
||||
return base + '.webp';
|
||||
}
|
||||
|
||||
export async function compressImages(files: File[]): Promise<File[]> {
|
||||
return Promise.all(files.map((f) => compressImage(f)));
|
||||
}
|
||||
Reference in New Issue
Block a user