1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-06-27 23:28:38 +02:00

Fixed webview issues

This commit is contained in:
Laurent Cozic
2020-10-16 11:56:21 +01:00
parent f537d22d7f
commit 1e0d2b7b86
17 changed files with 460 additions and 449 deletions

View File

@ -0,0 +1,26 @@
import { useCallback } from 'react';
const shared = require('lib/components/shared/note-screen-shared');
export default function useOnMessage(onCheckboxChange:Function, noteBody:string, onMarkForDownload:Function, onJoplinLinkClick:Function, onResourceLongPress:Function) {
return useCallback((event:any) => {
// Since RN 58 (or 59) messages are now escaped twice???
let msg = unescape(unescape(event.nativeEvent.data));
console.info('Got IPC message: ', msg);
if (msg.indexOf('checkboxclick:') === 0) {
const newBody = shared.toggleCheckbox(msg, noteBody);
if (onCheckboxChange) onCheckboxChange(newBody);
} else if (msg.indexOf('markForDownload:') === 0) {
const splittedMsg = msg.split(':');
const resourceId = splittedMsg[1];
if (onMarkForDownload) onMarkForDownload({ resourceId: resourceId });
} else if (msg.startsWith('longclick:')) {
onResourceLongPress(msg);
} else if (msg.startsWith('joplin:')) {
onJoplinLinkClick(msg);
} else if (msg.startsWith('error:')) {
console.error('Webview injected script error: ' + msg);
}
}, [onCheckboxChange, noteBody, onMarkForDownload, onJoplinLinkClick, onResourceLongPress]);
}

View File

@ -0,0 +1,48 @@
import { useCallback } from 'react';
import Setting from 'lib/models/Setting';
import shim from 'lib/shim';
const { ToastAndroid } = require('react-native');
const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js');
const { dialogs } = require('lib/dialogs.js');
const Resource = require('lib/models/Resource.js');
const Share = require('react-native-share').default;
export default function onResourceLongPress(onJoplinLinkClick:Function, dialogBoxRef:any) {
return useCallback(async (msg:string) => {
try {
const resourceId = msg.split(':')[1];
const resource = await Resource.load(resourceId);
const name = resource.title ? resource.title : resource.file_name;
const action = await dialogs.pop({ dialogbox: dialogBoxRef.current }, name, [
{ text: _('Open'), id: 'open' },
{ text: _('Share'), id: 'share' },
]);
if (action === 'open') {
onJoplinLinkClick(`joplin://${resourceId}`);
} else if (action === 'share') {
const filename = resource.file_name ?
`${resource.file_name}.${resource.file_extension}` :
resource.title;
const targetPath = `${Setting.value('resourceDir')}/${filename}`;
await shim.fsDriver().copy(Resource.fullPath(resource), targetPath);
await Share.open({
type: resource.mime,
filename: resource.title,
url: `file://${targetPath}`,
failOnCancel: false,
});
await shim.fsDriver().remove(targetPath);
}
} catch (e) {
reg.logger().error('Could not handle link long press', e);
ToastAndroid.show('An error occurred, check log for details', ToastAndroid.SHORT);
}
}, [onJoplinLinkClick]);
}

View File

@ -0,0 +1,152 @@
import { useEffect, useState, useMemo } from 'react';
import shim from 'lib/shim';
import Setting from 'lib/models/Setting';
const { themeStyle } = require('lib/components/global-style.js');
const markupLanguageUtils = require('lib/markupLanguageUtils');
const { assetsToHeaders } = require('lib/joplin-renderer');
interface Source {
uri: string,
baseUrl: string,
}
interface UseSourceResult {
source: Source,
injectedJs: string[],
}
let markupToHtml_:any = null;
function markupToHtml() {
if (markupToHtml_) return markupToHtml_;
markupToHtml_ = markupLanguageUtils.newMarkupToHtml();
return markupToHtml_;
}
export default function useSource(noteBody:string, noteMarkupLanguage:number, themeId:number, highlightedKeywords:string[], noteResources:any, paddingBottom:number, noteHash:string):UseSourceResult {
const [source, setSource] = useState<Source>(undefined);
const [injectedJs, setInjectedJs] = useState<string[]>([]);
const [resourceLoadedTime, setResourceLoadedTime] = useState(0);
const rendererTheme = useMemo(() => {
return {
bodyPaddingTop: '.8em', // Extra top padding on the rendered MD so it doesn't touch the border
bodyPaddingBottom: paddingBottom, // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
...themeStyle(themeId),
};
}, [themeId, paddingBottom]);
useEffect(() => {
let cancelled = false;
async function renderNote() {
const theme = themeStyle(themeId);
const bodyToRender = noteBody || '';
const mdOptions = {
onResourceLoaded: () => {
setResourceLoadedTime(Date.now());
},
highlightedKeywords: highlightedKeywords,
resources: noteResources,
codeTheme: theme.codeThemeCss,
postMessageSyntax: 'window.joplinPostMessage_',
enableLongPress: shim.mobilePlatform() === 'android', // On iOS, there's already a built-on open/share menu
longPressDelay: 500, // TODO use system value
};
// Whenever a resource state changes, for example when it goes from "not downloaded" to "downloaded", the "noteResources"
// props changes, thus triggering a render. The **content** of this noteResources array however is not changed because
// it doesn't contain info about the resource download state. Because of that, if we were to use the markupToHtml() cache
// it wouldn't re-render at all. We don't need this cache in any way because this hook is only triggered when we know
// something has changed.
markupToHtml().clearCache(noteMarkupLanguage);
const result = await markupToHtml().render(
noteMarkupLanguage,
bodyToRender,
rendererTheme,
mdOptions
);
if (cancelled) return;
let html = result.html;
const resourceDownloadMode = Setting.value('sync.resourceDownloadMode');
const js = [];
js.push('try {');
js.push(shim.injectedJs('webviewLib'));
// Note that this postMessage function accepts two arguments, for compatibility with the desktop version, but
// the ReactNativeWebView actually supports only one, so the second arg is ignored (and currently not needed for the mobile app).
js.push('window.joplinPostMessage_ = (msg, args) => { return window.ReactNativeWebView.postMessage(msg); };');
js.push('webviewLib.initialize({ postMessage: msg => { return window.ReactNativeWebView.postMessage(msg); } });');
js.push(`
const readyStateCheckInterval = setInterval(function() {
if (document.readyState === "complete") {
clearInterval(readyStateCheckInterval);
if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload();
const hash = "${noteHash}";
// Gives it a bit of time before scrolling to the anchor
// so that images are loaded.
if (hash) {
setTimeout(() => {
const e = document.getElementById(hash);
if (!e) {
console.warn('Cannot find hash', hash);
return;
}
e.scrollIntoView();
}, 500);
}
}
}, 10);
`);
js.push('} catch (e) {');
js.push(' window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))');
js.push(' true;');
js.push('}');
js.push('true;');
html =
`
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
${assetsToHeaders(result.pluginAssets, { asHtml: true })}
</head>
<body>
${html}
</body>
</html>
`;
const tempFile = `${Setting.value('resourceDir')}/NoteBodyViewer.html`
await shim.fsDriver().writeFile(tempFile, html, 'utf8');
if (cancelled) return;
// Now that we are sending back a file instead of an HTML string, we're always sending back the
// same file. So we add a cache busting query parameter to it, to make sure that the WebView re-renders.
//
// `baseUrl` is where the images will be loaded from. So images must use a path relative to resourceDir.
setSource({
uri: 'file://' + tempFile + '?r=' + Math.round(Math.random() * 100000000),
baseUrl: `file://${Setting.value('resourceDir')}/`,
});
setInjectedJs(js);
}
renderNote();
return () => {
cancelled = true;
}
}, [resourceLoadedTime, noteBody, noteMarkupLanguage, themeId, rendererTheme, highlightedKeywords, noteResources, noteHash]);
return { source, injectedJs };
}