Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions .changeset/dmv-62-phone-icon-no-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixed the voice-call phone icon being shown in direct messages with users that have no phone extension assigned. When internal calls are routed through SIP, the call action is now only offered if the other user has an extension; with SIP routing disabled the action keeps showing as before.
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import type { IUser, IRoom } from '@rocket.chat/core-typings';
import { mockAppRoot } from '@rocket.chat/mock-providers';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { useMediaCallAction } from '@rocket.chat/ui-voip';
import { act, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';

import { useMediaCallRoomAction } from './useMediaCallRoomAction';
import FakeRoomProvider from '../../../tests/mocks/client/FakeRoomProvider';
import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../tests/mocks/data';

jest.mock('@rocket.chat/ui-contexts', () => ({
...jest.requireActual('@rocket.chat/ui-contexts'),
useUserAvatarPath: jest.fn((_args: any) => 'avatar-url'),
useUserAvatarPath: jest.fn(() => (_args: any) => 'avatar-url'),
}));

jest.mock('@rocket.chat/ui-voip', () => ({
Expand All @@ -19,11 +19,14 @@ jest.mock('@rocket.chat/ui-voip', () => ({

const getUserInfoMocked = jest.fn().mockResolvedValue({ user: createFakeUser({ _id: 'peer-uid', username: 'peer-username' }) });

const appRoot = (overrides: { user?: IUser | null; room?: IRoom; subscription?: SubscriptionWithRoom } = {}) => {
const appRoot = (
overrides: { user?: IUser | null; room?: IRoom; subscription?: SubscriptionWithRoom; settings?: Record<string, unknown> } = {},
) => {
const {
user = createFakeUser({ _id: 'own-uid', username: 'own-username' }),
room = createFakeRoom({ uids: ['own-uid', 'peer-uid'] }),
subscription = createFakeSubscription(),
settings = {},
} = overrides;

const root = mockAppRoot()
Expand All @@ -35,6 +38,10 @@ const appRoot = (overrides: { user?: IUser | null; room?: IRoom; subscription?:
</FakeRoomProvider>
));

for (const [key, value] of Object.entries(settings)) {
root.withSetting(key as any, value as any);
}

if (user !== null) {
root.withUser(user);
}
Expand Down Expand Up @@ -143,4 +150,41 @@ describe('useMediaCallRoomAction', () => {
act(() => result.current?.action?.());
expect(actionMock).toHaveBeenCalled();
});

describe('SIP routing for internal calls (DMV-62)', () => {
const SIP_ROUTING_SETTING = 'VoIP_TeamCollab_SIP_Integration_For_Internal_Calls';

it('should return undefined when SIP routing is enabled and the peer has no extension', async () => {
getUserInfoMocked.mockResolvedValue({ user: createFakeUser({ _id: 'peer-uid', username: 'peer-username' }) });

const { result } = renderHook(() => useMediaCallRoomAction(), {
wrapper: appRoot({ settings: { [SIP_ROUTING_SETTING]: true } }),
});

// The peer never gains an extension, so the action must stay hidden.
await waitFor(() => expect(result.current).toBeUndefined());
});

it('should return the action when SIP routing is enabled and the peer has an extension', async () => {
getUserInfoMocked.mockResolvedValue({
user: createFakeUser({ _id: 'peer-uid', username: 'peer-username', freeSwitchExtension: '1001' }),
});

const { result } = renderHook(() => useMediaCallRoomAction(), {
wrapper: appRoot({ settings: { [SIP_ROUTING_SETTING]: true } }),
});

await waitFor(() => expect(result.current).toMatchObject({ id: 'start-voice-call', icon: 'phone' }));
});

it('should return the action when SIP routing is disabled even if the peer has no extension', async () => {
getUserInfoMocked.mockResolvedValue({ user: createFakeUser({ _id: 'peer-uid', username: 'peer-username' }) });

const { result } = renderHook(() => useMediaCallRoomAction(), {
wrapper: appRoot({ settings: { [SIP_ROUTING_SETTING]: false } }),
});

await waitFor(() => expect(result.current).toMatchObject({ id: 'start-voice-call', icon: 'phone' }));
});
});
});
15 changes: 13 additions & 2 deletions apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isRoomFederated } from '@rocket.chat/core-typings';
import { useUserAvatarPath, useUserId } from '@rocket.chat/ui-contexts';
import { useSetting, useUserAvatarPath, useUserId } from '@rocket.chat/ui-contexts';
import type { TranslationKey, RoomToolboxActionConfig } from '@rocket.chat/ui-contexts';
import type { PeerInfo } from '@rocket.chat/ui-voip';
import { useMediaCallAction } from '@rocket.chat/ui-voip';
Expand Down Expand Up @@ -36,6 +36,12 @@ export const useMediaCallRoomAction = () => {

const { data } = useUserInfoQuery({ userId: peerId as string }, { enabled: !!peerId });

// When internal calls are routed through SIP, the callee is only reachable if they
// have a phone extension assigned. Without SIP routing the call is peer-to-peer and
// no extension is required.
const routeInternalCallsViaSip = useSetting('VoIP_TeamCollab_SIP_Integration_For_Internal_Calls', false);
const peerHasExtension = Boolean(data?.user?.freeSwitchExtension);

const peerInfo = useMemo<PeerInfo | undefined>(() => {
if (!data?.user?._id) {
return undefined;
Expand All @@ -59,6 +65,11 @@ export const useMediaCallRoomAction = () => {
return undefined;
}

// DMV-62: don't offer the call when SIP routing is required but the peer has no extension.
Comment thread
ggazzo marked this conversation as resolved.
Outdated
if (routeInternalCallsViaSip && !peerHasExtension) {
return undefined;
}

const { action, title, icon } = callAction;

return {
Expand All @@ -69,5 +80,5 @@ export const useMediaCallRoomAction = () => {
action: () => action(),
groups: ['direct'] as const,
};
}, [peerId, callAction, blocked, federated]);
}, [peerId, callAction, blocked, federated, routeInternalCallsViaSip, peerHasExtension]);
};
Loading