You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-23 23:33:01 +02:00
Mobile: Upgraded React Native to v0.63
commit2fb6cee901Merge:4e303be85fdb509955f6Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 16:24:07 2020 +0100 Merge branch 'dev' into rn_63 commit4e303be85fAuthor: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 16:14:39 2020 +0100 Clean up commite3a37ec2d6Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 15:57:55 2020 +0100 Use different script for pre-commit and manual start commitbd236648fcAuthor: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 15:56:45 2020 +0100 Removed RN eslint config commite7feda41c9Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 15:27:08 2020 +0100 Revert "Disable git hook for now" This reverts commit89263ac742. commitcfd63fe46fAuthor: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 13:02:32 2020 +0100 Ask permission to use geo-location commit66059939a3Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 12:26:20 2020 +0100 Fixed WebView race condition commit1e0d2b7b86Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 11:56:21 2020 +0100 Fixed webview issues commitf537d22d7fAuthor: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 11:08:29 2020 +0100 Improve resource file watching commiteec32cf70aAuthor: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 18:40:13 2020 +0100 Removed cache package dependency and implemented one more suitable for React Native commitefa346fea4Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 14:57:21 2020 +0100 iOS: Added fonts to Info.plist although it was working without it commit572b647bc0Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 14:56:49 2020 +0100 Specify content-type header for OneDrive to prevent network error commitbcedf6c7f0Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 12:45:01 2020 +0100 iOS: Disable long press menu since it is already built-in commit7359dd61d1Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 12:37:40 2020 +0100 Removed unused react-native-device-info commit2d63ab36d3Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 12:35:54 2020 +0100 iOS: Fixed taking a picture commit8e2875a91cAuthor: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 12:11:13 2020 +0100 iOS: Restored camera roll functionality commit75f5edf2adAuthor: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 11:40:13 2020 +0100 iOS: Fixed build settings commitb220c98419Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 11:40:03 2020 +0100 iOS: Got images to work with WebKit commitc34b43e841Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 10:24:52 2020 +0100 iOS: Restore more settings commit32997611e6Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 10:15:14 2020 +0100 iOS: Added back icons and other properties commitb5811d7f7cAuthor: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 23:53:14 2020 +0100 Got iOS build to work commitdc6d7c00e0Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 18:40:06 2020 +0100 Imported old settings in gradle build commitdff59f5603Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 18:20:00 2020 +0100 Restored sharing commit0bdb449e72Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 17:25:40 2020 +0100 Updated NoteBodyViewer commit0c0d228815Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 16:54:42 2020 +0100 Fixed networking commit6ff45ce485Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 13:11:00 2020 +0100 Fixed document picker commitcc889182b6Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 12:56:27 2020 +0100 Added back support for alarms commit040261abfaAuthor: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 22:04:49 2020 +0100 Fixed Clipboard and remove image-picker package commit1077ad8f16Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 21:54:52 2020 +0100 Fixed Select Alarm dialog and PoorManIntervals class commit8296676fd5Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 21:32:52 2020 +0100 Fixed icons and warnings commit3b0e3f6f43Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 17:02:59 2020 +0100 Got app to build again commit89263ac742Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 15:41:17 2020 +0100 Disable git hook for now commitd6da162f67Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 15:39:12 2020 +0100 Restored back all RN packages commit7f8ce3732cAuthor: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 15:13:12 2020 +0100 Restored base packages commitea59726eb3Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 15:05:17 2020 +0100 Started over from scratch
This commit is contained in:
@@ -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}
|
||||
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???
|
||||
const 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 useOnResourceLongPress(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,165 @@
|
||||
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 [isFirstRender, setIsFirstRender] = useState(true);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// When mounted, we need to render the webview in two stages;
|
||||
// - First without any source, so that all webview props are setup properly
|
||||
// - Secondly with the source to actually render the note
|
||||
// This is necessary to prevent a race condition that could cause an ERR_ACCESS_DENIED error
|
||||
// https://github.com/react-native-webview/react-native-webview/issues/656#issuecomment-551312436
|
||||
|
||||
if (isFirstRender) {
|
||||
setIsFirstRender(false);
|
||||
setSource(undefined);
|
||||
setInjectedJs([]);
|
||||
} else {
|
||||
renderNote();
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [resourceLoadedTime, noteBody, noteMarkupLanguage, themeId, rendererTheme, highlightedKeywords, noteResources, noteHash, isFirstRender]);
|
||||
|
||||
return { source, injectedJs };
|
||||
}
|
||||
Reference in New Issue
Block a user