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 | |
|---|---|---|---|
|
|
f4c47d4ce3 | ||
|
|
121f1001e1 | ||
|
|
e8f077dba2 | ||
|
|
3fbbdbc6a7 | ||
|
|
43013bd464 | ||
|
|
391ddb62de |
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Component for displaying nostr posts
|
* Component for displaying nostr posts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Global npm libraries
|
// Global npm libraries
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { Card, Spinner } from 'react-bootstrap'
|
import { Card, Spinner } from 'react-bootstrap'
|
||||||
@@ -8,12 +9,15 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { faUser, faHeart as faHeartSolid } from '@fortawesome/free-solid-svg-icons'
|
import { faUser, faHeart as faHeartSolid } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faHeart } from '@fortawesome/free-regular-svg-icons'
|
import { faHeart } from '@fortawesome/free-regular-svg-icons'
|
||||||
import * as nip19 from 'nostr-tools/nip19'
|
import * as nip19 from 'nostr-tools/nip19'
|
||||||
import NostrFormat from '../nostr-format'
|
|
||||||
import { finalizeEvent } from 'nostr-tools/pure'
|
import { finalizeEvent } from 'nostr-tools/pure'
|
||||||
import { Relay } from 'nostr-tools/relay'
|
import { Relay } from 'nostr-tools/relay'
|
||||||
import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
||||||
import { RelayPool } from 'nostr'
|
import { RelayPool } from 'nostr'
|
||||||
|
|
||||||
|
// Local libraries
|
||||||
|
import CopyOnClick from '../../bch-wallet/copy-on-click.js'
|
||||||
|
import NostrFormat from '../nostr-format'
|
||||||
|
|
||||||
function FeedCard (props) {
|
function FeedCard (props) {
|
||||||
const { post, appData, profiles } = props
|
const { post, appData, profiles } = props
|
||||||
const { nostrKeyPair } = appData.bchWalletState
|
const { nostrKeyPair } = appData.bchWalletState
|
||||||
@@ -21,7 +25,6 @@ function FeedCard (props) {
|
|||||||
const [profile, setProfile] = useState(profiles[post.pubkey])
|
const [profile, setProfile] = useState(profiles[post.pubkey])
|
||||||
|
|
||||||
const [npub, setNpub] = useState('')
|
const [npub, setNpub] = useState('')
|
||||||
const [isClicked, setIsClicked] = useState(false)
|
|
||||||
const [isLiked, setIsLiked] = useState(false)
|
const [isLiked, setIsLiked] = useState(false)
|
||||||
const [likesCount, setLikesCount] = useState(null)
|
const [likesCount, setLikesCount] = useState(null)
|
||||||
const [likesFetched, setLikesFetched] = useState(false)
|
const [likesFetched, setLikesFetched] = useState(false)
|
||||||
@@ -104,13 +107,6 @@ function FeedCard (props) {
|
|||||||
}
|
}
|
||||||
}, [profiles, post])
|
}, [profiles, post])
|
||||||
|
|
||||||
// copy to clipboard
|
|
||||||
const copyToClipboard = useCallback((value) => {
|
|
||||||
setIsClicked(true)
|
|
||||||
appData.appUtil.copyToClipboard(value)
|
|
||||||
setTimeout(() => setIsClicked(false), 200)
|
|
||||||
}, [appData])
|
|
||||||
|
|
||||||
// get short npub
|
// get short npub
|
||||||
const getShortNpub = useCallback((npub) => {
|
const getShortNpub = useCallback((npub) => {
|
||||||
return npub.slice(0, 8) + '...' + npub.slice(-5)
|
return npub.slice(0, 8) + '...' + npub.slice(-5)
|
||||||
@@ -168,6 +164,11 @@ function FeedCard (props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goToProfile = () => {
|
||||||
|
const profileUrl = `${window.location.origin}/profile/${npub}#single-view`
|
||||||
|
window.open(profileUrl, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='mb-4 bg-light rounded-4 shadow-sm border-0'>
|
<Card className='mb-4 bg-light rounded-4 shadow-sm border-0'>
|
||||||
<Card.Body className='p-3'>
|
<Card.Body className='p-3'>
|
||||||
@@ -186,20 +187,21 @@ function FeedCard (props) {
|
|||||||
src={profile.picture}
|
src={profile.picture}
|
||||||
alt='Profile'
|
alt='Profile'
|
||||||
className='rounded-circle w-100 h-100'
|
className='rounded-circle w-100 h-100'
|
||||||
style={{ objectFit: 'cover' }}
|
style={{ objectFit: 'cover', cursor: 'pointer' }}
|
||||||
onError={handleProfilePictureError}
|
onError={handleProfilePictureError}
|
||||||
onLoad={handleProfilePictureLoad}
|
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>
|
||||||
<div className='flex-grow-1'>
|
<div className='flex-grow-1'>
|
||||||
<div className='fw-bold mb-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 && (
|
{!profile?.name && (
|
||||||
<span>
|
<span style={{ cursor: 'pointer' }} onClick={goToProfile}>
|
||||||
{post.pubkey.slice(0, 8) + '...'}
|
{post.pubkey.slice(0, 8) + '...'}
|
||||||
{!profile?.loaded && <Spinner animation='border' size='sm' className='ms-2' />}
|
{!profile?.loaded && <Spinner animation='border' size='sm' className='ms-2' />}
|
||||||
</span>
|
</span>
|
||||||
@@ -211,14 +213,20 @@ function FeedCard (props) {
|
|||||||
title='Copy to clipboard'
|
title='Copy to clipboard'
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transform: isClicked ? 'scale(0.95)' : 'scale(1)',
|
|
||||||
transition: 'transform 0.1s ease',
|
transition: 'transform 0.1s ease',
|
||||||
display: 'inline-block'
|
display: 'inline-block',
|
||||||
|
marginRight: '5px'
|
||||||
}}
|
}}
|
||||||
onClick={() => copyToClipboard(npub)}
|
onClick={goToProfile}
|
||||||
|
|
||||||
>
|
>
|
||||||
{getShortNpub(npub)}
|
{getShortNpub(npub)}
|
||||||
</span>
|
</span>
|
||||||
|
<CopyOnClick
|
||||||
|
walletProp='npub'
|
||||||
|
appData={props.appData}
|
||||||
|
value={npub}
|
||||||
|
/>
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Component for reading the nostr feeds.
|
Component for reading the nostr feeds.
|
||||||
|
|
||||||
TODO:
|
|
||||||
- fetchProfile() should retrieve a profile from multiple relays. If the first relay returns a profile,
|
|
||||||
then that profile can be used and promise resolved. If the first relay returns no profile, the next
|
|
||||||
one should be tried until all relays are exhausted or one returns a profile.
|
|
||||||
|
|
||||||
- useEffect() retrieves the feeds. This should cycle through each relay and posts from each one.
|
|
||||||
Once each relays has been tried, the posts should remove duplicate entries. Finally posts should
|
|
||||||
be sorted by date.
|
|
||||||
|
|
||||||
- Clicking on a profile picture, name, or npub should open the profile for that user in a new tab.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Global npm libraries
|
// Global npm libraries
|
||||||
@@ -27,91 +16,121 @@ function Feeds (props) {
|
|||||||
const { appData } = props
|
const { appData } = props
|
||||||
const [activeTab, setActiveTab] = useState(appData.lastFeedTab)
|
const [activeTab, setActiveTab] = useState(appData.lastFeedTab)
|
||||||
|
|
||||||
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({})
|
const [profiles, setProfiles] = useState({})
|
||||||
|
|
||||||
// function to fetch profile and set it to profiles state
|
// Load profile from nostr relays
|
||||||
const fetchProfile = useCallback(async (pubkey) => {
|
// It uses multiple relays. It will exit after the first successful retrieval
|
||||||
// no fetch profile again if it exist
|
// from any relay. If one relay fails, it will move on to the next one.
|
||||||
let hasProfileRequest = false
|
const fetchProfile = useCallback(async (pubKey) => {
|
||||||
setProfiles(currentProfiles => {
|
// Looking for the profile in each relay sequentially
|
||||||
if (currentProfiles[pubkey]) {
|
for (let i = 0; i < config.nostrRelays.length; i++) {
|
||||||
hasProfileRequest = true
|
const profile = await new Promise((resolve) => {
|
||||||
return currentProfiles
|
const relay = config.nostrRelays[i]
|
||||||
} else {
|
// const pool = RelayPool(config.nostrRelays)
|
||||||
const newProfiles = { ...currentProfiles }
|
const pool = RelayPool([relay])
|
||||||
newProfiles[pubkey] = { loaded: false }
|
pool.on('open', relay => {
|
||||||
return newProfiles
|
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubKey] })
|
||||||
}
|
})
|
||||||
})
|
|
||||||
if (hasProfileRequest) return
|
|
||||||
|
|
||||||
// const pool = RelayPool(config.nostrRelays)
|
pool.on('eose', relay => {
|
||||||
const pool = RelayPool([config.nostrRelay])
|
console.log('Closing Relay')
|
||||||
pool.on('open', relay => {
|
relay.close()
|
||||||
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubkey] })
|
resolve(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
pool.on('eose', relay => {
|
pool.on('event', (relay, subId, ev) => {
|
||||||
relay.close()
|
try {
|
||||||
try {
|
const profile = JSON.parse(ev.content)
|
||||||
// Mark unknown profiles. From this way we can know which profile was fetched and not found.
|
// console.log('profile', profile)
|
||||||
setProfiles(currentProfiles => {
|
console.log(`Profile found for ${pubKey} at ${relay.url}`)
|
||||||
if (!currentProfiles[pubkey]) {
|
resolve(profile)
|
||||||
const newProfiles = { ...currentProfiles }
|
} catch (error) {
|
||||||
newProfiles[pubkey] = { loaded: true }
|
resolve(false)
|
||||||
return newProfiles
|
|
||||||
}
|
}
|
||||||
return currentProfiles
|
relay.close()
|
||||||
})
|
})
|
||||||
} catch (error) {
|
})
|
||||||
console.warn(error)
|
// Stop looking for profile if found
|
||||||
// skip error
|
if (profile) {
|
||||||
|
return profile
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
pool.on('event', (relay, subId, ev) => {
|
|
||||||
try {
|
|
||||||
// update profiles data
|
|
||||||
const profile = JSON.parse(ev.content)
|
|
||||||
setProfiles(currentProfiles => {
|
|
||||||
const newProfiles = { ...currentProfiles }
|
|
||||||
newProfiles[pubkey] = profile
|
|
||||||
return newProfiles
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
// skip error
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Get global feed posts
|
// Get array of feeds from relays
|
||||||
useEffect(() => {
|
const fetchFeeds = useCallback(async () => {
|
||||||
const start = () => {
|
let feeds = await new Promise((resolve, reject) => {
|
||||||
// const pool = RelayPool(config.nostrRelays)
|
let list = []
|
||||||
const pool = RelayPool([config.nostrRelay])
|
let closedRelays = 0
|
||||||
|
|
||||||
|
const pool = RelayPool(config.nostrRelays)
|
||||||
|
// const pool = RelayPool([config.nostrRelay])
|
||||||
pool.on('open', relay => {
|
pool.on('open', relay => {
|
||||||
relay.subscribe('REQ', { limit: 10, kinds: [1], '#t': ['slpdex-socialmedia'] })
|
relay.subscribe('REQ', { limit: 10, kinds: [1], '#t': ['slpdex-socialmedia'] })
|
||||||
})
|
})
|
||||||
|
|
||||||
pool.on('eose', relay => {
|
pool.on('eose', relay => {
|
||||||
setLoaded(true)
|
|
||||||
relay.close()
|
relay.close()
|
||||||
|
closedRelays++
|
||||||
|
// Resolve list if all relays are closed
|
||||||
|
if (closedRelays === config.nostrRelays.length) {
|
||||||
|
resolve(list)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
pool.on('event', async (relay, subId, ev) => {
|
pool.on('event', (relay, subId, ev) => {
|
||||||
setPosts(currentPosts => [...currentPosts, ev])
|
// console.log('post retrieved from ', relay.url, ev.sig)
|
||||||
// fetch post profile and set it to profiles state
|
list = [...list, ev]
|
||||||
fetchProfile(ev.pubkey)
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
setPosts(feeds)
|
||||||
|
return feeds
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Load data on component mount.
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
// Get feeds
|
||||||
|
const feeds = await fetchFeeds()
|
||||||
|
setLoaded(true)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
const exist = loadedProfiles.find((val) => { return val === pubKey })
|
||||||
|
if (exist) { continue }
|
||||||
|
// Fech profile.
|
||||||
|
const profile = await fetchProfile(pubKey)
|
||||||
|
loadedProfiles.push(pubKey) // mark as loaded
|
||||||
|
|
||||||
|
// Update profile state
|
||||||
|
setProfiles(currentProfiles => {
|
||||||
|
const newProfiles = { ...currentProfiles }
|
||||||
|
newProfiles[pubKey] = profile
|
||||||
|
return newProfiles
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
start()
|
loadData()
|
||||||
}
|
}
|
||||||
}, [bchWalletState, loaded, fetchProfile])
|
}, [loaded, fetchFeeds, fetchProfile])
|
||||||
|
|
||||||
const onChangeTab = (tab) => {
|
const onChangeTab = (tab) => {
|
||||||
setActiveTab(tab)
|
setActiveTab(tab)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function Profile (props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container>
|
<Container>
|
||||||
<ProfileRead {...props} setProfile={setProfile} npub={npub} />
|
<ProfileRead {...props} onProfileRead={setProfile} npub={npub} />
|
||||||
<PublicRead {...props} profile={profile} npub={npub} />
|
<PublicRead {...props} profile={profile} npub={npub} />
|
||||||
<SlpTokensDisplay {...props} npub={npub} />
|
<SlpTokensDisplay {...props} npub={npub} />
|
||||||
<NFTForSale {...props} npub={npub} />
|
<NFTForSale {...props} npub={npub} />
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Component to read nostr information kind 0 (normal posts)
|
* Component to read nostr information kind 0 (normal posts)
|
||||||
*
|
*
|
||||||
* TODO:
|
|
||||||
* - Currently feeds are retrieved from only one relay. It should retrieve Kind
|
|
||||||
* 0 posts from all relays. It should then remove any duplicate entries.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Global npm libraries
|
// Global npm libraries
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState, useCallback } from 'react'
|
||||||
import { Container, Button } from 'react-bootstrap'
|
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'
|
||||||
@@ -21,47 +18,66 @@ import { RelayPool } from 'nostr'
|
|||||||
|
|
||||||
function ProfileRead (props) {
|
function ProfileRead (props) {
|
||||||
const { npub } = props
|
const { npub } = props
|
||||||
const { setProfile } = props
|
const { onProfileRead } = props
|
||||||
const [post, setPost] = useState({})
|
const [profile, setProfile] = useState({})
|
||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
const [imageError, setImageError] = useState({ picture: false, banner: false })
|
const [imageError, setImageError] = useState({ picture: false, banner: false })
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
const fetchProfile = useCallback(async (pubKey) => {
|
||||||
|
console.log('fetching profile for pubkey ', pubKey)
|
||||||
|
// Looking for the profile in each relay sequentially
|
||||||
|
for (let i = 0; i < config.nostrRelays.length; i++) {
|
||||||
|
const profile = await new Promise((resolve) => {
|
||||||
|
const relay = config.nostrRelays[i]
|
||||||
|
// const pool = RelayPool(config.nostrRelays)
|
||||||
|
const pool = RelayPool([relay])
|
||||||
|
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)
|
||||||
|
console.log(`Profile found for ${pubKey} at ${relay.url}`)
|
||||||
|
resolve(profile)
|
||||||
|
} catch (error) {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
relay.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Stop looking for profile if found
|
||||||
|
if (profile) {
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const start = () => {
|
const start = async () => {
|
||||||
const pubHexData = nip19.decode(npub)
|
const pubHexData = nip19.decode(npub)
|
||||||
const pubHex = pubHexData.data
|
const pubHex = pubHexData.data
|
||||||
|
|
||||||
// const pool = RelayPool(config.nostrRelays)
|
const profile = await fetchProfile(pubHex)
|
||||||
const pool = new RelayPool([config.nostrRelay])
|
onProfileRead(profile)
|
||||||
pool.on('open', relay => {
|
setProfile(profile)
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setLoaded(true)
|
setLoaded(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded && npub) {
|
if (!loaded && npub) {
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
}, [loaded, setProfile, npub])
|
}, [loaded, onProfileRead, npub, fetchProfile])
|
||||||
|
|
||||||
const handleImageError = (type) => {
|
const handleImageError = (type) => {
|
||||||
setImageError(prev => ({ ...prev, [type]: true }))
|
setImageError(prev => ({ ...prev, [type]: true }))
|
||||||
@@ -74,10 +90,10 @@ function ProfileRead (props) {
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{/* Banner Section */}
|
{/* Banner Section */}
|
||||||
{post.banner && !imageError.banner && (
|
{profile.banner && !imageError.banner && (
|
||||||
<div className='position-relative'>
|
<div className='position-relative'>
|
||||||
<img
|
<img
|
||||||
src={post.banner}
|
src={profile.banner}
|
||||||
alt='Profile banner'
|
alt='Profile banner'
|
||||||
className='w-100 rounded-4 shadow-sm'
|
className='w-100 rounded-4 shadow-sm'
|
||||||
style={{ height: '200px', objectFit: 'cover' }}
|
style={{ height: '200px', objectFit: 'cover' }}
|
||||||
@@ -90,10 +106,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'>
|
<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 */}
|
{/* Profile Picture */}
|
||||||
<div style={{ width: '120px', height: '120px' }} className='mb-3 mb-md-0'>
|
<div style={{ width: '120px', height: '120px' }} className='mb-3 mb-md-0'>
|
||||||
{post.picture && !imageError.picture
|
{profile.picture && !imageError.picture
|
||||||
? (
|
? (
|
||||||
<img
|
<img
|
||||||
src={post.picture}
|
src={profile.picture}
|
||||||
alt='Profile'
|
alt='Profile'
|
||||||
className='rounded-circle shadow w-100 h-100'
|
className='rounded-circle shadow w-100 h-100'
|
||||||
style={{ objectFit: 'cover' }}
|
style={{ objectFit: 'cover' }}
|
||||||
@@ -117,7 +133,7 @@ function ProfileRead (props) {
|
|||||||
|
|
||||||
{/* Profile Information */}
|
{/* 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'>
|
<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'>
|
<div className='text-muted small mb-3 d-flex align-items-center flex-column flex-md-row'>
|
||||||
<span className='text-truncate me-2 mb-2 mb-md-0'>
|
<span className='text-truncate me-2 mb-2 mb-md-0'>
|
||||||
@@ -132,24 +148,24 @@ function ProfileRead (props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* About Section */}
|
{/* About Section */}
|
||||||
{post.about && (
|
{profile.about && (
|
||||||
<div className='fs-6 text-secondary'>
|
<div className='fs-6 text-secondary'>
|
||||||
<NostrFormat content={post.about} />
|
<NostrFormat content={profile.about} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Website Link */}
|
{/* Website Link */}
|
||||||
{post.website && (
|
{profile.website && (
|
||||||
<div className='mb-3 d-flex align-items-center gap-2'>
|
<div className='mb-3 d-flex align-items-center gap-2'>
|
||||||
<FontAwesomeIcon icon={faGlobe} className='text-muted' size='sm' />
|
<FontAwesomeIcon icon={faGlobe} className='text-muted' size='sm' />
|
||||||
<a
|
<a
|
||||||
href={post.website.startsWith('http') ? post.website : `https://${post.website}`}
|
href={profile.website.startsWith('http') ? profile.website : `https://${profile.website}`}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='text-decoration-none text-muted small'
|
className='text-decoration-none text-muted small'
|
||||||
style={{ wordBreak: 'break-all' }}
|
style={{ wordBreak: 'break-all' }}
|
||||||
>
|
>
|
||||||
{post.website}
|
{profile.website}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Component for read nostr information kind 1
|
* Component for read nostr information kind 1
|
||||||
*/
|
*/
|
||||||
// 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, 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'
|
||||||
@@ -20,35 +20,60 @@ function PublicRead (props) {
|
|||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
const [profilePictureError, setProfilePictureError] = useState(false)
|
const [profilePictureError, setProfilePictureError] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
const getUserFeeds = useCallback(async () => {
|
||||||
// Get Last post from a author
|
let feeds = await new Promise((resolve) => {
|
||||||
const start = () => {
|
let list = []
|
||||||
|
let closedRelays = 0
|
||||||
const pubHexData = nip19.decode(npub)
|
const pubHexData = nip19.decode(npub)
|
||||||
const pubHex = pubHexData.data
|
const pubHex = pubHexData.data
|
||||||
console.log('pubhex', pubHex)
|
console.log('pubhex', pubHex)
|
||||||
|
|
||||||
const pool = RelayPool(config.nostrRelays)
|
const pool = RelayPool(config.nostrRelays)
|
||||||
|
|
||||||
pool.on('open', relay => {
|
pool.on('open', relay => {
|
||||||
relay.subscribe('subid', { limit: 5, kinds: [1], authors: [pubHex] })
|
relay.subscribe('subid', { limit: 5, kinds: [1], authors: [pubHex] })
|
||||||
setLoaded(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
pool.on('eose', relay => {
|
pool.on('eose', relay => {
|
||||||
console.log('Closing Relay')
|
console.log('Closing Relay')
|
||||||
setLoaded(true)
|
|
||||||
relay.close()
|
relay.close()
|
||||||
|
closedRelays++
|
||||||
|
// Resolve list if all relays are closed
|
||||||
|
if (closedRelays === config.nostrRelays.length) {
|
||||||
|
resolve(list)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
pool.on('event', (relay, subId, ev) => {
|
pool.on('event', (relay, subId, ev) => {
|
||||||
console.log('Received event:', ev)
|
console.log('Received event:', ev)
|
||||||
setPosts(currentPosts => [...currentPosts, ev])
|
list = [...list, ev]
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
console.log('feeds', feeds)
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
setPosts(feeds)
|
||||||
|
}, [npub])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Get Last post from a author
|
||||||
|
const start = async () => {
|
||||||
|
setLoaded(true)
|
||||||
|
await getUserFeeds()
|
||||||
|
setLoaded(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded && npub) {
|
if (!loaded && npub) {
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
}, [bchWalletState, loaded, npub])
|
}, [bchWalletState, loaded, npub, getUserFeeds])
|
||||||
|
|
||||||
const handleProfilePictureError = () => {
|
const handleProfilePictureError = () => {
|
||||||
setProfilePictureError(true)
|
setProfilePictureError(true)
|
||||||
|
|||||||
Reference in New Issue
Block a user