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:
Shivam Mishra
2024-12-19 18:41:55 +05:30
committed by GitHub
parent 9279175199
commit eef70b9bd7
30 changed files with 922 additions and 866 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"
>

View File

@@ -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(() => {

View File

@@ -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"

View File

@@ -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="{

View File

@@ -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"

View File

@@ -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 = () => {

View File

@@ -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>

View File

@@ -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">

View File

@@ -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"