feat: server image
This commit is contained in:
@@ -308,18 +308,29 @@ export class RoomSettingsEffects {
|
||||
updateServerIcon$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RoomsActions.updateServerIcon),
|
||||
withLatestFrom(this.store.select(selectCurrentUser), this.store.select(selectCurrentRoom)),
|
||||
withLatestFrom(
|
||||
this.store.select(selectCurrentUser),
|
||||
this.store.select(selectCurrentRoom),
|
||||
this.store.select(selectSavedRooms)
|
||||
),
|
||||
mergeMap(([
|
||||
{ roomId, icon },
|
||||
currentUser,
|
||||
currentRoom
|
||||
currentRoom,
|
||||
savedRooms
|
||||
]) => {
|
||||
if (!currentUser || !currentRoom || currentRoom.id !== roomId) {
|
||||
return of(RoomsActions.updateServerIconFailure({ error: 'Not in room' }));
|
||||
if (!currentUser) {
|
||||
return of(RoomsActions.updateServerIconFailure({ error: 'Not logged in' }));
|
||||
}
|
||||
|
||||
const isOwner = currentRoom.hostId === currentUser.id;
|
||||
const canByRole = resolveRoomPermission(currentRoom, currentUser, 'manageIcon');
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
|
||||
if (!room) {
|
||||
return of(RoomsActions.updateServerIconFailure({ error: 'Room not found' }));
|
||||
}
|
||||
|
||||
const isOwner = room.hostId === currentUser.id || room.hostId === currentUser.oderId;
|
||||
const canByRole = resolveRoomPermission(room, currentUser, 'manageIcon');
|
||||
|
||||
if (!isOwner && !canByRole) {
|
||||
return of(RoomsActions.updateServerIconFailure({ error: 'Permission denied' }));
|
||||
@@ -329,15 +340,32 @@ export class RoomSettingsEffects {
|
||||
const changes: Partial<Room> = { icon,
|
||||
iconUpdatedAt };
|
||||
|
||||
this.db.updateRoom(roomId, changes);
|
||||
this.db.updateRoom(room.id, changes);
|
||||
this.webrtc.broadcastMessage({
|
||||
type: 'server-icon-update',
|
||||
roomId,
|
||||
roomId: room.id,
|
||||
icon,
|
||||
iconUpdatedAt
|
||||
});
|
||||
this.webrtc.sendRawMessage({
|
||||
type: 'server_icon_available',
|
||||
serverId: room.id,
|
||||
iconUpdatedAt
|
||||
});
|
||||
|
||||
return of(RoomsActions.updateServerIconSuccess({ roomId,
|
||||
this.serverDirectory.updateServer(room.id, {
|
||||
currentOwnerId: currentUser.id,
|
||||
actingRole: isOwner ? 'host' : undefined,
|
||||
icon,
|
||||
iconUpdatedAt
|
||||
}, {
|
||||
sourceId: room.sourceId,
|
||||
sourceUrl: room.sourceUrl
|
||||
}).subscribe({
|
||||
error: () => {}
|
||||
});
|
||||
|
||||
return of(RoomsActions.updateServerIconSuccess({ roomId: room.id,
|
||||
icon,
|
||||
iconUpdatedAt }));
|
||||
})
|
||||
|
||||
@@ -1,44 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import {
|
||||
Actions,
|
||||
createEffect,
|
||||
ofType
|
||||
} from '@ngrx/effects';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import { Store, type Action } from '@ngrx/store';
|
||||
import {
|
||||
of,
|
||||
from,
|
||||
EMPTY
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
mergeMap,
|
||||
withLatestFrom,
|
||||
tap,
|
||||
switchMap,
|
||||
catchError
|
||||
} from 'rxjs/operators';
|
||||
import { of, from, EMPTY } from 'rxjs';
|
||||
import { map, mergeMap, withLatestFrom, tap, switchMap, catchError } from 'rxjs/operators';
|
||||
import { RoomsActions } from './rooms.actions';
|
||||
import { UsersActions } from '../users/users.actions';
|
||||
import { selectCurrentUser, selectAllUsers } from '../users/users.selectors';
|
||||
import {
|
||||
selectActiveChannelId,
|
||||
selectCurrentRoom,
|
||||
selectSavedRooms
|
||||
} from './rooms.selectors';
|
||||
import { selectActiveChannelId, selectCurrentRoom, selectSavedRooms } from './rooms.selectors';
|
||||
import { RealtimeSessionFacade } from '../../core/realtime';
|
||||
import { DatabaseService } from '../../infrastructure/persistence';
|
||||
import { resolveRoomPermission } from '../../domains/access-control';
|
||||
import type {
|
||||
ChatEvent,
|
||||
Room,
|
||||
RoomSettings,
|
||||
RoomPermissions,
|
||||
BanEntry,
|
||||
User,
|
||||
VoiceState
|
||||
} from '../../shared-kernel';
|
||||
import type { ChatEvent, Room, RoomSettings, RoomPermissions, BanEntry, User, VoiceState } from '../../shared-kernel';
|
||||
import { NotificationAudioService, AppSound } from '../../core/services/notification-audio.service';
|
||||
import { hasRoomBanForUser } from '../../domains/access-control';
|
||||
import { RECONNECT_SOUND_GRACE_MS } from '../../core/constants';
|
||||
@@ -55,6 +28,8 @@ import {
|
||||
} from './rooms.helpers';
|
||||
import type { RoomPresenceSignalingMessage } from './rooms.helpers';
|
||||
|
||||
const SERVER_ICON_SYNC_REQUEST_DELAYS_MS = [1_500, 3_000, 5_000, 8_000];
|
||||
|
||||
/**
|
||||
* NgRx effects for real-time state synchronisation: signaling presence
|
||||
* events (server_users, user_joined, user_left, access_denied), P2P
|
||||
@@ -75,6 +50,7 @@ export class RoomStateSyncEffects {
|
||||
* preventing false join/leave sounds during state refreshes.
|
||||
*/
|
||||
private knownVoiceUsers = new Set<string>();
|
||||
private pendingServerIconRequestsByPeer = new Map<string, Set<string>>();
|
||||
/**
|
||||
* When a user leaves (e.g. socket drops), record the timestamp so
|
||||
* that a rapid re-join (reconnect) does not trigger a false
|
||||
@@ -87,17 +63,8 @@ export class RoomStateSyncEffects {
|
||||
/** Handles WebRTC signaling events for user presence (join, leave, server_users). */
|
||||
signalingMessages$ = createEffect(() =>
|
||||
this.webrtc.onSignalingMessage.pipe(
|
||||
withLatestFrom(
|
||||
this.store.select(selectCurrentUser),
|
||||
this.store.select(selectCurrentRoom),
|
||||
this.store.select(selectSavedRooms)
|
||||
),
|
||||
mergeMap(([
|
||||
message,
|
||||
currentUser,
|
||||
currentRoom,
|
||||
savedRooms
|
||||
]) => {
|
||||
withLatestFrom(this.store.select(selectCurrentUser), this.store.select(selectCurrentRoom), this.store.select(selectSavedRooms)),
|
||||
mergeMap(([message, currentUser, currentRoom, savedRooms]) => {
|
||||
const signalingMessage: RoomPresenceSignalingMessage = message;
|
||||
const myId = currentUser?.oderId || currentUser?.id;
|
||||
const viewedServerId = currentRoom?.id;
|
||||
@@ -106,8 +73,7 @@ export class RoomStateSyncEffects {
|
||||
|
||||
switch (signalingMessage.type) {
|
||||
case 'server_users': {
|
||||
if (!Array.isArray(signalingMessage.users) || !signalingMessage.serverId)
|
||||
return EMPTY;
|
||||
if (!Array.isArray(signalingMessage.users) || !signalingMessage.serverId) return EMPTY;
|
||||
|
||||
const syncedUsers = signalingMessage.users
|
||||
.filter((user) => user.oderId !== myId)
|
||||
@@ -136,11 +102,9 @@ export class RoomStateSyncEffects {
|
||||
}
|
||||
|
||||
case 'user_joined': {
|
||||
if (!signalingMessage.serverId || signalingMessage.oderId === myId)
|
||||
return EMPTY;
|
||||
if (!signalingMessage.serverId || signalingMessage.oderId === myId) return EMPTY;
|
||||
|
||||
if (!signalingMessage.oderId)
|
||||
return EMPTY;
|
||||
if (!signalingMessage.oderId) return EMPTY;
|
||||
|
||||
const joinedUser = {
|
||||
oderId: signalingMessage.oderId,
|
||||
@@ -168,12 +132,9 @@ export class RoomStateSyncEffects {
|
||||
}
|
||||
|
||||
case 'user_left': {
|
||||
if (!signalingMessage.oderId)
|
||||
return EMPTY;
|
||||
if (!signalingMessage.oderId) return EMPTY;
|
||||
|
||||
const remainingServerIds = Array.isArray(signalingMessage.serverIds)
|
||||
? signalingMessage.serverIds
|
||||
: undefined;
|
||||
const remainingServerIds = Array.isArray(signalingMessage.serverIds) ? signalingMessage.serverIds : undefined;
|
||||
|
||||
if (!remainingServerIds || remainingServerIds.length === 0) {
|
||||
if (this.knownVoiceUsers.has(signalingMessage.oderId)) {
|
||||
@@ -199,24 +160,15 @@ export class RoomStateSyncEffects {
|
||||
}
|
||||
|
||||
case 'status_update': {
|
||||
if (!signalingMessage.oderId || !signalingMessage.status)
|
||||
return EMPTY;
|
||||
if (!signalingMessage.oderId || !signalingMessage.status) return EMPTY;
|
||||
|
||||
const validStatuses = [
|
||||
'online',
|
||||
'away',
|
||||
'busy',
|
||||
'offline'
|
||||
];
|
||||
const validStatuses = ['online', 'away', 'busy', 'offline'];
|
||||
|
||||
if (!validStatuses.includes(signalingMessage.status))
|
||||
return EMPTY;
|
||||
if (!validStatuses.includes(signalingMessage.status)) return EMPTY;
|
||||
|
||||
// 'offline' from the server means the user chose Invisible;
|
||||
// display them as disconnected to other users.
|
||||
const mappedStatus = signalingMessage.status === 'offline'
|
||||
? 'disconnected'
|
||||
: signalingMessage.status as 'online' | 'away' | 'busy';
|
||||
const mappedStatus = signalingMessage.status === 'offline' ? 'disconnected' : (signalingMessage.status as 'online' | 'away' | 'busy');
|
||||
|
||||
return [
|
||||
UsersActions.updateRemoteUserStatus({
|
||||
@@ -227,21 +179,75 @@ export class RoomStateSyncEffects {
|
||||
}
|
||||
|
||||
case 'access_denied': {
|
||||
if (isWrongServer(signalingMessage.serverId, viewedServerId))
|
||||
return EMPTY;
|
||||
if (isWrongServer(signalingMessage.serverId, viewedServerId)) return EMPTY;
|
||||
|
||||
if (signalingMessage.reason !== 'SERVER_NOT_FOUND')
|
||||
return EMPTY;
|
||||
if (signalingMessage.reason !== 'SERVER_NOT_FOUND') return EMPTY;
|
||||
|
||||
// When multiple signal URLs are configured, the room may already
|
||||
// be successfully joined on a different signal server. Only show
|
||||
// the reconnect notice when the room is not reachable at all.
|
||||
if (signalingMessage.serverId && this.webrtc.hasJoinedServer(signalingMessage.serverId))
|
||||
return EMPTY;
|
||||
if (signalingMessage.serverId && this.webrtc.hasJoinedServer(signalingMessage.serverId)) return EMPTY;
|
||||
|
||||
return [RoomsActions.setSignalServerReconnecting({ isReconnecting: true })];
|
||||
}
|
||||
|
||||
case 'server_icon_sync_peers': {
|
||||
if (!signalingMessage.serverId || !Array.isArray(signalingMessage.users)) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const serverId = signalingMessage.serverId;
|
||||
|
||||
for (const user of signalingMessage.users) {
|
||||
if (!user.oderId || user.oderId === myId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.queueServerIconSyncRequest(user.oderId, serverId);
|
||||
this.webrtc.sendRawMessage({
|
||||
type: 'server_icon_peer_request',
|
||||
targetUserId: user.oderId,
|
||||
serverId
|
||||
});
|
||||
}
|
||||
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
case 'server_icon_peer_request': {
|
||||
const serverId = signalingMessage.serverId;
|
||||
const targetUserId = signalingMessage.fromUserId;
|
||||
const room = resolveRoom(serverId, currentRoom, savedRooms);
|
||||
|
||||
if (!serverId || !targetUserId || !room?.icon) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
this.webrtc.sendRawMessage({
|
||||
type: 'server_icon_peer_data',
|
||||
targetUserId,
|
||||
serverId,
|
||||
icon: room.icon,
|
||||
iconUpdatedAt: room.iconUpdatedAt || 0
|
||||
});
|
||||
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
case 'server_icon_peer_data': {
|
||||
if (!signalingMessage.serverId || typeof signalingMessage.icon !== 'string') {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return of(
|
||||
RoomsActions.receiveSearchServerIcon({
|
||||
roomId: signalingMessage.serverId,
|
||||
icon: signalingMessage.icon,
|
||||
iconUpdatedAt: signalingMessage.iconUpdatedAt || Date.now()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return EMPTY;
|
||||
}
|
||||
@@ -257,8 +263,7 @@ export class RoomStateSyncEffects {
|
||||
this.webrtc.onPeerConnected.pipe(
|
||||
withLatestFrom(this.store.select(selectCurrentRoom)),
|
||||
tap(([peerId, room]) => {
|
||||
if (!room)
|
||||
return;
|
||||
if (!room) return;
|
||||
|
||||
this.webrtc.sendToPeer(peerId, {
|
||||
type: 'server-state-request',
|
||||
@@ -273,12 +278,16 @@ export class RoomStateSyncEffects {
|
||||
roomEntryServerStateSync$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(
|
||||
RoomsActions.createRoomSuccess,
|
||||
RoomsActions.joinRoomSuccess,
|
||||
RoomsActions.viewServerSuccess
|
||||
),
|
||||
ofType(RoomsActions.createRoomSuccess, RoomsActions.joinRoomSuccess, RoomsActions.viewServerSuccess),
|
||||
tap(({ room }) => {
|
||||
if (room.iconUpdatedAt) {
|
||||
this.webrtc.sendRawMessage({
|
||||
type: 'server_icon_available',
|
||||
serverId: room.id,
|
||||
iconUpdatedAt: room.iconUpdatedAt
|
||||
});
|
||||
}
|
||||
|
||||
for (const peerId of this.webrtc.getConnectedPeers()) {
|
||||
try {
|
||||
this.webrtc.sendToPeer(peerId, {
|
||||
@@ -304,14 +313,7 @@ export class RoomStateSyncEffects {
|
||||
this.store.select(selectCurrentUser),
|
||||
this.store.select(selectActiveChannelId)
|
||||
),
|
||||
mergeMap(([
|
||||
event,
|
||||
currentRoom,
|
||||
savedRooms,
|
||||
allUsers,
|
||||
currentUser,
|
||||
activeChannelId
|
||||
]) => {
|
||||
mergeMap(([event, currentRoom, savedRooms, allUsers, currentUser, activeChannelId]) => {
|
||||
switch (event.type) {
|
||||
case 'voice-state':
|
||||
return this.handleVoiceOrScreenState(event, allUsers, currentUser ?? null, 'voice');
|
||||
@@ -351,8 +353,7 @@ export class RoomStateSyncEffects {
|
||||
this.webrtc.onPeerConnected.pipe(
|
||||
withLatestFrom(this.store.select(selectCurrentRoom)),
|
||||
tap(([_peerId, room]) => {
|
||||
if (!room)
|
||||
return;
|
||||
if (!room) return;
|
||||
|
||||
const iconUpdatedAt = room.iconUpdatedAt || 0;
|
||||
|
||||
@@ -366,18 +367,29 @@ export class RoomStateSyncEffects {
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
/** Sends queued discovery icon requests as soon as a temporary peer channel opens. */
|
||||
peerConnectedDiscoveryIconSync$ = createEffect(
|
||||
() =>
|
||||
this.webrtc.onPeerConnected.pipe(
|
||||
tap((peerId) => {
|
||||
const serverIds = this.pendingServerIconRequestsByPeer.get(peerId);
|
||||
|
||||
if (!serverIds) return;
|
||||
|
||||
for (const serverId of serverIds) {
|
||||
this.sendServerIconSyncRequest(peerId, serverId);
|
||||
}
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
// ── Voice / Screen / Camera handlers ───────────────────────────
|
||||
|
||||
private handleVoiceOrScreenState(
|
||||
event: ChatEvent,
|
||||
allUsers: User[],
|
||||
currentUser: User | null,
|
||||
kind: 'voice' | 'screen' | 'camera'
|
||||
) {
|
||||
private handleVoiceOrScreenState(event: ChatEvent, allUsers: User[], currentUser: User | null, kind: 'voice' | 'screen' | 'camera') {
|
||||
const userId: string | undefined = event.fromPeerId ?? event.oderId;
|
||||
|
||||
if (!userId)
|
||||
return EMPTY;
|
||||
if (!userId) return EMPTY;
|
||||
|
||||
const existingUser = allUsers.find((user) => user.id === userId || user.oderId === userId);
|
||||
const userExists = !!existingUser;
|
||||
@@ -385,18 +397,17 @@ export class RoomStateSyncEffects {
|
||||
if (kind === 'voice') {
|
||||
const vs = event.voiceState as Partial<VoiceState> | undefined;
|
||||
|
||||
if (!vs)
|
||||
return EMPTY;
|
||||
if (!vs) return EMPTY;
|
||||
|
||||
const presenceRefreshAction = vs.serverId && !existingUser?.presenceServerIds?.includes(vs.serverId)
|
||||
? UsersActions.userJoined({
|
||||
user: buildSignalingUser(
|
||||
{ oderId: userId,
|
||||
displayName: event.displayName || existingUser?.displayName || 'User' },
|
||||
{ presenceServerIds: [vs.serverId] }
|
||||
)
|
||||
})
|
||||
: null;
|
||||
const presenceRefreshAction =
|
||||
vs.serverId && !existingUser?.presenceServerIds?.includes(vs.serverId)
|
||||
? UsersActions.userJoined({
|
||||
user: buildSignalingUser(
|
||||
{ oderId: userId, displayName: event.displayName || existingUser?.displayName || 'User' },
|
||||
{ presenceServerIds: [vs.serverId] }
|
||||
)
|
||||
})
|
||||
: null;
|
||||
// Detect voice-connection transitions to play join/leave sounds.
|
||||
const weAreInVoice = this.webrtc.isVoiceConnected();
|
||||
const nowConnected = vs.isConnected ?? false;
|
||||
@@ -427,8 +438,7 @@ export class RoomStateSyncEffects {
|
||||
return of(
|
||||
UsersActions.userJoined({
|
||||
user: buildSignalingUser(
|
||||
{ oderId: userId,
|
||||
displayName: event.displayName || 'User' },
|
||||
{ oderId: userId, displayName: event.displayName || 'User' },
|
||||
{
|
||||
presenceServerIds: vs.serverId ? [vs.serverId] : undefined,
|
||||
voiceState: {
|
||||
@@ -453,8 +463,7 @@ export class RoomStateSyncEffects {
|
||||
actions.push(presenceRefreshAction);
|
||||
}
|
||||
|
||||
actions.push(UsersActions.updateVoiceState({ userId,
|
||||
voiceState: vs }));
|
||||
actions.push(UsersActions.updateVoiceState({ userId, voiceState: vs }));
|
||||
|
||||
return actions;
|
||||
}
|
||||
@@ -462,17 +471,12 @@ export class RoomStateSyncEffects {
|
||||
if (kind === 'screen') {
|
||||
const isSharing = event.isScreenSharing as boolean | undefined;
|
||||
|
||||
if (isSharing === undefined)
|
||||
return EMPTY;
|
||||
if (isSharing === undefined) return EMPTY;
|
||||
|
||||
if (!userExists) {
|
||||
return of(
|
||||
UsersActions.userJoined({
|
||||
user: buildSignalingUser(
|
||||
{ oderId: userId,
|
||||
displayName: event.displayName || 'User' },
|
||||
{ screenShareState: { isSharing } }
|
||||
)
|
||||
user: buildSignalingUser({ oderId: userId, displayName: event.displayName || 'User' }, { screenShareState: { isSharing } })
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -487,17 +491,12 @@ export class RoomStateSyncEffects {
|
||||
|
||||
const isCameraEnabled = event.isCameraEnabled as boolean | undefined;
|
||||
|
||||
if (isCameraEnabled === undefined)
|
||||
return EMPTY;
|
||||
if (isCameraEnabled === undefined) return EMPTY;
|
||||
|
||||
if (!userExists) {
|
||||
return of(
|
||||
UsersActions.userJoined({
|
||||
user: buildSignalingUser(
|
||||
{ oderId: userId,
|
||||
displayName: event.displayName || 'User' },
|
||||
{ cameraState: { isEnabled: isCameraEnabled } }
|
||||
)
|
||||
user: buildSignalingUser({ oderId: userId, displayName: event.displayName || 'User' }, { cameraState: { isEnabled: isCameraEnabled } })
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -510,12 +509,7 @@ export class RoomStateSyncEffects {
|
||||
);
|
||||
}
|
||||
|
||||
private handleVoiceChannelMove(
|
||||
event: ChatEvent,
|
||||
currentRoom: Room | null,
|
||||
savedRooms: Room[],
|
||||
currentUser: User | null
|
||||
) {
|
||||
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;
|
||||
@@ -566,22 +560,23 @@ export class RoomStateSyncEffects {
|
||||
voiceState: updatedVoiceState
|
||||
});
|
||||
|
||||
return of(UsersActions.updateVoiceState({
|
||||
userId: currentUser.id,
|
||||
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 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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -614,8 +609,7 @@ export class RoomStateSyncEffects {
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
const fromPeerId = event.fromPeerId;
|
||||
|
||||
if (!room || !fromPeerId)
|
||||
return EMPTY;
|
||||
if (!room || !fromPeerId) return EMPTY;
|
||||
|
||||
return from(this.db.getBansForRoom(room.id)).pipe(
|
||||
tap((bans) => {
|
||||
@@ -630,18 +624,12 @@ export class RoomStateSyncEffects {
|
||||
);
|
||||
}
|
||||
|
||||
private handleServerStateFull(
|
||||
event: ChatEvent,
|
||||
currentRoom: Room | null,
|
||||
savedRooms: Room[],
|
||||
currentUser: { id: string; oderId: string } | null
|
||||
) {
|
||||
private handleServerStateFull(event: ChatEvent, currentRoom: Room | null, savedRooms: Room[], currentUser: { id: string; oderId: string } | null) {
|
||||
const roomId = typeof event.roomId === 'string' ? event.roomId : currentRoom?.id;
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
const incomingRoom = event.room as Partial<Room> | undefined;
|
||||
|
||||
if (!room || !incomingRoom)
|
||||
return EMPTY;
|
||||
if (!room || !incomingRoom) return EMPTY;
|
||||
|
||||
const roomChanges = {
|
||||
...sanitizeRoomSnapshot(incomingRoom),
|
||||
@@ -651,19 +639,17 @@ export class RoomStateSyncEffects {
|
||||
|
||||
return this.syncBansToLocalRoom(room.id, bans).pipe(
|
||||
mergeMap(() => {
|
||||
const actions: (ReturnType<typeof RoomsActions.updateRoom>
|
||||
const actions: (
|
||||
| ReturnType<typeof RoomsActions.updateRoom>
|
||||
| ReturnType<typeof UsersActions.loadBansSuccess>
|
||||
| ReturnType<typeof RoomsActions.forgetRoom>)[] = [
|
||||
| ReturnType<typeof RoomsActions.forgetRoom>
|
||||
)[] = [
|
||||
RoomsActions.updateRoom({
|
||||
roomId: room.id,
|
||||
changes: roomChanges
|
||||
})
|
||||
];
|
||||
const isCurrentUserBanned = hasRoomBanForUser(
|
||||
bans,
|
||||
currentUser,
|
||||
getPersistedCurrentUserId()
|
||||
);
|
||||
const isCurrentUserBanned = hasRoomBanForUser(bans, currentUser, getPersistedCurrentUserId());
|
||||
|
||||
if (currentRoom?.id === room.id) {
|
||||
actions.push(UsersActions.loadBansSuccess({ bans }));
|
||||
@@ -684,8 +670,7 @@ export class RoomStateSyncEffects {
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
const settings = event.settings as Partial<RoomSettings> | undefined;
|
||||
|
||||
if (!room || !settings)
|
||||
return EMPTY;
|
||||
if (!room || !settings) return EMPTY;
|
||||
|
||||
return of(
|
||||
RoomsActions.updateRoom({
|
||||
@@ -699,7 +684,9 @@ export class RoomStateSyncEffects {
|
||||
hasPassword:
|
||||
typeof settings.hasPassword === 'boolean'
|
||||
? settings.hasPassword
|
||||
: (typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password),
|
||||
: typeof room.hasPassword === 'boolean'
|
||||
? room.hasPassword
|
||||
: !!room.password,
|
||||
maxUsers: settings.maxUsers ?? room.maxUsers
|
||||
}
|
||||
})
|
||||
@@ -712,17 +699,13 @@ export class RoomStateSyncEffects {
|
||||
const permissions = event.permissions as Partial<RoomPermissions> | undefined;
|
||||
const incomingRoom = event.room as Partial<Room> | undefined;
|
||||
|
||||
if (!room || (!permissions && !incomingRoom))
|
||||
return EMPTY;
|
||||
if (!room || (!permissions && !incomingRoom)) return EMPTY;
|
||||
|
||||
return of(
|
||||
RoomsActions.updateRoom({
|
||||
roomId: room.id,
|
||||
changes: {
|
||||
permissions: permissions
|
||||
? { ...(room.permissions || {}),
|
||||
...permissions } as RoomPermissions
|
||||
: room.permissions,
|
||||
permissions: permissions ? ({ ...(room.permissions || {}), ...permissions } as RoomPermissions) : room.permissions,
|
||||
roles: Array.isArray(incomingRoom?.roles) ? incomingRoom.roles : room.roles,
|
||||
roleAssignments: Array.isArray(incomingRoom?.roleAssignments) ? incomingRoom.roleAssignments : room.roleAssignments,
|
||||
channelPermissions: Array.isArray(incomingRoom?.channelPermissions) ? incomingRoom.channelPermissions : room.channelPermissions,
|
||||
@@ -732,12 +715,7 @@ export class RoomStateSyncEffects {
|
||||
);
|
||||
}
|
||||
|
||||
private handleChannelsUpdate(
|
||||
event: ChatEvent,
|
||||
currentRoom: Room | null,
|
||||
savedRooms: Room[],
|
||||
activeChannelId: string
|
||||
): Action[] {
|
||||
private handleChannelsUpdate(event: ChatEvent, currentRoom: Room | null, savedRooms: Room[], activeChannelId: string): Action[] {
|
||||
const roomId = typeof event.roomId === 'string' ? event.roomId : currentRoom?.id;
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
const channels = Array.isArray(event.channels) ? event.channels : null;
|
||||
@@ -754,8 +732,7 @@ export class RoomStateSyncEffects {
|
||||
];
|
||||
|
||||
if (!channels.some((channel) => channel.id === activeChannelId)) {
|
||||
const fallbackChannelId = channels.find((channel) => channel.type === 'text')?.id
|
||||
?? 'general';
|
||||
const fallbackChannelId = channels.find((channel) => channel.type === 'text')?.id ?? 'general';
|
||||
|
||||
actions.push(RoomsActions.selectChannel({ channelId: fallbackChannelId }));
|
||||
}
|
||||
@@ -769,8 +746,7 @@ export class RoomStateSyncEffects {
|
||||
const roomId = typeof event.roomId === 'string' ? event.roomId : currentRoom?.id;
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
|
||||
if (!room)
|
||||
return EMPTY;
|
||||
if (!room) return EMPTY;
|
||||
|
||||
const remoteUpdated = event.iconUpdatedAt || 0;
|
||||
const localUpdated = room.iconUpdatedAt || 0;
|
||||
@@ -789,8 +765,7 @@ export class RoomStateSyncEffects {
|
||||
const roomId = typeof event.roomId === 'string' ? event.roomId : currentRoom?.id;
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
|
||||
if (!room)
|
||||
return EMPTY;
|
||||
if (!room) return EMPTY;
|
||||
|
||||
if (event.fromPeerId) {
|
||||
this.webrtc.sendToPeer(event.fromPeerId, {
|
||||
@@ -809,20 +784,17 @@ export class RoomStateSyncEffects {
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
const senderId = event.fromPeerId;
|
||||
|
||||
if (!room || typeof event.icon !== 'string' || !senderId)
|
||||
return EMPTY;
|
||||
if (!room || typeof event.icon !== 'string' || !senderId) return this.handleSearchResultIconData(event, roomId);
|
||||
|
||||
return this.store.select(selectAllUsers).pipe(
|
||||
map((users) => users.find((user) => user.id === senderId)),
|
||||
mergeMap((sender) => {
|
||||
if (!sender)
|
||||
return EMPTY;
|
||||
if (!sender) return EMPTY;
|
||||
|
||||
const isOwner = room.hostId === sender.id;
|
||||
const canByRole = resolveRoomPermission(room, sender, 'manageIcon');
|
||||
|
||||
if (!isOwner && !canByRole)
|
||||
return EMPTY;
|
||||
if (!isOwner && !canByRole) return EMPTY;
|
||||
|
||||
const updates: Partial<Room> = {
|
||||
icon: event.icon,
|
||||
@@ -830,23 +802,63 @@ export class RoomStateSyncEffects {
|
||||
};
|
||||
|
||||
this.db.updateRoom(room.id, updates);
|
||||
return of(RoomsActions.updateRoom({ roomId: room.id,
|
||||
changes: updates }));
|
||||
this.webrtc.sendRawMessage({
|
||||
type: 'server_icon_available',
|
||||
serverId: room.id,
|
||||
iconUpdatedAt: updates.iconUpdatedAt
|
||||
});
|
||||
return of(RoomsActions.updateRoom({ roomId: room.id, changes: updates }));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleSearchResultIconData(event: ChatEvent, roomId: string | undefined) {
|
||||
if (!roomId || typeof event.icon !== 'string') {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const iconUpdatedAt = event.iconUpdatedAt || Date.now();
|
||||
|
||||
return of(
|
||||
RoomsActions.receiveSearchServerIcon({
|
||||
roomId,
|
||||
icon: event.icon,
|
||||
iconUpdatedAt
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private queueServerIconSyncRequest(peerId: string, serverId: string): void {
|
||||
const pendingServerIds = this.pendingServerIconRequestsByPeer.get(peerId) ?? new Set<string>();
|
||||
|
||||
pendingServerIds.add(serverId);
|
||||
this.pendingServerIconRequestsByPeer.set(peerId, pendingServerIds);
|
||||
this.scheduleServerIconSyncRequests(peerId, serverId);
|
||||
}
|
||||
|
||||
private scheduleServerIconSyncRequests(peerId: string, serverId: string): void {
|
||||
for (const delayMs of SERVER_ICON_SYNC_REQUEST_DELAYS_MS) {
|
||||
setTimeout(() => {
|
||||
this.sendServerIconSyncRequest(peerId, serverId);
|
||||
}, delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
private sendServerIconSyncRequest(peerId: string, serverId: string): void {
|
||||
this.webrtc.sendToPeer(peerId, {
|
||||
type: 'server-icon-request',
|
||||
roomId: serverId
|
||||
});
|
||||
}
|
||||
|
||||
// ── Internal helpers ───────────────────────────────────────────
|
||||
|
||||
private syncBansToLocalRoom(roomId: string, bans: BanEntry[]) {
|
||||
return from(this.db.getBansForRoom(roomId)).pipe(
|
||||
switchMap((localBans) => {
|
||||
const nextIds = new Set(bans.map((ban) => ban.oderId));
|
||||
const removals = localBans
|
||||
.filter((ban) => !nextIds.has(ban.oderId))
|
||||
.map((ban) => this.db.removeBan(ban.oderId));
|
||||
const saves = bans.map((ban) => this.db.saveBan({ ...ban,
|
||||
roomId }));
|
||||
const removals = localBans.filter((ban) => !nextIds.has(ban.oderId)).map((ban) => this.db.removeBan(ban.oderId));
|
||||
const saves = bans.map((ban) => this.db.saveBan({ ...ban, roomId }));
|
||||
|
||||
return from(Promise.all([...removals, ...saves]));
|
||||
})
|
||||
|
||||
@@ -72,6 +72,7 @@ export const RoomsActions = createActionGroup({
|
||||
'Update Server Icon': props<{ roomId: string; icon: string }>(),
|
||||
'Update Server Icon Success': props<{ roomId: string; icon: string; iconUpdatedAt: number }>(),
|
||||
'Update Server Icon Failure': props<{ error: string }>(),
|
||||
'Receive Search Server Icon': props<{ roomId: string; icon: string; iconUpdatedAt: number }>(),
|
||||
|
||||
'Set Current Room': props<{ room: Room }>(),
|
||||
'Clear Current Room': emptyProps(),
|
||||
|
||||
@@ -229,6 +229,8 @@ export class RoomsEffects {
|
||||
isPrivate: room.isPrivate,
|
||||
userCount: 1,
|
||||
maxUsers: room.maxUsers || 50,
|
||||
icon: room.icon,
|
||||
iconUpdatedAt: room.iconUpdatedAt,
|
||||
tags: [],
|
||||
channels: room.channels ?? defaultChannels()
|
||||
}, endpoint ? {
|
||||
@@ -288,6 +290,8 @@ export class RoomsEffects {
|
||||
const resolvedRoom: Room = {
|
||||
...room,
|
||||
isPrivate: typeof serverInfo?.isPrivate === 'boolean' ? serverInfo.isPrivate : room.isPrivate,
|
||||
icon: serverInfo?.icon ?? room.icon,
|
||||
iconUpdatedAt: serverInfo?.iconUpdatedAt ?? room.iconUpdatedAt,
|
||||
channels: resolveRoomChannels(room.channels, serverInfo?.channels),
|
||||
slowModeInterval: serverInfo?.slowModeInterval ?? room.slowModeInterval,
|
||||
roles: serverInfo?.roles ?? room.roles,
|
||||
@@ -309,6 +313,8 @@ export class RoomsEffects {
|
||||
roles: resolvedRoom.roles,
|
||||
roleAssignments: resolvedRoom.roleAssignments,
|
||||
channelPermissions: resolvedRoom.channelPermissions,
|
||||
icon: resolvedRoom.icon,
|
||||
iconUpdatedAt: resolvedRoom.iconUpdatedAt,
|
||||
hasPassword: resolvedRoom.hasPassword,
|
||||
isPrivate: resolvedRoom.isPrivate
|
||||
});
|
||||
@@ -337,6 +343,8 @@ export class RoomsEffects {
|
||||
createdAt: Date.now(),
|
||||
userCount: 1,
|
||||
maxUsers: 50,
|
||||
icon: serverInfo.icon,
|
||||
iconUpdatedAt: serverInfo.iconUpdatedAt,
|
||||
channels: resolveRoomChannels(undefined, serverInfo.channels),
|
||||
slowModeInterval: serverInfo.slowModeInterval,
|
||||
roles: serverInfo.roles,
|
||||
@@ -372,6 +380,8 @@ export class RoomsEffects {
|
||||
createdAt: serverData.createdAt || Date.now(),
|
||||
userCount: serverData.userCount,
|
||||
maxUsers: serverData.maxUsers,
|
||||
icon: serverData.icon,
|
||||
iconUpdatedAt: serverData.iconUpdatedAt,
|
||||
channels: resolveRoomChannels(undefined, serverData.channels),
|
||||
slowModeInterval: serverData.slowModeInterval,
|
||||
roles: serverData.roles,
|
||||
@@ -557,6 +567,8 @@ export class RoomsEffects {
|
||||
hasPassword: !!serverData.hasPassword,
|
||||
isPrivate: serverData.isPrivate,
|
||||
maxUsers: serverData.maxUsers,
|
||||
icon: serverData.icon ?? room.icon,
|
||||
iconUpdatedAt: serverData.iconUpdatedAt ?? room.iconUpdatedAt,
|
||||
channels: resolveRoomChannels(room.channels, serverData.channels),
|
||||
slowModeInterval: serverData.slowModeInterval ?? room.slowModeInterval,
|
||||
roles: serverData.roles ?? room.roles,
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
Room,
|
||||
BanEntry,
|
||||
User
|
||||
} from '../../shared-kernel';
|
||||
import { Room, BanEntry, User } from '../../shared-kernel';
|
||||
import { resolveLegacyRole, resolveRoomPermission } from '../../domains/access-control';
|
||||
import { findRoomMember } from './room-members.helpers';
|
||||
import { ROOM_URL_PATTERN } from '../../core/constants';
|
||||
|
||||
/** Build a minimal User object from signaling payload. */
|
||||
export function buildSignalingUser(
|
||||
data: { oderId: string; displayName?: string; status?: string },
|
||||
extras: Record<string, unknown> = {}
|
||||
) {
|
||||
export function buildSignalingUser(data: { oderId: string; displayName?: string; status?: string }, extras: Record<string, unknown> = {}) {
|
||||
const displayName = data.displayName?.trim() || 'User';
|
||||
const rawStatus = ([
|
||||
'online',
|
||||
'away',
|
||||
'busy',
|
||||
'offline'
|
||||
] as const).includes(data.status as 'online')
|
||||
? data.status as 'online' | 'away' | 'busy' | 'offline'
|
||||
const rawStatus = (['online', 'away', 'busy', 'offline'] as const).includes(data.status as 'online')
|
||||
? (data.status as 'online' | 'away' | 'busy' | 'offline')
|
||||
: 'online';
|
||||
// 'offline' from the server means the user chose Invisible;
|
||||
// display them as disconnected to other users.
|
||||
const status = rawStatus === 'offline' ? 'disconnected' as const : rawStatus;
|
||||
const status = rawStatus === 'offline' ? ('disconnected' as const) : rawStatus;
|
||||
|
||||
return {
|
||||
oderId: data.oderId,
|
||||
@@ -43,8 +31,7 @@ export function buildSignalingUser(
|
||||
export function buildKnownUserExtras(room: Room | null, identifier: string): Record<string, unknown> {
|
||||
const knownMember = room ? findRoomMember(room.members ?? [], identifier) : undefined;
|
||||
|
||||
if (!knownMember)
|
||||
return {};
|
||||
if (!knownMember) return {};
|
||||
|
||||
return {
|
||||
username: knownMember.username,
|
||||
@@ -60,10 +47,7 @@ export function buildKnownUserExtras(room: Room | null, identifier: string): Rec
|
||||
}
|
||||
|
||||
/** Returns true when the message's server ID does not match the viewed server. */
|
||||
export function isWrongServer(
|
||||
msgServerId: string | undefined,
|
||||
viewedServerId: string | undefined
|
||||
): boolean {
|
||||
export function isWrongServer(msgServerId: string | undefined, viewedServerId: string | undefined): boolean {
|
||||
return !!(msgServerId && viewedServerId && msgServerId !== viewedServerId);
|
||||
}
|
||||
|
||||
@@ -110,9 +94,7 @@ export function reconcileRoomSnapshotChannels(
|
||||
}
|
||||
|
||||
if (hasPersistedChannels(cachedChannels) && hasPersistedChannels(incomingChannels)) {
|
||||
return incomingChannels.length >= cachedChannels.length
|
||||
? incomingChannels
|
||||
: cachedChannels;
|
||||
return incomingChannels.length >= cachedChannels.length ? incomingChannels : cachedChannels;
|
||||
}
|
||||
|
||||
if (hasPersistedChannels(incomingChannels)) {
|
||||
@@ -122,10 +104,7 @@ export function reconcileRoomSnapshotChannels(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveTextChannelId(
|
||||
channels: Room['channels'] | undefined,
|
||||
preferredChannelId?: string | null
|
||||
): string | null {
|
||||
export function resolveTextChannelId(channels: Room['channels'] | undefined, preferredChannelId?: string | null): string | null {
|
||||
const textChannels = (channels ?? []).filter((channel) => channel.type === 'text');
|
||||
|
||||
if (preferredChannelId && textChannels.some((channel) => channel.id === preferredChannelId)) {
|
||||
@@ -136,11 +115,9 @@ export function resolveTextChannelId(
|
||||
}
|
||||
|
||||
export function resolveRoom(roomId: string | undefined, currentRoom: Room | null, savedRooms: Room[]): Room | null {
|
||||
if (!roomId)
|
||||
return currentRoom;
|
||||
if (!roomId) return currentRoom;
|
||||
|
||||
if (currentRoom?.id === roomId)
|
||||
return currentRoom;
|
||||
if (currentRoom?.id === roomId) return currentRoom;
|
||||
|
||||
return savedRooms.find((room) => room.id === roomId) ?? null;
|
||||
}
|
||||
@@ -152,9 +129,7 @@ export function sanitizeRoomSnapshot(room: Partial<Room>): Partial<Room> {
|
||||
topic: typeof room.topic === 'string' ? room.topic : undefined,
|
||||
hostId: typeof room.hostId === 'string' ? room.hostId : undefined,
|
||||
hasPassword:
|
||||
typeof room.hasPassword === 'boolean'
|
||||
? room.hasPassword
|
||||
: (typeof room.password === 'string' ? room.password.trim().length > 0 : undefined),
|
||||
typeof room.hasPassword === 'boolean' ? room.hasPassword : typeof room.password === 'string' ? room.password.trim().length > 0 : undefined,
|
||||
isPrivate: typeof room.isPrivate === 'boolean' ? room.isPrivate : undefined,
|
||||
maxUsers: typeof room.maxUsers === 'number' ? room.maxUsers : undefined,
|
||||
icon: typeof room.icon === 'string' ? room.icon : undefined,
|
||||
@@ -173,8 +148,7 @@ export function sanitizeRoomSnapshot(room: Partial<Room>): Partial<Room> {
|
||||
}
|
||||
|
||||
export function normalizeIncomingBans(roomId: string, bans: unknown): BanEntry[] {
|
||||
if (!Array.isArray(bans))
|
||||
return [];
|
||||
if (!Array.isArray(bans)) return [];
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
@@ -225,6 +199,9 @@ export interface RoomPresenceSignalingMessage {
|
||||
oderId?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
fromUserId?: string;
|
||||
icon?: string;
|
||||
iconUpdatedAt?: number;
|
||||
profileUpdatedAt?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,7 @@ import { normalizeRoomAccessControl } from '../../domains/access-control';
|
||||
import { type ServerInfo } from '../../domains/server-directory';
|
||||
import { RoomsActions } from './rooms.actions';
|
||||
import { defaultChannels } from './room-channels.defaults';
|
||||
import {
|
||||
isChannelNameTaken,
|
||||
normalizeChannelName,
|
||||
normalizeRoomChannels
|
||||
} from './room-channels.rules';
|
||||
import { isChannelNameTaken, normalizeChannelName, normalizeRoomChannels } from './room-channels.rules';
|
||||
import { pruneRoomMembers } from './room-members.helpers';
|
||||
|
||||
/** Deduplicate rooms by id, keeping the last occurrence */
|
||||
@@ -35,9 +31,7 @@ function enrichRoom(room: Room): Room {
|
||||
function resolveActiveTextChannelId(channels: Room['channels'], currentActiveChannelId: string): string {
|
||||
const textChannels = (channels ?? []).filter((channel) => channel.type === 'text');
|
||||
|
||||
return textChannels.some((channel) => channel.id === currentActiveChannelId)
|
||||
? currentActiveChannelId
|
||||
: (textChannels[0]?.id ?? 'general');
|
||||
return textChannels.some((channel) => channel.id === currentActiveChannelId) ? currentActiveChannelId : (textChannels[0]?.id ?? 'general');
|
||||
}
|
||||
|
||||
function getDefaultTextChannelId(room: Room): string {
|
||||
@@ -47,7 +41,7 @@ function getDefaultTextChannelId(room: Room): string {
|
||||
/** Upsert a room into a saved-rooms list (add or replace by id) */
|
||||
function upsertRoom(savedRooms: Room[], room: Room): Room[] {
|
||||
const normalizedRoom = enrichRoom(room);
|
||||
const idx = savedRooms.findIndex(existingRoom => existingRoom.id === room.id);
|
||||
const idx = savedRooms.findIndex((existingRoom) => existingRoom.id === room.id);
|
||||
|
||||
if (idx >= 0) {
|
||||
const updated = [...savedRooms];
|
||||
@@ -250,8 +244,7 @@ export const roomsReducer = createReducer(
|
||||
})),
|
||||
|
||||
on(RoomsActions.updateRoomSettingsSuccess, (state, { roomId, settings }) => {
|
||||
const baseRoom = state.savedRooms.find((savedRoom) => savedRoom.id === roomId)
|
||||
|| (state.currentRoom?.id === roomId ? state.currentRoom : null);
|
||||
const baseRoom = state.savedRooms.find((savedRoom) => savedRoom.id === roomId) || (state.currentRoom?.id === roomId ? state.currentRoom : null);
|
||||
|
||||
if (!baseRoom) {
|
||||
return {
|
||||
@@ -270,9 +263,9 @@ export const roomsReducer = createReducer(
|
||||
hasPassword:
|
||||
typeof settings.hasPassword === 'boolean'
|
||||
? settings.hasPassword
|
||||
: (typeof settings.password === 'string'
|
||||
: typeof settings.password === 'string'
|
||||
? settings.password.trim().length > 0
|
||||
: baseRoom.hasPassword),
|
||||
: baseRoom.hasPassword,
|
||||
maxUsers: settings.maxUsers
|
||||
});
|
||||
|
||||
@@ -330,33 +323,28 @@ export const roomsReducer = createReducer(
|
||||
|
||||
// Update room
|
||||
on(RoomsActions.updateRoom, (state, { roomId, changes }) => {
|
||||
const baseRoom = state.savedRooms.find((savedRoom) => savedRoom.id === roomId)
|
||||
|| (state.currentRoom?.id === roomId ? state.currentRoom : null);
|
||||
const baseRoom = state.savedRooms.find((savedRoom) => savedRoom.id === roomId) || (state.currentRoom?.id === roomId ? state.currentRoom : null);
|
||||
|
||||
if (!baseRoom)
|
||||
return state;
|
||||
if (!baseRoom) return state;
|
||||
|
||||
const updatedRoom = enrichRoom({ ...baseRoom,
|
||||
...changes });
|
||||
const updatedRoom = enrichRoom({ ...baseRoom, ...changes });
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentRoom: state.currentRoom?.id === roomId ? updatedRoom : state.currentRoom,
|
||||
savedRooms: upsertRoom(state.savedRooms, updatedRoom),
|
||||
activeChannelId: state.currentRoom?.id === roomId
|
||||
? resolveActiveTextChannelId(updatedRoom.channels, state.activeChannelId)
|
||||
: state.activeChannelId
|
||||
activeChannelId:
|
||||
state.currentRoom?.id === roomId ? resolveActiveTextChannelId(updatedRoom.channels, state.activeChannelId) : state.activeChannelId
|
||||
};
|
||||
}),
|
||||
|
||||
// Update server icon success
|
||||
on(RoomsActions.updateServerIconSuccess, (state, { roomId, icon, iconUpdatedAt }) => {
|
||||
if (state.currentRoom?.id !== roomId)
|
||||
return state;
|
||||
const baseRoom = state.savedRooms.find((savedRoom) => savedRoom.id === roomId) || (state.currentRoom?.id === roomId ? state.currentRoom : null);
|
||||
|
||||
const updatedRoom = enrichRoom({ ...state.currentRoom,
|
||||
icon,
|
||||
iconUpdatedAt });
|
||||
if (!baseRoom) return state;
|
||||
|
||||
const updatedRoom = enrichRoom({ ...baseRoom, icon, iconUpdatedAt });
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -365,13 +353,18 @@ export const roomsReducer = createReducer(
|
||||
};
|
||||
}),
|
||||
|
||||
on(RoomsActions.receiveSearchServerIcon, (state, { roomId, icon, iconUpdatedAt }) => ({
|
||||
...state,
|
||||
searchResults: state.searchResults.map((server) =>
|
||||
server.id === roomId && (!server.icon || (server.iconUpdatedAt ?? 0) < iconUpdatedAt) ? { ...server, icon, iconUpdatedAt } : server
|
||||
)
|
||||
})),
|
||||
|
||||
// Receive room update
|
||||
on(RoomsActions.receiveRoomUpdate, (state, { room }) => {
|
||||
if (!state.currentRoom)
|
||||
return state;
|
||||
if (!state.currentRoom) return state;
|
||||
|
||||
const updatedRoom = enrichRoom({ ...state.currentRoom,
|
||||
...room });
|
||||
const updatedRoom = enrichRoom({ ...state.currentRoom, ...room });
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -410,27 +403,17 @@ export const roomsReducer = createReducer(
|
||||
})),
|
||||
|
||||
on(RoomsActions.addChannel, (state, { channel }) => {
|
||||
if (!state.currentRoom)
|
||||
return state;
|
||||
if (!state.currentRoom) return state;
|
||||
|
||||
const existing = state.currentRoom.channels || defaultChannels();
|
||||
const normalizedName = normalizeChannelName(channel.name);
|
||||
|
||||
if (
|
||||
!normalizedName
|
||||
|| existing.some((entry) => entry.id === channel.id)
|
||||
|| isChannelNameTaken(existing, normalizedName, channel.type)
|
||||
) {
|
||||
if (!normalizedName || existing.some((entry) => entry.id === channel.id) || isChannelNameTaken(existing, normalizedName, channel.type)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const updatedChannels = [
|
||||
...existing,
|
||||
{ ...channel,
|
||||
name: normalizedName }
|
||||
];
|
||||
const updatedRoom = { ...state.currentRoom,
|
||||
channels: updatedChannels };
|
||||
const updatedChannels = [...existing, { ...channel, name: normalizedName }];
|
||||
const updatedRoom = { ...state.currentRoom, channels: updatedChannels };
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -441,13 +424,11 @@ export const roomsReducer = createReducer(
|
||||
}),
|
||||
|
||||
on(RoomsActions.removeChannel, (state, { channelId }) => {
|
||||
if (!state.currentRoom)
|
||||
return state;
|
||||
if (!state.currentRoom) return state;
|
||||
|
||||
const existing = state.currentRoom.channels || defaultChannels();
|
||||
const updatedChannels = existing.filter(channel => channel.id !== channelId);
|
||||
const updatedRoom = { ...state.currentRoom,
|
||||
channels: updatedChannels };
|
||||
const updatedChannels = existing.filter((channel) => channel.id !== channelId);
|
||||
const updatedRoom = { ...state.currentRoom, channels: updatedChannels };
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -458,8 +439,7 @@ export const roomsReducer = createReducer(
|
||||
}),
|
||||
|
||||
on(RoomsActions.renameChannel, (state, { channelId, name }) => {
|
||||
if (!state.currentRoom)
|
||||
return state;
|
||||
if (!state.currentRoom) return state;
|
||||
|
||||
const existing = state.currentRoom.channels || defaultChannels();
|
||||
const normalizedName = normalizeChannelName(name);
|
||||
@@ -469,10 +449,8 @@ export const roomsReducer = createReducer(
|
||||
return state;
|
||||
}
|
||||
|
||||
const updatedChannels = existing.map(channel => channel.id === channelId ? { ...channel,
|
||||
name: normalizedName } : channel);
|
||||
const updatedRoom = { ...state.currentRoom,
|
||||
channels: updatedChannels };
|
||||
const updatedChannels = existing.map((channel) => (channel.id === channelId ? { ...channel, name: normalizedName } : channel));
|
||||
const updatedRoom = { ...state.currentRoom, channels: updatedChannels };
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
Reference in New Issue
Block a user