mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 10:42:38 +00:00 
			
		
		
		
	feat: integrate new bubbles (#10550)
To test this, set the `useNextBubble` value to `true` in the
localstorage. Here's a quick command to run in the console
```js
localStorage.setItem('useNextBubble', true)
```
```js
localStorage.setItem('useNextBubble', false)
```
---------
Co-authored-by: Pranav <pranavrajs@gmail.com>
			
			
This commit is contained in:
		| @@ -124,6 +124,19 @@ | ||||
|     --teal-11: 0 133 115; | ||||
|     --teal-12: 13 61 56; | ||||
|  | ||||
|     --gray-1: 252 252 252; | ||||
|     --gray-2: 249 249 249; | ||||
|     --gray-3: 240 240 240; | ||||
|     --gray-4: 232 232 232; | ||||
|     --gray-5: 224 224 224; | ||||
|     --gray-6: 217 217 217; | ||||
|     --gray-7: 206 206 206; | ||||
|     --gray-8: 187 187 187; | ||||
|     --gray-9: 141 141 141; | ||||
|     --gray-10: 131 131 131; | ||||
|     --gray-11: 100 100 100; | ||||
|     --gray-12: 32 32 32; | ||||
|  | ||||
|     --background-color: 253 253 253; | ||||
|     --text-blue: 8 109 224; | ||||
|     --border-container: 236 236 236; | ||||
| @@ -213,6 +226,19 @@ | ||||
|     --teal-11: 11 216 182; | ||||
|     --teal-12: 173 240 221; | ||||
|  | ||||
|     --gray-1: 17 17 17; | ||||
|     --gray-2: 25 25 25; | ||||
|     --gray-3: 34 34 34; | ||||
|     --gray-4: 42 42 42; | ||||
|     --gray-5: 49 49 49; | ||||
|     --gray-6: 58 58 58; | ||||
|     --gray-7: 72 72 72; | ||||
|     --gray-8: 96 96 96; | ||||
|     --gray-9: 110 110 110; | ||||
|     --gray-10: 123 123 123; | ||||
|     --gray-11: 180 180 180; | ||||
|     --gray-12: 238 238 238; | ||||
|  | ||||
|     --background-color: 18 18 19; | ||||
|     --border-strong: 52 52 52; | ||||
|     --border-weak: 38 38 42; | ||||
| @@ -232,7 +258,7 @@ | ||||
|     --black-alpha-2: 0, 0, 0, 0.2; | ||||
|     --border-blue: 39, 129, 246, 0.5; | ||||
|     --border-container: 236, 236, 236, 0; | ||||
|     --white-alpha: 255, 255, 255, 0.8; | ||||
|     --white-alpha: 255, 255, 255, 0.1; | ||||
|   } | ||||
|   /* NEXT COLORS END */ | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,12 @@ | ||||
| <script setup> | ||||
| import { computed, defineAsyncComponent } from 'vue'; | ||||
| import { computed, ref, toRefs } from 'vue'; | ||||
| import { provideMessageContext } from './provider.js'; | ||||
| import { useTrack } from 'dashboard/composables'; | ||||
| import { emitter } from 'shared/helpers/mitt'; | ||||
| import { LocalStorage } from 'shared/helpers/localStorage'; | ||||
| import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; | ||||
| import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; | ||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||
| import { | ||||
|   MESSAGE_TYPES, | ||||
|   ATTACHMENT_TYPES, | ||||
| @@ -8,6 +14,7 @@ import { | ||||
|   SENDER_TYPES, | ||||
|   ORIENTATION, | ||||
|   MESSAGE_STATUS, | ||||
|   CONTENT_TYPES, | ||||
| } from './constants'; | ||||
|  | ||||
| import Avatar from 'next/avatar/Avatar.vue'; | ||||
| @@ -19,17 +26,14 @@ import FileBubble from './bubbles/File.vue'; | ||||
| import AudioBubble from './bubbles/Audio.vue'; | ||||
| import VideoBubble from './bubbles/Video.vue'; | ||||
| import InstagramStoryBubble from './bubbles/InstagramStory.vue'; | ||||
| import AttachmentsBubble from './bubbles/Attachments.vue'; | ||||
| import EmailBubble from './bubbles/Email/Index.vue'; | ||||
| import UnsupportedBubble from './bubbles/Unsupported.vue'; | ||||
| import ContactBubble from './bubbles/Contact.vue'; | ||||
| import DyteBubble from './bubbles/Dyte.vue'; | ||||
| const LocationBubble = defineAsyncComponent( | ||||
|   () => import('./bubbles/Location.vue') | ||||
| ); | ||||
| import LocationBubble from './bubbles/Location.vue'; | ||||
|  | ||||
| import MessageError from './MessageError.vue'; | ||||
| import MessageMeta from './MessageMeta.vue'; | ||||
| import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
| @@ -65,7 +69,7 @@ import MessageMeta from './MessageMeta.vue'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Props | ||||
|  * @property {('sent'|'delivered'|'read'|'failed')} status - The delivery status of the message | ||||
|  * @property {('sent'|'delivered'|'read'|'failed'|'progress')} status - The delivery status of the message | ||||
|  * @property {ContentAttributes} [contentAttributes={}] - Additional attributes of the message content | ||||
|  * @property {Attachment[]} [attachments=[]] - The attachments associated with the message | ||||
|  * @property {Sender|null} [sender=null] - The sender information | ||||
| @@ -78,6 +82,11 @@ import MessageMeta from './MessageMeta.vue'; | ||||
|  * @property {string|null} [error=null] - Error message if the message failed to send | ||||
|  * @property {string|null} [senderType=null] - The type of the sender | ||||
|  * @property {string} content - The message content | ||||
|  * @property {boolean} [groupWithNext=false] - Whether the message should be grouped with the next message | ||||
|  * @property {Object|null} [inReplyTo=null] - The message to which this message is a reply | ||||
|  * @property {boolean} [isEmailInbox=false] - Whether the message is from an email inbox | ||||
|  * @property {number} conversationId - The ID of the conversation to which the message belongs | ||||
|  * @property {number} inboxId - The ID of the inbox to which the message belongs | ||||
|  */ | ||||
|  | ||||
| // eslint-disable-next-line vue/define-macros-order | ||||
| @@ -93,70 +102,51 @@ const props = defineProps({ | ||||
|     required: true, | ||||
|     validator: value => Object.values(MESSAGE_STATUS).includes(value), | ||||
|   }, | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     default: () => [], | ||||
|   }, | ||||
|   private: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
|   createdAt: { | ||||
|     type: Number, | ||||
|     required: true, | ||||
|   }, | ||||
|   sender: { | ||||
|     type: Object, | ||||
|     default: null, | ||||
|   }, | ||||
|   senderId: { | ||||
|     type: Number, | ||||
|     default: null, | ||||
|   }, | ||||
|   senderType: { | ||||
|   attachments: { type: Array, default: () => [] }, | ||||
|   content: { type: String, default: null }, | ||||
|   contentAttributes: { type: Object, default: () => ({}) }, | ||||
|   contentType: { | ||||
|     type: String, | ||||
|     default: null, | ||||
|   }, | ||||
|   content: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|   }, | ||||
|   contentAttributes: { | ||||
|     type: Object, | ||||
|     default: () => {}, | ||||
|   }, | ||||
|   currentUserId: { | ||||
|     type: Number, | ||||
|     required: true, | ||||
|   }, | ||||
|   groupWithNext: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
|   inReplyTo: { | ||||
|     type: Object, | ||||
|     default: null, | ||||
|   }, | ||||
|   isEmailInbox: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|     default: 'text', | ||||
|     validator: value => Object.values(CONTENT_TYPES).includes(value), | ||||
|   }, | ||||
|   conversationId: { type: Number, required: true }, | ||||
|   createdAt: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties | ||||
|   currentUserId: { type: Number, required: true }, | ||||
|   groupWithNext: { type: Boolean, default: false }, | ||||
|   inboxId: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties | ||||
|   inboxSupportsReplyTo: { type: Object, default: () => ({}) }, | ||||
|   inReplyTo: { type: Object, default: null }, // eslint-disable-line vue/no-unused-properties | ||||
|   isEmailInbox: { type: Boolean, default: false }, | ||||
|   private: { type: Boolean, default: false }, | ||||
|   sender: { type: Object, default: null }, | ||||
|   senderId: { type: Number, default: null }, | ||||
|   senderType: { type: String, default: null }, | ||||
|   sourceId: { type: String, default: '' }, // eslint-disable-line vue/no-unused-properties | ||||
| }); | ||||
|  | ||||
| const contextMenuPosition = ref({}); | ||||
| const showContextMenu = ref(false); | ||||
| /** | ||||
|  * Computes the message variant based on props | ||||
|  * @type {import('vue').ComputedRef<'user'|'agent'|'activity'|'private'|'bot'|'template'>} | ||||
|  */ | ||||
| const variant = computed(() => { | ||||
|   if (props.private) return MESSAGE_VARIANTS.PRIVATE; | ||||
|  | ||||
|   if (props.isEmailInbox) { | ||||
|     const emailInboxTypes = [MESSAGE_TYPES.INCOMING, MESSAGE_TYPES.OUTGOING]; | ||||
|     if (emailInboxTypes.includes(props.messageType)) { | ||||
|       return MESSAGE_VARIANTS.EMAIL; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (props.contentType === CONTENT_TYPES.INCOMING_EMAIL) { | ||||
|     return MESSAGE_VARIANTS.EMAIL; | ||||
|   } | ||||
|  | ||||
|   if (props.status === MESSAGE_STATUS.FAILED) return MESSAGE_VARIANTS.ERROR; | ||||
|   if (props.contentAttributes.isUnsupported) | ||||
|   if (props.contentAttributes?.isUnsupported) | ||||
|     return MESSAGE_VARIANTS.UNSUPPORTED; | ||||
|  | ||||
|   const variants = { | ||||
| @@ -170,10 +160,20 @@ const variant = computed(() => { | ||||
| }); | ||||
|  | ||||
| const isMyMessage = computed(() => { | ||||
|   // if an outgoing message is still processing, then it's definitely a | ||||
|   // message sent by the current user | ||||
|   if ( | ||||
|     props.status === MESSAGE_STATUS.PROGRESS && | ||||
|     props.messageType === MESSAGE_TYPES.OUTGOING | ||||
|   ) { | ||||
|     return true; | ||||
|   } | ||||
|   const senderId = props.senderId ?? props.sender?.id; | ||||
|   const senderType = props.senderType ?? props.sender?.type; | ||||
|  | ||||
|   if (!senderType || !senderId) return false; | ||||
|   if (!senderType || !senderId) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase() && | ||||
| @@ -218,11 +218,9 @@ const gridTemplate = computed(() => { | ||||
|   const map = { | ||||
|     [ORIENTATION.LEFT]: ` | ||||
|       "avatar bubble" | ||||
|       "spacer meta" | ||||
|     `, | ||||
|     [ORIENTATION.RIGHT]: ` | ||||
|       "bubble" | ||||
|       "meta" | ||||
|     `, | ||||
|   }; | ||||
|  | ||||
| @@ -248,7 +246,11 @@ const componentToRender = computed(() => { | ||||
|     if (emailInboxTypes.includes(props.messageType)) return EmailBubble; | ||||
|   } | ||||
|  | ||||
|   if (props.contentAttributes.isUnsupported) { | ||||
|   if (props.contentType === CONTENT_TYPES.INCOMING_EMAIL) { | ||||
|     return EmailBubble; | ||||
|   } | ||||
|  | ||||
|   if (props.contentAttributes?.isUnsupported) { | ||||
|     return UnsupportedBubble; | ||||
|   } | ||||
|  | ||||
| @@ -260,7 +262,7 @@ const componentToRender = computed(() => { | ||||
|     return InstagramStoryBubble; | ||||
|   } | ||||
|  | ||||
|   if (props.attachments.length === 1) { | ||||
|   if (Array.isArray(props.attachments) && props.attachments.length === 1) { | ||||
|     const fileType = props.attachments[0].fileType; | ||||
|  | ||||
|     if (!props.content) { | ||||
| @@ -275,23 +277,93 @@ const componentToRender = computed(() => { | ||||
|     if (fileType === ATTACHMENT_TYPES.CONTACT) return ContactBubble; | ||||
|   } | ||||
|  | ||||
|   if (props.attachments.length > 1 && !props.content) { | ||||
|     return AttachmentsBubble; | ||||
|   } | ||||
|  | ||||
|   return TextBubble; | ||||
| }); | ||||
|  | ||||
| const shouldShowContextMenu = computed(() => { | ||||
|   return !( | ||||
|     props.status === MESSAGE_STATUS.FAILED || | ||||
|     props.status === MESSAGE_STATUS.PROGRESS || | ||||
|     props.contentAttributes?.isUnsupported | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| const isBubble = computed(() => { | ||||
|   return props.messageType !== MESSAGE_TYPES.ACTIVITY; | ||||
| }); | ||||
|  | ||||
| const isMessageDeleted = computed(() => { | ||||
|   return props.contentAttributes?.deleted; | ||||
| }); | ||||
|  | ||||
| const payloadForContextMenu = computed(() => { | ||||
|   return { | ||||
|     id: props.id, | ||||
|     content_attributes: props.contentAttributes, | ||||
|     content: props.content, | ||||
|     conversation_id: props.conversationId, | ||||
|   }; | ||||
| }); | ||||
|  | ||||
| const contextMenuEnabledOptions = computed(() => { | ||||
|   const hasText = !!props.content; | ||||
|   const hasAttachments = !!(props.attachments && props.attachments.length > 0); | ||||
|  | ||||
|   const isOutgoing = props.messageType === MESSAGE_TYPES.OUTGOING; | ||||
|  | ||||
|   return { | ||||
|     copy: hasText, | ||||
|     delete: hasText || hasAttachments, | ||||
|     cannedResponse: isOutgoing && hasText, | ||||
|     replyTo: !props.private && props.inboxSupportsReplyTo.outgoing, | ||||
|   }; | ||||
| }); | ||||
|  | ||||
| function openContextMenu(e) { | ||||
|   const shouldSkipContextMenu = | ||||
|     e.target?.classList.contains('skip-context-menu') || | ||||
|     e.target?.tagName.toLowerCase() === 'a'; | ||||
|   if (shouldSkipContextMenu || getSelection().toString()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   e.preventDefault(); | ||||
|   if (e.type === 'contextmenu') { | ||||
|     useTrack(ACCOUNT_EVENTS.OPEN_MESSAGE_CONTEXT_MENU); | ||||
|   } | ||||
|   contextMenuPosition.value = { | ||||
|     x: e.pageX || e.clientX, | ||||
|     y: e.pageY || e.clientY, | ||||
|   }; | ||||
|   showContextMenu.value = true; | ||||
| } | ||||
|  | ||||
| function closeContextMenu() { | ||||
|   showContextMenu.value = false; | ||||
|   contextMenuPosition.value = { x: null, y: null }; | ||||
| } | ||||
|  | ||||
| function handleReplyTo() { | ||||
|   const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO; | ||||
|   const { conversationId, id: replyTo } = props; | ||||
|  | ||||
|   LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo); | ||||
|   emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, props); | ||||
| } | ||||
|  | ||||
| provideMessageContext({ | ||||
|   ...toRefs(props), | ||||
|   isPrivate: computed(() => props.private), | ||||
|   variant, | ||||
|   inReplyTo: props.inReplyTo, | ||||
|   orientation, | ||||
|   isMyMessage, | ||||
|   shouldGroupWithNext, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     :id="`message${props.id}`" | ||||
|     class="flex w-full" | ||||
|     :data-message-id="props.id" | ||||
|     :class="[flexOrientationClass, shouldGroupWithNext ? 'mb-2' : 'mb-4']" | ||||
| @@ -324,12 +396,13 @@ provideMessageContext({ | ||||
|         /> | ||||
|       </div> | ||||
|       <div | ||||
|         class="[grid-area:bubble]" | ||||
|         class="[grid-area:bubble] flex" | ||||
|         :class="{ | ||||
|           'pl-9': ORIENTATION.RIGHT === orientation, | ||||
|           'pl-9 justify-end': orientation === ORIENTATION.RIGHT, | ||||
|         }" | ||||
|         @contextmenu="openContextMenu($event)" | ||||
|       > | ||||
|         <Component :is="componentToRender" v-bind="props" /> | ||||
|         <Component :is="componentToRender" /> | ||||
|       </div> | ||||
|       <MessageError | ||||
|         v-if="contentAttributes.externalError" | ||||
| @@ -337,15 +410,18 @@ provideMessageContext({ | ||||
|         :class="flexOrientationClass" | ||||
|         :error="contentAttributes.externalError" | ||||
|       /> | ||||
|       <MessageMeta | ||||
|         v-else-if="!shouldGroupWithNext" | ||||
|         class="[grid-area:meta]" | ||||
|         :class="flexOrientationClass" | ||||
|         :sender="props.sender" | ||||
|         :status="props.status" | ||||
|         :private="props.private" | ||||
|         :is-my-message="isMyMessage" | ||||
|         :created-at="props.createdAt" | ||||
|     </div> | ||||
|     <div v-if="shouldShowContextMenu" class="context-menu-wrap"> | ||||
|       <ContextMenu | ||||
|         v-if="isBubble && !isMessageDeleted" | ||||
|         :context-menu-position="contextMenuPosition" | ||||
|         :is-open="showContextMenu" | ||||
|         :enabled-options="contextMenuEnabledOptions" | ||||
|         :message="payloadForContextMenu" | ||||
|         hide-button | ||||
|         @open="openContextMenu" | ||||
|         @close="closeContextMenu" | ||||
|         @reply-to="handleReplyTo" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
							
								
								
									
										124
									
								
								app/javascript/dashboard/components-next/message/MessageList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								app/javascript/dashboard/components-next/message/MessageList.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| <script setup> | ||||
| import { defineProps, computed } from 'vue'; | ||||
| import Message from './Message.vue'; | ||||
| import { useCamelCase } from 'dashboard/composables/useTransformKeys'; | ||||
|  | ||||
| /** | ||||
|  * Props definition for the component | ||||
|  * @typedef {Object} Props | ||||
|  * @property {Array} readMessages - Array of read messages | ||||
|  * @property {Array} unReadMessages - Array of unread messages | ||||
|  * @property {Number} currentUserId - ID of the current user | ||||
|  * @property {Boolean} isAnEmailChannel - Whether this is an email channel | ||||
|  * @property {Object} inboxSupportsReplyTo - Inbox reply support configuration | ||||
|  * @property {Array} messages - Array of all messages | ||||
|  */ | ||||
| const props = defineProps({ | ||||
|   readMessages: { | ||||
|     type: Array, | ||||
|     default: () => [], | ||||
|   }, | ||||
|   unReadMessages: { | ||||
|     type: Array, | ||||
|     default: () => [], | ||||
|   }, | ||||
|   currentUserId: { | ||||
|     type: Number, | ||||
|     required: true, | ||||
|   }, | ||||
|   isAnEmailChannel: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
|   inboxSupportsReplyTo: { | ||||
|     type: Object, | ||||
|     default: () => ({ incoming: false, outgoing: false }), | ||||
|   }, | ||||
|   messages: { | ||||
|     type: Array, | ||||
|     default: () => [], | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const unread = computed(() => { | ||||
|   return useCamelCase(props.unReadMessages, { deep: true }); | ||||
| }); | ||||
|  | ||||
| const read = computed(() => { | ||||
|   return useCamelCase(props.readMessages, { deep: true }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * Determines if a message should be grouped with the next message | ||||
|  * @param {Number} index - Index of the current message | ||||
|  * @param {Array} messages - Array of messages to check | ||||
|  * @returns {Boolean} - Whether the message should be grouped with next | ||||
|  */ | ||||
| const shouldGroupWithNext = (index, messages) => { | ||||
|   if (index === messages.length - 1) return false; | ||||
|  | ||||
|   const current = messages[index]; | ||||
|   const next = messages[index + 1]; | ||||
|  | ||||
|   if (next.status === 'failed') return false; | ||||
|  | ||||
|   const nextSenderId = next.senderId ?? next.sender?.id; | ||||
|   const currentSenderId = current.senderId ?? current.sender?.id; | ||||
|   if (currentSenderId !== nextSenderId) return false; | ||||
|  | ||||
|   // Check if messages are in the same minute by rounding down to nearest minute | ||||
|   return Math.floor(next.createdAt / 60) === Math.floor(current.createdAt / 60); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Gets the message that was replied to | ||||
|  * @param {Object} parentMessage - The message containing the reply reference | ||||
|  * @returns {Object|null} - The message being replied to, or null if not found | ||||
|  */ | ||||
| const getInReplyToMessage = parentMessage => { | ||||
|   if (!parentMessage) return null; | ||||
|  | ||||
|   const inReplyToMessageId = | ||||
|     parentMessage.contentAttributes?.inReplyTo ?? | ||||
|     parentMessage.content_attributes?.in_reply_to; | ||||
|  | ||||
|   if (!inReplyToMessageId) return null; | ||||
|  | ||||
|   // Find in-reply-to message in the messages prop | ||||
|   const replyMessage = props.messages?.find( | ||||
|     message => message.id === inReplyToMessageId | ||||
|   ); | ||||
|  | ||||
|   return replyMessage ? useCamelCase(replyMessage) : null; | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <ul class="px-4 bg-n-background"> | ||||
|     <slot name="beforeAll" /> | ||||
|     <template v-for="(message, index) in read" :key="message.id"> | ||||
|       <Message | ||||
|         v-bind="message" | ||||
|         :is-email-inbox="isAnEmailChannel" | ||||
|         :in-reply-to="getInReplyToMessage(message)" | ||||
|         :group-with-next="shouldGroupWithNext(index, readMessages)" | ||||
|         :inbox-supports-reply-to="inboxSupportsReplyTo" | ||||
|         :current-user-id="currentUserId" | ||||
|         data-clarity-mask="True" | ||||
|       /> | ||||
|     </template> | ||||
|     <slot name="beforeUnread" /> | ||||
|     <template v-for="(message, index) in unread" :key="message.id"> | ||||
|       <Message | ||||
|         v-bind="message" | ||||
|         :in-reply-to="getInReplyToMessage(message)" | ||||
|         :group-with-next="shouldGroupWithNext(index, unReadMessages)" | ||||
|         :inbox-supports-reply-to="inboxSupportsReplyTo" | ||||
|         :current-user-id="currentUserId" | ||||
|         :is-email-inbox="isAnEmailChannel" | ||||
|         data-clarity-mask="True" | ||||
|       /> | ||||
|     </template> | ||||
|     <slot name="after" /> | ||||
|   </ul> | ||||
| </template> | ||||
| @@ -4,74 +4,116 @@ import { messageTimestamp } from 'shared/helpers/timeHelper'; | ||||
|  | ||||
| import MessageStatus from './MessageStatus.vue'; | ||||
| import Icon from 'next/icon/Icon.vue'; | ||||
| import { useInbox } from 'dashboard/composables/useInbox'; | ||||
| import { useMessageContext } from './provider.js'; | ||||
|  | ||||
| import { MESSAGE_STATUS } from './constants'; | ||||
| import { MESSAGE_STATUS, MESSAGE_TYPES } from './constants'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Sender | ||||
|  * @property {Object} additional_attributes - Additional attributes of the sender | ||||
|  * @property {Object} custom_attributes - Custom attributes of the sender | ||||
|  * @property {string} email - Email of the sender | ||||
|  * @property {number} id - ID of the sender | ||||
|  * @property {string|null} identifier - Identifier of the sender | ||||
|  * @property {string} name - Name of the sender | ||||
|  * @property {string|null} phone_number - Phone number of the sender | ||||
|  * @property {string} thumbnail - Thumbnail URL of the sender | ||||
|  * @property {string} type - Type of sender | ||||
|  */ | ||||
| const { | ||||
|   isAFacebookInbox, | ||||
|   isALineChannel, | ||||
|   isAPIInbox, | ||||
|   isASmsInbox, | ||||
|   isATelegramChannel, | ||||
|   isATwilioChannel, | ||||
|   isAWebWidgetInbox, | ||||
|   isAWhatsAppChannel, | ||||
|   isAnEmailChannel, | ||||
| } = useInbox(); | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Props | ||||
|  * @property {('sent'|'delivered'|'read'|'failed')} status - The delivery status of the message | ||||
|  * @property {boolean} [private=false] - Whether the message is private | ||||
|  * @property {isMyMessage} [private=false] - Whether the message is sent by the current user or not | ||||
|  * @property {number} createdAt - Timestamp when the message was created | ||||
|  * @property {Sender|null} [sender=null] - The sender information | ||||
|  */ | ||||
|  | ||||
| const props = defineProps({ | ||||
|   sender: { | ||||
|     type: Object, | ||||
|     required: true, | ||||
|   }, | ||||
|   status: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|     validator: value => Object.values(MESSAGE_STATUS).includes(value), | ||||
|   }, | ||||
|   private: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
|   isMyMessage: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
|   createdAt: { | ||||
|     type: Number, | ||||
|     required: true, | ||||
|   }, | ||||
| }); | ||||
| const { status, isPrivate, createdAt, sourceId, messageType } = | ||||
|   useMessageContext(); | ||||
|  | ||||
| const readableTime = computed(() => | ||||
|   messageTimestamp(props.createdAt, 'LLL d, h:mm a') | ||||
|   messageTimestamp(createdAt.value, 'LLL d, h:mm a') | ||||
| ); | ||||
|  | ||||
| const showSender = computed(() => !props.isMyMessage && props.sender); | ||||
| const showStatusIndicator = computed(() => { | ||||
|   if (isPrivate.value) return false; | ||||
|   if (messageType.value === MESSAGE_TYPES.OUTGOING) return true; | ||||
|   if (messageType.value === MESSAGE_TYPES.TEMPLATE) return true; | ||||
|  | ||||
|   return false; | ||||
| }); | ||||
|  | ||||
| const isSent = computed(() => { | ||||
|   if (!showStatusIndicator.value) return false; | ||||
|  | ||||
|   // Messages will be marked as sent for the Email channel if they have a source ID. | ||||
|   if (isAnEmailChannel.value) return !!sourceId.value; | ||||
|  | ||||
|   if ( | ||||
|     isAWhatsAppChannel.value || | ||||
|     isATwilioChannel.value || | ||||
|     isAFacebookInbox.value || | ||||
|     isASmsInbox.value || | ||||
|     isATelegramChannel.value | ||||
|   ) { | ||||
|     return sourceId.value && status.value === MESSAGE_STATUS.SENT; | ||||
|   } | ||||
|  | ||||
|   // All messages will be mark as sent for the Line channel, as there is no source ID. | ||||
|   if (isALineChannel.value) return true; | ||||
|  | ||||
|   return false; | ||||
| }); | ||||
|  | ||||
| const isDelivered = computed(() => { | ||||
|   if (!showStatusIndicator.value) return false; | ||||
|  | ||||
|   if ( | ||||
|     isAWhatsAppChannel.value || | ||||
|     isATwilioChannel.value || | ||||
|     isASmsInbox.value || | ||||
|     isAFacebookInbox.value | ||||
|   ) { | ||||
|     return sourceId.value && status.value === MESSAGE_STATUS.DELIVERED; | ||||
|   } | ||||
|   // All messages marked as delivered for the web widget inbox and API inbox once they are sent. | ||||
|   if (isAWebWidgetInbox.value || isAPIInbox.value) { | ||||
|     return status.value === MESSAGE_STATUS.SENT; | ||||
|   } | ||||
|   if (isALineChannel.value) { | ||||
|     return status.value === MESSAGE_STATUS.DELIVERED; | ||||
|   } | ||||
|  | ||||
|   return false; | ||||
| }); | ||||
|  | ||||
| const isRead = computed(() => { | ||||
|   if (!showStatusIndicator.value) return false; | ||||
|  | ||||
|   if ( | ||||
|     isAWhatsAppChannel.value || | ||||
|     isATwilioChannel.value || | ||||
|     isAFacebookInbox.value | ||||
|   ) { | ||||
|     return sourceId.value && status.value === MESSAGE_STATUS.READ; | ||||
|   } | ||||
|  | ||||
|   if (isAWebWidgetInbox.value || isAPIInbox.value) { | ||||
|     return status.value === MESSAGE_STATUS.READ; | ||||
|   } | ||||
|  | ||||
|   return false; | ||||
| }); | ||||
|  | ||||
| const statusToShow = computed(() => { | ||||
|   if (isRead.value) return MESSAGE_STATUS.READ; | ||||
|   if (isDelivered.value) return MESSAGE_STATUS.DELIVERED; | ||||
|   if (isSent.value) return MESSAGE_STATUS.SENT; | ||||
|  | ||||
|   return MESSAGE_STATUS.PROGRESS; | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div class="text-xs text-n-slate-11 flex items-center gap-1.5"> | ||||
|   <div class="text-xs flex items-center gap-1.5"> | ||||
|     <div class="inline"> | ||||
|       <span v-if="showSender" class="inline capitalize">{{ sender.name }}</span> | ||||
|       <span v-if="showSender && readableTime" class="inline"> • </span> | ||||
|       <span class="inline">{{ readableTime }}</span> | ||||
|     </div> | ||||
|     <Icon | ||||
|       v-if="props.private" | ||||
|       icon="i-lucide-lock-keyhole" | ||||
|       class="text-n-slate-10 size-3" | ||||
|     /> | ||||
|     <MessageStatus v-if="props.isMyMessage" :status /> | ||||
|     <Icon v-if="isPrivate" icon="i-lucide-lock-keyhole" class="size-3" /> | ||||
|     <MessageStatus v-if="showStatusIndicator" :status="statusToShow" /> | ||||
|   </div> | ||||
| </template> | ||||
| ` | ||||
|   | ||||
| @@ -1,16 +1,25 @@ | ||||
| <script setup> | ||||
| import { computed } from 'vue'; | ||||
| import { messageTimestamp } from 'shared/helpers/timeHelper'; | ||||
| import BaseBubble from './Base.vue'; | ||||
| import { useMessageContext } from '../provider.js'; | ||||
|  | ||||
| defineProps({ | ||||
|   content: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|   }, | ||||
| }); | ||||
| const { content, createdAt } = useMessageContext(); | ||||
|  | ||||
| const readableTime = computed(() => | ||||
|   messageTimestamp(createdAt.value, 'LLL d, h:mm a') | ||||
| ); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <BaseBubble class="px-2 py-0.5" data-bubble-name="activity"> | ||||
|   <BaseBubble | ||||
|     class="px-2 py-0.5 !rounded-full flex items-center gap-2" | ||||
|     data-bubble-name="activity" | ||||
|   > | ||||
|     <span v-dompurify-html="content" /> | ||||
|     <div v-if="readableTime" class="w-px h-3 rounded-full bg-n-slate-7" /> | ||||
|     <span class="text-n-slate-10"> | ||||
|       {{ readableTime }} | ||||
|     </span> | ||||
|   </BaseBubble> | ||||
| </template> | ||||
|   | ||||
| @@ -1,30 +0,0 @@ | ||||
| <script setup> | ||||
| import BaseBubble from 'next/message/bubbles/Base.vue'; | ||||
| import AttachmentChips from 'next/message/chips/AttachmentChips.vue'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
|  * @property {number} id - Unique identifier for the attachment | ||||
|  * @property {number} messageId - ID of the associated message | ||||
|  * @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image) | ||||
|  * @property {number} accountId - ID of the associated account | ||||
|  * @property {string|null} extension - File extension | ||||
|  * @property {string} dataUrl - URL to access the full attachment data | ||||
|  * @property {string} thumbUrl - URL to access the thumbnail version | ||||
|  * @property {number} fileSize - Size of the file in bytes | ||||
|  * @property {number|null} width - Width of the image if applicable | ||||
|  * @property {number|null} height - Height of the image if applicable | ||||
|  */ | ||||
| defineProps({ | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     default: () => [], | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <BaseBubble class="grid gap-2 bg-transparent" data-bubble-name="attachments"> | ||||
|     <AttachmentChips :attachments="attachments" class="gap-1" /> | ||||
|   </BaseBubble> | ||||
| </template> | ||||
| @@ -2,43 +2,17 @@ | ||||
| import { computed } from 'vue'; | ||||
| import BaseBubble from './Base.vue'; | ||||
| import AudioChip from 'next/message/chips/Audio.vue'; | ||||
| import { useMessageContext } from '../provider.js'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
|  * @property {number} id - Unique identifier for the attachment | ||||
|  * @property {number} messageId - ID of the associated message | ||||
|  * @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image) | ||||
|  * @property {number} accountId - ID of the associated account | ||||
|  * @property {string|null} extension - File extension | ||||
|  * @property {string} dataUrl - URL to access the full attachment data | ||||
|  * @property {string} thumbUrl - URL to access the thumbnail version | ||||
|  * @property {number} fileSize - Size of the file in bytes | ||||
|  * @property {number|null} width - Width of the image if applicable | ||||
|  * @property {number|null} height - Height of the image if applicable | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Props | ||||
|  * @property {Attachment[]} [attachments=[]] - The attachments associated with the message | ||||
|  */ | ||||
|  | ||||
| const props = defineProps({ | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     required: true, | ||||
|   }, | ||||
| }); | ||||
| const { attachments } = useMessageContext(); | ||||
|  | ||||
| const attachment = computed(() => { | ||||
|   return props.attachments[0]; | ||||
|   return attachments.value[0]; | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <BaseBubble class="bg-transparent" data-bubble-name="audio"> | ||||
|     <AudioChip | ||||
|       :attachment="attachment" | ||||
|       class="p-2 text-n-slate-12 bg-n-alpha-3" | ||||
|     /> | ||||
|     <AudioChip :attachment="attachment" class="p-2 text-n-slate-12" /> | ||||
|   </BaseBubble> | ||||
| </template> | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| <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'; | ||||
| @@ -8,14 +10,15 @@ import { useI18n } from 'vue-i18n'; | ||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||
| import { MESSAGE_VARIANTS, ORIENTATION } from '../constants'; | ||||
|  | ||||
| const { variant, orientation, inReplyTo } = useMessageContext(); | ||||
| 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.USER]: 'bg-n-gray-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', | ||||
| @@ -31,6 +34,16 @@ const orientationMap = { | ||||
|   [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]]; | ||||
|  | ||||
| @@ -72,7 +85,7 @@ const previewMessage = computed(() => { | ||||
|     :class="[ | ||||
|       messageClass, | ||||
|       { | ||||
|         'max-w-md': variant !== MESSAGE_VARIANTS.EMAIL, | ||||
|         'max-w-lg': variant !== MESSAGE_VARIANTS.EMAIL, | ||||
|       }, | ||||
|     ]" | ||||
|   > | ||||
| @@ -86,5 +99,16 @@ const previewMessage = computed(() => { | ||||
|       </span> | ||||
|     </div> | ||||
|     <slot /> | ||||
|     <MessageMeta | ||||
|       v-if="!shouldGroupWithNext && variant !== MESSAGE_VARIANTS.ACTIVITY" | ||||
|       :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> | ||||
|   | ||||
| @@ -3,11 +3,11 @@ import { computed } from 'vue'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
| import BaseBubble from './Base.vue'; | ||||
| import Icon from 'next/icon/Icon.vue'; | ||||
| import { useMessageContext } from '../provider.js'; | ||||
|  | ||||
| const props = defineProps({ | ||||
| defineProps({ | ||||
|   icon: { type: [String, Object], required: true }, | ||||
|   iconBgColor: { type: String, default: 'bg-n-alpha-3' }, | ||||
|   sender: { type: Object, default: () => ({}) }, | ||||
|   senderTranslationKey: { type: String, required: true }, | ||||
|   content: { type: String, required: true }, | ||||
|   action: { | ||||
| @@ -19,20 +19,21 @@ const props = defineProps({ | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const { sender } = useMessageContext(); | ||||
| const { t } = useI18n(); | ||||
|  | ||||
| const senderName = computed(() => { | ||||
|   return props.sender.name; | ||||
|   return sender?.value.name; | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <BaseBubble | ||||
|     class="overflow-hidden grid gap-4 min-w-64 p-0" | ||||
|     class="overflow-hidden p-3 !bg-n-solid-2 shadow-[0px_0px_12px_0px_rgba(0,0,0,0.05)]" | ||||
|     data-bubble-name="attachment" | ||||
|   > | ||||
|     <slot name="before" /> | ||||
|     <div class="grid gap-3 px-3 pt-3 z-20"> | ||||
|     <div class="grid gap-4 min-w-64"> | ||||
|       <div class="grid gap-3 z-20"> | ||||
|         <div | ||||
|           class="size-8 rounded-lg grid place-content-center" | ||||
|           :class="iconBgColor" | ||||
| @@ -56,23 +57,24 @@ const senderName = computed(() => { | ||||
|           </slot> | ||||
|         </div> | ||||
|       </div> | ||||
|     <div v-if="action" class="px-3 pb-3"> | ||||
|       <div v-if="action"> | ||||
|         <a | ||||
|           v-if="action.href" | ||||
|           :href="action.href" | ||||
|           rel="noreferrer noopener nofollow" | ||||
|           target="_blank" | ||||
|         class="w-full block bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center" | ||||
|           class="w-full block bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center border border-n-container" | ||||
|         > | ||||
|           {{ action.label }} | ||||
|         </a> | ||||
|         <button | ||||
|           v-else | ||||
|         class="w-full bg-n-solid-3 px-4 py-2 rounded-lg text-sm" | ||||
|           class="w-full bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center border border-n-container" | ||||
|           @click="action.onClick" | ||||
|         > | ||||
|           {{ action.label }} | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </BaseBubble> | ||||
| </template> | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { computed } from 'vue'; | ||||
| import { useAlert } from 'dashboard/composables'; | ||||
| import { useStore } from 'dashboard/composables/store'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
| import { useMessageContext } from '../provider.js'; | ||||
| import BaseAttachmentBubble from './BaseAttachment.vue'; | ||||
|  | ||||
| import { | ||||
| @@ -10,45 +11,13 @@ import { | ||||
|   ExceptionWithMessage, | ||||
| } from 'shared/helpers/CustomErrors'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
|  * @property {number} id - Unique identifier for the attachment | ||||
|  * @property {number} messageId - ID of the associated message | ||||
|  * @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image) | ||||
|  * @property {number} accountId - ID of the associated account | ||||
|  * @property {string|null} extension - File extension | ||||
|  * @property {string} dataUrl - URL to access the full attachment data | ||||
|  * @property {string} thumbUrl - URL to access the thumbnail version | ||||
|  * @property {number} fileSize - Size of the file in bytes | ||||
|  * @property {number|null} width - Width of the image if applicable | ||||
|  * @property {number|null} height - Height of the image if applicable | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Props | ||||
|  * @property {Attachment[]} [attachments=[]] - The attachments associated with the message | ||||
|  */ | ||||
|  | ||||
| const props = defineProps({ | ||||
|   content: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|   }, | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     required: true, | ||||
|   }, | ||||
|   sender: { | ||||
|     type: Object, | ||||
|     default: () => ({}), | ||||
|   }, | ||||
| }); | ||||
| const { content, attachments } = useMessageContext(); | ||||
|  | ||||
| const $store = useStore(); | ||||
| const { t } = useI18n(); | ||||
|  | ||||
| const attachment = computed(() => { | ||||
|   return props.attachments[0]; | ||||
|   return attachments.value[0]; | ||||
| }); | ||||
|  | ||||
| const phoneNumber = computed(() => { | ||||
| @@ -64,7 +33,7 @@ const rawPhoneNumber = computed(() => { | ||||
| }); | ||||
|  | ||||
| const name = computed(() => { | ||||
|   return props.content; | ||||
|   return content.value; | ||||
| }); | ||||
|  | ||||
| function getContactObject() { | ||||
| @@ -129,7 +98,6 @@ const action = computed(() => ({ | ||||
|   <BaseAttachmentBubble | ||||
|     icon="i-teenyicons-user-circle-solid" | ||||
|     icon-bg-color="bg-[#D6409F]" | ||||
|     :sender="sender" | ||||
|     sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.CONTACT" | ||||
|     :content="phoneNumber" | ||||
|     :action="formattedPhoneNumber ? action : null" | ||||
|   | ||||
| @@ -5,23 +5,16 @@ import { buildDyteURL } from 'shared/helpers/IntegrationHelper'; | ||||
| import { useCamelCase } from 'dashboard/composables/useTransformKeys'; | ||||
| import { useAlert } from 'dashboard/composables'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
|  | ||||
| import { useMessageContext } from '../provider.js'; | ||||
| import BaseAttachmentBubble from './BaseAttachment.vue'; | ||||
|  | ||||
| const props = defineProps({ | ||||
|   contentAttributes: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|   }, | ||||
|   sender: { | ||||
|     type: Object, | ||||
|     default: () => ({}), | ||||
|   }, | ||||
| }); | ||||
| const { contentAttributes } = useMessageContext(); | ||||
|  | ||||
| const { t } = useI18n(); | ||||
|  | ||||
| const meetingData = computed(() => { | ||||
|   return useCamelCase(props.contentAttributes.data); | ||||
|   return useCamelCase(contentAttributes.value.data); | ||||
| }); | ||||
|  | ||||
| const isLoading = ref(false); | ||||
| @@ -57,7 +50,6 @@ const action = computed(() => ({ | ||||
|   <BaseAttachmentBubble | ||||
|     icon="i-ph-video-camera-fill" | ||||
|     icon-bg-color="bg-[#2781F6]" | ||||
|     :sender="sender" | ||||
|     sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.MEETING" | ||||
|     :action="action" | ||||
|   > | ||||
|   | ||||
| @@ -1,57 +1,44 @@ | ||||
| <script setup> | ||||
| import { computed } from 'vue'; | ||||
| import { MESSAGE_STATUS } from '../../constants'; | ||||
| import { useMessageContext } from '../../provider.js'; | ||||
|  | ||||
| const props = defineProps({ | ||||
|   contentAttributes: { | ||||
|     type: Object, | ||||
|     default: () => ({}), | ||||
|   }, | ||||
|   status: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|     validator: value => Object.values(MESSAGE_STATUS).includes(value), | ||||
|   }, | ||||
|   sender: { | ||||
|     type: Object, | ||||
|     default: () => ({}), | ||||
|   }, | ||||
| }); | ||||
| const { contentAttributes, status, sender } = useMessageContext(); | ||||
|  | ||||
| const hasError = computed(() => { | ||||
|   return props.status === MESSAGE_STATUS.FAILED; | ||||
|   return status.value === MESSAGE_STATUS.FAILED; | ||||
| }); | ||||
|  | ||||
| const fromEmail = computed(() => { | ||||
|   return props.contentAttributes?.email?.from ?? []; | ||||
|   return contentAttributes.value?.email?.from ?? []; | ||||
| }); | ||||
|  | ||||
| const toEmail = computed(() => { | ||||
|   return props.contentAttributes?.email?.to ?? []; | ||||
|   return contentAttributes.value?.email?.to ?? []; | ||||
| }); | ||||
|  | ||||
| const ccEmail = computed(() => { | ||||
|   return ( | ||||
|     props.contentAttributes?.ccEmails ?? | ||||
|     props.contentAttributes?.email?.cc ?? | ||||
|     contentAttributes.value?.ccEmails ?? | ||||
|     contentAttributes.value?.email?.cc ?? | ||||
|     [] | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| const senderName = computed(() => { | ||||
|   return props.sender.name ?? ''; | ||||
|   return sender.value.name ?? ''; | ||||
| }); | ||||
|  | ||||
| const bccEmail = computed(() => { | ||||
|   return ( | ||||
|     props.contentAttributes?.bccEmails ?? | ||||
|     props.contentAttributes?.email?.bcc ?? | ||||
|     contentAttributes.value?.bccEmails ?? | ||||
|     contentAttributes.value?.email?.bcc ?? | ||||
|     [] | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| const subject = computed(() => { | ||||
|   return props.contentAttributes?.email?.subject ?? ''; | ||||
|   return contentAttributes.value?.email?.subject ?? ''; | ||||
| }); | ||||
|  | ||||
| const showMeta = computed(() => { | ||||
|   | ||||
| @@ -7,37 +7,13 @@ import { EmailQuoteExtractor } from './removeReply.js'; | ||||
| import BaseBubble from 'next/message/bubbles/Base.vue'; | ||||
| import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue'; | ||||
| import AttachmentChips from 'next/message/chips/AttachmentChips.vue'; | ||||
|  | ||||
| import EmailMeta from './EmailMeta.vue'; | ||||
| import { MESSAGE_STATUS, MESSAGE_TYPES } from '../../constants'; | ||||
|  | ||||
| const props = defineProps({ | ||||
|   content: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|   }, | ||||
|   contentAttributes: { | ||||
|     type: Object, | ||||
|     default: () => ({}), | ||||
|   }, | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     default: () => [], | ||||
|   }, | ||||
|   status: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|     validator: value => Object.values(MESSAGE_STATUS).includes(value), | ||||
|   }, | ||||
|   sender: { | ||||
|     type: Object, | ||||
|     default: () => ({}), | ||||
|   }, | ||||
|   messageType: { | ||||
|     type: Number, | ||||
|     required: true, | ||||
|   }, | ||||
| }); | ||||
| import { useMessageContext } from '../../provider.js'; | ||||
| import { MESSAGE_TYPES } from 'next/message/constants.js'; | ||||
|  | ||||
| const { content, contentAttributes, attachments, messageType } = | ||||
|   useMessageContext(); | ||||
|  | ||||
| const isExpandable = ref(false); | ||||
| const isExpanded = ref(false); | ||||
| @@ -49,11 +25,11 @@ onMounted(() => { | ||||
| }); | ||||
|  | ||||
| const isOutgoing = computed(() => { | ||||
|   return props.messageType === MESSAGE_TYPES.OUTGOING; | ||||
|   return messageType.value === MESSAGE_TYPES.OUTGOING; | ||||
| }); | ||||
|  | ||||
| const fullHTML = computed(() => { | ||||
|   return props.contentAttributes?.email?.htmlContent?.full ?? props.content; | ||||
|   return contentAttributes?.value?.email?.htmlContent?.full ?? content.value; | ||||
| }); | ||||
|  | ||||
| const unquotedHTML = computed(() => { | ||||
| @@ -66,14 +42,14 @@ const hasQuotedMessage = computed(() => { | ||||
|  | ||||
| const textToShow = computed(() => { | ||||
|   const text = | ||||
|     props.contentAttributes?.email?.textContent?.full ?? props.content; | ||||
|   return text.replace(/\n/g, '<br>'); | ||||
|     contentAttributes?.value?.email?.textContent?.full ?? content.value; | ||||
|   return text?.replace(/\n/g, '<br>'); | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <BaseBubble class="w-full overflow-hidden" data-bubble-name="email"> | ||||
|     <EmailMeta :status :sender :content-attributes /> | ||||
|     <EmailMeta /> | ||||
|     <section | ||||
|       ref="contentContainer" | ||||
|       class="p-4" | ||||
|   | ||||
| @@ -2,43 +2,16 @@ | ||||
| import { computed } from 'vue'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
|  | ||||
| import { useMessageContext } from '../provider.js'; | ||||
| import BaseAttachmentBubble from './BaseAttachment.vue'; | ||||
| import FileIcon from 'next/icon/FileIcon.vue'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
|  * @property {number} id - Unique identifier for the attachment | ||||
|  * @property {number} messageId - ID of the associated message | ||||
|  * @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image) | ||||
|  * @property {number} accountId - ID of the associated account | ||||
|  * @property {string|null} extension - File extension | ||||
|  * @property {string} dataUrl - URL to access the full attachment data | ||||
|  * @property {string} thumbUrl - URL to access the thumbnail version | ||||
|  * @property {number} fileSize - Size of the file in bytes | ||||
|  * @property {number|null} width - Width of the image if applicable | ||||
|  * @property {number|null} height - Height of the image if applicable | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Props | ||||
|  * @property {Attachment[]} [attachments=[]] - The attachments associated with the message | ||||
|  */ | ||||
|  | ||||
| const props = defineProps({ | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     required: true, | ||||
|   }, | ||||
|   sender: { | ||||
|     type: Object, | ||||
|     default: () => ({}), | ||||
|   }, | ||||
| }); | ||||
| const { attachments } = useMessageContext(); | ||||
|  | ||||
| const { t } = useI18n(); | ||||
|  | ||||
| const url = computed(() => { | ||||
|   return props.attachments[0].dataUrl; | ||||
|   return attachments.value[0].dataUrl; | ||||
| }); | ||||
|  | ||||
| const fileName = computed(() => { | ||||
| @@ -58,7 +31,6 @@ const fileType = computed(() => { | ||||
|   <BaseAttachmentBubble | ||||
|     icon="i-teenyicons-user-circle-solid" | ||||
|     icon-bg-color="bg-n-alpha-3 dark:bg-n-alpha-white" | ||||
|     :sender="sender" | ||||
|     sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.FILE" | ||||
|     :content="decodeURI(fileName)" | ||||
|     :action="{ | ||||
|   | ||||
| @@ -4,44 +4,18 @@ import BaseBubble from './Base.vue'; | ||||
| import Button from 'next/button/Button.vue'; | ||||
| import Icon from 'next/icon/Icon.vue'; | ||||
| import { useSnakeCase } from 'dashboard/composables/useTransformKeys'; | ||||
| import { useMessageContext } from 'next/message/provider.js'; | ||||
| import { useMessageContext } from '../provider.js'; | ||||
| import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
|  * @property {number} id - Unique identifier for the attachment | ||||
|  * @property {number} messageId - ID of the associated message | ||||
|  * @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image) | ||||
|  * @property {number} accountId - ID of the associated account | ||||
|  * @property {string|null} extension - File extension | ||||
|  * @property {string} dataUrl - URL to access the full attachment data | ||||
|  * @property {string} thumbUrl - URL to access the thumbnail version | ||||
|  * @property {number} fileSize - Size of the file in bytes | ||||
|  * @property {number|null} width - Width of the image if applicable | ||||
|  * @property {number|null} height - Height of the image if applicable | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Props | ||||
|  * @property {Attachment[]} [attachments=[]] - The attachments associated with the message | ||||
|  */ | ||||
|  | ||||
| const props = defineProps({ | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     required: true, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const emit = defineEmits(['error']); | ||||
| const { filteredCurrentChatAttachments, attachments } = useMessageContext(); | ||||
|  | ||||
| const attachment = computed(() => { | ||||
|   return props.attachments[0]; | ||||
|   return attachments.value[0]; | ||||
| }); | ||||
|  | ||||
| const hasError = ref(false); | ||||
| const showGallery = ref(false); | ||||
| const { filteredCurrentChatAttachments } = useMessageContext(); | ||||
|  | ||||
| const handleError = () => { | ||||
|   hasError.value = true; | ||||
| @@ -64,20 +38,17 @@ const downloadAttachment = async () => { | ||||
|  | ||||
| <template> | ||||
|   <BaseBubble | ||||
|     class="overflow-hidden relative group border-[4px] border-n-weak" | ||||
|     class="overflow-hidden p-3" | ||||
|     data-bubble-name="image" | ||||
|     @click="showGallery = true" | ||||
|   > | ||||
|     <div | ||||
|       v-if="hasError" | ||||
|       class="flex items-center gap-1 px-5 py-4 text-center rounded-lg bg-n-alpha-1" | ||||
|     > | ||||
|     <div v-if="hasError" class="flex items-center gap-1 text-center rounded-lg"> | ||||
|       <Icon icon="i-lucide-circle-off" class="text-n-slate-11" /> | ||||
|       <p class="mb-0 text-n-slate-11"> | ||||
|         {{ $t('COMPONENTS.MEDIA.IMAGE_UNAVAILABLE') }} | ||||
|       </p> | ||||
|     </div> | ||||
|     <template v-else> | ||||
|     <div v-else class="relative group rounded-lg overflow-hidden"> | ||||
|       <img | ||||
|         :src="attachment.dataUrl" | ||||
|         :width="attachment.width" | ||||
| @@ -98,7 +69,7 @@ const downloadAttachment = async () => { | ||||
|           @click="downloadAttachment" | ||||
|         /> | ||||
|       </div> | ||||
|     </template> | ||||
|     </div> | ||||
|   </BaseBubble> | ||||
|   <GalleryView | ||||
|     v-if="showGallery" | ||||
|   | ||||
| @@ -7,46 +7,22 @@ import BaseBubble from 'next/message/bubbles/Base.vue'; | ||||
| import MessageFormatter from 'shared/helpers/MessageFormatter.js'; | ||||
| import { MESSAGE_VARIANTS } from '../constants'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
|  * @property {number} id - Unique identifier for the attachment | ||||
|  * @property {number} messageId - ID of the associated message | ||||
|  * @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image) | ||||
|  * @property {number} accountId - ID of the associated account | ||||
|  * @property {string|null} extension - File extension | ||||
|  * @property {string} dataUrl - URL to access the full attachment data | ||||
|  * @property {string} thumbUrl - URL to access the thumbnail version | ||||
|  * @property {number} fileSize - Size of the file in bytes | ||||
|  * @property {number|null} width - Width of the image if applicable | ||||
|  * @property {number|null} height - Height of the image if applicable | ||||
|  */ | ||||
| const props = defineProps({ | ||||
|   content: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|   }, | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     default: () => [], | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const emit = defineEmits(['error']); | ||||
| const { variant, content, attachments } = useMessageContext(); | ||||
|  | ||||
| const attachment = computed(() => { | ||||
|   return props.attachments[0]; | ||||
|   return attachments.value[0]; | ||||
| }); | ||||
|  | ||||
| const { variant } = useMessageContext(); | ||||
| const hasImgStoryError = ref(false); | ||||
| const hasVideoStoryError = ref(false); | ||||
|  | ||||
| const formattedContent = computed(() => { | ||||
|   if (variant.value === MESSAGE_VARIANTS.ACTIVITY) { | ||||
|     return props.content; | ||||
|     return content.value; | ||||
|   } | ||||
|  | ||||
|   return new MessageFormatter(props.content).formattedMessage; | ||||
|   return new MessageFormatter(content.value).formattedMessage; | ||||
| }); | ||||
|  | ||||
| const onImageLoadError = () => { | ||||
|   | ||||
| @@ -1,43 +1,14 @@ | ||||
| <script setup> | ||||
| import { computed, onMounted, nextTick, useTemplateRef } from 'vue'; | ||||
| import { computed } from 'vue'; | ||||
| import BaseAttachmentBubble from './BaseAttachment.vue'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
| import maplibregl from 'maplibre-gl'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
|  * @property {number} id - Unique identifier for the attachment | ||||
|  * @property {number} messageId - ID of the associated message | ||||
|  * @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image) | ||||
|  * @property {number} accountId - ID of the associated account | ||||
|  * @property {string|null} extension - File extension | ||||
|  * @property {string} dataUrl - URL to access the full attachment data | ||||
|  * @property {string} thumbUrl - URL to access the thumbnail version | ||||
|  * @property {number} fileSize - Size of the file in bytes | ||||
|  * @property {number|null} width - Width of the image if applicable | ||||
|  * @property {number|null} height - Height of the image if applicable | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Props | ||||
|  * @property {Attachment[]} [attachments=[]] - The attachments associated with the message | ||||
|  */ | ||||
|  | ||||
| const props = defineProps({ | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     required: true, | ||||
|   }, | ||||
|   sender: { | ||||
|     type: Object, | ||||
|     default: () => ({}), | ||||
|   }, | ||||
| }); | ||||
| import { useMessageContext } from '../provider.js'; | ||||
|  | ||||
| const { attachments } = useMessageContext(); | ||||
| const { t } = useI18n(); | ||||
|  | ||||
| const attachment = computed(() => { | ||||
|   return props.attachments[0]; | ||||
|   return attachments.value[0]; | ||||
| }); | ||||
|  | ||||
| const lat = computed(() => { | ||||
| @@ -48,61 +19,23 @@ const long = computed(() => { | ||||
| }); | ||||
|  | ||||
| const title = computed(() => { | ||||
|   return attachment.value.fallbackTitle; | ||||
|   return attachment.value.fallbackTitle ?? attachment.value.fallback_title; | ||||
| }); | ||||
|  | ||||
| const mapUrl = computed( | ||||
|   () => `https://maps.google.com/?q=${lat.value},${long.value}` | ||||
| ); | ||||
|  | ||||
| const mapContainer = useTemplateRef('mapContainer'); | ||||
|  | ||||
| const setupMap = () => { | ||||
|   const map = new maplibregl.Map({ | ||||
|     style: 'https://tiles.openfreemap.org/styles/positron', | ||||
|     center: [long.value, lat.value], | ||||
|     zoom: 9.5, | ||||
|     container: mapContainer.value, | ||||
|     attributionControl: false, | ||||
|     dragPan: false, | ||||
|     dragRotate: false, | ||||
|     scrollZoom: false, | ||||
|     touchZoom: false, | ||||
|     touchRotate: false, | ||||
|     keyboard: false, | ||||
|     doubleClickZoom: false, | ||||
|   }); | ||||
|  | ||||
|   return map; | ||||
| }; | ||||
|  | ||||
| onMounted(async () => { | ||||
|   await nextTick(); | ||||
|   setupMap(); | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <BaseAttachmentBubble | ||||
|     icon="i-ph-navigation-arrow-fill" | ||||
|     icon-bg-color="bg-[#0D9B8A]" | ||||
|     :sender="sender" | ||||
|     sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.LOCATION" | ||||
|     :content="title" | ||||
|     :action="{ | ||||
|       label: t('COMPONENTS.LOCATION_BUBBLE.SEE_ON_MAP'), | ||||
|       href: mapUrl, | ||||
|     }" | ||||
|   > | ||||
|     <template #before> | ||||
|       <div | ||||
|         ref="mapContainer" | ||||
|         class="z-10 w-full max-w-md -mb-12 min-w-64 h-28" | ||||
|   /> | ||||
|     </template> | ||||
|   </BaseAttachmentBubble> | ||||
| </template> | ||||
|  | ||||
| <style> | ||||
| @import 'maplibre-gl/dist/maplibre-gl.css'; | ||||
| </style> | ||||
|   | ||||
| @@ -4,47 +4,25 @@ import BaseBubble from 'next/message/bubbles/Base.vue'; | ||||
| import FormattedContent from './FormattedContent.vue'; | ||||
| import AttachmentChips from 'next/message/chips/AttachmentChips.vue'; | ||||
| import { MESSAGE_TYPES } from '../../constants'; | ||||
| import { useMessageContext } from '../../provider.js'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
|  * @property {number} id - Unique identifier for the attachment | ||||
|  * @property {number} messageId - ID of the associated message | ||||
|  * @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image) | ||||
|  * @property {number} accountId - ID of the associated account | ||||
|  * @property {string|null} extension - File extension | ||||
|  * @property {string} dataUrl - URL to access the full attachment data | ||||
|  * @property {string} thumbUrl - URL to access the thumbnail version | ||||
|  * @property {number} fileSize - Size of the file in bytes | ||||
|  * @property {number|null} width - Width of the image if applicable | ||||
|  * @property {number|null} height - Height of the image if applicable | ||||
|  */ | ||||
| const props = defineProps({ | ||||
|   content: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|   }, | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     default: () => [], | ||||
|   }, | ||||
|   contentAttributes: { | ||||
|     type: Object, | ||||
|     default: () => ({}), | ||||
|   }, | ||||
|   messageType: { | ||||
|     type: Number, | ||||
|     required: true, | ||||
|     validator: value => Object.values(MESSAGE_TYPES).includes(value), | ||||
|   }, | ||||
| }); | ||||
| const { content, attachments, contentAttributes, messageType } = | ||||
|   useMessageContext(); | ||||
|  | ||||
| const isTemplate = computed(() => { | ||||
|   return props.messageType === MESSAGE_TYPES.TEMPLATE; | ||||
|   return messageType.value === MESSAGE_TYPES.TEMPLATE; | ||||
| }); | ||||
|  | ||||
| const isEmpty = computed(() => { | ||||
|   return !content.value && !attachments.value.length; | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <BaseBubble class="flex flex-col gap-3 px-4 py-3" data-bubble-name="text"> | ||||
|     <span v-if="isEmpty" class="text-n-slate-11"> | ||||
|       {{ $t('CONVERSATION.NO_CONTENT') }} | ||||
|     </span> | ||||
|     <FormattedContent v-if="content" :content="content" /> | ||||
|     <AttachmentChips :attachments="attachments" class="gap-2" /> | ||||
|     <template v-if="isTemplate"> | ||||
|   | ||||
| @@ -3,39 +3,14 @@ import { ref, computed } from 'vue'; | ||||
| import BaseBubble from './Base.vue'; | ||||
| import Icon from 'next/icon/Icon.vue'; | ||||
| import { useSnakeCase } from 'dashboard/composables/useTransformKeys'; | ||||
| import { useMessageContext } from 'next/message/provider.js'; | ||||
| import { useMessageContext } from '../provider.js'; | ||||
| import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue'; | ||||
| import { ATTACHMENT_TYPES } from '../constants'; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
|  * @property {number} id - Unique identifier for the attachment | ||||
|  * @property {number} messageId - ID of the associated message | ||||
|  * @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image) | ||||
|  * @property {number} accountId - ID of the associated account | ||||
|  * @property {string|null} extension - File extension | ||||
|  * @property {string} dataUrl - URL to access the full attachment data | ||||
|  * @property {string} thumbUrl - URL to access the thumbnail version | ||||
|  * @property {number} fileSize - Size of the file in bytes | ||||
|  * @property {number|null} width - Width of the image if applicable | ||||
|  * @property {number|null} height - Height of the image if applicable | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Props | ||||
|  * @property {Attachment[]} [attachments=[]] - The attachments associated with the message | ||||
|  */ | ||||
|  | ||||
| const props = defineProps({ | ||||
|   attachments: { | ||||
|     type: Array, | ||||
|     required: true, | ||||
|   }, | ||||
| }); | ||||
| const emit = defineEmits(['error']); | ||||
| const hasError = ref(false); | ||||
| const showGallery = ref(false); | ||||
| const { filteredCurrentChatAttachments } = useMessageContext(); | ||||
| const { filteredCurrentChatAttachments, attachments } = useMessageContext(); | ||||
|  | ||||
| const handleError = () => { | ||||
|   hasError.value = true; | ||||
| @@ -43,7 +18,7 @@ const handleError = () => { | ||||
| }; | ||||
|  | ||||
| const attachment = computed(() => { | ||||
|   return props.attachments[0]; | ||||
|   return attachments.value[0]; | ||||
| }); | ||||
|  | ||||
| const isReel = computed(() => { | ||||
| @@ -53,18 +28,20 @@ const isReel = computed(() => { | ||||
|  | ||||
| <template> | ||||
|   <BaseBubble | ||||
|     class="overflow-hidden relative group border-[4px] border-n-weak" | ||||
|     class="overflow-hidden p-3" | ||||
|     data-bubble-name="video" | ||||
|     @click="showGallery = true" | ||||
|   > | ||||
|     <div class="relative group rounded-lg overflow-hidden"> | ||||
|       <div | ||||
|         v-if="isReel" | ||||
|       class="absolute p-2 flex items-start justify-end size-12 bg-gradient-to-bl from-n-alpha-black1 to-transparent right-0" | ||||
|         class="absolute p-2 flex items-start justify-end right-0" | ||||
|       > | ||||
|       <Icon icon="i-lucide-instagram" class="text-white" /> | ||||
|         <Icon icon="i-lucide-instagram" class="text-white shadow-lg" /> | ||||
|       </div> | ||||
|       <video | ||||
|         controls | ||||
|         class="rounded-lg" | ||||
|         :src="attachment.dataUrl" | ||||
|         :class="{ | ||||
|           'max-w-48': isReel, | ||||
| @@ -72,6 +49,7 @@ const isReel = computed(() => { | ||||
|         }" | ||||
|         @error="handleError" | ||||
|       /> | ||||
|     </div> | ||||
|   </BaseBubble> | ||||
|   <GalleryView | ||||
|     v-if="showGallery" | ||||
|   | ||||
| @@ -53,7 +53,7 @@ const textColorClass = computed(() => { | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     class="h-9 bg-n-alpha-white gap-2 items-center flex px-2 rounded-lg border border-n-strong" | ||||
|     class="h-9 bg-n-alpha-white gap-2 items-center flex px-2 rounded-lg border border-n-container" | ||||
|   > | ||||
|     <FileIcon class="flex-shrink-0" :file-type="fileType" /> | ||||
|     <span class="mr-1 max-w-32 truncate" :class="textColorClass"> | ||||
|   | ||||
| @@ -5,6 +5,112 @@ import { ATTACHMENT_TYPES } from './constants'; | ||||
|  | ||||
| const MessageControl = Symbol('MessageControl'); | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Attachment | ||||
|  * @property {number} id - Unique identifier for the attachment | ||||
|  * @property {number} messageId - ID of the associated message | ||||
|  * @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image) | ||||
|  * @property {number} accountId - ID of the associated account | ||||
|  * @property {string|null} extension - File extension | ||||
|  * @property {string} dataUrl - URL to access the full attachment data | ||||
|  * @property {string} thumbUrl - URL to access the thumbnail version | ||||
|  * @property {number} fileSize - Size of the file in bytes | ||||
|  * @property {number|null} width - Width of the image if applicable | ||||
|  * @property {number|null} height - Height of the image if applicable | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Sender | ||||
|  * @property {Object} additional_attributes - Additional attributes of the sender | ||||
|  * @property {Object} custom_attributes - Custom attributes of the sender | ||||
|  * @property {string} email - Email of the sender | ||||
|  * @property {number} id - ID of the sender | ||||
|  * @property {string|null} identifier - Identifier of the sender | ||||
|  * @property {string} name - Name of the sender | ||||
|  * @property {string|null} phone_number - Phone number of the sender | ||||
|  * @property {string} thumbnail - Thumbnail URL of the sender | ||||
|  * @property {string} type - Type of sender | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} EmailContent | ||||
|  * @property {string[]|null} bcc - BCC recipients | ||||
|  * @property {string[]|null} cc - CC recipients | ||||
|  * @property {string} contentType - Content type of the email | ||||
|  * @property {string} date - Date the email was sent | ||||
|  * @property {string[]} from - From email address | ||||
|  * @property {Object} htmlContent - HTML content of the email | ||||
|  * @property {string} htmlContent.full - Full HTML content | ||||
|  * @property {string} htmlContent.reply - Reply HTML content | ||||
|  * @property {string} htmlContent.quoted - Quoted HTML content | ||||
|  * @property {string|null} inReplyTo - Message ID being replied to | ||||
|  * @property {string} messageId - Unique message identifier | ||||
|  * @property {boolean} multipart - Whether the email is multipart | ||||
|  * @property {number} numberOfAttachments - Number of attachments | ||||
|  * @property {string} subject - Email subject line | ||||
|  * @property {Object} textContent - Text content of the email | ||||
|  * @property {string} textContent.full - Full text content | ||||
|  * @property {string} textContent.reply - Reply text content | ||||
|  * @property {string} textContent.quoted - Quoted text content | ||||
|  * @property {string[]} to - To email addresses | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} ContentAttributes | ||||
|  * @property {string} externalError - an error message to be shown if the message failed to send | ||||
|  * @property {Object} [data] - Optional data object containing roomName and messageId | ||||
|  * @property {string} data.roomName - Name of the room | ||||
|  * @property {string} data.messageId - ID of the message | ||||
|  * @property {'story_mention'} [imageType] - Flag to indicate this is a story mention | ||||
|  * @property {'dyte'} [type] - Flag to indicate this is a dyte call | ||||
|  * @property {EmailContent} [email] - Email content and metadata | ||||
|  * @property {string|null} [ccEmail] - CC email addresses | ||||
|  * @property {string|null} [bccEmail] - BCC email addresses | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @typedef {'sent'|'delivered'|'read'|'failed'|'progress'} MessageStatus | ||||
|  * @typedef {'text'|'input_text'|'input_textarea'|'input_email'|'input_select'|'cards'|'form'|'article'|'incoming_email'|'input_csat'|'integrations'|'sticker'} MessageContentType | ||||
|  * @typedef {0|1|2|3} MessageType | ||||
|  * @typedef {'contact'|'user'|'Contact'|'User'} SenderType | ||||
|  * @typedef {'user'|'agent'|'activity'|'private'|'bot'|'error'|'template'|'email'|'unsupported'} MessageVariant | ||||
|  * @typedef {'left'|'center'|'right'} MessageOrientation | ||||
|  | ||||
|  * @typedef {Object} MessageContext | ||||
|  * @property {import('vue').Ref<string>} content - The message content | ||||
|  * @property {import('vue').Ref<number>} conversationId - The ID of the conversation to which the message belongs | ||||
|  * @property {import('vue').Ref<number>} createdAt - Timestamp when the message was created | ||||
|  * @property {import('vue').Ref<number>} currentUserId - The ID of the current user | ||||
|  * @property {import('vue').Ref<number>} id - The unique identifier for the message | ||||
|  * @property {import('vue').Ref<number>} inboxId - The ID of the inbox to which the message belongs | ||||
|  * @property {import('vue').Ref<boolean>} [groupWithNext=false] - Whether the message should be grouped with the next message | ||||
|  * @property {import('vue').Ref<boolean>} [isEmailInbox=false] - Whether the message is from an email inbox | ||||
|  * @property {import('vue').Ref<boolean>} [private=false] - Whether the message is private | ||||
|  * @property {import('vue').Ref<number|null>} [senderId=null] - The ID of the sender | ||||
|  * @property {import('vue').Ref<string|null>} [error=null] - Error message if the message failed to send | ||||
|  * @property {import('vue').Ref<Attachment[]>} [attachments=[]] - The attachments associated with the message | ||||
|  * @property {import('vue').Ref<ContentAttributes>} [contentAttributes={}] - Additional attributes of the message content | ||||
|  * @property {import('vue').Ref<MessageContentType>} contentType - Content type of the message | ||||
|  * @property {import('vue').Ref<MessageStatus>} status - The delivery status of the message | ||||
|  * @property {import('vue').Ref<MessageType>} messageType - The type of message (must be one of MESSAGE_TYPES) | ||||
|  * @property {import('vue').Ref<Object|null>} [inReplyTo=null] - The message to which this message is a reply | ||||
|  * @property {import('vue').Ref<SenderType>} [senderType=null] - The type of the sender | ||||
|  * @property {import('vue').Ref<Sender|null>} [sender=null] - The sender information | ||||
|  * @property {import('vue').ComputedRef<MessageOrientation>} orientation - The visual variant of the message | ||||
|  * @property {import('vue').ComputedRef<MessageVariant>} variant - The visual variant of the message | ||||
|  * @property {import('vue').ComputedRef<boolean>} isMyMessage - Does the message belong to the current user | ||||
|  * @property {import('vue').ComputedRef<boolean>} isPrivate - Proxy computed value for private | ||||
|  * @property {import('vue').ComputedRef<boolean>} shouldGroupWithNext - Should group with the next message or not, it is differnt from groupWithNext, this has a bypass for a failed message | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Retrieves the message context from the parent Message component. | ||||
|  * Must be used within a component that is a child of a Message component. | ||||
|  * | ||||
|  * @returns {MessageContext & { filteredCurrentChatAttachments: import('vue').ComputedRef<Attachment[]> }} | ||||
|  * Message context object containing message properties and computed values | ||||
|  * @throws {Error} If used outside of a Message component context | ||||
|  */ | ||||
| export function useMessageContext() { | ||||
|   const context = inject(MessageControl, null); | ||||
|   if (context === null) { | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import { useAI } from 'dashboard/composables/useAI'; | ||||
| // components | ||||
| import ReplyBox from './ReplyBox.vue'; | ||||
| import Message from './Message.vue'; | ||||
| import NextMessageList from 'next/message/MessageList.vue'; | ||||
| import ConversationLabelSuggestion from './conversation/LabelSuggestion.vue'; | ||||
| import Banner from 'dashboard/components/ui/Banner.vue'; | ||||
|  | ||||
| @@ -37,6 +38,7 @@ import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; | ||||
| export default { | ||||
|   components: { | ||||
|     Message, | ||||
|     NextMessageList, | ||||
|     ReplyBox, | ||||
|     Banner, | ||||
|     ConversationLabelSuggestion, | ||||
| @@ -80,6 +82,10 @@ export default { | ||||
|       fetchLabelSuggestions, | ||||
|     } = useAI(); | ||||
|  | ||||
|     const showNextBubbles = LocalStorage.get( | ||||
|       LOCAL_STORAGE_KEYS.USE_NEXT_BUBBLE | ||||
|     ); | ||||
|  | ||||
|     return { | ||||
|       isEnterprise, | ||||
|       isPopOutReplyBox, | ||||
| @@ -89,6 +95,7 @@ export default { | ||||
|       isLabelSuggestionFeatureEnabled, | ||||
|       fetchIntegrationsIfRequired, | ||||
|       fetchLabelSuggestions, | ||||
|       showNextBubbles, | ||||
|     }; | ||||
|   }, | ||||
|   data() { | ||||
| @@ -106,6 +113,7 @@ export default { | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       currentChat: 'getSelectedChat', | ||||
|       currentUserId: 'getCurrentUserID', | ||||
|       listLoadingStatus: 'getAllMessagesLoaded', | ||||
|       currentAccountId: 'getCurrentAccountId', | ||||
|     }), | ||||
| @@ -436,7 +444,6 @@ export default { | ||||
|     makeMessagesRead() { | ||||
|       this.$store.dispatch('markMessagesRead', { id: this.currentChat.id }); | ||||
|     }, | ||||
|  | ||||
|     getInReplyToMessage(parentMessage) { | ||||
|       if (!parentMessage) return {}; | ||||
|       const inReplyToMessageId = parentMessage.content_attributes?.in_reply_to; | ||||
| @@ -473,7 +480,46 @@ export default { | ||||
|         @click="onToggleContactPanel" | ||||
|       /> | ||||
|     </div> | ||||
|     <ul class="conversation-panel"> | ||||
|     <NextMessageList | ||||
|       v-if="showNextBubbles" | ||||
|       class="conversation-panel" | ||||
|       :read-messages="readMessages" | ||||
|       :un-read-messages="unReadMessages" | ||||
|       :current-user-id="currentUserId" | ||||
|       :is-an-email-channel="isAnEmailChannel" | ||||
|       :inbox-supports-reply-to="inboxSupportsReplyTo" | ||||
|       :messages="currentChat ? currentChat.messages : []" | ||||
|     > | ||||
|       <template #beforeAll> | ||||
|         <transition name="slide-up"> | ||||
|           <!-- eslint-disable-next-line vue/require-toggle-inside-transition --> | ||||
|           <li class="min-h-[4rem]"> | ||||
|             <span v-if="shouldShowSpinner" class="spinner message" /> | ||||
|           </li> | ||||
|         </transition> | ||||
|       </template> | ||||
|       <template #beforeUnread> | ||||
|         <li v-show="unreadMessageCount != 0" class="unread--toast"> | ||||
|           <span> | ||||
|             {{ unreadMessageCount > 9 ? '9+' : unreadMessageCount }} | ||||
|             {{ | ||||
|               unreadMessageCount > 1 | ||||
|                 ? $t('CONVERSATION.UNREAD_MESSAGES') | ||||
|                 : $t('CONVERSATION.UNREAD_MESSAGE') | ||||
|             }} | ||||
|           </span> | ||||
|         </li> | ||||
|       </template> | ||||
|       <template #after> | ||||
|         <ConversationLabelSuggestion | ||||
|           v-if="shouldShowLabelSuggestions" | ||||
|           :suggested-labels="labelSuggestions" | ||||
|           :chat-labels="currentChat.labels" | ||||
|           :conversation-id="currentChat.id" | ||||
|         /> | ||||
|       </template> | ||||
|     </NextMessageList> | ||||
|     <ul v-else class="conversation-panel"> | ||||
|       <transition name="slide-up"> | ||||
|         <!-- eslint-disable-next-line vue/require-toggle-inside-transition --> | ||||
|         <li class="min-h-[4rem]"> | ||||
| @@ -556,7 +602,6 @@ export default { | ||||
|  | ||||
| <style scoped> | ||||
| @tailwind components; | ||||
|  | ||||
| @layer components { | ||||
|   .rounded-bl-calc { | ||||
|     border-bottom-left-radius: calc(1.5rem + 1px); | ||||
|   | ||||
							
								
								
									
										141
									
								
								app/javascript/dashboard/composables/useInbox.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								app/javascript/dashboard/composables/useInbox.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| import { computed } from 'vue'; | ||||
| import { useMapGetter } from 'dashboard/composables/store'; | ||||
| import { useCamelCase } from 'dashboard/composables/useTransformKeys'; | ||||
| import { INBOX_TYPES } from 'dashboard/helper/inbox'; | ||||
|  | ||||
| export const INBOX_FEATURES = { | ||||
|   REPLY_TO: 'replyTo', | ||||
|   REPLY_TO_OUTGOING: 'replyToOutgoing', | ||||
| }; | ||||
|  | ||||
| // This is a single source of truth for inbox features | ||||
| // This is used to check if a feature is available for a particular inbox or not | ||||
| export const INBOX_FEATURE_MAP = { | ||||
|   [INBOX_FEATURES.REPLY_TO]: [ | ||||
|     INBOX_TYPES.FB, | ||||
|     INBOX_TYPES.WEB, | ||||
|     INBOX_TYPES.TWITTER, | ||||
|     INBOX_TYPES.WHATSAPP, | ||||
|     INBOX_TYPES.TELEGRAM, | ||||
|     INBOX_TYPES.API, | ||||
|   ], | ||||
|   [INBOX_FEATURES.REPLY_TO_OUTGOING]: [ | ||||
|     INBOX_TYPES.WEB, | ||||
|     INBOX_TYPES.TWITTER, | ||||
|     INBOX_TYPES.WHATSAPP, | ||||
|     INBOX_TYPES.TELEGRAM, | ||||
|     INBOX_TYPES.API, | ||||
|   ], | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Composable for handling macro-related functionality | ||||
|  * @returns {Object} An object containing the getMacroDropdownValues function | ||||
|  */ | ||||
| export const useInbox = () => { | ||||
|   const currentChat = useMapGetter('getSelectedChat'); | ||||
|   const inboxGetter = useMapGetter('inboxes/getInboxById'); | ||||
|  | ||||
|   const inbox = computed(() => { | ||||
|     const inboxId = currentChat.value.inbox_id; | ||||
|  | ||||
|     return useCamelCase(inboxGetter.value(inboxId), { deep: true }); | ||||
|   }); | ||||
|  | ||||
|   const channelType = computed(() => { | ||||
|     return inbox.value.channelType; | ||||
|   }); | ||||
|  | ||||
|   const isAPIInbox = computed(() => { | ||||
|     return channelType.value === INBOX_TYPES.API; | ||||
|   }); | ||||
|  | ||||
|   const isAFacebookInbox = computed(() => { | ||||
|     return channelType.value === INBOX_TYPES.FB; | ||||
|   }); | ||||
|  | ||||
|   const isAWebWidgetInbox = computed(() => { | ||||
|     return channelType.value === INBOX_TYPES.WEB; | ||||
|   }); | ||||
|  | ||||
|   const isATwilioChannel = computed(() => { | ||||
|     return channelType.value === INBOX_TYPES.TWILIO; | ||||
|   }); | ||||
|  | ||||
|   const isALineChannel = computed(() => { | ||||
|     return channelType.value === INBOX_TYPES.LINE; | ||||
|   }); | ||||
|  | ||||
|   const isAnEmailChannel = computed(() => { | ||||
|     return channelType.value === INBOX_TYPES.EMAIL; | ||||
|   }); | ||||
|  | ||||
|   const isATelegramChannel = computed(() => { | ||||
|     return channelType.value === INBOX_TYPES.TELEGRAM; | ||||
|   }); | ||||
|  | ||||
|   const whatsAppAPIProvider = computed(() => { | ||||
|     return inbox.value.provider || ''; | ||||
|   }); | ||||
|  | ||||
|   const isAMicrosoftInbox = computed(() => { | ||||
|     return isAnEmailChannel.value && inbox.value.provider === 'microsoft'; | ||||
|   }); | ||||
|  | ||||
|   const isAGoogleInbox = computed(() => { | ||||
|     return isAnEmailChannel.value && inbox.value.provider === 'google'; | ||||
|   }); | ||||
|  | ||||
|   const isATwilioSMSChannel = computed(() => { | ||||
|     const { medium: medium = '' } = inbox.value; | ||||
|     return isATwilioChannel.value && medium === 'sms'; | ||||
|   }); | ||||
|  | ||||
|   const isASmsInbox = computed(() => { | ||||
|     return channelType.value === INBOX_TYPES.SMS || isATwilioSMSChannel.value; | ||||
|   }); | ||||
|  | ||||
|   const isATwilioWhatsAppChannel = computed(() => { | ||||
|     const { medium: medium = '' } = inbox.value; | ||||
|     return isATwilioChannel.value && medium === 'whatsapp'; | ||||
|   }); | ||||
|  | ||||
|   const isAWhatsAppCloudChannel = computed(() => { | ||||
|     return ( | ||||
|       channelType.value === INBOX_TYPES.WHATSAPP && | ||||
|       whatsAppAPIProvider.value === 'whatsapp_cloud' | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   const is360DialogWhatsAppChannel = computed(() => { | ||||
|     return ( | ||||
|       channelType.value === INBOX_TYPES.WHATSAPP && | ||||
|       whatsAppAPIProvider.value === 'default' | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   const isAWhatsAppChannel = computed(() => { | ||||
|     return ( | ||||
|       channelType.value === INBOX_TYPES.WHATSAPP || | ||||
|       isATwilioWhatsAppChannel.value | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   return { | ||||
|     inbox, | ||||
|     isAFacebookInbox, | ||||
|     isALineChannel, | ||||
|     isAPIInbox, | ||||
|     isASmsInbox, | ||||
|     isATelegramChannel, | ||||
|     isATwilioChannel, | ||||
|     isAWebWidgetInbox, | ||||
|     isAWhatsAppChannel, | ||||
|     isAMicrosoftInbox, | ||||
|     isAGoogleInbox, | ||||
|     isATwilioWhatsAppChannel, | ||||
|     isAWhatsAppCloudChannel, | ||||
|     is360DialogWhatsAppChannel, | ||||
|     isAnEmailChannel, | ||||
|   }; | ||||
| }; | ||||
| @@ -3,6 +3,7 @@ | ||||
| import { unref } from 'vue'; | ||||
| import camelcaseKeys from 'camelcase-keys'; | ||||
| import snakecaseKeys from 'snakecase-keys'; | ||||
| import * as Sentry from '@sentry/vue'; | ||||
|  | ||||
| /** | ||||
|  * Vue composable that converts object keys to camelCase | ||||
| @@ -12,8 +13,18 @@ import snakecaseKeys from 'snakecase-keys'; | ||||
|  * @returns {Object|Array} Converted payload with camelCase keys | ||||
|  */ | ||||
| export function useCamelCase(payload, options) { | ||||
|   try { | ||||
|     const unrefPayload = unref(payload); | ||||
|     return camelcaseKeys(unrefPayload, options); | ||||
|   } catch (e) { | ||||
|     Sentry.setContext('transform-keys-error', { | ||||
|       payload, | ||||
|       options, | ||||
|       op: 'camelCase', | ||||
|     }); | ||||
|     Sentry.captureException(e); | ||||
|     return payload; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -24,6 +35,16 @@ export function useCamelCase(payload, options) { | ||||
|  * @returns {Object|Array} Converted payload with snake_case keys | ||||
|  */ | ||||
| export function useSnakeCase(payload, options) { | ||||
|   try { | ||||
|     const unrefPayload = unref(payload); | ||||
|     return snakecaseKeys(unrefPayload, options); | ||||
|   } catch (e) { | ||||
|     Sentry.setContext('transform-keys-error', { | ||||
|       payload, | ||||
|       options, | ||||
|       op: 'snakeCase', | ||||
|     }); | ||||
|     Sentry.captureException(e); | ||||
|     return payload; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -5,4 +5,5 @@ export const LOCAL_STORAGE_KEYS = { | ||||
|   COLOR_SCHEME: 'color_scheme', | ||||
|   DISMISSED_LABEL_SUGGESTIONS: 'labelSuggestionsDismissed', | ||||
|   MESSAGE_REPLY_TO: 'messageReplyTo', | ||||
|   USE_NEXT_BUBBLE: 'useNextBubble', | ||||
| }; | ||||
|   | ||||
| @@ -39,6 +39,7 @@ | ||||
|     "DOWNLOAD": "Download", | ||||
|     "UNKNOWN_FILE_TYPE": "Unknown File", | ||||
|     "SAVE_CONTACT": "Save Contact", | ||||
|     "NO_CONTENT": "No content to display", | ||||
|     "SHARED_ATTACHMENT": { | ||||
|       "CONTACT": "{sender} has shared a contact", | ||||
|       "LOCATION": "{sender} has shared a location", | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { mapGetters } from 'vuex'; | ||||
| import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||
| import ContextMenu from 'dashboard/components/ui/ContextMenu.vue'; | ||||
| import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue'; | ||||
| import { useSnakeCase } from 'dashboard/composables/useTransformKeys'; | ||||
| import { copyTextToClipboard } from 'shared/helpers/clipboard'; | ||||
| import { conversationUrl, frontendURL } from '../../../helper/URLHelper'; | ||||
| import { | ||||
| @@ -38,6 +39,10 @@ export default { | ||||
|       type: Object, | ||||
|       default: () => ({}), | ||||
|     }, | ||||
|     hideButton: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   emits: ['open', 'close', 'replyTo'], | ||||
|   setup() { | ||||
| @@ -62,7 +67,7 @@ export default { | ||||
|       return this.getPlainText(this.messageContent); | ||||
|     }, | ||||
|     conversationId() { | ||||
|       return this.message.conversation_id; | ||||
|       return this.message.conversation_id ?? this.message.conversationId; | ||||
|     }, | ||||
|     messageId() { | ||||
|       return this.message.id; | ||||
| @@ -71,7 +76,9 @@ export default { | ||||
|       return this.message.content; | ||||
|     }, | ||||
|     contentAttributes() { | ||||
|       return this.message.content_attributes; | ||||
|       return useSnakeCase( | ||||
|         this.message.content_attributes ?? this.message.contentAttributes | ||||
|       ); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
| @@ -183,6 +190,7 @@ export default { | ||||
|       :reject-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.CANCEL')" | ||||
|     /> | ||||
|     <woot-button | ||||
|       v-if="!hideButton" | ||||
|       icon="more-vertical" | ||||
|       color-scheme="secondary" | ||||
|       variant="clear" | ||||
|   | ||||
| @@ -72,7 +72,6 @@ | ||||
|     "idb": "^8.0.0", | ||||
|     "js-cookie": "^3.0.5", | ||||
|     "libphonenumber-js": "^1.11.9", | ||||
|     "maplibre-gl": "^4.7.1", | ||||
|     "markdown-it": "^13.0.2", | ||||
|     "markdown-it-link-attributes": "^4.0.1", | ||||
|     "md5": "^2.3.0", | ||||
|   | ||||
							
								
								
									
										259
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										259
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -139,9 +139,6 @@ importers: | ||||
|       libphonenumber-js: | ||||
|         specifier: ^1.11.9 | ||||
|         version: 1.11.9 | ||||
|       maplibre-gl: | ||||
|         specifier: ^4.7.1 | ||||
|         version: 4.7.1 | ||||
|       markdown-it: | ||||
|         specifier: ^13.0.2 | ||||
|         version: 13.0.2 | ||||
| @@ -994,34 +991,6 @@ packages: | ||||
|     resolution: {integrity: sha512-dUz8OmYvlY5A9wXaroHIMSPASpSYRLCqbPvxGSyHguhtTQIy24lC+EGxQlwv71AhRCO55WOtgwhzQLpw27JaJQ==} | ||||
|     engines: {node: '>=8'} | ||||
|  | ||||
|   '@mapbox/geojson-rewind@0.5.2': | ||||
|     resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} | ||||
|     hasBin: true | ||||
|  | ||||
|   '@mapbox/jsonlint-lines-primitives@2.0.2': | ||||
|     resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} | ||||
|     engines: {node: '>= 0.6'} | ||||
|  | ||||
|   '@mapbox/point-geometry@0.1.0': | ||||
|     resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} | ||||
|  | ||||
|   '@mapbox/tiny-sdf@2.0.6': | ||||
|     resolution: {integrity: sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==} | ||||
|  | ||||
|   '@mapbox/unitbezier@0.0.1': | ||||
|     resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} | ||||
|  | ||||
|   '@mapbox/vector-tile@1.3.1': | ||||
|     resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==} | ||||
|  | ||||
|   '@mapbox/whoots-js@3.1.0': | ||||
|     resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} | ||||
|     engines: {node: '>=6.0.0'} | ||||
|  | ||||
|   '@maplibre/maplibre-gl-style-spec@20.4.0': | ||||
|     resolution: {integrity: sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==} | ||||
|     hasBin: true | ||||
|  | ||||
|   '@material/mwc-icon@0.25.3': | ||||
|     resolution: {integrity: sha512-36076AWZIRSr8qYOLjuDDkxej/HA0XAosrj7TS1ZeLlUBnLUtbDtvc1S7KSa0hqez7ouzOqGaWK24yoNnTa2OA==} | ||||
|     deprecated: MWC beta is longer supported. Please upgrade to @material/web | ||||
| @@ -1771,24 +1740,12 @@ packages: | ||||
|   '@types/fs-extra@9.0.13': | ||||
|     resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} | ||||
|  | ||||
|   '@types/geojson-vt@3.2.5': | ||||
|     resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} | ||||
|  | ||||
|   '@types/geojson@7946.0.14': | ||||
|     resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} | ||||
|  | ||||
|   '@types/json5@0.0.29': | ||||
|     resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} | ||||
|  | ||||
|   '@types/linkify-it@5.0.0': | ||||
|     resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} | ||||
|  | ||||
|   '@types/mapbox__point-geometry@0.1.4': | ||||
|     resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==} | ||||
|  | ||||
|   '@types/mapbox__vector-tile@1.3.4': | ||||
|     resolution: {integrity: sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==} | ||||
|  | ||||
|   '@types/markdown-it@12.2.3': | ||||
|     resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} | ||||
|  | ||||
| @@ -1798,12 +1755,6 @@ packages: | ||||
|   '@types/node@22.7.0': | ||||
|     resolution: {integrity: sha512-MOdOibwBs6KW1vfqz2uKMlxq5xAfAZ98SZjO8e3XnAbFnTJtAspqhWk7hrdSAs9/Y14ZWMiy7/MxMUzAOadYEw==} | ||||
|  | ||||
|   '@types/pbf@3.0.5': | ||||
|     resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} | ||||
|  | ||||
|   '@types/supercluster@7.1.3': | ||||
|     resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} | ||||
|  | ||||
|   '@types/trusted-types@2.0.2': | ||||
|     resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==} | ||||
|  | ||||
| @@ -2609,9 +2560,6 @@ packages: | ||||
|     resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} | ||||
|     engines: {node: '>=4'} | ||||
|  | ||||
|   earcut@3.0.0: | ||||
|     resolution: {integrity: sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==} | ||||
|  | ||||
|   eastasianwidth@0.2.0: | ||||
|     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} | ||||
|  | ||||
| @@ -2982,9 +2930,6 @@ packages: | ||||
|   functions-have-names@1.2.3: | ||||
|     resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} | ||||
|  | ||||
|   geojson-vt@4.0.2: | ||||
|     resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} | ||||
|  | ||||
|   get-caller-file@2.0.5: | ||||
|     resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} | ||||
|     engines: {node: 6.* || 8.* || >= 10.*} | ||||
| @@ -3016,9 +2961,6 @@ packages: | ||||
|     resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} | ||||
|     engines: {node: '>= 0.4'} | ||||
|  | ||||
|   gl-matrix@3.4.3: | ||||
|     resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==} | ||||
|  | ||||
|   glob-parent@5.1.2: | ||||
|     resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} | ||||
|     engines: {node: '>= 6'} | ||||
| @@ -3039,10 +2981,6 @@ packages: | ||||
|     resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} | ||||
|     engines: {node: '>=18'} | ||||
|  | ||||
|   global-prefix@4.0.0: | ||||
|     resolution: {integrity: sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==} | ||||
|     engines: {node: '>=16'} | ||||
|  | ||||
|   global@4.4.0: | ||||
|     resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} | ||||
|  | ||||
| @@ -3182,9 +3120,6 @@ packages: | ||||
|   idb@8.0.0: | ||||
|     resolution: {integrity: sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==} | ||||
|  | ||||
|   ieee754@1.2.1: | ||||
|     resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} | ||||
|  | ||||
|   ignore@5.2.4: | ||||
|     resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} | ||||
|     engines: {node: '>= 4'} | ||||
| @@ -3217,10 +3152,6 @@ packages: | ||||
|     resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} | ||||
|     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} | ||||
|  | ||||
|   ini@4.1.3: | ||||
|     resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} | ||||
|     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} | ||||
|  | ||||
|   internal-slot@1.0.5: | ||||
|     resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} | ||||
|     engines: {node: '>= 0.4'} | ||||
| @@ -3372,10 +3303,6 @@ packages: | ||||
|   isexe@2.0.0: | ||||
|     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} | ||||
|  | ||||
|   isexe@3.1.1: | ||||
|     resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} | ||||
|     engines: {node: '>=16'} | ||||
|  | ||||
|   istanbul-lib-coverage@3.2.2: | ||||
|     resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} | ||||
|     engines: {node: '>=8'} | ||||
| @@ -3453,9 +3380,6 @@ packages: | ||||
|   json-stable-stringify-without-jsonify@1.0.1: | ||||
|     resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} | ||||
|  | ||||
|   json-stringify-pretty-compact@4.0.0: | ||||
|     resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==} | ||||
|  | ||||
|   json5@1.0.2: | ||||
|     resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} | ||||
|     hasBin: true | ||||
| @@ -3463,9 +3387,6 @@ packages: | ||||
|   jsonfile@6.1.0: | ||||
|     resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} | ||||
|  | ||||
|   kdbush@4.0.2: | ||||
|     resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} | ||||
|  | ||||
|   keycode@2.2.1: | ||||
|     resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==} | ||||
|  | ||||
| @@ -3619,10 +3540,6 @@ packages: | ||||
|     resolution: {integrity: sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==} | ||||
|     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} | ||||
|  | ||||
|   maplibre-gl@4.7.1: | ||||
|     resolution: {integrity: sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==} | ||||
|     engines: {node: '>=16.14.0', npm: '>=8.1.0'} | ||||
|  | ||||
|   markdown-it-anchor@8.6.7: | ||||
|     resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} | ||||
|     peerDependencies: | ||||
| @@ -3755,9 +3672,6 @@ packages: | ||||
|   ms@2.1.3: | ||||
|     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} | ||||
|  | ||||
|   murmurhash-js@1.0.0: | ||||
|     resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} | ||||
|  | ||||
|   mux.js@6.0.1: | ||||
|     resolution: {integrity: sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==} | ||||
|     engines: {node: '>=8', npm: '>=5'} | ||||
| @@ -3985,10 +3899,6 @@ packages: | ||||
|     resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} | ||||
|     engines: {node: '>= 14.16'} | ||||
|  | ||||
|   pbf@3.3.0: | ||||
|     resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} | ||||
|     hasBin: true | ||||
|  | ||||
|   picocolors@1.0.1: | ||||
|     resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} | ||||
|  | ||||
| @@ -4253,9 +4163,6 @@ packages: | ||||
|     resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} | ||||
|     engines: {node: ^10 || ^12 || >=14} | ||||
|  | ||||
|   potpack@2.0.0: | ||||
|     resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==} | ||||
|  | ||||
|   prelude-ls@1.2.1: | ||||
|     resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} | ||||
|     engines: {node: '>= 0.8.0'} | ||||
| @@ -4328,9 +4235,6 @@ packages: | ||||
|   proto-list@1.2.4: | ||||
|     resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} | ||||
|  | ||||
|   protocol-buffers-schema@3.6.0: | ||||
|     resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} | ||||
|  | ||||
|   proxy-from-env@1.1.0: | ||||
|     resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} | ||||
|  | ||||
| @@ -4355,12 +4259,6 @@ packages: | ||||
|     resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} | ||||
|     engines: {node: '>=12'} | ||||
|  | ||||
|   quickselect@2.0.0: | ||||
|     resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} | ||||
|  | ||||
|   quickselect@3.0.0: | ||||
|     resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} | ||||
|  | ||||
|   radix-vue@1.9.9: | ||||
|     resolution: {integrity: sha512-DuL2o7jxNjzlSP5Ko+kJgrW5db+jC3RlnYQIs3WITTqgzfdeP7hXjcqIUveY1f0uXRpOAN3OAd5MZ/SpRyQzQQ==} | ||||
|     peerDependencies: | ||||
| @@ -4409,9 +4307,6 @@ packages: | ||||
|     resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} | ||||
|     engines: {node: '>=4'} | ||||
|  | ||||
|   resolve-protobuf-schema@2.1.0: | ||||
|     resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} | ||||
|  | ||||
|   resolve@1.22.8: | ||||
|     resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} | ||||
|     hasBin: true | ||||
| @@ -4456,9 +4351,6 @@ packages: | ||||
|   rust-result@1.0.0: | ||||
|     resolution: {integrity: sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==} | ||||
|  | ||||
|   rw@1.3.3: | ||||
|     resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} | ||||
|  | ||||
|   sade@1.8.1: | ||||
|     resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} | ||||
|     engines: {node: '>=6'} | ||||
| @@ -4696,9 +4588,6 @@ packages: | ||||
|     engines: {node: '>=16 || 14 >=14.17'} | ||||
|     hasBin: true | ||||
|  | ||||
|   supercluster@8.0.1: | ||||
|     resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} | ||||
|  | ||||
|   supports-color@7.2.0: | ||||
|     resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} | ||||
|     engines: {node: '>=8'} | ||||
| @@ -4777,9 +4666,6 @@ packages: | ||||
|     resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} | ||||
|     engines: {node: ^18.0.0 || >=20.0.0} | ||||
|  | ||||
|   tinyqueue@3.0.0: | ||||
|     resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} | ||||
|  | ||||
|   tinyspy@3.0.2: | ||||
|     resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} | ||||
|     engines: {node: '>=14.0.0'} | ||||
| @@ -5037,9 +4923,6 @@ packages: | ||||
|       jsdom: | ||||
|         optional: true | ||||
|  | ||||
|   vt-pbf@3.1.3: | ||||
|     resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} | ||||
|  | ||||
|   vue-chartjs@5.3.1: | ||||
|     resolution: {integrity: sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==} | ||||
|     peerDependencies: | ||||
| @@ -5229,11 +5112,6 @@ packages: | ||||
|     engines: {node: '>= 8'} | ||||
|     hasBin: true | ||||
|  | ||||
|   which@4.0.0: | ||||
|     resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} | ||||
|     engines: {node: ^16.13.0 || >=18.0.0} | ||||
|     hasBin: true | ||||
|  | ||||
|   why-is-node-running@2.3.0: | ||||
|     resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} | ||||
|     engines: {node: '>=8'} | ||||
| @@ -6040,35 +5918,6 @@ snapshots: | ||||
|     dependencies: | ||||
|       '@lukeed/csprng': 1.0.1 | ||||
|  | ||||
|   '@mapbox/geojson-rewind@0.5.2': | ||||
|     dependencies: | ||||
|       get-stream: 6.0.1 | ||||
|       minimist: 1.2.8 | ||||
|  | ||||
|   '@mapbox/jsonlint-lines-primitives@2.0.2': {} | ||||
|  | ||||
|   '@mapbox/point-geometry@0.1.0': {} | ||||
|  | ||||
|   '@mapbox/tiny-sdf@2.0.6': {} | ||||
|  | ||||
|   '@mapbox/unitbezier@0.0.1': {} | ||||
|  | ||||
|   '@mapbox/vector-tile@1.3.1': | ||||
|     dependencies: | ||||
|       '@mapbox/point-geometry': 0.1.0 | ||||
|  | ||||
|   '@mapbox/whoots-js@3.1.0': {} | ||||
|  | ||||
|   '@maplibre/maplibre-gl-style-spec@20.4.0': | ||||
|     dependencies: | ||||
|       '@mapbox/jsonlint-lines-primitives': 2.0.2 | ||||
|       '@mapbox/unitbezier': 0.0.1 | ||||
|       json-stringify-pretty-compact: 4.0.0 | ||||
|       minimist: 1.2.8 | ||||
|       quickselect: 2.0.0 | ||||
|       rw: 1.3.3 | ||||
|       tinyqueue: 3.0.0 | ||||
|  | ||||
|   '@material/mwc-icon@0.25.3': | ||||
|     dependencies: | ||||
|       lit: 2.2.6 | ||||
| @@ -6910,24 +6759,10 @@ snapshots: | ||||
|     dependencies: | ||||
|       '@types/node': 22.7.0 | ||||
|  | ||||
|   '@types/geojson-vt@3.2.5': | ||||
|     dependencies: | ||||
|       '@types/geojson': 7946.0.14 | ||||
|  | ||||
|   '@types/geojson@7946.0.14': {} | ||||
|  | ||||
|   '@types/json5@0.0.29': {} | ||||
|  | ||||
|   '@types/linkify-it@5.0.0': {} | ||||
|  | ||||
|   '@types/mapbox__point-geometry@0.1.4': {} | ||||
|  | ||||
|   '@types/mapbox__vector-tile@1.3.4': | ||||
|     dependencies: | ||||
|       '@types/geojson': 7946.0.14 | ||||
|       '@types/mapbox__point-geometry': 0.1.4 | ||||
|       '@types/pbf': 3.0.5 | ||||
|  | ||||
|   '@types/markdown-it@12.2.3': | ||||
|     dependencies: | ||||
|       '@types/linkify-it': 5.0.0 | ||||
| @@ -6939,12 +6774,6 @@ snapshots: | ||||
|     dependencies: | ||||
|       undici-types: 6.19.8 | ||||
|  | ||||
|   '@types/pbf@3.0.5': {} | ||||
|  | ||||
|   '@types/supercluster@7.1.3': | ||||
|     dependencies: | ||||
|       '@types/geojson': 7946.0.14 | ||||
|  | ||||
|   '@types/trusted-types@2.0.2': {} | ||||
|  | ||||
|   '@types/web-bluetooth@0.0.20': {} | ||||
| @@ -7878,8 +7707,6 @@ snapshots: | ||||
|  | ||||
|   dset@3.1.4: {} | ||||
|  | ||||
|   earcut@3.0.0: {} | ||||
|  | ||||
|   eastasianwidth@0.2.0: {} | ||||
|  | ||||
|   editorconfig@1.0.4: | ||||
| @@ -8407,8 +8234,6 @@ snapshots: | ||||
|  | ||||
|   functions-have-names@1.2.3: {} | ||||
|  | ||||
|   geojson-vt@4.0.2: {} | ||||
|  | ||||
|   get-caller-file@2.0.5: {} | ||||
|  | ||||
|   get-east-asian-width@1.2.0: {} | ||||
| @@ -8438,8 +8263,6 @@ snapshots: | ||||
|       es-errors: 1.3.0 | ||||
|       get-intrinsic: 1.2.4 | ||||
|  | ||||
|   gl-matrix@3.4.3: {} | ||||
|  | ||||
|   glob-parent@5.1.2: | ||||
|     dependencies: | ||||
|       is-glob: 4.0.3 | ||||
| @@ -8470,12 +8293,6 @@ snapshots: | ||||
|     dependencies: | ||||
|       ini: 4.1.1 | ||||
|  | ||||
|   global-prefix@4.0.0: | ||||
|     dependencies: | ||||
|       ini: 4.1.3 | ||||
|       kind-of: 6.0.3 | ||||
|       which: 4.0.0 | ||||
|  | ||||
|   global@4.4.0: | ||||
|     dependencies: | ||||
|       min-document: 2.19.0 | ||||
| @@ -8671,8 +8488,6 @@ snapshots: | ||||
|  | ||||
|   idb@8.0.0: {} | ||||
|  | ||||
|   ieee754@1.2.1: {} | ||||
|  | ||||
|   ignore@5.2.4: {} | ||||
|  | ||||
|   immutable@4.3.7: | ||||
| @@ -8698,8 +8513,6 @@ snapshots: | ||||
|  | ||||
|   ini@4.1.1: {} | ||||
|  | ||||
|   ini@4.1.3: {} | ||||
|  | ||||
|   internal-slot@1.0.5: | ||||
|     dependencies: | ||||
|       get-intrinsic: 1.2.4 | ||||
| @@ -8832,8 +8645,6 @@ snapshots: | ||||
|  | ||||
|   isexe@2.0.0: {} | ||||
|  | ||||
|   isexe@3.1.1: {} | ||||
|  | ||||
|   istanbul-lib-coverage@3.2.2: {} | ||||
|  | ||||
|   istanbul-lib-report@3.0.1: | ||||
| @@ -8955,8 +8766,6 @@ snapshots: | ||||
|  | ||||
|   json-stable-stringify-without-jsonify@1.0.1: {} | ||||
|  | ||||
|   json-stringify-pretty-compact@4.0.0: {} | ||||
|  | ||||
|   json5@1.0.2: | ||||
|     dependencies: | ||||
|       minimist: 1.2.8 | ||||
| @@ -8967,8 +8776,6 @@ snapshots: | ||||
|     optionalDependencies: | ||||
|       graceful-fs: 4.2.11 | ||||
|  | ||||
|   kdbush@4.0.2: {} | ||||
|  | ||||
|   keycode@2.2.1: {} | ||||
|  | ||||
|   keyv@4.5.3: | ||||
| @@ -9142,35 +8949,6 @@ snapshots: | ||||
|  | ||||
|   map-obj@5.0.0: {} | ||||
|  | ||||
|   maplibre-gl@4.7.1: | ||||
|     dependencies: | ||||
|       '@mapbox/geojson-rewind': 0.5.2 | ||||
|       '@mapbox/jsonlint-lines-primitives': 2.0.2 | ||||
|       '@mapbox/point-geometry': 0.1.0 | ||||
|       '@mapbox/tiny-sdf': 2.0.6 | ||||
|       '@mapbox/unitbezier': 0.0.1 | ||||
|       '@mapbox/vector-tile': 1.3.1 | ||||
|       '@mapbox/whoots-js': 3.1.0 | ||||
|       '@maplibre/maplibre-gl-style-spec': 20.4.0 | ||||
|       '@types/geojson': 7946.0.14 | ||||
|       '@types/geojson-vt': 3.2.5 | ||||
|       '@types/mapbox__point-geometry': 0.1.4 | ||||
|       '@types/mapbox__vector-tile': 1.3.4 | ||||
|       '@types/pbf': 3.0.5 | ||||
|       '@types/supercluster': 7.1.3 | ||||
|       earcut: 3.0.0 | ||||
|       geojson-vt: 4.0.2 | ||||
|       gl-matrix: 3.4.3 | ||||
|       global-prefix: 4.0.0 | ||||
|       kdbush: 4.0.2 | ||||
|       murmurhash-js: 1.0.0 | ||||
|       pbf: 3.3.0 | ||||
|       potpack: 2.0.0 | ||||
|       quickselect: 3.0.0 | ||||
|       supercluster: 8.0.1 | ||||
|       tinyqueue: 3.0.0 | ||||
|       vt-pbf: 3.1.3 | ||||
|  | ||||
|   markdown-it-anchor@8.6.7(@types/markdown-it@12.2.3)(markdown-it@12.3.2): | ||||
|     dependencies: | ||||
|       '@types/markdown-it': 12.2.3 | ||||
| @@ -9297,8 +9075,6 @@ snapshots: | ||||
|  | ||||
|   ms@2.1.3: {} | ||||
|  | ||||
|   murmurhash-js@1.0.0: {} | ||||
|  | ||||
|   mux.js@6.0.1: | ||||
|     dependencies: | ||||
|       '@babel/runtime': 7.25.6 | ||||
| @@ -9519,11 +9295,6 @@ snapshots: | ||||
|  | ||||
|   pathval@2.0.0: {} | ||||
|  | ||||
|   pbf@3.3.0: | ||||
|     dependencies: | ||||
|       ieee754: 1.2.1 | ||||
|       resolve-protobuf-schema: 2.1.0 | ||||
|  | ||||
|   picocolors@1.0.1: {} | ||||
|  | ||||
|   picocolors@1.1.0: {} | ||||
| @@ -9814,8 +9585,6 @@ snapshots: | ||||
|       picocolors: 1.1.1 | ||||
|       source-map-js: 1.2.1 | ||||
|  | ||||
|   potpack@2.0.0: {} | ||||
|  | ||||
|   prelude-ls@1.2.1: {} | ||||
|  | ||||
|   prettier-linter-helpers@1.0.0: | ||||
| @@ -9921,8 +9690,6 @@ snapshots: | ||||
|  | ||||
|   proto-list@1.2.4: {} | ||||
|  | ||||
|   protocol-buffers-schema@3.6.0: {} | ||||
|  | ||||
|   proxy-from-env@1.1.0: {} | ||||
|  | ||||
|   psl@1.9.0: {} | ||||
| @@ -9937,10 +9704,6 @@ snapshots: | ||||
|  | ||||
|   quick-lru@6.1.2: {} | ||||
|  | ||||
|   quickselect@2.0.0: {} | ||||
|  | ||||
|   quickselect@3.0.0: {} | ||||
|  | ||||
|   radix-vue@1.9.9(vue@3.5.12(typescript@5.6.2)): | ||||
|     dependencies: | ||||
|       '@floating-ui/dom': 1.6.12 | ||||
| @@ -9996,10 +9759,6 @@ snapshots: | ||||
|  | ||||
|   resolve-from@4.0.0: {} | ||||
|  | ||||
|   resolve-protobuf-schema@2.1.0: | ||||
|     dependencies: | ||||
|       protocol-buffers-schema: 3.6.0 | ||||
|  | ||||
|   resolve@1.22.8: | ||||
|     dependencies: | ||||
|       is-core-module: 2.15.1 | ||||
| @@ -10060,8 +9819,6 @@ snapshots: | ||||
|     dependencies: | ||||
|       individual: 2.0.0 | ||||
|  | ||||
|   rw@1.3.3: {} | ||||
|  | ||||
|   sade@1.8.1: | ||||
|     dependencies: | ||||
|       mri: 1.2.0 | ||||
| @@ -10329,10 +10086,6 @@ snapshots: | ||||
|       pirates: 4.0.6 | ||||
|       ts-interface-checker: 0.1.13 | ||||
|  | ||||
|   supercluster@8.0.1: | ||||
|     dependencies: | ||||
|       kdbush: 4.0.2 | ||||
|  | ||||
|   supports-color@7.2.0: | ||||
|     dependencies: | ||||
|       has-flag: 4.0.0 | ||||
| @@ -10433,8 +10186,6 @@ snapshots: | ||||
|  | ||||
|   tinypool@1.0.0: {} | ||||
|  | ||||
|   tinyqueue@3.0.0: {} | ||||
|  | ||||
|   tinyspy@3.0.2: {} | ||||
|  | ||||
|   to-fast-properties@2.0.0: {} | ||||
| @@ -10719,12 +10470,6 @@ snapshots: | ||||
|       - supports-color | ||||
|       - terser | ||||
|  | ||||
|   vt-pbf@3.1.3: | ||||
|     dependencies: | ||||
|       '@mapbox/point-geometry': 0.1.0 | ||||
|       '@mapbox/vector-tile': 1.3.1 | ||||
|       pbf: 3.3.0 | ||||
|  | ||||
|   vue-chartjs@5.3.1(chart.js@4.4.4)(vue@3.5.12(typescript@5.6.2)): | ||||
|     dependencies: | ||||
|       chart.js: 4.4.4 | ||||
| @@ -10912,10 +10657,6 @@ snapshots: | ||||
|     dependencies: | ||||
|       isexe: 2.0.0 | ||||
|  | ||||
|   which@4.0.0: | ||||
|     dependencies: | ||||
|       isexe: 3.1.1 | ||||
|  | ||||
|   why-is-node-running@2.3.0: | ||||
|     dependencies: | ||||
|       siginfo: 2.0.0 | ||||
|   | ||||
| @@ -360,6 +360,21 @@ export const colors = { | ||||
|       12: 'rgb(var(--teal-12) / <alpha-value>)', | ||||
|     }, | ||||
|  | ||||
|     gray: { | ||||
|       1: 'rgb(var(--gray-1) / <alpha-value>)', | ||||
|       2: 'rgb(var(--gray-2) / <alpha-value>)', | ||||
|       3: 'rgb(var(--gray-3) / <alpha-value>)', | ||||
|       4: 'rgb(var(--gray-4) / <alpha-value>)', | ||||
|       5: 'rgb(var(--gray-5) / <alpha-value>)', | ||||
|       6: 'rgb(var(--gray-6) / <alpha-value>)', | ||||
|       7: 'rgb(var(--gray-7) / <alpha-value>)', | ||||
|       8: 'rgb(var(--gray-8) / <alpha-value>)', | ||||
|       9: 'rgb(var(--gray-9) / <alpha-value>)', | ||||
|       10: 'rgb(var(--gray-10) / <alpha-value>)', | ||||
|       11: 'rgb(var(--gray-11) / <alpha-value>)', | ||||
|       12: 'rgb(var(--gray-12) / <alpha-value>)', | ||||
|     }, | ||||
|  | ||||
|     black: '#000000', | ||||
|     brand: '#2781F6', | ||||
|     background: 'rgb(var(--background-color) / <alpha-value>)', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Shivam Mishra
					Shivam Mishra