2022-11-08 19:42:55 +00:00
import { relayPool , generatePrivateKey , getPublicKey , signEvent } from 'nostr-tools' ;
2022-12-09 12:52:59 +00:00
import { elem , parseTextContent } from './domutil.js' ;
2022-11-15 08:05:53 +00:00
import { dateTime , formatTime } from './timeutil.js' ;
2022-11-11 18:25:47 +00:00
// curl -H 'accept: application/nostr+json' https://nostr.x1ddos.ch
2022-11-06 01:35:27 +00:00
const pool = relayPool ( ) ;
2022-12-04 22:12:54 +00:00
pool . addRelay ( 'wss://relay.nostr.info' , { read : true , write : true } ) ;
pool . addRelay ( 'wss://relay.damus.io' , { read : true , write : true } ) ;
2022-11-15 08:05:53 +00:00
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});
2022-11-08 19:42:55 +00:00
// read only
// pool.addRelay('wss://nostr.rocks', {read: true, write: false});
2022-11-06 01:35:27 +00:00
function onEvent ( evt , relay ) {
2022-11-06 17:34:33 +00:00
switch ( evt . kind ) {
case 0 :
2022-11-13 08:19:49 +00:00
handleMetadata ( evt , relay ) ;
2022-11-06 17:34:33 +00:00
break ;
case 1 :
2022-11-13 12:15:56 +00:00
handleTextNote ( evt , relay ) ;
2022-11-06 17:34:33 +00:00
break ;
case 2 :
2022-11-13 14:19:02 +00:00
handleRecommendServer ( evt , relay ) ;
2022-11-06 17:34:33 +00:00
break ;
2022-11-11 18:25:47 +00:00
case 3 :
2022-11-21 21:04:39 +00:00
// handleContactList(evt, relay);
2022-11-11 18:25:47 +00:00
break ;
2022-11-20 13:08:38 +00:00
case 7 :
handleReaction ( evt , relay ) ;
2022-11-06 17:34:33 +00:00
default :
2022-11-14 22:01:30 +00:00
// console.log(`TODO: add support for event kind ${evt.kind}`/*, evt*/)
2022-11-06 15:35:46 +00:00
}
2022-11-06 17:34:33 +00:00
}
2022-11-21 21:04:39 +00:00
let pubkey = localStorage . getItem ( 'pub_key' ) || ( ( ) => {
const privatekey = generatePrivateKey ( ) ;
const pubkey = getPublicKey ( privatekey ) ;
localStorage . setItem ( 'private_key' , privatekey ) ;
localStorage . setItem ( 'pub_key' , pubkey ) ;
return pubkey ;
} ) ( ) ;
2022-11-08 19:42:55 +00:00
2022-11-07 19:54:41 +00:00
const subscription = pool . sub ( {
2022-11-06 17:34:33 +00:00
cb : onEvent ,
filter : {
2022-11-13 14:19:02 +00:00
// authors: [
// '52155da703585f25053830ac39863c80ea6adc57b360472c16c566a412d2bc38', // quark
// 'a6057742e73ff93b89587c27a74edf2cdab86904291416e90dc98af1c5f70cfa', // mosc
// '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d', // fiatjaf
// '52155da703585f25053830ac39863c80ea6adc57b360472c16c566a412d2bc38', // x1ddos
// // pubkey, // me
// '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', // jb55
// ],
2022-11-15 08:05:53 +00:00
// since: new Date(Date.now() - (24 * 60 * 60 * 1000)),
2022-12-09 23:48:48 +00:00
limit : 250 ,
2022-11-06 15:35:46 +00:00
}
2022-11-06 17:34:33 +00:00
} ) ;
2022-11-27 07:49:35 +00:00
const textNoteList = [ ] ; // could use indexDB
const eventRelayMap = { } ; // eventId: [relay1, relay2]
2022-11-12 07:46:51 +00:00
const hasEventTag = tag => tag [ 0 ] === 'e' ;
2022-11-13 12:15:56 +00:00
function handleTextNote ( evt , relay ) {
if ( eventRelayMap [ evt . id ] ) {
eventRelayMap [ evt . id ] = [ relay , ... ( eventRelayMap [ evt . id ] ) ] ;
} else {
eventRelayMap [ evt . id ] = [ relay ] ;
2022-11-14 22:01:30 +00:00
if ( evt . tags . some ( hasEventTag ) ) {
2022-11-15 08:05:53 +00:00
handleReply ( evt , relay ) ;
2022-11-14 22:01:30 +00:00
} else {
textNoteList . push ( evt ) ;
}
2022-11-14 07:15:20 +00:00
renderFeed ( ) ;
2022-11-13 12:15:56 +00:00
}
}
2022-11-20 13:08:38 +00:00
const replyList = [ ] ;
const reactionMap = { } ;
2022-12-09 21:19:11 +00:00
const getReactionList = ( id ) => {
return reactionMap [ id ] ? . map ( ( { content } ) => content ) || [ ] ;
} ;
2022-11-20 13:08:38 +00:00
function handleReaction ( evt , relay ) {
if ( ! evt . content . length ) {
// console.log('reaction with no content', evt)
return ;
}
const eventTags = evt . tags . filter ( hasEventTag ) ;
let replies = eventTags . filter ( ( [ tag , eventId , relayUrl , marker ] ) => marker === 'reply' ) ;
if ( replies . length === 0 ) {
// deprecated https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated
replies = eventTags . filter ( ( tags ) => tags [ 3 ] === undefined ) ;
}
if ( replies . length !== 1 ) {
console . log ( 'call me' , evt ) ;
return ;
}
const [ tag , eventId /*, relayUrl, marker*/ ] = replies [ 0 ] ;
if ( reactionMap [ eventId ] ) {
if ( reactionMap [ eventId ] . find ( reaction => reaction . id === evt . id ) ) {
// already received this reaction from a different relay
return ;
}
reactionMap [ eventId ] = [ evt , ... ( reactionMap [ eventId ] ) ] ;
} else {
reactionMap [ eventId ] = [ evt ] ;
}
const article = feedDomMap [ eventId ] || replyDomMap [ eventId ] ;
if ( article ) {
const button = article . querySelector ( 'button[name="star"]' ) ;
const reactions = button . querySelector ( '[data-reactions]' ) ;
reactions . textContent = reactionMap [ eventId ] . length ;
if ( evt . pubkey === pubkey ) {
2022-12-09 21:19:11 +00:00
const star = button . querySelector ( 'img[src*="star"]' ) ;
star ? . setAttribute ( 'src' , 'assets/star-fill.svg' ) ;
star ? . setAttribute ( 'title' , getReactionList ( eventId ) . join ( ' ' ) ) ;
2022-11-20 13:08:38 +00:00
}
}
}
2022-11-13 12:15:56 +00:00
// feed
2022-11-19 18:27:33 +00:00
const feedContainer = document . querySelector ( '#homefeed' ) ;
2022-11-13 12:15:56 +00:00
const feedDomMap = { } ;
2022-12-04 15:22:41 +00:00
const replyDomMap = { } ;
const restoredReplyTo = localStorage . getItem ( 'reply_to' ) ;
2022-11-20 13:08:38 +00:00
2022-11-13 14:19:02 +00:00
const sortByCreatedAt = ( evt1 , evt2 ) => {
if ( evt1 . created _at === evt2 . created _at ) {
2022-11-14 07:15:20 +00:00
// console.log('TODO: OMG exactly at the same time, figure out how to sort then', evt1, evt2);
2022-11-13 14:19:02 +00:00
}
return evt1 . created _at > evt2 . created _at ? - 1 : 1 ;
} ;
2022-11-13 12:15:56 +00:00
function renderFeed ( ) {
2022-11-13 14:19:02 +00:00
const sortedFeeds = textNoteList . sort ( sortByCreatedAt ) . reverse ( ) ;
2022-12-04 15:27:13 +00:00
sortedFeeds . forEach ( ( evt , i ) => {
if ( feedDomMap [ evt . id ] ) {
2022-11-13 12:15:56 +00:00
// TODO check eventRelayMap if event was published to different relays
return ;
}
2022-12-04 15:27:13 +00:00
const article = createTextNote ( evt , eventRelayMap [ evt . id ] ) ;
2022-11-13 12:15:56 +00:00
if ( i === 0 ) {
2022-11-14 22:01:30 +00:00
feedContainer . append ( article ) ;
2022-11-13 12:15:56 +00:00
} else {
2022-11-14 22:01:30 +00:00
feedDomMap [ sortedFeeds [ i - 1 ] . id ] . before ( article ) ;
2022-11-13 12:15:56 +00:00
}
2022-12-04 15:27:13 +00:00
feedDomMap [ evt . id ] = article ;
2022-11-13 12:15:56 +00:00
} ) ;
}
2022-11-15 08:05:53 +00:00
setInterval ( ( ) => {
document . querySelectorAll ( 'time[datetime]' ) . forEach ( timeElem => {
timeElem . textContent = formatTime ( new Date ( timeElem . dateTime ) ) ;
} ) ;
} , 10000 ) ;
2022-11-12 07:46:51 +00:00
2022-12-09 23:48:48 +00:00
const getNoxyUrl = ( type , url , id , relay ) => {
if ( ! isHttpUrl ( url ) ) {
return false ;
}
const link = new URL ( ` https://noxy.nostr.ch/ ${ type } ` ) ;
link . searchParams . set ( 'id' , id ) ;
link . searchParams . set ( 'relay' , relay ) ;
link . searchParams . set ( 'url' , url ) ;
return link ;
}
2022-12-10 12:50:36 +00:00
const fetchQue = [ ] ;
let fetchPending ;
const fetchNext = ( href , id , relay ) => {
2022-12-09 23:48:48 +00:00
const noxy = getNoxyUrl ( 'meta' , href , id , relay ) ;
const previewId = noxy . searchParams . toString ( ) ;
2022-12-10 12:50:36 +00:00
if ( fetchPending ) {
fetchQue . push ( { href , id , relay } ) ;
return previewId ;
}
fetchPending = fetch ( noxy . href )
. then ( data => {
if ( data . status === 200 ) {
return data . json ( ) ;
}
// fetchQue.push({href, id, relay}); // could try one more time
return Promise . reject ( data ) ;
} )
2022-12-09 23:48:48 +00:00
. then ( meta => {
const container = document . getElementById ( previewId ) ;
2022-12-10 15:48:24 +00:00
const content = [ ] ;
if ( meta . images [ 0 ] ) {
content . push ( elem ( 'img' , { className : 'preview-image' , loading : 'lazy' , src : getNoxyUrl ( 'data' , meta . images [ 0 ] , id , relay ) . href } ) ) ;
}
if ( meta . title ) {
content . push ( elem ( 'h2' , { className : 'preview-title' } , meta . title ) ) ;
}
if ( meta . descr ) {
content . push ( elem ( 'p' , { className : 'preview-descr' } , meta . descr ) )
}
if ( content . length ) {
container . append ( elem ( 'a' , { href , rel : 'noopener noreferrer' , target : '_blank' } , content ) ) ;
container . classList . add ( 'preview-loaded' ) ;
}
2022-12-09 23:48:48 +00:00
} )
2022-12-10 12:50:36 +00:00
. finally ( ( ) => {
fetchPending = false ;
if ( fetchQue . length ) {
const { href , id , relay } = fetchQue . shift ( ) ;
return fetchNext ( href , id , relay ) ;
}
} )
2022-12-09 23:48:48 +00:00
. catch ( console . warn ) ;
2022-12-10 12:50:36 +00:00
return previewId ;
} ;
function linkPreview ( href , id , relay ) {
if ( ( /\.(gif|jpe?g|png)$/i ) . test ( href ) ) {
return elem ( 'div' , { } ,
[ elem ( 'img' , { className : 'preview-image-only' , loading : 'lazy' , src : getNoxyUrl ( 'data' , href , id , relay ) . href } ) ]
) ;
}
const previewId = fetchNext ( href , id , relay ) ;
2022-12-09 23:48:48 +00:00
return elem ( 'div' , {
className : 'preview' ,
id : previewId
} ) ;
}
2022-11-14 22:01:30 +00:00
function createTextNote ( evt , relay ) {
2022-12-08 06:47:04 +00:00
const { host , img , isReply , name , replies , time , userName } = getMetadata ( evt , relay ) ;
2022-12-09 12:52:59 +00:00
// const isLongContent = evt.content.trimRight().length > 280;
// const content = isLongContent ? evt.content.slice(0, 280) : evt.content;
2022-11-20 13:08:38 +00:00
const hasReactions = reactionMap [ evt . id ] ? . length > 0 ;
const didReact = hasReactions && ! ! reactionMap [ evt . id ] . find ( reaction => reaction . pubkey === pubkey ) ;
const replyFeed = replies [ 0 ] ? replies . map ( e => replyDomMap [ e . id ] = createTextNote ( e , relay ) ) : [ ] ;
2022-12-09 23:48:48 +00:00
const [ content , { firstLink } ] = parseTextContent ( evt . content ) ;
2022-11-12 10:54:49 +00:00
const body = elem ( 'div' , { className : 'mbox-body' } , [
2022-11-18 22:28:28 +00:00
elem ( 'header' , {
className : 'mbox-header' ,
2022-11-19 18:27:33 +00:00
title : ` User: ${ userName } \n ${ time } \n \n User pubkey: ${ evt . pubkey } \n \n Relay: ${ host } \n \n Event-id: ${ evt . id }
2022-11-20 13:08:38 +00:00
$ { evt . tags . length ? ` \n Tags ${ JSON . stringify ( evt . tags ) } \n ` : '' }
2022-12-09 23:48:48 +00:00
$ { isReply ? ` \n Reply to ${ evt . tags [ 0 ] [ 1 ] } \n ` : '' }
$ { evt . content } `
2022-11-18 22:28:28 +00:00
} , [
2022-11-19 18:27:33 +00:00
elem ( 'small' , { } , [
2022-12-08 06:47:04 +00:00
elem ( 'strong' , { className : name ? 'mbox-kind0-name' : 'mbox-username' } , name || userName ) ,
2022-11-19 18:27:33 +00:00
' ' ,
2022-11-20 13:08:38 +00:00
elem ( 'time' , { dateTime : time . toISOString ( ) } , formatTime ( time ) ) ,
2022-11-19 18:27:33 +00:00
] ) ,
2022-11-12 10:54:49 +00:00
] ) ,
2022-12-09 23:48:48 +00:00
elem ( 'div' , { /* data: isLongContent ? {append: evt.content.slice(280)} : null*/ } , [
... content ,
firstLink ? linkPreview ( firstLink , evt . id , relay ) : ''
] ) ,
2022-11-15 08:05:53 +00:00
elem ( 'button' , {
2022-11-19 18:27:33 +00:00
className : 'btn-inline' , name : 'star' , type : 'button' ,
data : { 'eventId' : evt . id , relay } ,
2022-11-15 08:05:53 +00:00
} , [
2022-11-21 21:04:39 +00:00
elem ( 'img' , {
alt : didReact ? '✭' : '✩' , // ♥
height : 24 , width : 24 ,
src : ` assets/ ${ didReact ? 'star-fill' : 'star' } .svg ` ,
2022-12-09 21:19:11 +00:00
title : getReactionList ( evt . id ) . join ( ' ' ) ,
2022-11-21 21:04:39 +00:00
} ) ,
2022-11-20 13:08:38 +00:00
elem ( 'small' , { data : { reactions : evt . id } } , hasReactions ? reactionMap [ evt . id ] . length : '' ) ,
2022-11-15 08:05:53 +00:00
] ) ,
2022-11-21 21:04:39 +00:00
elem ( 'button' , {
className : 'btn-inline' , name : 'reply' , type : 'button' ,
data : { 'eventId' : evt . id , relay } ,
} , [ elem ( 'img' , { height : 24 , width : 24 , src : 'assets/comment.svg' } ) ] ) ,
2022-12-04 22:12:54 +00:00
// replies[0] ? elem('div', {className: 'mobx-replies'}, replyFeed.reverse()) : '',
] ) ;
if ( restoredReplyTo === evt . id ) {
appendReplyForm ( body . querySelector ( 'button[name="reply"]' ) ) ;
requestAnimationFrame ( ( ) => updateElemHeight ( writeInput ) ) ;
}
return rendernArticle ( [
2022-12-09 10:36:52 +00:00
elem ( 'div' , { className : 'mbox-img' } , [ img ] ) , body ,
2022-11-21 21:04:39 +00:00
replies [ 0 ] ? elem ( 'div' , { className : 'mobx-replies' } , replyFeed . reverse ( ) ) : '' ,
2022-11-06 15:35:46 +00:00
] ) ;
2022-11-06 01:35:27 +00:00
}
2022-11-15 08:05:53 +00:00
function handleReply ( evt , relay ) {
2022-11-20 13:08:38 +00:00
if ( replyDomMap [ evt . id ] ) {
console . log ( 'CALL ME already have reply in replyDomMap' , evt , relay ) ;
return ;
2022-11-15 08:05:53 +00:00
}
2022-11-20 13:08:38 +00:00
replyList . push ( evt ) ;
renderReply ( evt , relay ) ;
}
function renderReply ( evt , relay ) {
const eventId = evt . tags [ 0 ] [ 1 ] ; // TODO: double check
const article = feedDomMap [ eventId ] || replyDomMap [ eventId ] ;
2022-11-21 21:04:39 +00:00
if ( ! article ) { // root article has not been rendered
2022-11-20 13:08:38 +00:00
return ;
}
let replyContainer = article . querySelector ( '.mobx-replies' ) ;
if ( ! replyContainer ) {
replyContainer = elem ( 'div' , { className : 'mobx-replies' } ) ;
2022-12-04 22:12:54 +00:00
article . append ( replyContainer ) ;
2022-11-20 13:08:38 +00:00
}
const reply = createTextNote ( evt , relay ) ;
replyContainer . append ( reply ) ;
replyDomMap [ evt . id ] = reply ;
2022-11-15 08:05:53 +00:00
}
const sortEventCreatedAt = ( created _at ) => (
{ created _at : a } ,
{ created _at : b } ,
) => (
Math . abs ( a - created _at ) < Math . abs ( b - created _at ) ? - 1 : 1
) ;
2022-11-13 14:19:02 +00:00
function handleRecommendServer ( evt , relay ) {
if ( feedDomMap [ evt . id ] ) {
return ;
}
const art = renderRecommendServer ( evt , relay ) ;
if ( textNoteList . length < 2 ) {
feedContainer . append ( art ) ;
return ;
}
2022-11-15 08:05:53 +00:00
const closestTextNotes = textNoteList . sort ( sortEventCreatedAt ( evt . created _at ) ) ;
2022-11-13 14:19:02 +00:00
feedDomMap [ closestTextNotes [ 0 ] . id ] . after ( art ) ;
feedDomMap [ evt . id ] = art ;
}
2022-11-21 21:04:39 +00:00
function handleContactList ( evt , relay ) {
if ( feedDomMap [ evt . id ] ) {
return ;
}
const art = renderUpdateContact ( evt , relay ) ;
if ( textNoteList . length < 2 ) {
feedContainer . append ( art ) ;
return ;
}
const closestTextNotes = textNoteList . sort ( sortEventCreatedAt ( evt . created _at ) ) ;
feedDomMap [ closestTextNotes [ 0 ] . id ] . after ( art ) ;
feedDomMap [ evt . id ] = art ;
// const user = userList.find(u => u.pupkey === evt.pubkey);
// if (user) {
// console.log(`TODO: add contact list for ${evt.pubkey.slice(0, 8)} on ${relay}`, evt.tags);
// } else {
// tempContactList[relay] = tempContactList[relay]
// ? [...tempContactList[relay], evt]
// : [evt];
// }
}
function renderUpdateContact ( evt , relay ) {
const { img , time , userName } = getMetadata ( evt , relay ) ;
const body = elem ( 'div' , { className : 'mbox-body' , title : dateTime . format ( time ) } , [
elem ( 'header' , { className : 'mbox-header' } , [
elem ( 'small' , { } , [
] ) ,
] ) ,
elem ( 'pre' , { title : JSON . stringify ( evt . content ) } , [
elem ( 'strong' , { } , userName ) ,
' updated contacts: ' ,
JSON . stringify ( evt . tags ) ,
] ) ,
] ) ;
return rendernArticle ( [ img , body ] , { className : 'mbox-updated-contact' } ) ;
}
2022-11-06 17:34:33 +00:00
function renderRecommendServer ( evt , relay ) {
2022-12-08 07:43:08 +00:00
const { img , name , time , userName } = getMetadata ( evt , relay ) ;
2022-11-06 17:34:33 +00:00
const body = elem ( 'div' , { className : 'mbox-body' , title : dateTime . format ( time ) } , [
2022-11-14 22:01:30 +00:00
elem ( 'header' , { className : 'mbox-header' } , [
elem ( 'small' , { } , [
2022-11-15 08:05:53 +00:00
elem ( 'strong' , { } , userName )
2022-11-14 22:01:30 +00:00
] ) ,
] ) ,
2022-11-15 08:05:53 +00:00
` recommends server: ${ evt . content } ` ,
2022-11-06 17:34:33 +00:00
] ) ;
2022-12-09 10:36:52 +00:00
return rendernArticle ( [
elem ( 'div' , { className : 'mbox-img' } , [ img ] ) , body
2022-12-08 07:43:08 +00:00
] , { className : 'mbox-recommend-server' , data : { relay : evt . content } } ) ;
2022-11-06 17:34:33 +00:00
}
2022-11-06 15:35:46 +00:00
2022-11-20 13:08:38 +00:00
function rendernArticle ( content , props = { } ) {
const className = props . className ? [ 'mbox' , props ? . className ] . join ( ' ' ) : 'mbox' ;
2022-11-13 12:15:56 +00:00
return elem ( 'article' , { ... props , className } , content ) ;
2022-11-06 17:34:33 +00:00
}
2022-11-13 08:19:49 +00:00
const userList = [ ] ;
2022-11-21 21:04:39 +00:00
// const tempContactList = {};
2022-11-11 18:25:47 +00:00
2022-11-13 08:19:49 +00:00
function handleMetadata ( evt , relay ) {
try {
const content = JSON . parse ( evt . content ) ;
setMetadata ( evt , relay , content ) ;
} catch ( err ) {
console . log ( evt ) ;
console . error ( err ) ;
}
}
function setMetadata ( evt , relay , content ) {
2022-11-06 15:35:46 +00:00
const user = userList . find ( u => u . pubkey === evt . pubkey ) ;
2022-12-09 23:48:48 +00:00
const picture = getNoxyUrl ( 'data' , content . picture , evt . id , relay ) . href ;
2022-11-06 15:35:46 +00:00
if ( ! user ) {
userList . push ( {
2022-11-21 21:04:39 +00:00
metadata : { [ relay ] : content } ,
2022-12-06 21:03:00 +00:00
... ( content . picture && { picture } ) ,
2022-11-06 15:35:46 +00:00
pubkey : evt . pubkey ,
} ) ;
} else {
user . metadata [ relay ] = {
... user . metadata [ relay ] ,
timestamp : evt . created _at ,
... content ,
} ;
2022-12-06 21:03:00 +00:00
if ( ! user . picture ) {
user . picture = picture ;
} // no support (yet) for other picture from same pubkey on different relays
2022-11-06 15:35:46 +00:00
}
2022-11-21 21:04:39 +00:00
// if (tempContactList[relay]) {
// const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey);
// if (updates) {
// console.log('TODO: add contact list (kind 3)', updates);
// }
// }
2022-11-11 18:25:47 +00:00
}
2022-12-06 21:03:00 +00:00
function isHttpUrl ( string ) {
try {
return [ 'http:' , 'https:' ] . includes ( new URL ( string ) . protocol ) ;
} catch ( err ) {
return false ;
}
}
2022-11-14 22:01:30 +00:00
const getHost = ( url ) => {
try {
return new URL ( url ) . host ;
2022-11-15 08:05:53 +00:00
} catch ( err ) {
return err ;
2022-11-14 22:01:30 +00:00
}
}
2022-12-09 10:36:52 +00:00
const elemCanvas = ( text ) => {
const canvas = elem ( 'canvas' , { height : 80 , width : 80 } ) ;
const context = canvas . getContext ( '2d' ) ;
const color = ` # ${ text . slice ( 0 , 6 ) } ` ;
context . fillStyle = color ;
context . fillRect ( 0 , 0 , 80 , 80 ) ;
context . fillStyle = '#111' ;
context . fillRect ( 0 , 50 , 80 , 32 ) ;
context . font = 'bold 18px monospace' ;
if ( color === '#000000' ) {
context . fillStyle = '#fff' ;
}
context . fillText ( text , 2 , 46 ) ;
return canvas ;
}
2022-11-13 08:19:49 +00:00
function getMetadata ( evt , relay ) {
2022-11-15 08:05:53 +00:00
const host = getHost ( relay ) ;
2022-11-13 08:19:49 +00:00
const user = userList . find ( user => user . pubkey === evt . pubkey ) ;
2022-12-08 07:43:08 +00:00
const userImg = user ? . picture ;
2022-12-08 06:47:04 +00:00
const name = user ? . metadata [ relay ] ? . name ;
const userName = name || evt . pubkey . slice ( 0 , 8 ) ;
2022-11-13 08:19:49 +00:00
const userAbout = user ? . metadata [ relay ] ? . about || '' ;
2022-12-09 10:36:52 +00:00
const img = userImg ? elem ( 'img' , {
2022-11-19 18:27:33 +00:00
alt : ` ${ userName } ${ host } ` ,
2022-12-10 11:21:05 +00:00
loading : 'lazy' ,
src : userImg ,
2022-11-19 18:27:33 +00:00
title : ` ${ userName } on ${ host } ${ userAbout } ` ,
2022-12-09 10:36:52 +00:00
} ) : elemCanvas ( evt . pubkey ) ;
2022-11-18 22:28:28 +00:00
const isReply = evt . tags . some ( hasEventTag ) ;
2022-11-14 22:01:30 +00:00
const replies = replyList . filter ( ( reply ) => reply . tags [ 0 ] [ 1 ] === evt . id ) ;
2022-11-13 08:19:49 +00:00
const time = new Date ( evt . created _at * 1000 ) ;
2022-12-08 06:47:04 +00:00
return { host , img , isReply , name , replies , time , userName } ;
2022-11-13 08:19:49 +00:00
}
2022-11-15 08:05:53 +00:00
feedContainer . addEventListener ( 'click' , ( e ) => {
const button = e . target . closest ( 'button' ) ;
if ( button && button . name === 'reply' ) {
2022-12-08 21:25:33 +00:00
if ( localStorage . getItem ( 'reply_to' ) === button . dataset . eventId ) {
writeInput . blur ( ) ;
return ;
}
2022-12-04 15:22:41 +00:00
appendReplyForm ( button ) ;
localStorage . setItem ( 'reply_to' , button . dataset . eventId ) ;
2022-11-18 22:28:28 +00:00
return ;
2022-11-15 08:05:53 +00:00
}
2022-11-20 13:08:38 +00:00
if ( button && button . name === 'star' ) {
upvote ( button . dataset . eventId , button . dataset . relay )
return ;
}
2022-11-15 08:05:53 +00:00
} ) ;
2022-12-04 15:22:41 +00:00
const writeForm = document . querySelector ( '#writeForm' ) ;
const writeInput = document . querySelector ( 'textarea[name="message"]' ) ;
2022-12-08 21:25:33 +00:00
const elemShrink = ( ) => {
const height = writeInput . style . height || writeInput . getBoundingClientRect ( ) . height ;
2022-12-04 22:12:54 +00:00
const shrink = elem ( 'div' , { className : 'shrink-out' } ) ;
2022-12-08 21:25:33 +00:00
shrink . style . height = ` ${ height } px ` ;
shrink . addEventListener ( 'animationend' , ( ) => shrink . remove ( ) , { once : true } ) ;
return shrink ;
}
writeInput . addEventListener ( 'focusout' , ( ) => {
const reply _to = localStorage . getItem ( 'reply_to' ) ;
if ( reply _to && writeInput . value === '' ) {
writeInput . addEventListener ( 'transitionend' , ( event ) => {
if ( ! reply _to || reply _to === localStorage . getItem ( 'reply_to' ) && ! writeInput . style . height ) { // should prob use some class or data-attr instead of relying on height
writeForm . after ( elemShrink ( ) ) ;
writeForm . remove ( ) ;
localStorage . removeItem ( 'reply_to' ) ;
}
} , { once : true } ) ;
}
} ) ;
function appendReplyForm ( el ) {
writeForm . before ( elemShrink ( ) ) ;
2022-12-04 22:12:54 +00:00
writeInput . blur ( ) ;
writeInput . style . removeProperty ( 'height' ) ;
2022-12-04 15:22:41 +00:00
el . after ( writeForm ) ;
2022-12-04 22:12:54 +00:00
if ( writeInput . value && ! writeInput . value . trimRight ( ) ) {
writeInput . value = '' ;
} else {
requestAnimationFrame ( ( ) => updateElemHeight ( writeInput ) ) ;
}
requestAnimationFrame ( ( ) => writeInput . focus ( ) ) ;
2022-12-04 15:22:41 +00:00
}
2022-11-21 21:04:39 +00:00
const newMessageDiv = document . querySelector ( '#newMessage' ) ;
document . querySelector ( '#bubble' ) . addEventListener ( 'click' , ( e ) => {
2022-12-04 22:12:54 +00:00
localStorage . removeItem ( 'reply_to' ) ; // should it forget old replyto context?
2022-11-21 21:04:39 +00:00
newMessageDiv . prepend ( writeForm ) ;
2022-12-04 22:12:54 +00:00
hideNewMessage ( false ) ;
2022-12-04 15:22:41 +00:00
writeInput . focus ( ) ;
2022-12-04 22:12:54 +00:00
if ( writeInput . value . trimRight ( ) ) {
writeInput . style . removeProperty ( 'height' ) ;
}
document . body . style . overflow = 'hidden' ;
requestAnimationFrame ( ( ) => updateElemHeight ( writeInput ) ) ;
2022-11-21 21:04:39 +00:00
} ) ;
2022-12-04 22:12:54 +00:00
document . body . addEventListener ( 'keyup' , ( e ) => {
if ( e . key === 'Escape' ) {
hideNewMessage ( true ) ;
}
} ) ;
function hideNewMessage ( hide ) {
document . body . style . removeProperty ( 'overflow' ) ;
newMessageDiv . hidden = hide ;
}
2022-11-20 13:08:38 +00:00
async function upvote ( eventId , relay ) {
const privatekey = localStorage . getItem ( 'private_key' ) ;
const newReaction = {
kind : 7 ,
pubkey , // TODO: lib could check that this is the pubkey of the key to sign with
content : '+' ,
tags : [ [ 'e' , eventId , relay , 'reply' ] ] ,
created _at : Math . floor ( Date . now ( ) * 0.001 ) ,
} ;
const sig = await signEvent ( newReaction , privatekey ) . catch ( console . error ) ;
if ( sig ) {
const ev = await pool . publish ( { ... newReaction , sig } , ( status , url ) => {
if ( status === 0 ) {
console . info ( ` publish request sent to ${ url } ` ) ;
}
if ( status === 1 ) {
console . info ( ` event published by ${ url } ` ) ;
}
} ) . catch ( console . error ) ;
}
}
2022-11-13 20:20:42 +00:00
// send
const sendStatus = document . querySelector ( '#sendstatus' ) ;
2022-12-04 22:12:54 +00:00
const onSendError = err => sendStatus . textContent = err . message ;
2022-11-08 19:42:55 +00:00
const publish = document . querySelector ( '#publish' ) ;
2022-11-21 21:04:39 +00:00
writeForm . addEventListener ( 'submit' , async ( e ) => {
e . preventDefault ( ) ;
2022-11-20 13:08:38 +00:00
// const pubkey = localStorage.getItem('pub_key');
2022-11-08 19:42:55 +00:00
const privatekey = localStorage . getItem ( 'private_key' ) ;
if ( ! pubkey || ! privatekey ) {
2022-11-13 20:20:42 +00:00
return onSendError ( new Error ( 'no pubkey/privatekey' ) ) ;
}
2022-12-04 15:57:31 +00:00
const content = writeInput . value . trimRight ( ) ;
if ( ! content ) {
2022-11-13 20:20:42 +00:00
return onSendError ( new Error ( 'message is empty' ) ) ;
2022-11-08 19:42:55 +00:00
}
2022-12-04 15:22:41 +00:00
const replyTo = localStorage . getItem ( 'reply_to' ) ;
const tags = replyTo ? [ [ 'e' , replyTo , eventRelayMap [ replyTo ] [ 0 ] ] ] : [ ] ;
2022-11-08 19:42:55 +00:00
const newEvent = {
kind : 1 ,
pubkey ,
2022-12-04 15:57:31 +00:00
content ,
2022-11-15 08:05:53 +00:00
tags ,
2022-11-08 19:42:55 +00:00
created _at : Math . floor ( Date . now ( ) * 0.001 ) ,
} ;
2022-11-13 20:20:42 +00:00
const sig = await signEvent ( newEvent , privatekey ) . catch ( onSendError ) ;
if ( sig ) {
const ev = await pool . publish ( { ... newEvent , sig } , ( status , url ) => {
if ( status === 0 ) {
console . info ( ` publish request sent to ${ url } ` ) ;
}
if ( status === 1 ) {
2022-12-04 22:12:54 +00:00
sendStatus . textContent = '' ;
2022-12-04 15:22:41 +00:00
writeInput . value = '' ;
writeInput . style . removeProperty ( 'height' ) ;
2022-11-13 20:20:42 +00:00
publish . disabled = true ;
2022-12-04 15:22:41 +00:00
if ( replyTo ) {
localStorage . removeItem ( 'reply_to' ) ;
2022-11-21 21:04:39 +00:00
newMessageDiv . append ( writeForm ) ;
2022-11-15 08:05:53 +00:00
}
2022-12-10 08:20:38 +00:00
hideNewMessage ( true ) ;
2022-11-15 08:05:53 +00:00
// console.info(`event published by ${url}`, ev);
2022-11-13 20:20:42 +00:00
}
} ) ;
}
} ) ;
2022-12-04 15:22:41 +00:00
writeInput . addEventListener ( 'input' , ( ) => {
2022-12-04 15:57:31 +00:00
publish . disabled = ! writeInput . value . trimRight ( ) ;
2022-12-04 15:22:41 +00:00
updateElemHeight ( writeInput ) ;
} ) ;
writeInput . addEventListener ( 'blur' , ( ) => sendStatus . textContent = '' ) ;
function updateElemHeight ( el ) {
el . style . removeProperty ( 'height' ) ;
if ( el . value ) {
el . style . paddingBottom = 0 ;
el . style . paddingTop = 0 ;
el . style . height = el . scrollHeight + 'px' ;
el . style . removeProperty ( 'padding-bottom' ) ;
el . style . removeProperty ( 'padding-top' ) ;
}
}
2022-11-06 22:10:42 +00:00
2022-11-08 19:42:55 +00:00
// settings
2022-11-23 18:49:38 +00:00
const settingsForm = document . querySelector ( 'form[name="settings"]' ) ;
const privateKeyInput = settingsForm . querySelector ( '#privatekey' ) ;
const pubKeyInput = settingsForm . querySelector ( '#pubkey' ) ;
const statusMessage = settingsForm . querySelector ( '#keystatus' ) ;
const generateBtn = settingsForm . querySelector ( 'button[name="generate"]' ) ;
const importBtn = settingsForm . querySelector ( 'button[name="import"]' ) ;
const privateTgl = settingsForm . querySelector ( 'button[name="privatekey-toggle"]' ) ;
2022-11-06 22:10:42 +00:00
2022-11-07 19:54:41 +00:00
generateBtn . addEventListener ( 'click' , ( ) => {
2022-11-08 19:42:55 +00:00
const privatekey = generatePrivateKey ( ) ;
const pubkey = getPublicKey ( privatekey ) ;
if ( validKeys ( privatekey , pubkey ) ) {
privateKeyInput . value = privatekey ;
pubKeyInput . value = pubkey ;
2022-11-07 19:54:41 +00:00
statusMessage . textContent = 'private-key created!' ;
statusMessage . hidden = false ;
2022-11-06 22:10:42 +00:00
}
} ) ;
2022-11-07 19:54:41 +00:00
importBtn . addEventListener ( 'click' , ( ) => {
2022-11-08 19:42:55 +00:00
const privatekey = privateKeyInput . value ;
2022-11-20 13:08:38 +00:00
const pubkeyInput = pubKeyInput . value ;
if ( validKeys ( privatekey , pubkeyInput ) ) {
2022-11-08 19:42:55 +00:00
localStorage . setItem ( 'private_key' , privatekey ) ;
2022-11-20 13:08:38 +00:00
localStorage . setItem ( 'pub_key' , pubkeyInput ) ;
2022-11-08 19:42:55 +00:00
statusMessage . textContent = 'stored private and public key locally!' ;
2022-11-07 19:54:41 +00:00
statusMessage . hidden = false ;
2022-11-20 13:08:38 +00:00
pubkey = pubkeyInput ;
2022-11-06 22:10:42 +00:00
}
} ) ;
2022-11-23 18:49:38 +00:00
settingsForm . addEventListener ( 'input' , ( ) => validKeys ( privateKeyInput . value , pubKeyInput . value ) ) ;
2022-12-08 22:12:50 +00:00
privateKeyInput . addEventListener ( 'paste' , ( event ) => {
if ( pubKeyInput . value || ! event . clipboardData ) {
return ;
}
2022-12-10 13:05:46 +00:00
if ( privateKeyInput . value === '' || ( // either privatekey field is empty
privateKeyInput . selectionStart === 0 // or the whole text is selected and replaced with the clipboard
2022-12-08 22:12:50 +00:00
&& privateKeyInput . selectionEnd === privateKeyInput . value . length
2022-12-10 13:05:46 +00:00
) ) { // only generate the pubkey if no data other than the text from clipboard will be used
2022-12-08 22:12:50 +00:00
try {
pubKeyInput . value = getPublicKey ( event . clipboardData . getData ( 'text' ) ) ;
} catch ( err ) { } // settings form will call validKeys on input and display the error
}
} ) ;
2022-11-06 22:10:42 +00:00
2022-11-08 19:42:55 +00:00
function validKeys ( privatekey , pubkey ) {
2022-12-06 19:57:20 +00:00
try {
if ( getPublicKey ( privatekey ) === pubkey ) {
statusMessage . hidden = true ;
statusMessage . textContent = 'public-key corresponds to private-key' ;
importBtn . removeAttribute ( 'disabled' ) ;
return true ;
} else {
statusMessage . textContent = 'private-key does not correspond to public-key!'
2022-11-06 22:10:42 +00:00
}
2022-12-06 19:57:20 +00:00
} catch ( e ) {
statusMessage . textContent = ` not a valid private-key: ${ e . message || e } ` ;
2022-11-06 22:10:42 +00:00
}
2022-11-07 19:54:41 +00:00
statusMessage . hidden = false ;
2022-11-06 22:10:42 +00:00
importBtn . setAttribute ( 'disabled' , true ) ;
2022-11-19 18:27:33 +00:00
return false ;
2022-11-06 22:10:42 +00:00
}
2022-11-07 19:54:41 +00:00
privateTgl . addEventListener ( 'click' , ( ) => {
privateKeyInput . type = privateKeyInput . type === 'text' ? 'password' : 'text' ;
2022-11-06 22:10:42 +00:00
} ) ;
2022-11-07 19:54:41 +00:00
2022-11-08 19:42:55 +00:00
privateKeyInput . value = localStorage . getItem ( 'private_key' ) ;
pubKeyInput . value = localStorage . getItem ( 'pub_key' ) ;
2022-11-18 22:28:28 +00:00
document . body . addEventListener ( 'click' , ( e ) => {
2022-12-09 12:52:59 +00:00
// const container = e.target.closest('[data-append]');
// if (container) {
// container.append(...parseTextContent(container.dataset.append));
// delete container.dataset.append;
// return;
// }
2022-12-04 22:12:54 +00:00
const back = e . target . closest ( '[name="back"]' )
if ( back ) {
hideNewMessage ( true ) ;
}
2022-11-21 21:04:39 +00:00
} ) ;