feat: image optimization — client-side resize + server-side thumbnails

Avatars/banners now resize to max 512px/1920px and convert to WebP before
upload (zero server cost). Chat image uploads generate an 800px-wide WebP
thumbnail via Sharp; the feed shows the thumbnail, click opens the full-res
original. Adds lazy loading to avatars. Federation-compatible: remote
instances without this feature fall back gracefully.
This commit is contained in:
Jannis Braun
2026-03-13 16:44:14 +01:00
parent 12b450b7b9
commit 3e97c2b0f5
19 changed files with 449 additions and 20 deletions
+1
View File
@@ -83,6 +83,7 @@ export function buildDmMessageWithUser(
originalName: a.originalName,
mimetype: a.mimetype,
size: a.size,
thumbnailFilename: a.thumbnailFilename ?? null,
createdAt: a.createdAt,
})),
reactions,
+1
View File
@@ -143,6 +143,7 @@ export function buildMessageWithUser(
originalName: a.originalName,
mimetype: a.mimetype,
size: a.size,
thumbnailFilename: a.thumbnailFilename ?? null,
createdAt: a.createdAt,
})),
reactions,
+9
View File
@@ -8,6 +8,7 @@ import fs from 'fs';
import path from 'path';
import { pipeline } from 'stream/promises';
import type { Attachment } from '@backspace/shared';
import { generateThumbnail, isResizableImage } from '../utils/thumbnail.js';
export async function uploadRoutes(app: FastifyInstance): Promise<void> {
// Ensure upload directory exists
@@ -57,6 +58,12 @@ export async function uploadRoutes(app: FastifyInstance): Promise<void> {
const now = Date.now();
const db = getDb();
// Generate thumbnail for resizable images (non-blocking — upload succeeds regardless)
let thumbnailFilename: string | null = null;
if (isResizableImage(mimetype)) {
thumbnailFilename = await generateThumbnail(filepath, mimetype, config.uploadDir);
}
// Save attachment record
db.insert(schema.attachments).values({
id,
@@ -64,6 +71,7 @@ export async function uploadRoutes(app: FastifyInstance): Promise<void> {
originalName,
mimetype,
size,
thumbnailFilename,
createdAt: now,
}).run();
@@ -74,6 +82,7 @@ export async function uploadRoutes(app: FastifyInstance): Promise<void> {
originalName,
mimetype,
size,
thumbnailFilename: thumbnailFilename ?? undefined,
createdAt: now,
};