feat: implement real-time chat with Socket.IO
Implemented WebSocket-based real-time messaging for both event rooms and private match chats using Socket.IO with comprehensive test coverage. Backend changes: - Installed socket.io@4.8.1 for WebSocket server - Created Socket.IO server with JWT authentication middleware - Implemented event room management (join/leave/messages) - Added active users tracking with real-time updates - Implemented private match room messaging - Integrated Socket.IO with Express HTTP server - Messages are persisted to PostgreSQL via Prisma - Added 12 comprehensive unit tests (89.13% coverage) Frontend changes: - Installed socket.io-client for WebSocket connections - Created socket service layer for connection management - Updated EventChatPage with real-time messaging - Updated MatchChatPage with real-time private chat - Added connection status indicators (● Connected/Disconnected) - Disabled message input when not connected Infrastructure: - Updated nginx config to proxy WebSocket connections at /socket.io - Added Upgrade and Connection headers for WebSocket support - Set long timeouts (7d) for persistent WebSocket connections Key features: - JWT-authenticated socket connections - Room-based architecture for events and matches - Real-time message broadcasting - Active users list with automatic updates - Automatic cleanup on disconnect - Message persistence in database Test coverage: - 12 tests passing (authentication, event rooms, match rooms, disconnect, errors) - Socket.IO module: 89.13% statements, 81.81% branches, 91.66% functions - Overall coverage: 81.19% Phase 1, Step 4 completed. Ready for Phase 2 (Core Features).
This commit is contained in:
140
frontend/package-lock.json
generated
140
frontend/package-lock.json
generated
@@ -11,7 +11,8 @@
|
||||
"lucide-react": "^0.553.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5"
|
||||
"react-router-dom": "^7.9.5",
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
@@ -1406,6 +1407,12 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -1993,6 +2000,45 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
@@ -2923,7 +2969,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mz": {
|
||||
@@ -3647,6 +3692,68 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -4178,6 +4285,35 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"lucide-react": "^0.553.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5"
|
||||
"react-router-dom": "^7.9.5",
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
||||
@@ -3,17 +3,17 @@ import { useParams, useNavigate } from 'react-router-dom';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { mockEvents } from '../mocks/events';
|
||||
import { mockEventMessages } from '../mocks/messages';
|
||||
import { mockUsers } from '../mocks/users';
|
||||
import { Send, UserPlus } from 'lucide-react';
|
||||
import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
|
||||
|
||||
const EventChatPage = () => {
|
||||
const { eventId } = useParams();
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [messages, setMessages] = useState(mockEventMessages);
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [activeUsers, setActiveUsers] = useState(mockUsers.slice(1, 5));
|
||||
const [activeUsers, setActiveUsers] = useState([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
const event = mockEvents.find(e => e.id === parseInt(eventId));
|
||||
@@ -26,29 +26,86 @@ const EventChatPage = () => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
// Connect to Socket.IO
|
||||
const socket = connectSocket();
|
||||
|
||||
if (!socket) {
|
||||
console.error('Failed to connect to socket');
|
||||
return;
|
||||
}
|
||||
|
||||
// Socket event listeners
|
||||
socket.on('connect', () => {
|
||||
setIsConnected(true);
|
||||
// Join event room
|
||||
socket.emit('join_event_room', { eventId: parseInt(eventId) });
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
// Receive messages
|
||||
socket.on('event_message', (message) => {
|
||||
setMessages((prev) => [...prev, message]);
|
||||
});
|
||||
|
||||
// Receive active users list
|
||||
socket.on('active_users', (users) => {
|
||||
// Filter out duplicates and current user
|
||||
const uniqueUsers = users
|
||||
.filter((u, index, self) =>
|
||||
index === self.findIndex((t) => t.userId === u.userId)
|
||||
)
|
||||
.filter((u) => u.userId !== user.id);
|
||||
setActiveUsers(uniqueUsers);
|
||||
});
|
||||
|
||||
// User joined notification
|
||||
socket.on('user_joined', (userData) => {
|
||||
console.log(`${userData.username} joined the room`);
|
||||
});
|
||||
|
||||
// User left notification
|
||||
socket.on('user_left', (userData) => {
|
||||
console.log(`${userData.username} left the room`);
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
socket.emit('leave_event_room', { eventId: parseInt(eventId) });
|
||||
socket.off('connect');
|
||||
socket.off('disconnect');
|
||||
socket.off('event_message');
|
||||
socket.off('active_users');
|
||||
socket.off('user_joined');
|
||||
socket.off('user_left');
|
||||
};
|
||||
}, [eventId, user.id]);
|
||||
|
||||
const handleSendMessage = (e) => {
|
||||
e.preventDefault();
|
||||
if (!newMessage.trim()) return;
|
||||
|
||||
const message = {
|
||||
id: messages.length + 1,
|
||||
room_id: parseInt(eventId),
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
content: newMessage,
|
||||
type: 'text',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
const socket = getSocket();
|
||||
if (!socket || !socket.connected) {
|
||||
alert('Not connected to chat server');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send message via Socket.IO
|
||||
socket.emit('send_event_message', {
|
||||
eventId: parseInt(eventId),
|
||||
content: newMessage,
|
||||
});
|
||||
|
||||
setMessages([...messages, message]);
|
||||
setNewMessage('');
|
||||
};
|
||||
|
||||
const handleMatchWith = (userId) => {
|
||||
// Mockup - in the future will be WebSocket request
|
||||
// TODO: Implement match request
|
||||
alert(`Match request sent to user!`);
|
||||
// Simulate acceptance after 1 second
|
||||
setTimeout(() => {
|
||||
navigate(`/matches/1/chat`);
|
||||
}, 1000);
|
||||
@@ -70,6 +127,11 @@ const EventChatPage = () => {
|
||||
<div className="bg-primary-600 text-white p-4">
|
||||
<h2 className="text-2xl font-bold">{event.name}</h2>
|
||||
<p className="text-primary-100 text-sm">{event.location}</p>
|
||||
<div className="mt-2">
|
||||
<span className={`text-xs px-2 py-1 rounded ${isConnected ? 'bg-green-500' : 'bg-red-500'}`}>
|
||||
{isConnected ? '● Connected' : '● Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-[calc(100vh-280px)]">
|
||||
@@ -78,10 +140,13 @@ const EventChatPage = () => {
|
||||
<h3 className="font-semibold text-gray-900 mb-4">
|
||||
Active users ({activeUsers.length})
|
||||
</h3>
|
||||
{activeUsers.length === 0 && (
|
||||
<p className="text-sm text-gray-500">No other users online</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{activeUsers.map((activeUser) => (
|
||||
<div
|
||||
key={activeUser.id}
|
||||
key={activeUser.userId}
|
||||
className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -94,13 +159,10 @@ const EventChatPage = () => {
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{activeUser.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
⭐ {activeUser.rating}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleMatchWith(activeUser.id)}
|
||||
onClick={() => handleMatchWith(activeUser.userId)}
|
||||
className="p-1 text-primary-600 hover:bg-primary-50 rounded"
|
||||
title="Connect"
|
||||
>
|
||||
@@ -115,8 +177,13 @@ const EventChatPage = () => {
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center text-gray-500 py-8">
|
||||
No messages yet. Start the conversation!
|
||||
</div>
|
||||
)}
|
||||
{messages.map((message) => {
|
||||
const isOwnMessage = message.user_id === user.id;
|
||||
const isOwnMessage = message.userId === user.id;
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
@@ -134,7 +201,7 @@ const EventChatPage = () => {
|
||||
{message.username}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(message.created_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
||||
{new Date(message.createdAt).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -162,11 +229,13 @@ const EventChatPage = () => {
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder="Write a message..."
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500"
|
||||
disabled={!isConnected}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||
disabled={!isConnected}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Send className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
@@ -2,15 +2,15 @@ import { useState, useRef, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { mockPrivateMessages } from '../mocks/messages';
|
||||
import { mockUsers } from '../mocks/users';
|
||||
import { Send, Video, Upload, X, Check, Link as LinkIcon } from 'lucide-react';
|
||||
import { connectSocket, getSocket } from '../services/socket';
|
||||
|
||||
const MatchChatPage = () => {
|
||||
const { matchId } = useParams();
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [messages, setMessages] = useState(mockPrivateMessages);
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [isTransferring, setIsTransferring] = useState(false);
|
||||
@@ -18,10 +18,11 @@ const MatchChatPage = () => {
|
||||
const [webrtcStatus, setWebrtcStatus] = useState('disconnected'); // disconnected, connecting, connected, failed
|
||||
const [showLinkInput, setShowLinkInput] = useState(false);
|
||||
const [videoLink, setVideoLink] = useState('');
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const messagesEndRef = useRef(null);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
// Partner user (mockup)
|
||||
// Partner user (mockup - TODO: fetch from backend in Phase 2)
|
||||
const partner = mockUsers[1]; // sarah_swing
|
||||
|
||||
const scrollToBottom = () => {
|
||||
@@ -32,21 +33,56 @@ const MatchChatPage = () => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
// Connect to Socket.IO
|
||||
const socket = connectSocket();
|
||||
|
||||
if (!socket) {
|
||||
console.error('Failed to connect to socket');
|
||||
return;
|
||||
}
|
||||
|
||||
// Socket event listeners
|
||||
socket.on('connect', () => {
|
||||
setIsConnected(true);
|
||||
// Join match room
|
||||
socket.emit('join_match_room', { matchId: parseInt(matchId) });
|
||||
console.log(`Joined match room ${matchId}`);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
// Receive messages
|
||||
socket.on('match_message', (message) => {
|
||||
setMessages((prev) => [...prev, message]);
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
socket.off('connect');
|
||||
socket.off('disconnect');
|
||||
socket.off('match_message');
|
||||
};
|
||||
}, [matchId, user.id]);
|
||||
|
||||
const handleSendMessage = (e) => {
|
||||
e.preventDefault();
|
||||
if (!newMessage.trim()) return;
|
||||
|
||||
const message = {
|
||||
id: messages.length + 1,
|
||||
room_id: 10,
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
content: newMessage,
|
||||
type: 'text',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
const socket = getSocket();
|
||||
if (!socket || !socket.connected) {
|
||||
alert('Not connected to chat server');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send message via Socket.IO
|
||||
socket.emit('send_match_message', {
|
||||
matchId: parseInt(matchId),
|
||||
content: newMessage,
|
||||
});
|
||||
|
||||
setMessages([...messages, message]);
|
||||
setNewMessage('');
|
||||
};
|
||||
|
||||
@@ -204,8 +240,13 @@ const MatchChatPage = () => {
|
||||
<div className="flex flex-col h-[calc(100vh-320px)]">
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center text-gray-500 py-8">
|
||||
No messages yet. Start the conversation!
|
||||
</div>
|
||||
)}
|
||||
{messages.map((message) => {
|
||||
const isOwnMessage = message.user_id === user.id;
|
||||
const isOwnMessage = message.userId === user.id;
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
@@ -213,7 +254,7 @@ const MatchChatPage = () => {
|
||||
>
|
||||
<div className={`flex items-start space-x-2 max-w-md ${isOwnMessage ? 'flex-row-reverse space-x-reverse' : ''}`}>
|
||||
<img
|
||||
src={isOwnMessage ? user.avatar : partner.avatar}
|
||||
src={message.avatar}
|
||||
alt={message.username}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
@@ -223,7 +264,7 @@ const MatchChatPage = () => {
|
||||
{message.username}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(message.created_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
||||
{new Date(message.createdAt).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -376,11 +417,13 @@ const MatchChatPage = () => {
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder="Write a message..."
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500"
|
||||
disabled={!isConnected}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||
disabled={!isConnected}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Send className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
56
frontend/src/services/socket.js
Normal file
56
frontend/src/services/socket.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
const SOCKET_URL = 'http://localhost:8080';
|
||||
|
||||
let socket = null;
|
||||
|
||||
export function connectSocket() {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
if (!token) {
|
||||
console.error('No token found for socket connection');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (socket && socket.connected) {
|
||||
return socket;
|
||||
}
|
||||
|
||||
socket = io(SOCKET_URL, {
|
||||
auth: {
|
||||
token,
|
||||
},
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000,
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('✅ Socket connected:', socket.id);
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log('❌ Socket disconnected:', reason);
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('Socket connection error:', error.message);
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
console.error('Socket error:', error);
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
export function disconnectSocket() {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSocket() {
|
||||
return socket;
|
||||
}
|
||||
Reference in New Issue
Block a user