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

  1. Decoupled Modules: Services don't directly depend on notification services
  2. Single Responsibility: Each service focuses on core domain logic
  3. Extensibility: New event handlers can be added without modifying existing services
  4. 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

  1. Subscribe Selectively: Only subscribe to channels the user is actively viewing
  2. Unsubscribe on Navigation: Clean up subscriptions when leaving channels
  3. Batch Updates: Use bulk presence updates for efficiency
  4. 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