Skip to content

Commit b81ae0b

Browse files
chore: Disabled voice actions in federated rooms (#37714)
1 parent 4aa3634 commit b81ae0b

File tree

4 files changed

+301
-4
lines changed

4 files changed

+301
-4
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type { IUser, IRoom } from '@rocket.chat/core-typings';
2+
import { mockAppRoot } from '@rocket.chat/mock-providers';
3+
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
4+
import { useMediaCallAction } from '@rocket.chat/ui-voip';
5+
import { act, renderHook } from '@testing-library/react';
6+
7+
import { useMediaCallRoomAction } from './useMediaCallRoomAction';
8+
import FakeRoomProvider from '../../../tests/mocks/client/FakeRoomProvider';
9+
import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../tests/mocks/data';
10+
11+
jest.mock('@rocket.chat/ui-contexts', () => ({
12+
...jest.requireActual('@rocket.chat/ui-contexts'),
13+
useUserAvatarPath: jest.fn((_args: any) => 'avatar-url'),
14+
}));
15+
16+
jest.mock('@rocket.chat/ui-voip', () => ({
17+
useMediaCallAction: jest.fn(),
18+
}));
19+
20+
const getUserInfoMocked = jest.fn().mockResolvedValue({ user: createFakeUser({ _id: 'peer-uid', username: 'peer-username' }) });
21+
22+
const appRoot = (overrides: { user?: IUser | null; room?: IRoom; subscription?: SubscriptionWithRoom } = {}) => {
23+
const {
24+
user = createFakeUser({ _id: 'own-uid', username: 'own-username' }),
25+
room = createFakeRoom({ uids: ['own-uid', 'peer-uid'] }),
26+
subscription = createFakeSubscription(),
27+
} = overrides;
28+
29+
const root = mockAppRoot()
30+
.withRoom(room)
31+
.withEndpoint('GET', '/v1/users.info', getUserInfoMocked)
32+
.wrap((children) => (
33+
<FakeRoomProvider roomOverrides={room} subscriptionOverrides={subscription}>
34+
{children}
35+
</FakeRoomProvider>
36+
));
37+
38+
if (user !== null) {
39+
root.withUser(user);
40+
}
41+
42+
return root.build();
43+
};
44+
45+
describe('useMediaCallRoomAction', () => {
46+
const useMediaCallActionMocked = jest.mocked(useMediaCallAction);
47+
48+
beforeEach(() => {
49+
jest.clearAllMocks();
50+
51+
useMediaCallActionMocked.mockReturnValue({
52+
action: jest.fn(),
53+
title: 'Start_call',
54+
icon: 'phone',
55+
});
56+
});
57+
58+
it('should return undefined if ownUserId is not defined', () => {
59+
const { result } = renderHook(() => useMediaCallRoomAction(), {
60+
wrapper: appRoot({ user: null }),
61+
});
62+
63+
expect(result.current).toBeUndefined();
64+
});
65+
66+
it('should return undefined if there are no other users in the room', () => {
67+
const fakeRoom = createFakeRoom({ uids: ['own-uid'] });
68+
const { result } = renderHook(() => useMediaCallRoomAction(), {
69+
wrapper: appRoot({ room: fakeRoom }),
70+
});
71+
72+
expect(result.current).toBeUndefined();
73+
});
74+
75+
it('should return undefined if there are more than one other user (Group DM)', () => {
76+
const fakeRoom = createFakeRoom({ uids: ['own-uid', 'peer-uid-1', 'peer-uid-2'] });
77+
const { result } = renderHook(() => useMediaCallRoomAction(), {
78+
wrapper: appRoot({ room: fakeRoom }),
79+
});
80+
81+
expect(result.current).toBeUndefined();
82+
});
83+
84+
it('should return undefined if callAction is undefined', () => {
85+
useMediaCallActionMocked.mockReturnValue(undefined);
86+
87+
const { result } = renderHook(() => useMediaCallRoomAction(), {
88+
wrapper: appRoot(),
89+
});
90+
91+
expect(result.current).toBeUndefined();
92+
});
93+
94+
it('should return undefined if subscription is blocked', () => {
95+
const fakeBlockedSubscription = createFakeSubscription({ blocker: false, blocked: true });
96+
const { result } = renderHook(() => useMediaCallRoomAction(), {
97+
wrapper: appRoot({ subscription: fakeBlockedSubscription }),
98+
});
99+
100+
expect(result.current).toBeUndefined();
101+
});
102+
103+
it('should return undefined if subscription is blocker', () => {
104+
const fakeBlockedSubscription = createFakeSubscription({ blocked: false, blocker: true });
105+
const { result } = renderHook(() => useMediaCallRoomAction(), {
106+
wrapper: appRoot({ subscription: fakeBlockedSubscription }),
107+
});
108+
109+
expect(result.current).toBeUndefined();
110+
});
111+
112+
it('should return undefined if room is federated', () => {
113+
const fakeFederatedRoom = createFakeRoom({ uids: ['own-uid', 'peer-uid'], federated: true });
114+
const { result } = renderHook(() => useMediaCallRoomAction(), {
115+
wrapper: appRoot({ room: fakeFederatedRoom }),
116+
});
117+
118+
expect(result.current).toBeUndefined();
119+
});
120+
121+
it('should return the action config if all conditions are met', () => {
122+
const actionMock = jest.fn();
123+
useMediaCallActionMocked.mockReturnValue({
124+
action: actionMock,
125+
title: 'Start_call',
126+
icon: 'phone',
127+
});
128+
129+
const { result } = renderHook(() => useMediaCallRoomAction(), {
130+
wrapper: appRoot(),
131+
});
132+
133+
expect(result.current).toEqual({
134+
id: 'start-voice-call',
135+
title: 'Start_call',
136+
icon: 'phone',
137+
featured: true,
138+
action: expect.any(Function),
139+
groups: ['direct'],
140+
});
141+
142+
// Test the action trigger
143+
act(() => result.current?.action?.());
144+
expect(actionMock).toHaveBeenCalled();
145+
});
146+
});

apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isRoomFederated } from '@rocket.chat/core-typings';
12
import { useUserAvatarPath, useUserId } from '@rocket.chat/ui-contexts';
23
import type { TranslationKey, RoomToolboxActionConfig } from '@rocket.chat/ui-contexts';
34
import type { PeerInfo } from '@rocket.chat/ui-voip';
@@ -23,9 +24,11 @@ const getPeerId = (uids: string[], ownUserId: string | undefined) => {
2324
};
2425

2526
export const useMediaCallRoomAction = () => {
26-
const { uids = [] } = useRoom();
27+
const room = useRoom();
28+
const { uids = [] } = room;
2729
const subscription = useRoomSubscription();
2830
const ownUserId = useUserId();
31+
const federated = isRoomFederated(room);
2932

3033
const getAvatarUrl = useUserAvatarPath();
3134

@@ -52,7 +55,7 @@ export const useMediaCallRoomAction = () => {
5255
const blocked = subscription?.blocked || subscription?.blocker;
5356

5457
return useMemo((): RoomToolboxActionConfig | undefined => {
55-
if (!peerId || !callAction || blocked) {
58+
if (!peerId || !callAction || blocked || federated) {
5659
return undefined;
5760
}
5861

@@ -66,5 +69,5 @@ export const useMediaCallRoomAction = () => {
6669
action: () => action(),
6770
groups: ['direct'] as const,
6871
};
69-
}, [peerId, callAction, blocked]);
72+
}, [peerId, callAction, blocked, federated]);
7073
};
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { mockAppRoot } from '@rocket.chat/mock-providers';
2+
import { useMediaCallContext } from '@rocket.chat/ui-voip';
3+
import { act, renderHook } from '@testing-library/react';
4+
5+
import { useUserMediaCallAction } from './useUserMediaCallAction';
6+
import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../../../../tests/mocks/data';
7+
8+
jest.mock('@rocket.chat/ui-contexts', () => ({
9+
...jest.requireActual('@rocket.chat/ui-contexts'),
10+
useUserAvatarPath: jest.fn().mockReturnValue((_args: any) => 'avatar-url'),
11+
useUserCard: jest.fn().mockReturnValue({ closeUserCard: jest.fn() }),
12+
}));
13+
14+
jest.mock('@rocket.chat/ui-voip', () => ({
15+
...jest.requireActual('@rocket.chat/ui-voip'),
16+
useMediaCallContext: jest.fn().mockImplementation(() => ({
17+
state: 'closed',
18+
onToggleWidget: jest.fn(),
19+
})),
20+
}));
21+
22+
const useMediaCallContextMocked = jest.mocked(useMediaCallContext);
23+
24+
describe('useUserMediaCallAction', () => {
25+
const fakeUser = createFakeUser({ _id: 'own-uid' });
26+
const mockRid = 'room-id';
27+
28+
afterEach(() => {
29+
jest.clearAllMocks();
30+
});
31+
32+
it('should return undefined if room is federated', () => {
33+
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
34+
wrapper: mockAppRoot()
35+
.withJohnDoe()
36+
.withRoom(createFakeRoom({ federated: true }))
37+
.build(),
38+
});
39+
40+
expect(result.current).toBeUndefined();
41+
});
42+
43+
it('should return undefined if state is unauthorized', () => {
44+
useMediaCallContextMocked.mockReturnValueOnce({
45+
state: 'unauthorized',
46+
onToggleWidget: undefined,
47+
onEndCall: undefined,
48+
peerInfo: undefined,
49+
});
50+
51+
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), { wrapper: mockAppRoot().build() });
52+
expect(result.current).toBeUndefined();
53+
});
54+
55+
it('should return undefined if subscription is blocked', () => {
56+
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
57+
wrapper: mockAppRoot()
58+
.withJohnDoe()
59+
.withRoom(createFakeRoom())
60+
.withSubscription(createFakeSubscription({ blocker: false, blocked: true }))
61+
.build(),
62+
});
63+
64+
expect(result.current).toBeUndefined();
65+
});
66+
67+
it('should return undefined if subscription is blocker', () => {
68+
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
69+
wrapper: mockAppRoot()
70+
.withJohnDoe()
71+
.withRoom(createFakeRoom())
72+
.withSubscription(createFakeSubscription({ blocker: true, blocked: false }))
73+
.build(),
74+
});
75+
76+
expect(result.current).toBeUndefined();
77+
});
78+
79+
it('should return undefined if user is own user', () => {
80+
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
81+
wrapper: mockAppRoot().withUser(fakeUser).withRoom(createFakeRoom()).withSubscription(createFakeSubscription()).build(),
82+
});
83+
84+
expect(result.current).toBeUndefined();
85+
});
86+
87+
it('should return action if conditions are met', () => {
88+
const fakeUser = createFakeUser();
89+
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
90+
wrapper: mockAppRoot()
91+
.withJohnDoe()
92+
.withRoom(createFakeRoom())
93+
.withSubscription(createFakeSubscription())
94+
.withTranslations('en', 'core', {
95+
Voice_call__user_: 'Voice call {{user}}',
96+
})
97+
.build(),
98+
});
99+
100+
expect(result.current).toEqual(
101+
expect.objectContaining({
102+
type: 'communication',
103+
icon: 'phone',
104+
title: `Voice call ${fakeUser.name}`,
105+
disabled: false,
106+
}),
107+
);
108+
});
109+
110+
it('should call onClick handler correctly', () => {
111+
const mockOnToggleWidget = jest.fn();
112+
useMediaCallContextMocked.mockReturnValueOnce({
113+
state: 'closed',
114+
onToggleWidget: mockOnToggleWidget,
115+
peerInfo: undefined,
116+
onEndCall: () => undefined,
117+
});
118+
119+
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid));
120+
121+
act(() => result.current?.onClick());
122+
123+
expect(mockOnToggleWidget).toHaveBeenCalledWith({
124+
userId: fakeUser._id,
125+
displayName: fakeUser.name,
126+
avatarUrl: 'avatar-url',
127+
});
128+
});
129+
130+
it('should be disabled if state is not closed, new, or unlicensed', () => {
131+
useMediaCallContextMocked.mockReturnValueOnce({
132+
state: 'calling',
133+
onToggleWidget: jest.fn(),
134+
peerInfo: undefined,
135+
onEndCall: () => undefined,
136+
});
137+
138+
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid));
139+
140+
expect(result.current?.disabled).toBe(true);
141+
});
142+
});

apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { isRoomFederated } from '@rocket.chat/core-typings';
12
import type { IRoom, IUser } from '@rocket.chat/core-typings';
2-
import { useUserAvatarPath, useUserId, useUserSubscription, useUserCard } from '@rocket.chat/ui-contexts';
3+
import { useUserAvatarPath, useUserId, useUserSubscription, useUserCard, useUserRoom } from '@rocket.chat/ui-contexts';
34
import { useMediaCallContext } from '@rocket.chat/ui-voip';
45
import { useTranslation } from 'react-i18next';
56

@@ -13,9 +14,14 @@ export const useUserMediaCallAction = (user: Pick<IUser, '_id' | 'username' | 'n
1314
const getAvatarUrl = useUserAvatarPath();
1415

1516
const currentSubscription = useUserSubscription(rid);
17+
const room = useUserRoom(rid);
1618

1719
const blocked = currentSubscription?.blocked || currentSubscription?.blocker;
1820

21+
if (room && isRoomFederated(room)) {
22+
return undefined;
23+
}
24+
1925
if (state === 'unauthorized') {
2026
return undefined;
2127
}

0 commit comments

Comments
 (0)