From f968c3e1442ea27ff9ce66a58b1daef360ad2016 Mon Sep 17 00:00:00 2001 From: inmake Date: Fri, 31 Oct 2025 16:17:49 +0500 Subject: [PATCH] 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. --- client/src/components/popups/ChatPopup.tsx | 73 ++++++++++++++++++++-- client/src/hooks/useChatHistory.ts | 3 +- client/src/lib/webrtc.ts | 1 + client/src/pages/SessionPage.tsx | 17 ++++- server/src/index.ts | 1 + 5 files changed, 87 insertions(+), 8 deletions(-) diff --git a/client/src/components/popups/ChatPopup.tsx b/client/src/components/popups/ChatPopup.tsx index 0af78df..9a018f1 100644 --- a/client/src/components/popups/ChatPopup.tsx +++ b/client/src/components/popups/ChatPopup.tsx @@ -54,14 +54,39 @@ export default function ChatPopup({ sessionId: sessionIdProp }: ChatPopupProps = realtimeMessages: realtimeMessages.length, }); + // Фильтруем realtime сообщения только для текущей сессии + // Это важно, т.к. WebRTC сервис глобальный и может содержать сообщения из других сессий + const sessionRealtimeMessages = realtimeMessages.filter((m) => { + // Если у сообщения нет sessionId, включаем его (для обратной совместимости) + // Если sessionId есть, проверяем что он совпадает с текущей сессией + return !m.sessionId || m.sessionId === sessionId; + }); + // Объединяем историю и realtime сообщения const historyIds = new Set(historyMessages.map((m) => m.id)); - const newRealtimeMessages = realtimeMessages.filter( + const newRealtimeMessages = sessionRealtimeMessages.filter( (m) => !historyIds.has(m.id) ); - const allMessages = [...historyMessages, ...newRealtimeMessages]; + + // Дедупликация на всякий случай - убираем дубликаты по ID + const allMessagesMap = new Map(); + [...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] All messages count:", allMessages.length); + 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) { // Передаем имя пользователя и флаг авторизации @@ -109,6 +134,7 @@ interface MessageFeedProps { function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) { const messagesEndRef = useRef(null); const prevMessageCountRef = useRef(0); + const isInitialMount = useRef(true); console.log("[MessageFeed] Rendering with currentUserId:", currentUserId); console.log("[MessageFeed] Messages:", messages.map(m => ({ @@ -118,6 +144,15 @@ function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) { 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(() => { const currentCount = messages.length; @@ -125,7 +160,8 @@ function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) { // Скроллим только если добавилось новое сообщение if ( currentCount > prevMessageCountRef.current && - prevMessageCountRef.current > 0 + prevMessageCountRef.current > 0 && + !isInitialMount.current ) { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); } @@ -270,6 +306,34 @@ function MessageInput({ onMessageSend(message); } + function handleKeyDown(e: React.KeyboardEvent) { + 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 (
textareaRef.current?.focus()} @@ -279,6 +343,7 @@ function MessageInput({ ref={textareaRef} value={message} onChange={handleChange} + onKeyDown={handleKeyDown} className="w-[80%] resize-none focus:outline-none my-auto text-s" rows={1} placeholder="Сообщение..." diff --git a/client/src/hooks/useChatHistory.ts b/client/src/hooks/useChatHistory.ts index 5ebc2eb..939c0b0 100644 --- a/client/src/hooks/useChatHistory.ts +++ b/client/src/hooks/useChatHistory.ts @@ -43,10 +43,11 @@ export const useChatHistory = (sessionId: string | undefined, enabled = true) => } }, enabled: enabled && !!sessionId, - staleTime: 1000 * 60 * 5, // 5 минут - история считается актуальной + staleTime: Infinity, // История загружается один раз и больше не обновляется (новые сообщения приходят через WebSocket) gcTime: 1000 * 60 * 30, // 30 минут в кэше refetchOnWindowFocus: false, // Не перезагружать при фокусе refetchOnReconnect: false, // Не перезагружать при реконнекте + refetchInterval: false, // Не перезагружать автоматически }); }; diff --git a/client/src/lib/webrtc.ts b/client/src/lib/webrtc.ts index 65dd3d1..61af2ca 100644 --- a/client/src/lib/webrtc.ts +++ b/client/src/lib/webrtc.ts @@ -3,6 +3,7 @@ import { getOrCreateGuestId } from "./guestId"; export interface ChatMessage { id: string; + sessionId?: string; // ID сессии, к которой относится сообщение senderId: string; senderName?: string; content: string; diff --git a/client/src/pages/SessionPage.tsx b/client/src/pages/SessionPage.tsx index d4f4460..4c83e66 100644 --- a/client/src/pages/SessionPage.tsx +++ b/client/src/pages/SessionPage.tsx @@ -114,8 +114,14 @@ function SessionPage() { // } // }, [session?.status, navigate]); - const { localStream, toggleAudio, isAudioMuted, toggleVideo, isVideoMuted } = - useWebRTC(session?.id, true); + const { + localStream, + toggleAudio, + isAudioMuted, + toggleVideo, + isVideoMuted, + participants, + } = useWebRTC(session?.id, true); if (isLoading) { return ( @@ -219,12 +225,17 @@ function SessionPage() {
+ {participants.length > 0 && ( +
+ {participants.length + 1} +
+ )}
{ // senderId - это либо userId (приоритет), либо guestId const messageToSend = { id: savedMessage.id, + sessionId: savedMessage.sessionId, // Добавляем sessionId для фильтрации на клиенте senderId: savedMessage.userId || savedMessage.guestId, senderName: savedMessage.senderName, content: savedMessage.content,