feat(voice): wire Twilio voice flow through unified Call model
Moves call state off conversation.additional_attributes and
conversation.identifier onto first-class Call records, one per call.
- Call is the source of truth for status, direction, duration, started_at,
accepted_by_agent, and conference_sid (in meta).
- Conference names key off Call.id (conf_account_{aid}_call_{cid}), so
multiple calls on one conversation no longer collide. lock_to_single_conversation
inboxes append each call as a new voice_call bubble in the existing thread.
- Agent identity on conference join webhooks is parsed from the Twilio
ParticipantLabel (agent-{user_id}-account-{account_id}); stateless and
independent of the /conference#create API call order.
- ConversationCard.vue derives the call badge from the latest voice_call
message, not a denormalized cache on the conversation. Removes the stale
"Call ended" state that lingered after subsequent text messages.
This commit is contained in:
@@ -95,10 +95,13 @@ const isInboxNameVisible = computed(() => !activeInbox.value);
|
||||
|
||||
const lastMessageInChat = computed(() => getLastMessage(props.chat));
|
||||
|
||||
const voiceCallData = computed(() => ({
|
||||
status: props.chat.additional_attributes?.call_status,
|
||||
direction: props.chat.additional_attributes?.call_direction,
|
||||
}));
|
||||
const voiceCallData = computed(() => {
|
||||
const last = lastMessageInChat.value;
|
||||
if (last?.content_type !== 'voice_call')
|
||||
return { status: null, direction: null };
|
||||
const data = last.content_attributes?.data ?? {};
|
||||
return { status: data.status, direction: data.call_direction };
|
||||
});
|
||||
|
||||
const inboxId = computed(() => props.chat.inbox_id);
|
||||
|
||||
|
||||
@@ -60,9 +60,10 @@ export function handleVoiceCallUpdated(commit, message, currentUserId) {
|
||||
|
||||
callsStore.handleCallStatusChanged({ callSid, status, conversationId });
|
||||
|
||||
const callInfo = { conversationId, callStatus: status };
|
||||
commit(types.UPDATE_CONVERSATION_CALL_STATUS, callInfo);
|
||||
commit(types.UPDATE_MESSAGE_CALL_STATUS, callInfo);
|
||||
commit(types.UPDATE_MESSAGE_CALL_STATUS, {
|
||||
conversationId,
|
||||
callStatus: status,
|
||||
});
|
||||
|
||||
const isNewCall =
|
||||
status === 'ringing' &&
|
||||
|
||||
@@ -307,19 +307,6 @@ export const mutations = {
|
||||
}
|
||||
},
|
||||
|
||||
[types.UPDATE_CONVERSATION_CALL_STATUS](
|
||||
_state,
|
||||
{ conversationId, callStatus }
|
||||
) {
|
||||
const chat = getConversationById(_state)(conversationId);
|
||||
if (!chat) return;
|
||||
|
||||
chat.additional_attributes = {
|
||||
...chat.additional_attributes,
|
||||
call_status: callStatus,
|
||||
};
|
||||
},
|
||||
|
||||
[types.UPDATE_MESSAGE_CALL_STATUS](_state, { conversationId, callStatus }) {
|
||||
const chat = getConversationById(_state)(conversationId);
|
||||
if (!chat) return;
|
||||
|
||||
@@ -2,44 +2,6 @@ import { mutations } from '../index';
|
||||
import types from '../../../mutation-types';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#UPDATE_CONVERSATION_CALL_STATUS', () => {
|
||||
it('does nothing if conversation is not found', () => {
|
||||
const state = { allConversations: [] };
|
||||
mutations[types.UPDATE_CONVERSATION_CALL_STATUS](state, {
|
||||
conversationId: 1,
|
||||
callStatus: 'ringing',
|
||||
});
|
||||
expect(state.allConversations).toEqual([]);
|
||||
});
|
||||
|
||||
it('updates call_status preserving existing additional_attributes', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{ id: 1, additional_attributes: { other_attr: 'value' } },
|
||||
],
|
||||
};
|
||||
mutations[types.UPDATE_CONVERSATION_CALL_STATUS](state, {
|
||||
conversationId: 1,
|
||||
callStatus: 'in-progress',
|
||||
});
|
||||
expect(state.allConversations[0].additional_attributes).toEqual({
|
||||
other_attr: 'value',
|
||||
call_status: 'in-progress',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates additional_attributes if it does not exist', () => {
|
||||
const state = { allConversations: [{ id: 1 }] };
|
||||
mutations[types.UPDATE_CONVERSATION_CALL_STATUS](state, {
|
||||
conversationId: 1,
|
||||
callStatus: 'completed',
|
||||
});
|
||||
expect(state.allConversations[0].additional_attributes).toEqual({
|
||||
call_status: 'completed',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_MESSAGE_CALL_STATUS', () => {
|
||||
it('does nothing if conversation is not found', () => {
|
||||
const state = { allConversations: [] };
|
||||
|
||||
@@ -51,7 +51,6 @@ export default {
|
||||
UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES:
|
||||
'UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES',
|
||||
UPDATE_CONVERSATION_LAST_ACTIVITY: 'UPDATE_CONVERSATION_LAST_ACTIVITY',
|
||||
UPDATE_CONVERSATION_CALL_STATUS: 'UPDATE_CONVERSATION_CALL_STATUS',
|
||||
UPDATE_MESSAGE_CALL_STATUS: 'UPDATE_MESSAGE_CALL_STATUS',
|
||||
SET_MISSING_MESSAGES: 'SET_MISSING_MESSAGES',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user