mobile: fix editor cross contamination (#10157)

1. Use noteId defined in editorMessage when saving content, never use tabId since it can point to a different note since a tab can load a different note while a note save message is coming across the bridge.
2. If saving fails and editor saves save payloads in localStorage, add the noteId to them so we know which note the content belongs to.
3. Pending content saves must keep noteId and edit time so when we save them later, they save into the correct note and if the content is newer, we skip saving.
4. Fix the debounce key so cross contamination can never occur in new notes between two tabs.
This commit is contained in:
Ammar Ahmed
2026-07-30 22:55:06 +05:00
committed by GitHub
parent f7c755f2d6
commit 4765657423
5 changed files with 141 additions and 54 deletions

View File

@@ -153,18 +153,17 @@ export function useEditorController({
const titleChange = useCallback(async (title: string) => {
if (!isReactNative()) return;
const currentSessionId = globalThis.sessionId;
post(
EditorEvents.contentchange,
undefined,
tabRef.current.id,
tabRef.current.session?.noteId
);
const editedAt = Date.now();
const tabId = tabRef.current.id;
const noteId = tabRef.current.session?.noteId;
post(EditorEvents.contentchange, undefined, tabId, noteId);
const params = [
{
title
},
tabRef.current.id,
tabRef.current.session?.noteId,
tabId,
noteId,
currentSessionId,
1000
];
@@ -186,12 +185,12 @@ export function useEditorController({
`Saving title failed, setting pending request ${pendingTitleIds.length}`
);
if (params[2]) {
pendingSaveRequests.setTitle(params);
pendingSaveRequests.setTitle(params, editedAt);
}
const element = document.getElementById("editor-saving-failed-overlay");
if (element) {
element.style.display = "flex";
editors[tabRef.current.id]?.commands?.blur();
editors[tabId]?.commands?.blur();
element.focus();
}
});
@@ -217,26 +216,36 @@ export function useEditorController({
return;
}
const currentSessionId = globalThis.sessionId;
post(
EditorEvents.contentchange,
undefined,
tabRef.current.id,
tabRef.current.session?.noteId
);
const tabId = tabRef.current.id;
const noteId = tabRef.current.session?.noteId;
post(EditorEvents.contentchange, undefined, tabId, noteId);
if (!editor) return;
if (typeof timers.current.change === "number") {
clearTimeout(timers.current?.change);
}
timers.current.change = setTimeout(async () => {
if (tabRef.current.session?.noteId !== noteId) {
logger(
"info",
`Edit discarded, tab ${tabId} moved from note ${noteId} to ${tabRef.current.session?.noteId}`
);
return;
}
if (editorControllers[tabId]?.loading) {
logger("info", "Edit discarded, tab is in loading state");
return;
}
const editedAt = Date.now();
htmlContentRef.current = editor.getHTML();
const params = [
{
html: htmlContentRef.current,
ignoreEdit: ignoreEdit
},
tabRef.current.id,
tabRef.current.session?.noteId,
tabId,
noteId,
currentSessionId,
5000
];
@@ -262,7 +271,7 @@ export function useEditorController({
}`
);
if (params[2]) {
pendingSaveRequests.setContent(params);
pendingSaveRequests.setContent(params, editedAt);
}
const element = document.getElementById(

View File

@@ -23,13 +23,14 @@ class PendingSaveRequests {
static TITLES = "pendingTitles";
static CONTENT = "pendingContents";
async setTitle(value: any) {
async setTitle(value: any, editedAt: number) {
const pendingTitles = JSON.parse(
this.get(PendingSaveRequests.TITLES) || "[]"
);
(pendingTitles as any[]).push({
id: randId("title-pending"),
editedAt,
params: value
});
return localStorage.setItem(
@@ -45,13 +46,14 @@ class PendingSaveRequests {
return pendingTitles;
}
async setContent(value: any) {
async setContent(value: any, editedAt: number) {
const pendingContents = JSON.parse(
this.get(PendingSaveRequests.CONTENT) || "[]"
);
(pendingContents as any[]).push({
id: randId("content-pending"),
editedAt,
params: value
});
return localStorage.setItem(
@@ -118,7 +120,10 @@ class PendingSaveRequests {
const pendingTitles = await this.getPendingTitles();
this.remove(PendingSaveRequests.TITLES);
for (const pending of pendingTitles) {
if (pending.params[0]) pending.params[0].pendingChanges = true;
if (pending.params[0]) {
pending.params[0].pendingChanges = true;
pending.params[0].pendingChangesAt = pending.editedAt;
}
await postAsyncWithTimeout(EditorEvents.title, ...pending.params);
}
};
@@ -127,7 +132,10 @@ class PendingSaveRequests {
const pendingContents = await this.getPendingContent();
this.remove(PendingSaveRequests.CONTENT);
for (const pending of pendingContents) {
if (pending.params[0]) pending.params[0].pendingChanges = true;
if (pending.params[0]) {
pending.params[0].pendingChanges = true;
pending.params[0].pendingChangesAt = pending.editedAt;
}
await postAsyncWithTimeout(EditorEvents.content, ...pending.params);
}
};