Open file import

Summary: This change only adds the PWA as capable of handling files with the ".flipper" extension.

Reviewed By: aigoncharov

Differential Revision: D48353437

fbshipit-source-id: fd78942ac4dffb7d26d5ca5be826290018465b93
This commit is contained in:
Lorenzo Blasa
2023-08-17 13:46:08 -07:00
committed by Facebook GitHub Bot
parent 9728155cbf
commit ce13ee426f
13 changed files with 150 additions and 106 deletions

View File

@@ -67,7 +67,7 @@ interface NotificationConstructorOptions {
// Events that are emitted from the main.ts ovr the IPC process bridge in Electron
type MainProcessEvents = {
'flipper-protocol-handler': [query: string];
'open-flipper-file': [url: string];
'open-flipper-file': [name: string, data: string];
notificationEvent: [
eventName: NotificationEvents,
pluginNotification: PluginNotification,
@@ -88,7 +88,7 @@ type ChildProcessEvents = {
},
];
getLaunchTime: [];
componentDidMount: [];
storeRehydrated: [];
};
/**

View File

@@ -71,6 +71,7 @@ async function start() {
const flipperServerConfig = await flipperServer.exec('get-config');
initializeRenderHost(flipperServer, flipperServerConfig);
initializePWA();
// By turning this in a require, we force the JS that the body of this module (init) has completed (initializeElectron),
// before starting the rest of the Flipper process.
@@ -78,6 +79,7 @@ async function start() {
// but not set yet, which might happen when using normal imports.
// TODO: remove
window.flipperShowError?.('Connected to Flipper Server successfully');
// @ts-ignore
// eslint-disable-next-line import/no-commonjs
require('flipper-ui-core').startFlipperDesktop(flipperServer);
@@ -89,6 +91,76 @@ start().catch((e) => {
window.flipperShowError?.('Failed to start flipper-ui-browser: ' + e);
});
async function initializePWA() {
console.log('[PWA] Initialization');
let cachedFile: {name: string; data: string} | undefined;
let rehydrated = false;
const openFileIfAny = () => {
if (!cachedFile || !rehydrated) {
return;
}
window.dispatchEvent(
new CustomEvent('open-flipper-file', {
detail: [cachedFile.name, cachedFile.data],
}),
);
cachedFile = undefined;
};
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => {
console.log('[PWA] Service Worker has been registered');
})
.catch((e) => {
console.error('[PWA] failed to register Service Worker', e);
});
}
if ('launchQueue' in window) {
console.log('[PWA] File Handling API is supported');
// @ts-ignore
window.launchQueue.setConsumer(async (launchParams) => {
if (!launchParams || !launchParams.files) {
return;
}
console.log('[PWA] Attempt to to open a file');
for (const file of launchParams.files) {
const blob = await file.getFile();
blob.handle = file;
const data = await blob.text();
const name = file.name;
cachedFile = {name, data};
openFileIfAny();
}
});
} else {
console.warn('[PWA] File Handling API is not supported');
}
console.log('[PWA] Add before install prompt listener');
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent Chrome 67 and earlier from automatically showing the prompt.
e.preventDefault();
// Stash the event so it can be triggered later.
// @ts-ignore
global.PWAppInstallationEvent = e;
console.log('[PWA] Installation event has been captured');
});
window.addEventListener('storeRehydrated', () => {
console.info('[PWA] Store is rehydrated');
rehydrated = true;
openFileIfAny();
});
}
// getLogger() is not yet created when the electron app starts.
// we can't create it here yet, as the real logger is wired up to
// the redux store and the rest of the world. So we create a delegating logger

View File

@@ -153,11 +153,13 @@ export function initializeRenderHost(
hasFocus() {
return document.hasFocus();
},
onIpcEvent(_event) {
// no-op
onIpcEvent(event, cb) {
window.addEventListener(event as string, (ev) => {
cb(...((ev as CustomEvent).detail as any));
});
},
sendIpcEvent(_event, ..._args: any[]) {
// no-op
sendIpcEvent(event, ...args: any[]) {
window.dispatchEvent(new CustomEvent(event, {detail: args}));
},
shouldUseDarkColors() {
return !!(

View File

@@ -10,7 +10,7 @@
import {Store} from '../reducers/index';
import {Logger} from 'flipper-common';
import {
importFileToStore,
importDataToStore,
IMPORT_FLIPPER_TRACE_EVENT,
} from '../utils/exportData';
import {tryCatchReportPlatformFailures} from 'flipper-common';
@@ -67,9 +67,9 @@ export default (store: Store, logger: Logger) => {
});
});
renderHost.onIpcEvent('open-flipper-file', (url: string) => {
renderHost.onIpcEvent('open-flipper-file', (name: string, data: string) => {
tryCatchReportPlatformFailures(() => {
return importFileToStore(url, store);
return importDataToStore(name, data, store);
}, `${IMPORT_FLIPPER_TRACE_EVENT}:Deeplink`);
});
};

View File

@@ -26,7 +26,10 @@ import {Store} from '../reducers/index';
import {Dispatcher} from './types';
import {notNull} from '../utils/typeUtils';
export default function (store: Store, logger: Logger): () => Promise<void> {
export default async function (
store: Store,
logger: Logger,
): Promise<() => Promise<void>> {
// This only runs in development as when the reload
// kicks in it doesn't unregister the shortcuts
const dispatchers: Array<Dispatcher> = [
@@ -43,10 +46,11 @@ export default function (store: Store, logger: Logger): () => Promise<void> {
pluginChangeListener,
pluginsSourceUpdateListener,
].filter(notNull);
const globalCleanup = dispatchers
.map((dispatcher) => dispatcher(store, logger))
.filter(Boolean);
return () => {
return Promise.all(globalCleanup).then(() => {});
const globalCleanup = await Promise.all(
dispatchers.map((dispatcher) => dispatcher(store, logger)).filter(Boolean),
);
return async () => {
await Promise.all(globalCleanup);
};
}

View File

@@ -196,5 +196,4 @@ function registerStartupTime(logger: Logger) {
});
renderHost.sendIpcEvent('getLaunchTime');
renderHost.sendIpcEvent('componentDidMount');
}

View File

@@ -149,9 +149,10 @@ function init(flipperServer: FlipperServer) {
loadTheme(settings.darkMode);
// rehydrate app state before exposing init
const persistor = persistStore(store, undefined, () => {
const persistor = persistStore(store, undefined, async () => {
// Make sure process state is set before dispatchers run
dispatcher(store, logger);
await dispatcher(store, logger);
getRenderHostInstance().sendIpcEvent('storeRehydrated');
});
setPersistor(persistor);
@@ -168,17 +169,17 @@ function init(flipperServer: FlipperServer) {
connectFlipperServerToStore(flipperServer, store, logger);
enableConsoleHook();
enableConnectivityHook(flipperServer);
// TODO T116224873: Return the following code back instead of ReactDOM.react when the following issue is fixed: https://github.com/react-component/trigger/issues/288
// const root = createRoot(document.getElementById('root')!);
// root.render(<AppFrame logger={logger} persistor={persistor} />);
ReactDOM.render(
<AppFrame logger={logger} persistor={persistor} />,
document.getElementById('root')!,
);
enableConsoleHook();
enableConnectivityHook(flipperServer);
const root = document.getElementById('root');
if (root) {
ReactDOM.render(<AppFrame logger={logger} persistor={persistor} />, root);
}
const launcherMessage =
getRenderHostInstance().serverConfig.processConfig.launcherMsg;
@@ -193,8 +194,8 @@ function init(flipperServer: FlipperServer) {
}
}
export async function startFlipperDesktop(flipperServer: FlipperServer) {
getRenderHostInstance(); // renderHost instance should be set at this point!
export function startFlipperDesktop(flipperServer: FlipperServer) {
getRenderHostInstance();
init(flipperServer);
}

View File

@@ -30,7 +30,7 @@ import {TestIdler} from './Idler';
import {processMessageQueue} from './messageQueue';
import {getPluginTitle} from './pluginUtils';
import {capture} from './screenshot';
import {Dialog, getFlipperLib, Idler, path} from 'flipper-plugin';
import {Dialog, getFlipperLib, Idler} from 'flipper-plugin';
import {ClientQuery} from 'flipper-common';
import ShareSheetExportUrl from '../chrome/ShareSheetExportUrl';
import ShareSheetExportFile from '../chrome/ShareSheetExportFile';
@@ -43,6 +43,7 @@ import {safeFilename} from './safeFilename';
import {getExportablePlugins} from '../selectors/connections';
import {notification} from 'antd';
import openSupportRequestForm from '../fb-stubs/openSupportRequestForm';
import {getStore} from '../store';
export const IMPORT_FLIPPER_TRACE_EVENT = 'import-flipper-trace';
export const EXPORT_FLIPPER_TRACE_EVENT = 'export-flipper-trace';
@@ -526,7 +527,7 @@ export const exportStoreToFile = (
export async function importDataToStore(
source: string,
data: string,
store: Store,
store: Store = getStore(),
) {
getLogger().track('usage', IMPORT_FLIPPER_TRACE_EVENT);
const json: ExportType = JSON.parse(data);

View File

@@ -138,25 +138,6 @@
document.body.appendChild(script);
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => {
console.log('Flipper Service Worker has been registered');
})
.catch((e) => {
console.error('Flipper failed to register Service Worker', e);
});
}
window.addEventListener('beforeinstallprompt', (e) => {
console.log('Flipper PWA before install prompt with event', e);
// Prevent Chrome 67 and earlier from automatically showing the prompt.
e.preventDefault();
// Stash the event so it can be triggered later.
global.PWAppInstallationEvent = e;
});
init();
})();
</script>

View File

@@ -59,7 +59,7 @@
</div>
</div>
<div class="__infinity-dev-box __infinity-dev-box-error" hidden />
<div class="__infinity-dev-box __infinity-dev-box-error" hidden></div>
<script>
(function () {
// FIXME: needed to make Metro work
@@ -114,25 +114,6 @@
document.body.appendChild(script);
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => {
console.log('Flipper Service Worker has been registered');
})
.catch((e) => {
console.error('Flipper failed to register Service Worker', e);
});
}
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent Chrome 67 and earlier from automatically showing the prompt.
e.preventDefault();
// Stash the event so it can be triggered later.
global.PWAppInstallationEvent = e;
});
init();
})();
</script>

View File

@@ -63,14 +63,6 @@ if (process.platform === 'darwin') {
const argv = yargs
.usage('$0 [args]')
.options({
file: {
describe: 'Define a file to open on startup.',
type: 'string',
},
url: {
describe: 'Define a flipper:// URL to open on startup.',
type: 'string',
},
updater: {
default: true,
describe: 'Toggle the built-in update mechanism.',
@@ -114,9 +106,8 @@ if (argv['disable-gpu'] || process.env.FLIPPER_DISABLE_GPU === '1') {
// possible reference to main app window
let win: BrowserWindow;
let appReady = false;
let deeplinkURL: string | undefined = argv.url;
let filePath: string | undefined = argv.file;
let didMount = false;
let deeplinkURL: string | undefined;
let filePath: string | undefined;
// check if we already have an instance of this app open
const gotTheLock = app.requestSingleInstanceLock();
@@ -138,6 +129,24 @@ if (!gotTheLock) {
app.on('ready', () => {});
}
const openFileIfAny = () => {
if (!filePath || !win) {
return;
}
fs.readFile(filePath, {encoding: 'utf-8'}, (_err, data) => {
win.webContents.send('open-flipper-file', filePath, data);
filePath = undefined;
});
};
const openURLIfAny = () => {
if (!deeplinkURL || !win) {
return;
}
win.webContents.send('flipper-protocol-handler', deeplinkURL);
deeplinkURL = undefined;
};
// quit app once all windows are closed
app.on('window-all-closed', () => {
appReady = false;
@@ -148,22 +157,16 @@ app.on('will-finish-launching', () => {
// Protocol handler for osx
app.on('open-url', function (event, url) {
event.preventDefault();
argv.url = url;
if (win && didMount) {
win.webContents.send('flipper-protocol-handler', url);
} else {
deeplinkURL = url;
}
deeplinkURL = url;
openURLIfAny();
});
app.on('open-file', (event, path) => {
// When flipper app is running, and someone double clicks the import file, `componentDidMount` will not be called again and windows object will exist in that case. That's why calling `win.webContents.send('open-flipper-file', filePath);` again.
// When flipper app is running, and someone double clicks the import file,
// component and store is already mounted and the windows object will exist.
// In that case, the file can be immediately opened.
event.preventDefault();
filePath = path;
argv.file = path;
if (win) {
win.webContents.send('open-flipper-file', filePath);
filePath = undefined;
}
openFileIfAny();
});
});
@@ -256,17 +259,9 @@ function configureSession() {
);
}
ipcMain.on('componentDidMount', (_event) => {
didMount = true;
if (deeplinkURL) {
win.webContents.send('flipper-protocol-handler', deeplinkURL);
deeplinkURL = undefined;
}
if (filePath) {
// When flipper app is not running, the windows object might not exist in the callback of `open-file`, but after ``componentDidMount` it will definitely exist.
win.webContents.send('open-flipper-file', filePath);
filePath = undefined;
}
ipcMain.on('storeRehydrated', (_event) => {
openFileIfAny();
openURLIfAny();
});
ipcMain.on('getLaunchTime', (event) => {

View File

@@ -14,5 +14,15 @@
"type": "image/png",
"sizes": "256x256"
}
],
"file_handlers": [
{
"action": "/",
"accept": {
"text/*": [
".flipper"
]
}
}
]
}

View File

@@ -52,8 +52,7 @@ self.addEventListener('fetch', (event) => {
}
// Always try the network first (try flipper server)
const networkResponse = await fetch(event.request);
return networkResponse;
return await fetch(event.request);
} catch (error) {
// Catch is only triggered if an exception is thrown, which is likely
// due to a network error.
@@ -62,8 +61,7 @@ self.addEventListener('fetch', (event) => {
console.log('Fetch failed; returning offline page instead.', error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
return await cache.match(OFFLINE_URL);
}
})());
}