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 3e3d76a..aec14d1 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 ( @@ -218,12 +224,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,