import React , { FC , useRef , useEffect , useState } from "react" ; import { View , StyleSheet , Animated , StyleProp , ViewStyle , TextStyle , Text , TouchableWithoutFeedback , PanResponder , PanResponderInstance , PanResponderGestureState , Platform , } from "react-native" ; import { useDimensions } from "./utils/useDimensions" ; export interface ToastOptions { /**
     * Id is optional, you may provide an id only if you want to update toast later using toast.update()
     */ id ? : string ; /**
     * Customize toast icon
     */ icon ? : JSX . Element ; /**
     * Toast types, You can implement your custom types with JSX using renderType method on ToastContainer.
     */ type ? : "normal" | "success" | "danger" | "warning" | string ; /**
     * In ms, How long toast will stay before it go away
     */ duration ? : number ; /**
     * Customize when toast should be placed
     */ placement ? : "top" | "bottom" ; /**
     * Customize style of toast
     */ style ? : StyleProp < ViewStyle > ; /**
     * Customize style of toast text
     */ textStyle ? : StyleProp < TextStyle > ; /**
     * Customize how fast toast will show and hide
     */ animationDuration ? : number ; /**
     * Customize how toast is animated when added or removed
     */ animationType ? : "slide-in" | "zoom-in" ; /**
     * Customize success type icon
     */ successIcon ? : JSX . Element ; /**
     * Customize danger type icon
     */ dangerIcon ? : JSX . Element ; /**
     * Customize warning type icon
     */ warningIcon ? : JSX . Element ; /**
     * Customize success type color. changes toast background color
     */ successColor ? : string ; /**
     * Customize danger type color. changes toast background color
     */ dangerColor ? : string ; /**
     * Customize warning type color. changes toast background color
     */ warningColor ? : string ; /**
     * Customize normal type color. changes toast background color
     */ normalColor ? : string ; /**
     * Register event for when toast is pressed. If you're using a custom toast you have to pass this to a Touchable.
     */ onPress ? ( id ) : void ; /**
     * Execute event after toast is closed
     */ onClose ? ( ) : void ; /**
     * Payload data for custom toasts. You can pass whatever you want
     */ data ? : any ; swipeEnabled ? : boolean ; } export interface ToastProps extends ToastOptions { id : string ; onDestroy ( ) : void ; message : string | JSX . Element ; open : boolean ; renderToast ? ( toast ) : JSX . Element ; renderType ? : { [ type ] : ( toast : ToastProps ) => JSX . Element ; } ; onHide ( ) : void ; } const Toast = ( props ) => { let { id , onDestroy , icon , type = "normal" , message , duration = 5000 , style , textStyle , animationDuration = 250 , animationType = "slide-in" , successIcon , dangerIcon , warningIcon , successColor , dangerColor , warningColor , normalColor , placement , swipeEnabled , onPress , } = props ; const containerRef = useRef < View > ( null ) ; const [ animation ] = useState ( new Animated . Value ( 0 ) ) ; const panResponderRef = useRef < PanResponderInstance > ( ) ; const panResponderAnimRef = useRef < Animated . ValueXY > ( ) ; const closeTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ; const dims = useDimensions ( ) ; useEffect ( ( ) => { Animated . timing ( animation , { toValue : 1 , useNativeDriver : Platform . OS !== "web" , duration : animationDuration , } ) . start ( ) ; if ( duration !== 0 && typeof duration === "number" ) { closeTimeoutRef . current = setTimeout ( ( ) => { handleClose ( ) ; } , duration ) ; } return ( ) => { closeTimeoutRef . current && clearTimeout ( closeTimeoutRef . current ) ; } ; } , [ ] ) ; useEffect ( ( ) => { if ( ! props . open ) { closeTimeoutRef . current && clearTimeout ( closeTimeoutRef . current ) ; handleClose ( ) ; } } , [ props . open ] ) ; const handleClose = ( ) => { Animated . timing ( animation , { toValue : 0 , useNativeDriver : Platform . OS !== "web" , duration : animationDuration , } ) . start ( ( ) => onDestroy ( ) ) ; } ; const panReleaseToLeft = ( gestureState ) => { Animated . timing ( getPanResponderAnim ( ) , { toValue : { x : ( - dims . width / 10 ) * 9 , y : gestureState . dy } , useNativeDriver : Platform . OS !== "web" , duration : 250 , } ) . start ( ( ) => onDestroy ( ) ) ; } ; const panReleaseToRight = ( gestureState ) => { Animated . timing ( getPanResponderAnim ( ) , { toValue : { x : ( dims . width / 10 ) * 9 , y : gestureState . dy } , useNativeDriver : Platform . OS !== "web" , duration : 250 , } ) . start ( ( ) => onDestroy ( ) ) ; } ; const getPanResponder = ( ) => { if ( panResponderRef . current ) return panResponderRef . current ; panResponderRef . current = PanResponder . create ( { onMoveShouldSetPanResponder : ( _ , gestureState ) => { return ! ( gestureState . dx === 0 && gestureState . dy === 0 ) ; } , onPanResponderMove : ( _ , gestureState ) => { getPanResponderAnim ( ) ?. setValue ( { x : gestureState . dx , y : gestureState . dy , } ) ; } , onPanResponderRelease : ( _ , gestureState ) => { if ( gestureState . dx > 50 ) { panReleaseToRight ( gestureState ) ; } else if ( gestureState . dx < - 50 ) { panReleaseToLeft ( gestureState ) ; } else { Animated . spring ( getPanResponderAnim ( ) , { toValue : { x : 0 , y : 0 } , useNativeDriver : Platform . OS !== "web" , } ) . start ( ) ; } } , } ) ; return panResponderRef . current ; } ; const getPanResponderAnim = ( ) => { if ( panResponderAnimRef . current ) return panResponderAnimRef . current ; panResponderAnimRef . current = new Animated . ValueXY ( { x : 0 , y : 0 } ) ; return panResponderAnimRef . current ; } ; if ( icon === undefined ) { switch ( type ) { case "success" : { if ( successIcon ) { icon = successIcon ; } break ; } case "danger" : { if ( dangerIcon ) { icon = dangerIcon ; } break ; } case "warning" : { if ( warningIcon ) { icon = warningIcon ; } break ; } } } let backgroundColor = "" ; switch ( type ) { case "success" : backgroundColor = successColor || "#00C851" ; break ; case "danger" : backgroundColor = dangerColor || "#ff4444" ; break ; case "warning" : backgroundColor = warningColor || "#ffbb33" ; break ; default : backgroundColor = normalColor || "#333" ; } const animationStyle = { opacity : animation , transform : [ { translateY : animation . interpolate ( { inputRange : [ 0 , 1 ] , outputRange : placement === "bottom" ? [ 20 , 0 ] : [ - 20 , 0 ] , } ) , } , ] , } ; if ( swipeEnabled ) { animationStyle . transform ?. push ( getPanResponderAnim ( ) . getTranslateTransform ( ) [ 0 ] ) ; } if ( animationType === "zoom-in" ) { animationStyle . transform ?. push ( { scale : animation . interpolate ( { inputRange : [ 0 , 1 ] , outputRange : [ 0.7 , 1 ] , } ) , } ) ; } return ( < Animated . View ref = { containerRef } { ... ( swipeEnabled ? getPanResponder ( ) . panHandlers : null ) } style = { [ styles . container , animationStyle ] } > { props . renderType && props . renderType [ type ] ? ( props . renderType [ type ] ( props ) ) : props . renderToast ? ( props . renderToast ( props ) ) : ( < TouchableWithoutFeedback disabled = { ! onPress } onPress = { ( ) => onPress && onPress ( id ) } > < View style = { [ styles . toastContainer , { maxWidth : ( dims . width / 10 ) * 9 , backgroundColor } , style , ] } > { icon ? < View style = { styles . iconContainer } > { icon } < /View> : null} { React . isValidElement ( message ) ? ( message ) : ( < Text style = { [ styles . message , textStyle ] } > { message } < /Text>)} < /View> < /TouchableWithoutFeedback>)} < /Animated.View>); } ; const styles = StyleSheet . create ( { container : { width : "100%" , alignItems : "center" } , toastContainer : { paddingHorizontal : 12 , paddingVertical : 12 , borderRadius : 5 , marginVertical : 5 , flexDirection : "row" , alignItems : "center" , overflow : "hidden" , } , message : { color : "#fff" , fontWeight : "500" , } , iconContainer : { marginRight : 5 , } , } ) ; export default Toast ;