107 lines
3.5 KiB
TypeScript
107 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import type { ChatMessage } from '@/lib/api';
|
|
import { isSelectableMessage } from '@/lib/chat-message-utils';
|
|
|
|
export function useChatMessageSelection(messages: ChatMessage[]) {
|
|
const [selectionMode, setSelectionMode] = useState(false);
|
|
const [selectedMessageIds, setSelectedMessageIds] = useState<string[]>([]);
|
|
const anchorIndexRef = useRef<number | null>(null);
|
|
|
|
const exitSelectionMode = useCallback(() => {
|
|
setSelectionMode(false);
|
|
setSelectedMessageIds([]);
|
|
anchorIndexRef.current = null;
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!selectionMode) return;
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
exitSelectionMode();
|
|
}
|
|
};
|
|
window.addEventListener('keydown', onKeyDown);
|
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
}, [exitSelectionMode, selectionMode]);
|
|
|
|
const enterSelectionMode = useCallback((messageId?: string) => {
|
|
if (messageId) {
|
|
const message = messages.find((item) => item.id === messageId);
|
|
if (!message || !isSelectableMessage(message)) return;
|
|
}
|
|
setSelectionMode(true);
|
|
setSelectedMessageIds(messageId ? [messageId] : []);
|
|
anchorIndexRef.current = messageId ? messages.findIndex((item) => item.id === messageId) : null;
|
|
}, [messages]);
|
|
|
|
const addSelectedMessage = useCallback((messageId: string) => {
|
|
const message = messages.find((item) => item.id === messageId);
|
|
if (!message || !isSelectableMessage(message)) return;
|
|
setSelectionMode(true);
|
|
setSelectedMessageIds((current) => (current.includes(messageId) ? current : [...current, messageId]));
|
|
anchorIndexRef.current = messages.findIndex((item) => item.id === messageId);
|
|
}, [messages]);
|
|
|
|
const toggleSelectedMessage = useCallback((messageId: string) => {
|
|
setSelectedMessageIds((current) => {
|
|
const next = current.includes(messageId) ? current.filter((id) => id !== messageId) : [...current, messageId];
|
|
return next;
|
|
});
|
|
anchorIndexRef.current = messages.findIndex((item) => item.id === messageId);
|
|
}, [messages]);
|
|
|
|
const handleSelectionPointer = useCallback(
|
|
(messageId: string, event: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean }) => {
|
|
const index = messages.findIndex((item) => item.id === messageId);
|
|
if (index < 0) return false;
|
|
|
|
const multiKey = Boolean(event.ctrlKey || event.metaKey);
|
|
if (multiKey) {
|
|
if (!selectionMode) {
|
|
enterSelectionMode(messageId);
|
|
} else {
|
|
toggleSelectedMessage(messageId);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (event.shiftKey) {
|
|
if (!selectionMode) {
|
|
enterSelectionMode(messageId);
|
|
}
|
|
const anchor = anchorIndexRef.current ?? index;
|
|
const start = Math.min(anchor, index);
|
|
const end = Math.max(anchor, index);
|
|
const rangeIds = messages
|
|
.slice(start, end + 1)
|
|
.filter((item) => isSelectableMessage(item))
|
|
.map((item) => item.id);
|
|
setSelectedMessageIds(rangeIds);
|
|
return true;
|
|
}
|
|
|
|
if (selectionMode) {
|
|
toggleSelectedMessage(messageId);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
[enterSelectionMode, messages, selectionMode, toggleSelectedMessage]
|
|
);
|
|
|
|
return {
|
|
selectionMode,
|
|
selectedMessageIds,
|
|
setSelectedMessageIds,
|
|
enterSelectionMode,
|
|
exitSelectionMode,
|
|
toggleSelectedMessage,
|
|
addSelectedMessage,
|
|
handleSelectionPointer
|
|
};
|
|
}
|