feat: Add scripts for sending admin and test notes via Nostr
- Introduce `send_admin_note.js` for sending community announcements to Nostr relays. - Implement `send_test_note.js` for testing note sending with specified private key and relay URL. - Enhance `NostrFeed.vue` to filter notes based on admin pubkeys and display appropriate titles and descriptions for different feed types. - Update `Home.vue` to use the announcements feed type for the Nostr feed component.
This commit is contained in:
parent
ee7eb461c4
commit
97db2a2fec
5 changed files with 314 additions and 54 deletions
|
|
@ -4,10 +4,13 @@ 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 { Badge } from '@/components/ui/badge'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { Megaphone } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
relays?: string[]
|
||||
feedType?: 'all' | 'announcements' | 'events' | 'general'
|
||||
}>()
|
||||
|
||||
const notes = ref<NostrNote[]>([])
|
||||
|
|
@ -17,13 +20,50 @@ const error = ref<Error | null>(null)
|
|||
const relayUrls = props.relays || JSON.parse(import.meta.env.VITE_NOSTR_RELAYS as string)
|
||||
const { disconnect } = useNostr({ relays: relayUrls })
|
||||
|
||||
// Get admin/moderator pubkeys from environment
|
||||
// These should be hex pubkeys of trusted moderators/admins
|
||||
const adminPubkeys = import.meta.env.VITE_ADMIN_PUBKEYS
|
||||
? JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS as string)
|
||||
: []
|
||||
|
||||
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 })
|
||||
|
||||
// Configure fetch options based on feed type
|
||||
const fetchOptions: Parameters<typeof client.fetchNotes>[0] = {
|
||||
limit: 50,
|
||||
includeReplies: false
|
||||
}
|
||||
|
||||
// Filter by authors based on feed type
|
||||
if (props.feedType === 'announcements') {
|
||||
// Only show notes from admin/moderator pubkeys
|
||||
if (adminPubkeys.length > 0) {
|
||||
fetchOptions.authors = adminPubkeys
|
||||
} else {
|
||||
// If no admin pubkeys configured, show placeholder
|
||||
notes.value = []
|
||||
return
|
||||
}
|
||||
} else if (props.feedType === 'general') {
|
||||
// Show notes from everyone EXCEPT admins (if configured)
|
||||
// Note: This would require client-side filtering after fetch
|
||||
// For now, we'll fetch all and filter
|
||||
}
|
||||
// 'all' and 'events' types get all notes (no author filter)
|
||||
|
||||
notes.value = await client.fetchNotes(fetchOptions)
|
||||
|
||||
// Client-side filtering for 'general' feed (exclude admin posts)
|
||||
if (props.feedType === 'general' && adminPubkeys.length > 0) {
|
||||
notes.value = notes.value.filter(note => !adminPubkeys.includes(note.pubkey))
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err : new Error('Failed to load notes')
|
||||
} finally {
|
||||
|
|
@ -31,6 +71,36 @@ async function loadNotes() {
|
|||
}
|
||||
}
|
||||
|
||||
function isAdminPost(pubkey: string): boolean {
|
||||
return adminPubkeys.includes(pubkey)
|
||||
}
|
||||
|
||||
function getFeedTitle(): string {
|
||||
switch (props.feedType) {
|
||||
case 'announcements':
|
||||
return 'Community Announcements'
|
||||
case 'events':
|
||||
return 'Events & Calendar'
|
||||
case 'general':
|
||||
return 'General Discussion'
|
||||
default:
|
||||
return 'Community Feed'
|
||||
}
|
||||
}
|
||||
|
||||
function getFeedDescription(): string {
|
||||
switch (props.feedType) {
|
||||
case 'announcements':
|
||||
return 'Important announcements from community administrators'
|
||||
case 'events':
|
||||
return 'Upcoming events and calendar updates'
|
||||
case 'general':
|
||||
return 'Community discussions and general posts'
|
||||
default:
|
||||
return 'Latest posts from the community'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadNotes()
|
||||
})
|
||||
|
|
@ -48,8 +118,11 @@ function formatDate(timestamp: number): string {
|
|||
<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>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Megaphone v-if="feedType === 'announcements'" class="h-5 w-5" />
|
||||
{{ getFeedTitle() }}
|
||||
</CardTitle>
|
||||
<CardDescription>{{ getFeedDescription() }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea class="h-[600px] w-full pr-4">
|
||||
|
|
@ -62,17 +135,40 @@ function formatDate(timestamp: number): string {
|
|||
</div>
|
||||
|
||||
<div v-else-if="notes.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
No notes found
|
||||
<div v-if="feedType === 'announcements' && adminPubkeys.length === 0" class="space-y-2">
|
||||
<p>No admin pubkeys configured</p>
|
||||
<p class="text-xs">Set VITE_ADMIN_PUBKEYS environment variable</p>
|
||||
</div>
|
||||
<p v-else>No {{ feedType || 'notes' }} found</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<Card v-for="note in notes" :key="note.id" class="p-4">
|
||||
<Card
|
||||
v-for="note in notes"
|
||||
:key="note.id"
|
||||
:class="[
|
||||
'p-4 transition-all',
|
||||
isAdminPost(note.pubkey) && feedType !== 'announcements'
|
||||
? 'border-orange-200 bg-orange-50 dark:border-orange-800 dark:bg-orange-950'
|
||||
: ''
|
||||
]"
|
||||
>
|
||||
<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>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm">{{ note.pubkey.slice(0, 8) }}...</span>
|
||||
<Badge
|
||||
v-if="isAdminPost(note.pubkey)"
|
||||
variant="secondary"
|
||||
class="text-xs bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"
|
||||
>
|
||||
<Megaphone class="h-3 w-3 mr-1" />
|
||||
Admin
|
||||
</Badge>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">{{ formatDate(note.created_at) }}</span>
|
||||
</div>
|
||||
<p class="text-sm">{{ note.content }}</p>
|
||||
<p class="text-sm whitespace-pre-wrap">{{ 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>
|
||||
|
|
@ -90,6 +186,9 @@ function formatDate(timestamp: number): string {
|
|||
>
|
||||
Refresh
|
||||
</button>
|
||||
<span v-if="adminPubkeys.length > 0" class="text-xs text-muted-foreground">
|
||||
{{ adminPubkeys.length }} admin(s) configured
|
||||
</span>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue