@ -1,29 +1,99 @@
import { Event , nip19 } from 'nostr-tools' ;
import { Event , nip19 , signEvent } from 'nostr-tools' ;
import { elem } from './utils/dom' ;
import { dateTime } from './utils/time' ;
import { isPTag , sortByCreatedAt } from './events' ;
import { getViewContent } from './view' ;
import { isNotNonceTag , isPTag } from './events' ;
import { getViewContent , getViewElem , getViewOptions , setViewElem } from './view' ;
import { powEvent } from './system' ;
import { config } from './settings' ;
import { getMetadata } from './profiles' ;
import { publish } from './relays' ;
import { parseJSON } from './media' ;
const contactHistoryMap : {
[ pubkey : string ] : Event [ ]
[ pubkey : string ] : Event [ ] ;
} = { } ;
const hasOwnContactList = ( ) = > {
return ! ! contactHistoryMap [ config . pubkey ] ;
} ;
/ * *
* returns true if user is following pubkey
* /
export const isFollowing = ( pubkey : string ) = > {
const following = contactHistoryMap [ config . pubkey ] ? . at ( 0 ) ;
if ( ! following ) {
return false ;
}
return following . tags . some ( ( [ tag , value ] ) = > tag === 'p' && value === pubkey ) ;
} ;
export const updateFollowBtn = ( pubkey : string ) = > {
const followBtn = getViewElem ( ` followBtn- ${ pubkey } ` ) ;
const view = getViewOptions ( ) ;
if ( followBtn && ( view . type === 'contacts' || view . type === 'profile' ) ) {
const hasContact = isFollowing ( pubkey ) ;
const isMe = config . pubkey === pubkey ;
followBtn . textContent = isMe ? 'following' : hasContact ? 'unfollow' : 'follow' ;
followBtn . classList . remove ( 'primary' , 'secondary' ) ;
followBtn . classList . add ( hasContact ? 'secondary' : 'primary' ) ;
followBtn . hidden = false ;
}
} ;
const updateFollowing = ( evt : Event ) = > {
const following = getViewContent ( ) . querySelector ( ` [data-following=" ${ evt . pubkey } "] ` ) ;
const view = getViewOptions ( ) ;
if ( evt . pubkey === config . pubkey ) {
localStorage . setItem ( 'follwing' , JSON . stringify ( evt ) ) ;
}
switch ( view . type ) {
case 'contacts' :
if ( hasOwnContactList ( ) ) {
const lastContactList = contactHistoryMap [ config . pubkey ] ? . at ( 1 ) ;
if ( lastContactList ) {
const [ added , removed ] = findChanges ( evt , lastContactList ) ;
[
. . . added . map ( ( [ , pubkey ] ) = > pubkey ) ,
. . . removed . map ( ( [ , pubkey ] ) = > pubkey ) ,
] . forEach ( updateFollowBtn ) ;
} else {
evt . tags
. filter ( isPTag )
. forEach ( ( [ , pubkey ] ) = > updateFollowBtn ( pubkey ) ) ;
}
}
break ;
case 'profile' :
updateFollowBtn ( view . id ) ;
if ( view . id === evt . pubkey ) {
// update following link
const following = getViewElem ( 'following' ) as HTMLElement ;
if ( following ) {
const count = evt . tags . filter ( isPTag ) . length ;
const anchor = elem ( 'a' , {
data : { following : evt.pubkey } ,
href : ` / ${ evt . id } ` ,
title : dateTime.format ( new Date ( evt . created_at * 1000 ) ) ,
} , ` following ${ count } ` ) ;
href : ` /contacts/ ${ nip19 . npubEncode ( evt . pubkey ) } ` ,
title : dateTime.format ( evt . created_at * 1000 ) ,
} , [
'following ' ,
elem ( 'span' , { className : 'highlight' } , count ) ,
] ) ;
following . replaceWith ( anchor ) ;
setViewElem ( 'following' , anchor ) ;
}
}
break ;
}
} ;
export const refreshFollowing = ( id : string ) = > {
if ( contactHistoryMap [ id ] ? . at ( 0 ) ) {
updateFollowing ( contactHistoryMap [ id ] [ 0 ] ) ;
}
} ;
export const setContactList = ( evt : Event ) = > {
let contactHistory = contactHistoryMap [ evt . pubkey ] ;
cons t contactHistory = contactHistoryMap [ evt . pubkey ] ;
if ( ! contactHistory ) {
contactHistoryMap [ evt . pubkey ] = [ evt ] ;
updateFollowing ( evt ) ;
@ -32,9 +102,8 @@ export const setContactList = (evt: Event) => {
if ( contactHistory . find ( ( { id } ) = > id === evt . id ) ) {
return ;
}
contactHistory . push ( evt ) ;
contactHistory . sort ( sortByCreatedAt ) ;
updateFollowing ( contactHistory [ 0 ] ) ;
contactHistory . unshift ( evt ) ;
updateFollowing ( contactHistory [ 0 ] ) ; // TODO: ensure that this is newest contactlist?
} ;
/ * *
@ -49,13 +118,53 @@ const findChanges = (current: Event, previous: Event) => {
return [ addedContacts , removedContacts ] ;
} ;
export const resetContactList = ( pubkey : string ) = > {
delete contactHistoryMap [ pubkey ] ;
} ;
export const getContactUpdateMessage = (
addedList : string [ ] [ ] ,
removedList : string [ ] [ ] ,
) = > {
const content = [ ] ;
if ( addedList . length && addedList [ 0 ] ) {
const pubkey = addedList [ 0 ] [ 1 ] ;
const { userName } = getMetadata ( pubkey ) ;
const npub = nip19 . npubEncode ( pubkey ) ;
content . push (
'follows ' ,
elem ( 'a' , { href : ` / ${ npub } ` , data : { profile : pubkey } } , userName ) ,
) ;
}
if ( addedList . length > 1 ) {
content . push ( ` (+ ${ addedList . length - 1 } others) ` ) ;
}
if ( removedList ? . length > 0 ) {
if ( content . length ) {
content . push ( ' and' ) ;
}
content . push ( ' unfollowed ' ) ;
if ( removedList . length > 1 ) {
content . push ( ` ${ removedList . length } ` ) ;
} else {
const removedPubkey = removedList [ 0 ] [ 1 ] ;
const { userName : removeduserName } = getMetadata ( removedPubkey ) ;
const removedNpub = nip19 . npubEncode ( removedPubkey ) ;
content . push ( elem ( 'a' , { href : ` / ${ removedNpub } ` , data : { profile : removedPubkey } } , removeduserName ) ) ;
}
}
return content ;
} ;
export const updateContactList = ( evt : Event ) = > {
const contactHistory = contactHistoryMap [ evt . pubkey ] ;
if ( contactHistory . length === 1 ) {
return [ contactHistory [ 0 ] . tags . filter ( isPTag ) ] ;
}
const pos = contactHistory . findIndex ( ( { id } ) = > id === evt . id ) ;
if ( evt . id === contactHistory . at ( - 1 ) ? . id ) { // oldest known contact-list update
if ( evt . id !== contactHistory . at ( - 1 ) ? . id ) { // not oldest known contact-list update
return findChanges ( evt , contactHistory [ pos + 1 ] ) ;
}
// update existing contact entries
contactHistory
. slice ( 0 , - 1 )
@ -67,30 +176,109 @@ export const updateContactList = (evt: Event) => {
contactNote ? . replaceChildren ( . . . updated ) ;
} ) ;
return [ evt . tags . filter ( isPTag ) ] ;
} ;
/ * *
* returns list of pubkeys the given pubkey is following
* @param pubkey
* @returns { String [ ] } pubkeys
* /
export const getContacts = ( pubkey : string ) = > {
const following = contactHistoryMap [ pubkey ] ? . at ( 0 ) ;
if ( ! following ) {
return [ ] ;
}
return findChanges ( evt , contactHistory [ pos + 1 ] ) ;
return following . tags
. filter ( isPTag )
. map ( ( [ , pubkey ] ) = > pubkey ) ;
} ;
export const getContactUpdateMessage = (
addedList : string [ ] [ ] ,
removedList : string [ ] [ ] ,
/ * *
* returns list of pubkeys the user is following , if none found it will try from localstorage
* @returns { String [ ] } pubkeys
* /
export const getOwnContacts = ( ) = > {
const following = getContacts ( config . pubkey ) ;
if ( following . length ) {
return following ;
}
const followingFromStorage = localStorage . getItem ( 'follwing' ) ;
if ( followingFromStorage ) {
const follwingData = parseJSON ( followingFromStorage ) as Event ;
// TODO: ensure signature matches
if ( follwingData && follwingData . pubkey === config . pubkey ) {
return follwingData . tags
. filter ( isPTag )
. map ( ( [ , pubkey ] ) = > pubkey ) ;
}
}
return [ ] ;
} ;
const updateContactTags = (
followeeID : string ,
currentContactList : Event | undefined ,
) = > {
const content = [ ] ;
// console.log(addedContacts)
if ( addedList . length && addedList [ 0 ] ) {
const pubkey = addedList [ 0 ] [ 1 ] ;
const { userName } = getMetadata ( pubkey ) ;
const npub = nip19 . npubEncode ( pubkey ) ;
content . push (
'follows ' ,
elem ( 'a' , { href : ` / ${ npub } ` , data : { profile : pubkey } } , userName ) ,
) ;
if ( ! currentContactList ? . tags ) {
return [ [ 'p' , followeeID ] , [ 'p' , config . pubkey ] ] ;
}
if ( addedList . length > 1 ) {
content . push ( ` (+ ${ addedList . length - 1 } others) ` ) ;
if ( currentContactList . tags . some ( ( [ tag , id ] ) = > tag === 'p' && id === followeeID ) ) {
return currentContactList . tags
. filter ( ( [ tag , id ] ) = > tag === 'p' && id !== followeeID ) ;
}
if ( removedList ? . length > 0 ) {
content . push ( elem ( 'small' , { } , ` and unfollowed ${ removedList . length } ` ) ) ;
return [
[ 'p' , followeeID ] ,
. . . currentContactList . tags
. filter ( isNotNonceTag ) ,
] ;
} ;
export const followContact = async ( pubkey : string ) = > {
const followBtn = getViewElem ( ` followBtn- ${ pubkey } ` ) as HTMLButtonElement ;
const statusElem = getViewElem ( ` followStatus- ${ pubkey } ` ) as HTMLElement ;
if ( ! followBtn || ! statusElem ) {
return ;
}
const following = contactHistoryMap [ config . pubkey ] ? . at ( 0 ) ;
const unsignedEvent = {
kind : 3 ,
pubkey : config.pubkey ,
content : '' ,
tags : updateContactTags ( pubkey , following ) ,
created_at : Math.floor ( Date . now ( ) * 0.001 ) ,
} ;
followBtn . disabled = true ;
const newContactListEvent = await powEvent ( unsignedEvent , {
difficulty : config.difficulty ,
statusElem ,
timeout : config.timeout ,
} ) . catch ( console . warn ) ;
if ( ! newContactListEvent ) {
statusElem . textContent = '' ;
statusElem . hidden = false ;
followBtn . disabled = false ;
return ;
}
const privatekey = localStorage . getItem ( 'private_key' ) ;
if ( ! privatekey ) {
statusElem . textContent = 'no private key to sign' ;
statusElem . hidden = false ;
followBtn . disabled = false ;
return ;
}
const sig = signEvent ( newContactListEvent , privatekey ) ;
// TODO: validateEvent?
if ( sig ) {
statusElem . textContent = 'publishing…' ;
publish ( { . . . newContactListEvent , sig } , ( relay , error ) = > {
if ( error ) {
return console . error ( error , relay ) ;
}
statusElem . hidden = true ;
followBtn . disabled = false ;
console . info ( ` event published by ${ relay } ` ) ;
} ) ;
}
return content ;
} ;