2019-07-29 15:43:53 +02:00
|
|
|
const React = require('react');
|
|
|
|
const Component = React.Component;
|
2019-12-29 19:58:40 +02:00
|
|
|
const { Platform, View, Text } = require('react-native');
|
2019-07-29 15:43:53 +02:00
|
|
|
const { WebView } = require('react-native-webview');
|
2019-03-08 19:14:17 +02:00
|
|
|
const { themeStyle } = require('lib/components/global-style.js');
|
2018-03-09 22:59:12 +02:00
|
|
|
const Setting = require('lib/models/Setting.js');
|
|
|
|
const { reg } = require('lib/registry.js');
|
2019-03-08 19:14:17 +02:00
|
|
|
const { shim } = require('lib/shim');
|
2020-01-30 23:05:23 +02:00
|
|
|
const { assetsToHeaders } = require('lib/joplin-renderer');
|
2019-03-08 19:14:17 +02:00
|
|
|
const shared = require('lib/components/shared/note-screen-shared.js');
|
2019-12-29 19:58:40 +02:00
|
|
|
const markupLanguageUtils = require('lib/markupLanguageUtils');
|
|
|
|
|
|
|
|
import Async from 'react-async';
|
2017-07-30 21:51:18 +02:00
|
|
|
|
|
|
|
class NoteBodyViewer extends Component {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.state = {
|
|
|
|
resources: {},
|
2017-08-21 22:46:31 +02:00
|
|
|
webViewLoaded: false,
|
2019-12-29 19:58:40 +02:00
|
|
|
bodyHtml: '',
|
2019-07-29 15:43:53 +02:00
|
|
|
};
|
2017-08-21 22:46:31 +02:00
|
|
|
|
|
|
|
this.isMounted_ = false;
|
2019-12-29 19:58:40 +02:00
|
|
|
|
|
|
|
this.markupToHtml_ = markupLanguageUtils.newMarkupToHtml();
|
|
|
|
|
|
|
|
this.reloadNote = this.reloadNote.bind(this);
|
2017-07-30 21:51:18 +02:00
|
|
|
}
|
|
|
|
|
2019-12-29 19:58:40 +02:00
|
|
|
componentDidMount() {
|
2017-08-21 22:46:31 +02:00
|
|
|
this.isMounted_ = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
2019-12-29 19:58:40 +02:00
|
|
|
this.markupToHtml_ = null;
|
2017-08-21 22:46:31 +02:00
|
|
|
this.isMounted_ = false;
|
|
|
|
}
|
|
|
|
|
2019-12-29 19:58:40 +02:00
|
|
|
async reloadNote() {
|
2017-07-30 21:51:18 +02:00
|
|
|
const note = this.props.note;
|
2019-03-08 19:14:17 +02:00
|
|
|
const theme = themeStyle(this.props.theme);
|
|
|
|
|
|
|
|
const bodyToRender = note ? note.body : '';
|
2017-11-05 17:35:38 +02:00
|
|
|
|
|
|
|
const mdOptions = {
|
2017-11-06 20:05:12 +02:00
|
|
|
onResourceLoaded: () => {
|
2018-12-15 02:42:19 +02:00
|
|
|
if (this.resourceLoadedTimeoutId_) {
|
|
|
|
clearTimeout(this.resourceLoadedTimeoutId_);
|
|
|
|
this.resourceLoadedTimeoutId_ = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.resourceLoadedTimeoutId_ = setTimeout(() => {
|
|
|
|
this.resourceLoadedTimeoutId_ = null;
|
|
|
|
this.forceUpdate();
|
|
|
|
}, 100);
|
2017-11-05 17:35:38 +02:00
|
|
|
},
|
2018-03-09 22:59:12 +02:00
|
|
|
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
|
2018-12-16 19:32:42 +02:00
|
|
|
highlightedKeywords: this.props.highlightedKeywords,
|
2019-10-09 21:35:13 +02:00
|
|
|
resources: this.props.noteResources, // await shared.attachedResources(bodyToRender),
|
2019-03-08 19:14:17 +02:00
|
|
|
codeTheme: theme.codeThemeCss,
|
2019-06-14 09:11:15 +02:00
|
|
|
postMessageSyntax: 'window.ReactNativeWebView.postMessage',
|
2017-11-05 17:35:38 +02:00
|
|
|
};
|
|
|
|
|
2020-03-14 01:46:14 +02:00
|
|
|
const result = await this.markupToHtml_.render(note.markup_language, bodyToRender, this.props.webViewStyle, mdOptions);
|
2019-03-08 19:14:17 +02:00
|
|
|
let html = result.html;
|
|
|
|
|
2019-05-22 16:56:07 +02:00
|
|
|
const resourceDownloadMode = Setting.value('sync.resourceDownloadMode');
|
|
|
|
|
2019-12-29 19:58:40 +02:00
|
|
|
const injectedJs = [];
|
2019-03-08 19:14:17 +02:00
|
|
|
injectedJs.push(shim.injectedJs('webviewLib'));
|
2019-06-14 09:11:15 +02:00
|
|
|
injectedJs.push('webviewLib.initialize({ postMessage: msg => { return window.ReactNativeWebView.postMessage(msg); } });');
|
2019-05-22 16:56:07 +02:00
|
|
|
injectedJs.push(`
|
|
|
|
const readyStateCheckInterval = setInterval(function() {
|
|
|
|
if (document.readyState === "complete") {
|
|
|
|
clearInterval(readyStateCheckInterval);
|
|
|
|
if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload();
|
2019-09-09 19:16:00 +02:00
|
|
|
|
|
|
|
const hash = "${this.props.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);
|
|
|
|
}
|
2019-05-22 16:56:07 +02:00
|
|
|
}
|
|
|
|
}, 10);
|
|
|
|
`);
|
2019-02-28 01:38:50 +02:00
|
|
|
|
2019-07-29 15:43:53 +02:00
|
|
|
html =
|
|
|
|
`
|
2018-02-04 19:12:24 +02:00
|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
2019-06-14 10:14:01 +02:00
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
2020-02-08 13:11:04 +02:00
|
|
|
${assetsToHeaders(result.pluginAssets, { asHtml: true })}
|
2018-02-04 19:12:24 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
2019-09-20 00:02:29 +02:00
|
|
|
${html}
|
2018-02-04 19:12:24 +02:00
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
`;
|
2017-08-21 22:46:31 +02:00
|
|
|
|
2017-11-20 02:21:40 +02:00
|
|
|
// On iOS scalesPageToFit work like this:
|
|
|
|
//
|
|
|
|
// Find the widest image, resize it *and everything else* by x% so that
|
|
|
|
// the image fits within the viewport. The problem is that it means if there's
|
|
|
|
// a large image, everything is going to be scaled to a very small size, making
|
|
|
|
// the text unreadable.
|
|
|
|
//
|
|
|
|
// On Android:
|
|
|
|
//
|
|
|
|
// Find the widest elements and scale them (and them only) to fit within the viewport
|
|
|
|
// It means it's going to scale large images, but the text will remain at the normal
|
|
|
|
// size.
|
|
|
|
//
|
|
|
|
// That means we can use scalesPageToFix on Android but not on iOS.
|
|
|
|
// The weird thing is that on iOS, scalesPageToFix=false along with a CSS
|
|
|
|
// rule "img { max-width: 100% }", works like scalesPageToFix=true on Android.
|
|
|
|
// So we use scalesPageToFix=false on iOS along with that CSS rule.
|
|
|
|
|
2017-11-20 21:01:19 +02:00
|
|
|
// `baseUrl` is where the images will be loaded from. So images must use a path relative to resourceDir.
|
2019-12-29 19:58:40 +02:00
|
|
|
return {
|
|
|
|
source: {
|
|
|
|
html: html,
|
|
|
|
baseUrl: `file://${Setting.value('resourceDir')}/`,
|
|
|
|
},
|
|
|
|
injectedJs: injectedJs,
|
2017-11-21 21:47:29 +02:00
|
|
|
};
|
2019-12-29 19:58:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
onLoadEnd() {
|
|
|
|
setTimeout(() => {
|
|
|
|
if (this.props.onLoadEnd) this.props.onLoadEnd();
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
if (this.state.webViewLoaded) return;
|
|
|
|
|
|
|
|
// Need to display after a delay to avoid a white flash before
|
|
|
|
// the content is displayed.
|
|
|
|
setTimeout(() => {
|
|
|
|
if (!this.isMounted_) return;
|
|
|
|
this.setState({ webViewLoaded: true });
|
|
|
|
}, 100);
|
|
|
|
}
|
|
|
|
|
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
|
|
|
const safeGetNoteProp = (props, propName) => {
|
|
|
|
if (!props) return null;
|
|
|
|
if (!props.note) return null;
|
|
|
|
return props.note[propName];
|
|
|
|
};
|
|
|
|
|
|
|
|
// To address https://github.com/laurent22/joplin/issues/433
|
|
|
|
// If a checkbox in a note is ticked, the body changes, which normally would trigger a re-render
|
|
|
|
// of this component, which has the unfortunate side effect of making the view scroll back to the top.
|
|
|
|
// This re-rendering however is uncessary since the component is already visually updated via JS.
|
|
|
|
// So here, if the note has not changed, we prevent the component from updating.
|
|
|
|
// This fixes the above issue. A drawback of this is if the note is updated via sync, this change
|
|
|
|
// will not be displayed immediately.
|
|
|
|
const currentNoteId = safeGetNoteProp(this.props, 'id');
|
|
|
|
const nextNoteId = safeGetNoteProp(nextProps, 'id');
|
|
|
|
|
|
|
|
if (currentNoteId !== nextNoteId || nextState.webViewLoaded !== this.state.webViewLoaded) return true;
|
|
|
|
|
|
|
|
// If the length of the body has changed, then it's something other than a checkbox that has changed,
|
|
|
|
// for example a resource that has been attached to the note while in View mode. In that case, update.
|
|
|
|
return (`${safeGetNoteProp(this.props, 'body')}`).length !== (`${safeGetNoteProp(nextProps, 'body')}`).length;
|
|
|
|
}
|
|
|
|
|
|
|
|
rebuildMd() {
|
|
|
|
this.forceUpdate();
|
|
|
|
}
|
2017-11-20 21:01:19 +02:00
|
|
|
|
2019-12-29 19:58:40 +02:00
|
|
|
render() {
|
2019-06-14 09:11:15 +02:00
|
|
|
// Note: useWebKit={false} is needed to go around this bug:
|
|
|
|
// https://github.com/react-native-community/react-native-webview/issues/376
|
2019-06-14 10:14:01 +02:00
|
|
|
// However, if we add the <meta> tag as described there, it is no longer necessary and WebKit can be used!
|
|
|
|
// https://github.com/react-native-community/react-native-webview/issues/312#issuecomment-501991406
|
2019-06-20 00:16:37 +02:00
|
|
|
//
|
|
|
|
// However, on iOS, due to the bug below, we cannot use WebKit:
|
|
|
|
// https://github.com/react-native-community/react-native-webview/issues/312#issuecomment-503754654
|
2019-06-14 09:11:15 +02:00
|
|
|
|
2019-12-29 19:58:40 +02:00
|
|
|
|
2020-03-14 01:46:14 +02:00
|
|
|
const webViewStyle = { backgroundColor: this.props.webViewStyle.backgroundColor };
|
2019-12-29 19:58:40 +02:00
|
|
|
// On iOS, the onLoadEnd() event is never fired so always
|
|
|
|
// display the webview (don't do the little trick
|
|
|
|
// to avoid the white flash).
|
|
|
|
if (Platform.OS !== 'ios') {
|
|
|
|
webViewStyle.opacity = this.state.webViewLoaded ? 1 : 0.01;
|
|
|
|
}
|
|
|
|
|
2017-07-30 21:51:18 +02:00
|
|
|
return (
|
2019-12-29 19:58:40 +02:00
|
|
|
<View style={this.props.style}>
|
|
|
|
<Async promiseFn={this.reloadNote}>
|
|
|
|
{({ data, error, isPending }) => {
|
|
|
|
if (error) {
|
|
|
|
console.error(error);
|
|
|
|
return <Text>{error.message}</Text>;
|
2017-07-30 21:51:18 +02:00
|
|
|
}
|
2019-12-29 19:58:40 +02:00
|
|
|
|
|
|
|
if (isPending) return null;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<WebView
|
|
|
|
useWebKit={Platform.OS !== 'ios'}
|
|
|
|
style={webViewStyle}
|
|
|
|
source={data.source}
|
|
|
|
injectedJavaScript={data.injectedJs.join('\n')}
|
|
|
|
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
|
|
|
|
mixedContentMode="always"
|
|
|
|
allowFileAccess={true}
|
|
|
|
onLoadEnd={() => this.onLoadEnd()}
|
|
|
|
onError={() => reg.logger().error('WebView error')}
|
|
|
|
onMessage={event => {
|
|
|
|
// 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, this.props.note.body);
|
|
|
|
if (this.props.onCheckboxChange) this.props.onCheckboxChange(newBody);
|
|
|
|
} else if (msg.indexOf('markForDownload:') === 0) {
|
|
|
|
msg = msg.split(':');
|
|
|
|
const resourceId = msg[1];
|
|
|
|
if (this.props.onMarkForDownload) this.props.onMarkForDownload({ resourceId: resourceId });
|
|
|
|
} else {
|
|
|
|
this.props.onJoplinLinkClick(msg);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
2017-07-30 21:51:18 +02:00
|
|
|
}}
|
2019-12-29 19:58:40 +02:00
|
|
|
</Async>
|
2017-07-30 21:51:18 +02:00
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-29 15:43:53 +02:00
|
|
|
module.exports = { NoteBodyViewer };
|