Add a dialog before joining voice rooms #109
@@ -1,15 +1,18 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {goto} from "$app/navigation"
|
||||
import {loadProfile, displayProfileByPubkey} from "@welshman/app"
|
||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import RoomImage from "@app/components/RoomImage.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {handleJoinError} from "@app/components/VoiceWidget.svelte"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
||||
import {makeRoomId} from "@app/core/state"
|
||||
import {
|
||||
VoiceState,
|
||||
deriveVoiceParticipants,
|
||||
joinVoiceRoom,
|
||||
cancelJoinVoiceRoom,
|
||||
currentVoiceRoom,
|
||||
voiceState,
|
||||
@@ -28,21 +31,24 @@
|
||||
|
||||
const participants = deriveVoiceParticipants(url, h)
|
||||
const isActive = $derived(
|
||||
$voiceState === "connected" && $currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h,
|
||||
$voiceState === VoiceState.Connected && $currentVoiceRoom?.id === makeRoomId(url, h),
|
||||
)
|
||||
const isJoining = $derived(
|
||||
$voiceState === "joining" && $currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h,
|
||||
$voiceState === VoiceState.Joining && $currentVoiceRoom?.id === makeRoomId(url, h),
|
||||
)
|
||||
|
||||
const handleClick = async () => {
|
||||
const handleClick = async (e: MouseEvent) => {
|
||||
if (isActive) return
|
||||
|
||||
if (isJoining) {
|
||||
e.preventDefault()
|
||||
cancelJoinVoiceRoom()
|
||||
return
|
||||
}
|
||||
|
||||
await joinVoiceRoom(url, h).catch(handleJoinError)
|
||||
e.preventDefault()
|
||||
await goto(makeRoomPath(url, h), {replaceState})
|
||||
pushModal(VoiceRoomJoinDialog, {url, h})
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<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 {displayRoom} 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 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">{displayRoom(url, 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,8 @@
|
||||
<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">
|
||||
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"
|
||||
@@ -25,36 +11,69 @@
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {displayRoom} from "@app/core/state"
|
||||
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,
|
||||
rejoinVoiceRoom,
|
||||
cancelJoinVoiceRoom,
|
||||
} from "@app/voice"
|
||||
|
||||
const roomName = $derived(
|
||||
$currentVoiceRoom ? displayRoom($currentVoiceRoom.url, $currentVoiceRoom.h) : "",
|
||||
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 spaceName = $derived($currentVoiceRoom ? displayRelayUrl($currentVoiceRoom.url) : "")
|
||||
const routeDisplayedRoom = $derived($displayedRoomStore)
|
||||
|
||||
const handleRejoin = () => {
|
||||
void rejoinVoiceRoom().catch(handleJoinError)
|
||||
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
|
||||
|
hodlbod
commented
Just use Just use `$targetRoom` here, as long as you're in a svelte file you can avoid using get, even if you're not in a template
|
||||
}
|
||||
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 $currentVoiceRoom}
|
||||
{#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 === "joining"}
|
||||
{#if $voiceState === VoiceState.Joining}
|
||||
<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>
|
||||
{:else}
|
||||
<span class="text-sm font-semibold text-neutral-content">Disconnected</span>
|
||||
@@ -64,7 +83,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
{#if $voiceState === "joining"}
|
||||
{#if $voiceState === VoiceState.Joining}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
<Button
|
||||
data-tip="Cancel"
|
||||
@@ -72,7 +91,7 @@
|
||||
onclick={cancelJoinVoiceRoom}>
|
||||
<Icon icon={CloseCircle} size={4} />
|
||||
</Button>
|
||||
{:else if $voiceState === "connected" && $currentVoiceSession}
|
||||
{: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
|
||||
@@ -91,7 +110,7 @@
|
||||
<Button
|
||||
data-tip="Join Voice"
|
||||
class="center tooltip tooltip-top btn btn-sm btn-square btn-success"
|
||||
onclick={handleRejoin}>
|
||||
onclick={openJoinDialog}>
|
||||
<Icon icon={PhoneCallingRounded} size={4} />
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -669,7 +669,7 @@ export const deriveRoom = call(() => {
|
||||
return (url: string, h: string) =>
|
||||
derived(
|
||||
_deriveRoom(makeRoomId(url, h)),
|
||||
room => room || {url, id: makeRoomId(url, h), ...makeRoomMeta({h})},
|
||||
room => (room || {url, id: makeRoomId(url, h), ...makeRoomMeta({h})}) as Room,
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
+67
-44
@@ -2,7 +2,14 @@
|
||||
* Voice rooms via LiveKit. Note: Voice does not work on localhost in Firefox
|
||||
* (ICE candidate gathering fails). Use Chrome or test from deployed HTTPS.
|
||||
*/
|
||||
import {DisconnectReason, Room, RoomEvent, Track} from "livekit-client"
|
||||
import {
|
||||
DisconnectReason,
|
||||
Room as LiveKitRoom,
|
||||
RoomEvent,
|
||||
Track,
|
||||
type AudioCaptureOptions,
|
||||
type LocalParticipant,
|
||||
} from "livekit-client"
|
||||
import {derived, get, writable} from "svelte/store"
|
||||
import {map, removeUndefined, uniqBy} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
@@ -10,24 +17,17 @@ import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||
import {signer} from "@welshman/app"
|
||||
import {getLivekitEndpoint} from "$lib/livekit"
|
||||
import {AbortError, whenAborted, whenTimeout} from "$lib/util"
|
||||
import {deriveLatestEventForUrl} from "@app/core/state"
|
||||
import {deriveLatestEventForUrl, deriveRoom, makeRoomId, type Room} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
export const LIVEKIT_PARTICIPANTS = 39004
|
||||
|
||||
export {checkRelayHasLivekit} from "$lib/livekit"
|
||||
|
||||
export class VoiceJoinMembershipError extends Error {
|
||||
constructor() {
|
||||
super("Failed to join voice room: you must be a member.")
|
||||
this.name = "VoiceJoinMembershipError"
|
||||
}
|
||||
}
|
||||
|
||||
export type VoiceSession = {
|
||||
url: string
|
||||
h: string
|
||||
room: Room
|
||||
room: LiveKitRoom
|
||||
muted: boolean
|
||||
}
|
||||
|
||||
@@ -35,13 +35,17 @@ export type Pubkey = string
|
||||
|
||||
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
||||
|
||||
export type VoiceState = "joining" | "connected" | "disconnected"
|
||||
export enum VoiceState {
|
||||
|
hodlbod marked this conversation as resolved
Outdated
mplorentz
commented
idk why I didn't make this an enum before. idk why I didn't make this an enum before.
|
||||
Joining = "joining",
|
||||
Connected = "connected",
|
||||
Disconnected = "disconnected",
|
||||
}
|
||||
|
||||
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<Room | undefined>(undefined)
|
||||
|
hodlbod
commented
Any reason we can't just use Any reason we can't just use `Room` from core/state? Or maybe deriveRoom doesn't return that type.
mplorentz
commented
Yeah, I was able to do that. Yeah, I was able to do that.
|
||||
|
||||
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
||||
|
||||
@@ -102,7 +106,6 @@ const fetchLivekitToken = async (
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
if (response.status === 403) throw new VoiceJoinMembershipError()
|
||||
throw new Error(`Token request failed (${response.status}): ${text}`)
|
||||
}
|
||||
|
||||
@@ -118,10 +121,7 @@ export const deriveVoiceParticipants = (url: string, h: string) =>
|
||||
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
|
||||
],
|
||||
([$participantPubkeyMap, $currentVoiceRoom, $publishedParticipantList]) => {
|
||||
const inCall =
|
||||
$participantPubkeyMap.size > 0 &&
|
||||
$currentVoiceRoom?.url === url &&
|
||||
$currentVoiceRoom?.h === h
|
||||
const inCall = $participantPubkeyMap.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
|
||||
|
||||
if (inCall) {
|
||||
const participants = [...$participantPubkeyMap.keys()].map(participantFromLiveKitIdentity)
|
||||
@@ -140,10 +140,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) => {
|
||||
currentVoiceSession.set(undefined)
|
||||
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
||||
voiceState.set("disconnected")
|
||||
voiceState.set(VoiceState.Disconnected)
|
||||
const message =
|
||||
reason === DisconnectReason.JOIN_FAILURE
|
||||
? "Could not connect to voice room. Please try again."
|
||||
@@ -191,14 +214,19 @@ export const cancelJoinVoiceRoom = () => {
|
||||
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()
|
||||
|
||||
const session = get(currentVoiceSession)
|
||||
if (session) await leaveVoiceRoom()
|
||||
|
||||
currentVoiceRoom.set({url, h})
|
||||
voiceState.set("joining")
|
||||
currentVoiceRoom.set(get(deriveRoom(url, h)))
|
||||
voiceState.set(VoiceState.Joining)
|
||||
|
||||
const controller = new AbortController()
|
||||
joinAbortController = controller
|
||||
@@ -210,47 +238,42 @@ export const joinVoiceRoom = async (url: string, h: string): Promise<void> => {
|
||||
|
||||
if (signal.aborted) throw new AbortError()
|
||||
|
||||
const room = new Room({adaptiveStream: true, dynacast: true})
|
||||
const liveKitRoom = new LiveKitRoom({adaptiveStream: true, dynacast: true})
|
||||
|
||||
room.on(RoomEvent.Disconnected, onRoomDisconnected)
|
||||
room.on(RoomEvent.ParticipantConnected, onParticipantConnected)
|
||||
room.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
|
||||
room.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
||||
room.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||
room.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
||||
liveKitRoom.on(RoomEvent.Disconnected, onRoomDisconnected)
|
||||
liveKitRoom.on(RoomEvent.ParticipantConnected, onParticipantConnected)
|
||||
liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
|
||||
liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
||||
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
room.connect(server_url, participant_token, {maxRetries: 0}),
|
||||
liveKitRoom.connect(server_url, participant_token, {maxRetries: 0}),
|
||||
whenTimeout(5_000, {
|
||||
message: "Connection timed out. Please check your network and try again.",
|
||||
}),
|
||||
whenAborted(signal),
|
||||
])
|
||||
} catch (e) {
|
||||
room.disconnect()
|
||||
liveKitRoom.disconnect()
|
||||
throw e
|
||||
}
|
||||
|
||||
participantPubkeyMap.set(new Map())
|
||||
addParticipant(room.localParticipant.identity)
|
||||
for (const p of room.remoteParticipants.values()) {
|
||||
addParticipant(liveKitRoom.localParticipant.identity)
|
||||
for (const p of liveKitRoom.remoteParticipants.values()) {
|
||||
addParticipant(p.identity)
|
||||
}
|
||||
|
||||
let muted = false
|
||||
try {
|
||||
await room.localParticipant.setMicrophoneEnabled(true)
|
||||
} catch (e) {
|
||||
muted = true
|
||||
pushToast({theme: "error", message: "Could not access microphone"})
|
||||
}
|
||||
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
||||
|
||||
currentVoiceSession.set({url, h, room, muted})
|
||||
voiceState.set("connected")
|
||||
currentVoiceSession.set({url, h, room: liveKitRoom, muted})
|
||||
voiceState.set(VoiceState.Connected)
|
||||
playJoinSound()
|
||||
} catch (e) {
|
||||
if (isActive()) voiceState.set("disconnected")
|
||||
if (isActive()) voiceState.set(VoiceState.Disconnected)
|
||||
if (e instanceof AbortError) return
|
||||
throw e
|
||||
} finally {
|
||||
if (isActive()) joinAbortController = undefined
|
||||
@@ -264,7 +287,7 @@ export const leaveVoiceRoom = async () => {
|
||||
const audio = new Audio("/leave-voice-room.mp3")
|
||||
audio.play().catch(() => {})
|
||||
|
||||
voiceState.set("disconnected")
|
||||
voiceState.set(VoiceState.Disconnected)
|
||||
currentVoiceSession.set(undefined)
|
||||
session.room.disconnect()
|
||||
speakingParticipants.set([])
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
userSettingsValues,
|
||||
} from "@app/core/state"
|
||||
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 {popKey} from "@lib/implicit"
|
||||
import {checked} from "@app/util/notifications"
|
||||
@@ -494,7 +494,7 @@
|
||||
{/key}
|
||||
{/if}
|
||||
</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">
|
||||
<VoiceWidget />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user
This logic got more complex. Basically we need incorporate the room the user is currently viewing now.
Detailed explanation: now that you can join the text room without joining the voice call I needed to handle the case where you join voice room A, then view the text chat for voice room B, then disconnect from the call. In this case the
VoiceWidgetshould show a button to join the call for voice room B since that's the room you are currently viewing. It required some changes to state.ts which I'm not sure I'm happy with. More on that below.