mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 04:27:53 +00:00
revert: Next bubble improvements (#10795)
This commit is contained in:
@@ -146,19 +146,17 @@
|
|||||||
--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: 255 228 181;
|
--solid-amber: 252 232 193;
|
||||||
--solid-blue: 182 216 255;
|
--solid-blue: 218 236 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: 196, 197, 198, 0.22;
|
--alpha-2: 201, 202, 207, 0.15;
|
||||||
--alpha-3: 255, 255, 255, 0.96;
|
--alpha-3: 255, 255, 255, 0.96;
|
||||||
--black-alpha-1: 0, 0, 0, 0.08;
|
--black-alpha-1: 0, 0, 0, 0.12;
|
||||||
--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.84;
|
--white-alpha: 255, 255, 255, 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -242,18 +240,16 @@
|
|||||||
--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: 56 50 41;
|
--solid-amber: 42 37 30;
|
||||||
--solid-blue: 4 51 101;
|
--solid-blue: 16 49 91;
|
||||||
--solid-iris: 38 42 101;
|
--solid-iris: 38 42 101;
|
||||||
--solid-purple: 51 51 107;
|
--text-blue: 126 182 255;
|
||||||
--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'],
|
||||||
['#023B37', '#0BD8B6'],
|
|
||||||
['#2A2A2A', '#ADB1B8'],
|
['#2A2A2A', '#ADB1B8'],
|
||||||
|
['#023B37', '#0BD8B6'],
|
||||||
['#27264D', '#A19EFF'],
|
['#27264D', '#A19EFF'],
|
||||||
['#1D2E62', '#9EB1FF'],
|
['#1D2E62', '#9EB1FF'],
|
||||||
],
|
],
|
||||||
light: [
|
light: [
|
||||||
['#FBDCEF', '#C2298A'],
|
['#FBDCEF', '#C2298A'],
|
||||||
['#FFE0BB', '#99543A'],
|
['#FFE0BB', '#99543A'],
|
||||||
['#CCF3EA', '#008573'],
|
|
||||||
['#E8E8E8', '#60646C'],
|
['#E8E8E8', '#60646C'],
|
||||||
|
['#CCF3EA', '#008573'],
|
||||||
['#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 }, // eslint-disable-line vue/no-unused-properties
|
currentUserId: { type: Number, required: true },
|
||||||
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,11 +173,7 @@ const variant = computed(() => {
|
|||||||
return variants[props.messageType] || MESSAGE_VARIANTS.USER;
|
return variants[props.messageType] || MESSAGE_VARIANTS.USER;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isBotOrAgentMessage = computed(() => {
|
const isMyMessage = 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 (
|
||||||
@@ -186,15 +182,17 @@ const isBotOrAgentMessage = 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 true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase();
|
return (
|
||||||
|
senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase() &&
|
||||||
|
props.currentUserId === senderId
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,7 +200,7 @@ const isBotOrAgentMessage = 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 (isBotOrAgentMessage.value) {
|
if (isMyMessage.value) {
|
||||||
return ORIENTATION.RIGHT;
|
return ORIENTATION.RIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +221,8 @@ const flexOrientationClass = computed(() => {
|
|||||||
|
|
||||||
const gridClass = computed(() => {
|
const gridClass = computed(() => {
|
||||||
const map = {
|
const map = {
|
||||||
[ORIENTATION.LEFT]: 'grid grid-cols-1fr',
|
[ORIENTATION.LEFT]: 'grid grid-cols-[24px_1fr]',
|
||||||
[ORIENTATION.RIGHT]: 'grid grid-cols-[1fr_24px]',
|
[ORIENTATION.RIGHT]: 'grid grid-cols-1fr',
|
||||||
};
|
};
|
||||||
|
|
||||||
return map[orientation.value];
|
return map[orientation.value];
|
||||||
@@ -233,12 +231,12 @@ const gridClass = computed(() => {
|
|||||||
const gridTemplate = computed(() => {
|
const gridTemplate = computed(() => {
|
||||||
const map = {
|
const map = {
|
||||||
[ORIENTATION.LEFT]: `
|
[ORIENTATION.LEFT]: `
|
||||||
"bubble"
|
"avatar bubble"
|
||||||
"meta"
|
"spacer meta"
|
||||||
`,
|
`,
|
||||||
[ORIENTATION.RIGHT]: `
|
[ORIENTATION.RIGHT]: `
|
||||||
"bubble avatar"
|
"bubble"
|
||||||
"meta spacer"
|
"meta"
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -253,7 +251,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.LEFT) return false;
|
if (orientation.value === ORIENTATION.RIGHT) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -416,11 +414,6 @@ const avatarInfo = computed(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const avatarTooltip = computed(() => {
|
|
||||||
if (avatarInfo.value.name === '') return '';
|
|
||||||
return `${t('CONVERSATION.SENT_BY')} ${avatarInfo.value.name}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const setupHighlightTimer = () => {
|
const setupHighlightTimer = () => {
|
||||||
if (Number(route.query.messageId) !== Number(props.id)) {
|
if (Number(route.query.messageId) !== Number(props.id)) {
|
||||||
return;
|
return;
|
||||||
@@ -440,7 +433,7 @@ provideMessageContext({
|
|||||||
isPrivate: computed(() => props.private),
|
isPrivate: computed(() => props.private),
|
||||||
variant,
|
variant,
|
||||||
orientation,
|
orientation,
|
||||||
isBotOrAgentMessage,
|
isMyMessage,
|
||||||
shouldGroupWithNext,
|
shouldGroupWithNext,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -450,7 +443,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"
|
class="flex w-full message-bubble-container mb-2"
|
||||||
:data-message-id="props.id"
|
:data-message-id="props.id"
|
||||||
:class="[
|
:class="[
|
||||||
flexOrientationClass,
|
flexOrientationClass,
|
||||||
@@ -479,7 +472,6 @@ provideMessageContext({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!shouldGroupWithNext && shouldShowAvatar"
|
v-if="!shouldGroupWithNext && shouldShowAvatar"
|
||||||
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="24" />
|
||||||
@@ -487,13 +479,7 @@ provideMessageContext({
|
|||||||
<div
|
<div
|
||||||
class="[grid-area:bubble] flex"
|
class="[grid-area:bubble] flex"
|
||||||
:class="{
|
:class="{
|
||||||
'justify-end': orientation === ORIENTATION.RIGHT,
|
'ltr:pl-9 rtl:pl-0 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)"
|
||||||
|
|||||||
@@ -74,11 +74,7 @@ const shouldGroupWithNext = (index, searchList) => {
|
|||||||
nextMessageType === MESSAGE_TYPES.TEMPLATE &&
|
nextMessageType === MESSAGE_TYPES.TEMPLATE &&
|
||||||
currentMessageType === MESSAGE_TYPES.TEMPLATE;
|
currentMessageType === MESSAGE_TYPES.TEMPLATE;
|
||||||
|
|
||||||
const areBothActivity =
|
if (!hasSameSender || areBothTemplates) return false;
|
||||||
nextMessageType === MESSAGE_TYPES.ACTIVITY &&
|
|
||||||
currentMessageType === MESSAGE_TYPES.ACTIVITY;
|
|
||||||
|
|
||||||
if (!hasSameSender || areBothTemplates || areBothActivity) return false;
|
|
||||||
|
|
||||||
if (currentMessageType !== nextMessageType) return false;
|
if (currentMessageType !== nextMessageType) return false;
|
||||||
|
|
||||||
@@ -107,17 +103,6 @@ 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>
|
||||||
@@ -131,7 +116,6 @@ const getMessageSpacingClass = (message, messages, index) => {
|
|||||||
:group-with-next="shouldGroupWithNext(index, read)"
|
:group-with-next="shouldGroupWithNext(index, read)"
|
||||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||||
:current-user-id="currentUserId"
|
:current-user-id="currentUserId"
|
||||||
:class="getMessageSpacingClass(message, read, index)"
|
|
||||||
data-clarity-mask="True"
|
data-clarity-mask="True"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -144,7 +128,6 @@ const getMessageSpacingClass = (message, messages, index) => {
|
|||||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||||
:current-user-id="currentUserId"
|
:current-user-id="currentUserId"
|
||||||
:is-email-inbox="isAnEmailChannel"
|
:is-email-inbox="isAnEmailChannel"
|
||||||
:class="getMessageSpacingClass(message, unread, 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-3 py-1 !rounded-full flex min-w-0 items-center gap-2"
|
class="px-2 py-0.5 !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-purple text-n-slate-12',
|
[MESSAGE_VARIANTS.BOT]: 'bg-n-solid-iris text-n-slate-12',
|
||||||
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-purple text-n-slate-12',
|
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12',
|
||||||
[MESSAGE_VARIANTS.ERROR]: 'bg-n-solid-red text-n-ruby-12',
|
[MESSAGE_VARIANTS.ERROR]: 'bg-n-ruby-4 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',
|
||||||
|
|||||||
@@ -60,13 +60,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-alpha-white px-4 py-2 rounded-lg text-sm text-center border border-n-container"
|
class="w-full block bg-n-solid-3 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-alpha-white px-4 py-2 rounded-lg text-sm text-center border border-n-container"
|
class="w-full bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center border border-n-container"
|
||||||
@click="action.onClick"
|
@click="action.onClick"
|
||||||
>
|
>
|
||||||
{{ action.label }}
|
{{ action.label }}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const textToShow = computed(() => {
|
|||||||
<EmailMeta
|
<EmailMeta
|
||||||
class="p-3"
|
class="p-3"
|
||||||
:class="{
|
:class="{
|
||||||
'border-b border-n-slate-6': isIncoming,
|
'border-b border-n-strong': isIncoming,
|
||||||
'border-b border-n-slate-8/20': isOutgoing,
|
'border-b border-n-slate-8/20': isOutgoing,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@@ -73,7 +73,7 @@ const textToShow = 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-slate-4 via-n-strong/50 to-transparent dark:from-n-slate-5 dark:via-n-slate-4/50"
|
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"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
@@ -127,10 +127,7 @@ const textToShow = 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
|
<AttachmentChips :attachments="attachments" class="gap-1" />
|
||||||
:attachments="attachments"
|
|
||||||
class="flex flex-col gap-2.5"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</BaseBubble>
|
</BaseBubble>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,15 +20,12 @@ 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-4 flex flex-col">
|
<div class="gap-3 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
|
<AttachmentChips :attachments="attachments" class="gap-2" />
|
||||||
: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,7 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineOptions, useAttrs } from 'vue';
|
import { computed, defineOptions, useAttrs } from 'vue';
|
||||||
|
|
||||||
import ImageVideoChip from 'next/message/chips/AttachmentGrid.vue'; // Image and Video Grids are the same component
|
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 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';
|
||||||
@@ -36,7 +37,7 @@ const attrs = useAttrs();
|
|||||||
const { orientation } = useMessageContext();
|
const { orientation } = useMessageContext();
|
||||||
|
|
||||||
const classToApply = computed(() => {
|
const classToApply = computed(() => {
|
||||||
const baseClasses = [attrs.class, 'flex', 'flex-wrap', 'gap-2'];
|
const baseClasses = [attrs.class, 'flex', 'flex-wrap'];
|
||||||
|
|
||||||
if (orientation.value === 'right') {
|
if (orientation.value === 'right') {
|
||||||
baseClasses.push('justify-end');
|
baseClasses.push('justify-end');
|
||||||
@@ -76,25 +77,30 @@ const files = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="mediaAttachments.length" :class="classToApply">
|
<div v-if="mediaAttachments.length" :class="classToApply">
|
||||||
<ImageVideoChip :attachments="mediaAttachments" />
|
<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>
|
||||||
|
|
||||||
<div v-if="recordings.length" :class="classToApply">
|
<div v-if="recordings.length" :class="classToApply">
|
||||||
<div v-for="attachment in recordings" :key="attachment.id" class="w-full">
|
<div v-for="attachment in recordings" :key="attachment.id">
|
||||||
<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">
|
||||||
<div className="grid grid-cols-2 xl:grid-cols-3 gap-3">
|
<FileChip
|
||||||
<FileChip
|
v-for="attachment in files"
|
||||||
v-for="attachment in files"
|
:key="attachment.id"
|
||||||
:key="attachment.id"
|
:attachment="attachment"
|
||||||
:attachment="attachment"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -25,22 +25,16 @@ const isPlaying = ref(false);
|
|||||||
const isMuted = ref(false);
|
const isMuted = ref(false);
|
||||||
const currentTime = ref(0);
|
const currentTime = ref(0);
|
||||||
const duration = ref(0);
|
const duration = ref(0);
|
||||||
const playbackSpeed = ref(1);
|
|
||||||
|
|
||||||
const onLoadedMetadata = () => {
|
const onLoadedMetadata = () => {
|
||||||
duration.value = audioPlayer.value?.duration;
|
duration.value = audioPlayer.value?.duration;
|
||||||
};
|
};
|
||||||
|
|
||||||
const playbackSpeedLabel = computed(() => {
|
|
||||||
return `${playbackSpeed.value}x`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// There maybe a chance that the audioPlayer ref is not available
|
// There maybe a chance that the audioPlayer ref is not available
|
||||||
// When the onLoadMetadata is called, so we need to set the duration
|
// When the onLoadMetadata is called, so we need to set the duration
|
||||||
// value when the component is mounted
|
// value when the component is mounted
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
duration.value = audioPlayer.value?.duration;
|
duration.value = audioPlayer.value?.duration;
|
||||||
audioPlayer.value.playbackRate = playbackSpeed.value;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatTime = time => {
|
const formatTime = time => {
|
||||||
@@ -77,16 +71,6 @@ const playOrPause = () => {
|
|||||||
const onEnd = () => {
|
const onEnd = () => {
|
||||||
isPlaying.value = false;
|
isPlaying.value = false;
|
||||||
currentTime.value = 0;
|
currentTime.value = 0;
|
||||||
playbackSpeed.value = 1;
|
|
||||||
audioPlayer.value.playbackRate = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePlaybackSpeed = () => {
|
|
||||||
const speeds = [1, 1.5, 2];
|
|
||||||
const currentIndex = speeds.indexOf(playbackSpeed.value);
|
|
||||||
const nextIndex = (currentIndex + 1) % speeds.length;
|
|
||||||
playbackSpeed.value = speeds[nextIndex];
|
|
||||||
audioPlayer.value.playbackRate = playbackSpeed.value;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadAudio = async () => {
|
const downloadAudio = async () => {
|
||||||
@@ -121,7 +105,7 @@ const downloadAudio = async () => {
|
|||||||
<div class="tabular-nums text-xs">
|
<div class="tabular-nums text-xs">
|
||||||
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 items-center flex px-2">
|
<div class="flex items-center px-2">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -131,14 +115,6 @@ const downloadAudio = async () => {
|
|||||||
@input="seek"
|
@input="seek"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
class="border-0 w-10 h-6 grid place-content-center bg-n-alpha-2 hover:bg-alpha-3 rounded-2xl"
|
|
||||||
@click="changePlaybackSpeed"
|
|
||||||
>
|
|
||||||
<span class="text-xs text-n-slate-11 font-medium">
|
|
||||||
{{ playbackSpeedLabel }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="p-0 border-0 size-8 grid place-content-center"
|
class="p-0 border-0 size-8 grid place-content-center"
|
||||||
@click="toggleMute"
|
@click="toggleMute"
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ const fileType = computed(() => {
|
|||||||
return fileName.value.split('.').pop();
|
return fileName.value.split('.').pop();
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileNameWithoutExt = computed(() => {
|
|
||||||
const parts = fileName.value.split('.');
|
|
||||||
return parts.slice(0, -1).join('.') || fileName.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const textColorClass = computed(() => {
|
const textColorClass = computed(() => {
|
||||||
const colorMap = {
|
const colorMap = {
|
||||||
'7z': 'dark:text-[#EDEEF0] text-[#2F265F]',
|
'7z': 'dark:text-[#EDEEF0] text-[#2F265F]',
|
||||||
@@ -58,17 +53,11 @@ const textColorClass = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="h-9 bg-n-alpha-white gap-2 overflow-hidden items-center flex px-2 rounded-lg border border-n-container"
|
class="h-9 bg-n-alpha-white gap-2 items-center flex px-2 rounded-lg border border-n-container"
|
||||||
>
|
>
|
||||||
<FileIcon class="flex-shrink-0" :file-type="fileType" />
|
<FileIcon class="flex-shrink-0" :file-type="fileType" />
|
||||||
<span
|
<span class="mr-1 max-w-32 truncate" :class="textColorClass">
|
||||||
:class="textColorClass"
|
{{ fileName }}
|
||||||
class="inline-flex items-center text-sm overflow-hidden"
|
|
||||||
>
|
|
||||||
<span class="truncate min-w-6">
|
|
||||||
{{ fileNameWithoutExt }}
|
|
||||||
</span>
|
|
||||||
<span class="flex-shrink-0 whitespace-nowrap">.{{ fileType }}</span>
|
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
v-tooltip="t('CONVERSATION.DOWNLOAD')"
|
v-tooltip="t('CONVERSATION.DOWNLOAD')"
|
||||||
|
|||||||
@@ -11,16 +11,7 @@ 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);
|
||||||
|
|
||||||
@@ -29,20 +20,12 @@ 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="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
|
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer"
|
||||||
@click="handleGalleryClick"
|
@click="showGallery = true"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="hasError"
|
v-if="hasError"
|
||||||
@@ -51,7 +34,6 @@ const handleGalleryClose = () => {
|
|||||||
<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"
|
||||||
@@ -59,23 +41,12 @@ const handleGalleryClose = () => {
|
|||||||
@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="handleGalleryClose"
|
@close="() => (showGallery = false)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,33 +10,17 @@ 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="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
|
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group"
|
||||||
@click="handleGalleryClick"
|
@click="showGallery = true"
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
:src="attachment.dataUrl"
|
:src="attachment.dataUrl"
|
||||||
@@ -45,7 +29,6 @@ const handleGalleryClose = () => {
|
|||||||
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
|
||||||
@@ -58,21 +41,12 @@ const handleGalleryClose = () => {
|
|||||||
</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"
|
||||||
@close="handleGalleryClose"
|
@error="onError"
|
||||||
|
@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>} isBotOrAgentMessage - Does the message belong to the bot or agents
|
* @property {import('vue').ComputedRef<boolean>} isMyMessage - Does the message belong to the current user
|
||||||
* @property {import('vue').ComputedRef<boolean>} isPrivate - Proxy computed value for private
|
* @property {import('vue').ComputedRef<boolean>} 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -114,9 +114,6 @@ 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,8 +386,6 @@ 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