308 lines
10 KiB
TypeScript
308 lines
10 KiB
TypeScript
import { useEffect, type Dispatch, type MutableRefObject, type SetStateAction } from 'react';
|
|
import { mergeRecoveredChatMessages } from '../../mainChatPanel/chatUtils';
|
|
import { chatGateway } from '../data/chatGateway';
|
|
import type {
|
|
ChatConversationRequest,
|
|
ChatConversationSummary,
|
|
ChatMessage,
|
|
} from '../../mainChatPanel/types';
|
|
|
|
const INITIAL_CONVERSATION_MESSAGE_LIMIT = 3;
|
|
const OLDER_CONVERSATION_MESSAGE_PAGE_SIZE = 20;
|
|
|
|
function getCachedSessionMessages(cache: Map<string, ChatMessage[]>, sessionId: string) {
|
|
const normalizedSessionId = sessionId.trim();
|
|
|
|
if (!normalizedSessionId) {
|
|
return [] as ChatMessage[];
|
|
}
|
|
|
|
return cache.get(normalizedSessionId) ?? [];
|
|
}
|
|
|
|
function getBestAvailableSessionMessages(
|
|
cache: Map<string, ChatMessage[]>,
|
|
sessionId: string,
|
|
currentSessionId: string,
|
|
currentMessages: ChatMessage[],
|
|
) {
|
|
const cachedMessages = getCachedSessionMessages(cache, sessionId);
|
|
|
|
if (sessionId !== currentSessionId || currentMessages.length === 0) {
|
|
return cachedMessages;
|
|
}
|
|
|
|
return mergeRecoveredChatMessages(cachedMessages, currentMessages);
|
|
}
|
|
|
|
type UseConversationRoomDataOptions = {
|
|
activeSessionId: string;
|
|
connectionState: 'connecting' | 'connected' | 'disconnected';
|
|
shouldBlockConversationWhileLoading: (sessionId: string) => boolean;
|
|
captureViewportRestoreSnapshot: () => void;
|
|
sessionMessageCacheRef: MutableRefObject<Map<string, ChatMessage[]>>;
|
|
messagesRef: MutableRefObject<ChatMessage[]>;
|
|
pendingViewportRestoreRef: MutableRefObject<boolean>;
|
|
shouldRestoreConversationAfterReconnectRef: MutableRefObject<boolean>;
|
|
setConversationItems: Dispatch<SetStateAction<ChatConversationSummary[]>>;
|
|
setMessages: Dispatch<SetStateAction<ChatMessage[]>>;
|
|
setRequestItems: Dispatch<SetStateAction<ChatConversationRequest[]>>;
|
|
setConversationLoadingLabel: Dispatch<SetStateAction<string>>;
|
|
setIsConversationContentLoading: Dispatch<SetStateAction<boolean>>;
|
|
setIsDeferringAuxiliaryChatRequests: Dispatch<SetStateAction<boolean>>;
|
|
setHasOlderMessages: Dispatch<SetStateAction<boolean>>;
|
|
setOldestLoadedMessageId: Dispatch<SetStateAction<number | null>>;
|
|
setIsLoadingOlderMessages: Dispatch<SetStateAction<boolean>>;
|
|
queueViewportPrependRestore: (previousScrollHeight: number, previousScrollTop: number) => void;
|
|
viewportRef: MutableRefObject<HTMLDivElement | null>;
|
|
};
|
|
|
|
export function useConversationRoomData({
|
|
activeSessionId,
|
|
connectionState,
|
|
shouldBlockConversationWhileLoading,
|
|
captureViewportRestoreSnapshot,
|
|
sessionMessageCacheRef,
|
|
messagesRef,
|
|
pendingViewportRestoreRef,
|
|
shouldRestoreConversationAfterReconnectRef,
|
|
setConversationItems,
|
|
setMessages,
|
|
setRequestItems,
|
|
setConversationLoadingLabel,
|
|
setIsConversationContentLoading,
|
|
setIsDeferringAuxiliaryChatRequests,
|
|
setHasOlderMessages,
|
|
setOldestLoadedMessageId,
|
|
setIsLoadingOlderMessages,
|
|
queueViewportPrependRestore,
|
|
viewportRef,
|
|
}: UseConversationRoomDataOptions) {
|
|
useEffect(() => {
|
|
if (!activeSessionId.trim()) {
|
|
setMessages([]);
|
|
setRequestItems([]);
|
|
setIsConversationContentLoading(false);
|
|
setIsLoadingOlderMessages(false);
|
|
setHasOlderMessages(false);
|
|
setOldestLoadedMessageId(null);
|
|
return;
|
|
}
|
|
|
|
let isCancelled = false;
|
|
const requestedSessionId = activeSessionId;
|
|
|
|
const loadConversationDetail = async () => {
|
|
captureViewportRestoreSnapshot();
|
|
pendingViewportRestoreRef.current = true;
|
|
setConversationLoadingLabel('대화 내용을 불러오는 중입니다.');
|
|
setIsConversationContentLoading(shouldBlockConversationWhileLoading(requestedSessionId));
|
|
setIsDeferringAuxiliaryChatRequests(true);
|
|
|
|
try {
|
|
const response = await chatGateway.getConversationDetail(requestedSessionId, {
|
|
limit: INITIAL_CONVERSATION_MESSAGE_LIMIT,
|
|
});
|
|
|
|
if (!isCancelled && response.item.sessionId === requestedSessionId) {
|
|
setConversationItems((previous) => {
|
|
const exists = previous.some((item) => item.sessionId === response.item.sessionId);
|
|
if (!exists) {
|
|
return [response.item, ...previous];
|
|
}
|
|
|
|
return previous.map((item) => (item.sessionId === response.item.sessionId ? response.item : item));
|
|
});
|
|
|
|
const baseMessages = getBestAvailableSessionMessages(
|
|
sessionMessageCacheRef.current,
|
|
requestedSessionId,
|
|
activeSessionId,
|
|
messagesRef.current,
|
|
);
|
|
const nextMessages = mergeRecoveredChatMessages(baseMessages, response.messages);
|
|
sessionMessageCacheRef.current.set(requestedSessionId, nextMessages);
|
|
setMessages(nextMessages);
|
|
setRequestItems(response.requests);
|
|
setHasOlderMessages(response.hasOlderMessages);
|
|
setOldestLoadedMessageId(response.oldestLoadedMessageId);
|
|
}
|
|
} catch {
|
|
if (!isCancelled) {
|
|
const cachedMessages = getBestAvailableSessionMessages(
|
|
sessionMessageCacheRef.current,
|
|
requestedSessionId,
|
|
activeSessionId,
|
|
messagesRef.current,
|
|
);
|
|
|
|
if (cachedMessages.length > 0) {
|
|
setMessages(cachedMessages);
|
|
}
|
|
}
|
|
} finally {
|
|
if (!isCancelled) {
|
|
setIsConversationContentLoading(false);
|
|
setIsDeferringAuxiliaryChatRequests(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
void loadConversationDetail();
|
|
|
|
return () => {
|
|
isCancelled = true;
|
|
};
|
|
}, [
|
|
activeSessionId,
|
|
captureViewportRestoreSnapshot,
|
|
messagesRef,
|
|
pendingViewportRestoreRef,
|
|
sessionMessageCacheRef,
|
|
setConversationItems,
|
|
setConversationLoadingLabel,
|
|
setIsConversationContentLoading,
|
|
setIsDeferringAuxiliaryChatRequests,
|
|
setHasOlderMessages,
|
|
setMessages,
|
|
setOldestLoadedMessageId,
|
|
setRequestItems,
|
|
shouldBlockConversationWhileLoading,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (connectionState !== 'connected' || !shouldRestoreConversationAfterReconnectRef.current) {
|
|
return;
|
|
}
|
|
|
|
let isCancelled = false;
|
|
const requestedSessionId = activeSessionId;
|
|
|
|
const restoreConversationAfterReconnect = async () => {
|
|
setIsDeferringAuxiliaryChatRequests(true);
|
|
|
|
try {
|
|
const response = await chatGateway.getConversationDetail(requestedSessionId, {
|
|
limit: Math.max(INITIAL_CONVERSATION_MESSAGE_LIMIT, messagesRef.current.length || 0),
|
|
});
|
|
|
|
if (!isCancelled && response.item.sessionId === requestedSessionId) {
|
|
const baseMessages = getBestAvailableSessionMessages(
|
|
sessionMessageCacheRef.current,
|
|
requestedSessionId,
|
|
activeSessionId,
|
|
messagesRef.current,
|
|
);
|
|
const nextMessages = mergeRecoveredChatMessages(baseMessages, response.messages);
|
|
const hasMessageDiff = nextMessages !== baseMessages;
|
|
|
|
if (hasMessageDiff) {
|
|
captureViewportRestoreSnapshot();
|
|
pendingViewportRestoreRef.current = true;
|
|
setConversationLoadingLabel('채팅방을 다시 연결하고 내용을 복구하는 중입니다.');
|
|
setIsConversationContentLoading(true);
|
|
}
|
|
|
|
setConversationItems((previous) => {
|
|
const exists = previous.some((item) => item.sessionId === response.item.sessionId);
|
|
if (!exists) {
|
|
return [response.item, ...previous];
|
|
}
|
|
|
|
return previous.map((item) => (item.sessionId === response.item.sessionId ? response.item : item));
|
|
});
|
|
setRequestItems(response.requests);
|
|
setHasOlderMessages(response.hasOlderMessages);
|
|
setOldestLoadedMessageId(response.oldestLoadedMessageId);
|
|
|
|
if (hasMessageDiff) {
|
|
sessionMessageCacheRef.current.set(requestedSessionId, nextMessages);
|
|
setMessages(nextMessages);
|
|
window.requestAnimationFrame(() => {
|
|
if (!isCancelled) {
|
|
setIsConversationContentLoading(false);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} catch {
|
|
if (!isCancelled) {
|
|
setIsConversationContentLoading(false);
|
|
}
|
|
} finally {
|
|
if (!isCancelled) {
|
|
shouldRestoreConversationAfterReconnectRef.current = false;
|
|
if (!pendingViewportRestoreRef.current) {
|
|
setIsConversationContentLoading(false);
|
|
}
|
|
setIsDeferringAuxiliaryChatRequests(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
void restoreConversationAfterReconnect();
|
|
|
|
return () => {
|
|
isCancelled = true;
|
|
};
|
|
}, [
|
|
activeSessionId,
|
|
captureViewportRestoreSnapshot,
|
|
connectionState,
|
|
messagesRef,
|
|
pendingViewportRestoreRef,
|
|
sessionMessageCacheRef,
|
|
setConversationItems,
|
|
setConversationLoadingLabel,
|
|
setIsConversationContentLoading,
|
|
setIsDeferringAuxiliaryChatRequests,
|
|
setHasOlderMessages,
|
|
setMessages,
|
|
setOldestLoadedMessageId,
|
|
setRequestItems,
|
|
shouldRestoreConversationAfterReconnectRef,
|
|
]);
|
|
|
|
const loadOlderMessages = async () => {
|
|
const requestedSessionId = activeSessionId.trim();
|
|
const oldestVisibleMessageId = messagesRef.current[0]?.id ?? null;
|
|
|
|
if (!requestedSessionId || oldestVisibleMessageId == null) {
|
|
return;
|
|
}
|
|
|
|
setIsLoadingOlderMessages(true);
|
|
|
|
try {
|
|
const response = await chatGateway.getConversationDetail(requestedSessionId, {
|
|
limit: OLDER_CONVERSATION_MESSAGE_PAGE_SIZE,
|
|
beforeMessageId: oldestVisibleMessageId,
|
|
});
|
|
|
|
if (response.item.sessionId !== requestedSessionId || response.messages.length === 0) {
|
|
setHasOlderMessages(response.hasOlderMessages);
|
|
setOldestLoadedMessageId(response.oldestLoadedMessageId);
|
|
return;
|
|
}
|
|
|
|
const viewport = viewportRef.current;
|
|
const previousScrollHeight = viewport?.scrollHeight ?? 0;
|
|
const previousScrollTop = viewport?.scrollTop ?? 0;
|
|
const nextMessages = mergeRecoveredChatMessages(messagesRef.current, response.messages);
|
|
|
|
queueViewportPrependRestore(previousScrollHeight, previousScrollTop);
|
|
sessionMessageCacheRef.current.set(requestedSessionId, nextMessages);
|
|
setMessages(nextMessages);
|
|
setRequestItems(response.requests);
|
|
setHasOlderMessages(response.hasOlderMessages);
|
|
setOldestLoadedMessageId(response.oldestLoadedMessageId);
|
|
} finally {
|
|
setIsLoadingOlderMessages(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
loadOlderMessages,
|
|
};
|
|
}
|