WebSockets & Real-Time Apps
Chat Rooms and Private Messages
Chat Rooms and Private Messages
Extending chat functionality with rooms allows users to organize conversations by topics, while private messaging enables one-on-one communication. These features are essential for building scalable and flexible chat applications.
Understanding Socket.io Rooms
Rooms are server-side channels that sockets can join and leave. They allow you to broadcast messages to specific groups of users:
- Automatic Rooms: Each socket automatically joins a room identified by its socket ID
- Custom Rooms: You can create and join any number of named rooms
- Broadcasting: Send messages to all sockets in a specific room
- Dynamic: Users can join and leave rooms at any time
Server-Side Room Management
Implement the server-side logic for creating and managing chat rooms:
// server.js - Chat Rooms Server
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
// Store room information
const rooms = new Map(); // roomId => { name, users: Set, messages: [] }
// Store user information
const users = new Map(); // socketId => { id, username, currentRoom }
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
// Register user
users.set(socket.id, {
id: socket.id,
username: socket.handshake.auth.username || `User${socket.id.substring(0, 4)}`,
currentRoom: null
});
// Send available rooms
socket.emit('roomsList', Array.from(rooms.entries()).map(([id, room]) => ({
id,
name: room.name,
userCount: room.users.size
})));
// Create new room
socket.on('createRoom', (roomName, callback) => {
const roomId = `room_${Date.now()}`;
rooms.set(roomId, {
name: roomName,
users: new Set(),
messages: [],
createdAt: Date.now(),
createdBy: users.get(socket.id).username
});
console.log(`Room created: ${roomName} (${roomId})`);
// Notify all users about new room
io.emit('roomCreated', {
id: roomId,
name: roomName,
userCount: 0
});
callback({ success: true, roomId });
});
// Join room
socket.on('joinRoom', (roomId, callback) => {
const user = users.get(socket.id);
const room = rooms.get(roomId);
if (!room) {
return callback({ success: false, error: 'Room not found' });
}
// Leave current room if in one
if (user.currentRoom) {
socket.leave(user.currentRoom);
const oldRoom = rooms.get(user.currentRoom);
if (oldRoom) {
oldRoom.users.delete(socket.id);
// Notify room that user left
socket.to(user.currentRoom).emit('userLeftRoom', {
username: user.username,
userId: socket.id
});
}
}
// Join new room
socket.join(roomId);
room.users.add(socket.id);
user.currentRoom = roomId;
console.log(`${user.username} joined room: ${room.name}`);
// Send room info and history to user
callback({
success: true,
room: {
id: roomId,
name: room.name,
messages: room.messages,
users: Array.from(room.users).map(id => ({
id,
username: users.get(id)?.username
}))
}
});
// Notify room that user joined
socket.to(roomId).emit('userJoinedRoom', {
username: user.username,
userId: socket.id,
timestamp: Date.now()
});
// Update room user count for everyone
io.emit('roomUpdated', {
id: roomId,
userCount: room.users.size
});
});
// Send message to room
socket.on('roomMessage', (message, callback) => {
const user = users.get(socket.id);
if (!user.currentRoom) {
return callback({ success: false, error: 'Not in a room' });
}
const room = rooms.get(user.currentRoom);
if (!room) {
return callback({ success: false, error: 'Room not found' });
}
const messageData = {
id: Date.now() + Math.random(),
userId: socket.id,
username: user.username,
message: message,
timestamp: Date.now()
};
// Store message in room history
room.messages.push(messageData);
// Broadcast to all users in room
io.to(user.currentRoom).emit('roomMessage', messageData);
callback({ success: true });
});
// Leave room
socket.on('leaveRoom', () => {
const user = users.get(socket.id);
if (user.currentRoom) {
const room = rooms.get(user.currentRoom);
socket.leave(user.currentRoom);
room.users.delete(socket.id);
// Notify room
socket.to(user.currentRoom).emit('userLeftRoom', {
username: user.username,
userId: socket.id
});
// Update room count
io.emit('roomUpdated', {
id: user.currentRoom,
userCount: room.users.size
});
user.currentRoom = null;
}
});
// Delete room (only creator can delete)
socket.on('deleteRoom', (roomId, callback) => {
const room = rooms.get(roomId);
if (!room) {
return callback({ success: false, error: 'Room not found' });
}
const user = users.get(socket.id);
if (room.createdBy !== user.username) {
return callback({ success: false, error: 'Only room creator can delete' });
}
// Kick all users from room
room.users.forEach(userId => {
const userSocket = io.sockets.sockets.get(userId);
if (userSocket) {
userSocket.leave(roomId);
users.get(userId).currentRoom = null;
}
});
// Notify all users room was deleted
io.emit('roomDeleted', { id: roomId, name: room.name });
// Delete room
rooms.delete(roomId);
callback({ success: true });
});
// Handle disconnect
socket.on('disconnect', () => {
const user = users.get(socket.id);
if (user && user.currentRoom) {
const room = rooms.get(user.currentRoom);
if (room) {
room.users.delete(socket.id);
socket.to(user.currentRoom).emit('userLeftRoom', {
username: user.username,
userId: socket.id
});
io.emit('roomUpdated', {
id: user.currentRoom,
userCount: room.users.size
});
}
}
users.delete(socket.id);
});
});
const PORT = 3000;
http.listen(PORT, () => {
console.log(`Chat server running on port ${PORT}`);
});
Note: Each socket can only be in one room at a time in this implementation. To support multiple rooms, modify the user object to store an array of rooms.
Private Messaging Implementation
Private messages are sent directly between two users without broadcasting to a room:
// Add to server.js - Private Messaging
io.on('connection', (socket) => {
// ... previous code ...
// Send private message
socket.on('privateMessage', ({ recipientId, message }, callback) => {
const sender = users.get(socket.id);
const recipient = users.get(recipientId);
if (!recipient) {
return callback({ success: false, error: 'User not found' });
}
const messageData = {
id: Date.now() + Math.random(),
senderId: socket.id,
senderUsername: sender.username,
recipientId: recipientId,
recipientUsername: recipient.username,
message: message,
timestamp: Date.now(),
read: false
};
// Send to recipient
io.to(recipientId).emit('privateMessage', messageData);
// Send confirmation to sender
socket.emit('privateMessageSent', messageData);
callback({ success: true, messageId: messageData.id });
});
// Mark private message as read
socket.on('markMessageRead', (messageId) => {
// In production, update database
io.emit('messageRead', {
messageId,
readBy: socket.id,
readAt: Date.now()
});
});
// Get online users for private messaging
socket.on('getOnlineUsers', (callback) => {
const onlineUsers = Array.from(users.values())
.filter(user => user.id !== socket.id)
.map(user => ({
id: user.id,
username: user.username,
currentRoom: user.currentRoom
}));
callback(onlineUsers);
});
});
Client-Side Room Interface
Create the HTML interface for rooms and private messages:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat Rooms</title>
<style>
.container {
display: flex;
height: 100vh;
font-family: Arial, sans-serif;
}
.sidebar {
width: 250px;
background: #2c3e50;
color: white;
padding: 20px;
overflow-y: auto;
}
.sidebar h3 {
margin-bottom: 15px;
}
.room-item {
padding: 10px;
margin: 5px 0;
background: #34495e;
border-radius: 5px;
cursor: pointer;
display: flex;
justify-content: space-between;
}
.room-item:hover {
background: #3d5a75;
}
.room-item.active {
background: #3498db;
}
.user-count {
background: #e74c3c;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
}
.create-room-btn {
width: 100%;
padding: 10px;
margin-top: 10px;
background: #27ae60;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.main-chat {
flex: 1;
display: flex;
flex-direction: column;
background: #ecf0f1;
}
.chat-header {
padding: 20px;
background: white;
border-bottom: 2px solid #bdc3c7;
display: flex;
justify-content: space-between;
align-items: center;
}
.leave-room-btn {
padding: 8px 15px;
background: #e74c3c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.messages {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.message {
margin: 10px 0;
padding: 10px 15px;
background: white;
border-radius: 8px;
max-width: 60%;
}
.message.own {
margin-left: auto;
background: #3498db;
color: white;
}
.message.private {
background: #9b59b6;
color: white;
}
.input-area {
padding: 20px;
background: white;
border-top: 2px solid #bdc3c7;
display: flex;
gap: 10px;
}
.users-panel {
width: 200px;
background: #ecf0f1;
padding: 20px;
border-left: 2px solid #bdc3c7;
}
.user-item {
padding: 8px;
margin: 5px 0;
background: white;
border-radius: 5px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-item:hover {
background: #3498db;
color: white;
}
.pm-badge {
background: #9b59b6;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 11px;
}
</style>
</head>
<body>
<div class="container">
<div class="sidebar">
<h3>Chat Rooms</h3>
<div id="roomsList"></div>
<button class="create-room-btn" onclick="createRoom()">Create Room</button>
</div>
<div class="main-chat">
<div class="chat-header">
<h2 id="roomTitle">Select a room</h2>
<button class="leave-room-btn" onclick="leaveRoom()" style="display:none" id="leaveBtn">Leave Room</button>
</div>
<div id="messages" class="messages"></div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="Type a message..." style="flex:1;padding:10px;border:1px solid #bdc3c7;border-radius:5px;">
<button onclick="sendMessage()" style="padding:10px 20px;background:#3498db;color:white;border:none;border-radius:5px;cursor:pointer;">Send</button>
</div>
</div>
<div class="users-panel">
<h3>Room Users</h3>
<div id="roomUsers"></div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="rooms.js"></script>
</body>
</html>
Client-Side Room Logic
Implement the JavaScript for room and private message functionality:
// rooms.js - Client-Side Room Logic
const username = prompt('Enter your username:') || `User${Math.floor(Math.random() * 1000)}`;
const socket = io('http://localhost:3000', { auth: { username } });
let currentRoom = null;
let currentUserId = null;
// DOM elements
const roomsList = document.getElementById('roomsList');
const messages = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const roomTitle = document.getElementById('roomTitle');
const leaveBtn = document.getElementById('leaveBtn');
const roomUsers = document.getElementById('roomUsers');
// Receive available rooms
socket.on('roomsList', (rooms) => {
displayRooms(rooms);
});
// Room created
socket.on('roomCreated', (room) => {
addRoomToList(room);
});
// Room updated
socket.on('roomUpdated', (update) => {
const roomItem = document.querySelector(`[data-room-id="${update.id}"]`);
if (roomItem) {
const badge = roomItem.querySelector('.user-count');
if (badge) badge.textContent = update.userCount;
}
});
// Room deleted
socket.on('roomDeleted', (room) => {
const roomItem = document.querySelector(`[data-room-id="${room.id}"]`);
if (roomItem) roomItem.remove();
if (currentRoom === room.id) {
leaveRoom();
alert(`Room "${room.name}" was deleted`);
}
});
// User joined room
socket.on('userJoinedRoom', (user) => {
displaySystemMessage(`${user.username} joined the room`);
addUserToList(user);
});
// User left room
socket.on('userLeftRoom', (user) => {
displaySystemMessage(`${user.username} left the room`);
removeUserFromList(user.userId);
});
// Room message received
socket.on('roomMessage', (message) => {
displayMessage(message, false);
});
// Private message received
socket.on('privateMessage', (message) => {
displayMessage(message, true);
});
// Display rooms
function displayRooms(rooms) {
roomsList.innerHTML = rooms.map(room =>
`<div class="room-item" data-room-id="${room.id}" onclick="joinRoom('${room.id}')">
<span>${room.name}</span>
<span class="user-count">${room.userCount}</span>
</div>`
).join('');
}
function addRoomToList(room) {
const roomItem = document.createElement('div');
roomItem.className = 'room-item';
roomItem.dataset.roomId = room.id;
roomItem.onclick = () => joinRoom(room.id);
roomItem.innerHTML = `
<span>${room.name}</span>
<span class="user-count">${room.userCount}</span>
`;
roomsList.appendChild(roomItem);
}
// Create room
function createRoom() {
const roomName = prompt('Enter room name:');
if (!roomName) return;
socket.emit('createRoom', roomName, (response) => {
if (response.success) {
joinRoom(response.roomId);
} else {
alert('Failed to create room: ' + response.error);
}
});
}
// Join room
function joinRoom(roomId) {
socket.emit('joinRoom', roomId, (response) => {
if (response.success) {
currentRoom = roomId;
roomTitle.textContent = response.room.name;
leaveBtn.style.display = 'block';
// Clear and display messages
messages.innerHTML = '';
response.room.messages.forEach(msg => displayMessage(msg, false));
// Display users
displayRoomUsers(response.room.users);
// Highlight active room
document.querySelectorAll('.room-item').forEach(item => {
item.classList.toggle('active', item.dataset.roomId === roomId);
});
} else {
alert('Failed to join room: ' + response.error);
}
});
}
// Leave room
function leaveRoom() {
if (currentRoom) {
socket.emit('leaveRoom');
currentRoom = null;
roomTitle.textContent = 'Select a room';
leaveBtn.style.display = 'none';
messages.innerHTML = '';
roomUsers.innerHTML = '';
document.querySelectorAll('.room-item').forEach(item => {
item.classList.remove('active');
});
}
}
// Send message
function sendMessage() {
const message = messageInput.value.trim();
if (!message || !currentRoom) return;
socket.emit('roomMessage', message, (response) => {
if (response.success) {
messageInput.value = '';
} else {
alert('Failed to send message: ' + response.error);
}
});
}
// Send private message
function sendPrivateMessage(recipientId, recipientUsername) {
const message = prompt(`Send private message to ${recipientUsername}:`);
if (!message) return;
socket.emit('privateMessage', { recipientId, message }, (response) => {
if (response.success) {
displayMessage({
username: 'You (private)',
message,
timestamp: Date.now()
}, true);
} else {
alert('Failed to send private message: ' + response.error);
}
});
}
// Display message
function displayMessage(message, isPrivate) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
if (isPrivate) messageDiv.classList.add('private');
const time = new Date(message.timestamp).toLocaleTimeString();
messageDiv.innerHTML = `
<strong>${message.username || message.senderUsername}</strong>
${isPrivate ? '<span class="pm-badge">PM</span>' : ''}
<div>${escapeHtml(message.message)}</div>
<small style="opacity:0.7">${time}</small>
`;
messages.appendChild(messageDiv);
messages.scrollTop = messages.scrollHeight;
}
// Display system message
function displaySystemMessage(text) {
const messageDiv = document.createElement('div');
messageDiv.style.textAlign = 'center';
messageDiv.style.color = '#7f8c8d';
messageDiv.style.margin = '10px 0';
messageDiv.textContent = text;
messages.appendChild(messageDiv);
}
// Display room users
function displayRoomUsers(users) {
roomUsers.innerHTML = users.map(user =>
`<div class="user-item" onclick="sendPrivateMessage('${user.id}', '${user.username}')">
<span>${user.username}</span>
<span class="pm-badge">PM</span>
</div>`
).join('');
}
function addUserToList(user) {
const userDiv = document.createElement('div');
userDiv.className = 'user-item';
userDiv.id = `user-${user.userId}`;
userDiv.onclick = () => sendPrivateMessage(user.userId, user.username);
userDiv.innerHTML = `
<span>${user.username}</span>
<span class="pm-badge">PM</span>
`;
roomUsers.appendChild(userDiv);
}
function removeUserFromList(userId) {
const userDiv = document.getElementById(`user-${userId}`);
if (userDiv) userDiv.remove();
}
// Utility
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Send on Enter
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
Tip: For production applications, store private messages in a database and implement a proper inbox/messaging system with read receipts and message history.
Warning: Always validate and sanitize user input on the server side. Never trust client-side validation alone, especially for room names and messages.
Exercise: Enhance the room and messaging system with:
- Persistent rooms stored in a database (MongoDB or PostgreSQL)
- Room administrators with the ability to kick users
- Private message history with a dedicated inbox interface
- Unread message badges showing count of unread private messages
- Room search functionality to find rooms by name
- Password-protected private rooms
- File sharing within rooms (images, documents)
- Voice/video call initiation through private messages
Test with multiple users creating rooms, joining, leaving, and exchanging both room and private messages.