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 | |
|---|---|---|---|
|
|
faec078403 | ||
|
|
416941089b | ||
|
|
f4c47d4ce3 | ||
|
|
121f1001e1 | ||
|
|
e8f077dba2 | ||
|
|
3fbbdbc6a7 | ||
|
|
43013bd464 | ||
|
|
391ddb62de | ||
|
|
a7c3fc243b | ||
|
|
173cda717e | ||
|
|
e46068165f | ||
|
|
086deb4904 | ||
|
|
5386a07a62 | ||
|
|
a240fab588 | ||
|
|
0ab5702b48 | ||
|
|
97dd816330 | ||
|
|
b878939456 | ||
|
|
59cfecb485 |
Generated
+4
-4
@@ -15,7 +15,7 @@
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"axios": "0.27.2",
|
||||
"bch-dex-lib": "2.0.3",
|
||||
"bch-dex-lib": "2.2.0",
|
||||
"bch-message-lib": "2.2.1",
|
||||
"bch-nostr": "1.3.4",
|
||||
"bch-token-sweep": "2.2.1",
|
||||
@@ -10043,9 +10043,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bch-dex-lib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bch-dex-lib/-/bch-dex-lib-2.0.3.tgz",
|
||||
"integrity": "sha512-aPZIlDcRziZBRKRhEuAv6G/skFjZ28+sE30fkoUY8isUIAe3C0YS7yQMoOmGSA6N46/XuhRbfQi0SRQJfrASpA==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bch-dex-lib/-/bch-dex-lib-2.2.0.tgz",
|
||||
"integrity": "sha512-SORqn4qNmbxfL2SbbapOpbcCB+FtqasR+GT0XRifdkXFjA3aDL0gwMsHjh8IzLZnKtPYSBcfgzKQFxEsXCJNug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@chris.troutner/retry-queue": "1.0.10",
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"axios": "0.27.2",
|
||||
"bch-dex-lib": "2.0.3",
|
||||
"bch-dex-lib": "2.2.0",
|
||||
"bch-message-lib": "2.2.1",
|
||||
"bch-nostr": "1.3.4",
|
||||
"bch-token-sweep": "2.2.1",
|
||||
|
||||
@@ -19,7 +19,6 @@ 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
|
||||
@@ -31,63 +30,16 @@ function ContentCreators (props) {
|
||||
|
||||
const [followList, setFollowList] = useState([])
|
||||
|
||||
// Get the list of profiles followed by the user.
|
||||
// It aggregates all the followers from all the relays.
|
||||
const getFollowList = useCallback(async () => {
|
||||
const list = await new Promise((resolve, reject) => {
|
||||
let list = []
|
||||
const { nostrKeyPair } = appData.bchWalletState
|
||||
const { nostrKeyPair } = appData.bchWalletState
|
||||
|
||||
const pool = RelayPool(config.nostrRelays)
|
||||
// const pool = RelayPool([config.nostrRelay])
|
||||
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)
|
||||
const pubKeyHex = nostrKeyPair.pubHex
|
||||
const list = await appData.nostrQueries.getFollowList(pubKeyHex)
|
||||
setFollowList(list)
|
||||
}, [appData])
|
||||
|
||||
const loadProfile = useCallback(async (pubKey) => {
|
||||
return new Promise((resolve) => {
|
||||
// const pool = RelayPool(config.nostrRelays)
|
||||
const pool = RelayPool([config.nostrRelay])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubKey] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
console.log('Closing Relay')
|
||||
relay.close()
|
||||
resolve(false)
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
try {
|
||||
const profile = JSON.parse(ev.content)
|
||||
// console.log('profile', profile)
|
||||
resolve(profile)
|
||||
} catch (error) {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const loadCreators = async () => {
|
||||
try {
|
||||
@@ -100,7 +52,7 @@ function ContentCreators (props) {
|
||||
for (let i = 0; i < creators.length; i++) {
|
||||
try {
|
||||
const creator = creators[i]
|
||||
const profile = await loadProfile(creator.pubkey)
|
||||
const profile = await appData.nostrQueries.getProfile(creator.pubkey)
|
||||
|
||||
// the folowing lines should re-render the ContentCard
|
||||
setCreators(prevCreators => {
|
||||
@@ -121,7 +73,7 @@ function ContentCreators (props) {
|
||||
loadCreators()
|
||||
getFollowList()
|
||||
}
|
||||
}, [loaded, followList, getFollowList, loadProfile])
|
||||
}, [loaded, followList, getFollowList, appData])
|
||||
|
||||
const filteredCreators = useCallback(() => {
|
||||
if (!creators || creators.length === 0) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Component for displaying nostr posts
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Card, Spinner } from 'react-bootstrap'
|
||||
@@ -8,11 +9,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faUser, faHeart as faHeartSolid } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faHeart } from '@fortawesome/free-regular-svg-icons'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
import NostrFormat from '../nostr-format'
|
||||
import { finalizeEvent } from 'nostr-tools/pure'
|
||||
import { Relay } from 'nostr-tools/relay'
|
||||
import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
||||
import { RelayPool } from 'nostr'
|
||||
import config from '../../../../config/index.js'
|
||||
|
||||
// Local libraries
|
||||
import CopyOnClick from '../../bch-wallet/copy-on-click.js'
|
||||
import NostrFormat from '../nostr-format'
|
||||
|
||||
function FeedCard (props) {
|
||||
const { post, appData, profiles } = props
|
||||
@@ -21,7 +25,6 @@ function FeedCard (props) {
|
||||
const [profile, setProfile] = useState(profiles[post.pubkey])
|
||||
|
||||
const [npub, setNpub] = useState('')
|
||||
const [isClicked, setIsClicked] = useState(false)
|
||||
const [isLiked, setIsLiked] = useState(false)
|
||||
const [likesCount, setLikesCount] = useState(null)
|
||||
const [likesFetched, setLikesFetched] = useState(false)
|
||||
@@ -30,53 +33,19 @@ function FeedCard (props) {
|
||||
// function to fetch a post likes reaction
|
||||
const handleLikes = useCallback(async (post, userPubKey) => {
|
||||
try {
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
const res = await new Promise((resolve) => {
|
||||
let likesCnt = 0
|
||||
let userLikedCnt = 0
|
||||
const pool = RelayPool([psf])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { kinds: [7], '#e': [post.id] })
|
||||
})
|
||||
// Get all post likes events
|
||||
const likesArr = await appData.nostrQueries.getPostLikes(post.id)
|
||||
const likesCount = likesArr.length
|
||||
|
||||
pool.on('eose', relay => {
|
||||
relay.close()
|
||||
resolve({
|
||||
count: likesCnt,
|
||||
userLiked: userLikedCnt > 0
|
||||
})
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
try {
|
||||
// Count likes
|
||||
if (ev.content === '+') {
|
||||
likesCnt++
|
||||
// Count user likes
|
||||
if (ev.pubkey === userPubKey) {
|
||||
userLikedCnt++
|
||||
}
|
||||
}
|
||||
// Count dislikes
|
||||
if (ev.content === '-') {
|
||||
likesCnt--
|
||||
// Count user dislikes.
|
||||
if (ev.pubkey === userPubKey) {
|
||||
userLikedCnt--
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// skip error
|
||||
}
|
||||
})
|
||||
})
|
||||
setLikesCount(res.count)
|
||||
setIsLiked(res.userLiked)
|
||||
// Verify if the users pubkey is in the likes array
|
||||
const userLikedPost = likesArr.find(val => { return val.pubkey === userPubKey })
|
||||
setLikesCount(likesCount)
|
||||
setIsLiked(userLikedPost)
|
||||
setLikesFetched(true)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
}, [])
|
||||
}, [appData])
|
||||
|
||||
const handleProfilePictureError = () => {
|
||||
setProfilePictureError(true)
|
||||
@@ -104,13 +73,6 @@ function FeedCard (props) {
|
||||
}
|
||||
}, [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)
|
||||
@@ -126,9 +88,6 @@ function FeedCard (props) {
|
||||
// Convert private key to binary
|
||||
const privateKeyBin = hexToBytes(nostrKeyPair.privHex)
|
||||
|
||||
// Relay list
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
// Define if like or dislike
|
||||
let content = '+'
|
||||
if (isLiked) { content = '-' }
|
||||
@@ -138,27 +97,32 @@ function FeedCard (props) {
|
||||
kind: 7,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['e', post.id, psf],
|
||||
['p', psf, nostrKeyPair.pubHex]
|
||||
['e', post.id],
|
||||
['p', nostrKeyPair.pubHex]
|
||||
],
|
||||
content
|
||||
}
|
||||
console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`)
|
||||
|
||||
// Sign the post
|
||||
const signedEvent = finalizeEvent(eventTemplate, privateKeyBin)
|
||||
console.log('signedEvent: ', signedEvent)
|
||||
config.nostrRelays.map(async (relayUrl) => {
|
||||
try {
|
||||
// Sign the post
|
||||
const signedEvent = finalizeEvent(eventTemplate, privateKeyBin)
|
||||
console.log('signedEvent: ', signedEvent)
|
||||
// Connect to a relay.
|
||||
const relay = await Relay.connect(relayUrl)
|
||||
console.log(`connected to ${relay.url}`)
|
||||
|
||||
// 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)
|
||||
|
||||
// Publish the message to the relay.
|
||||
const result = await relay.publish(signedEvent)
|
||||
console.log('result: ', result)
|
||||
|
||||
// Close the connection to the relay.
|
||||
relay.close()
|
||||
// Close the connection to the relay.
|
||||
relay.close()
|
||||
} catch (err) {
|
||||
console.warn(`Skipping publishing to ${relayUrl} due to error: ${err}`)
|
||||
}
|
||||
})
|
||||
|
||||
await handleLikes(post, nostrKeyPair.pubHex)
|
||||
setLikesFetched(true)
|
||||
@@ -168,6 +132,11 @@ function FeedCard (props) {
|
||||
}
|
||||
}
|
||||
|
||||
const goToProfile = () => {
|
||||
const profileUrl = `${window.location.origin}/profile/${npub}#single-view`
|
||||
window.open(profileUrl, '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='mb-4 bg-light rounded-4 shadow-sm border-0'>
|
||||
<Card.Body className='p-3'>
|
||||
@@ -186,20 +155,21 @@ function FeedCard (props) {
|
||||
src={profile.picture}
|
||||
alt='Profile'
|
||||
className='rounded-circle w-100 h-100'
|
||||
style={{ objectFit: 'cover' }}
|
||||
style={{ objectFit: 'cover', cursor: 'pointer' }}
|
||||
onError={handleProfilePictureError}
|
||||
onLoad={handleProfilePictureLoad}
|
||||
onClick={goToProfile}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' />
|
||||
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' style={{ cursor: 'pointer' }} onClick={goToProfile} />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex-grow-1'>
|
||||
<div className='fw-bold mb-1'>
|
||||
{profile && profile.name && <span>{profile.name}</span>}
|
||||
{profile && profile.name && <span style={{ cursor: 'pointer' }} onClick={goToProfile}>{profile.name}</span>}
|
||||
{!profile?.name && (
|
||||
<span>
|
||||
<span style={{ cursor: 'pointer' }} onClick={goToProfile}>
|
||||
{post.pubkey.slice(0, 8) + '...'}
|
||||
{!profile?.loaded && <Spinner animation='border' size='sm' className='ms-2' />}
|
||||
</span>
|
||||
@@ -211,14 +181,20 @@ function FeedCard (props) {
|
||||
title='Copy to clipboard'
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
transform: isClicked ? 'scale(0.95)' : 'scale(1)',
|
||||
transition: 'transform 0.1s ease',
|
||||
display: 'inline-block'
|
||||
display: 'inline-block',
|
||||
marginRight: '5px'
|
||||
}}
|
||||
onClick={() => copyToClipboard(npub)}
|
||||
onClick={goToProfile}
|
||||
|
||||
>
|
||||
{getShortNpub(npub)}
|
||||
</span>
|
||||
<CopyOnClick
|
||||
walletProp='npub'
|
||||
appData={props.appData}
|
||||
value={npub}
|
||||
/>
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,102 +3,52 @@
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Container, Nav, Tab, Spinner } from 'react-bootstrap'
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
// Local libraries
|
||||
import Feed from './feed'
|
||||
import Following from './following'
|
||||
import config from '../../../../config'
|
||||
|
||||
function Feeds (props) {
|
||||
const { appData } = props
|
||||
const [activeTab, setActiveTab] = useState(appData.lastFeedTab)
|
||||
|
||||
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(async (pubkey) => {
|
||||
// no fetch profile again if it exist
|
||||
let hasProfileRequest = false
|
||||
setProfiles(currentProfiles => {
|
||||
if (currentProfiles[pubkey]) {
|
||||
hasProfileRequest = true
|
||||
return currentProfiles
|
||||
} else {
|
||||
const newProfiles = { ...currentProfiles }
|
||||
newProfiles[pubkey] = { loaded: false }
|
||||
return newProfiles
|
||||
}
|
||||
})
|
||||
if (hasProfileRequest) return
|
||||
// Load data on component mount.
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
// Get feeds
|
||||
const feeds = await appData.nostrQueries.getGlobalFeeds()
|
||||
setPosts(feeds)
|
||||
setLoaded(true)
|
||||
|
||||
const pool = RelayPool(config.nostrRelays)
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubkey] })
|
||||
})
|
||||
const loadedProfiles = [] // fetched profiles ( this will be used for prevent load the same profile multiple times.)
|
||||
// Map feeds and get feed owner profile.
|
||||
for (let i = 0; i < feeds.length; i++) {
|
||||
const pubKey = feeds[i].pubkey
|
||||
|
||||
pool.on('eose', relay => {
|
||||
relay.close()
|
||||
try {
|
||||
// Mark unknown profiles. From this way we can know which profile was fetched and not found.
|
||||
setProfiles(currentProfiles => {
|
||||
if (!currentProfiles[pubkey]) {
|
||||
const newProfiles = { ...currentProfiles }
|
||||
newProfiles[pubkey] = { loaded: true }
|
||||
return newProfiles
|
||||
}
|
||||
return currentProfiles
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
// skip error
|
||||
}
|
||||
})
|
||||
const exist = loadedProfiles.find((val) => { return val === pubKey })
|
||||
if (exist) { continue }
|
||||
// Fech profile.
|
||||
const profile = await appData.nostrQueries.getProfile(pubKey)
|
||||
loadedProfiles.push(pubKey) // mark as loaded
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
try {
|
||||
// update profiles data
|
||||
const profile = JSON.parse(ev.content)
|
||||
// Update profile state
|
||||
setProfiles(currentProfiles => {
|
||||
const newProfiles = { ...currentProfiles }
|
||||
newProfiles[pubkey] = profile
|
||||
newProfiles[pubKey] = profile
|
||||
return newProfiles
|
||||
})
|
||||
} catch (error) {
|
||||
// skip error
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Get global feed posts
|
||||
useEffect(() => {
|
||||
const start = () => {
|
||||
const pool = RelayPool(config.nostrRelays)
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('REQ', { limit: 10, kinds: [1], '#t': ['slpdex-socialmedia'] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
setLoaded(true)
|
||||
relay.close()
|
||||
})
|
||||
|
||||
pool.on('event', async (relay, subId, ev) => {
|
||||
setPosts(currentPosts => [...currentPosts, ev])
|
||||
// fetch post profile and set it to profiles state
|
||||
fetchProfile(ev.pubkey)
|
||||
})
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
start()
|
||||
loadData()
|
||||
}
|
||||
}, [bchWalletState, loaded, fetchProfile])
|
||||
}, [loaded, appData])
|
||||
|
||||
const onChangeTab = (tab) => {
|
||||
setActiveTab(tab)
|
||||
|
||||
@@ -9,13 +9,13 @@ 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
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../../../config'
|
||||
|
||||
function ProfilePost (props) {
|
||||
const { bchWalletState } = props.appData
|
||||
const { appData } = props
|
||||
const { bchWalletState } = appData
|
||||
const [accordionKey, setAccordionKey] = useState(null)
|
||||
const [onFetch, setOnFetch] = useState(false)
|
||||
const [formLoaded, setFormLoaded] = useState(false)
|
||||
@@ -34,40 +34,19 @@ function ProfilePost (props) {
|
||||
|
||||
useEffect(() => {
|
||||
// Get Last post from
|
||||
const getLastPost = () => {
|
||||
const { nostrKeyPair } = bchWalletState
|
||||
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const pool = RelayPool([psf])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 1, kinds: [0], authors: [nostrKeyPair.pubHex] })
|
||||
setFormLoaded(true)
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
console.log('Closing Relay')
|
||||
setFormLoaded(true)
|
||||
relay.close()
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
console.log('Received event:', ev)
|
||||
// setPosts(currentPosts => [...currentPosts, ev])
|
||||
try {
|
||||
const data = JSON.parse(ev.content)
|
||||
console.log('data', data)
|
||||
setFormData(data)
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
})
|
||||
const getLastProfilePost = async () => {
|
||||
const { nostrKeyPair } = appData.bchWalletState
|
||||
const lastProfile = await appData.nostrQueries.getProfile(nostrKeyPair.pubHex)
|
||||
if (lastProfile) {
|
||||
setFormData(lastProfile)
|
||||
}
|
||||
setFormLoaded(true)
|
||||
}
|
||||
|
||||
if (!formLoaded) {
|
||||
getLastPost()
|
||||
getLastProfilePost()
|
||||
}
|
||||
}, [bchWalletState, formLoaded])
|
||||
}, [appData, formLoaded])
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
|
||||
@@ -17,7 +17,7 @@ function Profile (props) {
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<ProfileRead {...props} setProfile={setProfile} npub={npub} />
|
||||
<ProfileRead {...props} onProfileRead={setProfile} npub={npub} />
|
||||
<PublicRead {...props} profile={profile} npub={npub} />
|
||||
<SlpTokensDisplay {...props} npub={npub} />
|
||||
<NFTForSale {...props} npub={npub} />
|
||||
|
||||
@@ -14,6 +14,7 @@ function NFTForSale (props) {
|
||||
const [offersAreLoaded, setOffersAreLoaded] = useState(false)
|
||||
const [iconsAreLoaded, setIconsAreLoaded] = useState(false)
|
||||
const [dataAreLoaded, setDataAreLoaded] = useState(false)
|
||||
const [startAsync, setStartAsync] = useState(false)
|
||||
|
||||
const getAddressByNpub = useCallback(async () => {
|
||||
const url = `${config.dexServer}/sm/npub/${npub}`
|
||||
@@ -24,6 +25,7 @@ function NFTForSale (props) {
|
||||
|
||||
// Handler for refresh button
|
||||
const handleRefresh = () => {
|
||||
console.log('handle refresh')
|
||||
loadNftOffers()
|
||||
}
|
||||
|
||||
@@ -218,9 +220,13 @@ function NFTForSale (props) {
|
||||
|
||||
// Effect to load NFTs on component mount
|
||||
useEffect(() => {
|
||||
console.log('loading nfts for sale')
|
||||
loadNftOffers()
|
||||
}, [loadNftOffers])
|
||||
// Prevent to load data twice
|
||||
if (!startAsync) {
|
||||
console.log('loading nfts for sale')
|
||||
loadNftOffers()
|
||||
setStartAsync(true)
|
||||
}
|
||||
}, [loadNftOffers, startAsync])
|
||||
|
||||
// Get Cid from url
|
||||
const parseCid = (url) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Component for read nostr information kind 0
|
||||
* Component to read nostr information kind 0 (normal posts)
|
||||
*
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Container, Button } from 'react-bootstrap'
|
||||
@@ -10,44 +12,25 @@ import { faUser, faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../../../config'
|
||||
import NostrFormat from '../nostr-format'
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
function ProfileRead (props) {
|
||||
const { npub } = props
|
||||
const { setProfile } = props
|
||||
const [post, setPost] = useState({})
|
||||
const { npub, appData } = props
|
||||
const { onProfileRead } = props
|
||||
const [profile, setProfile] = useState({})
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [imageError, setImageError] = useState({ picture: false, banner: false })
|
||||
|
||||
useEffect(() => {
|
||||
const start = () => {
|
||||
const start = async () => {
|
||||
const pubHexData = nip19.decode(npub)
|
||||
const pubHex = pubHexData.data
|
||||
|
||||
const pool = RelayPool(config.nostrRelays)
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubHex] })
|
||||
setLoaded(true)
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
console.log('Closing Relay')
|
||||
relay.close()
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
console.log('Received event:', ev)
|
||||
try {
|
||||
const profile = JSON.parse(ev.content)
|
||||
console.log('Profile:', profile)
|
||||
setPost(profile)
|
||||
setProfile(profile)
|
||||
} catch (error) {
|
||||
console.error('Error parsing profile:', error)
|
||||
}
|
||||
})
|
||||
const profile = await appData.nostrQueries.getProfile(pubHex)
|
||||
if (profile) {
|
||||
onProfileRead(profile)
|
||||
setProfile(profile)
|
||||
}
|
||||
|
||||
setLoaded(true)
|
||||
}
|
||||
@@ -55,7 +38,7 @@ function ProfileRead (props) {
|
||||
if (!loaded && npub) {
|
||||
start()
|
||||
}
|
||||
}, [loaded, setProfile, npub])
|
||||
}, [loaded, onProfileRead, npub, appData])
|
||||
|
||||
const handleImageError = (type) => {
|
||||
setImageError(prev => ({ ...prev, [type]: true }))
|
||||
@@ -68,10 +51,10 @@ function ProfileRead (props) {
|
||||
return (
|
||||
<Container>
|
||||
{/* Banner Section */}
|
||||
{post.banner && !imageError.banner && (
|
||||
{profile.banner && !imageError.banner && (
|
||||
<div className='position-relative'>
|
||||
<img
|
||||
src={post.banner}
|
||||
src={profile.banner}
|
||||
alt='Profile banner'
|
||||
className='w-100 rounded-4 shadow-sm'
|
||||
style={{ height: '200px', objectFit: 'cover' }}
|
||||
@@ -84,10 +67,10 @@ function ProfileRead (props) {
|
||||
<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'>
|
||||
{/* Profile Picture */}
|
||||
<div style={{ width: '120px', height: '120px' }} className='mb-3 mb-md-0'>
|
||||
{post.picture && !imageError.picture
|
||||
{profile.picture && !imageError.picture
|
||||
? (
|
||||
<img
|
||||
src={post.picture}
|
||||
src={profile.picture}
|
||||
alt='Profile'
|
||||
className='rounded-circle shadow w-100 h-100'
|
||||
style={{ objectFit: 'cover' }}
|
||||
@@ -111,7 +94,7 @@ function ProfileRead (props) {
|
||||
|
||||
{/* Profile Information */}
|
||||
<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 || 'username'}</h3>
|
||||
<h3 className='mb-2 fw-bold'>{profile.name || 'username'}</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'>
|
||||
@@ -126,24 +109,24 @@ function ProfileRead (props) {
|
||||
</div>
|
||||
|
||||
{/* About Section */}
|
||||
{post.about && (
|
||||
{profile.about && (
|
||||
<div className='fs-6 text-secondary'>
|
||||
<NostrFormat content={post.about} />
|
||||
<NostrFormat content={profile.about} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Website Link */}
|
||||
{post.website && (
|
||||
{profile.website && (
|
||||
<div className='mb-3 d-flex align-items-center gap-2'>
|
||||
<FontAwesomeIcon icon={faGlobe} className='text-muted' size='sm' />
|
||||
<a
|
||||
href={post.website.startsWith('http') ? post.website : `https://${post.website}`}
|
||||
href={profile.website.startsWith('http') ? profile.website : `https://${profile.website}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-decoration-none text-muted small'
|
||||
style={{ wordBreak: 'break-all' }}
|
||||
>
|
||||
{post.website}
|
||||
{profile.website}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -6,49 +6,30 @@ 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'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../../../config'
|
||||
import NostrFormat from '../nostr-format'
|
||||
|
||||
function PublicRead (props) {
|
||||
const { npub } = props
|
||||
const { bchWalletState } = props.appData
|
||||
const { npub, appData } = props
|
||||
const { bchWalletState } = appData
|
||||
const [posts, setPosts] = useState([])
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [profilePictureError, setProfilePictureError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Get Last post from a author
|
||||
const start = () => {
|
||||
const pubHexData = nip19.decode(npub)
|
||||
const pubHex = pubHexData.data
|
||||
console.log('pubhex', pubHex)
|
||||
|
||||
const pool = RelayPool(config.nostrRelays)
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 5, kinds: [1], authors: [pubHex] })
|
||||
setLoaded(true)
|
||||
})
|
||||
|
||||
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])
|
||||
})
|
||||
const start = async () => {
|
||||
const pubKeyHex = appData.nostrQueries.npubToHex(npub)
|
||||
const feeds = await appData.nostrQueries.getUserFeeds(pubKeyHex)
|
||||
setPosts(feeds)
|
||||
setLoaded(true)
|
||||
}
|
||||
|
||||
if (!loaded && npub) {
|
||||
start()
|
||||
}
|
||||
}, [bchWalletState, loaded, npub])
|
||||
}, [bchWalletState, loaded, npub, appData])
|
||||
|
||||
const handleProfilePictureError = () => {
|
||||
setProfilePictureError(true)
|
||||
|
||||
+3
-1
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
// import { useQueryParam, StringParam } from 'use-query-params'
|
||||
import useLocalStorageState from 'use-local-storage-state'
|
||||
import AppUtil from '../util'
|
||||
import NostrQueries from '../services/nostr-queries'
|
||||
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
@@ -156,7 +157,8 @@ function useAppState () {
|
||||
lastFeedTab,
|
||||
setLastFeedTab,
|
||||
isSingleView,
|
||||
setIsSingleView
|
||||
setIsSingleView,
|
||||
nostrQueries: new NostrQueries()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Nostr class for query into relay pools
|
||||
*
|
||||
*/
|
||||
import config from '../config'
|
||||
import { RelayPool } from 'nostr'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
|
||||
export default class NostrQueries {
|
||||
constructor () {
|
||||
this.relays = config.nostrRelays
|
||||
}
|
||||
|
||||
npubToHex (npub) {
|
||||
const pubHexData = nip19.decode(npub)
|
||||
const pubHex = pubHexData.data
|
||||
return pubHex
|
||||
}
|
||||
|
||||
// Load profile from nostr relays
|
||||
// It uses multiple relays. It will exit after the first successful retrieval
|
||||
// from any relay. If one relay fails, it will move on to the next one.
|
||||
async getProfile (pubHex) {
|
||||
try {
|
||||
for (let i = 0; i < this.relays.length; i++) {
|
||||
const profile = await new Promise((resolve) => {
|
||||
const relay = this.relays[i]
|
||||
// const pool = RelayPool(config.nostrRelays)
|
||||
const pool = RelayPool([relay])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubHex] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
relay.close()
|
||||
resolve(false)
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
try {
|
||||
const profile = JSON.parse(ev.content)
|
||||
// console.log('profile', profile)
|
||||
console.log(`Profile found for ${pubHex} at ${relay.url}`)
|
||||
resolve(profile)
|
||||
} catch (error) {
|
||||
resolve(false)
|
||||
}
|
||||
relay.close()
|
||||
})
|
||||
})
|
||||
// Stop looking for profile if found
|
||||
if (profile) {
|
||||
return profile
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Get Feeds by user pubkey
|
||||
async getUserFeeds (pubHex) {
|
||||
try {
|
||||
let feeds = await new Promise((resolve) => {
|
||||
let list = []
|
||||
let closedRelays = 0
|
||||
|
||||
const pool = RelayPool(this.relays)
|
||||
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 5, kinds: [1], authors: [pubHex] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
relay.close()
|
||||
closedRelays++
|
||||
// Resolve list if all relays are closed
|
||||
if (closedRelays === config.nostrRelays.length) {
|
||||
resolve(list)
|
||||
}
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
list = [...list, ev]
|
||||
})
|
||||
})
|
||||
// Remove duplicated feeds
|
||||
feeds = feeds.filter((val, i, list) => {
|
||||
const existingIndex = list.findIndex(value => value.id === val.id)
|
||||
return existingIndex === i
|
||||
})
|
||||
|
||||
// Sort from newest to oldest
|
||||
feeds.sort((a, b) => b.created_at - a.created_at)
|
||||
|
||||
return feeds
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Get global feeds
|
||||
async getGlobalFeeds () {
|
||||
try {
|
||||
let feeds = await new Promise((resolve, reject) => {
|
||||
let list = []
|
||||
let closedRelays = 0
|
||||
|
||||
const pool = RelayPool(config.nostrRelays)
|
||||
// const pool = RelayPool([config.nostrRelay])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('REQ', { limit: 10, kinds: [1], '#t': ['slpdex-socialmedia'] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
relay.close()
|
||||
closedRelays++
|
||||
// Resolve list if all relays are closed
|
||||
if (closedRelays === config.nostrRelays.length) {
|
||||
resolve(list)
|
||||
}
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
// console.log('post retrieved from ', relay.url, ev.sig)
|
||||
list = [...list, ev]
|
||||
})
|
||||
})
|
||||
|
||||
// Remove duplicated feeds
|
||||
feeds = feeds.filter((val, i, list) => {
|
||||
const existingIndex = list.findIndex(value => value.id === val.id)
|
||||
return existingIndex === i
|
||||
})
|
||||
|
||||
// Sort from newest to oldest
|
||||
feeds.sort((a, b) => b.created_at - a.created_at)
|
||||
|
||||
// console.log('feeds', feeds)
|
||||
|
||||
return feeds
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Get follow list by pubkey
|
||||
async getFollowList (pubHex) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let list = []
|
||||
let closedRelays = 0
|
||||
|
||||
const pool = RelayPool(config.nostrRelays)
|
||||
// const pool = RelayPool([config.nostrRelay])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 1, kinds: [3], authors: [pubHex] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
relay.close()
|
||||
closedRelays++
|
||||
// Resolve list if all relays are closed
|
||||
if (closedRelays === this.relays.length) {
|
||||
resolve(list)
|
||||
}
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
// console.log('Received event:', ev)
|
||||
// Merge list received from all relays
|
||||
list = [...list, ...ev.tags]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Get event likes
|
||||
async getPostLikes (postId) {
|
||||
try {
|
||||
let likesRes = await new Promise((resolve) => {
|
||||
const likes = []
|
||||
let closedRelays = 0
|
||||
|
||||
const pool = RelayPool(this.relays)
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { kinds: [7], '#e': [postId] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
relay.close()
|
||||
closedRelays++
|
||||
// Resolve list if all relays are closed
|
||||
if (closedRelays === this.relays.length) {
|
||||
resolve(likes)
|
||||
}
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
try {
|
||||
// Count likes
|
||||
if (ev.content === '+' || ev.content === '-') {
|
||||
likes.push(ev)
|
||||
}
|
||||
} catch (error) {
|
||||
// skip error
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Remove duplicated events
|
||||
likesRes = likesRes.filter((val, i, list) => {
|
||||
const existingIndex = list.findIndex(value => value.id === val.id)
|
||||
return existingIndex === i
|
||||
})
|
||||
// Get likes
|
||||
const likesArr = likesRes.filter((val, i, list) => {
|
||||
return val.content === '+'
|
||||
})
|
||||
|
||||
// Get dislikes
|
||||
const dislikesArr = likesRes.filter((val, i, list) => {
|
||||
return val.content === '-'
|
||||
})
|
||||
// For every user dislike remove a user like from the array
|
||||
for (let i = 0; i < dislikesArr.length; i++) {
|
||||
const disLike = dislikesArr[i]
|
||||
const likeExist = likesArr.findIndex(val => val.pubkey === disLike.pubkey)
|
||||
if (likeExist >= 0) likesArr.splice(likeExist, 1)
|
||||
}
|
||||
// Return array of likes.
|
||||
return likesArr
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user