chore: clean up voice message components

This commit is contained in:
Sojan
2025-05-04 05:00:20 -07:00
parent 196d7afccf
commit 4348c4ab87
27 changed files with 2652 additions and 985 deletions

View File

@@ -61,15 +61,15 @@ export default {
// Define local fallback translations in case i18n fails
const translations = {
'CONVERSATION.END_CALL': 'End call',
'CONVERSATION.JOIN_CALL': 'Join call',
'CONVERSATION.REJECT_CALL': 'Reject',
'CONVERSATION.CALL_ENDED': 'Call ended',
'CONVERSATION.CALL_END_ERROR': 'Failed to end call',
'CONVERSATION.CALL_ACCEPTED': 'Joining call...',
'CONVERSATION.CALL_REJECTED': 'Call rejected',
'CONVERSATION.CALL_JOIN_ERROR': 'Failed to join call',
'CONVERSATION.INCOMING_CALL': 'Incoming call',
'CONVERSATION.VOICE_CALL.END_CALL': 'End call',
'CONVERSATION.VOICE_CALL.JOIN_CALL': 'Join call',
'CONVERSATION.VOICE_CALL.REJECT_CALL': 'Reject',
'CONVERSATION.VOICE_CALL.CALL_ENDED': 'Call ended',
'CONVERSATION.VOICE_CALL.CALL_END_ERROR': 'Failed to end call',
'CONVERSATION.VOICE_CALL.CALL_ACCEPTED': 'Joining call...',
'CONVERSATION.VOICE_CALL.CALL_REJECTED': 'Call rejected',
'CONVERSATION.VOICE_CALL.CALL_JOIN_ERROR': 'Failed to join call',
'CONVERSATION.VOICE_CALL.INCOMING_CALL': 'Incoming call',
};
// Computed properties
@@ -402,7 +402,7 @@ export default {
const { callSid, conversationId } = incomingCall.value;
// Show user feedback
useAlert(safeTranslate('CONVERSATION.CALL_REJECTED'));
useAlert(safeTranslate('CONVERSATION.VOICE_CALL.CALL_REJECTED'));
// Make API call to reject the call (optional, the caller will stay in the queue)
await VoiceAPI.rejectCall(callSid, conversationId);
@@ -1602,12 +1602,12 @@ export default {
{{ displayContactName }}
</h3>
<div class="call-subtitle">
{{ isIncoming ? $t('CONVERSATION.INCOMING_CALL') : (callInfo.inboxName || 'Voice Call') }}
{{ isIncoming ? $t('CONVERSATION.VOICE_CALL.INCOMING_CALL') : $t('CONVERSATION.VOICE_CALL.OUTGOING_CALL') }}
</div>
</div>
</div>
<div class="call-duration" v-if="!isIncoming">
{{ formattedCallDuration }}
<span class="call-duration-label">{{ formattedCallDuration }}</span>
</div>
</div>
</div>
@@ -1617,27 +1617,27 @@ export default {
v-if="isIncoming"
class="control-button accept-call-button"
@click="acceptCall"
:title="$t('CONVERSATION.JOIN_CALL')"
:title="$t('CONVERSATION.VOICE_CALL.JOIN_CALL')"
>
<span class="i-ph-phone" />
<span class="button-text">{{ $t('CONVERSATION.JOIN_CALL') }}</span>
<span class="button-text">{{ $t('CONVERSATION.VOICE_CALL.JOIN_CALL') }}</span>
</button>
<button
v-if="isIncoming"
class="control-button reject-call-button"
@click="rejectCall"
:title="$t('CONVERSATION.REJECT_CALL')"
:title="$t('CONVERSATION.VOICE_CALL.REJECT_CALL')"
>
<span class="i-ph-phone-x" />
<span class="button-text">{{ $t('CONVERSATION.REJECT_CALL') }}</span>
<span class="button-text">{{ $t('CONVERSATION.VOICE_CALL.REJECT_CALL') }}</span>
</button>
<button
v-if="!isIncoming"
class="control-button end-call-button"
@click="handleEndCallClick"
:title="$t('CONVERSATION.END_CALL')"
:title="$t('CONVERSATION.VOICE_CALL.END_CALL')"
>
<span class="i-ph-phone-x" />
</button>

View File

@@ -9,7 +9,6 @@ import BubbleLocation from './bubble/Location.vue';
import BubbleMailHead from './bubble/MailHead.vue';
import BubbleReplyTo from './bubble/ReplyTo.vue';
import BubbleText from './bubble/Text.vue';
import VoiceCall from './VoiceCall.vue';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
import InstagramStory from './bubble/InstagramStory.vue';
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
@@ -44,7 +43,6 @@ export default {
InstagramStoryReply,
Spinner,
NextButton,
VoiceCall,
},
props: {
data: {
@@ -496,15 +494,11 @@ export default {
</template>
</div>
<BubbleText
v-else-if="data.content && !isVoiceCall"
v-else-if="data.content"
:message="message"
:is-email="isEmailContentType"
:display-quoted-button="displayQuotedButton"
/>
<VoiceCall
v-else-if="isVoiceCall"
:message="data"
/>
<BubbleIntegration
v-else-if="isAnIntegrationMessage"
:message-id="data.id"

View File

@@ -1,321 +0,0 @@
<template>
<div class="voice-call-widget" :class="statusClass">
<div class="status-icon">
<span :class="statusIcon"></span>
</div>
<div class="call-info">
<div class="call-status">{{ callStatusText }}</div>
<div v-if="statusSubtext" class="call-subtext">{{ statusSubtext }}</div>
</div>
<NextButton
v-if="showJoinButton"
size="sm"
icon="i-lucide-phone"
variant="success"
:label="$t('CONVERSATION.VOICE_CALL.JOIN_CALL')"
:is-loading="isLoading"
@click="joinCall"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import VoiceAPI from 'dashboard/api/channels/voice';
import { useAlert } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
message: {
type: Object,
required: true,
},
},
data() {
return {
isLoading: false,
status: this.message.content_attributes?.data?.status || 'ringing',
joinedAt: null,
duration: this.message.content_attributes?.data?.duration || null,
};
},
watch: {
// Watch for changes to message content_attributes to update call status
'message.content_attributes.data': {
handler(newData) {
if (newData) {
if (newData.status && newData.status !== this.status) {
const oldStatus = this.status;
this.status = newData.status;
// If call status changes to 'ended' or 'missed', close the floating call widget
if ((newData.status === 'ended' || newData.status === 'missed') &&
['ringing', 'active'].includes(oldStatus)) {
this.closeCallWidget();
}
}
if (newData.duration && newData.duration !== this.duration) {
this.duration = newData.duration;
}
}
},
deep: true,
},
},
computed: {
...mapGetters({
currentConversationId: 'getSelectedChatConversationId',
}),
hasActiveCall() {
return this.$store.getters['calls/hasActiveCall'];
},
callData() {
return this.message.content_attributes?.data || {};
},
callStatusText() {
switch (this.status) {
case 'ringing':
return this.$t('CONVERSATION.VOICE_CALL.RINGING');
case 'active':
return this.$t('CONVERSATION.VOICE_CALL.ACTIVE');
case 'missed':
return this.$t('CONVERSATION.VOICE_CALL.MISSED');
case 'ended':
return this.$t('CONVERSATION.VOICE_CALL.ENDED');
default:
return this.$t('CONVERSATION.VOICE_CALL.INCOMING_CALL');
}
},
statusIcon() {
switch (this.status) {
case 'ringing':
return 'i-lucide-phone-incoming';
case 'active':
return 'i-lucide-phone';
case 'missed':
return 'i-lucide-phone-missed';
case 'ended':
return 'i-lucide-phone-off';
default:
return 'i-lucide-phone-incoming';
}
},
statusClass() {
return {
'ringing': this.status === 'ringing',
'active': this.status === 'active',
'missed': this.status === 'missed',
'ended': this.status === 'ended',
};
},
callConversationId() {
return this.callData.conversation_id;
},
showJoinButton() {
// Show join button only if the call is ringing or active and we're on the same conversation
return (['ringing', 'active'].includes(this.status)) &&
(this.callConversationId === this.currentConversationId);
},
formattedDuration() {
if (!this.duration) return '';
const minutes = Math.floor(this.duration / 60);
const seconds = this.duration % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
},
statusSubtext() {
if (this.status === 'ended' && this.formattedDuration) {
return this.$t('CONVERSATION.VOICE_CALL.DURATION', { duration: this.formattedDuration });
}
if (this.status === 'missed') {
return this.$t('CONVERSATION.VOICE_CALL.MISSED_CALL');
}
return '';
},
isIncoming() {
return this.message.message_type === 0; // 0 = incoming
},
isOutgoing() {
return this.message.message_type === 1; // 1 = outgoing
},
senderText() {
if (this.isIncoming) {
return this.$t('CONVERSATION.VOICE_CALL.INCOMING_FROM', { name: this.message.sender?.name || 'Unknown' });
}
if (this.isOutgoing) {
return this.$t('CONVERSATION.VOICE_CALL.OUTGOING_FROM', { name: this.message.sender?.name || 'Unknown' });
}
return '';
},
},
mounted() {
// We don't need custom subscriptions anymore as we'll
// get updates through Chatwoot's standard message update system
// Just check if we have an active call with the same call_sid
this.checkActiveCall();
},
methods: {
async joinCall() {
this.isLoading = true;
try {
// Update UI immediately
this.status = 'active';
this.joinedAt = new Date();
// Get the current conversation
const conversation = await this.$store.dispatch('getConversation', {
conversationId: this.callConversationId,
});
const callSid = this.callData.call_sid;
if (!callSid) {
throw new Error('No call SID found');
}
// Show floating call widget
this.$store.dispatch('calls/setActiveCall', {
callSid,
conversationId: this.callConversationId,
inboxId: this.message.inbox_id,
contactId: conversation.contact_id,
contactName: conversation.contact?.name || 'Unknown',
messageId: this.message.id,
});
// Join the call via API
await VoiceAPI.joinCall({
call_sid: callSid,
conversation_id: this.callConversationId,
});
// Success notification
useAlert(this.$t('CONVERSATION.VOICE_CALL.CALL_JOINED'));
} catch (err) {
console.error('Failed to join call:', err);
useAlert(this.$t('CONVERSATION.VOICE_CALL.JOIN_ERROR'));
// Reset status if join failed
this.status = 'ringing';
} finally {
this.isLoading = false;
}
},
// We don't need custom subscription methods anymore as we'll use
// Chatwoot's standard message update events
checkActiveCall() {
// If there's an active call in the store with the same conversation ID
// update our local state to match
const activeCall = this.$store.getters['calls/getActiveCall'];
if (activeCall && activeCall.conversationId === this.callConversationId) {
this.status = 'active';
this.joinedAt = new Date();
}
},
updateStatus(newStatus, duration = null) {
this.status = newStatus;
if (newStatus === 'ended') {
if (duration) {
this.duration = duration;
} else if (this.joinedAt) {
this.duration = Math.floor((new Date() - this.joinedAt) / 1000);
}
// Close the floating call widget when call ends
this.closeCallWidget();
}
},
closeCallWidget() {
// Get the call SID from the message data
const callSid = this.callData.call_sid;
if (!callSid) return;
// Handle the call status change directly
this.$store.dispatch('calls/handleCallStatusChanged', {
callSid,
status: 'ended'
});
},
},
};
</script>
<style lang="scss" scoped>
.voice-call-widget {
@apply flex items-center gap-2 p-3 rounded-lg my-1;
@apply bg-slate-50 dark:bg-slate-700;
@apply border border-slate-200 dark:border-slate-600;
@apply transition-all duration-200;
&.ringing {
@apply border-green-500 dark:border-green-500;
animation: pulse 1.5s infinite;
}
&.active {
@apply border-woot-500 dark:border-woot-500 bg-woot-50 dark:bg-woot-800;
}
&.missed {
@apply border-red-300 dark:border-slate-600 bg-red-50 dark:bg-slate-700;
}
&.ended {
@apply border-slate-300 dark:border-slate-600;
}
.status-icon {
@apply flex items-center justify-center;
@apply w-10 h-10 rounded-full;
@apply bg-slate-200 dark:bg-slate-600;
@apply text-slate-700 dark:text-slate-200;
.i-lucide-phone-incoming {
@apply text-green-600 dark:text-green-400;
}
.i-lucide-phone {
@apply text-woot-600 dark:text-woot-400;
}
.i-lucide-phone-missed {
@apply text-red-600 dark:text-red-400;
}
.i-lucide-phone-off {
@apply text-slate-600 dark:text-slate-400;
}
}
.call-info {
@apply flex-1;
.call-status {
@apply font-medium text-slate-800 dark:text-slate-200;
}
.call-subtext {
@apply text-xs text-slate-500 dark:text-slate-400;
}
}
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.3);
}
70% {
box-shadow: 0 0 0 8px rgba(16, 185, 129, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
</style>