diff --git a/src/lib/chat.svelte.ts b/src/lib/chat.svelte.ts index fa96e3e..6cab3ca 100644 --- a/src/lib/chat.svelte.ts +++ b/src/lib/chat.svelte.ts @@ -4,6 +4,7 @@ import { RELAY_URL } from "$lib/config"; import { auth } from "$lib/auth.svelte"; import { ingestNostrUser } from "$lib/profiles.svelte"; import { extractMentionPubkeys, buildPTagHints } from "$lib/mentions"; +import { convertForumUrls } from "$lib/linkify"; export type ChatMessageData = { id: string; @@ -131,6 +132,8 @@ export async function sendChatMessage( if (!currentGroup) throw new Error("No room selected"); const ownPubkey = await auth.signer.getPublicKey(); + content = convertForumUrls(content); + const previousRefs = messages .filter((m) => m.pubkey !== ownPubkey) .slice(-3) diff --git a/src/lib/components/ChatContent.svelte b/src/lib/components/ChatContent.svelte index 5c8f5c0..5f7684d 100644 --- a/src/lib/components/ChatContent.svelte +++ b/src/lib/components/ChatContent.svelte @@ -1,6 +1,11 @@ {#each tokens as t (t)}{#if t.type === "mention"}{@const u = - profiles[t.pubkey] ?? resolvedUsers[t.pubkey]}@{u?.shortName ?? t.fallback}{:else if t.type === "entity"}{t.label}{:else if t.type === "link"}{t.label}{:else}{t.value}{/if}{/each}@{u?.shortName ?? t.fallback}{:else if t.type === "entity"}{@const ref = t.id + ? resolvedThreads[t.id] + : undefined}{#if ref}{ref.title}{:else}{t.label}{/if}{:else if t.type === "link"}{t.label}{:else}{t.value}{/if}{/each} diff --git a/src/lib/components/ChatSidebar.svelte b/src/lib/components/ChatSidebar.svelte index 432e7f3..282aa76 100644 --- a/src/lib/components/ChatSidebar.svelte +++ b/src/lib/components/ChatSidebar.svelte @@ -15,6 +15,9 @@ import type { NostrUser } from "@nostr/gadgets/metadata"; import MentionAutocomplete from "$lib/components/MentionAutocomplete.svelte"; import ChatContent from "$lib/components/ChatContent.svelte"; + import { shortNostrEntity } from "$lib/linkify"; + import { resolveThreadRef } from "$lib/threadRefs"; + import * as nip19 from "@nostr/tools/nip19"; type Props = { expanded?: boolean; @@ -87,9 +90,43 @@ }); } + const ENTITY_PATTERN = "nostr:(note1[a-z0-9]+|nevent1[a-z0-9]+)"; + + // Resolved thread titles, keyed by the bech32 entity (note1…/nevent1…). + let resolvedTitles = $state>({}); + + $effect(() => { + const re = new RegExp(ENTITY_PATTERN, "gi"); + const entities = new Set(); + for (const m of messages) + for (const match of m.content.matchAll(re)) + entities.add(match[1].toLowerCase()); + for (const entity of entities) { + if (resolvedTitles[entity]) continue; + let id: string | null = null; + try { + const d = nip19.decode(entity); + if (d.type === "note") id = d.data; + else if (d.type === "nevent") id = d.data.id; + } catch { + // Invalid bech32, skip + } + if (!id) continue; + resolveThreadRef(id).then((ref) => { + if (ref) resolvedTitles = { ...resolvedTitles, [entity]: ref.title }; + }); + } + }); + function truncate(s: string, n = 60) { - // Collapse mentions to @… so the preview stays readable. - const stripped = s.replace(/nostr:(?:npub1|nprofile1)[a-z0-9]+/gi, "@…"); + // Collapse mentions to @… and show thread refs as their title. + const stripped = s + .replace(/nostr:(?:npub1|nprofile1)[a-z0-9]+/gi, "@…") + .replace( + new RegExp(ENTITY_PATTERN, "gi"), + (_m, entity) => + resolvedTitles[entity.toLowerCase()] ?? shortNostrEntity(entity), + ); const t = stripped.replace(/\s+/g, " ").trim(); return t.length > n ? t.slice(0, n) + "…" : t; } @@ -190,7 +227,7 @@