fix
This commit is contained in:
@@ -6,13 +6,14 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { createFilePreviewUrl, isCompressibleImageFile, readImageDimensions } from '@/lib/chat-image-compress';
|
import { createFilePreviewUrl, isCompressibleImageFile, readImageDimensions } from '@/lib/chat-image-compress';
|
||||||
import { formatFileSize } from '@/lib/chat-media';
|
import { formatFileSize, getComposeDialogTitle, getComposeMediaKind, type ComposeMediaKind } from '@/lib/chat-media';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export interface ChatMediaComposeItem {
|
export interface ChatMediaComposeItem {
|
||||||
id: string;
|
id: string;
|
||||||
file: File;
|
file: File;
|
||||||
previewUrl: string | null;
|
previewUrl: string | null;
|
||||||
|
mediaKind: ComposeMediaKind;
|
||||||
sendAsFile: boolean;
|
sendAsFile: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
@@ -25,6 +26,7 @@ function createComposeItem(file: File, sendAsFile = false): ChatMediaComposeItem
|
|||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
file,
|
file,
|
||||||
previewUrl: createFilePreviewUrl(file),
|
previewUrl: createFilePreviewUrl(file),
|
||||||
|
mediaKind: getComposeMediaKind(file),
|
||||||
sendAsFile
|
sendAsFile
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -159,32 +161,21 @@ export function ChatMediaComposeDialog({
|
|||||||
}: ChatMediaComposeDialogProps) {
|
}: ChatMediaComposeDialogProps) {
|
||||||
const addInputRef = useRef<HTMLInputElement>(null);
|
const addInputRef = useRef<HTMLInputElement>(null);
|
||||||
const activePreview = items[0] ?? null;
|
const activePreview = items[0] ?? null;
|
||||||
|
const dialogTitle = getComposeDialogTitle(items.map((item) => item.file));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(next) => !next && onClose()}>
|
<Dialog open={open} onOpenChange={(next) => !next && onClose()}>
|
||||||
<DialogContent className="overflow-hidden rounded-[20px] p-0 sm:max-w-[460px]">
|
<DialogContent className="overflow-hidden rounded-[20px] p-0 sm:max-w-[460px]">
|
||||||
<div className="border-b border-[#eceef4] px-5 py-4">
|
<div className="border-b border-[#eceef4] px-5 py-4">
|
||||||
<DialogHeader className="mb-0">
|
<DialogHeader className="mb-0">
|
||||||
<DialogTitle>{items.length > 1 ? `Отправить ${items.length} файла` : 'Отправить изображение'}</DialogTitle>
|
<DialogTitle>{dialogTitle}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 px-5 py-4">
|
<div className="space-y-4 px-5 py-4">
|
||||||
{activePreview ? (
|
{activePreview ? (
|
||||||
<div className="relative overflow-hidden rounded-2xl bg-[#0f1117]">
|
<div className="relative overflow-hidden rounded-2xl bg-[#0f1117]">
|
||||||
{activePreview.previewUrl ? (
|
<ComposeMediaPreview item={activePreview} className="mx-auto max-h-[320px] w-full object-contain" />
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
src={activePreview.previewUrl}
|
|
||||||
alt={activePreview.file.name}
|
|
||||||
className="mx-auto max-h-[320px] w-full object-contain"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex min-h-[180px] flex-col items-center justify-center gap-2 px-6 text-center text-white/80">
|
|
||||||
<p className="text-sm font-medium">{activePreview.file.name}</p>
|
|
||||||
<p className="text-xs text-white/55">{formatFileSize(activePreview.file.size)}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute right-3 top-3 rounded-full bg-black/55 p-1.5 text-white transition hover:bg-black/70"
|
className="absolute right-3 top-3 rounded-full bg-black/55 p-1.5 text-white transition hover:bg-black/70"
|
||||||
@@ -206,18 +197,15 @@ export function ChatMediaComposeDialog({
|
|||||||
onClick={() => onRemoveItem(item.id)}
|
onClick={() => onRemoveItem(item.id)}
|
||||||
title="Нажмите, чтобы удалить"
|
title="Нажмите, чтобы удалить"
|
||||||
>
|
>
|
||||||
{item.previewUrl ? (
|
<ComposeMediaPreview item={item} className="h-full w-full object-cover" thumbnail />
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img src={item.previewUrl} alt="" className="h-full w-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full items-center justify-center px-1 text-[10px] text-[#667085]">{item.file.name.slice(0, 8)}</div>
|
|
||||||
)}
|
|
||||||
<span className="absolute inset-0 bg-black/0 transition group-hover:bg-black/20" />
|
<span className="absolute inset-0 bg-black/0 transition group-hover:bg-black/20" />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : activePreview?.previewUrl ? (
|
) : activePreview?.mediaKind === 'image' && activePreview.previewUrl ? (
|
||||||
<p className="text-center text-xs text-[#667085]">Для редактирования нажмите на изображение</p>
|
<p className="text-center text-xs text-[#667085]">Для редактирования нажмите на изображение</p>
|
||||||
|
) : activePreview?.mediaKind === 'video' ? (
|
||||||
|
<p className="text-center text-xs text-[#667085]">Проверьте видео перед отправкой</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{hasImages ? (
|
{hasImages ? (
|
||||||
@@ -314,6 +302,43 @@ export function ChatMediaDropOverlay({ visible, onDropCompressed, onDropAsFile }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ComposeMediaPreview({
|
||||||
|
item,
|
||||||
|
className,
|
||||||
|
thumbnail = false
|
||||||
|
}: {
|
||||||
|
item: ChatMediaComposeItem;
|
||||||
|
className?: string;
|
||||||
|
thumbnail?: boolean;
|
||||||
|
}) {
|
||||||
|
if (item.mediaKind === 'video' && item.previewUrl) {
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
src={item.previewUrl}
|
||||||
|
className={cn('bg-black', className)}
|
||||||
|
controls={!thumbnail}
|
||||||
|
playsInline
|
||||||
|
muted={thumbnail}
|
||||||
|
preload="metadata"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.mediaKind === 'image' && item.previewUrl) {
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img src={item.previewUrl} alt={item.file.name} className={className} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[180px] flex-col items-center justify-center gap-2 px-6 text-center text-white/80">
|
||||||
|
<p className="text-sm font-medium">{item.file.name}</p>
|
||||||
|
<p className="text-xs text-white/55">{formatFileSize(item.file.size)}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DropZone({
|
function DropZone({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { resolveChatContentType } from '@/lib/chat-media';
|
||||||
|
|
||||||
const DEFAULT_MAX_EDGE = 2560;
|
const DEFAULT_MAX_EDGE = 2560;
|
||||||
const DEFAULT_QUALITY = 0.82;
|
const DEFAULT_QUALITY = 0.82;
|
||||||
|
|
||||||
@@ -107,7 +109,8 @@ export async function readImageDimensions(file: File) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createFilePreviewUrl(file: File) {
|
export function createFilePreviewUrl(file: File) {
|
||||||
if (file.type.startsWith('image/')) {
|
const contentType = resolveChatContentType(file);
|
||||||
|
if (contentType.startsWith('image/') || contentType.startsWith('video/')) {
|
||||||
return URL.createObjectURL(file);
|
return URL.createObjectURL(file);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -134,6 +134,32 @@ export function isVideoFile(file: Pick<File, 'type' | 'name'>) {
|
|||||||
return resolveChatContentType(file).startsWith('video/');
|
return resolveChatContentType(file).startsWith('video/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ComposeMediaKind = 'image' | 'video' | 'audio' | 'file';
|
||||||
|
|
||||||
|
export function getComposeMediaKind(file: Pick<File, 'type' | 'name'>): ComposeMediaKind {
|
||||||
|
const contentType = resolveChatContentType(file);
|
||||||
|
if (contentType.startsWith('image/')) return 'image';
|
||||||
|
if (contentType.startsWith('video/')) return 'video';
|
||||||
|
if (contentType.startsWith('audio/')) return 'audio';
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComposeDialogTitle(files: Array<Pick<File, 'type' | 'name'>>) {
|
||||||
|
if (!files.length) return 'Отправить файл';
|
||||||
|
if (files.length > 1) return `Отправить ${files.length} файла`;
|
||||||
|
|
||||||
|
switch (getComposeMediaKind(files[0]!)) {
|
||||||
|
case 'video':
|
||||||
|
return 'Отправить видео';
|
||||||
|
case 'audio':
|
||||||
|
return 'Отправить аудио';
|
||||||
|
case 'image':
|
||||||
|
return 'Отправить изображение';
|
||||||
|
default:
|
||||||
|
return 'Отправить файл';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function formatFileSize(bytes?: number) {
|
export function formatFileSize(bytes?: number) {
|
||||||
if (!bytes || bytes <= 0) return '';
|
if (!bytes || bytes <= 0) return '';
|
||||||
if (bytes < 1024) return `${bytes} Б`;
|
if (bytes < 1024) return `${bytes} Б`;
|
||||||
|
|||||||
Reference in New Issue
Block a user