Change klippy window behavour, Fix user management behavour, clean up search server page
This commit is contained in:
@@ -1,17 +1,25 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
import {
|
||||
Component,
|
||||
effect,
|
||||
inject,
|
||||
input
|
||||
input,
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Actions,
|
||||
ofType
|
||||
} from '@ngrx/effects';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { lucideX } from '@ng-icons/lucide';
|
||||
|
||||
import { Room, BanEntry } from '../../../../core/models/index';
|
||||
import { DatabaseService } from '../../../../core/services/database.service';
|
||||
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
|
||||
import { UsersActions } from '../../../../store/users/users.actions';
|
||||
import { selectBannedUsers } from '../../../../store/users/users.selectors';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bans-settings',
|
||||
@@ -26,16 +34,54 @@ import { selectBannedUsers } from '../../../../store/users/users.selectors';
|
||||
})
|
||||
export class BansSettingsComponent {
|
||||
private store = inject(Store);
|
||||
private actions$ = inject(Actions);
|
||||
private db = inject(DatabaseService);
|
||||
|
||||
/** The currently selected server, passed from the parent. */
|
||||
server = input<Room | null>(null);
|
||||
/** Whether the current user is admin of this server. */
|
||||
isAdmin = input(false);
|
||||
|
||||
bannedUsers = this.store.selectSignal(selectBannedUsers);
|
||||
bannedUsers = signal<BanEntry[]>([]);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const roomId = this.server()?.id;
|
||||
|
||||
if (!roomId) {
|
||||
this.bannedUsers.set([]);
|
||||
return;
|
||||
}
|
||||
|
||||
void this.loadBansForServer(roomId);
|
||||
});
|
||||
|
||||
this.actions$
|
||||
.pipe(
|
||||
ofType(
|
||||
UsersActions.banUserSuccess,
|
||||
UsersActions.unbanUserSuccess,
|
||||
UsersActions.loadBansSuccess,
|
||||
RoomsActions.updateRoom
|
||||
),
|
||||
takeUntilDestroyed()
|
||||
)
|
||||
.subscribe(() => {
|
||||
const roomId = this.server()?.id;
|
||||
|
||||
if (roomId) {
|
||||
void this.loadBansForServer(roomId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unbanUser(ban: BanEntry): void {
|
||||
this.store.dispatch(UsersActions.unbanUser({ oderId: ban.oderId }));
|
||||
this.store.dispatch(UsersActions.unbanUser({ roomId: ban.roomId,
|
||||
oderId: ban.oderId }));
|
||||
}
|
||||
|
||||
private async loadBansForServer(roomId: string): Promise<void> {
|
||||
this.bannedUsers.set(await this.db.getBansForRoom(roomId));
|
||||
}
|
||||
|
||||
formatExpiry(timestamp: number): string {
|
||||
|
||||
@@ -1,59 +1,68 @@
|
||||
@if (server()) {
|
||||
<div class="space-y-3 max-w-xl">
|
||||
@if (membersFiltered().length === 0) {
|
||||
<p class="text-sm text-muted-foreground text-center py-8">No other members online</p>
|
||||
@if (members().length === 0) {
|
||||
<p class="text-sm text-muted-foreground text-center py-8">No other members found for this server</p>
|
||||
} @else {
|
||||
@for (user of membersFiltered(); track user.id) {
|
||||
@for (member of members(); track member.oderId || member.id) {
|
||||
<div class="flex items-center gap-3 p-3 bg-secondary/50 rounded-lg">
|
||||
<app-user-avatar
|
||||
[name]="user.displayName || '?'"
|
||||
[name]="member.displayName || '?'"
|
||||
size="sm"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p class="text-sm font-medium text-foreground truncate">
|
||||
{{ user.displayName }}
|
||||
{{ member.displayName }}
|
||||
</p>
|
||||
@if (user.role === 'host') {
|
||||
@if (member.isOnline) {
|
||||
<span class="text-[10px] bg-emerald-500/20 text-emerald-400 px-1 py-0.5 rounded">Online</span>
|
||||
}
|
||||
@if (member.role === 'host') {
|
||||
<span class="text-[10px] bg-yellow-500/20 text-yellow-400 px-1 py-0.5 rounded">Owner</span>
|
||||
} @else if (user.role === 'admin') {
|
||||
} @else if (member.role === 'admin') {
|
||||
<span class="text-[10px] bg-blue-500/20 text-blue-400 px-1 py-0.5 rounded">Admin</span>
|
||||
} @else if (user.role === 'moderator') {
|
||||
} @else if (member.role === 'moderator') {
|
||||
<span class="text-[10px] bg-green-500/20 text-green-400 px-1 py-0.5 rounded">Mod</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (user.role !== 'host' && isAdmin()) {
|
||||
@if (member.role !== 'host' && isAdmin()) {
|
||||
<div class="flex items-center gap-1">
|
||||
<select
|
||||
[ngModel]="user.role"
|
||||
(ngModelChange)="changeRole(user, $event)"
|
||||
class="text-xs px-2 py-1 bg-secondary rounded border border-border text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
>
|
||||
<option value="member">Member</option>
|
||||
<option value="moderator">Moderator</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<button
|
||||
(click)="kickMember(user)"
|
||||
class="p-1 rounded hover:bg-destructive/20 text-muted-foreground hover:text-destructive transition-colors"
|
||||
title="Kick"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideUserX"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
(click)="banMember(user)"
|
||||
class="p-1 rounded hover:bg-destructive/20 text-muted-foreground hover:text-destructive transition-colors"
|
||||
title="Ban"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideBan"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</button>
|
||||
@if (canChangeRoles()) {
|
||||
<select
|
||||
[ngModel]="member.role"
|
||||
(ngModelChange)="changeRole(member, $event)"
|
||||
class="text-xs px-2 py-1 bg-secondary rounded border border-border text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
>
|
||||
<option value="member">Member</option>
|
||||
<option value="moderator">Moderator</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
}
|
||||
@if (canKickMembers()) {
|
||||
<button
|
||||
(click)="kickMember(member)"
|
||||
class="p-1 rounded hover:bg-destructive/20 text-muted-foreground hover:text-destructive transition-colors"
|
||||
title="Kick"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideUserX"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
@if (canBanMembers()) {
|
||||
<button
|
||||
(click)="banMember(member)"
|
||||
class="p-1 rounded hover:bg-destructive/20 text-muted-foreground hover:text-destructive transition-colors"
|
||||
title="Ban"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideBan"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input
|
||||
} from '@angular/core';
|
||||
@@ -10,12 +11,23 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { lucideUserX, lucideBan } from '@ng-icons/lucide';
|
||||
|
||||
import { Room, User } from '../../../../core/models/index';
|
||||
import {
|
||||
Room,
|
||||
RoomMember,
|
||||
User,
|
||||
UserRole
|
||||
} from '../../../../core/models/index';
|
||||
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
|
||||
import { UsersActions } from '../../../../store/users/users.actions';
|
||||
import { WebRTCService } from '../../../../core/services/webrtc.service';
|
||||
import { selectCurrentUser, selectOnlineUsers } from '../../../../store/users/users.selectors';
|
||||
import { selectCurrentUser, selectUsersEntities } from '../../../../store/users/users.selectors';
|
||||
import { selectCurrentRoom } from '../../../../store/rooms/rooms.selectors';
|
||||
import { UserAvatarComponent } from '../../../../shared';
|
||||
|
||||
interface ServerMemberView extends RoomMember {
|
||||
isOnline: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-members-settings',
|
||||
standalone: true,
|
||||
@@ -41,45 +53,104 @@ export class MembersSettingsComponent {
|
||||
server = input<Room | null>(null);
|
||||
/** Whether the current user is admin of this server. */
|
||||
isAdmin = input(false);
|
||||
accessRole = input<UserRole | null>(null);
|
||||
|
||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
||||
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
||||
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||
usersEntities = this.store.selectSignal(selectUsersEntities);
|
||||
|
||||
membersFiltered(): User[] {
|
||||
members = computed<ServerMemberView[]>(() => {
|
||||
const room = this.server();
|
||||
const me = this.currentUser();
|
||||
const currentRoom = this.currentRoom();
|
||||
const usersEntities = this.usersEntities();
|
||||
|
||||
return this.onlineUsers().filter((user) => user.id !== me?.id && user.oderId !== me?.oderId);
|
||||
if (!room)
|
||||
return [];
|
||||
|
||||
return (room.members ?? [])
|
||||
.filter((member) => member.id !== me?.id && member.oderId !== me?.oderId)
|
||||
.map((member) => {
|
||||
const liveUser = currentRoom?.id === room.id
|
||||
? (usersEntities[member.id]
|
||||
|| Object.values(usersEntities).find((user) => !!user && user.oderId === member.oderId)
|
||||
|| null)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...member,
|
||||
avatarUrl: liveUser?.avatarUrl || member.avatarUrl,
|
||||
displayName: liveUser?.displayName || member.displayName,
|
||||
isOnline: !!liveUser && (liveUser.isOnline === true || liveUser.status !== 'offline')
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
canChangeRoles(): boolean {
|
||||
const role = this.accessRole();
|
||||
|
||||
return role === 'host' || role === 'admin';
|
||||
}
|
||||
|
||||
changeRole(user: User, role: 'admin' | 'moderator' | 'member'): void {
|
||||
const roomId = this.server()?.id;
|
||||
canKickMembers(): boolean {
|
||||
const role = this.accessRole();
|
||||
|
||||
this.store.dispatch(UsersActions.updateUserRole({ userId: user.id,
|
||||
role }));
|
||||
return role === 'host' || role === 'admin' || role === 'moderator';
|
||||
}
|
||||
|
||||
canBanMembers(): boolean {
|
||||
const role = this.accessRole();
|
||||
|
||||
return role === 'host' || role === 'admin';
|
||||
}
|
||||
|
||||
changeRole(member: ServerMemberView, role: 'admin' | 'moderator' | 'member'): void {
|
||||
const room = this.server();
|
||||
|
||||
if (!room)
|
||||
return;
|
||||
|
||||
const members = (room.members ?? []).map((existingMember) =>
|
||||
existingMember.id === member.id || existingMember.oderId === member.oderId
|
||||
? { ...existingMember,
|
||||
role }
|
||||
: existingMember
|
||||
);
|
||||
|
||||
this.store.dispatch(RoomsActions.updateRoom({ roomId: room.id,
|
||||
changes: { members } }));
|
||||
|
||||
if (this.currentRoom()?.id === room.id) {
|
||||
this.store.dispatch(UsersActions.updateUserRole({ userId: member.id,
|
||||
role }));
|
||||
}
|
||||
|
||||
this.webrtcService.broadcastMessage({
|
||||
type: 'role-change',
|
||||
roomId,
|
||||
targetUserId: user.id,
|
||||
roomId: room.id,
|
||||
targetUserId: member.id,
|
||||
role
|
||||
});
|
||||
}
|
||||
|
||||
kickMember(user: User): void {
|
||||
this.store.dispatch(UsersActions.kickUser({ userId: user.id }));
|
||||
this.webrtcService.broadcastMessage({
|
||||
type: 'kick',
|
||||
targetUserId: user.id,
|
||||
kickedBy: this.currentUser()?.id
|
||||
});
|
||||
kickMember(member: ServerMemberView): void {
|
||||
const room = this.server();
|
||||
|
||||
if (!room)
|
||||
return;
|
||||
|
||||
this.store.dispatch(UsersActions.kickUser({ userId: member.id,
|
||||
roomId: room.id }));
|
||||
}
|
||||
|
||||
banMember(user: User): void {
|
||||
this.store.dispatch(UsersActions.banUser({ userId: user.id }));
|
||||
this.webrtcService.broadcastMessage({
|
||||
type: 'ban',
|
||||
targetUserId: user.id,
|
||||
bannedBy: this.currentUser()?.id
|
||||
});
|
||||
banMember(member: ServerMemberView): void {
|
||||
const room = this.server();
|
||||
|
||||
if (!room)
|
||||
return;
|
||||
|
||||
this.store.dispatch(UsersActions.banUser({ userId: member.id,
|
||||
roomId: room.id,
|
||||
displayName: member.displayName }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,9 +87,9 @@ export class ServerSettingsComponent {
|
||||
return;
|
||||
|
||||
this.store.dispatch(
|
||||
RoomsActions.updateRoom({
|
||||
RoomsActions.updateRoomSettings({
|
||||
roomId: room.id,
|
||||
changes: {
|
||||
settings: {
|
||||
name: this.roomName,
|
||||
description: this.roomDescription,
|
||||
isPrivate: this.isPrivate(),
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
}
|
||||
|
||||
<!-- Server section -->
|
||||
@if (savedRooms().length > 0) {
|
||||
@if (manageableRooms().length > 0) {
|
||||
<div class="mt-3 pt-3 border-t border-border">
|
||||
<p class="px-4 py-1.5 text-[11px] font-semibold text-muted-foreground/70 uppercase tracking-wider">Server</p>
|
||||
|
||||
@@ -69,13 +69,13 @@
|
||||
(change)="onServerSelect($event)"
|
||||
>
|
||||
<option value="">Select a server…</option>
|
||||
@for (room of savedRooms(); track room.id) {
|
||||
@for (room of manageableRooms(); track room.id) {
|
||||
<option [value]="room.id">{{ room.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (selectedServerId() && isSelectedServerAdmin()) {
|
||||
@if (selectedServerId() && canAccessSelectedServer()) {
|
||||
@for (page of serverPages; track page.id) {
|
||||
<button
|
||||
(click)="navigate(page.id)"
|
||||
@@ -166,26 +166,27 @@
|
||||
@case ('server') {
|
||||
<app-server-settings
|
||||
[server]="selectedServer()"
|
||||
[isAdmin]="isSelectedServerAdmin()"
|
||||
[isAdmin]="isSelectedServerOwner()"
|
||||
/>
|
||||
}
|
||||
@case ('members') {
|
||||
<app-members-settings
|
||||
[server]="selectedServer()"
|
||||
[isAdmin]="isSelectedServerAdmin()"
|
||||
[isAdmin]="canManageSelectedMembers()"
|
||||
[accessRole]="selectedServerRole()"
|
||||
/>
|
||||
}
|
||||
@case ('bans') {
|
||||
<app-bans-settings
|
||||
[server]="selectedServer()"
|
||||
[isAdmin]="isSelectedServerAdmin()"
|
||||
[isAdmin]="canManageSelectedBans()"
|
||||
/>
|
||||
}
|
||||
@case ('permissions') {
|
||||
<app-permissions-settings
|
||||
#permissionsComp
|
||||
[server]="selectedServer()"
|
||||
[isAdmin]="isSelectedServerAdmin()"
|
||||
[isAdmin]="isSelectedServerOwner()"
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,12 @@ import {
|
||||
import { SettingsModalService, SettingsPage } from '../../../core/services/settings-modal.service';
|
||||
import { selectSavedRooms, selectCurrentRoom } from '../../../store/rooms/rooms.selectors';
|
||||
import { selectCurrentUser } from '../../../store/users/users.selectors';
|
||||
import { Room } from '../../../core/models/index';
|
||||
import {
|
||||
Room,
|
||||
UserRole
|
||||
} from '../../../core/models/index';
|
||||
import { findRoomMember } from '../../../store/rooms/room-members.helpers';
|
||||
import { WebRTCService } from '../../../core/services/webrtc.service';
|
||||
|
||||
import { NetworkSettingsComponent } from './network-settings/network-settings.component';
|
||||
import { VoiceSettingsComponent } from './voice-settings/voice-settings.component';
|
||||
@@ -69,7 +74,9 @@ import { THIRD_PARTY_LICENSES, type ThirdPartyLicense } from './third-party-lice
|
||||
export class SettingsModalComponent {
|
||||
readonly modal = inject(SettingsModalService);
|
||||
private store = inject(Store);
|
||||
private webrtc = inject(WebRTCService);
|
||||
readonly thirdPartyLicenses: readonly ThirdPartyLicense[] = THIRD_PARTY_LICENSES;
|
||||
private lastRequestedServerId: string | null = null;
|
||||
|
||||
private permissionsComponent = viewChild<PermissionsSettingsComponent>('permissionsComp');
|
||||
|
||||
@@ -106,6 +113,19 @@ export class SettingsModalComponent {
|
||||
icon: 'lucideShield' }
|
||||
];
|
||||
|
||||
manageableRooms = computed<Room[]>(() => {
|
||||
const user = this.currentUser();
|
||||
|
||||
if (!user)
|
||||
return [];
|
||||
|
||||
return this.savedRooms().filter((room) => {
|
||||
const role = this.getUserRoleForRoom(room, user.id, user.oderId, this.currentRoom()?.id === room.id ? user.role : null);
|
||||
|
||||
return role === 'host' || role === 'admin' || role === 'moderator';
|
||||
});
|
||||
});
|
||||
|
||||
selectedServerId = signal<string | null>(null);
|
||||
selectedServer = computed<Room | null>(() => {
|
||||
const id = this.selectedServerId();
|
||||
@@ -113,21 +133,48 @@ export class SettingsModalComponent {
|
||||
if (!id)
|
||||
return null;
|
||||
|
||||
return this.savedRooms().find((room) => room.id === id) ?? null;
|
||||
return this.manageableRooms().find((room) => room.id === id) ?? null;
|
||||
});
|
||||
|
||||
showServerTabs = computed(() => {
|
||||
return this.savedRooms().length > 0 && !!this.selectedServerId();
|
||||
return this.manageableRooms().length > 0 && !!this.selectedServerId();
|
||||
});
|
||||
|
||||
isSelectedServerAdmin = computed(() => {
|
||||
selectedServerRole = computed<UserRole | null>(() => {
|
||||
const server = this.selectedServer();
|
||||
const user = this.currentUser();
|
||||
|
||||
if (!server || !user)
|
||||
return false;
|
||||
return null;
|
||||
|
||||
return server.hostId === user.id || server.hostId === user.oderId;
|
||||
return this.getUserRoleForRoom(
|
||||
server,
|
||||
user.id,
|
||||
user.oderId,
|
||||
this.currentRoom()?.id === server.id ? user.role : null
|
||||
);
|
||||
});
|
||||
|
||||
canAccessSelectedServer = computed(() => {
|
||||
const role = this.selectedServerRole();
|
||||
|
||||
return role === 'host' || role === 'admin' || role === 'moderator';
|
||||
});
|
||||
|
||||
canManageSelectedMembers = computed(() => {
|
||||
const role = this.selectedServerRole();
|
||||
|
||||
return role === 'host' || role === 'admin' || role === 'moderator';
|
||||
});
|
||||
|
||||
canManageSelectedBans = computed(() => {
|
||||
const role = this.selectedServerRole();
|
||||
|
||||
return role === 'host' || role === 'admin';
|
||||
});
|
||||
|
||||
isSelectedServerOwner = computed(() => {
|
||||
return this.selectedServerRole() === 'host';
|
||||
});
|
||||
|
||||
animating = signal(false);
|
||||
@@ -135,21 +182,27 @@ export class SettingsModalComponent {
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (this.isOpen()) {
|
||||
const targetId = this.modal.targetServerId();
|
||||
|
||||
if (targetId) {
|
||||
this.selectedServerId.set(targetId);
|
||||
} else {
|
||||
const currentRoom = this.currentRoom();
|
||||
|
||||
if (currentRoom) {
|
||||
this.selectedServerId.set(currentRoom.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.animating.set(true);
|
||||
if (!this.isOpen()) {
|
||||
this.lastRequestedServerId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const rooms = this.manageableRooms();
|
||||
const targetId = this.modal.targetServerId();
|
||||
const currentRoomId = this.currentRoom()?.id ?? null;
|
||||
const selectedId = this.selectedServerId();
|
||||
|
||||
const hasSelected = !!selectedId && rooms.some((room) => room.id === selectedId);
|
||||
|
||||
if (!hasSelected) {
|
||||
const fallbackId = [targetId, currentRoomId].find((candidateId) =>
|
||||
!!candidateId && rooms.some((room) => room.id === candidateId)
|
||||
) ?? rooms[0]?.id ?? null;
|
||||
|
||||
this.selectedServerId.set(fallbackId);
|
||||
}
|
||||
|
||||
this.animating.set(true);
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
@@ -163,7 +216,48 @@ export class SettingsModalComponent {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (!this.isOpen())
|
||||
return;
|
||||
|
||||
const serverId = this.selectedServerId();
|
||||
|
||||
if (!serverId || this.lastRequestedServerId === serverId)
|
||||
return;
|
||||
|
||||
this.lastRequestedServerId = serverId;
|
||||
|
||||
for (const peerId of this.webrtc.getConnectedPeers()) {
|
||||
try {
|
||||
this.webrtc.sendToPeer(peerId, {
|
||||
type: 'server-state-request',
|
||||
roomId: serverId
|
||||
});
|
||||
} catch {
|
||||
/* peer may have disconnected */
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getUserRoleForRoom(
|
||||
room: Room,
|
||||
userId: string,
|
||||
userOderId: string,
|
||||
currentRole: UserRole | null
|
||||
): UserRole | null {
|
||||
if (room.hostId === userId || room.hostId === userOderId)
|
||||
return 'host';
|
||||
|
||||
if (currentRole)
|
||||
return currentRole;
|
||||
|
||||
return findRoomMember(room.members ?? [], userId)?.role
|
||||
|| findRoomMember(room.members ?? [], userOderId)?.role
|
||||
|| null;
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.escape')
|
||||
onEscapeKey(): void {
|
||||
if (this.showThirdPartyLicenses()) {
|
||||
|
||||
Reference in New Issue
Block a user