feat: channel categories with drag-and-drop reordering

Add channel categories (named groups) with full CRUD, collapsible headers
with unread indicators, and native HTML5 drag-and-drop for reordering
channels within/between categories and reordering categories themselves.

- Schema: channel_categories table, category_id column on channels
- Server: category CRUD endpoints, batch channel-layout reorder endpoint
- WebSocket: categories in ready payload, category_created/updated/deleted
  and channel_layout_updated events with per-user VIEW_CHANNEL filtering
- Frontend: dynamic category-based sidebar layout replacing hardcoded
  Text/Voice sections, collapse state persisted to localStorage,
  category selector in CreateChannel modal, federation-aware store
This commit is contained in:
Jannis Braun
2026-03-12 01:19:06 +01:00
parent b6d44f1568
commit c51d6b1a0e
11 changed files with 1045 additions and 121 deletions
+16
View File
@@ -78,6 +78,7 @@ export interface JoinRequest {
export interface SpaceWithChannelsAndMembers extends Space {
channels: Channel[];
categories: ChannelCategory[];
members: MemberWithUser[];
roles: Role[];
myPermissions?: string; // Computed per-user BigInt decimal string (space-level)
@@ -125,6 +126,14 @@ export interface SpaceFolder {
export type ChannelType = 'text' | 'voice';
export interface ChannelCategory {
id: string;
spaceId: string;
name: string;
position: number;
createdAt: number;
}
export interface Channel {
id: string;
spaceId: string;
@@ -132,6 +141,7 @@ export interface Channel {
type: ChannelType;
topic: string | null;
position: number;
categoryId: string | null;
createdAt: number;
lastMessageId?: string | null;
myPermissions?: string; // Computed per-user BigInt decimal string
@@ -297,6 +307,10 @@ export type ServerEvent =
| { type: 'voice_disconnected'; userId: string; channelId: string }
| { type: 'user_updated'; user: User }
| { type: 'member_banned'; spaceId: string; reason: string | null }
| { type: 'category_created'; category: ChannelCategory; spaceId: string }
| { type: 'category_updated'; category: ChannelCategory; spaceId: string }
| { type: 'category_deleted'; categoryId: string; spaceId: string }
| { type: 'channel_layout_updated'; spaceId: string; channels: Channel[]; categories: ChannelCategory[] }
| { type: 'pong' }
| { type: 'error'; message: string };
@@ -334,12 +348,14 @@ export interface CreateChannelRequest {
name: string;
type: ChannelType;
topic?: string;
categoryId?: string;
}
export interface UpdateChannelRequest {
name?: string;
topic?: string;
position?: number;
categoryId?: string | null;
}
export interface UpdateSpaceRequest {