|
| 1 | +import React, { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react' |
| 2 | + |
| 3 | +import { useProfileContext } from '~/libs/core' |
| 4 | + |
| 5 | +import { dismiss, wasDismissed } from './localstorage.utils' |
| 6 | + |
| 7 | +export type NotificationType = 'success' | 'error' | 'info' | 'warning' | 'banner'; |
| 8 | + |
| 9 | +export interface Notification { |
| 10 | + id: string; |
| 11 | + type: NotificationType; |
| 12 | + icon?: ReactNode |
| 13 | + message: string; |
| 14 | + duration?: number; // in ms |
| 15 | +} |
| 16 | + |
| 17 | +type NotifyPayload = string | (Partial<Notification> & { message: string }) |
| 18 | + |
| 19 | +export interface NotificationContextType { |
| 20 | + notifications: Notification[]; |
| 21 | + notify: (message: NotifyPayload, type?: NotificationType, duration?: number) => Notification | void; |
| 22 | + showBannerNotification: (message: NotifyPayload) => Notification | void; |
| 23 | + removeNotification: (id: string) => void; |
| 24 | +} |
| 25 | + |
| 26 | +const NotificationContext = createContext<NotificationContextType | undefined>(undefined) |
| 27 | + |
| 28 | +export const useNotification = (): NotificationContextType => { |
| 29 | + const context = useContext(NotificationContext) |
| 30 | + if (!context) throw new Error('useNotification must be used within a NotificationProvider') |
| 31 | + return context |
| 32 | +} |
| 33 | + |
| 34 | +export const NotificationProvider: React.FC<{ |
| 35 | + children: ReactNode, |
| 36 | +}> = props => { |
| 37 | + const profileCtx = useProfileContext() |
| 38 | + const uuid = profileCtx.profile?.userId ?? 'annon' |
| 39 | + const [notifications, setNotifications] = useState<Notification[]>([]) |
| 40 | + |
| 41 | + const removeNotification = useCallback((id: string, persist?: boolean) => { |
| 42 | + setNotifications(prev => prev.filter(n => n.id !== id)) |
| 43 | + if (persist) { |
| 44 | + dismiss(id) |
| 45 | + } |
| 46 | + }, []) |
| 47 | + |
| 48 | + const notify = useCallback( |
| 49 | + (message: NotifyPayload, type: NotificationType = 'info', duration = 3000) => { |
| 50 | + const id = `${uuid}[${typeof message === 'string' ? message : message.id}]` |
| 51 | + const newNotification: Notification |
| 52 | + = typeof message === 'string' |
| 53 | + ? { duration, id, message, type } |
| 54 | + : { duration, type, ...message, id } |
| 55 | + |
| 56 | + if (wasDismissed(id)) { |
| 57 | + return undefined |
| 58 | + } |
| 59 | + |
| 60 | + setNotifications(prev => [...prev, newNotification]) |
| 61 | + |
| 62 | + if (duration > 0) { |
| 63 | + setTimeout(() => removeNotification(id), duration) |
| 64 | + } |
| 65 | + |
| 66 | + return newNotification |
| 67 | + }, |
| 68 | + [uuid], |
| 69 | + ) |
| 70 | + |
| 71 | + const showBannerNotification = useCallback(( |
| 72 | + message: NotifyPayload, |
| 73 | + ) => notify(message, 'banner', 0), [notify]) |
| 74 | + |
| 75 | + const ctxValue = useMemo(() => ({ |
| 76 | + notifications, |
| 77 | + notify, |
| 78 | + removeNotification, |
| 79 | + showBannerNotification, |
| 80 | + }), [ |
| 81 | + notifications, |
| 82 | + notify, |
| 83 | + removeNotification, |
| 84 | + showBannerNotification, |
| 85 | + ]) |
| 86 | + |
| 87 | + return ( |
| 88 | + <NotificationContext.Provider value={ctxValue}> |
| 89 | + {props.children} |
| 90 | + </NotificationContext.Provider> |
| 91 | + ) |
| 92 | +} |
0 commit comments