import { useCallback, useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { toast } from 'sonner' import { useNostr } from '@/providers/NostrProvider' import { getKindName } from '@/lib/event-kinds' import { SimplePool, type Event, type Filter } from 'nostr-tools' import { cn } from '@/lib/utils' interface FilterState { kinds: string authors: string ids: string since: string until: string limit: string } const EMPTY_FILTER: FilterState = { kinds: '', authors: '', ids: '', since: '', until: '', limit: '50', } function getRelayWsUrl(): string { const loc = window.location const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:' return `${proto}//${loc.host}` } function truncate(s: string, n: number): string { if (!s) return '' return s.length > n ? s.slice(0, n) + '...' : s } function truncatePubkey(pk: string): string { if (!pk) return '' return pk.slice(0, 8) + '...' + pk.slice(-8) } function buildFilter(state: FilterState): Filter { const filter: Filter = {} if (state.kinds.trim()) { filter.kinds = state.kinds .split(',') .map((s) => parseInt(s.trim(), 10)) .filter((n) => !isNaN(n)) } if (state.authors.trim()) { filter.authors = state.authors .split(',') .map((s) => s.trim()) .filter(Boolean) } if (state.ids.trim()) { filter.ids = state.ids .split(',') .map((s) => s.trim()) .filter(Boolean) } if (state.since) { filter.since = Math.floor(new Date(state.since).getTime() / 1000) } if (state.until) { filter.until = Math.floor(new Date(state.until).getTime() / 1000) } const limit = parseInt(state.limit, 10) filter.limit = !isNaN(limit) && limit > 0 ? limit : 50 return filter } export default function EventBrowserTab() { const { pubkey, publish } = useNostr() const [filterState, setFilterState] = useState(EMPTY_FILTER) const [jsonMode, setJsonMode] = useState(false) const [jsonText, setJsonText] = useState('') const [jsonError, setJsonError] = useState('') const [events, setEvents] = useState([]) const [expandedIds, setExpandedIds] = useState>(new Set()) const [isLoading, setIsLoading] = useState(false) const poolRef = useRef(null) useEffect(() => { poolRef.current = new SimplePool() return () => { poolRef.current?.close(poolRef.current ? [getRelayWsUrl()] : []) } }, []) const updateField = (field: keyof FilterState, value: string) => { setFilterState((prev) => ({ ...prev, [field]: value })) } const toggleJsonMode = () => { if (!jsonMode) { const filter = buildFilter(filterState) setJsonText(JSON.stringify(filter, null, 2)) setJsonError('') } setJsonMode(!jsonMode) } const queryEvents = useCallback(async () => { setIsLoading(true) setEvents([]) try { let filter: Filter if (jsonMode) { try { filter = JSON.parse(jsonText) setJsonError('') } catch (e) { setJsonError(e instanceof Error ? e.message : 'Invalid JSON') setIsLoading(false) return } } else { filter = buildFilter(filterState) } if (!filter.limit) filter.limit = 50 const pool = poolRef.current if (!pool) { toast.error('Pool not initialized') setIsLoading(false) return } const relayUrl = getRelayWsUrl() const collected: Event[] = [] await new Promise((resolve) => { const sub = pool.subscribeMany([relayUrl], filter, { onevent(evt: Event) { collected.push(evt) }, oneose() { sub.close() resolve() }, }) // Safety timeout setTimeout(() => { sub.close() resolve() }, 15000) }) collected.sort((a, b) => b.created_at - a.created_at) setEvents(collected) toast.success(`Found ${collected.length} events`) } catch (e) { toast.error(`Query failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setIsLoading(false) } }, [filterState, jsonMode, jsonText]) const toggleExpand = (id: string) => { setExpandedIds((prev) => { const next = new Set(prev) if (next.has(id)) { next.delete(id) } else { next.add(id) } return next }) } const copyEvent = (event: Event) => { navigator.clipboard.writeText(JSON.stringify(event)) toast.success('Copied to clipboard') } const deleteEvent = async (event: Event) => { if (!pubkey) { toast.error('Login required to delete events') return } if (!confirm(`Delete event ${event.id.slice(0, 16)}...?`)) return try { const tags: string[][] = [['k', String(event.kind)]] tags.push(['e', event.id]) const draft = { kind: 5, content: 'Request for deletion of the event.', tags, created_at: Math.floor(Date.now() / 1000), } await publish(draft) toast.success('Deletion request published') setEvents((prev) => prev.filter((e) => e.id !== event.id)) } catch (e) { toast.error(`Delete failed: ${e instanceof Error ? e.message : String(e)}`) } } const clearFilter = () => { setFilterState(EMPTY_FILTER) setJsonText('') setJsonError('') } return (

Event Browser

{jsonMode ? (