react-native-toast-lite
Version:
🍞 Este modulo se trata de mostrar Toast en React Native
316 lines (306 loc) • 10.1 kB
JavaScript
"use strict";
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { View, Text, Image, ActivityIndicator, StyleSheet } from 'react-native';
import Animated, { useSharedValue, useAnimatedStyle, withTiming, interpolate, runOnJS } from 'react-native-reanimated';
import ErrorSvg from "../ui/ErrorSvg.js";
import SuccessSvg from "../ui/SuccessSvg.js";
import InfoSvg from "../ui/InfoSvg.js";
import WarningSvg from "../ui/WarningSvg.js";
import CustomLoading from "./CustomLoading.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export const RenderIcon = ({
type,
toastStyle,
iconColor,
icon,
iconUrl,
iconSize,
iconStyle,
iconResizeMode,
iconRounded,
iconBorderRadius,
loadingDelayMs = 150
}) => {
const size = iconSize ?? 25;
const radius = typeof iconBorderRadius === 'number' ? Math.max(0, Math.round(iconBorderRadius)) : iconRounded ? Math.round(size / 2) : Math.round(size / 6);
// Firma desde props
const signature = useMemo(() => {
return iconUrl ? `url:${iconUrl}` : icon ? `emoji:${icon}` : `builtin:${type}:${toastStyle}:${iconColor ?? ''}:${iconStyle ?? ''}`;
}, [iconUrl, icon, type, toastStyle, iconColor, iconStyle]);
// Helpers
const kindFromSig = useCallback(sig => {
if (sig.startsWith('url:')) return 'url';
if (sig.startsWith('emoji:')) return 'emoji';
return 'builtin';
}, []);
const parseBuiltinFromSig = useCallback(sig => {
// builtin:<type>:<toastStyle>:<iconColor>:<iconStyle>
const parts = sig.split(':');
const typeFromSig = parts[1] || type;
const styleFromSig = parts[2] || toastStyle;
return {
typeFromSig,
styleFromSig
};
}, [type, toastStyle]);
// Estado “actual” y “pendiente”
const [currentSig, setCurrentSig] = useState(signature);
const [pendingSig, setPendingSig] = useState(null);
// Flags y timers
const [loadFailed, setLoadFailed] = useState(false);
const [showSpinner, setShowSpinner] = useState(false);
const spinnerTimeout = useRef(null);
const nonUrlTransitionRef = useRef(null);
// Opacidades
const currentOpacity = useSharedValue(1);
const pendingOpacity = useSharedValue(0);
const currentKind = useMemo(() => kindFromSig(currentSig), [currentSig, kindFromSig]);
const pendingKind = useMemo(() => pendingSig ? kindFromSig(pendingSig) : null, [pendingSig, kindFromSig]);
// Para que el spinner quede por encima y el contenido por debajo
const DIM_WHEN_SPINNER = 0.35;
const currentAnimatedStyle = useAnimatedStyle(() => ({
opacity: (showSpinner ? DIM_WHEN_SPINNER : 1) * interpolate(currentOpacity.value, [0, 1], [0, 1]),
zIndex: 1 // debajo del spinner
}));
const pendingAnimatedStyle = useAnimatedStyle(() => ({
opacity: (showSpinner ? DIM_WHEN_SPINNER : 1) * interpolate(pendingOpacity.value, [0, 1], [0, 1]),
zIndex: 1 // debajo del spinner
}));
const promotePending = useCallback(sig => {
currentOpacity.value = withTiming(0, {
duration: 200
}, () => {
runOnJS(setCurrentSig)(sig);
runOnJS(setPendingSig)(null);
currentOpacity.value = 1;
pendingOpacity.value = 0;
runOnJS(setShowSpinner)(false);
});
}, [currentOpacity, pendingOpacity]);
// Preparar transición al cambiar firma
useEffect(() => {
if (signature === currentSig || signature === pendingSig) return;
if (spinnerTimeout.current) clearTimeout(spinnerTimeout.current);
if (nonUrlTransitionRef.current) clearTimeout(nonUrlTransitionRef.current);
setLoadFailed(false);
setShowSpinner(false);
const nextKind = kindFromSig(signature);
setPendingSig(signature);
pendingOpacity.value = 0;
if (nextKind === 'url') {
// URL => spinner con delay; el promote ocurre en onLoadEnd
spinnerTimeout.current = setTimeout(() => setShowSpinner(true), loadingDelayMs);
} else {
// emoji/builtin => micro-spinner y crossfade
setShowSpinner(true);
nonUrlTransitionRef.current = setTimeout(() => {
pendingOpacity.value = withTiming(1, {
duration: 200
}, () => runOnJS(promotePending)(signature));
runOnJS(setShowSpinner)(false);
}, loadingDelayMs);
}
}, [signature, currentSig, pendingSig, loadingDelayMs, kindFromSig, pendingOpacity, promotePending]);
useEffect(() => {
return () => {
if (spinnerTimeout.current) clearTimeout(spinnerTimeout.current);
if (nonUrlTransitionRef.current) clearTimeout(nonUrlTransitionRef.current);
};
}, []);
// Render de capa
const renderChild = (sig, kind) => {
if (kind === 'url') {
const url = sig.slice(4);
if (loadFailed) return renderFallback();
return /*#__PURE__*/_jsx(Image, {
source: {
uri: url
},
resizeMode: iconResizeMode ?? 'contain',
onLoadStart: () => {},
onLoadEnd: () => {
if (pendingSig === sig) {
pendingOpacity.value = withTiming(1, {
duration: 160
}, () => runOnJS(promotePending)(sig) // ✅ ahora con argumento
);
} else {
setShowSpinner(false);
}
},
onError: () => {
setLoadFailed(true);
setShowSpinner(false);
},
style: StyleSheet.absoluteFill
});
}
if (kind === 'emoji') {
const emoji = sig.slice(6);
const node = /*#__PURE__*/_jsx(View, {
style: styles.centerFill,
children: /*#__PURE__*/_jsx(Text, {
style: {
fontSize: size
},
children: emoji
})
});
if (pendingSig === sig) {
pendingOpacity.value = withTiming(1, {
duration: 160
}, () => runOnJS(promotePending)(sig));
}
return node;
}
// builtin
const {
typeFromSig,
styleFromSig
} = sig.startsWith('builtin:') ? parseBuiltinFromSig(sig) : {
typeFromSig: type,
styleFromSig: toastStyle
};
const builtinNode = (() => {
switch (typeFromSig) {
case 'error':
return /*#__PURE__*/_jsx(ErrorSvg, {
toastStyle: styleFromSig,
iconColor: iconColor,
iconSize: size,
iconStyle: iconStyle
});
case 'success':
return /*#__PURE__*/_jsx(SuccessSvg, {
toastStyle: styleFromSig,
iconColor: iconColor,
iconSize: size,
iconStyle: iconStyle
});
case 'info':
return /*#__PURE__*/_jsx(InfoSvg, {
toastStyle: styleFromSig,
iconColor: iconColor,
iconSize: size,
iconStyle: iconStyle
});
case 'warning':
return /*#__PURE__*/_jsx(WarningSvg, {
toastStyle: styleFromSig,
iconColor: iconColor,
iconSize: size,
iconStyle: iconStyle
});
case 'loading':
return /*#__PURE__*/_jsx(CustomLoading, {
color: iconColor,
size: size
});
default:
return null;
}
})();
if (pendingSig === sig) {
pendingOpacity.value = withTiming(1, {
duration: 160
}, () => runOnJS(promotePending)(sig));
}
return /*#__PURE__*/_jsx(View, {
style: styles.centerFill,
children: builtinNode
});
};
const renderFallback = () => {
if (icon) {
return /*#__PURE__*/_jsx(View, {
style: styles.centerFill,
children: /*#__PURE__*/_jsx(Text, {
style: {
fontSize: size
},
children: icon
})
});
}
return /*#__PURE__*/_jsx(View, {
style: styles.centerFill,
children: type === 'loading' ? /*#__PURE__*/_jsx(CustomLoading, {
color: iconColor,
size: size
}) : (() => {
switch (type) {
case 'error':
return /*#__PURE__*/_jsx(ErrorSvg, {
toastStyle: toastStyle,
iconColor: iconColor,
iconSize: size,
iconStyle: iconStyle
});
case 'success':
return /*#__PURE__*/_jsx(SuccessSvg, {
toastStyle: toastStyle,
iconColor: iconColor,
iconSize: size,
iconStyle: iconStyle
});
case 'info':
return /*#__PURE__*/_jsx(InfoSvg, {
toastStyle: toastStyle,
iconColor: iconColor,
iconSize: size,
iconStyle: iconStyle
});
case 'warning':
return /*#__PURE__*/_jsx(WarningSvg, {
toastStyle: toastStyle,
iconColor: iconColor,
iconSize: size,
iconStyle: iconStyle
});
default:
return null;
}
})()
});
};
return /*#__PURE__*/_jsxs(View, {
style: {
width: size,
height: size,
borderRadius: radius,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
position: 'relative'
},
children: [/*#__PURE__*/_jsx(Animated.View, {
style: [StyleSheet.absoluteFill, currentAnimatedStyle],
pointerEvents: "none",
children: renderChild(currentSig, currentKind)
}), pendingSig && /*#__PURE__*/_jsx(Animated.View, {
style: [StyleSheet.absoluteFill, pendingAnimatedStyle],
pointerEvents: "none",
children: renderChild(pendingSig, pendingKind)
}), showSpinner && /*#__PURE__*/_jsx(View, {
style: styles.spinnerOverlay,
pointerEvents: "none",
children: /*#__PURE__*/_jsx(ActivityIndicator, {
size: "small",
color: iconColor ?? '#999'
})
})]
});
};
const styles = StyleSheet.create({
centerFill: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center'
},
spinnerOverlay: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
zIndex: 2 // <- arriba de todo
}
});