Files
IdP/apps/frontend/hooks/use-chat-message-selection.ts
2026-06-25 23:48:57 +03:00

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
};
}