mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex-taker-v2.git
synced 2026-09-21 16:52:01 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98603d3fc5 | ||
|
|
0f16c3e0fb | ||
|
|
43b35a3553 | ||
|
|
862377b929 | ||
|
|
02e800ff6a | ||
|
|
7e1b75f965 | ||
|
|
990e41e8f6 | ||
|
|
9d35d80bcf | ||
|
|
907bb7e468 | ||
|
|
7eb5ff7821 | ||
|
|
8a982d2ef2 | ||
|
|
24bb18c77c | ||
|
|
913745ba9f | ||
|
|
f24c199dd6 | ||
|
|
0da9d4c567 | ||
|
|
e05948aa32 | ||
|
|
3cf69cbd95 | ||
|
|
45b28c168f | ||
|
|
3d622e9d99 | ||
|
|
29ce96ae92 | ||
|
|
a89d4c21cc | ||
|
|
561ffbaaad | ||
|
|
2141f919dd | ||
|
|
3087533aff |
Generated
+17
-2
@@ -21,6 +21,7 @@
|
|||||||
"bch-token-sweep": "2.2.1",
|
"bch-token-sweep": "2.2.1",
|
||||||
"bootstrap": "5.2.0",
|
"bootstrap": "5.2.0",
|
||||||
"crypto-browserify": "3.12.1",
|
"crypto-browserify": "3.12.1",
|
||||||
|
"marked": "^15.0.12",
|
||||||
"nostr-tools": "2.10.4",
|
"nostr-tools": "2.10.4",
|
||||||
"p2wdb": "2.4.1",
|
"p2wdb": "2.4.1",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
@@ -18223,8 +18224,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "12.0.2",
|
"version": "15.0.12",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||||
|
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
@@ -26496,6 +26498,19 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/semantic-release/node_modules/marked": {
|
||||||
|
"version": "12.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
|
||||||
|
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semantic-release/node_modules/npm-run-path": {
|
"node_modules/semantic-release/node_modules/npm-run-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"bch-token-sweep": "2.2.1",
|
"bch-token-sweep": "2.2.1",
|
||||||
"bootstrap": "5.2.0",
|
"bootstrap": "5.2.0",
|
||||||
"crypto-browserify": "3.12.1",
|
"crypto-browserify": "3.12.1",
|
||||||
|
"marked": "15.0.12",
|
||||||
"nostr-tools": "2.10.4",
|
"nostr-tools": "2.10.4",
|
||||||
"p2wdb": "2.4.1",
|
"p2wdb": "2.4.1",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
|
|||||||
@@ -41,6 +41,9 @@
|
|||||||
width: auto!important;
|
width: auto!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Remove input number arrows Chrome, Safari, Edge, Opera */
|
/* Remove input number arrows Chrome, Safari, Edge, Opera */
|
||||||
input::-webkit-outer-spin-button,
|
input::-webkit-outer-spin-button,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import ServerSelectView from './configuration/select-server-view'
|
|||||||
import NostrPost from './nostr/nostr-post/index.js'
|
import NostrPost from './nostr/nostr-post/index.js'
|
||||||
import NostrRead from './nostr/nostr-read/index.js'
|
import NostrRead from './nostr/nostr-read/index.js'
|
||||||
import GlobalFeed from './nostr/global-feed/index.js'
|
import GlobalFeed from './nostr/global-feed/index.js'
|
||||||
|
import ContentCreators from './nostr/content-creators/index.js'
|
||||||
import UserDataReview from './user-data-review'
|
import UserDataReview from './user-data-review'
|
||||||
import Offers from './offers'
|
import Offers from './offers'
|
||||||
function AppBody (props) {
|
function AppBody (props) {
|
||||||
@@ -48,9 +49,10 @@ function AppBody (props) {
|
|||||||
<Route path='/sign' element={<SignMessage appData={appData} />} />
|
<Route path='/sign' element={<SignMessage appData={appData} />} />
|
||||||
<Route path='/configuration' element={<ServerSelectView appData={appData} />} />
|
<Route path='/configuration' element={<ServerSelectView appData={appData} />} />
|
||||||
<Route path='/nostr-post' element={<NostrPost appData={appData} />} />
|
<Route path='/nostr-post' element={<NostrPost appData={appData} />} />
|
||||||
<Route path='/nostr-read' element={<NostrRead appData={appData} />} />
|
<Route path='/nostr-read/:npub' element={<NostrRead appData={appData} />} />
|
||||||
<Route path='/global-feed' element={<GlobalFeed appData={appData} />} />
|
<Route path='/global-feed' element={<GlobalFeed appData={appData} />} />
|
||||||
<Route path='/user-data/:cid' element={<UserDataReview appData={appData} />} />
|
<Route path='/content-creators' element={<ContentCreators appData={appData} />} />
|
||||||
|
<Route path='/user-data/:tokenId' element={<UserDataReview appData={appData} />} />
|
||||||
<Route path='/offers' element={<Offers appData={appData} />} />
|
<Route path='/offers' element={<Offers appData={appData} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
{/** Show in all paths except the servers view */}
|
{/** Show in all paths except the servers view */}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function InfoButton (props) {
|
|||||||
<Col xs={4}><b>User Data</b>:</Col>
|
<Col xs={4}><b>User Data</b>:</Col>
|
||||||
<Col xs={8}>
|
<Col xs={8}>
|
||||||
<a
|
<a
|
||||||
href={`/user-data/${mutableDataCid}`}
|
href={`/user-data/${props.token.tokenId}`}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='btn btn-link p-0'
|
className='btn btn-link p-0'
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* Content creators card
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Card } from 'react-bootstrap'
|
||||||
|
import Jdenticon from '@chris.troutner/react-jdenticon'
|
||||||
|
import CopyOnClick from '../../bch-wallet/copy-on-click.js'
|
||||||
|
import FollowBtn from './follow-btn.js'
|
||||||
|
import { RelayPool } from 'nostr'
|
||||||
|
|
||||||
|
function ContentCard (props) {
|
||||||
|
const [profile, setProfile] = useState([])
|
||||||
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
const { creator, followList, refreshFollowList } = props
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadProfile = async () => {
|
||||||
|
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||||
|
|
||||||
|
const pool = RelayPool([psf])
|
||||||
|
pool.on('open', relay => {
|
||||||
|
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [creator.pubkey] })
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('eose', relay => {
|
||||||
|
console.log('Closing Relay')
|
||||||
|
relay.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('event', (relay, subId, ev) => {
|
||||||
|
try {
|
||||||
|
const profile = JSON.parse(ev.content)
|
||||||
|
console.log('profile', profile)
|
||||||
|
setProfile(profile)
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error parsing profile', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setLoaded(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loaded && creator.npub) {
|
||||||
|
loadProfile()
|
||||||
|
}
|
||||||
|
}, [loaded, creator])
|
||||||
|
|
||||||
|
const goToProfile = () => {
|
||||||
|
const profileUrl = `${window.location.origin}/nostr-read/${creator.npub}`
|
||||||
|
window.open(profileUrl, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<>
|
||||||
|
<Card className='mb-4 bg-light rounded-4 shadow-sm border-0'>
|
||||||
|
<Card.Body className='p-4'>
|
||||||
|
{/* Desktop Layout - Horizontal */}
|
||||||
|
<div className='d-flex justify-content-end mb-2' onClick={goToProfile}>
|
||||||
|
<small className='text-muted'>
|
||||||
|
{new Date(creator.createdAt).toLocaleString()}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div className='d-none d-md-flex align-items-center gap-4 cursor-pointer'>
|
||||||
|
{/* Profile Picture */}
|
||||||
|
<div className='flex-shrink-0' onClick={goToProfile}>
|
||||||
|
<Jdenticon size='140' value={creator.npub} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Creator Info */}
|
||||||
|
<div className='flex-grow-1'>
|
||||||
|
<h5 className='mb-2 fw-bold cursor-pointer' onClick={goToProfile}>{profile.name}</h5>
|
||||||
|
<p className='text-muted medium mb-3'>{profile.about}</p>
|
||||||
|
|
||||||
|
{/* Nostr Public Key */}
|
||||||
|
<div className='mb-2'>
|
||||||
|
<div className='d-flex align-items-center gap-2 mb-1'>
|
||||||
|
<small className='text-muted fw-semibold'>Nostr Public Key:</small>
|
||||||
|
</div>
|
||||||
|
<div className='d-flex align-items-center gap-2'>
|
||||||
|
<div className='text-break small text-secondary'>
|
||||||
|
{creator.npub}
|
||||||
|
</div>
|
||||||
|
<CopyOnClick
|
||||||
|
walletProp='npub'
|
||||||
|
appData={props.appData}
|
||||||
|
value={creator.npub}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BCH Address */}
|
||||||
|
<div className='mb-3'>
|
||||||
|
<div className='d-flex align-items-center gap-2 mb-1'>
|
||||||
|
<small className='text-muted fw-semibold'>BCH Address:</small>
|
||||||
|
</div>
|
||||||
|
<div className='d-flex align-items-center gap-2'>
|
||||||
|
<div className='text-break small text-secondary'>
|
||||||
|
{creator.bchAddr}
|
||||||
|
</div>
|
||||||
|
<CopyOnClick
|
||||||
|
walletProp='bchAddress'
|
||||||
|
appData={props.appData}
|
||||||
|
value={creator.bchAddr}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className='flex-shrink-0 d-flex flex-column gap-2'>
|
||||||
|
<FollowBtn
|
||||||
|
creator={creator}
|
||||||
|
appData={props.appData}
|
||||||
|
creatorProfile={profile}
|
||||||
|
followList={followList}
|
||||||
|
refreshFollowList={refreshFollowList}
|
||||||
|
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='btn btn-primary btn-sm rounded-pill px-3'
|
||||||
|
style={{ fontSize: '0.875rem', minWidth: '100px' }}
|
||||||
|
>
|
||||||
|
<i className='bi bi-chat-dots me-1' />
|
||||||
|
Message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Layout - Vertical */}
|
||||||
|
<div className='d-flex d-md-none flex-column align-items-center text-center'>
|
||||||
|
{/* Profile Picture */}
|
||||||
|
<div className='mb-3'>
|
||||||
|
<Jdenticon size='100' value={creator.npub} onClick={goToProfile} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Creator Info */}
|
||||||
|
<div className='w-100 mb-3'>
|
||||||
|
<h5 className='mb-2 fw-bold' onClick={goToProfile}>{profile.name}</h5>
|
||||||
|
<p className='text-muted small mb-3'>{profile.about}</p>
|
||||||
|
|
||||||
|
{/* Nostr Public Key */}
|
||||||
|
<div className='mb-3'>
|
||||||
|
<div className='mb-1'>
|
||||||
|
<small className='text-muted fw-semibold'>Nostr Public Key:</small>
|
||||||
|
</div>
|
||||||
|
<div className='d-flex align-items-center justify-content-center gap-2'>
|
||||||
|
<div className='text-break small text-secondary' style={{ maxWidth: '200px' }}>
|
||||||
|
{creator.npub}
|
||||||
|
</div>
|
||||||
|
<CopyOnClick
|
||||||
|
walletProp='npub'
|
||||||
|
appData={props.appData}
|
||||||
|
value={creator.npub}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BCH Address */}
|
||||||
|
<div className='mb-3'>
|
||||||
|
<div className='mb-1'>
|
||||||
|
<small className='text-muted fw-semibold'>BCH Address:</small>
|
||||||
|
</div>
|
||||||
|
<div className='d-flex align-items-center justify-content-center gap-2'>
|
||||||
|
<div className='text-break small text-secondary' style={{ maxWidth: '200px' }}>
|
||||||
|
{creator.bchAddr}
|
||||||
|
</div>
|
||||||
|
<CopyOnClick
|
||||||
|
walletProp='bchAddress'
|
||||||
|
appData={props.appData}
|
||||||
|
value={creator.bchAddr}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className='d-flex gap-2'>
|
||||||
|
<FollowBtn
|
||||||
|
creator={creator}
|
||||||
|
appData={props.appData}
|
||||||
|
creatorProfile={profile}
|
||||||
|
followList={followList}
|
||||||
|
refreshFollowList={refreshFollowList}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='btn btn-primary btn-sm rounded-pill px-3'
|
||||||
|
style={{ fontSize: '0.875rem' }}
|
||||||
|
>
|
||||||
|
<i className='bi bi-chat-dots me-1' />
|
||||||
|
Message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContentCard
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { Container, Spinner } from 'react-bootstrap'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
// Local libraries
|
||||||
|
import config from '../../../../config'
|
||||||
|
import ContentCard from './content-card'
|
||||||
|
import { RelayPool } from 'nostr'
|
||||||
|
|
||||||
|
// Global variables and constants
|
||||||
|
const SERVER = config.dexServer
|
||||||
|
function ContentCreators (props) {
|
||||||
|
const [creators, setCreators] = useState([])
|
||||||
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
const { appData } = props
|
||||||
|
|
||||||
|
const [followList, setFollowList] = useState([])
|
||||||
|
|
||||||
|
const getFollowList = useCallback(async () => {
|
||||||
|
const list = await new Promise((resolve, reject) => {
|
||||||
|
let list = []
|
||||||
|
const { nostrKeyPair } = appData.bchWalletState
|
||||||
|
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||||
|
|
||||||
|
const pool = RelayPool([psf])
|
||||||
|
pool.on('open', relay => {
|
||||||
|
relay.subscribe('subid', { limit: 1, kinds: [3], authors: [nostrKeyPair.pubHex] })
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('eose', relay => {
|
||||||
|
console.log('Closing Relay')
|
||||||
|
relay.close()
|
||||||
|
/** This ensures to return an empty array if no records are found!
|
||||||
|
* Applies for new users that don't have a follow list
|
||||||
|
*/
|
||||||
|
resolve(list)
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('event', (relay, subId, ev) => {
|
||||||
|
console.log('Received event:', ev)
|
||||||
|
list = ev.tags
|
||||||
|
resolve(list)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Follow List', list)
|
||||||
|
setFollowList(list)
|
||||||
|
}, [appData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCreators = async () => {
|
||||||
|
try {
|
||||||
|
const creators = await axios.get(`${SERVER}/sm/list/all/0`)
|
||||||
|
console.log(creators.data)
|
||||||
|
setCreators(creators.data)
|
||||||
|
setLoaded(true)
|
||||||
|
} catch (error) {
|
||||||
|
setLoaded(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
loadCreators()
|
||||||
|
getFollowList()
|
||||||
|
}
|
||||||
|
}, [loaded, followList, getFollowList])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className='mt-4 mb-5'>
|
||||||
|
<div className='mb-4'>
|
||||||
|
<h2 className='text-center mb-3'>Content Creators</h2>
|
||||||
|
<p className='text-center text-muted'>
|
||||||
|
Discover amazing content creators in the Bitcoin Cash ecosystem
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!loaded && (
|
||||||
|
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
|
||||||
|
<Spinner animation='border' />
|
||||||
|
<div className='mt-3'>Loading content creators...</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loaded && (
|
||||||
|
<div>
|
||||||
|
{creators.map((creator, i) => (
|
||||||
|
<ContentCard
|
||||||
|
key={`creator-key${i}`}
|
||||||
|
creator={creator}
|
||||||
|
appData={props.appData}
|
||||||
|
followList={followList}
|
||||||
|
refreshFollowList={getFollowList}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loaded && creators.length === 0 && (
|
||||||
|
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
|
||||||
|
<i className='bi bi-people-fill fs-1 mb-3 d-block opacity-50' />
|
||||||
|
<div>No content creators found</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContentCreators
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Spinner } from 'react-bootstrap'
|
||||||
|
import { finalizeEvent } from 'nostr-tools/pure'
|
||||||
|
import { Relay } from 'nostr-tools/relay'
|
||||||
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
|
function FollowBtn (props) {
|
||||||
|
const [onFetch, setOnFetch] = useState(false)
|
||||||
|
const [isFollowing, setIsFollowing] = useState(false)
|
||||||
|
const { creator, appData, creatorProfile, followList, refreshFollowList } = props
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isFollowing = followList.find(item => item[1] === creator.pubkey)
|
||||||
|
setIsFollowing(isFollowing)
|
||||||
|
}, [followList, creator.pubkey])
|
||||||
|
|
||||||
|
// Add creator to the follow list
|
||||||
|
const handleFollow = async () => {
|
||||||
|
try {
|
||||||
|
setOnFetch(true)
|
||||||
|
// Get current follow list
|
||||||
|
const currentList = followList
|
||||||
|
console.log('currentList', currentList)
|
||||||
|
|
||||||
|
// find if creator is in the list
|
||||||
|
const existing = currentList.find(item => item[1] === creator.pubkey)
|
||||||
|
if (!existing) {
|
||||||
|
const creatorName = creatorProfile.name || ''
|
||||||
|
|
||||||
|
// add creator to the list
|
||||||
|
currentList.push(['p', creator.pubkey, 'wss://nostr-relay.psfoundation.info', creatorName])
|
||||||
|
await submitFollowList(currentList)
|
||||||
|
await refreshFollowList()
|
||||||
|
} else {
|
||||||
|
console.log('Already Followed')
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnFetch(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
setOnFetch(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add creator to the follow list
|
||||||
|
const handleUnfollow = async () => {
|
||||||
|
try {
|
||||||
|
setOnFetch(true)
|
||||||
|
// Get current follow list
|
||||||
|
const currentList = followList
|
||||||
|
console.log('currentList', currentList)
|
||||||
|
|
||||||
|
// find if creator is in the list
|
||||||
|
const existing = currentList.find(item => item[1] === creator.pubkey)
|
||||||
|
if (existing) {
|
||||||
|
// remove creator from the list
|
||||||
|
currentList.splice(currentList.indexOf(existing), 1)
|
||||||
|
await submitFollowList(currentList)
|
||||||
|
await refreshFollowList()
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnFetch(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
setOnFetch(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitFollowList = async (newList) => {
|
||||||
|
try {
|
||||||
|
const { nostrKeyPair } = appData.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: 3,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: newList,
|
||||||
|
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()
|
||||||
|
setTimeout(() => {
|
||||||
|
setOnFetch(false)
|
||||||
|
}, 1000)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
setOnFetch(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{onFetch && (
|
||||||
|
<div className='d-flex justify-content-center'>
|
||||||
|
<Spinner animation='border' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!onFetch && !isFollowing && (
|
||||||
|
<button
|
||||||
|
className='btn btn-outline-danger btn-sm rounded-pill px-3'
|
||||||
|
style={{ fontSize: '0.875rem', minWidth: '100px' }}
|
||||||
|
onClick={handleFollow}
|
||||||
|
>
|
||||||
|
<i className='bi bi-person-plus me-1' />
|
||||||
|
Follow
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!onFetch && isFollowing && (
|
||||||
|
<button
|
||||||
|
className='btn btn-outline-danger btn-sm rounded-pill px-3'
|
||||||
|
style={{ fontSize: '0.875rem', minWidth: '100px' }}
|
||||||
|
onClick={handleUnfollow}
|
||||||
|
>
|
||||||
|
<i className='bi bi-person-plus me-1' />
|
||||||
|
Unfollow
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FollowBtn
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Container } from 'react-bootstrap'
|
||||||
|
import ContentCreatorsList from './content-creators-list.js'
|
||||||
|
|
||||||
|
function ContentCreators (props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container>
|
||||||
|
<ContentCreatorsList {...props} />
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContentCreators
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* Component for reading global nostr posts
|
||||||
|
*/
|
||||||
|
// Global npm libraries
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { Card, Spinner } from 'react-bootstrap'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faUser } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import * as nip19 from 'nostr-tools/nip19'
|
||||||
|
import NostrFormat from '../nostr-format'
|
||||||
|
|
||||||
|
function FeedCard (props) {
|
||||||
|
const { post, appData, profiles } = props
|
||||||
|
const [profile, setProfile] = useState(profiles[post.pubkey])
|
||||||
|
|
||||||
|
const [npub, setNpub] = useState('')
|
||||||
|
const [isClicked, setIsClicked] = useState(false)
|
||||||
|
|
||||||
|
// Get npub from pubkey
|
||||||
|
useEffect(() => {
|
||||||
|
const npub = nip19.npubEncode(post.pubkey)
|
||||||
|
setNpub(npub)
|
||||||
|
}, [post])
|
||||||
|
|
||||||
|
// Verify if pubkey profile exists into the profiles state
|
||||||
|
useEffect(() => {
|
||||||
|
if (profiles[post.pubkey]) {
|
||||||
|
setProfile(profiles[post.pubkey])
|
||||||
|
}
|
||||||
|
}, [profiles, post])
|
||||||
|
|
||||||
|
// copy to clipboard
|
||||||
|
const copyToClipboard = useCallback((value) => {
|
||||||
|
setIsClicked(true)
|
||||||
|
appData.appUtil.copyToClipboard(value)
|
||||||
|
setTimeout(() => setIsClicked(false), 200)
|
||||||
|
}, [appData])
|
||||||
|
|
||||||
|
// get short npub
|
||||||
|
const getShortNpub = useCallback((npub) => {
|
||||||
|
return npub.slice(0, 8) + '...' + npub.slice(-5)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card 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'>
|
||||||
|
{profile && profile.name && <span>{profile.name}</span>}
|
||||||
|
{!profile?.name && (
|
||||||
|
<span>
|
||||||
|
{post.pubkey.slice(0, 8) + '...'}
|
||||||
|
<Spinner animation='border' size='sm' className='ms-2' />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{npub && (
|
||||||
|
<small className='text-muted'>
|
||||||
|
<span
|
||||||
|
title='Copy to clipboard'
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
transform: isClicked ? 'scale(0.95)' : 'scale(1)',
|
||||||
|
transition: 'transform 0.1s ease',
|
||||||
|
display: 'inline-block'
|
||||||
|
}}
|
||||||
|
onClick={() => copyToClipboard(npub)}
|
||||||
|
>
|
||||||
|
{getShortNpub(npub)}
|
||||||
|
</span>
|
||||||
|
</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' }}>
|
||||||
|
<NostrFormat content={post.content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FeedCard
|
||||||
@@ -2,18 +2,53 @@
|
|||||||
* Component for reading global nostr posts
|
* Component for reading global nostr posts
|
||||||
*/
|
*/
|
||||||
// Global npm libraries
|
// Global npm libraries
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState, useCallback } from 'react'
|
||||||
import { Container, Card, Spinner } from 'react-bootstrap'
|
import { Container, Spinner } from 'react-bootstrap'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import { faUser } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
import { RelayPool } from 'nostr'
|
import { RelayPool } from 'nostr'
|
||||||
|
|
||||||
|
import FeedCard from './feed-card'
|
||||||
|
|
||||||
function Feed (props) {
|
function Feed (props) {
|
||||||
const { bchWalletState } = props.appData
|
const { appData } = props
|
||||||
|
const { bchWalletState } = appData
|
||||||
const [posts, setPosts] = useState([])
|
const [posts, setPosts] = useState([])
|
||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
const [profiles, setProfiles] = useState({})
|
||||||
|
|
||||||
|
// function to fetch profile and set it to profiles state
|
||||||
|
const fetchProfile = useCallback((pubkey) => {
|
||||||
|
// no fetch profile again if it exist
|
||||||
|
if (profiles[pubkey]) {
|
||||||
|
console.log('profile exists', profiles[pubkey])
|
||||||
|
return profiles[pubkey]
|
||||||
|
}
|
||||||
|
|
||||||
|
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||||
|
|
||||||
|
const pool = RelayPool([psf])
|
||||||
|
pool.on('open', relay => {
|
||||||
|
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubkey] })
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('eose', relay => {
|
||||||
|
console.log('Closing Relay')
|
||||||
|
relay.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('event', (relay, subId, ev) => {
|
||||||
|
try {
|
||||||
|
const profile = JSON.parse(ev.content)
|
||||||
|
const newProfiles = { ...profiles }
|
||||||
|
newProfiles[pubkey] = profile
|
||||||
|
setProfiles(newProfiles)
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error parsing profile', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [profiles])
|
||||||
|
|
||||||
|
// Get global feed posts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const start = () => {
|
const start = () => {
|
||||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||||
@@ -29,54 +64,25 @@ function Feed (props) {
|
|||||||
relay.close()
|
relay.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
pool.on('event', (relay, subId, ev) => {
|
pool.on('event', async (relay, subId, ev) => {
|
||||||
console.log('Received event:', ev)
|
console.log('Received event:', ev)
|
||||||
// const profile = JSON.parse(ev.content)
|
|
||||||
setPosts(currentPosts => [...currentPosts, ev])
|
setPosts(currentPosts => [...currentPosts, ev])
|
||||||
|
// fetch post profile and set it to profiles state
|
||||||
|
fetchProfile(ev.pubkey)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
}, [bchWalletState, loaded])
|
}, [bchWalletState, loaded, fetchProfile])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className='mt-4 mb-5' style={{ marginBottom: '50px' }}>
|
<Container className='mt-4 mb-5' style={{ marginBottom: '50px' }}>
|
||||||
<div>
|
<div>
|
||||||
{posts.map((post, index) => (
|
{posts.map((post, index) => (
|
||||||
<Card key={index} className='mb-4 bg-light rounded-4 shadow-sm border-0'>
|
<FeedCard key={index} post={post} appData={props.appData} profiles={profiles} />
|
||||||
<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 && (
|
{!posts.length && loaded && (
|
||||||
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
|
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Parse nostr content to html , support markdown , links , line breaks
|
||||||
|
*/
|
||||||
|
import React from 'react'
|
||||||
|
import { marked } from 'marked' // https://www.npmjs.com/package/marked/v/0.3.9
|
||||||
|
|
||||||
|
const NostrFormat = ({ content }) => {
|
||||||
|
const formatContent = (content) => {
|
||||||
|
if (!content) return ''
|
||||||
|
// create marked renderer
|
||||||
|
const renderer = new marked.Renderer()
|
||||||
|
// customize link renderer to open in new tab
|
||||||
|
renderer.link = function ({ href, text }) {
|
||||||
|
return `<a href="${href}" target="_blank" >${text}</a>`
|
||||||
|
}
|
||||||
|
// set marked options
|
||||||
|
marked.setOptions({
|
||||||
|
renderer,
|
||||||
|
breaks: true
|
||||||
|
|
||||||
|
})
|
||||||
|
// parse content to html
|
||||||
|
const html = marked.parse(content)
|
||||||
|
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: formatContent(content) }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NostrFormat
|
||||||
@@ -16,7 +16,12 @@ function ProfilePost (props) {
|
|||||||
const { bchWalletState } = props.appData
|
const { bchWalletState } = props.appData
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
about: ''
|
about: '',
|
||||||
|
picture: '',
|
||||||
|
display_name: '',
|
||||||
|
website: '',
|
||||||
|
banner: '',
|
||||||
|
bot: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const [errorMsg, setErrorMsg] = useState('')
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
@@ -90,7 +95,12 @@ function ProfilePost (props) {
|
|||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
about: ''
|
about: '',
|
||||||
|
picture: '',
|
||||||
|
display_name: '',
|
||||||
|
website: '',
|
||||||
|
banner: '',
|
||||||
|
bot: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +147,67 @@ function ProfilePost (props) {
|
|||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group className='mb-3'>
|
||||||
|
<Form.Label>Profile Picture URL</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type='url'
|
||||||
|
placeholder='Enter URL to your profile picture'
|
||||||
|
name='picture'
|
||||||
|
value={formData.picture}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group className='mb-3'>
|
||||||
|
<Form.Label>Display Name</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type='text'
|
||||||
|
placeholder='Enter your display name'
|
||||||
|
name='display_name'
|
||||||
|
value={formData.display_name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group className='mb-3'>
|
||||||
|
<Form.Label>Website</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type='url'
|
||||||
|
placeholder='Enter your website URL'
|
||||||
|
name='website'
|
||||||
|
value={formData.website}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group className='mb-3'>
|
||||||
|
<Form.Label>Banner URL</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type='url'
|
||||||
|
placeholder='Enter URL to your banner picture (1024x768 pixels)'
|
||||||
|
name='banner'
|
||||||
|
value={formData.banner}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group className='mb-3'>
|
||||||
|
<Form.Check
|
||||||
|
type='checkbox'
|
||||||
|
label='This account is for a bot'
|
||||||
|
name='bot'
|
||||||
|
checked={formData.bot}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
bot: e.target.checked
|
||||||
|
})
|
||||||
|
setSuccessMsg('')
|
||||||
|
setErrorMsg('')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<div className='d-flex justify-content-center'>
|
<div className='d-flex justify-content-center'>
|
||||||
{!onFetch && (
|
{!onFetch && (
|
||||||
<Button variant='primary' type='submit' disabled={onFetch}>
|
<Button variant='primary' type='submit' disabled={onFetch}>
|
||||||
|
|||||||
@@ -47,11 +47,14 @@ function PublicPost (props) {
|
|||||||
// Relay list
|
// Relay list
|
||||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||||
|
|
||||||
|
// BCH address of the user.
|
||||||
|
const bchAddr = bchWalletState.address
|
||||||
|
|
||||||
// Generate a post.
|
// Generate a post.
|
||||||
const eventTemplate = {
|
const eventTemplate = {
|
||||||
kind: 1,
|
kind: 1,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [['t', 'slpdex-socialmedia']],
|
tags: [['t', 'slpdex-socialmedia'], ['u', bchAddr]],
|
||||||
content: formData.content
|
content: formData.content
|
||||||
}
|
}
|
||||||
console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`)
|
console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`)
|
||||||
|
|||||||
@@ -7,14 +7,17 @@ import React, { useState } from 'react'
|
|||||||
import { Container } from 'react-bootstrap'
|
import { Container } from 'react-bootstrap'
|
||||||
import ProfileRead from './profile-read.js'
|
import ProfileRead from './profile-read.js'
|
||||||
import PublicRead from './public-read.js'
|
import PublicRead from './public-read.js'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
function NostrRead (props) {
|
function NostrRead (props) {
|
||||||
const [profile, setProfile] = useState(false)
|
const [profile, setProfile] = useState(false)
|
||||||
|
const { npub } = useParams()
|
||||||
|
console.log('npub', npub)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container>
|
<Container>
|
||||||
<ProfileRead {...props} setProfile={setProfile} />
|
<ProfileRead {...props} setProfile={setProfile} npub={npub} />
|
||||||
<PublicRead {...props} profile={profile} />
|
<PublicRead {...props} profile={profile} npub={npub} />
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,23 +7,29 @@ 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 } 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'
|
||||||
|
|
||||||
function ProfileRead (props) {
|
function ProfileRead (props) {
|
||||||
|
const { npub } = props
|
||||||
const { bchWalletState } = props.appData
|
const { bchWalletState } = props.appData
|
||||||
|
console.log('npub prop', npub)
|
||||||
const { setProfile } = props
|
const { setProfile } = props
|
||||||
const [post, setPost] = useState({})
|
const [post, setPost] = useState({})
|
||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const start = () => {
|
const start = () => {
|
||||||
const { nostrKeyPair } = bchWalletState
|
const pubHexData = nip19.decode(npub)
|
||||||
|
const pubHex = pubHexData.data
|
||||||
|
console.log('pubhex', pubHex)
|
||||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||||
|
|
||||||
const pool = RelayPool([psf])
|
const pool = RelayPool([psf])
|
||||||
pool.on('open', relay => {
|
pool.on('open', relay => {
|
||||||
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [nostrKeyPair.pubHex] })
|
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubHex] })
|
||||||
|
setLoaded(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
pool.on('eose', relay => {
|
pool.on('eose', relay => {
|
||||||
@@ -34,6 +40,7 @@ function ProfileRead (props) {
|
|||||||
pool.on('event', (relay, subId, ev) => {
|
pool.on('event', (relay, subId, ev) => {
|
||||||
console.log('Received event:', ev)
|
console.log('Received event:', ev)
|
||||||
const profile = JSON.parse(ev.content)
|
const profile = JSON.parse(ev.content)
|
||||||
|
console.log('Profile:', profile)
|
||||||
setPost(profile)
|
setPost(profile)
|
||||||
setProfile(profile)
|
setProfile(profile)
|
||||||
})
|
})
|
||||||
@@ -41,10 +48,10 @@ function ProfileRead (props) {
|
|||||||
setLoaded(true)
|
setLoaded(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded && npub) {
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
}, [bchWalletState, loaded, setProfile])
|
}, [bchWalletState, loaded, setProfile, npub])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@@ -74,7 +81,9 @@ function ProfileRead (props) {
|
|||||||
</span>
|
</span>
|
||||||
<CopyOnClick walletProp='npub' appData={props.appData} value={bchWalletState.nostrKeyPair.npub} />
|
<CopyOnClick walletProp='npub' appData={props.appData} value={bchWalletState.nostrKeyPair.npub} />
|
||||||
</div>
|
</div>
|
||||||
<div className='fs-6 text-secondary'>{post.about}</div>
|
<div className='fs-6 text-secondary'>
|
||||||
|
<NostrFormat content={post.about} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='d-flex gap-2 align-items-center flex-wrap justify-content-center'>
|
<div className='d-flex gap-2 align-items-center flex-wrap justify-content-center'>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ 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'
|
||||||
|
|
||||||
function PublicRead (props) {
|
function PublicRead (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)
|
||||||
@@ -17,13 +19,15 @@ function PublicRead (props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Get Last post from a author
|
// Get Last post from a author
|
||||||
const start = () => {
|
const start = () => {
|
||||||
const { nostrKeyPair } = bchWalletState
|
const pubHexData = nip19.decode(npub)
|
||||||
|
const pubHex = pubHexData.data
|
||||||
|
console.log('pubhex', pubHex)
|
||||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||||
|
|
||||||
const pool = RelayPool([psf])
|
const pool = RelayPool([psf])
|
||||||
pool.on('open', relay => {
|
pool.on('open', relay => {
|
||||||
relay.subscribe('subid', { limit: 5, kinds: [1], authors: [nostrKeyPair.pubHex] })
|
relay.subscribe('subid', { limit: 5, kinds: [1], authors: [pubHex] })
|
||||||
|
setLoaded(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
pool.on('eose', relay => {
|
pool.on('eose', relay => {
|
||||||
@@ -38,10 +42,10 @@ function PublicRead (props) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded && npub) {
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
}, [bchWalletState, loaded])
|
}, [bchWalletState, loaded, npub])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className='mt-4'>
|
<Container className='mt-4'>
|
||||||
@@ -72,7 +76,7 @@ function PublicRead (props) {
|
|||||||
</div>
|
</div>
|
||||||
<div className='mb-4'>
|
<div className='mb-4'>
|
||||||
<div className='fs-5 ms-5 text-dark' style={{ lineHeight: '1.3' }}>
|
<div className='fs-5 ms-5 text-dark' style={{ lineHeight: '1.3' }}>
|
||||||
{post.content}
|
<NostrFormat content={post.content} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
|
|||||||
@@ -85,6 +85,53 @@ function Offers (props) {
|
|||||||
console.warn('Error in getOffers() ', err)
|
console.warn('Error in getOffers() ', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const handleBuy = useCallback(async (event) => {
|
||||||
|
try {
|
||||||
|
console.log('Buy button clicked. Event: ', event)
|
||||||
|
|
||||||
|
const targetOfferEventId = event.target.id
|
||||||
|
console.log('targetOfferEventId: ', targetOfferEventId)
|
||||||
|
|
||||||
|
// Initialize modal
|
||||||
|
setShowModal(true)
|
||||||
|
setModalBody(['Generating Counter Offer...', '(This can take a couple minutes)'])
|
||||||
|
setHideSpinner(false)
|
||||||
|
setDenyClose(true)
|
||||||
|
|
||||||
|
// Generate a counter offer.
|
||||||
|
const bchDexLib = appData.dexLib
|
||||||
|
const { offerData, partialHex } = await bchDexLib.take.takeOffer(
|
||||||
|
targetOfferEventId
|
||||||
|
)
|
||||||
|
|
||||||
|
// Upload the counter offer to Nostr.
|
||||||
|
const nostr = appData.nostr
|
||||||
|
const { eventId, noteId } = await nostr.testNostrUpload({
|
||||||
|
offerData,
|
||||||
|
partialHex
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('eventId: ', eventId)
|
||||||
|
console.log('noteId: ', noteId)
|
||||||
|
|
||||||
|
// update output modal
|
||||||
|
const newModalBody = []
|
||||||
|
newModalBody.push('Success!')
|
||||||
|
newModalBody.push('What happens next:')
|
||||||
|
newModalBody.push('The money has not yet left your wallet! It is still under your control.')
|
||||||
|
newModalBody.push('If the sellers node is online, they will accept the Counter Offer you just generated in a few minutes.')
|
||||||
|
newModalBody.push('If the tokens never show up, you can sweep the funds back into your wallet.')
|
||||||
|
|
||||||
|
setModalBody(newModalBody)
|
||||||
|
setHideSpinner(true)
|
||||||
|
setDenyClose(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error in handleBuy() ', error)
|
||||||
|
setModalBody(['Buy failed: ', error.message])
|
||||||
|
setHideSpinner(true)
|
||||||
|
setDenyClose(false)
|
||||||
|
}
|
||||||
|
}, [appData]) // Dependencies: appData since it's used inside the callback
|
||||||
|
|
||||||
// Get Offer data and manipulate it for the sake of presentation.
|
// Get Offer data and manipulate it for the sake of presentation.
|
||||||
const handleOffers = useCallback(async () => {
|
const handleOffers = useCallback(async () => {
|
||||||
@@ -129,54 +176,7 @@ function Offers (props) {
|
|||||||
|
|
||||||
setOffers(formattedOffers)
|
setOffers(formattedOffers)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}, [appData])
|
}, [appData, handleBuy])
|
||||||
|
|
||||||
const handleBuy = async (event) => {
|
|
||||||
try {
|
|
||||||
console.log('Buy button clicked. Event: ', event)
|
|
||||||
|
|
||||||
const targetOfferEventId = event.target.id
|
|
||||||
console.log('targetOfferEventId: ', targetOfferEventId)
|
|
||||||
|
|
||||||
// Initialize modal
|
|
||||||
setShowModal(true)
|
|
||||||
setModalBody(['Generating Counter Offer...', '(This can take a couple minutes)'])
|
|
||||||
setHideSpinner(false)
|
|
||||||
setDenyClose(true)
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
method: 'post',
|
|
||||||
url: `${SERVER}offer/take`,
|
|
||||||
data: {
|
|
||||||
nostrEventId: targetOfferEventId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await axios.request(options)
|
|
||||||
const { eventId, noteId } = result.data
|
|
||||||
console.log(`Event Id : ${eventId}`)
|
|
||||||
console.log(`Note Id ${noteId}`)
|
|
||||||
|
|
||||||
// Add link to output
|
|
||||||
const newModalBody = []
|
|
||||||
newModalBody.push('Success!')
|
|
||||||
// newModalBody.push(<a href={`https://p2wdb.fullstack.cash/entry/hash/${p2wdbHash}`} target='_blank' rel='noreferrer'>P2WDB Entry</a>)
|
|
||||||
|
|
||||||
newModalBody.push('What happens next:')
|
|
||||||
newModalBody.push('The money has not yet left your wallet! It is still under your control.')
|
|
||||||
newModalBody.push('If the sellers node is online, they will accept the Counter Offer you just generated in a few minutes.')
|
|
||||||
newModalBody.push('If the tokens never show up, you can sweep the funds back into your wallet.')
|
|
||||||
|
|
||||||
setModalBody(newModalBody)
|
|
||||||
setHideSpinner(true)
|
|
||||||
setDenyClose(false)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Error in handleBuy() ', error)
|
|
||||||
setModalBody(['Buy failed: ', error.message])
|
|
||||||
setHideSpinner(true)
|
|
||||||
setDenyClose(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Retrieve initial offer data
|
// Retrieve initial offer data
|
||||||
|
|||||||
@@ -5,25 +5,34 @@
|
|||||||
// Global npm libraries
|
// Global npm libraries
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { Container, Row, Col, Tabs, Tab, Spinner } from 'react-bootstrap'
|
import { Container, Row, Col, Spinner, Carousel } from 'react-bootstrap'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
|
||||||
function UserDataReview (props) {
|
function UserDataReview (props) {
|
||||||
const appData = props.appData
|
const appData = props.appData
|
||||||
const [media, setMedia] = useState([])
|
const [media, setMedia] = useState([])
|
||||||
const [markdown, setMarkdown] = useState('')
|
const [markdown, setMarkdown] = useState('')
|
||||||
const [activeTab, setActiveTab] = useState('media')
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
|
const [tokenData, setTokenData] = useState(null)
|
||||||
// Get the id parameter from the URL
|
// Get the id parameter from the URL
|
||||||
const { cid } = useParams()
|
const { tokenId } = useParams()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadMedia = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true)
|
|
||||||
try {
|
try {
|
||||||
|
const tokenData = await appData.wallet.getTokenData(tokenId)
|
||||||
|
|
||||||
|
setTokenData(tokenData)
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
if (!tokenData.mutableData) throw new Error('Mutable data not found')
|
||||||
|
|
||||||
|
const cid = parseCid(tokenData.mutableData)
|
||||||
|
|
||||||
const { json } = await appData.wallet.cid2json({ cid })
|
const { json } = await appData.wallet.cid2json({ cid })
|
||||||
const userDataString = json.userData
|
const userDataString = json.userData
|
||||||
|
|
||||||
if (userDataString) {
|
if (userDataString) {
|
||||||
const userData = JSON.parse(userDataString)
|
const userData = JSON.parse(userDataString)
|
||||||
setMedia(userData.media)
|
setMedia(userData.media)
|
||||||
@@ -34,60 +43,80 @@ function UserDataReview (props) {
|
|||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
loadMedia()
|
loadData()
|
||||||
}, [cid, appData.wallet])
|
}, [tokenId, appData.wallet])
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
|
{/** Error message */}
|
||||||
{error && <p className='text-danger'>{error}</p>}
|
{error && <p className='text-danger'>{error}</p>}
|
||||||
{loading
|
|
||||||
? (
|
{/** Loading spinner */}
|
||||||
|
{loading &&
|
||||||
|
(
|
||||||
<div className='text-center my-5'>
|
<div className='text-center my-5'>
|
||||||
<Spinner animation='border' role='status' variant='primary'>
|
<Spinner animation='border' role='status' variant='primary'>
|
||||||
<span className='visually-hidden'>Loading...</span>
|
<span className='visually-hidden'>Loading...</span>
|
||||||
</Spinner>
|
</Spinner>
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
: (
|
{/** User data */}
|
||||||
<Tabs
|
{!loading && !error && (
|
||||||
activeKey={activeTab}
|
<div className='text-center'>
|
||||||
onSelect={(k) => setActiveTab(k)}
|
{/** Name */}
|
||||||
className='mb-4'
|
<h1>{tokenData.genesisData.name}</h1>
|
||||||
>
|
|
||||||
<Tab eventKey='media' title='Media'>
|
{/** Media Content Carousel */}
|
||||||
{media && media.length > 0
|
{media && media.length > 0
|
||||||
? (
|
? (
|
||||||
<Row className='mt-3'>
|
<div className='my-5 '>
|
||||||
{media.map((item, index) => (
|
<Carousel>
|
||||||
<Col key={index} xs={12} sm={6} md={4} lg={4} className='mb-3'>
|
{media.map((item, index) => (
|
||||||
<img
|
<Carousel.Item key={index}>
|
||||||
src={item.url}
|
<img
|
||||||
alt={`Review media ${index + 1}`}
|
className='d-block w-100'
|
||||||
style={{ width: '100%', height: '250px', objectFit: 'cover', borderRadius: '8px' }}
|
src={item.url}
|
||||||
/>
|
alt={`Review media ${index + 1}`}
|
||||||
</Col>
|
style={{ height: '400px', objectFit: 'contain' }}
|
||||||
))}
|
/>
|
||||||
</Row>
|
<Carousel.Caption>
|
||||||
)
|
<p>Image {index + 1} of {media.length}</p>
|
||||||
: (
|
</Carousel.Caption>
|
||||||
<p className='mt-3'>No media content available</p>
|
</Carousel.Item>
|
||||||
)}
|
))}
|
||||||
</Tab>
|
</Carousel>
|
||||||
<Tab eventKey='markdown' title='Content'>
|
<style>{'.carousel-control-next,.carousel-control-prev, .carousel-indicators {filter: invert(100%); } '}
|
||||||
{markdown
|
</style>
|
||||||
? (
|
</div>
|
||||||
<div className='markdown-content mt-3'>
|
)
|
||||||
<ReactMarkdown>{markdown}</ReactMarkdown>
|
: (
|
||||||
</div>
|
<p className='mt-3'>No media content available</p>
|
||||||
)
|
)}
|
||||||
: (
|
|
||||||
<p className='mt-3'>No content available</p>
|
{/** Markdown Content */}
|
||||||
)}
|
{markdown
|
||||||
</Tab>
|
? (
|
||||||
</Tabs>
|
<div className='markdown-content my-5 text-center'>
|
||||||
)}
|
<ReactMarkdown>{markdown}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<p className='mt-3'>No content available</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import Logo from './psf-logo.png'
|
|||||||
|
|
||||||
function NavMenu (props) {
|
function NavMenu (props) {
|
||||||
// Get the current path
|
// Get the current path
|
||||||
const { currentPath } = props.appData
|
const { currentPath, bchWalletState } = props.appData
|
||||||
|
|
||||||
// Navbar state
|
// Navbar state
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
@@ -72,11 +72,18 @@ function NavMenu (props) {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink
|
<NavLink
|
||||||
className={currentPath === '/nostr-read' ? 'nav-link-active' : 'nav-link-inactive'}
|
className={currentPath === '/nostr-read' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||||
to='/nostr-read'
|
to={`/nostr-read/${bchWalletState?.nostrKeyPair?.npub}`}
|
||||||
onClick={handleClickEvent}
|
onClick={handleClickEvent}
|
||||||
>
|
>
|
||||||
Nostr Profile
|
Nostr Profile
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
className={currentPath === '/content-creators' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||||
|
to='/content-creators'
|
||||||
|
onClick={handleClickEvent}
|
||||||
|
>
|
||||||
|
Content Creators
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -13,7 +13,8 @@ function useAppState () {
|
|||||||
ssr: true,
|
ssr: true,
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
serverUrl: 'https://free-bch.fullstack.cash' // Default server
|
serverUrl: 'https://free-bch.fullstack.cash' // Default server
|
||||||
}
|
},
|
||||||
|
nftData: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('lsState: ', lsState)
|
console.log('lsState: ', lsState)
|
||||||
@@ -38,7 +39,7 @@ function useAppState () {
|
|||||||
const [denyClose, setDenyClose] = useState(false)
|
const [denyClose, setDenyClose] = useState(false)
|
||||||
|
|
||||||
// NFTs for sale stored data to improve performance
|
// NFTs for sale stored data to improve performance
|
||||||
const [nftForSaleCacheData, setNftForSaleCacheData] = useState({})
|
const [nftForSaleCacheData, setNftForSaleCacheData] = useState(lsState.nftData || {})
|
||||||
|
|
||||||
// The wallet state makes this a true progressive web app (PWA). As
|
// The wallet state makes this a true progressive web app (PWA). As
|
||||||
// balances, UTXOs, and tokens are retrieved, this state is updated.
|
// balances, UTXOs, and tokens are retrieved, this state is updated.
|
||||||
@@ -107,6 +108,7 @@ function useAppState () {
|
|||||||
const newCacheData = Object.assign({}, cacheData, data) // Merge the new data with the cache data
|
const newCacheData = Object.assign({}, cacheData, data) // Merge the new data with the cache data
|
||||||
allCacheData[tokenId] = newCacheData // Update the cache data
|
allCacheData[tokenId] = newCacheData // Update the cache data
|
||||||
setNftForSaleCacheData(allCacheData) // Update the state
|
setNftForSaleCacheData(allCacheData) // Update the state
|
||||||
|
updateLocalStorage({ nftData: allCacheData }) // Update the local storage
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user