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