feat(nostr): Implement Nostr Feed with real-time note fetching
- Add NostrFeed component to display Nostr network notes - Integrate date-fns for human-readable timestamp formatting - Enhance NostrClient with fetchNotes and subscribeToNotes methods - Implement loading, error, and empty state handling - Add scrollable card-based UI for note display - Configure dynamic relay selection with fallback to environment variables
This commit is contained in:
parent
68d6001880
commit
00f4bfa583
5 changed files with 195 additions and 3 deletions
96
src/components/nostr/NostrFeed.vue
Normal file
96
src/components/nostr/NostrFeed.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { NostrClient, type NostrNote } from '@/lib/nostr/client'
|
||||
import { useNostr } from '@/composables/useNostr'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
|
||||
const props = defineProps<{
|
||||
relays?: string[]
|
||||
}>()
|
||||
|
||||
const notes = ref<NostrNote[]>([])
|
||||
const isLoading = ref(true)
|
||||
const error = ref<Error | null>(null)
|
||||
|
||||
const relayUrls = props.relays || JSON.parse(import.meta.env.VITE_NOSTR_RELAYS as string)
|
||||
const { isConnected, connect, disconnect } = useNostr({ relays: relayUrls })
|
||||
|
||||
async function loadNotes() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
const client = new NostrClient({ relays: relayUrls })
|
||||
await client.connect()
|
||||
notes.value = await client.fetchNotes({ limit: 50 })
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err : new Error('Failed to load notes')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadNotes()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnect()
|
||||
})
|
||||
|
||||
function formatDate(timestamp: number): string {
|
||||
return formatDistanceToNow(timestamp * 1000, { addSuffix: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-3xl mx-auto p-4">
|
||||
<Card class="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Nostr Feed</CardTitle>
|
||||
<CardDescription>Latest notes from the nostr network</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea class="h-[600px] w-full pr-4">
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="text-center py-8 text-destructive">
|
||||
{{ error.message }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="notes.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
No notes found
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<Card v-for="note in notes" :key="note.id" class="p-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm">{{ note.pubkey.slice(0, 8) }}...</span>
|
||||
<span class="text-xs text-muted-foreground">{{ formatDate(note.created_at) }}</span>
|
||||
</div>
|
||||
<p class="text-sm">{{ note.content }}</p>
|
||||
<div class="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span>{{ note.replyCount }} replies</span>
|
||||
<span>{{ note.reactionCount }} reactions</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
<CardFooter class="flex justify-between">
|
||||
<button
|
||||
class="text-sm text-primary hover:underline"
|
||||
:disabled="isLoading"
|
||||
@click="loadNotes"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue