refactor(voice): read call state from message.call on the frontend

- VoiceCall bubble, voice.js helper, ConversationCard, and the store
  mutation all switch from message.content_attributes.data to message.call.
- Message.vue gains a `call` prop that's provided through
  useMessageContext for the bubble.
- UPDATE_MESSAGE_CALL_STATUS mutation now updates message.call.status,
  matched by provider_call_id.
- content_attributes.data is still emitted by the backend for this
  commit; backend-side trim lands in a follow-up.
This commit is contained in:
Muhsin
2026-04-20 13:36:26 +04:00
parent 5ce77eedbe
commit c2a851c756
6 changed files with 59 additions and 39 deletions
@@ -113,6 +113,7 @@ const props = defineProps({
validator: value => Object.values(MESSAGE_STATUS).includes(value),
},
attachments: { type: Array, default: () => [] },
call: { type: Object, default: null }, // eslint-disable-line vue/no-unused-properties
content: { type: String, default: null },
contentAttributes: { type: Object, default: () => ({}) },
contentType: {
@@ -1,7 +1,7 @@
<script setup>
import { computed } from 'vue';
import { useMessageContext } from '../provider.js';
import { MESSAGE_TYPES, VOICE_CALL_STATUS } from '../constants';
import { VOICE_CALL_STATUS } from '../constants';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import BaseBubble from 'next/message/bubbles/Base.vue';
@@ -30,12 +30,10 @@ const BG_COLOR_MAP = {
[VOICE_CALL_STATUS.FAILED]: 'bg-n-ruby-9',
};
const { contentAttributes, messageType } = useMessageContext();
const { call } = useMessageContext();
const data = computed(() => contentAttributes.value?.data);
const status = computed(() => data.value?.status?.toString());
const isOutbound = computed(() => messageType.value === MESSAGE_TYPES.OUTGOING);
const status = computed(() => call.value?.status);
const isOutbound = computed(() => call.value?.direction === 'outgoing');
const isFailed = computed(() =>
[VOICE_CALL_STATUS.NO_ANSWER, VOICE_CALL_STATUS.FAILED].includes(status.value)
);
@@ -97,10 +97,13 @@ const lastMessageInChat = computed(() => getLastMessage(props.chat));
const voiceCallData = computed(() => {
const last = lastMessageInChat.value;
if (last?.content_type !== 'voice_call')
if (last?.content_type !== 'voice_call' || !last.call) {
return { status: null, direction: null };
const data = last.content_attributes?.data ?? {};
return { status: data.status, direction: data.call_direction };
}
return {
status: last.call.status,
direction: last.call.direction === 'outgoing' ? 'outbound' : 'inbound',
};
});
const inboxId = computed(() => props.chat.inbox_id);
+4 -4
View File
@@ -23,11 +23,11 @@ const shouldSkipCall = (callDirection, senderId, currentUserId) => {
};
function extractCallData(message) {
const contentData = message?.content_attributes?.data || {};
const call = message?.call || {};
return {
callSid: contentData.call_sid,
status: contentData.status,
callDirection: contentData.call_direction,
callSid: call.provider_call_id,
status: call.status,
callDirection: call.direction === 'outgoing' ? 'outbound' : 'inbound',
conversationId: message?.conversation_id,
senderId: message?.sender?.id,
};
@@ -317,15 +317,11 @@ export const mutations = {
const message = (chat.messages || []).find(
m =>
m.content_type === CONTENT_TYPES.VOICE_CALL &&
m.content_attributes?.data?.call_sid === callSid
m.call?.provider_call_id === callSid
);
if (!message) return;
if (!message?.call) return;
message.content_attributes ??= {};
message.content_attributes.data = {
...message.content_attributes.data,
status: callStatus,
};
message.call = { ...message.call, status: callStatus };
},
[types.SET_ACTIVE_INBOX](_state, inboxId) {
@@ -39,16 +39,12 @@ describe('#mutations', () => {
{
id: 1,
content_type: 'voice_call',
content_attributes: {
data: { call_sid: 'CA111', status: 'ringing' },
},
call: { provider_call_id: 'CA111', status: 'ringing' },
},
{
id: 2,
content_type: 'voice_call',
content_attributes: {
data: { call_sid: 'CA222', status: 'ringing' },
},
call: { provider_call_id: 'CA222', status: 'ringing' },
},
],
},
@@ -59,15 +55,13 @@ describe('#mutations', () => {
callStatus: 'in-progress',
callSid: 'CA111',
});
expect(
state.allConversations[0].messages[0].content_attributes.data.status
).toBe('in-progress');
expect(
state.allConversations[0].messages[1].content_attributes.data.status
).toBe('ringing');
expect(state.allConversations[0].messages[0].call.status).toBe(
'in-progress'
);
expect(state.allConversations[0].messages[1].call.status).toBe('ringing');
});
it('preserves existing data in content_attributes.data', () => {
it('preserves existing call fields when updating status', () => {
const state = {
allConversations: [
{
@@ -76,8 +70,11 @@ describe('#mutations', () => {
{
id: 1,
content_type: 'voice_call',
content_attributes: {
data: { call_sid: 'CA123', status: 'ringing' },
call: {
provider_call_id: 'CA123',
status: 'ringing',
direction: 'incoming',
duration_seconds: null,
},
},
],
@@ -89,11 +86,11 @@ describe('#mutations', () => {
callStatus: 'in-progress',
callSid: 'CA123',
});
expect(
state.allConversations[0].messages[0].content_attributes.data
).toEqual({
call_sid: 'CA123',
expect(state.allConversations[0].messages[0].call).toEqual({
provider_call_id: 'CA123',
status: 'in-progress',
direction: 'incoming',
duration_seconds: null,
});
});
@@ -108,5 +105,30 @@ describe('#mutations', () => {
});
expect(state.allConversations[0].messages).toEqual([]);
});
it('does nothing if matching message has no call object yet', () => {
const state = {
allConversations: [
{
id: 1,
messages: [
{
id: 1,
content_type: 'voice_call',
call: { provider_call_id: 'CA-OTHER' },
},
],
},
],
};
mutations[types.UPDATE_MESSAGE_CALL_STATUS](state, {
conversationId: 1,
callStatus: 'completed',
callSid: 'CA-MISSING',
});
expect(state.allConversations[0].messages[0].call).toEqual({
provider_call_id: 'CA-OTHER',
});
});
});
});