diff --git a/components/video-page-content.tsx b/components/video-page-content.tsx index 8576efc..bc9be61 100644 --- a/components/video-page-content.tsx +++ b/components/video-page-content.tsx @@ -414,6 +414,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi handleAddComment, handleImageSelect, handlePaste, + handleDrop, startRecording, stopRecording, cancelRecording, @@ -814,6 +815,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi replyImageInputRef={replyImageInputRef} handleImageSelect={handleImageSelect} handlePaste={handlePaste} + handleDrop={handleDrop} submitReplyWithMedia={commentsActions.onSubmitReplyWithMedia} isSubmittingReply={isSubmittingReply} isUploadingReplyAudio={isUploadingReplyAudio} diff --git a/components/video-page/assets-pane.tsx b/components/video-page/assets-pane.tsx index 696ed92..f09569d 100644 --- a/components/video-page/assets-pane.tsx +++ b/components/video-page/assets-pane.tsx @@ -256,7 +256,7 @@ export const AssetsPane = memo(function AssetsPane({ const handleImageUpload = useCallback(async (file: File) => { if (!file) return; - const imageError = validateImageFile(file); + const imageError = await validateImageFile(file); if (imageError) { toast.error(imageError); return; @@ -297,7 +297,7 @@ export const AssetsPane = memo(function AssetsPane({ const handleImageFileChange = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; - const imageError = validateImageFile(file); + const imageError = await validateImageFile(file); if (imageError) { toast.error(imageError); return; @@ -306,16 +306,16 @@ export const AssetsPane = memo(function AssetsPane({ toast.success('Image attached. Click Upload Image to send.'); }; - const handleImagePaste = (event: React.ClipboardEvent) => { + const handleImagePaste = async (event: React.ClipboardEvent) => { if (uploadTab !== 'image' || !canUploadAssets || isCreatingAsset) return; const pastedImage = extractPastedImageFile(event.clipboardData); if (!pastedImage) return; - const imageError = validateImageFile(pastedImage); + event.preventDefault(); + const imageError = await validateImageFile(pastedImage); if (imageError) { toast.error(imageError); return; } - event.preventDefault(); setPendingImageFile(pastedImage); toast.success('Image attached from clipboard. Click Upload Image to send.'); }; @@ -582,7 +582,7 @@ export const AssetsPane = memo(function AssetsPane({ if (!file) return; if (file.type.startsWith('image/')) { - const imageError = validateImageFile(file); + const imageError = await validateImageFile(file); if (imageError) { toast.error(imageError); return; } // Stage the file so the user can optionally set a name before uploading setUploadTab('image'); diff --git a/components/video-page/comments-pane.tsx b/components/video-page/comments-pane.tsx index eec36c8..b9e5982 100644 --- a/components/video-page/comments-pane.tsx +++ b/components/video-page/comments-pane.tsx @@ -1,6 +1,6 @@ 'use client'; -import { memo, type ReactNode, type RefObject } from 'react'; +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'; @@ -78,6 +78,7 @@ interface CommentsPaneProps { 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; @@ -147,6 +148,7 @@ export const CommentsPane = memo(function CommentsPane({ replyImageInputRef, handleImageSelect, handlePaste, + handleDrop, submitReplyWithMedia, isSubmittingReply, isUploadingReplyAudio, @@ -158,6 +160,8 @@ export const CommentsPane = memo(function CommentsPane({ 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

+
+ )}
diff --git a/components/video-page/hooks/use-comment-actions.ts b/components/video-page/hooks/use-comment-actions.ts index 7772f51..cf50d4c 100644 --- a/components/video-page/hooks/use-comment-actions.ts +++ b/components/video-page/hooks/use-comment-actions.ts @@ -7,6 +7,7 @@ import { useState, type ChangeEvent, type ClipboardEvent, + type DragEvent, type Dispatch, type RefObject, type SetStateAction, @@ -284,11 +285,11 @@ export function useCommentActions({ fetchAssets, ]); - const handleImageSelect = useCallback((e: ChangeEvent, isReply: boolean = false) => { + const handleImageSelect = useCallback(async (e: ChangeEvent, isReply: boolean = false) => { const file = e.target.files?.[0]; if (!file) return; - const imageError = validateImageFile(file); + const imageError = await validateImageFile(file); if (imageError) { toast.error(imageError); return; @@ -301,11 +302,12 @@ export function useCommentActions({ } }, []); - const handlePaste = useCallback((e: ClipboardEvent, isReply: boolean = false) => { + const handlePaste = useCallback(async (e: ClipboardEvent, isReply: boolean = false) => { const file = extractPastedImageFile(e.clipboardData); if (!file) return; + e.preventDefault(); - const imageError = validateImageFile(file); + const imageError = await validateImageFile(file); if (imageError) { toast.error(imageError); return; @@ -316,7 +318,24 @@ export function useCommentActions({ } else { setImageBlob(file); } + }, []); + + const handleDrop = useCallback(async (e: DragEvent, isReply: boolean = false) => { e.preventDefault(); + const file = extractPastedImageFile(e.dataTransfer); + if (!file) return; + + const imageError = await validateImageFile(file); + if (imageError) { + toast.error(imageError); + return; + } + + if (isReply) { + setReplyImageBlob(file); + } else { + setImageBlob(file); + } }, []); const startRecording = useCallback(async () => { @@ -961,6 +980,7 @@ export function useCommentActions({ handleAddComment, handleImageSelect, handlePaste, + handleDrop, startRecording, stopRecording, cancelRecording, diff --git a/components/video-page/image-upload-utils.ts b/components/video-page/image-upload-utils.ts index 4b045a5..05084bc 100644 --- a/components/video-page/image-upload-utils.ts +++ b/components/video-page/image-upload-utils.ts @@ -1,16 +1,20 @@ 'use client'; +import { detectImageMime } from '@/lib/image-upload-validation'; + export const MAX_IMAGE_UPLOAD_BYTES = 10 * 1024 * 1024; -export function validateImageFile(file: File): string | null { - if (!file.type.startsWith('image/')) { - return 'Please select an image file'; - } - +export async function validateImageFile(file: File): Promise { if (file.size > MAX_IMAGE_UPLOAD_BYTES) { return 'Image must be less than 10MB'; } + const header = await file.slice(0, 12).arrayBuffer(); + const detected = detectImageMime(new Uint8Array(header)); + if (!detected) { + return 'Unsupported image format. Allowed: JPEG, PNG, GIF, WEBP'; + } + return null; }