Files
Toju/toju-app/src/app/features/servers/servers-rail/servers-rail.component.html
T
2026-05-17 15:14:52 +02:00

239 lines
8.5 KiB
HTML

<nav class="relative flex h-full w-full flex-col items-center gap-2 border-r border-border bg-secondary/35 px-2 py-3">
<!-- Create button -->
<button
appThemeNode="serversRailCreateButton"
type="button"
class="flex h-10 w-10 items-center justify-center rounded-md bg-primary text-primary-foreground transition-colors hover:bg-primary/90"
title="Create Server"
(click)="createServer()"
>
<ng-icon
name="lucidePlus"
class="w-5 h-5"
/>
</button>
@if (dmRailComponent()) {
<ng-container *ngComponentOutlet="dmRailComponent()" />
}
@for (call of directCalls.visibleActiveSessions(); track call.callId + ':' + $index) {
<div class="group/call relative flex w-full justify-center">
<span
aria-hidden="true"
class="pointer-events-none absolute left-0 top-1/2 w-[3px] -translate-y-1/2 rounded-r-full bg-emerald-500 transition-[height,opacity] duration-100"
[ngClass]="isSelectedCall($index) ? 'h-5 opacity-100' : 'h-0 opacity-0 group-hover/call:h-2.5 group-hover/call:opacity-100'"
></span>
<button
type="button"
class="relative z-10 grid h-10 w-10 place-items-center overflow-hidden rounded-xl transition-colors hover:rounded-lg"
[ngClass]="
callAvatarUrls(call).length > 0
? 'bg-emerald-950 text-white shadow-sm hover:bg-emerald-900'
: 'bg-emerald-500/15 text-emerald-600 hover:bg-emerald-500/25'
"
[attr.data-testid]="'server-rail-call-' + call.callId"
title="Open private call"
[attr.aria-current]="isSelectedCall($index) ? 'page' : null"
(click)="openCall(call.callId)"
>
@let callAvatars = callAvatarUrls(call);
@if (callAvatars.length > 0) {
<span
aria-hidden="true"
class="absolute inset-0 bg-emerald-950"
></span>
@for (avatarUrl of callAvatars; track avatarUrl) {
<span
aria-hidden="true"
class="absolute inset-0 bg-cover bg-center opacity-65 mix-blend-screen saturate-125"
[style.backgroundImage]="'url(' + avatarUrl + ')'"
></span>
}
<span
aria-hidden="true"
class="absolute inset-0 bg-gradient-to-br from-black/10 via-emerald-950/20 to-black/45"
></span>
}
<ng-icon
name="lucidePhone"
class="relative z-10 h-5 w-5 drop-shadow"
/>
</button>
</div>
}
<!-- Saved servers icons -->
<div
appThemeNode="serversRailList"
class="no-scrollbar mt-2 flex w-full flex-1 flex-col items-center gap-2 overflow-y-auto pt-0.5"
>
@for (room of visibleSavedRooms(); track room.id) {
<div class="group/server relative flex w-full justify-center">
<span
aria-hidden="true"
class="pointer-events-none absolute left-0 top-1/2 w-[3px] -translate-y-1/2 rounded-r-full bg-primary transition-[height,opacity] duration-100"
[ngClass]="isSelectedRoom(room) ? 'h-5 opacity-100' : 'h-0 opacity-0 group-hover/server:h-2.5 group-hover/server:opacity-100'"
></span>
<button
appThemeNode="serversRailItem"
type="button"
class="relative z-10 flex h-10 w-10 cursor-pointer flex-shrink-0 items-center justify-center border border-transparent transition-[border-radius,box-shadow,background-color] duration-100 hover:rounded-lg hover:bg-card"
[ngClass]="isSelectedRoom(room) ? 'rounded-lg ring-2 ring-primary/40 bg-primary/10' : 'rounded-xl bg-card'"
[title]="room.name"
[attr.aria-current]="isSelectedRoom(room) ? 'page' : null"
(click)="joinSavedRoom(room)"
(contextmenu)="openContextMenu($event, room)"
>
<div class="h-full w-full overflow-hidden rounded-[inherit]">
@if (room.icon) {
<div
aria-hidden="true"
class="h-full w-full bg-cover bg-center bg-no-repeat"
[style.backgroundImage]="'url(' + room.icon + ')'"
></div>
} @else {
<div
class="flex h-full w-full items-center justify-center bg-secondary transition-colors"
[class.bg-primary/15]="isSelectedRoom(room)"
>
<span
class="text-sm font-semibold text-muted-foreground transition-colors"
[class.text-foreground]="isSelectedRoom(room)"
>{{ initial(room.name) }}</span
>
</div>
}
</div>
@if (roomUnreadCount(room.id) > 0) {
<span class="absolute -right-1 -top-1 min-w-5 rounded-full bg-amber-400 px-1.5 py-0.5 text-[10px] font-semibold text-black shadow-sm">
{{ formatUnreadCount(roomUnreadCount(room.id)) }}
</span>
}
@if (voicePresenceCount(room.id) > 0) {
<span
class="absolute -bottom-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-emerald-500 text-white shadow-sm ring-2 ring-card"
[title]="voicePresenceCount(room.id) + (voicePresenceCount(room.id) === 1 ? ' user in voice' : ' users in voice')"
>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
class="h-2.5 w-2.5 fill-current"
>
<path
d="M6.25 4.1a.75.75 0 0 1 1.25.57v6.66a.75.75 0 0 1-1.25.57L3.68 9.6H2.25A1.25 1.25 0 0 1 1 8.35v-.7c0-.69.56-1.25 1.25-1.25h1.43L6.25 4.1Zm4.1 1.35a.75.75 0 0 1 1.05.1 4.2 4.2 0 0 1 0 4.9.75.75 0 0 1-1.15-.96 2.7 2.7 0 0 0 0-2.98.75.75 0 0 1 .1-1.06Z"
/>
</svg>
</span>
}
</button>
</div>
}
</div>
<div
class="grid w-full overflow-hidden duration-200 ease-out motion-reduce:transition-none"
style="transition-property: grid-template-rows, opacity"
[style.gridTemplateRows]="isOnSearch() ? '1fr' : '0fr'"
[style.opacity]="isOnSearch() ? '1' : '0'"
[style.visibility]="isOnSearch() ? 'visible' : 'hidden'"
[class.pointer-events-none]="!isOnSearch()"
[attr.aria-hidden]="isOnSearch() ? null : 'true'"
>
<div class="overflow-hidden">
<app-user-bar />
</div>
</div>
</nav>
<!-- Context menu -->
@if (showMenu()) {
<app-context-menu
[x]="menuX()"
[y]="menuY()"
(closed)="closeMenu()"
[width]="'w-44'"
>
<button
type="button"
(click)="toggleRoomNotifications()"
class="context-menu-item"
>
{{ isRoomNotificationsMuted(contextRoom()?.id || '') ? 'Unmute Notifications' : 'Mute Notifications' }}
</button>
<div class="context-menu-divider"></div>
<button
type="button"
(click)="openLeaveConfirm()"
class="context-menu-item"
>
Leave Server
</button>
</app-context-menu>
}
@if (showBannedDialog()) {
<app-confirm-dialog
title="Banned"
confirmLabel="OK"
cancelLabel="Close"
variant="danger"
[widthClass]="'w-96 max-w-[90vw]'"
(confirmed)="closeBannedDialog()"
(cancelled)="closeBannedDialog()"
>
<p>You are banned from {{ bannedServerName() || 'this server' }}.</p>
</app-confirm-dialog>
}
@if (showPasswordDialog() && passwordPromptRoom()) {
<app-confirm-dialog
title="Password required"
confirmLabel="Join server"
cancelLabel="Cancel"
[widthClass]="'w-[420px] max-w-[92vw]'"
(confirmed)="confirmPasswordJoin()"
(cancelled)="closePasswordDialog()"
>
<div class="space-y-3 text-left">
<p>Enter the password to rejoin {{ passwordPromptRoom()!.name }}.</p>
<div>
<label
for="rail-join-password"
class="mb-1 block text-xs font-medium uppercase tracking-wide text-muted-foreground"
>
Server password
</label>
<input
id="rail-join-password"
type="password"
[(ngModel)]="joinPassword"
placeholder="Enter password"
class="w-full rounded-lg border border-border bg-secondary px-3 py-2 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
@if (joinPasswordError()) {
<p class="text-sm text-destructive">{{ joinPasswordError() }}</p>
}
</div>
</app-confirm-dialog>
}
@if (showLeaveConfirm() && contextRoom()) {
<app-leave-server-dialog
[room]="contextRoom()!"
[currentUser]="currentUser() ?? null"
(confirmed)="confirmLeave($event)"
(cancelled)="cancelLeave()"
/>
}