refactor: rename Server → Space across entire codebase

Renames all domain terminology from "Server" to "Space" throughout the
application — database schema, API routes, shared types, stores, components,
and UI strings. Files renamed: ServerSidebar → SpaceSidebar, CreateServer →
CreateSpace, JoinServer → JoinSpace, ServerSettings → SpaceSettings,
serverStore → spaceStore, routes/servers → routes/spaces.
This commit is contained in:
Jannis Braun
2026-03-08 20:08:24 +01:00
parent d18d9c51f7
commit fc06e25731
62 changed files with 1342 additions and 1342 deletions
+39 -39
View File
@@ -2,8 +2,8 @@ import { eq, inArray, and } from 'drizzle-orm';
import { getDb, schema } from '../db/index.js';
import { generateSnowflake } from '../utils/snowflake.js';
import { connectionManager } from './handler.js';
import type { VoiceRoom, DmRoomMeta, ServerRoomMeta } from './handler.js';
import { isMember, getChannelServerId, isDmMember, hasPermission, PermissionBits } from '../utils/permissions.js';
import type { VoiceRoom, DmRoomMeta, SpaceRoomMeta } from './handler.js';
import { isMember, getChannelSpaceId, isDmMember, hasPermission, PermissionBits } from '../utils/permissions.js';
import { broadcastDmMessage, getDmMessageWithUser } from '../routes/dm.js';
import type { MessageWithUser, Attachment, DmMessageWithUser } from '@backspace/shared';
import { sanitizeUser } from '../utils/sanitize.js';
@@ -173,13 +173,13 @@ function handleMessageCreate(event: Record<string, unknown>, userId: string): vo
return;
}
const serverId = getChannelServerId(channelId);
if (!serverId) {
const spaceId = getChannelSpaceId(channelId);
if (!spaceId) {
connectionManager.sendToUser(userId, { type: 'error', message: 'Channel not found' });
return;
}
if (!hasPermission(userId, serverId, PermissionBits.SEND_MESSAGES, channelId)) {
if (!hasPermission(userId, spaceId, PermissionBits.SEND_MESSAGES, channelId)) {
connectionManager.sendToUser(userId, { type: 'error', message: 'Missing SEND_MESSAGES permission' });
return;
}
@@ -200,7 +200,7 @@ function handleMessageCreate(event: Record<string, unknown>, userId: string): vo
const messageWithUser = getMessageWithUser(messageId);
if (messageWithUser) {
// Broadcast to members with VIEW_CHANNEL on this channel
connectionManager.sendToChannel(serverId, channelId, {
connectionManager.sendToChannel(spaceId, channelId, {
type: 'message_created',
message: messageWithUser,
});
@@ -239,12 +239,12 @@ function handleMessageEdit(event: Record<string, unknown>, userId: string): void
.where(eq(schema.messages.id, messageId))
.run();
const serverId = getChannelServerId(message.channelId);
if (!serverId) return;
const spaceId = getChannelSpaceId(message.channelId);
if (!spaceId) return;
const updatedMessage = getMessageWithUser(messageId);
if (updatedMessage) {
connectionManager.sendToChannel(serverId, message.channelId, {
connectionManager.sendToChannel(spaceId, message.channelId, {
type: 'message_updated',
message: updatedMessage,
});
@@ -266,12 +266,12 @@ function handleMessageDelete(event: Record<string, unknown>, userId: string): vo
return;
}
const serverId = getChannelServerId(message.channelId);
if (!serverId) return;
const spaceId = getChannelSpaceId(message.channelId);
if (!spaceId) return;
// Allow author or MANAGE_MESSAGES permission holder to delete
const isAuthor = message.userId === userId;
const canManageMessages = hasPermission(userId, serverId, PermissionBits.MANAGE_MESSAGES, message.channelId);
const canManageMessages = hasPermission(userId, spaceId, PermissionBits.MANAGE_MESSAGES, message.channelId);
if (!isAuthor && !canManageMessages) {
connectionManager.sendToUser(userId, { type: 'error', message: 'You cannot delete this message' });
@@ -282,7 +282,7 @@ function handleMessageDelete(event: Record<string, unknown>, userId: string): vo
db.delete(schema.attachments).where(eq(schema.attachments.messageId, messageId)).run();
db.delete(schema.messages).where(eq(schema.messages.id, messageId)).run();
connectionManager.sendToChannel(serverId, message.channelId, {
connectionManager.sendToChannel(spaceId, message.channelId, {
type: 'message_deleted',
messageId,
channelId: message.channelId,
@@ -294,10 +294,10 @@ function handleTypingStart(event: Record<string, unknown>, userId: string, usern
if (!channelId || typeof channelId !== 'string') return;
const serverId = getChannelServerId(channelId);
if (!serverId) return;
const spaceId = getChannelSpaceId(channelId);
if (!spaceId) return;
if (!hasPermission(userId, serverId, PermissionBits.SEND_MESSAGES, channelId)) return;
if (!hasPermission(userId, spaceId, PermissionBits.SEND_MESSAGES, channelId)) return;
// Clear previous typing timeout for this user+channel
const key = `${userId}:${channelId}`;
@@ -307,7 +307,7 @@ function handleTypingStart(event: Record<string, unknown>, userId: string, usern
}
// Broadcast typing event to channel viewers (exclude sender)
connectionManager.sendToChannel(serverId, channelId, {
connectionManager.sendToChannel(spaceId, channelId, {
type: 'typing',
channelId,
userId,
@@ -332,10 +332,10 @@ function handlePresenceUpdate(event: Record<string, unknown>, userId: string): v
const db = getDb();
db.update(schema.users).set({ status }).where(eq(schema.users.id, userId)).run();
// Broadcast to all servers user is in
const userServers = connectionManager.getUserServers(userId);
for (const serverId of userServers) {
connectionManager.sendToServer(serverId, {
// Broadcast to all spaces user is in
const userSpaces = connectionManager.getUserSpaces(userId);
for (const spaceId of userSpaces) {
connectionManager.sendToSpace(spaceId, {
type: 'presence_update',
userId,
status,
@@ -354,9 +354,9 @@ function handlePresenceUpdate(event: Record<string, unknown>, userId: string): v
/** Helper: broadcast a voice leave and auto-end empty DM calls. */
function broadcastRoomLeave(roomId: string, room: VoiceRoom, userId: string): void {
if (room.roomType === 'server') {
const meta = room.metadata as ServerRoomMeta;
connectionManager.sendToServer(meta.serverId, {
if (room.roomType === 'space') {
const meta = room.metadata as SpaceRoomMeta;
connectionManager.sendToSpace(meta.spaceId, {
type: 'voice_state_update',
channelId: roomId,
userId,
@@ -389,13 +389,13 @@ function handleVoiceJoin(event: Record<string, unknown>, userId: string): void {
return;
}
const serverId = getChannelServerId(channelId);
if (!serverId) {
const spaceId = getChannelSpaceId(channelId);
if (!spaceId) {
connectionManager.sendToUser(userId, { type: 'error', message: 'Channel not found' });
return;
}
if (!hasPermission(userId, serverId, PermissionBits.CONNECT, channelId)) {
if (!hasPermission(userId, spaceId, PermissionBits.CONNECT, channelId)) {
connectionManager.sendToUser(userId, { type: 'error', message: 'Missing CONNECT permission' });
return;
}
@@ -419,7 +419,7 @@ function handleVoiceJoin(event: Record<string, unknown>, userId: string): void {
return;
}
// Leave current room (server OR DM)
// Leave current room (space OR DM)
const left = connectionManager.leaveCurrentRoom(userId);
if (left) {
broadcastRoomLeave(left.roomId, left.room, userId);
@@ -440,8 +440,8 @@ function handleVoiceJoin(event: Record<string, unknown>, userId: string): void {
}
}
// Lazy-create server room
connectionManager.createRoom(channelId, 'server', { type: 'server', serverId });
// Lazy-create space room
connectionManager.createRoom(channelId, 'space', { type: 'space', spaceId });
// Join room
connectionManager.joinRoom(channelId, userId);
@@ -490,7 +490,7 @@ function handleVoiceStatus(event: Record<string, unknown>, userId: string): void
connectionManager.setVoiceUserStatus(userId, isMuted, isDeafened, isCameraOn, isScreenSharing);
// sendToRoom routes to sendToServer for server rooms, sendToDmMembers for DM rooms
// sendToRoom routes to sendToSpace for space rooms, sendToDmMembers for DM rooms
connectionManager.sendToRoom(userRoom.roomId, {
type: 'voice_status_update',
userId,
@@ -702,11 +702,11 @@ function handleReactionAdd(event: Record<string, unknown>, userId: string): void
const db = getDb();
// Try server message first
// Try space message first
const message = db.select().from(schema.messages).where(eq(schema.messages.id, messageId)).get();
if (message) {
const serverId = getChannelServerId(message.channelId);
if (!serverId || !isMember(serverId, userId)) return;
const spaceId = getChannelSpaceId(message.channelId);
if (!spaceId || !isMember(spaceId, userId)) return;
const reactionId = generateSnowflake();
const now = Date.now();
@@ -723,7 +723,7 @@ function handleReactionAdd(event: Record<string, unknown>, userId: string): void
const reactionUser = db.select().from(schema.users).where(eq(schema.users.id, userId)).get();
const userObj = reactionUser ? sanitizeUser(reactionUser) : undefined;
connectionManager.sendToChannel(serverId, message.channelId, {
connectionManager.sendToChannel(spaceId, message.channelId, {
type: 'reaction_added',
messageId,
reaction: { id: reactionId, messageId, userId, emoji, createdAt: now, user: userObj },
@@ -771,11 +771,11 @@ function handleReactionRemove(event: Record<string, unknown>, userId: string): v
const db = getDb();
// Try server message first
// Try space message first
const message = db.select().from(schema.messages).where(eq(schema.messages.id, messageId)).get();
if (message) {
const serverId = getChannelServerId(message.channelId);
if (!serverId || !isMember(serverId, userId)) return;
const spaceId = getChannelSpaceId(message.channelId);
if (!spaceId || !isMember(spaceId, userId)) return;
const result = db.delete(schema.reactions)
.where(and(
@@ -786,7 +786,7 @@ function handleReactionRemove(event: Record<string, unknown>, userId: string): v
.run();
if (result.changes > 0) {
connectionManager.sendToChannel(serverId, message.channelId, {
connectionManager.sendToChannel(spaceId, message.channelId, {
type: 'reaction_removed',
messageId,
userId,
+108 -108
View File
@@ -7,12 +7,12 @@ import { handleClientEvent } from './events.js';
import { computePermissions, PermissionBits, permissionsToString } from '../utils/permissions.js';
import type {
User,
ServerWithChannelsAndMembers,
SpaceWithChannelsAndMembers,
MemberWithUser,
Channel,
DmChannel,
ServerEvent,
ServerFolder,
SpaceFolder,
ReadState,
ActiveCallInfo,
} from '@backspace/shared';
@@ -39,9 +39,9 @@ export interface AuthenticatedSocket {
// ─── VoiceRoom Abstraction ─────────────────────────────────────────────────
export interface ServerRoomMeta {
type: 'server';
serverId: string;
export interface SpaceRoomMeta {
type: 'space';
spaceId: string;
}
export interface DmRoomMeta {
@@ -52,9 +52,9 @@ export interface DmRoomMeta {
export interface VoiceRoom {
roomId: string;
roomType: 'server' | 'dm';
roomType: 'space' | 'dm';
participants: Set<string>;
metadata: ServerRoomMeta | DmRoomMeta;
metadata: SpaceRoomMeta | DmRoomMeta;
startedAt: number;
}
@@ -63,8 +63,8 @@ export interface VoiceRoom {
class ConnectionManager {
// userId → Set of WebSocket connections (multiple tabs)
private connections: Map<string, Set<WebSocket>> = new Map();
// userId → Set of server IDs the user belongs to
private userServers: Map<string, Set<string>> = new Map();
// userId → Set of space IDs the user belongs to
private userSpaces: Map<string, Set<string>> = new Map();
// ws → userId (reverse lookup)
private wsToUser: Map<WebSocket, string> = new Map();
// Unified voice room tracking (replaces voiceStates + activeCalls)
@@ -134,13 +134,13 @@ class ConnectionManager {
const db = getDb();
db.update(schema.users).set({ status: 'offline' }).where(eq(schema.users.id, userId)).run();
// Leave voice room if in one (handles both server and DM rooms)
// Leave voice room if in one (handles both space and DM rooms)
const left = this.leaveCurrentRoom(userId);
this.clearVoiceUserStatus(userId);
if (left) {
if (left.room.roomType === 'server') {
const meta = left.room.metadata as ServerRoomMeta;
this.sendToServer(meta.serverId, {
if (left.room.roomType === 'space') {
const meta = left.room.metadata as SpaceRoomMeta;
this.sendToSpace(meta.spaceId, {
type: 'voice_state_update',
channelId: left.roomId,
userId: userId,
@@ -179,10 +179,10 @@ class ConnectionManager {
}
}
// Broadcast offline to all servers
const userServers = this.getUserServers(userId);
for (const serverId of userServers) {
this.sendToServer(serverId, {
// Broadcast offline to all spaces
const userSpaces = this.getUserSpaces(userId);
for (const spaceId of userSpaces) {
this.sendToSpace(spaceId, {
type: 'presence_update',
userId: userId,
status: 'offline',
@@ -199,25 +199,25 @@ class ConnectionManager {
return conns !== undefined && conns.size > 0;
}
setUserServers(userId: string, serverIds: string[]): void {
this.userServers.set(userId, new Set(serverIds));
setUserSpaces(userId: string, spaceIds: string[]): void {
this.userSpaces.set(userId, new Set(spaceIds));
}
addUserServer(userId: string, serverId: string): void {
if (!this.userServers.has(userId)) {
this.userServers.set(userId, new Set());
addUserSpace(userId: string, spaceId: string): void {
if (!this.userSpaces.has(userId)) {
this.userSpaces.set(userId, new Set());
}
this.userServers.get(userId)!.add(serverId);
this.userSpaces.get(userId)!.add(spaceId);
}
getUserServers(userId: string): Set<string> {
return this.userServers.get(userId) ?? new Set();
getUserSpaces(userId: string): Set<string> {
return this.userSpaces.get(userId) ?? new Set();
}
// ─── Unified VoiceRoom API ─────────────────────────────────────────────────
/** Create a room. Returns false if room already exists. */
createRoom(roomId: string, roomType: 'server' | 'dm', metadata: ServerRoomMeta | DmRoomMeta): boolean {
createRoom(roomId: string, roomType: 'space' | 'dm', metadata: SpaceRoomMeta | DmRoomMeta): boolean {
if (this.voiceRooms.has(roomId)) return false;
this.voiceRooms.set(roomId, {
roomId,
@@ -283,7 +283,7 @@ class ConnectionManager {
const oldRoom = this.voiceRooms.get(currentRoomId);
if (oldRoom) {
oldRoom.participants.delete(userId);
if (oldRoom.participants.size === 0 && oldRoom.roomType === 'server') {
if (oldRoom.participants.size === 0 && oldRoom.roomType === 'space') {
this.voiceRooms.delete(currentRoomId);
}
}
@@ -302,8 +302,8 @@ class ConnectionManager {
room.participants.delete(userId);
this.userToRoom.delete(userId);
// Auto-cleanup empty server rooms (they're lazy-created)
if (room.participants.size === 0 && room.roomType === 'server') {
// Auto-cleanup empty space rooms (they're lazy-created)
if (room.participants.size === 0 && room.roomType === 'space') {
this.voiceRooms.delete(roomId);
}
@@ -399,11 +399,11 @@ class ConnectionManager {
}
}
/** Send to all members of a server. */
sendToServer(serverId: string, event: ServerEvent, excludeUserId?: string): void {
/** Send to all members of a space. */
sendToSpace(spaceId: string, event: ServerEvent, excludeUserId?: string): void {
const message = JSON.stringify(event);
for (const [userId, serverIds] of this.userServers) {
if (serverIds.has(serverId) && userId !== excludeUserId) {
for (const [userId, spaceIds] of this.userSpaces) {
if (spaceIds.has(spaceId) && userId !== excludeUserId) {
const connections = this.getUserConnections(userId);
for (const ws of connections) {
if (ws.readyState === 1) {
@@ -414,12 +414,12 @@ class ConnectionManager {
}
}
/** Send to server members who have VIEW_CHANNEL on the given channel. */
sendToChannel(serverId: string, channelId: string, event: ServerEvent, excludeUserId?: string): void {
/** Send to space members who have VIEW_CHANNEL on the given channel. */
sendToChannel(spaceId: string, channelId: string, event: ServerEvent, excludeUserId?: string): void {
const message = JSON.stringify(event);
for (const [userId, serverIds] of this.userServers) {
if (serverIds.has(serverId) && userId !== excludeUserId) {
const perms = computePermissions(userId, serverId, channelId);
for (const [userId, spaceIds] of this.userSpaces) {
if (spaceIds.has(spaceId) && userId !== excludeUserId) {
const perms = computePermissions(userId, spaceId, channelId);
if ((perms & PermissionBits.VIEW_CHANNEL) !== 0n) {
const connections = this.getUserConnections(userId);
for (const ws of connections) {
@@ -432,9 +432,9 @@ class ConnectionManager {
}
}
/** Expose userServers iterator for pre-delete viewer collection. */
getUserServerEntries(): IterableIterator<[string, Set<string>]> {
return this.userServers.entries();
/** Expose userSpaces iterator for pre-delete viewer collection. */
getUserSpaceEntries(): IterableIterator<[string, Set<string>]> {
return this.userSpaces.entries();
}
/** Send to all DM channel members (queries dm_members table). */
@@ -452,14 +452,14 @@ class ConnectionManager {
}
}
/** Send to a room — routes to sendToServer (server rooms) or sendToDmMembers (DM rooms). */
/** Send to a room — routes to sendToSpace (space rooms) or sendToDmMembers (DM rooms). */
sendToRoom(roomId: string, event: ServerEvent, excludeUserId?: string): void {
const room = this.voiceRooms.get(roomId);
if (!room) return;
if (room.roomType === 'server') {
const meta = room.metadata as ServerRoomMeta;
this.sendToServer(meta.serverId, event, excludeUserId);
if (room.roomType === 'space') {
const meta = room.metadata as SpaceRoomMeta;
this.sendToSpace(meta.spaceId, event, excludeUserId);
} else {
this.sendToDmMembers(roomId, event, excludeUserId);
}
@@ -531,9 +531,9 @@ class WsRateLimiter {
function buildReadyPayload(userId: string): {
user: User;
servers: ServerWithChannelsAndMembers[];
spaces: SpaceWithChannelsAndMembers[];
dmChannels: DmChannel[];
folders: ServerFolder[];
folders: SpaceFolder[];
voiceStates: Record<string, string[]>;
voiceUserStates: Record<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }>;
readStates: ReadState[];
@@ -548,31 +548,31 @@ function buildReadyPayload(userId: string): {
}
const user = sanitizeUser(userRow);
// Get user's server memberships
// Get user's space memberships
const memberships = db.select()
.from(schema.serverMembers)
.where(eq(schema.serverMembers.userId, userId))
.from(schema.spaceMembers)
.where(eq(schema.spaceMembers.userId, userId))
.all();
const serverIds = memberships.map(m => m.serverId);
const spaceIds = memberships.map(m => m.spaceId);
const servers: ServerWithChannelsAndMembers[] = [];
const spaces: SpaceWithChannelsAndMembers[] = [];
if (serverIds.length > 0) {
const serverRows = db.select()
.from(schema.servers)
.where(inArray(schema.servers.id, serverIds))
if (spaceIds.length > 0) {
const spaceRows = db.select()
.from(schema.spaces)
.where(inArray(schema.spaces.id, spaceIds))
.all();
// Batch: all channels for all servers (1 query instead of N)
// Batch: all channels for all spaces (1 query instead of N)
const allChannels = batchInArray(
serverIds,
ids => db.select().from(schema.channels).where(inArray(schema.channels.serverId, ids)).all(),
spaceIds,
ids => db.select().from(schema.channels).where(inArray(schema.channels.spaceId, ids)).all(),
);
const channelsByServer = new Map<string, (typeof allChannels)>();
const channelsBySpace = new Map<string, (typeof allChannels)>();
for (const ch of allChannels) {
let arr = channelsByServer.get(ch.serverId);
if (!arr) { arr = []; channelsByServer.set(ch.serverId, arr); }
let arr = channelsBySpace.get(ch.spaceId);
if (!arr) { arr = []; channelsBySpace.set(ch.spaceId, arr); }
arr.push(ch);
}
@@ -592,18 +592,18 @@ function buildReadyPayload(userId: string): {
}
}
for (const serverRow of serverRows) {
const channels = channelsByServer.get(serverRow.id) ?? [];
for (const spaceRow of spaceRows) {
const channels = channelsBySpace.get(spaceRow.id) ?? [];
const roles = db.select()
.from(schema.roles)
.where(eq(schema.roles.serverId, serverRow.id))
.where(eq(schema.roles.spaceId, spaceRow.id))
.orderBy(schema.roles.position)
.all();
const memberRows = db.select()
.from(schema.serverMembers)
.where(eq(schema.serverMembers.serverId, serverRow.id))
.from(schema.spaceMembers)
.where(eq(schema.spaceMembers.spaceId, spaceRow.id))
.all();
const memberUserIds = memberRows.map(m => m.userId);
@@ -614,7 +614,7 @@ function buildReadyPayload(userId: string): {
const memberRoleRows = db.select()
.from(schema.memberRoles)
.where(eq(schema.memberRoles.serverId, serverRow.id))
.where(eq(schema.memberRoles.spaceId, spaceRow.id))
.all();
const members: MemberWithUser[] = memberRows
@@ -630,7 +630,7 @@ function buildReadyPayload(userId: string): {
.filter(r => assignedRoleIds.includes(r.id))
.map(r => ({
id: r.id,
serverId: r.serverId,
spaceId: r.spaceId,
name: r.name,
color: r.color ?? '#b9bbbe',
position: r.position ?? 0,
@@ -638,7 +638,7 @@ function buildReadyPayload(userId: string): {
}));
return {
serverId: m.serverId,
spaceId: m.spaceId,
userId: m.userId,
nickname: m.nickname,
joinedAt: m.joinedAt,
@@ -648,18 +648,18 @@ function buildReadyPayload(userId: string): {
})
.filter((m): m is MemberWithUser => m !== null);
// Compute server-level permissions for this user
const serverPerms = computePermissions(userId, serverRow.id);
// Compute space-level permissions for this user
const spacePerms = computePermissions(userId, spaceRow.id);
// Filter channels by VIEW_CHANNEL and attach per-channel permissions
const visibleChannels: Channel[] = [];
for (const ch of channels) {
const chPerms = computePermissions(userId, serverRow.id, ch.id);
const chPerms = computePermissions(userId, spaceRow.id, ch.id);
const hasView = (chPerms & PermissionBits.VIEW_CHANNEL) !== 0n || (chPerms & PermissionBits.ADMINISTRATOR) !== 0n;
if (hasView) {
visibleChannels.push({
id: ch.id,
serverId: ch.serverId,
spaceId: ch.spaceId,
name: ch.name,
type: ch.type as Channel['type'],
topic: ch.topic,
@@ -671,34 +671,34 @@ function buildReadyPayload(userId: string): {
}
}
servers.push({
id: serverRow.id,
name: serverRow.name,
icon: serverRow.icon,
ownerId: serverRow.ownerId,
inviteCode: serverRow.inviteCode,
visibility: (serverRow.visibility ?? 'private') as ServerWithChannelsAndMembers['visibility'],
description: serverRow.description ?? null,
createdAt: serverRow.createdAt,
spaces.push({
id: spaceRow.id,
name: spaceRow.name,
icon: spaceRow.icon,
ownerId: spaceRow.ownerId,
inviteCode: spaceRow.inviteCode,
visibility: (spaceRow.visibility ?? 'private') as SpaceWithChannelsAndMembers['visibility'],
description: spaceRow.description ?? null,
createdAt: spaceRow.createdAt,
channels: visibleChannels,
members,
roles: roles.map(r => ({
id: r.id,
serverId: r.serverId,
spaceId: r.spaceId,
name: r.name,
color: r.color ?? '#b9bbbe',
position: r.position ?? 0,
permissions: r.permissions ?? undefined,
isEveryone: r.id === serverRow.id,
isEveryone: r.id === spaceRow.id,
createdAt: r.createdAt,
})),
myPermissions: permissionsToString(serverPerms),
myPermissions: permissionsToString(spacePerms),
});
}
}
// Store user's server IDs for broadcasting
connectionManager.setUserServers(userId, serverIds);
// Store user's space IDs for broadcasting
connectionManager.setUserSpaces(userId, spaceIds);
// Get DM channels
const dmMemberships = db.select()
@@ -773,36 +773,36 @@ function buildReadyPayload(userId: string): {
}
}
// Get Server Folders
// Get Space Folders
const folderRows = db.select()
.from(schema.serverFolders)
.where(eq(schema.serverFolders.userId, userId))
.orderBy(schema.serverFolders.position)
.from(schema.spaceFolders)
.where(eq(schema.spaceFolders.userId, userId))
.orderBy(schema.spaceFolders.position)
.all();
const folders: any[] = [];
const folders: SpaceFolder[] = [];
for (const folder of folderRows) {
const serverIds = db.select()
.from(schema.serverFolderMembers)
.where(eq(schema.serverFolderMembers.folderId, folder.id))
const folderSpaceIds = db.select()
.from(schema.spaceFolderMembers)
.where(eq(schema.spaceFolderMembers.folderId, folder.id))
.all()
.map(m => m.serverId);
.map(m => m.spaceId);
folders.push({
id: folder.id,
userId: folder.userId,
name: folder.name,
color: folder.color,
position: folder.position,
serverIds,
position: folder.position ?? 0,
spaceIds: folderSpaceIds,
});
}
// Build voice states — tell the client who is currently in voice channels
// across all their servers
// across all their spaces
const voiceStates: Record<string, string[]> = {};
for (const srv of servers) {
for (const ch of srv.channels) {
for (const space of spaces) {
for (const ch of space.channels) {
if (ch.type === 'voice' || ch.type === 'video') {
const participants = connectionManager.getRoomParticipants(ch.id);
if (participants.size > 0) {
@@ -832,7 +832,7 @@ function buildReadyPayload(userId: string): {
}
}
// Build voice user states — includes both server and DM participants now
// Build voice user states — includes both space and DM participants now
const voiceUserStates: Record<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }> = {};
for (const chId of Object.keys(voiceStates)) {
const usersInChannel = voiceStates[chId];
@@ -857,7 +857,7 @@ function buildReadyPayload(userId: string): {
lastReadMessageId: rs.lastReadMessageId,
}));
return { user, servers, dmChannels, folders, voiceStates, voiceUserStates, readStates, activeCalls };
return { user, spaces, dmChannels, folders, voiceStates, voiceUserStates, readStates, activeCalls };
}
export async function registerWebSocket(app: FastifyInstance): Promise<void> {
@@ -915,10 +915,10 @@ export async function registerWebSocket(app: FastifyInstance): Promise<void> {
...readyData,
}));
// Broadcast presence update to all servers
const userServers = connectionManager.getUserServers(userId);
for (const serverId of userServers) {
connectionManager.sendToServer(serverId, {
// Broadcast presence update to all spaces
const userSpaces = connectionManager.getUserSpaces(userId);
for (const spaceId of userSpaces) {
connectionManager.sendToSpace(spaceId, {
type: 'presence_update',
userId,
status: 'online',