241 lines
9.2 KiB
HTML
241 lines
9.2 KiB
HTML
@if (isMobile()) {
|
|
<swiper-container
|
|
class="block h-full min-h-0 w-full"
|
|
direction="vertical"
|
|
slides-per-view="1"
|
|
space-between="0"
|
|
initial-slide="1"
|
|
threshold="10"
|
|
resistance-ratio="0"
|
|
(swiperslidechange)="onMobileCallSlideChange($event)"
|
|
>
|
|
<swiper-slide class="block h-full w-full" />
|
|
<swiper-slide class="block h-full w-full">
|
|
<ng-container *ngTemplateOutlet="privateCallSurface" />
|
|
</swiper-slide>
|
|
</swiper-container>
|
|
} @else {
|
|
<ng-container *ngTemplateOutlet="privateCallSurface" />
|
|
}
|
|
|
|
<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"
|
|
/>
|
|
</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>
|
|
|
|
@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>
|
|
}
|
|
<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>
|
|
|
|
@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>
|
|
}
|
|
</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 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>
|
|
|
|
<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()) {
|
|
<app-screen-share-quality-dialog
|
|
[selectedQuality]="screenShareQuality()"
|
|
[includeSystemAudio]="includeSystemAudio()"
|
|
(cancelled)="onScreenShareQualityCancelled()"
|
|
(confirmed)="onScreenShareQualityConfirmed($event)"
|
|
/>
|
|
}
|