-
Notifications
You must be signed in to change notification settings - Fork 550
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
Changes from 3 commits
16bf909
2d66fc9
07a1cbb
ebc957c
fddf143
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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[] = []; | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
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", | ||
|
@@ -13,7 +16,7 @@ export class FavoriteService { | |
user, | ||
}), | ||
}); | ||
|
||
this.realtimeService.broadcastFavouriteNotification(listing); | ||
return resource.status === 200; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
|
||
|
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 => { | ||
|
@@ -69,6 +71,7 @@ export class ListingService { | |
} | ||
|
||
async reserve(reservationDetails: ReservationRequest): Promise<CheckoutSession> { | ||
console.log("reservationDetails = ", reservationDetails) | ||
const resource = await fetch(`/api/checkout`, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove debugging statement |
||
method: "POST", | ||
headers: { | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
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."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
SERVICE_WEB_PUBSUB_CONNECTION_STRING= | ||
SERVICE_WEB_PUBSUB_PORT=4242 |
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 |
There was a problem hiding this comment.
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?