Compare commits

...
14 Commits
Author SHA1 Message Date
Chris Troutner b878939456 Merge pull request #48 from Permissionless-Software-Foundation/ct-unstable
fix(feeds): Removing duplicate posts
2025-08-06 19:49:30 -07:00
Chris Troutner 59cfecb485 fix(feeds): Removing duplicate posts 2025-08-06 19:48:09 -07:00
Chris Troutner bd3798f5aa Merge pull request #47 from Permissionless-Software-Foundation/dh-feed-pfps
feat(nostr): Added profile photos on feeds
2025-08-06 19:37:25 -07:00
Daniel Gonzalez 5f0103ba81 Merge branch 'master' into dh-feed-pfps 2025-08-06 21:54:44 -04:00
Daniel Gonzalez 55ebb7b25d feat(nostr): Added profile photos on feeds 2025-08-06 21:53:11 -04:00
Chris Troutner 44246a78d5 Merge pull request #46 from Permissionless-Software-Foundation/ct-unstable
fix(content creators): Fixing bug. Adding TODO items
2025-08-06 12:23:09 -07:00
Chris Troutner b32bbdae0a fix(content creators): Fixing bug. Adding TODO items 2025-08-06 12:21:03 -07:00
Chris Troutner f9c31b61c7 Merge pull request #45 from Permissionless-Software-Foundation/ct-unstable
fix(Nostr): Reading from pool of relays whenever we can
2025-08-04 12:55:46 -07:00
Chris Troutner e327704b57 fix(Nostr): Reading from pool of relays whenever we can 2025-08-04 12:51:16 -07:00
Chris Troutner c84b8ed9c0 Merge pull request #44 from Permissionless-Software-Foundation/ct-unstable
Minor design improvements
2025-08-04 12:38:59 -07:00
Chris Troutner 59f49f9813 fix(Token Info): Turning 'View User Data' link into a button 2025-08-04 12:37:41 -07:00
Chris Troutner 44537e9cdd fix(Nostr Profile Post): Posting to multiple relays 2025-08-04 12:22:19 -07:00
Chris Troutner e547c97a44 fix(Nostr Public Post): Posting to multiple Nostr relays 2025-08-04 12:18:49 -07:00
Chris Troutner 880a07819c fix(Nostr Post): Opening up public post on page load 2025-08-04 11:53:44 -07:00
12 changed files with 160 additions and 56 deletions
@@ -98,17 +98,16 @@ function InfoButton (props) {
</Row> </Row>
{mutableDataCid && ( {mutableDataCid && (
<Row> <Row style={{ paddingTop: '10px' }}>
<Col xs={4}><b>User Data</b>:</Col> <Col xs={4}><b>User Data</b>:</Col>
<Col xs={8}> <Col xs={8}>
<a <Button
href={`/user-data/${props.token.tokenId}#single-view`} href={`/user-data/${props.token.tokenId}#single-view`}
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
className='btn btn-link p-0'
> >
View User Data View User Data
</a> </Button>
</Col> </Col>
</Row> </Row>
)} )}
@@ -1,9 +1,21 @@
/*
This component is used to display the list of content creators.
TODO:
- getFollowList() should aggregate all the followers from all the relays.
Currently each relay overwrites the last one.
- loadProfile() should use multiple relays. It should exit after the first
successful retrieval of data from a relay. If the relay fails to give data,
it should move on to the next relay.
*/
import React, { useState, useEffect, useCallback } from 'react' import React, { useState, useEffect, useCallback } from 'react'
import { Container, Spinner, Dropdown } from 'react-bootstrap' import { Container, Spinner, Dropdown } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFilter } from '@fortawesome/free-solid-svg-icons' import { faFilter } from '@fortawesome/free-solid-svg-icons'
import axios from 'axios' import axios from 'axios'
// Local libraries // Local libraries
import config from '../../../../config' import config from '../../../../config'
import ContentCard from './content-card' import ContentCard from './content-card'
@@ -23,9 +35,9 @@ function ContentCreators (props) {
const list = await new Promise((resolve, reject) => { const list = await new Promise((resolve, reject) => {
let list = [] let list = []
const { nostrKeyPair } = appData.bchWalletState const { nostrKeyPair } = appData.bchWalletState
const psf = 'wss://nostr-relay.psfoundation.info'
const pool = RelayPool([psf]) const pool = RelayPool(config.nostrRelays)
// const pool = RelayPool([config.nostrRelay])
pool.on('open', relay => { pool.on('open', relay => {
relay.subscribe('subid', { limit: 1, kinds: [3], authors: [nostrKeyPair.pubHex] }) relay.subscribe('subid', { limit: 1, kinds: [3], authors: [nostrKeyPair.pubHex] })
}) })
@@ -52,9 +64,8 @@ function ContentCreators (props) {
const loadProfile = useCallback(async (pubKey) => { const loadProfile = useCallback(async (pubKey) => {
return new Promise((resolve) => { return new Promise((resolve) => {
const psf = 'wss://nostr-relay.psfoundation.info' // const pool = RelayPool(config.nostrRelays)
const pool = RelayPool([config.nostrRelay])
const pool = RelayPool([psf])
pool.on('open', relay => { pool.on('open', relay => {
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubKey] }) relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubKey] })
}) })
@@ -25,6 +25,7 @@ function FeedCard (props) {
const [isLiked, setIsLiked] = useState(false) const [isLiked, setIsLiked] = useState(false)
const [likesCount, setLikesCount] = useState(null) const [likesCount, setLikesCount] = useState(null)
const [likesFetched, setLikesFetched] = useState(false) const [likesFetched, setLikesFetched] = useState(false)
const [profilePictureError, setProfilePictureError] = useState(false)
// function to fetch a post likes reaction // function to fetch a post likes reaction
const handleLikes = useCallback(async (post, userPubKey) => { const handleLikes = useCallback(async (post, userPubKey) => {
@@ -76,6 +77,15 @@ function FeedCard (props) {
console.warn(error) console.warn(error)
} }
}, []) }, [])
const handleProfilePictureError = () => {
setProfilePictureError(true)
}
const handleProfilePictureLoad = () => {
setProfilePictureError(false)
}
// Get npub from pubkey // Get npub from pubkey
useEffect(() => { useEffect(() => {
const npub = nip19.npubEncode(post.pubkey) const npub = nip19.npubEncode(post.pubkey)
@@ -170,7 +180,20 @@ function FeedCard (props) {
background: 'linear-gradient(45deg, #6c757d, #495057)' background: 'linear-gradient(45deg, #6c757d, #495057)'
}} }}
> >
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' /> {profile?.picture && !profilePictureError
? (
<img
src={profile.picture}
alt='Profile'
className='rounded-circle w-100 h-100'
style={{ objectFit: 'cover' }}
onError={handleProfilePictureError}
onLoad={handleProfilePictureLoad}
/>
)
: (
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' />
)}
</div> </div>
<div className='flex-grow-1'> <div className='flex-grow-1'>
<div className='fw-bold mb-1'> <div className='fw-bold mb-1'>
@@ -4,9 +4,10 @@
// Global npm libraries // Global npm libraries
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Container, Spinner } from 'react-bootstrap' import { Container, Spinner } from 'react-bootstrap'
import { RelayPool } from 'nostr' import { RelayPool } from 'nostr'
// Local libraries
import config from '../../../../config'
import FeedCard from './feed-card' import FeedCard from './feed-card'
function Following (props) { function Following (props) {
@@ -19,9 +20,8 @@ function Following (props) {
const followingList = await new Promise((resolve, reject) => { const followingList = await new Promise((resolve, reject) => {
let list = [] let list = []
const { nostrKeyPair } = appData.bchWalletState const { nostrKeyPair } = appData.bchWalletState
const psf = 'wss://nostr-relay.psfoundation.info'
const pool = RelayPool([psf]) const pool = RelayPool(config.nostrRelays)
pool.on('open', relay => { pool.on('open', relay => {
relay.subscribe('subid', { limit: 1, kinds: [3], authors: [nostrKeyPair.pubHex] }) relay.subscribe('subid', { limit: 1, kinds: [3], authors: [nostrKeyPair.pubHex] })
}) })
+29 -10
View File
@@ -1,13 +1,27 @@
/* /*
Component for reading the nostr feeds. Component for reading the nostr feeds.
TODO:
- fetchProfile() should retrieve a profile from multiple relays. If the first relay returns a profile,
then that profile can be used and promise resolved. If the first relay returns no profile, the next
one should be tried until all relays are exhausted or one returns a profile.
- useEffect() retrieves the feeds. This should cycle through each relay and posts from each one.
Once each relays has been tried, the posts should remove duplicate entries. Finally posts should
be sorted by date.
- Clicking on a profile picture, name, or npub should open the profile for that user in a new tab.
*/ */
// Global npm libraries // Global npm libraries
import React, { useState, useEffect, useCallback } from 'react' import React, { useState, useEffect, useCallback } from 'react'
import { Container, Nav, Tab, Spinner } from 'react-bootstrap' import { Container, Nav, Tab, Spinner } from 'react-bootstrap'
import { RelayPool } from 'nostr'
// Local libraries
import Feed from './feed' import Feed from './feed'
import Following from './following' import Following from './following'
import { RelayPool } from 'nostr' import config from '../../../../config'
function Feeds (props) { function Feeds (props) {
const { appData } = props const { appData } = props
@@ -19,18 +33,23 @@ function Feeds (props) {
const [profiles, setProfiles] = useState({}) const [profiles, setProfiles] = useState({})
// function to fetch profile and set it to profiles state // function to fetch profile and set it to profiles state
const fetchProfile = useCallback((pubkey) => { const fetchProfile = useCallback(async (pubkey) => {
// no fetch profile again if it exist // no fetch profile again if it exist
let hasProfileRequest = false
setProfiles(currentProfiles => { setProfiles(currentProfiles => {
if (currentProfiles[pubkey]) { if (currentProfiles[pubkey]) {
hasProfileRequest = true
return currentProfiles return currentProfiles
} else {
const newProfiles = { ...currentProfiles }
newProfiles[pubkey] = { loaded: false }
return newProfiles
} }
return currentProfiles
}) })
if (hasProfileRequest) return
const psf = 'wss://nostr-relay.psfoundation.info' // const pool = RelayPool(config.nostrRelays)
const pool = RelayPool([config.nostrRelay])
const pool = RelayPool([psf])
pool.on('open', relay => { pool.on('open', relay => {
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubkey] }) relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubkey] })
}) })
@@ -48,6 +67,7 @@ function Feeds (props) {
return currentProfiles return currentProfiles
}) })
} catch (error) { } catch (error) {
console.warn(error)
// skip error // skip error
} }
}) })
@@ -70,9 +90,8 @@ function Feeds (props) {
// Get global feed posts // Get global feed posts
useEffect(() => { useEffect(() => {
const start = () => { const start = () => {
const psf = 'wss://nostr-relay.psfoundation.info' // const pool = RelayPool(config.nostrRelays)
const pool = RelayPool([config.nostrRelay])
const pool = RelayPool([psf])
pool.on('open', relay => { pool.on('open', relay => {
relay.subscribe('REQ', { limit: 10, kinds: [1], '#t': ['slpdex-socialmedia'] }) relay.subscribe('REQ', { limit: 10, kinds: [1], '#t': ['slpdex-socialmedia'] })
}) })
@@ -26,7 +26,7 @@ const NostrFormat = ({ content }) => {
} }
return ( return (
<div dangerouslySetInnerHTML={{ __html: formatContent(content) }} /> <div style={{ overflow: 'auto' }} dangerouslySetInnerHTML={{ __html: formatContent(content) }} />
) )
} }
@@ -1,10 +1,12 @@
/* /*
Component for posting nostr information. Component for posting nostr information.
*/ */
// Global npm libraries // Global npm libraries
import React from 'react' import React from 'react'
import { Container } from 'react-bootstrap' import { Container } from 'react-bootstrap'
// Local libraries
import ProfilePost from './profile-post' import ProfilePost from './profile-post'
import PublicPost from './public-post' import PublicPost from './public-post'
@@ -11,6 +11,9 @@ import { Relay } from 'nostr-tools/relay'
import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency
import { RelayPool } from 'nostr' import { RelayPool } from 'nostr'
// Local libraries
import config from '../../../../config'
function ProfilePost (props) { function ProfilePost (props) {
const { bchWalletState } = props.appData const { bchWalletState } = props.appData
const [accordionKey, setAccordionKey] = useState(null) const [accordionKey, setAccordionKey] = useState(null)
@@ -90,7 +93,7 @@ function ProfilePost (props) {
const privateKeyBin = hexToBytes(nostrKeyPair.privHex) const privateKeyBin = hexToBytes(nostrKeyPair.privHex)
// Relay list // Relay list
const psf = 'wss://nostr-relay.psfoundation.info' // const psf = 'wss://nostr-relay.psfoundation.info'
const formDataString = JSON.stringify(formData) const formDataString = JSON.stringify(formData)
@@ -107,16 +110,24 @@ function ProfilePost (props) {
const signedEvent = finalizeEvent(eventTemplate, privateKeyBin) const signedEvent = finalizeEvent(eventTemplate, privateKeyBin)
console.log('signedEvent: ', signedEvent) console.log('signedEvent: ', signedEvent)
// Connect to a relay. // Publish the post to each relay.
const relay = await Relay.connect(psf) config.nostrRelays.map(async (relayUrl) => {
console.log(`connected to ${relay.url}`) try {
// Connect to a relay.
const relay = await Relay.connect(relayUrl)
console.log(`connected to ${relay.url}`)
// Publish the message to the relay. // Publish the message to the relay.
const result = await relay.publish(signedEvent) const result = await relay.publish(signedEvent)
console.log('result: ', result) console.log('result: ', result)
// Close the connection to the relay.
relay.close()
} catch (err) {
console.warn(`Skipping publishing to ${relayUrl} due to error: ${err}`)
}
})
// Close the connection to the relay.
relay.close()
setSuccessMsg('Post successfully published!') setSuccessMsg('Post successfully published!')
setOnFetch(false) setOnFetch(false)
} catch (error) { } catch (error) {
@@ -1,5 +1,5 @@
/* /*
Component for posting nostr information on kind 1. Component for posting nostr information on kind 1.
*/ */
// Global npm libraries // Global npm libraries
@@ -10,8 +10,11 @@ import { finalizeEvent } from 'nostr-tools/pure'
import { Relay } from 'nostr-tools/relay' import { Relay } from 'nostr-tools/relay'
import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency
// Local libraries
import config from '../../../../config'
function PublicPost (props) { function PublicPost (props) {
const [accordionKey, setAccordionKey] = useState(null) const [accordionKey, setAccordionKey] = useState('0')
const [onFetch, setOnFetch] = useState(false) const [onFetch, setOnFetch] = useState(false)
const { bchWalletState } = props.appData const { bchWalletState } = props.appData
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -44,9 +47,6 @@ function PublicPost (props) {
// Convert private key to binary // Convert private key to binary
const privateKeyBin = hexToBytes(nostrKeyPair.privHex) const privateKeyBin = hexToBytes(nostrKeyPair.privHex)
// Relay list
const psf = 'wss://nostr-relay.psfoundation.info'
// BCH address of the user. // BCH address of the user.
const bchAddr = bchWalletState.address const bchAddr = bchWalletState.address
@@ -63,16 +63,24 @@ function PublicPost (props) {
const signedEvent = finalizeEvent(eventTemplate, privateKeyBin) const signedEvent = finalizeEvent(eventTemplate, privateKeyBin)
console.log('signedEvent: ', signedEvent) console.log('signedEvent: ', signedEvent)
// Connect to a relay. // Publish the post to each relay.
const relay = await Relay.connect(psf) config.nostrRelays.map(async (relayUrl) => {
console.log(`connected to ${relay.url}`) try {
// Connect to a relay.
const relay = await Relay.connect(relayUrl)
console.log(`connected to ${relay.url}`)
// Publish the message to the relay. // Publish the message to the relay.
const result = await relay.publish(signedEvent) const result = await relay.publish(signedEvent)
console.log('result: ', result) console.log('result: ', result)
// Close the connection to the relay.
relay.close()
} catch (err) {
console.warn(`Skipping publishing to ${relayUrl} due to error: ${err}`)
}
})
// Close the connection to the relay.
relay.close()
resetForm() resetForm()
setSuccessMsg('Post successfully published!') setSuccessMsg('Post successfully published!')
setOnFetch(false) setOnFetch(false)
@@ -7,9 +7,12 @@ import { Container, Button } from 'react-bootstrap'
import CopyOnClick from '../../bch-wallet/copy-on-click.js' import CopyOnClick from '../../bch-wallet/copy-on-click.js'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser, faGlobe } from '@fortawesome/free-solid-svg-icons' import { faUser, faGlobe } from '@fortawesome/free-solid-svg-icons'
import * as nip19 from 'nostr-tools/nip19'
// Local libraries
import config from '../../../../config'
import NostrFormat from '../nostr-format' import NostrFormat from '../nostr-format'
import { RelayPool } from 'nostr' import { RelayPool } from 'nostr'
import * as nip19 from 'nostr-tools/nip19'
function ProfileRead (props) { function ProfileRead (props) {
const { npub } = props const { npub } = props
@@ -22,9 +25,8 @@ function ProfileRead (props) {
const start = () => { const start = () => {
const pubHexData = nip19.decode(npub) const pubHexData = nip19.decode(npub)
const pubHex = pubHexData.data const pubHex = pubHexData.data
const psf = 'wss://nostr-relay.psfoundation.info'
const pool = RelayPool([psf]) const pool = RelayPool(config.nostrRelays)
pool.on('open', relay => { pool.on('open', relay => {
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubHex] }) relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubHex] })
setLoaded(true) setLoaded(true)
@@ -6,15 +6,19 @@ import React, { useEffect, useState } from 'react'
import { Container, Card, Spinner } from 'react-bootstrap' import { Container, Card, Spinner } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser } from '@fortawesome/free-solid-svg-icons' import { faUser } from '@fortawesome/free-solid-svg-icons'
import NostrFormat from '../nostr-format'
import { RelayPool } from 'nostr' import { RelayPool } from 'nostr'
import * as nip19 from 'nostr-tools/nip19' import * as nip19 from 'nostr-tools/nip19'
// Local libraries
import config from '../../../../config'
import NostrFormat from '../nostr-format'
function PublicRead (props) { function PublicRead (props) {
const { npub } = props const { npub } = props
const { bchWalletState } = props.appData const { bchWalletState } = props.appData
const [posts, setPosts] = useState([]) const [posts, setPosts] = useState([])
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false)
const [profilePictureError, setProfilePictureError] = useState(false)
useEffect(() => { useEffect(() => {
// Get Last post from a author // Get Last post from a author
@@ -22,9 +26,8 @@ function PublicRead (props) {
const pubHexData = nip19.decode(npub) const pubHexData = nip19.decode(npub)
const pubHex = pubHexData.data const pubHex = pubHexData.data
console.log('pubhex', pubHex) console.log('pubhex', pubHex)
const psf = 'wss://nostr-relay.psfoundation.info'
const pool = RelayPool([psf]) const pool = RelayPool(config.nostrRelays)
pool.on('open', relay => { pool.on('open', relay => {
relay.subscribe('subid', { limit: 5, kinds: [1], authors: [pubHex] }) relay.subscribe('subid', { limit: 5, kinds: [1], authors: [pubHex] })
setLoaded(true) setLoaded(true)
@@ -47,6 +50,14 @@ function PublicRead (props) {
} }
}, [bchWalletState, loaded, npub]) }, [bchWalletState, loaded, npub])
const handleProfilePictureError = () => {
setProfilePictureError(true)
}
const handleProfilePictureLoad = () => {
setProfilePictureError(false)
}
return ( return (
<Container className='mt-4 mb-5'> <Container className='mt-4 mb-5'>
{/* Nostr Feed Section */} {/* Nostr Feed Section */}
@@ -74,7 +85,20 @@ function PublicRead (props) {
background: 'linear-gradient(45deg, #6c757d, #495057)' background: 'linear-gradient(45deg, #6c757d, #495057)'
}} }}
> >
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' /> {props.profile?.picture && !profilePictureError
? (
<img
src={props.profile.picture}
alt='Profile'
className='rounded-circle w-100 h-100'
style={{ objectFit: 'cover' }}
onError={handleProfilePictureError}
onLoad={handleProfilePictureLoad}
/>
)
: (
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' />
)}
</div> </div>
<div className='flex-grow-1'> <div className='flex-grow-1'>
<div className='fw-bold mb-1'>{props.profile?.name}</div> <div className='fw-bold mb-1'>{props.profile?.name}</div>
+6 -1
View File
@@ -18,7 +18,12 @@ const config = {
// dexServer: 'http://localhost:5700', // dexServer: 'http://localhost:5700',
nostrTopic: 'bch-dex-test-topic-02', nostrTopic: 'bch-dex-test-topic-02',
nostrRelay: 'wss://nostr-relay.psfoundation.info' nostrRelay: 'wss://nostr-relay.psfoundation.info',
nostrRelays: [
'wss://nostr-relay.psfoundation.info',
'wss://nos.lol',
'wss://relay.damus.io'
]
} }