mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-30 10:36:35 +02:00
Desktop: Fixes #8661: Fix note editor blank after syncing an encrypted note with remote changes (#8666)
This commit is contained in:
parent
4804c1c0c3
commit
e7014492c5
@ -260,6 +260,7 @@ packages/app-desktop/gui/NoteEditor/utils/types.js
|
|||||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -246,6 +246,7 @@ packages/app-desktop/gui/NoteEditor/utils/types.js
|
|||||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||||
|
@ -78,6 +78,7 @@ function NoteEditor(props: NoteEditorProps) {
|
|||||||
|
|
||||||
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
||||||
syncStarted: props.syncStarted,
|
syncStarted: props.syncStarted,
|
||||||
|
decryptionStarted: props.decryptionStarted,
|
||||||
noteId: effectiveNoteId,
|
noteId: effectiveNoteId,
|
||||||
isProvisional: props.isProvisional,
|
isProvisional: props.isProvisional,
|
||||||
titleInputRef: titleInputRef,
|
titleInputRef: titleInputRef,
|
||||||
@ -633,6 +634,7 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
isProvisional: state.provisionalNoteIds.includes(noteId),
|
isProvisional: state.provisionalNoteIds.includes(noteId),
|
||||||
editorNoteStatuses: state.editorNoteStatuses,
|
editorNoteStatuses: state.editorNoteStatuses,
|
||||||
syncStarted: state.syncStarted,
|
syncStarted: state.syncStarted,
|
||||||
|
decryptionStarted: state.decryptionWorker?.state !== 'idle',
|
||||||
themeId: state.settings.theme,
|
themeId: state.settings.theme,
|
||||||
richTextBannerDismissed: state.settings.richTextBannerDismissed,
|
richTextBannerDismissed: state.settings.richTextBannerDismissed,
|
||||||
watchedNoteFiles: state.watchedNoteFiles,
|
watchedNoteFiles: state.watchedNoteFiles,
|
||||||
|
@ -27,6 +27,7 @@ export interface NoteEditorProps {
|
|||||||
isProvisional: boolean;
|
isProvisional: boolean;
|
||||||
editorNoteStatuses: any;
|
editorNoteStatuses: any;
|
||||||
syncStarted: boolean;
|
syncStarted: boolean;
|
||||||
|
decryptionStarted: boolean;
|
||||||
bodyEditor: string;
|
bodyEditor: string;
|
||||||
notesParentType: string;
|
notesParentType: string;
|
||||||
selectedNoteTags: any[];
|
selectedNoteTags: any[];
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
import Note from '@joplin/lib/models/Note';
|
||||||
|
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import useFormNote, { HookDependencies } from './useFormNote';
|
||||||
|
|
||||||
|
|
||||||
|
describe('useFormNote', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setupDatabaseAndSynchronizer(1);
|
||||||
|
await switchClient(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update note when decryption completes', async () => {
|
||||||
|
const testNote = await Note.save({ title: 'Test Note!' });
|
||||||
|
|
||||||
|
const makeFormNoteProps = (syncStarted: boolean, decryptionStarted: boolean): HookDependencies => {
|
||||||
|
return {
|
||||||
|
syncStarted,
|
||||||
|
decryptionStarted,
|
||||||
|
noteId: testNote.id,
|
||||||
|
isProvisional: false,
|
||||||
|
titleInputRef: null,
|
||||||
|
editorRef: null,
|
||||||
|
onBeforeLoad: ()=>{},
|
||||||
|
onAfterLoad: ()=>{},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const formNote = renderHook(props => useFormNote(props), {
|
||||||
|
initialProps: makeFormNoteProps(true, false),
|
||||||
|
});
|
||||||
|
await formNote.waitFor(() => {
|
||||||
|
expect(formNote.result.current.formNote).toMatchObject({
|
||||||
|
encryption_applied: 0,
|
||||||
|
title: testNote.title,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Note.save({
|
||||||
|
id: testNote.id,
|
||||||
|
encryption_cipher_text: 'cipher_text',
|
||||||
|
encryption_applied: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync starting should cause a re-render
|
||||||
|
formNote.rerender(makeFormNoteProps(false, false));
|
||||||
|
|
||||||
|
await formNote.waitFor(() => {
|
||||||
|
expect(formNote.result.current.formNote).toMatchObject({
|
||||||
|
encryption_applied: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
formNote.rerender(makeFormNoteProps(false, true));
|
||||||
|
|
||||||
|
await Note.save({
|
||||||
|
id: testNote.id,
|
||||||
|
encryption_applied: 0,
|
||||||
|
title: 'Test Note!',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ending decryption should also cause a re-render
|
||||||
|
formNote.rerender(makeFormNoteProps(false, false));
|
||||||
|
|
||||||
|
await formNote.waitFor(() => {
|
||||||
|
expect(formNote.result.current.formNote).toMatchObject({
|
||||||
|
encryption_applied: 0,
|
||||||
|
title: 'Test Note!',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -18,8 +18,9 @@ export interface OnLoadEvent {
|
|||||||
formNote: FormNote;
|
formNote: FormNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HookDependencies {
|
export interface HookDependencies {
|
||||||
syncStarted: boolean;
|
syncStarted: boolean;
|
||||||
|
decryptionStarted: boolean;
|
||||||
noteId: string;
|
noteId: string;
|
||||||
isProvisional: boolean;
|
isProvisional: boolean;
|
||||||
titleInputRef: any;
|
titleInputRef: any;
|
||||||
@ -61,15 +62,21 @@ function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function useFormNote(dependencies: HookDependencies) {
|
export default function useFormNote(dependencies: HookDependencies) {
|
||||||
const { syncStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad } = dependencies;
|
const {
|
||||||
|
syncStarted, decryptionStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad,
|
||||||
|
} = dependencies;
|
||||||
|
|
||||||
const [formNote, setFormNote] = useState<FormNote>(defaultFormNote());
|
const [formNote, setFormNote] = useState<FormNote>(defaultFormNote());
|
||||||
const [formNoteRefeshScheduled, setFormNoteRefreshScheduled] = useState<boolean>(false);
|
|
||||||
const [isNewNote, setIsNewNote] = useState(false);
|
const [isNewNote, setIsNewNote] = useState(false);
|
||||||
const prevSyncStarted = usePrevious(syncStarted);
|
const prevSyncStarted = usePrevious(syncStarted);
|
||||||
|
const prevDecryptionStarted = usePrevious(decryptionStarted);
|
||||||
const previousNoteId = usePrevious(formNote.id);
|
const previousNoteId = usePrevious(formNote.id);
|
||||||
const [resourceInfos, setResourceInfos] = useState<ResourceInfos>({});
|
const [resourceInfos, setResourceInfos] = useState<ResourceInfos>({});
|
||||||
|
|
||||||
|
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
||||||
|
// a new refresh.
|
||||||
|
const [formNoteRefeshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||||
|
|
||||||
async function initNoteState(n: any) {
|
async function initNoteState(n: any) {
|
||||||
let originalCss = '';
|
let originalCss = '';
|
||||||
|
|
||||||
@ -107,7 +114,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formNoteRefeshScheduled) return () => {};
|
if (formNoteRefeshScheduled <= 0) return () => {};
|
||||||
|
|
||||||
reg.logger().info('Sync has finished and note has never been changed - reloading it');
|
reg.logger().info('Sync has finished and note has never been changed - reloading it');
|
||||||
|
|
||||||
@ -126,7 +133,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await initNoteState(n);
|
await initNoteState(n);
|
||||||
setFormNoteRefreshScheduled(false);
|
setFormNoteRefreshScheduled(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
void loadNote();
|
void loadNote();
|
||||||
@ -136,21 +143,32 @@ export default function useFormNote(dependencies: HookDependencies) {
|
|||||||
};
|
};
|
||||||
}, [formNoteRefeshScheduled, noteId]);
|
}, [formNoteRefeshScheduled, noteId]);
|
||||||
|
|
||||||
|
const refreshFormNote = useCallback(() => {
|
||||||
|
// Increase the counter to cancel any ongoing refresh attempts
|
||||||
|
// and start a new one.
|
||||||
|
setFormNoteRefreshScheduled(formNoteRefeshScheduled + 1);
|
||||||
|
}, [formNoteRefeshScheduled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check that synchronisation has just finished - and
|
// Check that synchronisation has just finished - and
|
||||||
// if the note has never been changed, we reload it.
|
// if the note has never been changed, we reload it.
|
||||||
// If the note has already been changed, it's a conflict
|
// If the note has already been changed, it's a conflict
|
||||||
// that's already been handled by the synchronizer.
|
// that's already been handled by the synchronizer.
|
||||||
|
const decryptionJustEnded = prevDecryptionStarted && !decryptionStarted;
|
||||||
|
const syncJustEnded = prevSyncStarted && !syncStarted;
|
||||||
|
|
||||||
if (!prevSyncStarted) return;
|
if (!decryptionJustEnded && !syncJustEnded) return;
|
||||||
if (syncStarted) return;
|
|
||||||
if (formNote.hasChanged) return;
|
if (formNote.hasChanged) return;
|
||||||
|
|
||||||
// Refresh the form note.
|
// Refresh the form note.
|
||||||
// This is kept separate from the above logic so that when prevSyncStarted is changed
|
// This is kept separate from the above logic so that when prevSyncStarted is changed
|
||||||
// from true to false, it doesn't cancel the note from loading.
|
// from true to false, it doesn't cancel the note from loading.
|
||||||
setFormNoteRefreshScheduled(true);
|
refreshFormNote();
|
||||||
}, [prevSyncStarted, syncStarted, formNote.hasChanged]);
|
}, [
|
||||||
|
prevSyncStarted, syncStarted,
|
||||||
|
prevDecryptionStarted, decryptionStarted,
|
||||||
|
formNote.hasChanged, refreshFormNote,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
|
/* eslint-disable jest/require-top-level-describe */
|
||||||
|
|
||||||
const { default: Logger, TargetType } = require('@joplin/utils/Logger');
|
const { shimInit } = require('@joplin/lib/shim-init-node');
|
||||||
const initLib = require('@joplin/lib/initLib').default;
|
const sqlite3 = require('sqlite3');
|
||||||
|
const SyncTargetNone = require('@joplin/lib/SyncTargetNone').default;
|
||||||
// TODO: Some libraries required by test-utils.js seem to fail to import with the
|
|
||||||
// jsdom environment.
|
|
||||||
//
|
|
||||||
// Thus, require('@joplin/lib/testing/test-utils.js') fails and some setup must be
|
|
||||||
// copied.
|
|
||||||
|
|
||||||
const logger = new Logger();
|
|
||||||
logger.addTarget(TargetType.Console);
|
|
||||||
logger.setLevel(Logger.LEVEL_WARN);
|
|
||||||
Logger.initializeGlobalLogger(logger);
|
|
||||||
initLib(logger);
|
|
||||||
|
|
||||||
|
// Mock the S3 sync target -- the @aws-s3 libraries depend on an old version
|
||||||
|
// of uuid that doesn't work with jest without additional configuration.
|
||||||
|
jest.doMock('@joplin/lib/SyncTargetAmazonS3', () => {
|
||||||
|
return SyncTargetNone;
|
||||||
|
});
|
||||||
|
|
||||||
// @electron/remote requires electron to be running. Mock it.
|
// @electron/remote requires electron to be running. Mock it.
|
||||||
jest.mock('@electron/remote', () => {
|
jest.mock('@electron/remote', () => {
|
||||||
@ -25,3 +20,18 @@ jest.mock('@electron/remote', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Import after mocking problematic libraries
|
||||||
|
const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
||||||
|
|
||||||
|
|
||||||
|
shimInit({ nodeSqlite: sqlite3 });
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await afterEachCleanUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await afterAllCleanUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
@ -34,7 +34,6 @@ const { FileApiDriverLocal } = require('../file-api-driver-local');
|
|||||||
const { FileApiDriverWebDav } = require('../file-api-driver-webdav.js');
|
const { FileApiDriverWebDav } = require('../file-api-driver-webdav.js');
|
||||||
const { FileApiDriverDropbox } = require('../file-api-driver-dropbox.js');
|
const { FileApiDriverDropbox } = require('../file-api-driver-dropbox.js');
|
||||||
const { FileApiDriverOneDrive } = require('../file-api-driver-onedrive.js');
|
const { FileApiDriverOneDrive } = require('../file-api-driver-onedrive.js');
|
||||||
const { FileApiDriverAmazonS3 } = require('../file-api-driver-amazon-s3.js');
|
|
||||||
import SyncTargetRegistry from '../SyncTargetRegistry';
|
import SyncTargetRegistry from '../SyncTargetRegistry';
|
||||||
const SyncTargetMemory = require('../SyncTargetMemory.js');
|
const SyncTargetMemory = require('../SyncTargetMemory.js');
|
||||||
const SyncTargetFilesystem = require('../SyncTargetFilesystem.js');
|
const SyncTargetFilesystem = require('../SyncTargetFilesystem.js');
|
||||||
@ -60,7 +59,6 @@ import Synchronizer from '../Synchronizer';
|
|||||||
import SyncTargetNone from '../SyncTargetNone';
|
import SyncTargetNone from '../SyncTargetNone';
|
||||||
import { setRSA } from '../services/e2ee/ppk';
|
import { setRSA } from '../services/e2ee/ppk';
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
const { S3Client } = require('@aws-sdk/client-s3');
|
|
||||||
const { Dirnames } = require('../services/synchronizer/utils/types');
|
const { Dirnames } = require('../services/synchronizer/utils/types');
|
||||||
import RSA from '../services/e2ee/RSA.node';
|
import RSA from '../services/e2ee/RSA.node';
|
||||||
import { State as ShareState } from '../services/share/reducer';
|
import { State as ShareState } from '../services/share/reducer';
|
||||||
@ -627,6 +625,13 @@ async function initFileApi() {
|
|||||||
const appDir = await api.appDirectory();
|
const appDir = await api.appDirectory();
|
||||||
fileApi = new FileApi(appDir, new FileApiDriverOneDrive(api));
|
fileApi = new FileApi(appDir, new FileApiDriverOneDrive(api));
|
||||||
} else if (syncTargetId_ === SyncTargetRegistry.nameToId('amazon_s3')) {
|
} else if (syncTargetId_ === SyncTargetRegistry.nameToId('amazon_s3')) {
|
||||||
|
// (Most of?) the @aws-sdk libraries depend on an old version of uuid
|
||||||
|
// that doesn't work with jest (without converting ES6 exports to CommonJS).
|
||||||
|
//
|
||||||
|
// Require it dynamically so that this doesn't break test environments that
|
||||||
|
// aren't configured to do this conversion.
|
||||||
|
const { FileApiDriverAmazonS3 } = require('../file-api-driver-amazon-s3.js');
|
||||||
|
const { S3Client } = require('@aws-sdk/client-s3');
|
||||||
|
|
||||||
// We make sure for S3 tests run in band because tests
|
// We make sure for S3 tests run in band because tests
|
||||||
// share the same directory which will cause locking errors.
|
// share the same directory which will cause locking errors.
|
||||||
|
Loading…
Reference in New Issue
Block a user