From 56d386cb6b7dd75faade68781cddd5363d79f1b6 Mon Sep 17 00:00:00 2001 From: mplorentz Date: Wed, 25 Mar 2026 15:57:10 -0400 Subject: [PATCH 1/2] Show better error when failing to join a voice room due to membership requirements. --- src/app/components/VoiceRoomItem.svelte | 8 +------- src/app/voice.ts | 27 +++++++++++++++++++++---- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/app/components/VoiceRoomItem.svelte b/src/app/components/VoiceRoomItem.svelte index 5676c314..f17748e3 100644 --- a/src/app/components/VoiceRoomItem.svelte +++ b/src/app/components/VoiceRoomItem.svelte @@ -5,7 +5,6 @@ import ProfileCircle from "@app/components/ProfileCircle.svelte" import RoomImage from "@app/components/RoomImage.svelte" import RoomName from "@app/components/RoomName.svelte" - import {pushToast} from "@app/util/toast" import {makeRoomPath} from "@app/util/routes" import { deriveVoiceParticipants, @@ -42,12 +41,7 @@ return } - try { - await joinVoiceRoom(url, h) - } catch (e) { - console.error("Failed to join voice room", e) - pushToast({theme: "error", message: "Failed to join voice room"}) - } + await joinVoiceRoom(url, h) } $effect(() => { diff --git a/src/app/voice.ts b/src/app/voice.ts index e8222e99..8e13a098 100644 --- a/src/app/voice.ts +++ b/src/app/voice.ts @@ -9,7 +9,7 @@ import type {TrustedEvent} from "@welshman/util" 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 {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util" import {deriveLatestEventForUrl} from "@app/core/state" import {pushToast} from "@app/util/toast" @@ -17,6 +17,24 @@ 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" + } +} + +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 type VoiceSession = { url: string h: string @@ -95,6 +113,7 @@ 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}`) } @@ -243,8 +262,7 @@ export const joinVoiceRoom = async (url: string, h: string): Promise => { playJoinSound() } catch (e) { if (isActive()) voiceState.set("disconnected") - if (e instanceof AbortError) return - throw e + handleJoinError(e) } finally { if (isActive()) joinAbortController = undefined } @@ -266,7 +284,8 @@ export const leaveVoiceRoom = async () => { export const rejoinVoiceRoom = () => { const target = get(currentVoiceRoom) - if (target) joinVoiceRoom(target.url, target.h) + if (!target) return + void joinVoiceRoom(target.url, target.h) } export const toggleMute = async () => { -- 2.52.0 From e8a22ceba60d49f89953948cb5ae6a325bffab69 Mon Sep 17 00:00:00 2001 From: mplorentz Date: Fri, 27 Mar 2026 13:24:54 -0400 Subject: [PATCH 2/2] Move handleJoinError to VoiceWidget --- src/app/components/VoiceRoomItem.svelte | 3 ++- src/app/components/VoiceWidget.svelte | 23 ++++++++++++++++++++++- src/app/voice.ts | 19 ++++--------------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/app/components/VoiceRoomItem.svelte b/src/app/components/VoiceRoomItem.svelte index f17748e3..30c7fe9f 100644 --- a/src/app/components/VoiceRoomItem.svelte +++ b/src/app/components/VoiceRoomItem.svelte @@ -5,6 +5,7 @@ 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 { deriveVoiceParticipants, @@ -41,7 +42,7 @@ return } - await joinVoiceRoom(url, h) + await joinVoiceRoom(url, h).catch(handleJoinError) } $effect(() => { diff --git a/src/app/components/VoiceWidget.svelte b/src/app/components/VoiceWidget.svelte index ae3f3eb5..239ba8f4 100644 --- a/src/app/components/VoiceWidget.svelte +++ b/src/app/components/VoiceWidget.svelte @@ -1,3 +1,20 @@ + + {#if $currentVoiceRoom} @@ -70,7 +91,7 @@ {/if} diff --git a/src/app/voice.ts b/src/app/voice.ts index 8e13a098..8856c2d2 100644 --- a/src/app/voice.ts +++ b/src/app/voice.ts @@ -9,7 +9,7 @@ import type {TrustedEvent} from "@welshman/util" import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util" import {signer} from "@welshman/app" import {getLivekitEndpoint} from "$lib/livekit" -import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util" +import {AbortError, whenAborted, whenTimeout} from "$lib/util" import {deriveLatestEventForUrl} from "@app/core/state" import {pushToast} from "@app/util/toast" @@ -24,17 +24,6 @@ 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 type VoiceSession = { url: string h: string @@ -262,7 +251,7 @@ export const joinVoiceRoom = async (url: string, h: string): Promise => { playJoinSound() } catch (e) { if (isActive()) voiceState.set("disconnected") - handleJoinError(e) + throw e } finally { if (isActive()) joinAbortController = undefined } @@ -282,10 +271,10 @@ export const leaveVoiceRoom = async () => { participantPubkeyMap.set(new Map()) } -export const rejoinVoiceRoom = () => { +export const rejoinVoiceRoom = async (): Promise => { const target = get(currentVoiceRoom) if (!target) return - void joinVoiceRoom(target.url, target.h) + return joinVoiceRoom(target.url, target.h) } export const toggleMute = async () => { -- 2.52.0