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
@@ -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