chore: code cleanup

This commit is contained in:
Sojan
2025-05-03 02:09:34 -07:00
parent 3692cde1a9
commit 4c48a565f6
16 changed files with 1143 additions and 581 deletions
@@ -9,6 +9,7 @@ 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';
@@ -43,6 +44,7 @@ export default {
InstagramStoryReply,
Spinner,
NextButton,
VoiceCall,
},
props: {
data: {
@@ -111,7 +113,8 @@ export default {
this.data.content ||
this.isEmailContentType ||
this.isUnsupported ||
this.isAnIntegrationMessage
this.isAnIntegrationMessage ||
this.isVoiceCall
);
},
emailMessageContent() {
@@ -258,6 +261,9 @@ export default {
isAnIntegrationMessage() {
return this.contentType === 'integrations';
},
isVoiceCall() {
return this.contentType === 'voice_call';
},
emailHeadAttributes() {
return {
email: this.contentAttributes.email,
@@ -490,12 +496,17 @@ export default {
</template>
</div>
<BubbleText
v-else-if="data.content"
v-else-if="data.content && !isVoiceCall"
: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"
:content-attributes="contentAttributes"
:inbox-id="data.inbox_id"
@@ -0,0 +1,321 @@
<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>
@@ -28,6 +28,12 @@ export default {
return this.$store.getters['inboxes/getInbox'](this.inboxId);
},
},
mounted() {
// Log integration type for debugging if needed
if (process.env.NODE_ENV !== 'production') {
console.log('Integration component mounted with type:', this.contentAttributes.type);
}
},
};
</script>
@@ -38,4 +44,13 @@ export default {
:message-id="messageId"
:meeting-data="contentAttributes.data"
/>
<div v-else class="integration-not-supported">
{{ contentAttributes.type || 'Unknown' }} integration
</div>
</template>
<style lang="scss" scoped>
.integration-not-supported {
@apply text-xs text-slate-500 dark:text-slate-400 p-2 bg-slate-50 dark:bg-slate-700 rounded;
}
</style>
@@ -234,8 +234,13 @@ class ActionCableConnector extends BaseActionCableConnector {
inboxId: data.inbox_id,
};
// Update store
this.app.$store.dispatch('calls/setActiveCall', normalizedPayload);
// Update store with call status change
this.app.$store.dispatch('calls/handleCallStatusChanged', normalizedPayload);
// For non-terminal statuses, update the active call
if (!['ended', 'missed', 'completed'].includes(data.status)) {
this.app.$store.dispatch('calls/setActiveCall', normalizedPayload);
}
};
}
@@ -11,10 +11,29 @@ const getters = {
};
const actions = {
setActiveCall({ commit }, callData) {
// This action will handle both message updates and direct call status changes
handleCallStatusChanged({ state, dispatch }, { callSid, status }) {
// If this is the active call and it has ended or was missed, close the widget
if (callSid === state.activeCall?.callSid &&
(status === 'ended' || status === 'missed' || status === 'completed')) {
dispatch('clearActiveCall');
}
},
setActiveCall({ commit, dispatch, state }, callData) {
if (!callData || !callData.callSid) {
throw new Error('Invalid call data provided');
}
// If the call has a status, check if it's a terminal status
if (callData.status && ['ended', 'missed', 'completed'].includes(callData.status)) {
// If the call is already in a terminal state, clear any active call
if (callData.callSid === state.activeCall?.callSid) {
return dispatch('clearActiveCall');
}
// Otherwise just ignore it - don't set an already ended call as active
return;
}
commit('SET_ACTIVE_CALL', callData);
@@ -25,12 +44,18 @@ const actions = {
},
clearActiveCall({ commit }) {
// Store the messageId before clearing the call
const messageId = state.activeCall?.messageId;
commit('CLEAR_ACTIVE_CALL');
// Update app state if in browser environment
if (typeof window !== 'undefined' && window.app?.$data) {
window.app.$data.showCallWidget = false;
}
// We no longer need to update call widget status as we'll use reactive Vue props
// and updates will come through Chatwoot's standard message update events
},
setIncomingCall({ commit, state }, callData) {
@@ -48,16 +73,30 @@ const actions = {
return;
}
commit('SET_INCOMING_CALL', callData);
const enrichedCallData = {
...callData,
receivedAt: Date.now(),
};
commit('SET_INCOMING_CALL', enrichedCallData);
// Update app state if in browser environment
if (typeof window !== 'undefined' && window.app && window.app.$data) {
window.app.$data.showCallWidget = true;
}
// We no longer need to update call widget status as we'll use reactive Vue props
// and updates will come through Chatwoot's standard message update events
},
clearIncomingCall({ commit }) {
// Store the messageId before clearing the call
const messageId = state.incomingCall?.messageId;
commit('CLEAR_INCOMING_CALL');
// We no longer need to update call widget status as we'll use reactive Vue props
// and updates will come through Chatwoot's standard message update events
},
acceptIncomingCall({ commit, state }) {
@@ -70,8 +109,12 @@ const actions = {
commit('SET_ACTIVE_CALL', {
...incomingCall,
isJoined: true,
startedAt: Date.now(),
});
commit('CLEAR_INCOMING_CALL');
// We no longer need to update call widget status as we'll use reactive Vue props
// and updates will come through Chatwoot's standard message update events
},
};
@@ -88,6 +131,9 @@ const mutations = {
CLEAR_INCOMING_CALL($state) {
$state.incomingCall = null;
},
// We no longer need to update call widget status as we'll use reactive Vue props
// We no longer need subscription mutations
};
export default {