'use client'; import { memo, useState, type ReactNode, type RefObject } from 'react'; import { ArrowUpRight, CheckCircle2, ChevronDown, Circle, Clock, Download, FileText, FolderOpen, Image as ImageIcon, Loader2, MessageSquare, Mic, MoreVertical, Pause, Pencil, Play, Reply, Tag, Trash2, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { cn } from '@/lib/utils'; import { MentionTextarea } from '@/components/video-page/mention-textarea'; import { CommentRichText } from '@/components/video-page/comment-rich-text'; import type { Comment, CommentTag, Version, VideoAsset } from '@/components/video-page/types'; interface CommentsPaneProps { isMobileCommentsOpen: boolean; setIsMobileCommentsOpen: (open: boolean) => void; isFullscreenMode: boolean; showComments: boolean; comments: Comment[]; filteredComments: Comment[]; sortedComments: Comment[]; showResolved: boolean; handleToggleShowResolved: () => void; activeVersion: Version | undefined; isGuest: boolean; isExportingCsv: boolean; isExportingPdf: boolean; handleExportComments: (format: 'csv' | 'pdf') => void; canResolveComments: boolean; handleResolveComment: (commentId: string, currentlyResolved: boolean) => void; handleSeekToTimestamp: ( timestamp: number, annotation?: string | null, options?: { pauseAfterSeek?: boolean } ) => void; currentUserId: string | null; projectOwnerId: string; editingCommentId: string | null; setEditingCommentId: (id: string | null) => void; editText: string; setEditText: (value: string) => void; editTagId: string | null; setEditTagId: (value: string | null) => void; setEditAnnotationData: (value: string | null | undefined) => void; setIsEditingAnnotation: (value: boolean) => void; onStartEditAnnotation: () => void; isSubmittingEdit: boolean; availableTags: CommentTag[]; handleEditComment: (commentId: string) => void; handleDeleteComment: (commentId: string) => void; playVoice: (commentId: string, voiceUrl: string, knownDuration?: number) => void; playingVoiceId: string | null; voiceProgress: number; voiceCurrentTime: number; voicePlaybackRate: number; toggleVoiceSpeed: () => void; formatTime: (seconds: number) => string; setPreviewImage: (url: string | null) => void; replyingTo: string | null; setReplyingTo: (id: string | null) => void; replyText: string; setReplyText: (value: string) => void; handleReplyComment: (parentId: string, voiceData?: { url: string; duration: number }, imageData?: { url: string }) => void; startReplyRecording: () => void; isReplyRecording: boolean; replyRecordingTime: number; stopReplyRecording: () => void; cancelReplyRecording: () => void; replyAudioBlob: Blob | null; replyImageBlob: File | null; setReplyImageBlob: (file: File | null) => void; replyImageInputRef: RefObject; handleImageSelect: (e: React.ChangeEvent, isReply?: boolean) => void; handlePaste: (e: React.ClipboardEvent, isReply?: boolean) => void; handleDrop: (e: React.DragEvent, isReply?: boolean) => void; submitReplyWithMedia: (parentId: string) => void; isSubmittingReply: boolean; isUploadingReplyAudio: boolean; isUploadingReplyImage: boolean; composer: ReactNode; assets: VideoAsset[]; onAssetMentionClick: (assetId: string) => void; activePane: 'comments' | 'assets'; setActivePane: (pane: 'comments' | 'assets') => void; assetsPane: ReactNode; } export const CommentsPane = memo(function CommentsPane({ isMobileCommentsOpen, setIsMobileCommentsOpen, isFullscreenMode, showComments, comments, filteredComments, sortedComments, showResolved, handleToggleShowResolved, activeVersion, isGuest, isExportingCsv, isExportingPdf, handleExportComments, canResolveComments, handleResolveComment, handleSeekToTimestamp, currentUserId, projectOwnerId, editingCommentId, setEditingCommentId, editText, setEditText, editTagId, setEditTagId, setEditAnnotationData, setIsEditingAnnotation, onStartEditAnnotation, isSubmittingEdit, availableTags, handleEditComment, handleDeleteComment, playVoice, playingVoiceId, voiceProgress, voiceCurrentTime, voicePlaybackRate, toggleVoiceSpeed, formatTime, setPreviewImage, replyingTo, setReplyingTo, replyText, setReplyText, handleReplyComment, startReplyRecording, isReplyRecording, replyRecordingTime, stopReplyRecording, cancelReplyRecording, replyAudioBlob, replyImageBlob, setReplyImageBlob, replyImageInputRef, handleImageSelect, handlePaste, handleDrop, submitReplyWithMedia, isSubmittingReply, isUploadingReplyAudio, isUploadingReplyImage, composer, assets, onAssetMentionClick, activePane, setActivePane, assetsPane, }: CommentsPaneProps) { const [isPaneDraggingOver, setIsPaneDraggingOver] = useState(false); return ( <>
setIsMobileCommentsOpen(false)} />
{ if (activePane !== 'comments') return; e.preventDefault(); setIsPaneDraggingOver(true); }} onDragEnter={(e) => { if (activePane !== 'comments') return; e.preventDefault(); setIsPaneDraggingOver(true); }} onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsPaneDraggingOver(false); }} onDrop={(e) => { setIsPaneDraggingOver(false); if (activePane !== 'comments') return; handleDrop(e, replyingTo !== null); }} > {isPaneDraggingOver && (

Drop image to attach

)}
{activePane === 'comments' && (
{ e.stopPropagation(); handleExportComments('csv'); }} title={isGuest ? 'CSV export requires an authenticated account' : 'Download comments as CSV'} > Download CSV { e.stopPropagation(); handleExportComments('pdf'); }} title="Download comments as PDF" > Download PDF
)}
{assetsPane}
{filteredComments.length === 0 ? (

No comments yet

Be the first to leave feedback!

) : ( sortedComments.map((comment) => { const authorName = comment.author?.name || comment.guestName || 'Anonymous'; const isEditing = editingCommentId === comment.id; const isReplying = replyingTo === comment.id; const canEditComment = comment.canEdit ?? (comment.author?.id === currentUserId); const canDeleteComment = comment.canDelete ?? (comment.author?.id === currentUserId || projectOwnerId === currentUserId); const canManageComment = canEditComment || canDeleteComment; return (
{authorName.charAt(0)} {authorName}
{canResolveComments && ( )} {canManageComment && ( { setReplyingTo(comment.id); setReplyText(''); }}> Reply {canEditComment && ( { setEditingCommentId(comment.id); setEditText(comment.content || ''); setEditTagId(comment.tag?.id || null); }}> Edit )} {canDeleteComment && ( handleDeleteComment(comment.id)} > Delete )} )}
{isEditing ? (
{ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { handleEditComment(comment.id); } if (e.key === 'Escape') { setEditingCommentId(null); setEditText(''); setEditTagId(null); setEditAnnotationData(undefined); setIsEditingAnnotation(false); } }} />
{availableTags.length > 0 && ( setEditTagId(null)} className="gap-2"> No Tag {!editTagId && } {availableTags.map((tag) => ( setEditTagId(tag.id)} className="gap-2" > {tag.name} {editTagId === tag.id && } ))} )}
) : (
{comment.content && (

)} {comment.imageUrl && (
setPreviewImage(comment.imageUrl)} > {/* eslint-disable-next-line @next/next/no-img-element */} Attachment
)}
)} {comment.voiceUrl && (
{playingVoiceId === comment.id ? `${formatTime(voiceCurrentTime)} / ${formatTime(comment.voiceDuration || 0)}` : formatTime(comment.voiceDuration || 0)} {playingVoiceId === comment.id && ( )}
)}

{new Date(comment.createdAt).toLocaleDateString()}

{comment.annotationData && ( Annotated )} {comment.tag && ( {comment.tag.name} )}
{comment.replies && comment.replies.length > 0 && (
{comment.replies.map((reply) => { const replyAuthor = reply.author?.name || reply.guestName || 'Anonymous'; const isEditingReply = editingCommentId === reply.id; const canEditReply = reply.canEdit ?? (reply.author?.id === currentUserId); const canDeleteReply = reply.canDelete ?? (reply.author?.id === currentUserId || projectOwnerId === currentUserId); const canManageReply = canEditReply || canDeleteReply; return (
{replyAuthor.charAt(0)} {replyAuthor} {new Date(reply.createdAt).toLocaleDateString()}
{canManageReply && ( {canEditReply && ( { setEditingCommentId(reply.id); setEditText(reply.content || ''); }}> Edit )} {canDeleteReply && ( handleDeleteComment(reply.id)} > Delete )} )}
{isEditingReply ? (
{ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { handleEditComment(reply.id); } if (e.key === 'Escape') { setEditingCommentId(null); setEditText(''); } }} />
) : (
{reply.content && (

)} {reply.imageUrl && (
setPreviewImage(reply.imageUrl)} > {/* eslint-disable-next-line @next/next/no-img-element */} Attachment
)}
)} {reply.voiceUrl && (
{playingVoiceId === reply.id ? `${formatTime(voiceCurrentTime)} / ${formatTime(reply.voiceDuration || 0)}` : formatTime(reply.voiceDuration || 0)} {playingVoiceId === reply.id && ( )}
)}
); })}
)} {isReplying && (
{isReplyRecording ? (
{formatTime(replyRecordingTime)}
) : replyAudioBlob ? (
{playingVoiceId === 'reply-preview' ? `${formatTime(voiceCurrentTime)} / ${formatTime(replyRecordingTime)}` : formatTime(replyRecordingTime)} {playingVoiceId === 'reply-preview' && ( )}
{replyImageBlob && (
{/* eslint-disable-next-line @next/next/no-img-element */} Preview
)}
) : ( <> {replyImageBlob && (
{/* eslint-disable-next-line @next/next/no-img-element */} Preview
)}
{ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { handleReplyComment(comment.id); } if (e.key === 'Escape') { setReplyingTo(null); setReplyText(''); } }} onPaste={(e) => handlePaste(e, true)} /> handleImageSelect(e, true)} />
)}
)} {!isReplying && !isEditing && ( )}
); }) )}
{activePane === 'comments' ? composer : null}
); });