xref: /expo/home/menu/DevMenuView.tsx (revision bb8f4f99)
1import React from 'react';
2import { Clipboard, PixelRatio, StyleSheet } from 'react-native';
3
4import { StyledView } from '../components/Views';
5import DevMenuBottomSheetContext, { Context } from './DevMenuBottomSheetContext';
6import DevMenuButton from './DevMenuButton';
7import DevMenuCloseButton from './DevMenuCloseButton';
8import * as DevMenu from './DevMenuModule';
9import DevMenuOnboarding from './DevMenuOnboarding';
10import DevMenuTaskInfo from './DevMenuTaskInfo';
11
12type Props = {
13  task: { [key: string]: any };
14  uuid: string;
15};
16
17type State = {
18  enableDevMenuTools: boolean;
19  devMenuItems: { [key: string]: any };
20  isOnboardingFinished: boolean;
21  isLoaded: boolean;
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
35const MENU_ITEMS_ICON_MAPPINGS = {
36  'dev-hmr': 'run-fast',
37  'dev-remote-debug': 'remote-desktop',
38  'dev-perf-monitor': 'speedometer',
39  'dev-inspector': 'border-style',
40};
41
42class DevMenuView extends React.PureComponent<Props, State> {
43  static contextType = DevMenuBottomSheetContext;
44
45  context!: Context;
46
47  constructor(props, context) {
48    super(props, context);
49
50    this.state = {
51      enableDevMenuTools: false,
52      devMenuItems: {},
53      isOnboardingFinished: false,
54      isLoaded: false,
55    };
56  }
57
58  componentDidMount() {
59    this.loadStateAsync();
60  }
61
62  componentDidUpdate(prevProps: Props) {
63    if (this.props.uuid !== prevProps.uuid) {
64      this.loadStateAsync();
65    }
66  }
67
68  collapse = async () => {
69    if (this.context) {
70      await this.context.collapse();
71    }
72  };
73
74  collapseAndCloseDevMenuAsync = async () => {
75    await this.collapse();
76    await DevMenu.closeAsync();
77  };
78
79  loadStateAsync = async () => {
80    this.setState({ isLoaded: false });
81
82    const [enableDevMenuTools, devMenuItems, isOnboardingFinished] = await Promise.all([
83      DevMenu.doesCurrentTaskEnableDevtoolsAsync(),
84      DevMenu.getItemsToShowAsync(),
85      DevMenu.isOnboardingFinishedAsync(),
86    ]);
87
88    this.setState({
89      enableDevMenuTools,
90      devMenuItems,
91      isOnboardingFinished,
92      isLoaded: true,
93    });
94  };
95
96  onAppReload = () => {
97    this.collapse();
98    DevMenu.reloadAppAsync();
99  };
100
101  onCopyTaskUrl = async () => {
102    const { manifestUrl } = this.props.task;
103
104    await this.collapseAndCloseDevMenuAsync();
105    Clipboard.setString(manifestUrl);
106    alert(`Copied "${manifestUrl}" to the clipboard!`);
107  };
108
109  onGoToHome = () => {
110    this.collapse();
111    DevMenu.goToHomeAsync();
112  };
113
114  onPressDevMenuButton = key => {
115    DevMenu.selectItemWithKeyAsync(key);
116  };
117
118  onOnboardingFinished = () => {
119    DevMenu.setOnboardingFinishedAsync(true);
120    this.setState({ isOnboardingFinished: true });
121  };
122
123  maybeRenderDevMenuTools() {
124    const devMenuItems = Object.keys(this.state.devMenuItems).sort(
125      (a, b) => DEV_MENU_ORDER.indexOf(a) - DEV_MENU_ORDER.indexOf(b)
126    );
127
128    if (this.state.enableDevMenuTools && this.state.devMenuItems) {
129      return (
130        <>
131          <StyledView style={styles.separator} />
132          {devMenuItems.map(key => {
133            return this.renderDevMenuItem(key, this.state.devMenuItems[key]);
134          })}
135        </>
136      );
137    }
138    return null;
139  }
140
141  renderDevMenuItem(key, item) {
142    const { label, isEnabled, detail } = item;
143
144    return (
145      <DevMenuButton
146        key={key}
147        buttonKey={key}
148        label={label}
149        onPress={this.onPressDevMenuButton}
150        icon={MENU_ITEMS_ICON_MAPPINGS[key]}
151        isEnabled={isEnabled}
152        detail={detail}
153      />
154    );
155  }
156
157  renderContent() {
158    const { task } = this.props;
159    const { isLoaded, isOnboardingFinished } = this.state;
160
161    if (!isLoaded) {
162      return null;
163    }
164
165    return (
166      <>
167        {!isOnboardingFinished && <DevMenuOnboarding onClose={this.onOnboardingFinished} />}
168
169        <DevMenuTaskInfo task={task} />
170
171        <StyledView style={styles.separator} />
172
173        <DevMenuButton buttonKey="reload" label="Reload" onPress={this.onAppReload} icon="reload" />
174        {task && task.manifestUrl && (
175          <DevMenuButton
176            buttonKey="copy"
177            label="Copy link to clipboard"
178            onPress={this.onCopyTaskUrl}
179            icon="clipboard-text"
180          />
181        )}
182        <DevMenuButton buttonKey="home" label="Go to Home" onPress={this.onGoToHome} icon="home" />
183
184        {this.maybeRenderDevMenuTools()}
185        <DevMenuCloseButton
186          style={styles.closeButton}
187          onPress={this.collapseAndCloseDevMenuAsync}
188        />
189      </>
190    );
191  }
192
193  render() {
194    return (
195      <StyledView style={styles.container} darkBackgroundColor="#000">
196        {this.renderContent()}
197      </StyledView>
198    );
199  }
200}
201
202const styles = StyleSheet.create({
203  container: {
204    flex: 1,
205    paddingTop: 10,
206    borderTopLeftRadius: 10,
207    borderTopRightRadius: 10,
208  },
209  scrollView: {
210    flex: 1,
211  },
212  buttonContainer: {
213    backgroundColor: 'transparent',
214  },
215  separator: {
216    borderTopWidth: 1 / PixelRatio.get(),
217    height: 12,
218    marginVertical: 4,
219    marginHorizontal: -1,
220  },
221  closeButton: {
222    position: 'absolute',
223    right: 12,
224    top: 12,
225    zIndex: 3, // should be higher than zIndex of onboarding container
226  },
227});
228
229export default DevMenuView;
230