Persistent call banner (#341)

Co-authored-by: Aditya Chaudhary <30+useradityaa@noreply.coracle.social>
This commit is contained in:
Aditya Chaudhary 2026-08-11 17:04:47 +00:00 committed by hodlbod
parent c8d003524b
commit a572918e6c
2 changed files with 109 additions and 8 deletions

View file

@ -3,6 +3,7 @@
import Dialog from "@lib/components/Dialog.svelte"
import Landing from "@app/components/Landing.svelte"
import Toast from "@app/components/Toast.svelte"
import CallBanner from "@app/components/CallBanner.svelte"
import PrimaryNav from "@app/components/PrimaryNav.svelte"
import {app} from "@app/core"
import {modal} from "@app/modal"
@ -14,13 +15,16 @@
const {children}: Props = $props()
</script>
<div class="flex h-screen overflow-hidden">
{#if $app.user?.pubkey}
<PrimaryNav>
{@render children?.()}
</PrimaryNav>
{:else if !$modal}
<Dialog noEscape children={{component: Landing, props: {}}} />
{/if}
<div class="flex h-screen flex-col overflow-hidden">
<div class="flex min-h-0 flex-1 overflow-hidden">
{#if $app.user?.pubkey}
<PrimaryNav>
{@render children?.()}
</PrimaryNav>
{:else if !$modal}
<Dialog noEscape children={{component: Landing, props: {}}} />
{/if}
</div>
<CallBanner />
</div>
<Toast />

View file

@ -0,0 +1,97 @@
<script lang="ts">
import {fly} from "svelte/transition"
import {goto} from "$app/navigation"
import {page} from "$app/stores"
import cx from "classnames"
import {displayRelayUrl} from "@welshman/util"
import Microphone from "@assets/icons/microphone.svg?dataurl"
import MicrophoneOff from "@assets/icons/microphone-off.svg?dataurl"
import EndCall from "@assets/icons/end-call-rounded.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import {decodeRelay} from "@app/relays"
import {displayRoom} from "@app/rooms"
import {makeRoomPath} from "@app/routes"
import {
CallState,
callState,
callTargetRoom,
callMicMuted,
leaveVoiceRoom,
toggleMute,
} from "@app/call"
const {relay, h} = $derived($page.params)
const routeUrl = $derived(relay ? decodeRelay(relay) : undefined)
// The call's own room page already shows full controls (VoiceWidget), so
// the banner would just be redundant clutter there.
const isViewingCurrentVoiceRoom = $derived(
$callTargetRoom !== undefined &&
routeUrl !== undefined &&
typeof h === "string" &&
$callTargetRoom.url === routeUrl &&
$callTargetRoom.h === h,
)
const visible = $derived(
($callState === CallState.Joining || $callState === CallState.Connected) &&
$callTargetRoom !== undefined &&
!isViewingCurrentVoiceRoom,
)
const roomName = $derived(
$callTargetRoom ? displayRoom($callTargetRoom.url, $callTargetRoom.h) : "",
)
const spaceName = $derived($callTargetRoom ? displayRelayUrl($callTargetRoom.url) : "")
const goToRoom = () => {
if (!$callTargetRoom) return
void goto(makeRoomPath($callTargetRoom.url, $callTargetRoom.h))
}
</script>
{#if visible}
<div
in:fly={{y: 60, duration: 250}}
out:fly={{y: 60, duration: 200}}
class="relative flex shrink-0 items-center gap-3 border-t border-line bg-surface py-2 pl-4 pr-2 mb-[calc(var(--saib)+3.5rem)] md:mb-[var(--saib)] md:pl-6">
<Button
class="flex min-w-0 flex-1 items-center gap-2 text-left"
onclick={goToRoom}
aria-label="Return to call in {roomName}">
<span
class={cx("h-2 w-2 shrink-0 rounded-full", {
"animate-pulse bg-warning": $callState === CallState.Joining,
"bg-success": $callState === CallState.Connected,
})}
aria-hidden="true"></span>
<span class="min-w-0 truncate text-sm">
<span class="font-semibold">
{$callState === CallState.Joining ? "Joining call…" : "Voice connected"}
</span>
<span class="text-content-muted">· {roomName} / {spaceName}</span>
</span>
</Button>
<div class="flex shrink-0 items-center gap-2">
{#if $callState === CallState.Connected}
<Button
aria-label={$callMicMuted ? "Unmute microphone" : "Mute microphone"}
aria-pressed={!$callMicMuted}
class={cx(
"button button-circle button-sm",
$callMicMuted ? "button-neutral" : "button-primary",
)}
onclick={toggleMute}>
<Icon icon={$callMicMuted ? MicrophoneOff : Microphone} size={4} />
</Button>
{/if}
<Button
aria-label="Leave voice room"
class="button button-circle button-sm button-error"
onclick={leaveVoiceRoom}>
<Icon icon={EndCall} size={4} />
</Button>
</div>
</div>
{/if}