@ -1,26 +1,23 @@
import { relayPool , generatePrivateKey , getPublicKey , signEvent } from 'nostr-tools' ;
import { relayPool , generatePrivateKey , getPublicKey , signEvent } from 'nostr-tools' ;
import { elem } from './domutil.js' ;
import { elem } from './domutil.js' ;
import { dateTime , formatTime } from './timeutil.js' ;
// curl -H 'accept: application/nostr+json' https://nostr.x1ddos.ch
// curl -H 'accept: application/nostr+json' https://nostr.x1ddos.ch
const pool = relayPool ( ) ;
const pool = relayPool ( ) ;
pool . addRelay ( 'wss://nostr.x1ddos.ch' , { read : true , write : true } ) ;
// pool.addRelay('wss://nostr.bitcoiner.social/', {read: true, write: true});
pool . addRelay ( 'wss://nostr.openchain.fr' , { read : true , write : true } ) ;
pool . addRelay ( 'wss://relay.nostr.info' , { read : true , write : true } ) ;
pool . addRelay ( 'wss://relay.nostr.info' , { read : true , write : true } ) ;
// pool.addRelay('wss://relay.damus.io', {read: true, write: true});
// pool.addRelay('wss://relay.damus.io', {read: true, write: true});
pool . addRelay ( 'wss://nostr.x1ddos.ch' , { read : true , write : true } ) ;
// pool.addRelay('wss://nostr.openchain.fr', {read: true, write: true});
// pool.addRelay('wss://nostr.bitcoiner.social/', {read: true, write: true});
// read only
// read only
// pool.addRelay('wss://nostr.rocks', {read: true, write: false});
// pool.addRelay('wss://nostr.rocks', {read: true, write: false});
// pool.addRelay('wss://nostr-relay.wlvs.space', {read: true, write: false});
const dateTime = new Intl . DateTimeFormat ( 'de-ch' /* navigator.language */ , {
dateStyle : 'short' ,
timeStyle : 'medium' ,
} ) ;
let max = 0 ;
let max = 0 ;
function onEvent ( evt , relay ) {
function onEvent ( evt , relay ) {
if ( max ++ >= 223 ) {
// if (max++ >= 223) {
return subscription . unsub ( ) ;
// return subscription.unsub();
}
// }
switch ( evt . kind ) {
switch ( evt . kind ) {
case 0 :
case 0 :
handleMetadata ( evt , relay ) ;
handleMetadata ( evt , relay ) ;
@ -52,7 +49,8 @@ const subscription = pool.sub({
// // pubkey, // me
// // pubkey, // me
// '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', // jb55
// '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', // jb55
// ],
// ],
limit : 200 ,
// since: new Date(Date.now() - (24 * 60 * 60 * 1000)),
limit : 2000 ,
}
}
} ) ;
} ) ;
@ -67,10 +65,8 @@ function handleTextNote(evt, relay) {
} else {
} else {
eventRelayMap [ evt . id ] = [ relay ] ;
eventRelayMap [ evt . id ] = [ relay ] ;
if ( evt . tags . some ( hasEventTag ) ) {
if ( evt . tags . some ( hasEventTag ) ) {
replyList . push ( evt )
replyList . push ( evt ) ;
if ( feedDomMap [ evt . tags [ 0 ] [ 1 ] ] ) {
handleReply ( evt , relay ) ;
console . log ( 'CALL ME' , evt . tags [ 0 ] [ 1 ] , feedDomMap [ evt . tags [ 0 ] [ 1 ] ] ) ;
}
} else {
} else {
textNoteList . push ( evt ) ;
textNoteList . push ( evt ) ;
}
}
@ -88,62 +84,88 @@ const sortByCreatedAt = (evt1, evt2) => {
return evt1 . created _at > evt2 . created _at ? - 1 : 1 ;
return evt1 . created _at > evt2 . created _at ? - 1 : 1 ;
} ;
} ;
let debounceDebugMessageTimer ;
// let debounceDebugMessageTimer;
function renderFeed ( ) {
function renderFeed ( ) {
const sortedFeeds = textNoteList . sort ( sortByCreatedAt ) . reverse ( ) ;
const sortedFeeds = textNoteList . sort ( sortByCreatedAt ) . reverse ( ) ;
// debug
// debug
clearTimeout ( debounceDebugMessageTimer ) ;
// clearTimeout(debounceDebugMessageTimer);
debounceDebugMessageTimer = setTimeout ( ( ) => {
// debounceDebugMessageTimer = setTimeout(() => {
console . log ( ` ${ sortedFeeds . reverse ( ) . map ( e => dateTime . format ( e . created _at * 1000 ) ) . join ( '\n' ) } ` )
// console.log(`${sortedFeeds.reverse().map(e => dateTime.format(e.created_at * 1000)).join('\n')}`)
} , 2000 ) ;
// }, 2000);
sortedFeeds . forEach ( ( textNoteEvent , i ) => {
sortedFeeds . forEach ( ( textNoteEvent , i ) => {
if ( feedDomMap [ textNoteEvent . id ] ) {
if ( feedDomMap [ textNoteEvent . id ] ) {
// TODO check eventRelayMap if event was published to different relays
// TODO check eventRelayMap if event was published to different relays
return ;
return ;
}
}
const article = createTextNote ( textNoteEvent , eventRelayMap [ textNoteEvent . id ] ) ;
const article = createTextNote ( textNoteEvent , eventRelayMap [ textNoteEvent . id ] ) ;
feedDomMap [ textNoteEvent . id ] = article ;
if ( i === 0 ) {
if ( i === 0 ) {
feedContainer . append ( article ) ;
feedContainer . append ( article ) ;
} else {
} else {
feedDomMap [ sortedFeeds [ i - 1 ] . id ] . before ( article ) ;
feedDomMap [ sortedFeeds [ i - 1 ] . id ] . before ( article ) ;
}
}
feedDomMap [ textNoteEvent . id ] = article ;
} ) ;
} ) ;
}
}
const sortEventCreatedAt = ( evt ) => (
setInterval ( ( ) => {
{ created _at : a } ,
document . querySelectorAll ( 'time[datetime]' ) . forEach ( timeElem => {
{ created _at : b } ,
timeElem . textContent = formatTime ( new Date ( timeElem . dateTime ) ) ;
) => (
} ) ;
Math . abs ( a - evt . created _at ) < Math . abs ( b - evt . created _at ) ? - 1 : 1
} , 10000 ) ;
) ;
function createTextNote ( evt , relay ) {
function createTextNote ( evt , relay ) {
const { host , img , isReply , replies , time , userName } = getMetadata ( evt , relay ) ;
const { host , img , isReply , replies , time , userName } = getMetadata ( evt , relay ) ;
const name = elem ( 'strong' , { className : 'mbox-username' , title : evt . pubkey } , userName ) ;
const timeElem = elem ( 'time' , { dateTime : time . toISOString ( ) } , formatTime ( time ) ) ;
const headerInfo = isReply ? [
const headerInfo = isReply ? [
elem ( 'strong' , { title : evt . pubkey } , userName )
name, ' ' , timeElem
] : [
] : [
elem( 'stro ng', { title : evt . pubkey } , userN ame) ,
name,
elem ( 'span' , {
elem ( 'span' , {
title : ` Event ${ evt . id }
title : ` Event ${ evt . id }
$ { isReply ? ` \n Reply ${ evt . tags [ 0 ] [ 1 ] } \n ` : '' } \ n$ { time } `
$ { isReply ? ` \n Reply ${ evt . tags [ 0 ] [ 1 ] } \n ` : '' } `
} , ` on ${ host } ` ) ,
} , ` on ${ host } ` ) ,
timeElem ,
] ;
] ;
const body = elem ( 'div' , { className : 'mbox-body' } , [
const body = elem ( 'div' , { className : 'mbox-body' } , [
elem ( 'header' , {
elem ( 'header' , { className : 'mbox-header' } , [
className : 'mbox-header' ,
} , [
elem ( 'small' , { } , headerInfo ) ,
elem ( 'small' , { } , headerInfo ) ,
] ) ,
] ) ,
evt . content , // text
evt . content , // text
elem ( 'br' ) ,
elem ( 'button' , {
className : 'button-inline' ,
name : 'reply' , type : 'button' ,
data : { 'eventId' : evt . id , relay }
} , [
elem ( 'small' , { } , 'reply' )
] ) ,
replies [ 0 ] ? elem ( 'div' , { className : 'mobx-replies' } , replies . map ( e => createTextNote ( e , relay ) ) ) : '' ,
replies [ 0 ] ? elem ( 'div' , { className : 'mobx-replies' } , replies . map ( e => createTextNote ( e , relay ) ) ) : '' ,
] ) ;
] ) ;
return rendernArticle ( [ img , body ] ) ;
return rendernArticle ( [ img , body ] ) ;
}
}
function handleReply ( evt , relay ) {
const article = feedDomMap [ evt . tags [ 0 ] [ 1 ] ] ;
if ( article ) {
let replyContainer = article . querySelector ( '.mobx-replies' ) ;
if ( ! replyContainer ) {
replyContainer = elem ( 'div' , { className : 'mobx-replies' } ) ;
article . querySelector ( '.mbox-body' ) . append ( replyContainer ) ;
}
replyContainer . append ( createTextNote ( evt , relay ) )
}
}
const sortEventCreatedAt = ( created _at ) => (
{ created _at : a } ,
{ created _at : b } ,
) => (
Math . abs ( a - created _at ) < Math . abs ( b - created _at ) ? - 1 : 1
) ;
function handleRecommendServer ( evt , relay ) {
function handleRecommendServer ( evt , relay ) {
if ( feedDomMap [ evt . id ] ) {
if ( feedDomMap [ evt . id ] ) {
// TODO event might also be published to different relays
return ;
return ;
}
}
const art = renderRecommendServer ( evt , relay ) ;
const art = renderRecommendServer ( evt , relay ) ;
@ -151,21 +173,20 @@ function handleRecommendServer(evt, relay) {
feedContainer . append ( art ) ;
feedContainer . append ( art ) ;
return ;
return ;
}
}
const closestTextNotes = textNoteList . sort ( sortEventCreatedAt ( evt )) ;
const closestTextNotes = textNoteList . sort ( sortEventCreatedAt ( evt .created _at )) ;
feedDomMap [ closestTextNotes [ 0 ] . id ] . after ( art ) ;
feedDomMap [ closestTextNotes [ 0 ] . id ] . after ( art ) ;
feedDomMap [ evt . id ] = art ;
feedDomMap [ evt . id ] = art ;
}
}
function renderRecommendServer ( evt , relay ) {
function renderRecommendServer ( evt , relay ) {
const { host, img, time , userName } = getMetadata ( evt , relay ) ;
const { img, time , userName } = getMetadata ( evt , relay ) ;
const body = elem ( 'div' , { className : 'mbox-body' , title : dateTime . format ( time ) } , [
const body = elem ( 'div' , { className : 'mbox-body' , title : dateTime . format ( time ) } , [
elem ( 'header' , { className : 'mbox-header' } , [
elem ( 'header' , { className : 'mbox-header' } , [
elem ( 'small' , { } , [
elem ( 'small' , { } , [
elem ( 'strong' , { } , userName ) ,
elem ( 'strong' , { } , userName )
` on ${ host } ` ,
] ) ,
] ) ,
] ) ,
] ) ,
` recommends server: ${ evt . content } ` ,
` recommends server: ${ evt . content } ` ,
] ) ;
] ) ;
return rendernArticle ( [ img , body ] , { className : 'mbox-recommend-server' } ) ;
return rendernArticle ( [ img , body ] , { className : 'mbox-recommend-server' } ) ;
}
}
@ -215,13 +236,13 @@ function setMetadata(evt, relay, content) {
const getHost = ( url ) => {
const getHost = ( url ) => {
try {
try {
return new URL ( url ) . host ;
return new URL ( url ) . host ;
} catch ( e ) {
} catch ( e rr ) {
return false ;
return err ;
}
}
}
}
function getMetadata ( evt , relay ) {
function getMetadata ( evt , relay ) {
const host = getHost ( relay [0 ] );
const host = getHost ( relay );
const user = userList . find ( user => user . pubkey === evt . pubkey ) ;
const user = userList . find ( user => user . pubkey === evt . pubkey ) ;
const userImg = /*user?.metadata[relay]?.picture || */ 'bubble.svg' ; // TODO: enable pic once we have proxy
const userImg = /*user?.metadata[relay]?.picture || */ 'bubble.svg' ; // TODO: enable pic once we have proxy
const userName = user ? . metadata [ relay ] ? . name || evt . pubkey . slice ( 0 , 8 ) ;
const userName = user ? . metadata [ relay ] ? . name || evt . pubkey . slice ( 0 , 8 ) ;
@ -231,9 +252,8 @@ function getMetadata(evt, relay) {
className : 'mbox-img' ,
className : 'mbox-img' ,
src : userImg ,
src : userImg ,
alt : ` ${ userName } @ ${ host } ` ,
alt : ` ${ userName } @ ${ host } ` ,
title : userAbout } ,
title : userAbout ,
''
} , '' ) ;
) ;
const replies = replyList . filter ( ( reply ) => reply . tags [ 0 ] [ 1 ] === evt . id ) ;
const replies = replyList . filter ( ( reply ) => reply . tags [ 0 ] [ 1 ] === evt . id ) ;
const time = new Date ( evt . created _at * 1000 ) ;
const time = new Date ( evt . created _at * 1000 ) ;
return { host , img , isReply , replies , time , userName } ;
return { host , img , isReply , replies , time , userName } ;
@ -252,6 +272,26 @@ function updateContactList(evt, relay) {
// check pool.status
// check pool.status
// reply
const writeForm = document . querySelector ( '#writeForm' ) ;
const input = document . querySelector ( 'input[name="message"]' ) ;
let lastReplyBtn = null ;
let replyTo = null ;
feedContainer . addEventListener ( 'click' , ( e ) => {
const button = e . target . closest ( 'button' ) ;
if ( button && button . name === 'reply' ) {
if ( lastReplyBtn ) {
lastReplyBtn . hidden = false ;
}
lastReplyBtn = button ;
button . hidden = true ;
button . after ( writeForm ) ;
writeForm . hidden = false ;
replyTo = [ 'e' , button . dataset . eventId , button . dataset . relay ] ;
input . focus ( ) ;
}
} ) ;
// send
// send
const sendStatus = document . querySelector ( '#sendstatus' ) ;
const sendStatus = document . querySelector ( '#sendstatus' ) ;
const onSendError = err => {
const onSendError = err => {
@ -268,11 +308,12 @@ publish.addEventListener('click', async () => {
if ( ! input . value ) {
if ( ! input . value ) {
return onSendError ( new Error ( 'message is empty' ) ) ;
return onSendError ( new Error ( 'message is empty' ) ) ;
}
}
const tags = replyTo ? [ replyTo ] : [ ] ;
const newEvent = {
const newEvent = {
kind : 1 ,
kind : 1 ,
pubkey ,
pubkey ,
content : input . value ,
content : input . value ,
tags : [ ] ,
tags ,
created _at : Math . floor ( Date . now ( ) * 0.001 ) ,
created _at : Math . floor ( Date . now ( ) * 0.001 ) ,
} ;
} ;
const sig = await signEvent ( newEvent , privatekey ) . catch ( onSendError ) ;
const sig = await signEvent ( newEvent , privatekey ) . catch ( onSendError ) ;
@ -285,13 +326,18 @@ publish.addEventListener('click', async () => {
sendStatus . hidden = true ;
sendStatus . hidden = true ;
input . value = '' ;
input . value = '' ;
publish . disabled = true ;
publish . disabled = true ;
console . info ( ` event published by ${ url } ` , ev ) ;
if ( lastReplyBtn ) {
lastReplyBtn . hidden = false ;
lastReplyBtn = null ;
replyTo = null ;
document . querySelector ( '#newMessage' ) . append ( writeForm ) ;
}
// console.info(`event published by ${url}`, ev);
}
}
} ) ;
} ) ;
}
}
} ) ;
} ) ;
const input = document . querySelector ( 'input[name="message"]' ) ;
input . addEventListener ( 'input' , ( ) => publish . disabled = ! input . value ) ;
input . addEventListener ( 'input' , ( ) => publish . disabled = ! input . value ) ;
// settings
// settings