Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/five-frogs-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/i18n": minor
"@rocket.chat/model-typings": minor
"@rocket.chat/models": minor
"@rocket.chat/media-calls": minor
---

Introduces in-chat messages for when a voice call ends
1 change: 1 addition & 0 deletions apps/meteor/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export default {
'<rootDir>/app/api/server/**.spec.ts',
'<rootDir>/app/api/server/helpers/**.spec.ts',
'<rootDir>/app/api/server/middlewares/**.spec.ts',
'<rootDir>/server/services/media-call/**.spec.ts',
],
coveragePathIgnorePatterns: ['/node_modules/'],
},
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/server/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
BannersDismissRaw,
BannersRaw,
CalendarEventRaw,
CallHistoryRaw,
CredentialTokensRaw,
CronHistoryRaw,
CustomSoundsRaw,
Expand Down Expand Up @@ -95,6 +96,7 @@ registerModel('IAvatarsModel', new AvatarsRaw(db));
registerModel('IBannersDismissModel', new BannersDismissRaw(db));
registerModel('IBannersModel', new BannersRaw(db));
registerModel('ICalendarEventModel', new CalendarEventRaw(db));
registerModel('ICallHistoryModel', new CallHistoryRaw(db));
registerModel('ICredentialTokensModel', new CredentialTokensRaw(db));
registerModel('ICronHistoryModel', new CronHistoryRaw(db));
registerModel('ICustomSoundsModel', new CustomSoundsRaw(db));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { callStateToTranslationKey, callStateToIcon, getFormattedCallDuration, getHistoryMessagePayload } from './getHistoryMessagePayload';

const appId = 'media-call-core';
describe('callStateToTranslationKey', () => {
it('should return correct translation key for "ended" state', () => {
const result = callStateToTranslationKey('ended');
expect(result).toEqual({ type: 'mrkdwn', i18n: { key: 'Call_ended_bold' }, text: 'Call ended' });
});

it('should return correct translation key for "not-answered" state', () => {
const result = callStateToTranslationKey('not-answered');
expect(result).toEqual({ type: 'mrkdwn', i18n: { key: 'Call_not_answered_bold' }, text: 'Call not answered' });
});

it('should return correct translation key for "failed" state', () => {
const result = callStateToTranslationKey('failed');
expect(result).toEqual({ type: 'mrkdwn', i18n: { key: 'Call_failed_bold' }, text: 'Call failed' });
});

it('should return correct translation key for "error" state', () => {
const result = callStateToTranslationKey('error');
expect(result).toEqual({ type: 'mrkdwn', i18n: { key: 'Call_failed_bold' }, text: 'Call failed' });
});

it('should return correct translation key for "transferred" state', () => {
const result = callStateToTranslationKey('transferred');
expect(result).toEqual({ type: 'mrkdwn', i18n: { key: 'Call_transferred_bold' }, text: 'Call transferred' });
});
});

describe('callStateToIcon', () => {
it('should return correct icon for "ended" state', () => {
const result = callStateToIcon('ended');
expect(result).toEqual({ type: 'icon', icon: 'phone-off', variant: 'secondary' });
});

it('should return correct icon for "not-answered" state', () => {
const result = callStateToIcon('not-answered');
expect(result).toEqual({ type: 'icon', icon: 'clock', variant: 'danger' });
});

it('should return correct icon for "failed" state', () => {
const result = callStateToIcon('failed');
expect(result).toEqual({ type: 'icon', icon: 'phone-issue', variant: 'danger' });
});

it('should return correct icon for "error" state', () => {
const result = callStateToIcon('error');
expect(result).toEqual({ type: 'icon', icon: 'phone-issue', variant: 'danger' });
});

it('should return correct icon for "transferred" state', () => {
const result = callStateToIcon('transferred');
expect(result).toEqual({ type: 'icon', icon: 'arrow-forward', variant: 'secondary' });
});
});

describe('getFormattedCallDuration', () => {
it('should return undefined when callDuration is undefined', () => {
const result = getFormattedCallDuration(undefined);
expect(result).toBeUndefined();
});

it('should return undefined when callDuration is 0', () => {
const result = getFormattedCallDuration(0);
expect(result).toBeUndefined();
});

it('should format duration correctly for seconds only (less than 60 seconds)', () => {
const result = getFormattedCallDuration(30);
expect(result).toEqual({ type: 'mrkdwn', text: '*00:30*' });
});

it('should format duration correctly for minutes and seconds (less than 1 hour)', () => {
const result = getFormattedCallDuration(125); // 2 minutes 5 seconds
expect(result).toEqual({ type: 'mrkdwn', text: '*02:05*' });
});

it('should format duration correctly for exactly 1 minute', () => {
const result = getFormattedCallDuration(60);
expect(result).toEqual({ type: 'mrkdwn', text: '*01:00*' });
});

it('should format duration correctly for hours, minutes, and seconds', () => {
const result = getFormattedCallDuration(3665); // 1 hour 1 minute 5 seconds
expect(result).toEqual({ type: 'mrkdwn', text: '*01:01:05*' });
});

it('should format duration correctly for multiple hours', () => {
const result = getFormattedCallDuration(7325); // 2 hours 2 minutes 5 seconds
expect(result).toEqual({ type: 'mrkdwn', text: '*02:02:05*' });
});

it('should pad single digit values with zeros', () => {
const result = getFormattedCallDuration(61); // 1 minute 1 second
expect(result).toEqual({ type: 'mrkdwn', text: '*01:01*' });
});

it('should handle large durations correctly', () => {
const result = getFormattedCallDuration(36661); // 10 hours 11 minutes 1 second
expect(result).toEqual({ type: 'mrkdwn', text: '*10:11:01*' });
});
});

describe('getHistoryMessagePayload', () => {
it('should return correct payload for "ended" state without duration', () => {
const result = getHistoryMessagePayload('ended', undefined);
expect(result).toEqual({
msg: '',
groupable: false,
blocks: [
{
appId,
type: 'info_card',
rows: [
{
background: 'default',
elements: [
{ type: 'icon', icon: 'phone-off', variant: 'secondary' },
{ type: 'mrkdwn', i18n: { key: 'Call_ended_bold' }, text: 'Call ended' },
],
},
],
},
],
});
});

it('should return correct payload for "ended" state with duration', () => {
const result = getHistoryMessagePayload('ended', 125);
expect(result).toEqual({
msg: '',
groupable: false,
blocks: [
{
appId,
type: 'info_card',
rows: [
{
background: 'default',
elements: [
{ type: 'icon', icon: 'phone-off', variant: 'secondary' },
{ type: 'mrkdwn', i18n: { key: 'Call_ended_bold' }, text: 'Call ended' },
],
},
{
background: 'secondary',
elements: [{ type: 'mrkdwn', text: '*02:05*' }],
},
],
},
],
});
});

it('should return correct payload for "not-answered" state', () => {
const result = getHistoryMessagePayload('not-answered', undefined);
expect(result).toEqual({
msg: '',
groupable: false,
blocks: [
{
appId,
type: 'info_card',
rows: [
{
background: 'default',
elements: [
{ type: 'icon', icon: 'clock', variant: 'danger' },
{ type: 'mrkdwn', i18n: { key: 'Call_not_answered_bold' }, text: 'Call not answered' },
],
},
],
},
],
});
});

it('should return correct payload for "failed" state', () => {
const result = getHistoryMessagePayload('failed', undefined);
expect(result).toEqual({
msg: '',
groupable: false,
blocks: [
{
appId,
type: 'info_card',
rows: [
{
background: 'default',
elements: [
{ type: 'icon', icon: 'phone-issue', variant: 'danger' },
{ type: 'mrkdwn', i18n: { key: 'Call_failed_bold' }, text: 'Call failed' },
],
},
],
},
],
});
});

it('should return correct payload for "error" state', () => {
const result = getHistoryMessagePayload('error', undefined);
expect(result).toEqual({
msg: '',
groupable: false,
blocks: [
{
appId,
type: 'info_card',
rows: [
{
background: 'default',
elements: [
{ type: 'icon', icon: 'phone-issue', variant: 'danger' },
{ type: 'mrkdwn', i18n: { key: 'Call_failed_bold' }, text: 'Call failed' },
],
},
],
},
],
});
});

it('should return correct payload for "transferred" state', () => {
const result = getHistoryMessagePayload('transferred', undefined);
expect(result).toEqual({
msg: '',
groupable: false,
blocks: [
{
appId,
type: 'info_card',
rows: [
{
background: 'default',
elements: [
{ type: 'icon', icon: 'arrow-forward', variant: 'secondary' },
{ type: 'mrkdwn', i18n: { key: 'Call_transferred_bold' }, text: 'Call transferred' },
],
},
],
},
],
});
});

it('should include duration row when duration is provided', () => {
const result = getHistoryMessagePayload('ended', 3665);

expect(result.blocks[0].rows).toHaveLength(2);
expect(result.blocks[0].rows[1]).toEqual({
background: 'secondary',
elements: [{ type: 'mrkdwn', text: '*01:01:05*' }],
});
});

it('should not include duration row when duration is undefined', () => {
const result = getHistoryMessagePayload('ended', undefined);
expect(result.blocks[0].rows).toHaveLength(1);
});

it('should handle all call states with duration correctly', () => {
const states = ['ended', 'transferred', 'not-answered', 'failed', 'error'] as const;
const duration = 125;

states.forEach((state) => {
const result = getHistoryMessagePayload(state, duration);
expect(result.msg).toBe('');
expect(result.groupable).toBe(false);
expect(result.blocks).toHaveLength(1);
expect(result.blocks[0].type).toBe('info_card');
expect(result.blocks[0].rows).toHaveLength(2);
expect(result.blocks[0].rows[1].background).toBe('secondary');
expect(result.blocks[0].rows[1].elements[0].type).toBe('mrkdwn');
});
});
});
Loading
Loading