UNPKG

@backpackapp-io/react-native-toast

Version:

A toasting library for React Native. Built in features such as swipe to dismiss, multiple toasts, & no context power this library.

220 lines (193 loc) 4.97 kB
import { useEffect, useState } from 'react'; import { DefaultToastOptions, DismissReason, Toast, ToastType } from './types'; const TOAST_LIMIT = 20; export enum ActionType { ADD_TOAST, UPDATE_TOAST, UPSERT_TOAST, DISMISS_TOAST, REMOVE_TOAST, START_PAUSE, END_PAUSE, } export type Action = | { type: ActionType.ADD_TOAST; toast: Toast; } | { type: ActionType.UPSERT_TOAST; toast: Toast; } | { type: ActionType.UPDATE_TOAST; toast: Partial<Toast>; } | { type: ActionType.DISMISS_TOAST; toastId?: string; reason: DismissReason; } | { type: ActionType.REMOVE_TOAST; toastId?: string; reason?: DismissReason; } | { type: ActionType.START_PAUSE; time: number; } | { type: ActionType.END_PAUSE; time: number; }; interface State { toasts: Toast[]; pausedAt: number | undefined; } const toastTimeouts = new Map<Toast['id'], ReturnType<typeof setTimeout>>(); const addToRemoveQueue = (toastId: string, reason: DismissReason) => { if (toastTimeouts.has(toastId)) { return; } const timeout = setTimeout(() => { toastTimeouts.delete(toastId); dispatch({ type: ActionType.REMOVE_TOAST, toastId: toastId, reason, }); }, 1000); toastTimeouts.set(toastId, timeout); }; const clearFromRemoveQueue = (toastId: string) => { const timeout = toastTimeouts.get(toastId); if (timeout) { clearTimeout(timeout); } }; export const reducer = (state: State, action: Action): State => { switch (action.type) { case ActionType.ADD_TOAST: return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), }; case ActionType.UPDATE_TOAST: // ! Side effects ! if (action.toast.id) { clearFromRemoveQueue(action.toast.id); } return { ...state, toasts: state.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t ), }; case ActionType.UPSERT_TOAST: const { toast } = action; return state.toasts.find((t) => t.id === toast.id) ? reducer(state, { type: ActionType.UPDATE_TOAST, toast }) : reducer(state, { type: ActionType.ADD_TOAST, toast }); case ActionType.DISMISS_TOAST: const { toastId, reason } = action; // ! Side effects ! - This could be execrated into a dismissToast() action, but I'll keep it here for simplicity if (toastId) { addToRemoveQueue(toastId, reason); } else { state.toasts.forEach((toast) => { addToRemoveQueue(toast.id, reason); }); } return { ...state, toasts: state.toasts.map((t) => t.id === toastId || toastId === undefined ? { ...t, visible: false, dismissReason: reason, } : t ), }; case ActionType.REMOVE_TOAST: if (action.toastId === undefined) { return { ...state, toasts: [], }; } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), }; case ActionType.START_PAUSE: return { ...state, pausedAt: action.time, }; case ActionType.END_PAUSE: const diff = action.time - (state.pausedAt || 0); return { ...state, pausedAt: undefined, toasts: state.toasts.map((t) => ({ ...t, pauseDuration: t.pauseDuration + diff, })), }; } }; const listeners: Array<(state: State) => void> = []; let memoryState: State = { toasts: [], pausedAt: undefined }; export const dispatch = (action: Action) => { memoryState = reducer(memoryState, action); listeners.forEach((listener) => { listener(memoryState); }); }; const defaultTimeouts: { [key in ToastType]: number; } = { blank: 4000, error: 4000, success: 2000, loading: Infinity, }; export const useStore = (toastOptions: DefaultToastOptions = {}): State => { const [state, setState] = useState<State>(memoryState); useEffect(() => { listeners.push(setState); return () => { const index = listeners.indexOf(setState); if (index > -1) { listeners.splice(index, 1); } }; }, [state]); const mergedToasts = state.toasts .filter( (t) => toastOptions?.providerKey === undefined || t.providerKey === toastOptions?.providerKey || t.providerKey === 'PERSISTS' ) .map((t) => ({ ...toastOptions, ...toastOptions[t.type], ...t, duration: t.duration || toastOptions[t.type]?.duration || toastOptions?.duration || defaultTimeouts[t.type], styles: { ...(t?.styles ?? {}), }, })); return { ...state, toasts: mergedToasts, }; };