mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Mobile: Fixes #9321: Restore scroll position when returning to the note viewer from the editor or camera (#9324)
This commit is contained in:
parent
18e86a7ba3
commit
d0955b4ca2
@ -1,14 +1,14 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
|
||||
import useSource from './hooks/useSource';
|
||||
import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
|
||||
import useOnMessage, { HandleMessageCallback, HandleScrollCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
|
||||
import useOnResourceLongPress from './hooks/useOnResourceLongPress';
|
||||
|
||||
const React = require('react');
|
||||
import { View } from 'react-native';
|
||||
import BackButtonDialogBox from '../BackButtonDialogBox';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ExtendedWebView from '../ExtendedWebView';
|
||||
import ExtendedWebView, { WebViewControl } from '../ExtendedWebView';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@ -18,11 +18,13 @@ interface Props {
|
||||
highlightedKeywords: string[];
|
||||
noteResources: any;
|
||||
paddingBottom: number;
|
||||
initialScroll: number|null;
|
||||
noteHash: string;
|
||||
onJoplinLinkClick: HandleMessageCallback;
|
||||
onCheckboxChange?: HandleMessageCallback;
|
||||
onRequestEditResource?: HandleMessageCallback;
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
onScroll: HandleScrollCallback;
|
||||
onLoadEnd?: ()=> void;
|
||||
}
|
||||
|
||||
@ -32,6 +34,7 @@ const webViewStyle = {
|
||||
|
||||
export default function NoteBodyViewer(props: Props) {
|
||||
const dialogBoxRef = useRef(null);
|
||||
const webviewRef = useRef<WebViewControl>(null);
|
||||
|
||||
const { html, injectedJs } = useSource(
|
||||
props.noteBody,
|
||||
@ -41,6 +44,7 @@ export default function NoteBodyViewer(props: Props) {
|
||||
props.noteResources,
|
||||
props.paddingBottom,
|
||||
props.noteHash,
|
||||
props.initialScroll,
|
||||
);
|
||||
|
||||
const onResourceLongPress = useOnResourceLongPress(
|
||||
@ -59,6 +63,7 @@ export default function NoteBodyViewer(props: Props) {
|
||||
onJoplinLinkClick: props.onJoplinLinkClick,
|
||||
onRequestEditResource: props.onRequestEditResource,
|
||||
onResourceLongPress,
|
||||
onMainContainerScroll: props.onScroll,
|
||||
},
|
||||
);
|
||||
|
||||
@ -96,6 +101,7 @@ export default function NoteBodyViewer(props: Props) {
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<ExtendedWebView
|
||||
ref={webviewRef}
|
||||
webviewInstanceId='NoteBodyViewer'
|
||||
themeId={props.themeId}
|
||||
style={webViewStyle}
|
||||
|
@ -3,6 +3,7 @@ import shared from '@joplin/lib/components/shared/note-screen-shared';
|
||||
|
||||
export type HandleMessageCallback = (message: string)=> void;
|
||||
export type OnMarkForDownloadCallback = (resource: { resourceId: string })=> void;
|
||||
export type HandleScrollCallback = (scrollTop: number)=> void;
|
||||
|
||||
interface MessageCallbacks {
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
@ -10,6 +11,7 @@ interface MessageCallbacks {
|
||||
onResourceLongPress: HandleMessageCallback;
|
||||
onRequestEditResource?: HandleMessageCallback;
|
||||
onCheckboxChange: HandleMessageCallback;
|
||||
onMainContainerScroll: HandleScrollCallback;
|
||||
}
|
||||
|
||||
export default function useOnMessage(
|
||||
@ -24,6 +26,7 @@ export default function useOnMessage(
|
||||
// Thus, useCallback should depend on each callback individually.
|
||||
const {
|
||||
onMarkForDownload, onResourceLongPress, onCheckboxChange, onRequestEditResource, onJoplinLinkClick,
|
||||
onMainContainerScroll,
|
||||
} = callbacks;
|
||||
|
||||
return useCallback((event: any) => {
|
||||
@ -35,10 +38,23 @@ export default function useOnMessage(
|
||||
// https://github.com/laurent22/joplin/issues/4494
|
||||
const msg = event.nativeEvent.data;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Got IPC message: ', msg);
|
||||
const isScrollMessage = msg.startsWith('onscroll:');
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
// Scroll messages are very frequent so we avoid logging them.
|
||||
if (!isScrollMessage) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Got IPC message: ', msg);
|
||||
}
|
||||
|
||||
if (isScrollMessage) {
|
||||
const eventData = JSON.parse(msg.substring(msg.indexOf(':') + 1));
|
||||
|
||||
if (typeof eventData.scrollTop !== 'number') {
|
||||
throw new Error(`Invalid scroll message, ${msg}`);
|
||||
}
|
||||
|
||||
onMainContainerScroll?.(eventData.scrollTop);
|
||||
} else if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const newBody = shared.toggleCheckbox(msg, noteBody);
|
||||
onCheckboxChange?.(newBody);
|
||||
} else if (msg.indexOf('markForDownload:') === 0) {
|
||||
@ -63,5 +79,6 @@ export default function useOnMessage(
|
||||
onJoplinLinkClick,
|
||||
onResourceLongPress,
|
||||
onRequestEditResource,
|
||||
onMainContainerScroll,
|
||||
]);
|
||||
}
|
||||
|
@ -40,7 +40,16 @@ const onlyCheckboxHasChangedHack = (previousBody: string, newBody: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export default function useSource(noteBody: string, noteMarkupLanguage: number, themeId: number, highlightedKeywords: string[], noteResources: any, paddingBottom: number, noteHash: string): UseSourceResult {
|
||||
export default function useSource(
|
||||
noteBody: string,
|
||||
noteMarkupLanguage: number,
|
||||
themeId: number,
|
||||
highlightedKeywords: string[],
|
||||
noteResources: any,
|
||||
paddingBottom: number,
|
||||
noteHash: string,
|
||||
initialScroll: number|null,
|
||||
): UseSourceResult {
|
||||
const [html, setHtml] = useState<string>('');
|
||||
const [injectedJs, setInjectedJs] = useState<string[]>([]);
|
||||
const [resourceLoadedTime, setResourceLoadedTime] = useState(0);
|
||||
@ -142,6 +151,12 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
|
||||
const resourceDownloadMode = Setting.value('sync.resourceDownloadMode');
|
||||
|
||||
// On iOS, the root container has slow inertial scroll, which feels very different from
|
||||
// the native scroll in other apps. This is not the case, however, when a child (e.g. a div)
|
||||
// scrolls the content instead.
|
||||
// Use a div to scroll on iOS instead of the main container:
|
||||
const scrollRenderedMdContainer = shim.mobilePlatform() === 'ios';
|
||||
|
||||
const js = [];
|
||||
js.push('try {');
|
||||
js.push(shim.injectedJs('webviewLib'));
|
||||
@ -149,15 +164,46 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
// 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 scrollingElement =
|
||||
${scrollRenderedMdContainer ? 'document.querySelector("#rendered-md")' : 'document.scrollingElement'};
|
||||
let lastScrollTop;
|
||||
const onMainContentScroll = () => {
|
||||
const newScrollTop = scrollingElement.scrollTop;
|
||||
if (lastScrollTop !== newScrollTop) {
|
||||
const eventData = { scrollTop: newScrollTop };
|
||||
window.ReactNativeWebView.postMessage('onscroll:' + JSON.stringify(eventData));
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for events on both scrollingElement and window
|
||||
// - On Android, scrollingElement.addEventListener('scroll', callback) doesn't call callback on
|
||||
// scroll. However, window.addEventListener('scroll', callback) does.
|
||||
// - iOS needs a listener to be added to scrollingElement -- events aren't received when
|
||||
// the listener is added to window with window.addEventListener('scroll', ...).
|
||||
scrollingElement.addEventListener('scroll', onMainContentScroll);
|
||||
window.addEventListener('scroll', onMainContentScroll);
|
||||
|
||||
const scrollContentToPosition = (position) => {
|
||||
scrollingElement.scrollTop = position;
|
||||
};
|
||||
`);
|
||||
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) {
|
||||
const initialScroll = ${JSON.stringify(initialScroll)};
|
||||
|
||||
// Don't scroll to a hash if we're given initial scroll (initial scroll
|
||||
// overrides scrolling to a hash).
|
||||
if ((initialScroll ?? null) !== null) {
|
||||
scrollContentToPosition(initialScroll);
|
||||
} else if (hash) {
|
||||
// Gives it a bit of time before scrolling to the anchor
|
||||
// so that images are loaded.
|
||||
setTimeout(() => {
|
||||
const e = document.getElementById(hash);
|
||||
if (!e) {
|
||||
@ -171,6 +217,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
}, 10);
|
||||
`);
|
||||
js.push('} catch (e) {');
|
||||
js.push(' console.error(e);');
|
||||
js.push(' window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))');
|
||||
js.push(' true;');
|
||||
js.push('}');
|
||||
@ -186,10 +233,11 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS seems to increase inertial scrolling friction when the WebView body/root elements
|
||||
scroll. Scroll the main container instead.
|
||||
*/
|
||||
:root > body {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
const scrollRenderedMdContainerCss = `
|
||||
body > #rendered-md {
|
||||
width: 100vw;
|
||||
overflow: auto;
|
||||
@ -197,10 +245,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
padding-bottom: ${paddingBottom}px;
|
||||
padding-top: ${paddingTop};
|
||||
}
|
||||
|
||||
:root > body {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
const defaultCss = `
|
||||
code {
|
||||
@ -219,6 +263,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
<style>
|
||||
${defaultCss}
|
||||
${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''}
|
||||
${scrollRenderedMdContainer ? scrollRenderedMdContainerCss : ''}
|
||||
${editPopupCss}
|
||||
</style>
|
||||
${assetsToHeaders(result.pluginAssets, { asHtml: true })}
|
||||
|
@ -61,6 +61,9 @@ const emptyArray: any[] = [];
|
||||
const logger = Logger.create('screens/Note');
|
||||
|
||||
class NoteScreenComponent extends BaseScreenComponent {
|
||||
// This isn't in this.state because we don't want changing scroll to trigger
|
||||
// a re-render.
|
||||
private lastBodyScroll: number|undefined = undefined;
|
||||
|
||||
public static navigationOptions(): any {
|
||||
return { header: null };
|
||||
@ -1257,6 +1260,10 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}, 5);
|
||||
}
|
||||
|
||||
private onBodyViewerScroll = (scrollTop: number) => {
|
||||
this.lastBodyScroll = scrollTop;
|
||||
};
|
||||
|
||||
public onBodyViewerCheckboxChange(newBody: string) {
|
||||
void this.saveOneProperty('body', newBody);
|
||||
}
|
||||
@ -1331,6 +1338,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
onMarkForDownload={this.onMarkForDownload}
|
||||
onRequestEditResource={this.onEditResource}
|
||||
onLoadEnd={this.onBodyViewerLoadEnd}
|
||||
onScroll={this.onBodyViewerScroll}
|
||||
initialScroll={this.lastBodyScroll}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
Loading…
Reference in New Issue
Block a user