Refactor ChatPopup to filter real-time messages by sessionId, enhance message deduplication logic, and improve logging for message statistics. Update MessageFeed to handle initial scroll behavior and add keydown event for message input handling. Adjust useChatHistory to prevent automatic refetching of chat history.

This commit is contained in:
2025-10-31 16:17:49 +05:00
parent f88e4a80c0
commit f968c3e144
5 changed files with 87 additions and 8 deletions
+69 -4
View File
@@ -54,14 +54,39 @@ export default function ChatPopup({ sessionId: sessionIdProp }: ChatPopupProps =
realtimeMessages: realtimeMessages.length, realtimeMessages: realtimeMessages.length,
}); });
// Фильтруем realtime сообщения только для текущей сессии
// Это важно, т.к. WebRTC сервис глобальный и может содержать сообщения из других сессий
const sessionRealtimeMessages = realtimeMessages.filter((m) => {
// Если у сообщения нет sessionId, включаем его (для обратной совместимости)
// Если sessionId есть, проверяем что он совпадает с текущей сессией
return !m.sessionId || m.sessionId === sessionId;
});
// Объединяем историю и realtime сообщения // Объединяем историю и realtime сообщения
const historyIds = new Set(historyMessages.map((m) => m.id)); const historyIds = new Set(historyMessages.map((m) => m.id));
const newRealtimeMessages = realtimeMessages.filter( const newRealtimeMessages = sessionRealtimeMessages.filter(
(m) => !historyIds.has(m.id) (m) => !historyIds.has(m.id)
); );
const allMessages = [...historyMessages, ...newRealtimeMessages];
console.log("[ChatPopup] All messages count:", allMessages.length); // Дедупликация на всякий случай - убираем дубликаты по ID
const allMessagesMap = new Map<string, typeof historyMessages[0]>();
[...historyMessages, ...newRealtimeMessages].forEach((msg) => {
allMessagesMap.set(msg.id, msg);
});
const allMessages = Array.from(allMessagesMap.values()).sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
console.log("[ChatPopup] Messages stats:", {
sessionId,
history: historyMessages.length,
realtimeTotal: realtimeMessages.length,
realtimeForSession: sessionRealtimeMessages.length,
newRealtime: newRealtimeMessages.length,
total: allMessages.length,
historyIds: Array.from(historyIds),
realtimeIds: realtimeMessages.map(m => ({ id: m.id, sessionId: m.sessionId })),
});
function onMessageSend(message: string) { function onMessageSend(message: string) {
// Передаем имя пользователя и флаг авторизации // Передаем имя пользователя и флаг авторизации
@@ -109,6 +134,7 @@ interface MessageFeedProps {
function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) { function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) {
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const prevMessageCountRef = useRef(0); const prevMessageCountRef = useRef(0);
const isInitialMount = useRef(true);
console.log("[MessageFeed] Rendering with currentUserId:", currentUserId); console.log("[MessageFeed] Rendering with currentUserId:", currentUserId);
console.log("[MessageFeed] Messages:", messages.map(m => ({ console.log("[MessageFeed] Messages:", messages.map(m => ({
@@ -118,6 +144,15 @@ function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) {
content: m.content.substring(0, 20) content: m.content.substring(0, 20)
}))); })));
// Скролл при первой загрузке сообщений
useEffect(() => {
if (isInitialMount.current && messages.length > 0 && !isLoading) {
messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
isInitialMount.current = false;
prevMessageCountRef.current = messages.length;
}
}, [messages.length, isLoading]);
// Умный скролл - только при добавлении новых сообщений // Умный скролл - только при добавлении новых сообщений
useEffect(() => { useEffect(() => {
const currentCount = messages.length; const currentCount = messages.length;
@@ -125,7 +160,8 @@ function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) {
// Скроллим только если добавилось новое сообщение // Скроллим только если добавилось новое сообщение
if ( if (
currentCount > prevMessageCountRef.current && currentCount > prevMessageCountRef.current &&
prevMessageCountRef.current > 0 prevMessageCountRef.current > 0 &&
!isInitialMount.current
) { ) {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
} }
@@ -270,6 +306,34 @@ function MessageInput({
onMessageSend(message); onMessageSend(message);
} }
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === "Enter") {
if (e.ctrlKey || e.shiftKey) {
// Ctrl+Enter или Shift+Enter - добавляем перенос строки
e.preventDefault();
const textarea = textareaRef.current;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newMessage = message.substring(0, start) + "\n" + message.substring(end);
setMessage(newMessage);
// Устанавливаем курсор после вставленного переноса строки
setTimeout(() => {
textarea.selectionStart = textarea.selectionEnd = start + 1;
}, 0);
} else {
// Enter без модификаторов - отправка сообщения
e.preventDefault();
if (message.trim().length > 0) {
sendMessage();
}
}
}
}
return ( return (
<div <div
onClick={() => textareaRef.current?.focus()} onClick={() => textareaRef.current?.focus()}
@@ -279,6 +343,7 @@ function MessageInput({
ref={textareaRef} ref={textareaRef}
value={message} value={message}
onChange={handleChange} onChange={handleChange}
onKeyDown={handleKeyDown}
className="w-[80%] resize-none focus:outline-none my-auto text-s" className="w-[80%] resize-none focus:outline-none my-auto text-s"
rows={1} rows={1}
placeholder="Сообщение..." placeholder="Сообщение..."
+2 -1
View File
@@ -43,10 +43,11 @@ export const useChatHistory = (sessionId: string | undefined, enabled = true) =>
} }
}, },
enabled: enabled && !!sessionId, enabled: enabled && !!sessionId,
staleTime: 1000 * 60 * 5, // 5 минут - история считается актуальной staleTime: Infinity, // История загружается один раз и больше не обновляется (новые сообщения приходят через WebSocket)
gcTime: 1000 * 60 * 30, // 30 минут в кэше gcTime: 1000 * 60 * 30, // 30 минут в кэше
refetchOnWindowFocus: false, // Не перезагружать при фокусе refetchOnWindowFocus: false, // Не перезагружать при фокусе
refetchOnReconnect: false, // Не перезагружать при реконнекте refetchOnReconnect: false, // Не перезагружать при реконнекте
refetchInterval: false, // Не перезагружать автоматически
}); });
}; };
+1
View File
@@ -3,6 +3,7 @@ import { getOrCreateGuestId } from "./guestId";
export interface ChatMessage { export interface ChatMessage {
id: string; id: string;
sessionId?: string; // ID сессии, к которой относится сообщение
senderId: string; senderId: string;
senderName?: string; senderName?: string;
content: string; content: string;
+14 -3
View File
@@ -114,8 +114,14 @@ function SessionPage() {
// } // }
// }, [session?.status, navigate]); // }, [session?.status, navigate]);
const { localStream, toggleAudio, isAudioMuted, toggleVideo, isVideoMuted } = const {
useWebRTC(session?.id, true); localStream,
toggleAudio,
isAudioMuted,
toggleVideo,
isVideoMuted,
participants,
} = useWebRTC(session?.id, true);
if (isLoading) { if (isLoading) {
return ( return (
@@ -219,12 +225,17 @@ function SessionPage() {
</div> </div>
</FloatingActionButton> </FloatingActionButton>
<FloatingActionButton <FloatingActionButton
className="max-2xl:hidden" className="relative max-2xl:hidden"
onClick={handleParticipantsOpen} onClick={handleParticipantsOpen}
> >
<div className="size-[1.111vw] text-white"> <div className="size-[1.111vw] text-white">
<UsersFilledIcon /> <UsersFilledIcon />
</div> </div>
{participants.length > 0 && (
<div className="bg-white absolute 2xl:-top-[0.278vw] 2xl:-right-[0.278vw] -top-1 -right-1 2xl:size-[1.111vw] size-4 text-[#7B60F3] font-semibold font-mono text-caption-xs rounded-full flex items-center justify-center 2xl:pt-[0.069vw] pt-px">
<span className="scale-[0.8]">{participants.length + 1}</span>
</div>
)}
</FloatingActionButton> </FloatingActionButton>
<FloatingActionButton <FloatingActionButton
className="max-2xl:hidden" className="max-2xl:hidden"
+1
View File
@@ -328,6 +328,7 @@ io.on("connection", (socket) => {
// senderId - это либо userId (приоритет), либо guestId // senderId - это либо userId (приоритет), либо guestId
const messageToSend = { const messageToSend = {
id: savedMessage.id, id: savedMessage.id,
sessionId: savedMessage.sessionId, // Добавляем sessionId для фильтрации на клиенте
senderId: savedMessage.userId || savedMessage.guestId, senderId: savedMessage.userId || savedMessage.guestId,
senderName: savedMessage.senderName, senderName: savedMessage.senderName,
content: savedMessage.content, content: savedMessage.content,