Skip to content

Commit 7f1b834

Browse files
feat: In-Chat Messages for Voice Calls (#37378)
Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com>
1 parent 5f075ea commit 7f1b834

File tree

17 files changed

+639
-4
lines changed

17 files changed

+639
-4
lines changed

.changeset/five-frogs-kneel.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@rocket.chat/meteor": minor
3+
"@rocket.chat/core-typings": minor
4+
"@rocket.chat/i18n": minor
5+
"@rocket.chat/model-typings": minor
6+
"@rocket.chat/models": minor
7+
"@rocket.chat/media-calls": minor
8+
---
9+
10+
Introduces in-chat messages for when a voice call ends

apps/meteor/jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export default {
4343
'<rootDir>/app/api/server/**.spec.ts',
4444
'<rootDir>/app/api/server/helpers/**.spec.ts',
4545
'<rootDir>/app/api/server/middlewares/**.spec.ts',
46+
'<rootDir>/server/services/media-call/**.spec.ts',
4647
],
4748
coveragePathIgnorePatterns: ['/node_modules/'],
4849
},

apps/meteor/server/models.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
BannersDismissRaw,
1010
BannersRaw,
1111
CalendarEventRaw,
12+
CallHistoryRaw,
1213
CredentialTokensRaw,
1314
CronHistoryRaw,
1415
CustomSoundsRaw,
@@ -95,6 +96,7 @@ registerModel('IAvatarsModel', new AvatarsRaw(db));
9596
registerModel('IBannersDismissModel', new BannersDismissRaw(db));
9697
registerModel('IBannersModel', new BannersRaw(db));
9798
registerModel('ICalendarEventModel', new CalendarEventRaw(db));
99+
registerModel('ICallHistoryModel', new CallHistoryRaw(db));
98100
registerModel('ICredentialTokensModel', new CredentialTokensRaw(db));
99101
registerModel('ICronHistoryModel', new CronHistoryRaw(db));
100102
registerModel('ICustomSoundsModel', new CustomSoundsRaw(db));
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import { callStateToTranslationKey, callStateToIcon, getFormattedCallDuration, getHistoryMessagePayload } from './getHistoryMessagePayload';
2+
3+
const appId = 'media-call-core';
4+
describe('callStateToTranslationKey', () => {
5+
it('should return correct translation key for "ended" state', () => {
6+
const result = callStateToTranslationKey('ended');
7+
expect(result).toEqual({ type: 'mrkdwn', i18n: { key: 'Call_ended_bold' }, text: 'Call ended' });
8+
});
9+
10+
it('should return correct translation key for "not-answered" state', () => {
11+
const result = callStateToTranslationKey('not-answered');
12+
expect(result).toEqual({ type: 'mrkdwn', i18n: { key: 'Call_not_answered_bold' }, text: 'Call not answered' });
13+
});
14+
15+
it('should return correct translation key for "failed" state', () => {
16+
const result = callStateToTranslationKey('failed');
17+
expect(result).toEqual({ type: 'mrkdwn', i18n: { key: 'Call_failed_bold' }, text: 'Call failed' });
18+
});
19+
20+
it('should return correct translation key for "error" state', () => {
21+
const result = callStateToTranslationKey('error');
22+
expect(result).toEqual({ type: 'mrkdwn', i18n: { key: 'Call_failed_bold' }, text: 'Call failed' });
23+
});
24+
25+
it('should return correct translation key for "transferred" state', () => {
26+
const result = callStateToTranslationKey('transferred');
27+
expect(result).toEqual({ type: 'mrkdwn', i18n: { key: 'Call_transferred_bold' }, text: 'Call transferred' });
28+
});
29+
});
30+
31+
describe('callStateToIcon', () => {
32+
it('should return correct icon for "ended" state', () => {
33+
const result = callStateToIcon('ended');
34+
expect(result).toEqual({ type: 'icon', icon: 'phone-off', variant: 'secondary' });
35+
});
36+
37+
it('should return correct icon for "not-answered" state', () => {
38+
const result = callStateToIcon('not-answered');
39+
expect(result).toEqual({ type: 'icon', icon: 'clock', variant: 'danger' });
40+
});
41+
42+
it('should return correct icon for "failed" state', () => {
43+
const result = callStateToIcon('failed');
44+
expect(result).toEqual({ type: 'icon', icon: 'phone-issue', variant: 'danger' });
45+
});
46+
47+
it('should return correct icon for "error" state', () => {
48+
const result = callStateToIcon('error');
49+
expect(result).toEqual({ type: 'icon', icon: 'phone-issue', variant: 'danger' });
50+
});
51+
52+
it('should return correct icon for "transferred" state', () => {
53+
const result = callStateToIcon('transferred');
54+
expect(result).toEqual({ type: 'icon', icon: 'arrow-forward', variant: 'secondary' });
55+
});
56+
});
57+
58+
describe('getFormattedCallDuration', () => {
59+
it('should return undefined when callDuration is undefined', () => {
60+
const result = getFormattedCallDuration(undefined);
61+
expect(result).toBeUndefined();
62+
});
63+
64+
it('should return undefined when callDuration is 0', () => {
65+
const result = getFormattedCallDuration(0);
66+
expect(result).toBeUndefined();
67+
});
68+
69+
it('should format duration correctly for seconds only (less than 60 seconds)', () => {
70+
const result = getFormattedCallDuration(30);
71+
expect(result).toEqual({ type: 'mrkdwn', text: '*00:30*' });
72+
});
73+
74+
it('should format duration correctly for minutes and seconds (less than 1 hour)', () => {
75+
const result = getFormattedCallDuration(125); // 2 minutes 5 seconds
76+
expect(result).toEqual({ type: 'mrkdwn', text: '*02:05*' });
77+
});
78+
79+
it('should format duration correctly for exactly 1 minute', () => {
80+
const result = getFormattedCallDuration(60);
81+
expect(result).toEqual({ type: 'mrkdwn', text: '*01:00*' });
82+
});
83+
84+
it('should format duration correctly for hours, minutes, and seconds', () => {
85+
const result = getFormattedCallDuration(3665); // 1 hour 1 minute 5 seconds
86+
expect(result).toEqual({ type: 'mrkdwn', text: '*01:01:05*' });
87+
});
88+
89+
it('should format duration correctly for multiple hours', () => {
90+
const result = getFormattedCallDuration(7325); // 2 hours 2 minutes 5 seconds
91+
expect(result).toEqual({ type: 'mrkdwn', text: '*02:02:05*' });
92+
});
93+
94+
it('should pad single digit values with zeros', () => {
95+
const result = getFormattedCallDuration(61); // 1 minute 1 second
96+
expect(result).toEqual({ type: 'mrkdwn', text: '*01:01*' });
97+
});
98+
99+
it('should handle large durations correctly', () => {
100+
const result = getFormattedCallDuration(36661); // 10 hours 11 minutes 1 second
101+
expect(result).toEqual({ type: 'mrkdwn', text: '*10:11:01*' });
102+
});
103+
});
104+
105+
describe('getHistoryMessagePayload', () => {
106+
it('should return correct payload for "ended" state without duration', () => {
107+
const result = getHistoryMessagePayload('ended', undefined);
108+
expect(result).toEqual({
109+
msg: '',
110+
groupable: false,
111+
blocks: [
112+
{
113+
appId,
114+
type: 'info_card',
115+
rows: [
116+
{
117+
background: 'default',
118+
elements: [
119+
{ type: 'icon', icon: 'phone-off', variant: 'secondary' },
120+
{ type: 'mrkdwn', i18n: { key: 'Call_ended_bold' }, text: 'Call ended' },
121+
],
122+
},
123+
],
124+
},
125+
],
126+
});
127+
});
128+
129+
it('should return correct payload for "ended" state with duration', () => {
130+
const result = getHistoryMessagePayload('ended', 125);
131+
expect(result).toEqual({
132+
msg: '',
133+
groupable: false,
134+
blocks: [
135+
{
136+
appId,
137+
type: 'info_card',
138+
rows: [
139+
{
140+
background: 'default',
141+
elements: [
142+
{ type: 'icon', icon: 'phone-off', variant: 'secondary' },
143+
{ type: 'mrkdwn', i18n: { key: 'Call_ended_bold' }, text: 'Call ended' },
144+
],
145+
},
146+
{
147+
background: 'secondary',
148+
elements: [{ type: 'mrkdwn', text: '*02:05*' }],
149+
},
150+
],
151+
},
152+
],
153+
});
154+
});
155+
156+
it('should return correct payload for "not-answered" state', () => {
157+
const result = getHistoryMessagePayload('not-answered', undefined);
158+
expect(result).toEqual({
159+
msg: '',
160+
groupable: false,
161+
blocks: [
162+
{
163+
appId,
164+
type: 'info_card',
165+
rows: [
166+
{
167+
background: 'default',
168+
elements: [
169+
{ type: 'icon', icon: 'clock', variant: 'danger' },
170+
{ type: 'mrkdwn', i18n: { key: 'Call_not_answered_bold' }, text: 'Call not answered' },
171+
],
172+
},
173+
],
174+
},
175+
],
176+
});
177+
});
178+
179+
it('should return correct payload for "failed" state', () => {
180+
const result = getHistoryMessagePayload('failed', undefined);
181+
expect(result).toEqual({
182+
msg: '',
183+
groupable: false,
184+
blocks: [
185+
{
186+
appId,
187+
type: 'info_card',
188+
rows: [
189+
{
190+
background: 'default',
191+
elements: [
192+
{ type: 'icon', icon: 'phone-issue', variant: 'danger' },
193+
{ type: 'mrkdwn', i18n: { key: 'Call_failed_bold' }, text: 'Call failed' },
194+
],
195+
},
196+
],
197+
},
198+
],
199+
});
200+
});
201+
202+
it('should return correct payload for "error" state', () => {
203+
const result = getHistoryMessagePayload('error', undefined);
204+
expect(result).toEqual({
205+
msg: '',
206+
groupable: false,
207+
blocks: [
208+
{
209+
appId,
210+
type: 'info_card',
211+
rows: [
212+
{
213+
background: 'default',
214+
elements: [
215+
{ type: 'icon', icon: 'phone-issue', variant: 'danger' },
216+
{ type: 'mrkdwn', i18n: { key: 'Call_failed_bold' }, text: 'Call failed' },
217+
],
218+
},
219+
],
220+
},
221+
],
222+
});
223+
});
224+
225+
it('should return correct payload for "transferred" state', () => {
226+
const result = getHistoryMessagePayload('transferred', undefined);
227+
expect(result).toEqual({
228+
msg: '',
229+
groupable: false,
230+
blocks: [
231+
{
232+
appId,
233+
type: 'info_card',
234+
rows: [
235+
{
236+
background: 'default',
237+
elements: [
238+
{ type: 'icon', icon: 'arrow-forward', variant: 'secondary' },
239+
{ type: 'mrkdwn', i18n: { key: 'Call_transferred_bold' }, text: 'Call transferred' },
240+
],
241+
},
242+
],
243+
},
244+
],
245+
});
246+
});
247+
248+
it('should include duration row when duration is provided', () => {
249+
const result = getHistoryMessagePayload('ended', 3665);
250+
251+
expect(result.blocks[0].rows).toHaveLength(2);
252+
expect(result.blocks[0].rows[1]).toEqual({
253+
background: 'secondary',
254+
elements: [{ type: 'mrkdwn', text: '*01:01:05*' }],
255+
});
256+
});
257+
258+
it('should not include duration row when duration is undefined', () => {
259+
const result = getHistoryMessagePayload('ended', undefined);
260+
expect(result.blocks[0].rows).toHaveLength(1);
261+
});
262+
263+
it('should handle all call states with duration correctly', () => {
264+
const states = ['ended', 'transferred', 'not-answered', 'failed', 'error'] as const;
265+
const duration = 125;
266+
267+
states.forEach((state) => {
268+
const result = getHistoryMessagePayload(state, duration);
269+
expect(result.msg).toBe('');
270+
expect(result.groupable).toBe(false);
271+
expect(result.blocks).toHaveLength(1);
272+
expect(result.blocks[0].type).toBe('info_card');
273+
expect(result.blocks[0].rows).toHaveLength(2);
274+
expect(result.blocks[0].rows[1].background).toBe('secondary');
275+
expect(result.blocks[0].rows[1].elements[0].type).toBe('mrkdwn');
276+
});
277+
});
278+
});

0 commit comments

Comments
 (0)