diff --git a/src/components/app-body/index.js b/src/components/app-body/index.js index 6aa02a9..09a98cd 100644 --- a/src/components/app-body/index.js +++ b/src/components/app-body/index.js @@ -24,7 +24,7 @@ import SweepWif from './sweep/index.js' import SignMessage from './sign/index.js' import ServerSelectView from './configuration/select-server-view' import NostrPost from './nostr/nostr-post/index.js' -import NostrRead from './nostr/nostr-read/index.js' +import Profile from './nostr/profile/index.js' import GlobalFeed from './nostr/global-feed/index.js' import ContentCreators from './nostr/content-creators/index.js' import UserDataReview from './user-data-review' @@ -49,7 +49,7 @@ function AppBody (props) { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/components/app-body/nostr/content-creators/content-card.js b/src/components/app-body/nostr/content-creators/content-card.js index d0156e1..d52005d 100644 --- a/src/components/app-body/nostr/content-creators/content-card.js +++ b/src/components/app-body/nostr/content-creators/content-card.js @@ -46,7 +46,7 @@ function ContentCard (props) { }, [loaded, creator]) const goToProfile = () => { - const profileUrl = `${window.location.origin}/nostr-read/${creator.npub}` + const profileUrl = `${window.location.origin}/profile/${creator.npub}` window.open(profileUrl, '_blank') } diff --git a/src/components/app-body/nostr/nostr-read/public-read.js b/src/components/app-body/nostr/nostr-read/public-read.js deleted file mode 100644 index c3c3d1c..0000000 --- a/src/components/app-body/nostr/nostr-read/public-read.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Component for read nostr information kind 1 - */ -// Global npm libraries -import React, { useEffect, useState } from 'react' -import { Container, Card, Spinner } from 'react-bootstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faUser } from '@fortawesome/free-solid-svg-icons' -import NostrFormat from '../nostr-format' -import { RelayPool } from 'nostr' -import * as nip19 from 'nostr-tools/nip19' - -function PublicRead (props) { - const { npub } = props - const { bchWalletState } = props.appData - const [posts, setPosts] = useState([]) - const [loaded, setLoaded] = 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 psf = 'wss://nostr-relay.psfoundation.info' - - const pool = RelayPool([psf]) - 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]) - }) - } - - if (!loaded && npub) { - start() - } - }, [bchWalletState, loaded, npub]) - - return ( - -
- {posts.map((post, index) => ( - - -
-
- -
-
-
{props.profile?.name}
- - {`${bchWalletState.nostrKeyPair.npub.slice(0, 8)}...${bchWalletState.nostrKeyPair.npub.slice(-5)}`} - -
- - {new Date(post.created_at * 1000).toLocaleString()} - -
-
-
- -
-
-
-
- ))} - {!posts.length && loaded && ( -
- -
No posts found
-
- )} - {!loaded && ( -
- -
- )} -
-
- ) -} - -export default PublicRead diff --git a/src/components/app-body/nostr/nostr-read/index.js b/src/components/app-body/nostr/profile/index.js similarity index 79% rename from src/components/app-body/nostr/nostr-read/index.js rename to src/components/app-body/nostr/profile/index.js index 1cc910b..936d853 100644 --- a/src/components/app-body/nostr/nostr-read/index.js +++ b/src/components/app-body/nostr/profile/index.js @@ -8,8 +8,9 @@ import { Container } from 'react-bootstrap' import ProfileRead from './profile-read.js' import PublicRead from './public-read.js' import { useParams } from 'react-router-dom' +import SlpTokensDisplay from './slp-tokens-display' -function NostrRead (props) { +function Profile (props) { const [profile, setProfile] = useState(false) const { npub } = useParams() console.log('npub', npub) @@ -18,9 +19,10 @@ function NostrRead (props) { + ) } -export default NostrRead +export default Profile diff --git a/src/components/app-body/nostr/nostr-read/profile-read.js b/src/components/app-body/nostr/profile/profile-read.js similarity index 100% rename from src/components/app-body/nostr/nostr-read/profile-read.js rename to src/components/app-body/nostr/profile/profile-read.js diff --git a/src/components/app-body/nostr/profile/public-read.js b/src/components/app-body/nostr/profile/public-read.js new file mode 100644 index 0000000..ea702b1 --- /dev/null +++ b/src/components/app-body/nostr/profile/public-read.js @@ -0,0 +1,114 @@ +/** + * Component for read nostr information kind 1 + */ +// Global npm libraries +import React, { useEffect, useState } from 'react' +import { Container, Card, Spinner } from 'react-bootstrap' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faUser } from '@fortawesome/free-solid-svg-icons' +import NostrFormat from '../nostr-format' +import { RelayPool } from 'nostr' +import * as nip19 from 'nostr-tools/nip19' + +function PublicRead (props) { + const { npub } = props + const { bchWalletState } = props.appData + const [posts, setPosts] = useState([]) + const [loaded, setLoaded] = 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 psf = 'wss://nostr-relay.psfoundation.info' + + const pool = RelayPool([psf]) + 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]) + }) + } + + if (!loaded && npub) { + start() + } + }, [bchWalletState, loaded, npub]) + + return ( + + {/* Nostr Feed Section */} +
+
+
Nostr Feed
+
+
+ {posts.map((post, index) => ( + + +
+
+ +
+
+
{props.profile?.name}
+ + {`${bchWalletState.nostrKeyPair.npub.slice(0, 8)}...${bchWalletState.nostrKeyPair.npub.slice(-5)}`} + +
+ + {new Date(post.created_at * 1000).toLocaleString()} + +
+
+
+ +
+
+
+
+ ))} + {!posts.length && loaded && ( +
+ +
No posts found
+
+ )} + {!loaded && ( +
+ +
+ )} +
+
+
+ ) +} + +export default PublicRead diff --git a/src/components/app-body/nostr/profile/slp-tokens-display.js b/src/components/app-body/nostr/profile/slp-tokens-display.js new file mode 100644 index 0000000..a503711 --- /dev/null +++ b/src/components/app-body/nostr/profile/slp-tokens-display.js @@ -0,0 +1,169 @@ +/** + * Component for displaying SLP tokens in a grid layout + */ +import React, { useState, useEffect, useCallback } from 'react' +import { Container, Row, Col, Spinner } from 'react-bootstrap' +import config from '../../../../config' +import axios from 'axios' +import TokenCard from '../../slp-tokens/token-card' + +function SlpTokensDisplay (props) { + const { appData } = props + const { bchWalletState, wallet } = props.appData + const { npub } = props + const [loaded, setLoaded] = useState(false) + const [tokens, setTokens] = useState([]) + const [iconsAreLoaded, setIconsAreLoaded] = useState(false) + + // 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 + } + + // Fetch mutable data if it exist and get the token icon url + const fetchTokenIcon = useCallback(async (token) => { + try { + // Get the token data + const tokenData = await appData.wallet.getTokenData(token.tokenId) + if (!tokenData.mutableData) return false // Return false if no mutable data + // Get the token icon from the mutable data + const cid = parseCid(tokenData.mutableData) + console.log('mutable data cid', cid) + + const { json } = await appData.wallet.cid2json({ cid }) + + if (!json) return false + + const iconUrl = json.tokenIcon + // Return icon url + return iconUrl + } catch (error) { + return false + } + }, [appData]) + + // This function loads the token icons from the ipfs gateways. + const lazyLoadTokenIcons = useCallback(async (tokenList) => { + try { + setIconsAreLoaded(false) + + // map each token and fetch the icon url + for (let i = 0; i < tokenList.length; i++) { + try { + const thisToken = tokenList[i] + + // Incon does not need to be downloaded, so continue with the next one + if (thisToken.iconAlreadyDownloaded) continue + + // Try to get token icon url from mutable data. + const iconUrl = await fetchTokenIcon(thisToken) + console.log('iconUrl', iconUrl) + if (iconUrl) { + // Set the icon url to the token , this can be used to display the icon in the token card component. + thisToken.icon = iconUrl + } + + // Mark token to prevent fetch token icon again. + thisToken.iconAlreadyDownloaded = true + // replace the token in the tokens array + const updatedTokens = [...tokenList] + updatedTokens[i] = thisToken + setTokens(updatedTokens) + } catch (error) { + continue + } + } + + setIconsAreLoaded(true) + } catch (error) { + console.warn(error) + setIconsAreLoaded(true) + } + }, [fetchTokenIcon, setTokens]) + + useEffect(() => { + // setTokens(bchWalletState.slpTokens || []) + const getTokens = async () => { + try { + const { nostrKeyPair } = bchWalletState + if (nostrKeyPair.npub === npub) { + setTokens(bchWalletState.slpTokens) + setLoaded(true) + return + } + setLoaded(false) + const url = `${config.dexServer}/sm/npub/${npub}` + const response = await axios.get(url) + console.log('response', response.data) + const addr = response.data.bchAddr + + const tokenList = await wallet.listTokens(addr) + setTokens(tokenList) + // setTokens(response.data) + setLoaded(true) + + lazyLoadTokenIcons(tokenList) + } catch (error) { + console.error('Error fetching tokens:', error) + setLoaded(true) + } + } + if (!loaded) { + getTokens() + } + }, [bchWalletState, npub, loaded, lazyLoadTokenIcons, wallet]) + + return ( + + {!loaded && ( +
+ +
+ )} + {loaded && tokens.length !== 0 && +
+
+
SLP Tokens
+
+
+ + {!iconsAreLoaded && + +
+ Loading Token Icons + +
+ + } + + {tokens.map(thisToken => ( + { }} + hideSendBtn + /> + ))} + +
+ +
+
} +
+ ) +} + +export default SlpTokensDisplay diff --git a/src/components/app-body/slp-tokens/token-card.js b/src/components/app-body/slp-tokens/token-card.js index 2ceaa3a..4180184 100644 --- a/src/components/app-body/slp-tokens/token-card.js +++ b/src/components/app-body/slp-tokens/token-card.js @@ -12,7 +12,7 @@ import InfoButton from './info-button' import SendTokenButton from './send-token-button' function TokenCard (props) { - const { token } = props + const { token, hideSendBtn } = props const [icon, setIcon] = useState(token.icon) // Update icon state every token.icon changes @@ -64,13 +64,15 @@ function TokenCard (props) { - - - + {!hideSendBtn && ( + + + + )} diff --git a/src/components/nav-menu/index.js b/src/components/nav-menu/index.js index 0737add..eda91fb 100644 --- a/src/components/nav-menu/index.js +++ b/src/components/nav-menu/index.js @@ -71,8 +71,8 @@ function NavMenu (props) { Nostr Post Nostr Profile