From c9daf7065590acbc9511bd75322cf91e51f0e264 Mon Sep 17 00:00:00 2001 From: Sojan Date: Sun, 4 May 2025 06:02:51 -0700 Subject: [PATCH] chore: message previews in conversation list --- .../ConversationCard/CardMessagePreview.vue | 249 +++++------------- .../CardMessagePreviewWithMeta.vue | 220 +++++----------- .../ConversationCard/ConversationCard.vue | 247 +++++++++++++---- .../widgets/conversation/ConversationCard.vue | 1 + .../widgets/conversation/MessagePreview.vue | 154 ++++++++++- .../composables/useVoiceCallHelpers.js | 33 ++- 6 files changed, 521 insertions(+), 383 deletions(-) diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue index 560d471b4..5224c04b1 100644 --- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue +++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue @@ -2,7 +2,6 @@ import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; -import { useVoiceCallHelpers } from 'dashboard/composables/useVoiceCallHelpers'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; @@ -16,197 +15,84 @@ const props = defineProps({ const { t } = useI18n(); const { getPlainText } = useMessageFormatter(); -// Use our shared voice call helper -const { - isVoiceChannelConversation, - hasArrow: checkHasArrow, - isIncomingCall: checkIsIncoming, - normalizeCallStatus, - getCallIconName, - getStatusText, - processArrowContent, -} = useVoiceCallHelpers(props, { t }); - -// Utility function to find the last voice call message in conversation -const findVoiceCallMessage = (conversation) => { - // If conversation has messages property, look for voice call messages - if (conversation && conversation.messages && Array.isArray(conversation.messages)) { - // Look through messages in reverse to find the latest voice call - for (let i = conversation.messages.length - 1; i >= 0; i--) { - const msg = conversation.messages[i]; - if (msg.content_type === 'voice_call' || msg.content_type === 'voice') { - return msg; - } - } - } - - // If no voice call found in messages or messages not available, check lastNonActivityMessage - const { lastNonActivityMessage } = conversation || {}; - - if (lastNonActivityMessage?.content_type === 'voice_call' || - lastNonActivityMessage?.content_type === 'voice') { - return lastNonActivityMessage; - } - - // Check if conversation has a call_status in additional_attributes - // This is a strong indicator of a voice call conversation - if (conversation?.additional_attributes?.call_status) { - // If we have a call status but no voice call message, the lastNonActivityMessage - // might still be related to the call (even if it doesn't have the right content_type) - if (lastNonActivityMessage) { - return lastNonActivityMessage; - } - } - - // As a fallback, check if last message content includes "Voice Call" or common call-related terms - if (lastNonActivityMessage?.content && - typeof lastNonActivityMessage.content === 'string') { - const content = lastNonActivityMessage.content.toLowerCase(); - if (content.includes('voice call') || - content.includes('call ') || - content.includes('missed call') || - content.includes('incoming call') || - content.includes('outgoing call') || - content.startsWith('←') || - content.startsWith('→')) { - return lastNonActivityMessage; - } - } - - return null; -}; - -const voiceCallMessage = computed(() => { - return findVoiceCallMessage(props.conversation); -}); - -const isVoiceCall = computed(() => { - // Force voice call view for voice channel conversations - if (isVoiceChannelConversation.value) { - return true; - } - - // Check if conversation has a call_status in additional_attributes - if (props.conversation?.additional_attributes?.call_status) { - return true; - } - - // Check for voice call message - return !!voiceCallMessage.value; -}); - -const callData = computed(() => { - if (!isVoiceCall.value) return {}; - - // First check for data directly in conversation attributes - const conversationAttributes = props.conversation?.custom_attributes || - props.conversation?.additional_attributes || {}; - if (conversationAttributes.call_data) { - return conversationAttributes.call_data; - } - - // Then check message content attributes - if (voiceCallMessage.value?.content_attributes?.data) { - return voiceCallMessage.value.content_attributes.data; - } - - return {}; -}); - -const hasArrow = computed(() => { - return checkHasArrow(voiceCallMessage.value); +// Simple check: Is this a voice channel conversation? +const isVoiceChannel = computed(() => { + return props.conversation?.meta?.inbox?.channel_type === 'Channel::Voice'; }); +// Get call direction: inbound or outbound const isIncomingCall = computed(() => { - if (!isVoiceCall.value) return null; + if (!isVoiceChannel.value) return false; - // Get the conversation call_status - const conversationCallStatus = props.conversation?.additional_attributes?.call_status; - - return checkIsIncoming(callData.value, voiceCallMessage.value); + const direction = props.conversation?.additional_attributes?.call_direction; + return direction === 'inbound'; }); +// Simple function to normalize call status const normalizedCallStatus = computed(() => { - if (!isVoiceCall.value) return ''; + if (!isVoiceChannel.value) return null; - // First check for direct call_status in the conversation additional_attributes - // This is the most authoritative source for call status - const conversationCallStatus = props.conversation?.additional_attributes?.call_status; - if (conversationCallStatus) { - return normalizeCallStatus(conversationCallStatus, isIncomingCall.value); - } + // Get the raw status directly from conversation + const status = props.conversation?.additional_attributes?.call_status; - // If there's an arrow in the message, this is a legacy format message - if (hasArrow.value) { - const content = voiceCallMessage.value?.content || ''; - if (content.includes('ended') || content.includes('Call ended')) { - return 'ended'; - } - if (content.includes('missed') || content.includes('Missed call') || content.includes('no answer')) { - return isIncomingCall.value ? 'missed' : 'no-answer'; - } - if (content.includes('in progress') || content.includes('active') || content.includes('answered')) { - return 'active'; - } - - // For voice channel conversations, default to ended for better display - if (isVoiceChannelConversation.value) { - return 'ended'; - } - - // Default to ended for legacy messages - return 'ended'; - } + // Simple mapping of call statuses + if (status === 'in-progress') return 'active'; + if (status === 'completed') return 'ended'; + if (status === 'canceled') return 'ended'; + if (status === 'failed') return 'ended'; + if (status === 'busy') return 'no-answer'; + if (status === 'no-answer') return isIncomingCall.value ? 'missed' : 'no-answer'; - // Apply the same status mapping logic as VoiceCall component - const callStatus = callData.value?.status; - if (callStatus) { - return normalizeCallStatus(callStatus, isIncomingCall.value); - } + // Return the status as is for explicit values + if (status === 'active') return 'active'; + if (status === 'missed') return 'missed'; + if (status === 'ended') return 'ended'; + if (status === 'ringing') return 'ringing'; - // Determine status from timestamps - if (callData.value?.ended_at) { - return 'ended'; - } - if (callData.value?.missed) { - return isIncomingCall.value ? 'missed' : 'no-answer'; - } - if (callData.value?.started_at || props.conversation?.additional_attributes?.call_started_at) { - return 'active'; - } - - // For voice channel conversations, default to ended for better display - if (isVoiceChannelConversation.value) { - return 'ended'; - } - - // Default to ended for any remaining cases to avoid showing incorrect status + // If no status is set, default to 'ended' return 'ended'; }); -const callIconName = computed(() => { - return getCallIconName(normalizedCallStatus.value, isIncomingCall.value); -}); - +// Get formatted call status text for voice channel conversations const callStatusText = computed(() => { - if (!isVoiceCall.value) return ''; + if (!isVoiceChannel.value) return ''; - // For voice channel conversations, force more descriptive text - if (isVoiceChannelConversation.value) { - return getStatusText(normalizedCallStatus.value, isIncomingCall.value); + const status = normalizedCallStatus.value; + const isIncoming = isIncomingCall.value; + + if (status === 'active') { + return t('CONVERSATION.VOICE_CALL.CALL_IN_PROGRESS'); } - // For legacy messages with arrows, just use a cleaner version of the content - if (hasArrow.value && voiceCallMessage.value?.content) { - return processArrowContent( - voiceCallMessage.value.content, - isIncomingCall.value, - normalizedCallStatus.value - ); + if (isIncoming) { + if (status === 'ringing') { + return t('CONVERSATION.VOICE_CALL.INCOMING_CALL'); + } + + if (status === 'missed') { + return t('CONVERSATION.VOICE_CALL.MISSED_CALL'); + } + + if (status === 'ended') { + return t('CONVERSATION.VOICE_CALL.CALL_ENDED'); + } + } else { + if (status === 'ringing') { + return t('CONVERSATION.VOICE_CALL.OUTGOING_CALL'); + } + + if (status === 'no-answer') { + return t('CONVERSATION.VOICE_CALL.NO_ANSWER'); + } + + if (status === 'ended') { + return t('CONVERSATION.VOICE_CALL.CALL_ENDED'); + } } - // Generate the correct status text based on call status and direction - return getStatusText(normalizedCallStatus.value, isIncomingCall.value); + return isIncoming + ? t('CONVERSATION.VOICE_CALL.INCOMING_CALL') + : t('CONVERSATION.VOICE_CALL.OUTGOING_CALL'); }); // Return proper message content based on message type @@ -216,7 +102,7 @@ const lastNonActivityMessageContent = computed(() => { const { email: { subject } = {} } = customAttributes; // Return special formatting for voice calls - if (isVoiceCall.value) { + if (isVoiceChannel.value) { return callStatusText.value; } @@ -244,7 +130,7 @@ const unreadMessagesCount = computed(() => {
- + + + - + + + + + + + diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue index 86827395e..045451389 100644 --- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue +++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue @@ -2,7 +2,6 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; -import { useVoiceCallHelpers } from 'dashboard/composables/useVoiceCallHelpers'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; import CardLabels from 'dashboard/components-next/Conversation/ConversationCard/CardLabels.vue'; @@ -25,171 +24,89 @@ const slaCardLabelRef = ref(null); const { getPlainText } = useMessageFormatter(); -// Use our voice call helpers composable -const { - isVoiceChannelConversation, - hasArrow, - isIncomingCall: checkIsIncoming, - normalizeCallStatus, - getCallIconName, - getStatusText, - processArrowContent, -} = useVoiceCallHelpers(props, { t }); - -// Force voice call view for voice channel conversations -const isVoiceCall = computed(() => { - if (isVoiceChannelConversation.value) { - return true; - } - - // Check if conversation has a call_status in additional_attributes - if (props.conversation?.additional_attributes?.call_status) { - return true; - } - - // Check for voice call in last message - const { lastNonActivityMessage } = props.conversation || {}; - if (lastNonActivityMessage?.content_type === 'voice_call' || - lastNonActivityMessage?.content_type === 'voice') { - return true; - } - - // Look for voice call content with expanded terms - if (lastNonActivityMessage?.content && - typeof lastNonActivityMessage.content === 'string') { - const content = lastNonActivityMessage.content.toLowerCase(); - if (content.includes('voice call') || - content.includes('call ') || - content.includes('missed call') || - content.includes('incoming call') || - content.includes('outgoing call') || - content.startsWith('←') || - content.startsWith('→')) { - return true; - } - } - - return false; -}); - -// Check if content has arrow prefix using our helper -const messageHasArrow = computed(() => { - const { lastNonActivityMessage } = props.conversation || {}; - return hasArrow(lastNonActivityMessage); -}); - -// Get call data from multiple sources -const callData = computed(() => { - // First check for data directly in conversation attributes - const conversationAttributes = props.conversation?.custom_attributes || {}; - if (conversationAttributes.call_data) { - return conversationAttributes.call_data; - } - - // Then check message content attributes - const { lastNonActivityMessage } = props.conversation || {}; - if (lastNonActivityMessage?.content_attributes?.data) { - return lastNonActivityMessage.content_attributes.data; - } - - return {}; +// Simple check: Is this a voice channel conversation? +const isVoiceChannel = computed(() => { + return props.conversation?.meta?.inbox?.channel_type === 'Channel::Voice'; }); +// Get call direction: inbound or outbound const isIncomingCall = computed(() => { - if (!isVoiceCall.value) return null; + if (!isVoiceChannel.value) return false; - const { lastNonActivityMessage } = props.conversation || {}; - return checkIsIncoming(callData.value, lastNonActivityMessage); + const direction = props.conversation?.additional_attributes?.call_direction; + return direction === 'inbound'; }); +// Simple function to normalize call status const normalizedCallStatus = computed(() => { - if (!isVoiceCall.value) return ''; + if (!isVoiceChannel.value) return null; - // First check for direct call_status in the conversation additional_attributes - // This is the most authoritative source for call status - const conversationCallStatus = props.conversation?.additional_attributes?.call_status; - if (conversationCallStatus) { - return normalizeCallStatus(conversationCallStatus, isIncomingCall.value); - } + // Get the raw status directly from conversation + const status = props.conversation?.additional_attributes?.call_status; - // If there's an arrow in the message, this is a legacy format message - if (messageHasArrow.value) { - const { lastNonActivityMessage } = props.conversation || {}; - const content = lastNonActivityMessage?.content || ''; - - if (content.includes('ended') || content.includes('Call ended')) { - return 'ended'; - } - if (content.includes('missed') || content.includes('Missed call') || content.includes('no answer')) { - return isIncomingCall.value ? 'missed' : 'no-answer'; - } - if (content.includes('in progress') || content.includes('active') || content.includes('answered')) { - return 'active'; - } - - // For voice channel conversations, default to ended for better display - if (isVoiceChannelConversation.value) { - return 'ended'; - } - - // Default to 'ended' for any legacy messages without clear status - return 'ended'; - } + // Simple mapping of call statuses + if (status === 'in-progress') return 'active'; + if (status === 'completed') return 'ended'; + if (status === 'canceled') return 'ended'; + if (status === 'failed') return 'ended'; + if (status === 'busy') return 'no-answer'; + if (status === 'no-answer') return isIncomingCall.value ? 'missed' : 'no-answer'; - // Apply status mapping logic to call data status - const callStatus = callData.value?.status; - if (callStatus) { - return normalizeCallStatus(callStatus, isIncomingCall.value); - } + // Return the status as is for explicit values + if (status === 'active') return 'active'; + if (status === 'missed') return 'missed'; + if (status === 'ended') return 'ended'; + if (status === 'ringing') return 'ringing'; - // Determine status from timestamps - if (callData.value?.ended_at) { - return 'ended'; - } - if (callData.value?.missed) { - return isIncomingCall.value ? 'missed' : 'no-answer'; - } - if (callData.value?.started_at || props.conversation?.additional_attributes?.call_started_at) { - return 'active'; - } - - // For voice channel conversations, default to ended for better display - if (isVoiceChannelConversation.value) { - return 'ended'; - } - - // Default to ended for any remaining cases to avoid showing incorrect status + // If no status is set, default to 'ended' return 'ended'; }); -const callIconName = computed(() => { - return getCallIconName(normalizedCallStatus.value, isIncomingCall.value); -}); - +// Get formatted call status text for voice channel conversations const callStatusText = computed(() => { - if (!isVoiceCall.value) return ''; + if (!isVoiceChannel.value) return ''; - // For legacy messages with arrows, process using our helper - if (messageHasArrow.value) { - const { lastNonActivityMessage } = props.conversation || {}; - const content = lastNonActivityMessage?.content || ''; + const status = normalizedCallStatus.value; + const isIncoming = isIncomingCall.value; + + if (status === 'active') { + return t('CONVERSATION.VOICE_CALL.CALL_IN_PROGRESS'); + } + + if (isIncoming) { + if (status === 'ringing') { + return t('CONVERSATION.VOICE_CALL.INCOMING_CALL'); + } - // Process arrow content with our helper - return processArrowContent(content, isIncomingCall.value, normalizedCallStatus.value); + if (status === 'missed') { + return t('CONVERSATION.VOICE_CALL.MISSED_CALL'); + } + + if (status === 'ended') { + return t('CONVERSATION.VOICE_CALL.CALL_ENDED'); + } + } else { + if (status === 'ringing') { + return t('CONVERSATION.VOICE_CALL.OUTGOING_CALL'); + } + + if (status === 'no-answer') { + return t('CONVERSATION.VOICE_CALL.NO_ANSWER'); + } + + if (status === 'ended') { + return t('CONVERSATION.VOICE_CALL.CALL_ENDED'); + } } - // For voice channel conversations, use our helper for descriptive text - if (isVoiceChannelConversation.value) { - return getStatusText(normalizedCallStatus.value, isIncomingCall.value); - } - - // Generate the correct status text based on call status and direction - return getStatusText(normalizedCallStatus.value, isIncomingCall.value); + return isIncoming + ? t('CONVERSATION.VOICE_CALL.INCOMING_CALL') + : t('CONVERSATION.VOICE_CALL.OUTGOING_CALL'); }); const lastNonActivityMessageContent = computed(() => { // If it's a voice call, use the voice call text with icon - if (isVoiceCall.value) { + if (isVoiceChannel.value) { return callStatusText.value; } @@ -232,7 +149,7 @@ defineExpose({
- + + + - + + + + + + + diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue index 0d406fa9c..bf0019c6f 100644 --- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue +++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue @@ -4,6 +4,7 @@ import { getInboxIconByType } from 'dashboard/helper/inbox'; import { useRouter, useRoute } from 'vue-router'; import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper.js'; import { dynamicTime, shortTimestamp } from 'shared/helpers/timeHelper'; +import { useI18n } from 'vue-i18n'; import Icon from 'dashboard/components-next/icon/Icon.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; @@ -32,6 +33,7 @@ const props = defineProps({ const router = useRouter(); const route = useRoute(); +const { t } = useI18n(); const cardMessagePreviewWithMetaRef = ref(null); @@ -61,58 +63,42 @@ const lastNonActivityMessage = computed(() => { return props.conversation?.lastNonActivityMessage || {}; }); -const isVoiceCall = computed(() => { - return lastNonActivityMessage.value?.content_type === 'voice_call' || - lastNonActivityMessage.value?.content_type === 'voice'; -}); - -const callData = computed(() => { - if (!isVoiceCall.value) return null; - return lastNonActivityMessage.value?.content_attributes?.data || {}; +// Simple check: Is this a voice channel conversation? +const isVoiceChannel = computed(() => { + return props.conversation?.meta?.inbox?.channel_type === 'Channel::Voice'; }); +// Get call direction: inbound or outbound const isIncomingCall = computed(() => { - if (!isVoiceCall.value) return false; + if (!isVoiceChannel.value) return false; - const direction = callData.value?.call_direction; - if (direction) { - return direction === 'inbound'; - } - - return lastNonActivityMessage.value?.message_type === 0; + const direction = props.conversation?.additional_attributes?.call_direction; + return direction === 'inbound'; }); +// Simple function to normalize call status const normalizedCallStatus = computed(() => { - if (!isVoiceCall.value) return null; + if (!isVoiceChannel.value) return null; - // Apply the same status mapping as in VoiceCall component - const callStatus = callData.value?.status; - if (callStatus) { - const statusMap = { - 'in-progress': 'active', - 'completed': 'ended', - 'canceled': 'ended', - 'failed': 'ended', - 'busy': 'no-answer', - 'no-answer': isIncomingCall.value ? 'missed' : 'no-answer' - }; - - return statusMap[callStatus] || callStatus; - } + // Get the raw status directly from conversation + const status = props.conversation?.additional_attributes?.call_status; - // Determine status from timestamps - if (callData.value?.ended_at) { - return 'ended'; - } - if (callData.value?.missed) { - return isIncomingCall.value ? 'missed' : 'no-answer'; - } - if (callData.value?.started_at) { - return 'active'; - } + // Simple mapping of call statuses + if (status === 'in-progress') return 'active'; + if (status === 'completed') return 'ended'; + if (status === 'canceled') return 'ended'; + if (status === 'failed') return 'ended'; + if (status === 'busy') return 'no-answer'; + if (status === 'no-answer') return isIncomingCall.value ? 'missed' : 'no-answer'; - // Default to ringing - return 'ringing'; + // Return the status as is for explicit values + if (status === 'active') return 'active'; + if (status === 'missed') return 'missed'; + if (status === 'ended') return 'ended'; + if (status === 'ringing') return 'ringing'; + + // If no status is set, default to 'ended' + return 'ended'; }); const isRingingCall = computed(() => { @@ -123,6 +109,73 @@ const isActiveCall = computed(() => { return normalizedCallStatus.value === 'active'; }); +// Get formatted call status text for voice channel conversations +const callStatusText = computed(() => { + if (!isVoiceChannel.value) return ''; + + const status = normalizedCallStatus.value; + const isIncoming = isIncomingCall.value; + + if (status === 'active') { + return t('CONVERSATION.VOICE_CALL.CALL_IN_PROGRESS'); + } + + if (isIncoming) { + if (status === 'ringing') { + return t('CONVERSATION.VOICE_CALL.INCOMING_CALL'); + } + + if (status === 'missed') { + return t('CONVERSATION.VOICE_CALL.MISSED_CALL'); + } + + if (status === 'ended') { + return t('CONVERSATION.VOICE_CALL.CALL_ENDED'); + } + } else { + if (status === 'ringing') { + return t('CONVERSATION.VOICE_CALL.OUTGOING_CALL'); + } + + if (status === 'no-answer') { + return t('CONVERSATION.VOICE_CALL.NO_ANSWER'); + } + + if (status === 'ended') { + return t('CONVERSATION.VOICE_CALL.CALL_ENDED'); + } + } + + return isIncoming + ? t('CONVERSATION.VOICE_CALL.INCOMING_CALL') + : t('CONVERSATION.VOICE_CALL.OUTGOING_CALL'); +}); + +// Get icon class based on call status +const callIconClass = computed(() => { + if (!isVoiceChannel.value) return ''; + + const status = normalizedCallStatus.value; + const isIncoming = isIncomingCall.value; + + if (status === 'missed' || status === 'no-answer') { + return 'i-ph-phone-x-fill'; + } + + if (status === 'active') { + return 'i-ph-phone-call-fill'; + } + + if (status === 'ended' || status === 'completed') { + return 'i-ph-phone-fill'; + } + + // Default phone icon for ringing state + return isIncoming + ? 'i-ph-phone-incoming-fill' + : 'i-ph-phone-outgoing-fill'; +}); + const showMessagePreviewWithoutMeta = computed(() => { const { labels = [] } = props.conversation; return ( @@ -186,11 +239,33 @@ const onCardClick = e => { v-tooltip.left="inboxName" class="flex items-center justify-center flex-shrink-0 rounded-full bg-n-alpha-2 size-5" > - + + v-if="isVoiceChannel && normalizedCallStatus === 'missed'" + class="i-ph-phone-x-fill text-red-600 dark:text-red-400 size-3 inline-block" + > + + + + + {
- - + + +
+ + + + + + + + + + + + {{ callStatusText }} + + + + ({{ t('CONVERSATION.VOICE_CALL.JOIN_CALL') }}) + +
+ + +
@@ -221,6 +340,10 @@ const onCardClick = e => { animation: border-pulse 1.5s infinite; } +.pulse-animation { + animation: icon-pulse 1.5s infinite; +} + @keyframes border-pulse { 0% { border-color: rgba(34, 197, 94, 0.8); /* Green for ringing */ @@ -232,4 +355,16 @@ const onCardClick = e => { border-color: rgba(34, 197, 94, 0.8); } } + +@keyframes icon-pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +} diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 7d771ad88..d5d0aba6c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -306,6 +306,7 @@ export default { diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagePreview.vue b/app/javascript/dashboard/components/widgets/conversation/MessagePreview.vue index 4df6e16e1..05d930b51 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagePreview.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagePreview.vue @@ -10,6 +10,10 @@ export default { type: Object, required: true, }, + conversation: { + type: Object, + default: () => ({}), + }, showMessageType: { type: Boolean, default: true, @@ -38,7 +42,127 @@ export default { const { private: isPrivate } = this.message; return isPrivate; }, + // Simple check: Is this a voice channel conversation? + isVoiceChannel() { + return this.conversation?.meta?.inbox?.channel_type === 'Channel::Voice'; + }, + // Check if this is a voice call message + isVoiceCall() { + return ( + this.message?.content_type === 'voice_call' || + this.message?.content_attributes?.type === 'voice_call' || + this.message?.content_attributes?.data?.callType === 'voice_call' || + this.isVoiceChannel + ); + }, + // Get call direction for voice calls + isIncomingCall() { + if (!this.isVoiceCall) return false; + + // First check conversation attributes + const direction = this.conversation?.additional_attributes?.call_direction; + if (direction) { + return direction === 'inbound'; + } + + // Then fall back to message type + return this.message.message_type === MESSAGE_TYPE.INCOMING; + }, + // Get normalized call status + callStatus() { + if (!this.isVoiceCall) return null; + + // Get raw status from conversation + const status = this.conversation?.additional_attributes?.call_status; + + // Map status to normalized values + if (status === 'in-progress') return 'active'; + if (status === 'completed') return 'ended'; + if (status === 'canceled') return 'ended'; + if (status === 'failed') return 'ended'; + if (status === 'busy') return 'no-answer'; + if (status === 'no-answer') return this.isIncomingCall ? 'missed' : 'no-answer'; + + // Return explicit status values as-is + if (status === 'active') return 'active'; + if (status === 'missed') return 'missed'; + if (status === 'ended') return 'ended'; + if (status === 'ringing') return 'ringing'; + + // Default status + return 'ended'; + }, + // Voice call icon based on status + voiceCallIcon() { + if (!this.isVoiceCall) return null; + + const status = this.callStatus; + const isIncoming = this.isIncomingCall; + + if (status === 'missed' || status === 'no-answer') { + return 'phone-missed-call'; + } + + if (status === 'active') { + return 'phone-in-talk'; + } + + if (status === 'ended') { + return isIncoming ? 'phone-incoming' : 'phone-outgoing'; + } + + if (status === 'ringing') { + return isIncoming ? 'phone-incoming' : 'phone-outgoing'; + } + + // Default based on direction + return isIncoming ? 'phone-incoming' : 'phone-outgoing'; + }, parsedLastMessage() { + // For voice calls, return status text + if (this.isVoiceCall) { + // Get status-based text + const status = this.callStatus; + const isIncoming = this.isIncomingCall; + + // Return appropriate status text based on call status and direction + if (status === 'active') { + return this.$t('CONVERSATION.VOICE_CALL.CALL_IN_PROGRESS'); + } + + if (isIncoming) { + if (status === 'ringing') { + return this.$t('CONVERSATION.VOICE_CALL.INCOMING_CALL'); + } + + if (status === 'missed') { + return this.$t('CONVERSATION.VOICE_CALL.MISSED_CALL'); + } + + if (status === 'ended') { + return this.$t('CONVERSATION.VOICE_CALL.CALL_ENDED'); + } + } else { + if (status === 'ringing') { + return this.$t('CONVERSATION.VOICE_CALL.OUTGOING_CALL'); + } + + if (status === 'no-answer') { + return this.$t('CONVERSATION.VOICE_CALL.NO_ANSWER'); + } + + if (status === 'ended') { + return this.$t('CONVERSATION.VOICE_CALL.CALL_ENDED'); + } + } + + // Default fallback based on direction + return isIncoming + ? this.$t('CONVERSATION.VOICE_CALL.INCOMING_CALL') + : this.$t('CONVERSATION.VOICE_CALL.OUTGOING_CALL'); + } + + // Default behavior for non-voice calls const { content_attributes: contentAttributes } = this.message; const { email: { subject } = {} } = contentAttributes || {}; return this.getPlainText(subject || this.message.content); @@ -69,6 +193,34 @@ export default { class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block" icon="lock-closed" /> + + + + + + + + + + + + + + + + {{ $t('CHAT_LIST.ATTACHMENTS.image.CONTENT') }} - + {{ parsedLastMessage }} diff --git a/app/javascript/dashboard/composables/useVoiceCallHelpers.js b/app/javascript/dashboard/composables/useVoiceCallHelpers.js index 98a417cdd..04c99b1f2 100644 --- a/app/javascript/dashboard/composables/useVoiceCallHelpers.js +++ b/app/javascript/dashboard/composables/useVoiceCallHelpers.js @@ -3,9 +3,37 @@ import { computed } from 'vue'; export const useVoiceCallHelpers = (props, { t }) => { // Check if the conversation is from a voice channel const isVoiceChannelConversation = computed(() => { - return props.conversation?.meta?.inbox?.channel_type === 'Channel::Voice'; + // First check the meta.inbox.channel_type + if (props.conversation?.meta?.inbox?.channel_type === 'Channel::Voice') { + return true; + } + + // Also check the inbox_id to find the channel type + // This is useful when the meta.inbox is not fully populated + if (props.conversation?.inbox_id && props.conversation?.meta?.channel_type === 'Channel::Voice') { + return true; + } + + return false; }); + // Function to check if a conversation is a voice channel conversation (non-computed version) + const isConversationFromVoiceChannel = (conversation) => { + if (!conversation) return false; + + // Check meta inbox channel type + if (conversation.meta?.inbox?.channel_type === 'Channel::Voice') { + return true; + } + + // Check meta channel type + if (conversation.meta?.channel_type === 'Channel::Voice') { + return true; + } + + return false; + }; + // Helper function to find call information from various sources const getCallData = (conversation) => { if (!conversation) return {}; @@ -79,7 +107,7 @@ export const useVoiceCallHelpers = (props, { t }) => { } if (status === 'ended' || status === 'completed') { - return 'i-ph-phone-fill'; + return isIncoming ? 'i-ph-phone-incoming-fill' : 'i-ph-phone-outgoing-fill'; } // Default phone icon for ringing state @@ -140,6 +168,7 @@ export const useVoiceCallHelpers = (props, { t }) => { return { isVoiceChannelConversation, + isConversationFromVoiceChannel, getCallData, hasArrow, isIncomingCall,