🎯 Recommended Samples
Balanced sample collections from various categories for you to explore
Pusher Samples
Real-time communication examples using Pusher APIs including channels, events, presence, and client authentication
💻 Basic Real-time Chat with Pusher javascript
🟢 simple
⭐⭐
Simple but powerful chat application using Pusher public and private channels with authentication
⏱️ 20 min
🏷️ pusher, chat, client, real-time
Prerequisites:
Pusher account, JavaScript ES6+, HTTP basics
// Basic Real-time Chat Application using Pusher
// Client-side implementation
// Initialize Pusher with your app credentials
const pusher = new Pusher('your-pusher-app-key', {
cluster: 'your-pusher-cluster',
forceTLS: true,
authEndpoint: '/api/pusher/auth',
auth: {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
}
});
class ChatApp {
constructor() {
this.currentUser = null;
this.currentChannel = null;
this.messages = [];
this.users = new Map();
this.init();
}
async init() {
// Authenticate user
await this.authenticateUser();
// Connect to Pusher
this.connectToPusher();
// Setup UI event listeners
this.setupUIListeners();
// Subscribe to notifications channel
this.subscribeToNotifications();
}
async authenticateUser() {
try {
const response = await fetch('/api/user/me', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
});
if (response.ok) {
this.currentUser = await response.json();
} else {
// Redirect to login
window.location.href = '/login';
}
} catch (error) {
console.error('Authentication failed:', error);
window.location.href = '/login';
}
}
connectToPusher() {
// Bind to connection events
pusher.connection.bind('connected', () => {
console.log('Connected to Pusher');
this.updateConnectionStatus(true);
});
pusher.connection.bind('disconnected', () => {
console.log('Disconnected from Pusher');
this.updateConnectionStatus(false);
});
pusher.connection.bind('error', (err) => {
console.error('Pusher connection error:', err);
this.showErrorMessage('Connection error. Please refresh.');
});
}
// Subscribe to a chat room (private channel)
joinRoom(roomId) {
if (this.currentChannel) {
this.leaveCurrentRoom();
}
// Subscribe to private channel for the room
this.currentChannel = pusher.subscribe(`private-chat-${roomId}`);
// Bind to chat events
this.currentChannel.bind('pusher:subscription_succeeded', () => {
console.log(`Successfully joined room: ${roomId}`);
this.onRoomJoined(roomId);
});
this.currentChannel.bind('pusher:subscription_error', (err) => {
console.error('Failed to join room:', err);
this.showErrorMessage('Failed to join room');
});
// Listen for new messages
this.currentChannel.bind('new_message', (data) => {
this.addMessage(data);
});
// Listen for user events
this.currentChannel.bind('user_joined', (data) => {
this.onUserJoined(data);
});
this.currentChannel.bind('user_left', (data) => {
this.onUserLeft(data);
});
// Listen for typing indicators
this.currentChannel.bind('user_typing', (data) => {
this.onUserTyping(data);
});
this.currentChannel.bind('user_stop_typing', (data) => {
this.onUserStopTyping(data);
});
}
// Subscribe to presence channel for user presence
subscribeToPresence(roomId) {
const presenceChannel = pusher.subscribe(`presence-room-${roomId}`);
presenceChannel.bind('pusher:subscription_succeeded', (members) => {
console.log('Presence channel joined');
this.updateOnlineUsers(members.members);
});
presenceChannel.bind('pusher:member_added', (member) => {
this.onUserPresenceJoined(member);
});
presenceChannel.bind('pusher:member_removed', (member) => {
this.onUserPresenceLeft(member);
});
// Trigger presence event when joining
presenceChannel.trigger('client-joined', {
user: this.currentUser,
timestamp: new Date()
});
}
// Leave current room
leaveCurrentRoom() {
if (this.currentChannel) {
// Notify others we're leaving
this.currentChannel.trigger('client-leaving', {
user: this.currentUser,
timestamp: new Date()
});
pusher.unsubscribe(this.currentChannel.name);
this.currentChannel = null;
}
}
// Send a message
sendMessage(message, type = 'text') {
if (!this.currentChannel || !message.trim()) return;
const messageData = {
id: this.generateId(),
user: this.currentUser,
message: message.trim(),
type,
timestamp: new Date(),
roomId: this.getRoomId()
};
// Trigger client event (will be received by server and broadcast)
this.currentChannel.trigger('client-send_message', messageData);
// Optimistically add message to UI
this.addMessage(messageData);
}
// Send file
sendFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('roomId', this.getRoomId());
fetch('/api/upload', {
method: 'POST',
body: formData,
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
this.sendMessage({
type: 'file',
url: data.fileUrl,
fileName: file.name,
fileSize: file.size
}, 'file');
} else {
this.showErrorMessage('Failed to upload file');
}
})
.catch(error => {
console.error('File upload error:', error);
this.showErrorMessage('Failed to upload file');
});
}
// Start typing indicator
startTyping() {
if (this.currentChannel && !this.isTyping) {
this.isTyping = true;
this.currentChannel.trigger('client-typing', {
user: this.currentUser,
timestamp: new Date()
});
// Auto-stop typing after 3 seconds
clearTimeout(this.typingTimeout);
this.typingTimeout = setTimeout(() => this.stopTyping(), 3000);
}
}
// Stop typing indicator
stopTyping() {
if (this.currentChannel && this.isTyping) {
this.isTyping = false;
this.currentChannel.trigger('client-stop_typing', {
user: this.currentUser,
timestamp: new Date()
});
clearTimeout(this.typingTimeout);
}
}
// Subscribe to notifications
subscribeToNotifications() {
const notificationChannel = pusher.subscribe(`private-notifications-${this.currentUser.id}`);
notificationChannel.bind('new_notification', (data) => {
this.showNotification(data);
});
notificationChannel.bind('message_mention', (data) => {
this.showMentionNotification(data);
});
}
// Event handlers
onRoomJoined(roomId) {
this.messages = [];
this.updateMessagesList();
// Subscribe to presence for this room
this.subscribeToPresence(roomId);
// Load message history
this.loadMessageHistory(roomId);
}
onUserJoined(data) {
this.addSystemMessage(`${data.user.name} joined the chat`);
}
onUserLeft(data) {
this.addSystemMessage(`${data.user.name} left the chat`);
}
onUserTyping(data) {
this.updateTypingIndicator(data.user, true);
}
onUserStopTyping(data) {
this.updateTypingIndicator(data.user, false);
}
onUserPresenceJoined(member) {
this.users.set(member.id, member.info);
this.updateOnlineUsersList();
}
onUserPresenceLeft(member) {
this.users.delete(member.id);
this.updateOnlineUsersList();
}
// UI helpers
addMessage(messageData) {
this.messages.push(messageData);
this.updateMessagesList();
this.scrollToBottom();
// Play notification sound if message is not from current user
if (messageData.user.id !== this.currentUser.id) {
this.playNotificationSound();
}
}
addSystemMessage(text) {
const systemMessage = {
id: this.generateId(),
type: 'system',
message: text,
timestamp: new Date()
};
this.messages.push(systemMessage);
this.updateMessagesList();
this.scrollToBottom();
}
updateMessagesList() {
const container = document.getElementById('messages');
if (!container) return;
container.innerHTML = this.messages.map(msg => this.renderMessage(msg)).join('');
}
renderMessage(msg) {
if (msg.type === 'system') {
return `
<div class="system-message">
<span class="system-text">${msg.message}</span>
<span class="timestamp">${this.formatTime(msg.timestamp)}</span>
</div>
`;
}
const isOwnMessage = msg.user.id === this.currentUser.id;
const messageClass = isOwnMessage ? 'own-message' : 'other-message';
if (msg.type === 'file') {
return `
<div class="message ${messageClass}">
<div class="message-header">
<span class="username">${msg.user.name}</span>
<span class="timestamp">${this.formatTime(msg.timestamp)}</span>
</div>
<div class="file-message">
📎 <a href="${msg.message.url}" target="_blank">
${msg.message.fileName}
</a>
<span class="file-size">(${this.formatFileSize(msg.message.fileSize)})</span>
</div>
</div>
`;
}
return `
<div class="message ${messageClass}">
<div class="message-header">
<span class="username">${msg.user.name}</span>
<span class="timestamp">${this.formatTime(msg.timestamp)}</span>
</div>
<div class="message-content">${this.escapeHtml(msg.message)}</div>
</div>
`;
}
updateTypingIndicator(user, isTyping) {
const typingUsers = this.typingUsers || new Set();
if (isTyping) {
typingUsers.add(user.id);
} else {
typingUsers.delete(user.id);
}
this.typingUsers = typingUsers;
this.renderTypingIndicator();
}
renderTypingIndicator() {
const container = document.getElementById('typing-indicator');
if (!container) return;
if (this.typingUsers.size === 0) {
container.style.display = 'none';
return;
}
const typingNames = Array.from(this.typingUsers)
.map(userId => this.users.get(userId)?.name)
.filter(Boolean);
container.innerHTML = `
<div class="typing-indicator-content">
<span class="typing-dots"></span>
${typingNames.join(', ')} ${typingNames.length === 1 ? 'is' : 'are'} typing...
</div>
`;
container.style.display = 'block';
}
updateOnlineUsers(members) {
members.forEach((member, userId) => {
this.users.set(userId, member);
});
this.updateOnlineUsersList();
}
updateOnlineUsersList() {
const container = document.getElementById('online-users');
if (!container) return;
container.innerHTML = Array.from(this.users.values())
.map(user => `
<div class="online-user">
<div class="user-avatar" style="background-color: ${user.color}">
${user.name[0].toUpperCase()}
</div>
<span class="user-name">${user.name}</span>
</div>
`)
.join('');
}
setupUIListeners() {
// Message input
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
messageInput?.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage(messageInput.value);
messageInput.value = '';
} else {
this.startTyping();
}
});
messageInput?.addEventListener('blur', () => this.stopTyping());
sendButton?.addEventListener('click', () => {
this.sendMessage(messageInput.value);
messageInput.value = '';
});
// File upload
const fileInput = document.getElementById('file-input');
fileInput?.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
this.sendFile(file);
}
e.target.value = ''; // Reset input
});
// Room switching
const roomSelect = document.getElementById('room-select');
roomSelect?.addEventListener('change', (e) => {
this.joinRoom(e.target.value);
});
}
// Utility functions
generateId() {
return Math.random().toString(36).substr(2, 9);
}
formatTime(date) {
return new Date(date).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
scrollToBottom() {
const container = document.getElementById('messages-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
}
updateConnectionStatus(connected) {
const indicator = document.getElementById('connection-status');
if (indicator) {
indicator.className = connected ? 'connected' : 'disconnected';
indicator.textContent = connected ? 'Connected' : 'Disconnected';
}
}
showNotification(data) {
// Show browser notification if permitted
if (Notification.permission === 'granted') {
new Notification(data.title, {
body: data.message,
icon: '/notification-icon.png',
tag: data.id
});
}
// Show in-app notification
this.showInAppNotification(data);
}
showMentionNotification(data) {
this.showNotification({
title: 'You were mentioned!',
message: `${data.message} in ${data.roomName}`,
type: 'mention',
roomId: data.roomId
});
}
showInAppNotification(data) {
// Implementation depends on your UI framework
console.log('Notification:', data);
}
showErrorMessage(message) {
alert(message); // Replace with better UI
}
playNotificationSound() {
// Play a subtle notification sound
const audio = new Audio('/notification.mp3');
audio.volume = 0.3;
audio.play().catch(() => {});
}
getRoomId() {
const roomSelect = document.getElementById('room-select');
return roomSelect?.value || 'general';
}
async loadMessageHistory(roomId) {
try {
const response = await fetch(`/api/messages/${roomId}`, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
});
if (response.ok) {
const history = await response.json();
this.messages = history;
this.updateMessagesList();
this.scrollToBottom();
}
} catch (error) {
console.error('Failed to load message history:', error);
}
}
}
// Initialize the chat app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// Initialize chat app
window.chatApp = new ChatApp();
});
export { ChatApp };
💻 Server-side Pusher Integration javascript
🟡 intermediate
⭐⭐⭐⭐
Complete Node.js server with Pusher integration for authentication, channel management, and event broadcasting
⏱️ 30 min
🏷️ pusher, server, authentication, integration
Prerequisites:
Node.js, Express, Pusher account
// Server-side Pusher Integration with Node.js/Express
// Complete implementation with authentication, channels, and event broadcasting
const express = require('express');
const cors = require('cors');
const jwt = require('jsonwebtoken');
const Pusher = require('pusher');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const app = express();
const PORT = process.env.PORT || 3001;
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Initialize Pusher
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_KEY,
secret: process.env.PUSHER_SECRET,
cluster: process.env.PUSHER_CLUSTER,
useTLS: true,
encrypted: true
});
// JWT Secret
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key';
// File upload configuration
const upload = multer({
dest: 'uploads/',
limits: {
fileSize: 10 * 1024 * 1024 // 10MB limit
},
fileFilter: (req, file, cb) => {
// Allow common file types
const allowedTypes = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf', 'text/plain',
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'), false);
}
}
});
// In-memory storage (replace with database in production)
const users = new Map();
const rooms = new Map();
const messages = new Map();
const userSessions = new Map();
// Sample user data (in production, use a proper database)
const sampleUsers = [
{ id: '1', name: 'Alice', email: '[email protected]', role: 'user' },
{ id: '2', name: 'Bob', email: '[email protected]', role: 'user' },
{ id: '3', name: 'Charlie', email: '[email protected]', role: 'admin' }
];
sampleUsers.forEach(user => {
users.set(user.id, user);
});
// Authentication middleware
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user;
next();
});
};
// Authentication routes
app.post('/api/login', (req, res) => {
const { email, password } = req.body;
// In production, verify against database with hashed passwords
const user = Array.from(users.values()).find(u => u.email === email);
if (!user || password !== 'password') { // Simplified for demo
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ userId: user.id, name: user.name, role: user.role },
JWT_SECRET,
{ expiresIn: '24h' }
);
// Generate a unique color for the user
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
const userColor = colors[user.id.charCodeAt(0) % colors.length];
res.json({
token,
user: {
...user,
color: userColor
}
});
});
app.get('/api/user/me', authenticateToken, (req, res) => {
const user = users.get(req.user.userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Generate color if not present
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
if (!user.color) {
user.color = colors[user.id.charCodeAt(0) % colors.length];
}
res.json(user);
});
// Pusher authentication endpoint
app.post('/api/pusher/auth', authenticateToken, (req, res) => {
const socketId = req.body.socket_id;
const channel = req.body.channel_name;
// Check if user has permission to access this channel
if (!hasChannelPermission(req.user, channel)) {
return res.status(403).json({ error: 'Permission denied' });
}
// Get presence data for presence channels
let presenceData = {};
if (channel.startsWith('presence-')) {
const user = users.get(req.user.userId);
presenceData = {
user_id: user.id,
user_info: {
name: user.name,
email: user.email,
role: user.role,
color: user.color
}
};
}
const auth = pusher.authenticate(socketId, channel, presenceData);
res.send(auth);
});
// Check channel permissions
function hasChannelPermission(user, channelName) {
// Public channels - everyone can access
if (channelName.startsWith('public-')) {
return true;
}
// Private channels - authenticated users can access
if (channelName.startsWith('private-')) {
return true;
}
// Presence channels - authenticated users can access
if (channelName.startsWith('presence-')) {
return true;
}
// Admin channels - only admin users can access
if (channelName.startsWith('private-admin-')) {
return user.role === 'admin';
}
// Notification channels - user can only access their own
if (channelName.startsWith('private-notifications-')) {
const userId = channelName.split('-')[2];
return userId === user.userId;
}
return false;
}
// Handle client events and broadcast them
app.post('/api/client-events', authenticateToken, (req, res) => {
const { channel_name, event_name, data } = req.body;
// Validate event name (must start with 'client-')
if (!event_name.startsWith('client-')) {
return res.status(400).json({ error: 'Invalid event name' });
}
// Check if user has permission to trigger event on this channel
if (!hasChannelPermission(req.user, channel_name)) {
return res.status(403).json({ error: 'Permission denied' });
}
try {
// Add user info to event data
const enrichedData = {
...data,
user: {
id: req.user.userId,
name: req.user.name,
role: req.user.role
},
server_timestamp: new Date().toISOString()
};
// Broadcast to channel (remove 'client-' prefix for actual event name)
const actualEventName = event_name.replace('client-', '');
pusher.trigger(channel_name, actualEventName, enrichedData);
// Handle specific events
if (actualEventName === 'send_message') {
saveMessage(enrichedData);
} else if (actualEventName === 'typing') {
handleTypingIndicator(enrichedData, true);
} else if (actualEventName === 'stop_typing') {
handleTypingIndicator(enrichedData, false);
}
res.json({ success: true });
} catch (error) {
console.error('Error broadcasting event:', error);
res.status(500).json({ error: 'Failed to broadcast event' });
}
});
// Save message to storage
function saveMessage(messageData) {
const roomId = messageData.roomId;
if (!messages.has(roomId)) {
messages.set(roomId, []);
}
const roomMessages = messages.get(roomId);
roomMessages.push(messageData);
// Limit message history
if (roomMessages.length > 1000) {
roomMessages.splice(0, roomMessages.length - 1000);
}
// Check for mentions and send notifications
const mentions = extractMentions(messageData.message);
if (mentions.length > 0) {
mentions.forEach(userId => {
sendMentionNotification(userId, messageData);
});
}
}
// Handle typing indicators
function handleTypingIndicator(data, isTyping) {
const typingData = {
user: data.user,
isTyping,
timestamp: data.timestamp
};
// Broadcast typing event to the channel
const eventName = isTyping ? 'user_typing' : 'user_stop_typing';
pusher.trigger(data.channel_name, eventName, typingData);
}
// Extract user mentions from message
function extractMentions(message) {
const mentionRegex = /@(\w+)/g;
const mentions = [];
let match;
while ((match = mentionRegex.exec(message)) !== null) {
const username = match[1];
const user = Array.from(users.values()).find(u =>
u.name.toLowerCase() === username.toLowerCase()
);
if (user) {
mentions.push(user.id);
}
}
return mentions;
}
// Send mention notification
function sendMentionNotification(userId, messageData) {
const notificationData = {
type: 'mention',
title: 'You were mentioned in a message',
message: `${messageData.user.name} mentioned you in a chat`,
roomId: messageData.roomId,
messageId: messageData.id,
timestamp: new Date().toISOString()
};
pusher.trigger(`private-notifications-${userId}`, 'message_mention', notificationData);
}
// Send general notification
function sendNotification(userId, notificationData) {
pusher.trigger(`private-notifications-${userId}`, 'new_notification', notificationData);
}
// File upload endpoint
app.post('/api/upload', authenticateToken, upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
try {
const file = req.file;
const roomId = req.body.roomId;
// Generate unique filename
const fileExtension = path.extname(file.originalname);
const fileName = `${Date.now()}-${file.originalname}`;
const filePath = path.join(__dirname, 'uploads', fileName);
// Move file to uploads directory
fs.renameSync(file.path, filePath);
// Create file URL
const fileUrl = `${req.protocol}://${req.get('host')}/uploads/${fileName}`;
// Create file message data
const fileMessage = {
id: Date.now().toString(),
type: 'file',
url: fileUrl,
fileName: file.originalname,
fileSize: file.size,
mimeType: file.mimetype
};
// Broadcast file message to room
const channelName = `private-chat-${roomId}`;
pusher.trigger(channelName, 'new_message', {
...fileMessage,
user: {
id: req.user.userId,
name: req.user.name,
role: req.user.role
},
timestamp: new Date().toISOString(),
roomId
});
res.json({
success: true,
fileUrl,
fileName: file.originalname,
fileSize: file.size
});
} catch (error) {
console.error('File upload error:', error);
res.status(500).json({ error: 'Failed to upload file' });
}
});
// Serve uploaded files
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Get message history for a room
app.get('/api/messages/:roomId', authenticateToken, (req, res) => {
const roomId = req.params.roomId;
const roomMessages = messages.get(roomId) || [];
// Return last 50 messages
const recentMessages = roomMessages.slice(-50);
res.json(recentMessages);
});
// Get available rooms
app.get('/api/rooms', authenticateToken, (req, res) => {
const roomList = Array.from(rooms.values()).map(room => ({
id: room.id,
name: room.name,
description: room.description,
memberCount: room.memberCount || 0,
isPrivate: room.isPrivate || false
}));
res.json(roomList);
});
// Create new room
app.post('/api/rooms', authenticateToken, (req, res) => {
const { name, description, isPrivate = false } = req.body;
if (!name) {
return res.status(400).json({ error: 'Room name is required' });
}
const roomId = generateId();
const newRoom = {
id: roomId,
name,
description,
isPrivate,
createdBy: req.user.userId,
createdAt: new Date(),
memberCount: 0
};
rooms.set(roomId, newRoom);
messages.set(roomId, []);
// Broadcast room creation (for admin channels)
pusher.trigger('private-admin-rooms', 'room_created', newRoom);
res.json(newRoom);
});
// Get online users in a room
app.get('/api/rooms/:roomId/users', authenticateToken, (req, res) => {
const roomId = req.params.roomId;
// This would typically come from Pusher's presence channels
// For now, return mock data
res.json([]);
});
// Send system announcement (admin only)
app.post('/api/announce', authenticateToken, (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
const { message, targetChannel = 'public-announcements' } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
const announcement = {
type: 'announcement',
message,
from: {
id: req.user.userId,
name: req.user.name
},
timestamp: new Date().toISOString()
};
pusher.trigger(targetChannel, 'announcement', announcement);
// Also send to all private notification channels for important announcements
if (targetChannel === 'public-announcements') {
Array.from(users.values()).forEach(user => {
sendNotification(user.id, {
type: 'announcement',
title: 'System Announcement',
message,
timestamp: announcement.timestamp
});
});
}
res.json({ success: true });
});
// Get Pusher stats
app.get('/api/stats', authenticateToken, (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
const stats = {
totalUsers: users.size,
totalRooms: rooms.size,
totalMessages: Array.from(messages.values())
.reduce((total, roomMessages) => total + roomMessages.length, 0),
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString()
};
res.json(stats);
});
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// Utility functions
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
// Error handling middleware
app.use((error, req, res, next) => {
console.error('Server error:', error);
res.status(500).json({ error: 'Internal server error' });
});
// Start server
app.listen(PORT, () => {
console.log(`Pusher server running on port ${PORT}`);
console.log(`Pusher app ID: ${process.env.PUSHER_APP_ID}`);
console.log(`Pusher cluster: ${process.env.PUSHER_CLUSTER}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});
module.exports = app;
💻 React Hooks for Pusher Integration typescript
🔴 complex
⭐⭐⭐⭐
Modern React hooks for seamless Pusher integration with TypeScript support and custom hooks
⏱️ 35 min
🏷️ pusher, react, hooks, typescript
Prerequisites:
React, TypeScript, Pusher basics
// React Hooks for Pusher Integration
// Modern TypeScript hooks with state management and error handling
import { useState, useEffect, useCallback, useRef } from 'react';
import Pusher, { Channel, Members } from 'pusher-js';
// Type definitions
interface User {
id: string;
name: string;
email?: string;
role?: string;
color?: string;
avatar?: string;
}
interface Message {
id: string;
user: User;
message: string;
type: 'text' | 'file' | 'system';
timestamp: Date;
roomId?: string;
metadata?: Record<string, any>;
}
interface TypingUser {
id: string;
name: string;
color?: string;
lastSeen: Date;
}
interface NotificationSettings {
mentions: boolean;
replies: boolean;
systemMessages: boolean;
soundEnabled: boolean;
}
interface PusherConfig {
key: string;
cluster: string;
authEndpoint?: string;
forceTLS?: boolean;
}
// Custom hook for Pusher connection management
export const usePusher = (config: PusherConfig) => {
const [isConnected, setIsConnected] = useState(false);
const [connectionState, setConnectionState] = useState<string>('disconnected');
const [error, setError] = useState<string | null>(null);
const pusherRef = useRef<Pusher | null>(null);
useEffect(() => {
// Initialize Pusher
pusherRef.current = new Pusher(config.key, {
cluster: config.cluster,
authEndpoint: config.authEndpoint,
forceTLS: config.forceTLS ?? true,
auth: {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
}
});
// Connection event handlers
const handleConnected = () => {
setIsConnected(true);
setConnectionState('connected');
setError(null);
console.log('Connected to Pusher');
};
const handleDisconnected = () => {
setIsConnected(false);
setConnectionState('disconnected');
console.log('Disconnected from Pusher');
};
const handleError = (err: any) => {
setError(err.message || 'Connection error');
setConnectionState('error');
console.error('Pusher connection error:', err);
};
const handleConnecting = () => {
setConnectionState('connecting');
};
pusherRef.current.connection.bind('connected', handleConnected);
pusherRef.current.connection.bind('disconnected', handleDisconnected);
pusherRef.current.connection.bind('error', handleError);
pusherRef.current.connection.bind('connecting', handleConnecting);
return () => {
if (pusherRef.current) {
pusherRef.current.connection.unbind('connected', handleConnected);
pusherRef.current.connection.unbind('disconnected', handleDisconnected);
pusherRef.current.connection.unbind('error', handleError);
pusherRef.current.connection.unbind('connecting', handleConnecting);
pusherRef.current.disconnect();
}
};
}, [config]);
return {
pusher: pusherRef.current,
isConnected,
connectionState,
error
};
};
// Custom hook for channel subscription
export const useChannel = (
pusher: Pusher | null,
channelName: string,
options?: {
type?: 'public' | 'private' | 'presence';
onSubscribed?: () => void;
onSubscriptionError?: (error: any) => void;
}
) => {
const [channel, setChannel] = useState<Channel | null>(null);
const [isSubscribed, setIsSubscribed] = useState(false);
const [subscriptionError, setSubscriptionError] = useState<string | null>(null);
const channelRef = useRef<Channel | null>(null);
useEffect(() => {
if (!pusher || !channelName) return;
// Subscribe to channel
const fullChannelName = getFullChannelName(channelName, options?.type);
channelRef.current = pusher.subscribe(fullChannelName);
setChannel(channelRef.current);
// Bind to subscription events
const handleSubscribed = () => {
setIsSubscribed(true);
setSubscriptionError(null);
options?.onSubscribed?.();
};
const handleSubscriptionError = (err: any) => {
setSubscriptionError(err.message || 'Subscription error');
options?.onSubscriptionError?.(err);
};
channelRef.current.bind('pusher:subscription_succeeded', handleSubscribed);
channelRef.current.bind('pusher:subscription_error', handleSubscriptionError);
return () => {
if (channelRef.current) {
channelRef.current.unbind('pusher:subscription_succeeded', handleSubscribed);
channelRef.current.unbind('pusher:subscription_error', handleSubscriptionError);
pusher.unsubscribe(fullChannelName);
}
setChannel(null);
setIsSubscribed(false);
};
}, [pusher, channelName, options]);
return {
channel,
isSubscribed,
subscriptionError
};
};
// Custom hook for presence channel management
export const usePresenceChannel = (
pusher: Pusher | null,
channelName: string,
options?: {
onMemberJoined?: (member: any) => void;
onMemberLeft?: (member: any) => void;
}
) => {
const [members, setMembers] = useState<Map<string, any>>(new Map());
const [count, setCount] = useState(0);
const channelHook = useChannel(pusher, channelName, { type: 'presence' });
useEffect(() => {
if (!channelHook.channel) return;
const channel = channelHook.channel;
// Handle presence events
const handleSubscriptionSucceeded = (data: any) => {
const membersMap = new Map<string, any>();
data.members.forEach((member: any) => {
membersMap.set(member.id, member.info);
});
setMembers(membersMap);
setCount(data.count);
};
const handleMemberAdded = (member: any) => {
setMembers(prev => {
const newMembers = new Map(prev);
newMembers.set(member.id, member.info);
return newMembers;
});
setCount(prev => prev + 1);
options?.onMemberJoined?.(member);
};
const handleMemberRemoved = (member: any) => {
setMembers(prev => {
const newMembers = new Map(prev);
newMembers.delete(member.id);
return newMembers;
});
setCount(prev => prev - 1);
options?.onMemberLeft?.(member);
};
channel.bind('pusher:subscription_succeeded', handleSubscriptionSucceeded);
channel.bind('pusher:member_added', handleMemberAdded);
channel.bind('pusher:member_removed', handleMemberRemoved);
return () => {
channel.unbind('pusher:subscription_succeeded', handleSubscriptionSucceeded);
channel.unbind('pusher:member_added', handleMemberAdded);
channel.unbind('pusher:member_removed', handleMemberRemoved);
};
}, [channelHook.channel, options]);
return {
...channelHook,
members: Array.from(members.values()),
count,
getMember: (id: string) => members.get(id)
};
};
// Custom hook for real-time messaging
export const useMessaging = (
pusher: Pusher | null,
channelId: string,
options?: {
onNewMessage?: (message: Message) => void;
onTypingStart?: (user: TypingUser) => void;
onTypingStop?: (userId: string) => void;
onError?: (error: string) => void;
}
) => {
const [messages, setMessages] = useState<Message[]>([]);
const [typingUsers, setTypingUsers] = useState<Map<string, TypingUser>>(new Map());
const channelHook = useChannel(pusher, channelId, { type: 'private' });
// Send message function
const sendMessage = useCallback(async (content: string, type: 'text' | 'file' = 'text') => {
if (!channelHook.channel || !content.trim()) {
return false;
}
try {
const response = await fetch('/api/client-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
channel_name: channelHook.channel.name,
event_name: 'client-send_message',
data: {
message: content.trim(),
type,
timestamp: new Date().toISOString()
}
})
});
if (!response.ok) {
throw new Error('Failed to send message');
}
return true;
} catch (error) {
options?.onError?.(error instanceof Error ? error.message : 'Unknown error');
return false;
}
}, [channelHook.channel, options]);
// Send typing indicator
const sendTypingIndicator = useCallback((isTyping: boolean) => {
if (!channelHook.channel) return;
channelHook.channel.trigger(`client-${isTyping ? 'typing' : 'stop_typing'}`, {
timestamp: new Date().toISOString()
});
}, [channelHook.channel]);
// Handle channel events
useEffect(() => {
if (!channelHook.channel) return;
const channel = channelHook.channel;
// Listen for new messages
const handleNewMessage = (data: any) => {
const message: Message = {
id: data.id,
user: data.user,
message: data.message,
type: data.type || 'text',
timestamp: new Date(data.timestamp),
metadata: data.metadata
};
setMessages(prev => [...prev, message]);
options?.onNewMessage?.(message);
};
// Listen for typing indicators
const handleTypingStart = (data: any) => {
const typingUser: TypingUser = {
id: data.user.id,
name: data.user.name,
color: data.user.color,
lastSeen: new Date()
};
setTypingUsers(prev => {
const newMap = new Map(prev);
newMap.set(typingUser.id, typingUser);
return newMap;
});
options?.onTypingStart?.(typingUser);
};
const handleTypingStop = (data: any) => {
setTypingUsers(prev => {
const newMap = new Map(prev);
newMap.delete(data.user.id);
return newMap;
});
options?.onTypingStop?.(data.user.id);
};
channel.bind('new_message', handleNewMessage);
channel.bind('user_typing', handleTypingStart);
channel.bind('user_stop_typing', handleTypingStop);
return () => {
channel.unbind('new_message', handleNewMessage);
channel.unbind('user_typing', handleTypingStart);
channel.unbind('user_stop_typing', handleTypingStop);
};
}, [channelHook.channel, options]);
// Clean up old typing indicators
useEffect(() => {
const interval = setInterval(() => {
const now = new Date();
setTypingUsers(prev => {
const newMap = new Map(prev);
prev.forEach((user, id) => {
// Remove typing indicator if user hasn't typed in 3 seconds
if (now.getTime() - user.lastSeen.getTime() > 3000) {
newMap.delete(id);
}
});
return newMap;
});
}, 1000);
return () => clearInterval(interval);
}, []);
return {
...channelHook,
messages,
typingUsers: Array.from(typingUsers.values()),
sendMessage,
sendTypingIndicator
};
};
// Custom hook for notifications
export const useNotifications = (
pusher: Pusher | null,
userId: string,
settings: NotificationSettings = {
mentions: true,
replies: true,
systemMessages: true,
soundEnabled: true
}
) => {
const [notifications, setNotifications] = useState<any[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const channelHook = useChannel(pusher, `notifications-${userId}`, { type: 'private' });
// Mark notification as read
const markAsRead = useCallback((notificationId: string) => {
setNotifications(prev =>
prev.map(notif =>
notif.id === notificationId
? { ...notif, read: true }
: notif
)
);
// Update unread count
setUnreadCount(prev => Math.max(0, prev - 1));
}, []);
// Mark all as read
const markAllAsRead = useCallback(() => {
setNotifications(prev =>
prev.map(notif => ({ ...notif, read: true }))
);
setUnreadCount(0);
}, []);
// Clear notifications
const clearNotifications = useCallback(() => {
setNotifications([]);
setUnreadCount(0);
}, []);
// Listen for notifications
useEffect(() => {
if (!channelHook.channel) return;
const channel = channelHook.channel;
const handleNewNotification = (data: any) => {
// Check user preferences
if (data.type === 'mention' && !settings.mentions) return;
if (data.type === 'reply' && !settings.replies) return;
if (data.type === 'system' && !settings.systemMessages) return;
const notification = {
...data,
id: data.id || Date.now().toString(),
timestamp: new Date(data.timestamp),
read: false
};
setNotifications(prev => [notification, ...prev]);
setUnreadCount(prev => prev + 1);
// Play notification sound if enabled
if (settings.soundEnabled) {
playNotificationSound();
}
// Show browser notification
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(data.title, {
body: data.message,
icon: '/notification-icon.png',
tag: notification.id
});
}
};
channel.bind('new_notification', handleNewNotification);
channel.bind('message_mention', handleNewNotification);
return () => {
channel.unbind('new_notification', handleNewNotification);
channel.unbind('message_mention', handleNewNotification);
};
}, [channelHook.channel, settings]);
// Request notification permission
useEffect(() => {
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
}, []);
return {
...channelHook,
notifications,
unreadCount,
markAsRead,
markAllAsRead,
clearNotifications
};
};
// Utility functions
function getFullChannelName(channelName: string, type?: string): string {
switch (type) {
case 'private':
return `private-${channelName}`;
case 'presence':
return `presence-${channelName}`;
case 'public':
default:
return `public-${channelName}`;
}
}
function playNotificationSound() {
try {
const audio = new Audio('/notification.mp3');
audio.volume = 0.3;
audio.play().catch(() => {});
} catch (error) {
// Ignore audio errors
}
}
// Example usage component
export const ChatRoom: React.FC<{
roomId: string;
currentUser: User;
}> = ({ roomId, currentUser }) => {
const config: PusherConfig = {
key: process.env.REACT_APP_PUSHER_KEY!,
cluster: process.env.REACT_APP_PUSHER_CLUSTER!,
authEndpoint: '/api/pusher/auth'
};
const { pusher, isConnected } = usePusher(config);
const {
messages,
typingUsers,
sendMessage,
sendTypingIndicator
} = useMessaging(pusher, roomId, {
onNewMessage: (message) => {
console.log('New message:', message);
},
onError: (error) => {
console.error('Message error:', error);
}
});
const { members, count } = usePresenceChannel(pusher, roomId, {
onMemberJoined: (member) => {
console.log('Member joined:', member);
}
});
const [inputValue, setInputValue] = useState('');
const [isTyping, setIsTyping] = useState(false);
const handleSend = useCallback(() => {
if (sendMessage(inputValue)) {
setInputValue('');
setIsTyping(false);
}
}, [inputValue, sendMessage]);
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
if (!isTyping && e.target.value.trim()) {
setIsTyping(true);
sendTypingIndicator(true);
} else if (isTyping && !e.target.value.trim()) {
setIsTyping(false);
sendTypingIndicator(false);
}
}, [isTyping, sendTypingIndicator]);
return (
<div className="chat-room">
<div className="chat-header">
<h3>Room: {roomId}</h3>
<div className="connection-status">
{isConnected ? '🟢 Connected' : '🔴 Disconnected'}
</div>
<div className="online-count">
{count} users online
</div>
</div>
<div className="messages">
{messages.map(message => (
<div key={message.id} className="message">
<strong>{message.user.name}: </strong>
{message.message}
<small>{new Date(message.timestamp).toLocaleTimeString()}</small>
</div>
))}
</div>
{typingUsers.length > 0 && (
<div className="typing-indicator">
{typingUsers.map(user => user.name).join(', ')} is typing...
</div>
)}
<div className="message-input">
<input
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
placeholder="Type a message..."
disabled={!isConnected}
/>
<button onClick={handleSend} disabled={!isConnected || !inputValue.trim()}>
Send
</button>
</div>
<div className="online-users">
<h4>Online Users:</h4>
{members.map(member => (
<div key={member.id} className="user">
{member.name}
</div>
))}
</div>
</div>
);
};
export default {
usePusher,
useChannel,
usePresenceChannel,
useMessaging,
useNotifications,
ChatRoom
};