95 lines
2.8 KiB
TypeScript
95 lines
2.8 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useRef, useState, type MouseEvent as ReactMouseEvent } from 'react';
|
|
|
|
const DRAG_THRESHOLD_PX = 6;
|
|
|
|
interface UseChatDragSelectionOptions {
|
|
selectionMode: boolean;
|
|
enterSelectionMode: (messageId: string) => void;
|
|
addSelectedMessage: (messageId: string) => void;
|
|
}
|
|
|
|
const dragSuppressedUntilRef = { current: 0 };
|
|
|
|
export function suppressChatDragSelection(durationMs = 400) {
|
|
dragSuppressedUntilRef.current = Date.now() + durationMs;
|
|
}
|
|
|
|
function isDragSuppressed() {
|
|
return Date.now() < dragSuppressedUntilRef.current;
|
|
}
|
|
|
|
export function useChatDragSelection({
|
|
selectionMode,
|
|
enterSelectionMode,
|
|
addSelectedMessage
|
|
}: UseChatDragSelectionOptions) {
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const visitedRef = useRef<Set<string>>(new Set());
|
|
const pendingDragRef = useRef<{ messageId: string; x: number; y: number } | null>(null);
|
|
|
|
const startDragSelection = useCallback(
|
|
(messageId: string) => {
|
|
if (isDragSuppressed()) return;
|
|
setIsDragging(true);
|
|
visitedRef.current = new Set([messageId]);
|
|
if (!selectionMode) {
|
|
enterSelectionMode(messageId);
|
|
} else {
|
|
addSelectedMessage(messageId);
|
|
}
|
|
},
|
|
[addSelectedMessage, enterSelectionMode, selectionMode]
|
|
);
|
|
|
|
const handleMessagePointerDown = useCallback(
|
|
(messageId: string, event: ReactMouseEvent) => {
|
|
if (event.button !== 0 || isDragSuppressed()) return;
|
|
pendingDragRef.current = { messageId, x: event.clientX, y: event.clientY };
|
|
},
|
|
[]
|
|
);
|
|
|
|
const handleDragEnterMessage = useCallback(
|
|
(messageId: string) => {
|
|
if (!isDragging || visitedRef.current.has(messageId)) return;
|
|
visitedRef.current.add(messageId);
|
|
addSelectedMessage(messageId);
|
|
},
|
|
[addSelectedMessage, isDragging]
|
|
);
|
|
|
|
const endDragSelection = useCallback(() => {
|
|
pendingDragRef.current = null;
|
|
setIsDragging(false);
|
|
visitedRef.current.clear();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const onMouseMove = (event: MouseEvent) => {
|
|
const pending = pendingDragRef.current;
|
|
if (!pending || isDragSuppressed()) return;
|
|
const distance = Math.hypot(event.clientX - pending.x, event.clientY - pending.y);
|
|
if (distance < DRAG_THRESHOLD_PX) return;
|
|
const messageId = pending.messageId;
|
|
pendingDragRef.current = null;
|
|
startDragSelection(messageId);
|
|
};
|
|
|
|
window.addEventListener('mousemove', onMouseMove);
|
|
window.addEventListener('mouseup', endDragSelection);
|
|
return () => {
|
|
window.removeEventListener('mousemove', onMouseMove);
|
|
window.removeEventListener('mouseup', endDragSelection);
|
|
};
|
|
}, [endDragSelection, startDragSelection]);
|
|
|
|
return {
|
|
isDragging,
|
|
handleMessagePointerDown,
|
|
handleDragEnterMessage,
|
|
endDragSelection
|
|
};
|
|
}
|