Files
flotilla/src/app/components/VoiceWidget.svelte
T
mplorentz 16a73f27c9 Add a dialog before joining voice rooms (#109)
After using the voice rooms more since we removed the option for voice-only rooms I think you were right to suggest a dialog box before joining rooms. It felt far to clunky to have to join the voice call any time you just wanted to try to view room members, edit room settings, or just view the recent text chat.

This adds a dialog that allows the user to decline to join the call but still access the text part of the room along with associated settings and controls. It also acts as another confirmation step before turning on the user's microphone, and allows them to choose an audio input so they don't have to mess with the (generally terrible) browser controls for doing so. We should probably have controls to change your audio input and output from within the call as well, but I think this is enough for an MVP.

![Screenshot 2026-03-27 at 11.10.53 AM.png](/attachments/3ac271a6-5d17-4063-9ac6-3e5bdef10ccf)

Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social>
Reviewed-on: coracle/flotilla#109
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-03-27 19:02:56 +00:00

120 lines
4.2 KiB
Svelte

<script lang="ts">
import {readable} from "svelte/store"
import {fly} from "svelte/transition"
import {goto} from "$app/navigation"
import {page} from "$app/stores"
import {displayRelayUrl} from "@welshman/util"
import Microphone from "@assets/icons/microphone.svg?dataurl"
import MicrophoneOff from "@assets/icons/microphone-off.svg?dataurl"
import PhoneRounded from "@assets/icons/phone-rounded.svg?dataurl"
import PhoneCallingRounded from "@assets/icons/phone-calling-rounded.svg?dataurl"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
import {
decodeRelay,
deriveRoom,
displayRoom,
getRoomType,
RoomType,
type Room,
} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {makeRoomPath} from "@app/util/routes"
import {
VoiceState,
currentVoiceSession,
currentVoiceRoom,
voiceState,
leaveVoiceRoom,
toggleMute,
cancelJoinVoiceRoom,
} from "@app/voice"
const {relay, h} = $derived($page.params)
const url = $derived(relay ? decodeRelay(relay) : undefined)
const displayedRoomStore = $derived(
url && h && typeof h === "string" ? deriveRoom(url, h) : readable(undefined),
)
const routeDisplayedRoom = $derived($displayedRoomStore)
const targetRoom = $derived.by((): Room | undefined => {
if ($voiceState === VoiceState.Joining || $voiceState === VoiceState.Connected) {
return $currentVoiceRoom
}
if ($voiceState === VoiceState.Disconnected) {
if (routeDisplayedRoom) {
if (getRoomType(routeDisplayedRoom) === RoomType.Voice) {
return routeDisplayedRoom
}
return undefined
}
return $currentVoiceRoom
}
return $currentVoiceRoom
})
const roomName = $derived(targetRoom ? displayRoom(targetRoom.url, targetRoom.h) : "")
const spaceName = $derived(targetRoom ? displayRelayUrl(targetRoom.url) : "")
const openJoinDialog = async () => {
if (!targetRoom) return
await goto(makeRoomPath(targetRoom.url, targetRoom.h))
pushModal(VoiceRoomJoinDialog, {url: targetRoom.url, h: targetRoom.h})
}
</script>
{#if targetRoom}
<div
in:fly={{y: 60, duration: 350}}
out:fly={{y: 60, duration: 250}}
class="flex flex-col gap-2 rounded-box bg-base-100 p-3">
<div class="flex flex-col gap-0.5">
{#if $voiceState === VoiceState.Joining}
<span class="text-sm font-semibold text-warning">Joining...</span>
{:else if $voiceState === VoiceState.Connected}
<span class="text-sm font-semibold text-success">Voice Connected</span>
{:else}
<span class="text-sm font-semibold text-neutral-content">Disconnected</span>
{/if}
<span class="ellipsize text-xs opacity-70">
{roomName} / {spaceName}
</span>
</div>
<div class="flex items-center gap-1">
{#if $voiceState === VoiceState.Joining}
<span class="loading loading-spinner loading-sm"></span>
<Button
data-tip="Cancel"
class="center tooltip tooltip-top btn btn-sm btn-square btn-ghost"
onclick={cancelJoinVoiceRoom}>
<Icon icon={CloseCircle} size={4} />
</Button>
{:else if $voiceState === VoiceState.Connected && $currentVoiceSession}
<Button
data-tip={$currentVoiceSession.muted ? "Unmute" : "Mute"}
class="center tooltip tooltip-top btn btn-sm btn-square {$currentVoiceSession.muted
? 'btn-error'
: 'btn-ghost'}"
onclick={toggleMute}>
<Icon icon={$currentVoiceSession.muted ? MicrophoneOff : Microphone} size={4} />
</Button>
<Button
data-tip="Leave room"
class="center tooltip tooltip-top btn btn-sm btn-square btn-error"
onclick={leaveVoiceRoom}>
<Icon icon={PhoneRounded} size={4} />
</Button>
{:else}
<Button
data-tip="Join Voice"
class="center tooltip tooltip-top btn btn-sm btn-square btn-success"
onclick={openJoinDialog}>
<Icon icon={PhoneCallingRounded} size={4} />
</Button>
{/if}
</div>
</div>
{/if}