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:
@@ -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,60 +19,62 @@ 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="size-8 rounded-lg grid place-content-center"
|
||||
:class="iconBgColor"
|
||||
>
|
||||
<slot name="icon">
|
||||
<Icon :icon="icon" class="text-white size-4" />
|
||||
</slot>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div v-if="senderName" class="text-n-slate-12 text-sm truncate">
|
||||
{{
|
||||
t(senderTranslationKey, {
|
||||
sender: senderName,
|
||||
})
|
||||
}}
|
||||
<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"
|
||||
>
|
||||
<slot name="icon">
|
||||
<Icon :icon="icon" class="text-white size-4" />
|
||||
</slot>
|
||||
</div>
|
||||
<slot>
|
||||
<div v-if="content" class="truncate text-sm text-n-slate-11">
|
||||
{{ content }}
|
||||
<div class="space-y-1">
|
||||
<div v-if="senderName" class="text-n-slate-12 text-sm truncate">
|
||||
{{
|
||||
t(senderTranslationKey, {
|
||||
sender: senderName,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</slot>
|
||||
<slot>
|
||||
<div v-if="content" class="truncate text-sm text-n-slate-11">
|
||||
{{ content }}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<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 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 text-center border border-n-container"
|
||||
@click="action.onClick"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="action" class="px-3 pb-3">
|
||||
<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"
|
||||
>
|
||||
{{ action.label }}
|
||||
</a>
|
||||
<button
|
||||
v-else
|
||||
class="w-full bg-n-solid-3 px-4 py-2 rounded-lg text-sm"
|
||||
@click="action.onClick"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</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,25 +28,28 @@ 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
|
||||
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"
|
||||
>
|
||||
<Icon icon="i-lucide-instagram" class="text-white" />
|
||||
<div class="relative group rounded-lg overflow-hidden">
|
||||
<div
|
||||
v-if="isReel"
|
||||
class="absolute p-2 flex items-start justify-end right-0"
|
||||
>
|
||||
<Icon icon="i-lucide-instagram" class="text-white shadow-lg" />
|
||||
</div>
|
||||
<video
|
||||
controls
|
||||
class="rounded-lg"
|
||||
:src="attachment.dataUrl"
|
||||
:class="{
|
||||
'max-w-48': isReel,
|
||||
'max-w-full': !isReel,
|
||||
}"
|
||||
@error="handleError"
|
||||
/>
|
||||
</div>
|
||||
<video
|
||||
controls
|
||||
:src="attachment.dataUrl"
|
||||
:class="{
|
||||
'max-w-48': isReel,
|
||||
'max-w-full': !isReel,
|
||||
}"
|
||||
@error="handleError"
|
||||
/>
|
||||
</BaseBubble>
|
||||
<GalleryView
|
||||
v-if="showGallery"
|
||||
|
||||
Reference in New Issue
Block a user