Compare commits

...
18 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
Chris Troutner a1f2c78059 Merge pull request #43 from Permissionless-Software-Foundation/dh-userdata-loading
feat(user-data): Added loading optimization to user-data page
2025-08-03 08:32:24 -07:00
Daniel Gonzalez 1d6ebbfd92 feat(user-data): Added loading optimization to user-data page 2025-08-02 20:22:16 -04:00
Chris Troutner 7e168c6cf1 Merge pull request #42 from Permissionless-Software-Foundation/dh-tokens-sale
feat(profile): Added tokens-for-sale section to profile
2025-08-01 10:54:41 -07:00
Daniel Gonzalez b031fa98de feat(profile): Added tokens-for-sale section to profile 2025-08-01 12:43:01 -04:00
18 changed files with 501 additions and 81 deletions
+7 -1
View File
@@ -14,7 +14,10 @@ import NavMenu from './components/nav-menu'
import useAppState from './hooks/state' import useAppState from './hooks/state'
import { UninitializedView, InitializedView } from './components/starter-views' import { UninitializedView, InitializedView } from './components/starter-views'
const sigleViewPaths = ['/profile'] // Paths with custom async loader
// Single Views, load minimal async services in order to prevent the long-time screen loader.
const sigleViewPaths = ['/profile', 'user-data']
function App (props) { function App (props) {
// Load all the app state into a single object that can be passed to child // Load all the app state into a single object that can be passed to child
// components. // components.
@@ -45,6 +48,9 @@ function App (props) {
await asyncLoad.loadWalletLib() await asyncLoad.loadWalletLib()
const walletTemp = await asyncLoad.initStarterWallet(appData.serverUrl, appData.lsState.mnemonic, appData) const walletTemp = await asyncLoad.initStarterWallet(appData.serverUrl, appData.lsState.mnemonic, appData)
appData.setWallet(walletTemp) appData.setWallet(walletTemp)
// Get the BCH spot price
addToModal('Getting BCH spot price in USD', appData)
await asyncLoad.getUSDExchangeRate(walletTemp, appData.updateBchWalletState, appData)
} }
// Update Modal State // Update Modal State
@@ -172,7 +172,7 @@ function NftsForSale (props) {
} catch (error) { } catch (error) {
setIconsAreLoaded(true) setIconsAreLoaded(true)
} }
}, [fetchTokenMutableData, appData]) }, [fetchTokenMutableData])
// Check if token data exists in the cache and add it to the tokens object. // Check if token data exists in the cache and add it to the tokens object.
const reviewNftCachedData = useCallback(async (offers) => { const reviewNftCachedData = useCallback(async (offers) => {
@@ -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}`} 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>
)} )}
@@ -12,7 +12,7 @@ import InfoButton from './info-button'
import BuyButton from './buy-button' import BuyButton from './buy-button'
function TokenCard (props) { function TokenCard (props) {
const { token, appData, handleRefresh } = props const { token, appData, handleRefresh, hideBuyBtn } = props
const [icon, setIcon] = useState(token.icon) const [icon, setIcon] = useState(token.icon)
const [tokenData, setTokenData] = useState(token.tokenData) const [tokenData, setTokenData] = useState(token.tokenData)
@@ -64,16 +64,16 @@ function TokenCard (props) {
</Row> </Row>
<br /> <br />
<Row> <Row className='text-center'>
<Col> <Col>
<InfoButton token={token} disabled={!token.tokenData} /> <InfoButton token={token} disabled={!token.tokenData} />
</Col> </Col>
<Col /> {!hideBuyBtn && (
<Col>
<Col> <BuyButton token={token} disabled={!token.tokenData} appData={appData} onSuccess={handleRefresh} />
<BuyButton token={token} disabled={!token.tokenData} appData={appData} onSuccess={handleRefresh} /> </Col>
</Col> )}
</Row> </Row>
</Container> </Container>
@@ -83,17 +83,5 @@ function TokenCard (props) {
</> </>
) )
} }
// eslint-disable-next-line
{/* <Col>
<InfoButton token={props.token} />
</Col>
<Col>
<FlagButton appData={props.appData} offer={props.token} />
</Col>
<Col>
<BuyNftButton appData={props.appData} offer={props.token} />
</Col> */ }
export default TokenCard export default TokenCard
@@ -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)
@@ -9,7 +9,7 @@ import ProfileRead from './profile-read.js'
import PublicRead from './public-read.js' import PublicRead from './public-read.js'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import SlpTokensDisplay from './slp-tokens-display' import SlpTokensDisplay from './slp-tokens-display'
import NFTForSale from './nft-for-sale.js'
function Profile (props) { function Profile (props) {
const [profile, setProfile] = useState(false) const [profile, setProfile] = useState(false)
const { npub } = useParams() const { npub } = useParams()
@@ -20,6 +20,7 @@ function Profile (props) {
<ProfileRead {...props} setProfile={setProfile} npub={npub} /> <ProfileRead {...props} setProfile={setProfile} npub={npub} />
<PublicRead {...props} profile={profile} npub={npub} /> <PublicRead {...props} profile={profile} npub={npub} />
<SlpTokensDisplay {...props} npub={npub} /> <SlpTokensDisplay {...props} npub={npub} />
<NFTForSale {...props} npub={npub} />
</Container> </Container>
</> </>
) )
@@ -0,0 +1,321 @@
/**
* Component for displaying SLP tokens in a grid layout
*/
import React, { useState, useEffect, useCallback } from 'react'
import { Container, Row, Col, Spinner } from 'react-bootstrap'
import config from '../../../../config'
import axios from 'axios'
import TokenCard from '../../nfts-for-sale/token-card'
function NFTForSale (props) {
const { appData, npub } = props
const [offers, setOffers] = useState([])
const [offersAreLoaded, setOffersAreLoaded] = useState(false)
const [iconsAreLoaded, setIconsAreLoaded] = useState(false)
const [dataAreLoaded, setDataAreLoaded] = useState(false)
const getAddressByNpub = useCallback(async () => {
const url = `${config.dexServer}/sm/npub/${npub}`
const response = await axios.get(url)
const addr = response.data.bchAddr
return addr
}, [npub])
// Handler for refresh button
const handleRefresh = () => {
loadNftOffers()
}
// Function to process token data
const processTokenData = useCallback(async (offer) => {
try {
const { wallet, bchWalletState } = appData
const { bchjs } = wallet
// Calculate USD price
const rateInSats = parseInt(offer.rateInBaseUnit)
const bchCost = bchjs.BitcoinCash.toBitcoinCash(rateInSats)
const usdPrice = bchCost * bchWalletState.bchUsdPrice * offer.numTokens
offer.usdPrice = `$${usdPrice.toFixed(3)}`
return offer
} catch (err) {
console.error('Error processing token:', err)
return offer
}
}, [appData])
// Fetch offers
const getNftOffers = useCallback(async (page = 0) => {
try {
setOffersAreLoaded(false)
const addr = await getAddressByNpub(npub)
const url = `${config.dexServer}/offer/list/addr/${addr}`
const result = await axios.get(url)
const rawOffers = result.data
console.log('rawOffers: ', rawOffers)
// Process each offer
const processedOffers = []
for (let i = 0; i < rawOffers.length; i++) {
const offer = rawOffers[i]
const processedOffer = await processTokenData(offer)
processedOffers.push(processedOffer)
}
setOffersAreLoaded(true)
return processedOffers
} catch (err) {
console.error('getNftOffers error:', err)
setOffersAreLoaded(true)
throw err
}
}, [processTokenData, getAddressByNpub, npub])
// This function loads the token data .
const lazyLoadTokenData = useCallback(async (tokens) => {
try {
setDataAreLoaded(false)
// map each token and fetch the token data
for (let i = 0; i < tokens.length; i++) {
const thisToken = tokens[i]
// data does not need to be downloaded, so continue with the next one
if (thisToken.dataAlreadyDownloaded) continue
// Try to get token data.
const tokenData = await appData.wallet.getTokenData(thisToken.tokenId)
console.log('tokenData', tokenData)
if (tokenData) {
// Set data to the token object , this can be used to display the token name in the token card component.
thisToken.tokenData = tokenData
}
// Mark token to prevent fetch token data again.
thisToken.dataAlreadyDownloaded = true
}
setDataAreLoaded(true)
} catch (error) {
setDataAreLoaded(true)
}
}, [appData])
// Fetch mutable data if it exist and get the token icon url
const fetchTokenMutableData = useCallback(async (token) => {
try {
// Get the token data
const tokenData = token.tokenData
if (!tokenData.mutableData) return false // Return false if no mutable data
// Get the token icon from the mutable data
const cid = parseCid(tokenData.mutableData)
console.log('mutable data cid', cid)
const { json } = await appData.wallet.cid2json({ cid })
console.log('json: ', json)
if (!json) return false
let iconUrl = json.tokenIcon
if (json.fullSizedUrl && json.fullSizedUrl.includes('http')) {
iconUrl = json.fullSizedUrl
}
const userData = json.userData
// Return icon url
return { iconUrl, userData }
} catch (error) {
return false
}
}, [appData])
// This function loads the token icons from the ipfs gateways.
const lazyLoadMutableData = useCallback(async (tokens) => {
try {
setIconsAreLoaded(false)
// map each token and fetch the icon url
for (let i = 0; i < tokens.length; i++) {
const thisToken = tokens[i]
// Incon does not need to be downloaded, so continue with the next one
if (thisToken.iconAlreadyDownloaded) continue
// Try to get token icon url from mutable data.
const { iconUrl, userData } = await fetchTokenMutableData(thisToken)
console.log('iconUrl', iconUrl)
if (iconUrl) {
// Set the icon url to the token , this can be used to display the icon in the token card component.
thisToken.icon = iconUrl
thisToken.tokenData.userData = userData
}
// Mark token to prevent fetch token icon again.
thisToken.iconAlreadyDownloaded = true
}
setIconsAreLoaded(true)
} catch (error) {
setIconsAreLoaded(true)
}
}, [fetchTokenMutableData])
// Check if token data exists in the cache and add it to the tokens object.
const reviewNftCachedData = useCallback(async (offers) => {
const cacheData = appData.nftForSaleCacheData
console.log(`Token data from cache: ${JSON.stringify(cacheData, null, 2)}`)
// Map all offers
for (let i = 0; i < offers.length; i++) {
const thisToken = offers[i]
const cacheToken = cacheData[thisToken.tokenId]
// If cache data exists, add it to the token object
if (cacheToken) {
thisToken.tokenData = cacheToken.tokenData
thisToken.icon = cacheToken.tokenIcon
}
}
}, [appData])
// Check if token data exists in the cache and add it to the tokens object.
const updateNFTCachedData = useCallback(async (offers) => {
// Map all offers
for (let i = 0; i < offers.length; i++) {
const thisToken = offers[i]
// save token icon and token data in cache
const newTokenCacheData = {
tokenIcon: thisToken.icon,
tokenData: thisToken.tokenData
}
console.log('newTokenCacheData: ', newTokenCacheData)
appData.updateNFTCachedData(thisToken.tokenId, newTokenCacheData)
}
}, [appData])
// Main function to load NFT offers
const loadNftOffers = useCallback(async () => {
try {
// Get tokens in offers
const offers = await getNftOffers()
console.log('offers: ', offers)
// Review cached data to populate token data from cached data
await reviewNftCachedData(offers)
// set state to start displaying tokens cards.
setOffers(offers)
// Load tokens data for any updates to the token data
await lazyLoadTokenData(offers)
// Load tokens icons from mutable data
await lazyLoadMutableData(offers)
// Update cached data with the latest retrieved data
await updateNFTCachedData(offers)
} catch (err) {
console.error('Error loading NFT offers:', err)
}
}, [lazyLoadTokenData, lazyLoadMutableData, getNftOffers, reviewNftCachedData, updateNFTCachedData])
// Effect to load NFTs on component mount
useEffect(() => {
console.log('loading nfts for sale')
loadNftOffers()
}, [loadNftOffers])
// Get Cid from url
const parseCid = (url) => {
// get the cid from the url format 'ipfs://bafybeicem27xbzs65uvbcgykcmscsgln3lmhbfrcoec3gdttkdgtxv5acq
if (url && url.includes('ipfs://')) {
const cid = url.split('ipfs://')[1]
return cid
}
return url
}
// This function generates a Token Card for each token in the wallet.
function generateCards (offers) {
console.log('generateCards() offerData: ', offers)
const tokens = offers
const tokenCards = []
for (let i = 0; i < tokens.length; i++) {
const thisToken = tokens[i]
const thisTokenCard = (
<TokenCard
appData={appData}
token={thisToken}
handleRefresh={handleRefresh}
key={`${thisToken.tokenId + i}`}
hideBuyBtn
hideSendBtn
/>
)
tokenCards.push(thisTokenCard)
}
return tokenCards
}
return (
<Container>
{!offersAreLoaded && (
<div className='d-flex justify-content-center align-items-center h-100 mb-4'>
<Spinner />
</div>
)}
{offersAreLoaded && offers.length !== 0 && (
<div className='bg-white rounded-1 shadow-sm border mb-4'>
<div className='p-3 border-bottom bg-light rounded-top-4'>
<h5 className='mb-0 fw-bold text-dark'>Tokens For Sale</h5>
</div>
<div
className='p-3'
style={{
height: '800px',
overflowY: 'auto',
maxHeight: '800px'
}}
>
<Row>
<Col xs={12} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{/** Show spinner info if tokens are loaded but data is not loaded */
offersAreLoaded && !dataAreLoaded && (
<div style={{ borderRadius: '10px', backgroundColor: '#f0f0f0', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center', width: 'fit-content' }}>
<span style={{ marginRight: '10px' }}>Loading Token Data </span>
<Spinner animation='border' />
</div>
)
}
{/** Show spinner info if tokens are loaded but icons are not loaded */
dataAreLoaded && !iconsAreLoaded && (
<div style={{ borderRadius: '10px', backgroundColor: '#f0f0f0', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center', width: 'fit-content' }}>
<span style={{ marginRight: '10px' }}>Loading Token Icons </span>
<Spinner animation='border' />
</div>
)
}
</Col>
</Row>
{!offersAreLoaded && (
<Row className='d-block text-center'>
<Spinner animation='border' variant='primary' style={{ maegin: '0 auto' }} />
</Row>
)}
<Row>
{offersAreLoaded && generateCards(offers)}
</Row>
</div>
</div>
)}
</Container>
)
}
export default NFTForSale
@@ -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>
@@ -13,11 +13,11 @@ import { Button, Modal, Container, Row, Col } from 'react-bootstrap'
// returned with a link. Otherwise the original string is returned. // returned with a link. Otherwise the original string is returned.
function linkIfUrl (url) { function linkIfUrl (url) {
// Convert the URL into a link if it contains 'http' // Convert the URL into a link if it contains 'http'
if (url.includes('http')) { if (url?.includes('http')) {
url = (<a href={url} target='_blank' rel='noreferrer'>{url}</a>) url = (<a href={url} target='_blank' rel='noreferrer'>{url}</a>)
// //
} else if (url.includes('ipfs://')) { } else if (url?.includes('ipfs://')) {
// Convert to a Filecoin link if its an IPFS reference. // Convert to a Filecoin link if its an IPFS reference.
const cid = url.substring(7) const cid = url.substring(7)
+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'
]
} }