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