aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-03-02 19:26:41 -0800
committerAdam Soltys <[email protected]>2026-03-02 19:26:41 -0800
commit90f29dce3d675f850e48ede2edb187e6c53b84da (patch)
tree104f3f5a397b94571be6272bc28329f24ee2a893 /src/components
parent9e7ea9ad86fcd7ac11d4b5f845ebe558089ddd2d (diff)
downloadcoinos-ui-90f29dce3d675f850e48ede2edb187e6c53b84da.tar.gz
coinos-ui-90f29dce3d675f850e48ede2edb187e6c53b84da.zip
parse links
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ChatMessage.svelte87
-rw-r--r--src/components/Event.svelte2
2 files changed, 89 insertions, 0 deletions
diff --git a/src/components/ChatMessage.svelte b/src/components/ChatMessage.svelte
new file mode 100644
index 00000000..7283430f
--- /dev/null
+++ b/src/components/ChatMessage.svelte
@@ -0,0 +1,87 @@
+<script lang="ts">
+ import { parseContent, isImage, isVideo, isAudio } from "$lib/parseContent";
+
+ let { content, tags = [] }: { content: string; tags?: any[] } = $props();
+
+ let parts = $derived(parseContent({ content, tags }));
+
+ const truncUrl = (url: string) => {
+ const short = url.replace(/^https?:\/\/(www\.)?/, "");
+ return short.length > 40 ? short.slice(0, 40) + "..." : short;
+ };
+
+ const resolveName = async (pubkey: string): Promise<string> => {
+ try {
+ const res = await fetch(`/api/users/${pubkey}`);
+ if (res.ok) {
+ const info = await res.json();
+ if (!info.anon && info.username) return `@${info.username}`;
+ }
+ } catch {}
+ return `@${pubkey.slice(0, 8)}...`;
+ };
+</script>
+
+{#each parts as { type, value }}
+ {#if type === "text"}
+ {value}
+ {:else if type === "newline"}
+ <br />
+ {:else if type === "link"}
+ {#if isImage(value.url)}
+ <a href={value.url} target="_blank" rel="noopener noreferrer">
+ <img src={value.url} alt="" class="chat-img" />
+ </a>
+ {:else if isVideo(value.url)}
+ <!-- svelte-ignore a11y_media_has_caption -->
+ <video src={value.url} controls class="chat-video"></video>
+ {:else if isAudio(value.url)}
+ <audio src={value.url} controls>
+ <a href={value.url}>{truncUrl(value.url)}</a>
+ </audio>
+ {:else}
+ <a href={value.url} target="_blank" rel="noopener noreferrer" class="chat-link">{truncUrl(value.url)}</a>
+ {/if}
+ {:else if type === "topic"}
+ <a href={`/tag/${encodeURIComponent(value)}`} class="chat-topic">#{value}</a>
+ {:else if type.match(/^nostr:n(profile|pub)$/)}
+ {#await resolveName(value.pubkey)}
+ <a href={`/${value.pubkey}`} class="chat-mention">@{value.pubkey.slice(0, 8)}...</a>
+ {:then name}
+ <a href={`/${value.pubkey}`} class="chat-mention">{name}</a>
+ {/await}
+ {:else if type === "nostr:note"}
+ <a href={`/e/${value.id}`} class="chat-link">nostr:{value.entity?.slice(0, 16)}...</a>
+ {:else if type === "nostr:nevent"}
+ <a href={`/e/${value.id}`} class="chat-link">nostr:{value.entity?.slice(0, 16)}...</a>
+ {/if}
+{/each}
+
+<style>
+ .chat-img {
+ max-width: 100%;
+ border-radius: 8px;
+ margin: 4px 0;
+ }
+
+ .chat-video {
+ max-width: 100%;
+ border-radius: 8px;
+ margin: 4px 0;
+ }
+
+ .chat-link {
+ text-decoration: underline;
+ word-break: break-all;
+ }
+
+ .chat-topic {
+ font-weight: bold;
+ color: var(--accent, #6366f1);
+ }
+
+ .chat-mention {
+ font-weight: bold;
+ color: var(--accent, #6366f1);
+ }
+</style>
diff --git a/src/components/Event.svelte b/src/components/Event.svelte
index 7a9c75d4..9d25e26e 100644
--- a/src/components/Event.svelte
+++ b/src/components/Event.svelte
@@ -70,6 +70,8 @@
{value}
{:else if type === "link"}
<Media {value} {minimal} />
+ {:else if type === "topic"}
+ <a href={`/tag/${encodeURIComponent(value)}`} class="font-bold text-accent">#{value}</a>
{:else if type.match(/^nostr:np(rofile|ub)$/)}
<a href={`/${value.pubkey}`} class="font-bold">@{names[value.pubkey]}</a>
{/if}