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:
@@ -15,6 +15,7 @@ import type {
|
||||
DmChannel,
|
||||
ServerEvent,
|
||||
SpaceFolder,
|
||||
SpaceLayoutItem,
|
||||
ReadState,
|
||||
ActiveCallInfo,
|
||||
} from '@backspace/shared';
|
||||
@@ -652,6 +653,7 @@ function buildReadyPayload(userId: string): {
|
||||
spaces: SpaceWithChannelsAndMembers[];
|
||||
dmChannels: DmChannel[];
|
||||
folders: SpaceFolder[];
|
||||
spaceLayout: SpaceLayoutItem[] | null;
|
||||
voiceStates: Record<string, string[]>;
|
||||
voiceUserStates: Record<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }>;
|
||||
spaceVoiceStates: Record<string, { spaceMuted: boolean; spaceDeafened: boolean; permissionMuted: boolean }>;
|
||||
@@ -926,6 +928,7 @@ function buildReadyPayload(userId: string): {
|
||||
const folderSpaceIds = db.select()
|
||||
.from(schema.spaceFolderMembers)
|
||||
.where(eq(schema.spaceFolderMembers.folderId, folder.id))
|
||||
.orderBy(schema.spaceFolderMembers.position)
|
||||
.all()
|
||||
.map(m => m.spaceId);
|
||||
|
||||
@@ -939,6 +942,11 @@ function buildReadyPayload(userId: string): {
|
||||
});
|
||||
}
|
||||
|
||||
// Get user space layout
|
||||
const layoutRow = db.select().from(schema.userSpaceLayout)
|
||||
.where(eq(schema.userSpaceLayout.userId, userId)).get();
|
||||
const spaceLayout: SpaceLayoutItem[] | null = layoutRow ? JSON.parse(layoutRow.layout) : null;
|
||||
|
||||
// Build voice states — tell the client who is currently in voice channels
|
||||
// across all their spaces
|
||||
const voiceStates: Record<string, string[]> = {};
|
||||
@@ -1029,7 +1037,7 @@ function buildReadyPayload(userId: string): {
|
||||
lastReadMessageId: rs.lastReadMessageId,
|
||||
}));
|
||||
|
||||
return { user, spaces, dmChannels, folders, voiceStates, voiceUserStates, spaceVoiceStates, readStates, activeCalls };
|
||||
return { user, spaces, dmChannels, folders, spaceLayout, voiceStates, voiceUserStates, spaceVoiceStates, readStates, activeCalls };
|
||||
}
|
||||
|
||||
export async function registerWebSocket(app: FastifyInstance): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user