1
0
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:
Henry Heino 2023-11-16 04:19:48 -08:00 committed by GitHub
parent 18e86a7ba3
commit d0955b4ca2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 94 additions and 17 deletions

View File

@ -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}

View File

@ -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,
]);
}

View File

@ -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 })}

View File

@ -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 {