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 | |
|---|---|---|---|
|
|
29ce96ae92 | ||
|
|
561ffbaaad | ||
|
|
2141f919dd | ||
|
|
3087533aff |
@@ -41,6 +41,9 @@
|
||||
width: auto!important;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Remove input number arrows Chrome, Safari, Edge, Opera */
|
||||
input::-webkit-outer-spin-button,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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'
|
||||
|
||||
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' }}>
|
||||
{post.content}
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default FeedCard
|
||||
@@ -2,18 +2,53 @@
|
||||
* 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 React, { useEffect, useState, useCallback } from 'react'
|
||||
import { Container, Spinner } from 'react-bootstrap'
|
||||
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
import FeedCard from './feed-card'
|
||||
|
||||
function Feed (props) {
|
||||
const { bchWalletState } = props.appData
|
||||
const { appData } = props
|
||||
const { bchWalletState } = appData
|
||||
const [posts, setPosts] = useState([])
|
||||
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(() => {
|
||||
const start = () => {
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
@@ -29,54 +64,25 @@ function Feed (props) {
|
||||
relay.close()
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
pool.on('event', async (relay, subId, ev) => {
|
||||
console.log('Received event:', ev)
|
||||
// const profile = JSON.parse(ev.content)
|
||||
|
||||
setPosts(currentPosts => [...currentPosts, ev])
|
||||
// fetch post profile and set it to profiles state
|
||||
fetchProfile(ev.pubkey)
|
||||
})
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
start()
|
||||
}
|
||||
}, [bchWalletState, loaded])
|
||||
}, [bchWalletState, loaded, fetchProfile])
|
||||
|
||||
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>
|
||||
<FeedCard key={index} post={post} appData={props.appData} profiles={profiles} />
|
||||
))}
|
||||
{!posts.length && loaded && (
|
||||
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
|
||||
|
||||
@@ -85,6 +85,53 @@ function Offers (props) {
|
||||
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.
|
||||
const handleOffers = useCallback(async () => {
|
||||
@@ -129,54 +176,7 @@ function Offers (props) {
|
||||
|
||||
setOffers(formattedOffers)
|
||||
setIsLoading(false)
|
||||
}, [appData])
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}, [appData, handleBuy])
|
||||
|
||||
useEffect(() => {
|
||||
// Retrieve initial offer data
|
||||
|
||||
Reference in New Issue
Block a user