-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Feat]: Notification List UI Implementation (#2711)
--------- Co-authored-by: Sebastian Leidig <[email protected]>
- Loading branch information
Showing
14 changed files
with
619 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { MatMenuTrigger } from "@angular/material/menu"; | ||
|
||
/** | ||
* Close the mat-menu of the given menu trigger | ||
* and stop propagation of the event to avoid closing parent menus as well. | ||
* @param menu | ||
* @param event | ||
*/ | ||
export function closeOnlySubmenu(menu: MatMenuTrigger, event: MouseEvent) { | ||
menu.closeMenu(); | ||
event.stopPropagation(); | ||
} |
57 changes: 57 additions & 0 deletions
57
src/app/features/notification/mock-notification.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { Injectable } from "@angular/core"; | ||
import moment from "moment"; | ||
import { v4 as uuid } from "uuid"; | ||
|
||
// TODO: Need to remove this file once the notification UI testing is completed. | ||
|
||
@Injectable({ | ||
providedIn: "root", | ||
}) | ||
export class MockNotificationsService { | ||
mockNotifications = [ | ||
createDummyNotification(true, moment().subtract(1, "hour").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "day").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "week").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "month").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
createDummyNotification(true, moment().subtract(1, "year").toDate()), | ||
]; | ||
|
||
getNotifications() { | ||
return this.mockNotifications; | ||
} | ||
} | ||
|
||
function createDummyNotification( | ||
read: boolean = true, | ||
date: Date = new Date(), | ||
title: string = "Dummy Notification Title", | ||
) { | ||
return { | ||
_id: "Notification:" + uuid(), | ||
_rev: "1-4d6e4511f7e4dd8679e19f0cc2d9c22e", | ||
title: title, | ||
body: "This is a dummy notification body.", | ||
actionURL: "http://localhost:4200", | ||
sentBy: "demo", | ||
fcmToken: "", | ||
readStatus: read, | ||
created: { | ||
at: date.toISOString(), | ||
}, | ||
updated: { | ||
at: date.toISOString(), | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Entity } from "../../../core/entity/model/entity"; | ||
import { DatabaseField } from "../../../core/entity/database-field.decorator"; | ||
import { DatabaseEntity } from "../../../core/entity/database-entity.decorator"; | ||
|
||
/** | ||
* This represents one specific notification event for one specific user, | ||
* displayed in the UI through the notification indicator in the toolbar. | ||
*/ | ||
@DatabaseEntity("NotificationEvent") | ||
export class NotificationEvent extends Entity { | ||
@DatabaseField() title: string; | ||
@DatabaseField() body: string; | ||
@DatabaseField() actionURL: string; | ||
|
||
@DatabaseField() sentBy: string; | ||
@DatabaseField() fcmToken: string; | ||
@DatabaseField() readStatus: boolean; | ||
|
||
public override toString(): string { | ||
return `Notification: ${this.title} - ${this.body}`; | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
src/app/features/notification/notification-item/notification-item.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<div | ||
class="flex-row justify-space-between align-center notification-item-wrapper" | ||
[ngClass]="{ 'read-notification': notification.readStatus }" | ||
> | ||
<div class="notification-details"> | ||
<div i18n class="notification-title">{{ notification.title }}</div> | ||
<div i18n class="notification-body">{{ notification.body }}</div> | ||
<div | ||
i18n | ||
class="notification-time" | ||
[matTooltip]="notification.created?.at | date: 'short'" | ||
> | ||
{{ notification.created?.at | notificationTime }} | ||
</div> | ||
</div> | ||
|
||
<!-- Blue indicator for unread/read notifications --> | ||
<span | ||
[ngClass]="notification.readStatus ? 'read-indicator' : 'unread-indicator'" | ||
matTooltip="{{ | ||
notification.readStatus ? 'Read notification' : 'Unread notification' | ||
}}" | ||
i18n-matTooltip | ||
[matMenuTriggerFor]="actionsMenu" | ||
#actionsMenuTrigger="matMenuTrigger" | ||
></span> | ||
|
||
<mat-menu #actionsMenu="matMenu"> | ||
<div (click)="closeOnlySubmenu(actionsMenuTrigger, $event)"> | ||
@if (!notification.readStatus) { | ||
<button mat-menu-item (click)="updateReadStatus(true)"> | ||
<fa-icon icon="check"></fa-icon> | ||
<span class="menu-option" i18n>Mark as read</span> | ||
</button> | ||
} @else { | ||
<button mat-menu-item (click)="updateReadStatus(false)"> | ||
<fa-icon icon="eye-slash"></fa-icon> | ||
<span class="menu-option" i18n>Mark as unread</span> | ||
</button> | ||
} | ||
|
||
<button mat-menu-item (click)="handleDeleteNotification()"> | ||
<fa-icon icon="trash"></fa-icon> | ||
<span class="menu-option" i18n>Delete notification</span> | ||
</button> | ||
</div> | ||
</mat-menu> | ||
</div> |
52 changes: 52 additions & 0 deletions
52
src/app/features/notification/notification-item/notification-item.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
@use "variables/colors"; | ||
|
||
.notification-item-wrapper { | ||
padding: 8px 10px 0px; | ||
} | ||
|
||
.notification-details { | ||
flex: 1; | ||
text-align: left; | ||
} | ||
|
||
.notification-title { | ||
font-weight: bold; | ||
} | ||
|
||
.notification-body { | ||
font-size: 0.85em; | ||
} | ||
|
||
.notification-time { | ||
color: colors.$inactive; | ||
font-size: 12px; | ||
} | ||
|
||
.indicator { | ||
height: 12px; | ||
width: 12px; | ||
border-radius: 50%; | ||
box-sizing: border-box; | ||
display: inline-block; | ||
margin-right: 8px; | ||
cursor: pointer; | ||
} | ||
|
||
.unread-indicator { | ||
@extend .indicator; | ||
background-color: blue; | ||
} | ||
|
||
.read-indicator { | ||
@extend .indicator; | ||
background-color: transparent; | ||
border: 2px solid colors.$inactive; | ||
} | ||
|
||
.menu-option { | ||
margin-left: 12px; | ||
} | ||
|
||
.read-notification { | ||
color: colors.$inactive; | ||
} |
32 changes: 32 additions & 0 deletions
32
src/app/features/notification/notification-item/notification-item.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { ComponentFixture, TestBed } from "@angular/core/testing"; | ||
|
||
import { NotificationItemComponent } from "./notification-item.component"; | ||
import { NotificationEvent } from "../model/notification-event"; | ||
import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; | ||
import { NoopAnimationsModule } from "@angular/platform-browser/animations"; | ||
|
||
describe("NotificationItemComponent", () => { | ||
let component: NotificationItemComponent; | ||
let fixture: ComponentFixture<NotificationItemComponent>; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
imports: [ | ||
NotificationItemComponent, | ||
FontAwesomeTestingModule, | ||
NoopAnimationsModule, | ||
], | ||
}).compileComponents(); | ||
|
||
fixture = TestBed.createComponent(NotificationItemComponent); | ||
component = fixture.componentInstance; | ||
|
||
component.notification = new NotificationEvent(); | ||
|
||
fixture.detectChanges(); | ||
}); | ||
|
||
it("should create", () => { | ||
expect(component).toBeTruthy(); | ||
}); | ||
}); |
48 changes: 48 additions & 0 deletions
48
src/app/features/notification/notification-item/notification-item.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { Component, EventEmitter, Input, Output } from "@angular/core"; | ||
import { MatBadgeModule } from "@angular/material/badge"; | ||
import { CommonModule } from "@angular/common"; | ||
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; | ||
import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu"; | ||
import { MatButtonModule } from "@angular/material/button"; | ||
import { FormsModule } from "@angular/forms"; | ||
import { MatTooltipModule } from "@angular/material/tooltip"; | ||
import { MatTabsModule } from "@angular/material/tabs"; | ||
import { NotificationEvent } from "../model/notification-event"; | ||
import { NotificationTimePipe } from "../notification-time.pipe"; | ||
import { closeOnlySubmenu } from "../close-only-submenu"; | ||
|
||
@Component({ | ||
selector: "app-notification-item", | ||
standalone: true, | ||
imports: [ | ||
MatBadgeModule, | ||
FontAwesomeModule, | ||
MatMenu, | ||
MatButtonModule, | ||
MatMenuTrigger, | ||
MatMenuItem, | ||
FormsModule, | ||
MatTooltipModule, | ||
MatTabsModule, | ||
CommonModule, | ||
NotificationTimePipe, | ||
], | ||
templateUrl: "./notification-item.component.html", | ||
styleUrl: "./notification-item.component.scss", | ||
}) | ||
export class NotificationItemComponent { | ||
@Input() notification: NotificationEvent; | ||
|
||
@Output() readStatusChange = new EventEmitter<boolean>(); | ||
@Output() deleteClick = new EventEmitter<void>(); | ||
|
||
updateReadStatus(newStatus: boolean) { | ||
this.readStatusChange.emit(newStatus); | ||
} | ||
|
||
handleDeleteNotification() { | ||
this.deleteClick.emit(); | ||
} | ||
|
||
protected readonly closeOnlySubmenu = closeOnlySubmenu; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/** | ||
* Converts a timestamp into a human-readable time format, | ||
* such as "Just Now", "5m", "2h", "Yesterday", "3d", or "Jan 2024". | ||
*/ | ||
import { Pipe, PipeTransform } from "@angular/core"; | ||
|
||
@Pipe({ | ||
name: "notificationTime", | ||
standalone: true, | ||
}) | ||
export class NotificationTimePipe implements PipeTransform { | ||
transform(value: any): string { | ||
if (!value) return ""; | ||
|
||
const currentTime = new Date(); | ||
const notificationTime = new Date(value); | ||
if ( | ||
!(notificationTime instanceof Date) || | ||
isNaN(notificationTime.getTime()) | ||
) { | ||
return ""; | ||
} | ||
const timeDifference = currentTime.getTime() - notificationTime.getTime(); | ||
|
||
const seconds = Math.floor(timeDifference / 1000); | ||
const minutes = Math.floor(seconds / 60); | ||
const hours = Math.floor(minutes / 60); | ||
const days = Math.floor(hours / 24); | ||
|
||
if (seconds < 60) { | ||
return $localize`Just Now`; | ||
} else if (minutes < 60) { | ||
return $localize`${minutes}m`; | ||
} else if (hours < 24) { | ||
return $localize`${hours}h ago`; | ||
} else if (days === 1) { | ||
return $localize`Yesterday`; | ||
} else if (days < 7) { | ||
return $localize`${days}d ago`; | ||
} else if (days >= 7 && days < 30) { | ||
return $localize`${days}d ago`; | ||
} else { | ||
const monthYear = notificationTime.toLocaleString("en-US", { | ||
month: "short", | ||
year: "numeric", | ||
}); | ||
return monthYear; | ||
} | ||
} | ||
} |
Oops, something went wrong.