122 lines
2.8 KiB
TypeScript
122 lines
2.8 KiB
TypeScript
/**
|
|
* NotificationToast.tsx
|
|
*
|
|
* Banner animado que cae desde arriba cuando llega una notificación
|
|
* nueva del backend. Se autodescarta a los 5 segundos.
|
|
*
|
|
* Limitación: solo se muestra cuando la app está en primer plano. Para
|
|
* notificaciones del SO con la app cerrada se necesita un Development
|
|
* Build + expo-notifications (Expo Go SDK 53+ no lo soporta).
|
|
*/
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import {
|
|
Animated,
|
|
Pressable,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
Platform,
|
|
} from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
|
|
import { useApp } from "../context/AppContext";
|
|
|
|
const AUTO_DISMISS_MS = 5000;
|
|
|
|
export default function NotificationToast() {
|
|
const { toast, dismissToast } = useApp();
|
|
const translateY = useRef(new Animated.Value(-180)).current;
|
|
|
|
useEffect(() => {
|
|
if (!toast) return;
|
|
|
|
// Slide in
|
|
Animated.spring(translateY, {
|
|
toValue: 0,
|
|
useNativeDriver: true,
|
|
damping: 18,
|
|
}).start();
|
|
|
|
// Auto dismiss después de N segundos
|
|
const timer = setTimeout(() => {
|
|
Animated.timing(translateY, {
|
|
toValue: -180,
|
|
duration: 250,
|
|
useNativeDriver: true,
|
|
}).start(() => dismissToast());
|
|
}, AUTO_DISMISS_MS);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [toast, translateY, dismissToast]);
|
|
|
|
if (!toast) return null;
|
|
|
|
return (
|
|
<Animated.View
|
|
style={[
|
|
styles.container,
|
|
{ transform: [{ translateY }] },
|
|
]}
|
|
pointerEvents="box-none"
|
|
>
|
|
<Pressable style={styles.card} onPress={dismissToast}>
|
|
<View style={styles.iconWrap}>
|
|
<Ionicons name="notifications" size={22} color="#FFFFFF" />
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={styles.title} numberOfLines={1}>
|
|
{toast.title}
|
|
</Text>
|
|
<Text style={styles.body} numberOfLines={2}>
|
|
{toast.body}
|
|
</Text>
|
|
</View>
|
|
<Ionicons name="close" size={18} color="#FFFFFF" />
|
|
</Pressable>
|
|
</Animated.View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
position: "absolute",
|
|
top: Platform.OS === "ios" ? 50 : 30,
|
|
left: 16,
|
|
right: 16,
|
|
zIndex: 9999,
|
|
},
|
|
card: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
backgroundColor: "#0E8A61",
|
|
borderRadius: 14,
|
|
padding: 14,
|
|
shadowColor: "#000",
|
|
shadowOffset: { width: 0, height: 6 },
|
|
shadowOpacity: 0.2,
|
|
shadowRadius: 10,
|
|
elevation: 10,
|
|
},
|
|
iconWrap: {
|
|
width: 38,
|
|
height: 38,
|
|
borderRadius: 19,
|
|
backgroundColor: "rgba(255,255,255,0.2)",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
marginRight: 12,
|
|
},
|
|
title: {
|
|
fontSize: 14,
|
|
fontWeight: "800",
|
|
color: "#FFFFFF",
|
|
marginBottom: 2,
|
|
},
|
|
body: {
|
|
fontSize: 12,
|
|
color: "rgba(255,255,255,0.95)",
|
|
lineHeight: 16,
|
|
},
|
|
});
|