diff --git a/src/app/components/VoiceRoomItem.svelte b/src/app/components/VoiceRoomItem.svelte
index 5df903d5..2fd4f9b7 100644
--- a/src/app/components/VoiceRoomItem.svelte
+++ b/src/app/components/VoiceRoomItem.svelte
@@ -6,7 +6,12 @@
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import RoomName from "@app/components/RoomName.svelte"
import {pushToast} from "@app/util/toast"
- import {deriveVoiceParticipants, joinVoiceRoom, currentVoiceSession} from "@app/voice"
+ import {
+ deriveVoiceParticipants,
+ joinVoiceRoom,
+ leaveVoiceRoom,
+ currentVoiceSession,
+ } from "@app/voice"
interface Props {
url: string
@@ -18,17 +23,29 @@
const participants = deriveVoiceParticipants(url, h)
const isActive = $derived($currentVoiceSession?.url === url && $currentVoiceSession?.h === h)
let isJoining = $state(false)
+ let joinAbortController: AbortController | undefined
const handleClick = async () => {
- if (isJoining) return
+ if (isActive) {
+ await leaveVoiceRoom()
+ return
+ }
+ if (isJoining) {
+ joinAbortController?.abort()
+ return
+ }
+ joinAbortController = new AbortController()
isJoining = true
try {
- await joinVoiceRoom(url, h)
+ await joinVoiceRoom(url, h, joinAbortController.signal)
} catch (e) {
+ if (e instanceof Error && e.message === "Join cancelled") return
+ if (e instanceof DOMException && e.name === "AbortError") return
const message = e instanceof Error ? e.message : String(e)
pushToast({theme: "error", message: `Failed to join voice room: ${message}`})
} finally {
isJoining = false
+ joinAbortController = undefined
}
}
@@ -40,10 +57,7 @@
-
+
{#if isJoining}
{:else}
diff --git a/src/app/voice.ts b/src/app/voice.ts
index 365be1e2..b3db81c7 100644
--- a/src/app/voice.ts
+++ b/src/app/voice.ts
@@ -24,6 +24,7 @@ export const currentVoiceSession = writable(undefined)
const fetchLivekitToken = async (
url: string,
groupId: string,
+ signal?: AbortSignal,
): Promise<{server_url: string; participant_token: string}> => {
const httpUrl = url
.replace(/^wss:\/\//, "https://")
@@ -34,6 +35,8 @@ const fetchLivekitToken = async (
const $signer = signer.get()
if (!$signer) throw new Error("No signer available")
+ if (signal?.aborted) throw new Error("Join cancelled")
+
const authHeader = await getToken(
endpoint,
"GET",
@@ -47,9 +50,16 @@ const fetchLivekitToken = async (
true,
)
- const response = await fetch(endpoint, {
- headers: {Authorization: authHeader},
- })
+ let response: Response
+ try {
+ response = await fetch(endpoint, {
+ headers: {Authorization: authHeader},
+ signal,
+ })
+ } catch (e) {
+ if (e instanceof DOMException && e.name === "AbortError") throw new Error("Join cancelled")
+ throw e
+ }
if (!response.ok) {
const text = await response.text()
@@ -104,7 +114,11 @@ const stopPresenceHeartbeat = () => {
}
}
-export const joinVoiceRoom = async (url: string, h: string) => {
+export const joinVoiceRoom = async (
+ url: string,
+ h: string,
+ signal?: AbortSignal,
+): Promise => {
const session = get(currentVoiceSession)
if (session) {
@@ -112,7 +126,9 @@ export const joinVoiceRoom = async (url: string, h: string) => {
await leaveVoiceRoom()
}
- const {server_url, participant_token} = await fetchLivekitToken(url, h)
+ const {server_url, participant_token} = await fetchLivekitToken(url, h, signal)
+
+ if (signal?.aborted) throw new Error("Join cancelled")
const room = new Room({
adaptiveStream: true,
@@ -144,6 +160,17 @@ export const joinVoiceRoom = async (url: string, h: string) => {
track.detach().forEach(el => el.remove())
})
+ const onAbort = () => {
+ room.disconnect()
+ }
+ if (signal) {
+ if (signal.aborted) {
+ room.disconnect()
+ throw new Error("Join cancelled")
+ }
+ signal.addEventListener("abort", onAbort, {once: true})
+ }
+
const CONNECT_TIMEOUT_MS = 5_000
try {
@@ -158,8 +185,14 @@ export const joinVoiceRoom = async (url: string, h: string) => {
])
} catch (e) {
room.disconnect()
+ if (signal?.aborted) {
+ throw new Error("Join cancelled")
+ }
throw e
+ } finally {
+ signal?.removeEventListener("abort", onAbort)
}
+ if (signal?.aborted) throw new Error("Join cancelled")
await room.localParticipant.setMicrophoneEnabled(true)
currentVoiceSession.set({url, h, room, muted: false})