2020-06-02 22:13:15 +02:00
import Async from 'react-async' ;
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' ) ;
2020-10-09 19:35:46 +02:00
const Setting = require ( 'lib/models/Setting' ) . default ;
2018-03-09 22:59:12 +02:00
const { reg } = require ( 'lib/registry.js' ) ;
2020-10-09 19:35:46 +02:00
const shim = require ( 'lib/shim' ) . default ;
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' ) ;
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
2020-04-08 19:40:32 +02:00
this . forceUpdate _ = false ;
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 ) ;
2020-04-08 19:40:32 +02:00
this . watchFn = this . watchFn . 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 ( ) {
2020-04-08 19:40:32 +02:00
this . forceUpdate _ = false ;
2017-07-30 21:51:18 +02:00
const note = this . props . note ;
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2019-03-08 19:14:17 +02:00
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 _ ) {
2020-10-09 19:35:46 +02:00
shim . clearTimeout ( this . resourceLoadedTimeoutId _ ) ;
2018-12-15 02:42:19 +02:00
this . resourceLoadedTimeoutId _ = null ;
}
2020-10-09 19:35:46 +02:00
this . resourceLoadedTimeoutId _ = shim . setTimeout ( ( ) => {
2018-12-15 02:42:19 +02:00
this . resourceLoadedTimeoutId _ = null ;
this . forceUpdate ( ) ;
} , 100 ) ;
2017-11-05 17:35:38 +02:00
} ,
2018-12-16 19:32:42 +02:00
highlightedKeywords : this . props . highlightedKeywords ,
2020-06-08 08:41:04 +02:00
resources : this . props . noteResources ,
2019-03-08 19:14:17 +02:00
codeTheme : theme . codeThemeCss ,
2020-06-08 08:41:04 +02:00
postMessageSyntax : 'window.joplinPostMessage_' ,
2017-11-05 17:35:38 +02:00
} ;
2020-03-23 02:47:25 +02:00
const result = await this . markupToHtml _ . render (
note . markup _language ,
bodyToRender ,
{
2020-04-08 02:22:18 +02:00
bodyPaddingTop : '.8em' , // Extra top padding on the rendered MD so it doesn't touch the border
bodyPaddingBottom : this . props . paddingBottom , // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
2020-03-23 02:47:25 +02:00
... 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' ) ) ;
2020-06-08 08:41:04 +02:00
// 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).
2020-06-08 08:43:09 +02:00
injectedJs . push ( 'window.joplinPostMessage_ = (msg, args) => { return window.ReactNativeWebView.postMessage(msg); };' ) ;
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 ( `
2020-10-09 19:35:46 +02:00
const readyStateCheckInterval = shim . setInterval ( function ( ) {
2019-05-22 16:56:07 +02:00
if ( document . readyState === "complete" ) {
2020-10-09 19:35:46 +02:00
shim . clearInterval ( readyStateCheckInterval ) ;
2019-05-22 16:56:07 +02:00
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 ) {
2020-10-09 19:35:46 +02:00
shim . setTimeout ( ( ) => {
2019-09-09 19:16:00 +02:00
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
< / h e a d >
< body >
2019-09-20 00:02:29 +02:00
$ { html }
2018-02-04 19:12:24 +02:00
< / b o d y >
< / h t m l >
` ;
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 ( ) {
2020-10-09 19:35:46 +02:00
shim . setTimeout ( ( ) => {
2019-12-29 19:58:40 +02:00
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.
2020-10-09 19:35:46 +02:00
shim . setTimeout ( ( ) => {
2019-12-29 19:58:40 +02:00
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 ( ) {
2020-04-08 19:40:32 +02:00
this . forceUpdate _ = true ;
2019-12-29 19:58:40 +02:00
this . forceUpdate ( ) ;
}
2017-11-20 21:01:19 +02:00
2020-04-08 19:40:32 +02:00
watchFn ( ) {
// react-async will not fetch the data again after the first render
// so we use this watchFn function to force it to reload in certain
// cases. It is used in particular when re-rendering the note when
// a resource has been downloaded in auto mode.
return this . forceUpdate _ ;
}
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 } >
2020-04-08 19:40:32 +02:00
< Async promiseFn = { this . reloadNote } watchFn = { this . watchFn } >
2019-12-29 19:58:40 +02:00
{ ( { data , error , isPending } ) => {
if ( error ) {
console . error ( error ) ;
return < Text > { error . message } < / T e x t > ;
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' ) }
2020-05-21 10:14:33 +02:00
onMessage = { event => {
2019-12-29 19:58:40 +02:00
// 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
< / A s y n c >
2017-07-30 21:51:18 +02:00
< / V i e w >
) ;
}
}
2019-07-29 15:43:53 +02:00
module . exports = { NoteBodyViewer } ;