From 000c4caacb1ad4cb7aefb486cc00dec22763b8c1 Mon Sep 17 00:00:00 2001 From: inmake Date: Thu, 30 Oct 2025 19:27:35 +0500 Subject: [PATCH] Update environment configurations for client and server, refactor ChatPopup to accept sessionId as a prop, enhance chat message handling with senderName and guestId, and improve error logging in chat message processing. Adjust useChatHistory and useWebRTC hooks for better state management and message retrieval. --- client/.env | 8 +- client/src/components/popups/ChatPopup.tsx | 73 ++++- client/src/components/ui/ControlsPopover.tsx | 2 +- client/src/hooks/useChatHistory.ts | 29 +- client/src/hooks/useWebRTC.ts | 4 +- client/src/lib/webrtc.ts | 15 +- client/src/pages/SessionPage.tsx | 2 +- server/.env | 4 +- server/src/db/schema/chatMessages.ts | 4 +- server/src/index.ts | 273 ++++++++++++------- server/src/services/chat/index.ts | 29 +- 11 files changed, 309 insertions(+), 134 deletions(-) diff --git a/client/.env b/client/.env index 5e374be..0f22f67 100644 --- a/client/.env +++ b/client/.env @@ -1,4 +1,4 @@ -# VITE_API_URL=http://localhost:3000 -# VITE_WEBRTC_URL=http://localhost:3001 -VITE_API_URL=https://stream.graff.estate/api -VITE_WEBRTC_URL=https://stream.graff.estate \ No newline at end of file +VITE_API_URL=http://localhost:3000 +VITE_WEBRTC_URL=http://localhost:3001 +# VITE_API_URL=https://stream.graff.estate/api +# VITE_WEBRTC_URL=https://stream.graff.estate \ No newline at end of file diff --git a/client/src/components/popups/ChatPopup.tsx b/client/src/components/popups/ChatPopup.tsx index b9f7edf..0081a1b 100644 --- a/client/src/components/popups/ChatPopup.tsx +++ b/client/src/components/popups/ChatPopup.tsx @@ -1,25 +1,48 @@ import { useRef, useEffect, useState } from "react"; import SendIcon from "../icons/SendIcon"; import Button from "../ui/Button"; -import { useMe } from "../../hooks/useAuth"; import clsx from "clsx"; import PopupWrapper from "../PopupWrapper"; import DraggableContainer from "../DraggableContainer"; import { useWebRTC } from "../../hooks/useWebRTC"; import { useChatHistory } from "../../hooks/useChatHistory"; import { useParams } from "react-router"; +import { useMe } from "../../hooks/useAuth"; -export default function ChatPopup() { +interface ChatPopupProps { + sessionId?: string; +} + +export default function ChatPopup({ sessionId: sessionIdProp }: ChatPopupProps = {}) { const headerRef = useRef(null); - const { id: sessionId } = useParams<{ id: string }>(); + // Получаем sessionId из пропсов или из URL параметров + const { id: sessionIdFromParams } = useParams<{ id: string }>(); + const sessionId = sessionIdProp || sessionIdFromParams; const { chatMessages: realtimeMessages, sendMessage, currentUserId, } = useWebRTC(); + // Получаем данные текущего пользователя (если авторизован) + const { data: user } = useMe(); + + // currentUserId содержит либо userId (если авторизован), либо guestId (если гость) + // Это и есть наш идентификатор для сравнения с senderId в сообщениях + const myIdentifier = currentUserId; + + console.log("[ChatPopup] Rendering with sessionId:", sessionId); + console.log("[ChatPopup] My identifier:", myIdentifier, "isAuthenticated:", !!user); + // Загружаем историю через REST API - const { data: historyMessages = [], isLoading } = useChatHistory(sessionId); + const { data: historyMessages = [], isLoading, error } = useChatHistory(sessionId); + + console.log("[ChatPopup] Chat history state:", { + historyMessages: historyMessages.length, + isLoading, + error, + realtimeMessages: realtimeMessages.length, + }); // Объединяем историю и realtime сообщения const historyIds = new Set(historyMessages.map((m) => m.id)); @@ -28,8 +51,11 @@ export default function ChatPopup() { ); const allMessages = [...historyMessages, ...newRealtimeMessages]; + console.log("[ChatPopup] All messages count:", allMessages.length); + function onMessageSend(message: string) { - sendMessage(message); + // Передаем имя пользователя и флаг авторизации + sendMessage(message, user?.fullName, !!user); } return ( @@ -48,7 +74,7 @@ export default function ChatPopup() {
@@ -62,6 +88,7 @@ interface MessageFeedProps { messages: Array<{ id: string; senderId: string; + senderName?: string; content: string; timestamp: Date | string; }>; @@ -73,6 +100,14 @@ function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) { const messagesEndRef = useRef(null); const prevMessageCountRef = useRef(0); + console.log("[MessageFeed] Rendering with currentUserId:", currentUserId); + console.log("[MessageFeed] Messages:", messages.map(m => ({ + id: m.id, + senderId: m.senderId, + isFromMe: m.senderId === currentUserId, + content: m.content.substring(0, 20) + }))); + // Умный скролл - только при добавлении новых сообщений useEffect(() => { const currentCount = messages.length; @@ -124,8 +159,18 @@ function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) { } )} isFromMe={message.senderId === currentUserId} + senderName={message.senderName} /> ))} + + {/* + + + */}
)} @@ -137,11 +182,15 @@ interface MessageItemProps { timestamp: string; content: string; isFromMe: boolean; + senderName?: string; } -function MessageItem({ timestamp, content, isFromMe }: MessageItemProps) { - const { data: user } = useMe(); - +function MessageItem({ + timestamp, + content, + isFromMe, + senderName, +}: MessageItemProps) { return (
{!isFromMe && ( -
{user?.fullName}
+
+ {senderName || "Guest"} +
)} -
{content}
+
{content}
{timestamp}
diff --git a/client/src/components/ui/ControlsPopover.tsx b/client/src/components/ui/ControlsPopover.tsx index 05b9ee8..ba20557 100644 --- a/client/src/components/ui/ControlsPopover.tsx +++ b/client/src/components/ui/ControlsPopover.tsx @@ -33,7 +33,7 @@ function ControlsPopover({ session }: ControlsPopoverProps) { function handleClickOpenChatPopup() { setIsOpened(false); - setPopup(); + setPopup(); } function handleClickOpenParticipantsPopup() { diff --git a/client/src/hooks/useChatHistory.ts b/client/src/hooks/useChatHistory.ts index 4563934..5ebc2eb 100644 --- a/client/src/hooks/useChatHistory.ts +++ b/client/src/hooks/useChatHistory.ts @@ -10,22 +10,37 @@ interface ChatHistoryResponse { } export const useChatHistory = (sessionId: string | undefined, enabled = true) => { + console.log("[useChatHistory] Hook called with:", { sessionId, enabled, willExecute: enabled && !!sessionId }); + return useQuery({ queryKey: ["chat-history", sessionId], queryFn: async () => { + console.log("[useChatHistory] Fetching chat history for session:", sessionId); + if (!sessionId) { + console.error("[useChatHistory] Session ID is required but not provided"); throw new Error("Session ID is required"); } - const response = await api - .get(`sessions/${sessionId}/messages`) - .json(); + try { + console.log("[useChatHistory] Making API request to:", `sessions/${sessionId}/messages`); + const response = await api + .get(`sessions/${sessionId}/messages`) + .json(); - if (!response.success) { - throw new Error(response.error || "Failed to load chat history"); + console.log("[useChatHistory] API response:", response); + + if (!response.success) { + console.error("[useChatHistory] API returned error:", response.error); + throw new Error(response.error || "Failed to load chat history"); + } + + console.log("[useChatHistory] Successfully loaded", response.messages.length, "messages"); + return response.messages; + } catch (error) { + console.error("[useChatHistory] Error fetching chat history:", error); + throw error; } - - return response.messages; }, enabled: enabled && !!sessionId, staleTime: 1000 * 60 * 5, // 5 минут - история считается актуальной diff --git a/client/src/hooks/useWebRTC.ts b/client/src/hooks/useWebRTC.ts index 3e3a34f..d725cbb 100644 --- a/client/src/hooks/useWebRTC.ts +++ b/client/src/hooks/useWebRTC.ts @@ -202,9 +202,9 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { setIsVideoMuted(!newState); }; - const sendMessage = (content: string) => { + const sendMessage = (content: string, senderName?: string, isAuthenticated?: boolean) => { if (!webrtcServiceInstance) return; - webrtcServiceInstance.sendChatMessage(content); + webrtcServiceInstance.sendChatMessage(content, senderName, isAuthenticated); }; const joinRoom = async (roomId: string) => { diff --git a/client/src/lib/webrtc.ts b/client/src/lib/webrtc.ts index 4156af5..b47f7e8 100644 --- a/client/src/lib/webrtc.ts +++ b/client/src/lib/webrtc.ts @@ -300,7 +300,9 @@ function setupSocketListeners() { }); socket.on("chat-error", (error: { message: string }) => { - console.error("📨 Chat error:", error.message); + console.error("📨 Chat error received from server:", error); + console.error("📨 Error message:", error.message); + alert(`Ошибка при отправке сообщения: ${error.message}`); callAllCallbacks("onError", new Error(error.message)); }); @@ -850,17 +852,22 @@ function updateSpeakingState(isSpeaking: boolean): void { }); } -function sendChatMessage(content: string, userName?: string): void { +function sendChatMessage(content: string, senderName?: string, isAuthenticated?: boolean): void { if (!state || !content.trim() || !state.roomId) return; console.log("📤 Sending message via Socket.IO:", content); + // Определяем userId и guestId + const userId = isAuthenticated ? state.userId : null; + const guestId = !isAuthenticated ? state.userId : null; + // Отправляем сообщение через Socket.IO state.socket.emit("chat-message", { roomId: state.roomId, - userId: state.userId, + userId, + guestId, content: content.trim(), - userName: userName || "Anonymous", + senderName: senderName || null, }); } diff --git a/client/src/pages/SessionPage.tsx b/client/src/pages/SessionPage.tsx index b5e1827..32a4361 100644 --- a/client/src/pages/SessionPage.tsx +++ b/client/src/pages/SessionPage.tsx @@ -81,7 +81,7 @@ function SessionPage() { function handleChatOpen() { console.log("handleChatOpen"); - setPopup(); + setPopup(); } function handleParticipantsOpen() { diff --git a/server/.env b/server/.env index 9f5c018..e2aaa31 100644 --- a/server/.env +++ b/server/.env @@ -1,4 +1,4 @@ DATABASE_URL=postgres://postgres:v1sq3vD5faXL@194.26.138.94:5432/stream JWT_SECRET=b5cf2bd3894fb24191f13dc9dddaeecccc92d0ee298e7ee41c2d0aab51c28fa1 -PORT=6000 -SOCKET_PORT=6001 \ No newline at end of file +PORT=3000 +SOCKET_PORT=3001 \ No newline at end of file diff --git a/server/src/db/schema/chatMessages.ts b/server/src/db/schema/chatMessages.ts index dd31e0a..9fdbb78 100644 --- a/server/src/db/schema/chatMessages.ts +++ b/server/src/db/schema/chatMessages.ts @@ -1,4 +1,4 @@ -import { pgTable, uuid, text, timestamp, pgEnum } from "drizzle-orm/pg-core"; +import { pgTable, uuid, text, timestamp, pgEnum, varchar } from "drizzle-orm/pg-core"; import { relations } from "drizzle-orm"; import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { serverSessions } from "./serverSessions"; @@ -13,6 +13,8 @@ export const chatMessages = pgTable("chat_messages", { .notNull() .references(() => serverSessions.id, { onDelete: "cascade" }), userId: uuid("user_id").references(() => users.id), // Nullable для системных сообщений или анонимных пользователей + guestId: uuid("guest_id"), // ID гостя (для неавторизованных пользователей) + senderName: varchar("sender_name", { length: 255 }).notNull(), // Имя отправителя (из БД для авторизованных, "Guest" для неавторизованных) content: text("content").notNull(), type: messageTypeEnum("type").notNull().default("text"), createdAt: timestamp("created_at", { withTimezone: true }) diff --git a/server/src/index.ts b/server/src/index.ts index 3641463..813a361 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -83,72 +83,79 @@ io.on("connection", (socket) => { console.log(`[WebRTC] User connected: ${socket.id}`); // Присоединение к комнате - socket.on("join-room", async ({ roomId, userId, isAudioEnabled, isVideoEnabled }) => { - console.log( - `[WebRTC] User ${userId} (socket: ${socket.id}) joining room ${roomId}, audio: ${isAudioEnabled}, video: ${isVideoEnabled}` - ); - - // Покинуть предыдущую комнату если была - const existingUser = users.get(userId); - if (existingUser?.roomId) { + socket.on( + "join-room", + async ({ roomId, userId, isAudioEnabled, isVideoEnabled }) => { console.log( - `[WebRTC] User ${userId} leaving previous room ${existingUser.roomId}` + `[WebRTC] User ${userId} (socket: ${socket.id}) joining room ${roomId}, audio: ${isAudioEnabled}, video: ${isVideoEnabled}` ); - socket.leave(existingUser.roomId); - const prevRoom = rooms.get(existingUser.roomId); - if (prevRoom) { - prevRoom.participants.delete(userId); - socket.to(existingUser.roomId).emit("user-left", userId); + + // Покинуть предыдущую комнату если была + const existingUser = users.get(userId); + if (existingUser?.roomId) { + console.log( + `[WebRTC] User ${userId} leaving previous room ${existingUser.roomId}` + ); + socket.leave(existingUser.roomId); + const prevRoom = rooms.get(existingUser.roomId); + if (prevRoom) { + prevRoom.participants.delete(userId); + socket.to(existingUser.roomId).emit("user-left", userId); + } } - } - // Присоединиться к новой комнате - socket.join(roomId); + // Присоединиться к новой комнате + socket.join(roomId); - // Создать комнату если не существует - if (!rooms.has(roomId)) { - console.log(`[WebRTC] Creating new room: ${roomId}`); - rooms.set(roomId, { - id: roomId, - participants: new Set(), + // Создать комнату если не существует + if (!rooms.has(roomId)) { + console.log(`[WebRTC] Creating new room: ${roomId}`); + rooms.set(roomId, { + id: roomId, + participants: new Set(), + }); + } + + const room = rooms.get(roomId)!; + room.participants.add(userId); + + // Сохранить пользователя + users.set(userId, { + id: userId, + roomId, + socketId: socket.id, }); + + console.log( + `[WebRTC] Room ${roomId} now has participants:`, + Array.from(room.participants) + ); + + // Уведомить других участников + socket.to(roomId).emit("user-joined", userId); + console.log( + `[WebRTC] Notified room ${roomId} about user ${userId} joining` + ); + + // Отправить состояние аудио/видео нового пользователя всем в комнате + socket + .to(roomId) + .emit("audio-toggle", { userId, isEnabled: isAudioEnabled !== false }); + socket + .to(roomId) + .emit("video-toggle", { userId, isEnabled: isVideoEnabled !== false }); + + // Отправить список участников новому пользователю + const participants = Array.from(room.participants).filter( + (id) => id !== userId + ); + console.log( + `[WebRTC] Sending participant list to ${userId}:`, + participants + ); + socket.emit("room-participants", participants); } - - const room = rooms.get(roomId)!; - room.participants.add(userId); - - // Сохранить пользователя - users.set(userId, { - id: userId, - roomId, - socketId: socket.id, - }); - - console.log( - `[WebRTC] Room ${roomId} now has participants:`, - Array.from(room.participants) - ); - - // Уведомить других участников - socket.to(roomId).emit("user-joined", userId); - console.log( - `[WebRTC] Notified room ${roomId} about user ${userId} joining` - ); - - // Отправить состояние аудио/видео нового пользователя всем в комнате - socket.to(roomId).emit("audio-toggle", { userId, isEnabled: isAudioEnabled !== false }); - socket.to(roomId).emit("video-toggle", { userId, isEnabled: isVideoEnabled !== false }); - - // Отправить список участников новому пользователю - const participants = Array.from(room.participants).filter( - (id) => id !== userId - ); - console.log( - `[WebRTC] Sending participant list to ${userId}:`, - participants - ); - socket.emit("room-participants", participants); - }); + ); // Покидание комнаты socket.on("leave-room", ({ roomId, userId }) => { @@ -215,13 +222,17 @@ io.on("connection", (socket) => { // Обработка audio/video toggle socket.on("audio-toggle", ({ roomId, userId, isEnabled }) => { - console.log(`[WebRTC] Audio toggle from ${userId} in room ${roomId}: ${isEnabled}`); + console.log( + `[WebRTC] Audio toggle from ${userId} in room ${roomId}: ${isEnabled}` + ); // Отправляем всем в комнате (кроме отправителя) socket.to(roomId).emit("audio-toggle", { userId, isEnabled }); }); socket.on("video-toggle", ({ roomId, userId, isEnabled }) => { - console.log(`[WebRTC] Video toggle from ${userId} in room ${roomId}: ${isEnabled}`); + console.log( + `[WebRTC] Video toggle from ${userId} in room ${roomId}: ${isEnabled}` + ); // Отправляем всем в комнате (кроме отправителя) socket.to(roomId).emit("video-toggle", { userId, isEnabled }); }); @@ -233,44 +244,118 @@ io.on("connection", (socket) => { }); // Обработка сообщений чата - socket.on("chat-message", async ({ roomId, userId, content, userName }) => { - console.log(`[Chat] Message from ${userId} in room ${roomId}: ${content}`); - - const user = findUserBySocketId(socket.id); - if (!user || user.roomId !== roomId) { - console.warn(`[Chat] User ${socket.id} is not in room ${roomId}`); - return; - } - - try { - // Сохраняем сообщение в БД - const savedMessage = await saveChatMessage({ - sessionId: roomId, - userId: userId || null, // null для анонимных пользователей + socket.on( + "chat-message", + async ({ roomId, userId, content, senderName, guestId }) => { + console.log(`[Chat] Received message:`, { + roomId, + userId, + guestId, + senderName, content, - type: "text", }); - // Формируем сообщение для отправки клиентам - const messageToSend = { - id: savedMessage.id, - senderId: userId, - senderName: userName, - content: savedMessage.content, - timestamp: savedMessage.createdAt, - type: savedMessage.type, - }; + const user = findUserBySocketId(socket.id); + if (!user || user.roomId !== roomId) { + console.warn(`[Chat] User ${socket.id} is not in room ${roomId}`); + return; + } - // Отправляем всем в комнате (включая отправителя) - io.to(roomId).emit("chat-message", messageToSend); - console.log(`[Chat] Message broadcasted to room ${roomId}`); - } catch (error) { - console.error(`[Chat] Error saving message:`, error); - socket.emit("chat-error", { - message: "Failed to save message", - }); + try { + // Определяем имя отправителя + const finalSenderName = senderName || "Guest"; + + console.log( + `[Chat] Preparing to save message with senderName: "${finalSenderName}"` + ); + + // Валидация UUID + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + + if (!uuidRegex.test(roomId)) { + console.error(`[Chat] Invalid roomId/sessionId format: ${roomId}`); + throw new Error("Invalid sessionId format"); + } + + if (userId && !uuidRegex.test(userId)) { + console.error(`[Chat] Invalid userId format: ${userId}`); + throw new Error("Invalid userId format"); + } + + if (guestId && !uuidRegex.test(guestId)) { + console.error(`[Chat] Invalid guestId format: ${guestId}`); + throw new Error("Invalid guestId format"); + } + + // Проверяем существование сессии + console.log(`[Chat] Checking if session exists: ${roomId}`); + try { + const sessionExists = await serverSessionService.findById(roomId); + if (!sessionExists) { + console.error(`[Chat] Session not found in database: ${roomId}`); + console.error( + `[Chat] This might cause foreign key constraint error` + ); + // Не возвращаемся - попробуем сохранить и увидим точную ошибку БД + } else { + console.log(`[Chat] Session found:`, sessionExists.id); + } + } catch (sessionCheckError) { + console.error(`[Chat] Error checking session:`, sessionCheckError); + // Продолжаем - увидим точную ошибку при сохранении + } + + // Сохраняем сообщение в БД + const messageData = { + sessionId: roomId, + userId: userId || null, // null для анонимных пользователей + guestId: guestId || null, // ID гостя для неавторизованных + senderName: finalSenderName, // Имя отправителя + content, + type: "text" as const, + }; + + console.log( + `[Chat] Saving message to DB:`, + JSON.stringify(messageData, null, 2) + ); + const savedMessage = await saveChatMessage(messageData); + console.log(`[Chat] Message saved successfully:`, savedMessage); + + // Формируем сообщение для отправки клиентам + const messageToSend = { + id: savedMessage.id, + senderId: userId || guestId, + senderName: savedMessage.senderName, + content: savedMessage.content, + timestamp: savedMessage.createdAt, + type: savedMessage.type, + }; + + // Отправляем всем в комнате (включая отправителя) + io.to(roomId).emit("chat-message", messageToSend); + console.log(`[Chat] Message broadcasted to room ${roomId}`); + } catch (error) { + console.error(`[Chat] Error saving message:`, error); + console.error(`[Chat] Error details:`, JSON.stringify(error, null, 2)); + + // Отправляем детальную ошибку на клиент + const errorMessage = + error instanceof Error ? error.message : "Failed to save message"; + const errorDetails = + error && typeof error === "object" + ? (error as any).detail || (error as any).message || errorMessage + : errorMessage; + + console.error(`[Chat] Sending error to client:`, errorDetails); + + socket.emit("chat-error", { + message: errorDetails, + }); + } } - }); + ); // Отключение socket.on("disconnect", () => { diff --git a/server/src/services/chat/index.ts b/server/src/services/chat/index.ts index 14e9f5c..938edd2 100644 --- a/server/src/services/chat/index.ts +++ b/server/src/services/chat/index.ts @@ -3,17 +3,24 @@ import { chatMessages, type NewChatMessage, } from "../../db/schema/chatMessages"; -import { eq, desc } from "drizzle-orm"; +import { eq, desc, sql } from "drizzle-orm"; /** * Сохранить новое сообщение в чате */ export async function saveChatMessage(message: NewChatMessage) { - const [newMessage] = await db - .insert(chatMessages) - .values(message) - .returning(); - return newMessage; + try { + console.log("[saveChatMessage] Attempting to insert message:", message); + const [newMessage] = await db + .insert(chatMessages) + .values(message) + .returning(); + console.log("[saveChatMessage] Message inserted successfully:", newMessage); + return newMessage; + } catch (error) { + console.error("[saveChatMessage] Database error:", error); + throw error; + } } /** @@ -23,7 +30,15 @@ export async function saveChatMessage(message: NewChatMessage) { */ export async function getChatHistory(sessionId: string, limit = 100) { const messages = await db - .select() + .select({ + id: chatMessages.id, + // senderId - это либо userId (для авторизованных), либо guestId (для гостей) + senderId: sql`COALESCE(${chatMessages.userId}, ${chatMessages.guestId})`.as('senderId'), + senderName: chatMessages.senderName, + content: chatMessages.content, + timestamp: chatMessages.createdAt, + type: chatMessages.type, + }) .from(chatMessages) .where(eq(chatMessages.sessionId, sessionId)) .orderBy(desc(chatMessages.createdAt))