Skip to content

Commit e7807d0

Browse files
gabriellshtassoevancoderabbitai[bot]
authored
refactor: Media Call client data structure and rendering (#38778)
Co-authored-by: Tasso Evangelista <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 92a3e47 commit e7807d0

File tree

79 files changed

+11912
-1139
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+11912
-1139
lines changed

apps/meteor/client/providers/MediaCallProvider.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { Emitter } from '@rocket.chat/emitter';
12
import { usePermission } from '@rocket.chat/ui-contexts';
2-
import { MediaCallProvider as MediaCallProviderBase, MediaCallContext } from '@rocket.chat/ui-voip';
3+
import { MediaCallProvider as MediaCallProviderBase, MediaCallInstanceContext } from '@rocket.chat/ui-voip';
34
import type { ReactNode } from 'react';
45
import { useMemo } from 'react';
56

@@ -13,17 +14,18 @@ const MediaCallProvider = ({ children }: { children: ReactNode }) => {
1314

1415
const unauthorizedContextValue = useMemo(
1516
() => ({
16-
state: 'unauthorized' as const,
17-
onToggleWidget: undefined,
18-
onEndCall: undefined,
19-
peerInfo: undefined,
20-
setOpenRoomId: undefined,
17+
instance: undefined,
18+
signalEmitter: new Emitter<any>(),
19+
audioElement: undefined,
20+
openRoomId: undefined,
21+
setOpenRoomId: () => undefined,
22+
getAutocompleteOptions: () => Promise.resolve([]),
2123
}),
2224
[],
2325
);
2426

2527
if (!hasModule || (!canMakeInternalCall && !canMakeExternalCall)) {
26-
return <MediaCallContext.Provider value={unauthorizedContextValue}>{children}</MediaCallContext.Provider>;
28+
return <MediaCallInstanceContext.Provider value={unauthorizedContextValue}>{children}</MediaCallInstanceContext.Provider>;
2729
}
2830

2931
return <MediaCallProviderBase>{children}</MediaCallProviderBase>;

apps/meteor/client/views/mediaCallHistory/CallHistoryRowExternalUser.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GenericMenu } from '@rocket.chat/ui-client';
22
import type { CallHistoryTableExternalContact, CallHistoryTableRowProps } from '@rocket.chat/ui-voip';
3-
import { CallHistoryTableRow, useMediaCallContext, isCallingBlocked } from '@rocket.chat/ui-voip';
3+
import { CallHistoryTableRow, usePeekMediaSessionState, useWidgetExternalControls } from '@rocket.chat/ui-voip';
44
import { useCallback, useMemo } from 'react';
55
import { useTranslation } from 'react-i18next';
66

@@ -11,27 +11,29 @@ type CallHistoryRowExternalUserProps = Omit<CallHistoryTableRowProps<CallHistory
1111
const CallHistoryRowExternalUser = ({ _id, contact, type, status, duration, timestamp, onClick }: CallHistoryRowExternalUserProps) => {
1212
const { t } = useTranslation();
1313

14-
const { onToggleWidget, state } = useMediaCallContext();
14+
const state = usePeekMediaSessionState();
15+
const { toggleWidget } = useWidgetExternalControls();
1516

1617
const handleClick = useCallback(() => {
1718
onClick(_id);
1819
}, [onClick, _id]);
1920

2021
const actions = useMemo(() => {
21-
if (state === 'unauthorized' || state === 'unlicensed' || !onToggleWidget) {
22+
if (state === 'unavailable') {
2223
return [];
2324
}
25+
const disabled = state !== 'available';
2426
return [
2527
{
2628
id: 'voiceCall',
2729
icon: 'phone',
2830
content: t('Voice_call'),
29-
disabled: isCallingBlocked(state),
30-
tooltip: isCallingBlocked(state) ? t('Call_in_progress') : undefined,
31-
onClick: () => onToggleWidget({ number: contact.number }),
31+
disabled,
32+
tooltip: disabled ? t('Call_in_progress') : undefined,
33+
onClick: () => toggleWidget({ number: contact.number }),
3234
} as const,
3335
];
34-
}, [contact, onToggleWidget, t, state]);
36+
}, [contact, toggleWidget, t, state]);
3537

3638
return (
3739
<CallHistoryTableRow

apps/meteor/client/views/mediaCallHistory/CallHistoryRowInternalUser.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Keys as IconName } from '@rocket.chat/icons';
22
import { GenericMenu } from '@rocket.chat/ui-client';
3-
import type { CallHistoryTableRowProps, CallHistoryTableInternalContact, MediaCallState } from '@rocket.chat/ui-voip';
4-
import { CallHistoryTableRow, useMediaCallContext, isCallingBlocked } from '@rocket.chat/ui-voip';
3+
import { CallHistoryTableRow, usePeekMediaSessionState } from '@rocket.chat/ui-voip';
4+
import type { CallHistoryTableRowProps, CallHistoryTableInternalContact, PeekMediaSessionStateReturn } from '@rocket.chat/ui-voip';
55
import type { TFunction } from 'i18next';
66
import { useCallback } from 'react';
77
import { useTranslation } from 'react-i18next';
@@ -37,11 +37,11 @@ const i18nDictionary: Record<HistoryActions, string> = {
3737
userInfo: 'User_info',
3838
} as const;
3939

40-
const getItems = (actions: HistoryActionCallbacks, t: TFunction, state: MediaCallState) => {
40+
const getItems = (actions: HistoryActionCallbacks, t: TFunction, state: PeekMediaSessionStateReturn) => {
4141
return (Object.entries(actions) as [HistoryActions, () => void][])
4242
.filter(([_, callback]) => callback)
4343
.map(([action, callback]) => {
44-
const disabled = action === 'voiceCall' && isCallingBlocked(state);
44+
const disabled = action === 'voiceCall' && state !== 'available';
4545
return {
4646
id: action,
4747
icon: iconDictionary[action],
@@ -66,7 +66,7 @@ const CallHistoryRowInternalUser = ({
6666
onClick,
6767
}: CallHistoryRowInternalUserProps) => {
6868
const { t } = useTranslation();
69-
const { state } = useMediaCallContext();
69+
const state = usePeekMediaSessionState();
7070
const actions = useMediaCallInternalHistoryActions({
7171
contact: {
7272
_id: contact._id,

apps/meteor/client/views/mediaCallHistory/MediaCallHistoryExternal.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { CallHistoryItem, IExternalMediaCallHistoryItem, IMediaCall, Serialized } from '@rocket.chat/core-typings';
2-
import { CallHistoryContextualBar, useMediaCallContext } from '@rocket.chat/ui-voip';
2+
import { CallHistoryContextualBar, useWidgetExternalControls, usePeekMediaSessionState } from '@rocket.chat/ui-voip';
33
import { useMemo } from 'react';
44

55
type ExternalCallEndpointData = Serialized<{
@@ -33,16 +33,17 @@ const MediaCallHistoryExternal = ({ data, onClose }: MediaCallHistoryExternalPro
3333
state: data.item.state,
3434
};
3535
}, [data]);
36-
const { onToggleWidget } = useMediaCallContext();
36+
const state = usePeekMediaSessionState();
37+
const { toggleWidget } = useWidgetExternalControls();
3738

3839
const actions = useMemo(() => {
39-
if (!onToggleWidget) {
40+
if (state !== 'available') {
4041
return {};
4142
}
4243
return {
43-
voiceCall: () => onToggleWidget(contact),
44+
voiceCall: () => toggleWidget(contact),
4445
};
45-
}, [contact, onToggleWidget]);
46+
}, [contact, state, toggleWidget]);
4647

4748
return <CallHistoryContextualBar onClose={onClose} actions={actions} contact={contact} data={historyData} />;
4849
};

apps/meteor/client/views/mediaCallHistory/useMediaCallInternalHistoryActions.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
22
import { useGoToDirectMessage } from '@rocket.chat/ui-client';
33
import { useRouter, useUserAvatarPath } from '@rocket.chat/ui-contexts';
4-
import { useMediaCallContext } from '@rocket.chat/ui-voip';
4+
import { useWidgetExternalControls, usePeekMediaSessionState } from '@rocket.chat/ui-voip';
55
import { useMemo } from 'react';
66

77
export type InternalCallHistoryContact = {
@@ -28,17 +28,18 @@ export const useMediaCallInternalHistoryActions = ({
2828
messageRoomId,
2929
openUserInfo,
3030
}: UseMediaCallInternalHistoryActionsBaseOptions) => {
31-
const { onToggleWidget, state } = useMediaCallContext();
31+
const state = usePeekMediaSessionState();
32+
const { toggleWidget } = useWidgetExternalControls();
3233
const router = useRouter();
3334

3435
const getAvatarUrl = useUserAvatarPath();
3536

3637
const voiceCall = useEffectEvent(() => {
37-
if (state === 'unauthorized' || state === 'unlicensed' || !onToggleWidget) {
38+
if (state !== 'available') {
3839
return;
3940
}
4041

41-
onToggleWidget({
42+
toggleWidget({
4243
userId: contact._id,
4344
displayName: contact.displayName ?? '',
4445
username: contact.username,

apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { mockAppRoot } from '@rocket.chat/mock-providers';
2-
import { useMediaCallContext } from '@rocket.chat/ui-voip';
32
import { act, renderHook } from '@testing-library/react';
43

54
import { useUserMediaCallAction } from './useUserMediaCallAction';
65
import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../../../../tests/mocks/data';
76

7+
const usePeekMediaSessionStateMock = jest.fn().mockReturnValue('available');
8+
const toggleWidgetMock = jest.fn();
9+
810
jest.mock('@rocket.chat/ui-contexts', () => ({
911
...jest.requireActual('@rocket.chat/ui-contexts'),
1012
useUserAvatarPath: jest.fn().mockReturnValue((_args: any) => 'avatar-url'),
@@ -13,14 +15,10 @@ jest.mock('@rocket.chat/ui-contexts', () => ({
1315

1416
jest.mock('@rocket.chat/ui-voip', () => ({
1517
...jest.requireActual('@rocket.chat/ui-voip'),
16-
useMediaCallContext: jest.fn().mockImplementation(() => ({
17-
state: 'closed',
18-
onToggleWidget: jest.fn(),
19-
})),
18+
useWidgetExternalControls: jest.fn().mockReturnValue({ toggleWidget: (...args: any[]) => toggleWidgetMock(...args) }),
19+
usePeekMediaSessionState: () => usePeekMediaSessionStateMock(),
2020
}));
2121

22-
const useMediaCallContextMocked = jest.mocked(useMediaCallContext);
23-
2422
describe('useUserMediaCallAction', () => {
2523
const fakeUser = createFakeUser({ _id: 'own-uid' });
2624
const mockRid = 'room-id';
@@ -29,6 +27,10 @@ describe('useUserMediaCallAction', () => {
2927
jest.clearAllMocks();
3028
});
3129

30+
beforeEach(() => {
31+
usePeekMediaSessionStateMock.mockReturnValue('available');
32+
});
33+
3234
it('should return undefined if room is federated', () => {
3335
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
3436
wrapper: mockAppRoot()
@@ -41,13 +43,7 @@ describe('useUserMediaCallAction', () => {
4143
});
4244

4345
it('should return undefined if state is unauthorized', () => {
44-
useMediaCallContextMocked.mockReturnValueOnce({
45-
state: 'unauthorized',
46-
onToggleWidget: undefined,
47-
onEndCall: undefined,
48-
peerInfo: undefined,
49-
setOpenRoomId: undefined,
50-
});
46+
usePeekMediaSessionStateMock.mockReturnValueOnce('unavailable');
5147

5248
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), { wrapper: mockAppRoot().build() });
5349
expect(result.current).toBeUndefined();
@@ -109,34 +105,21 @@ describe('useUserMediaCallAction', () => {
109105
});
110106

111107
it('should call onClick handler correctly', () => {
112-
const mockOnToggleWidget = jest.fn();
113-
useMediaCallContextMocked.mockReturnValueOnce({
114-
state: 'closed',
115-
onToggleWidget: mockOnToggleWidget,
116-
peerInfo: undefined,
117-
onEndCall: () => undefined,
118-
setOpenRoomId: () => undefined,
119-
});
108+
usePeekMediaSessionStateMock.mockReturnValueOnce('available');
120109

121110
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid));
122111

123112
act(() => result.current?.onClick());
124113

125-
expect(mockOnToggleWidget).toHaveBeenCalledWith({
114+
expect(toggleWidgetMock).toHaveBeenCalledWith({
126115
userId: fakeUser._id,
127116
displayName: fakeUser.name,
128117
avatarUrl: 'avatar-url',
129118
});
130119
});
131120

132121
it('should be disabled if state is not closed, new, or unlicensed', () => {
133-
useMediaCallContextMocked.mockReturnValueOnce({
134-
state: 'calling',
135-
onToggleWidget: jest.fn(),
136-
peerInfo: undefined,
137-
onEndCall: () => undefined,
138-
setOpenRoomId: () => undefined,
139-
});
122+
usePeekMediaSessionStateMock.mockReturnValueOnce('calling');
140123

141124
const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid));
142125

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { isRoomFederated } from '@rocket.chat/core-typings';
22
import type { IRoom, IUser } from '@rocket.chat/core-typings';
33
import { useUserAvatarPath, useUserId, useUserSubscription, useUserCard, useUserRoom } from '@rocket.chat/ui-contexts';
4-
import { useMediaCallContext } from '@rocket.chat/ui-voip';
4+
import { usePeekMediaSessionState, useWidgetExternalControls } from '@rocket.chat/ui-voip';
55
import { useTranslation } from 'react-i18next';
66

77
import type { UserInfoAction } from '../useUserInfoActions';
@@ -10,7 +10,8 @@ export const useUserMediaCallAction = (user: Pick<IUser, '_id' | 'username' | 'n
1010
const { t } = useTranslation();
1111
const ownUserId = useUserId();
1212
const { closeUserCard } = useUserCard();
13-
const { state, onToggleWidget } = useMediaCallContext();
13+
const state = usePeekMediaSessionState();
14+
const { toggleWidget } = useWidgetExternalControls();
1415
const getAvatarUrl = useUserAvatarPath();
1516

1617
const currentSubscription = useUserSubscription(rid);
@@ -22,15 +23,15 @@ export const useUserMediaCallAction = (user: Pick<IUser, '_id' | 'username' | 'n
2223
return undefined;
2324
}
2425

25-
if (state === 'unauthorized') {
26+
if (state === 'unavailable') {
2627
return undefined;
2728
}
2829

2930
if (blocked) {
3031
return undefined;
3132
}
3233

33-
const disabled = !['closed', 'new', 'unlicensed'].includes(state);
34+
const disabled = state !== 'available';
3435

3536
if (user._id === ownUserId) {
3637
return undefined;
@@ -44,7 +45,7 @@ export const useUserMediaCallAction = (user: Pick<IUser, '_id' | 'username' | 'n
4445
icon: 'phone',
4546
onClick: () => {
4647
closeUserCard();
47-
onToggleWidget({
48+
toggleWidget({
4849
userId: user._id,
4950
displayName: user.name || user.username || '',
5051
avatarUrl,

packages/ui-voip/src/components/DevicePicker.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { forwardRef, useCallback, useState } from 'react';
88
import { useTranslation } from 'react-i18next';
99

1010
import { ActionButton } from '.';
11-
import { useMediaCallContext } from '../context';
11+
import { useMediaCallView } from '../context/MediaCallViewContext';
1212
import { useDevicePermissionPrompt2, stopTracks } from '../hooks/useDevicePermissionPrompt';
1313

1414
type DevicePickerButtonProps = {
@@ -39,7 +39,7 @@ const getDefaultDeviceItem = (label: string, type: 'input' | 'output') => ({
3939
const DevicePicker = ({ secondary = false }: { secondary?: boolean }) => {
4040
const { t } = useTranslation();
4141

42-
const { onDeviceChange } = useMediaCallContext();
42+
const { onDeviceChange } = useMediaCallView();
4343

4444
const availableDevices = useAvailableDevices();
4545
const selectedAudioDevices = useSelectedDevices();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { composeStories } from '@storybook/react';
2+
import { render } from '@testing-library/react';
3+
import { axe } from 'jest-axe';
4+
5+
import * as stories from './Keypad.stories';
6+
7+
const testCases = Object.values(composeStories(stories))
8+
.filter((Story) => !Story.tags?.includes('skip'))
9+
.map((Story) => [Story.storyName || 'Story', Story]);
10+
11+
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
12+
const view = render(<Story />);
13+
expect(view.baseElement).toMatchSnapshot();
14+
});
15+
16+
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
17+
const { container } = render(<Story />);
18+
19+
const results = await axe(container);
20+
expect(results).toHaveNoViolations();
21+
});

packages/ui-voip/src/components/Keypad/Keypad.stories.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Meta, StoryFn } from '@storybook/react';
22

33
import Keypad from './Keypad';
4-
import { useTonePlayer } from '../../context/useTonePlayer';
4+
import { useTonePlayer } from '../../hooks/useTonePlayer';
55

66
export default {
77
title: 'V2/Components/Keypad',
@@ -13,3 +13,5 @@ export const KeypadStoryWithTone: StoryFn<typeof Keypad> = () => {
1313
const playTone = useTonePlayer();
1414
return <Keypad onKeyPress={(key) => playTone(key as any)} />;
1515
};
16+
17+
KeypadStoryWithTone.tags = ['skip'];

0 commit comments

Comments
 (0)