| // src/components/async/AsyncVideoThread.tsx | |
| import { useState, useRef } from "react"; | |
| import { VideoNoteRecorder } from "@/components/social/VideoNoteRecorder"; | |
| import { VideoNoteBubble } from "@/components/messages/VideoNoteBubble"; | |
| import { MessageCircle, Play, Clock, ChevronDown, Reply, Send, User } from "lucide-react"; | |
| interface VideoThreadMessage { | |
| id: string; | |
| type: "video_note" | "audio_note" | "text"; | |
| url?: string; | |
| duration?: number; | |
| body?: string; | |
| author: string; | |
| timestamp: string; | |
| replies: { id: string; body: string; author: string; timestamp: string }[]; | |
| } | |
| interface AsyncVideoThreadProps { | |
| threadId: string; | |
| title: string; | |
| messages: VideoThreadMessage[]; | |
| onAddVideoNote: (blob: Blob, duration: number, mode: "video" | "audio") => void; | |
| onAddTextReply: (threadId: string, body: string, parentMessageId?: string) => void; | |
| } | |
| export function AsyncVideoThread({ threadId, title, messages, onAddVideoNote, onAddTextReply }: AsyncVideoThreadProps) { | |
| const [showRecorder, setShowRecorder] = useState(false); | |
| const [expandedMessageId, setExpandedMessageId] = useState<string | null>(null); | |
| const [replyInput, setReplyInput] = useState(""); | |
| const [replyingToId, setReplyingToId] = useState<string | null>(null); | |
| return ( | |
| <div className="glass rounded-2xl border border-white/20 dark:border-white/10 overflow-hidden flex flex-col max-h-[80vh]"> | |
| {/* Header */} | |
| <div className="px-5 py-4 border-b border-white/10 flex items-center justify-between bg-linear-to-r from-blue-500/5 to-purple-500/5"> | |
| <div> | |
| <h3 className="font-bold flex items-center gap-2"> | |
| <Play className="w-5 h-5 text-blue-500" /> Async Video Thread | |
| </h3> | |
| <p className="text-xs text-gray-500 mt-0.5">{title}</p> | |
| </div> | |
| <button onClick={() => setShowRecorder(!showRecorder)} className="btn btn-primary btn-sm gap-1.5"> | |
| <Play className="w-3.5 h-3.5" /> Record Note | |
| </button> | |
| </div> | |
| {/* Recorder */} | |
| {showRecorder && ( | |
| <div className="p-4 border-b border-white/10"> | |
| <VideoNoteRecorder | |
| onSend={(blob, duration, mode) => { | |
| onAddVideoNote(blob, duration, mode); | |
| setShowRecorder(false); | |
| }} | |
| onCancel={() => setShowRecorder(false)} | |
| /> | |
| </div> | |
| )} | |
| {/* Messages */} | |
| <div className="flex-1 overflow-y-auto p-4 space-y-4"> | |
| {messages.map(msg => ( | |
| <div key={msg.id}> | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2 text-xs text-gray-400"> | |
| <User className="w-3 h-3" /> {msg.author} | |
| <Clock className="w-3 h-3" /> {new Date(msg.timestamp).toLocaleString()} | |
| </div> | |
| {msg.type === "video_note" || msg.type === "audio_note" ? ( | |
| <VideoNoteBubble | |
| url={msg.url!} | |
| duration={msg.duration!} | |
| mode={msg.type === "video_note" ? "video" : "audio"} | |
| isOwn={false} | |
| timestamp={msg.timestamp} | |
| /> | |
| ) : ( | |
| <div className="bg-white/30 dark:bg-white/10 rounded-2xl px-4 py-2.5 text-sm"> | |
| {msg.body} | |
| </div> | |
| )} | |
| {/* Replies */} | |
| {msg.replies.length > 0 && ( | |
| <div className="ml-8 space-y-2"> | |
| {msg.replies.map(reply => ( | |
| <div key={reply.id} className="flex items-start gap-2 text-sm"> | |
| <span className="text-xs font-semibold text-gray-500 mt-0.5">{reply.author}:</span> | |
| <span>{reply.body}</span> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {/* Reply button */} | |
| <button | |
| onClick={() => { setReplyingToId(replyingToId === msg.id ? null : msg.id); setReplyInput(""); }} | |
| className="text-xs text-primary hover:underline flex items-center gap-1" | |
| > | |
| <Reply className="w-3 h-3" /> Reply | |
| </button> | |
| {/* Reply input */} | |
| {replyingToId === msg.id && ( | |
| <div className="ml-4 flex gap-2 mt-2"> | |
| <input | |
| type="text" | |
| value={replyInput} | |
| onChange={e => setReplyInput(e.target.value)} | |
| placeholder="Write a reply..." | |
| className="flex-1 text-xs px-3 py-1.5 border rounded-lg dark:bg-gray-700" | |
| autoFocus | |
| onKeyDown={e => { if (e.key === "Enter" && replyInput.trim()) { onAddTextReply(threadId, replyInput.trim(), msg.id); setReplyInput(""); setReplyingToId(null); } }} | |
| /> | |
| <button | |
| onClick={() => { onAddTextReply(threadId, replyInput.trim(), msg.id); setReplyInput(""); setReplyingToId(null); }} | |
| disabled={!replyInput.trim()} | |
| className="btn btn-primary btn-xs" | |
| > | |
| <Send className="w-3 h-3" /> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 5.34 kB
- Xet hash:
- f6267f931aeb468334fea9b19b05e5a3d4cbcae07eb7a7c7431963c174f7e5fa
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.