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:
@@ -111,6 +111,12 @@ export function runMigrations(db: Database.Database): void {
|
||||
columns: [
|
||||
{ name: 'avatar_color', type: 'TEXT' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'channels',
|
||||
columns: [
|
||||
{ name: 'category_id', type: 'TEXT' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -168,6 +174,17 @@ export function runMigrations(db: Database.Database): void {
|
||||
);
|
||||
`);
|
||||
|
||||
// Ensure channel_categories table exists (idempotent)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS channel_categories (
|
||||
id TEXT PRIMARY KEY,
|
||||
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
position INTEGER DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// Ensure voice_restrictions table exists (idempotent)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS voice_restrictions (
|
||||
|
||||
@@ -42,6 +42,14 @@ export const spaceMembers = sqliteTable('space_members', {
|
||||
pk: primaryKey({ columns: [table.spaceId, table.userId] }),
|
||||
}));
|
||||
|
||||
export const channelCategories = sqliteTable('channel_categories', {
|
||||
id: text('id').primaryKey(),
|
||||
spaceId: text('space_id').notNull().references(() => spaces.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
position: integer('position').default(0),
|
||||
createdAt: integer('created_at').notNull(),
|
||||
});
|
||||
|
||||
export const channels = sqliteTable('channels', {
|
||||
id: text('id').primaryKey(),
|
||||
spaceId: text('space_id').notNull().references(() => spaces.id, { onDelete: 'cascade' }),
|
||||
@@ -49,6 +57,7 @@ export const channels = sqliteTable('channels', {
|
||||
type: text('type').notNull(),
|
||||
topic: text('topic'),
|
||||
position: integer('position').default(0),
|
||||
categoryId: text('category_id'),
|
||||
createdAt: integer('created_at').notNull(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user