revert: Next bubble improvements (#10795)

This commit is contained in:
Shivam Mishra
2025-01-30 09:34:45 +05:30
committed by GitHub
parent 2c1a8e59f5
commit d997734837
18 changed files with 71 additions and 283 deletions

View File

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

View File

@@ -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'],
], ],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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))',