feat(voice): WhatsApp Cloud Calling — UI

This commit is contained in:
Tanmay Deep Sharma
2026-05-02 16:06:48 +07:00
parent 442631102c
commit 6835f9c85b
15 changed files with 828 additions and 24 deletions
@@ -5,6 +5,7 @@ import { VOICE_CALL_STATUS } from '../constants';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import BaseBubble from 'next/message/bubbles/Base.vue';
import AudioChip from 'dashboard/components-next/message/chips/Audio.vue';
const LABEL_MAP = {
[VOICE_CALL_STATUS.IN_PROGRESS]: 'CONVERSATION.VOICE_CALL.CALL_IN_PROGRESS',
@@ -30,7 +31,7 @@ const BG_COLOR_MAP = {
[VOICE_CALL_STATUS.FAILED]: 'bg-n-ruby-9',
};
const { call } = useMessageContext();
const { call, attachments, contentAttributes } = useMessageContext();
const status = computed(() => call.value?.status);
const isOutbound = computed(() => call.value?.direction === 'outgoing');
@@ -38,6 +39,30 @@ const isFailed = computed(() =>
[VOICE_CALL_STATUS.NO_ANSWER, VOICE_CALL_STATUS.FAILED].includes(status.value)
);
const audioAttachment = computed(() =>
(attachments?.value || []).find(a => a.fileType === 'audio')
);
// Duration lives in two places depending on which payload the FE got:
// - call.duration_seconds / call.durationSeconds (push_event_data shape)
// - content_attributes.data.duration_seconds (message-side mirror)
// Both can be camelCased by useTransformKeys upstream — check every variant.
const durationSeconds = computed(() => {
const fromCall = call.value?.durationSeconds || call.value?.duration_seconds;
if (fromCall != null) return fromCall;
const data = contentAttributes?.value?.data || contentAttributes?.value?.data;
return data?.durationSeconds || data?.duration_seconds;
});
const formattedDuration = computed(() => {
const s = Number(durationSeconds.value);
if (!s || Number.isNaN(s)) return '';
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
});
const labelKey = computed(() => {
if (LABEL_MAP[status.value]) return LABEL_MAP[status.value];
if (status.value === VOICE_CALL_STATUS.RINGING) {
@@ -93,10 +118,19 @@ const bgColor = computed(() => BG_COLOR_MAP[status.value] || 'bg-n-teal-9');
{{ $t(labelKey) }}
</span>
<span class="text-xs text-n-slate-11">
{{ $t(subtextKey) }}
<!-- When the audio chip is rendered it already shows duration in
its own player; suppress here to avoid two competing numbers. -->
{{
audioAttachment
? $t(subtextKey)
: formattedDuration || $t(subtextKey)
}}
</span>
</div>
</div>
<div v-if="audioAttachment" class="px-3 pb-3 w-full">
<AudioChip :attachment="audioAttachment" class="text-n-slate-12" />
</div>
</div>
</BaseBubble>
</template>