diff --git a/apps/demo/src/app/pages/notification/examples/03-close-notification/index.html b/apps/demo/src/app/pages/notification/examples/03-close-notification/index.html
index e46c535c9..e993760a3 100644
--- a/apps/demo/src/app/pages/notification/examples/03-close-notification/index.html
+++ b/apps/demo/src/app/pages/notification/examples/03-close-notification/index.html
@@ -1,6 +1,7 @@
+
+
+ Notifications spawned by Service Worker support only
+
+ click
+
+ and
+
+ close
+
+ events!
+
inject(NAVIGATOR).serviceWorker,
+ },
+);
diff --git a/libs/notification/src/index.ts b/libs/notification/src/index.ts
index 704a29e93..19c0c8496 100644
--- a/libs/notification/src/index.ts
+++ b/libs/notification/src/index.ts
@@ -3,4 +3,5 @@
*/
export * from './tokens/support';
+export * from './tokens/notification-factory';
export * from './services/notification.service';
diff --git a/libs/notification/src/services/notification.service.ts b/libs/notification/src/services/notification.service.ts
index a7ef701cd..f49d5db45 100644
--- a/libs/notification/src/services/notification.service.ts
+++ b/libs/notification/src/services/notification.service.ts
@@ -1,8 +1,23 @@
import {Inject, Injectable} from '@angular/core';
-import {defer, fromEvent, Observable, throwError} from 'rxjs';
+import {SERVICE_WORKER} from '@ng-web-apis/common';
+import {
+ filter,
+ from,
+ fromEvent,
+ map,
+ NEVER,
+ Observable,
+ shareReplay,
+ switchMap,
+ throwError,
+} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
+import {NOTIFICATION_SW_CLICKS} from '../tokens/notification-clicks';
+import {NOTIFICATION_SW_CLOSES} from '../tokens/notification-closes';
+import {NOTIFICATION_FACTORY} from '../tokens/notification-factory';
import {NOTIFICATION_SUPPORT} from '../tokens/support';
+import {InjectionTokenType} from '../types/injection-token-type';
const NOT_SUPPORTED_ERROR$ = throwError(
() => new Error('Notification API is not supported in your browser'),
@@ -12,7 +27,26 @@ const NOT_SUPPORTED_ERROR$ = throwError(
providedIn: 'root',
})
export class NotificationService {
- constructor(@Inject(NOTIFICATION_SUPPORT) private readonly support: boolean) {}
+ private readonly swRegistration$ = from(this.sw.getRegistration()).pipe(
+ shareReplay(1),
+ );
+
+ constructor(
+ @Inject(NOTIFICATION_SUPPORT) private readonly support: boolean,
+ @Inject(NOTIFICATION_FACTORY)
+ private readonly createNotification: InjectionTokenType<
+ typeof NOTIFICATION_FACTORY
+ >,
+ @Inject(NOTIFICATION_SW_CLICKS)
+ private readonly notificationSwClicks$: InjectionTokenType<
+ typeof NOTIFICATION_SW_CLICKS
+ >,
+ @Inject(NOTIFICATION_SW_CLOSES)
+ private readonly notificationSwCloses$: InjectionTokenType<
+ typeof NOTIFICATION_SW_CLOSES
+ >,
+ @Inject(SERVICE_WORKER) private readonly sw: ServiceWorkerContainer,
+ ) {}
requestPermission(): Observable {
if (!this.support) {
@@ -36,15 +70,48 @@ export class NotificationService {
return NOT_SUPPORTED_ERROR$;
}
- return defer(() => {
- const notification = new Notification(title, options);
- const close$ = fromEvent(notification, 'close');
+ return from(this.createNotification(title, options)).pipe(
+ switchMap(notification => {
+ const close$ = this.fromEvent(notification, 'close');
- return new Observable(subscriber => {
- subscriber.next(notification);
+ return new Observable(subscriber => {
+ subscriber.next(notification);
- return () => notification.close();
- }).pipe(takeUntil(close$));
- });
+ return () => notification.close();
+ }).pipe(takeUntil(close$));
+ }),
+ );
+ }
+
+ fromEvent(
+ targetNotification: Notification,
+ eventName: E,
+ ): Observable<{action: string} | void> {
+ const mapToVoid = map(() => undefined);
+
+ return this.swRegistration$.pipe(
+ switchMap(swRegistration => {
+ if (!swRegistration) {
+ return fromEvent(targetNotification, eventName).pipe(mapToVoid);
+ }
+
+ const isTargetNotification = (notification: {timestamp?: number}) =>
+ notification.timestamp === targetNotification.timestamp;
+
+ switch (eventName) {
+ case 'click':
+ return this.notificationSwClicks$.pipe(
+ filter(x => isTargetNotification(x.notification)),
+ );
+ case 'close':
+ return this.notificationSwCloses$.pipe(
+ filter(isTargetNotification),
+ mapToVoid,
+ );
+ default:
+ return NEVER;
+ }
+ }),
+ );
}
}
diff --git a/libs/notification/src/tokens/notification-clicks.ts b/libs/notification/src/tokens/notification-clicks.ts
new file mode 100644
index 000000000..4bd4adac5
--- /dev/null
+++ b/libs/notification/src/tokens/notification-clicks.ts
@@ -0,0 +1,14 @@
+import {inject, InjectFlags, InjectionToken} from '@angular/core';
+import {SwPush} from '@angular/service-worker';
+import {NEVER} from 'rxjs';
+
+export const NOTIFICATION_SW_CLICKS = new InjectionToken(
+ `Global listener for events when ANY system notification spawned by Notification API (and only inside Service Worker!) has been clicked`,
+ {
+ factory: () => {
+ const swPush = inject(SwPush, InjectFlags.Optional);
+
+ return swPush && swPush.isEnabled ? swPush.notificationClicks : NEVER;
+ },
+ },
+);
diff --git a/libs/notification/src/tokens/notification-closes.ts b/libs/notification/src/tokens/notification-closes.ts
new file mode 100644
index 000000000..7db38dbf0
--- /dev/null
+++ b/libs/notification/src/tokens/notification-closes.ts
@@ -0,0 +1,44 @@
+import {inject, InjectionToken, NgZone} from '@angular/core';
+import {ANIMATION_FRAME, SERVICE_WORKER} from '@ng-web-apis/common';
+import {
+ combineLatest,
+ filter,
+ from,
+ map,
+ NEVER,
+ Observable,
+ pairwise,
+ share,
+ switchMap,
+} from 'rxjs';
+import {zoneOptimized} from '../utils/zone';
+
+export const NOTIFICATION_SW_CLOSES = new InjectionToken(
+ `Global listener for events when ANY system notification spawned by Notification API (and only inside Service Worker!) has been closed`,
+ {
+ /**
+ * TODO: refactor the token's factory after this issue will be solved:
+ * https://github.com/angular/angular/issues/52244
+ * ```
+ * const swPush = inject(SwPush, InjectFlags.Optional);
+ * return swPush && swPush.isEnabled ? swPush.notificationCloses : NEVER;
+ * ```
+ */
+ factory: (): Observable => {
+ return combineLatest([
+ from(inject(SERVICE_WORKER).getRegistration()),
+ inject(ANIMATION_FRAME),
+ ]).pipe(
+ switchMap(([reg]) => (reg ? from(reg.getNotifications()) : NEVER)),
+ pairwise(),
+ filter(([prev, cur]) => prev.length > cur.length),
+ map(
+ ([prev, cur]) =>
+ prev.find((notification, i) => notification !== cur[i])!,
+ ),
+ zoneOptimized(inject(NgZone)),
+ share(),
+ );
+ },
+ },
+);
diff --git a/libs/notification/src/tokens/notification-factory.ts b/libs/notification/src/tokens/notification-factory.ts
new file mode 100644
index 000000000..d0ed5887c
--- /dev/null
+++ b/libs/notification/src/tokens/notification-factory.ts
@@ -0,0 +1,27 @@
+import {inject, InjectionToken} from '@angular/core';
+import {SERVICE_WORKER} from '@ng-web-apis/common';
+
+export const NOTIFICATION_FACTORY = new InjectionToken(
+ 'An async function to create Notification using Notification API (with and without service worker)',
+ {
+ factory: () => {
+ const sw = inject(SERVICE_WORKER);
+
+ return async (
+ ...args: ConstructorParameters
+ ): Promise => {
+ const registration = await sw.getRegistration();
+
+ if (registration) {
+ await registration.showNotification(...args);
+
+ const notifications = await registration.getNotifications();
+
+ return notifications[notifications.length - 1];
+ } else {
+ return new Notification(...args);
+ }
+ };
+ },
+ },
+);
diff --git a/libs/notification/src/types/injection-token-type.ts b/libs/notification/src/types/injection-token-type.ts
new file mode 100644
index 000000000..615a48f5b
--- /dev/null
+++ b/libs/notification/src/types/injection-token-type.ts
@@ -0,0 +1,6 @@
+import type {InjectionToken} from '@angular/core';
+
+// TODO: discuss with team what should we do with this code duplication
+// Could we use `@taiga-ui/cdk` inside `@ng-web-apis/notification` ?
+
+export type InjectionTokenType = Token extends InjectionToken ? T : never;
diff --git a/libs/notification/src/utils/zone.ts b/libs/notification/src/utils/zone.ts
new file mode 100644
index 000000000..c034346ac
--- /dev/null
+++ b/libs/notification/src/utils/zone.ts
@@ -0,0 +1,27 @@
+import {NgZone} from '@angular/core';
+import {MonoTypeOperatorFunction, Observable, pipe} from 'rxjs';
+
+// TODO: discuss with team what should we do with this code duplication
+// Could we use `@taiga-ui/cdk` inside `@ng-web-apis/notification` ?
+
+export function zonefree(zone: NgZone): MonoTypeOperatorFunction {
+ return source =>
+ new Observable(subscriber =>
+ zone.runOutsideAngular(() => source.subscribe(subscriber)),
+ );
+}
+
+export function zonefull(zone: NgZone): MonoTypeOperatorFunction {
+ return source =>
+ new Observable(subscriber =>
+ source.subscribe({
+ next: value => zone.run(() => subscriber.next(value)),
+ error: (error: unknown) => zone.run(() => subscriber.error(error)),
+ complete: () => zone.run(() => subscriber.complete()),
+ }),
+ );
+}
+
+export function zoneOptimized(zone: NgZone): MonoTypeOperatorFunction {
+ return pipe(zonefree(zone), zonefull(zone));
+}