looood / src /components /media-preview-modal.tsx
looda3131's picture
Clean push without any binary history
cc276cc
"use client";
import { useState, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Textarea } from '@/components/ui/textarea';
import { Loader2, Send, X, Image as ImageIcon, Eye } from 'lucide-react';
import { useSettings } from '@/contexts/settings-context';
import type { ReplyTo } from '@/lib/types';
import { RadioGroup, RadioGroupItem } from './ui/radio-group';
import { Label } from './ui/label';
import { cn } from '@/lib/utils';
import Image from 'next/image';
import { Switch } from './ui/switch';
interface MediaPreviewModalProps {
isOpen: boolean;
onClose: () => void;
file: File;
originalSize: number;
onSend: (file: File, text?: string, replyTo?: ReplyTo, options?: { deleteAfterDelivery: boolean }) => Promise<void>;
}
function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
// Implemented a more aggressive image compression
async function compressImage(file: File): Promise<Blob | null> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target?.result as string;
img.onload = () => {
const canvas = document.createElement('canvas');
const MAX_WIDTH = 1024; // Reduced from 1280 for stronger compression
const MAX_HEIGHT = 1024; // Reduced from 1280
let width = img.width;
let height = img.height;
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx?.drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => resolve(blob), 'image/jpeg', 0.65); // Reduced quality from 0.7
};
};
});
}
export function MediaPreviewModal({ isOpen, onClose, file, originalSize, onSend }: MediaPreviewModalProps) {
const { addToast, t } = useSettings();
const [caption, setCaption] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [quality, setQuality] = useState<'standard' | 'hd'>('standard');
const [isViewOnce, setIsViewOnce] = useState(false);
const [compressedBlob, setCompressedBlob] = useState<Blob | null>(null);
const isImage = file.type.startsWith('image/');
const isVideo = file.type.startsWith('video/');
useEffect(() => {
if (file && isOpen) {
const url = URL.createObjectURL(file);
setPreviewUrl(url);
if (isImage) {
compressImage(file).then(blob => {
setCompressedBlob(blob);
});
}
return () => URL.revokeObjectURL(url);
}
}, [file, isOpen, isImage]);
const handleSend = async () => {
if (!file) return;
setIsLoading(true);
let fileToSend = file;
if (isImage && quality === 'standard' && compressedBlob) {
fileToSend = new File([compressedBlob], file.name, { type: 'image/jpeg' });
}
try {
await onSend(fileToSend, caption.trim(), undefined, { deleteAfterDelivery: isViewOnce });
addToast('Media sent!');
handleClose();
} catch (error) {
console.error("Failed to send media:", error);
addToast('Could not send the media. Please try again.', { variant: 'destructive' });
} finally {
setIsLoading(false);
}
};
const handleClose = () => {
setCaption('');
setIsLoading(false);
setCompressedBlob(null);
setQuality('standard');
setIsViewOnce(false);
onClose();
};
if (!isOpen || !file) return null;
const standardSize = isImage && compressedBlob ? compressedBlob.size : originalSize;
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent
className={cn(
"p-0 border-0 bg-black/80 max-w-none w-screen h-screen rounded-none flex flex-col items-center justify-center",
"data-[state=open]:animate-in data-[state=open]:fade-in-0",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0"
)}
>
<DialogHeader className="sr-only">
<DialogTitle>{t('sendMediaTitle')}</DialogTitle>
<DialogDescription>Preview your media before sending.</DialogDescription>
</DialogHeader>
<Button onClick={handleClose} variant="ghost" size="icon" className="absolute top-4 right-4 z-50 h-10 w-10 bg-black/30 hover:bg-black/50 text-white rounded-full">
<X className="h-6 w-6" />
</Button>
<div className="relative w-full h-full flex flex-col items-center justify-center scaleIn">
<div className="relative w-full h-full flex items-center justify-center p-8">
{previewUrl && (
isImage ? (
<Image
src={previewUrl}
alt={file.name}
fill
style={{objectFit:"contain"}}
className="rounded-lg shadow-2xl"
/>
) : (
<video src={previewUrl} controls autoPlay className="max-w-full max-h-full rounded-lg shadow-2xl" />
)
)}
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 via-black/50 to-transparent">
<div className="max-w-md mx-auto mb-2 p-1 rounded-lg bg-black/40 backdrop-blur-sm flex flex-col gap-2">
{isImage && (
<RadioGroup value={quality} onValueChange={(val) => setQuality(val as 'standard' | 'hd')} className="grid grid-cols-2 gap-1">
<Label htmlFor="q-standard" className={cn("text-center p-2 rounded-md cursor-pointer text-sm text-white", quality === 'standard' && 'bg-primary/80')}>
<RadioGroupItem value="standard" id="q-standard" className="sr-only"/>
Standard quality <span className="text-xs opacity-70">({formatBytes(standardSize)})</span>
</Label>
<Label htmlFor="q-hd" className={cn("text-center p-2 rounded-md cursor-pointer text-sm text-white", quality === 'hd' && 'bg-primary/80')}>
<RadioGroupItem value="hd" id="q-hd" className="sr-only"/>
HD quality <span className="text-xs opacity-70">({formatBytes(originalSize)})</span>
</Label>
</RadioGroup>
)}
{isVideo && (
<div className="text-center p-2 rounded-md text-sm text-white/80">
<p>Videos are sent in their original quality.</p>
<p className="text-xs opacity-70">({formatBytes(originalSize)})</p>
</div>
)}
<div className="flex items-center justify-center p-2 rounded-md">
<Label htmlFor="view-once-switch" className="text-white text-sm flex items-center gap-2 cursor-pointer">
<Eye className="h-4 w-4" />
<span>View Once</span>
</Label>
<Switch id="view-once-switch" checked={isViewOnce} onCheckedChange={setIsViewOnce} className="ml-auto" />
</div>
</div>
<div className="flex items-center gap-2 max-w-2xl mx-auto">
<Textarea
placeholder={t('addCaption')}
value={caption}
onChange={(e) => setCaption(e.target.value)}
className="bg-black/40 border-white/20 text-white placeholder:text-gray-300 focus-visible:ring-1 focus-visible:ring-primary ring-offset-0 resize-none"
rows={1}
/>
<Button onClick={handleSend} disabled={isLoading} size="icon" className="w-12 h-12 rounded-full bg-primary flex-shrink-0">
{isLoading ? <Loader2 className="animate-spin" /> : <Send />}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}