239 lines
8.5 KiB
HTML
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()"
|
|
/>
|
|
}
|