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:
2026-04-21 12:11:09 +02:00
parent 48ac9d2922
commit 1303c8e26f
71 changed files with 1077 additions and 114 deletions
+57
View File
@@ -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)));
}