Skip to content

Commit

Permalink
Update Notifications (#186)
Browse files Browse the repository at this point in the history
* added shared notification types & services

* updated notification tests
  • Loading branch information
dills122 authored Nov 27, 2024
1 parent 7cbf2e9 commit c2fb403
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 49 deletions.
3 changes: 2 additions & 1 deletion apps/chat-backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AlertController } from './alert/alert.controller';
import { JwtTokenService } from './services/jwt-token/jwt-token.service';
import { RoomManagementService } from './services/room-management/room-management.service';
import { RedisModule } from './infrastructure/redis/redis.module';
import { NotificationService } from './services/notification/notification.service';

@Module({
imports: [
Expand All @@ -16,6 +17,6 @@ import { RedisModule } from './infrastructure/redis/redis.module';
RedisModule
],
controllers: [AlertController],
providers: [ChatGateway, AlertGateway, JwtTokenService, RoomManagementService]
providers: [ChatGateway, AlertGateway, JwtTokenService, RoomManagementService, NotificationService]
})
export class AppModule {}
35 changes: 21 additions & 14 deletions apps/chat-backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
EventStatuses,
EventTypes,
MessageFormat,
NotificationFormat,
NotificationTypes,
SessionCreation,
SessionCreationResponse
Expand All @@ -18,8 +17,9 @@ import {
TokenValidationInput,
UserDataInput
} from 'src/services/jwt-token/jwt-token.service';
import { Omit } from 'utility-types';
import { NotificationService } from 'src/services/notification/notification.service';
import { RoomManagementService } from 'src/services/room-management/room-management.service';
import { Omit } from 'utility-types';

@WebSocketGateway(3001, {
cors: true,
Expand All @@ -32,7 +32,8 @@ export class ChatGateway implements OnGatewayInit {

constructor(
private jwtTokenService: JwtTokenService,
private roomManagementService: RoomManagementService
private roomManagementService: RoomManagementService,
private notificationService: NotificationService
) {}

afterInit(): void {
Expand Down Expand Up @@ -88,10 +89,13 @@ export class ChatGateway implements OnGatewayInit {
status: EventStatuses.SUCCESS
};
client.emit(EventTypes.LOGIN, login);
const notif: NotificationFormat = {
type: NotificationTypes.NEW_USER
};
this.wss.in(room).emit(EventTypes.NOTIFICATION, notif);
this.wss.in(room).emit(
EventTypes.NOTIFICATION,
this.notificationService.buildNotificationMessage({
type: NotificationTypes.NEW_USER,
room
})
);
} catch (err) {
this.logger.error(err);
}
Expand All @@ -104,13 +108,16 @@ export class ChatGateway implements OnGatewayInit {
room: message.room,
uid: message.uid
});
const notif: NotificationFormat = {
type: NotificationTypes.USER_LEFT,
data: {
uid: message.uid
}
};
this.wss.in(message.room).emit(EventTypes.NOTIFICATION, notif);
this.wss.in(message.room).emit(
EventTypes.NOTIFICATION,
this.notificationService.buildNotificationMessage({
type: NotificationTypes.USER_LEFT,
room: message.room,
data: {
uid: message.uid
}
})
);
await client.leave(message.room);
} catch (err) {
this.logger.error(err);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotificationService } from './notification.service';

describe('NotificationService', () => {
let service: NotificationService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [NotificationService]
}).compile();

service = module.get<NotificationService>(NotificationService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { NotificationFormat, NotificationTypes } from 'shared-sdk';

@Injectable()
export class NotificationService {
buildNotificationMessage({
type,
room,
data
}: {
type: NotificationTypes;
room: string;
data?: {
[key: string]: string;
};
}): NotificationFormat {
return {
type,
room,
timestamp: new Date().toISOString(),
data
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MessageFormat } from 'shared-sdk';
import { CanComponentDeactivate } from 'src/app/guards/can-deactivate.guard';
import { ChatServiceService } from 'src/app/services/chat/chat-service.service';
import { LoginService } from 'src/app/services/login/login.service';
import { NotificationServiceService as NotificationService } from 'src/app/services/notification/notification-service.service';
import { SessionStorageService } from 'src/app/services/session-storage/session-storage.service';

@Component({
Expand Down Expand Up @@ -35,7 +36,8 @@ export class ChatRoomComponent implements OnInit, OnDestroy, CanComponentDeactiv
constructor(
private sessionStorageService: SessionStorageService,
private chatService: ChatServiceService,
private loginService: LoginService
private loginService: LoginService,
private notificationService: NotificationService
) {
this.messages = [];
}
Expand All @@ -53,7 +55,9 @@ export class ChatRoomComponent implements OnInit, OnDestroy, CanComponentDeactiv
uid: this.username
});
this.messages$.subscribe();
this.notificationService.subscribeToNotifications().subscribe();
}

@HostListener('window:beforeunload')
canDeactivate() {
if (this.messages && this.messages.length > 0) {
Expand Down
19 changes: 7 additions & 12 deletions apps/chat-frontend/src/app/features/login/login/login.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { NbGlobalPhysicalPosition, NbToastrService } from '@nebular/theme';
import { ActivatedRoute, Router } from '@angular/router';
import { NotificationTypes } from 'shared-sdk';

import { LoginService } from 'src/app/services/login/login.service';
import { NotificationServiceService } from 'src/app/services/notification/notification-service.service';
import { UtilService } from 'src/app/services/util/util.service';

@Component({
Expand All @@ -18,7 +19,7 @@ export class LoginComponent implements OnInit {

constructor(
private loginService: LoginService,
private toastrService: NbToastrService,
private notificationService: NotificationServiceService,
private router: Router,
private route: ActivatedRoute,
private utilService: UtilService
Expand All @@ -38,9 +39,7 @@ export class LoginComponent implements OnInit {

login() {
if (!this.sessionId || !this.sessionHash) {
this.toastrService.danger('Issue gathering required info from link', 'Login Issue', {
position: NbGlobalPhysicalPosition.TOP_RIGHT
});
this.notificationService.showNotification(NotificationTypes.LOGIN_ISSUES);
} else {
try {
this.createTimeoutwarningTimer();
Expand All @@ -52,18 +51,14 @@ export class LoginComponent implements OnInit {
});
} catch (err) {
this.utilService.clearTimeoutIfExists(this.timeoutId as string);
this.toastrService.danger('Issue verifying participant data', 'Login Issue', {
position: NbGlobalPhysicalPosition.TOP_RIGHT
});
this.notificationService.showNotification(NotificationTypes.LOGIN_ISSUES);
}
}
}

createTimeoutwarningTimer() {
this.timeoutId = setTimeout(() => {
this.toastrService.warning('Login is taking awhile, try refreshing', 'Timeout', {
position: NbGlobalPhysicalPosition.TOP_RIGHT
});
this.notificationService.showNotification(NotificationTypes.LOGIN_TIMEOUT);
}, 10000);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { TestBed } from '@angular/core/testing';
import { Socket } from 'ngx-socket-io';

import { NbToastrService } from '@nebular/theme';
import { of } from 'rxjs';
import { NotificationFormat, NotificationTypes } from 'shared-sdk';
import { NotificationServiceService } from './notification-service.service';

describe('NotificationServiceService', () => {
let service: NotificationServiceService;
let socketIO: jasmine.SpyObj<Socket>;
let socketIOMock: jasmine.SpyObj<Socket>;
let NbToastrMock: jasmine.SpyObj<NbToastrService>;

beforeEach(() => {
socketIO = jasmine.createSpyObj('Socket', ['emit', 'fromEvent']);
socketIOMock = jasmine.createSpyObj('Socket', ['emit', 'fromEvent']);
NbToastrMock = jasmine.createSpyObj('NbToastrService', ['info', 'danger', 'warning']);
TestBed.configureTestingModule({
providers: [
{
provide: Socket,
useValue: socketIO
useValue: socketIOMock
},
{
provide: NbToastrService,
useValue: NbToastrMock
}
]
});
Expand All @@ -23,4 +32,34 @@ describe('NotificationServiceService', () => {
it('should be created', () => {
expect(service).toBeTruthy();
});

it('should call showNotification when a socket event is emitted', () => {
const spy = spyOn(service, 'showNotification');
socketIOMock.fromEvent.and.returnValue(
of({
type: NotificationTypes.NEW_USER,
room: 'room',
timestamp: ''
} as NotificationFormat)
);
service.subscribeToNotifications().subscribe();
expect(spy).toHaveBeenCalled();
});

it('should show an info based notification if USER_LEFT type is displayed', () => {
service.showNotification(NotificationTypes.USER_LEFT);
expect(NbToastrMock.info).toHaveBeenCalled();
});
it('should show an info based notification if NEW_USER type is displayed', () => {
service.showNotification(NotificationTypes.NEW_USER);
expect(NbToastrMock.info).toHaveBeenCalled();
});
it('should show an danger based notification if LOGIN_ISSUES type is displayed', () => {
service.showNotification(NotificationTypes.LOGIN_ISSUES);
expect(NbToastrMock.danger).toHaveBeenCalled();
});
it('should show an warn based notification if LOGIN_TIMEOUT type is displayed', () => {
service.showNotification(NotificationTypes.LOGIN_TIMEOUT);
expect(NbToastrMock.warning).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
import { Injectable } from '@angular/core';
import { NbGlobalPhysicalPosition, NbToastrService } from '@nebular/theme';
import { Socket } from 'ngx-socket-io';
import { EventTypes } from 'shared-sdk';

export interface NotificationMessageFormat {
type: string;
room: string;
uid: string;
timestamp: string;
}
import { tap } from 'rxjs';
import { EventTypes, NotificationFormat, NotificationMapping, NotificationTypes } from 'shared-sdk';

@Injectable({
providedIn: 'root'
})
export class NotificationServiceService {
constructor(private socket: Socket) {}
constructor(
private socket: Socket,
private toastrService: NbToastrService
) {}

subscribeToNotifications() {
return this.socket.fromEvent<NotificationMessageFormat>(EventTypes.NOTIFICATION);
return this.socket.fromEvent<NotificationFormat>(EventTypes.NOTIFICATION).pipe(
tap((notification) => {
//TODO implement additional data object at some point
this.showNotification(notification.type);
})
);
}

showNotification(notificationType: NotificationTypes) {
const notificationData = NotificationMapping[notificationType];
const { title, message, type } = notificationData;
switch (type) {
case 'info':
return this.showInformationalNotification(message, title);
case 'danger':
return this.showErrorNotification(message, title);
case 'warn':
return this.showWarningotification(message, title);
}
}

private showInformationalNotification(message: string, title?: string) {
this.toastrService.info(message, title, {
position: NbGlobalPhysicalPosition.TOP_RIGHT
});
}

private showErrorNotification(message: string, title?: string) {
this.toastrService.danger(message, title, {
position: NbGlobalPhysicalPosition.TOP_RIGHT
});
}

private showWarningotification(message: string, title?: string) {
this.toastrService.warning(message, title, {
position: NbGlobalPhysicalPosition.TOP_RIGHT
});
}
}
Loading

0 comments on commit c2fb403

Please sign in to comment.