mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-27 08:21:03 +02:00
Fixed webview issues
This commit is contained in:
parent
f537d22d7f
commit
1e0d2b7b86
@ -202,7 +202,11 @@ ReactNativeClient/lib/commands/historyForward.js
|
||||
ReactNativeClient/lib/commands/synchronize.js
|
||||
ReactNativeClient/lib/components/BackButtonDialogBox.js
|
||||
ReactNativeClient/lib/components/CameraView.js
|
||||
ReactNativeClient/lib/components/NoteBodyViewer.js
|
||||
ReactNativeClient/lib/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
ReactNativeClient/lib/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
ReactNativeClient/lib/components/NoteBodyViewer/hooks/useSource.js
|
||||
ReactNativeClient/lib/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
ReactNativeClient/lib/components/screens/Note.js
|
||||
ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js
|
||||
ReactNativeClient/lib/components/SelectDateTimeDialog.js
|
||||
ReactNativeClient/lib/errorUtils.js
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -196,7 +196,11 @@ ReactNativeClient/lib/commands/historyForward.js
|
||||
ReactNativeClient/lib/commands/synchronize.js
|
||||
ReactNativeClient/lib/components/BackButtonDialogBox.js
|
||||
ReactNativeClient/lib/components/CameraView.js
|
||||
ReactNativeClient/lib/components/NoteBodyViewer.js
|
||||
ReactNativeClient/lib/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
ReactNativeClient/lib/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
ReactNativeClient/lib/components/NoteBodyViewer/hooks/useSource.js
|
||||
ReactNativeClient/lib/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
ReactNativeClient/lib/components/screens/Note.js
|
||||
ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js
|
||||
ReactNativeClient/lib/components/SelectDateTimeDialog.js
|
||||
ReactNativeClient/lib/errorUtils.js
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
Image,
|
||||
} from 'react-native';
|
||||
import { renderFormatButtons } from './renderButtons';
|
||||
import NoteBodyViewer from 'lib/components/NoteBodyViewer';
|
||||
import NoteBodyViewer from 'lib/components/NoteBodyViewer/NoteBodyViewer';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
|
@ -698,6 +698,7 @@ export default class BaseApplication {
|
||||
initArgs = Object.assign(initArgs, extraFlags);
|
||||
|
||||
this.logger_.addTarget(TargetType.File, { path: `${profileDir}/log.txt` });
|
||||
// this.logger_.addTarget(TargetType.Console, { level: Logger.LEVEL_DEBUG });
|
||||
this.logger_.setLevel(initArgs.logLevel);
|
||||
|
||||
reg.setLogger(this.logger_);
|
||||
|
@ -1,3 +1,11 @@
|
||||
// On mobile all the setTimeout and setInterval should go through this class
|
||||
// as it will either use the native timeout/interval for short intervals or
|
||||
// the custom one for long intervals.
|
||||
|
||||
// For custom intervals, they are triggered
|
||||
// whenever the update() function is called, and in mobile it's called for
|
||||
// example on the Redux action middleware or when the app gets focus.
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
type IntervalId = number;
|
||||
|
@ -1,338 +0,0 @@
|
||||
import Setting from 'lib/models/Setting';
|
||||
import shim from 'lib/shim';
|
||||
|
||||
const Async = require('react-async').default;
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { Platform, View, Text, ToastAndroid } = require('react-native');
|
||||
const { WebView } = require('react-native-webview');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const BackButtonDialogBox = require('lib/components/BackButtonDialogBox').default;
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { assetsToHeaders } = require('lib/joplin-renderer');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const markupLanguageUtils = require('lib/markupLanguageUtils');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Share = require('react-native-share').default;
|
||||
|
||||
export default class NoteBodyViewer extends Component {
|
||||
|
||||
private forceUpdate_:boolean = false;
|
||||
private isMounted_:boolean = false;
|
||||
private markupToHtml_:any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
resources: {},
|
||||
webViewLoaded: false,
|
||||
bodyHtml: '',
|
||||
};
|
||||
|
||||
this.markupToHtml_ = markupLanguageUtils.newMarkupToHtml();
|
||||
|
||||
this.reloadNote = this.reloadNote.bind(this);
|
||||
this.watchFn = this.watchFn.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isMounted_ = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.markupToHtml_ = null;
|
||||
this.isMounted_ = false;
|
||||
}
|
||||
|
||||
async reloadNote() {
|
||||
this.forceUpdate_ = false;
|
||||
|
||||
const note = this.props.note;
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const bodyToRender = note ? note.body : '';
|
||||
|
||||
const mdOptions = {
|
||||
onResourceLoaded: () => {
|
||||
if (this.resourceLoadedTimeoutId_) {
|
||||
shim.clearTimeout(this.resourceLoadedTimeoutId_);
|
||||
this.resourceLoadedTimeoutId_ = null;
|
||||
}
|
||||
|
||||
this.resourceLoadedTimeoutId_ = shim.setTimeout(() => {
|
||||
this.resourceLoadedTimeoutId_ = null;
|
||||
this.forceUpdate();
|
||||
}, 100);
|
||||
},
|
||||
highlightedKeywords: this.props.highlightedKeywords,
|
||||
resources: this.props.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
|
||||
};
|
||||
|
||||
const result = await this.markupToHtml_.render(
|
||||
note.markup_language,
|
||||
bodyToRender,
|
||||
{
|
||||
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)
|
||||
...this.props.webViewStyle,
|
||||
},
|
||||
mdOptions
|
||||
);
|
||||
let html = result.html;
|
||||
|
||||
const resourceDownloadMode = Setting.value('sync.resourceDownloadMode');
|
||||
|
||||
const injectedJs = [];
|
||||
injectedJs.push('try {');
|
||||
injectedJs.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).
|
||||
injectedJs.push('window.joplinPostMessage_ = (msg, args) => { return window.ReactNativeWebView.postMessage(msg); };');
|
||||
injectedJs.push('webviewLib.initialize({ postMessage: msg => { return window.ReactNativeWebView.postMessage(msg); } });');
|
||||
injectedJs.push(`
|
||||
const readyStateCheckInterval = setInterval(function() {
|
||||
if (document.readyState === "complete") {
|
||||
clearInterval(readyStateCheckInterval);
|
||||
if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
`);
|
||||
injectedJs.push('} catch (e) {');
|
||||
injectedJs.push(' window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))');
|
||||
injectedJs.push(' true;');
|
||||
injectedJs.push('}');
|
||||
injectedJs.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');
|
||||
|
||||
// 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.
|
||||
|
||||
// `baseUrl` is where the images will be loaded from. So images must use a path relative to resourceDir.
|
||||
return {
|
||||
source: {
|
||||
// html: html,
|
||||
uri: 'file://' + tempFile,
|
||||
baseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
},
|
||||
injectedJs: injectedJs,
|
||||
};
|
||||
}
|
||||
|
||||
onLoadEnd() {
|
||||
shim.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.
|
||||
shim.setTimeout(() => {
|
||||
if (!this.isMounted_) return;
|
||||
this.setState({ webViewLoaded: true });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps:any, nextState:any) {
|
||||
const safeGetNoteProp = (props:any, propName:string) => {
|
||||
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_ = true;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
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_;
|
||||
}
|
||||
|
||||
async onResourceLongPress(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(this, name, [
|
||||
{ text: _('Open'), id: 'open' },
|
||||
{ text: _('Share'), id: 'share' },
|
||||
]);
|
||||
|
||||
if (action === 'open') {
|
||||
this.props.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);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// Note: useWebKit={false} is needed to go around this bug:
|
||||
// https://github.com/react-native-community/react-native-webview/issues/376
|
||||
// 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
|
||||
//
|
||||
// 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
|
||||
|
||||
|
||||
const webViewStyle:any = { backgroundColor: this.props.webViewStyle.backgroundColor };
|
||||
// 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;
|
||||
}
|
||||
|
||||
const useWebkit = true; //Platform.OS !== 'ios'
|
||||
|
||||
return (
|
||||
<View style={this.props.style}>
|
||||
<Async promiseFn={this.reloadNote} watchFn={this.watchFn}>
|
||||
{(args:any) => {
|
||||
const { data, error, isPending } = args;
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return <Text>{error.message}</Text>;
|
||||
}
|
||||
|
||||
if (isPending) return null;
|
||||
|
||||
return (
|
||||
<WebView
|
||||
useWebKit={useWebkit}
|
||||
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
|
||||
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: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, this.props.note.body);
|
||||
if (this.props.onCheckboxChange) this.props.onCheckboxChange(newBody);
|
||||
} else if (msg.indexOf('markForDownload:') === 0) {
|
||||
const splittedMsg = msg.split(':');
|
||||
const resourceId = splittedMsg[1];
|
||||
if (this.props.onMarkForDownload) this.props.onMarkForDownload({ resourceId: resourceId });
|
||||
} else if (msg.startsWith('longclick:')) {
|
||||
this.onResourceLongPress(msg);
|
||||
} else if (msg.startsWith('joplin:')) {
|
||||
this.props.onJoplinLinkClick(msg);
|
||||
} else if (msg.startsWith('error:')) {
|
||||
console.error('Webview injected script error: ' + msg);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Async>
|
||||
<BackButtonDialogBox
|
||||
ref={(dialogbox:any) => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
import { useRef, useMemo, useCallback } from 'react';
|
||||
|
||||
import Setting from 'lib/models/Setting';
|
||||
import useSource from './hooks/useSource';
|
||||
import useOnMessage from './hooks/useOnMessage';
|
||||
import useOnResourceLongPress from './hooks/useOnResourceLongPress';
|
||||
|
||||
const React = require('react');
|
||||
const { View } = require('react-native');
|
||||
const { WebView } = require('react-native-webview');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const BackButtonDialogBox = require('lib/components/BackButtonDialogBox').default;
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
interface Props {
|
||||
themeId: number,
|
||||
style: any,
|
||||
noteBody: string,
|
||||
noteMarkupLanguage: number,
|
||||
highlightedKeywords: string[],
|
||||
noteResources: any,
|
||||
paddingBottom: number,
|
||||
noteHash: string,
|
||||
onJoplinLinkClick: Function,
|
||||
onCheckboxChange?: Function,
|
||||
onMarkForDownload?: Function,
|
||||
onLoadEnd?: Function,
|
||||
}
|
||||
|
||||
export default function NoteBodyViewer(props:Props) {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const webViewStyle:any = useMemo(() => {
|
||||
return { backgroundColor: theme.backgroundColor };
|
||||
}, [theme.backgroundColor]);
|
||||
|
||||
const dialogBoxRef = useRef(null);
|
||||
|
||||
const { source, injectedJs } = useSource(
|
||||
props.noteBody,
|
||||
props.noteMarkupLanguage,
|
||||
props.themeId,
|
||||
props.highlightedKeywords,
|
||||
props.noteResources,
|
||||
props.paddingBottom,
|
||||
props.noteHash
|
||||
);
|
||||
|
||||
const onResourceLongPress = useOnResourceLongPress(
|
||||
props.onJoplinLinkClick,
|
||||
dialogBoxRef
|
||||
);
|
||||
|
||||
const onMessage = useOnMessage(
|
||||
props.onCheckboxChange,
|
||||
props.noteBody,
|
||||
props.onMarkForDownload,
|
||||
props.onJoplinLinkClick,
|
||||
onResourceLongPress
|
||||
);
|
||||
|
||||
const onLoadEnd = useCallback(() => {
|
||||
if (props.onLoadEnd) props.onLoadEnd();
|
||||
}, [props.onLoadEnd]);
|
||||
|
||||
function onError() {
|
||||
reg.logger().error('WebView error')
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 2020-10-15: As we've now fully switched to WebKit for iOS (useWebKit=true) and
|
||||
// since the WebView package went through many versions it's possible that
|
||||
// the above no longer applies.
|
||||
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<WebView
|
||||
theme={theme}
|
||||
useWebKit={true}
|
||||
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
|
||||
style={webViewStyle}
|
||||
source={source}
|
||||
injectedJavaScript={injectedJs.join('\n')}
|
||||
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
|
||||
mixedContentMode="always"
|
||||
allowFileAccess={true} // TODO: Implement logic to avoid race condition between source and allowFileAccess
|
||||
onLoadEnd={onLoadEnd}
|
||||
onError={onError}
|
||||
onMessage={onMessage}
|
||||
/>
|
||||
<BackButtonDialogBox ref={dialogBoxRef}/>
|
||||
</View>
|
||||
);
|
||||
}
|
@ -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]);
|
||||
}
|
@ -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]);
|
||||
}
|
@ -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 };
|
||||
}
|
@ -1,16 +1,18 @@
|
||||
import FileViewer from 'react-native-file-viewer';
|
||||
import AsyncActionQueue from '../../AsyncActionQueue';
|
||||
import UndoRedoService from 'lib/services/UndoRedoService';
|
||||
import uuid from 'lib/uuid';
|
||||
import Setting from 'lib/models/Setting';
|
||||
import shim from 'lib/shim';
|
||||
import NoteBodyViewer from 'lib/components/NoteBodyViewer/NoteBodyViewer';
|
||||
|
||||
const FileViewer = require('react-native-file-viewer').default;
|
||||
const React = require('react');
|
||||
const { Platform, Keyboard, View, TextInput, StyleSheet, Linking, Image, Share } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const uuid = require('lib/uuid').default;
|
||||
const { MarkdownEditor } = require('../../../MarkdownEditor/index.js');
|
||||
const RNFS = require('react-native-fs');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const UndoRedoService = require('lib/services/UndoRedoService.js').default;
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Setting = require('lib/models/Setting').default;
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Clipboard = require('@react-native-community/clipboard').default;
|
||||
@ -27,13 +29,11 @@ const { time } = require('lib/time-utils.js');
|
||||
const { Checkbox } = require('lib/components/checkbox.js');
|
||||
const { _ } = require('lib/locale');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const shim = require('lib/shim').default;
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { themeStyle, editorFont } = require('lib/components/global-style.js');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const NoteBodyViewer = require('lib/components/NoteBodyViewer').default;
|
||||
const DocumentPicker = require('react-native-document-picker').default;
|
||||
const ImageResizer = require('react-native-image-resizer').default;
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
@ -43,8 +43,10 @@ const ShareExtension = require('lib/ShareExtension.js').default;
|
||||
const CameraView = require('lib/components/CameraView').default;
|
||||
const urlUtils = require('lib/urlUtils');
|
||||
|
||||
const emptyArray:any[] = [];
|
||||
|
||||
class NoteScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
static navigationOptions():any {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
@ -152,7 +154,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ noteTagDialogShown: false });
|
||||
};
|
||||
|
||||
this.onJoplinLinkClick_ = async msg => {
|
||||
this.onJoplinLinkClick_ = async (msg:string) => {
|
||||
try {
|
||||
if (msg.indexOf('joplin://') === 0) {
|
||||
const resourceUrlInfo = urlUtils.parseResourceUrl(msg);
|
||||
@ -195,7 +197,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
};
|
||||
|
||||
this.refreshResource = async (resource, noteBody = null) => {
|
||||
this.refreshResource = async (resource:any, noteBody:string = null) => {
|
||||
if (noteBody === null && this.state.note && this.state.note.body) noteBody = this.state.note.body;
|
||||
if (noteBody === null) return;
|
||||
|
||||
@ -203,9 +205,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
if (resourceIds.indexOf(resource.id) >= 0) {
|
||||
shared.clearResourceCache();
|
||||
const attachedResources = await shared.attachedResources(noteBody);
|
||||
this.setState({ noteResources: attachedResources }, () => {
|
||||
if (this.refs.noteBodyViewer) this.refs.noteBodyViewer.rebuildMd();
|
||||
});
|
||||
this.setState({ noteResources: attachedResources });
|
||||
}
|
||||
};
|
||||
|
||||
@ -231,6 +231,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.screenHeader_undoButtonPress = this.screenHeader_undoButtonPress.bind(this);
|
||||
this.screenHeader_redoButtonPress = this.screenHeader_redoButtonPress.bind(this);
|
||||
this.body_selectionChange = this.body_selectionChange.bind(this);
|
||||
this.onBodyViewerLoadEnd = this.onBodyViewerLoadEnd.bind(this);
|
||||
this.onBodyViewerCheckboxChange = this.onBodyViewerCheckboxChange.bind(this);
|
||||
}
|
||||
|
||||
undoRedoService_stackChange() {
|
||||
@ -240,11 +242,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
} });
|
||||
}
|
||||
|
||||
async undoRedo(type) {
|
||||
async undoRedo(type:string) {
|
||||
const undoState = await this.undoRedoService_[type](this.undoState());
|
||||
if (!undoState) return;
|
||||
|
||||
this.setState((state) => {
|
||||
this.setState((state:any) => {
|
||||
const newNote = Object.assign({}, state.note);
|
||||
newNote.body = undoState.body;
|
||||
return {
|
||||
@ -271,7 +273,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.styles_ = {};
|
||||
|
||||
// TODO: Clean up these style names and nesting
|
||||
const styles = {
|
||||
const styles:any = {
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
@ -300,12 +302,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
},
|
||||
noteBodyViewerPreview: {
|
||||
borderTopColor: theme.dividerColor,
|
||||
borderTopWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
checkbox: {
|
||||
color: theme.color,
|
||||
paddingRight: 10,
|
||||
@ -319,6 +315,14 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
},
|
||||
};
|
||||
|
||||
styles.noteBodyViewerPreview = {
|
||||
...styles.noteBodyViewer,
|
||||
borderTopColor: theme.dividerColor,
|
||||
borderTopWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
borderBottomWidth: 1,
|
||||
}
|
||||
|
||||
styles.titleContainer = {
|
||||
flex: 0,
|
||||
flexDirection: 'row',
|
||||
@ -354,7 +358,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return shared.isModified(this);
|
||||
}
|
||||
|
||||
undoState(noteBody = null) {
|
||||
undoState(noteBody:string = null) {
|
||||
return {
|
||||
body: noteBody === null ? this.state.note.body : noteBody,
|
||||
};
|
||||
@ -378,11 +382,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
onMarkForDownload(event) {
|
||||
onMarkForDownload(event:any) {
|
||||
ResourceFetcher.instance().markForDownload(event.resourceId);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps:any) {
|
||||
if (this.doFocusUpdate_) {
|
||||
this.doFocusUpdate_ = false;
|
||||
this.focusUpdate();
|
||||
@ -413,13 +417,13 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
if (this.undoRedoService_) this.undoRedoService_.off('stackChange', this.undoRedoService_stackChange);
|
||||
}
|
||||
|
||||
title_changeText(text) {
|
||||
title_changeText(text:string) {
|
||||
shared.noteComponent_change(this, 'title', text);
|
||||
this.setState({ newAndNoTitleChangeNoteId: null });
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
body_changeText(text) {
|
||||
body_changeText(text:string) {
|
||||
if (!this.undoRedoService_.canUndo) {
|
||||
this.undoRedoService_.push(this.undoState());
|
||||
} else {
|
||||
@ -429,7 +433,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
body_selectionChange(event) {
|
||||
body_selectionChange(event:any) {
|
||||
this.selection = event.nativeEvent.selection;
|
||||
}
|
||||
|
||||
@ -439,7 +443,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
};
|
||||
}
|
||||
|
||||
saveActionQueue(noteId) {
|
||||
saveActionQueue(noteId:string) {
|
||||
if (!this.saveActionQueues_[noteId]) {
|
||||
this.saveActionQueues_[noteId] = new AsyncActionQueue(500);
|
||||
}
|
||||
@ -450,13 +454,13 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.saveActionQueue(this.state.note.id).push(this.makeSaveAction());
|
||||
}
|
||||
|
||||
async saveNoteButton_press(folderId = null) {
|
||||
async saveNoteButton_press(folderId:string = null) {
|
||||
await shared.saveNoteButton_press(this, folderId);
|
||||
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
|
||||
async saveOneProperty(name, value) {
|
||||
async saveOneProperty(name:string, value:any) {
|
||||
await shared.saveOneProperty(this, name, value);
|
||||
}
|
||||
|
||||
@ -492,32 +496,32 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
async imageDimensions(uri) {
|
||||
async imageDimensions(uri:string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Image.getSize(
|
||||
uri,
|
||||
(width, height) => {
|
||||
(width:number, height:number) => {
|
||||
resolve({ width: width, height: height });
|
||||
},
|
||||
error => {
|
||||
(error:any) => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
showImagePicker(options) {
|
||||
showImagePicker(options:any) {
|
||||
return new Promise((resolve) => {
|
||||
ImagePicker.launchImageLibrary(options, response => {
|
||||
ImagePicker.launchImageLibrary(options, (response:any) => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async resizeImage(localFilePath, targetPath, mimeType) {
|
||||
async resizeImage(localFilePath:string, targetPath:string, mimeType:string) {
|
||||
const maxSize = Resource.IMAGE_MAX_DIMENSION;
|
||||
|
||||
const dimensions = await this.imageDimensions(localFilePath);
|
||||
const dimensions:any = await this.imageDimensions(localFilePath);
|
||||
|
||||
reg.logger().info('Original dimensions ', dimensions);
|
||||
|
||||
@ -563,7 +567,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return true;
|
||||
}
|
||||
|
||||
async attachFile(pickerResponse, fileType) {
|
||||
async attachFile(pickerResponse:any, fileType:string) {
|
||||
if (!pickerResponse) {
|
||||
// User has cancelled
|
||||
return;
|
||||
@ -673,7 +677,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ showCamera: true });
|
||||
}
|
||||
|
||||
cameraView_onPhoto(data) {
|
||||
cameraView_onPhoto(data:any) {
|
||||
this.attachFile(
|
||||
{
|
||||
uri: data.uri,
|
||||
@ -723,7 +727,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ alarmDialogShown: true });
|
||||
}
|
||||
|
||||
async onAlarmDialogAccept(date) {
|
||||
async onAlarmDialogAccept(date:Date) {
|
||||
const newNote = Object.assign({}, this.state.note);
|
||||
newNote.todo_due = date ? date.getTime() : 0;
|
||||
|
||||
@ -899,11 +903,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return output;
|
||||
}
|
||||
|
||||
async todoCheckbox_change(checked) {
|
||||
async todoCheckbox_change(checked:boolean) {
|
||||
await this.saveOneProperty('todo_completed', checked ? time.unixMs() : 0);
|
||||
}
|
||||
|
||||
titleTextInput_contentSizeChange(event) {
|
||||
titleTextInput_contentSizeChange(event:any) {
|
||||
if (!this.enableMultilineTitle_) return;
|
||||
|
||||
const height = event.nativeEvent.contentSize.height;
|
||||
@ -937,7 +941,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
async folderPickerOptions_valueChanged(itemValue) {
|
||||
async folderPickerOptions_valueChanged(itemValue:any) {
|
||||
const note = this.state.note;
|
||||
const isProvisionalNote = this.props.provisionalNoteIds.includes(note.id);
|
||||
|
||||
@ -971,6 +975,19 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return this.folderPickerOptions_;
|
||||
}
|
||||
|
||||
onBodyViewerLoadEnd() {
|
||||
shim.setTimeout(() => {
|
||||
this.setState({ HACK_webviewLoadingState: 1 });
|
||||
shim.setTimeout(() => {
|
||||
this.setState({ HACK_webviewLoadingState: 0 });
|
||||
}, 50);
|
||||
}, 5);
|
||||
}
|
||||
|
||||
onBodyViewerCheckboxChange(newBody:string) {
|
||||
this.saveOneProperty('body', newBody);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.isLoading) {
|
||||
return (
|
||||
@ -988,62 +1005,33 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return <CameraView themeId={this.props.themeId} style={{ flex: 1 }} onPhoto={this.cameraView_onPhoto} onCancel={this.cameraView_onCancel} />;
|
||||
}
|
||||
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
const keywords = this.props.searchQuery && !!this.props.ftsEnabled ? this.props.highlightedWords : emptyArray;
|
||||
|
||||
let bodyComponent = null;
|
||||
if (this.state.mode == 'view' && !this.useBetaEditor()) {
|
||||
const onCheckboxChange = newBody => {
|
||||
this.saveOneProperty('body', newBody);
|
||||
};
|
||||
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
let keywords = [];
|
||||
if (this.props.searchQuery && !!this.props.ftsEnabled) {
|
||||
keywords = this.props.highlightedWords;
|
||||
}
|
||||
|
||||
// Note: as of 2018-12-29 it's important not to display the viewer if the note body is empty,
|
||||
// to avoid the HACK_webviewLoadingState related bug.
|
||||
bodyComponent =
|
||||
!note || !note.body.trim() ? null : (
|
||||
<NoteBodyViewer
|
||||
onJoplinLinkClick={this.onJoplinLinkClick_}
|
||||
ref="noteBodyViewer"
|
||||
style={this.styles().noteBodyViewer}
|
||||
webViewStyle={theme}
|
||||
// Extra bottom padding to make it possible to scroll past the
|
||||
// action button (so that it doesn't overlap the text)
|
||||
paddingBottom="150"
|
||||
note={note}
|
||||
paddingBottom={150}
|
||||
noteBody={note.body}
|
||||
noteMarkupLanguage={note.markup_language}
|
||||
noteResources={this.state.noteResources}
|
||||
highlightedKeywords={keywords}
|
||||
themeId={this.props.themeId}
|
||||
noteHash={this.props.noteHash}
|
||||
onCheckboxChange={newBody => {
|
||||
onCheckboxChange(newBody);
|
||||
}}
|
||||
onCheckboxChange={this.onBodyViewerCheckboxChange}
|
||||
onMarkForDownload={this.onMarkForDownload}
|
||||
onLoadEnd={() => {
|
||||
shim.setTimeout(() => {
|
||||
this.setState({ HACK_webviewLoadingState: 1 });
|
||||
shim.setTimeout(() => {
|
||||
this.setState({ HACK_webviewLoadingState: 0 });
|
||||
}, 50);
|
||||
}, 5);
|
||||
}}
|
||||
onLoadEnd={this.onBodyViewerLoadEnd}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// autoFocus={fieldToFocus === 'body'}
|
||||
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
let keywords = [];
|
||||
if (this.props.searchQuery && !!this.props.ftsEnabled) {
|
||||
keywords = this.props.highlightedWords;
|
||||
}
|
||||
|
||||
const onCheckboxChange = newBody => {
|
||||
this.saveOneProperty('body', newBody);
|
||||
};
|
||||
|
||||
bodyComponent = this.useBetaEditor()
|
||||
// Note: blurOnSubmit is necessary to get multiline to work.
|
||||
// See https://github.com/facebook/react-native/issues/12717#issuecomment-327001997
|
||||
@ -1055,7 +1043,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
value={note.body}
|
||||
borderColor={this.styles().markdownButtons.borderColor}
|
||||
markdownButtonsColor={this.styles().markdownButtons.color}
|
||||
saveText={text => this.body_changeText(text)}
|
||||
saveText={(text:string) => this.body_changeText(text)}
|
||||
blurOnSubmit={false}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
@ -1063,29 +1051,18 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
placeholderTextColor={theme.colorFaded}
|
||||
noteBodyViewer={{
|
||||
onJoplinLinkClick: this.onJoplinLinkClick_,
|
||||
ref: 'noteBodyViewer',
|
||||
style: {
|
||||
...this.styles().noteBodyViewer,
|
||||
...this.styles().noteBodyViewerPreview,
|
||||
},
|
||||
style: this.styles().noteBodyViewerPreview,
|
||||
paddingBottom: 0,
|
||||
webViewStyle: theme,
|
||||
note: note,
|
||||
noteBody: note.body,
|
||||
noteMarkupLanguage: note.markup_language,
|
||||
noteResources: this.state.noteResources,
|
||||
highlightedKeywords: keywords,
|
||||
themeId: this.props.themeId,
|
||||
noteHash: this.props.noteHash,
|
||||
onCheckboxChange: newBody => {
|
||||
onCheckboxChange(newBody);
|
||||
},
|
||||
onCheckboxChange: this.onBodyViewerCheckboxChange,
|
||||
onMarkForDownload: this.onMarkForDownload,
|
||||
onLoadEnd: () => {
|
||||
shim.setTimeout(() => {
|
||||
this.setState({ HACK_webviewLoadingState: 1 });
|
||||
shim.setTimeout(() => {
|
||||
this.setState({ HACK_webviewLoadingState: 0 });
|
||||
}, 50);
|
||||
}, 5);
|
||||
},
|
||||
onLoadEnd: this.onBodyViewerLoadEnd,
|
||||
}}
|
||||
|
||||
/>
|
||||
@ -1112,7 +1089,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
ref="noteBodyTextField"
|
||||
multiline={true}
|
||||
value={note.body}
|
||||
onChangeText={(text) => this.body_changeText(text)}
|
||||
onChangeText={(text:string) => this.body_changeText(text)}
|
||||
onSelectionChange={this.body_selectionChange}
|
||||
blurOnSubmit={false}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
@ -1198,7 +1175,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
<SelectDateTimeDialog themeId={this.props.themeId} shown={this.state.alarmDialogShown} date={dueDate} onAccept={this.onAlarmDialogAccept} onReject={this.onAlarmDialogReject} />
|
||||
|
||||
<DialogBox
|
||||
ref={dialogbox => {
|
||||
ref={(dialogbox:any) => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
@ -1208,7 +1185,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const NoteScreen = connect(state => {
|
||||
const NoteScreen = connect((state:any) => {
|
||||
return {
|
||||
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||
noteHash: state.selectedNoteHash,
|
||||
@ -1226,4 +1203,4 @@ const NoteScreen = connect(state => {
|
||||
};
|
||||
})(NoteScreenComponent);
|
||||
|
||||
module.exports = { NoteScreen };
|
||||
export default NoteScreen;
|
@ -1,6 +1,10 @@
|
||||
// Use this to show which props have been changed within a component.
|
||||
//
|
||||
// Usage: usePropsDebugger(props);
|
||||
|
||||
import useEffectDebugger from './useEffectDebugger';
|
||||
|
||||
export default function usePropsDebugger(effectHook:any, props:any) {
|
||||
export default function usePropsDebugger(props:any) {
|
||||
const dependencies:any[] = [];
|
||||
const dependencyNames:string[] = [];
|
||||
|
||||
@ -9,5 +13,5 @@ export default function usePropsDebugger(effectHook:any, props:any) {
|
||||
dependencyNames.push(k);
|
||||
}
|
||||
|
||||
useEffectDebugger(effectHook, dependencies, dependencyNames);
|
||||
useEffectDebugger(() => {}, dependencies, dependencyNames);
|
||||
}
|
||||
|
@ -63,6 +63,11 @@ class MarkupToHtml {
|
||||
return output;
|
||||
}
|
||||
|
||||
clearCache(markupLanguage) {
|
||||
const r = this.renderer(markupLanguage);
|
||||
if (r.clearCache) r.clearCache();
|
||||
}
|
||||
|
||||
async render(markupLanguage, markup, theme, options) {
|
||||
return this.renderer(markupLanguage).render(markup, theme, options);
|
||||
}
|
||||
|
@ -178,7 +178,11 @@ class MdToHtml {
|
||||
return html.substring(3, html.length - 5);
|
||||
}
|
||||
|
||||
// "style" here is really the theme, as returned by themeStyle()
|
||||
clearCache() {
|
||||
this.cachedOutputs_ = {};
|
||||
}
|
||||
|
||||
// "theme" is the theme as returned by themeStyle()
|
||||
async render(body, theme = null, options = null) {
|
||||
options = Object.assign({}, {
|
||||
// In bodyOnly mode, the rendered Markdown is returned without the wrapper DIV
|
||||
|
@ -127,7 +127,7 @@ export default class ResourceEditWatcher {
|
||||
// handle and once in the "raw" event handler, due to a bug in chokidar. So having
|
||||
// this check means we don't unecessarily save the resource twice when the file is
|
||||
// modified by the user.
|
||||
this.logger().debug(`ResourceEditWatcher: No timestamp change - skip: ${resourceId}`);
|
||||
this.logger().debug(`ResourceEditWatcher: No timestamp and file size change - skip: ${resourceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { NotesScreen } = require('lib/components/screens/notes.js');
|
||||
const { TagsScreen } = require('lib/components/screens/tags.js');
|
||||
const { NoteScreen } = require('lib/components/screens/note.js');
|
||||
const NoteScreen = require('lib/components/screens/Note').default;
|
||||
const { ConfigScreen } = require('lib/components/screens/config.js');
|
||||
const { FolderScreen } = require('lib/components/screens/folder.js');
|
||||
const { LogScreen } = require('lib/components/screens/log.js');
|
||||
|
@ -706,7 +706,12 @@
|
||||
"ReactNativeClient/lib/components/CameraView.js": true,
|
||||
"ReactNativeClient/lib/components/NoteBodyViewer.js": true,
|
||||
"CliClient/tests/InMemoryCache.js": true,
|
||||
"ReactNativeClient/lib/InMemoryCache.js": true
|
||||
"ReactNativeClient/lib/InMemoryCache.js": true,
|
||||
"ReactNativeClient/lib/components/NoteBodyViewer/hooks/useOnMessage.js": true,
|
||||
"ReactNativeClient/lib/components/NoteBodyViewer/hooks/useOnResourceLongPress.js": true,
|
||||
"ReactNativeClient/lib/components/NoteBodyViewer/hooks/useSource.js": true,
|
||||
"ReactNativeClient/lib/components/NoteBodyViewer/NoteBodyViewer.js": true,
|
||||
"ReactNativeClient/lib/components/screens/Note.js": true
|
||||
},
|
||||
"spellright.language": [
|
||||
"en"
|
||||
|
Loading…
Reference in New Issue
Block a user