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