/* eslint-disable @typescript-eslint/member-ordering */ import { Component, computed, effect, inject, input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { Store } from '@ngrx/store'; import { lucideArrowDown, lucideArrowUp, lucideCheck, lucidePlus, lucideTrash2 } from '@ng-icons/lucide'; import { ChannelPermissionOverride, PermissionState, Room, RoomPermissionKey, RoomRole } from '../../../../shared-kernel'; import { selectCurrentUser } from '../../../../store/users/users.selectors'; import { RoomsActions } from '../../../../store/rooms/rooms.actions'; import { canManageRole, createCustomRoomRole, normalizeRoomAccessControl, removeRole, reorderRoles, resolveRoomPermission, ROOM_PERMISSION_DEFINITIONS, sortRolesForDisplay, withUpdatedRole } from '../../../../domains/access-control'; import { APP_TRANSLATE_IMPORTS, AppI18nService } from '../../../../core/i18n'; import { SelectOnFocusDirective, SubmitOnEnterDirective } from '../../../../shared/directives'; function upsertRoleChannelOverride( overrides: readonly ChannelPermissionOverride[] | undefined, channelId: string, roleId: string, permission: RoomPermissionKey, value: PermissionState ): ChannelPermissionOverride[] { const filteredOverrides = (overrides ?? []).filter( (override) => !(override.channelId === channelId && override.targetType === 'role' && override.targetId === roleId && override.permission === permission) ); if (value === 'inherit') { return filteredOverrides; } return [ ...filteredOverrides, { channelId, targetType: 'role', targetId: roleId, permission, value } ]; } @Component({ selector: 'app-permissions-settings', standalone: true, imports: [ CommonModule, FormsModule, NgIcon, SelectOnFocusDirective, SubmitOnEnterDirective, ...APP_TRANSLATE_IMPORTS ], viewProviders: [ provideIcons({ lucideArrowDown, lucideArrowUp, lucideCheck, lucidePlus, lucideTrash2 }) ], templateUrl: './permissions-settings.component.html' }) export class PermissionsSettingsComponent { private store = inject(Store); private readonly appI18n = inject(AppI18nService); server = input(null); isAdmin = input(false); currentUser = this.store.selectSignal(selectCurrentUser); permissionDefinitions = computed(() => ROOM_PERMISSION_DEFINITIONS.map((definition) => ({ key: definition.key, label: this.appI18n.instant(`permissions.${definition.key}.label`), description: this.appI18n.instant(`permissions.${definition.key}.description`) })) ); permissionStates: PermissionState[] = [ 'inherit', 'allow', 'deny' ]; normalizedServer = computed(() => { const room = this.server(); return room ? normalizeRoomAccessControl(room) : null; }); roles = computed(() => sortRolesForDisplay(this.normalizedServer()?.roles ?? [])); channels = computed(() => this.normalizedServer()?.channels ?? []); canManageRoles = computed(() => { const room = this.normalizedServer(); const user = this.currentUser(); return !!room && !!user && (room.hostId === user.id || room.hostId === user.oderId || resolveRoomPermission(room, user, 'manageRoles')); }); canManageServer = computed(() => { const room = this.normalizedServer(); const user = this.currentUser(); return !!room && !!user && (room.hostId === user.id || room.hostId === user.oderId || resolveRoomPermission(room, user, 'manageServer')); }); selectedRoleKey: string | null = null; selectedChannelKey = ''; roleName = ''; roleColor = '#94a3b8'; constructor() { effect(() => { const room = this.normalizedServer(); const roles = this.roles(); const channels = this.channels(); if (!room || roles.length === 0) { this.selectedRoleKey = null; this.selectedChannelKey = ''; this.roleName = ''; this.roleColor = '#94a3b8'; return; } if (!this.selectedRoleKey || !roles.some((role) => role.id === this.selectedRoleKey)) { this.selectedRoleKey = roles[0]?.id ?? null; } if (!this.selectedChannelKey || !channels.some((channel) => channel.id === this.selectedChannelKey)) { this.selectedChannelKey = channels[0]?.id ?? ''; } const selectedRole = roles.find((role) => role.id === this.selectedRoleKey) ?? null; this.roleName = selectedRole?.name ?? ''; this.roleColor = selectedRole?.color ?? '#94a3b8'; }); } loadPermissions(room: Room): void { const normalizedRoom = normalizeRoomAccessControl(room); this.selectedRoleKey = sortRolesForDisplay(normalizedRoom.roles ?? [])[0]?.id ?? null; this.selectedChannelKey = normalizedRoom.channels?.[0]?.id ?? ''; } selectedRole(): RoomRole | null { return this.roles().find((role) => role.id === this.selectedRoleKey) ?? null; } selectedChannel() { return this.channels().find((channel) => channel.id === this.selectedChannelKey) ?? null; } canEditSelectedRole(): boolean { const room = this.normalizedServer(); const user = this.currentUser(); const role = this.selectedRole(); return !!room && !!user && !!role && canManageRole(room, user, role.id); } canEditSelectedRoleMetadata(): boolean { const role = this.selectedRole(); return !!role && !role.isSystem && this.canEditSelectedRole(); } selectRole(roleId: string): void { this.selectedRoleKey = roleId; } selectChannel(channelId: string): void { this.selectedChannelKey = channelId; } createRole(): void { const room = this.normalizedServer(); if (!room || !this.canManageRoles()) return; const role = createCustomRoomRole(this.appI18n.instant('settings.permissions.newRole'), room.roles ?? []); this.selectedRoleKey = role.id; this.roleName = role.name; this.roleColor = role.color ?? '#94a3b8'; this.store.dispatch( RoomsActions.updateRoomAccessControl({ roomId: room.id, changes: { roles: [...(room.roles ?? []), role] } }) ); } saveRoleDetails(): void { const room = this.normalizedServer(); const role = this.selectedRole(); if (!room || !role || !this.canEditSelectedRoleMetadata()) return; const roles = withUpdatedRole(room.roles ?? [], role.id, { name: this.roleName.trim() || role.name, color: this.roleColor.trim() || undefined }); this.store.dispatch( RoomsActions.updateRoomAccessControl({ roomId: room.id, changes: { roles } }) ); } coercePermissionState(value: string): PermissionState { return value === 'allow' || value === 'deny' || value === 'inherit' ? value : 'inherit'; } permissionStateLabel(state: PermissionState): string { return this.appI18n.instant(`settings.permissions.states.${state}`); } slowModeValue(interval: number | undefined): string { return String(interval ?? 0); } canMoveSelectedRoleUp(): boolean { const role = this.selectedRole(); if (!role || role.isSystem || !this.canEditSelectedRoleMetadata()) { return false; } return ( this.roles() .filter((candidateRole) => !candidateRole.isSystem) .findIndex((candidateRole) => candidateRole.id === role.id) > 0 ); } canMoveSelectedRoleDown(): boolean { const role = this.selectedRole(); const customRoles = this.roles().filter((candidateRole) => !candidateRole.isSystem); if (!role || role.isSystem || !this.canEditSelectedRoleMetadata()) { return false; } const index = customRoles.findIndex((candidateRole) => candidateRole.id === role.id); return index >= 0 && index < customRoles.length - 1; } moveSelectedRole(direction: 'up' | 'down'): void { const room = this.normalizedServer(); const role = this.selectedRole(); if (!room || !role || !this.canEditSelectedRoleMetadata()) return; const orderedRoleIds = this.roles() .filter((candidateRole) => !candidateRole.isSystem) .map((candidateRole) => candidateRole.id); const currentIndex = orderedRoleIds.findIndex((candidateRoleId) => candidateRoleId === role.id); const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; if (currentIndex < 0 || targetIndex < 0 || targetIndex >= orderedRoleIds.length) return; const nextOrderedRoleIds = [...orderedRoleIds]; [nextOrderedRoleIds[currentIndex], nextOrderedRoleIds[targetIndex]] = [nextOrderedRoleIds[targetIndex], nextOrderedRoleIds[currentIndex]]; this.store.dispatch( RoomsActions.updateRoomAccessControl({ roomId: room.id, changes: { roles: reorderRoles(room.roles ?? [], nextOrderedRoleIds) } }) ); } deleteSelectedRole(): void { const room = this.normalizedServer(); const role = this.selectedRole(); if (!room || !role || !this.canEditSelectedRoleMetadata()) return; const nextState = removeRole(room.roles ?? [], room.roleAssignments, role.id); this.store.dispatch( RoomsActions.updateRoomAccessControl({ roomId: room.id, changes: { roles: nextState.roles, roleAssignments: nextState.roleAssignments } }) ); } updateSlowMode(intervalValue: string): void { const room = this.normalizedServer(); if (!room || !this.canManageServer()) return; this.store.dispatch( RoomsActions.updateRoomAccessControl({ roomId: room.id, changes: { slowModeInterval: Number(intervalValue) || 0 } }) ); } permissionState(permission: RoomPermissionKey): PermissionState { return this.selectedRole()?.permissions?.[permission] ?? 'inherit'; } setSelectedRolePermission(permission: RoomPermissionKey, value: PermissionState): void { const room = this.normalizedServer(); const role = this.selectedRole(); if (!room || !role || !this.canEditSelectedRole()) return; const roles = withUpdatedRole(room.roles ?? [], role.id, { permissions: { ...role.permissions, [permission]: value } }); this.store.dispatch( RoomsActions.updateRoomAccessControl({ roomId: room.id, changes: { roles } }) ); } channelOverrideState(permission: RoomPermissionKey): PermissionState { const room = this.normalizedServer(); const role = this.selectedRole(); const channel = this.selectedChannel(); if (!room || !role || !channel) { return 'inherit'; } return ( room.channelPermissions?.find( (override) => override.channelId === channel.id && override.targetType === 'role' && override.targetId === role.id && override.permission === permission )?.value ?? 'inherit' ); } setChannelOverride(permission: RoomPermissionKey, value: PermissionState): void { const room = this.normalizedServer(); const role = this.selectedRole(); const channel = this.selectedChannel(); if (!room || !role || !channel || !this.canEditSelectedRole()) return; this.store.dispatch( RoomsActions.updateRoomAccessControl({ roomId: room.id, changes: { channelPermissions: upsertRoleChannelOverride(room.channelPermissions, channel.id, role.id, permission, value) } }) ); } trackRole(_: number, role: RoomRole): string { return role.id; } }