mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex-taker-v2.git
synced 2026-09-22 09:12:00 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a93241f880 | ||
|
|
76dcd45d16 | ||
|
|
acb39ceff8 | ||
|
|
63b7bad1d1 | ||
|
|
c140255a26 | ||
|
|
947cb43535 | ||
|
|
93980ed703 | ||
|
|
36c81e7ce9 | ||
|
|
6ad3d40591 | ||
|
|
a4027b2fcb | ||
|
|
192497b5cf | ||
|
|
fda2de5362 | ||
|
|
131720ed58 | ||
|
|
64b1aec4cf | ||
|
|
70ce941eba |
Generated
+1177
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
||||
"react": "19.0.0",
|
||||
"react-bootstrap": "2.10.7",
|
||||
"react-dom": "19.0.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router-dom": "7.1.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"stream-browserify": "3.0.0",
|
||||
|
||||
@@ -165,28 +165,28 @@ function WalletSummary (props) {
|
||||
</Row>
|
||||
<Row style={{ padding: '25px', backgroundColor: '#eee' }}>
|
||||
<Col xs={10} sm={10} lg={8} style={{ padding: '10px' }}>
|
||||
<b>Nostr Priv Key:</b> <span className={blurredNostrPrivKey ? 'blurred' : null}>{nostrKeyPair.privHex}</span>
|
||||
<b>Nostr Priv Key:</b> <span className={blurredNostrPrivKey ? 'blurred' : null}>{nostrKeyPair.nsec}</span>
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<FontAwesomeIcon
|
||||
style={{ cursor: 'pointer' }}
|
||||
icon={eyeIcon.privateKey}
|
||||
size='lg'
|
||||
onClick={() => toggleNostrPrivateKeyBlur(nostrKeyPair.privHex)}
|
||||
onClick={() => toggleNostrPrivateKeyBlur(nostrKeyPair.nsec)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<CopyOnClick appData={appData} value={nostrKeyPair.privHex} />
|
||||
<CopyOnClick appData={appData} value={nostrKeyPair.nsec} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{ padding: '25px', backgroundColor: '#eee' }}>
|
||||
<Col xs={10} sm={10} lg={8} style={{ padding: '10px' }}>
|
||||
<b>Nostr Pub Key:</b> {nostrKeyPair.pubHex}
|
||||
<b>Nostr Pub Key:</b> {nostrKeyPair.npub}
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }} />
|
||||
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<CopyOnClick appData={appData} value={nostrKeyPair.pubHex} />
|
||||
<CopyOnClick appData={appData} value={nostrKeyPair.npub} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -23,9 +23,10 @@ import SlpTokens from './slp-tokens'
|
||||
import SweepWif from './sweep/index.js'
|
||||
import SignMessage from './sign/index.js'
|
||||
import ServerSelectView from './configuration/select-server-view'
|
||||
import NostrPost from './nostr/nostr-post.js'
|
||||
import NostrRead from './nostr/nostr-read.js'
|
||||
|
||||
import NostrPost from './nostr/nostr-post/index.js'
|
||||
import NostrRead from './nostr/nostr-read/index.js'
|
||||
import GlobalFeed from './nostr/global-feed/index.js'
|
||||
import UserDataReview from './user-data-review'
|
||||
function AppBody (props) {
|
||||
// Dependency injection through props
|
||||
const appData = props.appData
|
||||
@@ -47,7 +48,8 @@ function AppBody (props) {
|
||||
<Route path='/configuration' element={<ServerSelectView appData={appData} />} />
|
||||
<Route path='/nostr-post' element={<NostrPost appData={appData} />} />
|
||||
<Route path='/nostr-read' element={<NostrRead appData={appData} />} />
|
||||
|
||||
<Route path='/global-feed' element={<GlobalFeed appData={appData} />} />
|
||||
<Route path='/user-data/:cid' element={<UserDataReview appData={appData} />} />
|
||||
</Routes>
|
||||
{/** Show in all paths except the servers view */}
|
||||
{/* {appData.currentPath !== '/servers' && <SelectServerButton linkTo='/servers' appData={appData} />} */}
|
||||
|
||||
@@ -98,7 +98,7 @@ function NftsForSale (props) {
|
||||
|
||||
// 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
|
||||
@@ -115,7 +115,7 @@ function NftsForSale (props) {
|
||||
}, [appData])
|
||||
|
||||
// Fetch mutable data if it exist and get the token icon url
|
||||
const fetchTokenInfo = useCallback(async (token) => {
|
||||
const fetchTokenMutableData = useCallback(async (token) => {
|
||||
try {
|
||||
// Get the token data
|
||||
const tokenData = token.tokenData
|
||||
@@ -135,15 +135,16 @@ function NftsForSale (props) {
|
||||
if (json.fullSizedUrl && json.fullSizedUrl.includes('http')) {
|
||||
iconUrl = json.fullSizedUrl
|
||||
}
|
||||
const userData = json.userData
|
||||
// Return icon url
|
||||
return iconUrl
|
||||
return { iconUrl, userData }
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}, [appData])
|
||||
|
||||
// This function loads the token icons from the ipfs gateways.
|
||||
const lazyLoadTokenIcons = useCallback(async (tokens) => {
|
||||
const lazyLoadMutableData = useCallback(async (tokens) => {
|
||||
try {
|
||||
setIconsAreLoaded(false)
|
||||
// map each token and fetch the icon url
|
||||
@@ -154,11 +155,12 @@ function NftsForSale (props) {
|
||||
if (thisToken.iconAlreadyDownloaded) continue
|
||||
|
||||
// Try to get token icon url from mutable data.
|
||||
const iconUrl = await fetchTokenInfo(thisToken)
|
||||
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.
|
||||
@@ -169,7 +171,39 @@ function NftsForSale (props) {
|
||||
} catch (error) {
|
||||
setIconsAreLoaded(true)
|
||||
}
|
||||
}, [fetchTokenInfo])
|
||||
}, [fetchTokenMutableData, appData])
|
||||
|
||||
// Check if exiist token data 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 exiist token data 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 () => {
|
||||
@@ -178,17 +212,23 @@ function NftsForSale (props) {
|
||||
// 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
|
||||
// Load tokens data for any updates to the token data
|
||||
await lazyLoadTokenData(offers)
|
||||
// Load tokens icons
|
||||
await lazyLoadTokenIcons(offers)
|
||||
// Load tokens icons from mutable data
|
||||
await lazyLoadMutableData(offers)
|
||||
|
||||
// Update cached data with the latest retrieved data
|
||||
await updateNFTCachedData(offers)
|
||||
setIsLoading(false)
|
||||
} catch (err) {
|
||||
setIsLoading(false)
|
||||
console.error('Error loading NFT offers:', err)
|
||||
}
|
||||
}, [lazyLoadTokenData, lazyLoadTokenIcons, getNftOffers])
|
||||
}, [lazyLoadTokenData, lazyLoadMutableData, getNftOffers, reviewNftCachedData, updateNFTCachedData])
|
||||
|
||||
// Effect to load NFTs on component mount
|
||||
useEffect(() => {
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Button, Modal, Container, Row, Col } from 'react-bootstrap'
|
||||
|
||||
function InfoButton (props) {
|
||||
const [show, setShow] = useState(false)
|
||||
const [mutableDataCid, setMutableDataCid] = useState(null)
|
||||
|
||||
const handleClose = () => {
|
||||
console.log('handleClose()')
|
||||
@@ -22,6 +23,32 @@ function InfoButton (props) {
|
||||
console.log('handleOpen()')
|
||||
setShow(true)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// Get token user data if it exists and verify if it contains media or markdown
|
||||
useEffect(() => {
|
||||
try {
|
||||
const userDataStr = props.token.tokenData.userData
|
||||
if (userDataStr) {
|
||||
const userData = JSON.parse(userDataStr)
|
||||
|
||||
// If user data contains media or markdown, set the mutable data cid
|
||||
if (userData?.media || userData?.markdown) {
|
||||
setMutableDataCid(parseCid(props.token.tokenData.mutableData))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
}, [props.token, show])
|
||||
|
||||
// Replace with dummy button until token data is loaded.
|
||||
if (!props.token.tokenData) {
|
||||
@@ -70,6 +97,22 @@ function InfoButton (props) {
|
||||
<Col xs={8}>{props.token.tokenType}</Col>
|
||||
</Row>
|
||||
|
||||
{mutableDataCid && (
|
||||
<Row>
|
||||
<Col xs={4}><b>User Data</b>:</Col>
|
||||
<Col xs={8}>
|
||||
<a
|
||||
href={`/user-data/${mutableDataCid}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='btn btn-link p-0'
|
||||
>
|
||||
View User Data
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
</Container>
|
||||
</Modal.Body>
|
||||
<Modal.Footer />
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Component for reading global nostr posts
|
||||
*/
|
||||
// Global npm libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Container, Card, Spinner } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
function Feed (props) {
|
||||
const { bchWalletState } = props.appData
|
||||
const [posts, setPosts] = useState([])
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const start = () => {
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const pool = RelayPool([psf])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('REQ', { limit: 10, kinds: [1], '#t': ['slpdex-socialmedia'] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
console.log('Closing Relay')
|
||||
setLoaded(true)
|
||||
relay.close()
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
console.log('Received event:', ev)
|
||||
// const profile = JSON.parse(ev.content)
|
||||
setPosts(currentPosts => [...currentPosts, ev])
|
||||
})
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
start()
|
||||
}
|
||||
}, [bchWalletState, loaded])
|
||||
|
||||
return (
|
||||
<Container className='mt-4 mb-5' style={{ marginBottom: '50px' }}>
|
||||
<div>
|
||||
{posts.map((post, index) => (
|
||||
<Card key={index} className='mb-4 bg-light rounded-4 shadow-sm border-0'>
|
||||
<Card.Body className='p-3'>
|
||||
<div className='d-flex align-items-center gap-3 mb-3'>
|
||||
<div
|
||||
className='rounded-circle bg-gradient shadow d-flex align-items-center justify-content-center'
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
background: 'linear-gradient(45deg, #6c757d, #495057)'
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' />
|
||||
</div>
|
||||
<div className='flex-grow-1'>
|
||||
<div className='fw-bold mb-1'>
|
||||
{post.pubkey.slice(0, 8) + '...'}
|
||||
</div>
|
||||
<small className='text-muted'>
|
||||
{`${post.pubkey.slice(0, 8)}...${post.pubkey.slice(-5)}`}
|
||||
</small>
|
||||
</div>
|
||||
<small className='text-muted'>
|
||||
{new Date(post.created_at * 1000).toLocaleString()}
|
||||
</small>
|
||||
</div>
|
||||
<div className='mb-4'>
|
||||
<div className='fs-5 ms-5 text-dark' style={{ lineHeight: '1.3' }}>
|
||||
{post.content}
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
))}
|
||||
{!posts.length && loaded && (
|
||||
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
|
||||
<i className='bi bi-inbox-fill fs-1 mb-3 d-block opacity-50' />
|
||||
<div>No posts found</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loaded && (
|
||||
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
|
||||
<Spinner animation='border' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Feed
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Component for reading the nostr global feed.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container } from 'react-bootstrap'
|
||||
import Feed from './feed'
|
||||
|
||||
function GlobalFeed (props) {
|
||||
const { appData } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<h2 style={{
|
||||
textAlign: 'center',
|
||||
margin: '20px 0 50px 0',
|
||||
padding: '10px',
|
||||
borderBottom: '2px solid #ccc'
|
||||
}}
|
||||
>
|
||||
Global Feed
|
||||
</h2>
|
||||
<Feed appData={appData} />
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GlobalFeed
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Component for posting nostr information.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container } from 'react-bootstrap'
|
||||
import ProfilePost from './profile-post'
|
||||
import PublicPost from './public-post'
|
||||
|
||||
function NostrPost (props) {
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<h2 style={{
|
||||
textAlign: 'center',
|
||||
margin: '20px 0',
|
||||
padding: '10px',
|
||||
borderBottom: '2px solid #ccc'
|
||||
}}
|
||||
>
|
||||
Nostr Post
|
||||
</h2>
|
||||
<ProfilePost {...props} />
|
||||
<PublicPost {...props} />
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NostrPost
|
||||
+7
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Component for posting nostr information.
|
||||
Component for posting nostr information on kind 0.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
@@ -10,8 +10,8 @@ import { finalizeEvent } from 'nostr-tools/pure'
|
||||
import { Relay } from 'nostr-tools/relay'
|
||||
import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
||||
|
||||
function NostrPost (props) {
|
||||
const [accordionKey, setAccordionKey] = useState('1')
|
||||
function ProfilePost (props) {
|
||||
const [accordionKey, setAccordionKey] = useState(null)
|
||||
const [onFetch, setOnFetch] = useState(false)
|
||||
const { bchWalletState } = props.appData
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -96,10 +96,10 @@ function NostrPost (props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Container className='mt-4'>
|
||||
<Accordion activeKey={accordionKey} onSelect={handleAccordionChange}>
|
||||
<Accordion.Item eventKey='1'>
|
||||
<Accordion.Header>Post</Accordion.Header>
|
||||
<Accordion.Item eventKey='0'>
|
||||
<Accordion.Header>Profile Post</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
{errorMsg && (
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
@@ -155,4 +155,4 @@ function NostrPost (props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default NostrPost
|
||||
export default ProfilePost
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
Component for posting nostr information on kind 1.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container, Form, Button, Spinner } from 'react-bootstrap'
|
||||
import Accordion from 'react-bootstrap/Accordion'
|
||||
import { finalizeEvent } from 'nostr-tools/pure'
|
||||
import { Relay } from 'nostr-tools/relay'
|
||||
import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
||||
|
||||
function PublicPost (props) {
|
||||
const [accordionKey, setAccordionKey] = useState(null)
|
||||
const [onFetch, setOnFetch] = useState(false)
|
||||
const { bchWalletState } = props.appData
|
||||
const [formData, setFormData] = useState({
|
||||
content: ''
|
||||
})
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const [successMsg, setSuccessMsg] = useState('')
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value
|
||||
})
|
||||
|
||||
setSuccessMsg('')
|
||||
setErrorMsg('')
|
||||
}
|
||||
// Post on nostr network
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
setErrorMsg('')
|
||||
setSuccessMsg('')
|
||||
setOnFetch(true)
|
||||
|
||||
const { nostrKeyPair } = bchWalletState
|
||||
|
||||
// Convert private key to binary
|
||||
const privateKeyBin = hexToBytes(nostrKeyPair.privHex)
|
||||
|
||||
// Relay list
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
// Generate a post.
|
||||
const eventTemplate = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['t', 'slpdex-socialmedia']],
|
||||
content: formData.content
|
||||
}
|
||||
console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`)
|
||||
|
||||
// Sign the post
|
||||
const signedEvent = finalizeEvent(eventTemplate, privateKeyBin)
|
||||
console.log('signedEvent: ', signedEvent)
|
||||
|
||||
// Connect to a relay.
|
||||
const relay = await Relay.connect(psf)
|
||||
console.log(`connected to ${relay.url}`)
|
||||
|
||||
// Publish the message to the relay.
|
||||
const result = await relay.publish(signedEvent)
|
||||
console.log('result: ', result)
|
||||
|
||||
// Close the connection to the relay.
|
||||
relay.close()
|
||||
resetForm()
|
||||
setSuccessMsg('Post successfully published!')
|
||||
setOnFetch(false)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
setErrorMsg(error.message || 'An error occurred while posting')
|
||||
setOnFetch(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAccordionChange = (key) => {
|
||||
setAccordionKey(key)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
content: ''
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className='mt-4'>
|
||||
<Accordion activeKey={accordionKey} onSelect={handleAccordionChange}>
|
||||
<Accordion.Item eventKey='0'>
|
||||
<Accordion.Header>Public Post</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
{errorMsg && (
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
{successMsg && (
|
||||
<div className='alert alert-success' role='alert'>
|
||||
{successMsg}
|
||||
</div>
|
||||
)}
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className='mb-3'>
|
||||
<Form.Label>Message</Form.Label>
|
||||
<Form.Control
|
||||
as='textarea'
|
||||
rows={7}
|
||||
placeholder="What's happening?"
|
||||
name='content'
|
||||
value={formData.content}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<div className='d-flex justify-content-center'>
|
||||
{!onFetch && (
|
||||
<Button variant='primary' type='submit' disabled={onFetch}>
|
||||
Post
|
||||
</Button>
|
||||
)}
|
||||
{onFetch && <Spinner animation='border' variant='primary' />}
|
||||
</div>
|
||||
|
||||
</Form>
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PublicPost
|
||||
@@ -1,67 +0,0 @@
|
||||
// Global npm libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Container } from 'react-bootstrap'
|
||||
|
||||
import Accordion from 'react-bootstrap/Accordion'
|
||||
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
function NostrRead (props) {
|
||||
const { bchWalletState } = props.appData
|
||||
const [posts, setPosts] = useState([])
|
||||
const [accordionKey, setAccordionKey] = useState('0')
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Get Last post from a author
|
||||
const start = () => {
|
||||
const { nostrKeyPair } = bchWalletState
|
||||
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const pool = RelayPool([psf])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 10, kinds: [0], authors: [nostrKeyPair.pubHex] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
console.log('Closing Relay')
|
||||
relay.close()
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
console.log('Received event:', ev)
|
||||
setPosts(currentPosts => [...currentPosts, ev])
|
||||
})
|
||||
|
||||
setLoaded(true)
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
start()
|
||||
}
|
||||
}, [bchWalletState, loaded])
|
||||
|
||||
const handleAccordionChange = (key) => {
|
||||
setAccordionKey(key)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Accordion activeKey={accordionKey} onSelect={handleAccordionChange}>
|
||||
<Accordion.Item eventKey='0'>
|
||||
<Accordion.Header>Read ( {posts.length} post found )</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
{posts.map((post, index) => (
|
||||
<div key={index}>
|
||||
<span>{post.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default NostrRead
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Component for read nostr information.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container } from 'react-bootstrap'
|
||||
import ProfileRead from './profile-read.js'
|
||||
import PublicRead from './public-read.js'
|
||||
|
||||
function NostrRead (props) {
|
||||
const [profile, setProfile] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<ProfileRead {...props} setProfile={setProfile} />
|
||||
<PublicRead {...props} profile={profile} />
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NostrRead
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Component for read nostr information kind 0
|
||||
*/
|
||||
// Global npm libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Container, Button } from 'react-bootstrap'
|
||||
import CopyOnClick from '../../bch-wallet/copy-on-click.js'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
function ProfileRead (props) {
|
||||
const { bchWalletState } = props.appData
|
||||
const { setProfile } = props
|
||||
const [post, setPost] = useState({})
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const start = () => {
|
||||
const { nostrKeyPair } = bchWalletState
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const pool = RelayPool([psf])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [nostrKeyPair.pubHex] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
console.log('Closing Relay')
|
||||
relay.close()
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
console.log('Received event:', ev)
|
||||
const profile = JSON.parse(ev.content)
|
||||
setPost(profile)
|
||||
setProfile(profile)
|
||||
})
|
||||
|
||||
setLoaded(true)
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
start()
|
||||
}
|
||||
}, [bchWalletState, loaded, setProfile])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className='d-flex flex-column flex-md-row align-items-center gap-4 mb-5 p-3 p-md-5 bg-light rounded-4 shadow-sm'>
|
||||
<div style={{ width: '100px', height: '100px' }} className='mb-3 mb-md-0'>
|
||||
<div
|
||||
className='rounded-circle bg-gradient shadow d-flex align-items-center justify-content-center'
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(45deg, #6c757d, #495057)'
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} size='3x' color='#7c7c7d' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-grow-1 text-center text-md-start mb-3 mb-md-0 d-flex flex-column align-items-center align-items-md-start'>
|
||||
<h3 className='mb-2 fw-bold'>{post.name}</h3>
|
||||
<div className='text-muted small mb-3 d-flex align-items-center flex-column flex-md-row'>
|
||||
<span className='text-truncate me-2 mb-2 mb-md-0'>
|
||||
<span className='d-md-none'>
|
||||
{`${bchWalletState.nostrKeyPair.npub.slice(0, 8)}...${bchWalletState.nostrKeyPair.npub.slice(-5)}`}
|
||||
</span>
|
||||
<span className='d-none d-md-inline'>
|
||||
{bchWalletState.nostrKeyPair.npub}
|
||||
</span>
|
||||
</span>
|
||||
<CopyOnClick walletProp='npub' appData={props.appData} value={bchWalletState.nostrKeyPair.npub} />
|
||||
</div>
|
||||
<div className='fs-6 text-secondary'>{post.about}</div>
|
||||
</div>
|
||||
<div className='d-flex gap-2 align-items-center flex-wrap justify-content-center'>
|
||||
<Button
|
||||
variant='outline-danger'
|
||||
className='px-3 px-md-4 py-2 rounded-pill fw-semibold'
|
||||
style={{ minWidth: '120px' }}
|
||||
>
|
||||
<i className='bi bi-person-plus me-2' />
|
||||
Follow
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='px-3 px-md-4 py-2 rounded-pill fw-semibold'
|
||||
style={{ minWidth: '120px' }}
|
||||
>
|
||||
<i className='bi bi-chat-dots me-2' />
|
||||
Message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileRead
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Component for read nostr information kind 1
|
||||
*/
|
||||
// Global npm libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Container, Card, Spinner } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
function PublicRead (props) {
|
||||
const { bchWalletState } = props.appData
|
||||
const [posts, setPosts] = useState([])
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Get Last post from a author
|
||||
const start = () => {
|
||||
const { nostrKeyPair } = bchWalletState
|
||||
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const pool = RelayPool([psf])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 5, kinds: [1], authors: [nostrKeyPair.pubHex] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
console.log('Closing Relay')
|
||||
setLoaded(true)
|
||||
relay.close()
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
console.log('Received event:', ev)
|
||||
setPosts(currentPosts => [...currentPosts, ev])
|
||||
})
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
start()
|
||||
}
|
||||
}, [bchWalletState, loaded])
|
||||
|
||||
return (
|
||||
<Container className='mt-4'>
|
||||
<div>
|
||||
{posts.map((post, index) => (
|
||||
<Card key={index} className='mb-4 bg-light rounded-4 shadow-sm border-0'>
|
||||
<Card.Body className='p-3'>
|
||||
<div className='d-flex align-items-center gap-3 mb-3'>
|
||||
<div
|
||||
className='rounded-circle bg-gradient shadow d-flex align-items-center justify-content-center'
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
background: 'linear-gradient(45deg, #6c757d, #495057)'
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' />
|
||||
</div>
|
||||
<div className='flex-grow-1'>
|
||||
<div className='fw-bold mb-1'>{props.profile?.name}</div>
|
||||
<small className='text-muted'>
|
||||
{`${bchWalletState.nostrKeyPair.npub.slice(0, 8)}...${bchWalletState.nostrKeyPair.npub.slice(-5)}`}
|
||||
</small>
|
||||
</div>
|
||||
<small className='text-muted'>
|
||||
{new Date(post.created_at * 1000).toLocaleString()}
|
||||
</small>
|
||||
</div>
|
||||
<div className='mb-4'>
|
||||
<div className='fs-5 ms-5 text-dark' style={{ lineHeight: '1.3' }}>
|
||||
{post.content}
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
))}
|
||||
{!posts.length && loaded && (
|
||||
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
|
||||
<i className='bi bi-inbox-fill fs-1 mb-3 d-block opacity-50' />
|
||||
<div>No posts found</div>
|
||||
</div>
|
||||
)}
|
||||
{!loaded && (
|
||||
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
|
||||
<Spinner animation='border' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default PublicRead
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
Search in the provided cid of a mutable data, get the data from the user data and show it
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Container, Row, Col, Tabs, Tab, Spinner } from 'react-bootstrap'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
function UserDataReview (props) {
|
||||
const appData = props.appData
|
||||
const [media, setMedia] = useState([])
|
||||
const [markdown, setMarkdown] = useState('')
|
||||
const [activeTab, setActiveTab] = useState('media')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
// Get the id parameter from the URL
|
||||
const { cid } = useParams()
|
||||
|
||||
useEffect(() => {
|
||||
const loadMedia = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const { json } = await appData.wallet.cid2json({ cid })
|
||||
const userDataString = json.userData
|
||||
if (userDataString) {
|
||||
const userData = JSON.parse(userDataString)
|
||||
setMedia(userData.media)
|
||||
setMarkdown(userData.markdown)
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error.message)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
loadMedia()
|
||||
}, [cid, appData.wallet])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
{error && <p className='text-danger'>{error}</p>}
|
||||
{loading
|
||||
? (
|
||||
<div className='text-center my-5'>
|
||||
<Spinner animation='border' role='status' variant='primary'>
|
||||
<span className='visually-hidden'>Loading...</span>
|
||||
</Spinner>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onSelect={(k) => setActiveTab(k)}
|
||||
className='mb-4'
|
||||
>
|
||||
<Tab eventKey='media' title='Media'>
|
||||
{media && media.length > 0
|
||||
? (
|
||||
<Row className='mt-3'>
|
||||
{media.map((item, index) => (
|
||||
<Col key={index} xs={12} sm={6} md={4} lg={4} className='mb-3'>
|
||||
<img
|
||||
src={item.url}
|
||||
alt={`Review media ${index + 1}`}
|
||||
style={{ width: '100%', height: '250px', objectFit: 'cover', borderRadius: '8px' }}
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)
|
||||
: (
|
||||
<p className='mt-3'>No media content available</p>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab eventKey='markdown' title='Content'>
|
||||
{markdown
|
||||
? (
|
||||
<div className='markdown-content mt-3'>
|
||||
<ReactMarkdown>{markdown}</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<p className='mt-3'>No content available</p>
|
||||
)}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserDataReview
|
||||
@@ -114,6 +114,13 @@ function NavMenu (props) {
|
||||
>
|
||||
Nostr Profile
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={currentPath === '/global-feed' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/global-feed'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Global Feed
|
||||
</NavLink>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
|
||||
+17
-1
@@ -37,6 +37,9 @@ function useAppState () {
|
||||
const [hideSpinner, setHideSpinner] = useState(false)
|
||||
const [denyClose, setDenyClose] = useState(false)
|
||||
|
||||
// NFTs for sale stored data to improve performance
|
||||
const [nftForSaleCacheData, setNftForSaleCacheData] = useState({})
|
||||
|
||||
// The wallet state makes this a true progressive web app (PWA). As
|
||||
// balances, UTXOs, and tokens are retrieved, this state is updated.
|
||||
// properties are enumerated here for the purpose of documentation.
|
||||
@@ -96,6 +99,16 @@ function useAppState () {
|
||||
}
|
||||
}
|
||||
|
||||
// Update NFT for sale tokens data
|
||||
function updateNFTCachedData (tokenId, data) {
|
||||
const allCacheData = nftForSaleCacheData // Get all cache data
|
||||
const cacheData = allCacheData[tokenId] || {} // Get cache data for the tokenId
|
||||
|
||||
const newCacheData = Object.assign({}, cacheData, data) // Merge the new data with the cache data
|
||||
allCacheData[tokenId] = newCacheData // Update the cache data
|
||||
setNftForSaleCacheData(allCacheData) // Update the state
|
||||
}
|
||||
|
||||
return {
|
||||
serverUrl,
|
||||
setServerUrl,
|
||||
@@ -131,7 +144,10 @@ function useAppState () {
|
||||
dexLib,
|
||||
setDexLib,
|
||||
nostr,
|
||||
setNostr
|
||||
setNostr,
|
||||
nftForSaleCacheData,
|
||||
setNftForSaleCacheData,
|
||||
updateNFTCachedData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import P2WDB from 'p2wdb'
|
||||
import { base58_to_binary as base58ToBinary } from 'base58-js'
|
||||
import { bytesToHex } from '@noble/hashes/utils' // already an installed dependency
|
||||
import { getPublicKey } from 'nostr-tools/pure'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
|
||||
class AsyncLoad {
|
||||
constructor () {
|
||||
@@ -65,7 +66,7 @@ class AsyncLoad {
|
||||
|
||||
// Get Nostr key pair from WIF
|
||||
const nostrKeyPair = this.nostrKeyPairFromWIF(wallet.walletInfo.privateKey)
|
||||
|
||||
console.log('nostrKeyPair: ', nostrKeyPair)
|
||||
const walletInfo = wallet.walletInfo
|
||||
walletInfo.nostrKeyPair = nostrKeyPair
|
||||
// Update the state of the wallet.
|
||||
@@ -265,15 +266,21 @@ class AsyncLoad {
|
||||
const wifBuf = base58ToBinary(WIF)
|
||||
const privBuf = wifBuf.slice(1, 33)
|
||||
// console.log('privBuf: ', privBuf)
|
||||
|
||||
// Convert the private key to a hex string
|
||||
const privHex = bytesToHex(privBuf)
|
||||
console.log('BCH & Nostr private key (HEX format): ', privHex)
|
||||
// Convert the private key to a Nostr NSEC key
|
||||
const nsec = nip19.nsecEncode(privBuf)
|
||||
|
||||
// Get the public key hex string from the private key
|
||||
const pubHex = getPublicKey(privBuf)
|
||||
// Convert the public key to a Nostr NPUB key
|
||||
const npub = nip19.npubEncode(pubHex)
|
||||
|
||||
return {
|
||||
privHex,
|
||||
pubHex
|
||||
pubHex,
|
||||
nsec,
|
||||
npub
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user