From 1def4aa35afe8fe96343ea5976930cda7d381041 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Sun, 23 Aug 2026 21:10:43 +0800 Subject: [PATCH] fix(app): avoid replaying streamed markdown fade (#44396) --- .../src/components/markdown-solid.test.tsx | 20 ++++++++++++++++++ .../src/components/markdown-solid.tsx | 18 ++++++++-------- .../session-ui/src/components/markdown.tsx | 21 ++++++++++++------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/session-ui/src/components/markdown-solid.test.tsx b/packages/session-ui/src/components/markdown-solid.test.tsx index 5cd4c9a58b0..2fd7b95c5a7 100644 --- a/packages/session-ui/src/components/markdown-solid.test.tsx +++ b/packages/session-ui/src/components/markdown-solid.test.tsx @@ -40,3 +40,23 @@ test("keeps completed block text compact", () => { }, ]) }) + +test("marks words for animation only when requested", () => { + expect( + parseMarkdownNodes("

Hello

", true).flatMap((node) => (node.type === "element" ? node.children : [node]))[0], + ).toEqual({ + key: "0.0:0", + type: "word", + text: "Hello", + }) + expect( + parseMarkdownNodes("

Hello

", true, true).flatMap((node) => + node.type === "element" ? node.children : [node], + )[0], + ).toEqual({ + key: "0.0:0", + type: "word", + text: "Hello", + animate: true, + }) +}) diff --git a/packages/session-ui/src/components/markdown-solid.tsx b/packages/session-ui/src/components/markdown-solid.tsx index 1ea2befb14a..e302c0123b7 100644 --- a/packages/session-ui/src/components/markdown-solid.tsx +++ b/packages/session-ui/src/components/markdown-solid.tsx @@ -5,7 +5,7 @@ import { Dynamic, render } from "solid-js/web" type MarkdownNode = | { key: string; type: "element"; tag: string; attributes: Record; children: MarkdownNode[] } | { key: string; type: "text"; text: string } - | { key: string; type: "word"; text: string } + | { key: string; type: "word"; text: string; animate?: true } export function createMarkdownRenderer(root: HTMLDivElement, html: string, words: boolean) { const [nodes, setNodes] = createStore(parseMarkdownNodes(html, words)) @@ -17,8 +17,8 @@ export function createMarkdownRenderer(root: HTMLDivElement, html: string, words ready = true return { - update(next: string, nextWords: boolean) { - setNodes(reconcile(parseMarkdownNodes(next, nextWords), { key: "key" })) + update(next: string, nextWords: boolean, animate = true) { + setNodes(reconcile(parseMarkdownNodes(next, nextWords, animate), { key: "key" })) }, dispose, } @@ -30,7 +30,7 @@ function MarkdownDomNode(props: { node: MarkdownNode; animate: () => boolean }) if (node.type === "word") { let ref: HTMLSpanElement | undefined onMount(() => { - if (props.animate()) ref?.setAttribute("data-markdown-enter", "") + if (props.animate() && node.animate) ref?.setAttribute("data-markdown-enter", "") }) return ( @@ -45,19 +45,19 @@ function MarkdownDomNode(props: { node: MarkdownNode; animate: () => boolean }) ) } -export function parseMarkdownNodes(html: string, words: boolean) { +export function parseMarkdownNodes(html: string, words: boolean, animate = false) { const template = document.createElement("template") template.innerHTML = html - return Array.from(template.content.childNodes).flatMap((node, index) => parseNode(node, `${index}`, words)) + return Array.from(template.content.childNodes).flatMap((node, index) => parseNode(node, `${index}`, words, animate)) } -function parseNode(node: Node, key: string, words: boolean): MarkdownNode[] { +function parseNode(node: Node, key: string, words: boolean, animate: boolean): MarkdownNode[] { if (node instanceof Text) { if (!words) return [{ key, type: "text", text: node.data }] return node.data.split(/(\s+)/).flatMap((text, index): MarkdownNode[] => { if (!text) return [] if (/^\s+$/.test(text)) return [{ key: `${key}:${index}`, type: "text", text }] - return [{ key: `${key}:${index}`, type: "word", text }] + return [{ key: `${key}:${index}`, type: "word", text, ...(animate ? { animate: true as const } : {}) }] }) } if (!(node instanceof Element)) return [] @@ -67,7 +67,7 @@ function parseNode(node: Node, key: string, words: boolean): MarkdownNode[] { type: "element", tag: node.tagName.toLowerCase(), attributes: Object.fromEntries(Array.from(node.attributes).map((attribute) => [attribute.name, attribute.value])), - children: Array.from(node.childNodes).flatMap((child, index) => parseNode(child, `${key}.${index}`, words)), + children: Array.from(node.childNodes).flatMap((child, index) => parseNode(child, `${key}.${index}`, words, animate)), }, ] } diff --git a/packages/session-ui/src/components/markdown.tsx b/packages/session-ui/src/components/markdown.tsx index 2852674b7eb..4dd58e302a1 100644 --- a/packages/session-ui/src/components/markdown.tsx +++ b/packages/session-ui/src/components/markdown.tsx @@ -54,7 +54,10 @@ type RenderResult = { } const renderedCodeTokens = new WeakMap() -const renderedMarkdown = new WeakMap>() +const renderedMarkdown = new WeakMap< + HTMLDivElement, + { renderer: ReturnType; raw: string } +>() function escape(text: string) { return text @@ -184,7 +187,7 @@ function disposeRenderedMarkdown(root: Element) { ...Array.from(root.querySelectorAll("[data-markdown-block]")), ] blocks.forEach((block) => { - renderedMarkdown.get(block)?.dispose() + renderedMarkdown.get(block)?.renderer.dispose() renderedMarkdown.delete(block) }) } @@ -629,17 +632,21 @@ function updateBlock(container: HTMLDivElement, index: number, block: RenderedBl const html = source.innerHTML if (existing) { - const renderer = renderedMarkdown.get(existing) - if (renderer) { - renderer.update(html, block.mode === "live") + const rendered = renderedMarkdown.get(existing) + if (rendered) { + rendered.renderer.update(html, block.mode === "live", rendered.raw !== block.raw) + rendered.raw = block.raw return } existing.innerHTML = "" - renderedMarkdown.set(existing, createMarkdownRenderer(existing, html, block.mode === "live")) + renderedMarkdown.set(existing, { + renderer: createMarkdownRenderer(existing, html, block.mode === "live"), + raw: block.raw, + }) return } - renderedMarkdown.set(next, createMarkdownRenderer(next, html, block.mode === "live")) + renderedMarkdown.set(next, { renderer: createMarkdownRenderer(next, html, block.mode === "live"), raw: block.raw }) if (!current) { container.appendChild(next) return