Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ed150fd82 | ||
|
|
019e2de124 | ||
|
|
eaf7f8d4a7 | ||
|
|
390f8bf9ac | ||
|
|
89b8276946 | ||
|
|
27946f5af8 | ||
|
|
898020e7c3 | ||
|
|
daf3ef2496 | ||
|
|
707ec5dfde | ||
|
|
fd115c8ab1 | ||
|
|
45869c4bfa | ||
|
|
30ebf7724e | ||
|
|
8733c0c64a | ||
|
|
b378cb5f82 | ||
|
|
5fffa86dc2 | ||
|
|
05d29fb046 | ||
|
|
dff4a2208d | ||
|
|
4f48d52eba | ||
|
|
b1ffce88e2 | ||
|
|
b5994e4b70 | ||
|
|
1c2f2ecad1 | ||
|
|
d9ce71d21d | ||
|
|
e94a58d976 | ||
|
|
c41f7507bf | ||
|
|
ad9a34949b | ||
|
|
182cddd799 | ||
|
|
5c55584038 | ||
|
|
0a6df5b67a | ||
|
|
d0ca703da2 | ||
|
|
0cac9e1a6d | ||
|
|
7bdc4d78b2 | ||
|
|
2500341278 | ||
|
|
d09701db37 | ||
|
|
8ac679e042 | ||
|
|
cc4817b233 | ||
|
|
3c227f8048 | ||
|
|
79545ba2b8 | ||
|
|
1dd9e867cd | ||
|
|
123f0cb61f | ||
|
|
78220565a9 | ||
|
|
cbcb32acf5 | ||
|
|
d451329a81 | ||
|
|
5f083b5c17 | ||
|
|
a296847dd4 | ||
|
|
0a0d1e07f4 | ||
|
|
077a8fd498 | ||
|
|
11e77e9349 |
@@ -146,17 +146,19 @@
|
|||||||
--solid-2: 255 255 255;
|
--solid-2: 255 255 255;
|
||||||
--solid-3: 255 255 255;
|
--solid-3: 255 255 255;
|
||||||
--solid-active: 255 255 255;
|
--solid-active: 255 255 255;
|
||||||
--solid-amber: 252 232 193;
|
--solid-amber: 255 228 181;
|
||||||
--solid-blue: 218 236 255;
|
--solid-blue: 182 216 255;
|
||||||
--solid-iris: 230 231 255;
|
--solid-iris: 230 231 255;
|
||||||
|
--solid-purple: 202 202 255;
|
||||||
|
--solid-red: 255 212 219;
|
||||||
|
|
||||||
--alpha-1: 67, 67, 67, 0.06;
|
--alpha-1: 67, 67, 67, 0.06;
|
||||||
--alpha-2: 201, 202, 207, 0.15;
|
--alpha-2: 196, 197, 198, 0.22;
|
||||||
--alpha-3: 255, 255, 255, 0.96;
|
--alpha-3: 255, 255, 255, 0.96;
|
||||||
--black-alpha-1: 0, 0, 0, 0.12;
|
--black-alpha-1: 0, 0, 0, 0.08;
|
||||||
--black-alpha-2: 0, 0, 0, 0.04;
|
--black-alpha-2: 0, 0, 0, 0.04;
|
||||||
--border-blue: 39, 129, 246, 0.5;
|
--border-blue: 39, 129, 246, 0.5;
|
||||||
--white-alpha: 255, 255, 255, 0.8;
|
--white-alpha: 255, 255, 255, 0.84;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -240,16 +242,18 @@
|
|||||||
--gray-12: 238 238 238;
|
--gray-12: 238 238 238;
|
||||||
|
|
||||||
--background-color: 18 18 19;
|
--background-color: 18 18 19;
|
||||||
|
--text-blue: 126 182 255;
|
||||||
--border-strong: 52 52 52;
|
--border-strong: 52 52 52;
|
||||||
--border-weak: 38 38 42;
|
--border-weak: 38 38 42;
|
||||||
--solid-1: 23 23 26;
|
--solid-1: 23 23 26;
|
||||||
--solid-2: 29 30 36;
|
--solid-2: 29 30 36;
|
||||||
--solid-3: 44 45 54;
|
--solid-3: 44 45 54;
|
||||||
--solid-active: 53 57 66;
|
--solid-active: 53 57 66;
|
||||||
--solid-amber: 42 37 30;
|
--solid-amber: 56 50 41;
|
||||||
--solid-blue: 16 49 91;
|
--solid-blue: 4 51 101;
|
||||||
--solid-iris: 38 42 101;
|
--solid-iris: 38 42 101;
|
||||||
--text-blue: 126 182 255;
|
--solid-purple: 51 51 107;
|
||||||
|
--solid-red: 78 46 55;
|
||||||
|
|
||||||
--alpha-1: 36, 36, 36, 0.8;
|
--alpha-1: 36, 36, 36, 0.8;
|
||||||
--alpha-2: 139, 147, 182, 0.15;
|
--alpha-2: 139, 147, 182, 0.15;
|
||||||
|
|||||||
@@ -50,16 +50,16 @@ const AVATAR_COLORS = {
|
|||||||
dark: [
|
dark: [
|
||||||
['#4B143D', '#FF8DCC'],
|
['#4B143D', '#FF8DCC'],
|
||||||
['#3F220D', '#FFA366'],
|
['#3F220D', '#FFA366'],
|
||||||
['#2A2A2A', '#ADB1B8'],
|
|
||||||
['#023B37', '#0BD8B6'],
|
['#023B37', '#0BD8B6'],
|
||||||
|
['#2A2A2A', '#ADB1B8'],
|
||||||
['#27264D', '#A19EFF'],
|
['#27264D', '#A19EFF'],
|
||||||
['#1D2E62', '#9EB1FF'],
|
['#1D2E62', '#9EB1FF'],
|
||||||
],
|
],
|
||||||
light: [
|
light: [
|
||||||
['#FBDCEF', '#C2298A'],
|
['#FBDCEF', '#C2298A'],
|
||||||
['#FFE0BB', '#99543A'],
|
['#FFE0BB', '#99543A'],
|
||||||
['#E8E8E8', '#60646C'],
|
|
||||||
['#CCF3EA', '#008573'],
|
['#CCF3EA', '#008573'],
|
||||||
|
['#E8E8E8', '#60646C'],
|
||||||
['#EBEBFE', '#4747C2'],
|
['#EBEBFE', '#4747C2'],
|
||||||
['#E1E9FF', '#3A5BC7'],
|
['#E1E9FF', '#3A5BC7'],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
conversationId: { type: Number, required: true },
|
conversationId: { type: Number, required: true },
|
||||||
createdAt: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
|
createdAt: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
|
||||||
currentUserId: { type: Number, required: true },
|
currentUserId: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
|
||||||
groupWithNext: { type: Boolean, default: false },
|
groupWithNext: { type: Boolean, default: false },
|
||||||
inboxId: { type: Number, default: null }, // eslint-disable-line vue/no-unused-properties
|
inboxId: { type: Number, default: null }, // eslint-disable-line vue/no-unused-properties
|
||||||
inboxSupportsReplyTo: { type: Object, default: () => ({}) },
|
inboxSupportsReplyTo: { type: Object, default: () => ({}) },
|
||||||
@@ -173,7 +173,11 @@ const variant = computed(() => {
|
|||||||
return variants[props.messageType] || MESSAGE_VARIANTS.USER;
|
return variants[props.messageType] || MESSAGE_VARIANTS.USER;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isMyMessage = computed(() => {
|
const isBotOrAgentMessage = computed(() => {
|
||||||
|
if (props.messageType === MESSAGE_TYPES.ACTIVITY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// if an outgoing message is still processing, then it's definitely a
|
// if an outgoing message is still processing, then it's definitely a
|
||||||
// message sent by the current user
|
// message sent by the current user
|
||||||
if (
|
if (
|
||||||
@@ -182,17 +186,15 @@ const isMyMessage = computed(() => {
|
|||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const senderId = props.senderId ?? props.sender?.id;
|
const senderId = props.senderId ?? props.sender?.id;
|
||||||
const senderType = props.senderType ?? props.sender?.type;
|
const senderType = props.senderType ?? props.sender?.type;
|
||||||
|
|
||||||
if (!senderType || !senderId) {
|
if (!senderType || !senderId) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase();
|
||||||
senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase() &&
|
|
||||||
props.currentUserId === senderId
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -200,7 +202,7 @@ const isMyMessage = computed(() => {
|
|||||||
* @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation
|
* @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation
|
||||||
*/
|
*/
|
||||||
const orientation = computed(() => {
|
const orientation = computed(() => {
|
||||||
if (isMyMessage.value) {
|
if (isBotOrAgentMessage.value) {
|
||||||
return ORIENTATION.RIGHT;
|
return ORIENTATION.RIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,8 +223,8 @@ const flexOrientationClass = computed(() => {
|
|||||||
|
|
||||||
const gridClass = computed(() => {
|
const gridClass = computed(() => {
|
||||||
const map = {
|
const map = {
|
||||||
[ORIENTATION.LEFT]: 'grid grid-cols-[24px_1fr]',
|
[ORIENTATION.LEFT]: 'grid grid-cols-1fr',
|
||||||
[ORIENTATION.RIGHT]: 'grid grid-cols-1fr',
|
[ORIENTATION.RIGHT]: 'grid grid-cols-[1fr_24px]',
|
||||||
};
|
};
|
||||||
|
|
||||||
return map[orientation.value];
|
return map[orientation.value];
|
||||||
@@ -231,13 +233,13 @@ const gridClass = computed(() => {
|
|||||||
const gridTemplate = computed(() => {
|
const gridTemplate = computed(() => {
|
||||||
const map = {
|
const map = {
|
||||||
[ORIENTATION.LEFT]: `
|
[ORIENTATION.LEFT]: `
|
||||||
"avatar bubble"
|
|
||||||
"spacer meta"
|
|
||||||
`,
|
|
||||||
[ORIENTATION.RIGHT]: `
|
|
||||||
"bubble"
|
"bubble"
|
||||||
"meta"
|
"meta"
|
||||||
`,
|
`,
|
||||||
|
[ORIENTATION.RIGHT]: `
|
||||||
|
"bubble avatar"
|
||||||
|
"meta spacer"
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return map[orientation.value];
|
return map[orientation.value];
|
||||||
@@ -251,7 +253,7 @@ const shouldGroupWithNext = computed(() => {
|
|||||||
|
|
||||||
const shouldShowAvatar = computed(() => {
|
const shouldShowAvatar = computed(() => {
|
||||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
|
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
|
||||||
if (orientation.value === ORIENTATION.RIGHT) return false;
|
if (orientation.value === ORIENTATION.LEFT) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -438,7 +440,7 @@ provideMessageContext({
|
|||||||
isPrivate: computed(() => props.private),
|
isPrivate: computed(() => props.private),
|
||||||
variant,
|
variant,
|
||||||
orientation,
|
orientation,
|
||||||
isMyMessage,
|
isBotOrAgentMessage,
|
||||||
shouldGroupWithNext,
|
shouldGroupWithNext,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -448,7 +450,7 @@ provideMessageContext({
|
|||||||
<div
|
<div
|
||||||
v-if="shouldRenderMessage"
|
v-if="shouldRenderMessage"
|
||||||
:id="`message${props.id}`"
|
:id="`message${props.id}`"
|
||||||
class="flex w-full message-bubble-container mb-2"
|
class="flex w-full message-bubble-container"
|
||||||
:data-message-id="props.id"
|
:data-message-id="props.id"
|
||||||
:class="[
|
:class="[
|
||||||
flexOrientationClass,
|
flexOrientationClass,
|
||||||
@@ -470,22 +472,28 @@ provideMessageContext({
|
|||||||
'w-full': variant === MESSAGE_VARIANTS.EMAIL,
|
'w-full': variant === MESSAGE_VARIANTS.EMAIL,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
class="gap-x-3"
|
class="gap-x-2"
|
||||||
:style="{
|
:style="{
|
||||||
gridTemplateAreas: gridTemplate,
|
gridTemplateAreas: gridTemplate,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!shouldGroupWithNext && shouldShowAvatar"
|
v-if="!shouldGroupWithNext && shouldShowAvatar"
|
||||||
v-tooltip.right-end="avatarTooltip"
|
v-tooltip.left-end="avatarTooltip"
|
||||||
class="[grid-area:avatar] flex items-end"
|
class="[grid-area:avatar] flex items-end"
|
||||||
>
|
>
|
||||||
<Avatar v-bind="avatarInfo" :size="24" />
|
<Avatar v-bind="avatarInfo" :size="20" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="[grid-area:bubble] flex"
|
class="[grid-area:bubble] flex"
|
||||||
:class="{
|
:class="{
|
||||||
'ltr:pl-9 rtl:pl-0 justify-end': orientation === ORIENTATION.RIGHT,
|
'justify-end': orientation === ORIENTATION.RIGHT,
|
||||||
|
'ltr:pl-12 rtl:pr-12 2xl:ltr:pl-0 2xl:rtl:pr-0':
|
||||||
|
orientation === ORIENTATION.RIGHT &&
|
||||||
|
variant !== MESSAGE_VARIANTS.EMAIL,
|
||||||
|
'ltr:pr-12 rtl:pl-12 2xl:ltr:pr-0 2xl:rtl:pl-0':
|
||||||
|
orientation === ORIENTATION.LEFT &&
|
||||||
|
variant !== MESSAGE_VARIANTS.EMAIL,
|
||||||
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
||||||
}"
|
}"
|
||||||
@contextmenu="openContextMenu($event)"
|
@contextmenu="openContextMenu($event)"
|
||||||
|
|||||||
@@ -66,7 +66,11 @@ const shouldGroupWithNext = (index, searchList) => {
|
|||||||
nextMessageType === MESSAGE_TYPES.TEMPLATE &&
|
nextMessageType === MESSAGE_TYPES.TEMPLATE &&
|
||||||
currentMessageType === MESSAGE_TYPES.TEMPLATE;
|
currentMessageType === MESSAGE_TYPES.TEMPLATE;
|
||||||
|
|
||||||
if (!hasSameSender || areBothTemplates) return false;
|
const areBothActivity =
|
||||||
|
nextMessageType === MESSAGE_TYPES.ACTIVITY &&
|
||||||
|
currentMessageType === MESSAGE_TYPES.ACTIVITY;
|
||||||
|
|
||||||
|
if (!hasSameSender || areBothTemplates || areBothActivity) return false;
|
||||||
|
|
||||||
if (currentMessageType !== nextMessageType) return false;
|
if (currentMessageType !== nextMessageType) return false;
|
||||||
|
|
||||||
@@ -95,6 +99,17 @@ const getInReplyToMessage = parentMessage => {
|
|||||||
|
|
||||||
return replyMessage ? useCamelCase(replyMessage) : null;
|
return replyMessage ? useCamelCase(replyMessage) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getMessageSpacingClass = (message, messages, index) => {
|
||||||
|
// For non-activity messages, use groupWithNext logic
|
||||||
|
if (message.messageType !== MESSAGE_TYPES.ACTIVITY) {
|
||||||
|
return shouldGroupWithNext(index, messages) ? 'mb-1' : 'mb-6';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For activity messages, check next message exists and is also an activity
|
||||||
|
const nextMessage = messages[index + 1];
|
||||||
|
return nextMessage?.messageType === MESSAGE_TYPES.ACTIVITY ? 'mb-2' : 'mb-6';
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -112,6 +127,7 @@ const getInReplyToMessage = parentMessage => {
|
|||||||
:group-with-next="shouldGroupWithNext(index, allMessages)"
|
:group-with-next="shouldGroupWithNext(index, allMessages)"
|
||||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||||
:current-user-id="currentUserId"
|
:current-user-id="currentUserId"
|
||||||
|
:class="getMessageSpacingClass(message, allMessages, index)"
|
||||||
data-clarity-mask="True"
|
data-clarity-mask="True"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const readableTime = computed(() =>
|
|||||||
<template>
|
<template>
|
||||||
<BaseBubble
|
<BaseBubble
|
||||||
v-tooltip.top="readableTime"
|
v-tooltip.top="readableTime"
|
||||||
class="px-2 py-0.5 !rounded-full flex min-w-0 items-center gap-2"
|
class="px-3 py-1 !rounded-full flex min-w-0 items-center gap-2"
|
||||||
data-bubble-name="activity"
|
data-bubble-name="activity"
|
||||||
>
|
>
|
||||||
<span v-dompurify-html="content" :title="content" />
|
<span v-dompurify-html="content" :title="content" />
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ const varaintBaseMap = {
|
|||||||
'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold',
|
'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold',
|
||||||
[MESSAGE_VARIANTS.USER]: 'bg-n-slate-4 text-n-slate-12',
|
[MESSAGE_VARIANTS.USER]: 'bg-n-slate-4 text-n-slate-12',
|
||||||
[MESSAGE_VARIANTS.ACTIVITY]: 'bg-n-alpha-1 text-n-slate-11 text-sm',
|
[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.BOT]: 'bg-n-solid-purple text-n-slate-12',
|
||||||
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12',
|
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-purple text-n-slate-12',
|
||||||
[MESSAGE_VARIANTS.ERROR]: 'bg-n-ruby-4 text-n-ruby-12',
|
[MESSAGE_VARIANTS.ERROR]: 'bg-n-solid-red text-n-ruby-12',
|
||||||
[MESSAGE_VARIANTS.EMAIL]: 'w-full',
|
[MESSAGE_VARIANTS.EMAIL]: 'w-full',
|
||||||
[MESSAGE_VARIANTS.UNSUPPORTED]:
|
[MESSAGE_VARIANTS.UNSUPPORTED]:
|
||||||
'bg-n-solid-amber/70 border border-dashed border-n-amber-12 text-n-amber-12',
|
'bg-n-solid-amber/70 border border-dashed border-n-amber-12 text-n-amber-12',
|
||||||
|
|||||||
@@ -64,13 +64,13 @@ const senderName = computed(() => {
|
|||||||
:href="action.href"
|
:href="action.href"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="w-full block bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center border border-n-container"
|
class="w-full block bg-n-alpha-white px-4 py-2 rounded-lg text-sm text-center border border-n-container"
|
||||||
>
|
>
|
||||||
{{ action.label }}
|
{{ action.label }}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
class="w-full bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center border border-n-container"
|
class="w-full bg-n-alpha-white px-4 py-2 rounded-lg text-sm text-center border border-n-container"
|
||||||
@click="action.onClick"
|
@click="action.onClick"
|
||||||
>
|
>
|
||||||
{{ action.label }}
|
{{ action.label }}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const hasQuotedMessage = computed(() => {
|
|||||||
<EmailMeta
|
<EmailMeta
|
||||||
class="p-3"
|
class="p-3"
|
||||||
:class="{
|
:class="{
|
||||||
'border-b border-n-strong': isIncoming,
|
'border-b border-n-slate-6': isIncoming,
|
||||||
'border-b border-n-slate-8/20': isOutgoing,
|
'border-b border-n-slate-8/20': isOutgoing,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@@ -75,7 +75,7 @@ const hasQuotedMessage = computed(() => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isExpandable && !isExpanded"
|
v-if="isExpandable && !isExpanded"
|
||||||
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end bg-gradient-to-t from-n-gray-3 via-n-gray-3 via-20% to-transparent"
|
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end bg-gradient-to-t from-n-slate-4 via-n-strong/50 to-transparent dark:from-n-slate-5 dark:via-n-slate-4/50"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
|
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
|
||||||
@@ -139,7 +139,10 @@ const hasQuotedMessage = computed(() => {
|
|||||||
v-if="Array.isArray(attachments) && attachments.length"
|
v-if="Array.isArray(attachments) && attachments.length"
|
||||||
class="px-4 pb-4 space-y-2"
|
class="px-4 pb-4 space-y-2"
|
||||||
>
|
>
|
||||||
<AttachmentChips :attachments="attachments" class="gap-1" />
|
<AttachmentChips
|
||||||
|
:attachments="attachments"
|
||||||
|
class="flex flex-col gap-2.5"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</BaseBubble>
|
</BaseBubble>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,12 +20,15 @@ const isEmpty = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BaseBubble class="px-4 py-3" data-bubble-name="text">
|
<BaseBubble class="px-4 py-3" data-bubble-name="text">
|
||||||
<div class="gap-3 flex flex-col">
|
<div class="gap-4 flex flex-col">
|
||||||
<span v-if="isEmpty" class="text-n-slate-11">
|
<span v-if="isEmpty" class="text-n-slate-11">
|
||||||
{{ $t('CONVERSATION.NO_CONTENT') }}
|
{{ $t('CONVERSATION.NO_CONTENT') }}
|
||||||
</span>
|
</span>
|
||||||
<FormattedContent v-if="content" :content="content" />
|
<FormattedContent v-if="content" :content="content" />
|
||||||
<AttachmentChips :attachments="attachments" class="gap-2" />
|
<AttachmentChips
|
||||||
|
:attachments="attachments"
|
||||||
|
class="flex flex-col gap-2.5"
|
||||||
|
/>
|
||||||
<template v-if="isTemplate">
|
<template v-if="isTemplate">
|
||||||
<div
|
<div
|
||||||
v-if="contentAttributes.submittedEmail"
|
v-if="contentAttributes.submittedEmail"
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineOptions, useAttrs } from 'vue';
|
import { computed, defineOptions, useAttrs } from 'vue';
|
||||||
|
|
||||||
import ImageChip from 'next/message/chips/Image.vue';
|
import ImageVideoChip from 'next/message/chips/AttachmentGrid.vue'; // Image and Video Grids are the same component
|
||||||
import VideoChip from 'next/message/chips/Video.vue';
|
|
||||||
import AudioChip from 'next/message/chips/Audio.vue';
|
import AudioChip from 'next/message/chips/Audio.vue';
|
||||||
import FileChip from 'next/message/chips/File.vue';
|
import FileChip from 'next/message/chips/File.vue';
|
||||||
import { useMessageContext } from '../provider.js';
|
import { useMessageContext } from '../provider.js';
|
||||||
@@ -37,7 +36,7 @@ const attrs = useAttrs();
|
|||||||
const { orientation } = useMessageContext();
|
const { orientation } = useMessageContext();
|
||||||
|
|
||||||
const classToApply = computed(() => {
|
const classToApply = computed(() => {
|
||||||
const baseClasses = [attrs.class, 'flex', 'flex-wrap'];
|
const baseClasses = [attrs.class, 'flex', 'flex-wrap', 'gap-2'];
|
||||||
|
|
||||||
if (orientation.value === 'right') {
|
if (orientation.value === 'right') {
|
||||||
baseClasses.push('justify-end');
|
baseClasses.push('justify-end');
|
||||||
@@ -77,30 +76,25 @@ const files = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="mediaAttachments.length" :class="classToApply">
|
<div v-if="mediaAttachments.length" :class="classToApply">
|
||||||
<template v-for="attachment in mediaAttachments" :key="attachment.id">
|
<ImageVideoChip :attachments="mediaAttachments" />
|
||||||
<ImageChip
|
|
||||||
v-if="attachment.fileType === ATTACHMENT_TYPES.IMAGE"
|
|
||||||
:attachment="attachment"
|
|
||||||
/>
|
|
||||||
<VideoChip
|
|
||||||
v-else-if="attachment.fileType === ATTACHMENT_TYPES.VIDEO"
|
|
||||||
:attachment="attachment"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="recordings.length" :class="classToApply">
|
<div v-if="recordings.length" :class="classToApply">
|
||||||
<div v-for="attachment in recordings" :key="attachment.id">
|
<div v-for="attachment in recordings" :key="attachment.id" class="w-full">
|
||||||
<AudioChip
|
<AudioChip
|
||||||
class="bg-n-alpha-3 dark:bg-n-alpha-2 text-n-slate-12"
|
class="bg-n-alpha-3 dark:bg-n-alpha-2 text-n-slate-12"
|
||||||
:attachment="attachment"
|
:attachment="attachment"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="files.length" :class="classToApply">
|
<div v-if="files.length" :class="classToApply">
|
||||||
<FileChip
|
<div className="grid grid-cols-1 2xl:grid-cols-2 gap-3">
|
||||||
v-for="attachment in files"
|
<FileChip
|
||||||
:key="attachment.id"
|
v-for="attachment in files"
|
||||||
:attachment="attachment"
|
:key="attachment.id"
|
||||||
/>
|
:attachment="attachment"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import ImageChip from './Image.vue';
|
||||||
|
import VideoChip from './Video.vue';
|
||||||
|
|
||||||
|
import { ATTACHMENT_TYPES } from '../constants';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
attachments: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
validator: value => Array.isArray(value) && value.length > 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MAX_DISPLAYED = 5;
|
||||||
|
|
||||||
|
const visibleAttachments = computed(() =>
|
||||||
|
props.attachments.slice(0, MAX_DISPLAYED)
|
||||||
|
);
|
||||||
|
|
||||||
|
const remainingCount = computed(() =>
|
||||||
|
Math.max(0, props.attachments.length - MAX_DISPLAYED)
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridClass = computed(() => {
|
||||||
|
const count = props.attachments.length;
|
||||||
|
const base = 'grid gap-2 w-full';
|
||||||
|
|
||||||
|
if (count === 1) return `${base} grid-cols-1`;
|
||||||
|
|
||||||
|
const classes = {
|
||||||
|
2: `${base} grid-cols-2 max-h-[500px]`,
|
||||||
|
3: `${base} grid-cols-2 max-h-[500px]`,
|
||||||
|
4: `${base} grid-cols-2 max-h-[500px]`,
|
||||||
|
5: `${base} grid-cols-3 max-h-[500px]`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return classes[count] || classes[5];
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemClass = computed(() => index => {
|
||||||
|
const count = props.attachments.length;
|
||||||
|
const base = 'relative overflow-hidden rounded-lg';
|
||||||
|
|
||||||
|
if (count === 1) return `${base} w-full h-auto`;
|
||||||
|
|
||||||
|
if (count === 3 && index === 0) return `${base} row-span-2 h-[492px]`;
|
||||||
|
if (count >= 5 && index === 0) return `${base} col-span-2 h-[242px]`;
|
||||||
|
|
||||||
|
return count === 2 ? `${base} h-[500px]` : `${base} h-[242px]`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldShowOverlay = computed(
|
||||||
|
() => index => remainingCount.value > 0 && index === MAX_DISPLAYED - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
const componentMap = {
|
||||||
|
[ATTACHMENT_TYPES.IMAGE]: ImageChip,
|
||||||
|
[ATTACHMENT_TYPES.VIDEO]: VideoChip,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getComponent = fileType =>
|
||||||
|
componentMap[fileType?.toLowerCase()] || componentMap[ATTACHMENT_TYPES.IMAGE];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="gridClass">
|
||||||
|
<div
|
||||||
|
v-for="(attachment, index) in visibleAttachments"
|
||||||
|
:key="attachment.id"
|
||||||
|
:class="itemClass(index)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="getComponent(attachment.fileType)"
|
||||||
|
:attachment="attachment"
|
||||||
|
:remaining-count="remainingCount"
|
||||||
|
:should-show-overlay="shouldShowOverlay(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -11,7 +11,16 @@ defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
remainingCount: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
shouldShowOverlay: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasError = ref(false);
|
const hasError = ref(false);
|
||||||
const showGallery = ref(false);
|
const showGallery = ref(false);
|
||||||
|
|
||||||
@@ -20,12 +29,20 @@ const { filteredCurrentChatAttachments } = useMessageContext();
|
|||||||
const handleError = () => {
|
const handleError = () => {
|
||||||
hasError.value = true;
|
hasError.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGalleryClick = () => {
|
||||||
|
showGallery.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGalleryClose = () => {
|
||||||
|
showGallery.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer"
|
class="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
|
||||||
@click="showGallery = true"
|
@click="handleGalleryClick"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="hasError"
|
v-if="hasError"
|
||||||
@@ -34,6 +51,7 @@ const handleError = () => {
|
|||||||
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
|
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
|
||||||
{{ $t('COMPONENTS.MEDIA.LOADING_FAILED') }}
|
{{ $t('COMPONENTS.MEDIA.LOADING_FAILED') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
class="object-cover w-full h-full"
|
class="object-cover w-full h-full"
|
||||||
@@ -41,12 +59,23 @@ const handleError = () => {
|
|||||||
@error="handleError"
|
@error="handleError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="shouldShowOverlay"
|
||||||
|
class="absolute inset-0 flex items-center cursor-pointer justify-center bg-n-black/25 dark:bg-n-alpha-1 rounded-lg"
|
||||||
|
@click="handleGalleryClick"
|
||||||
|
>
|
||||||
|
<span class="text-white text-2xl font-semibold">
|
||||||
|
+{{ remainingCount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<GalleryView
|
<GalleryView
|
||||||
v-if="showGallery"
|
v-if="showGallery"
|
||||||
v-model:show="showGallery"
|
v-model:show="showGallery"
|
||||||
:attachment="useSnakeCase(attachment)"
|
:attachment="useSnakeCase(attachment)"
|
||||||
:all-attachments="filteredCurrentChatAttachments"
|
:all-attachments="filteredCurrentChatAttachments"
|
||||||
@error="handleError"
|
@error="handleError"
|
||||||
@close="() => (showGallery = false)"
|
@close="handleGalleryClose"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,17 +10,33 @@ defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
remainingCount: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
shouldShowOverlay: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const showGallery = ref(false);
|
const showGallery = ref(false);
|
||||||
|
|
||||||
const { filteredCurrentChatAttachments } = useMessageContext();
|
const { filteredCurrentChatAttachments } = useMessageContext();
|
||||||
|
|
||||||
|
const handleGalleryClick = () => {
|
||||||
|
showGallery.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGalleryClose = () => {
|
||||||
|
showGallery.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group"
|
class="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
|
||||||
@click="showGallery = true"
|
@click="handleGalleryClick"
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
:src="attachment.dataUrl"
|
:src="attachment.dataUrl"
|
||||||
@@ -29,6 +45,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
|
|||||||
playsInline
|
playsInline
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
v-if="!shouldShowOverlay"
|
||||||
class="absolute w-full h-full inset-0 p-1 flex items-center justify-center"
|
class="absolute w-full h-full inset-0 p-1 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -41,12 +58,21 @@ const { filteredCurrentChatAttachments } = useMessageContext();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="shouldShowOverlay"
|
||||||
|
class="absolute inset-0 flex items-center cursor-pointer justify-center bg-n-black/25 dark:bg-n-alpha-1 rounded-lg"
|
||||||
|
@click="handleGalleryClick"
|
||||||
|
>
|
||||||
|
<span class="text-white text-2xl font-semibold">
|
||||||
|
+{{ remainingCount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<GalleryView
|
<GalleryView
|
||||||
v-if="showGallery"
|
v-if="showGallery"
|
||||||
v-model:show="showGallery"
|
v-model:show="showGallery"
|
||||||
:attachment="useSnakeCase(attachment)"
|
:attachment="useSnakeCase(attachment)"
|
||||||
:all-attachments="filteredCurrentChatAttachments"
|
:all-attachments="filteredCurrentChatAttachments"
|
||||||
@error="onError"
|
@close="handleGalleryClose"
|
||||||
@close="() => (showGallery = false)"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const MessageControl = Symbol('MessageControl');
|
|||||||
* @property {import('vue').Ref<Sender|null>} [sender=null] - The sender information
|
* @property {import('vue').Ref<Sender|null>} [sender=null] - The sender information
|
||||||
* @property {import('vue').ComputedRef<MessageOrientation>} orientation - The visual variant of the message
|
* @property {import('vue').ComputedRef<MessageOrientation>} orientation - The visual variant of the message
|
||||||
* @property {import('vue').ComputedRef<MessageVariant>} variant - The visual variant of the message
|
* @property {import('vue').ComputedRef<MessageVariant>} variant - The visual variant of the message
|
||||||
* @property {import('vue').ComputedRef<boolean>} isMyMessage - Does the message belong to the current user
|
* @property {import('vue').ComputedRef<boolean>} isBotOrAgentMessage - Does the message belong to the bot or agents
|
||||||
* @property {import('vue').ComputedRef<boolean>} isPrivate - Proxy computed value for private
|
* @property {import('vue').ComputedRef<boolean>} isPrivate - Proxy computed value for private
|
||||||
* @property {import('vue').ComputedRef<boolean>} shouldGroupWithNext - Should group with the next message or not, it is differnt from groupWithNext, this has a bypass for a failed message
|
* @property {import('vue').ComputedRef<boolean>} shouldGroupWithNext - Should group with the next message or not, it is differnt from groupWithNext, this has a bypass for a failed message
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ const tailwindConfig = {
|
|||||||
margin: '0 1em 0.5em 0',
|
margin: '0 1em 0.5em 0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'ul > li::marker': {
|
||||||
|
color: 'rgb(var(--slate-9))',
|
||||||
|
},
|
||||||
'ol li': {
|
'ol li': {
|
||||||
margin: '0 0 0.5em 1em',
|
margin: '0 0 0.5em 1em',
|
||||||
listStyleType: 'decimal',
|
listStyleType: 'decimal',
|
||||||
|
|||||||
@@ -386,6 +386,8 @@ export const colors = {
|
|||||||
amber: 'rgb(var(--solid-amber) / <alpha-value>)',
|
amber: 'rgb(var(--solid-amber) / <alpha-value>)',
|
||||||
blue: 'rgb(var(--solid-blue) / <alpha-value>)',
|
blue: 'rgb(var(--solid-blue) / <alpha-value>)',
|
||||||
iris: 'rgb(var(--solid-iris) / <alpha-value>)',
|
iris: 'rgb(var(--solid-iris) / <alpha-value>)',
|
||||||
|
purple: 'rgb(var(--solid-purple) / <alpha-value>)',
|
||||||
|
red: 'rgb(var(--solid-red) / <alpha-value>)',
|
||||||
},
|
},
|
||||||
alpha: {
|
alpha: {
|
||||||
1: 'rgba(var(--alpha-1))',
|
1: 'rgba(var(--alpha-1))',
|
||||||
|
|||||||
Reference in New Issue
Block a user