mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
imrpoved toast.
This commit is contained in:
parent
d4af07938b
commit
b2ef847720
6 changed files with 62 additions and 149 deletions
40
package-lock.json
generated
40
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}, [
|
||||
|
|
|
|||
Loading…
Reference in a new issue