Skip to content

Commit 80663c7

Browse files
authored
feat(voip): display video conf provider as subtitle (#7160)
1 parent d2e27d4 commit 80663c7

4 files changed

Lines changed: 1784 additions & 1 deletion

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React from 'react';
2+
import { View, StyleSheet } from 'react-native';
3+
4+
import CallSection from './CallSection';
5+
import type { ISubscription, TSubscriptionModel } from '../../../definitions';
6+
import { SubscriptionType } from '../../../definitions';
7+
import { clearEnterpriseModules, setEnterpriseModules } from '../../../actions/enterpriseModules';
8+
import { setPermissions } from '../../../actions/permissions';
9+
import { setUser } from '../../../actions/login';
10+
import { mockedStore } from '../../../reducers/mockedStore';
11+
import { addSettings } from '../../../actions/settings';
12+
13+
const styles = StyleSheet.create({
14+
container: {
15+
padding: 24,
16+
minHeight: 200
17+
}
18+
});
19+
20+
const Wrapper = ({ children }: { children: React.ReactNode }) => <View style={styles.container}>{children}</View>;
21+
22+
const createMockRoom = (overrides: Partial<ISubscription> = {}): TSubscriptionModel =>
23+
({
24+
_id: 'room1',
25+
rid: 'room1',
26+
id: 'room1',
27+
t: SubscriptionType.DIRECT,
28+
name: 'john.doe',
29+
fname: 'John Doe',
30+
uids: ['abc', 'user123'],
31+
ls: new Date(),
32+
ts: new Date(),
33+
lm: '',
34+
lr: '',
35+
unread: 0,
36+
userMentions: 0,
37+
groupMentions: 0,
38+
tunread: [],
39+
open: true,
40+
alert: false,
41+
f: false,
42+
archived: false,
43+
roomUpdatedAt: new Date(),
44+
ro: false,
45+
...overrides
46+
} as TSubscriptionModel);
47+
48+
const withVoiceAndVideoCallEnabled = (Story: React.ComponentType) => {
49+
mockedStore.dispatch(setEnterpriseModules(['teams-voip']));
50+
mockedStore.dispatch(addSettings({ VideoConf_Enable_DMs: true }));
51+
mockedStore.dispatch(
52+
setPermissions({
53+
'allow-internal-voice-calls': ['user'],
54+
'allow-external-voice-calls': ['user'],
55+
'call-management': ['user']
56+
})
57+
);
58+
mockedStore.dispatch(setUser({ roles: ['user'] }));
59+
return <Story />;
60+
};
61+
62+
const withVoiceCallDisabled = (Story: React.ComponentType) => {
63+
mockedStore.dispatch(clearEnterpriseModules());
64+
return <Story />;
65+
};
66+
67+
export default {
68+
title: 'RoomActionsView/CallSection',
69+
component: CallSection,
70+
decorators: [withVoiceAndVideoCallEnabled]
71+
};
72+
73+
export const Default = () => (
74+
<Wrapper>
75+
<CallSection room={createMockRoom()} disabled={false} />
76+
</Wrapper>
77+
);
78+
79+
export const Disabled = () => (
80+
<Wrapper>
81+
<CallSection room={createMockRoom()} disabled={true} />
82+
</Wrapper>
83+
);
84+
85+
export const NoVoiceCall = {
86+
render: () => (
87+
<Wrapper>
88+
<CallSection room={createMockRoom()} disabled={false} />
89+
</Wrapper>
90+
),
91+
decorators: [withVoiceCallDisabled]
92+
};
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import React from 'react';
2+
import { fireEvent, render, waitFor } from '@testing-library/react-native';
3+
import { Provider } from 'react-redux';
4+
5+
import CallSection from './CallSection';
6+
import { mockedStore } from '../../../reducers/mockedStore';
7+
import * as stories from './CallSection.stories';
8+
import { generateSnapshots } from '../../../../.rnstorybook/generateSnapshots';
9+
import type { ISubscription, TSubscriptionModel } from '../../../definitions';
10+
import { SubscriptionType } from '../../../definitions';
11+
import i18n from '../../../i18n';
12+
import * as restApi from '../../../lib/services/restApi';
13+
14+
const mockShowInitCallActionSheet = jest.fn();
15+
const mockOpenNewMediaCall = jest.fn();
16+
const noopOpenNewMediaCall = () => undefined;
17+
18+
const mockUseVideoConf = jest.fn();
19+
const mockUseNewMediaCall = jest.fn();
20+
21+
mockUseVideoConf.mockReturnValue({
22+
callEnabled: true,
23+
disabledTooltip: false,
24+
showInitCallActionSheet: mockShowInitCallActionSheet
25+
});
26+
mockUseNewMediaCall.mockReturnValue({
27+
openNewMediaCall: noopOpenNewMediaCall,
28+
hasMediaCallPermission: true
29+
});
30+
31+
jest.mock('../../../lib/hooks/useVideoConf', () => ({
32+
useVideoConf: (...args: unknown[]) => mockUseVideoConf(...args)
33+
}));
34+
35+
jest.mock('../../../lib/hooks/useNewMediaCall', () => ({
36+
useNewMediaCall: (...args: unknown[]) => mockUseNewMediaCall(...args)
37+
}));
38+
39+
jest.mock('../../../lib/services/restApi', () => ({
40+
...jest.requireActual('../../../lib/services/restApi'),
41+
videoConferenceGetCapabilities: jest.fn()
42+
}));
43+
44+
const mockVideoConferenceGetCapabilities = jest.mocked(restApi.videoConferenceGetCapabilities);
45+
46+
const Wrapper = ({ children }: { children: React.ReactNode }) => <Provider store={mockedStore}>{children}</Provider>;
47+
48+
const createMockRoom = (overrides: Partial<ISubscription> = {}): TSubscriptionModel =>
49+
({
50+
_id: 'room1',
51+
rid: 'room1',
52+
id: 'room1',
53+
t: SubscriptionType.DIRECT,
54+
name: 'john.doe',
55+
fname: 'John Doe',
56+
uids: ['abc', 'user123'],
57+
ls: new Date(),
58+
ts: new Date(),
59+
lm: '',
60+
lr: '',
61+
unread: 0,
62+
userMentions: 0,
63+
groupMentions: 0,
64+
tunread: [],
65+
open: true,
66+
alert: false,
67+
f: false,
68+
archived: false,
69+
roomUpdatedAt: new Date(),
70+
ro: false,
71+
...overrides
72+
} as TSubscriptionModel);
73+
74+
describe('CallSection', () => {
75+
beforeEach(() => {
76+
jest.clearAllMocks();
77+
mockUseVideoConf.mockReturnValue({
78+
callEnabled: true,
79+
disabledTooltip: false,
80+
showInitCallActionSheet: mockShowInitCallActionSheet
81+
});
82+
mockUseNewMediaCall.mockReturnValue({
83+
openNewMediaCall: noopOpenNewMediaCall,
84+
hasMediaCallPermission: true
85+
});
86+
mockVideoConferenceGetCapabilities.mockRejectedValue(new Error('test capabilities'));
87+
});
88+
89+
it('should return null when voice and video are both unavailable', () => {
90+
mockUseVideoConf.mockReturnValue({
91+
callEnabled: false,
92+
disabledTooltip: false,
93+
showInitCallActionSheet: mockShowInitCallActionSheet
94+
});
95+
mockUseNewMediaCall.mockReturnValue({
96+
openNewMediaCall: noopOpenNewMediaCall,
97+
hasMediaCallPermission: false
98+
});
99+
const { toJSON, queryByTestId } = render(
100+
<Wrapper>
101+
<CallSection room={createMockRoom()} disabled={false} />
102+
</Wrapper>
103+
);
104+
expect(toJSON()).toBeNull();
105+
expect(queryByTestId('room-actions-voice-call')).toBeNull();
106+
expect(queryByTestId('room-actions-call')).toBeNull();
107+
});
108+
109+
it('should render voice call only when video is disabled', () => {
110+
mockUseVideoConf.mockReturnValue({
111+
callEnabled: false,
112+
disabledTooltip: false,
113+
showInitCallActionSheet: mockShowInitCallActionSheet
114+
});
115+
mockUseNewMediaCall.mockReturnValue({
116+
openNewMediaCall: mockOpenNewMediaCall,
117+
hasMediaCallPermission: true
118+
});
119+
const { getByTestId, queryByTestId } = render(
120+
<Wrapper>
121+
<CallSection room={createMockRoom()} disabled={false} />
122+
</Wrapper>
123+
);
124+
expect(getByTestId('room-actions-voice-call')).toBeTruthy();
125+
expect(queryByTestId('room-actions-call')).toBeNull();
126+
});
127+
128+
it('should render video call only when voice permission is off', () => {
129+
mockUseNewMediaCall.mockReturnValue({
130+
openNewMediaCall: noopOpenNewMediaCall,
131+
hasMediaCallPermission: false
132+
});
133+
const { getByTestId, queryByTestId } = render(
134+
<Wrapper>
135+
<CallSection room={createMockRoom()} disabled={false} />
136+
</Wrapper>
137+
);
138+
expect(queryByTestId('room-actions-voice-call')).toBeNull();
139+
expect(getByTestId('room-actions-call')).toBeTruthy();
140+
});
141+
142+
it('should render both voice and video rows when both are enabled', () => {
143+
const { getByTestId } = render(
144+
<Wrapper>
145+
<CallSection room={createMockRoom()} disabled={false} />
146+
</Wrapper>
147+
);
148+
expect(getByTestId('room-actions-voice-call')).toBeTruthy();
149+
expect(getByTestId('room-actions-call')).toBeTruthy();
150+
});
151+
152+
it('should call openNewMediaCall when voice row is pressed', () => {
153+
mockUseNewMediaCall.mockReturnValue({
154+
openNewMediaCall: mockOpenNewMediaCall,
155+
hasMediaCallPermission: true
156+
});
157+
const { getByTestId } = render(
158+
<Wrapper>
159+
<CallSection room={createMockRoom()} disabled={false} />
160+
</Wrapper>
161+
);
162+
fireEvent.press(getByTestId('room-actions-voice-call'));
163+
expect(mockOpenNewMediaCall).toHaveBeenCalledTimes(1);
164+
});
165+
166+
it('should call showInitCallActionSheet when video row is pressed', () => {
167+
const { getByTestId } = render(
168+
<Wrapper>
169+
<CallSection room={createMockRoom()} disabled={false} />
170+
</Wrapper>
171+
);
172+
fireEvent.press(getByTestId('room-actions-call'));
173+
expect(mockShowInitCallActionSheet).toHaveBeenCalledTimes(1);
174+
});
175+
176+
it('should show video provider subtitle after capabilities load', async () => {
177+
mockVideoConferenceGetCapabilities.mockReset();
178+
mockVideoConferenceGetCapabilities.mockResolvedValue({ providerName: 'Rocket.Chat' } as never);
179+
const { getByText } = render(
180+
<Wrapper>
181+
<CallSection room={createMockRoom()} disabled={false} />
182+
</Wrapper>
183+
);
184+
await waitFor(() => {
185+
expect(getByText(i18n.t('Video_call'))).toBeTruthy();
186+
expect(getByText('(Rocket.Chat)')).toBeTruthy();
187+
});
188+
});
189+
190+
it('should not invoke handlers when disabled prop is true', () => {
191+
mockUseNewMediaCall.mockReturnValue({
192+
openNewMediaCall: mockOpenNewMediaCall,
193+
hasMediaCallPermission: true
194+
});
195+
const { getByTestId } = render(
196+
<Wrapper>
197+
<CallSection room={createMockRoom()} disabled={true} />
198+
</Wrapper>
199+
);
200+
fireEvent.press(getByTestId('room-actions-voice-call'));
201+
fireEvent.press(getByTestId('room-actions-call'));
202+
expect(mockOpenNewMediaCall).not.toHaveBeenCalled();
203+
expect(mockShowInitCallActionSheet).not.toHaveBeenCalled();
204+
});
205+
});
206+
207+
generateSnapshots(stories);

app/views/RoomActionsView/components/CallSection.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React from 'react';
1+
import React, { useEffect, useState } from 'react';
22

33
import * as List from '../../../containers/List';
44
import { useVideoConf } from '../../../lib/hooks/useVideoConf';
55
import type { TSubscriptionModel } from '../../../definitions';
66
import { useNewMediaCall } from '../../../lib/hooks/useNewMediaCall';
7+
import { videoConferenceGetCapabilities } from '../../../lib/services/restApi';
78

89
export default function CallSection({
910
room,
@@ -14,6 +15,15 @@ export default function CallSection({
1415
}): React.ReactElement | null {
1516
const { callEnabled, showInitCallActionSheet, disabledTooltip } = useVideoConf(room.rid);
1617
const { openNewMediaCall, hasMediaCallPermission } = useNewMediaCall(room.rid);
18+
const [providerName, setProviderName] = useState<string>();
19+
20+
useEffect(() => {
21+
if (callEnabled) {
22+
videoConferenceGetCapabilities()
23+
.then((res: any) => setProviderName(res.providerName))
24+
.catch(() => {});
25+
}
26+
}, [callEnabled]);
1727

1828
if (!hasMediaCallPermission && !callEnabled) {
1929
return null;
@@ -39,6 +49,8 @@ export default function CallSection({
3949
<>
4050
<List.Item
4151
title={'Video_call'}
52+
subtitle={providerName ? `(${providerName})` : undefined}
53+
translateSubtitle={false}
4254
onPress={showInitCallActionSheet}
4355
testID='room-actions-call'
4456
left={() => <List.Icon name='video' />}

0 commit comments

Comments
 (0)