feat: space sidebar drag-and-drop reordering with folder system

Add user_space_layout table and PUT /api/users/@me/space-layout endpoint
for persisting per-user sidebar ordering. Spaces can be freely reordered
via drag-and-drop, folders created by dragging one space onto another,
and folders auto-dissolve when they have fewer than 2 members. Includes
folder context menu (rename, color, ungroup), collapsed folder mini-grid
icons, multi-tab sync via WebSocket, and localStorage collapse state.
Removes the rigid native/federated split — federated spaces now intermix
freely while keeping their globe badge.
This commit is contained in:
Jannis Braun
2026-03-12 03:38:22 +01:00
parent 39174e5454
commit dbd964d40c
12 changed files with 1006 additions and 93 deletions
+5 -5
View File
@@ -1089,15 +1089,15 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
const userIds = [...new Set(banRows.map(b => b.userId))];
const bannedByIds = [...new Set(banRows.map(b => b.bannedBy))];
const allUserIds = [...new Set([...userIds, ...bannedByIds])];
const users = db.select().from(schema.users)
.where(inArray(schema.users.id, allUserIds))
.all();
const allUserIds = [...new Set([...userIds, ...bannedByIds].filter((id): id is string => id !== null))];
const users = allUserIds.length > 0
? db.select().from(schema.users).where(inArray(schema.users.id, allUserIds)).all()
: [];
const userMap = new Map(users.map(u => [u.id, u]));
const bans = banRows.map(b => {
const user = userMap.get(b.userId);
const moderator = userMap.get(b.bannedBy);
const moderator = b.bannedBy ? userMap.get(b.bannedBy) : undefined;
return {
spaceId: b.spaceId,
userId: b.userId,