| // src/components/messages/VideoNoteBubble.tsx | |
| import { useState, useRef } from "react"; | |
| import { Play, Pause, Download, Maximize2 } from "lucide-react"; | |
| interface VideoNoteBubbleProps { | |
| url: string; | |
| duration: number; // seconds | |
| mode: "video" | "audio"; | |
| isOwn: boolean; | |
| timestamp: string; | |
| } | |
| export function VideoNoteBubble({ url, duration, mode, isOwn, timestamp }: VideoNoteBubbleProps) { | |
| const [playing, setPlaying] = useState(false); | |
| const [progress, setProgress] = useState(0); | |
| const mediaRef = useRef<HTMLVideoElement | HTMLAudioElement>(null); | |
| const togglePlay = () => { | |
| const el = mediaRef.current; | |
| if (!el) return; | |
| if (playing) { | |
| el.pause(); | |
| setPlaying(false); | |
| } else { | |
| el.play(); | |
| setPlaying(true); | |
| } | |
| }; | |
| const handleTimeUpdate = () => { | |
| if (mediaRef.current && mediaRef.current.duration) { | |
| const pct = (mediaRef.current.currentTime / mediaRef.current.duration) * 100; | |
| setProgress(pct); | |
| } | |
| }; | |
| const handleEnded = () => { | |
| setPlaying(false); | |
| setProgress(0); | |
| if (mediaRef.current) { | |
| mediaRef.current.currentTime = 0; | |
| } | |
| }; | |
| const formatDuration = (sec: number) => { | |
| const m = Math.floor(sec / 60); | |
| const s = Math.floor(sec % 60); | |
| return `${m}:${s.toString().padStart(2, "0")}`; | |
| }; | |
| const containerClasses = `relative group max-w-[280px] ${ | |
| isOwn ? "ml-auto" : "mr-auto" | |
| }`; | |
| return ( | |
| <div className={containerClasses}> | |
| <div | |
| className={`rounded-2xl overflow-hidden ${ | |
| isOwn | |
| ? "bg-primary text-white" | |
| : "bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200" | |
| }`} | |
| > | |
| {mode === "video" ? ( | |
| <video | |
| ref={mediaRef as any} | |
| src={url} | |
| className="w-full rounded-t-2xl cursor-pointer" | |
| onClick={togglePlay} | |
| onTimeUpdate={handleTimeUpdate} | |
| onPlay={() => setPlaying(true)} | |
| onPause={() => setPlaying(false)} | |
| onEnded={handleEnded} | |
| playsInline | |
| preload="metadata" | |
| /> | |
| ) : ( | |
| <div className="p-3 flex items-center gap-3"> | |
| <button | |
| onClick={togglePlay} | |
| className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center" | |
| > | |
| {playing ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />} | |
| </button> | |
| <div className="flex-1 min-w-0"> | |
| <div className="h-1.5 bg-white/30 rounded-full overflow-hidden"> | |
| <div | |
| className="h-full bg-white/80 rounded-full transition-all" | |
| style={{ width: `${progress}%` }} | |
| /> | |
| </div> | |
| <span className="text-xs mt-0.5">{formatDuration(duration)}</span> | |
| </div> | |
| </div> | |
| )} | |
| {/* Audio element hidden for audio notes */} | |
| {mode === "audio" && ( | |
| <audio | |
| ref={mediaRef as any} | |
| src={url} | |
| onTimeUpdate={handleTimeUpdate} | |
| onPlay={() => setPlaying(true)} | |
| onPause={() => setPlaying(false)} | |
| onEnded={handleEnded} | |
| preload="metadata" | |
| className="hidden" | |
| /> | |
| )} | |
| {/* Controls bar */} | |
| <div className="px-3 py-1.5 flex items-center justify-between text-xs"> | |
| <span> | |
| {new Date(timestamp).toLocaleTimeString([], { | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| })} | |
| </span> | |
| <a | |
| href={url} | |
| download | |
| className="hover:underline flex items-center gap-1 opacity-70 hover:opacity-100" | |
| > | |
| <Download className="w-3 h-3" /> | |
| </a> | |
| </div> | |
| {/* Play/pause overlay for video */} | |
| {mode === "video" && !playing && ( | |
| <button | |
| onClick={togglePlay} | |
| className="absolute inset-0 flex items-center justify-center bg-black/20 opacity-0 group-hover:opacity-100 transition" | |
| > | |
| <Play className="w-10 h-10 text-white" /> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 4.23 kB
- Xet hash:
- 1beacfbd5c62a818828fcc60eca945355fd9b97357c9c640af91c4f8ab70e83b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.