xref: /expo/home/menu/DevMenuView.tsx (revision 6de5be70)
1import { HomeFilledIcon, iconSize, RefreshIcon } from '@expo/styleguide-native';
2import MaterialCommunityIcons from '@expo/vector-icons/build/MaterialCommunityIcons';
3import { Divider, Row, Spacer, useExpoTheme, View } from 'expo-dev-client-components';
4import * as Font from 'expo-font';
5import React, { Fragment, useContext, useEffect, useRef } from 'react';
6import { Clipboard } from 'react-native';
7import { useSafeAreaInsets } from 'react-native-safe-area-context';
8
9import { ClipboardIcon } from './ClipboardIcon';
10import DevMenuBottomSheetContext from './DevMenuBottomSheetContext';
11import { DevMenuButton } from './DevMenuButton';
12import { DevMenuCloseButton } from './DevMenuCloseButton';
13import { DevMenuItem } from './DevMenuItem';
14import * as DevMenu from './DevMenuModule';
15import { DevMenuOnboarding } from './DevMenuOnboarding';
16import { DevMenuServerInfo } from './DevMenuServerInfo';
17import { DevMenuTaskInfo } from './DevMenuTaskInfo';
18
19type Props = {
20  task: { manifestUrl: string; manifestString: string };
21  uuid: string;
22};
23
24// These are defined in EXVersionManager.m in a dictionary, ordering needs to be
25// done here.
26const DEV_MENU_ORDER = [
27  'dev-live-reload',
28  'dev-hmr',
29  'dev-remote-debug',
30  'dev-reload',
31  'dev-perf-monitor',
32  'dev-inspector',
33];
34
35function ThemedMaterialIcon({
36  name,
37}: {
38  name: React.ComponentProps<typeof MaterialCommunityIcons>['name'];
39}) {
40  const theme = useExpoTheme();
41  return <MaterialCommunityIcons name={name} size={iconSize.regular} color={theme.icon.default} />;
42}
43
44const MENU_ITEMS_ICON_MAPPINGS: {
45  [key: string]: React.ReactNode;
46} = {
47  'dev-hmr': <ThemedMaterialIcon name="run-fast" />,
48  'dev-remote-debug': <ThemedMaterialIcon name="remote-desktop" />,
49  'dev-perf-monitor': <ThemedMaterialIcon name="speedometer" />,
50  'dev-inspector': <ThemedMaterialIcon name="border-style" />,
51};
52
53export function DevMenuView({ uuid, task }: Props) {
54  const context = useContext(DevMenuBottomSheetContext);
55
56  const [enableDevMenuTools, setEnableDevMenuTools] = React.useState(false);
57  const [devMenuItems, setDevMenuItems] = React.useState<{ [key: string]: any }>({});
58  const [isOnboardingFinished, setIsOnboardingFinished] = React.useState(false);
59  const [isLoaded, setIsLoaded] = React.useState(false);
60
61  const theme = useExpoTheme();
62  const insets = useSafeAreaInsets();
63
64  const prevUUIDRef = useRef(uuid);
65
66  useEffect(function didMount() {
67    loadStateAsync();
68  }, []);
69
70  useEffect(
71    function loadStateWhenUUIDChanges() {
72      if (prevUUIDRef.current !== uuid) {
73        loadStateAsync();
74      }
75
76      prevUUIDRef.current = uuid;
77    },
78    [uuid]
79  );
80
81  async function collapse() {
82    if (context) {
83      await context.collapse();
84    }
85  }
86
87  async function collapseAndCloseDevMenuAsync() {
88    await collapse();
89    await DevMenu.closeAsync();
90  }
91
92  async function loadStateAsync() {
93    setIsLoaded(false);
94
95    const [enableDevMenuTools, devMenuItems, isOnboardingFinished] = await Promise.all([
96      DevMenu.doesCurrentTaskEnableDevtoolsAsync(),
97      DevMenu.getItemsToShowAsync(),
98      DevMenu.isOnboardingFinishedAsync(),
99      Font.loadAsync({
100        'Inter-Black': require('../assets/Inter/Inter-Black.otf'),
101        'Inter-BlackItalic': require('../assets/Inter/Inter-BlackItalic.otf'),
102        'Inter-Bold': require('../assets/Inter/Inter-Bold.otf'),
103        'Inter-BoldItalic': require('../assets/Inter/Inter-BoldItalic.otf'),
104        'Inter-ExtraBold': require('../assets/Inter/Inter-ExtraBold.otf'),
105        'Inter-ExtraBoldItalic': require('../assets/Inter/Inter-ExtraBoldItalic.otf'),
106        'Inter-ExtraLight': require('../assets/Inter/Inter-ExtraLight.otf'),
107        'Inter-ExtraLightItalic': require('../assets/Inter/Inter-ExtraLightItalic.otf'),
108        'Inter-Regular': require('../assets/Inter/Inter-Regular.otf'),
109        'Inter-Italic': require('../assets/Inter/Inter-Italic.otf'),
110        'Inter-Light': require('../assets/Inter/Inter-Light.otf'),
111        'Inter-LightItalic': require('../assets/Inter/Inter-LightItalic.otf'),
112        'Inter-Medium': require('../assets/Inter/Inter-Medium.otf'),
113        'Inter-MediumItalic': require('../assets/Inter/Inter-MediumItalic.otf'),
114        'Inter-SemiBold': require('../assets/Inter/Inter-SemiBold.otf'),
115        'Inter-SemiBoldItalic': require('../assets/Inter/Inter-SemiBoldItalic.otf'),
116        'Inter-Thin': require('../assets/Inter/Inter-Thin.otf'),
117        'Inter-ThinItalic': require('../assets/Inter/Inter-ThinItalic.otf'),
118      }),
119    ]);
120
121    setEnableDevMenuTools(enableDevMenuTools);
122    setDevMenuItems(devMenuItems);
123    setIsOnboardingFinished(isOnboardingFinished);
124    setIsLoaded(true);
125  }
126
127  function onAppReload() {
128    collapse();
129    DevMenu.reloadAppAsync();
130  }
131
132  async function onCopyTaskUrl() {
133    const { manifestUrl } = task;
134
135    await collapseAndCloseDevMenuAsync();
136    Clipboard.setString(manifestUrl);
137    alert(`Copied "${manifestUrl}" to the clipboard!`);
138  }
139
140  function onGoToHome() {
141    collapse();
142    DevMenu.goToHomeAsync();
143  }
144
145  function onPressDevMenuButton(key: string) {
146    DevMenu.selectItemWithKeyAsync(key);
147  }
148
149  function onOnboardingFinished() {
150    DevMenu.setOnboardingFinishedAsync(true);
151    setIsOnboardingFinished(true);
152  }
153
154  const sortedDevMenuItems = Object.keys(devMenuItems).sort(
155    (a, b) => DEV_MENU_ORDER.indexOf(a) - DEV_MENU_ORDER.indexOf(b)
156  );
157
158  if (!isLoaded) {
159    return null;
160  }
161
162  return (
163    <View bg="secondary" flex="1" roundedTop="large" overflow="hidden" style={{ direction: 'ltr' }}>
164      <DevMenuTaskInfo task={task} />
165      <Divider />
166      <View>
167        {!isOnboardingFinished ? (
168          <DevMenuOnboarding onClose={onOnboardingFinished} />
169        ) : (
170          <View style={{ paddingBottom: insets.bottom }}>
171            <DevMenuServerInfo task={task} />
172            <Divider />
173            <Row align="center" padding="medium">
174              <DevMenuButton
175                buttonKey="reload"
176                label="Reload"
177                onPress={onAppReload}
178                icon={<RefreshIcon size={iconSize.small} color={theme.icon.default} />}
179              />
180              <Spacer.Horizontal size="medium" />
181              {task && task.manifestUrl && (
182                <>
183                  <DevMenuButton
184                    buttonKey="copy"
185                    label="Copy Link"
186                    onPress={onCopyTaskUrl}
187                    icon={<ClipboardIcon size={iconSize.regular} color={theme.icon.default} />}
188                  />
189                  <Spacer.Horizontal size="medium" />
190                </>
191              )}
192              <DevMenuButton
193                buttonKey="home"
194                label="Go Home"
195                onPress={onGoToHome}
196                icon={<HomeFilledIcon size={iconSize.regular} color={theme.icon.default} />}
197              />
198            </Row>
199            {enableDevMenuTools && devMenuItems && (
200              <View padding="medium" style={{ paddingTop: 0 }}>
201                <View bg="default" rounded="large">
202                  {sortedDevMenuItems.map((key, i) => {
203                    const item = devMenuItems[key];
204
205                    const { label, isEnabled } = item;
206                    return (
207                      <Fragment key={key}>
208                        <DevMenuItem
209                          buttonKey={key}
210                          label={label}
211                          onPress={onPressDevMenuButton}
212                          icon={MENU_ITEMS_ICON_MAPPINGS[key]}
213                          isEnabled={isEnabled}
214                        />
215                        {i < sortedDevMenuItems.length - 1 && <Divider />}
216                      </Fragment>
217                    );
218                  })}
219                </View>
220              </View>
221            )}
222          </View>
223        )}
224      </View>
225      <DevMenuCloseButton onPress={collapseAndCloseDevMenuAsync} />
226    </View>
227  );
228}
229