feat: signal server tag
This commit is contained in:
@@ -1,408 +1,418 @@
|
||||
<ng-template #pageContent>
|
||||
<div class="h-full min-h-0 overflow-y-auto bg-background text-foreground">
|
||||
<div class="mx-auto w-full max-w-5xl space-y-8 p-4 sm:p-6 lg:py-8">
|
||||
<header class="space-y-1">
|
||||
<h1 class="text-2xl font-semibold text-foreground">
|
||||
@if (currentUser()) {
|
||||
Welcome back, {{ currentUser()!.displayName || 'there' }}
|
||||
} @else {
|
||||
Welcome to MetoYou
|
||||
}
|
||||
</h1>
|
||||
<p class="text-sm text-muted-foreground">Find people, discover servers, or start your own community.</p>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
<div class="relative">
|
||||
<ng-icon
|
||||
name="lucideSearch"
|
||||
class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<input
|
||||
#searchInput
|
||||
type="text"
|
||||
aria-label="Search people, servers, and invites"
|
||||
class="h-12 w-full rounded-xl border border-border bg-secondary py-2 pl-11 pr-20 text-base text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Search for people, servers, or paste an invite..."
|
||||
[ngModel]="searchQuery()"
|
||||
(ngModelChange)="onSearchChange($event)"
|
||||
(keydown.enter)="submitSearch()"
|
||||
/>
|
||||
<kbd class="pointer-events-none absolute right-3 top-1/2 hidden -translate-y-1/2 items-center gap-1 rounded border border-border bg-card px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground sm:flex">
|
||||
Ctrl K
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
@if (!isSearchMode() && recentSearches().length > 0) {
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs font-medium text-muted-foreground">Recent:</span>
|
||||
@for (term of recentSearches(); track term) {
|
||||
<span class="group inline-flex items-center gap-1 rounded-full border border-border bg-secondary py-1 pl-3 pr-1 text-xs text-foreground">
|
||||
<button
|
||||
type="button"
|
||||
class="max-w-[10rem] truncate hover:text-primary"
|
||||
(click)="applyRecentSearch(term)"
|
||||
>
|
||||
{{ term }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-4 w-4 place-items-center rounded-full text-muted-foreground hover:bg-card hover:text-foreground"
|
||||
[attr.aria-label]="'Remove ' + term"
|
||||
(click)="removeRecentSearch(term)"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
class="h-3 w-3"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<div class="h-full min-h-0 overflow-y-auto bg-background text-foreground">
|
||||
<div class="mx-auto w-full max-w-5xl space-y-8 p-4 sm:p-6 lg:py-8">
|
||||
<header class="space-y-1">
|
||||
<h1 class="text-2xl font-semibold text-foreground">
|
||||
@if (currentUser()) {
|
||||
Welcome back, {{ currentUser()!.displayName || 'there' }}
|
||||
} @else {
|
||||
Welcome to MetoYou
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs font-medium text-muted-foreground hover:text-foreground hover:underline"
|
||||
(click)="clearRecentSearches()"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</h1>
|
||||
<p class="text-sm text-muted-foreground">Find people, discover servers, or start your own community.</p>
|
||||
</header>
|
||||
|
||||
@if (isSearchMode()) {
|
||||
<section class="space-y-5">
|
||||
@if (inviteResult(); as invite) {
|
||||
<div>
|
||||
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Invite</h2>
|
||||
<div>
|
||||
<div class="relative">
|
||||
<ng-icon
|
||||
name="lucideSearch"
|
||||
class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<input
|
||||
#searchInput
|
||||
type="text"
|
||||
aria-label="Search people, servers, and invites"
|
||||
class="h-12 w-full rounded-xl border border-border bg-secondary py-2 pl-11 pr-20 text-base text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Search for people, servers, or paste an invite..."
|
||||
[ngModel]="searchQuery()"
|
||||
(ngModelChange)="onSearchChange($event)"
|
||||
(keydown.enter)="submitSearch()"
|
||||
/>
|
||||
<kbd
|
||||
class="pointer-events-none absolute right-3 top-1/2 hidden -translate-y-1/2 items-center gap-1 rounded border border-border bg-card px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground sm:flex"
|
||||
>
|
||||
Ctrl K
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
@if (!isSearchMode() && recentSearches().length > 0) {
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs font-medium text-muted-foreground">Recent:</span>
|
||||
@for (term of recentSearches(); track term) {
|
||||
<span
|
||||
class="group inline-flex items-center gap-1 rounded-full border border-border bg-secondary py-1 pl-3 pr-1 text-xs text-foreground"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="max-w-[10rem] truncate hover:text-primary"
|
||||
(click)="applyRecentSearch(term)"
|
||||
>
|
||||
{{ term }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-4 w-4 place-items-center rounded-full text-muted-foreground hover:bg-card hover:text-foreground"
|
||||
[attr.aria-label]="'Remove ' + term"
|
||||
(click)="removeRecentSearch(term)"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
class="h-3 w-3"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
|
||||
(click)="openInvite()"
|
||||
class="text-xs font-medium text-muted-foreground hover:text-foreground hover:underline"
|
||||
(click)="clearRecentSearches()"
|
||||
>
|
||||
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-primary/10 text-primary">
|
||||
<ng-icon
|
||||
name="lucideTicket"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-foreground">Open invite</p>
|
||||
<p class="truncate text-xs text-muted-foreground">{{ invite }}</p>
|
||||
</div>
|
||||
<ng-icon
|
||||
name="lucideArrowRight"
|
||||
class="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (topServerResults().length > 0) {
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Servers</h2>
|
||||
<a
|
||||
routerLink="/servers"
|
||||
class="text-xs font-medium text-primary hover:underline"
|
||||
>View all</a
|
||||
@if (isSearchMode()) {
|
||||
<section class="space-y-5">
|
||||
@if (inviteResult(); as invite) {
|
||||
<div>
|
||||
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Invite</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
|
||||
(click)="openInvite()"
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@for (server of topServerResults(); track server.id) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
|
||||
(click)="openServer(server)"
|
||||
>
|
||||
<div class="grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-sm font-semibold text-foreground">
|
||||
@if (server.icon) {
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="h-full w-full bg-cover bg-center bg-no-repeat"
|
||||
[style.backgroundImage]="'url(' + server.icon + ')'"
|
||||
></div>
|
||||
} @else {
|
||||
{{ serverInitial(server) }}
|
||||
}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-foreground">{{ server.name }}</p>
|
||||
<p class="truncate text-xs text-muted-foreground">{{ serverMetaLabel(server) }}</p>
|
||||
</div>
|
||||
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-primary/10 text-primary">
|
||||
<ng-icon
|
||||
name="lucideArrowRight"
|
||||
class="h-4 w-4 text-muted-foreground"
|
||||
name="lucideTicket"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-foreground">Open invite</p>
|
||||
<p class="truncate text-xs text-muted-foreground">{{ invite }}</p>
|
||||
</div>
|
||||
<ng-icon
|
||||
name="lucideArrowRight"
|
||||
class="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (topServerResults().length > 0) {
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Servers</h2>
|
||||
<a
|
||||
routerLink="/servers"
|
||||
class="text-xs font-medium text-primary hover:underline"
|
||||
>View all</a
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@for (server of topServerResults(); track server.id) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
|
||||
(click)="openServer(server)"
|
||||
>
|
||||
<div
|
||||
class="grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-sm font-semibold text-foreground"
|
||||
>
|
||||
@if (server.icon) {
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="h-full w-full bg-cover bg-center bg-no-repeat"
|
||||
[style.backgroundImage]="'url(' + server.icon + ')'"
|
||||
></div>
|
||||
} @else {
|
||||
{{ serverInitial(server) }}
|
||||
}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-foreground">{{ server.name }}</p>
|
||||
<p class="truncate text-xs text-muted-foreground">{{ serverMetaLabel(server) }}</p>
|
||||
</div>
|
||||
<ng-icon
|
||||
name="lucideArrowRight"
|
||||
class="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (topPeopleResults().length > 0) {
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">People</h2>
|
||||
<a
|
||||
routerLink="/people"
|
||||
class="text-xs font-medium text-primary hover:underline"
|
||||
>View all</a
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@for (person of topPeopleResults(); track person.id) {
|
||||
<a
|
||||
routerLink="/people"
|
||||
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
|
||||
>
|
||||
<app-user-avatar
|
||||
[name]="personLabel(person)"
|
||||
[avatarUrl]="person.avatarUrl"
|
||||
size="md"
|
||||
[status]="person.status"
|
||||
[showStatusBadge]="true"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-foreground">{{ personLabel(person) }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
|
||||
</div>
|
||||
<app-friend-button [user]="person" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (hasNoQuickResults() && !isSearching()) {
|
||||
<div class="rounded-lg border border-border bg-card px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
No people, servers, or invites match
|
||||
<span class="font-medium text-foreground">{{ searchQuery() }}</span
|
||||
>.
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
} @else {
|
||||
<!-- Primary actions -->
|
||||
<section class="grid gap-3 sm:grid-cols-3">
|
||||
<a
|
||||
routerLink="/people"
|
||||
class="group flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
|
||||
>
|
||||
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-purple-500/15 text-purple-400">
|
||||
<ng-icon
|
||||
name="lucideUsers"
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-semibold text-foreground">Find People</p>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">Connect with friends.</p>
|
||||
</div>
|
||||
<ng-icon
|
||||
name="lucideArrowRight"
|
||||
class="h-4 w-4 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
routerLink="/servers"
|
||||
class="group flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
|
||||
>
|
||||
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-blue-500/15 text-blue-400">
|
||||
<ng-icon
|
||||
name="lucideCompass"
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-semibold text-foreground">Find Servers</p>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">Browse communities.</p>
|
||||
</div>
|
||||
<ng-icon
|
||||
name="lucideArrowRight"
|
||||
class="h-4 w-4 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
routerLink="/create-server"
|
||||
class="group flex items-center gap-3 rounded-xl border border-emerald-500/40 bg-emerald-500/10 p-4 transition-colors hover:bg-emerald-500/15"
|
||||
>
|
||||
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-emerald-500/20 text-emerald-400">
|
||||
<ng-icon
|
||||
name="lucidePlus"
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-semibold text-foreground">Create Server</p>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">Start your own.</p>
|
||||
</div>
|
||||
<ng-icon
|
||||
name="lucideArrowRight"
|
||||
class="h-4 w-4 text-emerald-400 transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
@if (isNewUser()) {
|
||||
<section class="rounded-xl border border-border bg-card p-6 text-center">
|
||||
<div class="mx-auto mb-3 grid h-12 w-12 place-items-center rounded-full bg-secondary">
|
||||
<ng-icon
|
||||
name="lucideServer"
|
||||
class="h-6 w-6 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="text-base font-semibold text-foreground">Get started</h2>
|
||||
<p class="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
You have not joined any servers yet. Find a community to join, or create your own to invite friends.
|
||||
</p>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (topPeopleResults().length > 0) {
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">People</h2>
|
||||
<!-- People + Popular servers -->
|
||||
<section class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-xl border border-border bg-card/40 p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-foreground">People you might know</h2>
|
||||
<a
|
||||
routerLink="/people"
|
||||
class="text-xs font-medium text-primary hover:underline"
|
||||
>View all</a
|
||||
>See all</a
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@for (person of topPeopleResults(); track person.id) {
|
||||
<a
|
||||
routerLink="/people"
|
||||
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
|
||||
>
|
||||
<app-user-avatar
|
||||
[name]="personLabel(person)"
|
||||
[avatarUrl]="person.avatarUrl"
|
||||
size="md"
|
||||
[status]="person.status"
|
||||
[showStatusBadge]="true"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-foreground">{{ personLabel(person) }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
|
||||
@if (peopleYouMightKnow().length > 0) {
|
||||
<div class="space-y-1">
|
||||
@for (person of peopleYouMightKnow(); track person.id) {
|
||||
<div class="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
|
||||
<app-user-avatar
|
||||
[name]="personLabel(person)"
|
||||
[avatarUrl]="person.avatarUrl"
|
||||
size="md"
|
||||
[status]="person.status"
|
||||
[showStatusBadge]="true"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(person) }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
|
||||
</div>
|
||||
<app-friend-button [user]="person" />
|
||||
</div>
|
||||
<app-friend-button [user]="person" />
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="py-6 text-center text-sm text-muted-foreground">No people to suggest yet.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border bg-card/40 p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-foreground">Popular Servers</h2>
|
||||
<a
|
||||
routerLink="/servers"
|
||||
class="text-xs font-medium text-primary hover:underline"
|
||||
>See all</a
|
||||
>
|
||||
</div>
|
||||
@if (popularServers().length > 0) {
|
||||
<div class="space-y-1">
|
||||
@for (server of popularServers(); track server.id) {
|
||||
<div class="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
|
||||
<div
|
||||
class="grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-sm font-semibold text-foreground"
|
||||
>
|
||||
@if (server.icon) {
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="h-full w-full bg-cover bg-center bg-no-repeat"
|
||||
[style.backgroundImage]="'url(' + server.icon + ')'"
|
||||
></div>
|
||||
} @else {
|
||||
{{ serverInitial(server) }}
|
||||
}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-foreground">{{ server.name }}</p>
|
||||
<p class="truncate text-xs text-muted-foreground">{{ serverMetaLabel(server) }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-md bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
(click)="openServer(server)"
|
||||
>
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="py-6 text-center text-sm text-muted-foreground">No popular servers right now.</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (hasNoQuickResults() && !isSearching()) {
|
||||
<div class="rounded-lg border border-border bg-card px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
No people, servers, or invites match
|
||||
<span class="font-medium text-foreground">{{ searchQuery() }}</span
|
||||
>.
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
} @else {
|
||||
<!-- Primary actions -->
|
||||
<section class="grid gap-3 sm:grid-cols-3">
|
||||
<a
|
||||
routerLink="/people"
|
||||
class="group flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
|
||||
>
|
||||
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-purple-500/15 text-purple-400">
|
||||
<ng-icon
|
||||
name="lucideUsers"
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-semibold text-foreground">Find People</p>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">Connect with friends.</p>
|
||||
</div>
|
||||
<ng-icon
|
||||
name="lucideArrowRight"
|
||||
class="h-4 w-4 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
routerLink="/servers"
|
||||
class="group flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
|
||||
>
|
||||
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-blue-500/15 text-blue-400">
|
||||
<ng-icon
|
||||
name="lucideCompass"
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-semibold text-foreground">Find Servers</p>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">Browse communities.</p>
|
||||
</div>
|
||||
<ng-icon
|
||||
name="lucideArrowRight"
|
||||
class="h-4 w-4 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
routerLink="/create-server"
|
||||
class="group flex items-center gap-3 rounded-xl border border-emerald-500/40 bg-emerald-500/10 p-4 transition-colors hover:bg-emerald-500/15"
|
||||
>
|
||||
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-emerald-500/20 text-emerald-400">
|
||||
<ng-icon
|
||||
name="lucidePlus"
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-semibold text-foreground">Create Server</p>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">Start your own.</p>
|
||||
</div>
|
||||
<ng-icon
|
||||
name="lucideArrowRight"
|
||||
class="h-4 w-4 text-emerald-400 transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
@if (isNewUser()) {
|
||||
<section class="rounded-xl border border-border bg-card p-6 text-center">
|
||||
<div class="mx-auto mb-3 grid h-12 w-12 place-items-center rounded-full bg-secondary">
|
||||
<ng-icon
|
||||
name="lucideServer"
|
||||
class="h-6 w-6 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="text-base font-semibold text-foreground">Get started</h2>
|
||||
<p class="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
You have not joined any servers yet. Find a community to join, or create your own to invite friends.
|
||||
</p>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- People + Popular servers -->
|
||||
<section class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-xl border border-border bg-card/40 p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-foreground">People you might know</h2>
|
||||
<a
|
||||
routerLink="/people"
|
||||
class="text-xs font-medium text-primary hover:underline"
|
||||
>See all</a
|
||||
>
|
||||
</div>
|
||||
@if (peopleYouMightKnow().length > 0) {
|
||||
<div class="space-y-1">
|
||||
@for (person of peopleYouMightKnow(); track person.id) {
|
||||
<div class="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
|
||||
<!-- Your friends -->
|
||||
@if (friends().length > 0) {
|
||||
<section>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-foreground">Your Friends</h2>
|
||||
<a
|
||||
routerLink="/people"
|
||||
class="text-xs font-medium text-primary hover:underline"
|
||||
>Manage</a
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@for (friend of friends(); track friend.id) {
|
||||
<div class="flex items-center gap-3 rounded-xl border border-border bg-card p-3">
|
||||
<app-user-avatar
|
||||
[name]="personLabel(person)"
|
||||
[avatarUrl]="person.avatarUrl"
|
||||
[name]="personLabel(friend)"
|
||||
[avatarUrl]="friend.avatarUrl"
|
||||
size="md"
|
||||
[status]="person.status"
|
||||
[status]="friend.status"
|
||||
[showStatusBadge]="true"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(person) }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
|
||||
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(friend) }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ isOnline(friend) ? 'Online' : 'Offline' }}</p>
|
||||
</div>
|
||||
<app-friend-button [user]="person" />
|
||||
<app-friend-button [user]="friend" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="py-6 text-center text-sm text-muted-foreground">No people to suggest yet.</p>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<div class="rounded-xl border border-border bg-card/40 p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-foreground">Popular Servers</h2>
|
||||
<a
|
||||
routerLink="/servers"
|
||||
class="text-xs font-medium text-primary hover:underline"
|
||||
>See all</a
|
||||
>
|
||||
</div>
|
||||
@if (popularServers().length > 0) {
|
||||
<div class="space-y-1">
|
||||
@for (server of popularServers(); track server.id) {
|
||||
<div class="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
|
||||
<div class="grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-sm font-semibold text-foreground">
|
||||
@if (server.icon) {
|
||||
<!-- Recently active servers -->
|
||||
@if (recentlyActiveServers().length > 0) {
|
||||
<section>
|
||||
<h2 class="mb-3 text-sm font-semibold text-foreground">Recently Active Servers</h2>
|
||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
||||
@for (room of recentlyActiveServers(); track room.id) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 text-center transition-colors hover:border-primary/50 hover:bg-card/80"
|
||||
(click)="openSavedRoom(room)"
|
||||
>
|
||||
<div
|
||||
class="grid h-12 w-12 shrink-0 place-items-center overflow-hidden rounded-xl bg-secondary text-base font-semibold text-foreground"
|
||||
>
|
||||
@if (room.icon) {
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="h-full w-full bg-cover bg-center bg-no-repeat"
|
||||
[style.backgroundImage]="'url(' + server.icon + ')'"
|
||||
[style.backgroundImage]="'url(' + room.icon + ')'"
|
||||
></div>
|
||||
} @else {
|
||||
{{ serverInitial(server) }}
|
||||
{{ room.name[0]?.toUpperCase() || '?' }}
|
||||
}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-foreground">{{ server.name }}</p>
|
||||
<p class="truncate text-xs text-muted-foreground">{{ serverMetaLabel(server) }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-md bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
(click)="openServer(server)"
|
||||
>
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
<p class="w-full truncate text-sm font-medium text-foreground">{{ room.name }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ room.userCount }} members</p>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="py-6 text-center text-sm text-muted-foreground">No popular servers right now.</p>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Your friends -->
|
||||
@if (friends().length > 0) {
|
||||
<section>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-foreground">Your Friends</h2>
|
||||
<a
|
||||
routerLink="/people"
|
||||
class="text-xs font-medium text-primary hover:underline"
|
||||
>Manage</a
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@for (friend of friends(); track friend.id) {
|
||||
<div class="flex items-center gap-3 rounded-xl border border-border bg-card p-3">
|
||||
<app-user-avatar
|
||||
[name]="personLabel(friend)"
|
||||
[avatarUrl]="friend.avatarUrl"
|
||||
size="md"
|
||||
[status]="friend.status"
|
||||
[showStatusBadge]="true"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(friend) }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ isOnline(friend) ? 'Online' : 'Offline' }}</p>
|
||||
</div>
|
||||
<app-friend-button [user]="friend" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Recently active servers -->
|
||||
@if (recentlyActiveServers().length > 0) {
|
||||
<section>
|
||||
<h2 class="mb-3 text-sm font-semibold text-foreground">Recently Active Servers</h2>
|
||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
||||
@for (room of recentlyActiveServers(); track room.id) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 text-center transition-colors hover:border-primary/50 hover:bg-card/80"
|
||||
(click)="openSavedRoom(room)"
|
||||
>
|
||||
<div class="grid h-12 w-12 shrink-0 place-items-center overflow-hidden rounded-xl bg-secondary text-base font-semibold text-foreground">
|
||||
@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 {
|
||||
{{ room.name[0]?.toUpperCase() || '?' }}
|
||||
}
|
||||
</div>
|
||||
<p class="w-full truncate text-sm font-medium text-foreground">{{ room.name }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ room.userCount }} members</p>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@if (isMobile()) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<article
|
||||
class="flex min-w-0 flex-col items-center justify-center overflow-hidden rounded-xl text-center"
|
||||
[ngClass]="compact() ? 'min-h-[9.5rem] w-[12rem] shrink-0 p-3 sm:w-[14rem] sm:p-4' : 'min-h-[14rem] w-full p-3 sm:min-h-[17rem] sm:p-[clamp(1.25rem,4vw,2rem)]'"
|
||||
[ngClass]="
|
||||
compact()
|
||||
? 'min-h-[9.5rem] w-[12rem] shrink-0 p-3 sm:w-[14rem] sm:p-4'
|
||||
: 'min-h-[14rem] w-full p-3 sm:min-h-[17rem] sm:p-[clamp(1.25rem,4vw,2rem)]'
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="relative h-[var(--participant-avatar-size)] w-[var(--participant-avatar-size)] rounded-full ring-2 transition-all duration-150 sm:h-[var(--participant-avatar-size-sm)] sm:w-[var(--participant-avatar-size-sm)]"
|
||||
|
||||
@@ -19,215 +19,215 @@
|
||||
}
|
||||
|
||||
<ng-template #privateCallSurface>
|
||||
<section
|
||||
class="grid h-full min-h-0 bg-background lg:grid-cols-[minmax(0,1fr)_var(--private-call-chat-width)]"
|
||||
[style.--private-call-chat-width]="chatWidthPx() + 'px'"
|
||||
>
|
||||
<main class="flex min-h-0 min-w-0 flex-col overflow-hidden bg-[radial-gradient(circle_at_top,rgba(16,185,129,0.10),transparent_34rem)]">
|
||||
<header class="flex min-h-16 shrink-0 items-center justify-between gap-3 border-b border-border/70 bg-background/80 px-3 backdrop-blur sm:px-5">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-2xl bg-emerald-500/10 text-emerald-500">
|
||||
<ng-icon
|
||||
name="lucidePhone"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
<section
|
||||
class="grid h-full min-h-0 bg-background lg:grid-cols-[minmax(0,1fr)_var(--private-call-chat-width)]"
|
||||
[style.--private-call-chat-width]="chatWidthPx() + 'px'"
|
||||
>
|
||||
<main class="flex min-h-0 min-w-0 flex-col overflow-hidden bg-[radial-gradient(circle_at_top,rgba(16,185,129,0.10),transparent_34rem)]">
|
||||
<header class="flex min-h-16 shrink-0 items-center justify-between gap-3 border-b border-border/70 bg-background/80 px-3 backdrop-blur sm:px-5">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-2xl bg-emerald-500/10 text-emerald-500">
|
||||
<ng-icon
|
||||
name="lucidePhone"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<h1 class="truncate text-base font-semibold text-foreground">Private Call</h1>
|
||||
<p class="truncate text-xs text-muted-foreground">
|
||||
@if (session()) {
|
||||
{{ participantUsers().length }} participants
|
||||
} @else {
|
||||
Call not found
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<h1 class="truncate text-base font-semibold text-foreground">Private Call</h1>
|
||||
<p class="truncate text-xs text-muted-foreground">
|
||||
@if (session()) {
|
||||
{{ participantUsers().length }} participants
|
||||
} @else {
|
||||
Call not found
|
||||
@if (session()) {
|
||||
<div class="flex items-center gap-2">
|
||||
@if (isMobile()) {
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-10 w-10 place-items-center rounded-full bg-secondary text-foreground transition-colors hover:bg-secondary/80"
|
||||
(click)="minimizeCall()"
|
||||
aria-label="Minimize call"
|
||||
title="Minimize call"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (session()) {
|
||||
<div class="flex items-center gap-2">
|
||||
@if (isMobile()) {
|
||||
<select
|
||||
class="hidden h-9 max-w-44 rounded-md border border-border bg-secondary px-2 text-sm text-foreground sm:block"
|
||||
[ngModel]="inviteUserId()"
|
||||
(ngModelChange)="inviteUserId.set($event)"
|
||||
aria-label="Add user to call"
|
||||
>
|
||||
<option value="">Add user</option>
|
||||
@for (user of inviteCandidates(); track userKey(user)) {
|
||||
<option [value]="userKey(user)">{{ user.displayName }}</option>
|
||||
}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-10 w-10 place-items-center rounded-full bg-secondary text-foreground transition-colors hover:bg-secondary/80"
|
||||
(click)="minimizeCall()"
|
||||
aria-label="Minimize call"
|
||||
title="Minimize call"
|
||||
class="hidden h-9 w-9 place-items-center rounded-md bg-secondary text-foreground transition-colors hover:bg-secondary/80 disabled:opacity-50 sm:grid"
|
||||
[disabled]="!inviteUserId()"
|
||||
(click)="inviteSelectedUser()"
|
||||
aria-label="Add user"
|
||||
title="Add user"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
class="h-5 w-5"
|
||||
name="lucideUserPlus"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
<select
|
||||
class="hidden h-9 max-w-44 rounded-md border border-border bg-secondary px-2 text-sm text-foreground sm:block"
|
||||
[ngModel]="inviteUserId()"
|
||||
(ngModelChange)="inviteUserId.set($event)"
|
||||
aria-label="Add user to call"
|
||||
>
|
||||
<option value="">Add user</option>
|
||||
@for (user of inviteCandidates(); track userKey(user)) {
|
||||
<option [value]="userKey(user)">{{ user.displayName }}</option>
|
||||
}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden h-9 w-9 place-items-center rounded-md bg-secondary text-foreground transition-colors hover:bg-secondary/80 disabled:opacity-50 sm:grid"
|
||||
[disabled]="!inviteUserId()"
|
||||
(click)="inviteSelectedUser()"
|
||||
aria-label="Add user"
|
||||
title="Add user"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideUserPlus"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
|
||||
@if (session()) {
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-hidden px-3 py-3 sm:px-5 sm:py-4">
|
||||
<div class="relative min-h-0 flex-1 overflow-hidden">
|
||||
@if (activeShares().length > 0) {
|
||||
@if (focusedShare()) {
|
||||
@if (hasMultipleShares()) {
|
||||
<div class="absolute right-3 top-3 z-10 sm:right-4 sm:top-4">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="private-call-show-all-streams"
|
||||
class="inline-flex h-10 items-center gap-2 rounded-full border border-white/10 bg-black/45 px-3 text-xs font-medium text-white/80 backdrop-blur transition hover:bg-black/65 hover:text-white"
|
||||
title="Show all streams"
|
||||
(click)="showAllStreams()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideUsers"
|
||||
class="h-3.5 w-3.5"
|
||||
/>
|
||||
All streams
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<app-voice-workspace-stream-tile
|
||||
[item]="focusedShare()!"
|
||||
[featured]="true"
|
||||
[focused]="true"
|
||||
data-testid="private-call-focused-stream"
|
||||
[immersive]="true"
|
||||
(focusRequested)="focusShare($event)"
|
||||
/>
|
||||
} @else if (hasMultipleShares()) {
|
||||
<div
|
||||
class="grid h-full min-h-0 auto-rows-[minmax(12rem,1fr)] grid-cols-1 gap-3 p-3 sm:grid-cols-2 sm:gap-4 sm:p-4"
|
||||
[ngClass]="{ '2xl:grid-cols-3': activeShares().length > 2 }"
|
||||
data-testid="private-call-stream-grid"
|
||||
>
|
||||
@for (share of activeShares(); track share.id) {
|
||||
<div class="min-h-0 overflow-hidden rounded-2xl bg-black">
|
||||
<app-voice-workspace-stream-tile
|
||||
[item]="share"
|
||||
[focused]="false"
|
||||
(focusRequested)="focusShare($event)"
|
||||
/>
|
||||
@if (session()) {
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-hidden px-3 py-3 sm:px-5 sm:py-4">
|
||||
<div class="relative min-h-0 flex-1 overflow-hidden">
|
||||
@if (activeShares().length > 0) {
|
||||
@if (focusedShare()) {
|
||||
@if (hasMultipleShares()) {
|
||||
<div class="absolute right-3 top-3 z-10 sm:right-4 sm:top-4">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="private-call-show-all-streams"
|
||||
class="inline-flex h-10 items-center gap-2 rounded-full border border-white/10 bg-black/45 px-3 text-xs font-medium text-white/80 backdrop-blur transition hover:bg-black/65 hover:text-white"
|
||||
title="Show all streams"
|
||||
(click)="showAllStreams()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideUsers"
|
||||
class="h-3.5 w-3.5"
|
||||
/>
|
||||
All streams
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<app-voice-workspace-stream-tile
|
||||
[item]="focusedShare()!"
|
||||
[featured]="true"
|
||||
[focused]="true"
|
||||
data-testid="private-call-focused-stream"
|
||||
[immersive]="true"
|
||||
(focusRequested)="focusShare($event)"
|
||||
/>
|
||||
} @else if (hasMultipleShares()) {
|
||||
<div
|
||||
class="grid h-full min-h-0 auto-rows-[minmax(12rem,1fr)] grid-cols-1 gap-3 p-3 sm:grid-cols-2 sm:gap-4 sm:p-4"
|
||||
[ngClass]="{ '2xl:grid-cols-3': activeShares().length > 2 }"
|
||||
data-testid="private-call-stream-grid"
|
||||
>
|
||||
@for (share of activeShares(); track share.id) {
|
||||
<div class="min-h-0 overflow-hidden rounded-2xl bg-black">
|
||||
<app-voice-workspace-stream-tile
|
||||
[item]="share"
|
||||
[focused]="false"
|
||||
(focusRequested)="focusShare($event)"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="flex h-full min-h-0 items-center justify-center p-1 sm:p-5">
|
||||
<div
|
||||
class="grid w-full max-w-7xl grid-cols-[repeat(auto-fit,minmax(min(11rem,100%),1fr))] items-stretch justify-center gap-3 sm:grid-cols-[repeat(auto-fit,minmax(min(16rem,100%),1fr))] sm:gap-5 lg:gap-7"
|
||||
>
|
||||
@for (user of participantUsers(); track trackUserKey($index, user)) {
|
||||
<app-private-call-participant-card
|
||||
[user]="user"
|
||||
[connected]="isParticipantConnected(user)"
|
||||
[speaking]="isSpeaking(user)"
|
||||
[issueLabel]="participantIssueLabel(user)"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="flex h-full min-h-0 items-center justify-center p-1 sm:p-5">
|
||||
<div
|
||||
class="grid w-full max-w-7xl grid-cols-[repeat(auto-fit,minmax(min(11rem,100%),1fr))] items-stretch justify-center gap-3 sm:grid-cols-[repeat(auto-fit,minmax(min(16rem,100%),1fr))] sm:gap-5 lg:gap-7"
|
||||
>
|
||||
</div>
|
||||
|
||||
@if (activeShares().length > 0) {
|
||||
<div class="shrink-0 pt-4">
|
||||
<div class="flex w-full items-stretch gap-3 overflow-x-auto pb-1">
|
||||
@for (user of participantUsers(); track trackUserKey($index, user)) {
|
||||
<app-private-call-participant-card
|
||||
[user]="user"
|
||||
[connected]="isParticipantConnected(user)"
|
||||
[speaking]="isSpeaking(user)"
|
||||
[issueLabel]="participantIssueLabel(user)"
|
||||
[compact]="true"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (hasMultipleShares()) {
|
||||
@for (share of focusedShare() ? thumbnailShares() : activeShares(); track share.id) {
|
||||
<article
|
||||
class="flex min-h-[8.75rem] w-[11rem] shrink-0 flex-col overflow-hidden rounded-2xl border border-border/80 bg-black shadow-sm sm:w-[12.5rem]"
|
||||
>
|
||||
<div class="min-h-0 flex-1">
|
||||
<app-voice-workspace-stream-tile
|
||||
[item]="share"
|
||||
[mini]="true"
|
||||
[focused]="false"
|
||||
(focusRequested)="focusShare($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="shrink-0 bg-black/80 px-3 py-2 text-xs font-semibold text-white/75">
|
||||
{{ streamLabel(share) }}
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (activeShares().length > 0) {
|
||||
<div class="shrink-0 pt-4">
|
||||
<div class="flex w-full items-stretch gap-3 overflow-x-auto pb-1">
|
||||
@for (user of participantUsers(); track trackUserKey($index, user)) {
|
||||
<app-private-call-participant-card
|
||||
[user]="user"
|
||||
[connected]="isParticipantConnected(user)"
|
||||
[speaking]="isSpeaking(user)"
|
||||
[issueLabel]="participantIssueLabel(user)"
|
||||
[compact]="true"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (hasMultipleShares()) {
|
||||
@for (share of focusedShare() ? thumbnailShares() : activeShares(); track share.id) {
|
||||
<article
|
||||
class="flex min-h-[8.75rem] w-[11rem] shrink-0 flex-col overflow-hidden rounded-2xl border border-border/80 bg-black shadow-sm sm:w-[12.5rem]"
|
||||
>
|
||||
<div class="min-h-0 flex-1">
|
||||
<app-voice-workspace-stream-tile
|
||||
[item]="share"
|
||||
[mini]="true"
|
||||
[focused]="false"
|
||||
(focusRequested)="focusShare($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="shrink-0 bg-black/80 px-3 py-2 text-xs font-semibold text-white/75">
|
||||
{{ streamLabel(share) }}
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="shrink-0 pt-3">
|
||||
<app-private-call-controls
|
||||
class="mx-auto block w-full max-w-5xl"
|
||||
[connected]="isConnected()"
|
||||
[muted]="isMuted()"
|
||||
[cameraEnabled]="isCameraEnabled()"
|
||||
[screenSharing]="isScreenSharing()"
|
||||
(joinRequested)="join()"
|
||||
(muteToggled)="toggleMute()"
|
||||
(cameraToggled)="toggleCamera()"
|
||||
(screenShareToggled)="toggleScreenShare()"
|
||||
(leaveRequested)="leave()"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="shrink-0 pt-3">
|
||||
<app-private-call-controls
|
||||
class="mx-auto block w-full max-w-5xl"
|
||||
[connected]="isConnected()"
|
||||
[muted]="isMuted()"
|
||||
[cameraEnabled]="isCameraEnabled()"
|
||||
[screenSharing]="isScreenSharing()"
|
||||
(joinRequested)="join()"
|
||||
(muteToggled)="toggleMute()"
|
||||
(cameraToggled)="toggleCamera()"
|
||||
(screenShareToggled)="toggleScreenShare()"
|
||||
(leaveRequested)="leave()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex flex-1 items-center justify-center px-6 text-sm text-muted-foreground">No active call for this route.</div>
|
||||
}
|
||||
</main>
|
||||
} @else {
|
||||
<div class="flex flex-1 items-center justify-center px-6 text-sm text-muted-foreground">No active call for this route.</div>
|
||||
}
|
||||
</main>
|
||||
|
||||
<aside class="relative hidden min-h-0 border-l border-border bg-card lg:block">
|
||||
<div
|
||||
class="group absolute inset-y-0 left-0 z-10 w-3 -translate-x-1/2 cursor-col-resize bg-transparent"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
title="Resize chat"
|
||||
data-testid="private-call-chat-resizer"
|
||||
(mousedown)="startChatResize($event)"
|
||||
>
|
||||
<div class="mx-auto h-full w-px bg-border transition group-hover:bg-primary"></div>
|
||||
</div>
|
||||
<app-dm-chat
|
||||
[conversationId]="session()?.conversationId ?? null"
|
||||
[showCallButton]="false"
|
||||
/>
|
||||
</aside>
|
||||
</section>
|
||||
<aside class="relative hidden min-h-0 border-l border-border bg-card lg:block">
|
||||
<div
|
||||
class="group absolute inset-y-0 left-0 z-10 w-3 -translate-x-1/2 cursor-col-resize bg-transparent"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
title="Resize chat"
|
||||
data-testid="private-call-chat-resizer"
|
||||
(mousedown)="startChatResize($event)"
|
||||
>
|
||||
<div class="mx-auto h-full w-px bg-border transition group-hover:bg-primary"></div>
|
||||
</div>
|
||||
<app-dm-chat
|
||||
[conversationId]="session()?.conversationId ?? null"
|
||||
[showCallButton]="false"
|
||||
/>
|
||||
</aside>
|
||||
</section>
|
||||
</ng-template>
|
||||
|
||||
@if (showScreenShareQualityDialog()) {
|
||||
|
||||
@@ -124,7 +124,9 @@
|
||||
|
||||
@if (immersive() && item().kind === 'screen' && !isFullscreen()) {
|
||||
<div class="absolute inset-x-3 bottom-3 z-20 sm:inset-x-5 sm:bottom-5">
|
||||
<div class="mx-auto flex w-full max-w-3xl flex-wrap items-center justify-center gap-2 rounded-2xl border border-white/10 bg-black/55 px-3 py-3 text-white/80 shadow-2xl backdrop-blur-lg sm:gap-3 sm:px-4">
|
||||
<div
|
||||
class="mx-auto flex w-full max-w-3xl flex-wrap items-center justify-center gap-2 rounded-2xl border border-white/10 bg-black/55 px-3 py-3 text-white/80 shadow-2xl backdrop-blur-lg sm:gap-3 sm:px-4"
|
||||
>
|
||||
@if (canControlStreamAudio()) {
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 rounded-full bg-white/10 px-2.5 py-2 sm:max-w-md">
|
||||
<button
|
||||
|
||||
@@ -148,9 +148,8 @@
|
||||
|
||||
<div class="rounded-lg border border-border bg-secondary/20 p-4 space-y-3">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
MetoYou prefers the currently focused window when detecting your game. Add process names here to permanently hide
|
||||
apps that get mistakenly identified as games (e.g. "spotify", "obs64"). Entries are matched case-insensitively
|
||||
against the executable name without its extension.
|
||||
MetoYou prefers the currently focused window when detecting your game. Add process names here to permanently hide apps that get mistakenly
|
||||
identified as games (e.g. "spotify", "obs64"). Entries are matched case-insensitively against the executable name without its extension.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user