fix
This commit is contained in:
parent
d349c5283e
commit
7ec72bc13e
5 changed files with 189 additions and 27 deletions
|
|
@ -110,6 +110,14 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
console.log(`👥 Clients in room ${payload.roomCode}: ${roomClients?.size || 0}`,
|
console.log(`👥 Clients in room ${payload.roomCode}: ${roomClients?.size || 0}`,
|
||||||
Array.from(roomClients || []));
|
Array.from(roomClients || []));
|
||||||
|
|
||||||
|
// ВАЖНО: Отправляем подтверждение подключения клиенту
|
||||||
|
client.emit('roomJoined', {
|
||||||
|
roomCode: payload.roomCode,
|
||||||
|
success: true,
|
||||||
|
clientsInRoom: roomClients?.size || 0
|
||||||
|
});
|
||||||
|
console.log(`✅ Sent roomJoined acknowledgement to client ${client.id}`);
|
||||||
|
|
||||||
// Получаем полное состояние для отправки присоединившемуся клиенту
|
// Получаем полное состояние для отправки присоединившемуся клиенту
|
||||||
const room = (await this.prisma.room.findUnique({
|
const room = (await this.prisma.room.findUnique({
|
||||||
where: { code: payload.roomCode },
|
where: { code: payload.roomCode },
|
||||||
|
|
@ -1035,4 +1043,42 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SubscribeMessage('joinAsParticipant')
|
||||||
|
async handleJoinAsParticipant(
|
||||||
|
client: Socket,
|
||||||
|
payload: {
|
||||||
|
roomId: string;
|
||||||
|
roomCode: string;
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
role: 'PLAYER' | 'SPECTATOR';
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
console.log(`🎭 Client ${client.id} joining as participant: ${payload.name} (${payload.role})`);
|
||||||
|
|
||||||
|
// Используем существующий сервис для создания участника
|
||||||
|
const participant = await this.roomsService.joinRoom(
|
||||||
|
payload.roomId,
|
||||||
|
payload.userId,
|
||||||
|
payload.name,
|
||||||
|
payload.role
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Participant created: ${participant.id}, broadcasting to room ${payload.roomCode}`);
|
||||||
|
|
||||||
|
// broadcastFullState уже вызван внутри roomsService.joinRoom
|
||||||
|
// Отправляем подтверждение клиенту
|
||||||
|
client.emit('participantJoined', {
|
||||||
|
success: true,
|
||||||
|
participantId: participant.id
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error joining as participant:', error);
|
||||||
|
client.emit('error', {
|
||||||
|
message: error.message || 'Failed to join as participant'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -161,28 +161,31 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
||||||
|
|
||||||
const joinRoom = useCallback(async (roomId, userId, name, role = 'PLAYER') => {
|
const joinRoom = useCallback(async (roomId, userId, name, role = 'PLAYER') => {
|
||||||
try {
|
try {
|
||||||
// ВАЖНО: Сначала подключаемся к WebSocket комнате
|
console.log('🚀 Starting join process...');
|
||||||
// Это гарантирует, что мы получим broadcast от backend после создания участника
|
|
||||||
if (roomCode) {
|
|
||||||
socketService.connect();
|
|
||||||
socketService.joinRoom(roomCode, userId);
|
|
||||||
// Даем время на установку WebSocket соединения
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Теперь вызываем REST API для создания участника
|
// Шаг 1: Подключаемся к WebSocket комнате и ЖДЕМ подтверждения
|
||||||
const response = await roomsApi.join(roomId, userId, name, role);
|
socketService.connect();
|
||||||
|
console.log('⏳ Waiting for WebSocket room join confirmation...');
|
||||||
|
const joinAck = await socketService.joinRoomWithAck(roomCode, userId);
|
||||||
|
console.log('✅ WebSocket connected to room:', joinAck);
|
||||||
|
|
||||||
// После успешного присоединения запрашиваем полное состояние через WebSocket
|
// Шаг 2: Теперь ГАРАНТИРОВАННО подключены, создаем участника через WebSocket
|
||||||
// (дополнительная гарантия получения актуального состояния)
|
console.log('⏳ Creating participant in database...');
|
||||||
if (roomCode) {
|
const participantAck = await socketService.joinAsParticipant(
|
||||||
setTimeout(() => {
|
roomId,
|
||||||
socketService.emit('requestFullState', { roomCode });
|
roomCode,
|
||||||
}, 50);
|
userId,
|
||||||
}
|
name,
|
||||||
|
role
|
||||||
|
);
|
||||||
|
console.log('✅ Participant created:', participantAck);
|
||||||
|
|
||||||
return response.data;
|
// broadcastFullState уже вызван backend, события gameStateUpdated придут автоматически
|
||||||
|
// Все клиенты (включая хоста) получат обновление
|
||||||
|
|
||||||
|
return { success: true, participantId: participantAck.participantId };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('❌ Join error:', err);
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useRoom } from '../hooks/useRoom';
|
import { useRoom } from '../hooks/useRoom';
|
||||||
|
import NameInputModal from '../components/NameInputModal';
|
||||||
|
|
||||||
const CreateRoom = () => {
|
const CreateRoom = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user, loading: authLoading, loginAnonymous } = useAuth();
|
||||||
const { createRoom, loading: roomLoading } = useRoom();
|
const { createRoom, loading: roomLoading } = useRoom();
|
||||||
|
|
||||||
const [settings, setSettings] = useState({
|
const [settings, setSettings] = useState({
|
||||||
|
|
@ -13,11 +14,32 @@ const CreateRoom = () => {
|
||||||
allowSpectators: true,
|
allowSpectators: true,
|
||||||
password: '',
|
password: '',
|
||||||
});
|
});
|
||||||
|
const [isNameModalOpen, setIsNameModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// Проверка авторизации и показ модального окна для ввода имени
|
||||||
|
// Только если загрузка завершена и пользователя нет
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authLoading && !user) {
|
||||||
|
setIsNameModalOpen(true);
|
||||||
|
} else if (user) {
|
||||||
|
setIsNameModalOpen(false);
|
||||||
|
}
|
||||||
|
}, [authLoading, user]);
|
||||||
|
|
||||||
|
// Обработка ввода имени и авторизация
|
||||||
|
const handleNameSubmit = async (name) => {
|
||||||
|
try {
|
||||||
|
await loginAnonymous(name);
|
||||||
|
setIsNameModalOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
alert('Ошибка при авторизации. Попробуйте еще раз.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateRoom = async () => {
|
const handleCreateRoom = async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
alert('Пожалуйста, задайте имя на главном экране');
|
setIsNameModalOpen(true);
|
||||||
navigate('/');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +114,7 @@ const CreateRoom = () => {
|
||||||
<div className="button-group">
|
<div className="button-group">
|
||||||
<button
|
<button
|
||||||
onClick={handleCreateRoom}
|
onClick={handleCreateRoom}
|
||||||
disabled={roomLoading || !user}
|
disabled={roomLoading}
|
||||||
className="primary"
|
className="primary"
|
||||||
>
|
>
|
||||||
{roomLoading ? 'Создание...' : 'Создать комнату'}
|
{roomLoading ? 'Создание...' : 'Создать комнату'}
|
||||||
|
|
@ -100,6 +122,12 @@ const CreateRoom = () => {
|
||||||
<button onClick={() => navigate('/')}>Назад</button>
|
<button onClick={() => navigate('/')}>Назад</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<NameInputModal
|
||||||
|
isOpen={isNameModalOpen}
|
||||||
|
onSubmit={handleNameSubmit}
|
||||||
|
onCancel={() => navigate('/')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { questionsApi } from '../services/api';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import socketService from '../services/socket';
|
import socketService from '../services/socket';
|
||||||
import QRModal from '../components/QRModal';
|
import QRModal from '../components/QRModal';
|
||||||
|
import NameInputModal from '../components/NameInputModal';
|
||||||
import PasswordModal from '../components/PasswordModal';
|
import PasswordModal from '../components/PasswordModal';
|
||||||
import RoleSelectionModal from '../components/RoleSelectionModal';
|
import RoleSelectionModal from '../components/RoleSelectionModal';
|
||||||
import GameManagementModal from '../components/GameManagementModal';
|
import GameManagementModal from '../components/GameManagementModal';
|
||||||
|
|
@ -14,7 +15,7 @@ import GameManagementModal from '../components/GameManagementModal';
|
||||||
const RoomPage = () => {
|
const RoomPage = () => {
|
||||||
const { roomCode } = useParams();
|
const { roomCode } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, loading: authLoading } = useAuth();
|
const { user, loading: authLoading, loginAnonymous } = useAuth();
|
||||||
const { changeTheme } = useTheme();
|
const { changeTheme } = useTheme();
|
||||||
|
|
||||||
// Храним предыдущий themeId комнаты для отслеживания изменений
|
// Храним предыдущий themeId комнаты для отслеживания изменений
|
||||||
|
|
@ -40,6 +41,7 @@ const RoomPage = () => {
|
||||||
const [qrCode, setQrCode] = useState('');
|
const [qrCode, setQrCode] = useState('');
|
||||||
const [joined, setJoined] = useState(false);
|
const [joined, setJoined] = useState(false);
|
||||||
const [isQRModalOpen, setIsQRModalOpen] = useState(false);
|
const [isQRModalOpen, setIsQRModalOpen] = useState(false);
|
||||||
|
const [isNameModalOpen, setIsNameModalOpen] = useState(false);
|
||||||
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
|
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
|
||||||
const [isRoleSelectionModalOpen, setIsRoleSelectionModalOpen] = useState(false);
|
const [isRoleSelectionModalOpen, setIsRoleSelectionModalOpen] = useState(false);
|
||||||
const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false);
|
const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false);
|
||||||
|
|
@ -87,6 +89,16 @@ const RoomPage = () => {
|
||||||
}
|
}
|
||||||
}, [requiresPassword, isPasswordModalOpen, loading]);
|
}, [requiresPassword, isPasswordModalOpen, loading]);
|
||||||
|
|
||||||
|
// Проверка имени: показываем модальное окно для ввода имени
|
||||||
|
// Только если загрузка завершена, пользователя нет, комната загружена и пароль не требуется
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authLoading && !user && room && !loading && !requiresPassword) {
|
||||||
|
setIsNameModalOpen(true);
|
||||||
|
} else if (user) {
|
||||||
|
setIsNameModalOpen(false);
|
||||||
|
}
|
||||||
|
}, [authLoading, user, room, loading, requiresPassword]);
|
||||||
|
|
||||||
// Обработка ввода пароля
|
// Обработка ввода пароля
|
||||||
const handlePasswordSubmit = async (enteredPassword) => {
|
const handlePasswordSubmit = async (enteredPassword) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -104,6 +116,17 @@ const RoomPage = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Обработка ввода имени и авторизация
|
||||||
|
const handleNameSubmit = async (name) => {
|
||||||
|
try {
|
||||||
|
await loginAnonymous(name);
|
||||||
|
setIsNameModalOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
alert('Ошибка при авторизации. Попробуйте еще раз.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Единая логика присоединения к комнате
|
// Единая логика присоединения к комнате
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleJoin = async () => {
|
const handleJoin = async () => {
|
||||||
|
|
@ -112,10 +135,8 @@ const RoomPage = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Перенаправляем на главный экран, если нет пользователя
|
// Если пользователя нет - модальное окно имени покажется через другой useEffect
|
||||||
if (!user) {
|
if (!user) {
|
||||||
alert('Пожалуйста, задайте имя на главном экране');
|
|
||||||
navigate('/');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,6 +450,12 @@ const RoomPage = () => {
|
||||||
roomCode={roomCode}
|
roomCode={roomCode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<NameInputModal
|
||||||
|
isOpen={isNameModalOpen}
|
||||||
|
onSubmit={handleNameSubmit}
|
||||||
|
onCancel={() => navigate('/')}
|
||||||
|
/>
|
||||||
|
|
||||||
<PasswordModal
|
<PasswordModal
|
||||||
isOpen={isPasswordModalOpen}
|
isOpen={isPasswordModalOpen}
|
||||||
onSubmit={handlePasswordSubmit}
|
onSubmit={handlePasswordSubmit}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,64 @@ class SocketService {
|
||||||
this.emit('joinRoom', { roomCode, userId });
|
this.emit('joinRoom', { roomCode, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Присоединение к WebSocket комнате с ожиданием подтверждения
|
||||||
|
joinRoomWithAck(roomCode, userId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error('WebSocket join timeout - room not joined within 5 seconds'));
|
||||||
|
}, 5000); // 5 секунд максимум
|
||||||
|
|
||||||
|
// Слушаем подтверждение один раз
|
||||||
|
this.socket.once('roomJoined', (data) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
console.log('✅ WebSocket room joined:', data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отправляем запрос на присоединение
|
||||||
|
console.log(`📤 Requesting to join WebSocket room: ${roomCode}`);
|
||||||
|
this.emit('joinRoom', { roomCode, userId });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Присоединение как участник (создание записи в БД) через WebSocket
|
||||||
|
joinAsParticipant(roomId, roomCode, userId, name, role) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error('Participant join timeout - not created within 5 seconds'));
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Обработчик успешного присоединения
|
||||||
|
const handleSuccess = (data) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.socket.off('error', handleError); // Убираем обработчик ошибки
|
||||||
|
console.log('✅ Participant joined successfully:', data);
|
||||||
|
resolve(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик ошибки
|
||||||
|
const handleError = (error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.socket.off('participantJoined', handleSuccess); // Убираем обработчик успеха
|
||||||
|
console.error('❌ Participant join error:', error);
|
||||||
|
reject(new Error(error.message || 'Failed to join as participant'));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.once('participantJoined', handleSuccess);
|
||||||
|
this.socket.once('error', handleError);
|
||||||
|
|
||||||
|
// Отправляем запрос
|
||||||
|
console.log(`📤 Requesting to join as participant: ${name} (${role})`);
|
||||||
|
this.emit('joinAsParticipant', {
|
||||||
|
roomId,
|
||||||
|
roomCode,
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
role
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
startGame(roomId, roomCode, userId) {
|
startGame(roomId, roomCode, userId) {
|
||||||
this.emit('startGame', { roomId, roomCode, userId });
|
this.emit('startGame', { roomId, roomCode, userId });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue