How to make chat app with SignalR and Angular

3 min read
C
Configure method
Inside the Configure method, add the following code:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<ChatHub>("api/hubs/messages");
endpoints.MapControllers();
});
}
ChatHub
This implementation is designed to support authentication.
using Microsoft.AspNetCore.SignalR;
using PlenumRMT.Business.DTO;
using Spider.Security.Services;
using Spider.Shared.Helpers;
namespace PlenumRMT.Business.SignalRHubs
{
public class ChatHub : Hub
{
private readonly AuthenticationService _authenticationService;
public ChatHub(AuthenticationService authenticationService)
{
_authenticationService = authenticationService;
}
public override async Task OnConnectedAsync()
{
if (!Helper.IsJwtTokenValid(await _authenticationService.GetAccessTokenAsync()))
{
// TODO: Log
return;
}
string connectedUserId = _authenticationService.GetCurrentUserId().ToString();
await Groups.AddToGroupAsync(Context.ConnectionId, connectedUserId);
}
public async Task SendMessage(SendMessageSaveBodyDTO saveBodyDTO)
{
if (!Helper.IsJwtTokenValid(await _authenticationService.GetAccessTokenAsync()))
{
// TODO: Log
return;
}
await Clients.Groups([saveBodyDTO.RecipientId.ToString(), saveBodyDTO.SenderId.ToString()]).SendAsync("ReceiveMessage", saveBodyDTO);
}
}
}
Angular
SignalRChatService
import { AuthService } from './../auth/auth.service';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
import { SendMessageSaveBody } from '../../entities/business-entities.generated';
import { ConfigService } from '../config.service';
@Injectable({
providedIn: 'root',
})
export class SignalRChatService {
private hubConnection: HubConnection;
constructor(
private config: ConfigService,
private authService: AuthService,
) {
this.hubConnection = new HubConnectionBuilder()
.configureLogging(LogLevel.None)
.withUrl(`${config.apiUrl}/hubs/messages`, {
accessTokenFactory: () => authService.getAccessToken()
}) // SignalR hub URL
.build();
}
startConnection(): Observable<void> {
return new Observable<void>((observer) => {
this.hubConnection
.start()
.then(() => {
observer.next();
observer.complete();
})
.catch((error) => {
console.error('Error connecting to SignalR hub:', error);
observer.error(error);
});
});
}
closeConnection() {
this.hubConnection.stop();
}
receiveMessage(): Observable<SendMessageSaveBody> {
return new Observable<SendMessageSaveBody>((observer) => {
this.hubConnection.on('ReceiveMessage', (message: SendMessageSaveBody) => {
observer.next(message);
});
});
}
sendMessage(message: SendMessageSaveBody): void {
this.hubConnection.invoke('SendMessage', message);
}
}
Message Component
TypeScript
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ApiService } from 'src/app/business/services/api/api.service';
import { AuthService } from 'src/app/business/services/auth/auth.service';
import { SpiderFormControl } from '@playerty/spider';
import { ActivatedRoute, Router } from '@angular/router';
import { SendMessageSaveBody, UserExtended, UserExtendedMessage } from 'src/app/business/entities/business-entities.generated';
import { firstValueFrom, Subscription } from 'rxjs';
import { SignalRChatService } from 'src/app/business/services/signalR/signalr-chat.service';
@Component({
templateUrl: './message.component.html',
})
export class MessageComponent implements OnInit, OnDestroy {
chatSubscription: Subscription;
currentUser: UserExtended;
users: UserExtended[];
messages: UserExtendedMessage[];
correspondentId: number;
messageFormControl = new SpiderFormControl<string>(null);
constructor(
private apiService: ApiService,
private authService: AuthService,
private route: ActivatedRoute,
private router: Router,
private signalRChatService: SignalRChatService
) {}
async ngOnInit() {
this.chatSubscription = this.signalRChatService.startConnection().subscribe(() => {
this.signalRChatService.receiveMessage().subscribe((sendMessageSaveBody: SendMessageSaveBody) => {
console.log('Is message received.')
if (
(sendMessageSaveBody.senderId == this.currentUser.id && sendMessageSaveBody.recipientId == this.correspondentId) ||
(sendMessageSaveBody.senderId == this.correspondentId && sendMessageSaveBody.recipientId == this.currentUser.id)
) {
this.getMessages();
}
});
});
this.route.params.subscribe((params) => {
this.correspondentId = +params['id'];
if (this.correspondentId > 0) {
this.openMessages(this.correspondentId)
}
else{
this.messageFormControl.disable();
}
});
this.currentUser = await firstValueFrom(this.authService.user$);
this.apiService.getUserExtendedList().subscribe(users => {
this.users = users.filter(x => x.id != this.currentUser.id);
});
}
openMessages(correspondentId: number) {
this.router.navigate([`messages/${correspondentId}`]);
this.messageFormControl.enable();
this.getMessages();
}
getMessages(){
this.apiService.getMessages(this.correspondentId).subscribe(res => {
this.messages = res;
});
}
sendMessage() {
if (!this.messageFormControl.value)
return;
let sendMessageSaveBody = new SendMessageSaveBody({
senderId: this.currentUser.id,
recipientId: this.correspondentId,
messageText: this.messageFormControl.value,
});
this.apiService.sendMessage(sendMessageSaveBody).subscribe(() => {
this.signalRChatService.sendMessage(sendMessageSaveBody);
this.messageFormControl.setValue(null);
});
}
ngOnDestroy(){
this.chatSubscription.unsubscribe();
this.signalRChatService.closeConnection();
}
}
HTML
<ng-container *transloco="let t">
<div style="display: flex; gap: 28px; height: calc(82vh); overflow-y: auto;">
<div class="card" style="width: 350px; margin-bottom: 0;">
@for (user of users; track $index) {
<div class="hover-card" (click)="openMessages(user.id)">
{{user.email}}
</div>
}
</div>
<div class="card" style="width: 100%; display: flex; flex-direction: column; justify-content: space-between;">
<div style="overflow-y: auto; display: flex; flex-direction: column-reverse;">
@for (message of messages; track $index) {
<div style="margin-bottom: 10px;">
{{message.senderDisplayName}}: {{message.messageDisplayName}}
</div>
}
</div>
<div style="width: 100%;">
<spider-textbox [control]="messageFormControl" [showButton]="true" buttonIcon="pi-send" (onButtonClick)="sendMessage()"></spider-textbox>
</div>
</div>
</div>
</ng-container>
Source code
0
Subscribe to my newsletter
Read articles from Filip Trivan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Filip Trivan
Filip Trivan
C#, Angular, Next.js, MERN stack, JavaScript, React.js, Node.js, MongoDB, C, Python, Artificial Intelligence, and Machine Learning. Stay up to date with the latest trends, expert insights, and practical tips.