Implement file drag & drop

This commit is contained in:
2025-09-25 18:47:29 +03:00
Unverified
parent 1db2d55f76
commit 0fbdcd04c9
7 changed files with 181 additions and 20 deletions
@@ -5,6 +5,7 @@ import { ChatInputWrapper } from "./ChatInputWrapper";
import { setGlobalMessageHandler } from "../../../core/websocket";
import type { Message } from "../../../core/types";
import defaultAvatar from "../../../resources/images/default-avatar.png";
import AnimatedOpacity from "../core/animations/AnimatedOpacity";
interface MessagePanelRendererProps {
panel: MessagePanel | null;
@@ -22,6 +23,20 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
const [editVisible, setEditVisible] = useState(Boolean(editMessage));
const [pendingAction, setPendingAction] = useState<null | { type: "reply" | "edit"; message: Message }>(null);
// Drag & drop
const [isDragging, setIsDragging] = useState(false);
const dragCounterRef = useRef(0);
const addFilesRef = useRef<null | ((files: File[]) => void)>(null);
useEffect(() => {
if (!panel || !panelState) return;
return () => {
dragCounterRef.current = 0;
setIsDragging(false);
};
}, [panel, panelState]);
useEffect(() => {
if (replyTo) {
setReplyToVisible(true);
@@ -116,7 +131,41 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
return (
<div className={`chat-container ${switchIn ? "chat-switch-in" : ""} ${switchOut ? "chat-switch-out" : ""}`}>
<div className="chat-main" id="chat-inner">
<div
className="chat-main"
id="chat-inner"
onDragEnter={(e) => {
if (!e.dataTransfer) return;
e.preventDefault();
e.stopPropagation();
dragCounterRef.current += 1;
// Only show overlay when actual files are dragged
const hasFiles = Array.from(e.dataTransfer.types || []).includes("Files");
if (hasFiles) setIsDragging(true);
}}
onDragOver={(e) => {
if (!e.dataTransfer) return;
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = "copy";
}}
onDragLeave={(e) => {
e.preventDefault();
e.stopPropagation();
dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
if (dragCounterRef.current === 0) setIsDragging(false);
}}
onDrop={(e) => {
if (!e.dataTransfer) return;
e.preventDefault();
e.stopPropagation();
const files = Array.from(e.dataTransfer.files || []);
if (files.length > 0 && addFilesRef.current) {
addFilesRef.current(files);
}
setIsDragging(false);
dragCounterRef.current = 0;
}}>
<div className="chat-header">
<img
src={panelState.profilePicture || defaultAvatar}
@@ -175,6 +224,19 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
<div ref={messagesEndRef} />
</ChatMessages>
)}
<AnimatedOpacity
visible={isDragging}
className="file-overlay"
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => e.preventDefault()}>
<div className="file-overlay-wrapper">
<div className="file-overlay-inner">
<mdui-icon name="upload_file" />
<span>Отпустите файл(ы) для добавления</span>
</div>
</div>
</AnimatedOpacity>
<ChatInputWrapper
onSendMessage={(text, files) => {
@@ -213,6 +275,7 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
setPendingAction(null);
}
}}
onProvideFileAdder={(adder) => { addFilesRef.current = adder; }}
/>
</div>
</div>