1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-27 10:32:58 +02:00
joplin/packages/app-mobile/components/plugins/PluginRunnerWebView.tsx
2024-08-02 14:51:49 +01:00

192 lines
5.8 KiB
TypeScript

import * as React from 'react';
import ExtendedWebView from '../ExtendedWebView';
import { WebViewControl } from '../ExtendedWebView/types';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import shim from '@joplin/lib/shim';
import PluginRunner from './PluginRunner';
import loadPlugins from '@joplin/lib/services/plugins/loadPlugins';
import { connect, useStore } from 'react-redux';
import Logger from '@joplin/utils/Logger';
import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService';
import { PluginHtmlContents, PluginStates } from '@joplin/lib/services/plugins/reducer';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import PluginDialogManager from './dialogs/PluginDialogManager';
import { AppState } from '../../utils/types';
import usePrevious from '@joplin/lib/hooks/usePrevious';
import PlatformImplementation from '../../services/plugins/PlatformImplementation';
import AccessibleView from '../accessibility/AccessibleView';
const logger = Logger.create('PluginRunnerWebView');
const usePluginSettings = (serializedPluginSettings: SerializedPluginSettings) => {
return useMemo(() => {
const pluginService = PluginService.instance();
return pluginService.unserializePluginSettings(serializedPluginSettings);
}, [serializedPluginSettings]);
};
const usePlugins = (
pluginRunner: PluginRunner,
webviewLoaded: boolean,
pluginSettings: PluginSettings,
) => {
const store = useStore<AppState>();
const lastPluginRunner = usePrevious(pluginRunner);
// Only set reloadAll to true here -- this ensures that all plugins are reloaded,
// even if loadPlugins is cancelled and re-run.
const reloadAllRef = useRef(false);
reloadAllRef.current ||= pluginRunner !== lastPluginRunner;
useAsyncEffect(async (event) => {
if (!webviewLoaded) {
return;
}
await loadPlugins({
pluginRunner,
pluginSettings,
platformImplementation: PlatformImplementation.instance(),
store,
reloadAll: reloadAllRef.current,
cancelEvent: event,
});
// A full reload, if it was necessary, has been completed.
if (!event.cancelled) {
reloadAllRef.current = false;
}
}, [pluginRunner, store, webviewLoaded, pluginSettings]);
};
const useUnloadPluginsOnGlobalDisable = (
pluginStates: PluginStates,
pluginSupportEnabled: boolean,
) => {
const pluginStatesRef = useRef(pluginStates);
pluginStatesRef.current = pluginStates;
useAsyncEffect(async event => {
if (!pluginSupportEnabled && Object.keys(pluginStatesRef.current).length) {
for (const pluginId in pluginStatesRef.current) {
await PluginService.instance().unloadPlugin(pluginId);
if (event.cancelled) return;
}
}
}, [pluginSupportEnabled]);
};
interface Props {
serializedPluginSettings: SerializedPluginSettings;
pluginSupportEnabled: boolean;
pluginStates: PluginStates;
pluginHtmlContents: PluginHtmlContents;
themeId: number;
}
const PluginRunnerWebViewComponent: React.FC<Props> = props => {
const webviewRef = useRef<WebViewControl>();
const [webviewLoaded, setLoaded] = useState(false);
const [webviewReloadCounter, setWebviewReloadCounter] = useState(0);
const pluginRunner = useMemo(() => {
if (webviewReloadCounter > 1) {
logger.debug(`Reloading the plugin runner (load #${webviewReloadCounter})`);
}
return new PluginRunner(webviewRef);
}, [webviewReloadCounter]);
const pluginSettings = usePluginSettings(props.serializedPluginSettings);
usePlugins(pluginRunner, webviewLoaded, pluginSettings);
useUnloadPluginsOnGlobalDisable(props.pluginStates, props.pluginSupportEnabled);
const onLoadStart = useCallback(() => {
// Handles the case where the webview reloads (e.g. due to an error or performance
// optimization).
// Increasing the counter recreates the plugin runner and reloads plugins.
setWebviewReloadCounter(webviewReloadCounter + 1);
}, [webviewReloadCounter]);
const onLoadEnd = useCallback(() => {
setLoaded(true);
}, []);
// To avoid increasing startup time/memory usage on devices with no plugins, don't
// load the webview if unnecessary.
// Note that we intentionally load the webview even if all plugins are disabled, as
// this allows any plugins we don't have settings for to run.
const loadWebView = props.pluginSupportEnabled;
useEffect(() => {
if (!loadWebView) {
setLoaded(false);
}
}, [loadWebView]);
const renderWebView = () => {
if (!loadWebView) {
return null;
}
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
</body>
</html>
`;
const injectedJs = `
if (!window.loadedBackgroundPage) {
${shim.injectedJs('pluginBackgroundPage')}
console.log('Loaded PluginRunnerWebView.');
// Necessary, because React Native WebView can re-run injectedJs
// without reloading the page.
window.loadedBackgroundPage = true;
}
`;
return (
<>
<ExtendedWebView
webviewInstanceId='PluginRunner2'
html={html}
injectedJavaScript={injectedJs}
hasPluginScripts={true}
onMessage={pluginRunner.onWebviewMessage}
onLoadEnd={onLoadEnd}
onLoadStart={onLoadStart}
ref={webviewRef}
/>
<PluginDialogManager
themeId={props.themeId}
pluginHtmlContents={props.pluginHtmlContents}
pluginStates={props.pluginStates}
/>
</>
);
};
return (
<AccessibleView style={{ display: 'none' }} inert={true}>
{renderWebView()}
</AccessibleView>
);
};
export default connect((state: AppState) => {
const result: Props = {
serializedPluginSettings: state.settings['plugins.states'],
pluginSupportEnabled: state.settings['plugins.pluginSupportEnabled'],
pluginStates: state.pluginService.plugins,
pluginHtmlContents: state.pluginService.pluginHtmlContents,
themeId: state.settings.theme,
};
return result;
})(PluginRunnerWebViewComponent);