Rework design part 1
This commit is contained in:
@@ -1,15 +1,15 @@
|
|||||||
<div class="h-screen bg-background text-foreground flex">
|
<div class="workspace-bright-theme relative flex h-screen overflow-hidden bg-background text-foreground">
|
||||||
<!-- Global left servers rail always visible -->
|
<!-- Global left servers rail always visible -->
|
||||||
<aside class="w-16 flex-shrink-0 border-r border-border bg-card">
|
<aside class="w-16 flex-shrink-0 bg-transparent">
|
||||||
<app-servers-rail class="h-full" />
|
<app-servers-rail class="h-full" />
|
||||||
</aside>
|
</aside>
|
||||||
<main class="flex-1 min-w-0 relative overflow-hidden">
|
<main class="relative min-w-0 flex-1 overflow-hidden bg-background">
|
||||||
<!-- Custom draggable title bar -->
|
<!-- Custom draggable title bar -->
|
||||||
<app-title-bar />
|
<app-title-bar />
|
||||||
|
|
||||||
@if (desktopUpdateState().restartRequired) {
|
@if (desktopUpdateState().restartRequired) {
|
||||||
<div class="absolute inset-x-0 top-10 z-20 px-4 pt-4 pointer-events-none">
|
<div class="absolute inset-x-0 top-10 z-20 px-4 pt-4 pointer-events-none">
|
||||||
<div class="pointer-events-auto mx-auto max-w-4xl rounded-xl border border-primary/30 bg-primary/10 p-4 shadow-2xl backdrop-blur-sm">
|
<div class="pointer-events-auto mx-auto max-w-4xl rounded-md border border-border bg-card p-4 shadow-sm">
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-foreground">Update ready to install</p>
|
<p class="text-sm font-semibold text-foreground">Update ready to install</p>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="openUpdatesSettings()"
|
(click)="openUpdatesSettings()"
|
||||||
class="inline-flex items-center rounded-lg border border-border bg-secondary px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
|
class="inline-flex items-center rounded-md border border-border bg-secondary px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||||
>
|
>
|
||||||
Update settings
|
Update settings
|
||||||
</button>
|
</button>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="restartToApplyUpdate()"
|
(click)="restartToApplyUpdate()"
|
||||||
class="inline-flex items-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
class="inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
Restart now
|
Restart now
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
(dragleave)="onDragLeave($event)"
|
(dragleave)="onDragLeave($event)"
|
||||||
(drop)="onDrop($event)"
|
(drop)="onDrop($event)"
|
||||||
placeholder="Type a message..."
|
placeholder="Type a message..."
|
||||||
class="chat-textarea w-full rounded-[1.35rem] border border-border pl-4 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
class="chat-textarea w-full rounded-md border border-border pl-4 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
[class.border-dashed]="dragActive()"
|
[class.border-dashed]="dragActive()"
|
||||||
[class.border-primary]="dragActive()"
|
[class.border-primary]="dragActive()"
|
||||||
[class.chat-textarea-expanded]="textareaExpanded()"
|
[class.chat-textarea-expanded]="textareaExpanded()"
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
|
|
||||||
@if (dragActive()) {
|
@if (dragActive()) {
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute inset-0 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary bg-primary/5"
|
class="pointer-events-none absolute inset-0 flex items-center justify-center rounded-md border-2 border-dashed border-primary bg-primary/5"
|
||||||
>
|
>
|
||||||
<div class="text-sm text-muted-foreground">Drop files to attach</div>
|
<div class="text-sm text-muted-foreground">Drop files to attach</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
@if (showFloatingControls()) {
|
@if (showFloatingControls()) {
|
||||||
<!-- Centered relative to rooms-side-panel (w-80 = 320px, so right-40 = 160px from right edge = center) -->
|
<div class="fixed bottom-4 right-4 z-50 border border-border bg-card shadow-lg">
|
||||||
<div class="fixed bottom-4 right-40 translate-x-1/2 z-50 bg-card border border-border rounded-xl shadow-lg">
|
<div class="flex items-center gap-2 p-2">
|
||||||
<div class="p-2 flex items-center gap-2">
|
|
||||||
<!-- Back to server button -->
|
<!-- Back to server button -->
|
||||||
<button
|
<button
|
||||||
(click)="navigateToServer()"
|
(click)="navigateToServer()"
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-1.5 px-2 py-1 bg-primary/10 hover:bg-primary/20 text-primary rounded-lg transition-colors"
|
class="flex items-center gap-1.5 rounded-md bg-secondary px-2 py-1 text-foreground transition-colors hover:bg-secondary/80"
|
||||||
title="Back to {{ voiceSession()?.serverName }}"
|
title="Back to {{ voiceSession()?.serverName }}"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -20,7 +19,7 @@
|
|||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="w-5 h-5 rounded bg-primary/20 flex items-center justify-center text-[10px] font-semibold">
|
<div class="flex h-5 w-5 items-center justify-center rounded-sm bg-muted text-[10px] font-semibold">
|
||||||
{{ voiceSession()?.serverName?.charAt(0)?.toUpperCase() || '?' }}
|
{{ voiceSession()?.serverName?.charAt(0)?.toUpperCase() || '?' }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -33,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
<div class="w-px h-6 bg-border"></div>
|
<div class="h-6 w-px bg-border"></div>
|
||||||
|
|
||||||
<!-- Voice controls -->
|
<!-- Voice controls -->
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
@@ -81,7 +80,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="disconnect()"
|
(click)="disconnect()"
|
||||||
type="button"
|
type="button"
|
||||||
class="w-7 h-7 inline-flex items-center justify-center bg-destructive text-destructive-foreground rounded-lg hover:bg-destructive/90 transition-colors"
|
class="inline-flex h-7 w-7 items-center justify-center rounded-md border border-destructive/20 bg-destructive/10 text-destructive transition-colors hover:bg-destructive/15"
|
||||||
title="Disconnect"
|
title="Disconnect"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
|
|||||||
@@ -219,57 +219,57 @@ export class FloatingVoiceControlsComponent implements OnInit {
|
|||||||
|
|
||||||
/** Return the CSS classes for the compact control button based on active state. */
|
/** Return the CSS classes for the compact control button based on active state. */
|
||||||
getCompactButtonClass(isActive: boolean): string {
|
getCompactButtonClass(isActive: boolean): string {
|
||||||
const base = 'w-7 h-7 inline-flex items-center justify-center rounded-lg transition-colors';
|
const base = 'inline-flex h-7 w-7 items-center justify-center rounded-md border transition-colors';
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return base + ' bg-destructive/20 text-destructive hover:bg-destructive/30';
|
return base + ' border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15';
|
||||||
}
|
}
|
||||||
|
|
||||||
return base + ' bg-secondary text-foreground hover:bg-secondary/80';
|
return base + ' border-border bg-card text-foreground hover:bg-secondary/70';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return the CSS classes for the compact screen-share button. */
|
/** Return the CSS classes for the compact screen-share button. */
|
||||||
getCompactScreenShareClass(): string {
|
getCompactScreenShareClass(): string {
|
||||||
const base = 'w-7 h-7 inline-flex items-center justify-center rounded-lg transition-colors';
|
const base = 'inline-flex h-7 w-7 items-center justify-center rounded-md border transition-colors';
|
||||||
|
|
||||||
if (this.isScreenSharing()) {
|
if (this.isScreenSharing()) {
|
||||||
return base + ' bg-primary/20 text-primary hover:bg-primary/30';
|
return base + ' border-primary/20 bg-primary/10 text-primary hover:bg-primary/15';
|
||||||
}
|
}
|
||||||
|
|
||||||
return base + ' bg-secondary text-foreground hover:bg-secondary/80';
|
return base + ' border-border bg-card text-foreground hover:bg-secondary/70';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return the CSS classes for the mute toggle button. */
|
/** Return the CSS classes for the mute toggle button. */
|
||||||
getMuteButtonClass(): string {
|
getMuteButtonClass(): string {
|
||||||
const base = 'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors';
|
const base = 'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors';
|
||||||
|
|
||||||
if (this.isMuted()) {
|
if (this.isMuted()) {
|
||||||
return base + ' bg-destructive/20 text-destructive hover:bg-destructive/30';
|
return base + ' border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15';
|
||||||
}
|
}
|
||||||
|
|
||||||
return base + ' bg-secondary text-foreground hover:bg-secondary/80';
|
return base + ' border-border bg-card text-foreground hover:bg-secondary/70';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return the CSS classes for the deafen toggle button. */
|
/** Return the CSS classes for the deafen toggle button. */
|
||||||
getDeafenButtonClass(): string {
|
getDeafenButtonClass(): string {
|
||||||
const base = 'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors';
|
const base = 'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors';
|
||||||
|
|
||||||
if (this.isDeafened()) {
|
if (this.isDeafened()) {
|
||||||
return base + ' bg-destructive/20 text-destructive hover:bg-destructive/30';
|
return base + ' border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15';
|
||||||
}
|
}
|
||||||
|
|
||||||
return base + ' bg-secondary text-foreground hover:bg-secondary/80';
|
return base + ' border-border bg-card text-foreground hover:bg-secondary/70';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return the CSS classes for the screen-share toggle button. */
|
/** Return the CSS classes for the screen-share toggle button. */
|
||||||
getScreenShareButtonClass(): string {
|
getScreenShareButtonClass(): string {
|
||||||
const base = 'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors';
|
const base = 'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors';
|
||||||
|
|
||||||
if (this.isScreenSharing()) {
|
if (this.isScreenSharing()) {
|
||||||
return base + ' bg-primary/20 text-primary hover:bg-primary/30';
|
return base + ' border-primary/20 bg-primary/10 text-primary hover:bg-primary/15';
|
||||||
}
|
}
|
||||||
|
|
||||||
return base + ' bg-secondary text-foreground hover:bg-secondary/80';
|
return base + ' border-border bg-card text-foreground hover:bg-secondary/70';
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncScreenShareSettings(): void {
|
private syncScreenShareSettings(): void {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<div class="bg-card border-border p-4">
|
<div
|
||||||
|
class="flex flex-col rounded-md border border-border bg-background px-3 py-2.5"
|
||||||
|
[style.height]="showConnectionError() ? null : 'calc(100px - 0.75rem)'"
|
||||||
|
>
|
||||||
<!-- Connection Error Banner -->
|
<!-- Connection Error Banner -->
|
||||||
@if (showConnectionError()) {
|
@if (showConnectionError()) {
|
||||||
<div class="mb-3 p-2 bg-destructive/20 border border-destructive/30 rounded-lg flex items-center gap-2">
|
<div class="mb-3 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 p-2">
|
||||||
<span class="w-2 h-2 rounded-full bg-destructive animate-pulse"></span>
|
<span class="w-2 h-2 rounded-full bg-destructive animate-pulse"></span>
|
||||||
<span class="text-xs text-destructive">{{ connectionErrorMessage() || 'Connection error' }}</span>
|
<span class="text-xs text-destructive">{{ connectionErrorMessage() || 'Connection error' }}</span>
|
||||||
<button
|
<button
|
||||||
@@ -15,7 +18,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- User Info -->
|
<!-- User Info -->
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="mb-2 flex items-center gap-3">
|
||||||
<app-user-avatar
|
<app-user-avatar
|
||||||
[name]="currentUser()?.displayName || '?'"
|
[name]="currentUser()?.displayName || '?'"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -24,15 +27,15 @@
|
|||||||
<p class="font-medium text-sm text-foreground truncate">
|
<p class="font-medium text-sm text-foreground truncate">
|
||||||
{{ currentUser()?.displayName || 'Unknown' }}
|
{{ currentUser()?.displayName || 'Unknown' }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-muted-foreground">
|
@if (showConnectionError() || isConnected()) {
|
||||||
@if (showConnectionError()) {
|
<p class="text-xs text-muted-foreground">
|
||||||
<span class="text-destructive">● Connection Error</span>
|
@if (showConnectionError()) {
|
||||||
} @else if (isConnected()) {
|
<span class="text-destructive">Connection Error</span>
|
||||||
<span class="text-green-500">● Connected</span>
|
} @else if (isConnected()) {
|
||||||
} @else {
|
<span class="text-green-500">Connected</span>
|
||||||
<span class="text-muted-foreground">● Disconnected</span>
|
}
|
||||||
}
|
</p>
|
||||||
</p>
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<app-debug-console
|
<app-debug-console
|
||||||
@@ -42,7 +45,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="toggleSettings()"
|
(click)="toggleSettings()"
|
||||||
class="p-2 hover:bg-secondary rounded-lg transition-colors"
|
class="rounded-md p-2 transition-colors hover:bg-secondary"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucideSettings"
|
name="lucideSettings"
|
||||||
@@ -53,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Voice Controls -->
|
<!-- Voice Controls -->
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="mt-auto flex items-center justify-center gap-2">
|
||||||
@if (isConnected()) {
|
@if (isConnected()) {
|
||||||
<!-- Mute Toggle -->
|
<!-- Mute Toggle -->
|
||||||
<button
|
<button
|
||||||
@@ -128,7 +131,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="disconnect()"
|
(click)="disconnect()"
|
||||||
class="w-10 h-10 inline-flex items-center justify-center bg-destructive text-destructive-foreground rounded-full hover:bg-destructive/90 transition-colors"
|
class="inline-flex h-10 w-10 items-center justify-center rounded-md border border-destructive/20 bg-destructive/10 text-destructive transition-colors hover:bg-destructive/15"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucidePhoneOff"
|
name="lucidePhoneOff"
|
||||||
|
|||||||
@@ -585,45 +585,45 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
getMuteButtonClass(): string {
|
getMuteButtonClass(): string {
|
||||||
const base =
|
const base =
|
||||||
'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
|
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
|
||||||
|
|
||||||
if (this.isMuted()) {
|
if (this.isMuted()) {
|
||||||
return `${base} bg-destructive/20 text-destructive hover:bg-destructive/30`;
|
return `${base} border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${base} bg-secondary text-foreground hover:bg-secondary/80`;
|
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDeafenButtonClass(): string {
|
getDeafenButtonClass(): string {
|
||||||
const base =
|
const base =
|
||||||
'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
|
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
|
||||||
|
|
||||||
if (this.isDeafened()) {
|
if (this.isDeafened()) {
|
||||||
return `${base} bg-destructive/20 text-destructive hover:bg-destructive/30`;
|
return `${base} border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${base} bg-secondary text-foreground hover:bg-secondary/80`;
|
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCameraButtonClass(): string {
|
getCameraButtonClass(): string {
|
||||||
const base =
|
const base =
|
||||||
'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
|
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
|
||||||
|
|
||||||
if (this.isCameraEnabled()) {
|
if (this.isCameraEnabled()) {
|
||||||
return `${base} bg-primary/20 text-primary hover:bg-primary/30`;
|
return `${base} border-primary/20 bg-primary/10 text-primary hover:bg-primary/15`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${base} bg-secondary text-foreground hover:bg-secondary/80`;
|
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScreenShareButtonClass(): string {
|
getScreenShareButtonClass(): string {
|
||||||
const base =
|
const base =
|
||||||
'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
|
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
|
||||||
|
|
||||||
if (this.isScreenSharing()) {
|
if (this.isScreenSharing()) {
|
||||||
return `${base} bg-primary/20 text-primary hover:bg-primary/30`;
|
return `${base} border-primary/20 bg-primary/10 text-primary hover:bg-primary/15`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${base} bg-secondary text-foreground hover:bg-secondary/80`;
|
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,16 @@
|
|||||||
<div class="h-full flex flex-col bg-background">
|
<div class="flex h-full flex-col bg-background">
|
||||||
@if (currentRoom()) {
|
@if (currentRoom()) {
|
||||||
<!-- Channel header bar -->
|
|
||||||
@if (!isVoiceWorkspaceExpanded()) {
|
|
||||||
<div class="h-12 flex items-center gap-2 px-4 border-b border-border bg-card flex-shrink-0">
|
|
||||||
<ng-icon
|
|
||||||
[name]="isVoiceWorkspaceExpanded() ? 'lucideMonitor' : 'lucideHash'"
|
|
||||||
class="w-4 h-4 text-muted-foreground"
|
|
||||||
/>
|
|
||||||
<span class="font-medium text-foreground text-sm">{{ headerTitle() }}</span>
|
|
||||||
|
|
||||||
@if (isVoiceWorkspaceExpanded()) {
|
|
||||||
<span class="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.2em] text-primary">
|
|
||||||
Voice streams
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="flex-1"></div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="flex-1 flex overflow-hidden">
|
<div class="flex min-h-0 flex-1 overflow-hidden">
|
||||||
|
<aside class="flex min-h-0 w-[17rem] shrink-0 overflow-hidden border-r border-border bg-card">
|
||||||
|
<app-rooms-side-panel
|
||||||
|
panelMode="channels"
|
||||||
|
class="block h-full w-full"
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<!-- Chat Area -->
|
<!-- Chat Area -->
|
||||||
<main class="relative flex-1 min-w-0">
|
<main class="relative min-h-0 min-w-0 flex-1 overflow-hidden bg-background">
|
||||||
@if (!isVoiceWorkspaceExpanded()) {
|
@if (!isVoiceWorkspaceExpanded()) {
|
||||||
@if (hasTextChannels()) {
|
@if (hasTextChannels()) {
|
||||||
<div class="h-full overflow-hidden">
|
<div class="h-full overflow-hidden">
|
||||||
@@ -45,20 +33,23 @@
|
|||||||
<app-voice-workspace />
|
<app-voice-workspace />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Sidebar always visible -->
|
<aside class="flex min-h-0 w-[17rem] shrink-0 overflow-hidden border-l border-border bg-card">
|
||||||
<aside class="w-80 flex-shrink-0 border-l border-border">
|
<app-rooms-side-panel
|
||||||
<app-rooms-side-panel class="h-full" />
|
panelMode="users"
|
||||||
|
[showVoiceControls]="false"
|
||||||
|
class="block h-full w-full"
|
||||||
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<!-- No Room Selected -->
|
<!-- No Room Selected -->
|
||||||
<div class="flex-1 flex items-center justify-center">
|
<div class="flex flex-1 items-center justify-center bg-background px-6">
|
||||||
<div class="text-center text-muted-foreground">
|
<div class="text-center text-muted-foreground">
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucideHash"
|
name="lucideHash"
|
||||||
class="w-16 h-16 mx-auto mb-4 opacity-30"
|
class="mx-auto mb-4 h-16 w-16 opacity-30"
|
||||||
/>
|
/>
|
||||||
<h2 class="text-xl font-medium mb-2">No room selected</h2>
|
<h2 class="mb-2 text-xl font-medium">No room selected</h2>
|
||||||
<p class="text-sm">Select or create a room to start chatting</p>
|
<p class="text-sm">Select or create a room to start chatting</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,12 +24,10 @@ import { VoiceWorkspaceComponent } from '../voice-workspace/voice-workspace.comp
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
selectCurrentRoom,
|
selectCurrentRoom,
|
||||||
selectActiveChannelId,
|
selectTextChannels
|
||||||
selectTextChannels,
|
|
||||||
selectVoiceChannels
|
|
||||||
} from '../../../store/rooms/rooms.selectors';
|
} from '../../../store/rooms/rooms.selectors';
|
||||||
import { SettingsModalService } from '../../../core/services/settings-modal.service';
|
import { SettingsModalService } from '../../../core/services/settings-modal.service';
|
||||||
import { selectCurrentUser, selectIsCurrentUserAdmin } from '../../../store/users/users.selectors';
|
import { selectIsCurrentUserAdmin } from '../../../store/users/users.selectors';
|
||||||
import { VoiceWorkspaceService } from '../../../domains/voice-session';
|
import { VoiceWorkspaceService } from '../../../domains/voice-session';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -59,47 +57,18 @@ import { VoiceWorkspaceService } from '../../../domains/voice-session';
|
|||||||
* Main chat room view combining the messages panel, side panels, and admin controls.
|
* Main chat room view combining the messages panel, side panels, and admin controls.
|
||||||
*/
|
*/
|
||||||
export class ChatRoomComponent {
|
export class ChatRoomComponent {
|
||||||
private store = inject(Store);
|
private readonly store = inject(Store);
|
||||||
private settingsModal = inject(SettingsModalService);
|
private readonly settingsModal = inject(SettingsModalService);
|
||||||
private voiceWorkspace = inject(VoiceWorkspaceService);
|
private voiceWorkspace = inject(VoiceWorkspaceService);
|
||||||
showMenu = signal(false);
|
showMenu = signal(false);
|
||||||
showAdminPanel = signal(false);
|
showAdminPanel = signal(false);
|
||||||
|
|
||||||
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||||
isAdmin = this.store.selectSignal(selectIsCurrentUserAdmin);
|
isAdmin = this.store.selectSignal(selectIsCurrentUserAdmin);
|
||||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
|
||||||
activeChannelId = this.store.selectSignal(selectActiveChannelId);
|
|
||||||
textChannels = this.store.selectSignal(selectTextChannels);
|
textChannels = this.store.selectSignal(selectTextChannels);
|
||||||
voiceChannels = this.store.selectSignal(selectVoiceChannels);
|
|
||||||
isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
|
isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
|
||||||
hasTextChannels = computed(() => this.textChannels().length > 0);
|
hasTextChannels = computed(() => this.textChannels().length > 0);
|
||||||
|
|
||||||
activeTextChannelName = computed(() => {
|
|
||||||
const textChannels = this.textChannels();
|
|
||||||
|
|
||||||
if (textChannels.length === 0) {
|
|
||||||
return 'No text channels';
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = this.activeChannelId();
|
|
||||||
const activeChannel = textChannels.find((channel) => channel.id === id) ?? textChannels[0];
|
|
||||||
|
|
||||||
return activeChannel ? activeChannel.name : id;
|
|
||||||
});
|
|
||||||
|
|
||||||
connectedVoiceChannelName = computed(() => {
|
|
||||||
const voiceChannelId = this.currentUser()?.voiceState?.roomId;
|
|
||||||
const voiceChannel = this.voiceChannels().find((channel) => channel.id === voiceChannelId);
|
|
||||||
|
|
||||||
return voiceChannel?.name || 'Voice Lounge';
|
|
||||||
});
|
|
||||||
|
|
||||||
headerTitle = computed(() =>
|
|
||||||
this.isVoiceWorkspaceExpanded()
|
|
||||||
? this.connectedVoiceChannelName()
|
|
||||||
: this.activeTextChannelName()
|
|
||||||
);
|
|
||||||
|
|
||||||
/** Open the settings modal to the Server admin page for the current room. */
|
/** Open the settings modal to the Server admin page for the current room. */
|
||||||
toggleAdminPanel() {
|
toggleAdminPanel() {
|
||||||
const room = this.currentRoom();
|
const room = this.currentRoom();
|
||||||
|
|||||||
@@ -1,54 +1,45 @@
|
|||||||
<!-- eslint-disable @angular-eslint/template/button-has-type, @angular-eslint/template/cyclomatic-complexity -->
|
<!-- eslint-disable @angular-eslint/template/button-has-type, @angular-eslint/template/cyclomatic-complexity -->
|
||||||
<aside class="w-80 bg-card h-full flex flex-col">
|
<aside class="flex h-full min-h-0 flex-col bg-card">
|
||||||
<!-- Minimalistic header with tabs -->
|
<div class="border-b border-border px-3 py-3">
|
||||||
<div class="border-b border-border">
|
@if (panelMode() === 'channels') {
|
||||||
<div class="flex items-center">
|
<div class="flex items-center gap-3">
|
||||||
<!-- Tab buttons -->
|
<div class="grid h-9 w-9 place-items-center rounded-md bg-secondary text-sm font-semibold text-foreground">
|
||||||
<button
|
{{ currentRoom()?.name?.charAt(0)?.toUpperCase() || '#' }}
|
||||||
(click)="activeTab.set('channels')"
|
</div>
|
||||||
class="flex-1 flex items-center justify-center gap-1.5 px-3 py-3 text-sm transition-colors border-b-2"
|
|
||||||
[class.border-primary]="activeTab() === 'channels'"
|
<div class="min-w-0 flex-1">
|
||||||
[class.text-foreground]="activeTab() === 'channels'"
|
<p class="truncate text-sm font-semibold text-foreground">{{ currentRoom()?.name || 'Server' }}</p>
|
||||||
[class.border-transparent]="activeTab() !== 'channels'"
|
<p class="truncate text-xs text-muted-foreground">{{ currentRoom()?.description || 'Choose a text channel or jump into voice.' }}</p>
|
||||||
[class.text-muted-foreground]="activeTab() !== 'channels'"
|
</div>
|
||||||
[class.hover:text-foreground]="activeTab() !== 'channels'"
|
</div>
|
||||||
>
|
} @else {
|
||||||
<ng-icon
|
<div class="flex items-center gap-3">
|
||||||
name="lucideHash"
|
<div class="grid h-9 w-9 place-items-center rounded-md bg-secondary text-muted-foreground">
|
||||||
class="w-4 h-4"
|
<ng-icon
|
||||||
/>
|
name="lucideUsers"
|
||||||
<span>Channels</span>
|
class="h-4 w-4"
|
||||||
</button>
|
/>
|
||||||
<button
|
</div>
|
||||||
(click)="activeTab.set('users')"
|
|
||||||
class="flex-1 flex items-center justify-center gap-1.5 px-3 py-3 text-sm transition-colors border-b-2"
|
<div class="min-w-0 flex-1">
|
||||||
[class.border-primary]="activeTab() === 'users'"
|
<p class="text-sm font-semibold text-foreground">{{ knownUserCount() }} members</p>
|
||||||
[class.text-foreground]="activeTab() === 'users'"
|
<p class="text-xs text-muted-foreground">{{ onlineRoomUsers().length + (currentUser() ? 1 : 0) }} online right now</p>
|
||||||
[class.border-transparent]="activeTab() !== 'users'"
|
</div>
|
||||||
[class.text-muted-foreground]="activeTab() !== 'users'"
|
</div>
|
||||||
[class.hover:text-foreground]="activeTab() !== 'users'"
|
}
|
||||||
>
|
|
||||||
<ng-icon
|
|
||||||
name="lucideUsers"
|
|
||||||
class="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<span>Users</span>
|
|
||||||
<span class="text-xs px-1.5 py-0.5 rounded-full bg-primary/15 text-primary">{{ knownUserCount() }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Channels View -->
|
<!-- Channels View -->
|
||||||
@if (activeTab() === 'channels') {
|
@if (panelMode() === 'channels') {
|
||||||
<div class="flex-1 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
<!-- Text Channels -->
|
<!-- Text Channels -->
|
||||||
<div class="p-3">
|
<section class="px-2 py-3">
|
||||||
<div class="flex items-center justify-between mb-2 px-1">
|
<div class="mb-2 flex items-center justify-between px-1">
|
||||||
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium">Text Channels</h4>
|
<h4 class="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">Text Channels</h4>
|
||||||
@if (canManageChannels()) {
|
@if (canManageChannels()) {
|
||||||
<button
|
<button
|
||||||
(click)="createChannel('text')"
|
(click)="createChannel('text')"
|
||||||
class="text-muted-foreground hover:text-foreground transition-colors"
|
class="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||||
title="Create Text Channel"
|
title="Create Text Channel"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -58,15 +49,15 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-1">
|
||||||
@for (ch of textChannels(); track ch.id) {
|
@for (ch of textChannels(); track ch.id) {
|
||||||
<button
|
<button
|
||||||
class="w-full px-2 py-1.5 text-sm rounded flex items-center gap-2 text-left transition-colors"
|
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors"
|
||||||
[class.bg-secondary]="activeChannelId() === ch.id"
|
[class.bg-secondary]="activeChannelId() === ch.id"
|
||||||
[class.text-foreground]="activeChannelId() === ch.id"
|
[class.text-foreground]="activeChannelId() === ch.id"
|
||||||
[class.font-medium]="activeChannelId() === ch.id"
|
[class.font-medium]="activeChannelId() === ch.id"
|
||||||
[class.text-foreground/60]="activeChannelId() !== ch.id"
|
[class.text-foreground/60]="activeChannelId() !== ch.id"
|
||||||
[class.hover:bg-secondary/60]="activeChannelId() !== ch.id"
|
[class.hover:bg-secondary/70]="activeChannelId() !== ch.id"
|
||||||
[class.hover:text-foreground/80]="activeChannelId() !== ch.id"
|
[class.hover:text-foreground/80]="activeChannelId() !== ch.id"
|
||||||
(click)="selectTextChannel(ch.id)"
|
(click)="selectTextChannel(ch.id)"
|
||||||
(contextmenu)="openChannelContextMenu($event, ch)"
|
(contextmenu)="openChannelContextMenu($event, ch)"
|
||||||
@@ -83,7 +74,7 @@
|
|||||||
(keydown.escape)="cancelRename()"
|
(keydown.escape)="cancelRename()"
|
||||||
(blur)="confirmRename($event)"
|
(blur)="confirmRename($event)"
|
||||||
(input)="clearChannelNameError()"
|
(input)="clearChannelNameError()"
|
||||||
class="flex-1 bg-secondary border border-border rounded px-1 py-0.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
class="flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -98,16 +89,16 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- Voice Channels -->
|
<!-- Voice Channels -->
|
||||||
<div class="p-3 pt-0">
|
<section class="border-t border-border px-2 py-3">
|
||||||
<div class="flex items-center justify-between mb-2 px-1">
|
<div class="mb-2 flex items-center justify-between px-1">
|
||||||
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium">Voice Channels</h4>
|
<h4 class="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">Voice Channels</h4>
|
||||||
@if (canManageChannels()) {
|
@if (canManageChannels()) {
|
||||||
<button
|
<button
|
||||||
(click)="createChannel('voice')"
|
(click)="createChannel('voice')"
|
||||||
class="text-muted-foreground hover:text-foreground transition-colors"
|
class="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||||
title="Create Voice Channel"
|
title="Create Voice Channel"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -118,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (!voiceEnabled()) {
|
@if (!voiceEnabled()) {
|
||||||
<p class="text-sm text-muted-foreground px-2 py-2">Voice is disabled by host</p>
|
<p class="px-2 py-2 text-sm text-muted-foreground">Voice is disabled by host</p>
|
||||||
}
|
}
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
@for (ch of voiceChannels(); track ch.id) {
|
@for (ch of voiceChannels(); track ch.id) {
|
||||||
@@ -132,10 +123,10 @@
|
|||||||
(drop)="onVoiceChannelDrop($event, ch.id)"
|
(drop)="onVoiceChannelDrop($event, ch.id)"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="w-full px-2 py-1.5 text-sm rounded hover:bg-secondary/60 flex items-center justify-between text-left transition-colors"
|
class="flex w-full items-center justify-between rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-secondary/60"
|
||||||
(click)="joinVoice(ch.id)"
|
(click)="joinVoice(ch.id)"
|
||||||
(contextmenu)="openChannelContextMenu($event, ch)"
|
(contextmenu)="openChannelContextMenu($event, ch)"
|
||||||
[class.bg-secondary/40]="isCurrentRoom(ch.id)"
|
[class.bg-secondary]="isCurrentRoom(ch.id)"
|
||||||
[disabled]="!voiceEnabled()"
|
[disabled]="!voiceEnabled()"
|
||||||
[title]="isCurrentRoom(ch.id) ? 'Open stream workspace' : 'Join voice channel'"
|
[title]="isCurrentRoom(ch.id) ? 'Open stream workspace' : 'Join voice channel'"
|
||||||
>
|
>
|
||||||
@@ -155,7 +146,7 @@
|
|||||||
(keydown.escape)="cancelRename()"
|
(keydown.escape)="cancelRename()"
|
||||||
(blur)="confirmRename($event)"
|
(blur)="confirmRename($event)"
|
||||||
(input)="clearChannelNameError()"
|
(input)="clearChannelNameError()"
|
||||||
class="flex-1 bg-secondary border border-border rounded px-1 py-0.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
class="flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -173,10 +164,10 @@
|
|||||||
</button>
|
</button>
|
||||||
<!-- Voice users connected to this channel -->
|
<!-- Voice users connected to this channel -->
|
||||||
@if (voiceUsersInRoom(ch.id).length > 0) {
|
@if (voiceUsersInRoom(ch.id).length > 0) {
|
||||||
<div class="ml-5 mt-1 space-y-1">
|
<div class="ml-5 mt-1 space-y-1 border-l border-border pb-1 pl-2">
|
||||||
@for (u of voiceUsersInRoom(ch.id); track u.id) {
|
@for (u of voiceUsersInRoom(ch.id); track u.id) {
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-secondary/40"
|
class="flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-secondary/50"
|
||||||
[class.cursor-pointer]="canDragVoiceUser(u)"
|
[class.cursor-pointer]="canDragVoiceUser(u)"
|
||||||
[class.opacity-60]="draggedVoiceUserId() === (u.id || u.oderId)"
|
[class.opacity-60]="draggedVoiceUserId() === (u.id || u.oderId)"
|
||||||
[draggable]="canDragVoiceUser(u)"
|
[draggable]="canDragVoiceUser(u)"
|
||||||
@@ -239,18 +230,18 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Users View -->
|
<!-- Users View -->
|
||||||
@if (activeTab() === 'users') {
|
@if (panelMode() === 'users') {
|
||||||
<div class="flex-1 overflow-auto p-3">
|
<div class="flex-1 overflow-auto px-2 py-3">
|
||||||
<!-- Current User (You) -->
|
<!-- Current User (You) -->
|
||||||
@if (currentUser()) {
|
@if (currentUser()) {
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2 px-1">You</h4>
|
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2 px-1">You</h4>
|
||||||
<div class="flex items-center gap-2 px-2 py-1.5 rounded bg-secondary/30">
|
<div class="flex items-center gap-2 rounded-md bg-secondary/60 px-3 py-2">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<app-user-avatar
|
<app-user-avatar
|
||||||
[name]="currentUser()?.displayName || '?'"
|
[name]="currentUser()?.displayName || '?'"
|
||||||
@@ -296,7 +287,7 @@
|
|||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
@for (user of onlineRoomUsers(); track user.id) {
|
@for (user of onlineRoomUsers(); track user.id) {
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-secondary/40 group/user"
|
class="group/user flex items-center gap-2 rounded-md px-3 py-2 transition-colors hover:bg-secondary/50"
|
||||||
(contextmenu)="openUserContextMenu($event, user)"
|
(contextmenu)="openUserContextMenu($event, user)"
|
||||||
>
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -354,7 +345,7 @@
|
|||||||
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2 px-1">Offline - {{ offlineRoomMembers().length }}</h4>
|
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2 px-1">Offline - {{ offlineRoomMembers().length }}</h4>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
@for (member of offlineRoomMembers(); track member.oderId || member.id) {
|
@for (member of offlineRoomMembers(); track member.oderId || member.id) {
|
||||||
<div class="flex items-center gap-2 px-2 py-1.5 rounded opacity-80">
|
<div class="flex items-center gap-2 rounded-md px-3 py-2 opacity-80">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<app-user-avatar
|
<app-user-avatar
|
||||||
[name]="member.displayName"
|
[name]="member.displayName"
|
||||||
@@ -392,15 +383,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- Voice controls pinned to sidebar bottom (hidden when floating controls visible) -->
|
<!-- Voice controls pinned to sidebar bottom (hidden when floating controls visible) -->
|
||||||
@if (voiceEnabled()) {
|
@if (panelMode() === 'channels' && showVoiceControls() && voiceEnabled()) {
|
||||||
<div [class.invisible]="showFloatingControls()">
|
<div
|
||||||
|
class="border-t border-border px-2 py-3"
|
||||||
|
[class.invisible]="showFloatingControls()"
|
||||||
|
>
|
||||||
<app-voice-controls />
|
<app-voice-controls />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Channel context menu -->
|
<!-- Channel context menu -->
|
||||||
@if (showChannelMenu()) {
|
@if (panelMode() === 'channels' && showChannelMenu()) {
|
||||||
<app-context-menu
|
<app-context-menu
|
||||||
[x]="channelMenuX()"
|
[x]="channelMenuX()"
|
||||||
[y]="channelMenuY()"
|
[y]="channelMenuY()"
|
||||||
@@ -440,7 +434,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- User context menu (kick / role management) -->
|
<!-- User context menu (kick / role management) -->
|
||||||
@if (showUserMenu()) {
|
@if (panelMode() === 'users' && showUserMenu()) {
|
||||||
<app-context-menu
|
<app-context-menu
|
||||||
[x]="userMenuX()"
|
[x]="userMenuX()"
|
||||||
[y]="userMenuY()"
|
[y]="userMenuY()"
|
||||||
@@ -497,7 +491,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- Per-user volume context menu -->
|
<!-- Per-user volume context menu -->
|
||||||
@if (showVolumeMenu()) {
|
@if (panelMode() === 'channels' && showVolumeMenu()) {
|
||||||
<app-user-volume-menu
|
<app-user-volume-menu
|
||||||
[x]="volumeMenuX()"
|
[x]="volumeMenuX()"
|
||||||
[y]="volumeMenuY()"
|
[y]="volumeMenuY()"
|
||||||
@@ -508,7 +502,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- Create channel dialog -->
|
<!-- Create channel dialog -->
|
||||||
@if (showCreateChannelDialog()) {
|
@if (panelMode() === 'channels' && showCreateChannelDialog()) {
|
||||||
<app-confirm-dialog
|
<app-confirm-dialog
|
||||||
[title]="'Create ' + (createChannelType() === 'text' ? 'Text' : 'Voice') + ' Channel'"
|
[title]="'Create ' + (createChannelType() === 'text' ? 'Text' : 'Voice') + ' Channel'"
|
||||||
confirmLabel="Create"
|
confirmLabel="Create"
|
||||||
@@ -519,7 +513,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
[(ngModel)]="newChannelName"
|
[(ngModel)]="newChannelName"
|
||||||
placeholder="Channel name"
|
placeholder="Channel name"
|
||||||
class="w-full px-3 py-2 bg-secondary rounded border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary text-sm"
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
[class.border-destructive]="!!channelNameError()"
|
[class.border-destructive]="!!channelNameError()"
|
||||||
(ngModelChange)="clearChannelNameError()"
|
(ngModelChange)="clearChannelNameError()"
|
||||||
(keydown.enter)="confirmCreateChannel()"
|
(keydown.enter)="confirmCreateChannel()"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
inject,
|
inject,
|
||||||
computed,
|
computed,
|
||||||
|
input,
|
||||||
signal
|
signal
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
@@ -58,7 +59,7 @@ import {
|
|||||||
} from '../../../shared-kernel';
|
} from '../../../shared-kernel';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
type TabView = 'channels' | 'users';
|
type PanelMode = 'channels' | 'users';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-rooms-side-panel',
|
selector: 'app-rooms-side-panel',
|
||||||
@@ -100,7 +101,8 @@ export class RoomsSidePanelComponent {
|
|||||||
private voicePlayback = inject(VoicePlaybackService);
|
private voicePlayback = inject(VoicePlaybackService);
|
||||||
voiceActivity = inject(VoiceActivityService);
|
voiceActivity = inject(VoiceActivityService);
|
||||||
|
|
||||||
activeTab = signal<TabView>('channels');
|
readonly panelMode = input<PanelMode>('channels');
|
||||||
|
readonly showVoiceControls = input(true);
|
||||||
showFloatingControls = this.voiceSessionService.showFloatingControls;
|
showFloatingControls = this.voiceSessionService.showFloatingControls;
|
||||||
isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
|
isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
|
||||||
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<nav class="h-full w-16 flex flex-col items-center gap-3 py-3 border-r border-border bg-card relative">
|
<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 -->
|
<!-- Create button -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-10 h-10 rounded-2xl flex items-center justify-center bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
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"
|
title="Create Server"
|
||||||
(click)="createServer()"
|
(click)="createServer()"
|
||||||
>
|
>
|
||||||
@@ -13,22 +13,21 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Saved servers icons -->
|
<!-- Saved servers icons -->
|
||||||
<div class="flex-1 w-full overflow-y-auto flex flex-col items-center gap-2 mt-2">
|
<div class="no-scrollbar mt-2 flex w-full flex-1 flex-col items-center gap-2 overflow-y-auto">
|
||||||
@for (room of visibleSavedRooms(); track room.id) {
|
@for (room of visibleSavedRooms(); track room.id) {
|
||||||
<div class="relative flex w-full justify-center pl-2">
|
<div class="relative flex w-full justify-center">
|
||||||
@if (isSelectedRoom(room)) {
|
@if (isSelectedRoom(room)) {
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="absolute left-1 top-1/2 h-7 w-1 -translate-y-1/2 rounded-full bg-primary shadow-[0_0_10px_rgba(59,130,246,0.45)]"
|
class="pointer-events-none absolute left-0 top-1/2 h-10 w-1 -translate-y-1/2 rounded-l-full bg-primary"
|
||||||
></span>
|
></span>
|
||||||
}
|
}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="relative w-10 h-10 flex-shrink-0 rounded-2xl border border-border hover:border-primary/60 hover:shadow-sm transition-all"
|
class="relative z-10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md border border-transparent bg-card transition-colors hover:border-border hover:bg-card"
|
||||||
[class.border-primary]="isSelectedRoom(room)"
|
[class.border-primary/30]="isSelectedRoom(room)"
|
||||||
[class.shadow-[0_0_0_1px_rgba(59,130,246,0.45),0_10px_20px_rgba(15,23,42,0.25)]]="isSelectedRoom(room)"
|
[class.bg-primary/10]="isSelectedRoom(room)"
|
||||||
[class.scale-105]="isSelectedRoom(room)"
|
|
||||||
[title]="room.name"
|
[title]="room.name"
|
||||||
[attr.aria-current]="isSelectedRoom(room) ? 'page' : null"
|
[attr.aria-current]="isSelectedRoom(room) ? 'page' : null"
|
||||||
(click)="joinSavedRoom(room)"
|
(click)="joinSavedRoom(room)"
|
||||||
@@ -39,17 +38,18 @@
|
|||||||
<img
|
<img
|
||||||
[ngSrc]="room.icon"
|
[ngSrc]="room.icon"
|
||||||
[alt]="room.name"
|
[alt]="room.name"
|
||||||
class="w-full h-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<div
|
<div
|
||||||
class="w-full h-full flex items-center justify-center bg-secondary transition-colors"
|
class="flex h-full w-full items-center justify-center bg-secondary transition-colors"
|
||||||
[class.bg-primary/15]="isSelectedRoom(room)"
|
[class.bg-primary/15]="isSelectedRoom(room)"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="text-sm font-semibold text-muted-foreground transition-colors"
|
class="text-sm font-semibold text-muted-foreground transition-colors"
|
||||||
[class.text-foreground]="isSelectedRoom(room)"
|
[class.text-foreground]="isSelectedRoom(room)"
|
||||||
>{{ initial(room.name) }}</span>
|
>{{ initial(room.name) }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -59,6 +59,23 @@
|
|||||||
{{ formatUnreadCount(roomUnreadCount(room.id)) }}
|
{{ formatUnreadCount(roomUnreadCount(room.id)) }}
|
||||||
</span>
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ import {
|
|||||||
import { Room, User } from '../../shared-kernel';
|
import { Room, User } from '../../shared-kernel';
|
||||||
import { VoiceSessionFacade } from '../../domains/voice-session';
|
import { VoiceSessionFacade } from '../../domains/voice-session';
|
||||||
import { selectSavedRooms, selectCurrentRoom } from '../../store/rooms/rooms.selectors';
|
import { selectSavedRooms, selectCurrentRoom } from '../../store/rooms/rooms.selectors';
|
||||||
import { selectCurrentUser } from '../../store/users/users.selectors';
|
import {
|
||||||
|
selectCurrentUser,
|
||||||
|
selectOnlineUsers
|
||||||
|
} from '../../store/users/users.selectors';
|
||||||
import { RoomsActions } from '../../store/rooms/rooms.actions';
|
import { RoomsActions } from '../../store/rooms/rooms.actions';
|
||||||
import { DatabaseService } from '../../infrastructure/persistence';
|
import { DatabaseService } from '../../infrastructure/persistence';
|
||||||
import { NotificationsFacade } from '../../domains/notifications';
|
import { NotificationsFacade } from '../../domains/notifications';
|
||||||
@@ -74,6 +77,7 @@ export class ServersRailComponent {
|
|||||||
contextRoom = signal<Room | null>(null);
|
contextRoom = signal<Room | null>(null);
|
||||||
showLeaveConfirm = signal(false);
|
showLeaveConfirm = signal(false);
|
||||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
currentUser = this.store.selectSignal(selectCurrentUser);
|
||||||
|
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
||||||
bannedRoomLookup = signal<Record<string, boolean>>({});
|
bannedRoomLookup = signal<Record<string, boolean>>({});
|
||||||
bannedServerName = signal('');
|
bannedServerName = signal('');
|
||||||
showBannedDialog = signal(false);
|
showBannedDialog = signal(false);
|
||||||
@@ -82,6 +86,46 @@ export class ServersRailComponent {
|
|||||||
joinPassword = signal('');
|
joinPassword = signal('');
|
||||||
joinPasswordError = signal<string | null>(null);
|
joinPasswordError = signal<string | null>(null);
|
||||||
visibleSavedRooms = computed(() => this.savedRooms().filter((room) => !this.isRoomMarkedBanned(room)));
|
visibleSavedRooms = computed(() => this.savedRooms().filter((room) => !this.isRoomMarkedBanned(room)));
|
||||||
|
voicePresenceByRoom = computed(() => {
|
||||||
|
const presence: Record<string, number> = {};
|
||||||
|
const seenByRoom = new Map<string, Set<string>>();
|
||||||
|
|
||||||
|
const addVoicePresence = (user: User | null | undefined): void => {
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const voiceState = user?.voiceState;
|
||||||
|
const roomId = voiceState?.serverId;
|
||||||
|
|
||||||
|
if (!voiceState?.isConnected || !roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userKey = user.oderId || user.id;
|
||||||
|
let seenUsers = seenByRoom.get(roomId);
|
||||||
|
|
||||||
|
if (!seenUsers) {
|
||||||
|
seenUsers = new Set<string>();
|
||||||
|
seenByRoom.set(roomId, seenUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seenUsers.has(userKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenUsers.add(userKey);
|
||||||
|
presence[roomId] = (presence[roomId] ?? 0) + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const user of this.onlineUsers()) {
|
||||||
|
addVoicePresence(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
addVoicePresence(this.currentUser());
|
||||||
|
|
||||||
|
return presence;
|
||||||
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@@ -236,6 +280,10 @@ export class ServersRailComponent {
|
|||||||
return this.notifications.roomUnreadCount(roomId);
|
return this.notifications.roomUnreadCount(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
voicePresenceCount(roomId: string): number {
|
||||||
|
return this.voicePresenceByRoom()[roomId] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
formatUnreadCount(count: number): string {
|
formatUnreadCount(count: number): string {
|
||||||
return count > 99 ? '99+' : String(count);
|
return count > 99 ? '99+' : String(count);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div
|
<div
|
||||||
class="fixed top-0 left-16 right-0 h-10 bg-card border-b border-border flex items-center justify-between px-4 z-50 select-none"
|
class="fixed left-16 right-0 top-0 z-50 flex h-10 items-center justify-between border-b border-border bg-card px-4 select-none"
|
||||||
style="-webkit-app-region: drag"
|
style="-webkit-app-region: drag"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
name="lucideHash"
|
name="lucideHash"
|
||||||
class="w-5 h-5 text-muted-foreground"
|
class="w-5 h-5 text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<span class="text-sm font-semibold text-foreground truncate">{{ roomName() }}</span>
|
<span class="truncate text-sm font-semibold text-foreground">{{ roomContextTitle() }}</span>
|
||||||
|
|
||||||
@if (showRoomCompatibilityNotice()) {
|
@if (showRoomCompatibilityNotice()) {
|
||||||
<span class="inline-flex items-center gap-1 rounded bg-destructive/15 px-2 py-0.5 text-xs text-destructive">
|
<span class="inline-flex items-center gap-1 rounded bg-destructive/15 px-2 py-0.5 text-xs text-destructive">
|
||||||
@@ -29,9 +29,9 @@
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (roomDescription()) {
|
@if (roomContextMeta()) {
|
||||||
<span class="hidden md:inline text-sm text-muted-foreground border-l border-border pl-2 truncate">
|
<span class="hidden truncate border-l border-border/70 pl-2 text-xs text-muted-foreground md:inline">
|
||||||
{{ roomDescription() }}
|
{{ roomContextMeta() }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
} @else {
|
} @else {
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="px-3 h-8 grid place-items-center hover:bg-secondary rounded text-sm text-foreground"
|
class="grid h-8 place-items-center rounded-md px-3 text-sm text-foreground transition-colors hover:bg-secondary"
|
||||||
[class.hidden]="isAuthed()"
|
[class.hidden]="isAuthed()"
|
||||||
(click)="goLogin()"
|
(click)="goLogin()"
|
||||||
title="Login"
|
title="Login"
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="toggleMenu()"
|
(click)="toggleMenu()"
|
||||||
class="ml-2 p-2 hover:bg-secondary rounded"
|
class="ml-2 rounded-md p-2 transition-colors hover:bg-secondary"
|
||||||
title="Menu"
|
title="Menu"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -72,13 +72,13 @@
|
|||||||
</button>
|
</button>
|
||||||
<!-- Anchored dropdown under the menu button -->
|
<!-- Anchored dropdown under the menu button -->
|
||||||
@if (showMenu()) {
|
@if (showMenu()) {
|
||||||
<div class="absolute right-0 top-full mt-1 z-50 w-64 rounded-lg border border-border bg-card shadow-lg">
|
<div class="absolute right-0 top-full z-50 mt-2 w-64 rounded-md border border-border bg-popover p-1 shadow-lg">
|
||||||
@if (inRoom()) {
|
@if (inRoom()) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="createInviteLink()"
|
(click)="createInviteLink()"
|
||||||
[disabled]="creatingInvite()"
|
[disabled]="creatingInvite()"
|
||||||
class="w-full text-left px-3 py-2 text-sm hover:bg-secondary transition-colors text-foreground disabled:cursor-not-allowed disabled:opacity-60"
|
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()) {
|
@if (creatingInvite()) {
|
||||||
Creating Invite Link…
|
Creating Invite Link…
|
||||||
@@ -89,22 +89,22 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="leaveServer()"
|
(click)="leaveServer()"
|
||||||
class="w-full text-left px-3 py-2 text-sm hover:bg-secondary transition-colors text-foreground"
|
class="w-full rounded-md px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
||||||
>
|
>
|
||||||
Leave Server
|
Leave Server
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<div
|
<div
|
||||||
class="border-t border-border px-3 py-2 text-xs leading-5 text-muted-foreground"
|
class="px-3 py-2 text-xs leading-5 text-muted-foreground"
|
||||||
[class.hidden]="!inviteStatus()"
|
[class.hidden]="!inviteStatus()"
|
||||||
>
|
>
|
||||||
{{ inviteStatus() }}
|
{{ inviteStatus() }}
|
||||||
</div>
|
</div>
|
||||||
<div class="border-t border-border"></div>
|
<div class="mx-2 my-1 h-px bg-border"></div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="logout()"
|
(click)="logout()"
|
||||||
class="w-full text-left px-3 py-2 text-sm hover:bg-secondary transition-colors text-foreground"
|
class="w-full rounded-md px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
@if (isElectron()) {
|
@if (isElectron()) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-8 h-8 grid place-items-center hover:bg-secondary rounded"
|
class="grid h-8 w-8 place-items-center rounded-md transition-colors hover:bg-secondary"
|
||||||
title="Minimize"
|
title="Minimize"
|
||||||
(click)="minimize()"
|
(click)="minimize()"
|
||||||
>
|
>
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-8 h-8 grid place-items-center hover:bg-secondary rounded"
|
class="grid h-8 w-8 place-items-center rounded-md transition-colors hover:bg-secondary"
|
||||||
title="Maximize"
|
title="Maximize"
|
||||||
(click)="maximize()"
|
(click)="maximize()"
|
||||||
>
|
>
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-8 h-8 grid place-items-center hover:bg-destructive/10 rounded"
|
class="grid h-8 w-8 place-items-center rounded-md transition-colors hover:bg-destructive/10"
|
||||||
title="Close"
|
title="Close"
|
||||||
(click)="close()"
|
(click)="close()"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import {
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
selectCurrentRoom,
|
selectCurrentRoom,
|
||||||
|
selectActiveChannelId,
|
||||||
|
selectTextChannels,
|
||||||
|
selectVoiceChannels,
|
||||||
selectIsSignalServerReconnecting,
|
selectIsSignalServerReconnecting,
|
||||||
selectSignalServerCompatibilityError
|
selectSignalServerCompatibilityError
|
||||||
} from '../../store/rooms/rooms.selectors';
|
} from '../../store/rooms/rooms.selectors';
|
||||||
@@ -33,6 +36,7 @@ import { PlatformService } from '../../core/platform';
|
|||||||
import { STORAGE_KEY_CURRENT_USER_ID } from '../../core/constants';
|
import { STORAGE_KEY_CURRENT_USER_ID } from '../../core/constants';
|
||||||
import { LeaveServerDialogComponent } from '../../shared';
|
import { LeaveServerDialogComponent } from '../../shared';
|
||||||
import { Room } from '../../shared-kernel';
|
import { Room } from '../../shared-kernel';
|
||||||
|
import { VoiceWorkspaceService } from '../../domains/voice-session';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-title-bar',
|
selector: 'app-title-bar',
|
||||||
@@ -63,6 +67,7 @@ export class TitleBarComponent {
|
|||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private webrtc = inject(RealtimeSessionFacade);
|
private webrtc = inject(RealtimeSessionFacade);
|
||||||
private platform = inject(PlatformService);
|
private platform = inject(PlatformService);
|
||||||
|
private voiceWorkspace = inject(VoiceWorkspaceService);
|
||||||
|
|
||||||
private getWindowControlsApi() {
|
private getWindowControlsApi() {
|
||||||
return this.electronBridge.getApi();
|
return this.electronBridge.getApi();
|
||||||
@@ -78,11 +83,62 @@ export class TitleBarComponent {
|
|||||||
isReconnecting = computed(() => !this.webrtc.isConnected() && this.webrtc.hasEverConnected());
|
isReconnecting = computed(() => !this.webrtc.isConnected() && this.webrtc.hasEverConnected());
|
||||||
isAuthed = computed(() => !!this.currentUser());
|
isAuthed = computed(() => !!this.currentUser());
|
||||||
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||||
|
activeChannelId = this.store.selectSignal(selectActiveChannelId);
|
||||||
|
textChannels = this.store.selectSignal(selectTextChannels);
|
||||||
|
voiceChannels = this.store.selectSignal(selectVoiceChannels);
|
||||||
|
isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
|
||||||
isSignalServerReconnecting = this.store.selectSignal(selectIsSignalServerReconnecting);
|
isSignalServerReconnecting = this.store.selectSignal(selectIsSignalServerReconnecting);
|
||||||
signalServerCompatibilityError = this.store.selectSignal(selectSignalServerCompatibilityError);
|
signalServerCompatibilityError = this.store.selectSignal(selectSignalServerCompatibilityError);
|
||||||
inRoom = computed(() => !!this.currentRoom());
|
inRoom = computed(() => !!this.currentRoom());
|
||||||
roomName = computed(() => this.currentRoom()?.name || '');
|
roomName = computed(() => this.currentRoom()?.name || '');
|
||||||
roomDescription = computed(() => this.currentRoom()?.description || '');
|
activeTextChannelName = computed(() => {
|
||||||
|
const textChannels = this.textChannels();
|
||||||
|
|
||||||
|
if (textChannels.length === 0) {
|
||||||
|
return 'No text channels';
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = this.activeChannelId();
|
||||||
|
const activeChannel = textChannels.find((channel) => channel.id === id) ?? textChannels[0];
|
||||||
|
|
||||||
|
return activeChannel ? activeChannel.name : id;
|
||||||
|
});
|
||||||
|
connectedVoiceChannelName = computed(() => {
|
||||||
|
const voiceChannelId = this.currentUser()?.voiceState?.roomId;
|
||||||
|
const voiceChannel = this.voiceChannels().find((channel) => channel.id === voiceChannelId);
|
||||||
|
|
||||||
|
return voiceChannel?.name || 'Voice Lounge';
|
||||||
|
});
|
||||||
|
roomContextTitle = computed(() => {
|
||||||
|
const room = this.currentRoom();
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isVoiceWorkspaceExpanded()) {
|
||||||
|
return `${room.name} / ${this.connectedVoiceChannelName()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.textChannels().length === 0) {
|
||||||
|
return room.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${room.name} / #${this.activeTextChannelName()}`;
|
||||||
|
});
|
||||||
|
roomContextMeta = computed(() => {
|
||||||
|
if (!this.currentRoom()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [`${this.textChannels().length} text`];
|
||||||
|
|
||||||
|
if (this.voiceChannels().length > 0) {
|
||||||
|
parts.push(`${this.voiceChannels().length} voice`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' | ');
|
||||||
|
});
|
||||||
showRoomCompatibilityNotice = computed(() =>
|
showRoomCompatibilityNotice = computed(() =>
|
||||||
this.inRoom() && !!this.signalServerCompatibilityError()
|
this.inRoom() && !!this.signalServerCompatibilityError()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -64,3 +64,26 @@
|
|||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-bright-theme {
|
||||||
|
--background: 220 14% 92%;
|
||||||
|
--foreground: 222 16% 18%;
|
||||||
|
--card: 220 18% 97%;
|
||||||
|
--card-foreground: 222 16% 18%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222 16% 18%;
|
||||||
|
--primary: 214 72% 50%;
|
||||||
|
--primary-foreground: 0 0% 100%;
|
||||||
|
--secondary: 220 14% 88%;
|
||||||
|
--secondary-foreground: 222 16% 18%;
|
||||||
|
--muted: 220 12% 85%;
|
||||||
|
--muted-foreground: 220 10% 38%;
|
||||||
|
--accent: 220 14% 90%;
|
||||||
|
--accent-foreground: 222 16% 18%;
|
||||||
|
--destructive: 0 72% 56%;
|
||||||
|
--destructive-foreground: 0 0% 100%;
|
||||||
|
--border: 220 12% 78%;
|
||||||
|
--input: 220 12% 78%;
|
||||||
|
--ring: 214 72% 50%;
|
||||||
|
--radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user