diff --git a/app/components/Spinner.tsx b/app/components/Spinner.tsx new file mode 100644 index 00000000..0c3de6f2 --- /dev/null +++ b/app/components/Spinner.tsx @@ -0,0 +1,33 @@ +import { forwardRef, type ComponentProps, type ElementRef } from 'react' +import { cn } from '~/utils/style' + +export const Spinner = forwardRef, ComponentProps<'svg'>>( + ({ className, ...rest }, ref) => { + return ( + + + + + ) + } +) + +Spinner.displayName = 'Spinner' diff --git a/app/routes/_room.$roomName._index.tsx b/app/routes/_room.$roomName._index.tsx index 16c03c28..dd11a78d 100644 --- a/app/routes/_room.$roomName._index.tsx +++ b/app/routes/_room.$roomName._index.tsx @@ -13,7 +13,9 @@ import { MicButton } from '~/components/MicButton' import { SelfView } from '~/components/SelfView' import { SettingsButton } from '~/components/SettingsDialog' +import { Spinner } from '~/components/Spinner' import { Tooltip } from '~/components/Tooltip' +import { useSubscribedState } from '~/hooks/rxjsHooks' import { useRoomContext } from '~/hooks/useRoomContext' import { errorMessageMap } from '~/hooks/useUserMedia' import getUsername from '~/utils/getUsername.server' @@ -27,8 +29,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { export default function Lobby() { const { roomName } = useParams() const navigate = useNavigate() - const { setJoined, userMedia, room } = useRoomContext() + const { setJoined, userMedia, room, peer } = useRoomContext() const { videoStreamTrack, audioStreamTrack, audioEnabled } = userMedia + const session = useSubscribedState(peer.session$) + const sessionError = useSubscribedState(peer.sessionError$) const joinedUsers = new Set( room.otherUsers.filter((u) => u.tracks.audio).map((u) => u.name) @@ -55,21 +59,33 @@ export default function Lobby() { className="aspect-[4/3] w-full" videoTrack={videoStreamTrack} /> - {audioStreamTrack && ( -
- {audioEnabled ? ( - - ) : ( - -
- - Mic is turned off -
-
- )} -
- )} + +
+ {!sessionError && !session?.sessionId ? ( + + ) : ( + audioStreamTrack && ( + <> + {audioEnabled ? ( + + ) : ( + +
+ + Mic is turned off +
+
+ )} + + ) + )} +
+ {sessionError && ( +
+ {sessionError} +
+ )} {(userMedia.audioUnavailableReason || userMedia.videoUnavailableReason) && (
@@ -123,6 +139,7 @@ export default function Lobby() { // the room without the JS having loaded navigate('room') }} + disabled={!session?.sessionId} > Join diff --git a/app/utils/rxjs/RxjsPeer.client.ts b/app/utils/rxjs/RxjsPeer.client.ts index fe1a59e2..73eabe32 100644 --- a/app/utils/rxjs/RxjsPeer.client.ts +++ b/app/utils/rxjs/RxjsPeer.client.ts @@ -1,7 +1,9 @@ import { Observable, + catchError, combineLatest, distinctUntilChanged, + filter, from, fromEvent, map, @@ -50,6 +52,7 @@ export class RxjsPeer { peerConnection: RTCPeerConnection sessionId: string }> + sessionError$: Observable peerConnectionState$: Observable config: PeerConfig @@ -137,6 +140,13 @@ export class RxjsPeer { }) ) + this.sessionError$ = this.session$.pipe( + catchError((err) => + of(err instanceof Error ? err.message : 'Caught non-error') + ), + filter((value) => typeof value === 'string') + ) + this.peerConnectionState$ = this.peerConnection$.pipe( switchMap((peerConnection) => fromEvent( @@ -171,12 +181,14 @@ export class RxjsPeer { async createSession(peerConnection: RTCPeerConnection) { console.debug('🆕 creating new session') const { apiBase } = this.config - const { sessionId } = await this.fetchWithRecordedHistory( + const response = await this.fetchWithRecordedHistory( `${apiBase}/sessions/new?SESSION`, - { - method: 'POST', - } - ).then((res) => res.json() as any) + { method: 'POST' } + ) + if (response.status > 400) { + throw new Error('Error creating Calls session') + } + const { sessionId } = (await response.json()) as any return { peerConnection, sessionId } }