mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 02:32:29 +00:00
revert: Next bubble improvements (#10795)
This commit is contained in:
@@ -146,19 +146,17 @@
|
||||
--solid-2: 255 255 255;
|
||||
--solid-3: 255 255 255;
|
||||
--solid-active: 255 255 255;
|
||||
--solid-amber: 255 228 181;
|
||||
--solid-blue: 182 216 255;
|
||||
--solid-amber: 252 232 193;
|
||||
--solid-blue: 218 236 255;
|
||||
--solid-iris: 230 231 255;
|
||||
--solid-purple: 202 202 255;
|
||||
--solid-red: 255 212 219;
|
||||
|
||||
--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;
|
||||
--black-alpha-1: 0, 0, 0, 0.08;
|
||||
--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.84;
|
||||
--white-alpha: 255, 255, 255, 0.8;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -242,18 +240,16 @@
|
||||
--gray-12: 238 238 238;
|
||||
|
||||
--background-color: 18 18 19;
|
||||
--text-blue: 126 182 255;
|
||||
--border-strong: 52 52 52;
|
||||
--border-weak: 38 38 42;
|
||||
--solid-1: 23 23 26;
|
||||
--solid-2: 29 30 36;
|
||||
--solid-3: 44 45 54;
|
||||
--solid-active: 53 57 66;
|
||||
--solid-amber: 56 50 41;
|
||||
--solid-blue: 4 51 101;
|
||||
--solid-amber: 42 37 30;
|
||||
--solid-blue: 16 49 91;
|
||||
--solid-iris: 38 42 101;
|
||||
--solid-purple: 51 51 107;
|
||||
--solid-red: 78 46 55;
|
||||
--text-blue: 126 182 255;
|
||||
|
||||
--alpha-1: 36, 36, 36, 0.8;
|
||||
--alpha-2: 139, 147, 182, 0.15;
|
||||
|
||||
@@ -50,16 +50,16 @@ const AVATAR_COLORS = {
|
||||
dark: [
|
||||
['#4B143D', '#FF8DCC'],
|
||||
['#3F220D', '#FFA366'],
|
||||
['#023B37', '#0BD8B6'],
|
||||
['#2A2A2A', '#ADB1B8'],
|
||||
['#023B37', '#0BD8B6'],
|
||||
['#27264D', '#A19EFF'],
|
||||
['#1D2E62', '#9EB1FF'],
|
||||
],
|
||||
light: [
|
||||
['#FBDCEF', '#C2298A'],
|
||||
['#FFE0BB', '#99543A'],
|
||||
['#CCF3EA', '#008573'],
|
||||
['#E8E8E8', '#60646C'],
|
||||
['#CCF3EA', '#008573'],
|
||||
['#EBEBFE', '#4747C2'],
|
||||
['#E1E9FF', '#3A5BC7'],
|
||||
],
|
||||
|
||||
@@ -117,7 +117,7 @@ const props = defineProps({
|
||||
},
|
||||
conversationId: { type: Number, required: true },
|
||||
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 },
|
||||
inboxId: { type: Number, default: null }, // eslint-disable-line vue/no-unused-properties
|
||||
inboxSupportsReplyTo: { type: Object, default: () => ({}) },
|
||||
@@ -173,11 +173,7 @@ const variant = computed(() => {
|
||||
return variants[props.messageType] || MESSAGE_VARIANTS.USER;
|
||||
});
|
||||
|
||||
const isBotOrAgentMessage = computed(() => {
|
||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isMyMessage = computed(() => {
|
||||
// if an outgoing message is still processing, then it's definitely a
|
||||
// message sent by the current user
|
||||
if (
|
||||
@@ -186,15 +182,17 @@ const isBotOrAgentMessage = computed(() => {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const senderId = props.senderId ?? props.sender?.id;
|
||||
const senderType = props.senderType ?? props.sender?.type;
|
||||
|
||||
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
|
||||
*/
|
||||
const orientation = computed(() => {
|
||||
if (isBotOrAgentMessage.value) {
|
||||
if (isMyMessage.value) {
|
||||
return ORIENTATION.RIGHT;
|
||||
}
|
||||
|
||||
@@ -223,8 +221,8 @@ const flexOrientationClass = computed(() => {
|
||||
|
||||
const gridClass = computed(() => {
|
||||
const map = {
|
||||
[ORIENTATION.LEFT]: 'grid grid-cols-1fr',
|
||||
[ORIENTATION.RIGHT]: 'grid grid-cols-[1fr_24px]',
|
||||
[ORIENTATION.LEFT]: 'grid grid-cols-[24px_1fr]',
|
||||
[ORIENTATION.RIGHT]: 'grid grid-cols-1fr',
|
||||
};
|
||||
|
||||
return map[orientation.value];
|
||||
@@ -233,12 +231,12 @@ const gridClass = computed(() => {
|
||||
const gridTemplate = computed(() => {
|
||||
const map = {
|
||||
[ORIENTATION.LEFT]: `
|
||||
"bubble"
|
||||
"meta"
|
||||
"avatar bubble"
|
||||
"spacer meta"
|
||||
`,
|
||||
[ORIENTATION.RIGHT]: `
|
||||
"bubble avatar"
|
||||
"meta spacer"
|
||||
"bubble"
|
||||
"meta"
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -253,7 +251,7 @@ const shouldGroupWithNext = computed(() => {
|
||||
|
||||
const shouldShowAvatar = computed(() => {
|
||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
|
||||
if (orientation.value === ORIENTATION.LEFT) return false;
|
||||
if (orientation.value === ORIENTATION.RIGHT) return false;
|
||||
|
||||
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 = () => {
|
||||
if (Number(route.query.messageId) !== Number(props.id)) {
|
||||
return;
|
||||
@@ -440,7 +433,7 @@ provideMessageContext({
|
||||
isPrivate: computed(() => props.private),
|
||||
variant,
|
||||
orientation,
|
||||
isBotOrAgentMessage,
|
||||
isMyMessage,
|
||||
shouldGroupWithNext,
|
||||
});
|
||||
</script>
|
||||
@@ -450,7 +443,7 @@ provideMessageContext({
|
||||
<div
|
||||
v-if="shouldRenderMessage"
|
||||
: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"
|
||||
:class="[
|
||||
flexOrientationClass,
|
||||
@@ -479,7 +472,6 @@ provideMessageContext({
|
||||
>
|
||||
<div
|
||||
v-if="!shouldGroupWithNext && shouldShowAvatar"
|
||||
v-tooltip.left-end="avatarTooltip"
|
||||
class="[grid-area:avatar] flex items-end"
|
||||
>
|
||||
<Avatar v-bind="avatarInfo" :size="24" />
|
||||
@@ -487,13 +479,7 @@ provideMessageContext({
|
||||
<div
|
||||
class="[grid-area:bubble] flex"
|
||||
:class="{
|
||||
'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,
|
||||
'ltr:pl-9 rtl:pl-0 justify-end': orientation === ORIENTATION.RIGHT,
|
||||
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
||||
}"
|
||||
@contextmenu="openContextMenu($event)"
|
||||
|
||||
@@ -74,11 +74,7 @@ const shouldGroupWithNext = (index, searchList) => {
|
||||
nextMessageType === MESSAGE_TYPES.TEMPLATE &&
|
||||
currentMessageType === MESSAGE_TYPES.TEMPLATE;
|
||||
|
||||
const areBothActivity =
|
||||
nextMessageType === MESSAGE_TYPES.ACTIVITY &&
|
||||
currentMessageType === MESSAGE_TYPES.ACTIVITY;
|
||||
|
||||
if (!hasSameSender || areBothTemplates || areBothActivity) return false;
|
||||
if (!hasSameSender || areBothTemplates) return false;
|
||||
|
||||
if (currentMessageType !== nextMessageType) return false;
|
||||
|
||||
@@ -107,17 +103,6 @@ const getInReplyToMessage = parentMessage => {
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -131,7 +116,6 @@ const getMessageSpacingClass = (message, messages, index) => {
|
||||
:group-with-next="shouldGroupWithNext(index, read)"
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:current-user-id="currentUserId"
|
||||
:class="getMessageSpacingClass(message, read, index)"
|
||||
data-clarity-mask="True"
|
||||
/>
|
||||
</template>
|
||||
@@ -144,7 +128,6 @@ const getMessageSpacingClass = (message, messages, index) => {
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:current-user-id="currentUserId"
|
||||
:is-email-inbox="isAnEmailChannel"
|
||||
:class="getMessageSpacingClass(message, unread, index)"
|
||||
data-clarity-mask="True"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -14,7 +14,7 @@ const readableTime = computed(() =>
|
||||
<template>
|
||||
<BaseBubble
|
||||
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"
|
||||
>
|
||||
<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',
|
||||
[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-purple text-n-slate-12',
|
||||
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-purple text-n-slate-12',
|
||||
[MESSAGE_VARIANTS.ERROR]: 'bg-n-solid-red text-n-ruby-12',
|
||||
[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]: 'w-full',
|
||||
[MESSAGE_VARIANTS.UNSUPPORTED]:
|
||||
'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"
|
||||
rel="noreferrer noopener nofollow"
|
||||
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 }}
|
||||
</a>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{{ action.label }}
|
||||
|
||||
@@ -60,7 +60,7 @@ const textToShow = computed(() => {
|
||||
<EmailMeta
|
||||
class="p-3"
|
||||
:class="{
|
||||
'border-b border-n-slate-6': isIncoming,
|
||||
'border-b border-n-strong': isIncoming,
|
||||
'border-b border-n-slate-8/20': isOutgoing,
|
||||
}"
|
||||
/>
|
||||
@@ -73,7 +73,7 @@ const textToShow = computed(() => {
|
||||
>
|
||||
<div
|
||||
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
|
||||
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"
|
||||
class="px-4 pb-4 space-y-2"
|
||||
>
|
||||
<AttachmentChips
|
||||
:attachments="attachments"
|
||||
class="flex flex-col gap-2.5"
|
||||
/>
|
||||
<AttachmentChips :attachments="attachments" class="gap-1" />
|
||||
</section>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
|
||||
@@ -20,15 +20,12 @@ const isEmpty = computed(() => {
|
||||
|
||||
<template>
|
||||
<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">
|
||||
{{ $t('CONVERSATION.NO_CONTENT') }}
|
||||
</span>
|
||||
<FormattedContent v-if="content" :content="content" />
|
||||
<AttachmentChips
|
||||
:attachments="attachments"
|
||||
class="flex flex-col gap-2.5"
|
||||
/>
|
||||
<AttachmentChips :attachments="attachments" class="gap-2" />
|
||||
<template v-if="isTemplate">
|
||||
<div
|
||||
v-if="contentAttributes.submittedEmail"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
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 FileChip from 'next/message/chips/File.vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
@@ -36,7 +37,7 @@ const attrs = useAttrs();
|
||||
const { orientation } = useMessageContext();
|
||||
|
||||
const classToApply = computed(() => {
|
||||
const baseClasses = [attrs.class, 'flex', 'flex-wrap', 'gap-2'];
|
||||
const baseClasses = [attrs.class, 'flex', 'flex-wrap'];
|
||||
|
||||
if (orientation.value === 'right') {
|
||||
baseClasses.push('justify-end');
|
||||
@@ -76,25 +77,30 @@ const files = computed(() => {
|
||||
|
||||
<template>
|
||||
<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 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
|
||||
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">
|
||||
<div className="grid grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
<FileChip
|
||||
v-for="attachment in files"
|
||||
:key="attachment.id"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
</div>
|
||||
<FileChip
|
||||
v-for="attachment in files"
|
||||
:key="attachment.id"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
</div>
|
||||
</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 currentTime = ref(0);
|
||||
const duration = ref(0);
|
||||
const playbackSpeed = ref(1);
|
||||
|
||||
const onLoadedMetadata = () => {
|
||||
duration.value = audioPlayer.value?.duration;
|
||||
};
|
||||
|
||||
const playbackSpeedLabel = computed(() => {
|
||||
return `${playbackSpeed.value}x`;
|
||||
});
|
||||
|
||||
// There maybe a chance that the audioPlayer ref is not available
|
||||
// When the onLoadMetadata is called, so we need to set the duration
|
||||
// value when the component is mounted
|
||||
onMounted(() => {
|
||||
duration.value = audioPlayer.value?.duration;
|
||||
audioPlayer.value.playbackRate = playbackSpeed.value;
|
||||
});
|
||||
|
||||
const formatTime = time => {
|
||||
@@ -77,16 +71,6 @@ const playOrPause = () => {
|
||||
const onEnd = () => {
|
||||
isPlaying.value = false;
|
||||
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 () => {
|
||||
@@ -121,7 +105,7 @@ const downloadAudio = async () => {
|
||||
<div class="tabular-nums text-xs">
|
||||
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||
</div>
|
||||
<div class="flex-1 items-center flex px-2">
|
||||
<div class="flex items-center px-2">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
@@ -131,14 +115,6 @@ const downloadAudio = async () => {
|
||||
@input="seek"
|
||||
/>
|
||||
</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
|
||||
class="p-0 border-0 size-8 grid place-content-center"
|
||||
@click="toggleMute"
|
||||
|
||||
@@ -27,11 +27,6 @@ const fileType = computed(() => {
|
||||
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 colorMap = {
|
||||
'7z': 'dark:text-[#EDEEF0] text-[#2F265F]',
|
||||
@@ -58,17 +53,11 @@ const textColorClass = computed(() => {
|
||||
|
||||
<template>
|
||||
<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" />
|
||||
<span
|
||||
:class="textColorClass"
|
||||
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 class="mr-1 max-w-32 truncate" :class="textColorClass">
|
||||
{{ fileName }}
|
||||
</span>
|
||||
<a
|
||||
v-tooltip="t('CONVERSATION.DOWNLOAD')"
|
||||
|
||||
@@ -11,16 +11,7 @@ defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
remainingCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
shouldShowOverlay: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const hasError = ref(false);
|
||||
const showGallery = ref(false);
|
||||
|
||||
@@ -29,20 +20,12 @@ const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
const handleError = () => {
|
||||
hasError.value = true;
|
||||
};
|
||||
|
||||
const handleGalleryClick = () => {
|
||||
showGallery.value = true;
|
||||
};
|
||||
|
||||
const handleGalleryClose = () => {
|
||||
showGallery.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
|
||||
@click="handleGalleryClick"
|
||||
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer"
|
||||
@click="showGallery = true"
|
||||
>
|
||||
<div
|
||||
v-if="hasError"
|
||||
@@ -51,7 +34,6 @@ const handleGalleryClose = () => {
|
||||
<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"
|
||||
@@ -59,23 +41,12 @@ const handleGalleryClose = () => {
|
||||
@error="handleError"
|
||||
/>
|
||||
</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
|
||||
v-if="showGallery"
|
||||
v-model:show="showGallery"
|
||||
:attachment="useSnakeCase(attachment)"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
@error="handleError"
|
||||
@close="handleGalleryClose"
|
||||
@close="() => (showGallery = false)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -10,33 +10,17 @@ defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
remainingCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
shouldShowOverlay: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const showGallery = ref(false);
|
||||
|
||||
const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
|
||||
const handleGalleryClick = () => {
|
||||
showGallery.value = true;
|
||||
};
|
||||
|
||||
const handleGalleryClose = () => {
|
||||
showGallery.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
|
||||
@click="handleGalleryClick"
|
||||
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group"
|
||||
@click="showGallery = true"
|
||||
>
|
||||
<video
|
||||
:src="attachment.dataUrl"
|
||||
@@ -45,7 +29,6 @@ const handleGalleryClose = () => {
|
||||
playsInline
|
||||
/>
|
||||
<div
|
||||
v-if="!shouldShowOverlay"
|
||||
class="absolute w-full h-full inset-0 p-1 flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
@@ -58,21 +41,12 @@ const handleGalleryClose = () => {
|
||||
</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
|
||||
v-if="showGallery"
|
||||
v-model:show="showGallery"
|
||||
:attachment="useSnakeCase(attachment)"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
@close="handleGalleryClose"
|
||||
@error="onError"
|
||||
@close="() => (showGallery = false)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -98,7 +98,7 @@ const MessageControl = Symbol('MessageControl');
|
||||
* @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<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>} 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',
|
||||
},
|
||||
},
|
||||
'ul > li::marker': {
|
||||
color: 'rgb(var(--slate-9))',
|
||||
},
|
||||
'ol li': {
|
||||
margin: '0 0 0.5em 1em',
|
||||
listStyleType: 'decimal',
|
||||
|
||||
@@ -386,8 +386,6 @@ export const colors = {
|
||||
amber: 'rgb(var(--solid-amber) / <alpha-value>)',
|
||||
blue: 'rgb(var(--solid-blue) / <alpha-value>)',
|
||||
iris: 'rgb(var(--solid-iris) / <alpha-value>)',
|
||||
purple: 'rgb(var(--solid-purple) / <alpha-value>)',
|
||||
red: 'rgb(var(--solid-red) / <alpha-value>)',
|
||||
},
|
||||
alpha: {
|
||||
1: 'rgba(var(--alpha-1))',
|
||||
|
||||
Reference in New Issue
Block a user