1import { Button } from 'expo-dev-client-components'; 2import * as React from 'react'; 3import { Animated, LayoutRectangle, useWindowDimensions, StyleSheet } from 'react-native'; 4 5import { 6 createAsyncStack, 7 StackItem, 8 Status, 9 StackItemComponent, 10 useStackItems, 11} from '../functions/createAsyncStack'; 12 13export type ToastOptions = { 14 durationMs?: number; 15}; 16 17export type ToastStackItem = { 18 element: StackItemComponent; 19 toastProps?: ToastOptions; 20}; 21 22export type ToastProps = { 23 animatedValue: Animated.Value; 24 pop: () => void; 25 status: Status; 26}; 27 28const defaultDistanceFromBottom = 100; 29 30type ToastStackContextProps = { 31 push: (element: StackItemComponent, options?: ToastOptions) => StackItem<ToastStackItem>; 32 pop: (amount?: number) => StackItem<ToastStackItem>[]; 33 getItems: () => StackItem<ToastStackItem>[]; 34}; 35 36const ToastStackContext = React.createContext<ToastStackContextProps | null>(null); 37export const useToastStack = () => { 38 const context = React.useContext(ToastStackContext); 39 40 if (!context) { 41 throw new Error(`useToastStack() was called outside of a <ToastStackContext /> provider`); 42 } 43 44 return context; 45}; 46 47export function ToastStackProvider({ children }) { 48 const toastStack = React.useRef(createAsyncStack<ToastStackItem>()).current; 49 const toasts = useStackItems(toastStack); 50 51 function push(element: StackItemComponent, options?: ToastOptions): StackItem<ToastStackItem> { 52 return toastStack.push({ element, toastProps: options }); 53 } 54 55 function pop(amount: number = 1) { 56 return toastStack.pop(amount); 57 } 58 59 function getItems() { 60 return toastStack.getState().items; 61 } 62 63 return ( 64 <ToastStackContext.Provider value={{ push, pop, getItems }}> 65 {children} 66 <Animated.View pointerEvents="box-none" style={[StyleSheet.absoluteFill]}> 67 {toasts.map((toast) => ( 68 <ToastItem {...toast} /> 69 ))} 70 </Animated.View> 71 </ToastStackContext.Provider> 72 ); 73} 74 75function ToastItem(props: StackItem<ToastStackItem>) { 76 const { status, data, onPopEnd, onPushEnd, pop, animatedValue } = props; 77 const { toastProps, element: Element } = data; 78 79 const { height } = useWindowDimensions(); 80 81 const [layout, setLayout] = React.useState<LayoutRectangle | null>(null); 82 const timerRef = React.useRef<NodeJS.Timeout | null>(null); 83 84 React.useEffect(() => { 85 if (status === 'pushing') { 86 Animated.spring(animatedValue, { 87 toValue: 1, 88 useNativeDriver: true, 89 }).start(onPushEnd); 90 } 91 92 if (status === 'popping') { 93 Animated.spring(animatedValue, { 94 toValue: 2, 95 useNativeDriver: true, 96 }).start(() => { 97 onPopEnd(); 98 99 if (timerRef.current != null) { 100 clearTimeout(timerRef.current); 101 timerRef.current = null; 102 } 103 }); 104 } 105 106 if (status === 'settled') { 107 timerRef.current = setTimeout(() => { 108 pop(); 109 timerRef.current = null; 110 }, toastProps?.durationMs || 2000); 111 } 112 113 return () => { 114 if (timerRef.current != null) { 115 clearTimeout(timerRef.current); 116 } 117 }; 118 }, [status, pop, toastProps?.durationMs]); 119 120 let distanceFromBottom = defaultDistanceFromBottom; 121 122 if (layout != null) { 123 distanceFromBottom = distanceFromBottom + layout.height; 124 } 125 126 const translateY = animatedValue.interpolate({ 127 inputRange: [0, 1, 2], 128 outputRange: [height, height - distanceFromBottom, height - distanceFromBottom], 129 }); 130 131 const opacity = animatedValue.interpolate({ 132 inputRange: [0, 1, 2], 133 outputRange: [1, 1, 0], 134 }); 135 136 const isPopping = status === 'popping' || status === 'popped'; 137 138 return ( 139 <Animated.View 140 onLayout={({ nativeEvent: { layout } }) => setLayout(layout)} 141 pointerEvents={isPopping ? 'none' : 'box-none'} 142 style={[ 143 { 144 position: 'absolute', 145 left: 0, 146 right: 0, 147 opacity, 148 transform: [{ translateY }], 149 }, 150 ]}> 151 <Button.FadeOnPressContainer onPress={pop}> 152 <Element {...props} /> 153 </Button.FadeOnPressContainer> 154 </Animated.View> 155 ); 156} 157