Real-time Features
ChatMinder provides comprehensive real-time functionality through SignalR, enabling instant messaging, presence updates, and live notifications.
SignalR Hub Connection Setup
Basic Connection
import { HubConnectionBuilder, HttpTransportType } from '@microsoft/signalr';
// Configure connection with JWT authentication
const connection = new HubConnectionBuilder()
.withUrl('/chatHub', {
accessTokenFactory: () => getStoredJwtToken(),
transport: HttpTransportType.WebSockets | HttpTransportType.LongPolling
})
.withAutomaticReconnect()
.build();
// Start connection
await connection.start();
Transport Options
- WebSockets: Preferred for real-time performance
- Long Polling: Fallback for corporate networks/firewalls
- Automatic Fallback: Recommended for production reliability
Connection Management
// Connection state handling
connection.onreconnecting(() => {
console.log('Reconnecting...');
showConnectionStatus('reconnecting');
});
connection.onreconnected(() => {
console.log('Reconnected');
showConnectionStatus('connected');
// Re-subscribe to channels if needed
resubscribeToChannels();
});
connection.onclose(() => {
console.log('Connection closed');
showConnectionStatus('disconnected');
});
Hub Methods (Client → Server)
Channel Subscriptions
// Subscribe to channel real-time updates
await connection.invoke('SubscribeToChannel', channelId);
// Unsubscribe from channel updates
await connection.invoke('UnsubscribeFromChannel', channelId);
// Subscribe to multiple channels
const channelIds = ['channel1', 'channel2', 'channel3'];
for (const channelId of channelIds) {
await connection.invoke('SubscribeToChannel', channelId);
}
Activity Tracking
// Send heartbeat for activity tracking
await connection.invoke('Heartbeat');
Hub Events (Server → Client)
Message Events
// New message received
connection.on('MessageReceived', (eventData) => {
console.log('New message:', eventData);
// eventData: { channelId, messageId, content, senderId, timestamp }
displayNewMessage(eventData);
});
// Message edited
connection.on('MessageEdited', (eventData) => {
console.log('Message edited:', eventData);
updateMessageInUI(eventData.messageId, eventData.content);
});
// Message deleted
connection.on('MessageDeleted', (eventData) => {
console.log('Message deleted:', eventData);
removeMessageFromUI(eventData.messageId);
});
Presence Events
// User online status
connection.on('UserOnline', (eventData) => {
console.log(`User ${eventData.userId} came online`);
updateUserPresence(eventData.userId, 'online');
});
connection.on('UserOffline', (eventData) => {
console.log(`User ${eventData.userId} went offline`);
updateUserPresence(eventData.userId, 'offline');
});
// Bulk presence updates
connection.on('PresenceUpdate', (presenceData) => {
// presenceData: { userId: string, status: 'online' | 'offline', lastSeen?: Date }[]
updateMultipleUserPresence(presenceData);
});
Typing Indicators
connection.on('UserTyping', (eventData) => {
// eventData: { userId, channelId, isTyping, timestamp }
if (eventData.isTyping) {
showTypingIndicator(eventData.userId, eventData.channelId);
} else {
hideTypingIndicator(eventData.userId, eventData.channelId);
}
});
Notification Events
// Real-time notifications
connection.on('NotificationReceived', (eventData) => {
showNotification(eventData.title, eventData.message);
// eventData: { id, title, message, type, data?, timestamp }
});
// Guardian approval notifications
connection.on('GuardianApprovalRequired', (eventData) => {
showGuardianNotification(eventData);
// eventData: { type: 'message' | 'contact' | 'channel', itemId, description }
});
Domain Events Architecture
ChatMinder uses a comprehensive event-driven architecture for decoupled module communication.
Event Publishing Pattern
// Events are automatically published for major actions
// Examples of events that trigger real-time updates:
// Message events
- MessageCreated
- MessageEdited
- MessageDeleted
// Channel events
- ChannelCreated
- ChannelMemberAdded
- ChannelMemberRemoved
// Presence events
- UserWentOnline
- UserWentOffline
// Guardian events
- GuardianApprovalRequested
- GuardianApprovalGranted
- GuardianApprovalDenied
Event Handler Benefits
- Decoupled Modules: Services don't directly depend on notification services
- Single Responsibility: Each service focuses on core domain logic
- Extensibility: New event handlers can be added without modifying existing services
- Testability: Services and event handlers can be tested independently
Push Notifications
Device Registration
// Register mobile device for push notifications
await fetch('/api/notifications/devices', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
deviceId: 'unique-device-id',
fcmToken: 'firebase-cloud-messaging-token',
platform: 'android', // 'android', 'ios', or 'web'
deviceInfo: 'Additional device information'
})
});
Notification Types
- Message Notifications: New messages in subscribed channels
- Guardian Approvals: Pending approval requests
- Channel Invitations: New channel membership invitations
- System Notifications: Important system-wide announcements
Push Notification Payload Structure
Push notifications follow a consistent payload structure for reliable client handling:
// Example message notification payload
{
"title": "New message from John",
"body": "Hello! How are you doing?",
"type": "new_message",
"data": {
"channelId": 123,
"messageId": 456,
"senderId": "user-id"
}
}
// Example guardian approval notification payload
{
"title": "Approval Required",
"body": "Emma wants to send a message",
"type": "message_pending_approval",
"data": {
"pendingMessageId": 789,
"protectedUserId": "emma-user-id",
"channelId": 123
}
}
// Example channel invitation payload
{
"title": "Channel Invitation",
"body": "You've been invited to join a conversation",
"type": "channel_invite_pending_approval",
"data": {
"inviteId": 456,
"channelId": 123,
"fromUserId": "sender-user-id"
}
}
Payload Structure Guidelines
- title: Short, descriptive notification title (max 50 characters)
- body: Detailed message content (max 200 characters)
- type: Notification type identifier for client routing
- data: Structured data for client action handling
Real-time Integration Examples
Complete Chat Implementation
class ChatClient {
constructor() {
this.connection = null;
this.subscribedChannels = new Set();
}
async connect(jwtToken) {
this.connection = new HubConnectionBuilder()
.withUrl('/chatHub', {
accessTokenFactory: () => jwtToken
})
.withAutomaticReconnect()
.build();
// Set up event handlers
this.setupEventHandlers();
// Start connection
await this.connection.start();
console.log('Connected to ChatHub');
}
setupEventHandlers() {
// Message events
this.connection.on('MessageReceived', (data) => {
this.handleNewMessage(data);
});
// Presence events
this.connection.on('UserOnline', (data) => {
this.updateUserPresence(data.userId, 'online');
});
this.connection.on('UserOffline', (data) => {
this.updateUserPresence(data.userId, 'offline');
});
// Typing indicators
this.connection.on('UserTyping', (data) => {
this.handleTypingIndicator(data);
});
}
async subscribeToChannel(channelId) {
if (!this.subscribedChannels.has(channelId)) {
await this.connection.invoke('SubscribeToChannel', channelId);
this.subscribedChannels.add(channelId);
}
}
async sendMessage(channelId, content) {
// Send via REST API
const response = await fetch(`/api/messages/channel/${channelId}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.jwtToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ content, messageType: 'text' })
});
return await response.json();
}
handleNewMessage(data) {
// Update UI with new message
const messageElement = createMessageElement(data);
appendToChannelMessages(data.channelId, messageElement);
// Show notification if not in current channel
if (data.channelId !== this.currentChannelId) {
showNewMessageNotification(data);
}
}
}
Typing Indicator Implementation
class TypingIndicator {
constructor(connection, channelId) {
this.connection = connection;
this.channelId = channelId;
this.typingTimeout = null;
this.isTyping = false;
}
startTyping() {
if (!this.isTyping) {
this.isTyping = true;
this.connection.invoke('SetTyping', {
channelId: this.channelId,
isTyping: true
});
}
// Clear existing timeout
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
}
// Stop typing after 3 seconds of inactivity
this.typingTimeout = setTimeout(() => {
this.stopTyping();
}, 3000);
}
stopTyping() {
if (this.isTyping) {
this.isTyping = false;
this.connection.invoke('SetTyping', {
channelId: this.channelId,
isTyping: false
});
}
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
this.typingTimeout = null;
}
}
}
Best Practices
Performance Optimization
- Subscribe Selectively: Only subscribe to channels the user is actively viewing
- Unsubscribe on Navigation: Clean up subscriptions when leaving channels
- Batch Updates: Use bulk presence updates for efficiency
- Connection Pooling: Reuse connections across your application
Connection Management
Error Handling: For comprehensive SignalR error handling patterns, including connection failures and recovery strategies, see the Error Handling Guide.
Connection State Management
// Track connection state
let connectionState = 'disconnected';
connection.onreconnecting(() => {
connectionState = 'reconnecting';
// Disable message sending
disableMessageInput();
});
connection.onreconnected(() => {
connectionState = 'connected';
// Re-enable message sending
enableMessageInput();
// Re-subscribe to all channels
resubscribeToAllChannels();
});
Next Steps
- API Reference: Explore REST endpoints that complement real-time features
- Architecture Guide: Understand the event-driven architecture
- Development Guide: Set up your development environment