How to make chat app with SignalR and Angular

Filip TrivanFilip Trivan
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

Spider Framework

Chat app

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.