1// Prevent pulling in all of expo-modules-core on web 2import { EventEmitter } from 'expo-modules-core/build/EventEmitter'; 3import React, { useEffect, useState, useRef, useMemo } from 'react'; 4import { Animated, StyleSheet, Text, Platform, View } from 'react-native'; 5 6import DevLoadingViewNativeModule from './DevLoadingViewNativeModule'; 7import { getInitialSafeArea } from './getInitialSafeArea'; 8 9export default function DevLoadingView() { 10 const [message, setMessage] = useState('Refreshing...'); 11 const [isDevLoading, setIsDevLoading] = useState(false); 12 const [isAnimating, setIsAnimating] = useState(false); 13 const translateY = useRef(new Animated.Value(0)).current; 14 const emitter = useMemo<EventEmitter>(() => { 15 try { 16 return new EventEmitter(DevLoadingViewNativeModule); 17 } catch (error) { 18 throw new Error( 19 'Failed to instantiate native emitter in `DevLoadingView` because the native module `DevLoadingView` is undefined: ' + 20 error.message 21 ); 22 } 23 }, []); 24 25 useEffect(() => { 26 if (!emitter) return; 27 28 function handleShowMessage(event: { message: string }) { 29 setMessage(event.message); 30 // TODO: if we show the refreshing banner and don't get a hide message 31 // for 3 seconds, warn the user that it's taking a while and suggest 32 // they reload 33 34 translateY.setValue(0); 35 setIsDevLoading(true); 36 } 37 38 function handleHide() { 39 // TODO: if we showed the 'refreshing' banner less than 250ms ago, delay 40 // switching to the 'finished' banner 41 42 setIsAnimating(true); 43 setIsDevLoading(false); 44 Animated.timing(translateY, { 45 toValue: 150, 46 delay: 1000, 47 duration: 350, 48 useNativeDriver: Platform.OS !== 'web', 49 }).start(({ finished }) => { 50 if (finished) { 51 setIsAnimating(false); 52 translateY.setValue(0); 53 } 54 }); 55 } 56 57 const showMessageSubscription = emitter.addListener( 58 'devLoadingView:showMessage', 59 handleShowMessage 60 ); 61 const hideSubscription = emitter.addListener('devLoadingView:hide', handleHide); 62 63 return function cleanup() { 64 showMessageSubscription.remove(); 65 hideSubscription.remove(); 66 }; 67 }, [translateY, emitter]); 68 69 if (!isDevLoading && !isAnimating) { 70 return null; 71 } 72 73 return ( 74 <Animated.View style={[styles.animatedContainer, { transform: [{ translateY }] }]}> 75 <View style={styles.banner}> 76 <View style={styles.contentContainer}> 77 <View style={{ flexDirection: 'row' }}> 78 <Text style={styles.text}>{message}</Text> 79 </View> 80 81 <View style={{ flex: 1 }}> 82 <Text style={styles.subtitle}> 83 {isDevLoading ? 'Using Fast Refresh' : "Don't see your changes? Reload the app"} 84 </Text> 85 </View> 86 </View> 87 </View> 88 </Animated.View> 89 ); 90} 91 92const styles = StyleSheet.create({ 93 animatedContainer: { 94 // @ts-expect-error: fixed is not a valid value for position in Yoga but it is on web. 95 position: Platform.select({ 96 web: 'fixed', 97 default: 'absolute', 98 }), 99 pointerEvents: 'none', 100 bottom: 0, 101 left: 0, 102 right: 0, 103 zIndex: 42, // arbitrary 104 }, 105 106 banner: { 107 flex: 1, 108 overflow: 'visible', 109 backgroundColor: 'rgba(0,0,0,0.75)', 110 paddingBottom: getInitialSafeArea().bottom, 111 }, 112 contentContainer: { 113 flex: 1, 114 paddingTop: 10, 115 paddingBottom: 5, 116 alignItems: 'center', 117 justifyContent: 'center', 118 textAlign: 'center', 119 }, 120 text: { 121 color: '#fff', 122 fontSize: 15, 123 }, 124 subtitle: { 125 color: 'rgba(255,255,255,0.8)', 126 }, 127}); 128