All files / services notification.service.ts

100% Statements 29/29
94.73% Branches 18/19
100% Functions 9/9
100% Lines 25/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 773x 3x     3x 3x 3x                     3x 16x   16x 16x     16x 16x     16x   13x 13x   13x                   9x       3x       4x       7x               7x 1x     7x 3x 2x       7x      
import { inject, Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
 
export type ToastKind = 'error' | 'info' | 'success';
const errorDurationMs = 6000;
const messageDurationMs = 3000;
const throttleTimeMs = 2000;
 
export interface PersistentNotificationConfig {
    readonly message: string;
    readonly actionLabel: string;
    readonly onAction: () => void;
    readonly onDismiss?: () => void;
    readonly kind?: ToastKind;
}
 
@Injectable({ providedIn: 'root' })
export class NotificationService {
    private readonly snackBar = inject(MatSnackBar);
 
    private lastKey: string | undefined = undefined;
    private lastAt = 0;
 
    private show(message: string, kind: ToastKind = 'info'): void {
        const key = `${kind}:${message}`;
        const now = Date.now();
 
        // Simple dedupe/throttle: ignore same message within throttleTimeMs
        if (this.lastKey === key && now - this.lastAt < throttleTimeMs) return;
 
        this.lastKey = key;
        this.lastAt = now;
 
        this.snackBar.open(message, 'OK', {
            duration: kind === 'error' ? errorDurationMs : messageDurationMs,
            horizontalPosition: 'end',
            verticalPosition: 'top',
            panelClass: [`toast-${kind}`],
            politeness: kind === 'error' ? 'assertive' : 'polite',
        });
    }
 
    error(message: string): void {
        this.show(message, 'error');
    }
 
    success(message: string): void {
        this.show(message, 'success');
    }
 
    info(message: string): void {
        this.show(message, 'info');
    }
 
    showPersistent(config: PersistentNotificationConfig): () => void {
        const ref = this.snackBar.open(config.message, config.actionLabel, {
            duration: 0,
            horizontalPosition: 'center',
            verticalPosition: 'bottom',
            panelClass: [`toast-${config.kind ?? 'info'}`, 'toast-persistent'],
            politeness: 'polite',
        });
 
        ref.onAction().subscribe(() => {
            config.onAction();
        });
 
        ref.afterDismissed().subscribe(({ dismissedByAction }) => {
            if (!dismissedByAction) {
                config.onDismiss?.();
            }
        });
 
        return () => ref.dismiss();
    }
}