Add seperation of voice channels, creation of new ones, and move around users
This commit is contained in:
@@ -56,6 +56,7 @@ import {
|
||||
import { NotificationAudioService, AppSound } from '../../core/services/notification-audio.service';
|
||||
import { hasRoomBanForUser } from '../../core/helpers/room-ban.helpers';
|
||||
import { ROOM_URL_PATTERN } from '../../core/constants';
|
||||
import { VoiceSessionFacade } from '../../domains/voice-session';
|
||||
import {
|
||||
findRoomMember,
|
||||
removeRoomMember,
|
||||
@@ -138,6 +139,7 @@ export class RoomsEffects {
|
||||
private webrtc = inject(RealtimeSessionFacade);
|
||||
private serverDirectory = inject(ServerDirectoryFacade);
|
||||
private audioService = inject(NotificationAudioService);
|
||||
private voiceSessionService = inject(VoiceSessionFacade);
|
||||
|
||||
/**
|
||||
* Tracks user IDs we already know are in voice. Lives outside the
|
||||
@@ -404,6 +406,42 @@ export class RoomsEffects {
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
refreshServerOwnedRoomMetadata$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RoomsActions.joinRoomSuccess, RoomsActions.viewServerSuccess),
|
||||
switchMap(({ room }) =>
|
||||
this.serverDirectory.getServer(room.id, {
|
||||
sourceId: room.sourceId,
|
||||
sourceUrl: room.sourceUrl
|
||||
}).pipe(
|
||||
map((serverData) => {
|
||||
if (!serverData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return RoomsActions.updateRoom({
|
||||
roomId: room.id,
|
||||
changes: {
|
||||
name: serverData.name,
|
||||
description: serverData.description,
|
||||
hostId: serverData.ownerId || room.hostId,
|
||||
hasPassword: !!serverData.hasPassword,
|
||||
isPrivate: serverData.isPrivate,
|
||||
maxUsers: serverData.maxUsers,
|
||||
channels: Array.isArray(serverData.channels) ? serverData.channels : room.channels,
|
||||
sourceId: serverData.sourceId ?? room.sourceId,
|
||||
sourceName: serverData.sourceName ?? room.sourceName,
|
||||
sourceUrl: serverData.sourceUrl ?? room.sourceUrl
|
||||
}
|
||||
});
|
||||
}),
|
||||
filter((action): action is ReturnType<typeof RoomsActions.updateRoom> => !!action),
|
||||
catchError(() => EMPTY)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
/** Switches the UI view to an already-joined server without leaving others. */
|
||||
viewServer$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
@@ -1024,9 +1062,11 @@ export class RoomsEffects {
|
||||
]) => {
|
||||
switch (event.type) {
|
||||
case 'voice-state':
|
||||
return currentRoom ? this.handleVoiceOrScreenState(event, allUsers, 'voice') : EMPTY;
|
||||
return currentRoom ? this.handleVoiceOrScreenState(event, allUsers, currentUser ?? null, 'voice') : EMPTY;
|
||||
case 'voice-channel-move':
|
||||
return this.handleVoiceChannelMove(event, currentRoom, savedRooms, currentUser ?? null);
|
||||
case 'screen-state':
|
||||
return currentRoom ? this.handleVoiceOrScreenState(event, allUsers, 'screen') : EMPTY;
|
||||
return currentRoom ? this.handleVoiceOrScreenState(event, allUsers, currentUser ?? null, 'screen') : EMPTY;
|
||||
case 'server-state-request':
|
||||
return this.handleServerStateRequest(event, currentRoom, savedRooms);
|
||||
case 'server-state-full':
|
||||
@@ -1051,13 +1091,14 @@ export class RoomsEffects {
|
||||
)
|
||||
);
|
||||
|
||||
private handleVoiceOrScreenState(event: ChatEvent, allUsers: User[], kind: 'voice' | 'screen') {
|
||||
private handleVoiceOrScreenState(event: ChatEvent, allUsers: User[], currentUser: User | null, kind: 'voice' | 'screen') {
|
||||
const userId: string | undefined = event.fromPeerId ?? event.oderId;
|
||||
|
||||
if (!userId)
|
||||
return EMPTY;
|
||||
|
||||
const userExists = allUsers.some((u) => u.id === userId || u.oderId === userId);
|
||||
const existingUser = allUsers.find((u) => u.id === userId || u.oderId === userId);
|
||||
const userExists = !!existingUser;
|
||||
|
||||
if (kind === 'voice') {
|
||||
const vs = event.voiceState as Partial<VoiceState> | undefined;
|
||||
@@ -1070,13 +1111,14 @@ export class RoomsEffects {
|
||||
// clearUsers() from server-switching doesn't create false transitions.
|
||||
const weAreInVoice = this.webrtc.isVoiceConnected();
|
||||
const nowConnected = vs.isConnected ?? false;
|
||||
const wasKnown = this.knownVoiceUsers.has(userId);
|
||||
const wasInCurrentVoiceRoom = this.isSameVoiceRoom(existingUser?.voiceState, currentUser?.voiceState);
|
||||
const isInCurrentVoiceRoom = this.isSameVoiceRoom(vs, currentUser?.voiceState);
|
||||
|
||||
if (weAreInVoice) {
|
||||
const wasKnown = this.knownVoiceUsers.has(userId);
|
||||
|
||||
if (!wasKnown && nowConnected) {
|
||||
if (((!wasKnown && isInCurrentVoiceRoom) || (userExists && !wasInCurrentVoiceRoom && isInCurrentVoiceRoom)) && nowConnected) {
|
||||
this.audioService.play(AppSound.Joining);
|
||||
} else if (wasKnown && !nowConnected) {
|
||||
} else if (wasInCurrentVoiceRoom && !isInCurrentVoiceRoom) {
|
||||
this.audioService.play(AppSound.Leave);
|
||||
}
|
||||
}
|
||||
@@ -1141,6 +1183,79 @@ export class RoomsEffects {
|
||||
);
|
||||
}
|
||||
|
||||
private handleVoiceChannelMove(
|
||||
event: ChatEvent,
|
||||
currentRoom: Room | null,
|
||||
savedRooms: Room[],
|
||||
currentUser: User | null
|
||||
) {
|
||||
const targetUserId = typeof event.targetUserId === 'string' ? event.targetUserId : null;
|
||||
const serverId = typeof event.roomId === 'string' ? event.roomId : currentUser?.voiceState?.serverId;
|
||||
const nextVoiceState = event.voiceState as Partial<VoiceState> | undefined;
|
||||
|
||||
if (!currentUser || !targetUserId || !serverId || !nextVoiceState?.roomId) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
if (targetUserId !== currentUser.id && targetUserId !== currentUser.oderId) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const room = this.resolveRoom(serverId, currentRoom, savedRooms);
|
||||
const movedChannel = room?.channels?.find((channel) => channel.id === nextVoiceState.roomId && channel.type === 'voice');
|
||||
|
||||
if (!room || !movedChannel) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const updatedVoiceState: Partial<VoiceState> = {
|
||||
isConnected: true,
|
||||
isMuted: currentUser.voiceState?.isMuted ?? false,
|
||||
isDeafened: currentUser.voiceState?.isDeafened ?? false,
|
||||
isSpeaking: currentUser.voiceState?.isSpeaking ?? false,
|
||||
isMutedByAdmin: currentUser.voiceState?.isMutedByAdmin,
|
||||
volume: currentUser.voiceState?.volume,
|
||||
roomId: movedChannel.id,
|
||||
serverId: room.id
|
||||
};
|
||||
const wasViewingVoiceServer = this.voiceSessionService.isViewingVoiceServer();
|
||||
|
||||
this.webrtc.startVoiceHeartbeat(movedChannel.id, room.id);
|
||||
this.voiceSessionService.startSession({
|
||||
serverId: room.id,
|
||||
serverName: room.name,
|
||||
roomId: movedChannel.id,
|
||||
roomName: `🔊 ${movedChannel.name}`,
|
||||
serverIcon: room.icon,
|
||||
serverDescription: room.description,
|
||||
serverRoute: `/room/${room.id}`
|
||||
});
|
||||
this.voiceSessionService.setViewingVoiceServer(wasViewingVoiceServer);
|
||||
this.webrtc.broadcastMessage({
|
||||
type: 'voice-state',
|
||||
oderId: currentUser.oderId || currentUser.id,
|
||||
displayName: currentUser.displayName || 'User',
|
||||
voiceState: updatedVoiceState
|
||||
});
|
||||
|
||||
return of(UsersActions.updateVoiceState({
|
||||
userId: currentUser.id,
|
||||
voiceState: updatedVoiceState
|
||||
}));
|
||||
}
|
||||
|
||||
private isSameVoiceRoom(
|
||||
voiceState: Partial<VoiceState> | undefined,
|
||||
currentUserVoiceState: Partial<VoiceState> | undefined
|
||||
): boolean {
|
||||
return !!voiceState?.isConnected
|
||||
&& !!currentUserVoiceState?.isConnected
|
||||
&& !!voiceState.roomId
|
||||
&& !!voiceState.serverId
|
||||
&& voiceState.roomId === currentUserVoiceState.roomId
|
||||
&& voiceState.serverId === currentUserVoiceState.serverId;
|
||||
}
|
||||
|
||||
private resolveRoom(roomId: string | undefined, currentRoom: Room | null, savedRooms: Room[]): Room | null {
|
||||
if (!roomId)
|
||||
return currentRoom;
|
||||
|
||||
Reference in New Issue
Block a user