Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
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');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { CallHistoryItemState, IMessage } from '@rocket.chat/core-typings';
import type { IconElement, InfoCardBlock, TextObject } from '@rocket.chat/ui-kit';
import { intervalToDuration, secondsToMilliseconds } from 'date-fns';

const APP_ID = 'media-call-core';

export const callStateToTranslationKey = (callState: CallHistoryItemState): TextObject => {
switch (callState) {
case 'ended':
return { type: 'mrkdwn', i18n: { key: 'Call_ended_bold' }, text: 'Call ended' };
case 'not-answered':
return { type: 'mrkdwn', i18n: { key: 'Call_not_answered_bold' }, text: 'Call not answered' };
case 'failed':
case 'error':
return { type: 'mrkdwn', i18n: { key: 'Call_failed_bold' }, text: 'Call failed' };
case 'transferred':
return { type: 'mrkdwn', i18n: { key: 'Call_transferred_bold' }, text: 'Call transferred' };
}
};

export const callStateToIcon = (callState: CallHistoryItemState): IconElement => {
switch (callState) {
case 'ended':
return { type: 'icon', icon: 'phone-off', variant: 'secondary' };
case 'not-answered':
return { type: 'icon', icon: 'clock', variant: 'danger' };
case 'failed':
case 'error':
return { type: 'icon', icon: 'phone-issue', variant: 'danger' };
case 'transferred':
return { type: 'icon', icon: 'arrow-forward', variant: 'secondary' };
}
};

const buildDurationString = (...values: number[]): string => {
return values.map((value) => value.toString().padStart(2, '0')).join(':');
};

export const getFormattedCallDuration = (callDuration: number | undefined): TextObject | undefined => {
if (!callDuration || typeof callDuration !== 'number') {
return undefined;
}

const milliseconds = secondsToMilliseconds(callDuration);
const duration = { minutes: 0, seconds: 0, ...intervalToDuration({ start: 0, end: milliseconds }) };

if (duration.hours && duration.hours > 0) {
return { type: 'mrkdwn', text: `*${buildDurationString(duration.hours, duration.minutes, duration.seconds)}*` } as const;
}

return {
type: 'mrkdwn',
text: `*${buildDurationString(duration.minutes, duration.seconds)}*`,
} as const;
};

export const getHistoryMessagePayload = (
callState: CallHistoryItemState,
callDuration: number | undefined,
): Pick<IMessage, 'msg' | 'groupable'> & { blocks: [InfoCardBlock] } => {
const callStateTranslationKey = callStateToTranslationKey(callState);
const icon = callStateToIcon(callState);
const callDurationFormatted = getFormattedCallDuration(callDuration);

return {
msg: '',
groupable: false,
blocks: [
{
appId: APP_ID,
type: 'info_card',
rows: [
{
background: 'default',
elements: [icon, callStateTranslationKey],
},
...(callDurationFormatted
? [
{
background: 'secondary',
elements: [callDurationFormatted],
} as const,
]
: []),
],
},
],
};
};
Loading
Loading