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>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import VoiceAPI from 'dashboard/api/channel/voice/voiceAPIClient';
|
||||
import TwilioVoiceClient from 'dashboard/api/channel/voice/twilioVoiceClient';
|
||||
import { useCallSession } from '../useCallSession';
|
||||
|
||||
vi.mock('vue', async importOriginal => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
onMounted: vi.fn(),
|
||||
onUnmounted: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('dashboard/composables', () => ({
|
||||
useAlert: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/api/channel/voice/voiceAPIClient', () => ({
|
||||
default: {
|
||||
joinConference: vi.fn(),
|
||||
leaveConference: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/api/channel/voice/twilioVoiceClient', () => ({
|
||||
default: {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
initializeDevice: vi.fn(),
|
||||
joinClientCall: vi.fn(),
|
||||
endClientCall: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: vi.fn(key => key),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useCallSession', () => {
|
||||
let consoleErrorSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
vi.clearAllMocks();
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('shows the backend conflict message when another agent already claimed the call', async () => {
|
||||
TwilioVoiceClient.initializeDevice.mockResolvedValue({ device: true });
|
||||
VoiceAPI.joinConference.mockRejectedValue({
|
||||
response: {
|
||||
data: {
|
||||
error: 'Jane Agent is already handling the call.',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { joinCall } = useCallSession();
|
||||
const result = await joinCall({
|
||||
conversationId: 42,
|
||||
inboxId: 1,
|
||||
callSid: 'CALL123',
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(useAlert).toHaveBeenCalledWith(
|
||||
'Jane Agent is already handling the call.'
|
||||
);
|
||||
expect(TwilioVoiceClient.joinClientCall).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,20 @@
|
||||
import { computed, ref, watch, onUnmounted, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import VoiceAPI from 'dashboard/api/channel/voice/voiceAPIClient';
|
||||
import TwilioVoiceClient from 'dashboard/api/channel/voice/twilioVoiceClient';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useCallsStore } from 'dashboard/stores/calls';
|
||||
import Timer from 'dashboard/helper/Timer';
|
||||
|
||||
export function useCallSession() {
|
||||
const callsStore = useCallsStore();
|
||||
const { t } = useI18n();
|
||||
const isJoining = ref(false);
|
||||
const callDuration = ref(0);
|
||||
const durationTimer = new Timer(elapsed => {
|
||||
callDuration.value = elapsed;
|
||||
});
|
||||
const handleCallDisconnected = () => callsStore.clearActiveCall();
|
||||
|
||||
const activeCall = computed(() => callsStore.activeCall);
|
||||
const incomingCalls = computed(() => callsStore.incomingCalls);
|
||||
@@ -30,15 +34,17 @@ export function useCallSession() {
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
TwilioVoiceClient.addEventListener('call:disconnected', () =>
|
||||
callsStore.clearActiveCall()
|
||||
TwilioVoiceClient.addEventListener(
|
||||
'call:disconnected',
|
||||
handleCallDisconnected
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
durationTimer.stop();
|
||||
TwilioVoiceClient.removeEventListener('call:disconnected', () =>
|
||||
callsStore.clearActiveCall()
|
||||
TwilioVoiceClient.removeEventListener(
|
||||
'call:disconnected',
|
||||
handleCallDisconnected
|
||||
);
|
||||
});
|
||||
|
||||
@@ -73,6 +79,7 @@ export function useCallSession() {
|
||||
|
||||
return { conferenceSid: joinResponse?.conference_sid };
|
||||
} catch (error) {
|
||||
useAlert(error.response?.data?.error || t('CONTACT_PANEL.CALL_FAILED'));
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to join call:', error);
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { CONTENT_TYPES } from 'dashboard/components-next/message/constants';
|
||||
import { useCallsStore } from 'dashboard/stores/calls';
|
||||
import {
|
||||
handleVoiceCallCreated,
|
||||
handleVoiceCallUpdated,
|
||||
syncConversationCallVisibility,
|
||||
} from '../voice';
|
||||
|
||||
vi.mock('dashboard/api/channel/voice/twilioVoiceClient', () => ({
|
||||
default: {
|
||||
endClientCall: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const buildVoiceMessage = overrides => ({
|
||||
content_type: CONTENT_TYPES.VOICE_CALL,
|
||||
content_attributes: {
|
||||
data: {
|
||||
call_sid: 'CALL123',
|
||||
status: 'ringing',
|
||||
call_direction: 'inbound',
|
||||
},
|
||||
},
|
||||
conversation_id: 42,
|
||||
conversation: {
|
||||
assignee_id: null,
|
||||
},
|
||||
sender: {
|
||||
id: 7,
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('voice helper', () => {
|
||||
const commit = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows inbound calls to everyone when the conversation is unassigned', () => {
|
||||
const callsStore = useCallsStore();
|
||||
|
||||
handleVoiceCallCreated(buildVoiceMessage(), 1);
|
||||
|
||||
expect(callsStore.calls).toEqual([
|
||||
expect.objectContaining({
|
||||
callSid: 'CALL123',
|
||||
conversationId: 42,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('hides inbound calls when they are assigned to another agent', () => {
|
||||
const callsStore = useCallsStore();
|
||||
|
||||
handleVoiceCallCreated(
|
||||
buildVoiceMessage({
|
||||
conversation: {
|
||||
assignee_id: 9,
|
||||
},
|
||||
}),
|
||||
1
|
||||
);
|
||||
|
||||
expect(callsStore.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('removes a visible call when an update shows it is assigned to another agent', () => {
|
||||
const callsStore = useCallsStore();
|
||||
callsStore.addCall({
|
||||
callSid: 'CALL123',
|
||||
conversationId: 42,
|
||||
callDirection: 'inbound',
|
||||
senderId: 7,
|
||||
});
|
||||
|
||||
handleVoiceCallUpdated(
|
||||
commit,
|
||||
buildVoiceMessage({
|
||||
conversation: {
|
||||
assignee_id: 9,
|
||||
},
|
||||
}),
|
||||
1
|
||||
);
|
||||
|
||||
expect(callsStore.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('removes the call widget when the conversation assignment changes to another agent', () => {
|
||||
const callsStore = useCallsStore();
|
||||
callsStore.addCall({
|
||||
callSid: 'CALL123',
|
||||
conversationId: 42,
|
||||
callDirection: 'inbound',
|
||||
senderId: 7,
|
||||
});
|
||||
|
||||
syncConversationCallVisibility(
|
||||
{
|
||||
id: 42,
|
||||
meta: {
|
||||
assignee: {
|
||||
id: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
1
|
||||
);
|
||||
|
||||
expect(callsStore.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,26 @@ const shouldSkipCall = (callDirection, senderId, currentUserId) => {
|
||||
return callDirection === 'outbound' && senderId !== currentUserId;
|
||||
};
|
||||
|
||||
const extractAssigneeId = conversation => {
|
||||
return conversation?.assignee_id || conversation?.meta?.assignee?.id || null;
|
||||
};
|
||||
|
||||
const isAssignedToAnotherAgent = (assigneeId, currentUserId) => {
|
||||
return !!assigneeId && assigneeId !== currentUserId;
|
||||
};
|
||||
|
||||
const shouldShowCall = ({
|
||||
callDirection,
|
||||
senderId,
|
||||
assigneeId,
|
||||
currentUserId,
|
||||
}) => {
|
||||
return (
|
||||
!shouldSkipCall(callDirection, senderId, currentUserId) &&
|
||||
!isAssignedToAnotherAgent(assigneeId, currentUserId)
|
||||
);
|
||||
};
|
||||
|
||||
function extractCallData(message) {
|
||||
const contentData = message?.content_attributes?.data || {};
|
||||
return {
|
||||
@@ -29,6 +49,7 @@ function extractCallData(message) {
|
||||
status: contentData.status,
|
||||
callDirection: contentData.call_direction,
|
||||
conversationId: message?.conversation_id,
|
||||
assigneeId: extractAssigneeId(message?.conversation),
|
||||
senderId: message?.sender?.id,
|
||||
};
|
||||
}
|
||||
@@ -36,10 +57,19 @@ function extractCallData(message) {
|
||||
export function handleVoiceCallCreated(message, currentUserId) {
|
||||
if (!isVoiceCallMessage(message)) return;
|
||||
|
||||
const { callSid, callDirection, conversationId, senderId } =
|
||||
const { callSid, callDirection, conversationId, assigneeId, senderId } =
|
||||
extractCallData(message);
|
||||
|
||||
if (shouldSkipCall(callDirection, senderId, currentUserId)) return;
|
||||
if (
|
||||
!shouldShowCall({
|
||||
callDirection,
|
||||
senderId,
|
||||
assigneeId,
|
||||
currentUserId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callsStore = useCallsStore();
|
||||
callsStore.addCall({
|
||||
@@ -53,8 +83,14 @@ export function handleVoiceCallCreated(message, currentUserId) {
|
||||
export function handleVoiceCallUpdated(commit, message, currentUserId) {
|
||||
if (!isVoiceCallMessage(message)) return;
|
||||
|
||||
const { callSid, status, callDirection, conversationId, senderId } =
|
||||
extractCallData(message);
|
||||
const {
|
||||
callSid,
|
||||
status,
|
||||
callDirection,
|
||||
conversationId,
|
||||
assigneeId,
|
||||
senderId,
|
||||
} = extractCallData(message);
|
||||
|
||||
const callsStore = useCallsStore();
|
||||
|
||||
@@ -64,9 +100,19 @@ export function handleVoiceCallUpdated(commit, message, currentUserId) {
|
||||
commit(types.UPDATE_CONVERSATION_CALL_STATUS, callInfo);
|
||||
commit(types.UPDATE_MESSAGE_CALL_STATUS, callInfo);
|
||||
|
||||
const isNewCall =
|
||||
status === 'ringing' &&
|
||||
!shouldSkipCall(callDirection, senderId, currentUserId);
|
||||
if (
|
||||
!shouldShowCall({
|
||||
callDirection,
|
||||
senderId,
|
||||
assigneeId,
|
||||
currentUserId,
|
||||
})
|
||||
) {
|
||||
callsStore.removeCallsForConversation(conversationId);
|
||||
return;
|
||||
}
|
||||
|
||||
const isNewCall = status === 'ringing';
|
||||
|
||||
if (isNewCall) {
|
||||
callsStore.addCall({
|
||||
@@ -77,3 +123,11 @@ export function handleVoiceCallUpdated(commit, message, currentUserId) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function syncConversationCallVisibility(conversation, currentUserId) {
|
||||
const assigneeId = extractAssigneeId(conversation);
|
||||
if (!isAssignedToAnotherAgent(assigneeId, currentUserId)) return;
|
||||
|
||||
const callsStore = useCallsStore();
|
||||
callsStore.removeCallsForConversation(conversation.id);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"MISSED_CALL": "Missed call",
|
||||
"CALL_ENDED": "Call ended",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"AGENT_ANSWERED": "{agentName} answered",
|
||||
"THEY_ANSWERED": "They answered",
|
||||
"YOU_ANSWERED": "You answered"
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as Sentry from '@sentry/vue';
|
||||
import {
|
||||
handleVoiceCallCreated,
|
||||
handleVoiceCallUpdated,
|
||||
syncConversationCallVisibility,
|
||||
} from 'dashboard/helper/voice';
|
||||
|
||||
export const hasMessageFailedWithExternalError = pendingMessage => {
|
||||
@@ -391,18 +392,19 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
updateConversation({ commit, dispatch }, conversation) {
|
||||
const {
|
||||
meta: { sender },
|
||||
} = conversation;
|
||||
updateConversation({ commit, dispatch, rootGetters }, conversation) {
|
||||
const sender = conversation.meta?.sender;
|
||||
commit(types.UPDATE_CONVERSATION, conversation);
|
||||
syncConversationCallVisibility(conversation, rootGetters?.getCurrentUserID);
|
||||
|
||||
dispatch('conversationLabels/setConversationLabel', {
|
||||
id: conversation.id,
|
||||
data: conversation.labels,
|
||||
});
|
||||
|
||||
dispatch('contacts/setContact', sender);
|
||||
if (sender) {
|
||||
dispatch('contacts/setContact', sender);
|
||||
}
|
||||
},
|
||||
|
||||
updateConversationLastActivity(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import actions, {
|
||||
hasMessageFailedWithExternalError,
|
||||
} from '../../conversations/actions';
|
||||
@@ -56,6 +57,11 @@ describe('#hasMessageFailedWithExternalError', () => {
|
||||
});
|
||||
|
||||
describe('#actions', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('#getConversation', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
|
||||
@@ -40,6 +40,20 @@ export const useCallsStore = defineStore('calls', {
|
||||
this.calls = this.calls.filter(c => c.callSid !== callSid);
|
||||
},
|
||||
|
||||
removeCallsForConversation(conversationId) {
|
||||
const callsToRemove = this.calls.filter(
|
||||
call => call.conversationId === conversationId
|
||||
);
|
||||
|
||||
if (callsToRemove.some(call => call.isActive)) {
|
||||
TwilioVoiceClient.endClientCall();
|
||||
}
|
||||
|
||||
this.calls = this.calls.filter(
|
||||
call => call.conversationId !== conversationId
|
||||
);
|
||||
},
|
||||
|
||||
setCallActive(callSid) {
|
||||
this.calls = this.calls.map(call => ({
|
||||
...call,
|
||||
|
||||
Reference in New Issue
Block a user