improve notifications performance
This commit is contained in:
parent
8d19b560fe
commit
cd70f370a7
44
apps/red-ui/src/app/components/notifications/notification.ts
Normal file
44
apps/red-ui/src/app/components/notifications/notification.ts
Normal file
@ -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<string>;
|
||||
readonly seenDate?: string;
|
||||
readonly softDeleted?: string;
|
||||
readonly target?: any;
|
||||
readonly userId?: string;
|
||||
readonly id: string;
|
||||
private readonly _readDate$: BehaviorSubject<string | null>;
|
||||
|
||||
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<string | null>(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);
|
||||
}
|
||||
}
|
||||
@ -1,32 +1,37 @@
|
||||
<iqser-circle-button [matMenuTriggerFor]="overlay" [showDot]="hasUnreadNotifications" icon="red:notification"></iqser-circle-button>
|
||||
<mat-menu #overlay="matMenu" backdropClass="notifications-backdrop" class="notifications-menu" xPosition="before">
|
||||
<iqser-empty-state
|
||||
*ngIf="!groupedNotifications.length"
|
||||
[horizontalPadding]="40"
|
||||
[text]="'notifications.no-data' | translate"
|
||||
[verticalPadding]="0"
|
||||
></iqser-empty-state>
|
||||
<iqser-circle-button [matMenuTriggerFor]="menu" [showDot]="hasUnreadNotifications$ | async" icon="red:notification"></iqser-circle-button>
|
||||
|
||||
<div *ngFor="let group of groupedNotifications">
|
||||
<div class="all-caps-label">{{ group.dateString }}</div>
|
||||
<div
|
||||
(click)="markRead(notification, $event)"
|
||||
*ngFor="let notification of group.notifications | sortBy: 'desc':'creationDate'"
|
||||
[class.unread]="!notification.readDate"
|
||||
class="notification"
|
||||
mat-menu-item
|
||||
>
|
||||
<redaction-initials-avatar [user]="notification.userId"></redaction-initials-avatar>
|
||||
<div class="notification-content">
|
||||
<div [innerHTML]="getNotificationMessage(notification)"></div>
|
||||
<div class="small-label mt-2">{{ getTime(notification.creationDate) }}</div>
|
||||
<mat-menu #menu="matMenu" backdropClass="notifications-backdrop" class="notifications-menu" xPosition="before">
|
||||
<ng-template matMenuContent>
|
||||
<ng-container *ngIf="groupedNotifications$ | async as groups">
|
||||
<iqser-empty-state
|
||||
*ngIf="!groups.length"
|
||||
[horizontalPadding]="40"
|
||||
[text]="'notifications.no-data' | translate"
|
||||
[verticalPadding]="0"
|
||||
></iqser-empty-state>
|
||||
|
||||
<div *ngFor="let group of groups">
|
||||
<div class="all-caps-label">{{ group.date }}</div>
|
||||
<div
|
||||
(click)="markRead(notification, $event)"
|
||||
*ngFor="let notification of group.notifications | sortBy: 'desc':'creationDate'"
|
||||
[class.unread]="(notification.readDate$ | async) === null"
|
||||
class="notification"
|
||||
mat-menu-item
|
||||
>
|
||||
<redaction-initials-avatar [user]="notification.userId"></redaction-initials-avatar>
|
||||
<div class="notification-content">
|
||||
<div [innerHTML]="notification.message"></div>
|
||||
<div class="small-label mt-2">{{ notification.time }}</div>
|
||||
</div>
|
||||
<div
|
||||
(click)="markRead(notification, $event, !notification.readDate)"
|
||||
class="dot"
|
||||
matTooltip="{{ 'notifications.mark-as' | translate: { type: notification.readDate ? 'unread' : 'read' } }}"
|
||||
matTooltipPosition="before"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
(click)="markRead(notification, $event, !notification.readDate)"
|
||||
[matTooltip]="'notifications.mark-as' | translate: { type: notification.readDate ? 'unread' : 'read' }"
|
||||
class="dot"
|
||||
matTooltipPosition="before"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</mat-menu>
|
||||
|
||||
@ -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(<NotificationType>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] }));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<NotificationResponse> {
|
||||
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<Notification[]> {
|
||||
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<NotificationResponse> {
|
||||
|
||||
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(<NotificationType>notification.notificationType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
@ -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<Notification>;
|
||||
notifications?: Array<INotification>;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user