feat: Allow admin to create new text channels

This commit is contained in:
2026-03-30 01:25:56 +02:00
parent 109402cdd6
commit 83694570e3
24 changed files with 563 additions and 64 deletions

View File

@@ -8,7 +8,10 @@ import {
createEffect,
ofType
} from '@ngrx/effects';
import { Store } from '@ngrx/store';
import {
Action,
Store
} from '@ngrx/store';
import {
of,
from,
@@ -29,7 +32,11 @@ import { RoomsActions } from './rooms.actions';
import { UsersActions } from '../users/users.actions';
import { MessagesActions } from '../messages/messages.actions';
import { selectCurrentUser, selectAllUsers } from '../users/users.selectors';
import { selectCurrentRoom, selectSavedRooms } from './rooms.selectors';
import {
selectActiveChannelId,
selectCurrentRoom,
selectSavedRooms
} from './rooms.selectors';
import { RealtimeSessionFacade } from '../../core/realtime';
import { DatabaseService } from '../../infrastructure/persistence';
import {
@@ -54,6 +61,7 @@ import {
removeRoomMember,
transferRoomOwnership
} from './room-members.helpers';
import { defaultChannels } from './room-channels.defaults';
/** Build a minimal User object from signaling payload. */
function buildSignalingUser(
@@ -224,6 +232,7 @@ export class RoomsEffects {
createdAt: Date.now(),
userCount: 1,
maxUsers: 50,
channels: defaultChannels(),
sourceId: endpoint?.id,
sourceName: endpoint?.name,
sourceUrl: endpoint?.url
@@ -246,7 +255,8 @@ export class RoomsEffects {
isPrivate: room.isPrivate,
userCount: 1,
maxUsers: room.maxUsers || 50,
tags: []
tags: [],
channels: room.channels ?? defaultChannels()
}, endpoint ? {
sourceId: endpoint.id,
sourceUrl: endpoint.url
@@ -290,6 +300,7 @@ export class RoomsEffects {
const resolvedRoom: Room = {
...room,
isPrivate: typeof serverInfo?.isPrivate === 'boolean' ? serverInfo.isPrivate : room.isPrivate,
channels: Array.isArray(serverInfo?.channels) ? serverInfo.channels : room.channels,
sourceId: serverInfo?.sourceId ?? room.sourceId,
sourceName: serverInfo?.sourceName ?? room.sourceName,
sourceUrl: serverInfo?.sourceUrl ?? room.sourceUrl,
@@ -303,6 +314,7 @@ export class RoomsEffects {
sourceId: resolvedRoom.sourceId,
sourceName: resolvedRoom.sourceName,
sourceUrl: resolvedRoom.sourceUrl,
channels: resolvedRoom.channels,
hasPassword: resolvedRoom.hasPassword,
isPrivate: resolvedRoom.isPrivate
});
@@ -322,6 +334,7 @@ export class RoomsEffects {
createdAt: Date.now(),
userCount: 1,
maxUsers: 50,
channels: Array.isArray(serverInfo.channels) ? serverInfo.channels : undefined,
sourceId: serverInfo.sourceId,
sourceName: serverInfo.sourceName,
sourceUrl: serverInfo.sourceUrl
@@ -346,6 +359,7 @@ export class RoomsEffects {
createdAt: serverData.createdAt || Date.now(),
userCount: serverData.userCount,
maxUsers: serverData.maxUsers,
channels: Array.isArray(serverData.channels) ? serverData.channels : undefined,
sourceId: serverData.sourceId,
sourceName: serverData.sourceName,
sourceUrl: serverData.sourceUrl
@@ -679,6 +693,50 @@ export class RoomsEffects {
{ dispatch: false }
);
syncChannelChanges$ = createEffect(
() =>
this.actions$.pipe(
ofType(RoomsActions.addChannel, RoomsActions.removeChannel, RoomsActions.renameChannel),
withLatestFrom(
this.store.select(selectCurrentUser),
this.store.select(selectCurrentRoom)
),
tap(([, currentUser, currentRoom]) => {
if (!currentUser || !currentRoom) {
return;
}
const role = this.getUserRoleForRoom(currentRoom, currentUser, currentRoom);
if (!this.canManageChannelsInRoom(currentRoom, currentUser, currentRoom, role)) {
return;
}
const channels = currentRoom.channels ?? defaultChannels();
this.db.updateRoom(currentRoom.id, { channels });
this.webrtc.broadcastMessage({
type: 'channels-update',
roomId: currentRoom.id,
channels
});
this.serverDirectory.updateServer(currentRoom.id, {
currentOwnerId: currentUser.id,
actingRole: role ?? undefined,
channels
}, {
sourceId: currentRoom.sourceId,
sourceUrl: currentRoom.sourceUrl
}).subscribe({
error: () => {}
});
})
),
{ dispatch: false }
);
/** Updates room permission grants (host-only) and broadcasts to peers. */
updateRoomPermissions$ = createEffect(() =>
this.actions$.pipe(
@@ -953,14 +1011,16 @@ export class RoomsEffects {
this.store.select(selectCurrentRoom),
this.store.select(selectSavedRooms),
this.store.select(selectAllUsers),
this.store.select(selectCurrentUser)
this.store.select(selectCurrentUser),
this.store.select(selectActiveChannelId)
),
mergeMap(([
event,
currentRoom,
savedRooms,
allUsers,
currentUser
currentUser,
activeChannelId
]) => {
switch (event.type) {
case 'voice-state':
@@ -975,6 +1035,8 @@ export class RoomsEffects {
return this.handleRoomSettingsUpdate(event, currentRoom, savedRooms);
case 'room-permissions-update':
return this.handleRoomPermissionsUpdate(event, currentRoom, savedRooms);
case 'channels-update':
return this.handleChannelsUpdate(event, currentRoom, savedRooms, activeChannelId);
case 'server-icon-summary':
return this.handleIconSummary(event, currentRoom, savedRooms);
case 'server-icon-request':
@@ -1261,6 +1323,37 @@ export class RoomsEffects {
);
}
private handleChannelsUpdate(
event: ChatEvent,
currentRoom: Room | null,
savedRooms: Room[],
activeChannelId: string
): Action[] {
const roomId = typeof event.roomId === 'string' ? event.roomId : currentRoom?.id;
const room = this.resolveRoom(roomId, currentRoom, savedRooms);
const channels = Array.isArray(event.channels) ? event.channels : null;
if (!room || !channels) {
return [];
}
const actions: Action[] = [
RoomsActions.updateRoom({
roomId: room.id,
changes: { channels }
})
];
if (!channels.some((channel) => channel.id === activeChannelId)) {
const fallbackChannelId = channels.find((channel) => channel.type === 'text')?.id
?? 'general';
actions.push(RoomsActions.selectChannel({ channelId: fallbackChannelId }));
}
return actions;
}
private handleIconSummary(event: ChatEvent, currentRoom: Room | null, savedRooms: Room[]) {
const roomId = typeof event.roomId === 'string' ? event.roomId : currentRoom?.id;
const room = this.resolveRoom(roomId, currentRoom, savedRooms);
@@ -1540,6 +1633,29 @@ export class RoomsEffects {
|| null;
}
private canManageChannelsInRoom(
room: Room,
currentUser: User,
currentRoom: Room | null,
currentUserRole = this.getUserRoleForRoom(room, currentUser, currentRoom)
): boolean {
if (currentUserRole === 'host') {
return true;
}
const permissions = room.permissions || {};
if (currentUserRole === 'admin' && permissions.adminsManageRooms) {
return true;
}
if (currentUserRole === 'moderator' && permissions.moderatorsManageRooms) {
return true;
}
return false;
}
private getPersistedCurrentUserId(): string | null {
return localStorage.getItem('metoyou_currentUserId');
}