@ -18,6 +18,49 @@ function App() {
const [ eventsHasMore , setEventsHasMore ] = useState ( true ) ;
const [ eventsHasMore , setEventsHasMore ] = useState ( true ) ;
const [ expandedEventId , setExpandedEventId ] = useState ( null ) ;
const [ expandedEventId , setExpandedEventId ] = useState ( null ) ;
/ / A l l E v e n t s l o g s t a t e ( a d m i n )
const [ allEvents , setAllEvents ] = useState ( [ ] ) ;
const [ allEventsLoading , setAllEventsLoading ] = useState ( false ) ;
const [ allEventsOffset , setAllEventsOffset ] = useState ( 0 ) ;
const [ allEventsHasMore , setAllEventsHasMore ] = useState ( true ) ;
const [ expandedAllEventId , setExpandedAllEventId ] = useState ( null ) ;
/ / P r o f i l e c a c h e f o r A l l E v e n t s L o g
const [ profileCache , setProfileCache ] = useState ( { } ) ;
/ / F u n c t i o n t o f e t c h a n d c a c h e p r o f i l e m e t a d a t a f o r a n a u t h o r
async function fetchAndCacheProfile ( pubkeyHex ) {
if ( ! pubkeyHex || profileCache [ pubkeyHex ] ) {
return profileCache [ pubkeyHex ] || null ;
}
try {
const profile = await fetchKind0FromRelay ( pubkeyHex ) ;
if ( profile ) {
setProfileCache ( prev => ( {
... prev ,
[ pubkeyHex ] : {
name : profile . name || ` user: ${ pubkeyHex . slice ( 0 , 8 ) } ` ,
display _name : profile . display _name ,
picture : profile . picture ,
about : profile . about
}
} ) ) ;
return profile ;
}
} catch ( error ) {
console . log ( 'Error fetching profile for' , pubkeyHex . slice ( 0 , 8 ) , ':' , error ) ;
}
return null ;
}
/ / F u n c t i o n t o f e t c h p r o f i l e s f o r a l l e v e n t s i n a b a t c h
async function fetchProfilesForEvents ( events ) {
const uniqueAuthors = [ ... new Set ( events . map ( event => event . author ) . filter ( Boolean ) ) ] ;
const fetchPromises = uniqueAuthors . map ( author => fetchAndCacheProfile ( author ) ) ;
await Promise . allSettled ( fetchPromises ) ;
}
/ / S e c t i o n r e v e a l e r s t a t e s
/ / S e c t i o n r e v e a l e r s t a t e s
const [ expandedSections , setExpandedSections ] = useState ( {
const [ expandedSections , setExpandedSections ] = useState ( {
welcome : true ,
welcome : true ,
@ -25,7 +68,8 @@ function App() {
exportAll : false ,
exportAll : false ,
exportSpecific : false ,
exportSpecific : false ,
importEvents : false ,
importEvents : false ,
eventsLog : false
eventsLog : false ,
allEventsLog : false
} ) ;
} ) ;
@ -88,8 +132,12 @@ function App() {
useEffect ( ( ) => {
useEffect ( ( ) => {
if ( user ? . pubkey ) {
if ( user ? . pubkey ) {
fetchEvents ( true ) ; / / t r u e = r e s e t
fetchEvents ( true ) ; / / t r u e = r e s e t
/ / A l s o f e t c h a l l e v e n t s i f u s e r i s a d m i n
if ( user . permission === 'admin' ) {
fetchAllEvents ( true ) ; / / t r u e = r e s e t
}
}
}
} , [ user ? . pubkey ] ) ;
} , [ user ? . pubkey , user ? . permission ] ) ;
function relayURL ( ) {
function relayURL ( ) {
try {
try {
@ -395,6 +443,11 @@ function App() {
setEventsOffset ( 0 ) ;
setEventsOffset ( 0 ) ;
setEventsHasMore ( true ) ;
setEventsHasMore ( true ) ;
setExpandedEventId ( null ) ;
setExpandedEventId ( null ) ;
/ / C l e a r a l l e v e n t s s t a t e
setAllEvents ( [ ] ) ;
setAllEventsOffset ( 0 ) ;
setAllEventsHasMore ( true ) ;
setExpandedAllEventId ( null ) ;
updateStatus ( 'Logged out' , 'info' ) ;
updateStatus ( 'Logged out' , 'info' ) ;
}
}
@ -540,15 +593,169 @@ function App() {
}
}
}
}
/ / W e b S o c k e t - b a s e d f u n c t i o n t o f e t c h a l l e v e n t s f r o m r e l a y ( a d m i n )
async function fetchAllEventsFromRelay ( reset = false , limit = 50 , timeoutMs = 10000 ) {
if ( ! user ? . pubkey || user . permission !== 'admin' ) return ;
if ( allEventsLoading ) return ;
if ( ! reset && ! allEventsHasMore ) return ;
console . log ( 'DEBUG: fetchAllEventsFromRelay called, reset:' , reset , 'offset:' , allEventsOffset ) ;
setAllEventsLoading ( true ) ;
return new Promise ( ( resolve ) => {
let resolved = false ;
let receivedEvents = [ ] ;
let ws ;
try {
ws = new WebSocket ( relayURL ( ) ) ;
} catch ( e ) {
console . error ( 'Failed to create WebSocket:' , e ) ;
setAllEventsLoading ( false ) ;
resolve ( ) ;
return ;
}
const subId = 'allevents-' + Math . random ( ) . toString ( 36 ) . slice ( 2 ) ;
const timer = setTimeout ( ( ) => {
if ( ws && ws . readyState === 1 ) {
try { ws . close ( ) ; } catch ( _ ) { }
}
if ( ! resolved ) {
resolved = true ;
console . log ( 'DEBUG: WebSocket timeout, received all events:' , receivedEvents . length ) ;
processAllEventsResponse ( receivedEvents , reset ) ;
resolve ( ) ;
}
} , timeoutMs ) ;
ws . onopen = ( ) => {
try {
/ / R e q u e s t a l l e v e n t s ( n o a u t h o r s f i l t e r f o r a d m i n )
const req = [
'REQ' ,
subId ,
{ }
] ;
console . log ( 'DEBUG: Sending WebSocket request for all events:' , req ) ;
ws . send ( JSON . stringify ( req ) ) ;
} catch ( e ) {
console . error ( 'Failed to send WebSocket request:' , e ) ;
}
} ;
ws . onmessage = ( msg ) => {
try {
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 ) {
/ / C o n v e r t t o t h e e x p e c t e d f o r m a t
const formattedEvent = {
id : event . id ,
kind : event . kind ,
created _at : event . created _at ,
content : event . content || '' ,
author : event . pubkey || '' ,
raw _json : JSON . stringify ( event )
} ;
receivedEvents . push ( formattedEvent ) ;
}
} else if ( type === 'EOSE' && data [ 1 ] === subId ) {
try {
ws . send ( JSON . stringify ( [ 'CLOSE' , subId ] ) ) ;
} catch ( _ ) { }
try { ws . close ( ) ; } catch ( _ ) { }
clearTimeout ( timer ) ;
if ( ! resolved ) {
resolved = true ;
console . log ( 'DEBUG: EOSE received, processing all events:' , receivedEvents . length ) ;
processAllEventsResponse ( receivedEvents , reset ) ;
resolve ( ) ;
}
}
} catch ( e ) {
console . error ( 'Error parsing WebSocket message:' , e ) ;
}
} ;
ws . onerror = ( error ) => {
console . error ( 'WebSocket error:' , error ) ;
try { ws . close ( ) ; } catch ( _ ) { }
clearTimeout ( timer ) ;
if ( ! resolved ) {
resolved = true ;
processAllEventsResponse ( receivedEvents , reset ) ;
resolve ( ) ;
}
} ;
ws . onclose = ( ) => {
clearTimeout ( timer ) ;
if ( ! resolved ) {
resolved = true ;
console . log ( 'DEBUG: WebSocket closed, processing all events:' , receivedEvents . length ) ;
processAllEventsResponse ( receivedEvents , reset ) ;
resolve ( ) ;
}
} ;
} ) ;
}
function processAllEventsResponse ( receivedEvents , reset ) {
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 ) ;
} else {
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 ) ;
} finally {
setAllEventsLoading ( false ) ;
}
}
/ / 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 ) ;
}
}
async function fetchAllEvents ( reset = false ) {
await fetchAllEventsFromRelay ( reset ) ;
}
function toggleEventExpansion ( eventId ) {
function toggleEventExpansion ( eventId ) {
setExpandedEventId ( current => current === eventId ? null : eventId ) ;
setExpandedEventId ( current => current === eventId ? null : eventId ) ;
}
}
function toggleAllEventExpansion ( eventId ) {
setExpandedAllEventId ( current => current === eventId ? null : eventId ) ;
}
function copyEventJSON ( eventJSON ) {
function copyEventJSON ( eventJSON ) {
try {
try {
navigator . clipboard . writeText ( eventJSON ) ;
navigator . clipboard . writeText ( eventJSON ) ;
@ -710,7 +917,7 @@ function App() {
< div className = "absolute inset-0 opacity-70 bg-cover bg-center" style = { { backgroundImage : ` url( ${ profileData . banner } ) ` } } > < / div >
< div className = "absolute inset-0 opacity-70 bg-cover bg-center" style = { { backgroundImage : ` url( ${ profileData . banner } ) ` } } > < / div >
) }
) }
< div className = "relative z-10 p-2 flex items-center h-full" >
< div className = "relative z-10 p-2 flex items-center h-full" >
{ profileData ? . picture && < img src = { profileData . picture } alt = "User Avatar" className = { ` h-full aspect-square w-auto rounded-full object-cover border-2 ${ getThemeClasses ( 'border-white' , 'border-gray-600' ) } mr-2 shadow box-border ` } / > }
{ profileData ? . picture && < img src = { profileData . picture } alt = "User Avatar" className = { ` w-8 h-8 rounded-full object-cover border-2 ${ getThemeClasses ( 'border-white' , 'border-gray-600' ) } mr-2 shadow box-border ` } / > }
< div className = { getTextClass ( ) } >
< div className = { getTextClass ( ) } >
< div className = "font-bold text-base block" >
< div className = "font-bold text-base block" >
{ profileData ? . display _name || profileData ? . name || user . pubkey . slice ( 0 , 8 ) }
{ profileData ? . display _name || profileData ? . name || user . pubkey . slice ( 0 , 8 ) }
@ -1018,6 +1225,144 @@ function App() {
) }
) }
< / div >
< / div >
{ /* All Events Log (admin only) */ }
{ user . permission === "admin" && (
< div className = { ` m-2 p-2 ${ getPanelBgClass ( ) } rounded-lg w-full ` } >
< 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' ) }
>
< span > All Events Log ( admin ) < / span >
< span className = "text-xl" >
{ expandedSections . allEventsLog ? '▼' : '▶' }
< / span >
< / div >
{ expandedSections . allEventsLog && (
< div className = "p-2 bg-gray-900 rounded-lg mt-2" >
< 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 >
< div
className = "block"
style = { {
position : 'relative'
} }
>
{ allEvents . length === 0 && ! allEventsLoading ? (
< div className = { ` text-center py-4 ${ getTextClass ( ) } ` } > No events found < / div >
) : (
< div className = "space-y-2" >
{ allEvents . map ( ( event ) => (
< div key = { event . id } className = { ` border rounded p-3 ${ getThemeClasses ( 'border-gray-300 bg-white' , 'border-gray-600 bg-gray-800' ) } ` } >
< div
className = "cursor-pointer"
onClick = { ( ) => toggleAllEventExpansion ( event . id ) }
>
< div className = "flex items-center justify-between" >
< div className = "flex items-center gap-6" >
{ /* User avatar and info - separated with more space */ }
< div className = "flex items-center gap-3 min-w-0" >
{ event . author && profileCache [ event . author ] && (
< >
{ profileCache [ event . author ] . picture && (
< img
src = { profileCache [ event . author ] . picture }
alt = { profileCache [ event . author ] . display _name || profileCache [ event . author ] . 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 min-w-0" >
< span className = { ` text-sm font-medium ${ getTextClass ( ) } ` } >
{ profileCache [ event . author ] . display _name || profileCache [ event . author ] . name || ` ${ event . author . slice ( 0 , 8 ) } ... ` }
< / span >
{ profileCache [ event . author ] . display _name && profileCache [ event . author ] . name && (
< span className = { ` text-xs ${ getTextClass ( ) } opacity-70 ` } >
{ profileCache [ event . author ] . name }
< / span >
) }
< / div >
< / >
) }
{ event . author && ! profileCache [ event . author ] && (
< span className = { ` text-sm font-medium ${ getTextClass ( ) } ` } >
{ ` ${ event . author . 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 ( ) } ` } >
{ formatTimestamp ( event . created _at ) }
< / span >
< / div >
< / div >
< span className = { ` text-lg ${ getTextClass ( ) } ` } >
{ expandedAllEventId === event . id ? '▼' : '▶' }
< / span >
< / div >
{ event . content && (
< div className = { ` mt-2 text-sm ${ getTextClass ( ) } ` } >
{ truncateContent ( event . content ) }
< / div >
) }
< / div >
{ expandedAllEventId === event . id && (
< div className = "mt-3 border-t pt-3" >
< div className = "flex items-center justify-between mb-2" >
< span className = { ` text-sm font-medium ${ getTextClass ( ) } ` } > Raw JSON : < / span >
< button
className = { ` ${ getThemeClasses ( 'bg-green-600 hover:bg-green-700' , 'bg-green-500 hover:bg-green-600' ) } text-white text-xs px-2 py-1 rounded ` }
onClick = { ( e ) => {
e . stopPropagation ( ) ;
copyEventJSON ( event . raw _json ) ;
} }
title = "Copy minified JSON"
>
Copy
< / button >
< / div >
< pre className = { ` text-xs p-2 rounded overflow-auto max-h-40 break-all whitespace-pre-wrap ${ getPanelBgClass ( ) } ${ getTextClass ( ) } ` } >
{ JSON . stringify ( JSON . parse ( event . raw _json ) , null , 2 ) }
< / pre >
< / div >
) }
< / div >
) ) }
{ allEventsLoading && (
< div className = { ` text-center py-4 ${ getTextClass ( ) } ` } >
< div className = "text-sm" > Loading more events ... < / div >
< / div >
) }
{ ! allEventsLoading && allEventsHasMore && (
< div className = "text-center py-4" >
< button
className = { ` ${ getThemeClasses ( 'bg-blue-600 hover:bg-blue-700' , 'bg-blue-500 hover:bg-blue-600' ) } text-white px-4 py-2 rounded ` }
onClick = { ( ) => fetchAllEvents ( false ) }
>
Load More
< / button >
< / div >
) }
< / div >
) }
< / div >
< / div >
) }
< / div >
) }
{ /* Empty flex grow box to ensure background fills entire viewport */ }
{ /* Empty flex grow box to ensure background fills entire viewport */ }
< div className = { ` flex-grow ${ getThemeClasses ( 'bg-gray-100' , 'bg-gray-900' ) } ` } > < / div >
< div className = { ` flex-grow ${ getThemeClasses ( 'bg-gray-100' , 'bg-gray-900' ) } ` } > < / div >
< / div >
< / div >