chore: Next bubble improvements (#10759)
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, defineOptions, useAttrs } from 'vue';
|
||||
|
||||
import ImageChip from 'next/message/chips/Image.vue';
|
||||
import VideoChip from 'next/message/chips/Video.vue';
|
||||
import ImageVideoChip from 'next/message/chips/AttachmentGrid.vue'; // Image and Video Grids are the same component
|
||||
import AudioChip from 'next/message/chips/Audio.vue';
|
||||
import FileChip from 'next/message/chips/File.vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
@@ -37,7 +36,7 @@ const attrs = useAttrs();
|
||||
const { orientation } = useMessageContext();
|
||||
|
||||
const classToApply = computed(() => {
|
||||
const baseClasses = [attrs.class, 'flex', 'flex-wrap'];
|
||||
const baseClasses = [attrs.class, 'flex', 'flex-wrap', 'gap-2'];
|
||||
|
||||
if (orientation.value === 'right') {
|
||||
baseClasses.push('justify-end');
|
||||
@@ -77,30 +76,25 @@ const files = computed(() => {
|
||||
|
||||
<template>
|
||||
<div v-if="mediaAttachments.length" :class="classToApply">
|
||||
<template v-for="attachment in mediaAttachments" :key="attachment.id">
|
||||
<ImageChip
|
||||
v-if="attachment.fileType === ATTACHMENT_TYPES.IMAGE"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
<VideoChip
|
||||
v-else-if="attachment.fileType === ATTACHMENT_TYPES.VIDEO"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
</template>
|
||||
<ImageVideoChip :attachments="mediaAttachments" />
|
||||
</div>
|
||||
|
||||
<div v-if="recordings.length" :class="classToApply">
|
||||
<div v-for="attachment in recordings" :key="attachment.id">
|
||||
<div v-for="attachment in recordings" :key="attachment.id" class="w-full">
|
||||
<AudioChip
|
||||
class="bg-n-alpha-3 dark:bg-n-alpha-2 text-n-slate-12"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="files.length" :class="classToApply">
|
||||
<FileChip
|
||||
v-for="attachment in files"
|
||||
:key="attachment.id"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
<div className="grid grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
<FileChip
|
||||
v-for="attachment in files"
|
||||
:key="attachment.id"
|
||||
:attachment="attachment"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import ImageChip from './Image.vue';
|
||||
import VideoChip from './Video.vue';
|
||||
|
||||
import { ATTACHMENT_TYPES } from '../constants';
|
||||
|
||||
const props = defineProps({
|
||||
attachments: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: value => Array.isArray(value) && value.length > 0,
|
||||
},
|
||||
});
|
||||
|
||||
const MAX_DISPLAYED = 5;
|
||||
|
||||
const visibleAttachments = computed(() =>
|
||||
props.attachments.slice(0, MAX_DISPLAYED)
|
||||
);
|
||||
|
||||
const remainingCount = computed(() =>
|
||||
Math.max(0, props.attachments.length - MAX_DISPLAYED)
|
||||
);
|
||||
|
||||
const gridClass = computed(() => {
|
||||
const count = props.attachments.length;
|
||||
const base = 'grid gap-2 w-full';
|
||||
|
||||
if (count === 1) return `${base} grid-cols-1`;
|
||||
|
||||
const classes = {
|
||||
2: `${base} grid-cols-2 max-h-[500px]`,
|
||||
3: `${base} grid-cols-2 max-h-[500px]`,
|
||||
4: `${base} grid-cols-2 max-h-[500px]`,
|
||||
5: `${base} grid-cols-3 max-h-[500px]`,
|
||||
};
|
||||
|
||||
return classes[count] || classes[5];
|
||||
});
|
||||
|
||||
const itemClass = computed(() => index => {
|
||||
const count = props.attachments.length;
|
||||
const base = 'relative overflow-hidden rounded-lg';
|
||||
|
||||
if (count === 1) return `${base} w-full h-auto`;
|
||||
|
||||
if (count === 3 && index === 0) return `${base} row-span-2 h-[492px]`;
|
||||
if (count >= 5 && index === 0) return `${base} col-span-2 h-[242px]`;
|
||||
|
||||
return count === 2 ? `${base} h-[500px]` : `${base} h-[242px]`;
|
||||
});
|
||||
|
||||
const shouldShowOverlay = computed(
|
||||
() => index => remainingCount.value > 0 && index === MAX_DISPLAYED - 1
|
||||
);
|
||||
|
||||
const componentMap = {
|
||||
[ATTACHMENT_TYPES.IMAGE]: ImageChip,
|
||||
[ATTACHMENT_TYPES.VIDEO]: VideoChip,
|
||||
};
|
||||
|
||||
const getComponent = fileType =>
|
||||
componentMap[fileType?.toLowerCase()] || componentMap[ATTACHMENT_TYPES.IMAGE];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="gridClass">
|
||||
<div
|
||||
v-for="(attachment, index) in visibleAttachments"
|
||||
:key="attachment.id"
|
||||
:class="itemClass(index)"
|
||||
>
|
||||
<component
|
||||
:is="getComponent(attachment.fileType)"
|
||||
:attachment="attachment"
|
||||
:remaining-count="remainingCount"
|
||||
:should-show-overlay="shouldShowOverlay(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,16 +25,22 @@ 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 => {
|
||||
@@ -71,6 +77,16 @@ 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 () => {
|
||||
@@ -105,7 +121,7 @@ const downloadAudio = async () => {
|
||||
<div class="tabular-nums text-xs">
|
||||
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||
</div>
|
||||
<div class="flex items-center px-2">
|
||||
<div class="flex-1 items-center flex px-2">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
@@ -115,6 +131,14 @@ 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,6 +27,11 @@ 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]',
|
||||
@@ -53,11 +58,17 @@ const textColorClass = computed(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-9 bg-n-alpha-white gap-2 items-center flex px-2 rounded-lg border border-n-container"
|
||||
class="h-9 bg-n-alpha-white gap-2 overflow-hidden items-center flex px-2 rounded-lg border border-n-container"
|
||||
>
|
||||
<FileIcon class="flex-shrink-0" :file-type="fileType" />
|
||||
<span class="mr-1 max-w-32 truncate" :class="textColorClass">
|
||||
{{ fileName }}
|
||||
<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>
|
||||
<a
|
||||
v-tooltip="t('CONVERSATION.DOWNLOAD')"
|
||||
|
||||
@@ -11,7 +11,16 @@ defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
remainingCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
shouldShowOverlay: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const hasError = ref(false);
|
||||
const showGallery = ref(false);
|
||||
|
||||
@@ -20,12 +29,20 @@ const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
const handleError = () => {
|
||||
hasError.value = true;
|
||||
};
|
||||
|
||||
const handleGalleryClick = () => {
|
||||
showGallery.value = true;
|
||||
};
|
||||
|
||||
const handleGalleryClose = () => {
|
||||
showGallery.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer"
|
||||
@click="showGallery = true"
|
||||
class="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
|
||||
@click="handleGalleryClick"
|
||||
>
|
||||
<div
|
||||
v-if="hasError"
|
||||
@@ -34,6 +51,7 @@ const handleError = () => {
|
||||
<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"
|
||||
@@ -41,12 +59,23 @@ const handleError = () => {
|
||||
@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="() => (showGallery = false)"
|
||||
@close="handleGalleryClose"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -10,17 +10,33 @@ 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="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group"
|
||||
@click="showGallery = true"
|
||||
class="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
|
||||
@click="handleGalleryClick"
|
||||
>
|
||||
<video
|
||||
:src="attachment.dataUrl"
|
||||
@@ -29,6 +45,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
playsInline
|
||||
/>
|
||||
<div
|
||||
v-if="!shouldShowOverlay"
|
||||
class="absolute w-full h-full inset-0 p-1 flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
@@ -41,12 +58,21 @@ const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
</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"
|
||||
@error="onError"
|
||||
@close="() => (showGallery = false)"
|
||||
@close="handleGalleryClose"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user