diff --git a/desktop/app/src/RenderHost.tsx b/desktop/app/src/RenderHost.tsx new file mode 100644 index 000000000..a8b25f529 --- /dev/null +++ b/desktop/app/src/RenderHost.tsx @@ -0,0 +1,45 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +/** + * Utilities provided by the render host, e.g. Electron, the Browser, etc + */ +export interface RenderHost { + readonly processId: number; + readTextFromClipboard(): string | undefined; + selectDirectory?(defaultPath?: string): Promise; + registerShortcut(shortCut: string, callback: () => void): void; + hasFocus(): boolean; +} + +let renderHostInstance: RenderHost | undefined; + +export function getRenderHostInstance(): RenderHost { + if (!renderHostInstance) { + throw new Error('setRenderHostInstance was never called'); + } + return renderHostInstance; +} + +export function setRenderHostInstance(instance: RenderHost) { + renderHostInstance = instance; +} + +if (process.env.NODE_ENV === 'test') { + setRenderHostInstance({ + processId: -1, + readTextFromClipboard() { + return ''; + }, + registerShortcut() {}, + hasFocus() { + return true; + }, + }); +} diff --git a/desktop/app/src/chrome/settings/configFields.tsx b/desktop/app/src/chrome/settings/configFields.tsx index b4c73904b..97f404669 100644 --- a/desktop/app/src/chrome/settings/configFields.tsx +++ b/desktop/app/src/chrome/settings/configFields.tsx @@ -7,7 +7,6 @@ * @format */ -import electron from 'electron'; import { FlexColumn, styled, @@ -19,11 +18,8 @@ import { } from '../../ui'; import React, {useState} from 'react'; import {promises as fs} from 'fs'; -// Used for dialogs. -// eslint-disable-next-line flipper/no-electron-remote-imports -import {remote} from 'electron'; -import path from 'path'; import {theme} from 'flipper-plugin'; +import {getRenderHostInstance} from '../../RenderHost'; export const ConfigFieldContainer = styled(FlexRow)({ paddingLeft: 10, @@ -71,6 +67,7 @@ export function FilePathConfigField(props: { // Defaults to allowing directories only, this changes to expect regular files. isRegularFile?: boolean; }) { + const renderHost = getRenderHostInstance(); const [value, setValue] = useState(props.defaultValue); const [isValid, setIsValid] = useState(true); fs.stat(value) @@ -102,27 +99,28 @@ export function FilePathConfigField(props: { .catch((_) => setIsValid(false)); }} /> - - remote.dialog - .showOpenDialog({ - properties: ['openDirectory', 'showHiddenFiles'], - defaultPath: path.resolve('/'), - }) - .then((result: electron.SaveDialogReturnValue) => { - if (result.filePath) { - const path: string = result.filePath.toString(); - setValue(path); - props.onChange(path); - } - }) - }> - - + {renderHost.selectDirectory && ( + { + renderHost + .selectDirectory?.() + .then((path) => { + if (path) { + setValue(path); + props.onChange(path); + } + }) + .catch((e) => { + console.warn('Failed to select dir', e); + }); + }}> + + + )} {props.resetValue && ( Promise { // This only runs in development as when the reload // kicks in it doesn't unregister the shortcuts - if (process.env.NODE_ENV === 'development') { - remote.globalShortcut.unregisterAll(); - } const dispatchers: Array = [ application, tracking, diff --git a/desktop/app/src/dispatcher/reactNative.tsx b/desktop/app/src/dispatcher/reactNative.tsx index 989124988..ed5ab2f9d 100644 --- a/desktop/app/src/dispatcher/reactNative.tsx +++ b/desktop/app/src/dispatcher/reactNative.tsx @@ -7,10 +7,8 @@ * @format */ -// Used to register a shortcut. Don't have an alternative for that. -// eslint-disable-next-line flipper/no-electron-remote-imports -import {remote} from 'electron'; import {Store} from '../reducers'; +import {getRenderHostInstance} from '../RenderHost'; type ShortcutEventCommand = | { @@ -21,6 +19,7 @@ type ShortcutEventCommand = export default (store: Store) => { const settings = store.getState().settingsState.reactNative; + const renderHost = getRenderHostInstance(); if (!settings.shortcuts.enabled) { return; @@ -41,7 +40,7 @@ export default (store: Store) => { (shortcut: ShortcutEventCommand) => shortcut && shortcut.shortcut && - remote.globalShortcut.register(shortcut.shortcut, () => { + renderHost.registerShortcut(shortcut.shortcut, () => { const devices = store .getState() .connections.devices.filter( diff --git a/desktop/app/src/dispatcher/tracking.tsx b/desktop/app/src/dispatcher/tracking.tsx index 5393fdc92..44dd36dad 100644 --- a/desktop/app/src/dispatcher/tracking.tsx +++ b/desktop/app/src/dispatcher/tracking.tsx @@ -9,7 +9,7 @@ // Used for PID tracking. // eslint-disable-next-line flipper/no-electron-remote-imports -import {ipcRenderer, remote} from 'electron'; +import {ipcRenderer} from 'electron'; import {performance} from 'perf_hooks'; import {EventEmitter} from 'events'; @@ -32,6 +32,7 @@ import {getCPUUsage} from 'process'; import {sideEffect} from '../utils/sideEffect'; import {getSelectionInfo} from '../utils/info'; import type {SelectionInfo} from '../utils/info'; +import {getRenderHostInstance} from '../RenderHost'; const TIME_SPENT_EVENT = 'time-spent'; @@ -77,6 +78,7 @@ export function emitBytesReceived(plugin: string, bytes: number) { } export default (store: Store, logger: Logger) => { + const renderHost = getRenderHostInstance(); sideEffect( store, { @@ -97,7 +99,7 @@ export default (store: Store, logger: Logger) => { const oldExitData = loadExitData(); if (oldExitData) { - const isReload = remote.process.pid === oldExitData.pid; + const isReload = renderHost.processId === oldExitData.pid; const timeSinceLastStartup = Date.now() - parseInt(oldExitData.lastSeen, 10); // console.log(isReload ? 'reload' : 'restart', oldExitData); @@ -373,7 +375,7 @@ export function persistExitData( ? deconstructClientId(state.selectedAppId).app : '', cleanExit, - pid: remote.process.pid, + pid: getRenderHostInstance().processId, }; window.localStorage.setItem( flipperExitDataKey, diff --git a/desktop/app/src/init.tsx b/desktop/app/src/init.tsx index 7d8dd5700..1c4fc2b7f 100644 --- a/desktop/app/src/init.tsx +++ b/desktop/app/src/init.tsx @@ -50,13 +50,15 @@ import {CopyOutlined} from '@ant-design/icons'; import {getVersionString} from './utils/versionString'; import {PersistGate} from 'redux-persist/integration/react'; // eslint-disable-next-line flipper/no-electron-remote-imports -import {ipcRenderer, remote} from 'electron'; +import {ipcRenderer, remote, SaveDialogReturnValue} from 'electron'; import { setLoggerInstance, setUserSessionManagerInstance, GK as flipperCommonGK, } from 'flipper-common'; import {internGraphPOSTAPIRequest} from './fb-stubs/user'; +import {setRenderHostInstance} from './RenderHost'; +import {clipboard} from 'electron'; if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') { // By default Node.JS has its internal certificate storage and doesn't use @@ -187,6 +189,7 @@ function setProcessState(store: Store) { } function init() { + initializeFlipperForElectron(); // TODO: centralise all those initialisations in a single configuration call flipperCommonGK.get = (name) => GK.get(name); const store = getStore(); @@ -262,3 +265,37 @@ const CodeBlock = styled(Input.TextArea)({ ...theme.monospace, color: theme.textColorSecondary, }); + +function initializeFlipperForElectron() { + setRenderHostInstance({ + processId: remote.process.pid, + readTextFromClipboard() { + return clipboard.readText(); + }, + selectDirectory(defaultPath = path.resolve('/')) { + return remote.dialog + .showOpenDialog({ + properties: ['openDirectory', 'showHiddenFiles'], + defaultPath, + }) + .then((result: SaveDialogReturnValue & {filePaths: string[]}) => { + if (result.filePath) { + return result.filePath.toString(); + } + // Electron typings seem of here, just in case, + // (can be tested with settings dialog) + // handle both situations + if (result.filePaths) { + return result.filePaths[0]; + } + return undefined; + }); + }, + registerShortcut(shortcut, callback) { + remote.globalShortcut.register(shortcut, callback); + }, + hasFocus() { + return remote.getCurrentWindow().isFocused(); + }, + }); +} diff --git a/desktop/app/src/reducers/application.tsx b/desktop/app/src/reducers/application.tsx index 4ea11e819..85e939daf 100644 --- a/desktop/app/src/reducers/application.tsx +++ b/desktop/app/src/reducers/application.tsx @@ -7,10 +7,8 @@ * @format */ -// This is fine, we're using a focus event. -// eslint-disable-next-line flipper/no-electron-remote-imports -import {remote} from 'electron'; import {v1 as uuidv1} from 'uuid'; +import {getRenderHostInstance} from '../RenderHost'; import {Actions} from './'; export type LauncherMsg = { @@ -83,7 +81,7 @@ export const initialState: () => State = () => ({ leftSidebarVisible: true, rightSidebarVisible: true, rightSidebarAvailable: false, - windowIsFocused: remote.getCurrentWindow().isFocused(), + windowIsFocused: getRenderHostInstance().hasFocus(), activeSheet: null, share: null, sessionId: uuidv1(), diff --git a/desktop/app/src/reducers/usageTracking.tsx b/desktop/app/src/reducers/usageTracking.tsx index 7cb386135..2e0294039 100644 --- a/desktop/app/src/reducers/usageTracking.tsx +++ b/desktop/app/src/reducers/usageTracking.tsx @@ -8,11 +8,9 @@ */ import {produce} from 'immer'; -// Used for focus events which is fine. -// eslint-disable-next-line flipper/no-electron-remote-imports -import {remote} from 'electron'; import {Actions} from './'; import {SelectionInfo} from '../utils/info'; +import {getRenderHostInstance} from '../RenderHost'; export type TrackingEvent = | { @@ -31,15 +29,15 @@ export type TrackingEvent = export type State = { timeline: TrackingEvent[]; }; -const INITAL_STATE: State = { +const INITAL_STATE: () => State = () => ({ timeline: [ { type: 'TIMELINE_START', time: Date.now(), - isFocused: remote.getCurrentWindow().isFocused(), + isFocused: getRenderHostInstance().hasFocus(), }, ], -}; +}); export type Action = | { @@ -53,7 +51,7 @@ export type Action = }; export default function reducer( - state: State = INITAL_STATE, + state: State = INITAL_STATE(), action: Actions, ): State { if (action.type === 'CLEAR_TIMELINE') { @@ -94,7 +92,7 @@ export function clearTimeline(time: number): Action { type: 'CLEAR_TIMELINE', payload: { time, - isFocused: remote.getCurrentWindow().isFocused(), + isFocused: getRenderHostInstance().hasFocus(), }, }; }