/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ import { DEFAULT_NOTIFICATION_STORE, ManagedNotificationStore, NotificationContext } from './context'; import { ManagedNotification, Notification, NotificationEmitter, NotificationInput, NotificationPriority, } from './types'; import { nanoid } from 'nanoid'; import { sortBy } from 'lodash'; import { useImmer } from 'use-immer'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; const generateId = () => nanoid(10); const DEFAULT_AUTO_DIMSISS_DURATION = 7500; export interface NotificationPriorityMapper { (notification: NotificationInput): number; } const DEFAULT_PRIORITY_MAPPER: NotificationPriorityMapper = (notification) => { if (notification.priority != null) return notification.priority; switch (notification.type) { case 'error': return NotificationPriority.HIGH; case 'warning': return NotificationPriority.MEDIUM; case 'info': return NotificationPriority.LOW; case 'success': return NotificationPriority.LOW; } }; export interface NotificationDefaults extends Pick, 'variant' | 'dismissible'> { priority: Notification['priority'] | NotificationPriorityMapper; } const NOTIFICATION_DEFAULTS: NotificationDefaults = { variant: 'default', dismissible: true, priority: DEFAULT_PRIORITY_MAPPER, }; export interface NotificationProviderProps { /** Default duration for "auto" dismiss. */ autoDismissDuration?: number; notificationDefaults?: Partial; } export const NotificationProvider: React.FC = ({ children, notificationDefaults: notificationDefaultsProp, autoDismissDuration = DEFAULT_AUTO_DIMSISS_DURATION, }) => { const emitter = useMemo(() => { return new NotificationEmitter(); }, []); const [store, updateStore] = useImmer(DEFAULT_NOTIFICATION_STORE); const defaults = useMemo(() => { return { ...NOTIFICATION_DEFAULTS, ...notificationDefaultsProp, }; }, [JSON.stringify(notificationDefaultsProp)]); // eslint-disable-line react-hooks/exhaustive-deps const autoDismissTimeoutsRef = useRef<[string, any][]>([]); // clean up auto dismiss timeouts on unmount useEffect(() => { return () => { autoDismissTimeoutsRef.current.forEach(([, timeout]) => clearTimeout(timeout)); }; }, []); const dismissNotification = useCallback( (id: string) => { updateStore((draft) => { const notificationIndex: number = draft.allNotifications.findIndex((v) => v.id === id); if (notificationIndex === -1) { console.error(`NotificationProvider:dismissNotification:: not found ${id}`); return; } const notification: ManagedNotification = draft.allNotifications[notificationIndex]; notification.originalOnDismiss && notification.originalOnDismiss(); // remove notification in place draft.allNotifications.splice(notificationIndex, 1); switch (notification.variant) { case 'default': { const index = draft.defaultNotifications.findIndex((v) => v.id === id); index !== -1 && draft.defaultNotifications.splice(index, 1); break; } case 'flashbar': { const index = draft.flashbarNotifications.findIndex((v) => v.id === id); index !== -1 && draft.flashbarNotifications.splice(index, 1); break; } case 'brief': { const index = draft.briefNotifications.findIndex((v) => v.id === id); index !== -1 && draft.briefNotifications.splice(index, 1); break; } } emitter.emit('DISMISSED', notification); }); }, [updateStore], ); const dismissAllNotifications = useCallback( (variant) => { updateStore((draft) => { const _store: ManagedNotificationStore = DEFAULT_NOTIFICATION_STORE; draft.allNotifications.forEach((notification) => { if (variant == null || notification.variant === variant) { notification.originalOnDismiss && notification.originalOnDismiss(); emitter.emit('DISMISSED', notification); } else { _store.allNotifications.push(notification); switch (notification.variant) { case 'default': { _store.defaultNotifications.push(notification); break; } case 'flashbar': { _store.flashbarNotifications.push(notification); break; } case 'brief': { _store.briefNotifications.push(notification); break; } } } }); return _store; }); }, [updateStore, emitter], ); const addNotification = useCallback( (_input: NotificationInput): string => { const input: Omit & Omit & Pick = { ...defaults, ..._input, priority: marshalPriority(_input, defaults), }; const id = input.id || generateId(); let dismissible = input.dismissible; const onDismiss = () => { dismissNotification(id); try { const autoDismissIndex = autoDismissTimeoutsRef.current.findIndex((tuple) => tuple[0] === id); if (autoDismissIndex !== -1) autoDismissTimeoutsRef.current.splice(autoDismissIndex, 1); } catch (error) { console.warn('Failed to remove notification from auto dismiss:', error); } }; if (typeof dismissible === 'number' || dismissible === 'auto') { autoDismissTimeoutsRef.current.push([ id, setTimeout(onDismiss, getAutoDismissDuration(dismissible, autoDismissDuration)), ]); dismissible = true; } const notification: ManagedNotification = { ...input, id, timestamp: input.timestamp || Date.now(), priority: input.priority, dismissible: dismissible !== false, originalOnDismiss: input.onDismiss, onDismiss, }; updateStore((draft) => { // ignore duplicate notifications if (draft.allNotifications.find((n) => n.id === id)) return; draft.allNotifications = sortBy(draft.allNotifications.concat([notification]), ['priority', 'timestamp']); switch (notification.variant) { case 'default': { draft.defaultNotifications = sortBy(draft.defaultNotifications.concat([notification]), [ 'priority', 'timestamp', ]); break; } case 'flashbar': { draft.flashbarNotifications = sortBy(draft.flashbarNotifications.concat([notification]), [ 'priority', 'timestamp', ]); break; } case 'brief': { draft.briefNotifications = sortBy(draft.briefNotifications.concat([notification]), [ 'priority', 'timestamp', ]); break; } } }); emitter.emit('ADDED', notification); return id; }, [dismissNotification, updateStore, defaults, autoDismissDuration, emitter], ); const addFatal = useCallback( (notification) => { return addNotification({ type: 'error', priority: NotificationPriority.EVEREST, variant: 'flashbar', ...notification, dismissible: false, onDismiss: undefined, }); }, [addNotification], ); const addError = useCallback( (notification) => { return addNotification({ type: 'error', variant: 'flashbar', ...notification, }); }, [addNotification], ); const addWarning = useCallback( (notification) => { return addNotification({ type: 'warning', variant: 'flashbar', ...notification, }); }, [addNotification], ); const addInfo = useCallback( (notification) => { return addNotification({ type: 'info', variant: 'flashbar', ...notification, }); }, [addNotification], ); const addSuccess = useCallback( ({ dismissible, ...notification }) => { return addNotification({ dismissible: dismissible || 'auto', type: 'success', variant: 'flashbar', ...notification, }); }, [addNotification], ); const addNotifications = useCallback( (notifications) => { notifications.forEach((notification) => addNotification(notification)); }, [addNotification], ); const addFlashbar = useCallback( (notification) => { return addNotification({ ...notification, variant: 'flashbar', elementOptions: { autoHideDuration: getAutoDismissDuration( notification.elementOptions?.autoHideDuration || notification.dismissible, autoDismissDuration, ), }, }); }, [addNotification, autoDismissDuration], ); const addBrief = useCallback( (notification) => { return addNotification({ ...notification, dismissible: true, variant: 'brief', elementOptions: { autoHideDuration: getAutoDismissDuration( notification.elementOptions?.autoHideDuration || notification.dismissible, autoDismissDuration, ), }, }); }, [addNotification, autoDismissDuration], ); const onAddedNotification = useCallback( (listener) => { emitter.on('ADDED', listener); return () => emitter.off('ADDED', listener); }, [emitter], ); const onDismissedNotification = useCallback( (listener) => { emitter.on('DISMISSED', listener); return () => emitter.off('DISMISSED', listener); }, [emitter], ); const context: NotificationContext = { ...store, addNotification, addFatal, addError, addWarning, addInfo, addSuccess, addNotifications, dismissNotification, dismissAllNotifications, addFlashbar, addBrief, onAddedNotification, onDismissedNotification, }; return {children}; }; function marshalPriority(input: NotificationInput, defaults: NotificationDefaults): number { if (input.priority != null) return input.priority; if (typeof defaults.priority === 'function') return defaults.priority(input); if (typeof defaults.priority === 'number') return defaults.priority; return NotificationPriority.MEDIUM; } function getAutoDismissDuration(dismissible: NotificationInput['dismissible'], defaultDuration: number): number { if (typeof dismissible === 'number') { return dismissible; } return defaultDuration; }