mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 18:47:51 +00:00
feat: Add new message bubbles (#10481)
--------- Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -135,6 +135,7 @@
|
||||
--solid-active: 255 255 255;
|
||||
--solid-amber: 252 232 193;
|
||||
--solid-blue: 218 236 255;
|
||||
--solid-iris: 230 231 255;
|
||||
|
||||
--alpha-1: 67, 67, 67, 0.06;
|
||||
--alpha-2: 201, 202, 207, 0.15;
|
||||
@@ -142,10 +143,10 @@
|
||||
--black-alpha-1: 0, 0, 0, 0.12;
|
||||
--black-alpha-2: 0, 0, 0, 0.04;
|
||||
--border-blue: 39, 129, 246, 0.5;
|
||||
--white-alpha: 255, 255, 255, 0.1;
|
||||
--white-alpha: 255, 255, 255, 0.8;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
.dark {
|
||||
/* slate */
|
||||
--slate-1: 17 17 19;
|
||||
--slate-2: 24 25 27;
|
||||
@@ -221,6 +222,7 @@
|
||||
--solid-active: 53 57 66;
|
||||
--solid-amber: 42 37 30;
|
||||
--solid-blue: 16 49 91;
|
||||
--solid-iris: 38 42 101;
|
||||
--text-blue: 126 182 255;
|
||||
|
||||
--alpha-1: 36, 36, 36, 0.8;
|
||||
@@ -230,7 +232,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.1;
|
||||
--white-alpha: 255, 255, 255, 0.8;
|
||||
}
|
||||
/* NEXT COLORS END */
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
import FileIcon from './FileIcon.vue';
|
||||
const files = [
|
||||
{ name: 'file.7z', type: '7z' },
|
||||
{ name: 'file.zip', type: 'zip' },
|
||||
{ name: 'file.rar', type: 'rar' },
|
||||
{ name: 'file.tar', type: 'tar' },
|
||||
{ name: 'file.csv', type: 'csv' },
|
||||
{ name: 'file.docx', type: 'docx' },
|
||||
{ name: 'file.doc', type: 'doc' },
|
||||
{ name: 'file.odt', type: 'odt' },
|
||||
{ name: 'file.pdf', type: 'pdf' },
|
||||
{ name: 'file.ppt', type: 'ppt' },
|
||||
{ name: 'file.pptx', type: 'pptx' },
|
||||
{ name: 'file.rtf', type: 'rtf' },
|
||||
{ name: 'file.json', type: 'json' },
|
||||
{ name: 'file.txt', type: 'txt' },
|
||||
{ name: 'file.xls', type: 'xls' },
|
||||
{ name: 'file.xlsx', type: 'xlsx' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Icons/FileIcon">
|
||||
<div class="grid grid-cols-4 gap-5">
|
||||
<div
|
||||
v-for="file in files"
|
||||
:key="file.type"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<FileIcon :file-type="file.type" class="size-6" />
|
||||
<p>{{ file.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Story>
|
||||
</template>
|
||||
38
app/javascript/dashboard/components-next/icon/FileIcon.vue
Normal file
38
app/javascript/dashboard/components-next/icon/FileIcon.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
|
||||
const { fileType } = defineProps({
|
||||
fileType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const fileTypeIcon = computed(() => {
|
||||
const fileIconMap = {
|
||||
'7z': 'i-woot-file-zip',
|
||||
csv: 'i-woot-file-csv',
|
||||
doc: 'i-woot-file-doc',
|
||||
docx: 'i-woot-file-doc',
|
||||
json: 'i-woot-file-txt',
|
||||
odt: 'i-woot-file-doc',
|
||||
pdf: 'i-woot-file-pdf',
|
||||
ppt: 'i-woot-file-ppt',
|
||||
pptx: 'i-woot-file-ppt',
|
||||
rar: 'i-woot-file-zip',
|
||||
rtf: 'i-woot-file-doc',
|
||||
tar: 'i-woot-file-zip',
|
||||
txt: 'i-woot-file-txt',
|
||||
xls: 'i-woot-file-xls',
|
||||
xlsx: 'i-woot-file-xls',
|
||||
zip: 'i-woot-file-zip',
|
||||
};
|
||||
|
||||
return fileIconMap[fileType] || 'i-teenyicons-text-document-solid';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Icon :icon="fileTypeIcon" />
|
||||
</template>
|
||||
352
app/javascript/dashboard/components-next/message/Message.vue
Normal file
352
app/javascript/dashboard/components-next/message/Message.vue
Normal file
@@ -0,0 +1,352 @@
|
||||
<script setup>
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
import { provideMessageContext } from './provider.js';
|
||||
import {
|
||||
MESSAGE_TYPES,
|
||||
ATTACHMENT_TYPES,
|
||||
MESSAGE_VARIANTS,
|
||||
SENDER_TYPES,
|
||||
ORIENTATION,
|
||||
MESSAGE_STATUS,
|
||||
} from './constants';
|
||||
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
|
||||
import TextBubble from './bubbles/Text/Index.vue';
|
||||
import ActivityBubble from './bubbles/Activity.vue';
|
||||
import ImageBubble from './bubbles/Image.vue';
|
||||
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 MessageError from './MessageError.vue';
|
||||
import MessageMeta from './MessageMeta.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} 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} ContentAttributes
|
||||
* @property {string} externalError - an error message to be shown if the message failed to send
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {('sent'|'delivered'|'read'|'failed')} 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
|
||||
* @property {boolean} [private=false] - Whether the message is private
|
||||
* @property {number|null} [senderId=null] - The ID of the sender
|
||||
* @property {number} createdAt - Timestamp when the message was created
|
||||
* @property {number} currentUserId - The ID of the current user
|
||||
* @property {number} id - The unique identifier for the message
|
||||
* @property {number} messageType - The type of message (must be one of MESSAGE_TYPES)
|
||||
* @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
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line vue/define-macros-order
|
||||
const props = defineProps({
|
||||
id: { type: Number, required: true },
|
||||
messageType: {
|
||||
type: Number,
|
||||
required: true,
|
||||
validator: value => Object.values(MESSAGE_TYPES).includes(value),
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
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: {
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.status === MESSAGE_STATUS.FAILED) return MESSAGE_VARIANTS.ERROR;
|
||||
if (props.contentAttributes.isUnsupported)
|
||||
return MESSAGE_VARIANTS.UNSUPPORTED;
|
||||
|
||||
const variants = {
|
||||
[MESSAGE_TYPES.INCOMING]: MESSAGE_VARIANTS.USER,
|
||||
[MESSAGE_TYPES.ACTIVITY]: MESSAGE_VARIANTS.ACTIVITY,
|
||||
[MESSAGE_TYPES.OUTGOING]: MESSAGE_VARIANTS.AGENT,
|
||||
[MESSAGE_TYPES.TEMPLATE]: MESSAGE_VARIANTS.TEMPLATE,
|
||||
};
|
||||
|
||||
return variants[props.messageType] || MESSAGE_VARIANTS.USER;
|
||||
});
|
||||
|
||||
const isMyMessage = computed(() => {
|
||||
const senderId = props.senderId ?? props.sender?.id;
|
||||
const senderType = props.senderType ?? props.sender?.type;
|
||||
|
||||
if (!senderType || !senderId) return false;
|
||||
|
||||
return (
|
||||
senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase() &&
|
||||
props.currentUserId === senderId
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the message orientation based on sender type and message type
|
||||
* @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation
|
||||
*/
|
||||
const orientation = computed(() => {
|
||||
if (isMyMessage.value) {
|
||||
return ORIENTATION.RIGHT;
|
||||
}
|
||||
|
||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return ORIENTATION.CENTER;
|
||||
|
||||
return ORIENTATION.LEFT;
|
||||
});
|
||||
|
||||
const flexOrientationClass = computed(() => {
|
||||
const map = {
|
||||
[ORIENTATION.LEFT]: 'justify-start',
|
||||
[ORIENTATION.RIGHT]: 'justify-end',
|
||||
[ORIENTATION.CENTER]: 'justify-center',
|
||||
};
|
||||
|
||||
return map[orientation.value];
|
||||
});
|
||||
|
||||
const gridClass = computed(() => {
|
||||
const map = {
|
||||
[ORIENTATION.LEFT]: 'grid grid-cols-[24px_1fr]',
|
||||
[ORIENTATION.RIGHT]: 'grid grid-cols-1fr',
|
||||
};
|
||||
|
||||
return map[orientation.value];
|
||||
});
|
||||
|
||||
const gridTemplate = computed(() => {
|
||||
const map = {
|
||||
[ORIENTATION.LEFT]: `
|
||||
"avatar bubble"
|
||||
"spacer meta"
|
||||
`,
|
||||
[ORIENTATION.RIGHT]: `
|
||||
"bubble"
|
||||
"meta"
|
||||
`,
|
||||
};
|
||||
|
||||
return map[orientation.value];
|
||||
});
|
||||
|
||||
const shouldGroupWithNext = computed(() => {
|
||||
if (props.status === MESSAGE_STATUS.FAILED) return false;
|
||||
|
||||
return props.groupWithNext;
|
||||
});
|
||||
|
||||
const shouldShowAvatar = computed(() => {
|
||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
|
||||
if (orientation.value === ORIENTATION.RIGHT) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const componentToRender = computed(() => {
|
||||
if (props.isEmailInbox && !props.private) {
|
||||
const emailInboxTypes = [MESSAGE_TYPES.INCOMING, MESSAGE_TYPES.OUTGOING];
|
||||
if (emailInboxTypes.includes(props.messageType)) return EmailBubble;
|
||||
}
|
||||
|
||||
if (props.contentAttributes.isUnsupported) {
|
||||
return UnsupportedBubble;
|
||||
}
|
||||
|
||||
if (props.contentAttributes.type === 'dyte') {
|
||||
return DyteBubble;
|
||||
}
|
||||
|
||||
if (props.contentAttributes.imageType === 'story_mention') {
|
||||
return InstagramStoryBubble;
|
||||
}
|
||||
|
||||
if (props.attachments.length === 1) {
|
||||
const fileType = props.attachments[0].fileType;
|
||||
|
||||
if (!props.content) {
|
||||
if (fileType === ATTACHMENT_TYPES.IMAGE) return ImageBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.FILE) return FileBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.AUDIO) return AudioBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.VIDEO) return VideoBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.IG_REEL) return VideoBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.LOCATION) return LocationBubble;
|
||||
}
|
||||
// Attachment content is the name of the contact
|
||||
if (fileType === ATTACHMENT_TYPES.CONTACT) return ContactBubble;
|
||||
}
|
||||
|
||||
if (props.attachments.length > 1 && !props.content) {
|
||||
return AttachmentsBubble;
|
||||
}
|
||||
|
||||
return TextBubble;
|
||||
});
|
||||
|
||||
provideMessageContext({
|
||||
variant,
|
||||
inReplyTo: props.inReplyTo,
|
||||
orientation,
|
||||
isMyMessage,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex w-full"
|
||||
:data-message-id="props.id"
|
||||
:class="[flexOrientationClass, shouldGroupWithNext ? 'mb-2' : 'mb-4']"
|
||||
>
|
||||
<div v-if="variant === MESSAGE_VARIANTS.ACTIVITY">
|
||||
<ActivityBubble :content="content" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="[
|
||||
gridClass,
|
||||
{
|
||||
'gap-y-2': !shouldGroupWithNext,
|
||||
'w-full': variant === MESSAGE_VARIANTS.EMAIL,
|
||||
},
|
||||
]"
|
||||
class="gap-x-3"
|
||||
:style="{
|
||||
gridTemplateAreas: gridTemplate,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="!shouldGroupWithNext && shouldShowAvatar"
|
||||
class="[grid-area:avatar] flex items-end"
|
||||
>
|
||||
<Avatar
|
||||
:name="sender ? sender.name : ''"
|
||||
:src="sender?.thumbnail"
|
||||
:size="24"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="[grid-area:bubble]"
|
||||
:class="{
|
||||
'pl-9': ORIENTATION.RIGHT === orientation,
|
||||
}"
|
||||
>
|
||||
<Component :is="componentToRender" v-bind="props" />
|
||||
</div>
|
||||
<MessageError
|
||||
v-if="contentAttributes.externalError"
|
||||
class="[grid-area:meta]"
|
||||
: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>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
error: { type: String, required: true },
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-xs text-n-ruby-11 flex items-center gap-1.5">
|
||||
<span>{{ t('CHAT_LIST.FAILED_TO_SEND') }}</span>
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="bg-n-alpha-2 rounded-md size-5 grid place-content-center cursor-pointer"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-alert-triangle"
|
||||
class="text-n-ruby-11 size-[14px]"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute bg-n-alpha-3 px-4 py-3 border rounded-xl border-n-strong text-n-slate-12 bottom-6 w-52 right-0 text-xs backdrop-blur-[100px] shadow-[0px_0px_24px_0px_rgba(0,0,0,0.12)] opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||
|
||||
import MessageStatus from './MessageStatus.vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
|
||||
import { MESSAGE_STATUS } 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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 readableTime = computed(() =>
|
||||
messageTimestamp(props.createdAt, 'LLL d, h:mm a')
|
||||
);
|
||||
|
||||
const showSender = computed(() => !props.isMyMessage && props.sender);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-xs text-n-slate-11 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 />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useIntervalFn } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { MESSAGE_STATUS } from './constants';
|
||||
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
|
||||
const { status } = defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: value => Object.values(MESSAGE_STATUS).includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const progresIconSequence = [
|
||||
'i-lucide-clock-1',
|
||||
'i-lucide-clock-2',
|
||||
'i-lucide-clock-3',
|
||||
'i-lucide-clock-4',
|
||||
'i-lucide-clock-5',
|
||||
'i-lucide-clock-6',
|
||||
'i-lucide-clock-7',
|
||||
'i-lucide-clock-8',
|
||||
'i-lucide-clock-9',
|
||||
'i-lucide-clock-10',
|
||||
'i-lucide-clock-11',
|
||||
'i-lucide-clock-12',
|
||||
];
|
||||
|
||||
const progessIcon = ref(progresIconSequence[0]);
|
||||
|
||||
const rotateIcon = () => {
|
||||
const currentIndex = progresIconSequence.indexOf(progessIcon.value);
|
||||
const nextIndex = (currentIndex + 1) % progresIconSequence.length;
|
||||
progessIcon.value = progresIconSequence[nextIndex];
|
||||
};
|
||||
|
||||
useIntervalFn(rotateIcon, 500, {
|
||||
immediate: status === MESSAGE_STATUS.PROGRESS,
|
||||
immediateCallback: false,
|
||||
});
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
const statusIconMap = {
|
||||
[MESSAGE_STATUS.SENT]: 'i-lucide-check',
|
||||
[MESSAGE_STATUS.DELIVERED]: 'i-lucide-check-check',
|
||||
[MESSAGE_STATUS.READ]: 'i-lucide-check-check',
|
||||
};
|
||||
|
||||
return statusIconMap[status];
|
||||
});
|
||||
|
||||
const statusColor = computed(() => {
|
||||
const statusIconMap = {
|
||||
[MESSAGE_STATUS.SENT]: 'text-n-slate-10',
|
||||
[MESSAGE_STATUS.DELIVERED]: 'text-n-slate-10',
|
||||
[MESSAGE_STATUS.READ]: 'text-[#7EB6FF]',
|
||||
};
|
||||
|
||||
return statusIconMap[status];
|
||||
});
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
const statusTextMap = {
|
||||
[MESSAGE_STATUS.SENT]: t('CHAT_LIST.SENT'),
|
||||
[MESSAGE_STATUS.DELIVERED]: t('CHAT_LIST.DELIVERED'),
|
||||
[MESSAGE_STATUS.READ]: t('CHAT_LIST.MESSAGE_READ'),
|
||||
[MESSAGE_STATUS.PROGRESS]: t('CHAT_LIST.SENDING'),
|
||||
};
|
||||
|
||||
return statusTextMap[status];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Icon
|
||||
v-if="status === MESSAGE_STATUS.PROGRESS"
|
||||
v-tooltip.top-start="tooltipText"
|
||||
:icon="progessIcon"
|
||||
class="text-n-slate-10"
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
v-tooltip.top-start="tooltipText"
|
||||
:icon="statusIcon"
|
||||
:class="statusColor"
|
||||
class="size-[14px]"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import BaseBubble from './Base.vue';
|
||||
|
||||
defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="px-2 py-0.5" data-bubble-name="activity">
|
||||
<span v-dompurify-html="content" />
|
||||
</BaseBubble>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<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>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import BaseBubble from './Base.vue';
|
||||
import AudioChip from 'next/message/chips/Audio.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 attachment = computed(() => {
|
||||
return props.attachments[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"
|
||||
/>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { MESSAGE_VARIANTS, ORIENTATION } from '../constants';
|
||||
|
||||
const { variant, orientation, inReplyTo } = useMessageContext();
|
||||
const { t } = useI18n();
|
||||
|
||||
const varaintBaseMap = {
|
||||
[MESSAGE_VARIANTS.AGENT]: 'bg-n-solid-blue text-n-slate-12',
|
||||
[MESSAGE_VARIANTS.PRIVATE]:
|
||||
'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold',
|
||||
[MESSAGE_VARIANTS.USER]: 'bg-n-slate-4 text-n-slate-12',
|
||||
[MESSAGE_VARIANTS.ACTIVITY]: 'bg-n-alpha-1 text-n-slate-11 text-sm',
|
||||
[MESSAGE_VARIANTS.BOT]: 'bg-n-solid-iris text-n-slate-12',
|
||||
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12',
|
||||
[MESSAGE_VARIANTS.ERROR]: 'bg-n-ruby-4 text-n-ruby-12',
|
||||
[MESSAGE_VARIANTS.EMAIL]: 'bg-n-alpha-2 w-full',
|
||||
[MESSAGE_VARIANTS.UNSUPPORTED]:
|
||||
'bg-n-solid-amber/70 border border-dashed border-n-amber-12 text-n-amber-12',
|
||||
};
|
||||
|
||||
const orientationMap = {
|
||||
[ORIENTATION.LEFT]: 'rounded-xl rounded-bl-sm',
|
||||
[ORIENTATION.RIGHT]: 'rounded-xl rounded-br-sm',
|
||||
[ORIENTATION.CENTER]: 'rounded-md',
|
||||
};
|
||||
|
||||
const messageClass = computed(() => {
|
||||
const classToApply = [varaintBaseMap[variant.value]];
|
||||
|
||||
if (variant.value !== MESSAGE_VARIANTS.ACTIVITY) {
|
||||
classToApply.push(orientationMap[orientation.value]);
|
||||
} else {
|
||||
classToApply.push('rounded-lg');
|
||||
}
|
||||
|
||||
return classToApply;
|
||||
});
|
||||
|
||||
const scrollToMessage = () => {
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
|
||||
messageId: this.message.id,
|
||||
});
|
||||
};
|
||||
|
||||
const previewMessage = computed(() => {
|
||||
if (!inReplyTo) return '';
|
||||
|
||||
const { content, attachments } = inReplyTo;
|
||||
|
||||
if (content) return content;
|
||||
if (attachments?.length) {
|
||||
const firstAttachment = attachments[0];
|
||||
const fileType = firstAttachment.fileType ?? firstAttachment.file_type;
|
||||
|
||||
return t(`CHAT_LIST.ATTACHMENTS.${fileType}.CONTENT`);
|
||||
}
|
||||
|
||||
return t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="text-sm min-w-32 break-words"
|
||||
:class="[
|
||||
messageClass,
|
||||
{
|
||||
'max-w-md': variant !== MESSAGE_VARIANTS.EMAIL,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="inReplyTo"
|
||||
class="bg-n-alpha-black1 rounded-lg p-2"
|
||||
@click="scrollToMessage"
|
||||
>
|
||||
<span class="line-clamp-2">
|
||||
{{ previewMessage }}
|
||||
</span>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BaseBubble from './Base.vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
|
||||
const props = 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: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: action => {
|
||||
return action.label && (action.href || action.onClick);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const senderName = computed(() => {
|
||||
return props.sender.name;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="overflow-hidden grid gap-4 min-w-64 p-0"
|
||||
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>
|
||||
<slot>
|
||||
<div v-if="content" class="truncate text-sm text-n-slate-11">
|
||||
{{ content }}
|
||||
</div>
|
||||
</slot>
|
||||
</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>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BaseAttachmentBubble from './BaseAttachment.vue';
|
||||
|
||||
import {
|
||||
DuplicateContactException,
|
||||
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 $store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const attachment = computed(() => {
|
||||
return props.attachments[0];
|
||||
});
|
||||
|
||||
const phoneNumber = computed(() => {
|
||||
return attachment.value.fallbackTitle;
|
||||
});
|
||||
|
||||
const formattedPhoneNumber = computed(() => {
|
||||
return phoneNumber.value.replace(/\s|-|[A-Za-z]/g, '');
|
||||
});
|
||||
|
||||
const rawPhoneNumber = computed(() => {
|
||||
return phoneNumber.value.replace(/\D/g, '');
|
||||
});
|
||||
|
||||
const name = computed(() => {
|
||||
return props.content;
|
||||
});
|
||||
|
||||
function getContactObject() {
|
||||
const contactItem = {
|
||||
name: name.value,
|
||||
phone_number: `+${rawPhoneNumber.value}`,
|
||||
};
|
||||
return contactItem;
|
||||
}
|
||||
|
||||
async function filterContactByNumber(searchCandidate) {
|
||||
const query = {
|
||||
attribute_key: 'phone_number',
|
||||
filter_operator: 'equal_to',
|
||||
values: [searchCandidate],
|
||||
attribute_model: 'standard',
|
||||
custom_attribute_type: '',
|
||||
};
|
||||
|
||||
const queryPayload = { payload: [query] };
|
||||
const contacts = await $store.dispatch('contacts/filter', {
|
||||
queryPayload,
|
||||
resetState: false,
|
||||
});
|
||||
return contacts.shift();
|
||||
}
|
||||
|
||||
function openContactNewTab(contactId) {
|
||||
const accountId = window.location.pathname.split('/')[3];
|
||||
const url = `/app/accounts/${accountId}/contacts/${contactId}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
async function addContact() {
|
||||
try {
|
||||
let contact = await filterContactByNumber(rawPhoneNumber);
|
||||
if (!contact) {
|
||||
contact = await $store.dispatch('contacts/create', getContactObject());
|
||||
useAlert(t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
}
|
||||
openContactNewTab(contact.id);
|
||||
} catch (error) {
|
||||
if (error instanceof DuplicateContactException) {
|
||||
if (error.data.includes('phone_number')) {
|
||||
useAlert(t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
useAlert(t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const action = computed(() => ({
|
||||
label: t('CONVERSATION.SAVE_CONTACT'),
|
||||
onClick: addContact,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import DyteAPI from 'dashboard/api/integrations/dyte';
|
||||
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
|
||||
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BaseAttachmentBubble from './BaseAttachment.vue';
|
||||
|
||||
const props = defineProps({
|
||||
contentAttributes: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sender: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const meetingData = computed(() => {
|
||||
return useCamelCase(props.contentAttributes.data);
|
||||
});
|
||||
|
||||
const isLoading = ref(false);
|
||||
const dyteAuthToken = ref('');
|
||||
|
||||
const meetingLink = computed(() => {
|
||||
return buildDyteURL(meetingData.value.roomName, dyteAuthToken.value);
|
||||
});
|
||||
|
||||
const joinTheCall = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const { data: { authResponse: { authToken } = {} } = {} } =
|
||||
await DyteAPI.addParticipantToMeeting(meetingData.value.messageId);
|
||||
dyteAuthToken.value = authToken;
|
||||
} catch (err) {
|
||||
useAlert(t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const leaveTheRoom = () => {
|
||||
this.dyteAuthToken = '';
|
||||
};
|
||||
const action = computed(() => ({
|
||||
label: t('INTEGRATION_SETTINGS.DYTE.CLICK_HERE_TO_JOIN'),
|
||||
onClick: joinTheCall,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseAttachmentBubble
|
||||
icon="i-ph-video-camera-fill"
|
||||
icon-bg-color="bg-[#2781F6]"
|
||||
:sender="sender"
|
||||
sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.MEETING"
|
||||
:action="action"
|
||||
>
|
||||
<div v-if="dyteAuthToken" class="video-call--container">
|
||||
<iframe
|
||||
:src="meetingLink"
|
||||
allow="camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;"
|
||||
/>
|
||||
<button
|
||||
class="bg-n-solid-3 px-4 py-2 rounded-lg text-sm"
|
||||
@click="leaveTheRoom"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ '' }}
|
||||
</div>
|
||||
</BaseAttachmentBubble>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.join-call-button {
|
||||
margin: var(--space-small) 0;
|
||||
}
|
||||
|
||||
.video-call--container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: var(--z-index-high);
|
||||
padding: var(--space-smaller);
|
||||
background: var(--b-800);
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: var(--space-smaller);
|
||||
right: 10rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { MESSAGE_STATUS } from '../../constants';
|
||||
|
||||
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 hasError = computed(() => {
|
||||
return props.status === MESSAGE_STATUS.FAILED;
|
||||
});
|
||||
|
||||
const fromEmail = computed(() => {
|
||||
return props.contentAttributes?.email?.from ?? [];
|
||||
});
|
||||
|
||||
const toEmail = computed(() => {
|
||||
return props.contentAttributes?.email?.to ?? [];
|
||||
});
|
||||
|
||||
const ccEmail = computed(() => {
|
||||
return (
|
||||
props.contentAttributes?.ccEmails ??
|
||||
props.contentAttributes?.email?.cc ??
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
const senderName = computed(() => {
|
||||
return props.sender.name ?? '';
|
||||
});
|
||||
|
||||
const bccEmail = computed(() => {
|
||||
return (
|
||||
props.contentAttributes?.bccEmails ??
|
||||
props.contentAttributes?.email?.bcc ??
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
const subject = computed(() => {
|
||||
return props.contentAttributes?.email?.subject ?? '';
|
||||
});
|
||||
|
||||
const showMeta = computed(() => {
|
||||
return (
|
||||
fromEmail.value[0] ||
|
||||
toEmail.value.length ||
|
||||
ccEmail.value.length ||
|
||||
bccEmail.value.length ||
|
||||
subject.value
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
v-show="showMeta"
|
||||
class="p-4 space-y-1 pr-9 border-b border-n-strong"
|
||||
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-11'"
|
||||
>
|
||||
<template v-if="showMeta">
|
||||
<div v-if="fromEmail[0]">
|
||||
<span :class="hasError ? 'text-n-ruby-11' : 'text-n-slate-12'">
|
||||
{{ senderName }}
|
||||
</span>
|
||||
<{{ fromEmail[0] }}>
|
||||
</div>
|
||||
<div v-if="toEmail.length">
|
||||
{{ $t('EMAIL_HEADER.TO') }}: {{ toEmail.join(', ') }}
|
||||
</div>
|
||||
<div v-if="ccEmail.length">
|
||||
{{ $t('EMAIL_HEADER.CC') }}:
|
||||
{{ ccEmail.join(', ') }}
|
||||
</div>
|
||||
<div v-if="bccEmail.length">
|
||||
{{ $t('EMAIL_HEADER.BCC') }}:
|
||||
{{ bccEmail.join(', ') }}
|
||||
</div>
|
||||
<div v-if="subject">
|
||||
{{ $t('EMAIL_HEADER.SUBJECT') }}:
|
||||
{{ subject }}
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup>
|
||||
import { computed, useTemplateRef, ref, onMounted } from 'vue';
|
||||
import { Letter } from 'vue-letter';
|
||||
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
const isExpandable = ref(false);
|
||||
const isExpanded = ref(false);
|
||||
const showQuotedMessage = ref(false);
|
||||
const contentContainer = useTemplateRef('contentContainer');
|
||||
|
||||
onMounted(() => {
|
||||
isExpandable.value = contentContainer.value.scrollHeight > 400;
|
||||
});
|
||||
|
||||
const isOutgoing = computed(() => {
|
||||
return props.messageType === MESSAGE_TYPES.OUTGOING;
|
||||
});
|
||||
|
||||
const fullHTML = computed(() => {
|
||||
return props.contentAttributes?.email?.htmlContent?.full ?? props.content;
|
||||
});
|
||||
|
||||
const unquotedHTML = computed(() => {
|
||||
return EmailQuoteExtractor.extractQuotes(fullHTML.value);
|
||||
});
|
||||
|
||||
const hasQuotedMessage = computed(() => {
|
||||
return EmailQuoteExtractor.hasQuotes(fullHTML.value);
|
||||
});
|
||||
|
||||
const textToShow = computed(() => {
|
||||
const text =
|
||||
props.contentAttributes?.email?.textContent?.full ?? props.content;
|
||||
return text.replace(/\n/g, '<br>');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="w-full overflow-hidden" data-bubble-name="email">
|
||||
<EmailMeta :status :sender :content-attributes />
|
||||
<section
|
||||
ref="contentContainer"
|
||||
class="p-4"
|
||||
:class="{
|
||||
'max-h-[400px] overflow-hidden relative': !isExpanded && isExpandable,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="isExpandable && !isExpanded"
|
||||
class="absolute left-0 right-0 bottom-0 h-40 p-8 flex items-end bg-gradient-to-t dark:from-[#24252b] from-[#F5F5F6] dark:via-[rgba(36,37,43,0.5)] via-[rgba(245,245,246,0.50)] dark:to-transparent to-[rgba(245,245,246,0.00)]"
|
||||
>
|
||||
<button
|
||||
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
|
||||
@click="isExpanded = true"
|
||||
>
|
||||
<Icon icon="i-lucide-maximize-2" />
|
||||
{{ $t('EMAIL_HEADER.EXPAND') }}
|
||||
</button>
|
||||
</div>
|
||||
<FormattedContent v-if="isOutgoing && content" :content="content" />
|
||||
<template v-else>
|
||||
<Letter
|
||||
v-if="showQuotedMessage"
|
||||
class-name="prose prose-email !max-w-none"
|
||||
:html="fullHTML"
|
||||
:text="textToShow"
|
||||
/>
|
||||
<Letter
|
||||
v-else
|
||||
class-name="prose prose-email !max-w-none"
|
||||
:html="unquotedHTML"
|
||||
:text="textToShow"
|
||||
/>
|
||||
</template>
|
||||
<button
|
||||
v-if="hasQuotedMessage"
|
||||
class="text-n-slate-11 px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2"
|
||||
@click="showQuotedMessage = !showQuotedMessage"
|
||||
>
|
||||
<template v-if="showQuotedMessage">
|
||||
{{ $t('CHAT_LIST.HIDE_QUOTED_TEXT') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('CHAT_LIST.SHOW_QUOTED_TEXT') }}
|
||||
</template>
|
||||
<Icon
|
||||
:icon="
|
||||
showQuotedMessage ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</section>
|
||||
<section v-if="attachments.length" class="px-4 pb-4 space-y-2">
|
||||
<AttachmentChips :attachments="attachments" class="gap-1" />
|
||||
</section>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
@@ -0,0 +1,126 @@
|
||||
// Quote detection strategies
|
||||
const QUOTE_INDICATORS = [
|
||||
'.gmail_quote_container',
|
||||
'.gmail_quote',
|
||||
'.OutlookQuote',
|
||||
'.email-quote',
|
||||
'.quoted-text',
|
||||
'.quote',
|
||||
'[class*="quote"]',
|
||||
'[class*="Quote"]',
|
||||
];
|
||||
|
||||
// Regex patterns for quote identification
|
||||
const QUOTE_PATTERNS = [
|
||||
/On .* wrote:/i,
|
||||
/-----Original Message-----/i,
|
||||
/Sent: /i,
|
||||
/From: /i,
|
||||
];
|
||||
|
||||
export class EmailQuoteExtractor {
|
||||
/**
|
||||
* Remove quotes from email HTML and return cleaned HTML
|
||||
* @param {string} htmlContent - Full HTML content of the email
|
||||
* @returns {string} HTML content with quotes removed
|
||||
*/
|
||||
static extractQuotes(htmlContent) {
|
||||
// Create a temporary DOM element to parse HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
|
||||
// Remove elements matching class selectors
|
||||
QUOTE_INDICATORS.forEach(selector => {
|
||||
tempDiv.querySelectorAll(selector).forEach(el => {
|
||||
el.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Remove text-based quotes
|
||||
const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
|
||||
textNodeQuotes.forEach(el => {
|
||||
el.remove();
|
||||
});
|
||||
|
||||
return tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if HTML content contains any quotes
|
||||
* @param {string} htmlContent - Full HTML content of the email
|
||||
* @returns {boolean} True if quotes are detected, false otherwise
|
||||
*/
|
||||
static hasQuotes(htmlContent) {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
|
||||
// Check for class-based quotes
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const selector of QUOTE_INDICATORS) {
|
||||
if (tempDiv.querySelector(selector)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for text-based quotes
|
||||
const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
|
||||
return textNodeQuotes.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find text nodes that match quote patterns
|
||||
* @param {Element} rootElement - Root element to search
|
||||
* @returns {Element[]} Array of parent block elements containing quote-like text
|
||||
*/
|
||||
static findTextNodeQuotes(rootElement) {
|
||||
const quoteBlocks = [];
|
||||
const treeWalker = document.createTreeWalker(
|
||||
rootElement,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
for (
|
||||
let currentNode = treeWalker.nextNode();
|
||||
currentNode !== null;
|
||||
currentNode = treeWalker.nextNode()
|
||||
) {
|
||||
const isQuoteLike = QUOTE_PATTERNS.some(pattern =>
|
||||
pattern.test(currentNode.textContent)
|
||||
);
|
||||
|
||||
if (isQuoteLike) {
|
||||
const parentBlock = this.findParentBlock(currentNode);
|
||||
if (parentBlock && !quoteBlocks.includes(parentBlock)) {
|
||||
quoteBlocks.push(parentBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return quoteBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest block-level parent element by recursively traversing up the DOM tree.
|
||||
* This method searches for common block-level elements like DIV, P, BLOCKQUOTE, and SECTION
|
||||
* that contain the text node. It's used to identify and remove entire block-level elements
|
||||
* that contain quote-like text, rather than just removing the text node itself. This ensures
|
||||
* proper structural removal of quoted content while maintaining HTML integrity.
|
||||
* @param {Node} node - Starting node to find parent
|
||||
* @returns {Element|null} Block-level parent element
|
||||
*/
|
||||
static findParentBlock(node) {
|
||||
const blockElements = ['DIV', 'P', 'BLOCKQUOTE', 'SECTION'];
|
||||
let current = node.parentElement;
|
||||
|
||||
while (current) {
|
||||
if (blockElements.includes(current.tagName)) {
|
||||
return current;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
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 { t } = useI18n();
|
||||
|
||||
const url = computed(() => {
|
||||
return props.attachments[0].dataUrl;
|
||||
});
|
||||
|
||||
const fileName = computed(() => {
|
||||
if (url.value) {
|
||||
const filename = url.value.substring(url.value.lastIndexOf('/') + 1);
|
||||
return filename || t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||
}
|
||||
return t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||
});
|
||||
|
||||
const fileType = computed(() => {
|
||||
return fileName.value.split('.').pop();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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="{
|
||||
href: url,
|
||||
label: $t('CONVERSATION.DOWNLOAD'),
|
||||
}"
|
||||
>
|
||||
<template #icon>
|
||||
<FileIcon :file-type="fileType" class="size-4" />
|
||||
</template>
|
||||
</BaseAttachmentBubble>
|
||||
</template>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
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 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 attachment = computed(() => {
|
||||
return props.attachments[0];
|
||||
});
|
||||
|
||||
const hasError = ref(false);
|
||||
const showGallery = ref(false);
|
||||
const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
|
||||
const handleError = () => {
|
||||
hasError.value = true;
|
||||
emit('error');
|
||||
};
|
||||
|
||||
const downloadAttachment = async () => {
|
||||
const response = await fetch(attachment.value.dataUrl);
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `attachment${attachment.value.extension || ''}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="overflow-hidden relative group border-[4px] border-n-weak"
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
<img
|
||||
:src="attachment.dataUrl"
|
||||
:width="attachment.width"
|
||||
:height="attachment.height"
|
||||
@click="onClick"
|
||||
@error="handleError"
|
||||
/>
|
||||
<div
|
||||
class="inset-0 p-2 absolute bg-gradient-to-tl from-n-slate-12/30 dark:from-n-slate-1/50 via-transparent to-transparent hidden group-hover:flex items-end justify-end gap-1.5"
|
||||
>
|
||||
<Button xs solid slate icon="i-lucide-expand" class="opacity-60" />
|
||||
<Button
|
||||
xs
|
||||
solid
|
||||
slate
|
||||
icon="i-lucide-download"
|
||||
class="opacity-60"
|
||||
@click="downloadAttachment"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</BaseBubble>
|
||||
<GalleryView
|
||||
v-if="showGallery"
|
||||
v-model:show="showGallery"
|
||||
:attachment="useSnakeCase(attachment)"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
@error="handleError"
|
||||
@close="() => (showGallery = false)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
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 attachment = computed(() => {
|
||||
return props.attachments[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 new MessageFormatter(props.content).formattedMessage;
|
||||
});
|
||||
|
||||
const onImageLoadError = () => {
|
||||
hasImgStoryError.value = true;
|
||||
emit('error');
|
||||
};
|
||||
|
||||
const onVideoLoadError = () => {
|
||||
hasVideoStoryError.value = true;
|
||||
emit('error');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="p-3 overflow-hidden" data-bubble-name="ig-story">
|
||||
<div v-if="content" class="mb-2" v-html="formattedContent" />
|
||||
<img
|
||||
v-if="!hasImgStoryError"
|
||||
class="rounded-lg max-w-80"
|
||||
:src="attachment.dataUrl"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<video
|
||||
v-else-if="!hasVideoStoryError"
|
||||
class="rounded-lg max-w-80"
|
||||
controls
|
||||
:src="attachment.dataUrl"
|
||||
@error="onVideoLoadError"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-1 px-5 py-4 text-center rounded-lg bg-n-alpha-1"
|
||||
>
|
||||
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
|
||||
<p class="mb-0 text-n-slate-11">
|
||||
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, nextTick, useTemplateRef } 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: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const attachment = computed(() => {
|
||||
return props.attachments[0];
|
||||
});
|
||||
|
||||
const lat = computed(() => {
|
||||
return attachment.value.coordinatesLat;
|
||||
});
|
||||
const long = computed(() => {
|
||||
return attachment.value.coordinatesLong;
|
||||
});
|
||||
|
||||
const title = computed(() => {
|
||||
return attachment.value.fallbackTitle;
|
||||
});
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useMessageContext } from '../../provider.js';
|
||||
|
||||
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
|
||||
import { MESSAGE_VARIANTS } from '../../constants';
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { variant } = useMessageContext();
|
||||
|
||||
const formattedContent = computed(() => {
|
||||
if (variant.value === MESSAGE_VARIANTS.ACTIVITY) {
|
||||
return props.content;
|
||||
}
|
||||
|
||||
return new MessageFormatter(props.content).formattedMessage;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
v-dompurify-html="formattedContent"
|
||||
class="[&>p:last-child]:mb-0 [&>ul]:list-inside"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
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';
|
||||
|
||||
/**
|
||||
* @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 isTemplate = computed(() => {
|
||||
return props.messageType === MESSAGE_TYPES.TEMPLATE;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="flex flex-col gap-3 px-4 py-3" data-bubble-name="text">
|
||||
<FormattedContent v-if="content" :content="content" />
|
||||
<AttachmentChips :attachments="attachments" class="gap-2" />
|
||||
<template v-if="isTemplate">
|
||||
<div
|
||||
v-if="contentAttributes.submittedEmail"
|
||||
class="px-2 py-1 rounded-lg bg-n-alpha-3"
|
||||
>
|
||||
{{ contentAttributes.submittedEmail }}
|
||||
</div>
|
||||
</template>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
import BaseBubble from './Base.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="px-4 py-3 text-sm" data-bubble-name="unsupported">
|
||||
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE') }}
|
||||
</BaseBubble>
|
||||
</template>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup>
|
||||
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 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 handleError = () => {
|
||||
hasError.value = true;
|
||||
emit('error');
|
||||
};
|
||||
|
||||
const attachment = computed(() => {
|
||||
return props.attachments[0];
|
||||
});
|
||||
|
||||
const isReel = computed(() => {
|
||||
return attachment.value.fileType === ATTACHMENT_TYPES.IG_REEL;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="overflow-hidden relative group border-[4px] border-n-weak"
|
||||
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>
|
||||
<video
|
||||
controls
|
||||
:src="attachment.dataUrl"
|
||||
:class="{
|
||||
'max-w-48': isReel,
|
||||
'max-w-full': !isReel,
|
||||
}"
|
||||
@error="handleError"
|
||||
/>
|
||||
</BaseBubble>
|
||||
<GalleryView
|
||||
v-if="showGallery"
|
||||
v-model:show="showGallery"
|
||||
:attachment="useSnakeCase(attachment)"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
@error="onError"
|
||||
@close="() => (showGallery = false)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import { computed, defineOptions, useAttrs } from 'vue';
|
||||
|
||||
import ImageChip from 'next/message/chips/Image.vue';
|
||||
import VideoChip from 'next/message/chips/Video.vue';
|
||||
import AudioChip from 'next/message/chips/Audio.vue';
|
||||
import FileChip from 'next/message/chips/File.vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
|
||||
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
|
||||
*/
|
||||
const props = defineProps({
|
||||
attachments: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const attrs = useAttrs();
|
||||
const { orientation } = useMessageContext();
|
||||
|
||||
const classToApply = computed(() => {
|
||||
const baseClasses = [attrs.class, 'flex', 'flex-wrap'];
|
||||
|
||||
if (orientation.value === 'right') {
|
||||
baseClasses.push('justify-end');
|
||||
}
|
||||
|
||||
return baseClasses;
|
||||
});
|
||||
|
||||
const allAttachments = computed(() => {
|
||||
return Array.isArray(props.attachments) ? props.attachments : [];
|
||||
});
|
||||
|
||||
const mediaAttachments = computed(() => {
|
||||
const allowedTypes = [ATTACHMENT_TYPES.IMAGE, ATTACHMENT_TYPES.VIDEO];
|
||||
const mediaTypes = allAttachments.value.filter(attachment =>
|
||||
allowedTypes.includes(attachment.fileType)
|
||||
);
|
||||
|
||||
return mediaTypes.sort(
|
||||
(a, b) =>
|
||||
allowedTypes.indexOf(a.fileType) - allowedTypes.indexOf(b.fileType)
|
||||
);
|
||||
});
|
||||
|
||||
const recordings = computed(() => {
|
||||
return allAttachments.value.filter(
|
||||
attachment => attachment.fileType === ATTACHMENT_TYPES.AUDIO
|
||||
);
|
||||
});
|
||||
|
||||
const files = computed(() => {
|
||||
return allAttachments.value.filter(
|
||||
attachment => attachment.fileType === ATTACHMENT_TYPES.FILE
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="mediaAttachments.length" :class="classToApply">
|
||||
<template v-for="attachment in mediaAttachments" :key="attachment.id">
|
||||
<ImageChip
|
||||
v-if="attachment.fileType === ATTACHMENT_TYPES.IMAGE"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
<VideoChip
|
||||
v-else-if="attachment.fileType === ATTACHMENT_TYPES.VIDEO"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="recordings.length" :class="classToApply">
|
||||
<div v-for="attachment in recordings" :key="attachment.id">
|
||||
<AudioChip
|
||||
class="bg-n-alpha-3 dark:bg-n-alpha-2 text-n-slate-12"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="files.length" :class="classToApply">
|
||||
<FileChip
|
||||
v-for="attachment in files"
|
||||
:key="attachment.id"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
133
app/javascript/dashboard/components-next/message/chips/Audio.vue
Normal file
133
app/javascript/dashboard/components-next/message/chips/Audio.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<script setup>
|
||||
import { computed, useTemplateRef, ref } from 'vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { timeStampAppendedURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
const { attachment } = defineProps({
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const timeStampURL = computed(() => {
|
||||
return timeStampAppendedURL(attachment.dataUrl);
|
||||
});
|
||||
|
||||
const audioPlayer = useTemplateRef('audioPlayer');
|
||||
|
||||
const isPlaying = ref(false);
|
||||
const isMuted = ref(false);
|
||||
const currentTime = ref(0);
|
||||
const duration = ref(0);
|
||||
|
||||
const onLoadedMetadata = () => {
|
||||
duration.value = audioPlayer.value.duration;
|
||||
};
|
||||
|
||||
const formatTime = time => {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
audioPlayer.value.muted = !audioPlayer.value.muted;
|
||||
isMuted.value = audioPlayer.value.muted;
|
||||
};
|
||||
|
||||
const onTimeUpdate = () => {
|
||||
currentTime.value = audioPlayer.value.currentTime;
|
||||
};
|
||||
|
||||
const seek = event => {
|
||||
const time = Number(event.target.value);
|
||||
audioPlayer.value.currentTime = time;
|
||||
currentTime.value = time;
|
||||
};
|
||||
|
||||
const playOrPause = () => {
|
||||
if (isPlaying.value) {
|
||||
audioPlayer.value.pause();
|
||||
isPlaying.value = false;
|
||||
} else {
|
||||
audioPlayer.value.play();
|
||||
isPlaying.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onEnd = () => {
|
||||
isPlaying.value = false;
|
||||
currentTime.value = 0;
|
||||
};
|
||||
|
||||
const downloadAudio = async () => {
|
||||
const response = await fetch(timeStampURL.value);
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
const filename = timeStampURL.value.split('/').pop().split('?')[0] || 'audio';
|
||||
anchor.download = filename;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(anchor);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<audio
|
||||
ref="audioPlayer"
|
||||
controls
|
||||
class="hidden"
|
||||
@loadedmetadata="onLoadedMetadata"
|
||||
@timeupdate="onTimeUpdate"
|
||||
@ended="onEnd"
|
||||
>
|
||||
<source :src="timeStampURL" />
|
||||
</audio>
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
class="rounded-xl w-full gap-1 p-1.5 bg-n-alpha-white flex items-center border border-n-container shadow-[0px_2px_8px_0px_rgba(94,94,94,0.06)]"
|
||||
>
|
||||
<button class="p-0 border-0 size-8" @click="playOrPause">
|
||||
<Icon
|
||||
v-if="isPlaying"
|
||||
class="size-8"
|
||||
icon="i-teenyicons-pause-small-solid"
|
||||
/>
|
||||
<Icon v-else class="size-8" icon="i-teenyicons-play-small-solid" />
|
||||
</button>
|
||||
<div class="tabular-nums text-xs">
|
||||
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||
</div>
|
||||
<div class="flex items-center px-2">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
:max="duration"
|
||||
:value="currentTime"
|
||||
class="w-full h-1 bg-n-slate-12/40 rounded-lg appearance-none cursor-pointer accent-current"
|
||||
@input="seek"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="p-0 border-0 size-8 grid place-content-center"
|
||||
@click="toggleMute"
|
||||
>
|
||||
<Icon v-if="isMuted" class="size-4" icon="i-lucide-volume-off" />
|
||||
<Icon v-else class="size-4" icon="i-lucide-volume-2" />
|
||||
</button>
|
||||
<button
|
||||
class="p-0 border-0 size-8 grid place-content-center"
|
||||
@click="downloadAudio"
|
||||
>
|
||||
<Icon class="size-4" icon="i-lucide-download" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import FileIcon from 'next/icon/FileIcon.vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
|
||||
const { attachment } = defineProps({
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const fileName = computed(() => {
|
||||
const url = attachment.dataUrl;
|
||||
if (url) {
|
||||
const filename = url.substring(url.lastIndexOf('/') + 1);
|
||||
return filename || t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||
}
|
||||
return t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||
});
|
||||
|
||||
const fileType = computed(() => {
|
||||
return fileName.value.split('.').pop();
|
||||
});
|
||||
|
||||
const textColorClass = computed(() => {
|
||||
const colorMap = {
|
||||
'7z': 'dark:text-[#EDEEF0] text-[#2F265F]',
|
||||
csv: 'text-amber-12',
|
||||
doc: 'dark:text-[#D6E1FF] text-[#1F2D5C]', // indigo-12
|
||||
docx: 'dark:text-[#D6E1FF] text-[#1F2D5C]', // indigo-12
|
||||
json: 'text-n-slate-12',
|
||||
odt: 'dark:text-[#D6E1FF] text-[#1F2D5C]', // indigo-12
|
||||
pdf: 'text-n-ruby-12',
|
||||
ppt: 'dark:text-[#FFE0C2] text-[#582D1D]',
|
||||
pptx: 'dark:text-[#FFE0C2] text-[#582D1D]',
|
||||
rar: 'dark:text-[#EDEEF0] text-[#2F265F]',
|
||||
rtf: 'dark:text-[#D6E1FF] text-[#1F2D5C]', // indigo-12
|
||||
tar: 'dark:text-[#EDEEF0] text-[#2F265F]',
|
||||
txt: 'text-n-slate-12',
|
||||
xls: 'text-n-teal-12',
|
||||
xlsx: 'text-n-teal-12',
|
||||
zip: 'dark:text-[#EDEEF0] text-[#2F265F]',
|
||||
};
|
||||
|
||||
return colorMap[fileType.value] || 'text-n-slate-12';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-9 bg-n-alpha-white gap-2 items-center flex px-2 rounded-lg border border-n-strong"
|
||||
>
|
||||
<FileIcon class="flex-shrink-0" :file-type="fileType" />
|
||||
<span class="mr-1 max-w-32 truncate" :class="textColorClass">
|
||||
{{ fileName }}
|
||||
</span>
|
||||
<a
|
||||
v-tooltip="t('CONVERSATION.DOWNLOAD')"
|
||||
class="flex-shrink-0 h-9 grid place-content-center cursor-pointer text-n-slate-11"
|
||||
:href="url"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon icon="i-lucide-download" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
|
||||
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
|
||||
|
||||
defineProps({
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const hasError = ref(false);
|
||||
const showGallery = ref(false);
|
||||
|
||||
const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
|
||||
const handleError = () => {
|
||||
hasError.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer"
|
||||
@click="showGallery = true"
|
||||
>
|
||||
<div
|
||||
v-if="hasError"
|
||||
class="flex flex-col items-center justify-center gap-1 text-xs text-center rounded-lg size-full bg-n-alpha-1 text-n-slate-11"
|
||||
>
|
||||
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
|
||||
{{ $t('COMPONENTS.MEDIA.LOADING_FAILED') }}
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
class="object-cover w-full h-full"
|
||||
:src="attachment.dataUrl"
|
||||
@error="handleError"
|
||||
/>
|
||||
</div>
|
||||
<GalleryView
|
||||
v-if="showGallery"
|
||||
v-model:show="showGallery"
|
||||
:attachment="useSnakeCase(attachment)"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
@error="handleError"
|
||||
@close="() => (showGallery = false)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
|
||||
|
||||
defineProps({
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const showGallery = ref(false);
|
||||
|
||||
const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group"
|
||||
@click="showGallery = true"
|
||||
>
|
||||
<video
|
||||
:src="attachment.dataUrl"
|
||||
class="w-full h-full object-cover"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
<div
|
||||
class="absolute w-full h-full inset-0 p-1 flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
class="size-7 bg-n-slate-1/60 backdrop-blur-sm rounded-full overflow-hidden shadow-[0_5px_15px_rgba(0,0,0,0.4)]"
|
||||
>
|
||||
<Icon
|
||||
icon="i-teenyicons-play-small-solid"
|
||||
class="size-7 text-n-slate-12/80 backdrop-blur"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GalleryView
|
||||
v-if="showGallery"
|
||||
v-model:show="showGallery"
|
||||
:attachment="useSnakeCase(attachment)"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
@error="onError"
|
||||
@close="() => (showGallery = false)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
export const MESSAGE_TYPES = {
|
||||
INCOMING: 0,
|
||||
OUTGOING: 1,
|
||||
ACTIVITY: 2,
|
||||
TEMPLATE: 3,
|
||||
};
|
||||
|
||||
export const MESSAGE_VARIANTS = {
|
||||
USER: 'user',
|
||||
AGENT: 'agent',
|
||||
ACTIVITY: 'activity',
|
||||
PRIVATE: 'private',
|
||||
BOT: 'bot',
|
||||
ERROR: 'error',
|
||||
TEMPLATE: 'template',
|
||||
EMAIL: 'email',
|
||||
UNSUPPORTED: 'unsupported',
|
||||
};
|
||||
|
||||
export const SENDER_TYPES = {
|
||||
CONTACT: 'Contact',
|
||||
USER: 'User',
|
||||
};
|
||||
|
||||
export const ORIENTATION = {
|
||||
LEFT: 'left',
|
||||
RIGHT: 'right',
|
||||
CENTER: 'center',
|
||||
};
|
||||
|
||||
export const MESSAGE_STATUS = {
|
||||
SENT: 'sent',
|
||||
DELIVERED: 'delivered',
|
||||
READ: 'read',
|
||||
FAILED: 'failed',
|
||||
PROGRESS: 'progress',
|
||||
};
|
||||
|
||||
export const ATTACHMENT_TYPES = {
|
||||
IMAGE: 'image',
|
||||
AUDIO: 'audio',
|
||||
VIDEO: 'video',
|
||||
FILE: 'file',
|
||||
LOCATION: 'location',
|
||||
FALLBACK: 'fallback',
|
||||
SHARE: 'share',
|
||||
STORY_MENTION: 'story_mention',
|
||||
CONTACT: 'contact',
|
||||
IG_REEL: 'ig_reel',
|
||||
};
|
||||
|
||||
export const CONTENT_TYPES = {
|
||||
TEXT: 'text',
|
||||
INPUT_TEXT: 'input_text',
|
||||
INPUT_TEXTAREA: 'input_textarea',
|
||||
INPUT_EMAIL: 'input_email',
|
||||
INPUT_SELECT: 'input_select',
|
||||
CARDS: 'cards',
|
||||
FORM: 'form',
|
||||
ARTICLE: 'article',
|
||||
INCOMING_EMAIL: 'incoming_email',
|
||||
INPUT_CSAT: 'input_csat',
|
||||
INTEGRATIONS: 'integrations',
|
||||
STICKER: 'sticker',
|
||||
};
|
||||
|
||||
export const MEDIA_TYPES = [
|
||||
ATTACHMENT_TYPES.IMAGE,
|
||||
ATTACHMENT_TYPES.VIDEO,
|
||||
ATTACHMENT_TYPES.AUDIO,
|
||||
ATTACHMENT_TYPES.IG_REEL,
|
||||
];
|
||||
@@ -0,0 +1,382 @@
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
|
||||
export default camelcaseKeys(
|
||||
[
|
||||
{
|
||||
id: 60913,
|
||||
content:
|
||||
'Dear Sam,\n\nWe are looking for high-quality cotton fabric for our T-shirt production.\nPlease find attached a document with our specifications and requirements.\nCould you provide us with a quotation and lead time?\n\nLooking forward to your response.\n\nBest regards,\nAlex\nT-Shirt Co.',
|
||||
inbox_id: 992,
|
||||
conversation_id: 134,
|
||||
message_type: 0,
|
||||
content_type: 'incoming_email',
|
||||
status: 'sent',
|
||||
content_attributes: {
|
||||
email: {
|
||||
bcc: null,
|
||||
cc: null,
|
||||
content_type:
|
||||
'multipart/mixed; boundary=00000000000098e88e0628704c8b',
|
||||
date: '2024-12-04T17:13:53+05:30',
|
||||
from: ['alex@paperlayer.test'],
|
||||
html_content: {
|
||||
full: '<div dir="ltr"><p>Dear Sam,</p><p>We are looking for high-quality cotton fabric for our T-shirt production. Please find attached a document with our specifications and requirements. Could you provide us with a quotation and lead time?</p><p>Looking forward to your response.</p><p>Best regards,<br>Alex<br>T-Shirt Co.</p></div>\n',
|
||||
reply:
|
||||
'Dear Sam,\n\nWe are looking for high-quality cotton fabric for our T-shirt production. Please find attached a document with our specifications and requirements. Could you provide us with a quotation and lead time?\n\nLooking forward to your response.\n\nBest regards,\nAlex\nT-Shirt Co.',
|
||||
quoted:
|
||||
'Dear Sam,\n\nWe are looking for high-quality cotton fabric for our T-shirt production. Please find attached a document with our specifications and requirements. Could you provide us with a quotation and lead time?\n\nLooking forward to your response.\n\nBest regards,\nAlex\nT-Shirt Co.',
|
||||
},
|
||||
in_reply_to: null,
|
||||
message_id:
|
||||
'CAM_Qp+-tdJ2Muy4XZmQfYKOPzsFwrH5H=6j=snsFZEDw@mail.gmail.com',
|
||||
multipart: true,
|
||||
number_of_attachments: 2,
|
||||
subject: 'Inquiry and Quotation for Cotton Fabric',
|
||||
text_content: {
|
||||
full: 'Dear Sam,\n\nWe are looking for high-quality cotton fabric for our T-shirt production.\nPlease find attached a document with our specifications and requirements.\nCould you provide us with a quotation and lead time?\n\nLooking forward to your response.\n\nBest regards,\nAlex\nT-Shirt Co.\n',
|
||||
reply:
|
||||
'Dear Sam,\n\nWe are looking for high-quality cotton fabric for our T-shirt production.\nPlease find attached a document with our specifications and requirements.\nCould you provide us with a quotation and lead time?\n\nLooking forward to your response.\n\nBest regards,\nAlex\nT-Shirt Co.',
|
||||
quoted:
|
||||
'Dear Sam,\n\nWe are looking for high-quality cotton fabric for our T-shirt production.\nPlease find attached a document with our specifications and requirements.\nCould you provide us with a quotation and lead time?\n\nLooking forward to your response.\n\nBest regards,\nAlex\nT-Shirt Co.',
|
||||
},
|
||||
to: ['sam@cottonmart.test'],
|
||||
},
|
||||
cc_email: null,
|
||||
bcc_email: null,
|
||||
},
|
||||
created_at: 1733312661,
|
||||
private: false,
|
||||
source_id: 'CAM_Qp+-tdJ2Muy4XZmQfYKOPzsFwrH5H=6j=snsFZEDw@mail.gmail.com',
|
||||
sender: {
|
||||
additional_attributes: {
|
||||
source_id: 'email:CAM_Qp+8beyon41DA@mail.gmail.com',
|
||||
},
|
||||
custom_attributes: {},
|
||||
email: 'alex@paperlayer.test',
|
||||
id: 111256,
|
||||
identifier: null,
|
||||
name: 'Alex',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
id: 826,
|
||||
message_id: 60913,
|
||||
file_type: 'file',
|
||||
account_id: 51,
|
||||
extension: null,
|
||||
data_url:
|
||||
'https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdFdKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--10170e22f42401a9259e17eba6e59877127353d0/requirements.pdf',
|
||||
thumb_url:
|
||||
'https://staging.chatwoot.com/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdFdKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--10170e22f42401a9259e17eba6e59877127353d0/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9UY21WemFYcGxYM1J2WDJacGJHeGJCMmtCK2pBPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--31a6ed995cc4ac2dd2fa023068ee23b23efa1efb/requirements.pdf',
|
||||
file_size: 841909,
|
||||
width: null,
|
||||
height: null,
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
message_id: 5307,
|
||||
file_type: 'file',
|
||||
account_id: 2,
|
||||
extension: null,
|
||||
data_url:
|
||||
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaUVLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4f7e671db635b73d12ee004e87608bc098ef6b3b/quantity-requirements.xls',
|
||||
thumb_url:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaUVLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4f7e671db635b73d12ee004e87608bc098ef6b3b/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9UY21WemFYcGxYM1J2WDJacGJHeGJCMmtCK2pBPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--5c454d5f03daf1f9f4068cb242cf9885cc1815b6/all-files.zip',
|
||||
file_size: 99844,
|
||||
width: null,
|
||||
height: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 60914,
|
||||
content:
|
||||
'Dear Alex,\r\n\r\nThank you for your inquiry. Please find attached our quotation based on your requirements. Let us know if you need further details or wish to discuss specific customizations.\r\n\r\nBest regards, \r\nSam \r\nFabricMart',
|
||||
account_id: 51,
|
||||
inbox_id: 992,
|
||||
conversation_id: 134,
|
||||
message_type: 1,
|
||||
created_at: 1733312726,
|
||||
updated_at: '2024-12-04T11:45:34.451Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id:
|
||||
'conversation/758d1f24-dc76-4abc-9c41-255ed8974f8e/messages/60914@reply.chatwoot.dev',
|
||||
content_type: 'text',
|
||||
content_attributes: {
|
||||
cc_emails: [],
|
||||
bcc_emails: [],
|
||||
to_emails: [],
|
||||
},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content:
|
||||
'Dear Alex,\r\n\r\nThank you for your inquiry. Please find attached our quotation based on your requirements. Let us know if you need further details or wish to discuss specific customizations.\r\n\r\nBest regards, \r\nSam \r\nFabricMart',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 110,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1733312726,
|
||||
contact_inbox: {
|
||||
source_id: 'alex@paperlayer.test',
|
||||
},
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
id: 827,
|
||||
message_id: 60914,
|
||||
file_type: 'file',
|
||||
account_id: 51,
|
||||
extension: null,
|
||||
data_url:
|
||||
'https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGFKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--940f9c3df19ce042ef3447809c9c451cfa4e905b/quotation.pdf',
|
||||
thumb_url:
|
||||
'https://staging.chatwoot.com/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGFKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--940f9c3df19ce042ef3447809c9c451cfa4e905b/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9UY21WemFYcGxYM1J2WDJacGJHeGJCMmtCK2pBPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--31a6ed995cc4ac2dd2fa023068ee23b23efa1efb/quotation.pdf',
|
||||
file_size: 841909,
|
||||
width: null,
|
||||
height: null,
|
||||
},
|
||||
],
|
||||
sender: {
|
||||
id: 110,
|
||||
name: 'Alex',
|
||||
available_name: 'Alex',
|
||||
avatar_url:
|
||||
'https://staging.chatwoot.com/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBbktJIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--25806e8b52810484d3d6cb53af9e2a1c0cf1b43d/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--988d66f5e450207265d5c21bb0edb3facb890a43/slick-deploy.png',
|
||||
type: 'user',
|
||||
availability_status: 'online',
|
||||
thumbnail:
|
||||
'https://staging.chatwoot.com/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBbktJIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--25806e8b52810484d3d6cb53af9e2a1c0cf1b43d/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--988d66f5e450207265d5c21bb0edb3facb890a43/slick-deploy.png',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 60915,
|
||||
content:
|
||||
'Dear Sam,\n\nThank you for the quotation. Could you share images or samples of the\nfabric for us to review before proceeding?\n\nBest,\nAlex\n\nOn Wed, 4 Dec 2024 at 17:15, Sam from CottonMart <sam@cottonmart.test> wrote:\n\n> Dear Alex,\n>\n> Thank you for your inquiry. Please find attached our quotation based on\n> your requirements. Let us know if you need further details or wish to\n> discuss specific customizations.\n>\n> Best regards,\n> Sam\n> FabricMart\n> attachment [click here to view\n> <https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGFKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--940f9c3df19ce042ef3447809c9c451cfa4e905b/quotation.pdf>]\n>',
|
||||
account_id: 51,
|
||||
inbox_id: 992,
|
||||
conversation_id: 134,
|
||||
message_type: 0,
|
||||
created_at: 1733312835,
|
||||
updated_at: '2024-12-04T11:47:15.876Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: 'CAM_Qp+_70EiYJ_nKMgJ6MZaD58Tq3E57QERcZgnd10g@mail.gmail.com',
|
||||
content_type: 'incoming_email',
|
||||
content_attributes: {
|
||||
email: {
|
||||
bcc: null,
|
||||
cc: null,
|
||||
content_type:
|
||||
'multipart/alternative; boundary=0000000000007191be06287054c4',
|
||||
date: '2024-12-04T17:16:07+05:30',
|
||||
from: ['alex@paperlayer.test'],
|
||||
html_content: {
|
||||
full: '<div dir="ltr"><p>Dear Sam,</p><p>Thank you for the quotation. Could you share images or samples of the fabric for us to review before proceeding?</p><p>Best,<br>Alex</p></div><br><div class="gmail_quote gmail_quote_container"><div dir="ltr" class="gmail_attr">On Wed, 4 Dec 2024 at 17:15, Sam from CottonMart <<a href="mailto:sam@cottonmart.test">sam@cottonmart.test</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"> <p>Dear Alex,</p>\n<p>Thank you for your inquiry. Please find attached our quotation based on your requirements. Let us know if you need further details or wish to discuss specific customizations.</p>\n<p>Best regards,<br>\nSam<br>\nFabricMart</p>\n\n attachment [<a href="https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGFKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--940f9c3df19ce042ef3447809c9c451cfa4e905b/quotation.pdf" target="_blank">click here to view</a>]\n</blockquote></div>\n',
|
||||
reply:
|
||||
'Dear Sam,\n\nThank you for the quotation. Could you share images or samples of the fabric for us to review before proceeding?\n\nBest,\nAlex\n\nOn Wed, 4 Dec 2024 at 17:15, Sam from CottonMart <sam@cottonmart.test> wrote:\n>',
|
||||
quoted:
|
||||
'Dear Sam,\n\nThank you for the quotation. Could you share images or samples of the fabric for us to review before proceeding?\n\nBest,\nAlex',
|
||||
},
|
||||
in_reply_to:
|
||||
'conversation/758d1f24-dc76-4abc-9c41-255ed8974f8e/messages/60914@reply.chatwoot.dev',
|
||||
message_id:
|
||||
'CAM_Qp+_70EiYJ_nKMgJ6MZaD58Tq3E57QERcZgnd10g@mail.gmail.com',
|
||||
multipart: true,
|
||||
number_of_attachments: 0,
|
||||
subject: 'Re: Inquiry and Quotation for Cotton Fabric',
|
||||
text_content: {
|
||||
full: 'Dear Sam,\n\nThank you for the quotation. Could you share images or samples of the\nfabric for us to review before proceeding?\n\nBest,\nAlex\n\nOn Wed, 4 Dec 2024 at 17:15, Sam from CottonMart <sam@cottonmart.test>\nwrote:\n\n> Dear Alex,\n>\n> Thank you for your inquiry. Please find attached our quotation based on\n> your requirements. Let us know if you need further details or wish to\n> discuss specific customizations.\n>\n> Best regards,\n> Sam\n> FabricMart\n> attachment [click here to view\n> <https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGFKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--940f9c3df19ce042ef3447809c9c451cfa4e905b/quotation.pdf>]\n>\n',
|
||||
reply:
|
||||
'Dear Sam,\n\nThank you for the quotation. Could you share images or samples of the\nfabric for us to review before proceeding?\n\nBest,\nAlex\n\nOn Wed, 4 Dec 2024 at 17:15, Sam from CottonMart <sam@cottonmart.test> wrote:\n\n> Dear Alex,\n>\n> Thank you for your inquiry. Please find attached our quotation based on\n> your requirements. Let us know if you need further details or wish to\n> discuss specific customizations.\n>\n> Best regards,\n> Sam\n> FabricMart\n> attachment [click here to view\n> <https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGFKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--940f9c3df19ce042ef3447809c9c451cfa4e905b/quotation.pdf>]\n>',
|
||||
quoted:
|
||||
'Dear Sam,\n\nThank you for the quotation. Could you share images or samples of the\nfabric for us to review before proceeding?\n\nBest,\nAlex',
|
||||
},
|
||||
to: ['sam@cottonmart.test'],
|
||||
},
|
||||
cc_email: null,
|
||||
bcc_email: null,
|
||||
},
|
||||
sender_type: 'Contact',
|
||||
sender_id: 111256,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content:
|
||||
'Dear Sam,\n\nThank you for the quotation. Could you share images or samples of the\nfabric for us to review before proceeding?\n\nBest,\nAlex',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 110,
|
||||
unread_count: 1,
|
||||
last_activity_at: 1733312835,
|
||||
contact_inbox: {
|
||||
source_id: 'alex@paperlayer.test',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
additional_attributes: {
|
||||
source_id: 'email:CAM_Qp+8beyon41DA@mail.gmail.com',
|
||||
},
|
||||
custom_attributes: {},
|
||||
email: 'alex@paperlayer.test',
|
||||
id: 111256,
|
||||
identifier: null,
|
||||
name: 'Alex',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
},
|
||||
{
|
||||
message_type: 1,
|
||||
content_type: 'text',
|
||||
source_id:
|
||||
'conversation/758d1f24-dc76-4abc-9c41-255ed8974f8e/messages/60916@reply.chatwoot.dev',
|
||||
processed_message_content:
|
||||
"Dear Alex,\r\n\r\nPlease find attached images of our cotton fabric samples. Let us know if you'd like physical samples sent to you. \r\n\r\nWarm regards, \r\nSam",
|
||||
id: 60916,
|
||||
content:
|
||||
"Dear Alex,\r\n\r\nPlease find attached images of our cotton fabric samples. Let us know if you'd like physical samples sent to you. \r\n\r\nWarm regards, \r\nSam",
|
||||
account_id: 51,
|
||||
inbox_id: 992,
|
||||
conversation_id: 134,
|
||||
created_at: 1733312866,
|
||||
updated_at: '2024-12-04T11:47:53.564Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
content_attributes: {
|
||||
cc_emails: [],
|
||||
bcc_emails: [],
|
||||
to_emails: [],
|
||||
},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 110,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1733312866,
|
||||
contact_inbox: {
|
||||
source_id: 'alex@paperlayer.test',
|
||||
},
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
id: 828,
|
||||
message_id: 60916,
|
||||
file_type: 'image',
|
||||
account_id: 51,
|
||||
extension: null,
|
||||
data_url:
|
||||
'https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGVKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--62ee3b99421bfe7d8db85959ae99ab03a899f351/image.png',
|
||||
thumb_url:
|
||||
'https://staging.chatwoot.com/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGVKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--62ee3b99421bfe7d8db85959ae99ab03a899f351/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--988d66f5e450207265d5c21bb0edb3facb890a43/image.png',
|
||||
file_size: 1617507,
|
||||
width: 1600,
|
||||
height: 900,
|
||||
},
|
||||
],
|
||||
sender: {
|
||||
id: 110,
|
||||
name: 'Alex',
|
||||
available_name: 'Alex',
|
||||
avatar_url:
|
||||
'https://staging.chatwoot.com/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBbktJIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--25806e8b52810484d3d6cb53af9e2a1c0cf1b43d/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--988d66f5e450207265d5c21bb0edb3facb890a43/slick-deploy.png',
|
||||
type: 'user',
|
||||
availability_status: 'online',
|
||||
thumbnail:
|
||||
'https://staging.chatwoot.com/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBbktJIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--25806e8b52810484d3d6cb53af9e2a1c0cf1b43d/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--988d66f5e450207265d5c21bb0edb3facb890a43/slick-deploy.png',
|
||||
},
|
||||
previous_changes: {
|
||||
updated_at: ['2024-12-04T11:47:46.879Z', '2024-12-04T11:47:53.564Z'],
|
||||
source_id: [
|
||||
null,
|
||||
'conversation/758d1f24-dc76-4abc-9c41-255ed8974f8e/messages/60916@reply.chatwoot.dev',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 60917,
|
||||
content:
|
||||
"Great we were looking for something in a different finish see image attached\n\n[image: image.png]\n\nLet me know if you have different finish options?\n\nBest Regards\n\nOn Wed, 4 Dec 2024 at 17:17, Sam from CottonMart <sam@cottonmart.test> wrote:\n\n> Dear Alex,\n>\n> Please find attached images of our cotton fabric samples. Let us know if\n> you'd like physical samples sent to you.\n>\n> Warm regards,\n> Sam\n> attachment [click here to view\n> <https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGVKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--62ee3b99421bfe7d8db85959ae99ab03a899f351/image.png>]\n>",
|
||||
account_id: 51,
|
||||
inbox_id: 992,
|
||||
conversation_id: 134,
|
||||
message_type: 0,
|
||||
created_at: 1733312969,
|
||||
updated_at: '2024-12-04T11:49:29.337Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: 'CAM_Qp+8LuzLTWZXkecjzJAgmb9RAQGm+qTmg@mail.gmail.com',
|
||||
content_type: 'incoming_email',
|
||||
content_attributes: {
|
||||
email: {
|
||||
bcc: null,
|
||||
cc: null,
|
||||
content_type:
|
||||
'multipart/related; boundary=0000000000007701030628705e31',
|
||||
date: '2024-12-04T17:18:54+05:30',
|
||||
from: ['alex@paperlayer.test'],
|
||||
html_content: {
|
||||
full: '<div dir="ltr">Great we were looking for something in a different finish see image attached<br><br><img src="https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGlKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--408309fa40f1cfea87ee3320a062a5d16ce09d4e/image.png" alt="image.png" width="472" height="305"><br><div><br></div><div>Let me know if you have different finish options?<br><br>Best Regards</div></div><br><div class="gmail_quote gmail_quote_container"><div dir="ltr" class="gmail_attr">On Wed, 4 Dec 2024 at 17:17, Sam from CottonMart <<a href="mailto:sam@cottonmart.test">sam@cottonmart.test</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"> <p>Dear Alex,</p>\n<p>Please find attached images of our cotton fabric samples. Let us know if you'd like physical samples sent to you.</p>\n<p>Warm regards,<br>\nSam</p>\n\n attachment [<a href="https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGVKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--62ee3b99421bfe7d8db85959ae99ab03a899f351/image.png" target="_blank">click here to view</a>]\n</blockquote></div>\n',
|
||||
reply:
|
||||
'Great we were looking for something in a different finish see image attached\n\n[image.png]\n\nLet me know if you have different finish options?\n\nBest Regards\n\nOn Wed, 4 Dec 2024 at 17:17, Sam from CottonMart <sam@cottonmart.test> wrote:\n>',
|
||||
quoted:
|
||||
'Great we were looking for something in a different finish see image attached\n\n[image.png]\n\nLet me know if you have different finish options?\n\nBest Regards',
|
||||
},
|
||||
in_reply_to:
|
||||
'conversation/758d1f24-dc76-4abc-9c41-255ed8974f8e/messages/60916@reply.chatwoot.dev',
|
||||
message_id: 'CAM_Qp+8LuzLTWZXkecjzJAgmb9RAQGm+qTmg@mail.gmail.com',
|
||||
multipart: true,
|
||||
number_of_attachments: 1,
|
||||
subject: 'Re: Inquiry and Quotation for Cotton Fabric',
|
||||
text_content: {
|
||||
full: "Great we were looking for something in a different finish see image attached\n\n[image: image.png]\n\nLet me know if you have different finish options?\n\nBest Regards\n\nOn Wed, 4 Dec 2024 at 17:17, Sam from CottonMart <sam@cottonmart.test> wrote:\n\n> Dear Alex,\n>\n> Please find attached images of our cotton fabric samples. Let us know if\n> you'd like physical samples sent to you.\n>\n> Warm regards,\n> Sam\n> attachment [click here to view\n> <https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGVKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--62ee3b99421bfe7d8db85959ae99ab03a899f351/image.png>]\n>",
|
||||
reply:
|
||||
"Great we were looking for something in a different finish see image attached\n\n[image: image.png]\n\nLet me know if you have different finish options?\n\nBest Regards\n\nOn Wed, 4 Dec 2024 at 17:17, Sam from CottonMart <sam@cottonmart.test> wrote:\n\n> Dear Alex,\n>\n> Please find attached images of our cotton fabric samples. Let us know if\n> you'd like physical samples sent to you.\n>\n> Warm regards,\n> Sam\n> attachment [click here to view\n> <https://staging.chatwoot.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBdGVKIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--62ee3b99421bfe7d8db85959ae99ab03a899f351/image.png>]\n>",
|
||||
quoted:
|
||||
'Great we were looking for something in a different finish see image attached\n\n[image: image.png]\n\nLet me know if you have different finish options?\n\nBest Regards',
|
||||
},
|
||||
to: ['sam@cottonmart.test'],
|
||||
},
|
||||
cc_email: null,
|
||||
bcc_email: null,
|
||||
},
|
||||
sender_type: 'Contact',
|
||||
sender_id: 111256,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content:
|
||||
'Great we were looking for something in a different finish see image attached\n\n[image: image.png]\n\nLet me know if you have different finish options?\n\nBest Regards',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 110,
|
||||
unread_count: 1,
|
||||
last_activity_at: 1733312969,
|
||||
contact_inbox: {
|
||||
source_id: 'alex@paperlayer.test',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
additional_attributes: {
|
||||
source_id: 'email:CAM_Qp+8beyon41DA@mail.gmail.com',
|
||||
},
|
||||
custom_attributes: {},
|
||||
email: 'alex@paperlayer.test',
|
||||
id: 111256,
|
||||
identifier: null,
|
||||
name: 'Alex',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
},
|
||||
],
|
||||
{ deep: true }
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,85 @@
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
|
||||
export default camelcaseKeys(
|
||||
[
|
||||
{
|
||||
id: 60716,
|
||||
content:
|
||||
"Hi Team,\n\nI hope this email finds you well! I wanted to share some updates regarding\nour integration with *Chatwoot* and outline some key features we’ve\nexplored.\n------------------------------\nKey Updates\n\n 1.\n\n *Integration Status*:\n The initial integration with Chatwoot has been successful. We've tested:\n - API connectivity\n - Multi-channel messaging\n - Real-time chat updates\n 2.\n\n *Upcoming Tasks*:\n - Streamlining notification workflows\n - Enhancing webhook reliability\n - Testing team collaboration features\n\n*Note:*\nDon’t forget to check out the automation capabilities in Chatwoot for\nhandling repetitive queries. It can save a ton of time!\n\n------------------------------\nFeatures We Love\n\nHere’s what stood out so far:\n\n - *Unified Inbox*: All customer conversations in one place.\n - *Customizable Workflows*: Tailored to our team’s unique needs.\n - *Integrations*: Works seamlessly with CRM and Slack.\n\n------------------------------\nAction Items For Next Week:\n\n 1. Implement the webhook for *ticket prioritization*.\n 2. Test *CSAT surveys* post-chat sessions.\n 3. Review *analytics dashboard* insights.\n\n------------------------------\nData Snapshot\n\nHere’s a quick overview of our conversation stats this week:\nMetric Value Change (%)\nTotal Conversations 350 +25%\nAverage Response Time 3 minutes -15%\nCSAT Score 92% +10%\n------------------------------\nFeedback\n\n*Do let me know if you have additional feedback or ideas to improve our\nworkflows. Here’s an image of how our Chatwoot dashboard looks with recent\nchanges:*\n\n[image: Chatwoot Dashboard Screenshot]\n------------------------------\n\nLooking forward to hearing your thoughts!\n\nBest regards,\n~ Shivam Mishra",
|
||||
account_id: 51,
|
||||
inbox_id: 991,
|
||||
conversation_id: 46,
|
||||
message_type: 0,
|
||||
created_at: 1733141025,
|
||||
updated_at: '2024-12-02T12:03:45.663Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id:
|
||||
'CAM_Qp+8bpiT5xFL7HmVL4a9RD0TmdYw7Lu6ZV02yu=eyon41DA@mail.gmail.com',
|
||||
content_type: 'incoming_email',
|
||||
content_attributes: {
|
||||
email: {
|
||||
bcc: null,
|
||||
cc: null,
|
||||
content_type:
|
||||
'multipart/alternative; boundary=0000000000009d889e0628477235',
|
||||
date: '2024-12-02T16:29:39+05:30',
|
||||
from: ['hey@shivam.dev'],
|
||||
html_content: {
|
||||
full: '<div dir="ltr"><h3><span style="font-size:small;font-weight:normal">Hi Team,</span></h3>\r\n<p>I hope this email finds you well! I wanted to share some updates regarding our integration with <strong>Chatwoot</strong> and outline some key features we’ve explored.</p>\r\n<hr>\r\n<h3>Key Updates</h3>\r\n<ol>\r\n<li>\r\n<p><strong>Integration Status</strong>:<br>\r\nThe initial integration with Chatwoot has been successful. We've tested:</p>\r\n<ul>\r\n<li>API connectivity</li>\r\n<li>Multi-channel messaging</li>\r\n<li>Real-time chat updates</li>\r\n</ul>\r\n</li>\r\n<li>\r\n<p><strong>Upcoming Tasks</strong>:</p>\r\n<ul>\r\n<li>Streamlining notification workflows</li>\r\n<li>Enhancing webhook reliability</li>\r\n<li>Testing team collaboration features</li>\r\n</ul>\r\n</li>\r\n</ol>\r\n<blockquote>\r\n<p><strong>Note:</strong><br>\r\nDon’t forget to check out the automation capabilities in Chatwoot for handling repetitive queries. It can save a ton of time!</p>\r\n</blockquote>\r\n<hr>\r\n<h3>Features We Love</h3>\r\n<p>Here’s what stood out so far:</p>\r\n<ul>\r\n<li><strong>Unified Inbox</strong>: All customer conversations in one place.</li>\r\n<li><strong>Customizable Workflows</strong>: Tailored to our team’s unique needs.</li>\r\n<li><strong>Integrations</strong>: Works seamlessly with CRM and Slack.</li>\r\n</ul>\r\n<hr>\r\n<h3>Action Items</h3>\r\n<h4>For Next Week:</h4>\r\n<ol>\r\n<li>Implement the webhook for <strong>ticket prioritization</strong>.</li>\r\n<li>Test <strong>CSAT surveys</strong> post-chat sessions.</li>\r\n<li>Review <strong>analytics dashboard</strong> insights.</li>\r\n</ol>\r\n<hr>\r\n<h3>Data Snapshot</h3>\r\n<p>Here’s a quick overview of our conversation stats this week:</p>\r\n<table>\r\n<thead>\r\n<tr>\r\n<th>Metric</th>\r\n<th>Value</th>\r\n<th>Change (%)</th>\r\n</tr>\r\n</thead>\r\n<tbody>\r\n<tr>\r\n<td>Total Conversations</td>\r\n<td>350</td>\r\n<td>+25%</td>\r\n</tr>\r\n<tr>\r\n<td>Average Response Time</td>\r\n<td>3 minutes</td>\r\n<td>-15%</td>\r\n</tr>\r\n<tr>\r\n<td>CSAT Score</td>\r\n<td>92%</td>\r\n<td>+10%</td>\r\n</tr>\r\n</tbody>\r\n</table>\r\n<hr>\r\n<h3>Feedback</h3>\r\n<p><i>Do let me know if you have additional feedback or ideas to improve our workflows. Here’s an image of how our Chatwoot dashboard looks with recent changes:</i></p>\r\n<p><img src="https://via.placeholder.com/600x300" alt="Chatwoot Dashboard Screenshot" title="Chatwoot Dashboard"></p>\r\n<hr>\r\n<p>Looking forward to hearing your thoughts!</p>\r\n<p>Best regards,<br>~ Shivam Mishra<br></p></div>\r\n',
|
||||
reply:
|
||||
"Hi Team,\n\nI hope this email finds you well! I wanted to share some updates regarding our integration with Chatwoot and outline some key features we’ve explored.\n\n---------------------------------------------------------------\n\nKey Updates\n\n-\n\nIntegration Status:\nThe initial integration with Chatwoot has been successful. We've tested:\n\n- API connectivity\n- Multi-channel messaging\n- Real-time chat updates\n\n-\n\nUpcoming Tasks:\n\n- Streamlining notification workflows\n- Enhancing webhook reliability\n- Testing team collaboration features\n\n>\n---------------------------------------------------------------\n\nFeatures We Love\n\nHere’s what stood out so far:\n\n- Unified Inbox: All customer conversations in one place.\n- Customizable Workflows: Tailored to our team’s unique needs.\n- Integrations: Works seamlessly with CRM and Slack.\n\n---------------------------------------------------------------\n\nAction Items\n\nFor Next Week:\n\n- Implement the webhook for ticket prioritization.\n- Test CSAT surveys post-chat sessions.\n- Review analytics dashboard insights.\n\n---------------------------------------------------------------\n\nData Snapshot\n\nHere’s a quick overview of our conversation stats this week:\n\nMetric\tValue\tChange (%)\nTotal Conversations\t350\t+25%\nAverage Response Time\t3 minutes\t-15%\nCSAT Score\t92%\t+10%\n---------------------------------------------------------------\n\nFeedback\n\nDo let me know if you have additional feedback or ideas to improve our workflows. Here’s an image of how our Chatwoot dashboard looks with recent changes:\n\n[Chatwoot Dashboard]\n\n---------------------------------------------------------------\n\nLooking forward to hearing your thoughts!\n\nBest regards,\n~ Shivam Mishra",
|
||||
quoted:
|
||||
'Hi Team,\n\nI hope this email finds you well! I wanted to share some updates regarding our integration with Chatwoot and outline some key features we’ve explored.',
|
||||
},
|
||||
in_reply_to: null,
|
||||
message_id:
|
||||
'CAM_Qp+8bpiT5xFL7HmVL4a9RD0TmdYw7Lu6ZV02yu=eyon41DA@mail.gmail.com',
|
||||
multipart: true,
|
||||
number_of_attachments: 0,
|
||||
subject: 'Update on Chatwoot Integration and Features',
|
||||
text_content: {
|
||||
full: "Hi Team,\r\n\r\nI hope this email finds you well! I wanted to share some updates regarding\r\nour integration with *Chatwoot* and outline some key features we’ve\r\nexplored.\r\n------------------------------\r\nKey Updates\r\n\r\n 1.\r\n\r\n *Integration Status*:\r\n The initial integration with Chatwoot has been successful. We've tested:\r\n - API connectivity\r\n - Multi-channel messaging\r\n - Real-time chat updates\r\n 2.\r\n\r\n *Upcoming Tasks*:\r\n - Streamlining notification workflows\r\n - Enhancing webhook reliability\r\n - Testing team collaboration features\r\n\r\n*Note:*\r\nDon’t forget to check out the automation capabilities in Chatwoot for\r\nhandling repetitive queries. It can save a ton of time!\r\n\r\n------------------------------\r\nFeatures We Love\r\n\r\nHere’s what stood out so far:\r\n\r\n - *Unified Inbox*: All customer conversations in one place.\r\n - *Customizable Workflows*: Tailored to our team’s unique needs.\r\n - *Integrations*: Works seamlessly with CRM and Slack.\r\n\r\n------------------------------\r\nAction Items For Next Week:\r\n\r\n 1. Implement the webhook for *ticket prioritization*.\r\n 2. Test *CSAT surveys* post-chat sessions.\r\n 3. Review *analytics dashboard* insights.\r\n\r\n------------------------------\r\nData Snapshot\r\n\r\nHere’s a quick overview of our conversation stats this week:\r\nMetric Value Change (%)\r\nTotal Conversations 350 +25%\r\nAverage Response Time 3 minutes -15%\r\nCSAT Score 92% +10%\r\n------------------------------\r\nFeedback\r\n\r\n*Do let me know if you have additional feedback or ideas to improve our\r\nworkflows. Here’s an image of how our Chatwoot dashboard looks with recent\r\nchanges:*\r\n\r\n[image: Chatwoot Dashboard Screenshot]\r\n------------------------------\r\n\r\nLooking forward to hearing your thoughts!\r\n\r\nBest regards,\r\n~ Shivam Mishra\r\n",
|
||||
reply:
|
||||
"Hi Team,\n\nI hope this email finds you well! I wanted to share some updates regarding\nour integration with *Chatwoot* and outline some key features we’ve\nexplored.\n------------------------------\nKey Updates\n\n 1.\n\n *Integration Status*:\n The initial integration with Chatwoot has been successful. We've tested:\n - API connectivity\n - Multi-channel messaging\n - Real-time chat updates\n 2.\n\n *Upcoming Tasks*:\n - Streamlining notification workflows\n - Enhancing webhook reliability\n - Testing team collaboration features\n\n*Note:*\nDon’t forget to check out the automation capabilities in Chatwoot for\nhandling repetitive queries. It can save a ton of time!\n\n------------------------------\nFeatures We Love\n\nHere’s what stood out so far:\n\n - *Unified Inbox*: All customer conversations in one place.\n - *Customizable Workflows*: Tailored to our team’s unique needs.\n - *Integrations*: Works seamlessly with CRM and Slack.\n\n------------------------------\nAction Items For Next Week:\n\n 1. Implement the webhook for *ticket prioritization*.\n 2. Test *CSAT surveys* post-chat sessions.\n 3. Review *analytics dashboard* insights.\n\n------------------------------\nData Snapshot\n\nHere’s a quick overview of our conversation stats this week:\nMetric Value Change (%)\nTotal Conversations 350 +25%\nAverage Response Time 3 minutes -15%\nCSAT Score 92% +10%\n------------------------------\nFeedback\n\n*Do let me know if you have additional feedback or ideas to improve our\nworkflows. Here’s an image of how our Chatwoot dashboard looks with recent\nchanges:*\n\n[image: Chatwoot Dashboard Screenshot]\n------------------------------\n\nLooking forward to hearing your thoughts!\n\nBest regards,\n~ Shivam Mishra",
|
||||
quoted:
|
||||
'Hi Team,\n\nI hope this email finds you well! I wanted to share some updates regarding\nour integration with *Chatwoot* and outline some key features we’ve\nexplored.',
|
||||
},
|
||||
to: ['shivam@chatwoot.com'],
|
||||
},
|
||||
cc_email: null,
|
||||
bcc_email: null,
|
||||
},
|
||||
sender_type: 'Contact',
|
||||
sender_id: 111256,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content:
|
||||
'Hi Team,\n\nI hope this email finds you well! I wanted to share some updates regarding\nour integration with *Chatwoot* and outline some key features we’ve\nexplored.',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: null,
|
||||
unread_count: 1,
|
||||
last_activity_at: 1733141025,
|
||||
contact_inbox: {
|
||||
source_id: 'hey@shivam.dev',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
additional_attributes: {
|
||||
source_id:
|
||||
'email:CAM_Qp+8bpiT5xFL7HmVL4a9RD0TmdYw7Lu6ZV02yu=eyon41DA@mail.gmail.com',
|
||||
},
|
||||
custom_attributes: {},
|
||||
email: 'hey@shivam.dev',
|
||||
id: 111256,
|
||||
identifier: null,
|
||||
name: 'Shivam Mishra',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
},
|
||||
],
|
||||
{ deep: true }
|
||||
);
|
||||
@@ -0,0 +1,715 @@
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
|
||||
export default camelcaseKeys(
|
||||
[
|
||||
{
|
||||
id: 5272,
|
||||
content: 'Hey, how are ya, I had a few questions about Chatwoot?',
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
status: 'sent',
|
||||
content_attributes: {
|
||||
in_reply_to: null,
|
||||
},
|
||||
created_at: 1732195656,
|
||||
private: false,
|
||||
source_id: null,
|
||||
sender: {
|
||||
additional_attributes: {},
|
||||
custom_attributes: {},
|
||||
email: 'hey@example.com',
|
||||
id: 597,
|
||||
identifier: null,
|
||||
name: 'hey',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5273,
|
||||
content: 'Give the team a way to reach you.',
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 3,
|
||||
content_type: 'text',
|
||||
status: 'read',
|
||||
content_attributes: {},
|
||||
created_at: 1732195656,
|
||||
private: false,
|
||||
source_id: null,
|
||||
},
|
||||
{
|
||||
id: 5274,
|
||||
content: 'Get notified by email',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 3,
|
||||
created_at: 1732195656,
|
||||
updated_at: '2024-11-21T13:27:53.612Z',
|
||||
private: false,
|
||||
status: 'read',
|
||||
source_id: null,
|
||||
content_type: 'input_email',
|
||||
content_attributes: {
|
||||
submitted_email: 'hey@example.com',
|
||||
},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content: 'Get notified by email',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: null,
|
||||
unread_count: 1,
|
||||
last_activity_at: 1732195656,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5275,
|
||||
content:
|
||||
'Does the Startup plan include the two users from the Free plan, or do I have to buy those separately?',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 0,
|
||||
created_at: 1732195735,
|
||||
updated_at: '2024-11-21T13:28:55.508Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {
|
||||
in_reply_to: null,
|
||||
},
|
||||
sender_type: 'Contact',
|
||||
sender_id: 597,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content:
|
||||
'Does the Startup plan include the two users from the Free plan, or do I have to buy those separately?',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: null,
|
||||
unread_count: 1,
|
||||
last_activity_at: 1732195735,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
additional_attributes: {},
|
||||
custom_attributes: {},
|
||||
email: 'hey@example.com',
|
||||
id: 597,
|
||||
identifier: null,
|
||||
name: 'hey',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
},
|
||||
{
|
||||
conversation_id: 43,
|
||||
status: 'read',
|
||||
content_type: 'text',
|
||||
processed_message_content: 'John self-assigned this conversation',
|
||||
id: 5276,
|
||||
content: 'John self-assigned this conversation',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
message_type: 2,
|
||||
created_at: 1732195741,
|
||||
updated_at: '2024-11-21T13:30:26.788Z',
|
||||
private: false,
|
||||
source_id: null,
|
||||
content_attributes: {},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1732195826,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
previous_changes: {
|
||||
updated_at: ['2024-11-21T13:29:01.570Z', '2024-11-21T13:30:26.788Z'],
|
||||
status: ['sent', 'read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
conversation_id: 43,
|
||||
status: 'read',
|
||||
content_type: 'text',
|
||||
processed_message_content:
|
||||
'Hey thanks for your interest in upgrading, no, the seats are not included, you will have to purchase them alongside the rest. How many seats are you planning to upgrade to?',
|
||||
id: 5277,
|
||||
content:
|
||||
'Hey thanks for your interest in upgrading, no, the seats are not included, you will have to purchase them alongside the rest. How many seats are you planning to upgrade to?',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
message_type: 1,
|
||||
created_at: 1732195826,
|
||||
updated_at: '2024-11-21T13:30:26.837Z',
|
||||
private: false,
|
||||
source_id: null,
|
||||
content_attributes: {},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1732195826,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
available_name: 'John',
|
||||
avatar_url:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaDBLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4e625d80e7ef2dc41354392bc214832fbe640840/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--ebe60765d222d11ade39165eae49cc4b2de18d89/picologo.png',
|
||||
type: 'user',
|
||||
availability_status: null,
|
||||
thumbnail:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaDBLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4e625d80e7ef2dc41354392bc214832fbe640840/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--ebe60765d222d11ade39165eae49cc4b2de18d89/picologo.png',
|
||||
},
|
||||
previous_changes: {
|
||||
updated_at: ['2024-11-21T13:30:26.149Z', '2024-11-21T13:30:26.837Z'],
|
||||
status: ['sent', 'read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5278,
|
||||
content: "Oh, that's unfortunate",
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 0,
|
||||
created_at: 1732195820,
|
||||
updated_at: '2024-11-21T13:30:38.070Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {
|
||||
in_reply_to: null,
|
||||
},
|
||||
sender_type: 'Contact',
|
||||
sender_id: 597,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content: "Oh, that's unfortunate",
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 1,
|
||||
last_activity_at: 1732195820,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
additional_attributes: {},
|
||||
custom_attributes: {},
|
||||
email: 'hey@example.com',
|
||||
id: 597,
|
||||
identifier: null,
|
||||
name: 'hey',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5279,
|
||||
content:
|
||||
'I plan to upgrade to 4 agents for now, but will grow to 6 in the next three months. ',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 0,
|
||||
created_at: 1732195820,
|
||||
updated_at: '2024-11-21T13:31:05.284Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {
|
||||
in_reply_to: null,
|
||||
},
|
||||
sender_type: 'Contact',
|
||||
sender_id: 597,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content:
|
||||
'I plan to upgrade to 4 agents for now, but will grow to 6 in the next three months. ',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 1,
|
||||
last_activity_at: 1732195885,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
additional_attributes: {},
|
||||
custom_attributes: {},
|
||||
email: 'hey@example.com',
|
||||
id: 597,
|
||||
identifier: null,
|
||||
name: 'hey',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 5280,
|
||||
content: 'Is it possible to get a discount?',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 0,
|
||||
created_at: 1732195886,
|
||||
updated_at: '2024-11-21T13:31:12.545Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {
|
||||
in_reply_to: null,
|
||||
},
|
||||
sender_type: 'Contact',
|
||||
sender_id: 597,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content: 'Is it possible to get a discount?',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 1,
|
||||
last_activity_at: 1732195872,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
additional_attributes: {},
|
||||
custom_attributes: {},
|
||||
email: 'hey@example.com',
|
||||
id: 597,
|
||||
identifier: null,
|
||||
name: 'hey',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
},
|
||||
{
|
||||
conversation_id: 43,
|
||||
status: 'read',
|
||||
content_type: 'text',
|
||||
processed_message_content:
|
||||
'[@Bruce](mention://user/30/Bruce) should we offer them a discount',
|
||||
id: 5281,
|
||||
content:
|
||||
'[@Bruce](mention://user/30/Bruce) should we offer them a discount',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
message_type: 1,
|
||||
created_at: 1732195887,
|
||||
updated_at: '2024-11-21T13:32:59.863Z',
|
||||
private: true,
|
||||
source_id: null,
|
||||
content_attributes: {},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1732195972,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
available_name: 'John',
|
||||
avatar_url:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaDBLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4e625d80e7ef2dc41354392bc214832fbe640840/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--ebe60765d222d11ade39165eae49cc4b2de18d89/picologo.png',
|
||||
type: 'user',
|
||||
availability_status: null,
|
||||
thumbnail:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaDBLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4e625d80e7ef2dc41354392bc214832fbe640840/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--ebe60765d222d11ade39165eae49cc4b2de18d89/picologo.png',
|
||||
},
|
||||
previous_changes: {
|
||||
updated_at: ['2024-11-21T13:31:27.914Z', '2024-11-21T13:32:59.863Z'],
|
||||
status: ['sent', 'read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
conversation_id: 43,
|
||||
status: 'read',
|
||||
content_type: 'text',
|
||||
processed_message_content:
|
||||
'Sure, you can use the discount code KQS3242A at the checkout to get 30% off on your yearly subscription. This coupon only applies for a year, I hope this helps',
|
||||
id: 5282,
|
||||
content:
|
||||
'Sure, you can use the discount code KQS3242A at the checkout to get 30% off on your yearly subscription. This coupon only applies for a year, I hope this helps',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
message_type: 1,
|
||||
created_at: 1732195972,
|
||||
updated_at: '2024-11-21T13:32:59.902Z',
|
||||
private: false,
|
||||
source_id: null,
|
||||
content_attributes: {},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1732195972,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
available_name: 'John',
|
||||
avatar_url:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaDBLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4e625d80e7ef2dc41354392bc214832fbe640840/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--ebe60765d222d11ade39165eae49cc4b2de18d89/picologo.png',
|
||||
type: 'user',
|
||||
availability_status: null,
|
||||
thumbnail:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaDBLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4e625d80e7ef2dc41354392bc214832fbe640840/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--ebe60765d222d11ade39165eae49cc4b2de18d89/picologo.png',
|
||||
},
|
||||
previous_changes: {
|
||||
updated_at: ['2024-11-21T13:32:52.722Z', '2024-11-21T13:32:59.902Z'],
|
||||
status: ['sent', 'read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5283,
|
||||
content: 'Great, thanks',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 0,
|
||||
created_at: 1732195982,
|
||||
updated_at: '2024-11-21T13:33:02.142Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {
|
||||
in_reply_to: null,
|
||||
},
|
||||
sender_type: 'Contact',
|
||||
sender_id: 597,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content: 'Great, thanks',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 1,
|
||||
last_activity_at: 1732195982,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
additional_attributes: {},
|
||||
custom_attributes: {},
|
||||
email: 'hey@example.com',
|
||||
id: 597,
|
||||
identifier: null,
|
||||
name: 'hey',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 5284,
|
||||
content: 'Really appreciate it',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 0,
|
||||
created_at: 1732195984,
|
||||
updated_at: '2024-11-21T13:33:04.856Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {
|
||||
in_reply_to: null,
|
||||
},
|
||||
sender_type: 'Contact',
|
||||
sender_id: 597,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content: 'Really appreciate it',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 1,
|
||||
last_activity_at: 1732195984,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
additional_attributes: {},
|
||||
custom_attributes: {},
|
||||
email: 'hey@example.com',
|
||||
id: 597,
|
||||
identifier: null,
|
||||
name: 'hey',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
},
|
||||
{
|
||||
conversation_id: 43,
|
||||
status: 'progress',
|
||||
content_type: 'text',
|
||||
processed_message_content: ' Happy to help :)',
|
||||
id: 5285,
|
||||
content: ' Happy to help :)',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
message_type: 1,
|
||||
created_at: 1732195991,
|
||||
updated_at: '2024-11-21T13:33:12.229Z',
|
||||
private: false,
|
||||
source_id: null,
|
||||
content_attributes: {},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1732195991,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
available_name: 'John',
|
||||
avatar_url:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaDBLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4e625d80e7ef2dc41354392bc214832fbe640840/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--ebe60765d222d11ade39165eae49cc4b2de18d89/picologo.png',
|
||||
type: 'user',
|
||||
availability_status: null,
|
||||
thumbnail:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaDBLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4e625d80e7ef2dc41354392bc214832fbe640840/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--ebe60765d222d11ade39165eae49cc4b2de18d89/picologo.png',
|
||||
},
|
||||
previous_changes: {
|
||||
updated_at: ['2024-11-21T13:33:11.667Z', '2024-11-21T13:33:12.229Z'],
|
||||
status: ['sent', 'read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
conversation_id: 43,
|
||||
status: 'failed',
|
||||
content_type: 'text',
|
||||
processed_message_content:
|
||||
"Let us know if you have any questions, I'll close this conversation for now",
|
||||
id: 5286,
|
||||
content:
|
||||
"Let us know if you have any questions, I'll close this conversation for now",
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
message_type: 1,
|
||||
created_at: 1732196013,
|
||||
updated_at: '2024-11-21T13:33:33.879Z',
|
||||
private: false,
|
||||
source_id: null,
|
||||
content_attributes: {
|
||||
external_error:
|
||||
'Business account is restricted from messaging users in this country.',
|
||||
},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1732196013,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
available_name: 'John',
|
||||
avatar_url:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaDBLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4e625d80e7ef2dc41354392bc214832fbe640840/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--ebe60765d222d11ade39165eae49cc4b2de18d89/picologo.png',
|
||||
type: 'user',
|
||||
availability_status: null,
|
||||
thumbnail:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaDBLIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4e625d80e7ef2dc41354392bc214832fbe640840/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--ebe60765d222d11ade39165eae49cc4b2de18d89/picologo.png',
|
||||
},
|
||||
previous_changes: {
|
||||
updated_at: ['2024-11-21T13:33:33.511Z', '2024-11-21T13:33:33.879Z'],
|
||||
status: ['sent', 'read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5287,
|
||||
content: 'John set the priority to urgent',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 2,
|
||||
created_at: 1732196017,
|
||||
updated_at: '2024-11-21T13:33:37.569Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content: 'John set the priority to urgent',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1732196017,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5288,
|
||||
content: 'John added billing',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 2,
|
||||
created_at: 1732196020,
|
||||
updated_at: '2024-11-21T13:33:40.207Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content: 'John added billing',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1732196020,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5289,
|
||||
content: 'John added delivery',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 2,
|
||||
created_at: 1732196020,
|
||||
updated_at: '2024-11-21T13:33:40.822Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content: 'John added delivery',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1732196020,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5290,
|
||||
content: 'Conversation was marked resolved by John',
|
||||
account_id: 1,
|
||||
inbox_id: 475,
|
||||
conversation_id: 43,
|
||||
message_type: 2,
|
||||
created_at: 1732196029,
|
||||
updated_at: '2024-11-21T13:33:49.059Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content: 'Conversation was marked resolved by John',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1732196029,
|
||||
contact_inbox: {
|
||||
source_id: 'b018c554-8e17-4102-8a0b-f6d20d021017',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{ deep: true }
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
33
app/javascript/dashboard/components-next/message/provider.js
Normal file
33
app/javascript/dashboard/components-next/message/provider.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { inject, provide, computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { ATTACHMENT_TYPES } from './constants';
|
||||
|
||||
const MessageControl = Symbol('MessageControl');
|
||||
|
||||
export function useMessageContext() {
|
||||
const context = inject(MessageControl, null);
|
||||
if (context === null) {
|
||||
throw new Error(`Component is missing a parent <Message /> component.`);
|
||||
}
|
||||
|
||||
const currentChatAttachments = useMapGetter('getSelectedChatAttachments');
|
||||
const filteredCurrentChatAttachments = computed(() => {
|
||||
const attachments = currentChatAttachments.value.filter(attachment =>
|
||||
[
|
||||
ATTACHMENT_TYPES.IMAGE,
|
||||
ATTACHMENT_TYPES.VIDEO,
|
||||
ATTACHMENT_TYPES.IG_REEL,
|
||||
ATTACHMENT_TYPES.AUDIO,
|
||||
].includes(attachment.file_type)
|
||||
);
|
||||
|
||||
return useSnakeCase(attachments);
|
||||
});
|
||||
|
||||
return { ...context, filteredCurrentChatAttachments };
|
||||
}
|
||||
|
||||
export function provideMessageContext(context) {
|
||||
provide(MessageControl, context);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import Message from '../Message.vue';
|
||||
|
||||
import simpleEmail from '../fixtures/simpleEmail.js';
|
||||
import fullConversation from '../fixtures/emailConversation.js';
|
||||
import newsletterEmail from '../fixtures/newsletterEmail.js';
|
||||
|
||||
const failedEmail = {
|
||||
...simpleEmail[0],
|
||||
status: 'failed',
|
||||
senderId: 1,
|
||||
senderType: 'User',
|
||||
contentAttributes: {
|
||||
...simpleEmail[0].contentAttributes,
|
||||
externalError: 'Failed to send email',
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Messages/Email"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Simple Email">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<template v-for="message in fullConversation" :key="message.id">
|
||||
<Message :current-user-id="1" is-email-inbox v-bind="message" />
|
||||
</template>
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Newsletter">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<template v-for="message in newsletterEmail" :key="message.id">
|
||||
<Message :current-user-id="1" is-email-inbox v-bind="message" />
|
||||
</template>
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Failed Email">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" is-email-inbox v-bind="failedEmail" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import Message from '../Message.vue';
|
||||
|
||||
const currentUserId = ref(1);
|
||||
|
||||
const state = reactive({
|
||||
useCurrentUserId: false,
|
||||
});
|
||||
|
||||
const getMessage = overrides => {
|
||||
const contentAttributes = {
|
||||
inReplyTo: null,
|
||||
...(overrides.contentAttributes ?? {}),
|
||||
};
|
||||
|
||||
const sender = {
|
||||
additionalAttributes: {},
|
||||
customAttributes: {},
|
||||
email: 'hey@example.com',
|
||||
id: 597,
|
||||
identifier: null,
|
||||
name: 'John Doe',
|
||||
phoneNumber: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
...(overrides.sender ?? {}),
|
||||
};
|
||||
|
||||
return {
|
||||
id: 5272,
|
||||
content: 'Hey, how are ya, I had a few questions about Chatwoot?',
|
||||
inboxId: 475,
|
||||
conversationId: 43,
|
||||
messageType: 0,
|
||||
contentType: 'text',
|
||||
status: 'sent',
|
||||
createdAt: 1732195656,
|
||||
private: false,
|
||||
sourceId: null,
|
||||
...overrides,
|
||||
sender,
|
||||
contentAttributes,
|
||||
};
|
||||
};
|
||||
|
||||
const getAttachment = (type, url, overrides) => {
|
||||
return {
|
||||
id: 22,
|
||||
messageId: 5319,
|
||||
fileType: type,
|
||||
accountId: 2,
|
||||
extension: null,
|
||||
dataUrl: url,
|
||||
thumbUrl: '',
|
||||
fileSize: 345644,
|
||||
width: null,
|
||||
height: null,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const baseSenderData = computed(() => {
|
||||
return {
|
||||
messageType: state.useCurrentUserId ? 1 : 0,
|
||||
senderId: state.useCurrentUserId ? currentUserId.value : 597,
|
||||
sender: {
|
||||
id: state.useCurrentUserId ? currentUserId.value : 597,
|
||||
type: state.useCurrentUserId ? 'User' : 'Contact',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const instagramStory = computed(() =>
|
||||
getMessage({
|
||||
content: 'cwtestinglocal mentioned you in the story: ',
|
||||
contentAttributes: {
|
||||
imageType: 'story_mention',
|
||||
},
|
||||
attachments: [
|
||||
getAttachment(
|
||||
'image',
|
||||
'https://images.pexels.com/photos/2587370/pexels-photo-2587370.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2'
|
||||
),
|
||||
],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
|
||||
const unsupported = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
contentAttributes: {
|
||||
isUnsupported: true,
|
||||
},
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
|
||||
const igReel = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
attachments: [
|
||||
getAttachment(
|
||||
'ig_reel',
|
||||
'https://videos.pexels.com/video-files/2023708/2023708-hd_720_1280_30fps.mp4'
|
||||
),
|
||||
],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Message Bubbles/Instagram"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Instagram Reel">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="igReel" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Instagram Story">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="instagramStory" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Unsupported">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="unsupported" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import Message from '../Message.vue';
|
||||
import instagramConversation from '../fixtures/instagramConversation.js';
|
||||
|
||||
const messages = instagramConversation;
|
||||
|
||||
const shouldGroupWithNext = index => {
|
||||
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);
|
||||
};
|
||||
|
||||
const getReplyToMessage = message => {
|
||||
const idToCheck = message.contentAttributes.inReplyTo;
|
||||
if (!idToCheck) return null;
|
||||
|
||||
return messages.find(candidate => idToCheck === candidate.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Messages/Instagram" :layout="{ type: 'single' }">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<template v-for="(message, index) in messages" :key="message.id">
|
||||
<Message
|
||||
:current-user-id="1"
|
||||
:group-with-next="shouldGroupWithNext(index)"
|
||||
:in-reply-to="getReplyToMessage(message)"
|
||||
v-bind="message"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,260 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import Message from '../Message.vue';
|
||||
|
||||
const currentUserId = ref(1);
|
||||
|
||||
const state = reactive({
|
||||
useCurrentUserId: false,
|
||||
});
|
||||
|
||||
const getMessage = overrides => {
|
||||
const contentAttributes = {
|
||||
inReplyTo: null,
|
||||
...(overrides.contentAttributes ?? {}),
|
||||
};
|
||||
|
||||
const sender = {
|
||||
additionalAttributes: {},
|
||||
customAttributes: {},
|
||||
email: 'hey@example.com',
|
||||
id: 597,
|
||||
identifier: null,
|
||||
name: 'John Doe',
|
||||
phoneNumber: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
...(overrides.sender ?? {}),
|
||||
};
|
||||
|
||||
return {
|
||||
id: 5272,
|
||||
content: 'Hey, how are ya, I had a few questions about Chatwoot?',
|
||||
inboxId: 475,
|
||||
conversationId: 43,
|
||||
messageType: 0,
|
||||
contentType: 'text',
|
||||
status: 'sent',
|
||||
createdAt: 1732195656,
|
||||
private: false,
|
||||
sourceId: null,
|
||||
...overrides,
|
||||
sender,
|
||||
contentAttributes,
|
||||
};
|
||||
};
|
||||
|
||||
const getAttachment = (type, url, overrides) => {
|
||||
return {
|
||||
id: 22,
|
||||
messageId: 5319,
|
||||
fileType: type,
|
||||
accountId: 2,
|
||||
extension: null,
|
||||
dataUrl: url,
|
||||
thumbUrl: '',
|
||||
fileSize: 345644,
|
||||
width: null,
|
||||
height: null,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const baseSenderData = computed(() => {
|
||||
return {
|
||||
messageType: state.useCurrentUserId ? 1 : 0,
|
||||
senderId: state.useCurrentUserId ? currentUserId.value : 597,
|
||||
sender: {
|
||||
id: state.useCurrentUserId ? currentUserId.value : 597,
|
||||
type: state.useCurrentUserId ? 'User' : 'Contact',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const audioMessage = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
attachments: [
|
||||
getAttachment(
|
||||
'audio',
|
||||
'https://cdn.freesound.org/previews/769/769025_16085454-lq.mp3'
|
||||
),
|
||||
],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
|
||||
const brokenImageMessage = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
attachments: [getAttachment('image', 'https://chatwoot.dev/broken.png')],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
|
||||
const imageMessage = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
attachments: [
|
||||
getAttachment(
|
||||
'image',
|
||||
'https://images.pexels.com/photos/28506417/pexels-photo-28506417/free-photo-of-motorbike-on-scenic-road-in-surat-thani-thailand.jpeg'
|
||||
),
|
||||
],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
|
||||
const videoMessage = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
attachments: [
|
||||
getAttachment(
|
||||
'video',
|
||||
'https://videos.pexels.com/video-files/1739010/1739010-hd_1920_1080_30fps.mp4'
|
||||
),
|
||||
],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
|
||||
const attachmentsOnly = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
attachments: [
|
||||
getAttachment('image', 'https://chatwoot.dev/broken.png'),
|
||||
getAttachment(
|
||||
'video',
|
||||
'https://videos.pexels.com/video-files/1739010/1739010-hd_1920_1080_30fps.mp4'
|
||||
),
|
||||
getAttachment(
|
||||
'image',
|
||||
'https://images.pexels.com/photos/28506417/pexels-photo-28506417/free-photo-of-motorbike-on-scenic-road-in-surat-thani-thailand.jpeg'
|
||||
),
|
||||
getAttachment('file', 'https://chatwoot.dev/invoice.pdf'),
|
||||
getAttachment('file', 'https://chatwoot.dev/logs.txt'),
|
||||
getAttachment('file', 'https://chatwoot.dev/contacts.xls'),
|
||||
getAttachment('file', 'https://chatwoot.dev/customers.csv'),
|
||||
getAttachment('file', 'https://chatwoot.dev/warehousing-policy.docx'),
|
||||
getAttachment('file', 'https://chatwoot.dev/pitch-deck.ppt'),
|
||||
getAttachment('file', 'https://chatwoot.dev/all-files.tar'),
|
||||
getAttachment(
|
||||
'audio',
|
||||
'https://cdn.freesound.org/previews/769/769025_16085454-lq.mp3'
|
||||
),
|
||||
],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
|
||||
const singleFile = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
attachments: [getAttachment('file', 'https://chatwoot.dev/all-files.tar')],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
|
||||
const contact = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
attachments: [
|
||||
getAttachment('contact', null, {
|
||||
fallbackTitle: '+919999999999',
|
||||
}),
|
||||
],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
|
||||
const location = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
attachments: [
|
||||
getAttachment('location', null, {
|
||||
coordinatesLat: 37.7937545,
|
||||
coordinatesLong: -122.3997472,
|
||||
fallbackTitle: 'Chatwoot Inc',
|
||||
}),
|
||||
],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
|
||||
const dyte = computed(() => {
|
||||
return getMessage({
|
||||
messageType: 1,
|
||||
contentType: 'integrations',
|
||||
contentAttributes: {
|
||||
type: 'dyte',
|
||||
data: {
|
||||
meetingId: 'f16bebe6-08b9-4593-899a-849f59c47397',
|
||||
roomName: 'zcufnc-adbjcg',
|
||||
},
|
||||
},
|
||||
senderId: 1,
|
||||
sender: {
|
||||
id: 1,
|
||||
name: 'Shivam Mishra',
|
||||
availableName: 'Shivam Mishra',
|
||||
type: 'user',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Message Bubbles/Media"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<!-- Media Types -->
|
||||
<Variant title="Audio">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="audioMessage" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Image">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="imageMessage" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Broken Image">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="brokenImageMessage" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Video">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="videoMessage" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<!-- Files and Attachments -->
|
||||
<Variant title="Multiple Attachments">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="attachmentsOnly" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="File">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="singleFile" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Contact">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="contact" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Location">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="location" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Dyte Video">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="dyte" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,181 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import Message from '../Message.vue';
|
||||
|
||||
const currentUserId = ref(1);
|
||||
|
||||
const state = reactive({
|
||||
useCurrentUserId: false,
|
||||
});
|
||||
|
||||
const getMessage = overrides => {
|
||||
const contentAttributes = {
|
||||
inReplyTo: null,
|
||||
...(overrides.contentAttributes ?? {}),
|
||||
};
|
||||
|
||||
const sender = {
|
||||
additionalAttributes: {},
|
||||
customAttributes: {},
|
||||
email: 'hey@example.com',
|
||||
id: 597,
|
||||
identifier: null,
|
||||
name: 'John Doe',
|
||||
phoneNumber: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
...(overrides.sender ?? {}),
|
||||
};
|
||||
|
||||
return {
|
||||
id: 5272,
|
||||
content: 'Hey, how are ya, I had a few questions about Chatwoot?',
|
||||
inboxId: 475,
|
||||
conversationId: 43,
|
||||
messageType: 0,
|
||||
contentType: 'text',
|
||||
status: 'sent',
|
||||
createdAt: 1732195656,
|
||||
private: false,
|
||||
sourceId: null,
|
||||
...overrides,
|
||||
sender,
|
||||
contentAttributes,
|
||||
};
|
||||
};
|
||||
|
||||
const getAttachment = (type, url, overrides) => {
|
||||
return {
|
||||
id: 22,
|
||||
messageId: 5319,
|
||||
fileType: type,
|
||||
accountId: 2,
|
||||
extension: null,
|
||||
dataUrl: url,
|
||||
thumbUrl: '',
|
||||
fileSize: 345644,
|
||||
width: null,
|
||||
height: null,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const baseSenderData = computed(() => {
|
||||
return {
|
||||
messageType: state.useCurrentUserId ? 1 : 0,
|
||||
senderId: state.useCurrentUserId ? currentUserId.value : 597,
|
||||
sender: {
|
||||
id: state.useCurrentUserId ? currentUserId.value : 597,
|
||||
type: state.useCurrentUserId ? 'User' : 'Contact',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const simpleText = computed(() =>
|
||||
getMessage({
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
const privateText = computed(() =>
|
||||
getMessage({ private: true, ...baseSenderData.value })
|
||||
);
|
||||
|
||||
const activityMessage = computed(() =>
|
||||
getMessage({
|
||||
content: 'John self-assigned this conversation',
|
||||
messageType: 2,
|
||||
})
|
||||
);
|
||||
|
||||
const email = computed(() =>
|
||||
getMessage({
|
||||
content: null,
|
||||
contentType: 'incoming_email',
|
||||
contentAttributes: {
|
||||
email: {
|
||||
bcc: null,
|
||||
cc: null,
|
||||
contentType:
|
||||
'multipart/alternative; boundary=0000000000009d889e0628477235',
|
||||
date: '2024-12-02T16:29:39+05:30',
|
||||
from: ['hey@shivam.dev'],
|
||||
htmlContent: {
|
||||
full: '<div dir="ltr"><h3><span style="font-size:small;font-weight:normal">Hi Team,</span></h3>\r\n<p>I hope this email finds you well! I wanted to share some updates regarding our integration with <strong>Chatwoot</strong> and outline some key features we’ve explored.</p>\r\n<hr>\r\n<h3>Key Updates</h3>\r\n<ol>\r\n<li>\r\n<p><strong>Integration Status</strong>:<br>\r\nThe initial integration with Chatwoot has been successful. We've tested:</p>\r\n<ul>\r\n<li>API connectivity</li>\r\n<li>Multi-channel messaging</li>\r\n<li>Real-time chat updates</li>\r\n</ul>\r\n</li>\r\n<li>\r\n<p><strong>Upcoming Tasks</strong>:</p>\r\n<ul>\r\n<li>Streamlining notification workflows</li>\r\n<li>Enhancing webhook reliability</li>\r\n<li>Testing team collaboration features</li>\r\n</ul>\r\n</li>\r\n</ol>\r\n<blockquote>\r\n<p><strong>Note:</strong><br>\r\nDon’t forget to check out the automation capabilities in Chatwoot for handling repetitive queries. It can save a ton of time!</p>\r\n</blockquote>\r\n<hr>\r\n<h3>Features We Love</h3>\r\n<p>Here’s what stood out so far:</p>\r\n<ul>\r\n<li><strong>Unified Inbox</strong>: All customer conversations in one place.</li>\r\n<li><strong>Customizable Workflows</strong>: Tailored to our team’s unique needs.</li>\r\n<li><strong>Integrations</strong>: Works seamlessly with CRM and Slack.</li>\r\n</ul>\r\n<hr>\r\n<h3>Action Items</h3>\r\n<h4>For Next Week:</h4>\r\n<ol>\r\n<li>Implement the webhook for <strong>ticket prioritization</strong>.</li>\r\n<li>Test <strong>CSAT surveys</strong> post-chat sessions.</li>\r\n<li>Review <strong>analytics dashboard</strong> insights.</li>\r\n</ol>\r\n<hr>\r\n<h3>Data Snapshot</h3>\r\n<p>Here’s a quick overview of our conversation stats this week:</p>\r\n<table>\r\n<thead>\r\n<tr>\r\n<th>Metric</th>\r\n<th>Value</th>\r\n<th>Change (%)</th>\r\n</tr>\r\n</thead>\r\n<tbody>\r\n<tr>\r\n<td>Total Conversations</td>\r\n<td>350</td>\r\n<td>+25%</td>\r\n</tr>\r\n<tr>\r\n<td>Average Response Time</td>\r\n<td>3 minutes</td>\r\n<td>-15%</td>\r\n</tr>\r\n<tr>\r\n<td>CSAT Score</td>\r\n<td>92%</td>\r\n<td>+10%</td>\r\n</tr>\r\n</tbody>\r\n</table>\r\n<hr>\r\n<h3>Feedback</h3>\r\n<p><i>Do let me know if you have additional feedback or ideas to improve our workflows. Here’s an image of how our Chatwoot dashboard looks with recent changes:</i></p>\r\n<p><img src="https://via.placeholder.com/600x300" alt="Chatwoot Dashboard Screenshot" title="Chatwoot Dashboard"></p>\r\n<hr>\r\n<p>Looking forward to hearing your thoughts!</p>\r\n<p>Best regards,<br>~ Shivam Mishra<br></p></div>\r\n',
|
||||
reply:
|
||||
"Hi Team,\n\nI hope this email finds you well! I wanted to share some updates regarding our integration with Chatwoot and outline some key features we’ve explored.\n\n---------------------------------------------------------------\n\nKey Updates\n\n-\n\nIntegration Status:\nThe initial integration with Chatwoot has been successful. We've tested:\n\n- API connectivity\n- Multi-channel messaging\n- Real-time chat updates\n\n-\n\nUpcoming Tasks:\n\n- Streamlining notification workflows\n- Enhancing webhook reliability\n- Testing team collaboration features\n\n>\n---------------------------------------------------------------\n\nFeatures We Love\n\nHere’s what stood out so far:\n\n- Unified Inbox: All customer conversations in one place.\n- Customizable Workflows: Tailored to our team’s unique needs.\n- Integrations: Works seamlessly with CRM and Slack.\n\n---------------------------------------------------------------\n\nAction Items\n\nFor Next Week:\n\n- Implement the webhook for ticket prioritization.\n- Test CSAT surveys post-chat sessions.\n- Review analytics dashboard insights.\n\n---------------------------------------------------------------\n\nData Snapshot\n\nHere’s a quick overview of our conversation stats this week:\n\nMetric\tValue\tChange (%)\nTotal Conversations\t350\t+25%\nAverage Response Time\t3 minutes\t-15%\nCSAT Score\t92%\t+10%\n---------------------------------------------------------------\n\nFeedback\n\nDo let me know if you have additional feedback or ideas to improve our workflows. Here’s an image of how our Chatwoot dashboard looks with recent changes:\n\n[Chatwoot Dashboard]\n\n---------------------------------------------------------------\n\nLooking forward to hearing your thoughts!\n\nBest regards,\n~ Shivam Mishra",
|
||||
quoted:
|
||||
'Hi Team,\n\nI hope this email finds you well! I wanted to share some updates regarding our integration with Chatwoot and outline some key features we’ve explored.',
|
||||
},
|
||||
inReplyTo: null,
|
||||
messageId:
|
||||
'CAM_Qp+8bpiT5xFL7HmVL4a9RD0TmdYw7Lu6ZV02yu=eyon41DA@mail.gmail.com',
|
||||
multipart: true,
|
||||
numberOfAttachments: 0,
|
||||
subject: 'Update on Chatwoot Integration and Features',
|
||||
textContent: {
|
||||
full: "Hi Team,\r\n\r\nI hope this email finds you well! I wanted to share some updates regarding\r\nour integration with *Chatwoot* and outline some key features we’ve\r\nexplored.\r\n------------------------------\r\nKey Updates\r\n\r\n 1.\r\n\r\n *Integration Status*:\r\n The initial integration with Chatwoot has been successful. We've tested:\r\n - API connectivity\r\n - Multi-channel messaging\r\n - Real-time chat updates\r\n 2.\r\n\r\n *Upcoming Tasks*:\r\n - Streamlining notification workflows\r\n - Enhancing webhook reliability\r\n - Testing team collaboration features\r\n\r\n*Note:*\r\nDon’t forget to check out the automation capabilities in Chatwoot for\r\nhandling repetitive queries. It can save a ton of time!\r\n\r\n------------------------------\r\nFeatures We Love\r\n\r\nHere’s what stood out so far:\r\n\r\n - *Unified Inbox*: All customer conversations in one place.\r\n - *Customizable Workflows*: Tailored to our team’s unique needs.\r\n - *Integrations*: Works seamlessly with CRM and Slack.\r\n\r\n------------------------------\r\nAction Items For Next Week:\r\n\r\n 1. Implement the webhook for *ticket prioritization*.\r\n 2. Test *CSAT surveys* post-chat sessions.\r\n 3. Review *analytics dashboard* insights.\r\n\r\n------------------------------\r\nData Snapshot\r\n\r\nHere’s a quick overview of our conversation stats this week:\r\nMetric Value Change (%)\r\nTotal Conversations 350 +25%\r\nAverage Response Time 3 minutes -15%\r\nCSAT Score 92% +10%\r\n------------------------------\r\nFeedback\r\n\r\n*Do let me know if you have additional feedback or ideas to improve our\r\nworkflows. Here’s an image of how our Chatwoot dashboard looks with recent\r\nchanges:*\r\n\r\n[image: Chatwoot Dashboard Screenshot]\r\n------------------------------\r\n\r\nLooking forward to hearing your thoughts!\r\n\r\nBest regards,\r\n~ Shivam Mishra\r\n",
|
||||
reply:
|
||||
"Hi Team,\n\nI hope this email finds you well! I wanted to share some updates regarding\nour integration with *Chatwoot* and outline some key features we’ve\nexplored.\n------------------------------\nKey Updates\n\n 1.\n\n *Integration Status*:\n The initial integration with Chatwoot has been successful. We've tested:\n - API connectivity\n - Multi-channel messaging\n - Real-time chat updates\n 2.\n\n *Upcoming Tasks*:\n - Streamlining notification workflows\n - Enhancing webhook reliability\n - Testing team collaboration features\n\n*Note:*\nDon’t forget to check out the automation capabilities in Chatwoot for\nhandling repetitive queries. It can save a ton of time!\n\n------------------------------\nFeatures We Love\n\nHere’s what stood out so far:\n\n - *Unified Inbox*: All customer conversations in one place.\n - *Customizable Workflows*: Tailored to our team’s unique needs.\n - *Integrations*: Works seamlessly with CRM and Slack.\n\n------------------------------\nAction Items For Next Week:\n\n 1. Implement the webhook for *ticket prioritization*.\n 2. Test *CSAT surveys* post-chat sessions.\n 3. Review *analytics dashboard* insights.\n\n------------------------------\nData Snapshot\n\nHere’s a quick overview of our conversation stats this week:\nMetric Value Change (%)\nTotal Conversations 350 +25%\nAverage Response Time 3 minutes -15%\nCSAT Score 92% +10%\n------------------------------\nFeedback\n\n*Do let me know if you have additional feedback or ideas to improve our\nworkflows. Here’s an image of how our Chatwoot dashboard looks with recent\nchanges:*\n\n[image: Chatwoot Dashboard Screenshot]\n------------------------------\n\nLooking forward to hearing your thoughts!\n\nBest regards,\n~ Shivam Mishra",
|
||||
quoted:
|
||||
'Hi Team,\n\nI hope this email finds you well! I wanted to share some updates regarding\nour integration with *Chatwoot* and outline some key features we’ve\nexplored.',
|
||||
},
|
||||
to: ['shivam@chatwoot.com'],
|
||||
},
|
||||
ccEmail: null,
|
||||
bccEmail: null,
|
||||
},
|
||||
attachments: [
|
||||
getAttachment(
|
||||
'video',
|
||||
'https://videos.pexels.com/video-files/1739010/1739010-hd_1920_1080_30fps.mp4'
|
||||
),
|
||||
getAttachment(
|
||||
'image',
|
||||
'https://images.pexels.com/photos/28506417/pexels-photo-28506417/free-photo-of-motorbike-on-scenic-road-in-surat-thani-thailand.jpeg'
|
||||
),
|
||||
getAttachment('file', 'https://chatwoot.dev/invoice.pdf'),
|
||||
getAttachment('file', 'https://chatwoot.dev/logs.txt'),
|
||||
getAttachment('file', 'https://chatwoot.dev/contacts.xls'),
|
||||
getAttachment('file', 'https://chatwoot.dev/customers.csv'),
|
||||
getAttachment('file', 'https://chatwoot.dev/warehousing-policy.docx'),
|
||||
getAttachment('file', 'https://chatwoot.dev/pitch-deck.ppt'),
|
||||
getAttachment('file', 'https://chatwoot.dev/all-files.tar'),
|
||||
getAttachment(
|
||||
'audio',
|
||||
'https://cdn.freesound.org/previews/769/769025_16085454-lq.mp3'
|
||||
),
|
||||
],
|
||||
...baseSenderData.value,
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Message Bubbles/Bubbles"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Text">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="simpleText" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Activity">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="activityMessage" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Private Message">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" v-bind="privateText" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<!-- Platform Specific -->
|
||||
<Variant title="Email">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<Message :current-user-id="1" is-email-inbox v-bind="email" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import Message from '../Message.vue';
|
||||
|
||||
import textWithMedia from '../fixtures/textWithMedia.js';
|
||||
|
||||
const messages = textWithMedia;
|
||||
|
||||
const shouldGroupWithNext = index => {
|
||||
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);
|
||||
};
|
||||
|
||||
const getReplyToMessage = message => {
|
||||
const idToCheck = message.contentAttributes.inReplyTo;
|
||||
if (!idToCheck) return null;
|
||||
|
||||
return messages.find(candidate => idToCheck === candidate.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Messages/Text" :layout="{ type: 'single' }">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-5xl grid">
|
||||
<template v-for="(message, index) in messages" :key="message.id">
|
||||
<Message
|
||||
:current-user-id="1"
|
||||
:group-with-next="shouldGroupWithNext(index)"
|
||||
:in-reply-to="getReplyToMessage(message)"
|
||||
v-bind="message"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -7,19 +7,23 @@ import snakecaseKeys from 'snakecase-keys';
|
||||
/**
|
||||
* Vue composable that converts object keys to camelCase
|
||||
* @param {Object|Array|import('vue').Ref<Object|Array>} payload - Object or array to convert
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.deep=false] - Should convert keys of nested objects
|
||||
* @returns {Object|Array} Converted payload with camelCase keys
|
||||
*/
|
||||
export function useCamelCase(payload) {
|
||||
export function useCamelCase(payload, options) {
|
||||
const unrefPayload = unref(payload);
|
||||
return camelcaseKeys(unrefPayload);
|
||||
return camelcaseKeys(unrefPayload, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue composable that converts object keys to snake_case
|
||||
* @param {Object|Array|import('vue').Ref<Object|Array>} payload - Object or array to convert
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.deep=false] - Should convert keys of nested objects
|
||||
* @returns {Object|Array} Converted payload with snake_case keys
|
||||
*/
|
||||
export function useSnakeCase(payload) {
|
||||
export function useSnakeCase(payload, options) {
|
||||
const unrefPayload = unref(payload);
|
||||
return snakecaseKeys(unrefPayload);
|
||||
return snakecaseKeys(unrefPayload, options);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"LIST": {
|
||||
"404": "There are no active conversations in this group."
|
||||
},
|
||||
"FAILED_TO_SEND": "Failed to send",
|
||||
"TAB_HEADING": "Conversations",
|
||||
"MENTION_HEADING": "Mentions",
|
||||
"UNATTENDED_HEADING": "Unattended",
|
||||
@@ -93,6 +94,9 @@
|
||||
"location": {
|
||||
"CONTENT": "Location"
|
||||
},
|
||||
"ig_reel": {
|
||||
"CONTENT": "Instagram Reel"
|
||||
},
|
||||
"fallback": {
|
||||
"CONTENT": "has shared a url"
|
||||
}
|
||||
@@ -126,6 +130,7 @@
|
||||
"NO_CONTENT": "No content available",
|
||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||
"SHOW_QUOTED_TEXT": "Show Quoted Text",
|
||||
"MESSAGE_READ": "Read"
|
||||
"MESSAGE_READ": "Read",
|
||||
"SENDING": "Sending"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,16 @@
|
||||
"REMOVE_SELECTION": "Remove Selection",
|
||||
"DOWNLOAD": "Download",
|
||||
"UNKNOWN_FILE_TYPE": "Unknown File",
|
||||
"SAVE_CONTACT": "Save",
|
||||
"SAVE_CONTACT": "Save Contact",
|
||||
"SHARED_ATTACHMENT": {
|
||||
"CONTACT": "{sender} has shared a contact",
|
||||
"LOCATION": "{sender} has shared a location",
|
||||
"FILE": "{sender} has shared a file",
|
||||
"MEETING": "{sender} has started a meeting"
|
||||
},
|
||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||
"REPLIED_TO_STORY": "Replied to your story",
|
||||
"UNSUPPORTED_MESSAGE": "This message is unsupported.",
|
||||
"UNSUPPORTED_MESSAGE": "This message is unsupported. You can view this message on the Facebook / Instagram app.",
|
||||
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
|
||||
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
|
||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
||||
@@ -314,7 +320,8 @@
|
||||
"TO": "To",
|
||||
"BCC": "Bcc",
|
||||
"CC": "Cc",
|
||||
"SUBJECT": "Subject"
|
||||
"SUBJECT": "Subject",
|
||||
"EXPAND": "Expand email"
|
||||
},
|
||||
"CONVERSATION_PARTICIPANTS": {
|
||||
"SIDEBAR_MENU_TITLE": "Participating",
|
||||
|
||||
@@ -236,6 +236,10 @@
|
||||
},
|
||||
"FORM_BUBBLE": {
|
||||
"SUBMIT": "Submit"
|
||||
},
|
||||
"MEDIA": {
|
||||
"IMAGE_UNAVAILABLE": "This image is no longer available.",
|
||||
"LOADING_FAILED": "Loading failed"
|
||||
}
|
||||
},
|
||||
"CONFIRM_EMAIL": "Verifying...",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"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",
|
||||
@@ -109,6 +110,7 @@
|
||||
"@iconify-json/lucide": "^1.2.11",
|
||||
"@iconify-json/ph": "^1.2.1",
|
||||
"@iconify-json/ri": "^1.2.3",
|
||||
"@iconify-json/teenyicons": "^1.2.1",
|
||||
"@size-limit/file": "^8.2.4",
|
||||
"@vitest/coverage-v8": "2.0.1",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
|
||||
269
pnpm-lock.yaml
generated
269
pnpm-lock.yaml
generated
@@ -139,6 +139,9 @@ 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
|
||||
@@ -245,6 +248,9 @@ importers:
|
||||
'@iconify-json/ri':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
'@iconify-json/teenyicons':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
'@size-limit/file':
|
||||
specifier: ^8.2.4
|
||||
version: 8.2.6(size-limit@8.2.6)
|
||||
@@ -893,6 +899,9 @@ packages:
|
||||
'@iconify-json/ri@1.2.3':
|
||||
resolution: {integrity: sha512-UVKofd5xkSevGd5K01pvO4NWsu+2C9spu+GxnMZUYymUiaWmpCAxtd22MFSpm6MGf0MP4GCwhDCo1Q8L8oZ9wg==}
|
||||
|
||||
'@iconify-json/teenyicons@1.2.1':
|
||||
resolution: {integrity: sha512-PaVv+zrQEO6I/9YfEwxkJfYSrCIWyOoSv/ZOVgETsr0MOqN9k7ecnHF/lPrgpyCLkwLzPX7MyFm3/gmziwjSiw==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
@@ -985,6 +994,34 @@ 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
|
||||
@@ -1734,12 +1771,24 @@ 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==}
|
||||
|
||||
@@ -1749,6 +1798,12 @@ 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==}
|
||||
|
||||
@@ -2554,6 +2609,9 @@ 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==}
|
||||
|
||||
@@ -2924,6 +2982,9 @@ 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.*}
|
||||
@@ -2955,6 +3016,9 @@ 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'}
|
||||
@@ -2975,6 +3039,10 @@ 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==}
|
||||
|
||||
@@ -3114,6 +3182,9 @@ 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'}
|
||||
@@ -3146,6 +3217,10 @@ 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'}
|
||||
@@ -3297,6 +3372,10 @@ 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'}
|
||||
@@ -3374,6 +3453,9 @@ 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
|
||||
@@ -3381,6 +3463,9 @@ 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==}
|
||||
|
||||
@@ -3534,6 +3619,10 @@ 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:
|
||||
@@ -3666,6 +3755,9 @@ 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'}
|
||||
@@ -3893,6 +3985,10 @@ 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==}
|
||||
|
||||
@@ -4157,6 +4253,9 @@ 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'}
|
||||
@@ -4229,6 +4328,9 @@ 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==}
|
||||
|
||||
@@ -4253,6 +4355,12 @@ 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:
|
||||
@@ -4301,6 +4409,9 @@ 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
|
||||
@@ -4345,6 +4456,9 @@ 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'}
|
||||
@@ -4582,6 +4696,9 @@ 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'}
|
||||
@@ -4660,6 +4777,9 @@ 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'}
|
||||
@@ -4917,6 +5037,9 @@ packages:
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
vt-pbf@3.1.3:
|
||||
resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==}
|
||||
|
||||
vue-chartjs@5.3.1:
|
||||
resolution: {integrity: sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==}
|
||||
peerDependencies:
|
||||
@@ -5106,6 +5229,11 @@ 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'}
|
||||
@@ -5786,6 +5914,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/teenyicons@1.2.1':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@iconify/utils@2.1.32':
|
||||
@@ -5908,6 +6040,35 @@ 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
|
||||
@@ -6749,10 +6910,24 @@ 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
|
||||
@@ -6764,6 +6939,12 @@ 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': {}
|
||||
@@ -7697,6 +7878,8 @@ snapshots:
|
||||
|
||||
dset@3.1.4: {}
|
||||
|
||||
earcut@3.0.0: {}
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
editorconfig@1.0.4:
|
||||
@@ -8224,6 +8407,8 @@ 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: {}
|
||||
@@ -8253,6 +8438,8 @@ 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
|
||||
@@ -8283,6 +8470,12 @@ 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
|
||||
@@ -8478,6 +8671,8 @@ snapshots:
|
||||
|
||||
idb@8.0.0: {}
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore@5.2.4: {}
|
||||
|
||||
immutable@4.3.7:
|
||||
@@ -8503,6 +8698,8 @@ snapshots:
|
||||
|
||||
ini@4.1.1: {}
|
||||
|
||||
ini@4.1.3: {}
|
||||
|
||||
internal-slot@1.0.5:
|
||||
dependencies:
|
||||
get-intrinsic: 1.2.4
|
||||
@@ -8635,6 +8832,8 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
isexe@3.1.1: {}
|
||||
|
||||
istanbul-lib-coverage@3.2.2: {}
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
@@ -8756,6 +8955,8 @@ 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
|
||||
@@ -8766,6 +8967,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
kdbush@4.0.2: {}
|
||||
|
||||
keycode@2.2.1: {}
|
||||
|
||||
keyv@4.5.3:
|
||||
@@ -8939,6 +9142,35 @@ 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
|
||||
@@ -9065,6 +9297,8 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
murmurhash-js@1.0.0: {}
|
||||
|
||||
mux.js@6.0.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.6
|
||||
@@ -9285,6 +9519,11 @@ 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: {}
|
||||
@@ -9575,6 +9814,8 @@ 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:
|
||||
@@ -9680,6 +9921,8 @@ snapshots:
|
||||
|
||||
proto-list@1.2.4: {}
|
||||
|
||||
protocol-buffers-schema@3.6.0: {}
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
psl@1.9.0: {}
|
||||
@@ -9694,6 +9937,10 @@ 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
|
||||
@@ -9749,6 +9996,10 @@ 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
|
||||
@@ -9809,6 +10060,8 @@ snapshots:
|
||||
dependencies:
|
||||
individual: 2.0.0
|
||||
|
||||
rw@1.3.3: {}
|
||||
|
||||
sade@1.8.1:
|
||||
dependencies:
|
||||
mri: 1.2.0
|
||||
@@ -10076,6 +10329,10 @@ 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
|
||||
@@ -10176,6 +10433,8 @@ snapshots:
|
||||
|
||||
tinypool@1.0.0: {}
|
||||
|
||||
tinyqueue@3.0.0: {}
|
||||
|
||||
tinyspy@3.0.2: {}
|
||||
|
||||
to-fast-properties@2.0.0: {}
|
||||
@@ -10460,6 +10719,12 @@ 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
|
||||
@@ -10647,6 +10912,10 @@ 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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { slateDark } = require('@radix-ui/colors');
|
||||
import { colors } from './theme/colors';
|
||||
import { icons } from './theme/icons';
|
||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||
const {
|
||||
iconsPlugin,
|
||||
@@ -28,6 +29,7 @@ const tailwindConfig = {
|
||||
'./app/javascript/portal/**/*.vue',
|
||||
'./app/javascript/shared/**/*.vue',
|
||||
'./app/javascript/survey/**/*.vue',
|
||||
'./app/javascript/dashboard/components-next/**/*.vue',
|
||||
'./app/javascript/dashboard/helper/**/*.js',
|
||||
'./app/javascript/dashboard/components-next/**/*.js',
|
||||
'./app/views/**/*.html.erb',
|
||||
@@ -39,6 +41,112 @@ const tailwindConfig = {
|
||||
inter: ['Inter', ...defaultSansFonts],
|
||||
interDisplay: ['Inter Display', ...defaultSansFonts],
|
||||
},
|
||||
typography: {
|
||||
email: {
|
||||
css: {
|
||||
color: 'rgb(var(--slate-11))',
|
||||
lineHeight: '1.6',
|
||||
fontSize: '14px',
|
||||
'*': {
|
||||
'&:first-child': {
|
||||
marginTop: '0',
|
||||
},
|
||||
},
|
||||
|
||||
strong: {
|
||||
color: 'rgb(var(--slate-12))',
|
||||
fontWeight: '700',
|
||||
},
|
||||
|
||||
b: {
|
||||
color: 'rgb(var(--slate-12))',
|
||||
fontWeight: '700',
|
||||
},
|
||||
|
||||
h1: {
|
||||
color: 'rgb(var(--slate-12))',
|
||||
fontWeight: '700',
|
||||
fontSize: '1.25rem',
|
||||
'&:first-child': {
|
||||
marginTop: '0',
|
||||
},
|
||||
},
|
||||
h2: {
|
||||
color: 'rgb(var(--slate-12))',
|
||||
fontWeight: '700',
|
||||
fontSize: '1rem',
|
||||
'&:first-child': {
|
||||
marginTop: '0',
|
||||
},
|
||||
},
|
||||
h3: {
|
||||
color: 'rgb(var(--slate-12))',
|
||||
fontWeight: '700',
|
||||
fontSize: '1rem',
|
||||
'&:first-child': {
|
||||
marginTop: '0',
|
||||
},
|
||||
},
|
||||
hr: {
|
||||
marginTop: '1.5em',
|
||||
marginBottom: '1.5em',
|
||||
},
|
||||
a: {
|
||||
color: 'rgb(var(--text-blue))',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
'ul li': {
|
||||
margin: '0 0 0.5em 1.5em',
|
||||
listStyleType: 'disc',
|
||||
},
|
||||
'ol li': {
|
||||
margin: '0 0 0.5em 1.5em',
|
||||
listStyleType: 'decimal',
|
||||
},
|
||||
blockquote: {
|
||||
fontStyle: 'italic',
|
||||
color: 'rgb(var(--slate-11))',
|
||||
borderLeft: `4px solid rgb(var(--border-strong))`,
|
||||
paddingLeft: '1em',
|
||||
},
|
||||
code: {
|
||||
backgroundColor: 'rgb(var(--alpha-3))',
|
||||
color: 'rgb(var(--solid-amber))',
|
||||
padding: '0.2em 0.4em',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.95em',
|
||||
},
|
||||
pre: {
|
||||
backgroundColor: 'rgb(var(--alpha-3))',
|
||||
padding: '1em',
|
||||
borderRadius: '6px',
|
||||
overflowX: 'auto',
|
||||
},
|
||||
table: {
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
},
|
||||
th: {
|
||||
padding: '0.75em',
|
||||
color: 'rgb(var(--slate-12))',
|
||||
borderBottom: `1px solid rgb(var(--border-strong))`,
|
||||
textAlign: 'left',
|
||||
fontWeight: '600',
|
||||
},
|
||||
tr: {
|
||||
borderBottom: `1px solid rgb(var(--border-strong))`,
|
||||
},
|
||||
td: {
|
||||
padding: '0.75em',
|
||||
borderBottom: `1px solid rgb(var(--border-strong))`,
|
||||
},
|
||||
img: {
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
screens: {
|
||||
xs: '480px',
|
||||
@@ -111,28 +219,8 @@ const tailwindConfig = {
|
||||
require('@tailwindcss/typography'),
|
||||
iconsPlugin({
|
||||
collections: {
|
||||
woot: {
|
||||
icons: {
|
||||
'logic-or': {
|
||||
body: `<rect x="14" y="5" width="2" height="13" rx="1" fill="currentColor"/><rect x="8" y="5" width="2" height="13" rx="1" fill="currentColor"/>`,
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
alert: {
|
||||
body: `<path d="M1.81348 0.9375L1.69727 7.95117H0.302734L0.179688 0.9375H1.81348ZM1 11.1025C0.494141 11.1025 0.0908203 10.7061 0.0976562 10.2207C0.0908203 9.72852 0.494141 9.33203 1 9.33203C1.49219 9.33203 1.89551 9.72852 1.90234 10.2207C1.89551 10.7061 1.49219 11.1025 1 11.1025Z" fill="currentColor" />`,
|
||||
width: 2,
|
||||
height: 12,
|
||||
},
|
||||
captain: {
|
||||
body: `<path d="M150.485 213.282C150.485 200.856 160.559 190.782 172.985 190.782C185.411 190.782 195.485 200.856 195.485 213.282V265.282C195.485 277.709 185.411 287.782 172.985 287.782C160.559 287.782 150.485 277.709 150.485 265.282V213.282Z" fill="currentColor"/>
|
||||
<path d="M222.485 213.282C222.485 200.856 232.559 190.782 244.985 190.782C257.411 190.782 267.485 200.856 267.485 213.282V265.282C267.485 277.709 257.411 287.782 244.985 287.782C232.559 287.782 222.485 277.709 222.485 265.282V213.282Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M412.222 109.961C317.808 96.6217 240.845 96.0953 144.309 109.902C119.908 113.392 103.762 115.751 91.4521 119.354C80.0374 122.694 73.5457 126.678 68.1762 132.687C57.0576 145.13 55.592 159.204 54.0765 208.287C52.587 256.526 55.5372 299.759 61.1249 348.403C64.1025 374.324 66.1515 391.817 69.4229 405.117C72.526 417.732 76.2792 424.515 81.4954 429.708C86.7533 434.942 93.4917 438.633 105.859 441.629C118.94 444.797 136.104 446.713 161.613 449.5C244.114 458.514 305.869 458.469 388.677 449.548C414.495 446.767 431.939 444.849 445.216 441.702C457.83 438.712 464.612 435.047 469.797 429.962C474.873 424.985 478.752 418.118 482.116 404.874C485.626 391.056 488.014 372.772 491.47 345.913C497.636 297.99 502.076 255.903 502.248 209.798C502.433 160.503 501.426 146.477 490.181 133.468C484.75 127.185 478.148 123.053 466.473 119.612C453.865 115.897 437.283 113.502 412.222 109.961ZM138.414 68.5711C238.977 54.1882 319.888 54.7514 418.047 68.6199L419.483 68.8227C442.724 72.1054 462.359 74.8786 478.244 79.5601C495.387 84.6124 509.724 92.2821 521.706 106.145C544.308 132.295 544.161 163.321 543.965 204.542C543.956 206.327 543.948 208.131 543.941 209.954C543.758 258.703 539.048 302.844 532.821 351.247L532.656 352.528C529.407 377.787 526.729 398.602 522.522 415.166C518.098 432.584 511.485 447.517 498.968 459.792C486.56 471.959 471.897 478.282 454.819 482.33C438.691 486.153 418.624 488.314 394.436 490.919L393.136 491.059C307.385 500.297 242.618 500.349 157.091 491.004L155.772 490.86C131.921 488.255 112.062 486.086 96.056 482.209C79.0408 478.087 64.4759 471.637 52.1005 459.316C39.6835 446.955 33.1618 432.265 28.94 415.102C24.9582 398.915 22.6435 378.759 19.8561 354.488L19.7052 353.174C13.9746 303.287 10.8315 257.908 12.4035 206.997C12.4606 205.15 12.5151 203.323 12.5691 201.516C13.7911 160.603 14.7077 129.914 37.1055 104.847C48.989 91.5477 63.035 84.1731 79.7563 79.2794C95.2643 74.7408 114.386 72.0068 137.018 68.7707C137.482 68.7044 137.948 68.6379 138.414 68.5711Z" fill="currentColor"/>`,
|
||||
width: 556,
|
||||
height: 556,
|
||||
},
|
||||
},
|
||||
},
|
||||
...getIconCollections(['lucide', 'logos', 'ri', 'ph']),
|
||||
woot: { icons },
|
||||
...getIconCollections(['lucide', 'logos', 'ri', 'ph', 'teenyicons']),
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -370,6 +370,7 @@ export const colors = {
|
||||
active: 'rgb(var(--solid-active) / <alpha-value>)',
|
||||
amber: 'rgb(var(--solid-amber) / <alpha-value>)',
|
||||
blue: 'rgb(var(--solid-blue) / <alpha-value>)',
|
||||
iris: 'rgb(var(--solid-iris) / <alpha-value>)',
|
||||
},
|
||||
alpha: {
|
||||
1: 'rgba(var(--alpha-1))',
|
||||
|
||||
116
theme/icons.js
Normal file
116
theme/icons.js
Normal file
@@ -0,0 +1,116 @@
|
||||
export const icons = {
|
||||
'logic-or': {
|
||||
body: `<rect x="14" y="5" width="2" height="13" rx="1" fill="currentColor"/><rect x="8" y="5" width="2" height="13" rx="1" fill="currentColor"/>`,
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
alert: {
|
||||
body: `<path d="M1.81348 0.9375L1.69727 7.95117H0.302734L0.179688 0.9375H1.81348ZM1 11.1025C0.494141 11.1025 0.0908203 10.7061 0.0976562 10.2207C0.0908203 9.72852 0.494141 9.33203 1 9.33203C1.49219 9.33203 1.89551 9.72852 1.90234 10.2207C1.89551 10.7061 1.49219 11.1025 1 11.1025Z" fill="currentColor" />`,
|
||||
width: 2,
|
||||
height: 12,
|
||||
},
|
||||
captain: {
|
||||
body: `<path d="M150.485 213.282C150.485 200.856 160.559 190.782 172.985 190.782C185.411 190.782 195.485 200.856 195.485 213.282V265.282C195.485 277.709 185.411 287.782 172.985 287.782C160.559 287.782 150.485 277.709 150.485 265.282V213.282Z" fill="currentColor"/>
|
||||
<path d="M222.485 213.282C222.485 200.856 232.559 190.782 244.985 190.782C257.411 190.782 267.485 200.856 267.485 213.282V265.282C267.485 277.709 257.411 287.782 244.985 287.782C232.559 287.782 222.485 277.709 222.485 265.282V213.282Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M412.222 109.961C317.808 96.6217 240.845 96.0953 144.309 109.902C119.908 113.392 103.762 115.751 91.4521 119.354C80.0374 122.694 73.5457 126.678 68.1762 132.687C57.0576 145.13 55.592 159.204 54.0765 208.287C52.587 256.526 55.5372 299.759 61.1249 348.403C64.1025 374.324 66.1515 391.817 69.4229 405.117C72.526 417.732 76.2792 424.515 81.4954 429.708C86.7533 434.942 93.4917 438.633 105.859 441.629C118.94 444.797 136.104 446.713 161.613 449.5C244.114 458.514 305.869 458.469 388.677 449.548C414.495 446.767 431.939 444.849 445.216 441.702C457.83 438.712 464.612 435.047 469.797 429.962C474.873 424.985 478.752 418.118 482.116 404.874C485.626 391.056 488.014 372.772 491.47 345.913C497.636 297.99 502.076 255.903 502.248 209.798C502.433 160.503 501.426 146.477 490.181 133.468C484.75 127.185 478.148 123.053 466.473 119.612C453.865 115.897 437.283 113.502 412.222 109.961ZM138.414 68.5711C238.977 54.1882 319.888 54.7514 418.047 68.6199L419.483 68.8227C442.724 72.1054 462.359 74.8786 478.244 79.5601C495.387 84.6124 509.724 92.2821 521.706 106.145C544.308 132.295 544.161 163.321 543.965 204.542C543.956 206.327 543.948 208.131 543.941 209.954C543.758 258.703 539.048 302.844 532.821 351.247L532.656 352.528C529.407 377.787 526.729 398.602 522.522 415.166C518.098 432.584 511.485 447.517 498.968 459.792C486.56 471.959 471.897 478.282 454.819 482.33C438.691 486.153 418.624 488.314 394.436 490.919L393.136 491.059C307.385 500.297 242.618 500.349 157.091 491.004L155.772 490.86C131.921 488.255 112.062 486.086 96.056 482.209C79.0408 478.087 64.4759 471.637 52.1005 459.316C39.6835 446.955 33.1618 432.265 28.94 415.102C24.9582 398.915 22.6435 378.759 19.8561 354.488L19.7052 353.174C13.9746 303.287 10.8315 257.908 12.4035 206.997C12.4606 205.15 12.5151 203.323 12.5691 201.516C13.7911 160.603 14.7077 129.914 37.1055 104.847C48.989 91.5477 63.035 84.1731 79.7563 79.2794C95.2643 74.7408 114.386 72.0068 137.018 68.7707C137.482 68.7044 137.948 68.6379 138.414 68.5711Z" fill="currentColor"/>`,
|
||||
width: 556,
|
||||
height: 556,
|
||||
},
|
||||
'file-csv': {
|
||||
body: `<g clip-path="url(#clip0_2931_148091)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6562 19.7578H2.94141C0.972656 19.7578 0 18.7734 0 16.7695V2.98828C0 0.996094 0.972656 0 2.94141 0H7.07812V6.80859C7.07812 8.05078 7.66406 8.63672 8.90625 8.63672H15.6094V16.7695C15.6094 18.7617 14.625 19.7578 12.6562 19.7578ZM3.46583 15.9741C3.66935 16.0317 3.86903 16.0605 4.06487 16.0605C4.23383 16.0605 4.39607 16.0374 4.55159 15.9914C4.70903 15.9434 4.84919 15.8771 4.97206 15.7926C5.09495 15.7082 5.19191 15.6102 5.26295 15.4989C5.33591 15.3875 5.37239 15.2675 5.37239 15.1389C5.37239 15.0198 5.33207 14.9238 5.25143 14.8509C5.17271 14.776 5.07863 14.7386 4.96919 14.7386C4.83671 14.7386 4.74359 14.7683 4.68983 14.8278C4.63799 14.8854 4.59575 14.9507 4.56311 15.0237C4.54007 15.0813 4.50935 15.1389 4.47095 15.1965C4.43447 15.2522 4.38359 15.2982 4.31831 15.3347C4.25495 15.3712 4.17046 15.3894 4.06487 15.3894C3.93431 15.3894 3.80951 15.352 3.69047 15.2771C3.57335 15.2022 3.47831 15.0803 3.40535 14.9114C3.33239 14.7405 3.29591 14.5139 3.29591 14.2317C3.29591 13.9475 3.33239 13.721 3.40535 13.552C3.47831 13.383 3.57335 13.2611 3.69047 13.1862C3.80951 13.1094 3.93431 13.071 4.06487 13.071C4.17046 13.071 4.25495 13.0893 4.31831 13.1258C4.38359 13.1622 4.43447 13.2093 4.47095 13.2669C4.50935 13.3226 4.54007 13.3792 4.56311 13.4368C4.59575 13.5098 4.63799 13.576 4.68983 13.6355C4.74359 13.6931 4.83671 13.7219 4.96919 13.7219C5.07863 13.7219 5.17271 13.6854 5.25143 13.6125C5.33207 13.5395 5.37239 13.4426 5.37239 13.3216C5.37239 13.193 5.33687 13.073 5.26583 12.9616C5.19479 12.8502 5.09687 12.7523 4.97206 12.6678C4.84919 12.5834 4.70999 12.5181 4.55447 12.472C4.39895 12.424 4.23575 12.4 4.06487 12.4C3.86903 12.4 3.66935 12.4288 3.46583 12.4864C3.26423 12.544 3.07799 12.6419 2.9071 12.7802C2.73623 12.9165 2.59895 13.1037 2.49527 13.3418C2.39159 13.5779 2.33975 13.8746 2.33975 14.2317C2.33975 14.5869 2.39159 14.8826 2.49527 15.1187C2.59895 15.3549 2.73623 15.5421 2.9071 15.6803C3.07799 15.8186 3.26423 15.9165 3.46583 15.9741ZM6.76232 15.9626C6.97736 16.0278 7.22408 16.0605 7.50248 16.0605C7.80584 16.0605 8.07368 16.0154 8.30599 15.9251C8.53832 15.8349 8.71976 15.7072 8.85032 15.5421C8.98088 15.375 9.04615 15.1792 9.04615 14.9546C9.04615 14.7376 8.99048 14.5581 8.87912 14.416C8.76776 14.2739 8.61416 14.1578 8.41831 14.0675C8.22439 13.9773 8.00072 13.9014 7.74728 13.84C7.49384 13.7766 7.30568 13.7094 7.1828 13.6384C7.06184 13.5674 7.00136 13.4829 7.00136 13.385C7.00136 13.3216 7.02248 13.2669 7.06472 13.2208C7.10696 13.1728 7.16648 13.1363 7.24328 13.1114C7.32008 13.0845 7.4084 13.071 7.50824 13.071C7.64456 13.071 7.75976 13.0922 7.85384 13.1344C7.94792 13.1766 8.02184 13.2294 8.07559 13.2928C8.12551 13.3408 8.18984 13.3926 8.26856 13.4483C8.34728 13.504 8.44136 13.5318 8.5508 13.5318C8.66216 13.5318 8.75144 13.5002 8.81863 13.4368C8.88776 13.3715 8.92232 13.2832 8.92232 13.1718C8.92232 13.0778 8.89832 12.9933 8.85032 12.9184C8.80232 12.8435 8.73896 12.7782 8.66024 12.7226C8.52776 12.617 8.36264 12.5373 8.16488 12.4835C7.96712 12.4278 7.76264 12.4 7.55143 12.4C7.26536 12.4 7.00808 12.4403 6.7796 12.521C6.55304 12.6016 6.37352 12.7168 6.24103 12.8666C6.10856 13.0163 6.04232 13.1939 6.04232 13.3994C6.04232 13.6778 6.14407 13.9168 6.3476 14.1165C6.55304 14.3162 6.88423 14.4678 7.3412 14.5715C7.59272 14.6291 7.77992 14.6877 7.9028 14.7472C8.02567 14.8048 8.08712 14.895 8.08712 15.0179C8.08712 15.0947 8.06215 15.161 8.01224 15.2166C7.96232 15.2723 7.89416 15.3155 7.80775 15.3462C7.72328 15.375 7.62728 15.3894 7.51976 15.3894C7.3604 15.3894 7.22888 15.3587 7.1252 15.2973C7.02152 15.2358 6.93032 15.1648 6.8516 15.0842C6.7844 15.0208 6.7124 14.9651 6.6356 14.9171C6.55879 14.8691 6.46472 14.8451 6.35335 14.8451C6.2228 14.8451 6.1268 14.8854 6.06536 14.9661C6.00392 15.0467 5.9732 15.1293 5.9732 15.2138C5.9732 15.4019 6.06344 15.5632 6.24392 15.6976C6.3764 15.807 6.5492 15.8954 6.76232 15.9626ZM10.6578 15.8877C10.7673 15.9664 10.9122 16.0058 11.0927 16.0058H11.2252C11.4076 16.0058 11.5535 15.9664 11.663 15.8877C11.7724 15.809 11.8511 15.6928 11.8991 15.5392L12.7055 12.9904C12.7132 12.9616 12.718 12.9338 12.7199 12.9069C12.7238 12.88 12.7257 12.8541 12.7257 12.8291C12.7257 12.6966 12.6825 12.593 12.5961 12.5181C12.5116 12.4432 12.4031 12.4058 12.2706 12.4058C12.1132 12.4058 12.0009 12.4461 11.9337 12.5267C11.8684 12.6054 11.8166 12.7245 11.7782 12.8838L11.159 15.2051L10.5455 12.8838C10.5071 12.7245 10.4543 12.6054 10.3871 12.5267C10.3218 12.4461 10.2095 12.4058 10.0502 12.4058C9.92153 12.4058 9.81305 12.4422 9.72472 12.5152C9.6364 12.5882 9.59225 12.6918 9.59225 12.8262C9.59225 12.8493 9.59416 12.8752 9.59801 12.904C9.60184 12.9328 9.6076 12.9616 9.61528 12.9904L10.4188 15.5392C10.4687 15.6928 10.5484 15.809 10.6578 15.8877Z" fill="#FFC53D"/>
|
||||
<path d="M15.5039 7.26562H8.90625C8.61328 7.26562 8.44922 7.11328 8.44922 6.80859V0.0820312C8.88281 0.152344 9.31641 0.457031 9.79688 0.949219L14.6602 5.91797C15.1523 6.42188 15.4453 6.83203 15.5039 7.26562Z" fill="#FFC53D"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2931_148091">
|
||||
<rect width="15.9609" height="19.7695" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>`,
|
||||
width: 16,
|
||||
height: 20,
|
||||
},
|
||||
'file-doc': {
|
||||
body: `<g clip-path="url(#clip0_2931_148089)">
|
||||
<path d="M4.2927 15.238C4.1847 15.28 4.06369 15.301 3.92969 15.301H3.63569V12.985H3.92969C4.06369 12.985 4.1847 13.007 4.2927 13.051C4.4007 13.093 4.4927 13.16 4.5687 13.252C4.6467 13.344 4.7067 13.464 4.7487 13.612C4.7907 13.758 4.8117 13.935 4.8117 14.143C4.8117 14.349 4.7907 14.526 4.7487 14.674C4.7067 14.822 4.6467 14.942 4.5687 15.034C4.4927 15.126 4.4007 15.194 4.2927 15.238Z" fill="#3E63DD"/>
|
||||
<path d="M8.36858 15.229C8.26458 15.319 8.14258 15.364 8.00258 15.364C7.86058 15.364 7.73658 15.319 7.63058 15.229C7.52457 15.137 7.44257 15.001 7.38457 14.821C7.32857 14.641 7.30057 14.419 7.30057 14.155C7.30057 13.893 7.32857 13.673 7.38457 13.495C7.44257 13.315 7.52457 13.179 7.63058 13.087C7.73658 12.995 7.86058 12.949 8.00258 12.949C8.14258 12.949 8.26458 12.995 8.36858 13.087C8.47258 13.179 8.55358 13.315 8.61158 13.495C8.66959 13.673 8.69859 13.893 8.69859 14.155C8.69859 14.419 8.66959 14.641 8.61158 14.821C8.55358 15.001 8.47258 15.137 8.36858 15.229Z" fill="#3E63DD"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6562 19.7578H2.94141C0.972656 19.7578 0 18.7734 0 16.7695V2.98828C0 0.996094 0.972656 0 2.94141 0H7.07812V6.80859C7.07812 8.05078 7.66406 8.63672 8.90625 8.63672H15.6094V16.7695C15.6094 18.7617 14.625 19.7578 12.6562 19.7578ZM4.3557 12.319C4.1937 12.297 4.0527 12.286 3.93269 12.286H3.20669C2.83868 12.286 2.65468 12.47 2.65468 12.838V15.448C2.65468 15.816 2.83868 16 3.20669 16H3.93269C4.0527 16 4.1937 15.989 4.3557 15.967C4.5177 15.945 4.6827 15.901 4.8507 15.835C5.0187 15.767 5.17471 15.666 5.31871 15.532C5.46471 15.396 5.58171 15.216 5.66971 14.992C5.75971 14.766 5.80471 14.483 5.80471 14.143C5.80471 13.803 5.75971 13.521 5.66971 13.297C5.58171 13.071 5.46471 12.891 5.31871 12.757C5.17471 12.621 5.0187 12.52 4.8507 12.454C4.6827 12.386 4.5177 12.341 4.3557 12.319ZM7.39057 15.964C7.59057 16.03 7.79458 16.063 8.00258 16.063C8.20658 16.063 8.40858 16.03 8.60858 15.964C8.81059 15.896 8.99259 15.788 9.15459 15.64C9.31859 15.492 9.44959 15.296 9.54759 15.052C9.6456 14.808 9.6946 14.509 9.6946 14.155C9.6946 13.801 9.6456 13.503 9.54759 13.261C9.44959 13.017 9.31859 12.821 9.15459 12.673C8.99259 12.525 8.81059 12.418 8.60858 12.352C8.40858 12.284 8.20658 12.25 8.00258 12.25C7.79458 12.25 7.59057 12.284 7.39057 12.352C7.19057 12.418 7.00757 12.525 6.84157 12.673C6.67757 12.821 6.54656 13.017 6.44856 13.261C6.35056 13.503 6.30156 13.801 6.30156 14.155C6.30156 14.509 6.35056 14.808 6.44856 15.052C6.54656 15.296 6.67757 15.492 6.84157 15.64C7.00757 15.788 7.19057 15.896 7.39057 15.964ZM11.4275 15.973C11.6395 16.033 11.8475 16.063 12.0515 16.063C12.2275 16.063 12.3965 16.039 12.5585 15.991C12.7225 15.941 12.8685 15.872 12.9965 15.784C13.1245 15.696 13.2255 15.594 13.2995 15.478C13.3755 15.362 13.4135 15.237 13.4135 15.103C13.4135 14.979 13.3715 14.879 13.2875 14.803C13.2055 14.725 13.1075 14.686 12.9935 14.686C12.8555 14.686 12.7585 14.717 12.7025 14.779C12.6485 14.839 12.6045 14.907 12.5705 14.983C12.5465 15.043 12.5145 15.103 12.4745 15.163C12.4365 15.221 12.3835 15.269 12.3155 15.307C12.2495 15.345 12.1615 15.364 12.0515 15.364C11.9155 15.364 11.7855 15.325 11.6615 15.247C11.5395 15.169 11.4405 15.042 11.3645 14.866C11.2885 14.688 11.2505 14.452 11.2505 14.158C11.2505 13.862 11.2885 13.626 11.3645 13.45C11.4405 13.274 11.5395 13.147 11.6615 13.069C11.7855 12.989 11.9155 12.949 12.0515 12.949C12.1615 12.949 12.2495 12.968 12.3155 13.006C12.3835 13.044 12.4365 13.093 12.4745 13.153C12.5145 13.211 12.5465 13.27 12.5705 13.33C12.6045 13.406 12.6485 13.475 12.7025 13.537C12.7585 13.597 12.8555 13.627 12.9935 13.627C13.1075 13.627 13.2055 13.589 13.2875 13.513C13.3715 13.437 13.4135 13.336 13.4135 13.21C13.4135 13.076 13.3765 12.951 13.3025 12.835C13.2285 12.719 13.1265 12.617 12.9965 12.529C12.8685 12.441 12.7235 12.373 12.5615 12.325C12.3995 12.275 12.2295 12.25 12.0515 12.25C11.8475 12.25 11.6395 12.28 11.4275 12.34C11.2175 12.4 11.0235 12.502 10.8455 12.646C10.6674 12.788 10.5244 12.983 10.4164 13.231C10.3084 13.477 10.2544 13.786 10.2544 14.158C10.2544 14.528 10.3084 14.836 10.4164 15.082C10.5244 15.328 10.6674 15.523 10.8455 15.667C11.0235 15.811 11.2175 15.913 11.4275 15.973Z" fill="#3E63DD"/>
|
||||
<path d="M15.5039 7.26562H8.90625C8.61328 7.26562 8.44922 7.11328 8.44922 6.80859V0.0820312C8.88281 0.152344 9.31641 0.457031 9.79688 0.949219L14.6602 5.91797C15.1523 6.42188 15.4453 6.83203 15.5039 7.26562Z" fill="#3E63DD"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2931_148089">
|
||||
<rect width="15.9609" height="19.7695" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>`,
|
||||
width: 16,
|
||||
height: 20,
|
||||
},
|
||||
'file-pdf': {
|
||||
body: `<g clip-path="url(#clip0_2931_148088)">
|
||||
<path d="M3.83969 14.011V12.985H4.1847C4.2547 12.985 4.3227 12.99 4.3887 13C4.4547 13.01 4.5137 13.032 4.5657 13.066C4.6197 13.1 4.6617 13.151 4.6917 13.219C4.7237 13.285 4.7397 13.374 4.7397 13.486C4.7397 13.6 4.7237 13.692 4.6917 13.762C4.6617 13.832 4.6197 13.885 4.5657 13.921C4.5137 13.955 4.4547 13.979 4.3887 13.993C4.3227 14.005 4.2547 14.011 4.1847 14.011H3.83969Z" fill="#E54666"/>
|
||||
<path d="M8.10758 15.238C7.99958 15.28 7.87858 15.301 7.74458 15.301H7.45057V12.985H7.74458C7.87858 12.985 7.99958 13.007 8.10758 13.051C8.21558 13.093 8.30758 13.16 8.38358 13.252C8.46158 13.344 8.52158 13.464 8.56358 13.612C8.60559 13.758 8.62659 13.935 8.62659 14.143C8.62659 14.349 8.60559 14.526 8.56358 14.674C8.52158 14.822 8.46158 14.942 8.38358 15.034C8.30758 15.126 8.21558 15.194 8.10758 15.238Z" fill="#E54666"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6562 19.7578H2.94141C0.972656 19.7578 0 18.7734 0 16.7695V2.98828C0 0.996094 0.972656 0 2.94141 0H7.07812V6.80859C7.07812 8.05078 7.66406 8.63672 8.90625 8.63672H15.6094V16.7695C15.6094 18.7617 14.625 19.7578 12.6562 19.7578ZM3.83969 14.71H4.3377C4.4297 14.71 4.5347 14.703 4.6527 14.689C4.7727 14.673 4.8947 14.642 5.0187 14.596C5.14471 14.55 5.26071 14.483 5.36671 14.395C5.47471 14.305 5.56071 14.186 5.62471 14.038C5.69071 13.89 5.72371 13.706 5.72371 13.486C5.72371 13.266 5.69071 13.084 5.62471 12.94C5.56071 12.794 5.47471 12.677 5.36671 12.589C5.26071 12.501 5.14471 12.436 5.0187 12.394C4.8947 12.35 4.7727 12.321 4.6527 12.307C4.5347 12.293 4.4297 12.286 4.3377 12.286H3.41069C3.04269 12.286 2.85868 12.47 2.85868 12.838V15.448C2.85868 15.816 3.02268 16 3.35069 16C3.67669 16 3.83969 15.816 3.83969 15.448V14.71ZM8.17058 12.319C8.00858 12.297 7.86758 12.286 7.74758 12.286H7.02157C6.65356 12.286 6.46956 12.47 6.46956 12.838V15.448C6.46956 15.816 6.65356 16 7.02157 16H7.74758C7.86758 16 8.00858 15.989 8.17058 15.967C8.33258 15.945 8.49758 15.901 8.66559 15.835C8.83359 15.767 8.98959 15.666 9.13359 15.532C9.27959 15.396 9.39659 15.216 9.48459 14.992C9.57459 14.766 9.61959 14.483 9.61959 14.143C9.61959 13.803 9.57459 13.521 9.48459 13.297C9.39659 13.071 9.27959 12.891 9.13359 12.757C8.98959 12.621 8.83359 12.52 8.66559 12.454C8.49758 12.386 8.33258 12.341 8.17058 12.319ZM13.1405 12.895C13.2345 12.833 13.2815 12.746 13.2815 12.634C13.2815 12.52 13.2345 12.434 13.1405 12.376C13.0465 12.316 12.9085 12.286 12.7265 12.286H11.1755C10.8095 12.286 10.6264 12.47 10.6264 12.838V15.502C10.6264 15.868 10.7894 16.051 11.1155 16.051C11.4455 16.051 11.6105 15.868 11.6105 15.502V14.788H12.5915C12.8955 14.788 13.0475 14.672 13.0475 14.44C13.0475 14.206 12.8955 14.089 12.5915 14.089H11.6105V12.985H12.7265C12.9085 12.985 13.0465 12.955 13.1405 12.895Z" fill="#E54666"/>
|
||||
<path d="M15.5039 7.26562H8.90625C8.61328 7.26562 8.44922 7.11328 8.44922 6.80859V0.0820312C8.88281 0.152344 9.31641 0.457031 9.79688 0.949219L14.6602 5.91797C15.1523 6.42188 15.4453 6.83203 15.5039 7.26562Z" fill="#E54666"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2931_148088">
|
||||
<rect width="15.9609" height="19.7695" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>`,
|
||||
width: 16,
|
||||
height: 20,
|
||||
},
|
||||
'file-ppt': {
|
||||
body: `<g clip-path="url(#clip0_2931_148086)">
|
||||
<path d="M3.83969 14.011V12.985H4.1847C4.2547 12.985 4.3227 12.99 4.3887 13C4.4547 13.01 4.5137 13.032 4.5657 13.066C4.6197 13.1 4.6617 13.151 4.6917 13.219C4.7237 13.285 4.7397 13.374 4.7397 13.486C4.7397 13.6 4.7237 13.692 4.6917 13.762C4.6617 13.832 4.6197 13.885 4.5657 13.921C4.5137 13.955 4.4547 13.979 4.3887 13.993C4.3227 14.005 4.2547 14.011 4.1847 14.011H3.83969Z" fill="#F76808"/>
|
||||
<path d="M7.65458 14.011V12.985H7.99958C8.06958 12.985 8.13758 12.99 8.20358 13C8.26958 13.01 8.32858 13.032 8.38058 13.066C8.43458 13.1 8.47658 13.151 8.50658 13.219C8.53858 13.285 8.55458 13.374 8.55458 13.486C8.55458 13.6 8.53858 13.692 8.50658 13.762C8.47658 13.832 8.43458 13.885 8.38058 13.921C8.32858 13.955 8.26958 13.979 8.20358 13.993C8.13758 14.005 8.06958 14.011 7.99958 14.011H7.65458Z" fill="#F76808"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6562 19.7578H2.94141C0.972656 19.7578 0 18.7734 0 16.7695V2.98828C0 0.996094 0.972656 0 2.94141 0H7.07812V6.80859C7.07812 8.05078 7.66406 8.63672 8.90625 8.63672H15.6094V16.7695C15.6094 18.7617 14.625 19.7578 12.6562 19.7578ZM3.83969 14.71H4.3377C4.4297 14.71 4.5347 14.703 4.6527 14.689C4.7727 14.673 4.8947 14.642 5.0187 14.596C5.14471 14.55 5.26071 14.483 5.36671 14.395C5.47471 14.305 5.56071 14.186 5.62471 14.038C5.69071 13.89 5.72371 13.706 5.72371 13.486C5.72371 13.266 5.69071 13.084 5.62471 12.94C5.56071 12.794 5.47471 12.677 5.36671 12.589C5.26071 12.501 5.14471 12.436 5.0187 12.394C4.8947 12.35 4.7727 12.321 4.6527 12.307C4.5347 12.293 4.4297 12.286 4.3377 12.286H3.41069C3.04269 12.286 2.85868 12.47 2.85868 12.838V15.448C2.85868 15.816 3.02268 16 3.35069 16C3.67669 16 3.83969 15.816 3.83969 15.448V14.71ZM7.65458 14.71H8.15258C8.24458 14.71 8.34958 14.703 8.46758 14.689C8.58758 14.673 8.70959 14.642 8.83359 14.596C8.95959 14.55 9.07559 14.483 9.18159 14.395C9.28959 14.305 9.37559 14.186 9.43959 14.038C9.50559 13.89 9.53859 13.706 9.53859 13.486C9.53859 13.266 9.50559 13.084 9.43959 12.94C9.37559 12.794 9.28959 12.677 9.18159 12.589C9.07559 12.501 8.95959 12.436 8.83359 12.394C8.70959 12.35 8.58758 12.321 8.46758 12.307C8.34958 12.293 8.24458 12.286 8.15258 12.286H7.22557C6.85757 12.286 6.67357 12.47 6.67357 12.838V15.448C6.67357 15.816 6.83757 16 7.16557 16C7.49157 16 7.65458 15.816 7.65458 15.448V14.71ZM13.4105 12.634C13.4105 12.402 13.2265 12.286 12.8585 12.286H10.7704C10.5924 12.286 10.4554 12.316 10.3594 12.376C10.2654 12.434 10.2184 12.52 10.2184 12.634C10.2184 12.746 10.2654 12.833 10.3594 12.895C10.4554 12.955 10.5924 12.985 10.7704 12.985H11.3225V15.502C11.3225 15.868 11.4855 16.051 11.8115 16.051C12.1395 16.051 12.3035 15.868 12.3035 15.502V12.985H12.9605C13.1085 12.985 13.2205 12.955 13.2965 12.895C13.3725 12.833 13.4105 12.746 13.4105 12.634Z" fill="#F76808"/>
|
||||
<path d="M15.5039 7.26562H8.90625C8.61328 7.26562 8.44922 7.11328 8.44922 6.80859V0.0820312C8.88281 0.152344 9.31641 0.457031 9.79688 0.949219L14.6602 5.91797C15.1523 6.42188 15.4453 6.83203 15.5039 7.26562Z" fill="#F76808"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2931_148086">
|
||||
<rect width="15.9609" height="19.7695" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>`,
|
||||
width: 16,
|
||||
height: 20,
|
||||
},
|
||||
'file-txt': {
|
||||
body: `<g clip-path="url(#clip0_2931_148087)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6562 19.7578H2.94141C0.972656 19.7578 0 18.7734 0 16.7695V2.98828C0 0.996094 0.972656 0 2.94141 0H7.07812V6.80859C7.07812 8.05078 7.66406 8.63672 8.90625 8.63672H15.6094V16.7695C15.6094 18.7617 14.625 19.7578 12.6562 19.7578ZM5.78071 12.634C5.78071 12.402 5.59671 12.286 5.22871 12.286H3.14069C2.96268 12.286 2.82568 12.316 2.72968 12.376C2.63568 12.434 2.58868 12.52 2.58868 12.634C2.58868 12.746 2.63568 12.833 2.72968 12.895C2.82568 12.955 2.96268 12.985 3.14069 12.985H3.69269V15.502C3.69269 15.868 3.85569 16.051 4.1817 16.051C4.5097 16.051 4.6737 15.868 4.6737 15.502V12.985H5.33071C5.47871 12.985 5.59071 12.955 5.66671 12.895C5.74271 12.833 5.78071 12.746 5.78071 12.634ZM6.63757 15.952C6.71557 16.022 6.82457 16.057 6.96457 16.057C7.09657 16.057 7.19657 16.022 7.26457 15.952C7.33457 15.882 7.41057 15.782 7.49257 15.652L7.99358 14.869L8.48858 15.652C8.57058 15.78 8.64559 15.88 8.71359 15.952C8.78359 16.022 8.88659 16.057 9.02259 16.057C9.15859 16.057 9.26759 16.024 9.34959 15.958C9.43359 15.892 9.47559 15.808 9.47559 15.706C9.47559 15.656 9.46659 15.605 9.44859 15.553C9.43259 15.501 9.40159 15.44 9.35559 15.37L8.54858 14.158L9.41559 12.907C9.47959 12.817 9.51159 12.723 9.51159 12.625C9.51159 12.497 9.46959 12.403 9.38559 12.343C9.30359 12.281 9.20159 12.25 9.07959 12.25C8.95159 12.25 8.84859 12.282 8.77059 12.346C8.69259 12.408 8.60458 12.52 8.50658 12.682L8.00558 13.501L7.49257 12.682C7.42657 12.576 7.36957 12.492 7.32157 12.43C7.27357 12.366 7.22057 12.32 7.16257 12.292C7.10657 12.264 7.03357 12.25 6.94357 12.25C6.82357 12.25 6.71857 12.282 6.62856 12.346C6.53856 12.41 6.49356 12.503 6.49356 12.625C6.49356 12.669 6.49856 12.714 6.50856 12.76C6.52056 12.804 6.54456 12.852 6.58056 12.904L7.45657 14.194L6.64056 15.37C6.59656 15.436 6.56556 15.493 6.54756 15.541C6.53156 15.589 6.52356 15.638 6.52356 15.688C6.52356 15.792 6.56156 15.88 6.63757 15.952ZM13.4105 12.634C13.4105 12.402 13.2265 12.286 12.8585 12.286H10.7704C10.5924 12.286 10.4554 12.316 10.3594 12.376C10.2654 12.434 10.2184 12.52 10.2184 12.634C10.2184 12.746 10.2654 12.833 10.3594 12.895C10.4554 12.955 10.5924 12.985 10.7704 12.985H11.3225V15.502C11.3225 15.868 11.4855 16.051 11.8115 16.051C12.1395 16.051 12.3035 15.868 12.3035 15.502V12.985H12.9605C13.1085 12.985 13.2205 12.955 13.2965 12.895C13.3725 12.833 13.4105 12.746 13.4105 12.634Z" fill="#696E77"/>
|
||||
<path d="M15.5039 7.26562H8.90625C8.61328 7.26562 8.44922 7.11328 8.44922 6.80859V0.0820312C8.88281 0.152344 9.31641 0.457031 9.79688 0.949219L14.6602 5.91797C15.1523 6.42188 15.4453 6.83203 15.5039 7.26562Z" fill="#696E77"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2931_148087">
|
||||
<rect width="15.9609" height="19.7695" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>`,
|
||||
width: 16,
|
||||
height: 20,
|
||||
},
|
||||
'file-xls': {
|
||||
body: `<g clip-path="url(#clip0_2931_148085)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6562 19.7578H2.94141C0.972656 19.7578 0 18.7734 0 16.7695V2.98828C0 0.996094 0.972656 0 2.94141 0H7.07812V6.80859C7.07812 8.05078 7.66406 8.63672 8.90625 8.63672H15.6094V16.7695C15.6094 18.7617 14.625 19.7578 12.6562 19.7578ZM2.82268 15.952C2.90068 16.022 3.00968 16.057 3.14969 16.057C3.28169 16.057 3.38169 16.022 3.44969 15.952C3.51969 15.882 3.59569 15.782 3.67769 15.652L4.1787 14.869L4.6737 15.652C4.7557 15.78 4.8307 15.88 4.8987 15.952C4.9687 16.022 5.07171 16.057 5.20771 16.057C5.34371 16.057 5.45271 16.024 5.53471 15.958C5.61871 15.892 5.66071 15.808 5.66071 15.706C5.66071 15.656 5.65171 15.605 5.63371 15.553C5.61771 15.501 5.58671 15.44 5.54071 15.37L4.7337 14.158L5.60071 12.907C5.66471 12.817 5.69671 12.723 5.69671 12.625C5.69671 12.497 5.65471 12.403 5.57071 12.343C5.48871 12.281 5.38671 12.25 5.26471 12.25C5.13671 12.25 5.0337 12.282 4.9557 12.346C4.8777 12.408 4.7897 12.52 4.6917 12.682L4.1907 13.501L3.67769 12.682C3.61169 12.576 3.55469 12.492 3.50669 12.43C3.45869 12.366 3.40569 12.32 3.34769 12.292C3.29169 12.264 3.21869 12.25 3.12869 12.25C3.00868 12.25 2.90368 12.282 2.81368 12.346C2.72368 12.41 2.67868 12.503 2.67868 12.625C2.67868 12.669 2.68368 12.714 2.69368 12.76C2.70568 12.804 2.72968 12.852 2.76568 12.904L3.64169 14.194L2.82568 15.37C2.78168 15.436 2.75068 15.493 2.73268 15.541C2.71668 15.589 2.70868 15.638 2.70868 15.688C2.70868 15.792 2.74668 15.88 2.82268 15.952ZM8.89059 15.301H7.90358V12.811C7.90358 12.441 7.73958 12.256 7.41157 12.256C7.08357 12.256 6.91957 12.441 6.91957 12.811V15.448C6.91957 15.816 7.10357 16 7.47157 16H8.89059C9.06859 16 9.20459 15.97 9.29859 15.91C9.39459 15.85 9.44259 15.764 9.44259 15.652C9.44259 15.54 9.39459 15.454 9.29859 15.394C9.20459 15.332 9.06859 15.301 8.89059 15.301ZM11.0465 15.961C11.2705 16.029 11.5275 16.063 11.8175 16.063C12.1335 16.063 12.4125 16.016 12.6545 15.922C12.8965 15.828 13.0855 15.695 13.2215 15.523C13.3575 15.349 13.4255 15.145 13.4255 14.911C13.4255 14.685 13.3675 14.498 13.2515 14.35C13.1355 14.202 12.9755 14.081 12.7715 13.987C12.5695 13.893 12.3365 13.814 12.0725 13.75C11.8085 13.684 11.6125 13.614 11.4845 13.54C11.3585 13.466 11.2955 13.378 11.2955 13.276C11.2955 13.21 11.3175 13.153 11.3615 13.105C11.4055 13.055 11.4675 13.017 11.5475 12.991C11.6275 12.963 11.7195 12.949 11.8235 12.949C11.9655 12.949 12.0855 12.971 12.1835 13.015C12.2815 13.059 12.3585 13.114 12.4145 13.18C12.4665 13.23 12.5335 13.284 12.6155 13.342C12.6975 13.4 12.7955 13.429 12.9095 13.429C13.0255 13.429 13.1185 13.396 13.1885 13.33C13.2605 13.262 13.2965 13.17 13.2965 13.054C13.2965 12.956 13.2715 12.868 13.2215 12.79C13.1715 12.712 13.1055 12.644 13.0235 12.586C12.8855 12.476 12.7135 12.393 12.5075 12.337C12.3015 12.279 12.0885 12.25 11.8685 12.25C11.5705 12.25 11.3025 12.292 11.0645 12.376C10.8285 12.46 10.6414 12.58 10.5034 12.736C10.3654 12.892 10.2964 13.077 10.2964 13.291C10.2964 13.581 10.4024 13.83 10.6144 14.038C10.8285 14.246 11.1735 14.404 11.6495 14.512C11.9115 14.572 12.1065 14.633 12.2345 14.695C12.3625 14.755 12.4265 14.849 12.4265 14.977C12.4265 15.057 12.4005 15.126 12.3485 15.184C12.2965 15.242 12.2255 15.287 12.1355 15.319C12.0475 15.349 11.9475 15.364 11.8355 15.364C11.6695 15.364 11.5325 15.332 11.4245 15.268C11.3165 15.204 11.2215 15.13 11.1395 15.046C11.0695 14.98 10.9945 14.922 10.9145 14.872C10.8345 14.822 10.7364 14.797 10.6204 14.797C10.4844 14.797 10.3844 14.839 10.3204 14.923C10.2564 15.007 10.2244 15.093 10.2244 15.181C10.2244 15.377 10.3184 15.545 10.5064 15.685C10.6444 15.799 10.8245 15.891 11.0465 15.961Z" fill="#29A383"/>
|
||||
<path d="M15.5039 7.26562H8.90625C8.61328 7.26562 8.44922 7.11328 8.44922 6.80859V0.0820312C8.88281 0.152344 9.31641 0.457031 9.79688 0.949219L14.6602 5.91797C15.1523 6.42188 15.4453 6.83203 15.5039 7.26562Z" fill="#29A383"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2931_148085">
|
||||
<rect width="15.9609" height="19.7695" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>`,
|
||||
width: 16,
|
||||
height: 20,
|
||||
},
|
||||
'file-zip': {
|
||||
body: `<g clip-path="url(#clip0_2931_148090)">
|
||||
<path d="M12.6562 19.7578H2.94141C0.972656 19.7578 0 18.7734 0 16.7695V2.98828C0 0.996094 0.972656 0 2.94141 0H3V2H2.5C2.22386 2 2 2.22386 2 2.5C2 2.77614 2.22386 3 2.5 3H3V4H2.5C2.22386 4 2 4.22386 2 4.5C2 4.77614 2.22386 5 2.5 5H3V6H2.5C2.22386 6 2 6.22386 2 6.5C2 6.77614 2.22386 7 2.5 7H3V8H2.5C2.22386 8 2 8.22386 2 8.5C2 8.77614 2.22386 9 2.5 9H3V10H2.5C2.22386 10 2 10.2239 2 10.5C2 10.7761 2.22386 11 2.5 11H3V12H2.5C2.22386 12 2 12.2239 2 12.5C2 12.7761 2.22386 13 2.5 13H3V14H2.75C2.33579 14 2 14.3358 2 14.75V15.5C2 16.3284 2.67157 17 3.5 17C4.32843 17 5 16.3284 5 15.5V14.75C5 14.3358 4.66421 14 4.25 14H4V13H4.5C4.77614 13 5 12.7761 5 12.5C5 12.2239 4.77614 12 4.5 12H4V11H4.5C4.77614 11 5 10.7761 5 10.5C5 10.2239 4.77614 10 4.5 10H4V9H4.5C4.77614 9 5 8.77614 5 8.5C5 8.22386 4.77614 8 4.5 8H4V7H4.5C4.77614 7 5 6.77614 5 6.5C5 6.22386 4.77614 6 4.5 6H4V5H4.5C4.77614 5 5 4.77614 5 4.5C5 4.22386 4.77614 4 4.5 4H4V3H4.5C4.77614 3 5 2.77614 5 2.5C5 2.22386 4.77614 2 4.5 2H4V0H7.07812V6.80859C7.07812 8.05078 7.66406 8.63672 8.90625 8.63672H15.6094V16.7695C15.6094 18.7617 14.625 19.7578 12.6562 19.7578Z" fill="#6958AD"/>
|
||||
<path d="M15.5039 7.26562H8.90625C8.61328 7.26562 8.44922 7.11328 8.44922 6.80859V0.0820312C8.88281 0.152344 9.31641 0.457031 9.79688 0.949219L14.6602 5.91797C15.1523 6.42188 15.4453 6.83203 15.5039 7.26562Z" fill="#6958AD"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2931_148090">
|
||||
<rect width="15.9609" height="19.7695" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>`,
|
||||
width: 16,
|
||||
height: 20,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user