WebSockets & Real-Time Apps

Chat Rooms and Private Messages

18 min Lesson 14 of 35

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:
  1. Persistent rooms stored in a database (MongoDB or PostgreSQL)
  2. Room administrators with the ability to kick users
  3. Private message history with a dedicated inbox interface
  4. Unread message badges showing count of unread private messages
  5. Room search functionality to find rooms by name
  6. Password-protected private rooms
  7. File sharing within rooms (images, documents)
  8. Voice/video call initiation through private messages

Test with multiple users creating rooms, joining, leaving, and exchanging both room and private messages.