Move toju-app into own its folder
This commit is contained in:
459
toju-app/src/app/store/rooms/rooms.reducer.ts
Normal file
459
toju-app/src/app/store/rooms/rooms.reducer.ts
Normal file
@@ -0,0 +1,459 @@
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import {
|
||||
Room,
|
||||
RoomSettings,
|
||||
Channel
|
||||
} from '../../shared-kernel';
|
||||
import { type ServerInfo } from '../../domains/server-directory';
|
||||
import { RoomsActions } from './rooms.actions';
|
||||
import { pruneRoomMembers } from './room-members.helpers';
|
||||
|
||||
/** Default channels for a new server */
|
||||
export function defaultChannels(): Channel[] {
|
||||
return [
|
||||
{ id: 'general',
|
||||
name: 'general',
|
||||
type: 'text',
|
||||
position: 0 },
|
||||
{ id: 'random',
|
||||
name: 'random',
|
||||
type: 'text',
|
||||
position: 1 },
|
||||
{ id: 'vc-general',
|
||||
name: 'General',
|
||||
type: 'voice',
|
||||
position: 0 },
|
||||
{ id: 'vc-afk',
|
||||
name: 'AFK',
|
||||
type: 'voice',
|
||||
position: 1 }
|
||||
];
|
||||
}
|
||||
|
||||
/** Deduplicate rooms by id, keeping the last occurrence */
|
||||
function deduplicateRooms(rooms: Room[]): Room[] {
|
||||
const seen = new Map<string, Room>();
|
||||
|
||||
for (const room of rooms) {
|
||||
seen.set(room.id, room);
|
||||
}
|
||||
|
||||
return Array.from(seen.values());
|
||||
}
|
||||
|
||||
/** Normalize room defaults and prune any stale persisted member entries. */
|
||||
function enrichRoom(room: Room): Room {
|
||||
return {
|
||||
...room,
|
||||
hasPassword: typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password,
|
||||
channels: room.channels || defaultChannels(),
|
||||
members: pruneRoomMembers(room.members || [])
|
||||
};
|
||||
}
|
||||
|
||||
/** 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);
|
||||
|
||||
if (idx >= 0) {
|
||||
const updated = [...savedRooms];
|
||||
|
||||
updated[idx] = normalizedRoom;
|
||||
return updated;
|
||||
}
|
||||
|
||||
return [...savedRooms, normalizedRoom];
|
||||
}
|
||||
|
||||
/** State shape for the rooms feature slice. */
|
||||
export interface RoomsState {
|
||||
/** The room the user is currently viewing. */
|
||||
currentRoom: Room | null;
|
||||
/** All rooms persisted locally (joined or created). */
|
||||
savedRooms: Room[];
|
||||
/** Editable settings for the current room. */
|
||||
roomSettings: RoomSettings | null;
|
||||
/** Results returned from the server directory search. */
|
||||
searchResults: ServerInfo[];
|
||||
/** Whether a server directory search is in progress. */
|
||||
isSearching: boolean;
|
||||
/** Whether a connection to a room is being established. */
|
||||
isConnecting: boolean;
|
||||
/** Whether the user is connected to a room. */
|
||||
isConnected: boolean;
|
||||
/** Whether the current room is using locally cached data while reconnecting. */
|
||||
isSignalServerReconnecting: boolean;
|
||||
/** Banner message when the viewed room's signaling endpoint is incompatible. */
|
||||
signalServerCompatibilityError: string | null;
|
||||
/** Whether rooms are being loaded from local storage. */
|
||||
loading: boolean;
|
||||
/** Most recent error message, if any. */
|
||||
error: string | null;
|
||||
/** ID of the currently selected text channel. */
|
||||
activeChannelId: string;
|
||||
}
|
||||
|
||||
export const initialState: RoomsState = {
|
||||
currentRoom: null,
|
||||
savedRooms: [],
|
||||
roomSettings: null,
|
||||
searchResults: [],
|
||||
isSearching: false,
|
||||
isConnecting: false,
|
||||
isConnected: false,
|
||||
isSignalServerReconnecting: false,
|
||||
signalServerCompatibilityError: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
activeChannelId: 'general'
|
||||
};
|
||||
|
||||
export const roomsReducer = createReducer(
|
||||
initialState,
|
||||
|
||||
// Load rooms
|
||||
on(RoomsActions.loadRooms, (state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
error: null
|
||||
})),
|
||||
|
||||
on(RoomsActions.loadRoomsSuccess, (state, { rooms }) => ({
|
||||
...state,
|
||||
savedRooms: deduplicateRooms(rooms.map(enrichRoom)),
|
||||
loading: false
|
||||
})),
|
||||
|
||||
on(RoomsActions.loadRoomsFailure, (state, { error }) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error
|
||||
})),
|
||||
|
||||
// Search servers
|
||||
on(RoomsActions.searchServers, (state) => ({
|
||||
...state,
|
||||
isSearching: true,
|
||||
error: null
|
||||
})),
|
||||
|
||||
on(RoomsActions.searchServersSuccess, (state, { servers }) => ({
|
||||
...state,
|
||||
searchResults: servers,
|
||||
isSearching: false
|
||||
})),
|
||||
|
||||
on(RoomsActions.searchServersFailure, (state, { error }) => ({
|
||||
...state,
|
||||
isSearching: false,
|
||||
error
|
||||
})),
|
||||
|
||||
// Create room
|
||||
on(RoomsActions.createRoom, (state) => ({
|
||||
...state,
|
||||
isConnecting: true,
|
||||
signalServerCompatibilityError: null,
|
||||
error: null
|
||||
})),
|
||||
|
||||
on(RoomsActions.createRoomSuccess, (state, { room }) => {
|
||||
const enriched = enrichRoom(room);
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentRoom: enriched,
|
||||
savedRooms: upsertRoom(state.savedRooms, enriched),
|
||||
isConnecting: false,
|
||||
isSignalServerReconnecting: false,
|
||||
signalServerCompatibilityError: null,
|
||||
isConnected: true,
|
||||
activeChannelId: 'general'
|
||||
};
|
||||
}),
|
||||
|
||||
on(RoomsActions.createRoomFailure, (state, { error }) => ({
|
||||
...state,
|
||||
isConnecting: false,
|
||||
error
|
||||
})),
|
||||
|
||||
// Join room
|
||||
on(RoomsActions.joinRoom, (state) => ({
|
||||
...state,
|
||||
isConnecting: true,
|
||||
signalServerCompatibilityError: null,
|
||||
error: null
|
||||
})),
|
||||
|
||||
on(RoomsActions.joinRoomSuccess, (state, { room }) => {
|
||||
const enriched = enrichRoom(room);
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentRoom: enriched,
|
||||
savedRooms: upsertRoom(state.savedRooms, enriched),
|
||||
isConnecting: false,
|
||||
isSignalServerReconnecting: false,
|
||||
signalServerCompatibilityError: null,
|
||||
isConnected: true,
|
||||
activeChannelId: 'general'
|
||||
};
|
||||
}),
|
||||
|
||||
on(RoomsActions.joinRoomFailure, (state, { error }) => ({
|
||||
...state,
|
||||
isConnecting: false,
|
||||
error
|
||||
})),
|
||||
|
||||
// Leave room
|
||||
on(RoomsActions.leaveRoom, (state) => ({
|
||||
...state,
|
||||
isConnecting: true
|
||||
})),
|
||||
|
||||
on(RoomsActions.leaveRoomSuccess, (state) => ({
|
||||
...state,
|
||||
currentRoom: null,
|
||||
roomSettings: null,
|
||||
isSignalServerReconnecting: false,
|
||||
signalServerCompatibilityError: null,
|
||||
isConnecting: false,
|
||||
isConnected: false
|
||||
})),
|
||||
|
||||
// View server - just switch the viewed room, stay connected
|
||||
on(RoomsActions.viewServer, (state) => ({
|
||||
...state,
|
||||
isConnecting: true,
|
||||
signalServerCompatibilityError: null,
|
||||
error: null
|
||||
})),
|
||||
|
||||
on(RoomsActions.viewServerSuccess, (state, { room }) => {
|
||||
const enriched = enrichRoom(room);
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentRoom: enriched,
|
||||
savedRooms: upsertRoom(state.savedRooms, enriched),
|
||||
isConnecting: false,
|
||||
signalServerCompatibilityError: null,
|
||||
isConnected: true,
|
||||
activeChannelId: 'general'
|
||||
};
|
||||
}),
|
||||
|
||||
// Update room settings
|
||||
on(RoomsActions.updateRoomSettings, (state) => ({
|
||||
...state,
|
||||
error: null
|
||||
})),
|
||||
|
||||
on(RoomsActions.updateRoomSettingsSuccess, (state, { roomId, settings }) => {
|
||||
const baseRoom = state.savedRooms.find((savedRoom) => savedRoom.id === roomId)
|
||||
|| (state.currentRoom?.id === roomId ? state.currentRoom : null);
|
||||
|
||||
if (!baseRoom) {
|
||||
return {
|
||||
...state,
|
||||
roomSettings: state.currentRoom?.id === roomId ? settings : state.roomSettings
|
||||
};
|
||||
}
|
||||
|
||||
const updatedRoom = enrichRoom({
|
||||
...baseRoom,
|
||||
name: settings.name,
|
||||
description: settings.description,
|
||||
topic: settings.topic,
|
||||
isPrivate: settings.isPrivate,
|
||||
password: settings.password === '' ? undefined : (settings.password ?? baseRoom.password),
|
||||
hasPassword:
|
||||
typeof settings.hasPassword === 'boolean'
|
||||
? settings.hasPassword
|
||||
: (typeof settings.password === 'string'
|
||||
? settings.password.trim().length > 0
|
||||
: baseRoom.hasPassword),
|
||||
maxUsers: settings.maxUsers
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
roomSettings: state.currentRoom?.id === roomId ? settings : state.roomSettings,
|
||||
currentRoom: state.currentRoom?.id === roomId ? updatedRoom : state.currentRoom,
|
||||
savedRooms: upsertRoom(state.savedRooms, updatedRoom)
|
||||
};
|
||||
}),
|
||||
|
||||
on(RoomsActions.updateRoomSettingsFailure, (state, { error }) => ({
|
||||
...state,
|
||||
error
|
||||
})),
|
||||
|
||||
// Delete room
|
||||
on(RoomsActions.deleteRoomSuccess, (state, { roomId }) => ({
|
||||
...state,
|
||||
isSignalServerReconnecting: state.currentRoom?.id === roomId ? false : state.isSignalServerReconnecting,
|
||||
signalServerCompatibilityError: state.currentRoom?.id === roomId ? null : state.signalServerCompatibilityError,
|
||||
savedRooms: state.savedRooms.filter((room) => room.id !== roomId),
|
||||
currentRoom: state.currentRoom?.id === roomId ? null : state.currentRoom
|
||||
})),
|
||||
|
||||
// Forget room (local only)
|
||||
on(RoomsActions.forgetRoomSuccess, (state, { roomId }) => ({
|
||||
...state,
|
||||
isSignalServerReconnecting: state.currentRoom?.id === roomId ? false : state.isSignalServerReconnecting,
|
||||
signalServerCompatibilityError: state.currentRoom?.id === roomId ? null : state.signalServerCompatibilityError,
|
||||
savedRooms: state.savedRooms.filter((room) => room.id !== roomId),
|
||||
currentRoom: state.currentRoom?.id === roomId ? null : state.currentRoom
|
||||
})),
|
||||
|
||||
// Set current room
|
||||
on(RoomsActions.setCurrentRoom, (state, { room }) => ({
|
||||
...state,
|
||||
currentRoom: enrichRoom(room),
|
||||
savedRooms: upsertRoom(state.savedRooms, room),
|
||||
isSignalServerReconnecting: false,
|
||||
signalServerCompatibilityError: null,
|
||||
isConnected: true
|
||||
})),
|
||||
|
||||
// Clear current room
|
||||
on(RoomsActions.clearCurrentRoom, (state) => ({
|
||||
...state,
|
||||
currentRoom: null,
|
||||
roomSettings: null,
|
||||
isSignalServerReconnecting: false,
|
||||
signalServerCompatibilityError: null,
|
||||
isConnected: false
|
||||
})),
|
||||
|
||||
// Update room
|
||||
on(RoomsActions.updateRoom, (state, { roomId, changes }) => {
|
||||
const baseRoom = state.savedRooms.find((savedRoom) => savedRoom.id === roomId)
|
||||
|| (state.currentRoom?.id === roomId ? state.currentRoom : null);
|
||||
|
||||
if (!baseRoom)
|
||||
return state;
|
||||
|
||||
const updatedRoom = enrichRoom({ ...baseRoom,
|
||||
...changes });
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentRoom: state.currentRoom?.id === roomId ? updatedRoom : state.currentRoom,
|
||||
savedRooms: upsertRoom(state.savedRooms, updatedRoom)
|
||||
};
|
||||
}),
|
||||
|
||||
// Update server icon success
|
||||
on(RoomsActions.updateServerIconSuccess, (state, { roomId, icon, iconUpdatedAt }) => {
|
||||
if (state.currentRoom?.id !== roomId)
|
||||
return state;
|
||||
|
||||
const updatedRoom = enrichRoom({ ...state.currentRoom,
|
||||
icon,
|
||||
iconUpdatedAt });
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentRoom: updatedRoom,
|
||||
savedRooms: upsertRoom(state.savedRooms, updatedRoom)
|
||||
};
|
||||
}),
|
||||
|
||||
// Receive room update
|
||||
on(RoomsActions.receiveRoomUpdate, (state, { room }) => {
|
||||
if (!state.currentRoom)
|
||||
return state;
|
||||
|
||||
const updatedRoom = enrichRoom({ ...state.currentRoom,
|
||||
...room });
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentRoom: updatedRoom,
|
||||
savedRooms: upsertRoom(state.savedRooms, updatedRoom)
|
||||
};
|
||||
}),
|
||||
|
||||
// Clear search results
|
||||
on(RoomsActions.clearSearchResults, (state) => ({
|
||||
...state,
|
||||
searchResults: []
|
||||
})),
|
||||
|
||||
// Set connecting
|
||||
on(RoomsActions.setConnecting, (state, { isConnecting }) => ({
|
||||
...state,
|
||||
isConnecting
|
||||
})),
|
||||
|
||||
on(RoomsActions.setSignalServerReconnecting, (state, { isReconnecting }) => ({
|
||||
...state,
|
||||
isSignalServerReconnecting: isReconnecting
|
||||
})),
|
||||
|
||||
on(RoomsActions.setSignalServerCompatibilityError, (state, { message }) => ({
|
||||
...state,
|
||||
signalServerCompatibilityError: message
|
||||
})),
|
||||
|
||||
// Channel management
|
||||
on(RoomsActions.selectChannel, (state, { channelId }) => ({
|
||||
...state,
|
||||
activeChannelId: channelId
|
||||
})),
|
||||
|
||||
on(RoomsActions.addChannel, (state, { channel }) => {
|
||||
if (!state.currentRoom)
|
||||
return state;
|
||||
|
||||
const existing = state.currentRoom.channels || defaultChannels();
|
||||
const updatedChannels = [...existing, channel];
|
||||
const updatedRoom = { ...state.currentRoom,
|
||||
channels: updatedChannels };
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentRoom: updatedRoom,
|
||||
savedRooms: upsertRoom(state.savedRooms, updatedRoom)
|
||||
};
|
||||
}),
|
||||
|
||||
on(RoomsActions.removeChannel, (state, { channelId }) => {
|
||||
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 };
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentRoom: updatedRoom,
|
||||
savedRooms: upsertRoom(state.savedRooms, updatedRoom),
|
||||
activeChannelId: state.activeChannelId === channelId ? 'general' : state.activeChannelId
|
||||
};
|
||||
}),
|
||||
|
||||
on(RoomsActions.renameChannel, (state, { channelId, name }) => {
|
||||
if (!state.currentRoom)
|
||||
return state;
|
||||
|
||||
const existing = state.currentRoom.channels || defaultChannels();
|
||||
const updatedChannels = existing.map(channel => channel.id === channelId ? { ...channel,
|
||||
name } : channel);
|
||||
const updatedRoom = { ...state.currentRoom,
|
||||
channels: updatedChannels };
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentRoom: updatedRoom,
|
||||
savedRooms: upsertRoom(state.savedRooms, updatedRoom)
|
||||
};
|
||||
})
|
||||
);
|
||||
Reference in New Issue
Block a user