diff --git a/src/index.tsx b/src/index.tsx index 62b9575b..ebe7c342 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -117,8 +117,8 @@ const Toast = (props: ToastProps) => { [toast.closeButton, closeButtonFromToaster], ); const duration = React.useMemo( - () => toast.duration || durationFromToaster || TOAST_LIFETIME, - [toast.duration, durationFromToaster], + () => (toast.persistent ? Infinity : toast.duration || durationFromToaster || TOAST_LIFETIME), + [toast.duration, durationFromToaster, toast.persistent], ); const closeTimerStartTimeRef = React.useRef(0); const offset = React.useRef(0); @@ -609,6 +609,7 @@ const Toaster = React.forwardRef(function Toaster(pro gap = GAP, icons, containerAriaLabel = 'Notifications', + storageKey = 'sonner-toasts', } = props; const [toasts, setToasts] = React.useState([]); const possiblePositions = React.useMemo(() => { @@ -676,6 +677,56 @@ const Toaster = React.forwardRef(function Toaster(pro }); }, [toasts]); + React.useEffect(() => { + if (typeof window !== 'undefined') { + const storedToasts = localStorage.getItem(storageKey); + if (storedToasts) { + try { + const persistentToasts = JSON.parse(storedToasts); + + if (Array.isArray(persistentToasts) && persistentToasts.length) { + //Add in reverse order to preserve original order + let i = 0; + let numPersistentToasts = persistentToasts.length; + persistentToasts.reverse().forEach((persistedToast) => { + // Skip loading toasts - they represented old operations which are not longer consistent with the current state + if (persistedToast.type === 'loading') { + return; + } + + const toastData = { ...persistedToast, persistent: true, id: i + 1 }; + setTimeout( + () => { + // Re-add through ToastState so animations / logic stay consistent + ToastState.addToast(toastData); + }, + // Add ramping delay only for visibleToasts + i > numPersistentToasts - visibleToasts ? (i - (numPersistentToasts - visibleToasts)) * 200 : 0, + ); + i++; + }); + // Set the toast count to the number of persistent toasts + ToastState.setToastCount(numPersistentToasts); + } + } catch (error) { + console.error('Failed to parse stored toasts:', error); + localStorage.removeItem(storageKey); + } + } + } + }, []); + + React.useEffect(() => { + if (typeof window !== 'undefined') { + const persistentToasts = toasts.filter((toast) => toast.persistent); + if (persistentToasts.length > 0) { + localStorage.setItem(storageKey, JSON.stringify(persistentToasts)); + } else { + localStorage.removeItem(storageKey); + } + } + }, [toasts]); + React.useEffect(() => { if (theme !== 'system') { setActualTheme(theme); diff --git a/src/state.ts b/src/state.ts index 23aa3717..430b86dd 100644 --- a/src/state.ts +++ b/src/state.ts @@ -10,19 +10,19 @@ import type { import React from 'react'; -let toastsCounter = 1; - type titleT = (() => React.ReactNode) | React.ReactNode; class Observer { subscribers: Array<(toast: ExternalToast | ToastToDismiss) => void>; toasts: Array; dismissedToasts: Set; + toastCounter: number; constructor() { this.subscribers = []; this.toasts = []; this.dismissedToasts = new Set(); + this.toastCounter = 1; } // We use arrow functions to maintain the correct `this` reference @@ -53,7 +53,7 @@ class Observer { }, ) => { const { message, ...rest } = data; - const id = typeof data?.id === 'number' || data.id?.length > 0 ? data.id : toastsCounter++; + const id = typeof data?.id === 'number' || data.id?.length > 0 ? data.id : this.toastCounter++; const alreadyExists = this.toasts.find((toast) => { return toast.id === id; }); @@ -98,6 +98,10 @@ class Observer { return id; }; + setToastCount = (count: number) => { + this.toastCounter = count + 1; + }; + message = (message: titleT | React.ReactNode, data?: ExternalToast) => { return this.create({ ...data, message }); }; @@ -241,7 +245,7 @@ class Observer { }; custom = (jsx: (id: number | string) => React.ReactElement, data?: ExternalToast) => { - const id = data?.id || toastsCounter++; + const id = data?.id || this.toastCounter++; this.create({ jsx: jsx(id), id, ...data }); return id; }; @@ -255,7 +259,7 @@ export const ToastState = new Observer(); // bind this to the toast function const toastFunction = (message: titleT, data?: ExternalToast) => { - const id = data?.id || toastsCounter++; + const id = data?.id || ToastState.toastCounter++; ToastState.addToast({ title: message, diff --git a/src/types.ts b/src/types.ts index ac0563f6..4edf441a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -86,6 +86,7 @@ export interface ToastT { classNames?: ToastClassnames; descriptionClassName?: string; position?: Position; + persistent?: boolean; } export function isAction(action: Action | React.ReactNode): action is Action { @@ -142,6 +143,7 @@ export interface ToasterProps { swipeDirections?: SwipeDirection[]; icons?: ToastIcons; containerAriaLabel?: string; + storageKey?: string; } export type SwipeDirection = 'top' | 'right' | 'bottom' | 'left'; diff --git a/test/src/app/page.tsx b/test/src/app/page.tsx index 940f302f..8b8497b5 100644 --- a/test/src/app/page.tsx +++ b/test/src/app/page.tsx @@ -245,7 +245,6 @@ export default function Home({ searchParams }: any) { > Extended Promise Toast - + +