mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	 6bdd4f0670
			
		
	
	6bdd4f0670
	
	
	
		
			
			This PR delivers the first slice of the voice channel: inbound call handling. When a customer calls a configured voice number, Chatwoot now creates a new conversation and shows a dedicated call bubble in the UI. As the call progresses (ringing, answered, completed), its status updates in real time in both the conversation list and the call bubble, so agents can instantly see what’s happening. This focuses on the inbound flow and is part of breaking the larger voice feature into smaller, functional, and testable units; further enhancements will follow in subsequent PRs. references: #11602 , #11481 ## Testing - Configure a Voice inbox in Chatwoot with your Twilio number. - Place a call to that number. - Verify a new conversation appears in the Voice inbox for the call. - Open it and confirm a dedicated voice call message bubble is shown. - Watch status update live (ringing/answered); hang up and see it change to completed in both the bubble and conversation list. - to test missed call status, make sure to hangup the call before the please wait while we connect you to an agent message plays ## Screens <img width="400" alt="Screenshot 2025-09-03 at 3 11 25 PM" src="https://github.com/user-attachments/assets/d6a1d2ff-2ded-47b7-9144-a9d898beb380" /> <img width="700" alt="Screenshot 2025-09-03 at 3 11 33 PM" src="https://github.com/user-attachments/assets/c25e6a1e-a885-47f7-b3d7-c3e15eef18c7" /> <img width="700" alt="Screenshot 2025-09-03 at 3 11 57 PM" src="https://github.com/user-attachments/assets/29e7366d-b1d4-4add-a062-4646d2bff435" /> <img width="442" height="255" alt="Screenshot 2025-09-04 at 11 55 01 PM" src="https://github.com/user-attachments/assets/703126f6-a448-49d9-9c02-daf3092cc7f9" /> --------- Co-authored-by: Muhsin <muhsinkeramam@gmail.com>
		
			
				
	
	
		
			128 lines
		
	
	
		
			3.3 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			128 lines
		
	
	
		
			3.3 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <script setup>
 | |
| import { computed } from 'vue';
 | |
| 
 | |
| import MessageMeta from '../MessageMeta.vue';
 | |
| 
 | |
| import { emitter } from 'shared/helpers/mitt';
 | |
| import { useMessageContext } from '../provider.js';
 | |
| import { useI18n } from 'vue-i18n';
 | |
| 
 | |
| import { BUS_EVENTS } from 'shared/constants/busEvents';
 | |
| import { MESSAGE_VARIANTS, ORIENTATION } from '../constants';
 | |
| 
 | |
| const props = defineProps({
 | |
|   hideMeta: { type: Boolean, default: false },
 | |
| });
 | |
| 
 | |
| const { variant, orientation, inReplyTo, shouldGroupWithNext } =
 | |
|   useMessageContext();
 | |
| const { t } = useI18n();
 | |
| 
 | |
| const varaintBaseMap = {
 | |
|   [MESSAGE_VARIANTS.AGENT]: 'bg-n-solid-blue text-n-slate-12',
 | |
|   [MESSAGE_VARIANTS.PRIVATE]:
 | |
|     'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold',
 | |
|   [MESSAGE_VARIANTS.USER]: 'bg-n-slate-4 text-n-slate-12',
 | |
|   [MESSAGE_VARIANTS.ACTIVITY]: 'bg-n-alpha-1 text-n-slate-11 text-sm',
 | |
|   [MESSAGE_VARIANTS.BOT]: 'bg-n-solid-iris text-n-slate-12',
 | |
|   [MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12',
 | |
|   [MESSAGE_VARIANTS.ERROR]: 'bg-n-ruby-4 text-n-ruby-12',
 | |
|   [MESSAGE_VARIANTS.EMAIL]: 'w-full',
 | |
|   [MESSAGE_VARIANTS.UNSUPPORTED]:
 | |
|     'bg-n-solid-amber/70 border border-dashed border-n-amber-12 text-n-amber-12',
 | |
| };
 | |
| 
 | |
| const orientationMap = {
 | |
|   [ORIENTATION.LEFT]:
 | |
|     'left-bubble rounded-xl ltr:rounded-bl-sm rtl:rounded-br-sm',
 | |
|   [ORIENTATION.RIGHT]:
 | |
|     'right-bubble rounded-xl ltr:rounded-br-sm rtl:rounded-bl-sm',
 | |
|   [ORIENTATION.CENTER]: 'rounded-md',
 | |
| };
 | |
| 
 | |
| const flexOrientationClass = computed(() => {
 | |
|   const map = {
 | |
|     [ORIENTATION.LEFT]: 'justify-start',
 | |
|     [ORIENTATION.RIGHT]: 'justify-end',
 | |
|     [ORIENTATION.CENTER]: 'justify-center',
 | |
|   };
 | |
| 
 | |
|   return map[orientation.value];
 | |
| });
 | |
| 
 | |
| const messageClass = computed(() => {
 | |
|   const classToApply = [varaintBaseMap[variant.value]];
 | |
| 
 | |
|   if (variant.value !== MESSAGE_VARIANTS.ACTIVITY) {
 | |
|     classToApply.push(orientationMap[orientation.value]);
 | |
|   } else {
 | |
|     classToApply.push('rounded-lg');
 | |
|   }
 | |
| 
 | |
|   return classToApply;
 | |
| });
 | |
| 
 | |
| const scrollToMessage = () => {
 | |
|   emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
 | |
|     messageId: inReplyTo.value.id,
 | |
|   });
 | |
| };
 | |
| 
 | |
| const shouldShowMeta = computed(
 | |
|   () =>
 | |
|     !props.hideMeta &&
 | |
|     !shouldGroupWithNext.value &&
 | |
|     variant.value !== MESSAGE_VARIANTS.ACTIVITY
 | |
| );
 | |
| 
 | |
| const replyToPreview = computed(() => {
 | |
|   if (!inReplyTo) return '';
 | |
| 
 | |
|   const { content, attachments } = inReplyTo.value;
 | |
| 
 | |
|   if (content) return content;
 | |
|   if (attachments?.length) {
 | |
|     const firstAttachment = attachments[0];
 | |
|     const fileType = firstAttachment.fileType ?? firstAttachment.file_type;
 | |
| 
 | |
|     return t(`CHAT_LIST.ATTACHMENTS.${fileType}.CONTENT`);
 | |
|   }
 | |
| 
 | |
|   return t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND');
 | |
| });
 | |
| </script>
 | |
| 
 | |
| <template>
 | |
|   <div
 | |
|     class="text-sm"
 | |
|     :class="[
 | |
|       messageClass,
 | |
|       {
 | |
|         'max-w-lg': variant !== MESSAGE_VARIANTS.EMAIL,
 | |
|       },
 | |
|     ]"
 | |
|   >
 | |
|     <div
 | |
|       v-if="inReplyTo"
 | |
|       class="p-2 -mx-1 mb-2 rounded-lg cursor-pointer bg-n-alpha-black1"
 | |
|       @click="scrollToMessage"
 | |
|     >
 | |
|       <span class="break-all line-clamp-2">
 | |
|         {{ replyToPreview }}
 | |
|       </span>
 | |
|     </div>
 | |
|     <slot />
 | |
|     <MessageMeta
 | |
|       v-if="shouldShowMeta"
 | |
|       :class="[
 | |
|         flexOrientationClass,
 | |
|         variant === MESSAGE_VARIANTS.EMAIL ? 'px-3 pb-3' : '',
 | |
|         variant === MESSAGE_VARIANTS.PRIVATE
 | |
|           ? 'text-n-amber-12/50'
 | |
|           : 'text-n-slate-11',
 | |
|       ]"
 | |
|       class="mt-2"
 | |
|     />
 | |
|   </div>
 | |
| </template>
 |