+
{{ note.authorName?.charAt(0)?.toUpperCase() ?? '?' }}
-
+
{{ note.authorName ?? 'anon' }}
-
+
{{ note.nip05 }}
-
+
{{ formatTime(note.created_at) }}
-
+
{{ note.content }}
-
+
kind:{{ note.kind }}
-
- {{ note.replies }} replies
-
-
- {{ note.zaps }} sats
-
+
No notes match your search
@@ -131,30 +167,15 @@
diff --git a/packages/app/src/composables/useNostr.ts b/packages/app/src/composables/useNostr.ts
new file mode 100644
index 00000000..665ec2a9
--- /dev/null
+++ b/packages/app/src/composables/useNostr.ts
@@ -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
([])
+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", , ]
+ 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,
+ }
+}