From 5030525c3e8d74766a4591a4db962b300ee4ec57 Mon Sep 17 00:00:00 2001 From: Claudio Costa Date: Mon, 16 Jun 2025 15:08:06 -0600 Subject: [PATCH] Derive connection state for current call from RTC peer (#8878) Co-authored-by: Mattermost Build --- app/products/calls/actions/calls.test.ts | 6 +++- app/products/calls/actions/calls.ts | 5 ++- .../calls/connection/connection.test.ts | 3 +- app/products/calls/connection/connection.ts | 6 ++-- .../calls/connection/websocket_client.ts | 4 +++ app/products/calls/state/actions.test.ts | 34 +++++++++++++++++++ app/products/calls/state/actions.ts | 14 ++++++++ app/products/calls/types/calls.ts | 2 +- 8 files changed, 67 insertions(+), 7 deletions(-) diff --git a/app/products/calls/actions/calls.test.ts b/app/products/calls/actions/calls.test.ts index 8126e2057..eb25d9e08 100644 --- a/app/products/calls/actions/calls.test.ts +++ b/app/products/calls/actions/calls.test.ts @@ -28,6 +28,7 @@ import { userJoinedCall, getCurrentCall, } from '@calls/state'; +import * as StateActions from '@calls/state/actions'; import { type Call, type CallsState, @@ -86,7 +87,7 @@ jest.mock('@calls/connection/connection', () => ({ disconnect: jest.fn((err?: Error) => onClose(err)), mute: jest.fn(), unmute: jest.fn(), - waitForPeerConnection: jest.fn(() => Promise.resolve()), + waitForPeerConnection: jest.fn(() => Promise.resolve('session-id')), initializeVoiceTrack: jest.fn(), sendReaction: jest.fn(), })), @@ -258,6 +259,7 @@ describe('Actions.Calls', () => { return [useCallsState('server1'), useCurrentCall()]; }); addFakeCall('server1', 'channel-id'); + const setCurrentCallConnectedMock = jest.spyOn(StateActions, 'setCurrentCallConnected'); let response: { data?: string }; await act(async () => { @@ -266,6 +268,8 @@ describe('Actions.Calls', () => { messages: {}, })); + expect(setCurrentCallConnectedMock).toHaveBeenCalledWith('channel-id', 'session-id'); + // manually call newCurrentConnection because newConnection is mocked newCurrentCall('server1', 'channel-id', 'myUserId'); }); diff --git a/app/products/calls/actions/calls.ts b/app/products/calls/actions/calls.ts index 4f3dda4d6..11322f798 100644 --- a/app/products/calls/actions/calls.ts +++ b/app/products/calls/actions/calls.ts @@ -21,6 +21,7 @@ import { getCurrentCall, myselfLeftCall, newCurrentCall, + setCurrentCallConnected, setCallForChannel, setCalls, setChannelEnabled, @@ -249,7 +250,9 @@ export const joinCall = async ( } try { - await connection.waitForPeerConnection(); + const sessionId = await connection.waitForPeerConnection(); + + setCurrentCallConnected(channelId, sessionId); // Follow the thread. const database = DatabaseManager.serverDatabases[serverUrl]?.database; diff --git a/app/products/calls/connection/connection.test.ts b/app/products/calls/connection/connection.test.ts index 53e6721bf..c78a222f9 100644 --- a/app/products/calls/connection/connection.test.ts +++ b/app/products/calls/connection/connection.test.ts @@ -585,6 +585,7 @@ describe('newConnection', () => { } }, send: jest.fn(), + sessionID: 'sessionID', })); connection = await newConnection( @@ -600,7 +601,7 @@ describe('newConnection', () => { res = connection.waitForPeerConnection(); jest.runAllTimers(); - await expect(res).resolves.not.toThrow(); + await expect(res).resolves.toBe('sessionID'); jest.useRealTimers(); }); }); diff --git a/app/products/calls/connection/connection.ts b/app/products/calls/connection/connection.ts index 3fbbc1272..8c740e621 100644 --- a/app/products/calls/connection/connection.ts +++ b/app/products/calls/connection/connection.ts @@ -458,7 +458,7 @@ export async function newConnection( }); const waitForPeerConnection = () => { - const waitForReadyImpl = (callback: () => void, fail: (reason: string) => void, timeout: number) => { + const waitForReadyImpl = (callback: (sessionId: string) => void, fail: (reason: string) => void, timeout: number) => { if (timeout <= 0) { fail('timed out waiting for peer connection'); return; @@ -466,14 +466,14 @@ export async function newConnection( setTimeout(() => { if (peer?.connected) { rtcMonitor?.start(); - callback(); + callback(ws.sessionID); } else { waitForReadyImpl(callback, fail, timeout - 200); } }, 200); }; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { waitForReadyImpl(resolve, reject, peerConnectTimeout); }); }; diff --git a/app/products/calls/connection/websocket_client.ts b/app/products/calls/connection/websocket_client.ts index af203581d..24503c92a 100644 --- a/app/products/calls/connection/websocket_client.ts +++ b/app/products/calls/connection/websocket_client.ts @@ -180,4 +180,8 @@ export class WebSocketClient extends EventEmitter { } return this.ws.readyState; } + + get sessionID() { + return this.originalConnID; + } } diff --git a/app/products/calls/state/actions.test.ts b/app/products/calls/state/actions.test.ts index 59c10b04d..69df8500b 100644 --- a/app/products/calls/state/actions.test.ts +++ b/app/products/calls/state/actions.test.ts @@ -30,6 +30,7 @@ import { useCallsConfig, useCallsState, useChannelsWithCalls, + setCurrentCallConnected, useCurrentCall, useGlobalCallsState, useIncomingCalls, @@ -1628,6 +1629,39 @@ describe('useCallsState', () => { assert.deepEqual(result.current[1], {'channel-2': true}); }); + it('setCurrentCallConnected', () => { + const initialCurrentCallState: CurrentCall = { + ...DefaultCurrentCall, + serverUrl: 'server1', + myUserId: 'myUserId', + connected: false, + mySessionId: '', + ...call1, + }; + + // setup + const {result} = renderHook(() => useCurrentCall()); + act(() => { + setCurrentCall(initialCurrentCallState); + }); + assert.deepEqual(result.current, initialCurrentCallState); + + // test + act(() => setCurrentCallConnected('channel-1', 'session-test-id')); + assert.deepEqual(result.current, { + ...initialCurrentCallState, + connected: true, + mySessionId: 'session-test-id', + }); + + // test with wrong channel ID (should not change state) + act(() => { + setCurrentCall(initialCurrentCallState); + setCurrentCallConnected('wrong-channel', 'session-test-id'); + }); + assert.deepEqual(result.current, initialCurrentCallState); + }); + it('captions', () => { const initialCallsState = { ...DefaultCallsState, diff --git a/app/products/calls/state/actions.ts b/app/products/calls/state/actions.ts index 591efb373..717d641d5 100644 --- a/app/products/calls/state/actions.ts +++ b/app/products/calls/state/actions.ts @@ -451,6 +451,20 @@ export const newCurrentCall = (serverUrl: string, channelId: string, myUserId: s }); }; +export const setCurrentCallConnected = (channelId: string, sessionId: string) => { + const currentCall = getCurrentCall(); + if (!currentCall || currentCall.channelId !== channelId) { + return; + } + + const nextCurrentCall: CurrentCall = { + ...currentCall, + connected: true, + mySessionId: sessionId, + }; + setCurrentCall(nextCurrentCall); +}; + export const myselfLeftCall = () => { setCurrentCall(null); diff --git a/app/products/calls/types/calls.ts b/app/products/calls/types/calls.ts index c6e1e4488..d66e95f6b 100644 --- a/app/products/calls/types/calls.ts +++ b/app/products/calls/types/calls.ts @@ -143,7 +143,7 @@ export type CallsConnection = { disconnect: (err?: Error) => void; mute: () => void; unmute: () => void; - waitForPeerConnection: () => Promise; + waitForPeerConnection: () => Promise; raiseHand: () => void; unraiseHand: () => void; initializeVoiceTrack: () => void;