Add error toast on connection failure.
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
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 RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
import {deriveVoiceParticipants, joinVoiceRoom, currentVoiceSession} from "@app/voice"
|
import {deriveVoiceParticipants, joinVoiceRoom, currentVoiceSession} from "@app/voice"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -17,7 +18,14 @@
|
|||||||
const participants = deriveVoiceParticipants(url, h)
|
const participants = deriveVoiceParticipants(url, h)
|
||||||
const isActive = $derived($currentVoiceSession?.url === url && $currentVoiceSession?.h === h)
|
const isActive = $derived($currentVoiceSession?.url === url && $currentVoiceSession?.h === h)
|
||||||
|
|
||||||
const handleClick = () => joinVoiceRoom(url, h)
|
const handleClick = async () => {
|
||||||
|
try {
|
||||||
|
await joinVoiceRoom(url, h)
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : String(e)
|
||||||
|
pushToast({theme: "error", message: `Failed to join voice room: ${message}`})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
for (const pk of $participants) {
|
for (const pk of $participants) {
|
||||||
|
|||||||
+47
-21
@@ -1,9 +1,11 @@
|
|||||||
|
import {DisconnectReason, Room, RoomEvent, Track} from "livekit-client"
|
||||||
|
import {getToken} from "nostr-tools/nip98"
|
||||||
import {derived, get, writable} from "svelte/store"
|
import {derived, get, writable} from "svelte/store"
|
||||||
import {Room, RoomEvent, Track} from "livekit-client"
|
|
||||||
import {now} from "@welshman/lib"
|
import {now} from "@welshman/lib"
|
||||||
import {makeEvent, getTagValue} from "@welshman/util"
|
import {makeEvent, getTagValue} from "@welshman/util"
|
||||||
import {signer, publishThunk} from "@welshman/app"
|
import {signer, publishThunk} from "@welshman/app"
|
||||||
import {deriveEventsForUrl} from "@app/core/state"
|
import {deriveEventsForUrl} from "@app/core/state"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
export const ROOM_PRESENCE = 10312
|
export const ROOM_PRESENCE = 10312
|
||||||
|
|
||||||
@@ -19,32 +21,34 @@ export type VoiceSession = {
|
|||||||
|
|
||||||
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
||||||
|
|
||||||
const buildNip98AuthEvent = async (url: string, method: string) => {
|
|
||||||
const $signer = signer.get()
|
|
||||||
if (!$signer) throw new Error("No signer available")
|
|
||||||
|
|
||||||
const event = makeEvent(27235, {
|
|
||||||
tags: [
|
|
||||||
["u", url],
|
|
||||||
["method", method],
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
return $signer.sign(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchLivekitToken = async (
|
const fetchLivekitToken = async (
|
||||||
url: string,
|
url: string,
|
||||||
groupId: string,
|
groupId: string,
|
||||||
): Promise<{server_url: string; participant_token: string}> => {
|
): Promise<{server_url: string; participant_token: string}> => {
|
||||||
const httpUrl = url.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://")
|
const httpUrl = url
|
||||||
|
.replace(/^wss:\/\//, "https://")
|
||||||
|
.replace(/^ws:\/\//, "http://")
|
||||||
|
.replace(/\/$/, "")
|
||||||
const endpoint = `${httpUrl}/.well-known/nip29/livekit/${groupId}`
|
const endpoint = `${httpUrl}/.well-known/nip29/livekit/${groupId}`
|
||||||
|
|
||||||
const authEvent = await buildNip98AuthEvent(endpoint, "GET")
|
const $signer = signer.get()
|
||||||
const encoded = btoa(JSON.stringify(authEvent))
|
if (!$signer) throw new Error("No signer available")
|
||||||
|
|
||||||
|
const authHeader = await getToken(
|
||||||
|
endpoint,
|
||||||
|
"GET",
|
||||||
|
template =>
|
||||||
|
$signer.sign(
|
||||||
|
makeEvent(template.kind, {
|
||||||
|
tags: template.tags,
|
||||||
|
content: template.content ?? "",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
headers: {Authorization: `Nostr ${encoded}`},
|
headers: {Authorization: authHeader},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -115,9 +119,16 @@ export const joinVoiceRoom = async (url: string, h: string) => {
|
|||||||
dynacast: true,
|
dynacast: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
room.on(RoomEvent.Disconnected, () => {
|
room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {
|
||||||
currentVoiceSession.set(undefined)
|
currentVoiceSession.set(undefined)
|
||||||
stopPresenceHeartbeat()
|
stopPresenceHeartbeat()
|
||||||
|
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
||||||
|
const message =
|
||||||
|
reason === DisconnectReason.JOIN_FAILURE
|
||||||
|
? "Could not connect to voice room. Please try again."
|
||||||
|
: "Voice connection lost."
|
||||||
|
pushToast({theme: "error", message})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
room.on(RoomEvent.TrackSubscribed, (track, _publication, _participant) => {
|
room.on(RoomEvent.TrackSubscribed, (track, _publication, _participant) => {
|
||||||
@@ -133,7 +144,22 @@ export const joinVoiceRoom = async (url: string, h: string) => {
|
|||||||
track.detach().forEach(el => el.remove())
|
track.detach().forEach(el => el.remove())
|
||||||
})
|
})
|
||||||
|
|
||||||
await room.connect(server_url, participant_token)
|
const CONNECT_TIMEOUT_MS = 5_000
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
room.connect(server_url, participant_token, {maxRetries: 0}),
|
||||||
|
new Promise<never>((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error("Connection timed out. Please check your network and try again.")),
|
||||||
|
CONNECT_TIMEOUT_MS,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
} catch (e) {
|
||||||
|
room.disconnect()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
await room.localParticipant.setMicrophoneEnabled(true)
|
await room.localParticipant.setMicrophoneEnabled(true)
|
||||||
|
|
||||||
currentVoiceSession.set({url, h, room, muted: false})
|
currentVoiceSession.set({url, h, room, muted: false})
|
||||||
|
|||||||
Reference in New Issue
Block a user