mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
Merge branch 'master' into ct-unstable
This commit is contained in:
Generated
+1166
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
|||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-bootstrap": "2.10.7",
|
"react-bootstrap": "2.10.7",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
|
"react-markdown": "10.1.0",
|
||||||
"react-router-dom": "7.1.3",
|
"react-router-dom": "7.1.3",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"use-local-storage-state": "19.5.0",
|
"use-local-storage-state": "19.5.0",
|
||||||
|
|||||||
+170
@@ -47,4 +47,174 @@ input::-webkit-inner-spin-button {
|
|||||||
/* Remove input number arrows Firefox */
|
/* Remove input number arrows Firefox */
|
||||||
input[type=number] {
|
input[type=number] {
|
||||||
--moz-appearance: textfield;
|
--moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown Content Styles */
|
||||||
|
.markdown-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 3px solid #3b82f6;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4b5563;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h4, .markdown-content h5, .markdown-content h6 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul, .markdown-content ol {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content li {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content blockquote {
|
||||||
|
border-left: 4px solid #3b82f6;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content code {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #e11d48;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
background: #1e293b;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre code {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content a {
|
||||||
|
color: #3b82f6;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content a:hover {
|
||||||
|
color: #1d4ed8;
|
||||||
|
border-bottom-color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content th, .markdown-content td {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content th {
|
||||||
|
background: #f8fafc;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content hr {
|
||||||
|
border: none;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, transparent, #e5e7eb, transparent);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.markdown-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-content h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
.markdown-content h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
.markdown-content p, .markdown-content li {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+38
-2
@@ -14,6 +14,8 @@ import NavMenu from './components/nav-menu'
|
|||||||
import useAppState from './hooks/state'
|
import useAppState from './hooks/state'
|
||||||
import { UninitializedView, InitializedView } from './components/starter-views'
|
import { UninitializedView, InitializedView } from './components/starter-views'
|
||||||
|
|
||||||
|
const sigleViewPaths = ['/profile', 'user-data']
|
||||||
|
|
||||||
function App (props) {
|
function App (props) {
|
||||||
// Load all the app state into a single object that can be passed to child
|
// Load all the app state into a single object that can be passed to child
|
||||||
// components.
|
// components.
|
||||||
@@ -30,12 +32,46 @@ function App (props) {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const isSignleView = useCallback(async () => {
|
||||||
|
// Get current path
|
||||||
|
const currentPath = window.location.pathname
|
||||||
|
// Get hash from url
|
||||||
|
const hash = window.location.hash
|
||||||
|
const allowedPath = sigleViewPaths.find((val) => { return currentPath.match(val) })
|
||||||
|
|
||||||
|
if (hash === '#single-view' && allowedPath) {
|
||||||
|
addToModal('Loading minimal-slp-wallet', appData)
|
||||||
|
const asyncLoad = new AsyncLoad()
|
||||||
|
if (!appData.wallet) {
|
||||||
|
await asyncLoad.loadWalletLib()
|
||||||
|
const walletTemp = await asyncLoad.initStarterWallet(appData.serverUrl, appData.lsState.mnemonic, appData)
|
||||||
|
appData.setWallet(walletTemp)
|
||||||
|
// Get the BCH spot price
|
||||||
|
addToModal('Getting BCH spot price in USD', appData)
|
||||||
|
await asyncLoad.getUSDExchangeRate(walletTemp, appData.updateBchWalletState, appData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Modal State
|
||||||
|
appData.setIsSingleView(true)
|
||||||
|
appData.setHideSpinner(true)
|
||||||
|
appData.setShowStartModal(false)
|
||||||
|
appData.setDenyClose(false)
|
||||||
|
|
||||||
|
/* // Update the startup state.
|
||||||
|
appData.setAsyncInitFinished(true)
|
||||||
|
appData.setAsyncInitSucceeded(false) */
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, [appData, addToModal])
|
||||||
|
|
||||||
/** Load all required data before component start. */
|
/** Load all required data before component start. */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function asyncEffect () {
|
async function asyncEffect () {
|
||||||
|
const singleView = await isSignleView()
|
||||||
console.log('asyncInitStarted: ', appData.asyncInitStarted)
|
console.log('asyncInitStarted: ', appData.asyncInitStarted)
|
||||||
|
|
||||||
if (!appData.asyncInitStarted) {
|
if (!appData.asyncInitStarted && !singleView) {
|
||||||
try {
|
try {
|
||||||
// Instantiate the async load object.
|
// Instantiate the async load object.
|
||||||
const asyncLoad = new AsyncLoad()
|
const asyncLoad = new AsyncLoad()
|
||||||
@@ -98,7 +134,7 @@ function App (props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
asyncEffect()
|
asyncEffect()
|
||||||
}, [appData, addToModal])
|
}, [appData, addToModal, isSignleView])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import SlpTokens from './slp-tokens'
|
|||||||
import SweepWif from './sweep/index.js'
|
import SweepWif from './sweep/index.js'
|
||||||
import SignMessage from './sign/index.js'
|
import SignMessage from './sign/index.js'
|
||||||
import ServerSelectView from './configuration/select-server-view'
|
import ServerSelectView from './configuration/select-server-view'
|
||||||
|
import UserDataReview from './user-data-review'
|
||||||
|
|
||||||
function AppBody (props) {
|
function AppBody (props) {
|
||||||
// Dependency injection through props
|
// Dependency injection through props
|
||||||
@@ -41,6 +42,7 @@ function AppBody (props) {
|
|||||||
<Route path='/sweep' element={<SweepWif appData={appData} />} />
|
<Route path='/sweep' element={<SweepWif appData={appData} />} />
|
||||||
<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='/user-data/:tokenId' element={<UserDataReview appData={appData} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
{/** Show in all paths except the servers view */}
|
{/** Show in all paths except the servers view */}
|
||||||
{/* {appData.currentPath !== '/servers' && <SelectServerButton linkTo='/servers' appData={appData} />} */}
|
{/* {appData.currentPath !== '/servers' && <SelectServerButton linkTo='/servers' appData={appData} />} */}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import RefreshTokenBalance from './refresh-tokens'
|
|||||||
const SlpTokens = (props) => {
|
const SlpTokens = (props) => {
|
||||||
const [appData, setAppData] = useState(props.appData)
|
const [appData, setAppData] = useState(props.appData)
|
||||||
const [iconsAreLoaded, setIconsAreLoaded] = useState(false)
|
const [iconsAreLoaded, setIconsAreLoaded] = useState(false)
|
||||||
|
const [dataAreLoaded, setDataAreLoaded] = useState(false)
|
||||||
const [tokens, setTokens] = useState([])
|
const [tokens, setTokens] = useState([])
|
||||||
|
|
||||||
const refreshTokenButtonRef = React.useRef()
|
const refreshTokenButtonRef = React.useRef()
|
||||||
@@ -44,37 +45,68 @@ const SlpTokens = (props) => {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function loads the token data .
|
||||||
|
const lazyLoadTokenData = useCallback(async (tokens) => {
|
||||||
|
try {
|
||||||
|
setDataAreLoaded(false)
|
||||||
|
// map each token and fetch the token data
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const thisToken = tokens[i]
|
||||||
|
|
||||||
|
// data does not need to be downloaded, so continue with the next one
|
||||||
|
if (thisToken.dataAlreadyDownloaded) continue
|
||||||
|
|
||||||
|
// Try to get token data.
|
||||||
|
const tokenData = await appData.wallet.getTokenData(thisToken.tokenId)
|
||||||
|
console.log('tokenData', tokenData)
|
||||||
|
if (tokenData) {
|
||||||
|
// Set data to the token object , this can be used to display the token name in the token card component.
|
||||||
|
thisToken.tokenData = tokenData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark token to prevent fetch token data again.
|
||||||
|
thisToken.dataAlreadyDownloaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
setDataAreLoaded(true)
|
||||||
|
} catch (error) {
|
||||||
|
setDataAreLoaded(true)
|
||||||
|
}
|
||||||
|
}, [appData])
|
||||||
|
|
||||||
// Fetch mutable data if it exist and get the token icon url
|
// Fetch mutable data if it exist and get the token icon url
|
||||||
const fetchTokenIcon = useCallback(async (token) => {
|
const fetchTokenMutableData = useCallback(async (token) => {
|
||||||
try {
|
try {
|
||||||
// Get the token data
|
// Get the token data
|
||||||
const tokenData = await appData.wallet.getTokenData(token.tokenId)
|
const tokenData = token.tokenData
|
||||||
|
|
||||||
if (!tokenData.mutableData) return false // Return false if no mutable data
|
if (!tokenData.mutableData) return false // Return false if no mutable data
|
||||||
|
|
||||||
// Get the token icon from the mutable data
|
// Get the token icon from the mutable data
|
||||||
const cid = parseCid(tokenData.mutableData)
|
const cid = parseCid(tokenData.mutableData)
|
||||||
console.log('mutable data cid', cid)
|
console.log('mutable data cid', cid)
|
||||||
|
|
||||||
const { json } = await appData.wallet.cid2json({ cid })
|
const { json } = await appData.wallet.cid2json({ cid })
|
||||||
|
console.log('json: ', json)
|
||||||
if (!json) return false
|
if (!json) return false
|
||||||
|
|
||||||
const iconUrl = json.tokenIcon
|
let iconUrl = json.tokenIcon
|
||||||
|
|
||||||
|
if (json.fullSizedUrl && json.fullSizedUrl.includes('http')) {
|
||||||
|
iconUrl = json.fullSizedUrl
|
||||||
|
}
|
||||||
|
const userData = json.userData
|
||||||
// Return icon url
|
// Return icon url
|
||||||
return iconUrl
|
return { iconUrl, userData }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}, [appData])
|
}, [appData])
|
||||||
|
|
||||||
// This function loads the token icons from the ipfs gateways.
|
// This function loads the token icons from the ipfs gateways.
|
||||||
const lazyLoadTokenIcons = useCallback(async () => {
|
const lazyLoadMutableData = useCallback(async (tokens) => {
|
||||||
try {
|
try {
|
||||||
setIconsAreLoaded(false)
|
setIconsAreLoaded(false)
|
||||||
|
|
||||||
const tokens = appData.bchWalletState.slpTokens
|
|
||||||
|
|
||||||
setTokens(tokens) // update token state
|
|
||||||
|
|
||||||
// map each token and fetch the icon url
|
// map each token and fetch the icon url
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
const thisToken = tokens[i]
|
const thisToken = tokens[i]
|
||||||
@@ -83,28 +115,35 @@ const SlpTokens = (props) => {
|
|||||||
if (thisToken.iconAlreadyDownloaded) continue
|
if (thisToken.iconAlreadyDownloaded) continue
|
||||||
|
|
||||||
// Try to get token icon url from mutable data.
|
// Try to get token icon url from mutable data.
|
||||||
const iconUrl = await fetchTokenIcon(thisToken)
|
const { iconUrl, userData } = await fetchTokenMutableData(thisToken)
|
||||||
console.log('iconUrl', iconUrl)
|
console.log('iconUrl', iconUrl)
|
||||||
if (iconUrl) {
|
if (iconUrl) {
|
||||||
// Set the icon url to the token , this can be used to display the icon in the token card component.
|
// Set the icon url to the token , this can be used to display the icon in the token card component.
|
||||||
thisToken.icon = iconUrl
|
thisToken.icon = iconUrl
|
||||||
|
thisToken.tokenData.userData = userData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark token to prevent fetch token icon again.
|
// Mark token to prevent fetch token icon again.
|
||||||
thisToken.iconAlreadyDownloaded = true
|
thisToken.iconAlreadyDownloaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
appData.updateBchWalletState({ walletObj: { slpTokens: tokens }, appData })
|
|
||||||
setIconsAreLoaded(true)
|
setIconsAreLoaded(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIconsAreLoaded(true)
|
setIconsAreLoaded(true)
|
||||||
}
|
}
|
||||||
}, [appData, fetchTokenIcon])
|
}, [fetchTokenMutableData])
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
const tokens = appData.bchWalletState.slpTokens
|
||||||
|
setTokens(tokens)
|
||||||
|
await lazyLoadTokenData(tokens)
|
||||||
|
await lazyLoadMutableData(tokens)
|
||||||
|
}, [appData, lazyLoadTokenData, lazyLoadMutableData])
|
||||||
|
|
||||||
// Start to load the token icons when the component is mounted
|
// Start to load the token icons when the component is mounted
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
lazyLoadTokenIcons()
|
loadData()
|
||||||
}, [lazyLoadTokenIcons])
|
}, [loadData])
|
||||||
|
|
||||||
// Generate the token cards for each token in the wallet.
|
// Generate the token cards for each token in the wallet.
|
||||||
const generateCards = () => {
|
const generateCards = () => {
|
||||||
@@ -127,7 +166,7 @@ const SlpTokens = (props) => {
|
|||||||
<RefreshTokenBalance
|
<RefreshTokenBalance
|
||||||
appData={appData}
|
appData={appData}
|
||||||
ref={refreshTokenButtonRef}
|
ref={refreshTokenButtonRef}
|
||||||
lazyLoadTokenIcons={lazyLoadTokenIcons}
|
lazyLoadTokenIcons={loadData}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={6} style={{ textAlign: 'right' }}>
|
<Col xs={6} style={{ textAlign: 'right' }}>
|
||||||
@@ -138,8 +177,16 @@ const SlpTokens = (props) => {
|
|||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={12} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
<Col xs={12} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
{
|
{/** Show spinner info if tokens are loaded but data is not loaded */
|
||||||
!iconsAreLoaded && (
|
!dataAreLoaded && (
|
||||||
|
<div style={{ borderRadius: '10px', backgroundColor: '#f0f0f0', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center', width: 'fit-content' }}>
|
||||||
|
<span style={{ marginRight: '10px' }}>Loading Token Data </span>
|
||||||
|
<Spinner animation='border' />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{/** Show spinner info if tokens are loaded but icons are not loaded */
|
||||||
|
dataAreLoaded && !iconsAreLoaded && (
|
||||||
<div style={{ borderRadius: '10px', backgroundColor: '#f0f0f0', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center', width: 'fit-content' }}>
|
<div style={{ borderRadius: '10px', backgroundColor: '#f0f0f0', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center', width: 'fit-content' }}>
|
||||||
<span style={{ marginRight: '10px' }}>Loading Token Icons </span>
|
<span style={{ marginRight: '10px' }}>Loading Token Icons </span>
|
||||||
<Spinner animation='border' />
|
<Spinner animation='border' />
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Global npm libraries
|
// Global npm libraries
|
||||||
import React, { useState } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Button, Modal, Container, Row, Col } from 'react-bootstrap'
|
import { Button, Modal, Container, Row, Col, Spinner } from 'react-bootstrap'
|
||||||
|
|
||||||
// Takes a string as input. If it matches a pattern for a link, a JSX object is
|
// Takes a string as input. If it matches a pattern for a link, a JSX object is
|
||||||
// returned with a link. Otherwise the original string is returned.
|
// returned with a link. Otherwise the original string is returned.
|
||||||
@@ -29,6 +29,7 @@ function linkIfUrl (url) {
|
|||||||
|
|
||||||
function InfoButton (props) {
|
function InfoButton (props) {
|
||||||
const [show, setShow] = useState(false)
|
const [show, setShow] = useState(false)
|
||||||
|
const [mutableDataCid, setMutableDataCid] = useState(null)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setShow(false)
|
setShow(false)
|
||||||
@@ -45,6 +46,33 @@ function InfoButton (props) {
|
|||||||
|
|
||||||
// console.log('props.token: ', props.token)
|
// console.log('props.token: ', props.token)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
// Get token user data if it exists and verify if it contains media or markdown
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
console.log('props token', props.token)
|
||||||
|
const userDataStr = props.token.tokenData.userData
|
||||||
|
if (userDataStr) {
|
||||||
|
const userData = JSON.parse(userDataStr)
|
||||||
|
|
||||||
|
// If user data contains media or markdown, set the mutable data cid
|
||||||
|
if (userData?.media || userData?.markdown) {
|
||||||
|
setMutableDataCid(parseCid(props.token.tokenData.mutableData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}, [props.token, show])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button variant='info' onClick={handleOpen}>Info</Button>
|
<Button variant='info' onClick={handleOpen}>Info</Button>
|
||||||
@@ -87,6 +115,25 @@ function InfoButton (props) {
|
|||||||
<Col xs={4}><b>URL</b>:</Col>
|
<Col xs={4}><b>URL</b>:</Col>
|
||||||
<Col xs={8}>{url}</Col>
|
<Col xs={8}>{url}</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
{!props.token.iconAlreadyDownloaded && (
|
||||||
|
<div className='text-center'>
|
||||||
|
<Spinner animation='border' size='sm' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{props.token.iconAlreadyDownloaded && mutableDataCid && (
|
||||||
|
<Row style={{ paddingTop: '10px' }}>
|
||||||
|
<Col xs={4}><b>User Data</b>:</Col>
|
||||||
|
<Col xs={8}>
|
||||||
|
<Button
|
||||||
|
href={`/user-data/${props.token.tokenId}#single-view`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
View User Data
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer />
|
<Modal.Footer />
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
Search in the provided cid of a mutable data, get the data from the user data and show it
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Global npm libraries
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { Container, Row, Col, Spinner, Carousel } from 'react-bootstrap'
|
||||||
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import '../../App.css'
|
||||||
|
|
||||||
|
function UserDataReview (props) {
|
||||||
|
const appData = props.appData
|
||||||
|
const [media, setMedia] = useState([])
|
||||||
|
const [markdown, setMarkdown] = useState('')
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const [tokenData, setTokenData] = useState(null)
|
||||||
|
// Get the id parameter from the URL
|
||||||
|
const { tokenId } = useParams()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
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 userDataString = json.userData
|
||||||
|
|
||||||
|
if (userDataString) {
|
||||||
|
const userData = JSON.parse(userDataString)
|
||||||
|
setMedia(userData.media)
|
||||||
|
setMarkdown(userData.markdown)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError(error.message)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
loadData()
|
||||||
|
}, [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 (
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
{/** Error message */}
|
||||||
|
{error && <p className='text-danger'>{error}</p>}
|
||||||
|
|
||||||
|
{/** Loading spinner */}
|
||||||
|
{loading &&
|
||||||
|
(
|
||||||
|
<div className='text-center my-5'>
|
||||||
|
<Spinner animation='border' role='status' variant='primary'>
|
||||||
|
<span className='visually-hidden'>Loading...</span>
|
||||||
|
</Spinner>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/** User data */}
|
||||||
|
{!loading && !error && (
|
||||||
|
<div className='text-center'>
|
||||||
|
{/** Name */}
|
||||||
|
<h1>{tokenData.genesisData.name}</h1>
|
||||||
|
|
||||||
|
{/** Media Content Carousel */}
|
||||||
|
{media && media.length > 0
|
||||||
|
? (
|
||||||
|
<div className='my-5 '>
|
||||||
|
<Carousel>
|
||||||
|
{media.map((item, index) => (
|
||||||
|
<Carousel.Item key={index}>
|
||||||
|
<img
|
||||||
|
className='d-block w-100'
|
||||||
|
src={item.url}
|
||||||
|
alt={`Review media ${index + 1}`}
|
||||||
|
style={{ height: '400px', objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
<Carousel.Caption>
|
||||||
|
<p>Image {index + 1} of {media.length}</p>
|
||||||
|
</Carousel.Caption>
|
||||||
|
</Carousel.Item>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
<style>{'.carousel-control-next,.carousel-control-prev, .carousel-indicators {filter: invert(100%); } '}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<p className='mt-3'>No media content available</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/** Markdown Content */}
|
||||||
|
{markdown
|
||||||
|
? (
|
||||||
|
<div className='markdown-content my-5'>
|
||||||
|
<ReactMarkdown>{markdown}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<p className='mt-3'>No content available</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserDataReview
|
||||||
+5
-1
@@ -35,6 +35,8 @@ function useAppState () {
|
|||||||
const [hideSpinner, setHideSpinner] = useState(false)
|
const [hideSpinner, setHideSpinner] = useState(false)
|
||||||
const [denyClose, setDenyClose] = useState(false)
|
const [denyClose, setDenyClose] = useState(false)
|
||||||
|
|
||||||
|
const [isSingleView, setIsSingleView] = useState(false)
|
||||||
|
|
||||||
// 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.
|
||||||
// properties are enumerated here for the purpose of documentation.
|
// properties are enumerated here for the purpose of documentation.
|
||||||
@@ -125,7 +127,9 @@ function useAppState () {
|
|||||||
updateLocalStorage,
|
updateLocalStorage,
|
||||||
updateBchWalletState,
|
updateBchWalletState,
|
||||||
appUtil: new AppUtil(),
|
appUtil: new AppUtil(),
|
||||||
currentPath: location.pathname
|
currentPath: location.pathname,
|
||||||
|
setIsSingleView,
|
||||||
|
isSingleView
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,44 @@ class AsyncLoad {
|
|||||||
return defaultOptions
|
return defaultOptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the BCH wallet without initialize() promise
|
||||||
|
async initStarterWallet (restURL, mnemonic, appData) {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
interface: 'consumer-api',
|
||||||
|
restURL,
|
||||||
|
noUpdate: true
|
||||||
|
}
|
||||||
|
|
||||||
|
let wallet
|
||||||
|
if (mnemonic) {
|
||||||
|
// Load the wallet from the mnemonic, if it's available from local storage.
|
||||||
|
wallet = new this.BchWallet(mnemonic, options)
|
||||||
|
} else {
|
||||||
|
// Generate a new mnemonic and wallet.
|
||||||
|
wallet = new this.BchWallet(null, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
await wallet.walletInfoPromise
|
||||||
|
|
||||||
|
const walletInfo = wallet.walletInfo
|
||||||
|
// Update the state of the wallet.
|
||||||
|
appData.updateBchWalletState({ walletObj: walletInfo, appData })
|
||||||
|
// Save the mnemonic to local storage.
|
||||||
|
if (!mnemonic) {
|
||||||
|
const newMnemonic = wallet.walletInfo.mnemonic
|
||||||
|
appData.updateLocalStorage({ mnemonic: newMnemonic })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wallet = wallet
|
||||||
|
|
||||||
|
return wallet
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initStarterWallet: ', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep (ms) {
|
function sleep (ms) {
|
||||||
|
|||||||
Reference in New Issue
Block a user