From 32e0f4cb5ca78e28a9cf2645328666d29662ae62 Mon Sep 17 00:00:00 2001 From: Ido Shamun <1993245+idoshamun@users.noreply.github.com> Date: Tue, 12 May 2026 16:53:01 +0300 Subject: [PATCH] fix: disconnect standup before navigating home --- .../components/liveRooms/LiveRoom.spec.tsx | 23 ++++++++++++++++++ .../src/components/liveRooms/LiveRoom.tsx | 12 ++++++---- .../liveRooms/LiveRoomControls.spec.tsx | 1 + .../shared/src/contexts/LiveRoomContext.tsx | 24 +++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/liveRooms/LiveRoom.spec.tsx b/packages/shared/src/components/liveRooms/LiveRoom.spec.tsx index 5626ad5767..c65a9bb4a0 100644 --- a/packages/shared/src/components/liveRooms/LiveRoom.spec.tsx +++ b/packages/shared/src/components/liveRooms/LiveRoom.spec.tsx @@ -199,6 +199,7 @@ const createContextValue = ( }, role: 'host', participantId: 'host', + disconnect: jest.fn().mockResolvedValue(undefined), preflightMediaPermissions: jest.fn(), startRoom: jest.fn(), endRoom: jest.fn(), @@ -415,6 +416,28 @@ describe('LiveRoom', () => { expect(screen.getByText('tile-speaker2')).toBeInTheDocument(); }); + it('disconnects before navigating home from the ended standup state', async () => { + const disconnect = jest.fn().mockResolvedValue(undefined); + mockUseLiveRoomConnection.mockReturnValue( + createContextValue({ + disconnect, + roomState: { + ...createContextValue().roomState!, + status: 'ended', + }, + }), + ); + + renderLiveRoom(); + + fireEvent.click(screen.getByRole('button', { name: 'Back home' })); + + await waitFor(() => { + expect(disconnect).toHaveBeenCalledTimes(1); + expect(mockPush).toHaveBeenCalledWith('/'); + }); + }); + it('passes raised hand queue positions to matching stage tiles', () => { mockUseLiveRoomConnection.mockReturnValue( createContextValue({ diff --git a/packages/shared/src/components/liveRooms/LiveRoom.tsx b/packages/shared/src/components/liveRooms/LiveRoom.tsx index e2c0c30b48..824e6645c4 100644 --- a/packages/shared/src/components/liveRooms/LiveRoom.tsx +++ b/packages/shared/src/components/liveRooms/LiveRoom.tsx @@ -37,7 +37,6 @@ import useLogEventOnce from '../../hooks/log/useLogEventOnce'; import { useToastNotification } from '../../hooks/useToastNotification'; import { useExitConfirmation } from '../../hooks/useExitConfirmation'; import { useViewSize, ViewSize } from '../../hooks'; -import { clearStoredLiveRoomResumeSession } from '../../lib/liveRoom/resumeSessionStorage'; import { useLiveRoomSubscription } from '../../hooks/liveRooms/useLiveRoomSubscription'; import { usePushNotificationContext } from '../../contexts/PushNotificationContext'; import { usePushNotificationMutation } from '../../hooks/notifications/usePushNotificationMutation'; @@ -73,6 +72,7 @@ const LiveRoomInner = ({ roomId }: LiveRoomProps): ReactElement => { roomState, role, participantId, + disconnect, sendChatMessage, deleteChatMessage, sendChatMessageReaction, @@ -135,10 +135,14 @@ const LiveRoomInner = ({ roomId }: LiveRoomProps): ReactElement => { videoSettings, }); - const handleLeave = (): void => { + const navigateHome = useCallback(async (): Promise => { onAskConfirmation(false); - clearStoredLiveRoomResumeSession(roomId); - router.push('/'); + await disconnect(); + await router.push('/'); + }, [disconnect, onAskConfirmation, router]); + + const handleLeave = (): void => { + navigateHome().catch(() => undefined); }; const handleNavigateBack = (surface: string): void => { logStandupAction(LogEvent.LeaveStandup, roomId, { surface }); diff --git a/packages/shared/src/components/liveRooms/LiveRoomControls.spec.tsx b/packages/shared/src/components/liveRooms/LiveRoomControls.spec.tsx index 93d97c5d0a..3d876bdf95 100644 --- a/packages/shared/src/components/liveRooms/LiveRoomControls.spec.tsx +++ b/packages/shared/src/components/liveRooms/LiveRoomControls.spec.tsx @@ -166,6 +166,7 @@ const createContextValue = ( }, role: 'audience', participantId: 'audience', + disconnect: jest.fn().mockResolvedValue(undefined), preflightMediaPermissions: jest.fn(), startRoom: jest.fn(), endRoom: jest.fn(), diff --git a/packages/shared/src/contexts/LiveRoomContext.tsx b/packages/shared/src/contexts/LiveRoomContext.tsx index 9c57bd6fbd..58e4a5bdff 100644 --- a/packages/shared/src/contexts/LiveRoomContext.tsx +++ b/packages/shared/src/contexts/LiveRoomContext.tsx @@ -138,6 +138,7 @@ export interface LiveRoomContextValue { role: LiveRoomParticipantRoleValue | null; participantId: string | null; + disconnect: () => Promise; preflightMediaPermissions: () => Promise; startRoom: () => Promise; endRoom: () => Promise; @@ -450,6 +451,7 @@ export const LiveRoomProvider = ({ recv: false, }); const mediaRebuildQueuedRef = useRef(false); + const intentionalDisconnectRef = useRef(false); const currentRole = (participantId && roomState?.participants[participantId]?.role) || role; const privilegeState = getLiveRoomPrivilegeState( @@ -691,6 +693,19 @@ export const LiveRoomProvider = ({ setIsMicOn(false); }, []); + const disconnect = useCallback(async () => { + intentionalDisconnectRef.current = true; + clearStoredLiveRoomResumeSession(roomId); + setStoredResumeSession(null); + setResumeSessionTtlMs(null); + setErrorMessage(null); + setStatus('idle'); + closeMediaSession(true); + stopLocalCapture(); + connectionRef.current?.close(); + connectionRef.current = null; + }, [closeMediaSession, roomId, stopLocalCapture]); + const queueMediaRebuild = useCallback(() => { if (mediaRebuildQueuedRef.current || status !== 'connected') { return; @@ -1064,6 +1079,7 @@ export const LiveRoomProvider = ({ : { token: joinToken?.token ?? '' }), }); connectionRef.current = connection; + intentionalDisconnectRef.current = false; setStatus('connecting'); setErrorMessage(null); const openingWithResume = !!storedResumeSession; @@ -1114,6 +1130,9 @@ export const LiveRoomProvider = ({ }, ); const offClose = connection.onClose(({ code, reason }) => { + if (intentionalDisconnectRef.current) { + return; + } if (openingWithResume && !sessionReady) { requestFreshJoinToken(); return; @@ -1126,6 +1145,9 @@ export const LiveRoomProvider = ({ setErrorMessage(reason || 'Standup connection closed'); }); const offError = connection.onError((error) => { + if (intentionalDisconnectRef.current) { + return; + } if (openingWithResume && !sessionReady) { return; } @@ -2163,6 +2185,7 @@ export const LiveRoomProvider = ({ roomState, role: currentRole, participantId, + disconnect, preflightMediaPermissions, startRoom, endRoom, @@ -2212,6 +2235,7 @@ export const LiveRoomProvider = ({ roomState, currentRole, participantId, + disconnect, preflightMediaPermissions, startRoom, endRoom,