7. SignalR
7.1 Hub
Route: /whatsapp-hub (mapped in Shumoul.Api/Program.cs) · Class: WhatsAppHub : Hub ·
Authentication: [Authorize] — requires an authenticated connection (JWT via query string or header,
per the platform's standard SignalR auth setup).
endpoints.MapHub<WhatsAppHub>("/whatsapp-hub", options => {
options.Transports = HttpTransportType.WebSockets | HttpTransportType.LongPolling;
});
A companion server-side pusher, WhatsAppHubService (IWhatsAppHubService), wraps IHubContext<WhatsAppHub>
and is injected into WhatsAppInboxService so REST actions can broadcast real-time events.
7.2 Groups
| Group name pattern | Membership |
|---|---|
whatsapp-tenant-{tenantId} | Every connection for a tenant |
whatsapp-conv-{conversationId} | Clients currently viewing a specific conversation |
whatsapp-user-{userId} | A specific user's own connections (for cross-device sync) |
7.3 Connection Lifecycle
OnConnectedAsync: joins the tenant group and the user's own group, marks the agent online viaIWhatsAppPresenceService.SetOnline, broadcastsAgentPresenceChangedto the tenant group.OnDisconnectedAsync: leaves the tenant group, marks the agent offline, broadcastsAgentPresenceChanged.
7.4 Client-Invokable Hub Methods
| Method | Parameters | Effect |
|---|---|---|
JoinConversation | conversationId: Guid | Joins the conversation group, sets the agent's current conversation, broadcasts AgentJoined |
LeaveConversation | conversationId: Guid | Leaves the conversation group, clears current conversation, broadcasts AgentLeft |
StartTyping | conversationId: Guid | Broadcasts TypingStarted; presence service auto-broadcasts TypingStopped after a timeout |
StopTyping | conversationId: Guid | Broadcasts TypingStopped immediately |
MarkConversationOpened | conversationId: Guid | Updates presence's current conversation, no broadcast |
MarkConversationClosed | none | Clears presence's current conversation, no broadcast |
7.5 Server-to-Client Events
| Event name | Payload | Triggered by |
|---|---|---|
AgentPresenceChanged | {userId, userName, isOnline, currentConversationId, lastSeenOn} | Connect/disconnect |
AgentJoined / AgentLeft | same shape as above | JoinConversation / LeaveConversation |
TypingStarted / TypingStopped | {conversationId, userId, userName, isTyping} | StartTyping / StopTyping |
ConversationAssigned | {conversationId, assignedToUserId, assignedToUserName, assignedByUserId, assignedByUserName, assignedOn, comment} | Assign |
ConversationTransferred | same shape as ConversationAssigned | Transfer |
ConversationClosed / ConversationReopened / ConversationArchived / ConversationUnarchived / ConversationPinned / ConversationUnpinned | {conversationId, status, isPinned, isArchived, triggeredByUserId, triggeredOn} | respective lifecycle actions |
UnreadCountChanged | {conversationId, unreadCount} | MarkAsRead / MarkAsUnread |
Reserved, not yet wired up: WhatsAppNewMessageEvent and WhatsAppMessageStatusEvent DTOs exist in
WhatsAppRealtimeEventDtos.cs, but no current broadcast call site uses them — the webhook controller
persists inbound messages and status updates to the database but does not currently push a SignalR event for
them. Frontend clients should poll or re-fetch GetMessages/GetConversations after receiving any of the
lifecycle events above, rather than assuming a dedicated "new message" push exists today.
7.6 Angular Usage
const connection = new signalR.HubConnectionBuilder()
.withUrl('/whatsapp-hub', { accessTokenFactory: () => authService.getToken() })
.withAutomaticReconnect()
.build();
connection.on('ConversationAssigned', (evt) => inboxStore.applyAssignment(evt));
connection.on('UnreadCountChanged', (evt) => inboxStore.updateUnreadCount(evt));
connection.on('TypingStarted', (evt) => chatStore.setTyping(evt.conversationId, evt.userId, true));
await connection.start();
await connection.invoke('JoinConversation', conversationId);
7.7 Flutter Usage
Use the signalr_netcore (or equivalent) package with the same /whatsapp-hub URL and bearer token; invoke
JoinConversation/LeaveConversation when a conversation screen is opened/closed, and listen for the same
event names as the Angular client. Because no dedicated "new message" event exists yet (§7.5), pair the hub
listener with a pull-to-refresh on the message list rather than relying solely on push updates for message
content.
