@ -4,15 +4,15 @@ import {elem} from './utils/dom';
import { bounce } from './utils/time' ;
import { bounce } from './utils/time' ;
import { isWssUrl } from './utils/url' ;
import { isWssUrl } from './utils/url' ;
import { closeSettingsView , config , toggleSettingsView } from './settings' ;
import { closeSettingsView , config , toggleSettingsView } from './settings' ;
import { sub 24h Feed, subEventID , subNote , subProfile } from './subscriptions'
import { sub Global Feed, subEventID , subNote , subProfile , subPubkeys , subOwnContacts , subContactList } from './subscriptions'
import { getReplyTo , hasEventTag , is Mention, sortByCreatedAt , sortEventCreatedAt } from './events' ;
import { getReplyTo , hasEventTag , is Event, is Mention, sortByCreatedAt , sortEventCreatedAt } from './events' ;
import { clearView , getViewContent , getViewElem , getViewOptions , setViewElem , view } from './view' ;
import { clearView , getViewContent , getViewElem , getViewOptions , setViewElem , view } from './view' ;
import { handleReaction , handleUpvote } from './reactions' ;
import { handleReaction , handleUpvote } from './reactions' ;
import { closePublishView , openWriteInput , togglePublishView } from './write' ;
import { closePublishView , openWriteInput , togglePublishView } from './write' ;
import { handleMetadata , renderProfile } from './profiles' ;
import { handleMetadata , renderProfile } from './profiles' ;
import { getContactUpdateMessage, setContactList, updateContactList } from './contacts' ;
import { followContact, getContactUpdateMessage, getContacts, getOwnContacts , refreshFollowing , resetContactList , setContactList, updateContactList , updateFollowBtn } from './contacts' ;
import { EventWithNip19 , EventWithNip19AndReplyTo , textNoteList , replyList } from './notes' ;
import { EventWithNip19 , EventWithNip19AndReplyTo , textNoteList , replyList } from './notes' ;
import { create TextNote, renderEventDetails , renderRecommendServer , renderUpdateContact } from './ui' ;
import { create Contact, create TextNote, renderEventDetails , renderRecommendServer , renderUpdateContact } from './ui' ;
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
@ -38,6 +38,18 @@ const renderNote = (
setViewElem ( evt . id , article ) ;
setViewElem ( evt . id , article ) ;
} ;
} ;
const renderContact = ( pubkey : string ) = > {
if ( getViewElem ( ` contact- ${ pubkey } ` ) ) { // contact already in view
updateFollowBtn ( pubkey ) ;
return ;
}
const contact = createContact ( pubkey ) ;
if ( contact ) {
getViewContent ( ) . append ( contact ) ;
setViewElem ( ` contact- ${ pubkey } ` , contact ) ;
}
} ;
const hasEnoughPOW = (
const hasEnoughPOW = (
[ tag , , commitment ] : string [ ] ,
[ tag , , commitment ] : string [ ] ,
eventId : string
eventId : string
@ -55,7 +67,6 @@ const renderFeed = bounce(() => {
. forEach ( renderNote ) ;
. forEach ( renderNote ) ;
break ;
break ;
case 'profile' :
case 'profile' :
const isEvent = < T > ( evt? : T ) : evt is T = > evt !== undefined ;
[
[
. . . textNoteList // get notes
. . . textNoteList // get notes
. filter ( note = > note . pubkey === view . id ) ,
. filter ( note = > note . pubkey === view . id ) ,
@ -65,9 +76,24 @@ const renderFeed = bounce(() => {
]
]
. sort ( sortByCreatedAt )
. sort ( sortByCreatedAt )
. reverse ( )
. reverse ( )
. forEach ( renderNote ) ; // render in-reply-to
. forEach ( renderNote ) ;
renderProfile ( view . id ) ;
renderProfile ( view . id ) ;
refreshFollowing ( view . id ) ;
break ;
case 'home' :
const ids = getOwnContacts ( ) ;
[
. . . textNoteList
. filter ( note = > ids . includes ( note . pubkey ) ) ,
. . . replyList // search id in notes and replies
. filter ( reply = > ids . includes ( reply . pubkey ) )
. map ( reply = > textNoteList . find ( note = > note . id === reply . replyTo ) )
. filter ( isEvent ) ,
]
. sort ( sortByCreatedAt )
. reverse ( )
. forEach ( renderNote ) ;
break ;
break ;
case 'feed' :
case 'feed' :
const now = Math . floor ( Date . now ( ) * 0.001 ) ;
const now = Math . floor ( Date . now ( ) * 0.001 ) ;
@ -82,12 +108,16 @@ const renderFeed = bounce(() => {
. reverse ( )
. reverse ( )
. forEach ( renderNote ) ;
. forEach ( renderNote ) ;
break ;
break ;
case 'contacts' :
getContacts ( view . id )
. forEach ( renderContact ) ;
break ;
}
}
} , 17 ) ; // (16.666 rounded, an arbitrary value to limit updates to max 60x per s)
} , 17 ) ; // (16.666 rounded, an arbitrary value to limit updates to max 60x per s)
const renderReply = ( evt : EventWithNip19AndReplyTo ) = > {
const renderReply = ( evt : EventWithNip19AndReplyTo ) = > {
const parent = getViewElem ( evt . replyTo ) ;
const parent = getViewElem ( evt . replyTo ) ;
if ( ! parent ) { // root article has not been rendered
if ( ! parent || getViewElem ( evt . id ) ) {
return ;
return ;
}
}
let replyContainer = parent . querySelector ( '.mbox-replies' ) ;
let replyContainer = parent . querySelector ( '.mbox-replies' ) ;
@ -110,7 +140,6 @@ const handleReply = (evt: EventWithNip19, relay: string) => {
}
}
const replyTo = getReplyTo ( evt ) ;
const replyTo = getReplyTo ( evt ) ;
if ( ! replyTo ) {
if ( ! replyTo ) {
console . warn ( 'expected to find reply-to-event-id' , evt ) ;
return ;
return ;
}
}
const evtWithReplyTo = { replyTo , . . . evt } ;
const evtWithReplyTo = { replyTo , . . . evt } ;
@ -124,7 +153,7 @@ const handleTextNote = (evt: Event, relay: string) => {
return ;
return ;
}
}
if ( eventRelayMap [ evt . id ] ) {
if ( eventRelayMap [ evt . id ] ) {
eventRelayMap [ evt . id ] = [ . . . ( eventRelayMap [ evt . id ] ) , relay ] ; // TODO: just push ?
eventRelayMap [ evt . id ] = [ . . . ( eventRelayMap [ evt . id ] ) , relay ] ; // TODO: remove eventRelayMap and just check for getViewElem ?
} else {
} else {
eventRelayMap [ evt . id ] = [ relay ] ;
eventRelayMap [ evt . id ] = [ relay ] ;
const evtWithNip19 = {
const evtWithNip19 = {
@ -145,38 +174,45 @@ const handleTextNote = (evt: Event, relay: string) => {
}
}
} ;
} ;
config . rerenderFeed = ( ) = > {
const rerenderFeed = ( ) = > {
clearView ( ) ;
clearView ( ) ;
renderFeed ( ) ;
renderFeed ( ) ;
} ;
} ;
config . rerenderFeed = rerenderFeed ;
const handleContactList = ( evt : Event , relay : string ) = > {
const handleContactList = ( evt : Event , relay : string ) = > {
// TODO: if newer and view.type === 'home' rerenderFeed()
setContactList ( evt ) ;
setContactList ( evt ) ;
const view = getViewOptions ( ) ;
const view = getViewOptions ( ) ;
if ( getViewElem ( evt . id ) ) {
return ;
}
if (
if (
getViewElem ( evt . id )
view . type === 'contacts'
|| view . type !== 'profile'
&& [ view . id , config . pubkey ] . includes ( evt . pubkey ) // render if contact-list is from current users or current view
|| view . id !== evt . pubkey
) {
) {
renderFeed ( ) ;
return ;
return ;
}
}
// use find instead of sort?
if ( view . type === 'profile' && view . id === evt . pubkey ) {
const closestTextNotes = textNoteList . sort ( sortEventCreatedAt ( evt . created_at ) ) ;
// use find instead of sort?
const closestNote = getViewElem ( closestTextNotes [ 0 ] . id ) ;
const closestTextNotes = textNoteList . sort ( sortEventCreatedAt ( evt . created_at ) ) ;
if ( ! closestNote ) {
const closestNote = getViewElem ( closestTextNotes [ 0 ] . id ) ;
// no close note, try later
if ( ! closestNote ) {
setTimeout ( ( ) = > handleContactList ( evt , relay ) , 1500 ) ;
// no close note, try later
return ;
setTimeout ( ( ) = > handleContactList ( evt , relay ) , 1500 ) ;
} ;
return ;
const [ addedContacts , removedContacts ] = updateContactList ( evt ) ;
} ;
const content = getContactUpdateMessage ( addedContacts , removedContacts ) ;
const [ addedContacts , removedContacts ] = updateContactList ( evt ) ;
if ( ! content . length ) {
const content = getContactUpdateMessage ( addedContacts , removedContacts ) ;
// P same as before, maybe only evt.content or 'a' tags changed?
if ( ! content . length ) {
return ;
// P same as before, maybe only evt.content or 'a' tags changed?
return ;
}
const art = renderUpdateContact ( { . . . evt , content } , relay ) ;
closestNote . after ( art ) ;
setViewElem ( evt . id , art ) ;
}
}
const art = renderUpdateContact ( { . . . evt , content } , relay ) ;
closestNote . after ( art ) ;
setViewElem ( evt . id , art ) ;
} ;
} ;
const handleRecommendServer = ( evt : Event , relay : string ) = > {
const handleRecommendServer = ( evt : Event , relay : string ) = > {
@ -198,12 +234,12 @@ const handleRecommendServer = (evt: Event, relay: string) => {
} ;
} ;
const onEventDetails = ( evt : Event , relay : string ) = > {
const onEventDetails = ( evt : Event , relay : string ) = > {
if ( getViewElem ( ` detail- ${ evt . id } ` ) ) {
if ( getViewElem ( evt . id ) ) {
return ;
return ;
}
}
const art = renderEventDetails ( evt , relay ) ;
const art icle = renderEventDetails ( evt , relay ) ;
getViewContent ( ) . append ( art ) ;
getViewContent ( ) . append ( art icle ) ;
setViewElem ( ` detail- ${ evt . id } ` , art ) ;
setViewElem ( evt . id , art icle ) ;
} ;
} ;
const onEvent = ( evt : Event , relay : string ) = > {
const onEvent = ( evt : Event , relay : string ) = > {
@ -230,8 +266,19 @@ const onEvent = (evt: Event, relay: string) => {
// subscribe and change view
// subscribe and change view
const route = ( path : string ) = > {
const route = ( path : string ) = > {
if ( path === '/' ) {
if ( path === '/' ) {
sub24hFeed ( onEvent ) ;
const contactList = getOwnContacts ( ) ;
view ( '/' , { type : 'feed' } ) ;
if ( contactList . length ) {
subPubkeys ( contactList , onEvent ) ;
view ( ` / ` , { type : 'home' } ) ;
} else {
subGlobalFeed ( onEvent ) ;
view ( '/feed' , { type : 'feed' } ) ;
}
return ;
}
if ( path === '/feed' ) {
subGlobalFeed ( onEvent ) ;
view ( '/feed' , { type : 'feed' } ) ;
} else if ( path . length === 64 && path . match ( /^\/[0-9a-z]+$/ ) ) {
} else if ( path . length === 64 && path . match ( /^\/[0-9a-z]+$/ ) ) {
const { type , data } = nip19 . decode ( path . slice ( 1 ) ) ;
const { type , data } = nip19 . decode ( path . slice ( 1 ) ) ;
if ( typeof data !== 'string' ) {
if ( typeof data !== 'string' ) {
@ -246,22 +293,31 @@ const route = (path: string) => {
case 'npub' :
case 'npub' :
subProfile ( data , onEvent ) ;
subProfile ( data , onEvent ) ;
view ( path , { type : 'profile' , id : data } ) ;
view ( path , { type : 'profile' , id : data } ) ;
updateFollowBtn ( data ) ;
break ;
break ;
default :
default :
console . warn ( ` type ${ type } not yet supported ` ) ;
console . warn ( ` type ${ type } not yet supported ` ) ;
}
}
renderFeed ( ) ;
renderFeed ( ) ;
} else if ( path . length === 73 && path . match ( /^\/contacts\/npub[0-9a-z]+$/ ) ) {
const contactNpub = path . slice ( 10 ) ;
const { type : contactType , data : contactPubkey } = nip19 . decode ( contactNpub ) ;
if ( contactType === 'npub' ) {
subContactList ( contactPubkey , onEvent ) ;
view ( path , { type : 'contacts' , id : contactPubkey } ) ;
}
} else if ( path . length === 65 ) {
} else if ( path . length === 65 ) {
const eventID = path . slice ( 1 ) ;
const eventID = path . slice ( 1 ) ;
subEventID ( eventID , onEventDetails ) ;
subEventID ( eventID , onEventDetails ) ;
view ( path , { type : 'event' , id : eventID } ) ;
view ( path , { type : 'event' , id : eventID } ) ;
} else {
} else {
console . warn ( 'no support for ' , path )
console . warn ( 'no support for ' , path ) ;
}
}
} ;
} ;
// onload
// onload
route ( location . pathname ) ;
route ( location . pathname ) ;
subOwnContacts ( onEvent ) ; // subscribe after route as routing unsubscribes current subs
// only push a new entry if there is no history onload
// only push a new entry if there is no history onload
if ( ! history . length ) {
if ( ! history . length ) {
@ -286,8 +342,11 @@ const handleLink = (a: HTMLAnchorElement, e: MouseEvent) => {
}
}
if (
if (
href === '/'
href === '/'
|| href . startsWith ( '/feed' )
|| href . startsWith ( '/note' )
|| href . startsWith ( '/note' )
|| href . startsWith ( '/npub' )
|| href . startsWith ( '/npub' )
|| href . startsWith ( '/contacts/npub' )
|| ( href . startsWith ( '/' ) && href . length === 65 )
) {
) {
route ( href ) ;
route ( href ) ;
history . pushState ( { } , '' , href ) ;
history . pushState ( { } , '' , href ) ;
@ -306,17 +365,26 @@ const handleButton = (button: HTMLButtonElement) => {
case 'back' :
case 'back' :
closePublishView ( ) ;
closePublishView ( ) ;
return ;
return ;
case 'import' :
resetContactList ( config . pubkey ) ;
rerenderFeed ( ) ;
subOwnContacts ( onEvent ) ;
subGlobalFeed ( onEvent ) ;
return ;
}
}
const id = ( button . closest ( '[data-id]' ) as HTMLElement ) ? . dataset . id ;
const id = button . dataset . id || ( button . closest ( '[data-id]' ) as HTMLElement ) ? . dataset . id ;
if ( id ) {
if ( id ) {
switch ( button . name ) {
switch ( button . name ) {
case 'reply' :
case 'reply' :
openWriteInput ( button , id ) ;
openWriteInput ( button , id ) ;
break ;
return ;
case 'star' :
case 'star' :
const note = replyList . find ( r = > r . id === id ) || textNoteList . find ( n = > n . id === ( id ) ) ;
const note = replyList . find ( r = > r . id === id ) || textNoteList . find ( n = > n . id === ( id ) ) ;
note && handleUpvote ( note ) ;
note && handleUpvote ( note ) ;
break ;
return ;
case 'follow' :
followContact ( id ) ;
return ;
}
}
}
}
// const container = e.target.closest('[data-append]');
// const container = e.target.closest('[data-append]');