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
+13 -1
View File
@@ -473,9 +473,17 @@ CREATE TABLE space_folders (
CREATE TABLE space_folder_members (
folder_id TEXT NOT NULL REFERENCES space_folders(id) ON DELETE CASCADE,
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
position INTEGER DEFAULT 0, -- ordering within folder
PRIMARY KEY (folder_id, space_id)
);
-- User Space Layout (per-user sidebar ordering)
CREATE TABLE user_space_layout (
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
layout TEXT NOT NULL DEFAULT '[]', -- JSON array of {t:'s',id} | {t:'f',id} items
updated_at INTEGER NOT NULL
);
-- Instance Settings (singleton row, id=1)
CREATE TABLE instance_settings (
id INTEGER PRIMARY KEY DEFAULT 1,
@@ -544,6 +552,7 @@ POST /api/users/@me/verify-password (auth) { password } →
POST /api/users/@me/change-password (auth) { currentPassword?, newPassword } → { token }
DELETE /api/users/@me (auth) { password, username } → { success }
GET /api/users/:id/mutuals (auth) ?homeUserId= → { mutualFriends[], mutualSpaces[] }
PUT /api/users/@me/space-layout (auth) { items, folders } → { items, folders }
# Spaces
POST /api/spaces (auth) { name, icon?, banner?, avatarColor?, visibility?, description? } → { space }
@@ -693,7 +702,7 @@ All WebSocket messages are JSON over `/ws`. Client authenticates by sending `{ t
### Server → Client
```
{ type: 'ready', user, spaces, dmChannels, folders, voiceStates, readStates, activeCalls }
{ type: 'ready', user, spaces, dmChannels, folders, spaceLayout, voiceStates, readStates, activeCalls }
{ type: 'pong' }
# Server Messages
@@ -751,6 +760,9 @@ All WebSocket messages are JSON over `/ws`. Client authenticates by sending `{ t
{ type: 'join_request_received', request }
{ type: 'join_request_accepted', request, space? }
{ type: 'join_request_declined', request }
# Space Layout
{ type: 'space_layout_updated', layout: SpaceLayoutItem[], folders: SpaceFolder[] }
```
## PERMISSION SYSTEM