fix(desktop): avoid redundant editor hydration

This commit is contained in:
tianma-if committed 2026-09-01 22:11:10 +08:00
1 parent 07a3da44d7
commit cf62dc1b92
3 files changed
+51 -5

No files matched your search

+11 -4
View File
@@ -222,7 +222,10 @@ import {
ResourceActionMenu,
type NoteLinkHintPosition,
} from "./editor/EditorPaneChrome";
import { resolveEditorDraftState } from "./editor/editor-draft-state";
import {
resolveEditorDraftState,
shouldReplaceEditorDocument,
} from "./editor/editor-draft-state";
import type { EdgeEverPluginHost, PluginEditorAdapter } from "@/lib/plugins/plugin-host";
import {
useEditorResourceActions,
@@ -2212,10 +2215,14 @@ const RichEditorPane = ({
} = resolvedDraft;
const alreadyHydratedSameMemo = sameMemo && hydratedMemoIdRef.current === memo.id;
const currentEditorDocument = isEditorReady(currentEditor)
? currentEditor.getJSON() as TiptapDoc
: null;
const shouldReplaceDocument = shouldReplaceEditorDocument(currentEditorDocument, nextContent);
const editorMarkdownMatches = Boolean(
alreadyHydratedSameMemo &&
isEditorReady(currentEditor) &&
docToMarkdown(currentEditor.getJSON() as TiptapDoc) === nextMarkdown &&
currentEditorDocument &&
docToMarkdown(currentEditorDocument) === nextMarkdown &&
title === nextTitle &&
tagsText === nextTagsText
);
@@ -2275,7 +2282,7 @@ const RichEditorPane = ({
setMarkdownSource(nextMarkdown);
setMobilePlainTextElementValue(mobileTextAreaRef.current, nextMarkdown);
if (isEditorReady(currentEditor)) {
if (isEditorReady(currentEditor) && shouldReplaceDocument) {
try {
currentEditor.commands.setContent(nextContent);
} catch (err) {
@@ -1,5 +1,8 @@
import { describe, expect, test } from "bun:test";
import { resolveEditorDraftState } from "./editor-draft-state.ts";
import {
resolveEditorDraftState,
shouldReplaceEditorDocument,
} from "./editor-draft-state.ts";
const doc = (text) => ({ type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text }] }] });
@@ -125,3 +128,28 @@ describe("editor draft source resolution", () => {
expect(state.hasUnsavedChanges).toBe(false);
});
});
describe("editor document hydration", () => {
test("does not replace a newly mounted empty note with the same empty document", () => {
const emptyDocument = { type: "doc", content: [{ type: "paragraph" }] };
expect(shouldReplaceEditorDocument(emptyDocument, emptyDocument)).toBe(false);
});
test("replaces the mounted memo when a recovered local draft has different content", () => {
expect(shouldReplaceEditorDocument(doc("remote body"), doc("recovered draft"))).toBe(true);
});
test("does not treat rich documents with the same markdown text as identical", () => {
const plainDocument = doc("same body");
const themedDocument = {
type: "doc",
content: [{ type: "edgeeverThemeBlock", attrs: { theme: "paper" }, content: plainDocument.content }],
};
expect(shouldReplaceEditorDocument(plainDocument, themedDocument)).toBe(true);
});
test("hydrates when the current editor document cannot be read", () => {
expect(shouldReplaceEditorDocument(null, doc("remote body"))).toBe(true);
});
});
@@ -24,6 +24,17 @@ export type EditorDraftState = {
hasUnsavedChanges: boolean;
};
/**
* TipTap is created with the memo snapshot before the asynchronous local-draft
* lookup finishes. Replacing that already-identical document during hydration
* resets the ProseMirror view and selection, which is visible in slower desktop
* runtimes as a second editor paint.
*/
export const shouldReplaceEditorDocument = (
currentDocument: TiptapDoc | null,
nextDocument: TiptapDoc,
) => currentDocument === null || JSON.stringify(currentDocument) !== JSON.stringify(nextDocument);
type ResolveEditorDraftStateInput = {
memo: MemoDetail;
draft?: LocalDraft | null;