From cd70f370a7a02b7a7f4b24dc7d6328b452784486 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Wed, 20 Oct 2021 17:13:20 +0300 Subject: [PATCH] improve notifications performance --- .../components/notifications/notification.ts | 44 +++++++++ .../notifications.component.html | 63 +++++++------ .../notifications/notifications.component.ts | 89 ++++--------------- .../src/app/services/notifications.service.ts | 63 +++++++++++-- libs/red-ui-http/src/lib/model/models.ts | 2 +- ...{notification.ts => notification.model.ts} | 4 +- .../src/lib/model/notificationResponse.ts | 4 +- 7 files changed, 159 insertions(+), 110 deletions(-) create mode 100644 apps/red-ui/src/app/components/notifications/notification.ts rename libs/red-ui-http/src/lib/model/{notification.ts => notification.model.ts} (91%) diff --git a/apps/red-ui/src/app/components/notifications/notification.ts b/apps/red-ui/src/app/components/notifications/notification.ts new file mode 100644 index 000000000..2ac41f706 --- /dev/null +++ b/apps/red-ui/src/app/components/notifications/notification.ts @@ -0,0 +1,44 @@ +import { INotification } from '@redaction/red-ui-http'; +import { IListable } from '@iqser/common-ui'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { shareReplay } from 'rxjs/operators'; + +export class Notification implements INotification, IListable { + readonly creationDate?: string; + readonly issuerId?: string; + readonly notificationDetails?: string; + readonly notificationType?: string; + readonly readDate$: Observable; + readonly seenDate?: string; + readonly softDeleted?: string; + readonly target?: any; + readonly userId?: string; + readonly id: string; + private readonly _readDate$: BehaviorSubject; + + constructor(notification: INotification, readonly message: string, readonly time: string) { + this.creationDate = notification.creationDate; + this.issuerId = notification.issuerId; + this.notificationDetails = notification.notificationDetails; + this.notificationType = notification.notificationType; + this.seenDate = notification.seenDate; + this.softDeleted = notification.softDeleted; + this.target = notification.target; + this.userId = notification.userId; + this.id = notification.id; + this._readDate$ = new BehaviorSubject(notification.readDate); + this.readDate$ = this._readDate$.asObservable().pipe(shareReplay(1)); + } + + get searchKey(): string { + return this.id; + } + + get readDate(): string { + return this._readDate$.value; + } + + setReadDate(value: string | undefined): void { + this._readDate$.next(value); + } +} diff --git a/apps/red-ui/src/app/components/notifications/notifications.component.html b/apps/red-ui/src/app/components/notifications/notifications.component.html index 5cd178b50..d67495ee9 100644 --- a/apps/red-ui/src/app/components/notifications/notifications.component.html +++ b/apps/red-ui/src/app/components/notifications/notifications.component.html @@ -1,32 +1,37 @@ - - - + -
-
{{ group.dateString }}
-
- -
-
-
{{ getTime(notification.creationDate) }}
+ + + + + +
+
{{ group.date }}
+
+ +
+
+
{{ notification.time }}
+
+
+
-
-
-
+ + diff --git a/apps/red-ui/src/app/components/notifications/notifications.component.ts b/apps/red-ui/src/app/components/notifications/notifications.component.ts index 16d302b4a..7356378dd 100644 --- a/apps/red-ui/src/app/components/notifications/notifications.component.ts +++ b/apps/red-ui/src/app/components/notifications/notifications.component.ts @@ -1,25 +1,27 @@ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import * as moment from 'moment'; import { TranslateService } from '@ngx-translate/core'; -import { Notification, NotificationResponse } from '@redaction/red-ui-http'; import { DatePipe } from '@shared/pipes/date.pipe'; import { AppStateService } from '@state/app-state.service'; -import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { UserService } from '@services/user.service'; -import { NotificationType, NotificationTypeEnum } from '@models/notification-types'; -import { notificationsTranslations } from '../../translations/notifications-translations'; import { DossiersService } from '@services/entity-services/dossiers.service'; import { NotificationsService } from '@services/notifications.service'; +import { Notification } from '@components/notifications/notification'; +import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; @Component({ selector: 'redaction-notifications', templateUrl: './notifications.component.html', styleUrls: ['./notifications.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class NotificationsComponent { - notifications: Notification[]; - groupedNotifications: { dateString: string; notifications: Notification[] }[] = []; - hasUnreadNotifications = false; + notifications$ = this._notificationsService.getNotifications(false).pipe(shareReplay(1)); + hasUnreadNotifications$ = this.notifications$.pipe( + map(notifications => notifications.filter(n => !n.readDate).length > 0), + distinctUntilChanged(), + ); + groupedNotifications$ = this.notifications$.pipe(map(notifications => this._groupNotifications(notifications))); constructor( private readonly _translateService: TranslateService, @@ -28,61 +30,21 @@ export class NotificationsComponent { private readonly _appStateService: AppStateService, private readonly _dossiersService: DossiersService, private readonly _datePipe: DatePipe, - ) { - this._notificationsService.getNotifications(false).subscribe((response: NotificationResponse) => { - this.notifications = response.notifications.filter(notification => this._isSupportedType(notification)); - this._groupNotifications(this.notifications); - this.hasUnreadNotifications = this.notifications?.filter(n => !n.readDate).length > 0; - }); - } + ) {} - getTime(date: string): string { - moment.locale(this._translateService.currentLang); - return moment(date).format('hh:mm A'); - } - - getNotificationMessage(notification: Notification) { - return this._translate(notification, notificationsTranslations[notification.notificationType]); - } - - async markRead(notification: Notification, $event, isRead: boolean = true) { + async markRead(notification: Notification, $event, isRead = true) { $event.stopPropagation(); - const ids = [`${notification.id}`]; - await this._notificationsService.toggleNotificationRead(ids, isRead).toPromise(); + await this._notificationsService.toggleNotificationRead([notification.id], isRead).toPromise(); if (isRead) { - notification.readDate = moment().format('YYYY-MM-DDTHH:mm:ss Z'); + notification.setReadDate(moment().format('YYYY-MM-DDTHH:mm:ss Z')); } else { - notification.readDate = null; + notification.setReadDate(null); } } - private _isSupportedType(notification: Notification) { - return Object.values(NotificationTypeEnum).includes(notification.notificationType); - } - - private _getDossierHref(dossierId: string) { - return `/ui/main/dossiers/${dossierId}`; - } - - private _getFileHref(dossierId: string, fileId: string) { - const dossierUrl = this._getDossierHref(dossierId); - return `${dossierUrl}/file/${fileId}`; - } - - private _translate(notification: Notification, translation: string) { - const fileId = notification.target.fileId; - const dossierId = notification.target.dossierId; - return this._translateService.instant(translation, { - fileHref: this._getFileHref(dossierId, fileId), - dossierHref: this._getDossierHref(dossierId), - dossierName: notification.target?.dossierName || this._getDossierName(notification.target?.dossierId), - fileName: this._getFileName(notification.target?.dossierId, notification.target?.fileId), - user: this._getUsername(notification.userId), - }); - } - - private _groupNotifications(notifications: Notification[]) { + private _groupNotifications(notifications: Notification[]): { date: string; notifications: Notification[] }[] { const res = {}; + for (const notification of notifications) { const date = this._datePipe.transform(notification.creationDate, 'sophisticatedDate'); if (!res[date]) { @@ -90,22 +52,7 @@ export class NotificationsComponent { } res[date].push(notification); } - for (const key of Object.keys(res)) { - this.groupedNotifications.push({ dateString: key, notifications: res[key] }); - } - } - private _getDossierName(dossierId: string | undefined) { - const dossier = this._dossiersService.find(dossierId); - return dossier?.dossierName || this._translateService.instant(_('dossier')); - } - - private _getFileName(dossierId: string | undefined, fileId: string | undefined) { - const file = this._dossiersService.find(dossierId, fileId); - return file?.filename || this._translateService.instant(_('file')); - } - - private _getUsername(userId: string | undefined) { - return this._userService.getNameForId(userId) || this._translateService.instant(_('unknown')); + return Object.keys(res).map(key => ({ date: key, notifications: res[key] })); } } diff --git a/apps/red-ui/src/app/services/notifications.service.ts b/apps/red-ui/src/app/services/notifications.service.ts index 8860df5a3..7f90d19b2 100644 --- a/apps/red-ui/src/app/services/notifications.service.ts +++ b/apps/red-ui/src/app/services/notifications.service.ts @@ -1,23 +1,48 @@ import { Injectable, Injector } from '@angular/core'; -import { GenericService, List, QueryParam, RequiredParam, Validate } from '@iqser/common-ui'; -import { NotificationResponse } from '@redaction/red-ui-http'; +import { GenericService, List, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui'; +import { INotification, NotificationResponse } from '@redaction/red-ui-http'; +import * as moment from 'moment'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { Notification } from '@components/notifications/notification'; +import { map } from 'rxjs/operators'; +import { notificationsTranslations } from '../translations/notifications-translations'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { DossiersService } from '@services/entity-services/dossiers.service'; +import { UserService } from '@services/user.service'; +import { NotificationType, NotificationTypeEnum } from '@models/notification-types'; @Injectable({ providedIn: 'root', }) export class NotificationsService extends GenericService { - constructor(protected readonly _injector: Injector) { + constructor( + protected readonly _injector: Injector, + private readonly _translateService: TranslateService, + private readonly _dossiersService: DossiersService, + private readonly _userService: UserService, + ) { super(_injector, 'notification'); } @Validate() - getNotifications(@RequiredParam() includeSeen: boolean) { + getNotifications(@RequiredParam() includeSeen: boolean): Observable { let queryParam: QueryParam; if (includeSeen !== undefined && includeSeen !== null) { queryParam = { key: 'includeSeen', value: includeSeen }; } - return this._getOne([], this._defaultModelPath, [queryParam]); + return this._getOne([], this._defaultModelPath, [queryParam]).pipe( + map(response => response.notifications.filter(notification => this._isSupportedType(notification))), + mapEach( + notification => + new Notification( + notification, + this._translate(notification, notificationsTranslations[notification.notificationType]), + this._getTime(notification.creationDate), + ), + ), + ); } @Validate() @@ -29,4 +54,32 @@ export class NotificationsService extends GenericService { return this._post(body, `${this._defaultModelPath}/toggle-read`, [queryParam]); } + + private _getTime(date: string): string { + moment.locale(this._translateService.currentLang); + return moment(date).format('hh:mm A'); + } + + private _translate(notification: INotification, translation: string) { + const fileId = notification.target.fileId; + const dossierId = notification.target.dossierId; + const dossier = this._dossiersService.find(dossierId); + const file = dossier.files.find(f => f.fileId === fileId); + + return this._translateService.instant(translation, { + fileHref: file?.routerLink, + dossierHref: dossier?.routerLink, + dossierName: notification.target?.dossierName || dossier?.dossierName || this._translateService.instant(_('dossier')), + fileName: file?.filename || this._translateService.instant(_('file')), + user: this._getUsername(notification.userId), + }); + } + + private _getUsername(userId: string | undefined) { + return this._userService.getNameForId(userId) || this._translateService.instant(_('unknown')); + } + + private _isSupportedType(notification: INotification) { + return Object.values(NotificationTypeEnum).includes(notification.notificationType); + } } diff --git a/libs/red-ui-http/src/lib/model/models.ts b/libs/red-ui-http/src/lib/model/models.ts index 6b0c060bc..bf9664134 100644 --- a/libs/red-ui-http/src/lib/model/models.ts +++ b/libs/red-ui-http/src/lib/model/models.ts @@ -49,7 +49,7 @@ export * from './manualRedactionEntry'; export * from './manualRedactions'; export * from './matchedDocument'; export * from './matchedSection'; -export * from './notification'; +export * from './notification.model'; export * from './notificationResponse'; export * from './pageExclusionRequest'; export * from './pageRange'; diff --git a/libs/red-ui-http/src/lib/model/notification.ts b/libs/red-ui-http/src/lib/model/notification.model.ts similarity index 91% rename from libs/red-ui-http/src/lib/model/notification.ts rename to libs/red-ui-http/src/lib/model/notification.model.ts index cd163288f..a4712ce1b 100644 --- a/libs/red-ui-http/src/lib/model/notification.ts +++ b/libs/red-ui-http/src/lib/model/notification.model.ts @@ -10,9 +10,9 @@ * Do not edit the class manually. */ -export interface Notification { +export interface INotification { creationDate?: string; - id?: number; + id?: string; issuerId?: string; notificationDetails?: string; notificationType?: string; diff --git a/libs/red-ui-http/src/lib/model/notificationResponse.ts b/libs/red-ui-http/src/lib/model/notificationResponse.ts index ceaa65eb9..161b9213e 100644 --- a/libs/red-ui-http/src/lib/model/notificationResponse.ts +++ b/libs/red-ui-http/src/lib/model/notificationResponse.ts @@ -9,8 +9,8 @@ * https://github.com/swagger-api/swagger-codegen.git * Do not edit the class manually. */ -import { Notification } from './notification'; +import { INotification } from './notification.model'; export interface NotificationResponse { - notifications?: Array; + notifications?: Array; }