Files
Toju/toju-app/src/app/features/settings/settings-modal/permissions-settings/permissions-settings.component.ts
Myx 31962aeb1a fix: restore build and stabilize E2E cross-signal behavior
Revert the automated member-ordering pass that broke Angular field init
(TS2729) and disable that rule until a safe reorder strategy exists.
Fix modal/confirm dialog i18n defaults via template fallbacks, search all
active endpoints (including offline), register foreign rooms with actor
owner IDs, sync profile display names from avatar summaries, and guard
dm-chat when a private call converts to a group conversation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 12:16:40 +02:00

410 lines
11 KiB
TypeScript

/* 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<Room | null>(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<RoomRole[]>(() => 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;
}
}