From 9403ded8dac6d831b96d9883629e29f131bb9f33 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:49:41 +0100 Subject: [PATCH] feat: add showActivity toggle to PATCH @me with activity broadcast clear - Accept showActivity boolean in PATCH /api/users/@me body - Validate and persist as integer in DB - Update ConnectionManager cache on toggle - Broadcast empty activities via presence_update when toggled off --- packages/server/src/routes/users.ts | 30 +++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/server/src/routes/users.ts b/packages/server/src/routes/users.ts index 29ad1d72..39ddce38 100644 --- a/packages/server/src/routes/users.ts +++ b/packages/server/src/routes/users.ts @@ -3,7 +3,7 @@ import { eq, or, and, inArray } from 'drizzle-orm'; import { getDb, schema } from '../db/index.js'; import { authenticate, verifyPassword, hashPassword, signJwt } from '../utils/auth.js'; import { connectionManager } from '../ws/handler.js'; -import type { UpdateUserRequest, VerifyPasswordRequest, VerifyPasswordResponse, ChangePasswordRequest, ChangePasswordResponse, DeleteAccountRequest, ReplicatedInstance, SpaceLayoutItem, SpaceFolder } from '@backspace/shared'; +import type { UpdateUserRequest, VerifyPasswordRequest, VerifyPasswordResponse, ChangePasswordRequest, ChangePasswordResponse, DeleteAccountRequest, ReplicatedInstance, SpaceLayoutItem, SpaceFolder, Activity } from '@backspace/shared'; import { AVATAR_COLORS } from '@backspace/shared'; import { sanitizeUser } from '../utils/sanitize.js'; import { deleteUploadFile, deleteAttachmentByFilename } from '../utils/fileCleanup.js'; @@ -151,7 +151,7 @@ export async function userRoutes(app: FastifyInstance): Promise { }); app.patch<{ Body: UpdateUserRequest }>('/api/users/@me', { preHandler: authenticate }, async (request, reply) => { - const { displayName, avatar, banner, accentColor, avatarColor, bio, customStatus, status, replicatedInstances, homeUserId, profileUpdatedAt, discoverable } = request.body; + const { displayName, avatar, banner, accentColor, avatarColor, bio, customStatus, status, replicatedInstances, homeUserId, profileUpdatedAt, discoverable, showActivity } = request.body; const db = getDb(); const updateData: Record = {}; @@ -312,6 +312,13 @@ export async function userRoutes(app: FastifyInstance): Promise { (updateData as Record).discoverable = discoverable ? 1 : 0; } + if (showActivity !== undefined) { + if (typeof showActivity !== 'boolean') { + return reply.code(400).send({ error: 'showActivity must be a boolean', statusCode: 400 }); + } + (updateData as Record).showActivity = showActivity ? 1 : 0; + } + if (Object.keys(updateData).length === 0) { return reply.code(400).send({ error: 'No fields to update', statusCode: 400 }); } @@ -340,6 +347,25 @@ export async function userRoutes(app: FastifyInstance): Promise { db.update(schema.users).set(updateData).where(eq(schema.users.id, request.userId)).run(); + // Update activity visibility cache and broadcast clear when toggled off + if (showActivity !== undefined) { + connectionManager.setUserShowActivity(request.userId, showActivity); + if (!showActivity) { + connectionManager.clearUserActivities(request.userId); + const userSpaces = connectionManager.getUserSpaces(request.userId); + const clearPayload = { + type: 'presence_update' as const, + userId: request.userId, + status: connectionManager.getUserStatus(request.userId), + activities: [] as Activity[], + }; + for (const spaceId of userSpaces) { + connectionManager.sendToSpace(spaceId, clearPayload, request.userId); + } + connectionManager.sendToUser(request.userId, clearPayload); + } + } + // Clean up old avatar/banner files that were replaced if (avatar !== undefined && oldAvatar && oldAvatar !== (avatar || null) && !oldAvatar.startsWith('http')) { deleteUploadFile(oldAvatar);