Multi server connection
This commit is contained in:
@@ -38,7 +38,8 @@ interface JoinRequest {
|
|||||||
interface ConnectedUser {
|
interface ConnectedUser {
|
||||||
oderId: string;
|
oderId: string;
|
||||||
ws: WebSocket;
|
ws: WebSocket;
|
||||||
serverId?: string;
|
serverIds: Set<string>; // all servers the user is a member of
|
||||||
|
viewedServerId?: string; // currently viewed/active server
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +362,7 @@ const wss = new WebSocketServer({ server });
|
|||||||
|
|
||||||
wss.on('connection', (ws: WebSocket) => {
|
wss.on('connection', (ws: WebSocket) => {
|
||||||
const connectionId = uuidv4();
|
const connectionId = uuidv4();
|
||||||
connectedUsers.set(connectionId, { oderId: connectionId, ws });
|
connectedUsers.set(connectionId, { oderId: connectionId, ws, serverIds: new Set() });
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
try {
|
try {
|
||||||
@@ -374,13 +375,16 @@ wss.on('connection', (ws: WebSocket) => {
|
|||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
const user = connectedUsers.get(connectionId);
|
const user = connectedUsers.get(connectionId);
|
||||||
if (user?.serverId) {
|
if (user) {
|
||||||
// Notify others in the room - use user.oderId (the actual user ID), not connectionId
|
// Notify all servers the user was a member of
|
||||||
broadcastToServer(user.serverId, {
|
user.serverIds.forEach((sid) => {
|
||||||
|
broadcastToServer(sid, {
|
||||||
type: 'user_left',
|
type: 'user_left',
|
||||||
oderId: user.oderId,
|
oderId: user.oderId,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
|
serverId: sid,
|
||||||
}, user.oderId);
|
}, user.oderId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
connectedUsers.delete(connectionId);
|
connectedUsers.delete(connectionId);
|
||||||
});
|
});
|
||||||
@@ -403,44 +407,76 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
|
|||||||
console.log(`User identified: ${user.displayName} (${user.oderId})`);
|
console.log(`User identified: ${user.displayName} (${user.oderId})`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'join_server':
|
case 'join_server': {
|
||||||
user.serverId = message.serverId;
|
const sid = message.serverId;
|
||||||
|
const isNew = !user.serverIds.has(sid);
|
||||||
|
user.serverIds.add(sid);
|
||||||
|
user.viewedServerId = sid;
|
||||||
connectedUsers.set(connectionId, user);
|
connectedUsers.set(connectionId, user);
|
||||||
console.log(`User ${user.displayName || 'Anonymous'} (${user.oderId}) joined server ${message.serverId}`);
|
console.log(`User ${user.displayName || 'Anonymous'} (${user.oderId}) joined server ${sid} (new=${isNew})`);
|
||||||
|
|
||||||
// Get list of current users in server (exclude this user by oderId)
|
// Always send the current user list for this server
|
||||||
// Only include users that have been identified (have displayName)
|
|
||||||
const usersInServer = Array.from(connectedUsers.values())
|
const usersInServer = Array.from(connectedUsers.values())
|
||||||
.filter(u => u.serverId === message.serverId && u.oderId !== user.oderId && u.displayName)
|
.filter(u => u.serverIds.has(sid) && u.oderId !== user.oderId && u.displayName)
|
||||||
.map(u => ({ oderId: u.oderId, displayName: u.displayName || 'Anonymous' }));
|
.map(u => ({ oderId: u.oderId, displayName: u.displayName || 'Anonymous' }));
|
||||||
|
|
||||||
console.log(`Sending server_users to ${user.displayName || 'Anonymous'}:`, usersInServer);
|
console.log(`Sending server_users to ${user.displayName || 'Anonymous'}:`, usersInServer);
|
||||||
user.ws.send(JSON.stringify({
|
user.ws.send(JSON.stringify({
|
||||||
type: 'server_users',
|
type: 'server_users',
|
||||||
|
serverId: sid,
|
||||||
users: usersInServer,
|
users: usersInServer,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Notify others (exclude by oderId, not connectionId)
|
// Only broadcast user_joined if this is a brand-new join (not a re-view)
|
||||||
broadcastToServer(message.serverId, {
|
if (isNew) {
|
||||||
|
broadcastToServer(sid, {
|
||||||
type: 'user_joined',
|
type: 'user_joined',
|
||||||
oderId: user.oderId,
|
oderId: user.oderId,
|
||||||
displayName: user.displayName || 'Anonymous',
|
displayName: user.displayName || 'Anonymous',
|
||||||
}, user.oderId);
|
serverId: sid,
|
||||||
break;
|
|
||||||
|
|
||||||
case 'leave_server':
|
|
||||||
const oldServerId = user.serverId;
|
|
||||||
user.serverId = undefined;
|
|
||||||
connectedUsers.set(connectionId, user);
|
|
||||||
|
|
||||||
if (oldServerId) {
|
|
||||||
broadcastToServer(oldServerId, {
|
|
||||||
type: 'user_left',
|
|
||||||
oderId: user.oderId,
|
|
||||||
displayName: user.displayName || 'Anonymous',
|
|
||||||
}, user.oderId);
|
}, user.oderId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'view_server': {
|
||||||
|
// Just switch the viewed server without joining/leaving
|
||||||
|
const viewSid = message.serverId;
|
||||||
|
user.viewedServerId = viewSid;
|
||||||
|
connectedUsers.set(connectionId, user);
|
||||||
|
console.log(`User ${user.displayName || 'Anonymous'} (${user.oderId}) viewing server ${viewSid}`);
|
||||||
|
|
||||||
|
// Send current user list for the viewed server
|
||||||
|
const viewUsers = Array.from(connectedUsers.values())
|
||||||
|
.filter(u => u.serverIds.has(viewSid) && u.oderId !== user.oderId && u.displayName)
|
||||||
|
.map(u => ({ oderId: u.oderId, displayName: u.displayName || 'Anonymous' }));
|
||||||
|
|
||||||
|
user.ws.send(JSON.stringify({
|
||||||
|
type: 'server_users',
|
||||||
|
serverId: viewSid,
|
||||||
|
users: viewUsers,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'leave_server': {
|
||||||
|
const leaveSid = message.serverId || user.viewedServerId;
|
||||||
|
if (leaveSid) {
|
||||||
|
user.serverIds.delete(leaveSid);
|
||||||
|
if (user.viewedServerId === leaveSid) {
|
||||||
|
user.viewedServerId = undefined;
|
||||||
|
}
|
||||||
|
connectedUsers.set(connectionId, user);
|
||||||
|
|
||||||
|
broadcastToServer(leaveSid, {
|
||||||
|
type: 'user_left',
|
||||||
|
oderId: user.oderId,
|
||||||
|
displayName: user.displayName || 'Anonymous',
|
||||||
|
serverId: leaveSid,
|
||||||
|
}, user.oderId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'offer':
|
case 'offer':
|
||||||
case 'answer':
|
case 'answer':
|
||||||
@@ -460,11 +496,13 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'chat_message':
|
case 'chat_message': {
|
||||||
// Broadcast chat message to all users in the server
|
// Broadcast chat message to all users in the server
|
||||||
if (user.serverId) {
|
const chatSid = message.serverId || user.viewedServerId;
|
||||||
broadcastToServer(user.serverId, {
|
if (chatSid && user.serverIds.has(chatSid)) {
|
||||||
|
broadcastToServer(chatSid, {
|
||||||
type: 'chat_message',
|
type: 'chat_message',
|
||||||
|
serverId: chatSid,
|
||||||
message: message.message,
|
message: message.message,
|
||||||
senderId: user.oderId,
|
senderId: user.oderId,
|
||||||
senderName: user.displayName,
|
senderName: user.displayName,
|
||||||
@@ -472,17 +510,21 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'typing':
|
case 'typing': {
|
||||||
// Broadcast typing indicator
|
// Broadcast typing indicator
|
||||||
if (user.serverId) {
|
const typingSid = message.serverId || user.viewedServerId;
|
||||||
broadcastToServer(user.serverId, {
|
if (typingSid && user.serverIds.has(typingSid)) {
|
||||||
|
broadcastToServer(typingSid, {
|
||||||
type: 'user_typing',
|
type: 'user_typing',
|
||||||
|
serverId: typingSid,
|
||||||
oderId: user.oderId,
|
oderId: user.oderId,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
}, user.oderId);
|
}, user.oderId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('Unknown message type:', message.type);
|
console.log('Unknown message type:', message.type);
|
||||||
@@ -492,7 +534,7 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
|
|||||||
function broadcastToServer(serverId: string, message: any, excludeOderId?: string): void {
|
function broadcastToServer(serverId: string, message: any, excludeOderId?: string): void {
|
||||||
console.log(`Broadcasting to server ${serverId}, excluding ${excludeOderId}:`, message.type);
|
console.log(`Broadcasting to server ${serverId}, excluding ${excludeOderId}:`, message.type);
|
||||||
connectedUsers.forEach((user) => {
|
connectedUsers.forEach((user) => {
|
||||||
if (user.serverId === serverId && user.oderId !== excludeOderId) {
|
if (user.serverIds.has(serverId) && user.oderId !== excludeOderId) {
|
||||||
console.log(` -> Sending to ${user.displayName} (${user.oderId})`);
|
console.log(` -> Sending to ${user.displayName} (${user.oderId})`);
|
||||||
user.ws.send(JSON.stringify(message));
|
user.ws.send(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,14 +85,19 @@ export class VoiceSessionService {
|
|||||||
navigateToVoiceServer(): void {
|
navigateToVoiceServer(): void {
|
||||||
const session = this._voiceSession();
|
const session = this._voiceSession();
|
||||||
if (session) {
|
if (session) {
|
||||||
// Dispatch joinRoom action to update the store state
|
// Use viewServer to switch view without leaving current server
|
||||||
this.store.dispatch(RoomsActions.joinRoom({
|
this.store.dispatch(RoomsActions.viewServer({
|
||||||
roomId: session.serverId,
|
room: {
|
||||||
serverInfo: {
|
id: session.serverId,
|
||||||
name: session.serverName,
|
name: session.serverName,
|
||||||
description: session.serverDescription,
|
description: session.serverDescription,
|
||||||
hostName: 'Unknown',
|
hostId: '',
|
||||||
},
|
isPrivate: false,
|
||||||
|
createdAt: 0,
|
||||||
|
userCount: 0,
|
||||||
|
maxUsers: 50,
|
||||||
|
icon: session.serverIcon,
|
||||||
|
} as any,
|
||||||
}));
|
}));
|
||||||
this._isViewingVoiceServer.set(true);
|
this._isViewingVoiceServer.set(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export class WebRTCService {
|
|||||||
private currentServerId: string | null = null;
|
private currentServerId: string | null = null;
|
||||||
private lastIdentify: { oderId: string; displayName: string } | null = null;
|
private lastIdentify: { oderId: string; displayName: string } | null = null;
|
||||||
private lastJoin: { serverId: string; userId: string } | null = null;
|
private lastJoin: { serverId: string; userId: string } | null = null;
|
||||||
|
// Track all servers the user has joined (for multi-server membership)
|
||||||
|
private joinedServerIds = new Set<string>();
|
||||||
|
|
||||||
// Signals for reactive state
|
// Signals for reactive state
|
||||||
private readonly _peerId = signal<string>(uuidv4());
|
private readonly _peerId = signal<string>(uuidv4());
|
||||||
@@ -190,7 +192,22 @@ export class WebRTCService {
|
|||||||
displayName: this.lastIdentify.displayName,
|
displayName: this.lastIdentify.displayName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Rejoin ALL servers the user was a member of (multi-server)
|
||||||
|
if (this.joinedServerIds.size > 0) {
|
||||||
|
this.joinedServerIds.forEach((sid) => {
|
||||||
|
this.sendRawMessage({
|
||||||
|
type: 'join_server',
|
||||||
|
serverId: sid,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Re-view the last viewed server
|
||||||
if (this.lastJoin) {
|
if (this.lastJoin) {
|
||||||
|
this.sendRawMessage({
|
||||||
|
type: 'view_server',
|
||||||
|
serverId: this.lastJoin.serverId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (this.lastJoin) {
|
||||||
this.sendRawMessage({
|
this.sendRawMessage({
|
||||||
type: 'join_server',
|
type: 'join_server',
|
||||||
serverId: this.lastJoin.serverId,
|
serverId: this.lastJoin.serverId,
|
||||||
@@ -619,8 +636,12 @@ export class WebRTCService {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'user_left':
|
case 'user_left':
|
||||||
this.log('User left', { displayName: message.displayName, oderId: message.oderId });
|
this.log('User left', { displayName: message.displayName, oderId: message.oderId, serverId: message.serverId });
|
||||||
this.removePeer(message.oderId);
|
// With multi-server membership, only remove peer if they are leaving the
|
||||||
|
// currently viewed server AND we don't share any other server with them.
|
||||||
|
// For now, don't remove peers on server-specific leave –
|
||||||
|
// the signaling server will send a proper disconnect when the WS closes.
|
||||||
|
// The store effect handles removing the user from the user list.
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'offer':
|
case 'offer':
|
||||||
@@ -1162,9 +1183,10 @@ export class WebRTCService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join a room
|
// Join a room (first-time join – adds to multi-server membership)
|
||||||
joinRoom(roomId: string, userId: string): void {
|
joinRoom(roomId: string, userId: string): void {
|
||||||
this.lastJoin = { serverId: roomId, userId };
|
this.lastJoin = { serverId: roomId, userId };
|
||||||
|
this.joinedServerIds.add(roomId);
|
||||||
this.sendRawMessage({
|
this.sendRawMessage({
|
||||||
type: 'join_server',
|
type: 'join_server',
|
||||||
serverId: roomId,
|
serverId: roomId,
|
||||||
@@ -1172,23 +1194,30 @@ export class WebRTCService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch to a different server without disconnecting voice or peers.
|
* Switch the viewed server without affecting multi-server membership.
|
||||||
* This is used when navigating between servers while already connected.
|
* If the server hasn't been joined yet, performs a full join.
|
||||||
* If voice is connected, the user stays in voice in the original server.
|
* If already joined, just sends view_server to refresh user list.
|
||||||
*/
|
*/
|
||||||
switchServer(serverId: string, userId: string): void {
|
switchServer(serverId: string, userId: string): void {
|
||||||
// Update the last join info for reconnection purposes
|
// Update the last join info for reconnection purposes
|
||||||
this.lastJoin = { serverId, userId };
|
this.lastJoin = { serverId, userId };
|
||||||
|
|
||||||
// Send join_server to switch rooms on the signaling server
|
if (this.joinedServerIds.has(serverId)) {
|
||||||
// This tells the server we're now in a different room, but doesn't
|
// Already a member – just switch the view
|
||||||
// affect our peer connections or voice state
|
this.sendRawMessage({
|
||||||
|
type: 'view_server',
|
||||||
|
serverId: serverId,
|
||||||
|
});
|
||||||
|
this.log('Viewed server (already joined)', { serverId, userId, voiceConnected: this._isVoiceConnected() });
|
||||||
|
} else {
|
||||||
|
// Not yet joined – do a full join
|
||||||
|
this.joinedServerIds.add(serverId);
|
||||||
this.sendRawMessage({
|
this.sendRawMessage({
|
||||||
type: 'join_server',
|
type: 'join_server',
|
||||||
serverId: serverId,
|
serverId: serverId,
|
||||||
});
|
});
|
||||||
|
this.log('Joined new server via switch', { serverId, userId, voiceConnected: this._isVoiceConnected() });
|
||||||
this.log('Switched server', { serverId, userId, voiceConnected: this._isVoiceConnected() });
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleReconnect(): void {
|
private scheduleReconnect(): void {
|
||||||
@@ -1329,12 +1358,37 @@ export class WebRTCService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leave room
|
// Leave a specific server (or all if no serverId given)
|
||||||
leaveRoom(): void {
|
leaveRoom(serverId?: string): void {
|
||||||
|
if (serverId) {
|
||||||
|
// Leave a specific server
|
||||||
|
this.joinedServerIds.delete(serverId);
|
||||||
this.sendRawMessage({
|
this.sendRawMessage({
|
||||||
type: 'leave_server',
|
type: 'leave_server',
|
||||||
|
serverId: serverId,
|
||||||
});
|
});
|
||||||
|
this.log('Left server', { serverId });
|
||||||
|
|
||||||
|
// If no servers left, do full cleanup
|
||||||
|
if (this.joinedServerIds.size === 0) {
|
||||||
|
this.fullCleanup();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave ALL servers and clean up
|
||||||
|
this.joinedServerIds.forEach((sid) => {
|
||||||
|
this.sendRawMessage({
|
||||||
|
type: 'leave_server',
|
||||||
|
serverId: sid,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.joinedServerIds.clear();
|
||||||
|
this.fullCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal full cleanup – close peers, stop media
|
||||||
|
private fullCleanup(): void {
|
||||||
// Clear all P2P reconnect timers
|
// Clear all P2P reconnect timers
|
||||||
this.clearAllP2PReconnectTimers();
|
this.clearAllP2PReconnectTimers();
|
||||||
|
|
||||||
@@ -1353,9 +1407,19 @@ export class WebRTCService {
|
|||||||
this.stopScreenShare();
|
this.stopScreenShare();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Check whether we are already a member of the given server */
|
||||||
|
hasJoinedServer(serverId: string): boolean {
|
||||||
|
return this.joinedServerIds.has(serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get all server IDs we are currently a member of */
|
||||||
|
getJoinedServerIds(): ReadonlySet<string> {
|
||||||
|
return this.joinedServerIds;
|
||||||
|
}
|
||||||
|
|
||||||
// Disconnect from signaling server
|
// Disconnect from signaling server
|
||||||
disconnect(): void {
|
disconnect(): void {
|
||||||
this.leaveRoom();
|
this.leaveRoom(); // leaves all servers
|
||||||
this.stopVoiceHeartbeat();
|
this.stopVoiceHeartbeat();
|
||||||
|
|
||||||
if (this.signalingSocket) {
|
if (this.signalingSocket) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { lucidePlus } from '@ng-icons/lucide';
|
|||||||
import { Room } from '../../core/models';
|
import { Room } from '../../core/models';
|
||||||
import { selectSavedRooms, selectCurrentRoom } from '../../store/rooms/rooms.selectors';
|
import { selectSavedRooms, selectCurrentRoom } from '../../store/rooms/rooms.selectors';
|
||||||
import { VoiceSessionService } from '../../core/services/voice-session.service';
|
import { VoiceSessionService } from '../../core/services/voice-session.service';
|
||||||
|
import { WebRTCService } from '../../core/services/webrtc.service';
|
||||||
import * as RoomsActions from '../../store/rooms/rooms.actions';
|
import * as RoomsActions from '../../store/rooms/rooms.actions';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -21,6 +22,7 @@ export class ServersRailComponent {
|
|||||||
private store = inject(Store);
|
private store = inject(Store);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private voiceSession = inject(VoiceSessionService);
|
private voiceSession = inject(VoiceSessionService);
|
||||||
|
private webrtc = inject(WebRTCService);
|
||||||
savedRooms = this.store.selectSignal(selectSavedRooms);
|
savedRooms = this.store.selectSignal(selectSavedRooms);
|
||||||
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||||
|
|
||||||
@@ -52,8 +54,6 @@ export class ServersRailComponent {
|
|||||||
|
|
||||||
joinSavedRoom(room: Room): void {
|
joinSavedRoom(room: Room): void {
|
||||||
// Require auth: if no current user, go to login
|
// Require auth: if no current user, go to login
|
||||||
const current = this.currentRoom();
|
|
||||||
// currentRoom presence does not indicate auth; check localStorage for currentUserId
|
|
||||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||||
if (!currentUserId) {
|
if (!currentUserId) {
|
||||||
this.router.navigate(['/login']);
|
this.router.navigate(['/login']);
|
||||||
@@ -64,13 +64,19 @@ export class ServersRailComponent {
|
|||||||
const voiceServerId = this.voiceSession.getVoiceServerId();
|
const voiceServerId = this.voiceSession.getVoiceServerId();
|
||||||
if (voiceServerId && voiceServerId !== room.id) {
|
if (voiceServerId && voiceServerId !== room.id) {
|
||||||
// User is switching to a different server while connected to voice
|
// User is switching to a different server while connected to voice
|
||||||
// Update voice session to show floating controls
|
// Update voice session to show floating controls (voice stays connected)
|
||||||
this.voiceSession.setViewingVoiceServer(false);
|
this.voiceSession.setViewingVoiceServer(false);
|
||||||
} else if (voiceServerId === room.id) {
|
} else if (voiceServerId === room.id) {
|
||||||
// Navigating back to the voice-connected server
|
// Navigating back to the voice-connected server
|
||||||
this.voiceSession.setViewingVoiceServer(true);
|
this.voiceSession.setViewingVoiceServer(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we've already joined this server, just switch the view
|
||||||
|
// (no user_joined broadcast, no leave from other servers)
|
||||||
|
if (this.webrtc.hasJoinedServer(room.id)) {
|
||||||
|
this.store.dispatch(RoomsActions.viewServer({ room }));
|
||||||
|
} else {
|
||||||
|
// First time joining this server
|
||||||
this.store.dispatch(RoomsActions.joinRoom({
|
this.store.dispatch(RoomsActions.joinRoom({
|
||||||
roomId: room.id,
|
roomId: room.id,
|
||||||
serverInfo: {
|
serverInfo: {
|
||||||
@@ -80,6 +86,7 @@ export class ServersRailComponent {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
openContextMenu(evt: MouseEvent, room: Room): void {
|
openContextMenu(evt: MouseEvent, room: Room): void {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|||||||
@@ -67,6 +67,17 @@ export const leaveRoom = createAction('[Rooms] Leave Room');
|
|||||||
|
|
||||||
export const leaveRoomSuccess = createAction('[Rooms] Leave Room Success');
|
export const leaveRoomSuccess = createAction('[Rooms] Leave Room Success');
|
||||||
|
|
||||||
|
// View server (switch view without disconnecting)
|
||||||
|
export const viewServer = createAction(
|
||||||
|
'[Rooms] View Server',
|
||||||
|
props<{ room: Room }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const viewServerSuccess = createAction(
|
||||||
|
'[Rooms] View Server Success',
|
||||||
|
props<{ room: Room }>()
|
||||||
|
);
|
||||||
|
|
||||||
// Delete room
|
// Delete room
|
||||||
export const deleteRoom = createAction(
|
export const deleteRoom = createAction(
|
||||||
'[Rooms] Delete Room',
|
'[Rooms] Delete Room',
|
||||||
|
|||||||
@@ -196,9 +196,8 @@ export class RoomsEffects {
|
|||||||
|
|
||||||
// Check if already connected to signaling server
|
// Check if already connected to signaling server
|
||||||
if (this.webrtc.isConnected()) {
|
if (this.webrtc.isConnected()) {
|
||||||
// Already connected - just switch to the new server without reconnecting WebSocket
|
// Already connected - join the new server (additive, multi-server)
|
||||||
// This preserves voice connections and peer state
|
console.log('Already connected to signaling, joining room:', room.id);
|
||||||
console.log('Already connected to signaling, switching to room:', room.id);
|
|
||||||
this.webrtc.setCurrentServer(room.id);
|
this.webrtc.setCurrentServer(room.id);
|
||||||
this.webrtc.switchServer(room.id, oderId);
|
this.webrtc.switchServer(room.id, oderId);
|
||||||
} else {
|
} else {
|
||||||
@@ -223,6 +222,37 @@ export class RoomsEffects {
|
|||||||
{ dispatch: false }
|
{ dispatch: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// View server – switch view to an already-joined server without leaving others
|
||||||
|
viewServer$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(RoomsActions.viewServer),
|
||||||
|
withLatestFrom(this.store.select(selectCurrentUser)),
|
||||||
|
mergeMap(([{ room }, user]) => {
|
||||||
|
const oderId = user?.oderId || this.webrtc.peerId();
|
||||||
|
|
||||||
|
if (this.webrtc.isConnected()) {
|
||||||
|
this.webrtc.setCurrentServer(room.id);
|
||||||
|
this.webrtc.switchServer(room.id, oderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['/room', room.id]);
|
||||||
|
return of(RoomsActions.viewServerSuccess({ room }));
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// When viewing a different server, reload messages and users for that server
|
||||||
|
onViewServerSuccess$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(RoomsActions.viewServerSuccess),
|
||||||
|
mergeMap(({ room }) => [
|
||||||
|
UsersActions.clearUsers(),
|
||||||
|
MessagesActions.loadMessages({ roomId: room.id }),
|
||||||
|
UsersActions.loadBans(),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Leave room
|
// Leave room
|
||||||
leaveRoom$ = createEffect(() =>
|
leaveRoom$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
@@ -280,10 +310,8 @@ export class RoomsEffects {
|
|||||||
// Delete from local DB
|
// Delete from local DB
|
||||||
this.db.deleteRoom(roomId);
|
this.db.deleteRoom(roomId);
|
||||||
|
|
||||||
// If currently in this room, disconnect
|
// Leave this specific server (doesn't affect other servers)
|
||||||
if (currentRoom?.id === roomId) {
|
this.webrtc.leaveRoom(roomId);
|
||||||
this.webrtc.disconnectAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return of(RoomsActions.forgetRoomSuccess({ roomId }));
|
return of(RoomsActions.forgetRoomSuccess({ roomId }));
|
||||||
})
|
})
|
||||||
@@ -451,12 +479,23 @@ export class RoomsEffects {
|
|||||||
// Listen to WebRTC signaling messages for user presence
|
// Listen to WebRTC signaling messages for user presence
|
||||||
signalingMessages$ = createEffect(() =>
|
signalingMessages$ = createEffect(() =>
|
||||||
this.webrtc.onSignalingMessage.pipe(
|
this.webrtc.onSignalingMessage.pipe(
|
||||||
withLatestFrom(this.store.select(selectCurrentUser)),
|
withLatestFrom(
|
||||||
mergeMap(([message, currentUser]: [any, any]) => {
|
this.store.select(selectCurrentUser),
|
||||||
|
this.store.select(selectCurrentRoom),
|
||||||
|
),
|
||||||
|
mergeMap(([message, currentUser, currentRoom]: [any, any, any]) => {
|
||||||
const actions: any[] = [];
|
const actions: any[] = [];
|
||||||
const myId = currentUser?.oderId || currentUser?.id;
|
const myId = currentUser?.oderId || currentUser?.id;
|
||||||
|
const viewedServerId = currentRoom?.id;
|
||||||
|
|
||||||
if (message.type === 'server_users' && message.users) {
|
if (message.type === 'server_users' && message.users) {
|
||||||
|
// Only populate for the currently viewed server
|
||||||
|
const msgServerId = message.serverId;
|
||||||
|
if (msgServerId && viewedServerId && msgServerId !== viewedServerId) {
|
||||||
|
return [{ type: 'NO_OP' }];
|
||||||
|
}
|
||||||
|
// Clear existing users first, then add the new set
|
||||||
|
actions.push(UsersActions.clearUsers());
|
||||||
// Add all existing users to the store (excluding ourselves)
|
// Add all existing users to the store (excluding ourselves)
|
||||||
message.users.forEach((user: { oderId: string; displayName: string }) => {
|
message.users.forEach((user: { oderId: string; displayName: string }) => {
|
||||||
// Don't add ourselves to the list
|
// Don't add ourselves to the list
|
||||||
@@ -478,6 +517,11 @@ export class RoomsEffects {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (message.type === 'user_joined') {
|
} else if (message.type === 'user_joined') {
|
||||||
|
// Only add to user list if this event is for the currently viewed server
|
||||||
|
const msgServerId = message.serverId;
|
||||||
|
if (msgServerId && viewedServerId && msgServerId !== viewedServerId) {
|
||||||
|
return [{ type: 'NO_OP' }];
|
||||||
|
}
|
||||||
// Don't add ourselves to the list
|
// Don't add ourselves to the list
|
||||||
if (message.oderId !== myId) {
|
if (message.oderId !== myId) {
|
||||||
actions.push(
|
actions.push(
|
||||||
@@ -496,6 +540,11 @@ export class RoomsEffects {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (message.type === 'user_left') {
|
} else if (message.type === 'user_left') {
|
||||||
|
// Only remove from user list if this event is for the currently viewed server
|
||||||
|
const msgServerId = message.serverId;
|
||||||
|
if (msgServerId && viewedServerId && msgServerId !== viewedServerId) {
|
||||||
|
return [{ type: 'NO_OP' }];
|
||||||
|
}
|
||||||
actions.push(UsersActions.userLeft({ userId: message.oderId }));
|
actions.push(UsersActions.userLeft({ userId: message.oderId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,20 @@ export const roomsReducer = createReducer(
|
|||||||
isConnected: false,
|
isConnected: false,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
// View server – just switch the viewed room, stay connected
|
||||||
|
on(RoomsActions.viewServer, (state) => ({
|
||||||
|
...state,
|
||||||
|
isConnecting: true,
|
||||||
|
error: null,
|
||||||
|
})),
|
||||||
|
|
||||||
|
on(RoomsActions.viewServerSuccess, (state, { room }) => ({
|
||||||
|
...state,
|
||||||
|
currentRoom: room,
|
||||||
|
isConnecting: false,
|
||||||
|
isConnected: true,
|
||||||
|
})),
|
||||||
|
|
||||||
// Update room settings
|
// Update room settings
|
||||||
on(RoomsActions.updateRoomSettings, (state) => ({
|
on(RoomsActions.updateRoomSettings, (state) => ({
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
Reference in New Issue
Block a user