Skip to content

Commit b5cc8f6

Browse files
committed
fix(files): render the file-viewer placeholder through the live node views
The collaborative markdown viewer painted a static generateHTML placeholder while the Yjs doc seeded, then swapped to the live editor. generateHTML runs only schema renderHTML — never the React node views or the ProseMirror decoration plugins — so every node whose live appearance comes from a node view or a decoration rendered differently in the placeholder and visibly repainted on the swap: syntax highlighting popped in, mention-chip icons shifted their labels, mermaid blocks jumped from source to diagram, and media embeds appeared out of nowhere. Render the placeholder through a read-only editor that shares the live editor's extension set instead. It uses the same node views and decoration plugins, so the placeholder is pixel-identical to the live editor and the swap neither repaints nor reflows — highlighting, mention icons, images, mermaid (via its existing SVG cache), and embeds (which already reserve their aspect-ratio box) all render up front. The placeholder editor carries no Collaboration extension, Y.Doc, or awareness, so it structurally cannot write to the shared document, preserving the seed-only-on-server invariant; editable={false} disables every editing affordance.
1 parent 77649d3 commit b5cc8f6

1 file changed

Lines changed: 37 additions & 20 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { memo, useCallback, useEffect, useRef, useState } from 'react'
44
import { cn, toast } from '@sim/emcn'
55
import { FILE_DOC_SEED, type JoinFileDocError } from '@sim/realtime-protocol/file-doc'
6-
import { type Extensions, generateHTML, type JSONContent } from '@tiptap/core'
6+
import type { Extensions, JSONContent } from '@tiptap/core'
77
import { isChangeOrigin } from '@tiptap/extension-collaboration'
88
import { Fragment, Slice } from '@tiptap/pm/model'
99
import { NodeSelection } from '@tiptap/pm/state'
@@ -81,6 +81,34 @@ const STREAM_REPARSE_THROTTLE_MS = 120
8181
/** Debounce before naming a still-untitled file after its leading heading, so it fires once typing settles. */
8282
const DERIVE_TITLE_DEBOUNCE_MS = 600
8383

84+
/**
85+
* Read-only editor that renders the already-fetched markdown while a collaborative doc waits for its
86+
* server seed, so the pane shows content instantly instead of blocking blank on the socket round-trip
87+
* (the seed IS the same markdown, so the swap on `collabReady` is seamless). It shares the live
88+
* editor's extension set ({@link EXTENSIONS}) — and therefore its node views and decoration plugins
89+
* (syntax highlighting, mention chips, images, mermaid diagrams, media embeds) — so the content is
90+
* pixel-identical to the live editor and the swap neither repaints nor reflows. It carries no
91+
* Collaboration extension, Y.Doc, or awareness, so it structurally cannot write to the shared document
92+
* (a client seed would duplicate it), and `editable={false}` disables every editing affordance. Mounted
93+
* only while the placeholder shows, so no second editor lingers once the live one takes over.
94+
*/
95+
function ReadOnlyPlaceholder({ content }: { content: JSONContent }) {
96+
const editor = useEditor({
97+
extensions: EXTENSIONS,
98+
editable: false,
99+
immediatelyRender: false,
100+
shouldRerenderOnTransaction: false,
101+
content,
102+
editorProps: { attributes: { class: 'rich-markdown-prose' } },
103+
})
104+
return (
105+
<EditorContent
106+
editor={editor}
107+
className='mx-auto flex w-full max-w-[48rem] flex-1 flex-col px-8 py-6'
108+
/>
109+
)
110+
}
111+
84112
interface RichMarkdownEditorProps {
85113
file: WorkspaceFileRecord
86114
workspaceId: string
@@ -332,16 +360,12 @@ export function LoadedRichMarkdownEditor({
332360
: parseMarkdownToDoc(splitFrontmatter(content).body)
333361
)
334362
/**
335-
* A read-only placeholder rendered from the already-fetched markdown while a collaborative doc waits
336-
* for its server seed, so the pane shows content instantly instead of blocking blank on the socket
337-
* round-trip (the seed IS the same markdown, so the swap on {@link collabReady} is seamless). Static
338-
* HTML — it holds no editor, doc, or awareness, so it structurally cannot write to the Y.Doc, which
339-
* is the invariant that keeps seeding out of the client (a client seed duplicates the doc).
363+
* The already-fetched markdown, parsed once, for the read-only {@link ReadOnlyPlaceholder} shown while
364+
* a collaborative doc waits for its server seed. Held only when collaborating; the local path seeds
365+
* the live editor directly, so it needs no placeholder.
340366
*/
341-
const [placeholderHtml] = useState<string | null>(() =>
342-
collaborationEnabled
343-
? generateHTML(parseMarkdownToDoc(splitFrontmatter(content).body), EXTENSIONS)
344-
: null
367+
const [placeholderContent] = useState<JSONContent | null>(() =>
368+
collaborationEnabled ? parseMarkdownToDoc(splitFrontmatter(content).body) : null
345369
)
346370
/**
347371
* The body currently shown in the editor: seeded from a settled mount, updated on local edits (via
@@ -1197,21 +1221,14 @@ export function LoadedRichMarkdownEditor({
11971221
if (images.length > 0) void insertImagesRef.current(images, at)
11981222
}}
11991223
/>
1200-
{showPlaceholder && placeholderHtml && (
1201-
// Instant read-only content while the collaborative doc seeds, swapped for the live editor
1202-
// once ready. The `ProseMirror` class is load-bearing: it gives the placeholder the same base
1203-
// text layout as the live editable (prosemirror-view sets `white-space: break-spaces` and
1204-
// disables ligatures), so a line wraps identically and never re-wraps on the swap.
1205-
<div
1206-
className='ProseMirror rich-markdown-prose mx-auto w-full max-w-[48rem] px-8 py-6'
1207-
dangerouslySetInnerHTML={{ __html: placeholderHtml }}
1208-
/>
1224+
{showPlaceholder && placeholderContent && (
1225+
<ReadOnlyPlaceholder content={placeholderContent} />
12091226
)}
12101227
<EditorContent
12111228
editor={editor}
12121229
className={cn(
12131230
'mx-auto flex w-full max-w-[48rem] flex-1 flex-col px-8 py-6 selection:bg-[var(--selection-bg)] selection:text-[var(--text-primary)] dark:selection:bg-[var(--selection-dark)] dark:selection:text-white',
1214-
showPlaceholder && placeholderHtml && 'hidden'
1231+
showPlaceholder && placeholderContent && 'hidden'
12151232
)}
12161233
/>
12171234
</div>

0 commit comments

Comments
 (0)