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>
410 lines
11 KiB
TypeScript
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;
|
|
}
|
|
}
|