Add a dialog before joining voice rooms
This commit is contained in:
@@ -1,15 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {loadProfile, displayProfileByPubkey} from "@welshman/app"
|
import {loadProfile, displayProfileByPubkey} from "@welshman/app"
|
||||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import RoomImage from "@app/components/RoomImage.svelte"
|
import RoomImage from "@app/components/RoomImage.svelte"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import {handleJoinError} from "@app/components/VoiceWidget.svelte"
|
|
||||||
import {makeRoomPath} from "@app/util/routes"
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
||||||
import {
|
import {
|
||||||
|
VoiceState,
|
||||||
deriveVoiceParticipants,
|
deriveVoiceParticipants,
|
||||||
joinVoiceRoom,
|
|
||||||
cancelJoinVoiceRoom,
|
cancelJoinVoiceRoom,
|
||||||
currentVoiceRoom,
|
currentVoiceRoom,
|
||||||
voiceState,
|
voiceState,
|
||||||
@@ -28,21 +30,28 @@
|
|||||||
|
|
||||||
const participants = deriveVoiceParticipants(url, h)
|
const participants = deriveVoiceParticipants(url, h)
|
||||||
const isActive = $derived(
|
const isActive = $derived(
|
||||||
$voiceState === "connected" && $currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h,
|
$voiceState === VoiceState.Connected &&
|
||||||
|
$currentVoiceRoom?.url === url &&
|
||||||
|
$currentVoiceRoom?.h === h,
|
||||||
)
|
)
|
||||||
const isJoining = $derived(
|
const isJoining = $derived(
|
||||||
$voiceState === "joining" && $currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h,
|
$voiceState === VoiceState.Joining &&
|
||||||
|
$currentVoiceRoom?.url === url &&
|
||||||
|
$currentVoiceRoom?.h === h,
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async (e: MouseEvent) => {
|
||||||
if (isActive) return
|
if (isActive) return
|
||||||
|
|
||||||
if (isJoining) {
|
if (isJoining) {
|
||||||
|
e.preventDefault()
|
||||||
cancelJoinVoiceRoom()
|
cancelJoinVoiceRoom()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await joinVoiceRoom(url, h).catch(handleJoinError)
|
e.preventDefault()
|
||||||
|
await goto(makeRoomPath(url, h), {replaceState})
|
||||||
|
pushModal(VoiceRoomJoinDialog, {url, h})
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Volume from "@assets/icons/volume.svg?dataurl"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import {deriveRoom} from "@app/core/state"
|
||||||
|
import {joinVoiceRoom} from "@app/voice"
|
||||||
|
import {popModal} from "@app/util/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
const spaceLabel = $derived(displayRelayUrl(url))
|
||||||
|
|
||||||
|
let audioInputs = $state<MediaDeviceInfo[]>([])
|
||||||
|
let selectedDeviceId = $state("")
|
||||||
|
let startWithoutMic = $state(false)
|
||||||
|
|
||||||
|
const loadDevices = async () => {
|
||||||
|
if (!navigator.mediaDevices?.enumerateDevices) return
|
||||||
|
try {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||||
|
audioInputs = devices.filter(d => d.kind === "audioinput")
|
||||||
|
} catch {
|
||||||
|
audioInputs = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
void loadDevices()
|
||||||
|
})
|
||||||
|
|
||||||
|
const goBack = () => history.back()
|
||||||
|
|
||||||
|
const joinVoice = async () => {
|
||||||
|
popModal()
|
||||||
|
await joinVoiceRoom(
|
||||||
|
url,
|
||||||
|
h,
|
||||||
|
startWithoutMic,
|
||||||
|
startWithoutMic ? undefined : selectedDeviceId || undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Join voice room?</ModalTitle>
|
||||||
|
<ModalSubtitle>
|
||||||
|
<span class="inline-flex flex-wrap items-center justify-center gap-x-1.5 gap-y-1">
|
||||||
|
<Icon icon={Volume} size={4} class="shrink-0" />
|
||||||
|
<span class="ellipsize min-w-0">{$room?.name || h}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{spaceLabel}</span>
|
||||||
|
</span>
|
||||||
|
</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<p class="text-sm opacity-80">Select a microphone to join the call.</p>
|
||||||
|
<div class="flex flex-col gap-4 pt-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id="voice-start-without-mic"
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox"
|
||||||
|
bind:checked={startWithoutMic} />
|
||||||
|
<label for="voice-start-without-mic" class="text-sm cursor-pointer">
|
||||||
|
Join without microphone (you can unmute later)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Microphone</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<select
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
bind:value={selectedDeviceId}
|
||||||
|
disabled={startWithoutMic}
|
||||||
|
aria-label="Microphone">
|
||||||
|
<option value="">Default microphone</option>
|
||||||
|
{#each audioInputs as d (d.deviceId)}
|
||||||
|
<option value={d.deviceId}>
|
||||||
|
{d.label || `Microphone ${d.deviceId.slice(0, 8)}…`}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={goBack}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Don't join
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={joinVoice}>
|
||||||
|
Join voice
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -1,22 +1,7 @@
|
|||||||
<script module lang="ts">
|
|
||||||
import {AbortError, TimeoutError} from "$lib/util"
|
|
||||||
import {VoiceJoinMembershipError} from "@app/voice"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
|
||||||
|
|
||||||
export function handleJoinError(e: unknown) {
|
|
||||||
if (e instanceof AbortError) return
|
|
||||||
console.error("Failed to join voice room", e)
|
|
||||||
let message = "Failed to join voice room"
|
|
||||||
if (e instanceof VoiceJoinMembershipError) message = e.message
|
|
||||||
else if (e instanceof TimeoutError)
|
|
||||||
message = "Connection timed out. Please check your network and try again."
|
|
||||||
else if (e instanceof Error && e.message === "No signer available") message = e.message
|
|
||||||
pushToast({theme: "error", message})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {derived, get} from "svelte/store"
|
||||||
import {fly} from "svelte/transition"
|
import {fly} from "svelte/transition"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||||
import MicrophoneOff from "@assets/icons/microphone-off.svg?dataurl"
|
import MicrophoneOff from "@assets/icons/microphone-off.svg?dataurl"
|
||||||
@@ -25,36 +10,59 @@
|
|||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import {displayRoom} from "@app/core/state"
|
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
||||||
|
import {deriveDisplayedRoom, displayRoom, getRoomType, RoomType} from "@app/core/state"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
import {
|
import {
|
||||||
|
VoiceState,
|
||||||
currentVoiceSession,
|
currentVoiceSession,
|
||||||
currentVoiceRoom,
|
currentVoiceRoom,
|
||||||
voiceState,
|
voiceState,
|
||||||
leaveVoiceRoom,
|
leaveVoiceRoom,
|
||||||
toggleMute,
|
toggleMute,
|
||||||
rejoinVoiceRoom,
|
|
||||||
cancelJoinVoiceRoom,
|
cancelJoinVoiceRoom,
|
||||||
} from "@app/voice"
|
} from "@app/voice"
|
||||||
|
|
||||||
const roomName = $derived(
|
const targetRoom = derived(
|
||||||
$currentVoiceRoom ? displayRoom($currentVoiceRoom.url, $currentVoiceRoom.h) : "",
|
[voiceState, currentVoiceRoom, deriveDisplayedRoom],
|
||||||
|
([voiceState, currentVoiceRoom, routeDisplayedRoom]) => {
|
||||||
|
if (voiceState === VoiceState.Joining || voiceState === VoiceState.Connected) {
|
||||||
|
return currentVoiceRoom
|
||||||
|
}
|
||||||
|
if (voiceState === VoiceState.Disconnected) {
|
||||||
|
if (routeDisplayedRoom) {
|
||||||
|
if (getRoomType(routeDisplayedRoom) === RoomType.Voice) {
|
||||||
|
return {url: routeDisplayedRoom.url, h: routeDisplayedRoom.h}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return currentVoiceRoom
|
||||||
|
}
|
||||||
|
return currentVoiceRoom
|
||||||
|
},
|
||||||
)
|
)
|
||||||
const spaceName = $derived($currentVoiceRoom ? displayRelayUrl($currentVoiceRoom.url) : "")
|
|
||||||
|
|
||||||
const handleRejoin = () => {
|
const roomName = $derived($targetRoom ? displayRoom($targetRoom.url, $targetRoom.h) : "")
|
||||||
void rejoinVoiceRoom().catch(handleJoinError)
|
const spaceName = $derived($targetRoom ? displayRelayUrl($targetRoom.url) : "")
|
||||||
|
|
||||||
|
const openJoinDialog = async () => {
|
||||||
|
const target = get(targetRoom)
|
||||||
|
if (!target) return
|
||||||
|
await goto(makeRoomPath(target.url, target.h))
|
||||||
|
pushModal(VoiceRoomJoinDialog, {url: target.url, h: target.h})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $currentVoiceRoom}
|
{#if $targetRoom}
|
||||||
<div
|
<div
|
||||||
in:fly={{y: 60, duration: 350}}
|
in:fly={{y: 60, duration: 350}}
|
||||||
out:fly={{y: 60, duration: 250}}
|
out:fly={{y: 60, duration: 250}}
|
||||||
class="flex flex-col gap-2 rounded-box bg-base-100 p-3">
|
class="flex flex-col gap-2 rounded-box bg-base-100 p-3">
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-0.5">
|
||||||
{#if $voiceState === "joining"}
|
{#if $voiceState === VoiceState.Joining}
|
||||||
<span class="text-sm font-semibold text-warning">Joining...</span>
|
<span class="text-sm font-semibold text-warning">Joining...</span>
|
||||||
{:else if $voiceState === "connected"}
|
{:else if $voiceState === VoiceState.Connected}
|
||||||
<span class="text-sm font-semibold text-success">Voice Connected</span>
|
<span class="text-sm font-semibold text-success">Voice Connected</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-sm font-semibold text-neutral-content">Disconnected</span>
|
<span class="text-sm font-semibold text-neutral-content">Disconnected</span>
|
||||||
@@ -64,7 +72,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
{#if $voiceState === "joining"}
|
{#if $voiceState === VoiceState.Joining}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Cancel"
|
data-tip="Cancel"
|
||||||
@@ -72,7 +80,7 @@
|
|||||||
onclick={cancelJoinVoiceRoom}>
|
onclick={cancelJoinVoiceRoom}>
|
||||||
<Icon icon={CloseCircle} size={4} />
|
<Icon icon={CloseCircle} size={4} />
|
||||||
</Button>
|
</Button>
|
||||||
{:else if $voiceState === "connected" && $currentVoiceSession}
|
{:else if $voiceState === VoiceState.Connected && $currentVoiceSession}
|
||||||
<Button
|
<Button
|
||||||
data-tip={$currentVoiceSession.muted ? "Unmute" : "Mute"}
|
data-tip={$currentVoiceSession.muted ? "Unmute" : "Mute"}
|
||||||
class="center tooltip tooltip-top btn btn-sm btn-square {$currentVoiceSession.muted
|
class="center tooltip tooltip-top btn btn-sm btn-square {$currentVoiceSession.muted
|
||||||
@@ -91,7 +99,7 @@
|
|||||||
<Button
|
<Button
|
||||||
data-tip="Join Voice"
|
data-tip="Join Voice"
|
||||||
class="center tooltip tooltip-top btn btn-sm btn-square btn-success"
|
class="center tooltip tooltip-top btn btn-sm btn-square btn-success"
|
||||||
onclick={handleRejoin}>
|
onclick={openJoinDialog}>
|
||||||
<Icon icon={PhoneCallingRounded} size={4} />
|
<Icon icon={PhoneCallingRounded} size={4} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+24
-1
@@ -1,7 +1,9 @@
|
|||||||
import twColors from "tailwindcss/colors"
|
import twColors from "tailwindcss/colors"
|
||||||
import {context as pomadeContext} from "@pomade/core"
|
import {context as pomadeContext} from "@pomade/core"
|
||||||
import {Capacitor} from "@capacitor/core"
|
import {Capacitor} from "@capacitor/core"
|
||||||
import {derived, readable, writable} from "svelte/store"
|
import {page} from "$app/stores"
|
||||||
|
import type {Page} from "@sveltejs/kit"
|
||||||
|
import {derived, readable, writable, type Readable} from "svelte/store"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {
|
import {
|
||||||
on,
|
on,
|
||||||
@@ -583,6 +585,8 @@ export type Room = PublishedRoomMeta & {
|
|||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RoomRef = {url: string; h: string}
|
||||||
|
|
||||||
export const getRoomType = (room: RoomMeta): RoomType =>
|
export const getRoomType = (room: RoomMeta): RoomType =>
|
||||||
room.livekit ? RoomType.Voice : RoomType.Text
|
room.livekit ? RoomType.Voice : RoomType.Text
|
||||||
|
|
||||||
@@ -805,6 +809,25 @@ export const deriveOtherRooms = (url: string) =>
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const parseDisplayedRoomParams = (params: Page["params"]): RoomRef | undefined => {
|
||||||
|
const relay = params.relay
|
||||||
|
const h = params.h
|
||||||
|
if (!relay || !h || typeof h !== "string") return undefined
|
||||||
|
return {url: decodeRelay(relay), h}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveDisplayedRoom = derived(
|
||||||
|
derived(page, $p => parseDisplayedRoomParams($p.params)),
|
||||||
|
($p, set) => {
|
||||||
|
if (!$p) {
|
||||||
|
set(undefined)
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
const inner = deriveRoom($p.url, $p.h)
|
||||||
|
return inner.subscribe(set)
|
||||||
|
},
|
||||||
|
) as Readable<(RoomMeta & {url: string; id: string}) | undefined>
|
||||||
|
|
||||||
// Space/room memberships
|
// Space/room memberships
|
||||||
|
|
||||||
export const deriveSpaceMembers = (url: string) =>
|
export const deriveSpaceMembers = (url: string) =>
|
||||||
|
|||||||
+67
-22
@@ -2,21 +2,26 @@
|
|||||||
* Voice rooms via LiveKit. Note: Voice does not work on localhost in Firefox
|
* Voice rooms via LiveKit. Note: Voice does not work on localhost in Firefox
|
||||||
* (ICE candidate gathering fails). Use Chrome or test from deployed HTTPS.
|
* (ICE candidate gathering fails). Use Chrome or test from deployed HTTPS.
|
||||||
*/
|
*/
|
||||||
import {DisconnectReason, Room, RoomEvent, Track} from "livekit-client"
|
import {
|
||||||
|
DisconnectReason,
|
||||||
|
Room,
|
||||||
|
RoomEvent,
|
||||||
|
Track,
|
||||||
|
type AudioCaptureOptions,
|
||||||
|
type LocalParticipant,
|
||||||
|
} from "livekit-client"
|
||||||
import {derived, get, writable} from "svelte/store"
|
import {derived, get, writable} from "svelte/store"
|
||||||
import {map, removeUndefined, uniqBy} from "@welshman/lib"
|
import {map, removeUndefined, uniqBy} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||||
import {signer} from "@welshman/app"
|
import {signer} from "@welshman/app"
|
||||||
import {getLivekitEndpoint} from "$lib/livekit"
|
import {getLivekitEndpoint} from "$lib/livekit"
|
||||||
import {AbortError, whenAborted, whenTimeout} from "$lib/util"
|
import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util"
|
||||||
import {deriveLatestEventForUrl} from "@app/core/state"
|
import {deriveLatestEventForUrl, type RoomRef} from "@app/core/state"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
export const LIVEKIT_PARTICIPANTS = 39004
|
export const LIVEKIT_PARTICIPANTS = 39004
|
||||||
|
|
||||||
export {checkRelayHasLivekit} from "$lib/livekit"
|
|
||||||
|
|
||||||
export class VoiceJoinMembershipError extends Error {
|
export class VoiceJoinMembershipError extends Error {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Failed to join voice room: you must be a member.")
|
super("Failed to join voice room: you must be a member.")
|
||||||
@@ -24,6 +29,19 @@ export class VoiceJoinMembershipError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleJoinError = (e: unknown) => {
|
||||||
|
if (e instanceof AbortError) return
|
||||||
|
console.error("Failed to join voice room", e)
|
||||||
|
let message = "Failed to join voice room"
|
||||||
|
if (e instanceof VoiceJoinMembershipError) message = e.message
|
||||||
|
else if (e instanceof TimeoutError)
|
||||||
|
message = "Connection timed out. Please check your network and try again."
|
||||||
|
else if (e instanceof Error && e.message === "No signer available") message = e.message
|
||||||
|
pushToast({theme: "error", message})
|
||||||
|
}
|
||||||
|
|
||||||
|
export {checkRelayHasLivekit} from "$lib/livekit"
|
||||||
|
|
||||||
export type VoiceSession = {
|
export type VoiceSession = {
|
||||||
url: string
|
url: string
|
||||||
h: string
|
h: string
|
||||||
@@ -35,13 +53,17 @@ export type Pubkey = string
|
|||||||
|
|
||||||
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
||||||
|
|
||||||
export type VoiceState = "joining" | "connected" | "disconnected"
|
export enum VoiceState {
|
||||||
|
Joining = "joining",
|
||||||
|
Connected = "connected",
|
||||||
|
Disconnected = "disconnected",
|
||||||
|
}
|
||||||
|
|
||||||
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
||||||
|
|
||||||
export const voiceState = writable<VoiceState>("disconnected")
|
export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
|
||||||
|
|
||||||
export const currentVoiceRoom = writable<{url: string; h: string} | undefined>(undefined)
|
export const currentVoiceRoom = writable<RoomRef | undefined>(undefined)
|
||||||
|
|
||||||
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
||||||
|
|
||||||
@@ -140,10 +162,33 @@ export const deriveVoiceParticipants = (url: string, h: string) =>
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const setUpMicrophone = async (
|
||||||
|
startMuted: boolean,
|
||||||
|
preferredMicId: string | undefined,
|
||||||
|
participant: LocalParticipant,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
if (startMuted) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let muted = true
|
||||||
|
let capture: AudioCaptureOptions | undefined = undefined
|
||||||
|
if (preferredMicId) {
|
||||||
|
capture = {deviceId: preferredMicId}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await participant.setMicrophoneEnabled(true, capture)
|
||||||
|
muted = false
|
||||||
|
} catch (e) {
|
||||||
|
pushToast({theme: "error", message: "Could not access microphone"})
|
||||||
|
}
|
||||||
|
return muted
|
||||||
|
}
|
||||||
|
|
||||||
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
||||||
currentVoiceSession.set(undefined)
|
currentVoiceSession.set(undefined)
|
||||||
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
||||||
voiceState.set("disconnected")
|
voiceState.set(VoiceState.Disconnected)
|
||||||
const message =
|
const message =
|
||||||
reason === DisconnectReason.JOIN_FAILURE
|
reason === DisconnectReason.JOIN_FAILURE
|
||||||
? "Could not connect to voice room. Please try again."
|
? "Could not connect to voice room. Please try again."
|
||||||
@@ -191,14 +236,19 @@ export const cancelJoinVoiceRoom = () => {
|
|||||||
joinAbortController?.abort()
|
joinAbortController?.abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const joinVoiceRoom = async (url: string, h: string): Promise<void> => {
|
export const joinVoiceRoom = async (
|
||||||
|
url: string,
|
||||||
|
h: string,
|
||||||
|
startMuted = true,
|
||||||
|
preferredMicId?: string,
|
||||||
|
): Promise<void> => {
|
||||||
cancelJoinVoiceRoom()
|
cancelJoinVoiceRoom()
|
||||||
|
|
||||||
const session = get(currentVoiceSession)
|
const session = get(currentVoiceSession)
|
||||||
if (session) await leaveVoiceRoom()
|
if (session) await leaveVoiceRoom()
|
||||||
|
|
||||||
currentVoiceRoom.set({url, h})
|
currentVoiceRoom.set({url, h})
|
||||||
voiceState.set("joining")
|
voiceState.set(VoiceState.Joining)
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
joinAbortController = controller
|
joinAbortController = controller
|
||||||
@@ -238,20 +288,15 @@ export const joinVoiceRoom = async (url: string, h: string): Promise<void> => {
|
|||||||
addParticipant(p.identity)
|
addParticipant(p.identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
let muted = false
|
const muted = await setUpMicrophone(startMuted, preferredMicId, room.localParticipant)
|
||||||
try {
|
|
||||||
await room.localParticipant.setMicrophoneEnabled(true)
|
|
||||||
} catch (e) {
|
|
||||||
muted = true
|
|
||||||
pushToast({theme: "error", message: "Could not access microphone"})
|
|
||||||
}
|
|
||||||
|
|
||||||
currentVoiceSession.set({url, h, room, muted})
|
currentVoiceSession.set({url, h, room, muted})
|
||||||
voiceState.set("connected")
|
voiceState.set(VoiceState.Connected)
|
||||||
playJoinSound()
|
playJoinSound()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isActive()) voiceState.set("disconnected")
|
if (isActive()) voiceState.set(VoiceState.Disconnected)
|
||||||
throw e
|
if (e instanceof AbortError) return
|
||||||
|
handleJoinError(e)
|
||||||
} finally {
|
} finally {
|
||||||
if (isActive()) joinAbortController = undefined
|
if (isActive()) joinAbortController = undefined
|
||||||
}
|
}
|
||||||
@@ -264,7 +309,7 @@ export const leaveVoiceRoom = async () => {
|
|||||||
const audio = new Audio("/leave-voice-room.mp3")
|
const audio = new Audio("/leave-voice-room.mp3")
|
||||||
audio.play().catch(() => {})
|
audio.play().catch(() => {})
|
||||||
|
|
||||||
voiceState.set("disconnected")
|
voiceState.set(VoiceState.Disconnected)
|
||||||
currentVoiceSession.set(undefined)
|
currentVoiceSession.set(undefined)
|
||||||
session.room.disconnect()
|
session.room.disconnect()
|
||||||
speakingParticipants.set([])
|
speakingParticipants.set([])
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||||
import {voiceState} from "@app/voice"
|
import {VoiceState, voiceState} from "@app/voice"
|
||||||
import {makeFeed} from "@app/core/requests"
|
import {makeFeed} from "@app/core/requests"
|
||||||
import {popKey} from "@lib/implicit"
|
import {popKey} from "@lib/implicit"
|
||||||
import {checked} from "@app/util/notifications"
|
import {checked} from "@app/util/notifications"
|
||||||
@@ -494,7 +494,7 @@
|
|||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if isVoiceRoom || $voiceState === "joining" || $voiceState === "connected"}
|
{#if isVoiceRoom || $voiceState === VoiceState.Joining || $voiceState === VoiceState.Connected}
|
||||||
<div class="hide-on-keyboard flex-shrink-0 p-2 md:hidden">
|
<div class="hide-on-keyboard flex-shrink-0 p-2 md:hidden">
|
||||||
<VoiceWidget />
|
<VoiceWidget />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user