feat(app): add nostr social embeds with bech32 NIP-19 decoding

Create NostrEmbed.vue component that renders nostr:note1, nostr:npub1,
and nostr:nevent1 URIs as rich embedded cards. Add bech32 decoder
utility with NIP-19 TLV support for nevent/nprofile. Extend useNostr
with fetchNote() for single-note relay lookups. ChatMessage.vue now
detects and strips nostr URIs, rendering them as inline embed cards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-03 20:42:11 +00:00
co-authored by Claude Opus 4.6
parent 557f9e6220
commit 771e13aaf2
4 changed files with 366 additions and 0 deletions
@@ -75,6 +75,14 @@
/>
</div>
<div v-if="nostrUris.length > 0" class="mt-3 space-y-1.5" @click.stop>
<NostrEmbed
v-for="uri in nostrUris"
:key="uri"
:uri="uri"
/>
</div>
<div v-if="inlineNewsLinks.length > 0" class="mt-3 space-y-1" @click.stop>
<NewsCard
v-for="(link, i) in inlineNewsLinks"
@@ -186,6 +194,7 @@ import SongCard from '@/components/content/SongCard.vue'
import PodcastCard from '@/components/content/PodcastCard.vue'
import PlaceCard from '@/components/content/PlaceCard.vue'
import NewsCard from '@/components/content/NewsCard.vue'
import NostrEmbed from '@/components/chat/NostrEmbed.vue'
const props = withDefaults(
defineProps<{
@@ -224,6 +233,13 @@ const inlineNewsLinks = computed(() => inlineContent.value.newsLinks ?? [])
const inlineWebsitesLinks = computed(() => inlineContent.value.websitesLinks ?? [])
const inlineMagazineSections = computed(() => inlineContent.value.magazineSections ?? [])
const NOSTR_URI_RE = /nostr:(note1[a-z0-9]{58}|npub1[a-z0-9]{58}|nevent1[a-z0-9]+|nprofile1[a-z0-9]+)/g
const nostrUris = computed(() => {
if (isUser.value) return []
const matches = props.message.content.match(NOSTR_URI_RE)
return matches ? [...new Set(matches)] : []
})
const isCodeResponse = computed(() =>
!isUser.value && props.triggeringQuery.trim().toLowerCase() === '/code'
)
@@ -261,6 +277,7 @@ const displayText = computed(() => {
if (isUser.value) return props.message.content
let text = stripContentTags(props.message.content)
if (inlineNewsLinks.value.length > 0 || inlineWebsitesLinks.value.length > 0) text = stripMarkdownLinks(text)
if (nostrUris.value.length > 0) text = text.replace(NOSTR_URI_RE, '').replace(/\n{3,}/g, '\n\n').trim()
return text
})