387 lines
13 KiB
HTML
387 lines
13 KiB
HTML
<!-- eslint-disable @angular-eslint/template/cyclomatic-complexity -->
|
|
<div
|
|
appThemeNode="titleBar"
|
|
class="relative z-50 flex h-10 w-full items-center justify-between border-b border-border bg-card px-4 select-none"
|
|
style="-webkit-app-region: drag"
|
|
>
|
|
<div
|
|
class="flex items-center gap-2 min-w-0 relative"
|
|
style="-webkit-app-region: no-drag"
|
|
>
|
|
<span
|
|
data-theme-slot="icon"
|
|
class="theme-icon-slot h-6 w-6 shrink-0 items-center justify-center rounded-xl border border-border/70 bg-secondary/70 bg-center bg-cover bg-no-repeat text-[10px] font-semibold uppercase tracking-[0.16em] text-foreground"
|
|
></span>
|
|
|
|
@if (inRoom()) {
|
|
<span
|
|
data-theme-slot="text"
|
|
class="flex min-w-0 items-center gap-2 text-sm font-semibold text-foreground"
|
|
>
|
|
<span class="truncate">{{ roomName() }}</span>
|
|
|
|
@if (isVoiceWorkspaceExpanded()) {
|
|
<span class="shrink-0 text-muted-foreground">/</span>
|
|
<span class="truncate">{{ connectedVoiceChannelName() }}</span>
|
|
} @else if (textChannels().length > 0) {
|
|
<span class="shrink-0 text-muted-foreground">/</span>
|
|
<span class="flex min-w-0 items-center gap-1 text-foreground">
|
|
<ng-icon
|
|
name="lucideHash"
|
|
class="h-4 w-4 shrink-0 text-muted-foreground"
|
|
/>
|
|
<span class="truncate">{{ activeTextChannelName() }}</span>
|
|
</span>
|
|
}
|
|
</span>
|
|
|
|
@if (showRoomCompatibilityNotice()) {
|
|
<span class="inline-flex items-center gap-1 rounded bg-destructive/15 px-2 py-0.5 text-xs text-destructive">
|
|
{{ signalServerCompatibilityError() }}
|
|
</span>
|
|
}
|
|
|
|
@if (showRoomReconnectNotice()) {
|
|
<span class="inline-flex items-center gap-1 rounded bg-destructive/15 px-2 py-0.5 text-xs text-destructive">
|
|
<ng-icon
|
|
name="lucideRefreshCw"
|
|
class="h-3.5 w-3.5 animate-spin"
|
|
/>
|
|
Reconnecting to signal server...
|
|
</span>
|
|
}
|
|
|
|
@if (roomContextMeta()) {
|
|
<span class="hidden truncate border-l border-border/70 pl-2 text-xs text-muted-foreground md:inline">
|
|
{{ roomContextMeta() }}
|
|
</span>
|
|
}
|
|
} @else if (!isInDirectMessage()) {
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
<span
|
|
data-theme-slot="text"
|
|
class="text-sm text-muted-foreground truncate"
|
|
>{{ username() }} | {{ serverName() }}</span
|
|
>
|
|
<span
|
|
class="text-xs px-2 py-0.5 rounded bg-destructive/15 text-destructive"
|
|
[class.hidden]="!isReconnecting()"
|
|
>Reconnecting...</span
|
|
>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div
|
|
class="flex items-center gap-2"
|
|
style="-webkit-app-region: no-drag"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="grid h-8 place-items-center rounded-md px-3 text-sm text-foreground transition-colors hover:bg-secondary"
|
|
[class.hidden]="isAuthed()"
|
|
(click)="goLogin()"
|
|
title="Login"
|
|
>
|
|
Login
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="flex h-8 items-center gap-1.5 rounded-md px-2 text-sm text-foreground transition-colors hover:bg-secondary"
|
|
[class.hidden]="!isAuthed()"
|
|
(click)="openPluginStore()"
|
|
title="Plugin Store"
|
|
>
|
|
<ng-icon
|
|
name="lucidePackage"
|
|
class="h-4 w-4 text-muted-foreground"
|
|
/>
|
|
</button>
|
|
|
|
@if (hasServerPlugins()) {
|
|
<button
|
|
type="button"
|
|
class="relative grid h-8 w-8 place-items-center rounded-md text-foreground transition-colors hover:bg-secondary"
|
|
(click)="openServerPlugins()"
|
|
title="Server plugins"
|
|
aria-label="Server plugins"
|
|
>
|
|
<ng-icon
|
|
name="lucideShield"
|
|
class="h-4 w-4 text-muted-foreground"
|
|
/>
|
|
<span class="absolute right-0 top-0 min-w-3 rounded-full bg-primary px-1 text-[9px] font-semibold leading-3 text-primary-foreground">
|
|
{{ serverPluginCount() }}
|
|
</span>
|
|
</button>
|
|
}
|
|
|
|
@if (isElectron()) {
|
|
<button
|
|
type="button"
|
|
class="grid h-8 w-8 place-items-center rounded-md transition-colors hover:bg-secondary"
|
|
title="Open Documentation"
|
|
(click)="openDocumentation()"
|
|
>
|
|
<ng-icon
|
|
name="lucideBookOpen"
|
|
class="h-4 w-4 text-muted-foreground"
|
|
/>
|
|
</button>
|
|
}
|
|
|
|
<div class="relative">
|
|
<button
|
|
type="button"
|
|
(click)="toggleMenu()"
|
|
class="grid h-8 w-8 place-items-center rounded-md transition-colors hover:bg-secondary"
|
|
title="Menu"
|
|
>
|
|
<ng-icon
|
|
name="lucideMenu"
|
|
class="h-4 w-4 text-muted-foreground"
|
|
/>
|
|
</button>
|
|
|
|
@if (showMenu()) {
|
|
<div
|
|
class="absolute right-0 top-full z-50 mt-2 w-64 rounded-md border border-border bg-popover p-1 shadow-lg"
|
|
style="-webkit-app-region: no-drag"
|
|
>
|
|
@if (inRoom()) {
|
|
<button
|
|
type="button"
|
|
(click)="createInviteLink()"
|
|
[disabled]="creatingInvite()"
|
|
class="w-full rounded-md px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
|
>
|
|
@if (creatingInvite()) {
|
|
Creating Invite Link...
|
|
} @else {
|
|
Create Invite Link
|
|
}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
(click)="leaveServer()"
|
|
class="w-full rounded-md px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
|
>
|
|
Leave Server
|
|
</button>
|
|
}
|
|
<div
|
|
class="px-3 py-2 text-xs leading-5 text-muted-foreground"
|
|
[class.hidden]="!inviteStatus()"
|
|
>
|
|
{{ inviteStatus() }}
|
|
</div>
|
|
<div class="mx-2 my-1 h-px bg-border"></div>
|
|
<button
|
|
type="button"
|
|
(click)="openPluginStore()"
|
|
class="w-full rounded-md px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
|
>
|
|
Plugin Store
|
|
</button>
|
|
<button
|
|
type="button"
|
|
(click)="openSettings()"
|
|
class="w-full rounded-md px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
|
>
|
|
Settings
|
|
</button>
|
|
<button
|
|
type="button"
|
|
(click)="openDocumentation()"
|
|
class="w-full rounded-md px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
|
>
|
|
Documentation
|
|
</button>
|
|
<div class="mx-2 my-1 h-px bg-border"></div>
|
|
<button
|
|
type="button"
|
|
(click)="logout()"
|
|
class="w-full rounded-md px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
@if (isElectron()) {
|
|
<button
|
|
type="button"
|
|
class="grid h-8 w-8 place-items-center rounded-md transition-colors hover:bg-secondary"
|
|
title="Minimize"
|
|
(click)="minimize()"
|
|
>
|
|
<ng-icon
|
|
name="lucideMinus"
|
|
class="w-4 h-4"
|
|
/>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="grid h-8 w-8 place-items-center rounded-md transition-colors hover:bg-secondary"
|
|
title="Maximize"
|
|
(click)="maximize()"
|
|
>
|
|
<ng-icon
|
|
name="lucideSquare"
|
|
class="w-4 h-4"
|
|
/>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="grid h-8 w-8 place-items-center rounded-md transition-colors hover:bg-destructive/10"
|
|
title="Close"
|
|
(click)="close()"
|
|
>
|
|
<ng-icon
|
|
name="lucideX"
|
|
class="w-4 h-4 text-destructive"
|
|
/>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@if (optionalPluginRequirement(); as requirement) {
|
|
<section
|
|
class="flex min-h-10 items-center justify-between gap-3 border-b border-border bg-primary/10 px-4 py-2 text-sm text-foreground"
|
|
role="status"
|
|
aria-live="polite"
|
|
style="-webkit-app-region: no-drag"
|
|
>
|
|
<div class="flex min-w-0 items-center gap-2">
|
|
<ng-icon
|
|
name="lucidePackage"
|
|
class="h-4 w-4 shrink-0 text-primary"
|
|
/>
|
|
<p class="truncate">
|
|
Optional server plugin available:
|
|
<span class="font-semibold">{{ requirement.manifest?.title || requirement.pluginId }}</span>
|
|
@if (optionalPluginRequirementCount() > 1) {
|
|
<span class="text-muted-foreground">+{{ optionalPluginRequirementCount() - 1 }} more</span>
|
|
}
|
|
</p>
|
|
</div>
|
|
<div class="flex shrink-0 items-center gap-2">
|
|
@if (pluginRequirementError()) {
|
|
<span class="max-w-56 truncate text-xs text-destructive">{{ pluginRequirementError() }}</span>
|
|
}
|
|
<button
|
|
type="button"
|
|
class="rounded-md border border-border bg-card px-2.5 py-1 text-xs font-semibold transition-colors hover:bg-secondary disabled:opacity-60"
|
|
[disabled]="pluginRequirementBusy()"
|
|
(click)="rejectOptionalServerPlugin(requirement)"
|
|
>
|
|
Reject
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="rounded-md border border-border bg-card px-2.5 py-1 text-xs font-semibold transition-colors hover:bg-secondary disabled:opacity-60"
|
|
[disabled]="pluginRequirementBusy()"
|
|
(click)="hideOptionalServerPlugin(requirement)"
|
|
>
|
|
Don't show again
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="rounded-md border border-primary bg-primary px-2.5 py-1 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-60"
|
|
[disabled]="pluginRequirementBusy()"
|
|
(click)="installOptionalServerPlugin(requirement)"
|
|
>
|
|
{{ pluginRequirementBusy() ? 'Installing' : 'Install' }}
|
|
</button>
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@if (requiredPluginRequirements().length > 0 && currentRoom()) {
|
|
<div
|
|
class="fixed inset-0 z-[80] bg-black/60"
|
|
role="presentation"
|
|
></div>
|
|
<section
|
|
class="fixed left-1/2 top-1/2 z-[81] flex max-h-[min(38rem,calc(100vh-2rem))] w-[min(32rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="required-server-plugin-title"
|
|
style="-webkit-app-region: no-drag"
|
|
>
|
|
<header class="border-b border-border p-4">
|
|
<p class="text-sm text-muted-foreground">Required server plugins</p>
|
|
<h2
|
|
id="required-server-plugin-title"
|
|
class="mt-1 text-lg font-semibold"
|
|
>
|
|
{{ currentRoom()!.name }} requires a plugin update
|
|
</h2>
|
|
</header>
|
|
|
|
<div class="min-h-0 space-y-3 overflow-auto p-4">
|
|
<p class="text-sm text-muted-foreground">
|
|
An admin added required plugins for this server. Install them to keep using the server, or leave the server.
|
|
</p>
|
|
@for (requirement of requiredPluginRequirements(); track requirement.pluginId) {
|
|
<article class="rounded-lg border border-border bg-background/50 px-3 py-2">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div class="min-w-0">
|
|
<p class="truncate text-sm font-semibold">{{ requirement.manifest?.title || requirement.pluginId }}</p>
|
|
@if (requirement.reason) {
|
|
<p class="mt-1 text-xs text-muted-foreground">{{ requirement.reason }}</p>
|
|
}
|
|
</div>
|
|
<span class="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-semibold text-primary">Required</span>
|
|
</div>
|
|
</article>
|
|
}
|
|
@if (pluginRequirementError()) {
|
|
<p class="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">{{ pluginRequirementError() }}</p>
|
|
}
|
|
</div>
|
|
|
|
<footer class="flex justify-end gap-2 border-t border-border p-4">
|
|
<button
|
|
type="button"
|
|
class="rounded-lg border border-border bg-card px-3 py-1.5 text-sm font-semibold transition-colors hover:bg-secondary disabled:opacity-60"
|
|
[disabled]="pluginRequirementBusy()"
|
|
(click)="confirmLeave({})"
|
|
>
|
|
Leave server
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="rounded-lg border border-primary bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-60"
|
|
[disabled]="pluginRequirementBusy()"
|
|
(click)="installRequiredServerPlugins()"
|
|
>
|
|
{{ pluginRequirementBusy() ? 'Installing' : 'Install plugins' }}
|
|
</button>
|
|
</footer>
|
|
</section>
|
|
}
|
|
<!-- Click-away overlay to close dropdown -->
|
|
@if (showMenu()) {
|
|
<div
|
|
class="fixed inset-0 z-40"
|
|
(click)="closeMenu()"
|
|
(keydown.enter)="closeMenu()"
|
|
(keydown.space)="closeMenu()"
|
|
tabindex="0"
|
|
role="button"
|
|
aria-label="Close menu overlay"
|
|
style="-webkit-app-region: no-drag"
|
|
></div>
|
|
}
|
|
|
|
@if (showLeaveConfirm() && currentRoom()) {
|
|
<app-leave-server-dialog
|
|
[room]="currentRoom()!"
|
|
[currentUser]="currentUser() ?? null"
|
|
(confirmed)="confirmLeave($event)"
|
|
(cancelled)="cancelLeave()"
|
|
/>
|
|
}
|