From 12fd552e1bd15b2877679bb9c8e720c5391eb9ce Mon Sep 17 00:00:00 2001 From: Khushvendra Date: Sat, 4 Apr 2026 16:48:29 +0530 Subject: [PATCH 1/5] feat: add room mentions and clickable room/relay refs --- package.json | 4 +- src/app/components/Content.svelte | 3 + src/app/components/ContentMinimal.svelte | 3 + src/app/components/ContentText.svelte | 24 ++++++ src/app/editor/RoomReferenceExtension.ts | 42 +++++++++++ src/app/editor/RoomReferenceNodeView.ts | 28 +++++++ src/app/editor/RoomSuggestion.svelte | 17 +++++ src/app/editor/index.ts | 68 ++++++++++++++++- src/lib/content-text.ts | 95 ++++++++++++++++++++++++ tests/content-text.test.ts | 47 ++++++++++++ 10 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 src/app/components/ContentText.svelte create mode 100644 src/app/editor/RoomReferenceExtension.ts create mode 100644 src/app/editor/RoomReferenceNodeView.ts create mode 100644 src/app/editor/RoomSuggestion.svelte create mode 100644 src/lib/content-text.ts create mode 100644 tests/content-text.test.ts diff --git a/package.json b/package.json index caa76285..bad0504b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "tauri:icons": "tauri icon assets/logo.png --output src-tauri/icons", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test": "vitest run", "lint": "prettier --check src && eslint src", "format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write", "format:all": "prettier --write src", @@ -39,7 +40,8 @@ "tailwindcss": "^4.2.2", "typescript": "^5.9.3", "typescript-eslint": "^8.53.1", - "vite": "^5.4.21" + "vite": "^5.4.21", + "vitest": "^2.1.9" }, "type": "module", "dependencies": { diff --git a/src/app/components/Content.svelte b/src/app/components/Content.svelte index e252b0bb..6af6d8d0 100644 --- a/src/app/components/Content.svelte +++ b/src/app/components/Content.svelte @@ -24,6 +24,7 @@ import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" + import ContentText from "@app/components/ContentText.svelte" import ContentToken from "@app/components/ContentToken.svelte" import ContentEmoji from "@app/components/ContentEmoji.svelte" import ContentCode from "@app/components/ContentCode.svelte" @@ -155,6 +156,8 @@ {#each shortContent as parsed, i} {#if isNewline(parsed) && !isBlock(i - 1)} + {:else if isText(parsed)} + {:else if isTopic(parsed)} {:else if isEmoji(parsed)} diff --git a/src/app/components/ContentMinimal.svelte b/src/app/components/ContentMinimal.svelte index e2e22bfa..a5a98439 100644 --- a/src/app/components/ContentMinimal.svelte +++ b/src/app/components/ContentMinimal.svelte @@ -22,6 +22,7 @@ import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" + import ContentText from "@app/components/ContentText.svelte" import ContentToken from "@app/components/ContentToken.svelte" import ContentEmoji from "@app/components/ContentEmoji.svelte" import ContentCode from "@app/components/ContentCode.svelte" @@ -105,6 +106,8 @@ {#each shortContent as parsed, i} {#if isNewline(parsed)} + {:else if isText(parsed)} + {:else if isTopic(parsed)} {:else if isEmoji(parsed)} diff --git a/src/app/components/ContentText.svelte b/src/app/components/ContentText.svelte new file mode 100644 index 00000000..f3347347 --- /dev/null +++ b/src/app/components/ContentText.svelte @@ -0,0 +1,24 @@ + + +{#each parts as part, i (i)} + {#if part.type === "room"} + {part.value} + {:else if part.type === "relay"} + {part.value} + {:else} + {part.value} + {/if} +{/each} diff --git a/src/app/editor/RoomReferenceExtension.ts b/src/app/editor/RoomReferenceExtension.ts new file mode 100644 index 00000000..5a134764 --- /dev/null +++ b/src/app/editor/RoomReferenceExtension.ts @@ -0,0 +1,42 @@ +import {mergeAttributes, Node} from "@tiptap/core" +import {RoomReferenceNodeView} from "@app/editor/RoomReferenceNodeView" + +export const RoomReferenceExtension = Node.create({ + name: "roomref", + + atom: true, + + inline: true, + + group: "inline", + + selectable: true, + + priority: 1000, + + addAttributes() { + return { + url: {default: undefined}, + h: {default: undefined}, + } + }, + + parseHTML() { + return [{tag: `span[data-type="${this.name}"]`}] + }, + + renderHTML({HTMLAttributes}) { + return ["span", mergeAttributes(HTMLAttributes, {"data-type": this.name}), "~"] + }, + + renderText({node}) { + const url = typeof node.attrs.url === "string" ? node.attrs.url : "" + const h = typeof node.attrs.h === "string" ? node.attrs.h : "" + + return `${url}'${h}` + }, + + addNodeView() { + return RoomReferenceNodeView + }, +}) diff --git a/src/app/editor/RoomReferenceNodeView.ts b/src/app/editor/RoomReferenceNodeView.ts new file mode 100644 index 00000000..c6503d4e --- /dev/null +++ b/src/app/editor/RoomReferenceNodeView.ts @@ -0,0 +1,28 @@ +import type {NodeViewRendererProps} from "@tiptap/core" +import {deriveRoom} from "@app/core/state" + +export const RoomReferenceNodeView = ({node}: NodeViewRendererProps) => { + const dom = document.createElement("span") + const url = typeof node.attrs.url === "string" ? node.attrs.url : "" + const h = typeof node.attrs.h === "string" ? node.attrs.h : "" + const room = deriveRoom(url, h) + + dom.classList.add("tiptap-object") + + const unsubRoom = room.subscribe($room => { + dom.textContent = `~${$room.name || h}` + }) + + return { + dom, + destroy: () => { + unsubRoom() + }, + selectNode() { + dom.classList.add("tiptap-active") + }, + deselectNode() { + dom.classList.remove("tiptap-active") + }, + } +} diff --git a/src/app/editor/RoomSuggestion.svelte b/src/app/editor/RoomSuggestion.svelte new file mode 100644 index 00000000..87cc80ca --- /dev/null +++ b/src/app/editor/RoomSuggestion.svelte @@ -0,0 +1,17 @@ + + +
+
~{$room.name || h}
+
{displayRelayUrl(url)}'{h}
+
diff --git a/src/app/editor/index.ts b/src/app/editor/index.ts index f56fd4b1..e2568b3e 100644 --- a/src/app/editor/index.ts +++ b/src/app/editor/index.ts @@ -4,7 +4,7 @@ import {get, derived} from "svelte/store" import {Router} from "@welshman/router" import {dec, inc} from "@welshman/lib" import {throttled} from "@welshman/store" -import type {PublishedProfile} from "@welshman/util" +import type {PublishedProfile, RoomMeta} from "@welshman/util" import { createSearch, profiles, @@ -14,12 +14,26 @@ import { getWotGraph, } from "@welshman/app" import type {FileAttributes} from "@welshman/editor" -import {Editor, MentionSuggestion, WelshmanExtension, editorProps} from "@welshman/editor" +import { + Editor, + MentionSuggestion, + TippySuggestion, + WelshmanExtension, + editorProps, +} from "@welshman/editor" import {escapeHtml} from "@lib/html" import {makeMentionNodeView} from "@app/editor/MentionNodeView" import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte" +import {RoomReferenceExtension} from "@app/editor/RoomReferenceExtension" +import RoomSuggestion from "@app/editor/RoomSuggestion.svelte" import {uploadFile} from "@app/core/commands" -import {deriveSpaceMembers} from "@app/core/state" +import { + deriveSpaceMembers, + makeRoomId, + splitRoomId, + userSpaceUrls, + roomsByUrl, +} from "@app/core/state" import {pushToast} from "@app/util/toast" export const makeEditor = async ({ @@ -82,11 +96,36 @@ export const makeEditor = async ({ }, ) + const roomReferenceSearch = derived( + [throttled(800, userSpaceUrls), throttled(800, roomsByUrl)], + ([$userSpaceUrls, $roomsByUrl]) => { + const roomIdByMeta = new WeakMap() + const options: RoomMeta[] = [] + + for (const roomUrl of $userSpaceUrls) { + for (const room of $roomsByUrl.get(roomUrl) || []) { + roomIdByMeta.set(room, makeRoomId(roomUrl, room.h)) + options.push(room) + } + } + + return createSearch(options, { + getValue: item => roomIdByMeta.get(item) || item.h, + fuseOptions: { + keys: ["name", "h"], + threshold: 0.3, + shouldSort: false, + }, + }) + }, + ) + const ed = new Editor({ content: typeof content === "string" ? escapeHtml(content) : content, editorProps, element: document.createElement("div"), extensions: [ + RoomReferenceExtension, WelshmanExtension.configure({ submit, extensions: { @@ -128,6 +167,29 @@ export const makeEditor = async ({ mount(ProfileSuggestion, {target, props: {value, url}}) + return target + }, + }), + TippySuggestion({ + char: "~", + name: "roomref", + editor: (this as any).editor, + search: (term: string) => get(roomReferenceSearch).searchValues(term), + updateSignal: roomReferenceSearch, + select: (id: string, props) => { + const [roomUrl, h] = splitRoomId(id) + + if (!roomUrl || !h) { + return + } + + return props.command({url: roomUrl, h}) + }, + createSuggestion: (value: string) => { + const target = document.createElement("div") + + mount(RoomSuggestion, {target, props: {value}}) + return target }, }), diff --git a/src/lib/content-text.ts b/src/lib/content-text.ts new file mode 100644 index 00000000..27e2ab7d --- /dev/null +++ b/src/lib/content-text.ts @@ -0,0 +1,95 @@ +import {isRelayUrl, normalizeRelayUrl} from "@welshman/util" + +export type ContentTextPart = + | {type: "text"; value: string} + | {type: "room"; value: string; url: string; h: string} + | {type: "relay"; value: string; url: string} + +const CONTENT_URL_PATTERN = /wss?:\/\/\S+/g +const TRAILING_PUNCTUATION_PATTERN = /[),.!?;:\]}]+$/ + +const appendTextPart = (parts: ContentTextPart[], text: string) => { + if (!text) { + return + } + + const last = parts.at(-1) + + if (last?.type === "text") { + last.value += text + + return + } + + parts.push({type: "text", value: text}) +} + +const splitTokenSuffix = (token: string) => { + const suffixMatch = token.match(TRAILING_PUNCTUATION_PATTERN) + const suffix = suffixMatch?.[0] || "" + + if (!suffix) { + return {trimmed: token, suffix} + } + + return { + trimmed: token.slice(0, -suffix.length), + suffix, + } +} + +const toRoomPart = (token: string) => { + const separatorIndex = token.indexOf("'") + + if (separatorIndex === -1) { + return undefined + } + + const url = token.slice(0, separatorIndex) + const h = token.slice(separatorIndex + 1) + + if (!h || !isRelayUrl(url)) { + return undefined + } + + return {type: "room", value: token, url: normalizeRelayUrl(url), h} as const +} + +const toRelayPart = (token: string) => { + if (!isRelayUrl(token)) { + return undefined + } + + return {type: "relay", value: token, url: normalizeRelayUrl(token)} as const +} + +export const parseContentTextParts = (text: string) => { + const parts: ContentTextPart[] = [] + let lastIndex = 0 + + for (const match of text.matchAll(CONTENT_URL_PATTERN)) { + const start = match.index || 0 + + appendTextPart(parts, text.slice(lastIndex, start)) + + const token = match[0] + const {trimmed, suffix} = splitTokenSuffix(token) + const roomPart = toRoomPart(trimmed) + const relayPart = toRelayPart(trimmed) + + if (roomPart) { + parts.push(roomPart) + } else if (relayPart) { + parts.push(relayPart) + } else { + appendTextPart(parts, trimmed) + } + + appendTextPart(parts, suffix) + lastIndex = start + token.length + } + + appendTextPart(parts, text.slice(lastIndex)) + + return parts +} diff --git a/tests/content-text.test.ts b/tests/content-text.test.ts new file mode 100644 index 00000000..b04e29b2 --- /dev/null +++ b/tests/content-text.test.ts @@ -0,0 +1,47 @@ +import {describe, expect, it} from "vitest" +import {parseContentTextParts} from "../src/lib/content-text" + +describe("parseContentTextParts", () => { + it("parses room references as relay_url'room_id", () => { + const parts = parseContentTextParts("Join wss://relay.example.com'general now") + + expect(parts).toHaveLength(3) + expect(parts[0]).toEqual({type: "text", value: "Join "}) + expect(parts[1]).toMatchObject({ + type: "room", + value: "wss://relay.example.com'general", + h: "general", + }) + expect(parts[2]).toEqual({type: "text", value: " now"}) + }) + + it("parses relay urls as relay parts", () => { + const parts = parseContentTextParts("Try wss://relay.example.com") + + expect(parts).toHaveLength(2) + expect(parts[0]).toEqual({type: "text", value: "Try "}) + expect(parts[1]).toMatchObject({ + type: "relay", + value: "wss://relay.example.com", + }) + }) + + it("keeps trailing punctuation outside links", () => { + const parts = parseContentTextParts("See wss://relay.example.com'chat), thanks") + + expect(parts).toHaveLength(3) + expect(parts[0]).toEqual({type: "text", value: "See "}) + expect(parts[1]).toMatchObject({ + type: "room", + value: "wss://relay.example.com'chat", + h: "chat", + }) + expect(parts[2]).toEqual({type: "text", value: "), thanks"}) + }) + + it("leaves non-relay urls as plain text", () => { + const parts = parseContentTextParts("https://example.com/path") + + expect(parts).toEqual([{type: "text", value: "https://example.com/path"}]) + }) +}) \ No newline at end of file -- 2.52.0 From ddb8391b02c624349e5cb78eb5daf37ef7114852 Mon Sep 17 00:00:00 2001 From: Khushvendra Date: Tue, 7 Apr 2026 00:30:46 +0530 Subject: [PATCH 2/5] refactor: move room and space mention rendering into link components --- package.json | 4 +- src/app/components/Content.svelte | 3 - src/app/components/ContentLinkBlock.svelte | 33 ++++++- src/app/components/ContentLinkInline.svelte | 25 +++++- src/app/components/ContentMinimal.svelte | 3 - src/app/components/ContentText.svelte | 24 ------ src/lib/content-text.ts | 95 --------------------- tests/content-text.test.ts | 47 ---------- 8 files changed, 53 insertions(+), 181 deletions(-) delete mode 100644 src/app/components/ContentText.svelte delete mode 100644 src/lib/content-text.ts delete mode 100644 tests/content-text.test.ts diff --git a/package.json b/package.json index bad0504b..caa76285 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "tauri:icons": "tauri icon assets/logo.png --output src-tauri/icons", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "test": "vitest run", "lint": "prettier --check src && eslint src", "format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write", "format:all": "prettier --write src", @@ -40,8 +39,7 @@ "tailwindcss": "^4.2.2", "typescript": "^5.9.3", "typescript-eslint": "^8.53.1", - "vite": "^5.4.21", - "vitest": "^2.1.9" + "vite": "^5.4.21" }, "type": "module", "dependencies": { diff --git a/src/app/components/Content.svelte b/src/app/components/Content.svelte index 6af6d8d0..e252b0bb 100644 --- a/src/app/components/Content.svelte +++ b/src/app/components/Content.svelte @@ -24,7 +24,6 @@ import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" - import ContentText from "@app/components/ContentText.svelte" import ContentToken from "@app/components/ContentToken.svelte" import ContentEmoji from "@app/components/ContentEmoji.svelte" import ContentCode from "@app/components/ContentCode.svelte" @@ -156,8 +155,6 @@ {#each shortContent as parsed, i} {#if isNewline(parsed) && !isBlock(i - 1)} - {:else if isText(parsed)} - {:else if isTopic(parsed)} {:else if isEmoji(parsed)} diff --git a/src/app/components/ContentLinkBlock.svelte b/src/app/components/ContentLinkBlock.svelte index c818f1ad..ac776d28 100644 --- a/src/app/components/ContentLinkBlock.svelte +++ b/src/app/components/ContentLinkBlock.svelte @@ -1,8 +1,10 @@ - -{#each parts as part, i (i)} - {#if part.type === "room"} - {part.value} - {:else if part.type === "relay"} - {part.value} - {:else} - {part.value} - {/if} -{/each} diff --git a/src/lib/content-text.ts b/src/lib/content-text.ts deleted file mode 100644 index 27e2ab7d..00000000 --- a/src/lib/content-text.ts +++ /dev/null @@ -1,95 +0,0 @@ -import {isRelayUrl, normalizeRelayUrl} from "@welshman/util" - -export type ContentTextPart = - | {type: "text"; value: string} - | {type: "room"; value: string; url: string; h: string} - | {type: "relay"; value: string; url: string} - -const CONTENT_URL_PATTERN = /wss?:\/\/\S+/g -const TRAILING_PUNCTUATION_PATTERN = /[),.!?;:\]}]+$/ - -const appendTextPart = (parts: ContentTextPart[], text: string) => { - if (!text) { - return - } - - const last = parts.at(-1) - - if (last?.type === "text") { - last.value += text - - return - } - - parts.push({type: "text", value: text}) -} - -const splitTokenSuffix = (token: string) => { - const suffixMatch = token.match(TRAILING_PUNCTUATION_PATTERN) - const suffix = suffixMatch?.[0] || "" - - if (!suffix) { - return {trimmed: token, suffix} - } - - return { - trimmed: token.slice(0, -suffix.length), - suffix, - } -} - -const toRoomPart = (token: string) => { - const separatorIndex = token.indexOf("'") - - if (separatorIndex === -1) { - return undefined - } - - const url = token.slice(0, separatorIndex) - const h = token.slice(separatorIndex + 1) - - if (!h || !isRelayUrl(url)) { - return undefined - } - - return {type: "room", value: token, url: normalizeRelayUrl(url), h} as const -} - -const toRelayPart = (token: string) => { - if (!isRelayUrl(token)) { - return undefined - } - - return {type: "relay", value: token, url: normalizeRelayUrl(token)} as const -} - -export const parseContentTextParts = (text: string) => { - const parts: ContentTextPart[] = [] - let lastIndex = 0 - - for (const match of text.matchAll(CONTENT_URL_PATTERN)) { - const start = match.index || 0 - - appendTextPart(parts, text.slice(lastIndex, start)) - - const token = match[0] - const {trimmed, suffix} = splitTokenSuffix(token) - const roomPart = toRoomPart(trimmed) - const relayPart = toRelayPart(trimmed) - - if (roomPart) { - parts.push(roomPart) - } else if (relayPart) { - parts.push(relayPart) - } else { - appendTextPart(parts, trimmed) - } - - appendTextPart(parts, suffix) - lastIndex = start + token.length - } - - appendTextPart(parts, text.slice(lastIndex)) - - return parts -} diff --git a/tests/content-text.test.ts b/tests/content-text.test.ts deleted file mode 100644 index b04e29b2..00000000 --- a/tests/content-text.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {describe, expect, it} from "vitest" -import {parseContentTextParts} from "../src/lib/content-text" - -describe("parseContentTextParts", () => { - it("parses room references as relay_url'room_id", () => { - const parts = parseContentTextParts("Join wss://relay.example.com'general now") - - expect(parts).toHaveLength(3) - expect(parts[0]).toEqual({type: "text", value: "Join "}) - expect(parts[1]).toMatchObject({ - type: "room", - value: "wss://relay.example.com'general", - h: "general", - }) - expect(parts[2]).toEqual({type: "text", value: " now"}) - }) - - it("parses relay urls as relay parts", () => { - const parts = parseContentTextParts("Try wss://relay.example.com") - - expect(parts).toHaveLength(2) - expect(parts[0]).toEqual({type: "text", value: "Try "}) - expect(parts[1]).toMatchObject({ - type: "relay", - value: "wss://relay.example.com", - }) - }) - - it("keeps trailing punctuation outside links", () => { - const parts = parseContentTextParts("See wss://relay.example.com'chat), thanks") - - expect(parts).toHaveLength(3) - expect(parts[0]).toEqual({type: "text", value: "See "}) - expect(parts[1]).toMatchObject({ - type: "room", - value: "wss://relay.example.com'chat", - h: "chat", - }) - expect(parts[2]).toEqual({type: "text", value: "), thanks"}) - }) - - it("leaves non-relay urls as plain text", () => { - const parts = parseContentTextParts("https://example.com/path") - - expect(parts).toEqual([{type: "text", value: "https://example.com/path"}]) - }) -}) \ No newline at end of file -- 2.52.0 From 540e9abe3d5c528a5f5f54e3172bda9e36e80c5c Mon Sep 17 00:00:00 2001 From: Khushvendra Date: Wed, 8 Apr 2026 14:51:26 +0530 Subject: [PATCH 3/5] refactor: align room mention labels with link rendering --- src/app/components/ContentLinkBlock.svelte | 45 +++++++++++++----- src/app/components/ContentLinkInline.svelte | 51 +++++++++++++++------ src/app/core/state.ts | 2 + src/app/editor/RoomReferenceNodeView.ts | 3 +- src/app/editor/RoomSuggestion.svelte | 7 +-- 5 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/app/components/ContentLinkBlock.svelte b/src/app/components/ContentLinkBlock.svelte index ac776d28..21b9de5c 100644 --- a/src/app/components/ContentLinkBlock.svelte +++ b/src/app/components/ContentLinkBlock.svelte @@ -1,6 +1,6 @@ -
-
~{$room.name || h}
-
{displayRelayUrl(url)}'{h}
+
+ {label}
-- 2.52.0 From ebc1e72b2838140bd08b45fcd869e7d33b1b035f Mon Sep 17 00:00:00 2001 From: Khushvendra Date: Thu, 9 Apr 2026 00:10:59 +0530 Subject: [PATCH 4/5] refactor: extract shared ContentLinkUrl renderer --- src/app/components/ContentLinkBlock.svelte | 153 +++++++------------- src/app/components/ContentLinkInline.svelte | 64 +------- src/app/components/ContentLinkUrl.svelte | 80 ++++++++++ 3 files changed, 139 insertions(+), 158 deletions(-) create mode 100644 src/app/components/ContentLinkUrl.svelte diff --git a/src/app/components/ContentLinkBlock.svelte b/src/app/components/ContentLinkBlock.svelte index 21b9de5c..1de5657b 100644 --- a/src/app/components/ContentLinkBlock.svelte +++ b/src/app/components/ContentLinkBlock.svelte @@ -1,76 +1,28 @@ - -
- {#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)} - - {:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)} - - {:else if roomReference || relayReference} -
- - {label} -
- {:else} - {#await loadPreview()} -
- -
- {:then preview} -
- {#if preview.image && !hideImage} - - {/if} -
- {preview.title || displayUrl(url)} -

{ellipsize(preview.description, 140)}

+{#if isRoomOrRelay} + +{:else} + +
+ {#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)} + + {:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)} + + {:else} + {#await loadPreview()} +
+
-
- {:catch} -

- Unable to load a preview for {url} -

- {/await} - {/if} -
- + {:then preview} +
+ {#if preview.image && !hideImage} + + {/if} +
+ {preview.title || displayUrl(url)} +

{ellipsize(preview.description, 140)}

+
+
+ {:catch} +

+ Unable to load a preview for {url} +

+ {/await} + {/if} +
+ +{/if} diff --git a/src/app/components/ContentLinkInline.svelte b/src/app/components/ContentLinkInline.svelte index abfe63cd..4b7754ea 100644 --- a/src/app/components/ContentLinkInline.svelte +++ b/src/app/components/ContentLinkInline.svelte @@ -1,69 +1,18 @@ @@ -78,8 +27,5 @@ {displayUrl(url)} {:else} - - - {label} - + {/if} diff --git a/src/app/components/ContentLinkUrl.svelte b/src/app/components/ContentLinkUrl.svelte new file mode 100644 index 00000000..7c6fe860 --- /dev/null +++ b/src/app/components/ContentLinkUrl.svelte @@ -0,0 +1,80 @@ + + + + {#if children} + {@render children()} + {:else if showIcon} + + {label} + {:else} + {label} + {/if} + -- 2.52.0 From bdc7d9e070803de3e283908696fdfa58bb0dba25 Mon Sep 17 00:00:00 2001 From: Khushvendra Date: Sat, 11 Apr 2026 18:56:23 +0530 Subject: [PATCH 5/5] refactor: simplify ContentLinkUrl rendering --- src/app/components/ContentLinkBlock.svelte | 19 +++++++++++-------- src/app/components/ContentLinkInline.svelte | 2 +- src/app/components/ContentLinkUrl.svelte | 17 ++--------------- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/app/components/ContentLinkBlock.svelte b/src/app/components/ContentLinkBlock.svelte index 1de5657b..a9bdd995 100644 --- a/src/app/components/ContentLinkBlock.svelte +++ b/src/app/components/ContentLinkBlock.svelte @@ -1,8 +1,9 @@ {#if isRoomOrRelay} - + {:else} - +
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}