feat: configuracion de alertas, WebSocket con fallback polling
This commit is contained in:
154
frontend/App.js
154
frontend/App.js
@@ -2,8 +2,6 @@ import { StatusBar } from 'expo-status-bar';
|
|||||||
import { StyleSheet, Text, View, TextInput, TouchableOpacity, ActivityIndicator, ScrollView, Alert, RefreshControl } from 'react-native';
|
import { StyleSheet, Text, View, TextInput, TouchableOpacity, ActivityIndicator, ScrollView, Alert, RefreshControl } from 'react-native';
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import * as Notifications from 'expo-notifications';
|
|
||||||
import * as Device from 'expo-device';
|
|
||||||
|
|
||||||
const API_URL = 'http://10.137.112.65:8000';
|
const API_URL = 'http://10.137.112.65:8000';
|
||||||
|
|
||||||
@@ -11,13 +9,6 @@ const COLONIAS = [
|
|||||||
'Zona Centro', 'Las Arboledas', 'Trojes', 'San Juanico',
|
'Zona Centro', 'Las Arboledas', 'Trojes', 'San Juanico',
|
||||||
'Los Olivos', 'Rancho Seco', 'Las Insurgentes'
|
'Los Olivos', 'Rancho Seco', 'Las Insurgentes'
|
||||||
];
|
];
|
||||||
Notifications.setNotificationHandler({
|
|
||||||
handleNotification: async () => ({
|
|
||||||
shouldShowAlert: true,
|
|
||||||
shouldPlaySound: true,
|
|
||||||
shouldSetBadge: false,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [screen, setScreen] = useState('splash');
|
const [screen, setScreen] = useState('splash');
|
||||||
@@ -35,25 +26,43 @@ export default function App() {
|
|||||||
const [mostrarColonias, setMostrarColonias] = useState(false);
|
const [mostrarColonias, setMostrarColonias] = useState(false);
|
||||||
const [direccion, setDireccion] = useState('');
|
const [direccion, setDireccion] = useState('');
|
||||||
const [codigoPostal, setCodigoPostal] = useState('');
|
const [codigoPostal, setCodigoPostal] = useState('');
|
||||||
|
const [notifConfig, setNotifConfig] = useState({
|
||||||
useEffect(() => {
|
routeStart: true,
|
||||||
cargarSesion();
|
proximity: true,
|
||||||
registrarNotificaciones();
|
completed: true,
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
cargarSesion();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let ws = null;
|
||||||
|
let interval = null;
|
||||||
if (screen === 'eta' && domicilioActivo && token) {
|
if (screen === 'eta' && domicilioActivo && token) {
|
||||||
const interval = setInterval(() => {
|
ws = conectarWebSocket(domicilioActivo.id, token);
|
||||||
|
interval = setInterval(() => {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
consultarETA(domicilioActivo.id, token, true);
|
consultarETA(domicilioActivo.id, token, true);
|
||||||
}, 120000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
}
|
||||||
|
}, 120000);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (ws) ws.close();
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
|
};
|
||||||
}, [screen, domicilioActivo, token]);
|
}, [screen, domicilioActivo, token]);
|
||||||
|
|
||||||
|
const enviarNotificacionLocal = async (titulo, cuerpo) => {
|
||||||
|
Alert.alert(titulo, cuerpo);
|
||||||
|
};
|
||||||
|
|
||||||
const cargarSesion = async () => {
|
const cargarSesion = async () => {
|
||||||
try {
|
try {
|
||||||
const t = await AsyncStorage.getItem('token');
|
const t = await AsyncStorage.getItem('token');
|
||||||
const dId = await AsyncStorage.getItem('domicilioId');
|
const dId = await AsyncStorage.getItem('domicilioId');
|
||||||
|
const config = await AsyncStorage.getItem('notifConfig');
|
||||||
|
if (config) setNotifConfig(JSON.parse(config));
|
||||||
if (t) {
|
if (t) {
|
||||||
setToken(t);
|
setToken(t);
|
||||||
const doms = await cargarDomicilios(t);
|
const doms = await cargarDomicilios(t);
|
||||||
@@ -87,6 +96,7 @@ useEffect(() => {
|
|||||||
setToken(null); setDomicilios([]); setDomicilioActivo(null);
|
setToken(null); setDomicilios([]); setDomicilioActivo(null);
|
||||||
setEta(null); setEmail(''); setPassword(''); setTelefono('');
|
setEta(null); setEmail(''); setPassword(''); setTelefono('');
|
||||||
setDireccion(''); setColoniaSeleccionada(''); setCodigoPostal('');
|
setDireccion(''); setColoniaSeleccionada(''); setCodigoPostal('');
|
||||||
|
setNotifConfig({ routeStart: true, proximity: true, completed: true });
|
||||||
setScreen('login');
|
setScreen('login');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,23 +190,17 @@ useEffect(() => {
|
|||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.mensaje) {
|
if (data.mensaje) {
|
||||||
if (data.evento === 'TRUCK_PROXIMITY' && eta?.evento !== 'TRUCK_PROXIMITY') {
|
if (data.evento === 'TRUCK_PROXIMITY' && eta?.evento !== 'TRUCK_PROXIMITY' && notifConfig.proximity) {
|
||||||
enviarNotificacionLocal(
|
enviarNotificacionLocal('🚨 ¡Camión cercano!',
|
||||||
'🚨 ¡Camión cercano!',
|
`El camión está a menos de 15 minutos de ${data.colonia}. Saca tus bolsas a la acera.`);
|
||||||
`El camión está a menos de 15 minutos de ${data.colonia}. Saca tus bolsas a la acera.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (data.evento === 'ROUTE_START' && eta?.evento !== 'ROUTE_START') {
|
if (data.evento === 'ROUTE_START' && eta?.evento !== 'ROUTE_START' && notifConfig.routeStart) {
|
||||||
enviarNotificacionLocal(
|
enviarNotificacionLocal('🟢 Ruta iniciada',
|
||||||
'🟢 Ruta iniciada',
|
`El camión de ${data.colonia} ha salido. Prepara tus residuos.`);
|
||||||
`El camión de ${data.colonia} ha salido. Prepara tus residuos.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (data.evento === 'ROUTE_COMPLETED' && eta?.evento !== 'ROUTE_COMPLETED') {
|
if (data.evento === 'ROUTE_COMPLETED' && eta?.evento !== 'ROUTE_COMPLETED' && notifConfig.completed) {
|
||||||
enviarNotificacionLocal(
|
enviarNotificacionLocal('✅ Servicio finalizado',
|
||||||
'✅ Servicio finalizado',
|
`El camión de ${data.colonia} ha concluido su jornada.`);
|
||||||
`El camión de ${data.colonia} ha concluido su jornada.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
setEta(data);
|
setEta(data);
|
||||||
}
|
}
|
||||||
@@ -210,25 +214,37 @@ useEffect(() => {
|
|||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}, [domicilioActivo, token]);
|
}, [domicilioActivo, token]);
|
||||||
|
|
||||||
const registrarNotificaciones = async () => {
|
|
||||||
if (!Device.isDevice) return;
|
|
||||||
const { status } = await Notifications.requestPermissionsAsync();
|
|
||||||
if (status !== 'granted') return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const enviarNotificacionLocal = async (titulo, cuerpo) => {
|
|
||||||
await Notifications.scheduleNotificationAsync({
|
|
||||||
content: { title: titulo, body: cuerpo, sound: true },
|
|
||||||
trigger: null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const seleccionarDomicilio = async (dom) => {
|
const seleccionarDomicilio = async (dom) => {
|
||||||
setDomicilioActivo(dom);
|
setDomicilioActivo(dom);
|
||||||
await AsyncStorage.setItem('domicilioId', String(dom.id));
|
await AsyncStorage.setItem('domicilioId', String(dom.id));
|
||||||
consultarETA(dom.id, token, true);
|
consultarETA(dom.id, token, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const conectarWebSocket = (id, t) => {
|
||||||
|
const ws = new WebSocket(`ws://10.137.112.65:8000/ws/eta/${id}?token=${t}`);
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.mensaje) {
|
||||||
|
if (data.evento === 'TRUCK_PROXIMITY' && eta?.evento !== 'TRUCK_PROXIMITY' && notifConfig.proximity) {
|
||||||
|
enviarNotificacionLocal('🚨 ¡Camión cercano!',
|
||||||
|
`El camión está a menos de 15 minutos de ${data.colonia}. Saca tus bolsas a la acera.`);
|
||||||
|
}
|
||||||
|
if (data.evento === 'ROUTE_START' && eta?.evento !== 'ROUTE_START' && notifConfig.routeStart) {
|
||||||
|
enviarNotificacionLocal('🟢 Ruta iniciada',
|
||||||
|
`El camión de ${data.colonia} ha salido. Prepara tus residuos.`);
|
||||||
|
}
|
||||||
|
if (data.evento === 'ROUTE_COMPLETED' && eta?.evento !== 'ROUTE_COMPLETED' && notifConfig.completed) {
|
||||||
|
enviarNotificacionLocal('✅ Servicio finalizado',
|
||||||
|
`El camión de ${data.colonia} ha concluido su jornada.`);
|
||||||
|
}
|
||||||
|
setEta(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onerror = () => console.log('WS error, usando polling');
|
||||||
|
ws.onclose = () => console.log('WS cerrado');
|
||||||
|
return ws;
|
||||||
|
};
|
||||||
|
|
||||||
if (screen === 'splash') return (
|
if (screen === 'splash') return (
|
||||||
<View style={styles.splashContainer}>
|
<View style={styles.splashContainer}>
|
||||||
<Text style={styles.splashEmoji}>🚛</Text>
|
<Text style={styles.splashEmoji}>🚛</Text>
|
||||||
@@ -243,7 +259,6 @@ useEffect(() => {
|
|||||||
<Text style={styles.bigEmoji}>🚛</Text>
|
<Text style={styles.bigEmoji}>🚛</Text>
|
||||||
<Text style={styles.title}>BasuraApp</Text>
|
<Text style={styles.title}>BasuraApp</Text>
|
||||||
<Text style={styles.subtitle}>Ingresa a tu cuenta</Text>
|
<Text style={styles.subtitle}>Ingresa a tu cuenta</Text>
|
||||||
|
|
||||||
<View style={styles.toggleRow}>
|
<View style={styles.toggleRow}>
|
||||||
<TouchableOpacity style={[styles.toggleBtn, !usarTelefono && styles.toggleActive]}
|
<TouchableOpacity style={[styles.toggleBtn, !usarTelefono && styles.toggleActive]}
|
||||||
onPress={() => setUsarTelefono(false)}>
|
onPress={() => setUsarTelefono(false)}>
|
||||||
@@ -254,7 +269,6 @@ useEffect(() => {
|
|||||||
<Text style={[styles.toggleText, usarTelefono && styles.toggleTextActive]}>📱 Teléfono</Text>
|
<Text style={[styles.toggleText, usarTelefono && styles.toggleTextActive]}>📱 Teléfono</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{usarTelefono
|
{usarTelefono
|
||||||
? <TextInput style={styles.input} placeholder="Número de teléfono" value={telefono}
|
? <TextInput style={styles.input} placeholder="Número de teléfono" value={telefono}
|
||||||
onChangeText={setTelefono} keyboardType="phone-pad" />
|
onChangeText={setTelefono} keyboardType="phone-pad" />
|
||||||
@@ -263,7 +277,6 @@ useEffect(() => {
|
|||||||
}
|
}
|
||||||
<TextInput style={styles.input} placeholder="Contraseña" value={password}
|
<TextInput style={styles.input} placeholder="Contraseña" value={password}
|
||||||
onChangeText={setPassword} secureTextEntry />
|
onChangeText={setPassword} secureTextEntry />
|
||||||
|
|
||||||
{loading ? <ActivityIndicator color="#1a7a4a" size="large" style={{ marginTop: 16 }} /> : <>
|
{loading ? <ActivityIndicator color="#1a7a4a" size="large" style={{ marginTop: 16 }} /> : <>
|
||||||
<TouchableOpacity style={styles.btn} onPress={login}>
|
<TouchableOpacity style={styles.btn} onPress={login}>
|
||||||
<Text style={styles.btnText}>Iniciar sesión</Text>
|
<Text style={styles.btnText}>Iniciar sesión</Text>
|
||||||
@@ -287,19 +300,16 @@ useEffect(() => {
|
|||||||
<ScrollView contentContainerStyle={styles.container}>
|
<ScrollView contentContainerStyle={styles.container}>
|
||||||
<Text style={styles.title}>📍 Agregar domicilio</Text>
|
<Text style={styles.title}>📍 Agregar domicilio</Text>
|
||||||
<Text style={styles.subtitle}>¿Dónde quieres recibir alertas?</Text>
|
<Text style={styles.subtitle}>¿Dónde quieres recibir alertas?</Text>
|
||||||
|
|
||||||
<TextInput style={styles.input} placeholder="Dirección (ej: Calle Morelos 123)"
|
<TextInput style={styles.input} placeholder="Dirección (ej: Calle Morelos 123)"
|
||||||
value={direccion} onChangeText={setDireccion} />
|
value={direccion} onChangeText={setDireccion} />
|
||||||
<TextInput style={styles.input} placeholder="Código postal (ej: 38000)"
|
<TextInput style={styles.input} placeholder="Código postal (ej: 38000)"
|
||||||
value={codigoPostal} onChangeText={setCodigoPostal} keyboardType="numeric" />
|
value={codigoPostal} onChangeText={setCodigoPostal} keyboardType="numeric" />
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.input, styles.combobox]}
|
<TouchableOpacity style={[styles.input, styles.combobox]}
|
||||||
onPress={() => setMostrarColonias(!mostrarColonias)}>
|
onPress={() => setMostrarColonias(!mostrarColonias)}>
|
||||||
<Text style={{ color: coloniaSeleccionada ? '#1a1a1a' : '#999', fontSize: 15 }}>
|
<Text style={{ color: coloniaSeleccionada ? '#1a1a1a' : '#999', fontSize: 15 }}>
|
||||||
{coloniaSeleccionada || 'Selecciona tu colonia ▾'}
|
{coloniaSeleccionada || 'Selecciona tu colonia ▾'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{mostrarColonias && (
|
{mostrarColonias && (
|
||||||
<View style={styles.dropdown}>
|
<View style={styles.dropdown}>
|
||||||
{COLONIAS.map(c => (
|
{COLONIAS.map(c => (
|
||||||
@@ -310,7 +320,6 @@ useEffect(() => {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{domicilios.length > 0 && (
|
{domicilios.length > 0 && (
|
||||||
<View style={styles.domiciliosExistentes}>
|
<View style={styles.domiciliosExistentes}>
|
||||||
<Text style={styles.domiciliosTitle}>Tus domicilios registrados:</Text>
|
<Text style={styles.domiciliosTitle}>Tus domicilios registrados:</Text>
|
||||||
@@ -323,17 +332,14 @@ useEffect(() => {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading ? <ActivityIndicator color="#1a7a4a" size="large" /> :
|
{loading ? <ActivityIndicator color="#1a7a4a" size="large" /> :
|
||||||
<TouchableOpacity style={styles.btn} onPress={guardarDomicilio}>
|
<TouchableOpacity style={styles.btn} onPress={guardarDomicilio}>
|
||||||
<Text style={styles.btnText}>Guardar domicilio</Text>
|
<Text style={styles.btnText}>Guardar domicilio</Text>
|
||||||
</TouchableOpacity>}
|
</TouchableOpacity>}
|
||||||
|
|
||||||
{domicilios.length > 0 &&
|
{domicilios.length > 0 &&
|
||||||
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('eta')}>
|
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('eta')}>
|
||||||
<Text style={styles.btnSecondaryText}>← Volver al horario</Text>
|
<Text style={styles.btnSecondaryText}>← Volver al horario</Text>
|
||||||
</TouchableOpacity>}
|
</TouchableOpacity>}
|
||||||
|
|
||||||
<TouchableOpacity onPress={cerrarSesion} style={{ marginTop: 24 }}>
|
<TouchableOpacity onPress={cerrarSesion} style={{ marginTop: 24 }}>
|
||||||
<Text style={styles.logoutText}>Cerrar sesión</Text>
|
<Text style={styles.logoutText}>Cerrar sesión</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -344,7 +350,6 @@ useEffect(() => {
|
|||||||
<ScrollView contentContainerStyle={styles.container}
|
<ScrollView contentContainerStyle={styles.container}
|
||||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#1a7a4a']} />}>
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#1a7a4a']} />}>
|
||||||
<Text style={styles.title}>🕐 Horario de recolección</Text>
|
<Text style={styles.title}>🕐 Horario de recolección</Text>
|
||||||
|
|
||||||
{domicilios.length > 1 && (
|
{domicilios.length > 1 && (
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}
|
||||||
style={{ width: '100%', marginBottom: 12 }}
|
style={{ width: '100%', marginBottom: 12 }}
|
||||||
@@ -360,7 +365,6 @@ useEffect(() => {
|
|||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading ? <ActivityIndicator size="large" color="#1a7a4a" /> : eta ? <>
|
{loading ? <ActivityIndicator size="large" color="#1a7a4a" /> : eta ? <>
|
||||||
<View style={styles.etaCard}>
|
<View style={styles.etaCard}>
|
||||||
<Text style={styles.etaEvento}>
|
<Text style={styles.etaEvento}>
|
||||||
@@ -390,6 +394,9 @@ useEffect(() => {
|
|||||||
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('reporte')}>
|
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('reporte')}>
|
||||||
<Text style={styles.btnSecondaryText}>📋 Reportar incidencia</Text>
|
<Text style={styles.btnSecondaryText}>📋 Reportar incidencia</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('configuracion')}>
|
||||||
|
<Text style={styles.btnSecondaryText}>⚙️ Configurar alertas</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={cerrarSesion} style={{ marginTop: 16 }}>
|
<TouchableOpacity onPress={cerrarSesion} style={{ marginTop: 16 }}>
|
||||||
<Text style={styles.logoutText}>Cerrar sesión</Text>
|
<Text style={styles.logoutText}>Cerrar sesión</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -441,6 +448,35 @@ useEffect(() => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (screen === 'configuracion') return (
|
||||||
|
<ScrollView contentContainerStyle={styles.container}>
|
||||||
|
<Text style={styles.title}>⚙️ Configurar alertas</Text>
|
||||||
|
<Text style={styles.subtitle}>Elige qué notificaciones recibir</Text>
|
||||||
|
{[
|
||||||
|
{ key: 'routeStart', label: '🟢 Ruta iniciada', desc: 'Cuando el camión sale del depósito' },
|
||||||
|
{ key: 'proximity', label: '🚨 Camión cercano', desc: 'Cuando el camión está a menos de 15 min' },
|
||||||
|
{ key: 'completed', label: '✅ Servicio finalizado', desc: 'Cuando el camión termina su recorrido' },
|
||||||
|
].map(item => (
|
||||||
|
<TouchableOpacity key={item.key}
|
||||||
|
style={[styles.notifItem, notifConfig[item.key] && styles.notifItemActivo]}
|
||||||
|
onPress={async () => {
|
||||||
|
const nueva = { ...notifConfig, [item.key]: !notifConfig[item.key] };
|
||||||
|
setNotifConfig(nueva);
|
||||||
|
await AsyncStorage.setItem('notifConfig', JSON.stringify(nueva));
|
||||||
|
}}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={styles.notifLabel}>{item.label}</Text>
|
||||||
|
<Text style={styles.notifDesc}>{item.desc}</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={{ fontSize: 24 }}>{notifConfig[item.key] ? '🔔' : '🔕'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('eta')}>
|
||||||
|
<Text style={styles.btnSecondaryText}>← Volver al horario</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
@@ -489,4 +525,8 @@ const styles = StyleSheet.create({
|
|||||||
separacionEmoji: { fontSize: 32 },
|
separacionEmoji: { fontSize: 32 },
|
||||||
separacionTipo: { fontSize: 16, fontWeight: 'bold', color: '#333' },
|
separacionTipo: { fontSize: 16, fontWeight: 'bold', color: '#333' },
|
||||||
separacionEjemplos: { fontSize: 12, color: '#666', marginTop: 2 },
|
separacionEjemplos: { fontSize: 12, color: '#666', marginTop: 2 },
|
||||||
|
notifItem: { width: '100%', backgroundColor: '#fff', borderRadius: 12, padding: 16, marginBottom: 10, flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderColor: '#ddd' },
|
||||||
|
notifItemActivo: { borderColor: '#1a7a4a', backgroundColor: '#e8f5ee' },
|
||||||
|
notifLabel: { fontSize: 15, fontWeight: 'bold', color: '#333' },
|
||||||
|
notifDesc: { fontSize: 12, color: '#666', marginTop: 2 },
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user