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