WebSockets & Real-Time Apps
Building a Real-Time Chat Application
Building a Real-Time Chat Application
Building a real-time chat application is one of the most practical applications of WebSockets. In this lesson, we'll create a full-featured chat system with user presence, message broadcasting, typing indicators, and message history.
Chat Application Architecture
A complete chat application consists of several key components:
- User Authentication: Verify users before allowing them to chat
- Connection Management: Track online users and their status
- Message Broadcasting: Distribute messages to all connected users
- Typing Indicators: Show when users are typing
- Message History: Store and retrieve past messages
- User Presence: Display online/offline status
Server-Side Chat Setup
Let's start by setting up the server-side structure for our chat application:
// server.js - Chat Server
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
// Store connected users
const users = new Map(); // socket.id => user object
// Store chat messages in memory (use database in production)
const messages = [];
// Authentication middleware
io.use((socket, next) => {
const username = socket.handshake.auth.username;
if (!username) {
return next(new Error('Username required'));
}
socket.username = username;
next();
});
io.on('connection', (socket) => {
console.log(`User ${socket.username} connected`);
// Add user to online users
users.set(socket.id, {
id: socket.id,
username: socket.username,
joinedAt: Date.now()
});
// Send user their socket ID
socket.emit('connected', {
id: socket.id,
username: socket.username
});
// Notify all users that someone joined
socket.broadcast.emit('userJoined', {
id: socket.id,
username: socket.username,
timestamp: Date.now()
});
// Send current online users list
socket.emit('onlineUsers', Array.from(users.values()));
// Send message history
socket.emit('messageHistory', messages);
// Handle new messages
socket.on('sendMessage', (message) => {
const messageData = {
id: Date.now() + Math.random(),
userId: socket.id,
username: socket.username,
message: message,
timestamp: Date.now()
};
// Store message
messages.push(messageData);
// Broadcast to all users including sender
io.emit('newMessage', messageData);
});
// Handle typing indicator
socket.on('typing', () => {
socket.broadcast.emit('userTyping', {
username: socket.username,
userId: socket.id
});
});
socket.on('stopTyping', () => {
socket.broadcast.emit('userStoppedTyping', {
userId: socket.id
});
});
// Handle disconnection
socket.on('disconnect', () => {
console.log(`User ${socket.username} disconnected`);
// Remove user from online list
users.delete(socket.id);
// Notify others that user left
socket.broadcast.emit('userLeft', {
id: socket.id,
username: socket.username,
timestamp: Date.now()
});
});
});
const PORT = 3000;
http.listen(PORT, () => {
console.log(`Chat server running on port ${PORT}`);
});
Note: This example stores messages in memory. In production, use a database like MongoDB or PostgreSQL to persist chat history.
Client-Side Chat Interface
Now let's create the HTML structure for the chat interface:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Real-Time Chat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #f0f2f5; }
.chat-container {
max-width: 800px;
margin: 20px auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: flex;
height: 600px;
}
.users-panel {
width: 200px;
border-right: 1px solid #e0e0e0;
padding: 20px;
}
.users-panel h3 {
margin-bottom: 15px;
color: #333;
}
.user-item {
padding: 8px;
margin: 5px 0;
background: #f5f5f5;
border-radius: 4px;
font-size: 14px;
}
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 20px;
border-bottom: 1px solid #e0e0e0;
background: #0084ff;
color: white;
}
.messages {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.message {
margin: 10px 0;
padding: 10px;
background: #f1f3f4;
border-radius: 8px;
max-width: 70%;
}
.message.own {
margin-left: auto;
background: #0084ff;
color: white;
}
.message-header {
font-size: 12px;
font-weight: bold;
margin-bottom: 5px;
}
.message-text {
font-size: 14px;
}
.message-time {
font-size: 11px;
opacity: 0.7;
margin-top: 5px;
}
.typing-indicator {
padding: 10px 20px;
font-size: 13px;
font-style: italic;
color: #666;
height: 30px;
}
.input-area {
padding: 20px;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 10px;
}
#messageInput {
flex: 1;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 20px;
outline: none;
font-size: 14px;
}
#sendButton {
padding: 12px 30px;
background: #0084ff;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-weight: bold;
}
#sendButton:hover {
background: #0073e6;
}
.system-message {
text-align: center;
color: #666;
font-size: 13px;
margin: 10px 0;
font-style: italic;
}
</style>
</head>
<body>
<div class="chat-container">
<div class="users-panel">
<h3>Online Users</h3>
<div id="usersList"></div>
</div>
<div class="chat-panel">
<div class="chat-header">
<h2>Chat Room</h2>
</div>
<div id="messages" class="messages"></div>
<div id="typingIndicator" class="typing-indicator"></div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="Type a message...">
<button id="sendButton">Send</button>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="chat.js"></script>
</body>
</html>
Client-Side Chat Logic
Implement the JavaScript logic to handle chat functionality:
// chat.js - Client-Side Chat Logic
let socket;
let currentUser;
let typingTimeout;
let usersTyping = new Set();
// Get username from user
const username = prompt('Enter your username:') || `User${Math.floor(Math.random() * 1000)}`;
// Connect to server
socket = io('http://localhost:3000', {
auth: {
username: username
}
});
// DOM elements
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
const usersList = document.getElementById('usersList');
const typingIndicator = document.getElementById('typingIndicator');
// Connection established
socket.on('connected', (user) => {
currentUser = user;
console.log('Connected as:', user);
});
// Display online users
socket.on('onlineUsers', (users) => {
usersList.innerHTML = users.map(user =>
`<div class="user-item">${user.username}</div>`
).join('');
});
// Load message history
socket.on('messageHistory', (history) => {
history.forEach(msg => displayMessage(msg));
scrollToBottom();
});
// New message received
socket.on('newMessage', (message) => {
displayMessage(message);
scrollToBottom();
});
// User joined notification
socket.on('userJoined', (user) => {
displaySystemMessage(`${user.username} joined the chat`);
// Add user to online list
const userDiv = document.createElement('div');
userDiv.className = 'user-item';
userDiv.id = `user-${user.id}`;
userDiv.textContent = user.username;
usersList.appendChild(userDiv);
});
// User left notification
socket.on('userLeft', (user) => {
displaySystemMessage(`${user.username} left the chat`);
// Remove user from online list
const userDiv = document.getElementById(`user-${user.id}`);
if (userDiv) userDiv.remove();
// Remove from typing indicator if present
usersTyping.delete(user.userId);
updateTypingIndicator();
});
// Typing indicator
socket.on('userTyping', (user) => {
usersTyping.add(user.username);
updateTypingIndicator();
});
socket.on('userStoppedTyping', (user) => {
usersTyping.delete(user.username);
updateTypingIndicator();
});
// Send message
function sendMessage() {
const message = messageInput.value.trim();
if (message) {
socket.emit('sendMessage', message);
messageInput.value = '';
socket.emit('stopTyping');
}
}
// Display message in chat
function displayMessage(message) {
const messageDiv = document.createElement('div');
messageDiv.className = message.userId === currentUser.id ? 'message own' : 'message';
const time = new Date(message.timestamp).toLocaleTimeString();
messageDiv.innerHTML = `
<div class="message-header">${message.username}</div>
<div class="message-text">${escapeHtml(message.message)}</div>
<div class="message-time">${time}</div>
`;
messagesDiv.appendChild(messageDiv);
}
// Display system message
function displaySystemMessage(text) {
const messageDiv = document.createElement('div');
messageDiv.className = 'system-message';
messageDiv.textContent = text;
messagesDiv.appendChild(messageDiv);
}
// Update typing indicator
function updateTypingIndicator() {
if (usersTyping.size === 0) {
typingIndicator.textContent = '';
} else if (usersTyping.size === 1) {
typingIndicator.textContent = `${Array.from(usersTyping)[0]} is typing...`;
} else {
typingIndicator.textContent = `${usersTyping.size} people are typing...`;
}
}
// Handle typing detection
messageInput.addEventListener('input', () => {
socket.emit('typing');
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('stopTyping');
}, 2000);
});
// Send message on button click
sendButton.addEventListener('click', sendMessage);
// Send message on Enter key
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
// Utility functions
function scrollToBottom() {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
Tip: Always escape user-generated content before displaying it in HTML to prevent XSS (Cross-Site Scripting) attacks.
Adding Timestamps and Formatting
Enhance messages with better timestamp formatting:
// Utility functions for time formatting
function formatTimestamp(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
// Less than 1 minute
if (diff < 60000) {
return 'Just now';
}
// Less than 1 hour
if (diff < 3600000) {
const minutes = Math.floor(diff / 60000);
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
}
// Today
if (date.toDateString() === now.toDateString()) {
return date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
}
// Yesterday
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === yesterday.toDateString()) {
return 'Yesterday ' + date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
}
// Older
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
}
Message Persistence with Database
In production, store messages in a database for persistence:
// Using MongoDB with Mongoose
const mongoose = require('mongoose');
const messageSchema = new mongoose.Schema({
userId: String,
username: String,
message: String,
timestamp: { type: Date, default: Date.now }
});
const Message = mongoose.model('Message', messageSchema);
// Connect to MongoDB
mongoose.connect('mongodb://localhost/chat', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// Save message to database
socket.on('sendMessage', async (message) => {
const newMessage = new Message({
userId: socket.id,
username: socket.username,
message: message
});
await newMessage.save();
io.emit('newMessage', {
id: newMessage._id,
userId: newMessage.userId,
username: newMessage.username,
message: newMessage.message,
timestamp: newMessage.timestamp
});
});
// Load message history from database
socket.on('connection', async (socket) => {
// Get last 100 messages
const messages = await Message.find()
.sort({ timestamp: -1 })
.limit(100)
.exec();
socket.emit('messageHistory', messages.reverse());
});
Exercise: Enhance the chat application with these features:
- Add emoji support using an emoji picker library
- Implement message editing (double-click to edit your own messages)
- Add message deletion (right-click menu for your own messages)
- Display user avatars next to messages (use Gravatar or a placeholder service)
- Add "read receipts" showing when messages are seen by others
- Implement a "scroll to bottom" button that appears when scrolled up
- Add sound notifications for new messages
- Create a search functionality to find old messages
Test the application with multiple browser windows to simulate different users.