feat: launch readiness — PWA, API hardening, memory leak fixes, sticker removal

- Add PWA infrastructure: vite-plugin-pwa, manifest, service worker,
  SW update prompt component, placeholder icons, Apple meta tags
- Harden API client: 401 auto-logout, AbortController timeouts
  (30s standard, 120s uploads), onUnauthorized callback
- Fix memory leaks: clear voice user status on leave, clean up all
  Maps (channelToSpaceMap, permissions, etc.) on removeSpace
- Upgrade error boundary to Aether Drift design with Try Again button,
  collapsible stack trace, and componentDidCatch logging
- Configure desktop icon paths in electron-builder.yml
- Remove sticker feature (server routes, schema, types, UI components)
- Fix Docker build: use **/node_modules in .dockerignore to prevent
  COPY from clobbering pnpm-installed workspace dependencies
- Add vite-env.d.ts declarations for noise suppressor wasm imports
- Exclude test files from tsc build via tsconfig
This commit is contained in:
Jannis Braun
2026-03-15 15:41:22 +01:00
parent b08f40feb7
commit 4d230711fc
36 changed files with 2740 additions and 1283 deletions
+3 -38
View File
@@ -121,28 +121,6 @@ export function fetchReplyToMessages(messages: (typeof schema.messages.$inferSel
return map;
}
/** Hydrate a sticker by ID, returns null if not found */
export function hydrateSticker(stickerId: string | null | undefined): any {
if (!stickerId) return null;
const db = getDb();
const row = db.select().from(schema.stickers).where(eq(schema.stickers.id, stickerId)).get();
if (!row) return null;
return {
id: row.id,
packId: row.packId,
spaceId: row.spaceId,
name: row.name,
tags: row.tags ?? '',
filename: row.filename,
mimetype: row.mimetype,
size: row.size,
width: row.width,
height: row.height,
uploadedBy: row.uploadedBy,
createdAt: row.createdAt,
};
}
export function buildMessageWithUser(
message: typeof schema.messages.$inferSelect,
user: typeof schema.users.$inferSelect,
@@ -150,7 +128,6 @@ export function buildMessageWithUser(
reactions: Reaction[] = [],
replyTo: MessageWithUser | null = null,
): MessageWithUser {
const stickerId = (message as any).stickerId ?? null;
return {
id: message.id,
channelId: message.channelId,
@@ -172,8 +149,6 @@ export function buildMessageWithUser(
})),
reactions,
replyTo,
stickerId,
sticker: hydrateSticker(stickerId),
};
}
@@ -277,7 +252,7 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
},
}, async (request, reply) => {
const { id } = request.params;
const { content, attachments: attachmentIds, replyToId, stickerId } = request.body;
const { content, attachments: attachmentIds, replyToId } = request.body;
const spaceId = getChannelSpaceId(id);
if (!spaceId) {
@@ -295,18 +270,9 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
const hasContent = content && typeof content === 'string' && content.trim().length > 0;
const hasAttachments = attachmentIds && attachmentIds.length > 0;
const hasSticker = stickerId && typeof stickerId === 'string';
if (!hasContent && !hasAttachments && !hasSticker) {
return reply.code(400).send({ error: 'Message must have content, attachments, or a sticker', statusCode: 400 });
}
// Validate sticker exists if provided
if (hasSticker) {
const sticker = getDb().select().from(schema.stickers).where(eq(schema.stickers.id, stickerId!)).get();
if (!sticker) {
return reply.code(400).send({ error: 'Sticker not found', statusCode: 400 });
}
if (!hasContent && !hasAttachments) {
return reply.code(400).send({ error: 'Message must have content or attachments', statusCode: 400 });
}
if (content && content.length > MAX_MESSAGE_LENGTH) {
@@ -339,7 +305,6 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
userId: request.userId,
replyToId: replyToId || null,
content: content?.trim() || null,
stickerId: stickerId || null,
createdAt: now,
}).run();