@ -5,7 +5,7 @@ function App() {
@@ -5,7 +5,7 @@ function App() {
const [ status , setStatus ] = useState ( 'Ready to authenticate' ) ;
const [ statusType , setStatusType ] = useState ( 'info' ) ;
const [ profileData , setProfileData ] = useState ( null ) ;
/ / T h e m e s t a t e f o r d a r k / l i g h t m o d e
const [ isDarkMode , setIsDarkMode ] = useState ( false ) ;
@ -95,18 +95,18 @@ function App() {
@@ -95,18 +95,18 @@ function App() {
useEffect ( ( ) => {
/ / C h e c k i f t h e b r o w s e r s u p p o r t s p r e f e r s - c o l o r - s c h e m e
const darkModeMediaQuery = window . matchMedia ( '(prefers-color-scheme: dark)' ) ;
/ / S e t t h e i n i t i a l t h e m e b a s e d o n s y s t e m p r e f e r e n c e
setIsDarkMode ( darkModeMediaQuery . matches ) ;
/ / A d d l i s t e n e r t o r e s p o n d t o s y s t e m t h e m e c h a n g e s
const handleThemeChange = ( e ) => {
setIsDarkMode ( e . matches ) ;
} ;
/ / M o d e r n b r o w s e r s
darkModeMediaQuery . addEventListener ( 'change' , handleThemeChange ) ;
/ / C l e a n u p l i s t e n e r o n c o m p o n e n t u n m o u n t
return ( ) => {
darkModeMediaQuery . removeEventListener ( 'change' , handleThemeChange ) ;
@ -179,7 +179,7 @@ function App() {
@@ -179,7 +179,7 @@ function App() {
function statusClassName ( ) {
const base = 'mt-5 mb-5 p-3 rounded' ;
/ / R e t u r n t h e m e - a p p r o p r i a t e s t a t u s c l a s s e s
switch ( statusType ) {
case 'success' :
@ -464,7 +464,7 @@ function App() {
@@ -464,7 +464,7 @@ function App() {
let resolved = false ;
let receivedEvents = [ ] ;
let ws ;
try {
ws = new WebSocket ( relayURL ( ) ) ;
} catch ( e ) {
@ -507,7 +507,7 @@ function App() {
@@ -507,7 +507,7 @@ function App() {
const data = JSON . parse ( msg . data ) ;
const type = data [ 0 ] ;
console . log ( 'DEBUG: WebSocket message:' , type , data . length > 2 ? 'with event' : '' ) ;
if ( type === 'EVENT' && data [ 1 ] === subId ) {
const event = data [ 2 ] ;
if ( event ) {
@ -566,14 +566,14 @@ function App() {
@@ -566,14 +566,14 @@ function App() {
try {
/ / S o r t e v e n t s b y c r e a t e d _ a t i n d e s c e n d i n g o r d e r ( n e w e s t f i r s t )
const sortedEvents = receivedEvents . sort ( ( a , b ) => b . created _at - a . created _at ) ;
/ / A p p l y p a g i n a t i o n m a n u a l l y s i n c e w e g e t a l l e v e n t s f r o m W e b S o c k e t
const currentOffset = reset ? 0 : eventsOffset ;
const limit = 50 ;
const paginatedEvents = sortedEvents . slice ( currentOffset , currentOffset + limit ) ;
console . log ( 'DEBUG: Processing events - total:' , sortedEvents . length , 'paginated:' , paginatedEvents . length , 'offset:' , currentOffset ) ;
if ( reset ) {
setEvents ( paginatedEvents ) ;
setEventsOffset ( paginatedEvents . length ) ;
@ -581,10 +581,10 @@ function App() {
@@ -581,10 +581,10 @@ function App() {
setEvents ( prev => [ ... prev , ... paginatedEvents ] ) ;
setEventsOffset ( prev => prev + paginatedEvents . length ) ;
}
/ / C h e c k i f t h e r e a r e m o r e e v e n t s a v a i l a b l e
setEventsHasMore ( currentOffset + paginatedEvents . length < sortedEvents . length ) ;
console . log ( 'DEBUG: Events updated, displayed count:' , paginatedEvents . length , 'has more:' , currentOffset + paginatedEvents . length < sortedEvents . length ) ;
} catch ( error ) {
console . error ( 'Error processing events response:' , error ) ;
@ -606,7 +606,7 @@ function App() {
@@ -606,7 +606,7 @@ function App() {
let resolved = false ;
let receivedEvents = [ ] ;
let ws ;
try {
ws = new WebSocket ( relayURL ( ) ) ;
} catch ( e ) {
@ -649,7 +649,7 @@ function App() {
@@ -649,7 +649,7 @@ function App() {
const data = JSON . parse ( msg . data ) ;
const type = data [ 0 ] ;
console . log ( 'DEBUG: WebSocket message:' , type , data . length > 2 ? 'with event' : '' ) ;
if ( type === 'EVENT' && data [ 1 ] === subId ) {
const event = data [ 2 ] ;
if ( event ) {
@ -709,14 +709,14 @@ function App() {
@@ -709,14 +709,14 @@ function App() {
try {
/ / S o r t e v e n t s b y c r e a t e d _ a t i n d e s c e n d i n g o r d e r ( n e w e s t f i r s t )
const sortedEvents = receivedEvents . sort ( ( a , b ) => b . created _at - a . created _at ) ;
/ / A p p l y p a g i n a t i o n m a n u a l l y s i n c e w e g e t a l l e v e n t s f r o m W e b S o c k e t
const currentOffset = reset ? 0 : allEventsOffset ;
const limit = 50 ;
const paginatedEvents = sortedEvents . slice ( currentOffset , currentOffset + limit ) ;
console . log ( 'DEBUG: Processing all events - total:' , sortedEvents . length , 'paginated:' , paginatedEvents . length , 'offset:' , currentOffset ) ;
if ( reset ) {
setAllEvents ( paginatedEvents ) ;
setAllEventsOffset ( paginatedEvents . length ) ;
@ -724,13 +724,13 @@ function App() {
@@ -724,13 +724,13 @@ function App() {
setAllEvents ( prev => [ ... prev , ... paginatedEvents ] ) ;
setAllEventsOffset ( prev => prev + paginatedEvents . length ) ;
}
/ / C h e c k i f t h e r e a r e m o r e e v e n t s a v a i l a b l e
setAllEventsHasMore ( currentOffset + paginatedEvents . length < sortedEvents . length ) ;
/ / F e t c h p r o f i l e s f o r t h e n e w e v e n t s
fetchProfilesForEvents ( paginatedEvents ) ;
console . log ( 'DEBUG: All events updated, displayed count:' , paginatedEvents . length , 'has more:' , currentOffset + paginatedEvents . length < sortedEvents . length ) ;
} catch ( error ) {
console . error ( 'Error processing all events response:' , error ) ;
@ -742,6 +742,10 @@ function App() {
@@ -742,6 +742,10 @@ function App() {
/ / E v e n t s l o g f u n c t i o n s
async function fetchEvents ( reset = false ) {
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 ) {
@ -780,6 +784,147 @@ function App() {
@@ -780,6 +784,147 @@ function App() {
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
function toggleSection ( sectionKey ) {
setExpandedSections ( prev => ( {
@ -869,32 +1014,32 @@ function App() {
@@ -869,32 +1014,32 @@ function App() {
window . location . href = ` /api/export? ${ qs } ` ;
} catch ( _ ) { }
}
/ / T h e m e u t i l i t y f u n c t i o n s f o r c o n d i t i o n a l s t y l i n g
function getThemeClasses ( lightClass , darkClass ) {
return isDarkMode ? darkClass : lightClass ;
}
/ / G e t b a c k g r o u n d c o l o r c l a s s f o r c o n t a i n e r p a n e l s
function getPanelBgClass ( ) {
return getThemeClasses ( 'bg-gray-200' , 'bg-gray-800' ) ;
}
/ / G e t t e x t c o l o r c l a s s f o r s t a n d a r d t e x t
function getTextClass ( ) {
return getThemeClasses ( 'text-gray-700' , 'text-gray-300' ) ;
}
/ / G e t b a c k g r o u n d c o l o r f o r b u t t o n s
function getButtonBgClass ( ) {
return getThemeClasses ( 'bg-gray-100' , 'bg-gray-700' ) ;
}
/ / G e t t e x t c o l o r f o r b u t t o n s
function getButtonTextClass ( ) {
return getThemeClasses ( 'text-gray-500' , 'text-gray-300' ) ;
}
/ / G e t h o v e r c l a s s e s f o r b u t t o n s
function getButtonHoverClass ( ) {
return getThemeClasses ( 'hover:text-gray-800' , 'hover:text-gray-100' ) ;
@ -945,7 +1090,7 @@ function App() {
@@ -945,7 +1090,7 @@ function App() {
style = { { display : 'none' } }
/ >
< div className = { ` m-2 p-2 w-full ${ getPanelBgClass ( ) } rounded-lg ` } >
< div
< div
className = { ` text-lg font-bold flex items-center justify-between cursor-pointer p-2 ${ getTextClass ( ) } ${ getThemeClasses ( 'hover:bg-gray-300' , 'hover:bg-gray-700' ) } rounded ` }
onClick = { ( ) => toggleSection ( 'welcome' ) }
>
@ -963,7 +1108,7 @@ function App() {
@@ -963,7 +1108,7 @@ function App() {
{ /* Export only my events */ }
< div className = { ` m-2 p-2 ${ getPanelBgClass ( ) } rounded-lg w-full ` } >
< div
< div
className = { ` text-lg font-bold flex items-center justify-between cursor-pointer p-2 ${ getTextClass ( ) } ${ getThemeClasses ( 'hover:bg-gray-300' , 'hover:bg-gray-700' ) } rounded ` }
onClick = { ( ) => toggleSection ( 'exportMine' ) }
>
@ -992,7 +1137,7 @@ function App() {
@@ -992,7 +1137,7 @@ function App() {
{ user . permission === "admin" && (
< >
< div className = { ` m-2 p-2 ${ getPanelBgClass ( ) } rounded-lg w-full ` } >
< div
< div
className = { ` text-lg font-bold flex items-center justify-between cursor-pointer p-2 ${ getTextClass ( ) } ${ getThemeClasses ( 'hover:bg-gray-300' , 'hover:bg-gray-700' ) } rounded ` }
onClick = { ( ) => toggleSection ( 'exportAll' ) }
>
@ -1020,7 +1165,7 @@ function App() {
@@ -1020,7 +1165,7 @@ function App() {
{ /* Export specific pubkeys (admin) */ }
< div className = { ` m-2 p-2 ${ getPanelBgClass ( ) } rounded-lg w-full ` } >
< div
< div
className = { ` text-lg font-bold flex items-center justify-between cursor-pointer p-2 ${ getTextClass ( ) } ${ getThemeClasses ( 'hover:bg-gray-300' , 'hover:bg-gray-700' ) } rounded ` }
onClick = { ( ) => toggleSection ( 'exportSpecific' ) }
>
@ -1098,7 +1243,7 @@ function App() {
@@ -1098,7 +1243,7 @@ function App() {
) }
< / div >
< div className = { ` m-2 p-2 ${ getPanelBgClass ( ) } rounded-lg w-full ` } >
< div
< div
className = { ` text-lg font-bold flex items-center justify-between cursor-pointer p-2 ${ getTextClass ( ) } ${ getThemeClasses ( 'hover:bg-gray-300' , 'hover:bg-gray-700' ) } rounded ` }
onClick = { ( ) => toggleSection ( 'importEvents' ) }
>
@ -1127,7 +1272,7 @@ function App() {
@@ -1127,7 +1272,7 @@ function App() {
) }
{ /* My Events Log */ }
< div className = { ` m-2 p-2 ${ getPanelBgClass ( ) } rounded-lg w-full ` } >
< div
< div
className = { ` text-lg font-bold flex items-center justify-between cursor-pointer p-2 ${ getTextClass ( ) } ${ getThemeClasses ( 'hover:bg-gray-300' , 'hover:bg-gray-700' ) } rounded ` }
onClick = { ( ) => toggleSection ( 'eventsLog' ) }
>
@ -1158,18 +1303,66 @@ function App() {
@@ -1158,18 +1303,66 @@ function App() {
className = "cursor-pointer"
onClick = { ( ) => toggleEventExpansion ( event . id ) }
>
< div className = "flex items-center justify-between" >
< div className = "flex items-center gap-3" >
< div className = "flex items-center justify-between w-full" >
< 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' ) } ` } >
Kind { event . kind }
< / span >
< span className = { ` text-sm ${ getTextClass ( ) } ` } >
< span className = { ` text-sm ${ getTextClass ( ) } ` } >
{ formatTimestamp ( event . created _at ) }
< / 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 >
< span className = { ` text-lg ${ getTextClass ( ) } ` } >
{ expandedEventId === event . id ? '▼' : '▶' }
< / span >
< / div >
{ event . content && (
@ -1228,7 +1421,7 @@ function App() {
@@ -1228,7 +1421,7 @@ function App() {
{ /* All Events Log (admin only) */ }
{ user . permission === "admin" && (
< div className = { ` m-2 p-2 ${ getPanelBgClass ( ) } rounded-lg w-full ` } >
< div
< div
className = { ` text-lg font-bold flex items-center justify-between cursor-pointer p-2 ${ getTextClass ( ) } ${ getThemeClasses ( 'hover:bg-gray-300' , 'hover:bg-gray-700' ) } rounded ` }
onClick = { ( ) => toggleSection ( 'allEventsLog' ) }
>
@ -1238,7 +1431,7 @@ function App() {
@@ -1238,7 +1431,7 @@ function App() {
< / span >
< / div >
{ 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" >
< 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 >
@ -1259,8 +1452,8 @@ function App() {
@@ -1259,8 +1452,8 @@ function App() {
className = "cursor-pointer"
onClick = { ( ) => toggleAllEventExpansion ( event . id ) }
>
< div className = "flex items-center justify-between" >
< div className = "flex items-center gap-6" >
< div className = "flex items-center justify-between w-full " >
< 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" >
{ event . author && profileCache [ event . author ] && (
@ -1275,7 +1468,7 @@ function App() {
@@ -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 ( ) } ` } >
{ profileCache [ event . author ] . display _name || profileCache [ event . author ] . name || ` ${ event . author . slice ( 0 , 8 ) } ... ` }
< / span >
@ -1293,7 +1486,7 @@ function App() {
@@ -1293,7 +1486,7 @@ function App() {
< / 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' ) } ` } >
@ -1304,9 +1497,21 @@ function App() {
@@ -1304,9 +1497,21 @@ function App() {
< / span >
< / div >
< / div >
< span className = { ` text-lg ${ getTextClass ( ) } ` } >
{ expandedAllEventId === event . id ? '▼' : '▶' }
< / span >
< div className = "justify-end ml-auto rounded-full h-16 w-16 flex items-center justify-center" >
< div className = { ` text-white text-xs px-4 py-4 rounded flex flex-grow items-center ${ getThemeClasses ( 'text-gray-700' , 'text-gray-300' ) } ` } >
{ 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 >
{ event . content && (
@ -1362,7 +1567,7 @@ function App() {
@@ -1362,7 +1567,7 @@ function App() {
) }
< / div >
) }
{ /* Empty flex grow box to ensure background fills entire viewport */ }
< div className = { ` flex-grow ${ getThemeClasses ( 'bg-gray-100' , 'bg-gray-900' ) } ` } > < / div >
< / div >