Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions app/lib/services/restApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ServerMediaSignal } from '@rocket.chat/media-signaling';

import { mediaCallsStateSignals } from './restApi';

const mockSdkGet = jest.fn();
jest.mock('./sdk', () => ({
__esModule: true,
default: {
get: (...args: unknown[]) => mockSdkGet(...args)
}
}));

describe('mediaCallsStateSignals', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('calls sdk.get with media-calls.stateSignals and the contractId', async () => {
mockSdkGet.mockResolvedValueOnce({ signals: [], success: true });

const result = await mediaCallsStateSignals('device-contract-id-123');

expect(mockSdkGet).toHaveBeenCalledWith('media-calls.stateSignals', { contractId: 'device-contract-id-123' });
expect(result).toEqual({ signals: [], success: true });
});

it('returns signals and success from the API response', async () => {
const mockSignals = [
{ type: 'new', callId: 'call-1' } as unknown as ServerMediaSignal,
{ type: 'notification', notification: 'ringing' } as unknown as ServerMediaSignal
];
mockSdkGet.mockResolvedValueOnce({ signals: mockSignals, success: true });

const result = await mediaCallsStateSignals('device-id');

expect(result.signals).toHaveLength(2);
expect(result.success).toBe(true);
});

it('returns empty signals and success false when sdk.get throws', async () => {
mockSdkGet.mockRejectedValueOnce(new Error('Network error'));

const result = await mediaCallsStateSignals('device-id');

expect(result.signals).toEqual([]);
expect(result.success).toBe(false);
});

it('returns empty signals and success false when sdk.get returns an error response', async () => {
mockSdkGet.mockResolvedValueOnce({ signals: [], success: false });

const result = await mediaCallsStateSignals('device-id');

expect(result.signals).toEqual([]);
expect(result.success).toBe(false);
});
});
12 changes: 12 additions & 0 deletions app/lib/services/restApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getUniqueId } from 'react-native-device-info';
import type { ServerMediaSignal } from '@rocket.chat/media-signaling';

import {
type IAvatarSuggestion,
Expand Down Expand Up @@ -1215,3 +1216,14 @@ export const getUsersRoles = async (): Promise<boolean | IRoleUser[]> => {

export const getSupportedVersionsCloud = (uniqueId?: string, domain?: string) =>
fetch(`https://releases.rocket.chat/v2/server/supportedVersions?uniqueId=${uniqueId}&domain=${domain}&source=mobile`);

export const mediaCallsStateSignals = async (contractId: string): Promise<{ signals: ServerMediaSignal[]; success: boolean }> => {
try {
const result = await (
sdk.get as unknown as (path: string, params?: object) => Promise<{ signals: ServerMediaSignal[]; success: boolean }>
)('media-calls.stateSignals', { contractId });
return result;
} catch {
return { signals: [], success: false };
}
};
36 changes: 23 additions & 13 deletions app/lib/services/voip/MediaSessionInstance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ type MockMediaSignalingSession = {
processSignal: jest.Mock;
setIceGatheringTimeout: jest.Mock;
startCall: jest.Mock;
getMainCall: jest.Mock;
getCallData: jest.Mock;
};

const createdSessions: MockMediaSignalingSession[] = [];
Expand All @@ -124,7 +124,7 @@ jest.mock('@rocket.chat/media-signaling', () => ({
this.processSignal = jest.fn().mockResolvedValue(undefined);
this.setIceGatheringTimeout = jest.fn();
this.startCall = jest.fn().mockResolvedValue(undefined);
this.getMainCall = jest.fn();
this.getCallData = jest.fn();
Object.defineProperty(this, 'sessionId', { value: `session-${config.userId}`, writable: false });
createdSessions.push(this);
})
Expand Down Expand Up @@ -160,13 +160,23 @@ function buildClientMediaCall(options: {
role: 'caller' | 'callee';
hidden?: boolean;
reject?: jest.Mock;
contact?: { username?: string; sipExtension?: string };
}): IClientMediaCall {
const reject = options.reject ?? jest.fn();
const emitter = { on: jest.fn(), off: jest.fn(), emit: jest.fn() };
return {
callId: options.callId,
role: options.role,
hidden: options.hidden ?? false,
localParticipant: { local: true, role: options.role, muted: false, held: false, contact: {} },
remoteParticipants: [
{
local: false,
role: options.role === 'caller' ? 'callee' : 'caller',
muted: false,
held: false,
contact: options.contact ?? {}
}
],
reject,
emitter: emitter as unknown as IClientMediaCall['emitter']
} as unknown as IClientMediaCall;
Expand Down Expand Up @@ -505,9 +515,9 @@ describe('MediaSessionInstance', () => {
newCallHandler({
call: {
hidden: false,
role: 'caller',
localParticipant: { role: 'caller' },
remoteParticipants: [{ contact: { username: 'alice', sipExtension: '' } }],
callId: 'c1',
contact: { username: 'alice', sipExtension: '' },
emitter: { on: jest.fn(), off: jest.fn() }
} as unknown as IClientMediaCall
});
Expand Down Expand Up @@ -536,9 +546,9 @@ describe('MediaSessionInstance', () => {
newCallHandler({
call: {
hidden: false,
role: 'caller',
localParticipant: { role: 'caller' },
remoteParticipants: [{ contact: { username: 'alice', sipExtension: '' } }],
callId: 'c1',
contact: { username: 'alice', sipExtension: '' },
emitter: { on: jest.fn(), off: jest.fn() }
} as unknown as IClientMediaCall
});
Expand All @@ -557,9 +567,9 @@ describe('MediaSessionInstance', () => {
newCallHandler({
call: {
hidden: false,
role: 'caller',
localParticipant: { role: 'caller' },
remoteParticipants: [{ contact: { username: 'alice', sipExtension: '100' } }],
callId: 'c1',
contact: { username: 'alice', sipExtension: '100' },
emitter: { on: jest.fn(), off: jest.fn() }
} as unknown as IClientMediaCall
});
Expand All @@ -575,9 +585,9 @@ describe('MediaSessionInstance', () => {
const mainCall = {
callId: 'call-ans',
accept: jest.fn().mockResolvedValue(undefined),
contact: { username: 'bob', sipExtension: '' }
remoteParticipants: [{ contact: { username: 'bob', sipExtension: '' } }]
};
session.getMainCall.mockReturnValue(mainCall);
session.getCallData.mockReturnValue(mainCall);

await mediaSessionInstance.answerCall('call-ans');

Expand All @@ -591,9 +601,9 @@ describe('MediaSessionInstance', () => {
const mainCall = {
callId: 'call-sip',
accept: jest.fn().mockResolvedValue(undefined),
contact: { username: 'bob', sipExtension: 'ext' }
remoteParticipants: [{ contact: { username: 'bob', sipExtension: 'ext' } }]
};
session.getMainCall.mockReturnValue(mainCall);
session.getCallData.mockReturnValue(mainCall);

await mediaSessionInstance.answerCall('call-sip');

Expand Down
15 changes: 8 additions & 7 deletions app/lib/services/voip/MediaSessionInstance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
MediaCallWebRTCProcessor,
type CallContact,
type ClientMediaSignal,
type IClientMediaCall,
type CallActorType,
Expand Down Expand Up @@ -91,11 +92,11 @@ class MediaSessionInstance {
console.log('🤙 [VoIP] New call data:', call);
});

if (call.role === 'caller') {
if (call.localParticipant.role === 'caller') {
useCallStore.getState().setCall(call);
Navigation.navigate('CallView');
if (useCallStore.getState().roomId == null) {
this.resolveRoomIdFromContact(call.contact).catch(error => {
this.resolveRoomIdFromContact(call.remoteParticipants[0]?.contact).catch(error => {
console.error('[VoIP] Error resolving room id from contact (newCall):', error);
});
}
Expand All @@ -116,7 +117,7 @@ class MediaSessionInstance {
}

console.log('[VoIP] Answering call:', callId);
const mainCall = this.instance?.getMainCall();
const mainCall = this.instance?.getCallData(callId);
console.log('[VoIP] Main call:', mainCall);

if (mainCall && mainCall.callId === callId) {
Expand All @@ -126,7 +127,7 @@ class MediaSessionInstance {
RNCallKeep.setCurrentCallActive(callId);
useCallStore.getState().setCall(mainCall);
Navigation.navigate('CallView');
this.resolveRoomIdFromContact(mainCall.contact).catch(error => {
this.resolveRoomIdFromContact(mainCall.remoteParticipants[0]?.contact).catch(error => {
console.error('[VoIP] Error resolving room id from contact (answerCall):', error);
});
} else {
Expand Down Expand Up @@ -154,7 +155,7 @@ class MediaSessionInstance {
};

public endCall = (callId: string) => {
const mainCall = this.instance?.getMainCall();
const mainCall = this.instance?.getCallData(callId);

if (mainCall && mainCall.callId === callId) {
if (mainCall.state === 'ringing') {
Expand All @@ -170,8 +171,8 @@ class MediaSessionInstance {
useCallStore.getState().reset();
};

private async resolveRoomIdFromContact(contact: IClientMediaCall['contact']): Promise<void> {
if (contact.sipExtension) {
private async resolveRoomIdFromContact(contact: CallContact | undefined): Promise<void> {
if (!contact || contact.sipExtension) {
return;
}
const { username } = contact;
Expand Down
3 changes: 2 additions & 1 deletion app/lib/services/voip/MediaSessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ class MediaSessionStore extends Emitter<{ change: void }> {
randomStringFactory,
logger: new MediaCallLogger(),
features: ['audio'],
mobileDeviceId
mobileDeviceId,
autoSync: true
});

this.change();
Expand Down
19 changes: 12 additions & 7 deletions app/lib/services/voip/mockCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,21 @@ export function createMockCall(overrides: MockCallOverrides = {}): IClientMediaC
const contact = { ...DEFAULT_CONTACT, ...overrides.contact };
const callState: CallState = overrides.callState ?? 'active';

const mock = {
callId: 'mock-call-id',
state: callState,
const localParticipant = {
local: true,
role: 'caller',
muted: overrides.isMuted ?? false,
held: overrides.isOnHold ?? false,
remoteMute: false,
remoteHeld: false,
contact,
contact: {},
setMuted: () => {},
setHeld: () => {},
setHeld: () => {}
};
const remoteParticipants = [{ local: false, role: 'callee', muted: false, held: false, contact }];
const mock = {
callId: 'mock-call-id',
state: callState,
localParticipant,
remoteParticipants,
hangup: () => {},
reject: () => {},
sendDTMF: () => {},
Expand Down
28 changes: 20 additions & 8 deletions app/lib/services/voip/useCallStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,31 @@ function createMockCall(callId: string) {
const emit = (ev: string, ...args: unknown[]) => {
listeners[ev]?.forEach(fn => fn(...args));
};
const localParticipant = {
local: true,
role: 'callee',
muted: false,
held: false,
contact: {},
setMuted: jest.fn(),
setHeld: jest.fn()
};
const remoteParticipants = [
{
local: false,
role: 'caller',
muted: false,
held: false,
contact: { id: 'u', displayName: 'U', username: 'u', sipExtension: '' }
}
];
const call = {
callId,
state: 'active',
muted: false,
held: false,
remoteMute: false,
remoteHeld: false,
hidden: false,
role: 'callee',
contact: { id: 'u', displayName: 'U', username: 'u', sipExtension: '' },
localParticipant,
remoteParticipants,
emitter,
setMuted: jest.fn(),
setHeld: jest.fn(),
sendDTMF: jest.fn(),
hangup: jest.fn(),
accept: jest.fn(),
Expand Down
31 changes: 17 additions & 14 deletions app/lib/services/voip/useCallStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,19 +140,21 @@ export const useCallStore = create<CallStore>((set, get) => ({
cleanupCallListeners();
get().resetNativeCallId();
// Update state with call info
const remote = call.remoteParticipants[0];
const remoteContact = remote?.contact;
set({
call,
callId: call.callId,
callState: call.state,
isMuted: call.muted,
isOnHold: call.held,
remoteMute: call.remoteMute,
remoteHeld: call.remoteHeld,
isMuted: call.localParticipant.muted,
isOnHold: call.localParticipant.held,
remoteMute: remote?.muted ?? false,
remoteHeld: remote?.held ?? false,
contact: {
id: call.contact.id,
displayName: call.contact.displayName,
username: call.contact.username,
sipExtension: call.contact.sipExtension
id: remoteContact?.id,
displayName: remoteContact?.displayName,
username: remoteContact?.username,
sipExtension: remoteContact?.sipExtension
},
callStartTime: call.state === 'active' ? Date.now() : null
});
Expand Down Expand Up @@ -187,11 +189,12 @@ export const useCallStore = create<CallStore>((set, get) => ({
const currentCall = get().call;
if (!currentCall) return;

const currentRemote = currentCall.remoteParticipants[0];
set({
isMuted: currentCall.muted,
isOnHold: currentCall.held,
remoteMute: currentCall.remoteMute,
remoteHeld: currentCall.remoteHeld,
isMuted: currentCall.localParticipant.muted,
isOnHold: currentCall.localParticipant.held,
remoteMute: currentRemote?.muted ?? false,
remoteHeld: currentRemote?.held ?? false,
controlsVisible: true
});
};
Expand Down Expand Up @@ -225,15 +228,15 @@ export const useCallStore = create<CallStore>((set, get) => ({
const { call, isMuted } = get();
if (!call) return;

call.setMuted(!isMuted);
call.localParticipant.setMuted(!isMuted);
set({ isMuted: !isMuted });
},

toggleHold: () => {
const { call, isOnHold } = get();
if (!call) return;

call.setHeld(!isOnHold);
call.localParticipant.setHeld(!isOnHold);
set({ isOnHold: !isOnHold });
},

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@react-navigation/elements": "^2.6.1",
"@react-navigation/native": "^7.1.16",
"@react-navigation/native-stack": "^7.3.23",
"@rocket.chat/media-signaling": "file:./packages/rocket.chat-media-signaling-0.1.3.tgz",
"@rocket.chat/media-signaling": "file:./packages/rocket.chat-media-signaling-0.2.0.tgz",
"@rocket.chat/message-parser": "0.31.32",
"@rocket.chat/mobile-crypto": "RocketChat/rocket.chat-mobile-crypto",
"@rocket.chat/sdk": "RocketChat/Rocket.Chat.js.SDK#mobile",
Expand Down
Binary file removed packages/rocket.chat-media-signaling-0.1.3.tgz
Binary file not shown.
Binary file added packages/rocket.chat-media-signaling-0.2.0.tgz
Binary file not shown.
Loading
Loading