Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
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');
});
});
});
91 changes: 91 additions & 0 deletions apps/meteor/server/services/media-call/getHistoryMessagePayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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';

// TODO bold the text
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;
};

// TODO proper translation keys
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