Skip to content

Commit 858c977

Browse files
disconnect after fadeout
1 parent 8e42d25 commit 858c977

File tree

7 files changed

+143
-96
lines changed

7 files changed

+143
-96
lines changed

app/ui/_room-provider.tsx

Lines changed: 0 additions & 17 deletions
This file was deleted.

app/ui/layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import * as React from 'react';
22
import { headers } from 'next/headers';
3+
import { SessionProvider } from '@/components/app/session-provider';
34
import { getAppConfig } from '@/lib/utils';
4-
import { RoomProvider } from './_room-provider';
55

66
export default async function ComponentsLayout({ children }: { children: React.ReactNode }) {
77
const hdrs = await headers();
88
const appConfig = await getAppConfig(hdrs);
99

1010
return (
11-
<RoomProvider appConfig={appConfig}>
11+
<SessionProvider appConfig={appConfig}>
1212
<div className="bg-muted/20 min-h-svh p-8">
1313
<div className="mx-auto max-w-3xl space-y-8">
1414
<header className="space-y-2">
@@ -25,6 +25,6 @@ export default async function ComponentsLayout({ children }: { children: React.R
2525
<main className="space-y-20">{children}</main>
2626
</div>
2727
</div>
28-
</RoomProvider>
28+
</SessionProvider>
2929
);
3030
}

components/app/app.tsx

Lines changed: 6 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,24 @@
11
'use client';
22

3-
import { AnimatePresence, motion } from 'motion/react';
4-
import { RoomAudioRenderer, RoomContext, StartAudio } from '@livekit/components-react';
3+
import { RoomAudioRenderer, StartAudio } from '@livekit/components-react';
54
import type { AppConfig } from '@/app-config';
6-
import { SessionView } from '@/components/app/session-view';
7-
import { WelcomeView } from '@/components/app/welcome-view';
5+
import { SessionProvider } from '@/components/app/session-provider';
6+
import { ViewController } from '@/components/app/view-controller';
87
import { Toaster } from '@/components/livekit/toaster';
9-
import { useRoom } from '@/hooks/useRoom';
10-
11-
const MotionWelcomeView = motion.create(WelcomeView);
12-
const MotionSessionView = motion.create(SessionView);
13-
14-
const VIEW_MOTION_PROPS = {
15-
variants: {
16-
visible: {
17-
opacity: 1,
18-
},
19-
hidden: {
20-
opacity: 0,
21-
},
22-
},
23-
initial: 'hidden',
24-
animate: 'visible',
25-
exit: 'hidden',
26-
transition: {
27-
duration: 0.5,
28-
ease: 'linear',
29-
},
30-
};
318

329
interface AppProps {
3310
appConfig: AppConfig;
3411
}
3512

3613
export function App({ appConfig }: AppProps) {
37-
const { room, isSessionActive, startSession } = useRoom(appConfig);
38-
const { startButtonText } = appConfig;
39-
4014
return (
41-
<RoomContext.Provider value={room}>
15+
<SessionProvider appConfig={appConfig}>
4216
<main className="grid h-svh grid-cols-1 place-content-center">
43-
<AnimatePresence mode="wait">
44-
{/* Welcome screen */}
45-
{!isSessionActive && (
46-
<MotionWelcomeView
47-
key="welcome"
48-
{...VIEW_MOTION_PROPS}
49-
startButtonText={startButtonText}
50-
onStartCall={startSession}
51-
/>
52-
)}
53-
54-
{/* Session view */}
55-
{isSessionActive && (
56-
<MotionSessionView key="session-view" {...VIEW_MOTION_PROPS} appConfig={appConfig} />
57-
)}
58-
</AnimatePresence>
17+
<ViewController />
5918
</main>
60-
6119
<StartAudio label="Start Audio" />
6220
<RoomAudioRenderer />
6321
<Toaster />
64-
</RoomContext.Provider>
22+
</SessionProvider>
6523
);
6624
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client';
2+
3+
import { createContext, useContext, useMemo } from 'react';
4+
import { RoomContext } from '@livekit/components-react';
5+
import { APP_CONFIG_DEFAULTS, type AppConfig } from '@/app-config';
6+
import { useRoom } from '@/hooks/useRoom';
7+
8+
const SessionContext = createContext<{
9+
appConfig: AppConfig;
10+
isSessionActive: boolean;
11+
startSession: () => void;
12+
endSession: () => void;
13+
}>({
14+
appConfig: APP_CONFIG_DEFAULTS,
15+
isSessionActive: false,
16+
startSession: () => {},
17+
endSession: () => {},
18+
});
19+
20+
interface SessionProviderProps {
21+
appConfig: AppConfig;
22+
children: React.ReactNode;
23+
}
24+
25+
export const SessionProvider = ({ appConfig, children }: SessionProviderProps) => {
26+
const { room, isSessionActive, startSession, endSession } = useRoom(appConfig);
27+
const contextValue = useMemo(
28+
() => ({ appConfig, isSessionActive, startSession, endSession }),
29+
[appConfig, isSessionActive, startSession, endSession]
30+
);
31+
32+
return (
33+
<RoomContext.Provider value={room}>
34+
<SessionContext.Provider value={contextValue}>{children}</SessionContext.Provider>
35+
</RoomContext.Provider>
36+
);
37+
};
38+
39+
export function useSession() {
40+
return useContext(SessionContext);
41+
}

components/app/view-controller.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use client';
2+
3+
import { useRef } from 'react';
4+
import { AnimatePresence, motion } from 'motion/react';
5+
import { useRoomContext } from '@livekit/components-react';
6+
import { useSession } from '@/components/app/session-provider';
7+
import { SessionView } from '@/components/app/session-view';
8+
import { WelcomeView } from '@/components/app/welcome-view';
9+
10+
const MotionWelcomeView = motion.create(WelcomeView);
11+
const MotionSessionView = motion.create(SessionView);
12+
13+
const VIEW_MOTION_PROPS = {
14+
variants: {
15+
visible: {
16+
opacity: 1,
17+
},
18+
hidden: {
19+
opacity: 0,
20+
},
21+
},
22+
initial: 'hidden',
23+
animate: 'visible',
24+
exit: 'hidden',
25+
transition: {
26+
duration: 0.5,
27+
ease: 'linear',
28+
},
29+
};
30+
31+
export function ViewController() {
32+
const room = useRoomContext();
33+
const isSessionActiveRef = useRef(false);
34+
const { appConfig, isSessionActive, startSession } = useSession();
35+
36+
// animation handler holds a reference to stale isSessionActive value
37+
isSessionActiveRef.current = isSessionActive;
38+
39+
// disconnect room after animation completes
40+
const handleAnimationComplete = () => {
41+
if (!isSessionActiveRef.current && room.state !== 'disconnected') {
42+
room.disconnect();
43+
}
44+
};
45+
46+
return (
47+
<AnimatePresence mode="wait">
48+
{/* Welcome screen */}
49+
{!isSessionActive && (
50+
<MotionWelcomeView
51+
key="welcome"
52+
{...VIEW_MOTION_PROPS}
53+
startButtonText={appConfig.startButtonText}
54+
onStartCall={startSession}
55+
/>
56+
)}
57+
{/* Session view */}
58+
{isSessionActive && (
59+
<MotionSessionView
60+
key="session-view"
61+
{...VIEW_MOTION_PROPS}
62+
appConfig={appConfig}
63+
onAnimationComplete={handleAnimationComplete}
64+
/>
65+
)}
66+
</AnimatePresence>
67+
);
68+
}

components/livekit/agent-control-bar/agent-control-bar.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import { type HTMLAttributes, useCallback, useState } from 'react';
44
import { Track } from 'livekit-client';
5-
import { useChat, useRemoteParticipants, useRoomContext } from '@livekit/components-react';
5+
import { useChat, useRemoteParticipants } from '@livekit/components-react';
66
import { ChatTextIcon, PhoneDisconnectIcon } from '@phosphor-icons/react/dist/ssr';
7+
import { useSession } from '@/components/app/session-provider';
78
import { TrackToggle } from '@/components/livekit/agent-control-bar/track-toggle';
89
import { Button } from '@/components/livekit/button';
910
import { Toggle } from '@/components/livekit/toggle';
@@ -41,11 +42,10 @@ export function AgentControlBar({
4142
...props
4243
}: AgentControlBarProps & HTMLAttributes<HTMLDivElement>) {
4344
const { send } = useChat();
44-
const room = useRoomContext();
4545
const participants = useRemoteParticipants();
4646
const [chatOpen, setChatOpen] = useState(false);
4747
const publishPermissions = usePublishPermissions();
48-
const [isDisconnecting, setIsDisconnecting] = useState(false);
48+
const { isSessionActive, endSession } = useSession();
4949
const [isSendingMessage, setIsSendingMessage] = useState(false);
5050

5151
const {
@@ -78,15 +78,9 @@ export function AgentControlBar({
7878
);
7979

8080
const handleDisconnect = useCallback(async () => {
81-
setIsDisconnecting(true);
82-
83-
if (room) {
84-
await room.disconnect();
85-
}
86-
87-
setIsDisconnecting(false);
81+
endSession();
8882
onDisconnect?.();
89-
}, [room, onDisconnect]);
83+
}, [endSession, onDisconnect]);
9084

9185
const visibleControls = {
9286
leave: controls?.leave ?? true,
@@ -176,7 +170,7 @@ export function AgentControlBar({
176170
<Button
177171
variant="destructive"
178172
onClick={handleDisconnect}
179-
disabled={isDisconnecting}
173+
disabled={!isSessionActive}
180174
className="font-mono"
181175
>
182176
<PhoneDisconnectIcon weight="bold" />

hooks/useRoom.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { useEffect, useMemo, useState } from 'react';
1+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import { Room, RoomEvent, TokenSource } from 'livekit-client';
33
import { AppConfig } from '@/app-config';
44
import { toastAlert } from '@/components/livekit/alert-toast';
55

66
export function useRoom(appConfig: AppConfig) {
7+
const aborted = useRef(false);
78
const room = useMemo(() => new Room(), []);
8-
const [isSessionActive, setIsSessionStarted] = useState(false);
9+
const [isSessionActive, setIsSessionActive] = useState(false);
910

1011
useEffect(() => {
1112
function onDisconnected() {
12-
setIsSessionStarted(false);
13+
setIsSessionActive(false);
1314
}
1415

1516
function onMediaDevicesError(error: Error) {
@@ -29,7 +30,14 @@ export function useRoom(appConfig: AppConfig) {
2930
}, [room]);
3031

3132
useEffect(() => {
32-
let aborted = false;
33+
return () => {
34+
aborted.current = true;
35+
room.disconnect();
36+
};
37+
}, [room]);
38+
39+
const startSession = useCallback(() => {
40+
setIsSessionActive(true);
3341

3442
const tokenSource = TokenSource.custom(async () => {
3543
const url = new URL(
@@ -59,7 +67,7 @@ export function useRoom(appConfig: AppConfig) {
5967
}
6068
});
6169

62-
if (isSessionActive && room.state === 'disconnected') {
70+
if (room.state === 'disconnected') {
6371
const { isPreConnectBufferEnabled } = appConfig;
6472
Promise.all([
6573
room.localParticipant.setMicrophoneEnabled(true, undefined, {
@@ -71,7 +79,7 @@ export function useRoom(appConfig: AppConfig) {
7179
room.connect(connectionDetails.serverUrl, connectionDetails.participantToken)
7280
),
7381
]).catch((error) => {
74-
if (aborted) {
82+
if (aborted.current) {
7583
// Once the effect has cleaned up after itself, drop any errors
7684
//
7785
// These errors are likely caused by this effect rerunning rapidly,
@@ -86,16 +94,11 @@ export function useRoom(appConfig: AppConfig) {
8694
});
8795
});
8896
}
97+
}, [room, appConfig]);
8998

90-
return () => {
91-
aborted = true;
92-
room.disconnect();
93-
};
94-
}, [room, isSessionActive, appConfig]);
95-
96-
const startSession = () => {
97-
setIsSessionStarted(true);
98-
};
99+
const endSession = useCallback(() => {
100+
setIsSessionActive(false);
101+
}, []);
99102

100-
return { room, isSessionActive, startSession };
103+
return { room, isSessionActive, startSession, endSession };
101104
}

0 commit comments

Comments
 (0)