You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +02:00
Fixed webview issues
This commit is contained in:
@ -202,7 +202,11 @@ ReactNativeClient/lib/commands/historyForward.js
|
|||||||
ReactNativeClient/lib/commands/synchronize.js
|
ReactNativeClient/lib/commands/synchronize.js
|
||||||
ReactNativeClient/lib/components/BackButtonDialogBox.js
|
ReactNativeClient/lib/components/BackButtonDialogBox.js
|
||||||
ReactNativeClient/lib/components/CameraView.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/screens/UpgradeSyncTargetScreen.js
|
||||||
ReactNativeClient/lib/components/SelectDateTimeDialog.js
|
ReactNativeClient/lib/components/SelectDateTimeDialog.js
|
||||||
ReactNativeClient/lib/errorUtils.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/commands/synchronize.js
|
||||||
ReactNativeClient/lib/components/BackButtonDialogBox.js
|
ReactNativeClient/lib/components/BackButtonDialogBox.js
|
||||||
ReactNativeClient/lib/components/CameraView.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/screens/UpgradeSyncTargetScreen.js
|
||||||
ReactNativeClient/lib/components/SelectDateTimeDialog.js
|
ReactNativeClient/lib/components/SelectDateTimeDialog.js
|
||||||
ReactNativeClient/lib/errorUtils.js
|
ReactNativeClient/lib/errorUtils.js
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { renderFormatButtons } from './renderButtons';
|
import { renderFormatButtons } from './renderButtons';
|
||||||
import NoteBodyViewer from 'lib/components/NoteBodyViewer';
|
import NoteBodyViewer from 'lib/components/NoteBodyViewer/NoteBodyViewer';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
buttonContainer: {
|
buttonContainer: {
|
||||||
|
@ -698,6 +698,7 @@ export default class BaseApplication {
|
|||||||
initArgs = Object.assign(initArgs, extraFlags);
|
initArgs = Object.assign(initArgs, extraFlags);
|
||||||
|
|
||||||
this.logger_.addTarget(TargetType.File, { path: `${profileDir}/log.txt` });
|
this.logger_.addTarget(TargetType.File, { path: `${profileDir}/log.txt` });
|
||||||
|
// this.logger_.addTarget(TargetType.Console, { level: Logger.LEVEL_DEBUG });
|
||||||
this.logger_.setLevel(initArgs.logLevel);
|
this.logger_.setLevel(initArgs.logLevel);
|
||||||
|
|
||||||
reg.setLogger(this.logger_);
|
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');
|
const { time } = require('lib/time-utils.js');
|
||||||
|
|
||||||
type IntervalId = number;
|
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 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 React = require('react');
|
||||||
const { Platform, Keyboard, View, TextInput, StyleSheet, Linking, Image, Share } = require('react-native');
|
const { Platform, Keyboard, View, TextInput, StyleSheet, Linking, Image, Share } = require('react-native');
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const uuid = require('lib/uuid').default;
|
|
||||||
const { MarkdownEditor } = require('../../../MarkdownEditor/index.js');
|
const { MarkdownEditor } = require('../../../MarkdownEditor/index.js');
|
||||||
const RNFS = require('react-native-fs');
|
const RNFS = require('react-native-fs');
|
||||||
const Note = require('lib/models/Note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const UndoRedoService = require('lib/services/UndoRedoService.js').default;
|
|
||||||
const BaseItem = require('lib/models/BaseItem.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
const Setting = require('lib/models/Setting').default;
|
|
||||||
const Resource = require('lib/models/Resource.js');
|
const Resource = require('lib/models/Resource.js');
|
||||||
const Folder = require('lib/models/Folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const Clipboard = require('@react-native-community/clipboard').default;
|
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 { Checkbox } = require('lib/components/checkbox.js');
|
||||||
const { _ } = require('lib/locale');
|
const { _ } = require('lib/locale');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
const shim = require('lib/shim').default;
|
|
||||||
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||||
const { themeStyle, editorFont } = require('lib/components/global-style.js');
|
const { themeStyle, editorFont } = require('lib/components/global-style.js');
|
||||||
const { dialogs } = require('lib/dialogs.js');
|
const { dialogs } = require('lib/dialogs.js');
|
||||||
const DialogBox = require('react-native-dialogbox').default;
|
const DialogBox = require('react-native-dialogbox').default;
|
||||||
const NoteBodyViewer = require('lib/components/NoteBodyViewer').default;
|
|
||||||
const DocumentPicker = require('react-native-document-picker').default;
|
const DocumentPicker = require('react-native-document-picker').default;
|
||||||
const ImageResizer = require('react-native-image-resizer').default;
|
const ImageResizer = require('react-native-image-resizer').default;
|
||||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
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 CameraView = require('lib/components/CameraView').default;
|
||||||
const urlUtils = require('lib/urlUtils');
|
const urlUtils = require('lib/urlUtils');
|
||||||
|
|
||||||
|
const emptyArray:any[] = [];
|
||||||
|
|
||||||
class NoteScreenComponent extends BaseScreenComponent {
|
class NoteScreenComponent extends BaseScreenComponent {
|
||||||
static navigationOptions() {
|
static navigationOptions():any {
|
||||||
return { header: null };
|
return { header: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +154,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
this.setState({ noteTagDialogShown: false });
|
this.setState({ noteTagDialogShown: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onJoplinLinkClick_ = async msg => {
|
this.onJoplinLinkClick_ = async (msg:string) => {
|
||||||
try {
|
try {
|
||||||
if (msg.indexOf('joplin://') === 0) {
|
if (msg.indexOf('joplin://') === 0) {
|
||||||
const resourceUrlInfo = urlUtils.parseResourceUrl(msg);
|
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 && this.state.note && this.state.note.body) noteBody = this.state.note.body;
|
||||||
if (noteBody === null) return;
|
if (noteBody === null) return;
|
||||||
|
|
||||||
@ -203,9 +205,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
if (resourceIds.indexOf(resource.id) >= 0) {
|
if (resourceIds.indexOf(resource.id) >= 0) {
|
||||||
shared.clearResourceCache();
|
shared.clearResourceCache();
|
||||||
const attachedResources = await shared.attachedResources(noteBody);
|
const attachedResources = await shared.attachedResources(noteBody);
|
||||||
this.setState({ noteResources: attachedResources }, () => {
|
this.setState({ noteResources: attachedResources });
|
||||||
if (this.refs.noteBodyViewer) this.refs.noteBodyViewer.rebuildMd();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -231,6 +231,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
this.screenHeader_undoButtonPress = this.screenHeader_undoButtonPress.bind(this);
|
this.screenHeader_undoButtonPress = this.screenHeader_undoButtonPress.bind(this);
|
||||||
this.screenHeader_redoButtonPress = this.screenHeader_redoButtonPress.bind(this);
|
this.screenHeader_redoButtonPress = this.screenHeader_redoButtonPress.bind(this);
|
||||||
this.body_selectionChange = this.body_selectionChange.bind(this);
|
this.body_selectionChange = this.body_selectionChange.bind(this);
|
||||||
|
this.onBodyViewerLoadEnd = this.onBodyViewerLoadEnd.bind(this);
|
||||||
|
this.onBodyViewerCheckboxChange = this.onBodyViewerCheckboxChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
undoRedoService_stackChange() {
|
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());
|
const undoState = await this.undoRedoService_[type](this.undoState());
|
||||||
if (!undoState) return;
|
if (!undoState) return;
|
||||||
|
|
||||||
this.setState((state) => {
|
this.setState((state:any) => {
|
||||||
const newNote = Object.assign({}, state.note);
|
const newNote = Object.assign({}, state.note);
|
||||||
newNote.body = undoState.body;
|
newNote.body = undoState.body;
|
||||||
return {
|
return {
|
||||||
@ -271,7 +273,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
this.styles_ = {};
|
this.styles_ = {};
|
||||||
|
|
||||||
// TODO: Clean up these style names and nesting
|
// TODO: Clean up these style names and nesting
|
||||||
const styles = {
|
const styles:any = {
|
||||||
screen: {
|
screen: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.backgroundColor,
|
backgroundColor: theme.backgroundColor,
|
||||||
@ -300,12 +302,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
paddingLeft: theme.marginLeft,
|
paddingLeft: theme.marginLeft,
|
||||||
paddingRight: theme.marginRight,
|
paddingRight: theme.marginRight,
|
||||||
},
|
},
|
||||||
noteBodyViewerPreview: {
|
|
||||||
borderTopColor: theme.dividerColor,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderBottomColor: theme.dividerColor,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
},
|
|
||||||
checkbox: {
|
checkbox: {
|
||||||
color: theme.color,
|
color: theme.color,
|
||||||
paddingRight: 10,
|
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 = {
|
styles.titleContainer = {
|
||||||
flex: 0,
|
flex: 0,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -354,7 +358,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
return shared.isModified(this);
|
return shared.isModified(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
undoState(noteBody = null) {
|
undoState(noteBody:string = null) {
|
||||||
return {
|
return {
|
||||||
body: noteBody === null ? this.state.note.body : noteBody,
|
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);
|
ResourceFetcher.instance().markForDownload(event.resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps:any) {
|
||||||
if (this.doFocusUpdate_) {
|
if (this.doFocusUpdate_) {
|
||||||
this.doFocusUpdate_ = false;
|
this.doFocusUpdate_ = false;
|
||||||
this.focusUpdate();
|
this.focusUpdate();
|
||||||
@ -413,13 +417,13 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
if (this.undoRedoService_) this.undoRedoService_.off('stackChange', this.undoRedoService_stackChange);
|
if (this.undoRedoService_) this.undoRedoService_.off('stackChange', this.undoRedoService_stackChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
title_changeText(text) {
|
title_changeText(text:string) {
|
||||||
shared.noteComponent_change(this, 'title', text);
|
shared.noteComponent_change(this, 'title', text);
|
||||||
this.setState({ newAndNoTitleChangeNoteId: null });
|
this.setState({ newAndNoTitleChangeNoteId: null });
|
||||||
this.scheduleSave();
|
this.scheduleSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
body_changeText(text) {
|
body_changeText(text:string) {
|
||||||
if (!this.undoRedoService_.canUndo) {
|
if (!this.undoRedoService_.canUndo) {
|
||||||
this.undoRedoService_.push(this.undoState());
|
this.undoRedoService_.push(this.undoState());
|
||||||
} else {
|
} else {
|
||||||
@ -429,7 +433,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
this.scheduleSave();
|
this.scheduleSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
body_selectionChange(event) {
|
body_selectionChange(event:any) {
|
||||||
this.selection = event.nativeEvent.selection;
|
this.selection = event.nativeEvent.selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,7 +443,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
saveActionQueue(noteId) {
|
saveActionQueue(noteId:string) {
|
||||||
if (!this.saveActionQueues_[noteId]) {
|
if (!this.saveActionQueues_[noteId]) {
|
||||||
this.saveActionQueues_[noteId] = new AsyncActionQueue(500);
|
this.saveActionQueues_[noteId] = new AsyncActionQueue(500);
|
||||||
}
|
}
|
||||||
@ -450,13 +454,13 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
this.saveActionQueue(this.state.note.id).push(this.makeSaveAction());
|
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);
|
await shared.saveNoteButton_press(this, folderId);
|
||||||
|
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveOneProperty(name, value) {
|
async saveOneProperty(name:string, value:any) {
|
||||||
await shared.saveOneProperty(this, name, value);
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
Image.getSize(
|
Image.getSize(
|
||||||
uri,
|
uri,
|
||||||
(width, height) => {
|
(width:number, height:number) => {
|
||||||
resolve({ width: width, height: height });
|
resolve({ width: width, height: height });
|
||||||
},
|
},
|
||||||
error => {
|
(error:any) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showImagePicker(options) {
|
showImagePicker(options:any) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
ImagePicker.launchImageLibrary(options, response => {
|
ImagePicker.launchImageLibrary(options, (response:any) => {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async resizeImage(localFilePath, targetPath, mimeType) {
|
async resizeImage(localFilePath:string, targetPath:string, mimeType:string) {
|
||||||
const maxSize = Resource.IMAGE_MAX_DIMENSION;
|
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);
|
reg.logger().info('Original dimensions ', dimensions);
|
||||||
|
|
||||||
@ -563,7 +567,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async attachFile(pickerResponse, fileType) {
|
async attachFile(pickerResponse:any, fileType:string) {
|
||||||
if (!pickerResponse) {
|
if (!pickerResponse) {
|
||||||
// User has cancelled
|
// User has cancelled
|
||||||
return;
|
return;
|
||||||
@ -673,7 +677,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
this.setState({ showCamera: true });
|
this.setState({ showCamera: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
cameraView_onPhoto(data) {
|
cameraView_onPhoto(data:any) {
|
||||||
this.attachFile(
|
this.attachFile(
|
||||||
{
|
{
|
||||||
uri: data.uri,
|
uri: data.uri,
|
||||||
@ -723,7 +727,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
this.setState({ alarmDialogShown: true });
|
this.setState({ alarmDialogShown: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAlarmDialogAccept(date) {
|
async onAlarmDialogAccept(date:Date) {
|
||||||
const newNote = Object.assign({}, this.state.note);
|
const newNote = Object.assign({}, this.state.note);
|
||||||
newNote.todo_due = date ? date.getTime() : 0;
|
newNote.todo_due = date ? date.getTime() : 0;
|
||||||
|
|
||||||
@ -899,11 +903,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
async todoCheckbox_change(checked) {
|
async todoCheckbox_change(checked:boolean) {
|
||||||
await this.saveOneProperty('todo_completed', checked ? time.unixMs() : 0);
|
await this.saveOneProperty('todo_completed', checked ? time.unixMs() : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
titleTextInput_contentSizeChange(event) {
|
titleTextInput_contentSizeChange(event:any) {
|
||||||
if (!this.enableMultilineTitle_) return;
|
if (!this.enableMultilineTitle_) return;
|
||||||
|
|
||||||
const height = event.nativeEvent.contentSize.height;
|
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 note = this.state.note;
|
||||||
const isProvisionalNote = this.props.provisionalNoteIds.includes(note.id);
|
const isProvisionalNote = this.props.provisionalNoteIds.includes(note.id);
|
||||||
|
|
||||||
@ -971,6 +975,19 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
return this.folderPickerOptions_;
|
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() {
|
render() {
|
||||||
if (this.state.isLoading) {
|
if (this.state.isLoading) {
|
||||||
return (
|
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} />;
|
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;
|
let bodyComponent = null;
|
||||||
if (this.state.mode == 'view' && !this.useBetaEditor()) {
|
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,
|
// 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.
|
// to avoid the HACK_webviewLoadingState related bug.
|
||||||
bodyComponent =
|
bodyComponent =
|
||||||
!note || !note.body.trim() ? null : (
|
!note || !note.body.trim() ? null : (
|
||||||
<NoteBodyViewer
|
<NoteBodyViewer
|
||||||
onJoplinLinkClick={this.onJoplinLinkClick_}
|
onJoplinLinkClick={this.onJoplinLinkClick_}
|
||||||
ref="noteBodyViewer"
|
|
||||||
style={this.styles().noteBodyViewer}
|
style={this.styles().noteBodyViewer}
|
||||||
webViewStyle={theme}
|
|
||||||
// Extra bottom padding to make it possible to scroll past the
|
// Extra bottom padding to make it possible to scroll past the
|
||||||
// action button (so that it doesn't overlap the text)
|
// action button (so that it doesn't overlap the text)
|
||||||
paddingBottom="150"
|
paddingBottom={150}
|
||||||
note={note}
|
noteBody={note.body}
|
||||||
|
noteMarkupLanguage={note.markup_language}
|
||||||
noteResources={this.state.noteResources}
|
noteResources={this.state.noteResources}
|
||||||
highlightedKeywords={keywords}
|
highlightedKeywords={keywords}
|
||||||
themeId={this.props.themeId}
|
themeId={this.props.themeId}
|
||||||
noteHash={this.props.noteHash}
|
noteHash={this.props.noteHash}
|
||||||
onCheckboxChange={newBody => {
|
onCheckboxChange={this.onBodyViewerCheckboxChange}
|
||||||
onCheckboxChange(newBody);
|
|
||||||
}}
|
|
||||||
onMarkForDownload={this.onMarkForDownload}
|
onMarkForDownload={this.onMarkForDownload}
|
||||||
onLoadEnd={() => {
|
onLoadEnd={this.onBodyViewerLoadEnd}
|
||||||
shim.setTimeout(() => {
|
|
||||||
this.setState({ HACK_webviewLoadingState: 1 });
|
|
||||||
shim.setTimeout(() => {
|
|
||||||
this.setState({ HACK_webviewLoadingState: 0 });
|
|
||||||
}, 50);
|
|
||||||
}, 5);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} 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()
|
bodyComponent = this.useBetaEditor()
|
||||||
// Note: blurOnSubmit is necessary to get multiline to work.
|
// Note: blurOnSubmit is necessary to get multiline to work.
|
||||||
// See https://github.com/facebook/react-native/issues/12717#issuecomment-327001997
|
// See https://github.com/facebook/react-native/issues/12717#issuecomment-327001997
|
||||||
@ -1055,7 +1043,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
value={note.body}
|
value={note.body}
|
||||||
borderColor={this.styles().markdownButtons.borderColor}
|
borderColor={this.styles().markdownButtons.borderColor}
|
||||||
markdownButtonsColor={this.styles().markdownButtons.color}
|
markdownButtonsColor={this.styles().markdownButtons.color}
|
||||||
saveText={text => this.body_changeText(text)}
|
saveText={(text:string) => this.body_changeText(text)}
|
||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
selectionColor={theme.textSelectionColor}
|
selectionColor={theme.textSelectionColor}
|
||||||
keyboardAppearance={theme.keyboardAppearance}
|
keyboardAppearance={theme.keyboardAppearance}
|
||||||
@ -1063,29 +1051,18 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
placeholderTextColor={theme.colorFaded}
|
placeholderTextColor={theme.colorFaded}
|
||||||
noteBodyViewer={{
|
noteBodyViewer={{
|
||||||
onJoplinLinkClick: this.onJoplinLinkClick_,
|
onJoplinLinkClick: this.onJoplinLinkClick_,
|
||||||
ref: 'noteBodyViewer',
|
style: this.styles().noteBodyViewerPreview,
|
||||||
style: {
|
paddingBottom: 0,
|
||||||
...this.styles().noteBodyViewer,
|
|
||||||
...this.styles().noteBodyViewerPreview,
|
|
||||||
},
|
|
||||||
webViewStyle: theme,
|
webViewStyle: theme,
|
||||||
note: note,
|
noteBody: note.body,
|
||||||
|
noteMarkupLanguage: note.markup_language,
|
||||||
noteResources: this.state.noteResources,
|
noteResources: this.state.noteResources,
|
||||||
highlightedKeywords: keywords,
|
highlightedKeywords: keywords,
|
||||||
themeId: this.props.themeId,
|
themeId: this.props.themeId,
|
||||||
noteHash: this.props.noteHash,
|
noteHash: this.props.noteHash,
|
||||||
onCheckboxChange: newBody => {
|
onCheckboxChange: this.onBodyViewerCheckboxChange,
|
||||||
onCheckboxChange(newBody);
|
|
||||||
},
|
|
||||||
onMarkForDownload: this.onMarkForDownload,
|
onMarkForDownload: this.onMarkForDownload,
|
||||||
onLoadEnd: () => {
|
onLoadEnd: this.onBodyViewerLoadEnd,
|
||||||
shim.setTimeout(() => {
|
|
||||||
this.setState({ HACK_webviewLoadingState: 1 });
|
|
||||||
shim.setTimeout(() => {
|
|
||||||
this.setState({ HACK_webviewLoadingState: 0 });
|
|
||||||
}, 50);
|
|
||||||
}, 5);
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
@ -1112,7 +1089,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
ref="noteBodyTextField"
|
ref="noteBodyTextField"
|
||||||
multiline={true}
|
multiline={true}
|
||||||
value={note.body}
|
value={note.body}
|
||||||
onChangeText={(text) => this.body_changeText(text)}
|
onChangeText={(text:string) => this.body_changeText(text)}
|
||||||
onSelectionChange={this.body_selectionChange}
|
onSelectionChange={this.body_selectionChange}
|
||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
selectionColor={theme.textSelectionColor}
|
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} />
|
<SelectDateTimeDialog themeId={this.props.themeId} shown={this.state.alarmDialogShown} date={dueDate} onAccept={this.onAlarmDialogAccept} onReject={this.onAlarmDialogReject} />
|
||||||
|
|
||||||
<DialogBox
|
<DialogBox
|
||||||
ref={dialogbox => {
|
ref={(dialogbox:any) => {
|
||||||
this.dialogbox = dialogbox;
|
this.dialogbox = dialogbox;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -1208,7 +1185,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoteScreen = connect(state => {
|
const NoteScreen = connect((state:any) => {
|
||||||
return {
|
return {
|
||||||
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||||
noteHash: state.selectedNoteHash,
|
noteHash: state.selectedNoteHash,
|
||||||
@ -1226,4 +1203,4 @@ const NoteScreen = connect(state => {
|
|||||||
};
|
};
|
||||||
})(NoteScreenComponent);
|
})(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';
|
import useEffectDebugger from './useEffectDebugger';
|
||||||
|
|
||||||
export default function usePropsDebugger(effectHook:any, props:any) {
|
export default function usePropsDebugger(props:any) {
|
||||||
const dependencies:any[] = [];
|
const dependencies:any[] = [];
|
||||||
const dependencyNames:string[] = [];
|
const dependencyNames:string[] = [];
|
||||||
|
|
||||||
@ -9,5 +13,5 @@ export default function usePropsDebugger(effectHook:any, props:any) {
|
|||||||
dependencyNames.push(k);
|
dependencyNames.push(k);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffectDebugger(effectHook, dependencies, dependencyNames);
|
useEffectDebugger(() => {}, dependencies, dependencyNames);
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,11 @@ class MarkupToHtml {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearCache(markupLanguage) {
|
||||||
|
const r = this.renderer(markupLanguage);
|
||||||
|
if (r.clearCache) r.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
async render(markupLanguage, markup, theme, options) {
|
async render(markupLanguage, markup, theme, options) {
|
||||||
return this.renderer(markupLanguage).render(markup, theme, options);
|
return this.renderer(markupLanguage).render(markup, theme, options);
|
||||||
}
|
}
|
||||||
|
@ -178,7 +178,11 @@ class MdToHtml {
|
|||||||
return html.substring(3, html.length - 5);
|
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) {
|
async render(body, theme = null, options = null) {
|
||||||
options = Object.assign({}, {
|
options = Object.assign({}, {
|
||||||
// In bodyOnly mode, the rendered Markdown is returned without the wrapper DIV
|
// 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
|
// 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
|
// this check means we don't unecessarily save the resource twice when the file is
|
||||||
// modified by the user.
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ const { JoplinDatabase } = require('lib/joplin-database.js');
|
|||||||
const { Database } = require('lib/database.js');
|
const { Database } = require('lib/database.js');
|
||||||
const { NotesScreen } = require('lib/components/screens/notes.js');
|
const { NotesScreen } = require('lib/components/screens/notes.js');
|
||||||
const { TagsScreen } = require('lib/components/screens/tags.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 { ConfigScreen } = require('lib/components/screens/config.js');
|
||||||
const { FolderScreen } = require('lib/components/screens/folder.js');
|
const { FolderScreen } = require('lib/components/screens/folder.js');
|
||||||
const { LogScreen } = require('lib/components/screens/log.js');
|
const { LogScreen } = require('lib/components/screens/log.js');
|
||||||
|
@ -706,7 +706,12 @@
|
|||||||
"ReactNativeClient/lib/components/CameraView.js": true,
|
"ReactNativeClient/lib/components/CameraView.js": true,
|
||||||
"ReactNativeClient/lib/components/NoteBodyViewer.js": true,
|
"ReactNativeClient/lib/components/NoteBodyViewer.js": true,
|
||||||
"CliClient/tests/InMemoryCache.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": [
|
"spellright.language": [
|
||||||
"en"
|
"en"
|
||||||
|
Reference in New Issue
Block a user