1
0
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:
Henry Heino
2024-11-08 07:32:05 -08:00
committed by GitHub
parent cbef725cc8
commit 4a88d6ff7a
163 changed files with 3303 additions and 1475 deletions

View File

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