imrpoved toast.

This commit is contained in:
tapframe 2025-08-09 01:13:07 +05:30
parent d4af07938b
commit b2ef847720
6 changed files with 62 additions and 149 deletions

40
package-lock.json generated
View file

@ -8,10 +8,12 @@
"name": "nuvio",
"version": "1.0.0",
"dependencies": {
"@backpackapp-io/react-native-toast": "^0.14.0",
"@expo/metro-runtime": "~4.0.1",
"@expo/vector-icons": "^14.1.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/blur": "^4.4.1",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-community/slider": "^4.5.6",
"@react-native-picker/picker": "^2.11.0",
"@react-navigation/bottom-tabs": "^7.3.10",
@ -2257,6 +2259,35 @@
"node": ">=6.9.0"
}
},
"node_modules/@backpackapp-io/react-native-toast": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@backpackapp-io/react-native-toast/-/react-native-toast-0.14.0.tgz",
"integrity": "sha512-vt75tajD+kgHnKiuUlDIEPqqg1qtZ8PMG1mce4WJOEeF+BVbsFrSmoDGCRbPUHkq7TFuIc/Hn89lYAUqPlTFVA==",
"license": "MIT",
"dependencies": {
"@backpackapp-io/react-native-toast": "^0.13.0"
},
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-gesture-handler": ">=2.2.1",
"react-native-reanimated": ">=2.8.0",
"react-native-safe-area-context": ">=4.2.4"
}
},
"node_modules/@backpackapp-io/react-native-toast/node_modules/@backpackapp-io/react-native-toast": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@backpackapp-io/react-native-toast/-/react-native-toast-0.13.0.tgz",
"integrity": "sha512-kutFSE1vi77ybNV24JSnKQ4WUgWZ+LcYsrdAyl5ztEWGP7FRsfinsuaOrBQwDBGxm0rzMFeKM5K7fRHa/Hvy8A==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-gesture-handler": ">=2.2.1",
"react-native-reanimated": ">=2.8.0",
"react-native-safe-area-context": ">=4.2.4"
}
},
"node_modules/@callstack/react-theme-provider": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz",
@ -3469,6 +3500,15 @@
"react-native": "*"
}
},
"node_modules/@react-native-community/netinfo": {
"version": "11.4.1",
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz",
"integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.59"
}
},
"node_modules/@react-native-community/slider": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.6.tgz",

View file

@ -9,10 +9,12 @@
"web": "expo start --web"
},
"dependencies": {
"@backpackapp-io/react-native-toast": "^0.14.0",
"@expo/metro-runtime": "~4.0.1",
"@expo/vector-icons": "^14.1.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/blur": "^4.4.1",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-community/slider": "^4.5.6",
"@react-native-picker/picker": "^2.11.0",
"@react-navigation/bottom-tabs": "^7.3.10",

View file

@ -1,120 +0,0 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { Animated, Easing, StyleSheet, Text, ViewStyle } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export type ToastType = 'success' | 'error' | 'info';
type Props = {
visible: boolean;
message: string;
type?: ToastType;
duration?: number; // ms
onHide?: () => void;
bottomOffset?: number; // extra offset above safe area / tab bar
containerStyle?: ViewStyle;
};
const colorsByType: Record<ToastType, string> = {
success: 'rgba(46,160,67,0.95)',
error: 'rgba(229, 62, 62, 0.95)',
info: 'rgba(99, 102, 241, 0.95)',
};
export const ToastOverlay: React.FC<Props> = ({
visible,
message,
type = 'info',
duration = 1800,
onHide,
bottomOffset = 90,
containerStyle,
}) => {
const insets = useSafeAreaInsets();
const opacity = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(12)).current;
const hideTimer = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (visible) {
// clear any existing timer
if (hideTimer.current) {
clearTimeout(hideTimer.current);
hideTimer.current = null;
}
opacity.setValue(0);
translateY.setValue(12);
Animated.parallel([
Animated.timing(opacity, { toValue: 1, duration: 160, easing: Easing.out(Easing.cubic), useNativeDriver: true }),
Animated.timing(translateY, { toValue: 0, duration: 160, easing: Easing.out(Easing.cubic), useNativeDriver: true }),
]).start(() => {
hideTimer.current = setTimeout(() => {
Animated.parallel([
Animated.timing(opacity, { toValue: 0, duration: 160, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
Animated.timing(translateY, { toValue: 12, duration: 160, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
]).start(() => {
if (onHide) onHide();
});
}, Math.max(800, duration));
});
} else {
// If toggled off externally, hide instantly
Animated.parallel([
Animated.timing(opacity, { toValue: 0, duration: 120, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
Animated.timing(translateY, { toValue: 12, duration: 120, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
]).start();
if (hideTimer.current) {
clearTimeout(hideTimer.current);
hideTimer.current = null;
}
}
return () => {
if (hideTimer.current) {
clearTimeout(hideTimer.current);
hideTimer.current = null;
}
};
}, [visible, duration, onHide, opacity, translateY]);
const bg = useMemo(() => colorsByType[type], [type]);
const bottom = (insets?.bottom || 0) + bottomOffset;
if (!visible && opacity.__getValue() === 0) {
// Avoid mounting when fully hidden to minimize layout cost
return null;
}
return (
<Animated.View
pointerEvents="none"
style={[styles.container, { bottom }, containerStyle, { opacity, transform: [{ translateY }] }]}
>
<Text style={[styles.text, { backgroundColor: bg }]} numberOfLines={3}>
{message}
</Text>
</Animated.View>
);
};
const styles = StyleSheet.create({
container: {
position: 'absolute',
left: 16,
right: 16,
zIndex: 999,
},
text: {
color: '#fff',
fontWeight: '700',
textAlign: 'center',
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 12,
overflow: 'hidden',
fontSize: 12,
},
});
export default ToastOverlay;

View file

@ -14,6 +14,7 @@ import { NuvioHeader } from '../components/NuvioHeader';
import { Stream } from '../types/streams';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext';
import { Toasts } from '@backpackapp-io/react-native-toast';
// Import screens with their proper types
import HomeScreen from '../screens/HomeScreen';
@ -1073,6 +1074,7 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
</Stack.Navigator>
</View>
</PaperProvider>
<Toasts />
</SafeAreaProvider>
);
};

View file

@ -7,7 +7,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { useAccount } from '../contexts/AccountContext';
import { useNavigation, useRoute } from '@react-navigation/native';
import * as Haptics from 'expo-haptics';
import ToastOverlay from '../components/common/ToastOverlay';
import { toast } from '@backpackapp-io/react-native-toast';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const { width, height } = Dimensions.get('window');
@ -41,7 +41,7 @@ const AuthScreen: React.FC = () => {
const ctaTextTranslateY = useRef(new Animated.Value(0)).current;
const modeAnim = useRef(new Animated.Value(0)).current; // 0 = signin, 1 = signup
const [switchWidth, setSwitchWidth] = useState(0);
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'success' | 'error' | 'info' }>({ visible: false, message: '', type: 'info' });
// Legacy local toast state removed in favor of global toast
const [headerHeight, setHeaderHeight] = useState(0);
const headerHideAnim = useRef(new Animated.Value(0)).current; // 0 visible, 1 hidden
const [keyboardHeight, setKeyboardHeight] = useState(0);
@ -144,21 +144,21 @@ const AuthScreen: React.FC = () => {
if (!isEmailValid) {
const msg = 'Enter a valid email address';
setError(msg);
showToast(msg, 'error');
toast.error(msg);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
return;
}
if (!isPasswordValid) {
const msg = 'Password must be at least 6 characters';
setError(msg);
showToast(msg, 'error');
toast.error(msg);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
return;
}
if (mode === 'signup' && !passwordsMatch) {
const msg = 'Passwords do not match';
setError(msg);
showToast(msg, 'error');
toast.error(msg);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
return;
}
@ -167,11 +167,11 @@ const AuthScreen: React.FC = () => {
const err = mode === 'signin' ? await signIn(email.trim(), password) : await signUp(email.trim(), password);
if (err) {
setError(err);
showToast(err, 'error');
toast.error(err);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
} else {
const msg = mode === 'signin' ? 'Logged in successfully' : 'Sign up successful';
showToast(msg, 'success');
toast.success(msg);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
}
setLoading(false);
@ -184,9 +184,7 @@ const AuthScreen: React.FC = () => {
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' as never }] } as any);
};
const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
setToast({ visible: true, message, type });
};
// showToast helper replaced with direct calls to toast.* API
return (
<View style={{ flex: 1 }}>
@ -521,15 +519,7 @@ const AuthScreen: React.FC = () => {
</View>
</KeyboardAvoidingView>
{/* Screen-level toast overlay so it is not clipped by the card */}
<ToastOverlay
visible={toast.visible}
message={toast.message}
type={toast.type}
duration={1600}
bottomOffset={(keyboardHeight > 0 ? keyboardHeight + 8 : 24)}
onHide={() => setToast(prev => ({ ...prev, visible: false }))}
/>
{/* Toasts rendered globally in App root */}
</SafeAreaView>
</View>
);

View file

@ -64,7 +64,7 @@ import type { Theme } from '../contexts/ThemeContext';
import * as ScreenOrientation from 'expo-screen-orientation';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import ToastOverlay from '../components/common/ToastOverlay';
import { toast, ToastPosition } from '@backpackapp-io/react-native-toast';
import FirstTimeWelcome from '../components/FirstTimeWelcome';
import { imageCacheService } from '../services/imageCacheService';
@ -321,6 +321,13 @@ const HomeScreen = () => {
setHintVisible(true);
await AsyncStorage.removeItem('showLoginHintToastOnce');
hideTimer = setTimeout(() => setHintVisible(false), 2000);
// Also show a global toast for consistency across screens
try {
toast('You can sign in anytime from Settings → Account', {
duration: 1600,
position: ToastPosition.BOTTOM,
});
} catch {}
}
} catch {}
})();
@ -728,15 +735,7 @@ const HomeScreen = () => {
disableIntervalMomentum={true}
scrollEventThrottle={16}
/>
{/* One-time hint toast after skipping sign-in */}
<ToastOverlay
visible={hintVisible}
message="You can sign in anytime from Settings → Account"
type="info"
duration={1600}
bottomOffset={88}
onHide={() => setHintVisible(false)}
/>
{/* Toasts are rendered globally at root */}
</View>
);
}, [