feat(app): add Nostr feed integration with real relay connections
Create useNostr composable with raw WebSocket connections to public relays (relay.damus.io, nos.lol, relay.snort.social). Subscribe to kind:1 notes with limit 50. Update NostrGrid to use real data instead of mock notes. Lazy-load on tab activation, clean disconnect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
31f2c2b261
commit
ebaee4c2b9
@@ -44,6 +44,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto custom-scrollbar px-4 pt-3 pb-16 space-y-2">
|
<div class="flex-1 overflow-y-auto custom-scrollbar px-4 pt-3 pb-16 space-y-2">
|
||||||
|
<!-- Loading state -->
|
||||||
|
<div
|
||||||
|
v-if="!isConnected && notes.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center py-12 gap-3"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 animate-spin"
|
||||||
|
:class="isDark ? 'text-white/30' : 'text-gray-400'"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-xs" :class="isDark ? 'text-white/30' : 'text-gray-400'">
|
||||||
|
Connecting to relays...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-for="note in filteredNotes"
|
v-for="note in filteredNotes"
|
||||||
:key="note.id"
|
:key="note.id"
|
||||||
@@ -54,74 +73,91 @@
|
|||||||
@click="$emit('selectNote', note)"
|
@click="$emit('selectNote', note)"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-2.5">
|
<div class="flex items-start gap-2.5">
|
||||||
<!-- Avatar -->
|
<div
|
||||||
<div class="w-8 h-8 rounded-full shrink-0 flex items-center justify-center text-[10px] font-bold"
|
class="w-8 h-8 rounded-full shrink-0 flex items-center justify-center text-[10px] font-bold"
|
||||||
:class="isDark ? 'bg-purple-500/20 text-purple-400' : 'bg-purple-100 text-purple-600'">
|
:class="isDark ? 'bg-purple-500/20 text-purple-400' : 'bg-purple-100 text-purple-600'"
|
||||||
|
>
|
||||||
{{ note.authorName?.charAt(0)?.toUpperCase() ?? '?' }}
|
{{ note.authorName?.charAt(0)?.toUpperCase() ?? '?' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<span class="text-xs font-semibold truncate"
|
<span
|
||||||
:class="isDark ? 'text-white/80' : 'text-gray-800'">
|
class="text-xs font-semibold truncate"
|
||||||
|
:class="isDark ? 'text-white/80' : 'text-gray-800'"
|
||||||
|
>
|
||||||
{{ note.authorName ?? 'anon' }}
|
{{ note.authorName ?? 'anon' }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="note.nip05" class="text-[9px] truncate"
|
<span
|
||||||
:class="isDark ? 'text-purple-400/60' : 'text-purple-500/60'">
|
v-if="note.nip05"
|
||||||
|
class="text-[9px] truncate"
|
||||||
|
:class="isDark ? 'text-purple-400/60' : 'text-purple-500/60'"
|
||||||
|
>
|
||||||
{{ note.nip05 }}
|
{{ note.nip05 }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[9px] ml-auto shrink-0"
|
<span
|
||||||
:class="isDark ? 'text-white/20' : 'text-gray-300'">
|
class="text-[9px] ml-auto shrink-0"
|
||||||
|
:class="isDark ? 'text-white/20' : 'text-gray-300'"
|
||||||
|
>
|
||||||
{{ formatTime(note.created_at) }}
|
{{ formatTime(note.created_at) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[11px] mt-1 leading-relaxed line-clamp-3"
|
<p
|
||||||
:class="isDark ? 'text-white/60' : 'text-gray-600'">
|
class="text-[11px] mt-1 leading-relaxed line-clamp-3"
|
||||||
|
:class="isDark ? 'text-white/60' : 'text-gray-600'"
|
||||||
|
>
|
||||||
{{ note.content }}
|
{{ note.content }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-3 mt-2">
|
<div class="flex items-center gap-3 mt-2">
|
||||||
<span v-if="note.kind !== 1" class="text-[9px] px-1.5 py-0.5 rounded"
|
<span
|
||||||
:class="isDark ? 'bg-white/5 text-white/30' : 'bg-black/5 text-gray-400'">
|
v-if="note.kind !== 1"
|
||||||
|
class="text-[9px] px-1.5 py-0.5 rounded"
|
||||||
|
:class="isDark ? 'bg-white/5 text-white/30' : 'bg-black/5 text-gray-400'"
|
||||||
|
>
|
||||||
kind:{{ note.kind }}
|
kind:{{ note.kind }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="note.replies" class="text-[9px]"
|
|
||||||
:class="isDark ? 'text-white/25' : 'text-gray-400'">
|
|
||||||
{{ note.replies }} replies
|
|
||||||
</span>
|
|
||||||
<span v-if="note.zaps" class="text-[9px] text-amber-500/70">
|
|
||||||
{{ note.zaps }} sats
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Relay status -->
|
<!-- Relay status -->
|
||||||
<div class="mt-4 pt-4" :style="isDark
|
<div
|
||||||
? 'border-top: 1px solid rgba(255, 255, 255, 0.05)'
|
class="mt-4 pt-4"
|
||||||
: 'border-top: 1px solid rgba(0, 0, 0, 0.05)'"
|
:style="isDark
|
||||||
|
? 'border-top: 1px solid rgba(255, 255, 255, 0.05)'
|
||||||
|
: 'border-top: 1px solid rgba(0, 0, 0, 0.05)'"
|
||||||
>
|
>
|
||||||
<p class="text-[10px] font-medium mb-2"
|
<p
|
||||||
:class="isDark ? 'text-white/30' : 'text-gray-400'">
|
class="text-[10px] font-medium mb-2"
|
||||||
|
:class="isDark ? 'text-white/30' : 'text-gray-400'"
|
||||||
|
>
|
||||||
Relays
|
Relays
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div
|
<div
|
||||||
v-for="relay in relays"
|
v-for="relay in relayStates"
|
||||||
:key="relay.url"
|
:key="relay.url"
|
||||||
class="flex items-center gap-2 text-[10px] px-2 py-1 rounded-lg"
|
class="flex items-center gap-2 text-[10px] px-2 py-1 rounded-lg"
|
||||||
:class="isDark ? 'bg-white/[0.02]' : 'bg-black/[0.02]'"
|
:class="isDark ? 'bg-white/[0.02]' : 'bg-black/[0.02]'"
|
||||||
>
|
>
|
||||||
<span class="w-1.5 h-1.5 rounded-full shrink-0"
|
<span
|
||||||
:class="relay.connected ? 'bg-emerald-500' : 'bg-red-400/60'" />
|
class="w-1.5 h-1.5 rounded-full shrink-0"
|
||||||
<span class="truncate font-mono"
|
:class="relay.connected ? 'bg-emerald-500' : 'bg-red-400/60'"
|
||||||
:class="isDark ? 'text-white/40' : 'text-gray-500'">
|
/>
|
||||||
|
<span
|
||||||
|
class="truncate font-mono"
|
||||||
|
:class="isDark ? 'text-white/40' : 'text-gray-500'"
|
||||||
|
>
|
||||||
{{ relay.url }}
|
{{ relay.url }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="filteredNotes.length === 0" class="flex items-center justify-center py-12">
|
<div
|
||||||
|
v-if="isConnected && filteredNotes.length === 0"
|
||||||
|
class="flex items-center justify-center py-12"
|
||||||
|
>
|
||||||
<p class="text-sm" :class="isDark ? 'text-white/30' : 'text-gray-400'">
|
<p class="text-sm" :class="isDark ? 'text-white/30' : 'text-gray-400'">
|
||||||
No notes match your search
|
No notes match your search
|
||||||
</p>
|
</p>
|
||||||
@@ -131,30 +167,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
import { useNostr, type NostrNote } from '@/composables/useNostr'
|
||||||
export interface NostrNote {
|
|
||||||
id: string
|
|
||||||
pubkey: string
|
|
||||||
authorName?: string
|
|
||||||
nip05?: string
|
|
||||||
kind: number
|
|
||||||
content: string
|
|
||||||
created_at: number
|
|
||||||
replies?: number
|
|
||||||
zaps?: number
|
|
||||||
tags?: string[][]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Relay {
|
|
||||||
url: string
|
|
||||||
connected: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
defineEmits<{ selectNote: [note: NostrNote] }>()
|
defineEmits<{ selectNote: [note: NostrNote] }>()
|
||||||
|
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
|
const { events: notes, isConnected, relayStates, connect } = useNostr()
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const activeKind = ref<number | null>(null)
|
const activeKind = ref<number | null>(null)
|
||||||
|
|
||||||
@@ -165,93 +186,6 @@ const noteKinds = [
|
|||||||
{ id: 6, label: 'Reposts' },
|
{ id: 6, label: 'Reposts' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const relays: Relay[] = [
|
|
||||||
{ url: 'wss://relay.damus.io', connected: true },
|
|
||||||
{ url: 'wss://relay.nostr.band', connected: true },
|
|
||||||
{ url: 'wss://nos.lol', connected: true },
|
|
||||||
{ url: 'wss://relay.snort.social', connected: false },
|
|
||||||
]
|
|
||||||
|
|
||||||
// Mock notes for demo — will be replaced with real Nostr websocket data
|
|
||||||
const mockNotes: NostrNote[] = [
|
|
||||||
{
|
|
||||||
id: 'note1abc',
|
|
||||||
pubkey: 'npub1abc',
|
|
||||||
authorName: 'fiatjaf',
|
|
||||||
nip05: 'fiatjaf@fiatjaf.com',
|
|
||||||
kind: 1,
|
|
||||||
content: 'nostr is the simplest open protocol that is able to create a censorship-resistant global "social" network once and for all.',
|
|
||||||
created_at: Date.now() / 1000 - 3600,
|
|
||||||
replies: 42,
|
|
||||||
zaps: 21000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'note2def',
|
|
||||||
pubkey: 'npub2def',
|
|
||||||
authorName: 'jb55',
|
|
||||||
nip05: 'jb55@jb55.com',
|
|
||||||
kind: 1,
|
|
||||||
content: 'Just shipped a new update to Damus. Lots of performance improvements and better zap UX. Let me know what you think!',
|
|
||||||
created_at: Date.now() / 1000 - 7200,
|
|
||||||
replies: 18,
|
|
||||||
zaps: 5000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'note3ghi',
|
|
||||||
pubkey: 'npub3ghi',
|
|
||||||
authorName: 'ODELL',
|
|
||||||
nip05: 'odell@citadeldispatch.com',
|
|
||||||
kind: 1,
|
|
||||||
content: 'Bitcoin and nostr are complementary protocols. One provides sound money, the other provides sound communication. Together they form the foundation of a free and open internet.',
|
|
||||||
created_at: Date.now() / 1000 - 14400,
|
|
||||||
replies: 31,
|
|
||||||
zaps: 15000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'note4jkl',
|
|
||||||
pubkey: 'npub4jkl',
|
|
||||||
authorName: 'Gigi',
|
|
||||||
nip05: 'gigi@dergigi.com',
|
|
||||||
kind: 30023,
|
|
||||||
content: 'New long-form article: "Why Nostr Matters" — exploring the implications of a truly censorship-resistant communication layer and what it means for the future of online discourse.',
|
|
||||||
created_at: Date.now() / 1000 - 28800,
|
|
||||||
replies: 12,
|
|
||||||
zaps: 50000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'note5mno',
|
|
||||||
pubkey: 'npub5mno',
|
|
||||||
authorName: 'Karnage',
|
|
||||||
kind: 1,
|
|
||||||
content: 'Building on nostr feels like the early web. Permissionless innovation, interoperable clients, and a community that actually cares about freedom. This is what I signed up for.',
|
|
||||||
created_at: Date.now() / 1000 - 36000,
|
|
||||||
replies: 8,
|
|
||||||
zaps: 3000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'note6pqr',
|
|
||||||
pubkey: 'npub6pqr',
|
|
||||||
authorName: 'Pablo',
|
|
||||||
nip05: 'pablo@fountain.fm',
|
|
||||||
kind: 1,
|
|
||||||
content: 'Podcasting 2.0 + Nostr = the future of content discovery. Imagine your podcast app pulling recommendations from your nostr social graph. We are building this.',
|
|
||||||
created_at: Date.now() / 1000 - 43200,
|
|
||||||
replies: 25,
|
|
||||||
zaps: 8000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'note7stu',
|
|
||||||
pubkey: 'npub7stu',
|
|
||||||
authorName: 'Calle',
|
|
||||||
nip05: 'calle@cashu.space',
|
|
||||||
kind: 1,
|
|
||||||
content: 'Cashu + Nostr ecash tokens make micropayments frictionless. Zap someone a few sats with zero fees and instant settlement. The Lightning Network as a settlement layer.',
|
|
||||||
created_at: Date.now() / 1000 - 50400,
|
|
||||||
replies: 15,
|
|
||||||
zaps: 12000,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function formatTime(ts: number): string {
|
function formatTime(ts: number): string {
|
||||||
const diff = Math.floor(Date.now() / 1000 - ts)
|
const diff = Math.floor(Date.now() / 1000 - ts)
|
||||||
if (diff < 60) return 'now'
|
if (diff < 60) return 'now'
|
||||||
@@ -261,7 +195,7 @@ function formatTime(ts: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filteredNotes = computed(() => {
|
const filteredNotes = computed(() => {
|
||||||
let result = mockNotes
|
let result = notes.value
|
||||||
if (activeKind.value !== null) {
|
if (activeKind.value !== null) {
|
||||||
result = result.filter(n => n.kind === activeKind.value)
|
result = result.filter(n => n.kind === activeKind.value)
|
||||||
}
|
}
|
||||||
@@ -275,4 +209,9 @@ const filteredNotes = computed(() => {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Connect to relays on mount (lazy-loaded on tab activation)
|
||||||
|
onMounted(() => {
|
||||||
|
connect()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { ref, shallowRef, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
export interface NostrEvent {
|
||||||
|
id: string
|
||||||
|
pubkey: string
|
||||||
|
kind: number
|
||||||
|
content: string
|
||||||
|
created_at: number
|
||||||
|
tags: string[][]
|
||||||
|
sig: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NostrNote {
|
||||||
|
id: string
|
||||||
|
pubkey: string
|
||||||
|
authorName?: string
|
||||||
|
nip05?: string
|
||||||
|
kind: number
|
||||||
|
content: string
|
||||||
|
created_at: number
|
||||||
|
tags: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RelayState {
|
||||||
|
url: string
|
||||||
|
ws: WebSocket | null
|
||||||
|
connected: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const RELAYS = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://nos.lol',
|
||||||
|
'wss://relay.snort.social',
|
||||||
|
]
|
||||||
|
|
||||||
|
const events = shallowRef<NostrNote[]>([])
|
||||||
|
const isConnected = ref(false)
|
||||||
|
const relayStates = ref<{ url: string; connected: boolean }[]>(
|
||||||
|
RELAYS.map(url => ({ url, connected: false }))
|
||||||
|
)
|
||||||
|
|
||||||
|
let relays: RelayState[] = []
|
||||||
|
let subscriptionId: string | null = null
|
||||||
|
let initialized = false
|
||||||
|
|
||||||
|
function generateSubId(): string {
|
||||||
|
return 'aiui-' + Math.random().toString(36).slice(2, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncatePubkey(pubkey: string): string {
|
||||||
|
if (pubkey.length <= 12) return pubkey
|
||||||
|
return pubkey.slice(0, 8) + '...' + pubkey.slice(-4)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEvent(data: unknown): NostrEvent | null {
|
||||||
|
if (!Array.isArray(data)) return null
|
||||||
|
// NIP-01: ["EVENT", <subscription_id>, <event>]
|
||||||
|
if (data[0] !== 'EVENT' || !data[2]) return null
|
||||||
|
const evt = data[2]
|
||||||
|
if (!evt.id || !evt.pubkey || typeof evt.kind !== 'number' || typeof evt.content !== 'string') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return evt as NostrEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEvent(evt: NostrEvent) {
|
||||||
|
// Deduplicate by ID
|
||||||
|
const existing = events.value.find(e => e.id === evt.id)
|
||||||
|
if (existing) return
|
||||||
|
|
||||||
|
const note: NostrNote = {
|
||||||
|
id: evt.id,
|
||||||
|
pubkey: evt.pubkey,
|
||||||
|
authorName: truncatePubkey(evt.pubkey),
|
||||||
|
kind: evt.kind,
|
||||||
|
content: evt.content,
|
||||||
|
created_at: evt.created_at,
|
||||||
|
tags: evt.tags ?? [],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert sorted by created_at (newest first)
|
||||||
|
const newEvents = [...events.value, note]
|
||||||
|
.sort((a, b) => b.created_at - a.created_at)
|
||||||
|
.slice(0, 200) // Cap at 200 events
|
||||||
|
|
||||||
|
events.value = newEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectRelay(relayState: RelayState) {
|
||||||
|
if (relayState.ws) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ws = new WebSocket(relayState.url)
|
||||||
|
relayState.ws = ws
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
relayState.connected = true
|
||||||
|
updateRelayStates()
|
||||||
|
isConnected.value = relays.some(r => r.connected)
|
||||||
|
|
||||||
|
// Subscribe to kind:1 (text notes) with limit 50
|
||||||
|
if (!subscriptionId) subscriptionId = generateSubId()
|
||||||
|
const req = JSON.stringify([
|
||||||
|
'REQ',
|
||||||
|
subscriptionId,
|
||||||
|
{ kinds: [1], limit: 50 },
|
||||||
|
])
|
||||||
|
ws.send(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (msg) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(msg.data)
|
||||||
|
const evt = parseEvent(data)
|
||||||
|
if (evt) addEvent(evt)
|
||||||
|
} catch {
|
||||||
|
// Skip malformed messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
relayState.connected = false
|
||||||
|
relayState.ws = null
|
||||||
|
updateRelayStates()
|
||||||
|
isConnected.value = relays.some(r => r.connected)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = () => {
|
||||||
|
// Will trigger onclose
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
relayState.connected = false
|
||||||
|
updateRelayStates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRelayStates() {
|
||||||
|
relayStates.value = relays.map(r => ({
|
||||||
|
url: r.url,
|
||||||
|
connected: r.connected,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
if (initialized) return
|
||||||
|
initialized = true
|
||||||
|
|
||||||
|
relays = RELAYS.map(url => ({ url, ws: null, connected: false }))
|
||||||
|
updateRelayStates()
|
||||||
|
relays.forEach(r => connectRelay(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (subscriptionId) {
|
||||||
|
relays.forEach(r => {
|
||||||
|
if (r.ws && r.connected) {
|
||||||
|
try {
|
||||||
|
r.ws.send(JSON.stringify(['CLOSE', subscriptionId]))
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
subscriptionId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
relays.forEach(r => {
|
||||||
|
if (r.ws) {
|
||||||
|
r.ws.close()
|
||||||
|
r.ws = null
|
||||||
|
r.connected = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
updateRelayStates()
|
||||||
|
isConnected.value = false
|
||||||
|
initialized = false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNostr() {
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Clean up on component unmount
|
||||||
|
disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
isConnected,
|
||||||
|
relayStates,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user