Skip to content

Commit 9d4bcf5

Browse files
gabriellshgaolin1
authored andcommitted
feat: Media Call History contextual bar (RocketChat#37739)
1 parent a0d9416 commit 9d4bcf5

18 files changed

+1168
-2
lines changed

packages/i18n/src/locales/en.i18n.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,8 @@
911911
"Calendar_settings": "Calendar settings",
912912
"Call": "Call",
913913
"Call_Already_Ended": "Call Already Ended",
914+
"Call_ID": "Call ID",
915+
"Call_info": "Call info",
914916
"Call_Information": "Call Information",
915917
"Call_again": "Call again",
916918
"Call_back": "Call back",
@@ -1803,6 +1805,7 @@
18031805
"Duplicate_file_name_found": "Duplicate file name found.",
18041806
"Duplicate_private_group_name": "A Private Group with name '%s' exists",
18051807
"Duplicated_Email_address_will_be_ignored": "Duplicated email address will be ignored.",
1808+
"Duration": "Duration",
18061809
"E2EE_Composer_Unencrypted_Message": "You're sending an unencrypted message",
18071810
"E2EE_password_reset": "E2EE password reset",
18081811
"E2EE_alert": "<b>Enabling E2EE affects other functionalities</b> <ul><li> - Encrypted content cannot be found by search</li> <li> - Encrypted content cannot be audited</li> <li> - Bot interactions may not work with encrypted messages</li></ul>",
@@ -2569,6 +2572,7 @@
25692572
"Incoming_call_from": "Incoming call from",
25702573
"Incoming_call_from__roomName__": "Incoming call from {{roomName}}",
25712574
"Incoming_call_transfer": "Incoming call transfer",
2575+
"Incoming_voice_call": "Incoming voice call",
25722576
"Incoming_voice_call_canceled_suddenly": "An Incoming Voice Call was canceled suddenly.",
25732577
"Incoming_voice_call_canceled_user_not_registered": "An Incoming Voice Call was canceled due to an unexpected error.",
25742578
"Industry": "Industry",
@@ -3969,6 +3973,7 @@
39693973
"Out_of_seats": "Out of Seats",
39703974
"Outdated": "Outdated",
39713975
"Outgoing": "Outgoing",
3976+
"Outgoing_voice_call": "Outgoing voice call",
39723977
"Outgoing_WebHook": "Outgoing WebHook",
39733978
"Outgoing_WebHook_Description": "Get data out of MedSense in real-time.",
39743979
"Outlook_Calendar": "Outlook Calendar",
@@ -5525,6 +5530,7 @@
55255530
"UserData_MessageLimitPerRequest": "Message Limit per Request",
55265531
"UserData_ProcessingFrequency": "Processing Frequency (Minutes)",
55275532
"User_Info": "User Info",
5533+
"User_info": "User info",
55285534
"User_Interface": "User Interface",
55295535
"User_Presence": "User Presence",
55305536
"User_Settings": "User Settings",

packages/ui-voip/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
"lint": "eslint --ext .js,.jsx,.ts,.tsx .",
1414
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix",
1515
"storybook": "storybook dev -p 6006",
16-
"test": "jest",
16+
"test": "yarn testunit",
1717
"test-storybook": "npx concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --ci\" \"npx wait-on tcp:127.0.0.1:6006 && yarn exec test-storybook\"",
18-
"testunit": "jest",
18+
"testunit": "TZ=UTC jest",
1919
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
2020
},
2121
"dependencies": {
@@ -35,6 +35,7 @@
3535
"@rocket.chat/fuselage": "^0.70.0",
3636
"@rocket.chat/fuselage-hooks": "~0.38.1",
3737
"@rocket.chat/fuselage-tokens": "~0.33.2",
38+
"@rocket.chat/fuselage-ui-kit": "workspace:^",
3839
"@rocket.chat/icons": "~0.46.0",
3940
"@rocket.chat/jest-presets": "workspace:~",
4041
"@rocket.chat/mock-providers": "workspace:~",
@@ -79,6 +80,7 @@
7980
"@rocket.chat/css-in-js": "*",
8081
"@rocket.chat/fuselage": "*",
8182
"@rocket.chat/fuselage-hooks": "*",
83+
"@rocket.chat/fuselage-ui-kit": "workspace:^",
8284
"@rocket.chat/icons": "*",
8385
"@rocket.chat/styled": "*",
8486
"@rocket.chat/ui-avatar": "workspace:^",

packages/ui-voip/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ export { MediaCallContext, useMediaCallExternalContext as useMediaCallContext, t
44

55
export { useMediaCallAction } from './hooks';
66

7+
export { CallHistoryContextualBar } from './views';
8+
export type { InternalCallHistoryContact, ExternalCallHistoryContact, CallHistoryData } from './views';
9+
710
export { getHistoryMessagePayload } from './ui-kit/getHistoryMessagePayload';
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { mockAppRoot } from '@rocket.chat/mock-providers';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
import type { ReactElement } from 'react';
4+
5+
import type { HistoryActionCallbacks } from './CallHistoryActions';
6+
import CallHistoryActions from './CallHistoryActions';
7+
8+
const noop = () => undefined;
9+
10+
const meta = {
11+
title: 'V2/Views/CallHistoryContextualbar/CallHistoryActions',
12+
component: CallHistoryActions,
13+
decorators: [
14+
mockAppRoot()
15+
.withTranslations('en', 'core', {
16+
Options: 'Options',
17+
Voice_call: 'Voice call',
18+
Video_call: 'Video call',
19+
Jump_to_message: 'Jump to message',
20+
Direct_Message: 'Direct Message',
21+
User_info: 'User info',
22+
})
23+
.withDefaultLanguage('en-US')
24+
.buildStoryDecorator(),
25+
(Story): ReactElement => <Story />,
26+
],
27+
} satisfies Meta<typeof CallHistoryActions>;
28+
29+
export default meta;
30+
31+
type Story = StoryObj<typeof meta>;
32+
33+
const actionList = ['voiceCall', 'videoCall', 'jumpToMessage', 'directMessage', 'userInfo'];
34+
35+
const getArgs = (index: number) => {
36+
return Object.fromEntries(actionList.slice(0, index).map((action) => [action, noop])) as HistoryActionCallbacks;
37+
};
38+
39+
export const Default: Story = {
40+
args: {
41+
onClose: noop,
42+
actions: getArgs(5),
43+
},
44+
};
45+
46+
export const WithLessActions: Story = {
47+
args: {
48+
onClose: noop,
49+
actions: getArgs(3),
50+
},
51+
};
52+
53+
export const WithSingleAction: Story = {
54+
args: {
55+
onClose: noop,
56+
actions: getArgs(1),
57+
},
58+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { Keys as IconName } from '@rocket.chat/icons';
2+
import { ContextualbarActions, ContextualbarClose, GenericMenu } from '@rocket.chat/ui-client';
3+
import type { TFunction } from 'i18next';
4+
import { useTranslation } from 'react-i18next';
5+
6+
type HistoryActions = 'voiceCall' | 'videoCall' | 'jumpToMessage' | 'directMessage' | 'userInfo';
7+
8+
export type HistoryActionCallbacks = {
9+
[K in HistoryActions]?: () => void;
10+
};
11+
12+
type CallHistoryActionsProps = {
13+
onClose: () => void;
14+
actions: HistoryActionCallbacks;
15+
};
16+
17+
const iconDictionary: Record<HistoryActions, IconName> = {
18+
voiceCall: 'phone',
19+
videoCall: 'video',
20+
jumpToMessage: 'jump',
21+
directMessage: 'balloon',
22+
userInfo: 'user',
23+
} as const;
24+
25+
const i18nDictionary: Record<HistoryActions, string> = {
26+
voiceCall: 'Voice_call',
27+
videoCall: 'Video_call',
28+
jumpToMessage: 'Jump_to_message',
29+
directMessage: 'Direct_Message',
30+
userInfo: 'User_info',
31+
} as const;
32+
33+
const getItems = (actions: HistoryActionCallbacks, t: TFunction) => {
34+
return (Object.entries(actions) as [HistoryActions, () => void][])
35+
.filter(([_, callback]) => callback)
36+
.map(([action, callback]) => ({
37+
id: action,
38+
icon: iconDictionary[action],
39+
content: t(i18nDictionary[action]),
40+
onClick: callback,
41+
}));
42+
};
43+
44+
const CallHistoryActions = ({ onClose, actions }: CallHistoryActionsProps) => {
45+
const { t } = useTranslation();
46+
47+
const items = getItems(actions, t);
48+
return (
49+
<ContextualbarActions>
50+
{items.length > 0 && <GenericMenu title={t('Options')} items={items} />}
51+
<ContextualbarClose onClick={onClose} />
52+
</ContextualbarActions>
53+
);
54+
};
55+
56+
export default CallHistoryActions;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { mockAppRoot } from '@rocket.chat/mock-providers';
2+
import { composeStories } from '@storybook/react';
3+
import { render } from '@testing-library/react';
4+
import { axe } from 'jest-axe';
5+
6+
import * as contextualbarStories from './CallHistoryContextualbar.stories';
7+
8+
const testCases = Object.values(composeStories(contextualbarStories)).map((Story) => [Story.storyName || 'Story', Story]);
9+
10+
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
11+
const view = render(<Story />, { wrapper: mockAppRoot().build() });
12+
expect(view.baseElement).toMatchSnapshot();
13+
});
14+
15+
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
16+
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });
17+
18+
const results = await axe(container);
19+
expect(results).toHaveNoViolations();
20+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { mockAppRoot } from '@rocket.chat/mock-providers';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
import type { ReactElement } from 'react';
4+
5+
import CallHistoryContextualbar from './CallHistoryContextualbar';
6+
7+
const noop = () => undefined;
8+
9+
const meta = {
10+
title: 'V2/Views/CallHistoryContextualbar',
11+
component: CallHistoryContextualbar,
12+
decorators: [
13+
mockAppRoot()
14+
.withTranslations('en', 'core', {
15+
Call_info: 'Call info',
16+
Direct_message: 'Direct message',
17+
Call: 'Call',
18+
Call_ended_bold: '*Voice call ended*',
19+
Incoming_voice_call: 'Incoming voice call',
20+
Outgoing_voice_call: 'Outgoing voice call',
21+
Duration: 'Duration',
22+
Voice_call_extension: 'Voice call extension',
23+
Call_ID: 'Call ID',
24+
Options: 'Options',
25+
Voice_call: 'Voice call',
26+
Video_call: 'Video call',
27+
Jump_to_message: 'Jump to message',
28+
Direct_Message: 'Direct Message',
29+
User_info: 'User info',
30+
})
31+
.withDefaultLanguage('en-US')
32+
.buildStoryDecorator(),
33+
(Story): ReactElement => <Story />,
34+
],
35+
} satisfies Meta<typeof CallHistoryContextualbar>;
36+
37+
export default meta;
38+
39+
type Story = StoryObj<typeof meta>;
40+
41+
const externalContact = {
42+
number: '1234567890',
43+
};
44+
45+
const internalContact = {
46+
_id: '1234567890',
47+
name: 'John Doe',
48+
username: 'john.doe',
49+
voiceCallExtension: '0000',
50+
};
51+
52+
export const Default: Story = {
53+
args: {
54+
onClose: noop,
55+
actions: {
56+
voiceCall: noop,
57+
videoCall: noop,
58+
jumpToMessage: noop,
59+
directMessage: noop,
60+
userInfo: noop,
61+
},
62+
contact: internalContact,
63+
data: {
64+
callId: '1234567890',
65+
direction: 'inbound',
66+
duration: 100,
67+
startedAt: new Date('2025-02-07T12:00:00.000Z'),
68+
state: 'ended',
69+
},
70+
},
71+
};
72+
73+
export const ExternalContact: Story = {
74+
args: {
75+
onClose: noop,
76+
actions: {
77+
voiceCall: noop,
78+
},
79+
data: {
80+
callId: '1234567890',
81+
direction: 'inbound',
82+
duration: 100,
83+
startedAt: new Date('2025-02-07T12:00:00.000Z'),
84+
state: 'ended',
85+
},
86+
contact: externalContact,
87+
},
88+
};

0 commit comments

Comments
 (0)