@ -742,6 +742,10 @@ function App() {
/ / E v e n t s l o g f u n c t i o n s
/ / E v e n t s l o g f u n c t i o n s
async function fetchEvents ( reset = false ) {
async function fetchEvents ( reset = false ) {
await fetchEventsFromRelay ( reset ) ;
await fetchEventsFromRelay ( reset ) ;
/ / A l s o f e t c h u s e r ' s o w n p r o f i l e f o r M y E v e n t s L o g
if ( user ? . pubkey ) {
await fetchAndCacheProfile ( user . pubkey ) ;
}
}
}
async function fetchAllEvents ( reset = false ) {
async function fetchAllEvents ( reset = false ) {
@ -780,6 +784,147 @@ function App() {
return date . toLocaleString ( ) ;
return date . toLocaleString ( ) ;
}
}
/ / F u n c t i o n t o d e l e t e a n e v e n t b y p u b l i s h i n g a k i n d 5 d e l e t e e v e n t
async function deleteEvent ( eventId , eventRawJson , eventAuthor = null ) {
if ( ! user ? . pubkey ) {
updateStatus ( 'Must be logged in to delete events' , 'error' ) ;
return ;
}
if ( ! window . nostr ) {
updateStatus ( 'Nostr extension not found' , 'error' ) ;
return ;
}
try {
/ / P a r s e t h e o r i g i n a l e v e n t t o g e t i t s d e t a i l s
const originalEvent = JSON . parse ( eventRawJson ) ;
/ / P e r m i s s i o n c h e c k : u s e r s c a n o n l y d e l e t e t h e i r o w n e v e n t s , a d m i n s c a n d e l e t e a n y e v e n t
const isOwnEvent = originalEvent . pubkey === user . pubkey ;
const isAdmin = user . permission === 'admin' ;
if ( ! isOwnEvent && ! isAdmin ) {
updateStatus ( 'You can only delete your own events' , 'error' ) ;
return ;
}
/ / C o n s t r u c t t h e d e l e t e e v e n t ( k i n d 5 ) a c c o r d i n g t o N I P - 0 9
const deleteEventTemplate = {
kind : 5 ,
created _at : Math . floor ( Date . now ( ) / 1000 ) ,
tags : [
[ 'e' , originalEvent . id ] ,
[ 'k' , originalEvent . kind . toString ( ) ]
] ,
content : isOwnEvent ? 'Deleted by author' : 'Deleted by admin'
} ;
/ / S i g n t h e d e l e t e e v e n t w i t h e x t e n s i o n
const signedDeleteEvent = await window . nostr . signEvent ( deleteEventTemplate ) ;
/ / P u b l i s h t h e d e l e t e e v e n t t o t h e r e l a y v i a W e b S o c k e t
await publishEventToRelay ( signedDeleteEvent ) ;
updateStatus ( 'Delete event published successfully' , 'success' ) ;
/ / R e f r e s h t h e e v e n t l i s t s t o r e f l e c t t h e d e l e t i o n
if ( isOwnEvent ) {
fetchEvents ( true ) ; / / R e f r e s h M y E v e n t s
}
if ( isAdmin ) {
fetchAllEvents ( true ) ; / / R e f r e s h A l l E v e n t s
}
} catch ( error ) {
updateStatus ( 'Failed to delete event: ' + error . message , 'error' ) ;
}
}
/ / F u n c t i o n t o p u b l i s h a n e v e n t t o t h e r e l a y v i a W e b S o c k e t
async function publishEventToRelay ( event , timeoutMs = 5000 ) {
return new Promise ( ( resolve , reject ) => {
let resolved = false ;
let ws ;
try {
ws = new WebSocket ( relayURL ( ) ) ;
} catch ( e ) {
reject ( new Error ( 'Failed to create WebSocket connection' ) ) ;
return ;
}
const timer = setTimeout ( ( ) => {
if ( ws && ws . readyState === 1 ) {
try { ws . close ( ) ; } catch ( _ ) { }
}
if ( ! resolved ) {
resolved = true ;
reject ( new Error ( 'Timeout publishing event' ) ) ;
}
} , timeoutMs ) ;
ws . onopen = ( ) => {
try {
/ / S e n d t h e e v e n t a s p e r N o s t r p r o t o c o l
const eventMessage = [ 'EVENT' , event ] ;
ws . send ( JSON . stringify ( eventMessage ) ) ;
} catch ( e ) {
clearTimeout ( timer ) ;
if ( ! resolved ) {
resolved = true ;
reject ( new Error ( 'Failed to send event: ' + e . message ) ) ;
}
}
} ;
ws . onmessage = ( msg ) => {
try {
const data = JSON . parse ( msg . data ) ;
const type = data [ 0 ] ;
if ( type === 'OK' ) {
const eventId = data [ 1 ] ;
const accepted = data [ 2 ] ;
const message = data [ 3 ] || '' ;
if ( eventId === event . id ) {
clearTimeout ( timer ) ;
try { ws . close ( ) ; } catch ( _ ) { }
if ( ! resolved ) {
resolved = true ;
if ( accepted ) {
resolve ( ) ;
} else {
reject ( new Error ( 'Event rejected: ' + message ) ) ;
}
}
}
}
} catch ( e ) {
/ / I g n o r e m a l f o r m e d m e s s a g e s
}
} ;
ws . onerror = ( error ) => {
clearTimeout ( timer ) ;
try { ws . close ( ) ; } catch ( _ ) { }
if ( ! resolved ) {
resolved = true ;
reject ( new Error ( 'WebSocket error' ) ) ;
}
} ;
ws . onclose = ( ) => {
clearTimeout ( timer ) ;
if ( ! resolved ) {
resolved = true ;
reject ( new Error ( 'WebSocket connection closed' ) ) ;
}
} ;
} ) ;
}
/ / S e c t i o n r e v e a l e r f u n c t i o n s
/ / S e c t i o n r e v e a l e r f u n c t i o n s
function toggleSection ( sectionKey ) {
function toggleSection ( sectionKey ) {
setExpandedSections ( prev => ( {
setExpandedSections ( prev => ( {
@ -1158,18 +1303,66 @@ function App() {
className = "cursor-pointer"
className = "cursor-pointer"
onClick = { ( ) => toggleEventExpansion ( event . id ) }
onClick = { ( ) => toggleEventExpansion ( event . id ) }
>
>
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between w-full" >
< div className = "flex items-center gap-3" >
< div className = "flex items-center gap-6 w-full" >
{ /* User avatar and info - separated with more space */ }
< div className = "flex items-center gap-3 min-w-0" >
{ user ? . pubkey && profileCache [ user . pubkey ] && (
< >
{ profileCache [ user . pubkey ] . picture && (
< img
src = { profileCache [ user . pubkey ] . picture }
alt = { profileCache [ user . pubkey ] . display _name || profileCache [ user . pubkey ] . name || 'User avatar' }
className = { ` w-8 h-8 rounded-full object-cover border h-16 ${ getThemeClasses ( 'border-gray-300' , 'border-gray-600' ) } ` }
onError = { ( e ) => {
e . currentTarget . style . display = 'none' ;
} }
/ >
) }
< div className = "flex flex-col flex-grow w-full" >
< span className = { ` text-sm font-medium ${ getTextClass ( ) } ` } >
{ profileCache [ user . pubkey ] . display _name || profileCache [ user . pubkey ] . name || ` ${ user . pubkey . slice ( 0 , 8 ) } ... ` }
< / span >
{ profileCache [ user . pubkey ] . display _name && profileCache [ user . pubkey ] . name && (
< span className = { ` text-xs ${ getTextClass ( ) } opacity-70 ` } >
{ profileCache [ user . pubkey ] . name }
< / span >
) }
< / div >
< / >
) }
{ user ? . pubkey && ! profileCache [ user . pubkey ] && (
< span className = { ` text-sm font-medium ${ getTextClass ( ) } ` } >
{ ` ${ user . pubkey . slice ( 0 , 8 ) } ... ` }
< / span >
) }
< / div >
{ /* Event metadata - separated to the right */ }
< div className = "flex items-center gap-3" >
< span className = { ` font-mono text-sm px-2 py-1 rounded ${ getThemeClasses ( 'bg-blue-100 text-blue-800' , 'bg-blue-900 text-blue-200' ) } ` } >
< span className = { ` font-mono text-sm px-2 py-1 rounded ${ getThemeClasses ( 'bg-blue-100 text-blue-800' , 'bg-blue-900 text-blue-200' ) } ` } >
Kind { event . kind }
Kind { event . kind }
< / span >
< / span >
< span className = { ` text-sm ${ getTextClass ( ) } ` } >
< span className = { ` text-sm ${ getTextClass ( ) } ` } >
{ formatTimestamp ( event . created _at ) }
{ formatTimestamp ( event . created _at ) }
< / span >
< / span >
< / div >
< / div >
< div className = "flex items-center gap-2 ml-auto" >
< div className = { ` text-lg rounded p-16 m-16 ${ getThemeClasses ( 'text-gray-700' , 'text-gray-300' ) } ` } >
{ expandedEventId === event . id ? '▼' : ' ' }
< / div >
< button
className = "bg-red-600 hover:bg-red-700 text-white text-xs px-1 py-1 rounded flex items-center"
onClick = { ( e ) => {
e . stopPropagation ( ) ;
deleteEvent ( event . id , event . raw _json ) ;
} }
title = "Delete this event"
>
🗑 ️
< / button >
< / div >
< / div >
< span className = { ` text-lg ${ getTextClass ( ) } ` } >
{ expandedEventId === event . id ? '▼' : '▶' }
< / span >
< / div >
< / div >
{ event . content && (
{ event . content && (
@ -1238,7 +1431,7 @@ function App() {
< / span >
< / span >
< / div >
< / div >
{ expandedSections . allEventsLog && (
{ expandedSections . allEventsLog && (
< div className = "p-2 bg-gray-900 rounded-lg mt-2" >
< div className = "p-2 bg-gray-900 rounded-lg mt-2 w-full " >
< div className = "mb-4" >
< div className = "mb-4" >
< p className = { ` text-sm ${ getTextClass ( ) } ` } > View all events from all users in reverse chronological order . Click on any event to view its raw JSON . < / p >
< p className = { ` text-sm ${ getTextClass ( ) } ` } > View all events from all users in reverse chronological order . Click on any event to view its raw JSON . < / p >
< / div >
< / div >
@ -1259,8 +1452,8 @@ function App() {
className = "cursor-pointer"
className = "cursor-pointer"
onClick = { ( ) => toggleAllEventExpansion ( event . id ) }
onClick = { ( ) => toggleAllEventExpansion ( event . id ) }
>
>
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between w-full " >
< div className = "flex items-center gap-6" >
< div className = "flex items-center gap-6 w-full " >
{ /* User avatar and info - separated with more space */ }
{ /* User avatar and info - separated with more space */ }
< div className = "flex items-center gap-3 min-w-0" >
< div className = "flex items-center gap-3 min-w-0" >
{ event . author && profileCache [ event . author ] && (
{ event . author && profileCache [ event . author ] && (
@ -1275,7 +1468,7 @@ function App() {
} }
} }
/ >
/ >
) }
) }
< div className = "flex flex-col min-w-0 " >
< div className = "flex flex-col flex-grow w-full " >
< span className = { ` text-sm font-medium ${ getTextClass ( ) } ` } >
< span className = { ` text-sm font-medium ${ getTextClass ( ) } ` } >
{ profileCache [ event . author ] . display _name || profileCache [ event . author ] . name || ` ${ event . author . slice ( 0 , 8 ) } ... ` }
{ profileCache [ event . author ] . display _name || profileCache [ event . author ] . name || ` ${ event . author . slice ( 0 , 8 ) } ... ` }
< / span >
< / span >
@ -1304,9 +1497,21 @@ function App() {
< / span >
< / span >
< / div >
< / div >
< / div >
< / div >
< span className = { ` text-lg ${ getTextClass ( ) } ` } >
< div className = "justify-end ml-auto rounded-full h-16 w-16 flex items-center justify-center" >
{ expandedAllEventId === event . id ? '▼' : '▶' }
< div className = { ` text-white text-xs px-4 py-4 rounded flex flex-grow items-center ${ getThemeClasses ( 'text-gray-700' , 'text-gray-300' ) } ` } >
< / span >
{ expandedAllEventId === event . id ? '▼' : ' ' }
< / div >
< button
className = "bg-red-600 hover:bg-red-700 text-white text-xs px-1 py-1 rounded flex items-center"
onClick = { ( e ) => {
e . stopPropagation ( ) ;
deleteEvent ( event . id , event . raw _json , event . author ) ;
} }
title = "Delete this event"
>
🗑 ️
< / button >
< / div >
< / div >
< / div >
{ event . content && (
{ event . content && (