203 lines
6.3 KiB
HTML
203 lines
6.3 KiB
HTML
<!-- Header -->
|
|
<div class="p-4 border-b border-border">
|
|
<h3 class="font-semibold text-foreground">Members</h3>
|
|
<p class="text-xs text-muted-foreground">{{ onlineUsers().length }} online · {{ voiceUsers().length }} in voice</p>
|
|
@if (voiceUsers().length > 0) {
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
@for (v of voiceUsers(); track v.id) {
|
|
<span class="px-2 py-1 text-xs rounded bg-secondary text-foreground flex items-center gap-1">
|
|
<span class="inline-block w-1.5 h-1.5 rounded-full bg-green-500"></span>
|
|
{{ v.displayName }}
|
|
</span>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- User List -->
|
|
<div class="flex-1 overflow-y-auto p-2 space-y-1">
|
|
@for (user of onlineUsers(); track user.id) {
|
|
<div
|
|
class="group relative flex items-center gap-3 p-2 rounded-lg hover:bg-secondary/50 transition-colors cursor-pointer"
|
|
(click)="toggleUserMenu(user.id)"
|
|
(keydown.enter)="toggleUserMenu(user.id)"
|
|
(keydown.space)="toggleUserMenu(user.id)"
|
|
(keyup.enter)="toggleUserMenu(user.id)"
|
|
(keyup.space)="toggleUserMenu(user.id)"
|
|
role="button"
|
|
tabindex="0"
|
|
>
|
|
<!-- Avatar with online indicator -->
|
|
<div class="relative">
|
|
<app-user-avatar
|
|
[name]="user.displayName"
|
|
size="sm"
|
|
/>
|
|
<span
|
|
class="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-card"
|
|
[class.bg-green-500]="user.isOnline !== false && user.status !== 'offline'"
|
|
[class.bg-gray-500]="user.isOnline === false || user.status === 'offline'"
|
|
></span>
|
|
</div>
|
|
|
|
<!-- User Info -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-1">
|
|
<span class="font-medium text-sm text-foreground truncate">
|
|
{{ user.displayName }}
|
|
</span>
|
|
@if (user.isAdmin) {
|
|
<ng-icon
|
|
name="lucideShield"
|
|
class="w-3 h-3 text-primary"
|
|
/>
|
|
}
|
|
@if (user.isRoomOwner) {
|
|
<ng-icon
|
|
name="lucideCrown"
|
|
class="w-3 h-3 text-yellow-500"
|
|
/>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Voice/Screen Status -->
|
|
<div class="flex items-center gap-1">
|
|
@if (user.voiceState?.isSpeaking) {
|
|
<ng-icon
|
|
name="lucideMic"
|
|
class="w-4 h-4 text-green-500 animate-pulse"
|
|
/>
|
|
} @else if (user.voiceState?.isMuted) {
|
|
<ng-icon
|
|
name="lucideMicOff"
|
|
class="w-4 h-4 text-muted-foreground"
|
|
/>
|
|
} @else if (user.voiceState?.isConnected) {
|
|
<ng-icon
|
|
name="lucideMic"
|
|
class="w-4 h-4 text-muted-foreground"
|
|
/>
|
|
}
|
|
|
|
@if (user.screenShareState?.isSharing) {
|
|
<ng-icon
|
|
name="lucideMonitor"
|
|
class="w-4 h-4 text-primary"
|
|
/>
|
|
}
|
|
</div>
|
|
|
|
<!-- User Menu -->
|
|
@if (showUserMenu() === user.id && isAdmin() && !isCurrentUser(user)) {
|
|
<div
|
|
class="absolute right-0 top-full mt-1 z-10 w-48 bg-card border border-border rounded-lg shadow-lg py-1"
|
|
(click)="$event.stopPropagation()"
|
|
(keydown)="$event.stopPropagation()"
|
|
role="menu"
|
|
tabindex="0"
|
|
>
|
|
@if (user.voiceState?.isConnected) {
|
|
<button
|
|
type="button"
|
|
(click)="muteUser(user)"
|
|
class="w-full px-4 py-2 text-left text-sm hover:bg-secondary flex items-center gap-2"
|
|
>
|
|
@if (user.voiceState?.isMutedByAdmin) {
|
|
<ng-icon
|
|
name="lucideVolume2"
|
|
class="w-4 h-4"
|
|
/>
|
|
<span>Unmute</span>
|
|
} @else {
|
|
<ng-icon
|
|
name="lucideVolumeX"
|
|
class="w-4 h-4"
|
|
/>
|
|
<span>Mute</span>
|
|
}
|
|
</button>
|
|
}
|
|
<button
|
|
type="button"
|
|
(click)="kickUser(user)"
|
|
class="w-full px-4 py-2 text-left text-sm hover:bg-secondary flex items-center gap-2 text-yellow-500"
|
|
>
|
|
<ng-icon
|
|
name="lucideUserX"
|
|
class="w-4 h-4"
|
|
/>
|
|
<span>Kick</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
(click)="banUser(user)"
|
|
class="w-full px-4 py-2 text-left text-sm hover:bg-destructive/10 flex items-center gap-2 text-destructive"
|
|
>
|
|
<ng-icon
|
|
name="lucideBan"
|
|
class="w-4 h-4"
|
|
/>
|
|
<span>Ban</span>
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (onlineUsers().length === 0) {
|
|
<div class="text-center py-8 text-muted-foreground text-sm">No users online</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Ban Dialog -->
|
|
@if (showBanDialog()) {
|
|
<app-confirm-dialog
|
|
title="Ban User"
|
|
confirmLabel="Ban User"
|
|
variant="danger"
|
|
[widthClass]="'w-96 max-w-[90vw]'"
|
|
(confirmed)="confirmBan()"
|
|
(cancelled)="closeBanDialog()"
|
|
>
|
|
<p class="mb-4">
|
|
Are you sure you want to ban <span class="font-semibold text-foreground">{{ userToBan()?.displayName }}</span
|
|
>?
|
|
</p>
|
|
|
|
<div class="mb-4">
|
|
<label
|
|
for="ban-reason-input"
|
|
class="block text-sm font-medium text-foreground mb-1"
|
|
>Reason (optional)</label
|
|
>
|
|
<input
|
|
type="text"
|
|
[(ngModel)]="banReason"
|
|
placeholder="Enter ban reason..."
|
|
id="ban-reason-input"
|
|
class="w-full px-3 py-2 bg-secondary rounded border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="ban-duration-select"
|
|
class="block text-sm font-medium text-foreground mb-1"
|
|
>Duration</label
|
|
>
|
|
<select
|
|
[(ngModel)]="banDuration"
|
|
id="ban-duration-select"
|
|
class="w-full px-3 py-2 bg-secondary rounded border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
>
|
|
<option value="3600000">1 hour</option>
|
|
<option value="86400000">1 day</option>
|
|
<option value="604800000">1 week</option>
|
|
<option value="2592000000">30 days</option>
|
|
<option value="0">Permanent</option>
|
|
</select>
|
|
</div>
|
|
</app-confirm-dialog>
|
|
}
|