- Add FileDropZone component with overlay for web drag-and-drop - Add useFileDropZone hook using native addEventListener for web - Wire up drag-drop to agent screen to add images to input - Improve image preview styling with visible border - Show X button on hover with dark overlay for removal 🤖 Generated with [Claude Code](https://claude.com/claude-code)
145 lines
3.7 KiB
TypeScript
145 lines
3.7 KiB
TypeScript
import { useState, useCallback, useRef, useEffect } from "react";
|
|
import { Platform } from "react-native";
|
|
import type { ImageAttachment } from "@/components/message-input";
|
|
|
|
interface UseFileDropZoneOptions {
|
|
onFilesDropped: (files: ImageAttachment[]) => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
interface UseFileDropZoneReturn {
|
|
isDragging: boolean;
|
|
containerRef: React.RefObject<HTMLElement | null>;
|
|
}
|
|
|
|
const IS_WEB = Platform.OS === "web";
|
|
|
|
function isImageFile(file: File): boolean {
|
|
return file.type.startsWith("image/");
|
|
}
|
|
|
|
async function fileToImageAttachment(file: File): Promise<ImageAttachment> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
if (typeof reader.result === "string") {
|
|
resolve({
|
|
uri: reader.result,
|
|
mimeType: file.type || "image/jpeg",
|
|
});
|
|
} else {
|
|
reject(new Error("Failed to read file as data URL"));
|
|
}
|
|
};
|
|
reader.onerror = () => reject(reader.error);
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
export function useFileDropZone({
|
|
onFilesDropped,
|
|
disabled = false,
|
|
}: UseFileDropZoneOptions): UseFileDropZoneReturn {
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const containerRef = useRef<HTMLElement | null>(null);
|
|
const dragCounterRef = useRef(0);
|
|
const onFilesDroppedRef = useRef(onFilesDropped);
|
|
|
|
// Keep callback ref up to date
|
|
useEffect(() => {
|
|
onFilesDroppedRef.current = onFilesDropped;
|
|
}, [onFilesDropped]);
|
|
|
|
// Reset drag state when disabled changes
|
|
useEffect(() => {
|
|
if (disabled) {
|
|
setIsDragging(false);
|
|
dragCounterRef.current = 0;
|
|
}
|
|
}, [disabled]);
|
|
|
|
// Set up event listeners on web
|
|
useEffect(() => {
|
|
if (!IS_WEB) return;
|
|
|
|
const element = containerRef.current;
|
|
if (!element) return;
|
|
|
|
function handleDragEnter(e: DragEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (disabled) return;
|
|
|
|
dragCounterRef.current++;
|
|
if (e.dataTransfer?.types.includes("Files")) {
|
|
setIsDragging(true);
|
|
}
|
|
}
|
|
|
|
function handleDragOver(e: DragEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (disabled) return;
|
|
|
|
if (e.dataTransfer) {
|
|
e.dataTransfer.dropEffect = "copy";
|
|
}
|
|
}
|
|
|
|
function handleDragLeave(e: DragEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (disabled) return;
|
|
|
|
dragCounterRef.current--;
|
|
if (dragCounterRef.current === 0) {
|
|
setIsDragging(false);
|
|
}
|
|
}
|
|
|
|
async function handleDrop(e: DragEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
setIsDragging(false);
|
|
dragCounterRef.current = 0;
|
|
|
|
if (disabled) return;
|
|
|
|
const files = Array.from(e.dataTransfer?.files ?? []);
|
|
const imageFiles = files.filter(isImageFile);
|
|
|
|
if (imageFiles.length === 0) return;
|
|
|
|
try {
|
|
const attachments = await Promise.all(
|
|
imageFiles.map(fileToImageAttachment)
|
|
);
|
|
onFilesDroppedRef.current(attachments);
|
|
} catch (error) {
|
|
console.error("[useFileDropZone] Failed to process dropped files:", error);
|
|
}
|
|
}
|
|
|
|
element.addEventListener("dragenter", handleDragEnter);
|
|
element.addEventListener("dragover", handleDragOver);
|
|
element.addEventListener("dragleave", handleDragLeave);
|
|
element.addEventListener("drop", handleDrop);
|
|
|
|
return () => {
|
|
element.removeEventListener("dragenter", handleDragEnter);
|
|
element.removeEventListener("dragover", handleDragOver);
|
|
element.removeEventListener("dragleave", handleDragLeave);
|
|
element.removeEventListener("drop", handleDrop);
|
|
};
|
|
}, [disabled]);
|
|
|
|
return {
|
|
isDragging,
|
|
containerRef,
|
|
};
|
|
}
|