'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; import { Loader2, Pause, Play } from 'lucide-react'; import { cn } from '@/lib/utils'; function seedWaveform(seed: string, count = 36) { let hash = 0; for (let index = 0; index < seed.length; index += 1) { hash = (hash * 31 + seed.charCodeAt(index)) | 0; } return Array.from({ length: count }, (_, index) => { const wave = Math.abs(Math.sin((hash + 1) * (index + 1) * 0.17)); return 0.25 + wave * 0.75; }); } function formatDuration(seconds: number) { if (!Number.isFinite(seconds) || seconds <= 0) return '0:00'; const whole = Math.floor(seconds); const mins = Math.floor(whole / 60); const secs = whole % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; } export function VoiceMessagePlayer({ src, seed, durationMs, variant = 'theirs' }: { src?: string; seed: string; durationMs?: number; variant?: 'mine' | 'theirs'; }) { const audioRef = useRef(null); const [playing, setPlaying] = useState(false); const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(0); const [duration, setDuration] = useState(durationMs ? durationMs / 1000 : 0); const bars = useMemo(() => seedWaveform(seed), [seed]); useEffect(() => { const audio = new Audio(); audio.preload = 'metadata'; audioRef.current = audio; const onLoaded = () => { if (Number.isFinite(audio.duration) && audio.duration > 0) { setDuration(audio.duration); } setLoading(false); }; const onTimeUpdate = () => { if (!audio.duration) return; setProgress(audio.currentTime / audio.duration); }; const onEnded = () => { setPlaying(false); setProgress(0); audio.currentTime = 0; }; const onWaiting = () => setLoading(true); const onCanPlay = () => setLoading(false); const onError = () => { setLoading(false); setPlaying(false); }; audio.addEventListener('loadedmetadata', onLoaded); audio.addEventListener('timeupdate', onTimeUpdate); audio.addEventListener('ended', onEnded); audio.addEventListener('waiting', onWaiting); audio.addEventListener('canplay', onCanPlay); audio.addEventListener('error', onError); return () => { audio.pause(); audio.removeEventListener('loadedmetadata', onLoaded); audio.removeEventListener('timeupdate', onTimeUpdate); audio.removeEventListener('ended', onEnded); audio.removeEventListener('waiting', onWaiting); audio.removeEventListener('canplay', onCanPlay); audio.removeEventListener('error', onError); audioRef.current = null; }; }, []); useEffect(() => { const audio = audioRef.current; if (!audio || !src) return; setLoading(true); audio.src = src; audio.load(); }, [src]); async function togglePlayback() { const audio = audioRef.current; if (!audio || !src) return; if (playing) { audio.pause(); setPlaying(false); return; } try { setLoading(true); await audio.play(); setPlaying(true); } catch { setPlaying(false); } finally { setLoading(false); } } function seekTo(ratio: number) { const audio = audioRef.current; if (!audio || !audio.duration) return; const next = Math.min(Math.max(ratio, 0), 1); audio.currentTime = next * audio.duration; setProgress(next); } const accent = variant === 'mine' ? 'bg-[#5bb85d]' : 'bg-[#3390ec]'; const accentSoft = variant === 'mine' ? 'bg-[#5bb85d]/25' : 'bg-[#3390ec]/25'; const remaining = duration * (1 - progress); return (
{formatDuration(playing || progress > 0 ? remaining : duration)}
); }