You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +02:00
Desktop: Multiple window support (#11181)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
@ -14,6 +14,7 @@ import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
|
||||
import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect';
|
||||
|
||||
const logger = Logger.create('useFormNote');
|
||||
|
||||
@ -22,9 +23,8 @@ export interface OnLoadEvent {
|
||||
}
|
||||
|
||||
export interface HookDependencies {
|
||||
syncStarted: boolean;
|
||||
decryptionStarted: boolean;
|
||||
noteId: string;
|
||||
editorId: string;
|
||||
isProvisional: boolean;
|
||||
titleInputRef: RefObject<HTMLInputElement>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@ -66,26 +66,85 @@ function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
type InitNoteStateCallback = (note: NoteEntity, isNew: boolean)=> Promise<FormNote>;
|
||||
const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId: string, noteId: string, initNoteState: InitNoteStateCallback) => {
|
||||
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
||||
// a new refresh.
|
||||
const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
|
||||
useQueuedAsyncEffect(async (event) => {
|
||||
if (formNoteRefreshScheduled <= 0) return;
|
||||
if (formNoteRef.current.hasChanged) {
|
||||
logger.info('Form note changed between scheduling a refresh and the refresh itself. Cancelling the refresh.');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Sync has finished and note has never been changed - reloading it');
|
||||
|
||||
const loadNote = async () => {
|
||||
const n = await Note.load(noteId);
|
||||
if (event.cancelled || formNoteRef.current.hasChanged) return;
|
||||
|
||||
// Normally should not happened because if the note has been deleted via sync
|
||||
// it would not have been loaded in the editor (due to note selection changing
|
||||
// on delete)
|
||||
if (!n) {
|
||||
logger.warn('Trying to reload note that has been deleted:', noteId);
|
||||
return;
|
||||
}
|
||||
|
||||
await initNoteState(n, false);
|
||||
if (event.cancelled) return;
|
||||
setFormNoteRefreshScheduled(0);
|
||||
};
|
||||
|
||||
await loadNote();
|
||||
}, [formNoteRefreshScheduled, noteId, editorId, initNoteState]);
|
||||
|
||||
const refreshFormNote = useCallback(() => {
|
||||
// Increase the counter to cancel any ongoing refresh attempts
|
||||
// and start a new one.
|
||||
setFormNoteRefreshScheduled(formNoteRefreshScheduled + 1);
|
||||
}, [formNoteRefreshScheduled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteId) return ()=>{};
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
type ChangeEventSlice = { itemId: string; changeId: string };
|
||||
const listener = ({ itemId, changeId }: ChangeEventSlice) => {
|
||||
// If this change came from the current editor, it should already be
|
||||
// handled by calls to `setFormNote`. If events from the current editor
|
||||
// aren't ignored, most user-activated note changes (e.g. a keypress)
|
||||
// cause the note to refresh. (Undesired refreshes can cause the cursor to jump).
|
||||
const isExternalChange = !(changeId ?? 'unknown').endsWith(editorId);
|
||||
if (itemId === noteId && !cancelled && isExternalChange) {
|
||||
if (formNoteRef.current.hasChanged) return;
|
||||
refreshFormNote();
|
||||
}
|
||||
};
|
||||
eventManager.on(EventName.ItemChange, listener);
|
||||
|
||||
return () => {
|
||||
eventManager.off(EventName.ItemChange, listener);
|
||||
cancelled = true;
|
||||
};
|
||||
}, [formNoteRef, noteId, editorId, refreshFormNote]);
|
||||
};
|
||||
|
||||
export default function useFormNote(dependencies: HookDependencies) {
|
||||
const {
|
||||
syncStarted, decryptionStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad,
|
||||
} = dependencies;
|
||||
const { noteId, editorId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad } = dependencies;
|
||||
|
||||
const [formNote, setFormNote] = useState<FormNote>(defaultFormNote());
|
||||
const [isNewNote, setIsNewNote] = useState(false);
|
||||
const prevSyncStarted = usePrevious(syncStarted);
|
||||
const prevDecryptionStarted = usePrevious(decryptionStarted);
|
||||
const previousNoteId = usePrevious(formNote.id);
|
||||
const [resourceInfos, setResourceInfos] = useState<ResourceInfos>({});
|
||||
|
||||
const formNoteRef = useRef(formNote);
|
||||
formNoteRef.current = formNote;
|
||||
|
||||
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
||||
// a new refresh.
|
||||
const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
|
||||
const initNoteState = useCallback(async (n: NoteEntity, isNewNote: boolean) => {
|
||||
const initNoteState: InitNoteStateCallback = useCallback(async (n, isNewNote) => {
|
||||
let originalCss = '';
|
||||
|
||||
if (n.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML) {
|
||||
@ -125,9 +184,9 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
logger.info('Cancelled note refresh -- form note changed while loading attached resources.');
|
||||
return null;
|
||||
}
|
||||
|
||||
setResourceInfos(resources);
|
||||
setFormNote(newFormNote);
|
||||
formNoteRef.current = newFormNote;
|
||||
|
||||
logger.debug('Resource info and form note set.');
|
||||
|
||||
@ -136,69 +195,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
return newFormNote;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (formNoteRefreshScheduled <= 0) return () => {};
|
||||
if (formNoteRef.current.hasChanged) {
|
||||
logger.info('Form note changed between scheduling a refresh and the refresh itself. Cancelling the refresh.');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
logger.info('Sync has finished and note has never been changed - reloading it');
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const loadNote = async () => {
|
||||
const n = await Note.load(noteId);
|
||||
if (cancelled) return;
|
||||
|
||||
// Normally should not happened because if the note has been deleted via sync
|
||||
// it would not have been loaded in the editor (due to note selection changing
|
||||
// on delete)
|
||||
if (!n) {
|
||||
logger.warn('Trying to reload note that has been deleted:', noteId);
|
||||
return;
|
||||
}
|
||||
|
||||
await initNoteState(n, false);
|
||||
|
||||
setFormNoteRefreshScheduled(0);
|
||||
};
|
||||
|
||||
void loadNote();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [formNoteRefreshScheduled, noteId, initNoteState]);
|
||||
|
||||
const refreshFormNote = useCallback(() => {
|
||||
// Increase the counter to cancel any ongoing refresh attempts
|
||||
// and start a new one.
|
||||
setFormNoteRefreshScheduled(formNoteRefreshScheduled + 1);
|
||||
}, [formNoteRefreshScheduled]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check that synchronisation has just finished - and
|
||||
// if the note has never been changed, we reload it.
|
||||
// If the note has already been changed, it's a conflict
|
||||
// that's already been handled by the synchronizer.
|
||||
const decryptionJustEnded = prevDecryptionStarted && !decryptionStarted;
|
||||
const syncJustEnded = prevSyncStarted && !syncStarted;
|
||||
|
||||
if (!decryptionJustEnded && !syncJustEnded) return;
|
||||
if (formNoteRef.current.hasChanged) return;
|
||||
|
||||
logger.debug('Sync or decryption finished with an unchanged formNote.');
|
||||
|
||||
// Refresh the form note.
|
||||
// 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.
|
||||
refreshFormNote();
|
||||
}, [
|
||||
prevSyncStarted, syncStarted,
|
||||
prevDecryptionStarted, decryptionStarted,
|
||||
refreshFormNote,
|
||||
]);
|
||||
useRefreshFormNoteOnChange(formNoteRef, editorId, noteId, initNoteState);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteId) {
|
||||
@ -296,14 +293,14 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
// changes, with no delay during which async code can run. Even a small delay (e.g. that introduced
|
||||
// by a setState -> useEffect) can lead to a race condition. See https://github.com/laurent22/joplin/issues/8960.
|
||||
const onSetFormNote: OnSetFormNote = useCallback(newFormNote => {
|
||||
let newNote;
|
||||
if (typeof newFormNote === 'function') {
|
||||
const newNote = newFormNote(formNoteRef.current);
|
||||
formNoteRef.current = newNote;
|
||||
setFormNote(newNote);
|
||||
newNote = newFormNote(formNoteRef.current);
|
||||
} else {
|
||||
formNoteRef.current = newFormNote;
|
||||
setFormNote(newFormNote);
|
||||
newNote = newFormNote;
|
||||
}
|
||||
formNoteRef.current = newNote;
|
||||
setFormNote(newNote);
|
||||
}, [setFormNote]);
|
||||
|
||||
return {
|
||||
|
Reference in New Issue
Block a user