Fix inbound voice call ownership flow
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import VoiceCallBubble from './VoiceCall.vue';
|
||||
import { MESSAGE_TYPES, VOICE_CALL_STATUS } from '../constants';
|
||||
|
||||
let messageContext;
|
||||
|
||||
vi.mock('../provider.js', () => ({
|
||||
useMessageContext: () => messageContext,
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params = {}) => {
|
||||
const translations = {
|
||||
'CONVERSATION.VOICE_CALL.CALL_IN_PROGRESS': 'Call in progress',
|
||||
'CONVERSATION.VOICE_CALL.AGENT_ANSWERED': `${params.agentName} answered`,
|
||||
'CONVERSATION.VOICE_CALL.YOU_ANSWERED': 'You answered',
|
||||
'CONVERSATION.VOICE_CALL.THEY_ANSWERED': 'They answered',
|
||||
'CONVERSATION.VOICE_CALL.NOT_ANSWERED_YET': 'Not answered yet',
|
||||
'CONVERSATION.VOICE_CALL.CALL_ENDED': 'Call ended',
|
||||
'CONVERSATION.VOICE_CALL.NO_ANSWER': 'No answer',
|
||||
};
|
||||
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mountComponent = () =>
|
||||
mount(VoiceCallBubble, {
|
||||
global: {
|
||||
stubs: {
|
||||
BaseBubble: {
|
||||
template: '<div><slot /></div>',
|
||||
},
|
||||
Icon: true,
|
||||
},
|
||||
mocks: {
|
||||
$t: key => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('VoiceCall.vue', () => {
|
||||
beforeEach(() => {
|
||||
messageContext = {
|
||||
contentAttributes: ref({
|
||||
data: {
|
||||
status: VOICE_CALL_STATUS.IN_PROGRESS,
|
||||
meta: {},
|
||||
},
|
||||
}),
|
||||
currentUserId: ref(1),
|
||||
messageType: ref(MESSAGE_TYPES.INCOMING),
|
||||
};
|
||||
});
|
||||
|
||||
it('shows the answering agent name when another agent answers the call', () => {
|
||||
messageContext.contentAttributes.value.data.meta.joinedBy = {
|
||||
id: 2,
|
||||
name: 'Ben',
|
||||
};
|
||||
|
||||
const wrapper = mountComponent();
|
||||
|
||||
expect(wrapper.text()).toContain('Ben answered');
|
||||
});
|
||||
|
||||
it('shows "You answered" when the current user answered the call', () => {
|
||||
messageContext.contentAttributes.value.data.meta.joinedBy = {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
};
|
||||
|
||||
const wrapper = mountComponent();
|
||||
|
||||
expect(wrapper.text()).toContain('You answered');
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,12 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import { MESSAGE_TYPES, VOICE_CALL_STATUS } from '../constants';
|
||||
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import BaseBubble from 'next/message/bubbles/Base.vue';
|
||||
|
||||
const LABEL_MAP = {
|
||||
[VOICE_CALL_STATUS.IN_PROGRESS]: 'CONVERSATION.VOICE_CALL.CALL_IN_PROGRESS',
|
||||
[VOICE_CALL_STATUS.COMPLETED]: 'CONVERSATION.VOICE_CALL.CALL_ENDED',
|
||||
};
|
||||
|
||||
const SUBTEXT_MAP = {
|
||||
[VOICE_CALL_STATUS.RINGING]: 'CONVERSATION.VOICE_CALL.NOT_ANSWERED_YET',
|
||||
[VOICE_CALL_STATUS.COMPLETED]: 'CONVERSATION.VOICE_CALL.CALL_ENDED',
|
||||
};
|
||||
|
||||
const ICON_MAP = {
|
||||
[VOICE_CALL_STATUS.IN_PROGRESS]: 'i-ph-phone-call',
|
||||
[VOICE_CALL_STATUS.NO_ANSWER]: 'i-ph-phone-x',
|
||||
@@ -30,38 +21,69 @@ const BG_COLOR_MAP = {
|
||||
[VOICE_CALL_STATUS.FAILED]: 'bg-n-ruby-9',
|
||||
};
|
||||
|
||||
const { contentAttributes, messageType } = useMessageContext();
|
||||
const { t } = useI18n();
|
||||
const { contentAttributes, currentUserId, messageType } = useMessageContext();
|
||||
|
||||
const data = computed(() => contentAttributes.value?.data);
|
||||
const status = computed(() => data.value?.status?.toString());
|
||||
const joinedBy = computed(() => {
|
||||
return data.value?.meta?.joinedBy || data.value?.meta?.joined_by;
|
||||
});
|
||||
|
||||
const isOutbound = computed(() => messageType.value === MESSAGE_TYPES.OUTGOING);
|
||||
const isFailed = computed(() =>
|
||||
[VOICE_CALL_STATUS.NO_ANSWER, VOICE_CALL_STATUS.FAILED].includes(status.value)
|
||||
);
|
||||
|
||||
const labelKey = computed(() => {
|
||||
if (LABEL_MAP[status.value]) return LABEL_MAP[status.value];
|
||||
if (status.value === VOICE_CALL_STATUS.RINGING) {
|
||||
return isOutbound.value
|
||||
? 'CONVERSATION.VOICE_CALL.OUTGOING_CALL'
|
||||
: 'CONVERSATION.VOICE_CALL.INCOMING_CALL';
|
||||
}
|
||||
return isFailed.value
|
||||
? 'CONVERSATION.VOICE_CALL.MISSED_CALL'
|
||||
: 'CONVERSATION.VOICE_CALL.INCOMING_CALL';
|
||||
const didCurrentUserAnswer = computed(() => {
|
||||
return joinedBy.value?.id === currentUserId.value;
|
||||
});
|
||||
|
||||
const subtextKey = computed(() => {
|
||||
if (SUBTEXT_MAP[status.value]) return SUBTEXT_MAP[status.value];
|
||||
const label = computed(() => {
|
||||
if (status.value === VOICE_CALL_STATUS.IN_PROGRESS) {
|
||||
return isOutbound.value
|
||||
? 'CONVERSATION.VOICE_CALL.THEY_ANSWERED'
|
||||
: 'CONVERSATION.VOICE_CALL.YOU_ANSWERED';
|
||||
return t('CONVERSATION.VOICE_CALL.CALL_IN_PROGRESS');
|
||||
}
|
||||
|
||||
if (status.value === VOICE_CALL_STATUS.COMPLETED) {
|
||||
return t('CONVERSATION.VOICE_CALL.CALL_ENDED');
|
||||
}
|
||||
|
||||
if (status.value === VOICE_CALL_STATUS.RINGING) {
|
||||
return isOutbound.value
|
||||
? t('CONVERSATION.VOICE_CALL.OUTGOING_CALL')
|
||||
: t('CONVERSATION.VOICE_CALL.INCOMING_CALL');
|
||||
}
|
||||
|
||||
return isFailed.value
|
||||
? 'CONVERSATION.VOICE_CALL.NO_ANSWER'
|
||||
: 'CONVERSATION.VOICE_CALL.NOT_ANSWERED_YET';
|
||||
? t('CONVERSATION.VOICE_CALL.MISSED_CALL')
|
||||
: t('CONVERSATION.VOICE_CALL.INCOMING_CALL');
|
||||
});
|
||||
|
||||
const subtext = computed(() => {
|
||||
if (status.value === VOICE_CALL_STATUS.RINGING) {
|
||||
return t('CONVERSATION.VOICE_CALL.NOT_ANSWERED_YET');
|
||||
}
|
||||
|
||||
if (status.value === VOICE_CALL_STATUS.COMPLETED) {
|
||||
return t('CONVERSATION.VOICE_CALL.CALL_ENDED');
|
||||
}
|
||||
|
||||
if (status.value === VOICE_CALL_STATUS.IN_PROGRESS) {
|
||||
if (didCurrentUserAnswer.value) {
|
||||
return t('CONVERSATION.VOICE_CALL.YOU_ANSWERED');
|
||||
}
|
||||
|
||||
if (joinedBy.value?.name) {
|
||||
return t('CONVERSATION.VOICE_CALL.AGENT_ANSWERED', {
|
||||
agentName: joinedBy.value.name,
|
||||
});
|
||||
}
|
||||
|
||||
return t('CONVERSATION.VOICE_CALL.THEY_ANSWERED');
|
||||
}
|
||||
|
||||
return isFailed.value
|
||||
? t('CONVERSATION.VOICE_CALL.NO_ANSWER')
|
||||
: t('CONVERSATION.VOICE_CALL.NOT_ANSWERED_YET');
|
||||
});
|
||||
|
||||
const iconName = computed(() => {
|
||||
@@ -92,10 +114,10 @@ const bgColor = computed(() => BG_COLOR_MAP[status.value] || 'bg-n-teal-9');
|
||||
|
||||
<div class="flex overflow-hidden flex-col flex-grow">
|
||||
<span class="text-sm font-medium truncate text-n-slate-12">
|
||||
{{ $t(labelKey) }}
|
||||
{{ label }}
|
||||
</span>
|
||||
<span class="text-xs text-n-slate-11">
|
||||
{{ $t(subtextKey) }}
|
||||
{{ subtext }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user