Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add realtime notification feature #431

Merged
merged 5 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
356 changes: 356 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"start:services": "docker compose up",
"start:api": "npm run start --workspace=api",
"start:website": "npm run start:swa --workspace=portal",
"start:realtime": "npm run start --workspace=realtime",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe start:notifications is more idomatic?

"test": "npm run test -ws --if-present",
"build": "npm run build -ws --if-present",
"format": "prettier --write .",
Expand Down
2 changes: 1 addition & 1 deletion packages/portal/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
],
"scripts": [],
"ngxEnv": {
"prefix": "(CODESPACE|GITHUB|AI)_"
"prefix": "(CODESPACE|GITHUB|AI|SERVICE)_"
}
},
"configurations": {
Expand Down
1 change: 1 addition & 0 deletions packages/portal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"apollo-angular": "^5.0.1",
"graphql": "^16.6.0",
"rxjs": "~7.5.0",
"socket.io-client": "^4.7.4",
"tslib": "^2.3.0",
"zone.js": "~0.13.0"
},
Expand Down
43 changes: 42 additions & 1 deletion packages/portal/src/app/homepage/homepage.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ import { Component, OnInit, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { MatDividerModule } from "@angular/material/divider";
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { environment } from "../../environments/environment";
import { CardListComponent } from "../shared/card-list/card-list.component";
import { FavoriteService } from "../shared/favorite.service";
import { InfiniteScrollingDirective } from "../shared/infinite-scrolling.directive";
import { ListingService } from "../shared/listing.service";
import { UserService } from "../shared/user/user.service";
import { RealtimeService } from "../shared/realtime.service";

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe NotificationService, to consolidate?

@Component({
selector: "app-homepage",
templateUrl: "./homepage.component.html",
styleUrls: ["./homepage.component.scss"],
standalone: true,
imports: [CommonModule, CardListComponent, MatButtonModule, MatDividerModule, InfiniteScrollingDirective],
imports: [CommonModule, CardListComponent, MatButtonModule, MatDividerModule, InfiniteScrollingDirective, MatSnackBarModule],
})
export class HomepageComponent implements OnInit {
featuredListings: Listing[] = [];
Expand All @@ -25,10 +27,49 @@ export class HomepageComponent implements OnInit {
private listingService = inject(ListingService);
private favoriteService = inject(FavoriteService);
private userService = inject(UserService);
private realtimeService = inject(RealtimeService);

constructor(private snackBar: MatSnackBar) { }

notify(message: string) {
this.snackBar.open(message, "Close", {
duration: 10000,
horizontalPosition: 'end',
verticalPosition: 'top'
});
}

async ngOnInit() {
this.user = await this.userService.currentUser();
this.featuredListings = await this.listingService.getFeaturedListings();

// Get notification when someone favourtied a listing
this.realtimeService.getNotifiedForFavourite((listingTitle) => {
const notifyMessage = this.user?.name?.length > 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need to move all those strings to global or component configs.

? `Hurry up! Another user has favorited the listing "${listingTitle}".`
: `A user has favorited the listing "${listingTitle}".`;
this.notify(notifyMessage);
});

this.realtimeService.getNotifiedForCheckout(async (listing: Listing, from: string, to: string) => {
// Get notification when someone booked any listing
let notifyMessage = this.user?.name?.length > 0
? `Hurry up! Another user has booked the listing "${listing.title}" but didn't complete the payment.`
: `A user has booked the listing "${listing}". but didn't complete the payment`;
this.notify(notifyMessage);

// Get notification when someone booked a listing which is favourited by current user
const favourites: Array<Listing> = await this.favoriteService.getFavoritesByUser(this.user) ?? [];

for (const favour of favourites) {
if (favour.id === listing.id) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just a styling suggestion but I feel the name is more representatitve. 'for (const fave of favorites) '

notifyMessage =
`"${favour.title}" in your favorites has been booked and it's no longer available between the dates ${from} ~ ${to}.`
this.notify(notifyMessage);
break;
}
}
});
}

async onFavoritedToggle(listing: Listing | null) {
Expand Down
7 changes: 5 additions & 2 deletions packages/portal/src/app/shared/favorite.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Injectable } from "@angular/core";
import { Injectable, inject } from "@angular/core";
import { UserRole } from "./user/user.service";
import { RealtimeService } from "./realtime.service";

@Injectable({
providedIn: "root",
})
export class FavoriteService {
private realtimeService = inject(RealtimeService);

async addFavorite(listing: Listing, user: User) {
const resource = await fetch("/api/favorites", {
method: "POST",
Expand All @@ -13,7 +16,7 @@ export class FavoriteService {
user,
}),
});

this.realtimeService.broadcastFavouriteNotification(listing);
return resource.status === 200;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For extensibility purposes... can that broadcast method be passed items that could be of different types? The we can just call it broadcast

}

Expand Down
10 changes: 10 additions & 0 deletions packages/portal/src/app/shared/listing.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Injectable, inject } from "@angular/core";
import { WindowService } from "../core/window/window.service";
import { RealtimeService } from "./realtime.service";

@Injectable({
providedIn: "root",
})
export class ListingService {
private windowService = inject(WindowService);
private realtimeService = inject(RealtimeService);

async getListings({ limit = 10, offset = 0 } = {}): Promise<Listing[]> {
const resource = await fetch(`/api/listings?limit=${limit}&offset=${offset}`).then(response => {
Expand Down Expand Up @@ -69,6 +71,7 @@ export class ListingService {
}

async reserve(reservationDetails: ReservationRequest): Promise<CheckoutSession> {
console.log("reservationDetails = ", reservationDetails)
const resource = await fetch(`/api/checkout`, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove debugging statement

method: "POST",
headers: {
Expand All @@ -80,6 +83,13 @@ export class ListingService {
if (resource.status !== 200) {
throw new Error(checkoutSession.error);
}
const listing = await this.getListingById(reservationDetails.listingId ?? "");
if (listing) {
this.realtimeService.broadcastCheckoutNotification(listing, reservationDetails.from, reservationDetails.to);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't gotten yet to the implementation of this broadcast. Maybe we can consolidate to one?

}
else {
console.log(`Invalid reservationDetails.listingId = ${reservationDetails.listingId}`);
}
return checkoutSession;
}
}
42 changes: 42 additions & 0 deletions packages/portal/src/app/shared/realtime.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Injectable } from "@angular/core";
import * as sioClient from "socket.io-client";
import { environment } from "src/environments/environment";

@Injectable({
providedIn: "root",
})
export class RealtimeService {
client = {} as sioClient.Socket;

constructor() {

if (!environment.notificationUrl || !environment.notificationPath) {
// TODO: disable this feature at the injector level if the URL or Path is not set.
alert("RealtimeService: Notification URL or Path is not set. Please check environment.ts file.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove alerts, we may want to only let deverlopers know on dev env?

}

this.client = sioClient.io(environment.notificationUrl, {
path: environment.notificationPath,
});
}

broadcastFavouriteNotification(listing: Listing) {
this.client.emit("sendFavourite", listing.title);
}

broadcastCheckoutNotification(listing: Listing, from: string, to: string) {
this.client.emit("sendCheckout", listing, from, to);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As suggested, maybe we make broadcast a single method that takes a type of { item, from?, to?) to consolidate maintenance and admit extensibility? This is not a blocker to merge at all. Just an optimization suggestion.

getNotifiedForFavourite(notifyAction: (listingTitle: string) => void | Promise<void>) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This two notifications could also be consolidated in the same fashion. on('event', message) {... }

this.client.on("notifyFavourite", async (listingTitle: string) => {
await notifyAction(listingTitle);
});
}

getNotifiedForCheckout(notifyAction: (listing: Listing, from: string, to: string) => void | Promise<void>) {
this.client.on("notifyCheckout", async (listing: Listing, from: string, to: string) => {
await notifyAction(listing, from, to);
});
}
}
1 change: 1 addition & 0 deletions packages/portal/src/environments/environment.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export const environment = {
strapiGraphQlUri: "{{SERVICE_CMS_URI_PLACEHOLDER}}/graphql",
aiEnableChat: "{{AI_ENABLE_CHAT_PLACEHOLDER}}",
aiChatApiUri: "{{AI_CHAT_API_URI_PLACEHOLDER}}",
notificationUrl: "{{WEB_PUB_SUB_URL_PLACEHOLDER}}",
};
2 changes: 2 additions & 0 deletions packages/portal/src/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const environment = {
strapiGraphQlUri: process.env["CODESPACE_NAME"] ? `https://${process.env["CODESPACE_NAME"]}-1337.${process.env["GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"]}/graphql` : 'http://localhost:1337/graphql',
aiEnableChat: process.env["AI_ENABLE_CHAT"],
aiChatApiUri: process.env["AI_CHAT_API_URI"],
notificationUrl: process.env["SERVICE_WEB_PUB_SUB_URL"] as string,
notificationPath: process.env["SERVICE_WEB_PUB_SUB_PATH"] as string,
};

/*
Expand Down
13 changes: 13 additions & 0 deletions packages/portal/src/styles/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,16 @@ ul {
height: 637px;
}
}


// ::ng-deep .success-snackbar {
// background: rgb(92, 147, 92) !important;
// background: rgb(92, 147, 92) !important;
// }

body {
.mdc-snackbar .mdc-snackbar__surface
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting -please move opening curly bracket to first line.

background-color: rgb(92, 147, 92);
}
}
2 changes: 2 additions & 0 deletions packages/realtime/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SERVICE_WEB_PUBSUB_CONNECTION_STRING=
SERVICE_WEB_PUBSUB_PORT=4242
43 changes: 43 additions & 0 deletions packages/realtime/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.

# Compiled output
/dist
/tmp
/out-tsc
/bazel-out

# Node
/node_modules
npm-debug.log
yarn-error.log

# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# Visual Studio Code

.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*

# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings

# System files
.DS_Store
Thumbs.db
Loading