Compare commits

..
21 Commits
Author SHA1 Message Date
Chris Troutner efdfb7971b Merge pull request #73 from Permissionless-Software-Foundation/dh-filter-chats
feat(chat): Filter out chat messages from deletedChat database
2025-11-04 08:48:22 -08:00
Daniel Gonzalez 8c59bdf6d8 feat(chat): Filter out chat messages from deletedChat database 2025-11-03 15:12:46 -04:00
Chris Troutner 143def9164 Merge pull request #72 from Permissionless-Software-Foundation/dh-update-btn
feat(update): Added update button
2025-10-25 11:23:40 -07:00
Daniel Gonzalez b58309d82a feat(update): Added update button 2025-10-24 19:59:48 -04:00
Chris Troutner 61a5ad7166 Merge pull request #71 from Permissionless-Software-Foundation/dh-load-nft-db
feat(nft): Load NFT card from bch-dex DB
2025-10-21 12:57:18 -07:00
Daniel Gonzalez 3b1b6248ab feat(nft): Load NFT card from bch-dex DB 2025-10-20 18:38:58 -04:00
Chris Troutner cd44f28f28 Merge pull request #70 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream bch-wallet-web-spa
2025-10-19 14:55:23 -07:00
Chris Troutner 5d7994cefe Syncing with upstream bch-wallet-web-spa 2025-10-19 14:53:01 -07:00
Chris Troutner ae4916e8df Merge pull request #18 from Permissionless-Software-Foundation/ct-unstable
Ct unstable
2025-10-19 14:48:30 -07:00
Chris Troutner fe108caf33 Merge branch 'master' into ct-unstable 2025-10-19 14:47:09 -07:00
Chris Troutner 131a41fcb4 Merge pull request #17 from Permissionless-Software-Foundation/dh-down-servers
fix(server): Web Wallet needs to handle down servers
2025-10-19 14:41:43 -07:00
Daniel Gonzalez f25ac56cdb fix(server): Web Wallet needs to handle down servers 2025-10-18 15:46:29 -04:00
Chris Troutner 0fb8bc586b Merge pull request #69 from Permissionless-Software-Foundation/ct-unstable
fix(category): Using Misc as default category
2025-10-14 07:47:39 -07:00
Chris Troutner 4f8a6540e5 fix(category): Using Misc as default category 2025-10-14 07:46:21 -07:00
Chris Troutner b29ff5fb1f Merge branch 'master' into ct-unstable 2025-10-14 07:17:47 -07:00
Chris Troutner 9e576cbe83 Merge pull request #16 from Permissionless-Software-Foundation/dh-load-times
fix(load): Load BCH & SLP info in the background
2025-10-14 07:13:20 -07:00
Daniel Gonzalez e3b3b76430 fix(load): Load BCH & SLP info in the background 2025-10-13 17:34:50 -04:00
Chris Troutner f8f46fcad8 Merge pull request #68 from Permissionless-Software-Foundation/dh-categories-ui
feat(NFTs): Added categories dropdown to nfts page
2025-10-12 16:42:04 -07:00
Daniel Gonzalez 8c00d5d079 feat(NFTs): Added categories dropdown to nfts page 2025-10-11 18:00:43 -04:00
Chris Troutner dc6e6d956c Merge branch 'master' into ct-unstable 2025-09-20 09:37:13 -07:00
Chris Troutner 360be9f875 Adding LLM file 2025-06-19 17:22:28 -07:00
21 changed files with 4870 additions and 105 deletions
File diff suppressed because it is too large Load Diff
+49 -9
View File
@@ -67,6 +67,44 @@ function App (props) {
return false
}, [appData, addToModal])
/**
* Run background process to get bch and slp balance.
* Also update the background state process.
* On error this function should trigger a info modal notifying the errors.
*/
const backgroundAsync = useCallback(async (asyncLoad, walletTemp) => {
try {
appData.setModalBody(['Getting BCH balance in background!.'])
// Get Wallet Balance
await asyncLoad.getWalletBchBalance(walletTemp, appData.updateBchWalletState, appData)
// Update Background state
appData.updateBackGroundInitState({ bchInitLoaded: true })
// Get SLP Balance
appData.setModalBody(['Getting SLP tokens in background!.'])
await asyncLoad.getSlpTokenBalances(walletTemp, appData.updateBchWalletState, appData)
// Update Background state
appData.updateBackGroundInitState({ slpInitLoaded: true, asyncBackgroundFinished: true })
} catch (err) {
console.log('App.js backgroundAsync() error!', err)
appData.updateBackGroundInitState({ asyncBackgroundFinished: true })
addToModal(`Error: ${err.message}`, appData)
addToModal('Try selecting a different back end server using the drop-down menu at the bottom of the app.\'', appData)
// Update Modal State
appData.setHideSpinner(true)
appData.setShowStartModal(true)
appData.setDenyClose(false)
// Update the startup state.
appData.setAsyncInitFinished(true)
appData.setAsyncInitSucceeded(false)
}
}, [appData, addToModal])
/** Load all required data before component start. */
useEffect(() => {
async function asyncEffect () {
@@ -99,14 +137,6 @@ function App (props) {
appData.setWallet(walletTemp)
// appData.updateBchWalletState({ walletObj: walletTemp.walletInfo, appData })
// Get the BCH balance of the wallet.
addToModal('Getting BCH balance', appData)
await asyncLoad.getWalletBchBalance(walletTemp, appData.updateBchWalletState, appData)
// Get the SLP tokens held by the wallet.
addToModal('Getting SLP tokens', appData)
await asyncLoad.getSlpTokenBalances(walletTemp, appData.updateBchWalletState, appData)
// Get the BCH spot price
addToModal('Getting BCH spot price in USD', appData)
await asyncLoad.getUSDExchangeRate(walletTemp, appData.updateBchWalletState, appData)
@@ -125,6 +155,9 @@ function App (props) {
const nostrLib = asyncLoad.getNostrLib({ bchWallet: walletTemp })
appData.setNostr(nostrLib)
const deletedChats = await asyncLoad.fetchDeletedChats()
appData.setDeletedChats(deletedChats)
// Update state
appData.setShowStartModal(false)
appData.setDenyClose(false)
@@ -133,6 +166,13 @@ function App (props) {
appData.setAsyncInitFinished(true)
appData.setAsyncInitSucceeded(true)
console.log('App.js useEffect() startup finished successfully')
backgroundAsync(asyncLoad, walletTemp)
// Get the BCH balance of the wallet.
// addToModal('Getting BCH balance', appData)
// Get the SLP tokens held by the wallet.
// addToModal('Getting SLP tokens', appData)
} catch (err) {
const errModalBody = [
`Error: ${err.message}`,
@@ -152,7 +192,7 @@ function App (props) {
}
}
asyncEffect()
}, [appData, addToModal, isSignleView])
}, [appData, addToModal, isSignleView, backgroundAsync])
return (
<>
@@ -3,18 +3,39 @@
*/
// Global npm libraries
import React from 'react'
import { Container, Row, Col, Card } from 'react-bootstrap'
import React, { useEffect, useState } from 'react'
import { Container, Row, Col, Card, Spinner } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCoins } from '@fortawesome/free-solid-svg-icons'
const BalanceCard = (props) => {
const { appData } = props
const [sats, setSats] = useState('')
const [bchBalance, setbchBalance] = useState('')
const [usdBalance, setusdBalance] = useState('')
const bchjs = appData.wallet.bchjs
const sats = appData.bchWalletState.bchBalance
const bchBalance = bchjs.BitcoinCash.toBitcoinCash(sats)
const usdBalance = bchjs.Util.floor2(bchBalance * appData.bchWalletState.bchUsdPrice)
const { bchInitLoaded, asyncBackgroundFinished } = appData.asyncBackGroundInitState
// Calculate balances if wallet is successfully loaded!
useEffect(() => {
try {
const bchjs = appData.wallet.bchjs
if (bchjs && appData.asyncInitSucceeded) {
const sats = appData.bchWalletState.bchBalance
const bchBalance = bchjs.BitcoinCash.toBitcoinCash(sats)
const usdBalance = bchjs.Util.floor2(bchBalance * appData.bchWalletState.bchUsdPrice)
setSats(sats)
setbchBalance(bchBalance)
setusdBalance(usdBalance)
}
} catch (error) {
// console.warn(error)
}
}, [appData])
// Background bch data loaded finished
const backgroundDataLoaded = bchInitLoaded || asyncBackgroundFinished
const backgroundDataError = !bchInitLoaded && asyncBackgroundFinished
return (
<>
@@ -25,25 +46,37 @@ const BalanceCard = (props) => {
</Card.Title>
<br />
<Container>
<Row>
<Col>
<b>USD</b>: ${usdBalance}
</Col>
</Row>
{bchInitLoaded && (
<Container>
<Row>
<Col>
<b>USD</b>: ${usdBalance}
</Col>
</Row>
<Row>
<Col>
<b>BCH</b>: {bchBalance}
</Col>
</Row>
<Row>
<Col>
<b>BCH</b>: {bchBalance}
</Col>
</Row>
<Row>
<Col>
<b>Satoshis</b>: {sats}
</Col>
</Row>
</Container>
<Row>
<Col>
<b>Satoshis</b>: {sats}
</Col>
</Row>
</Container>)}
{backgroundDataError && (
<Container>
<span style={{ color: 'red' }}>Balance could not be loaded!</span>
</Container>
)}
{!backgroundDataLoaded && appData.asyncInitSucceeded && (
<div className='balance-spinner-container'>
<Spinner animation='border' />
</div>
)}
</Card.Body>
</Card>
</>
@@ -50,6 +50,9 @@ export default function RefreshBchBalance (props) {
// Get the latest balance of the wallet.
const newBalance = await wallet.getBalance({ bchAddress: cashAddr })
// if bchInitLoaded is 'false', them set as true , to show the new balance.
appData.updateBackGroundInitState({ bchInitLoaded: true })
addToModal('Updating BCH per USD price...')
const bchUsdPrice = await wallet.getUsd()
@@ -16,6 +16,10 @@ import RefreshBchBalance from './refresh-balance'
function RefreshBchBalanceButton (props) {
// Dependency injections of props
const appData = props.appData
const { bchInitLoaded, asyncBackgroundFinished } = appData.asyncBackGroundInitState
// Background bch data loaded finished
const backgroundDataLoaded = bchInitLoaded || asyncBackgroundFinished
// Child function references
const refreshBchBalanceRef = useRef()
@@ -28,7 +32,7 @@ function RefreshBchBalanceButton (props) {
return (
<>
<Button variant='success' onClick={() => { handleButtonRefreshBalance(appData) }}>
<Button variant='success' onClick={() => { handleButtonRefreshBalance(appData) }} disabled={!backgroundDataLoaded}>
<FontAwesomeIcon icon={faRedo} size='lg' /> Refresh
</Button>
@@ -15,6 +15,7 @@ import RefreshBchBalance from './refresh-balance'
function SendCard (props) {
// Dependency injection through props
const appData = props.appData
const { bchInitLoaded, asyncBackgroundFinished } = appData.asyncBackGroundInitState
// Modal State
const [modalBody, setModalBody] = useState([])
@@ -29,6 +30,9 @@ function SendCard (props) {
const [oppositeUnits, setOppositeUnits] = useState('BCH')
const [oppositeQty, setOppositeQty] = useState(0)
// Background bch data loaded finished
const backgroundDataLoaded = bchInitLoaded || asyncBackgroundFinished
// Child function references
const refreshBchBalanceRef = useRef()
@@ -318,7 +322,7 @@ function SendCard (props) {
<Row>
<Col style={{ textAlign: 'center' }}>
<Button onClick={(e) => handleSendBch({ sendCardData, appData })}>Send</Button>
<Button onClick={(e) => handleSendBch({ sendCardData, appData })} disabled={!backgroundDataLoaded}>Send</Button>
</Col>
</Row>
@@ -0,0 +1,41 @@
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFilter } from '@fortawesome/free-solid-svg-icons'
function FilterDropdown (props) {
const { selectedFilter, setSelectedFilter } = props
return (
<Dropdown>
<Dropdown.Toggle
variant='outline-secondary'
className='d-flex align-items-center justify-content-around gap-2 mt-3'
style={{ minWidth: '150px' }}
>
<FontAwesomeIcon icon={faFilter} />
<span>{selectedFilter}</span>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onClick={() => setSelectedFilter('Misc')}>
Misc
</Dropdown.Item>
<Dropdown.Item onClick={() => setSelectedFilter('Art')}>
Art
</Dropdown.Item>
<Dropdown.Item onClick={() => setSelectedFilter('Video')}>
Video
</Dropdown.Item>
<Dropdown.Item onClick={() => setSelectedFilter('Writing')}>
Writing
</Dropdown.Item>
<Dropdown.Item onClick={() => setSelectedFilter('Download')}>
Download
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
}
export default FilterDropdown
+70 -12
View File
@@ -11,7 +11,7 @@
*/
// Global npm libraries
import React, { useState, useEffect, useCallback } from 'react'
import React, { useState, useEffect, useCallback, useRef } from 'react'
import { Container, Row, Col, Button, Spinner } from 'react-bootstrap'
import axios from 'axios'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@@ -20,6 +20,7 @@ import { faRedo } from '@fortawesome/free-solid-svg-icons'
// Local libraries
import config from '../../../config'
import TokenCard from './token-card'
import FilterDropdown from './filter-dropdown'
// Global variables and constants
const SERVER = config.dexServer
@@ -34,7 +35,12 @@ function NftsForSale (props) {
const [iconsAreLoaded, setIconsAreLoaded] = useState(false)
const [dataAreLoaded, setDataAreLoaded] = useState(false)
const [currentPage, setCurrentPage] = useState(0)
const [lastLoadedPage, setLastLoadedPage] = useState(0)
const [totalPages, setTotalPages] = useState(0)
const [selectedFilter, setSelectedFilter] = useState('Misc')
// Flag to prevent load data multiple times on component mount!
const componentMountRef = useRef(false)
// Handler for previous page
const handlePreviousPage = () => {
@@ -68,6 +74,20 @@ function NftsForSale (props) {
return offer
}
}, [appData])
// Function to process token metadata (iconUrl , userData).
const processOfferMetadata = useCallback(async (offer) => {
try {
// Token icon
if (offer.tokenIconUrl) {
offer.icon = offer.tokenIconUrl
offer.iconAlreadyDownloaded = true
offer.userData = JSON.parse(offer.userDataStr)
}
return offer
} catch (error) {
return offer
}
}, [])
// Fetch offers
const getNftOffers = useCallback(async (page = 0) => {
@@ -84,7 +104,8 @@ function NftsForSale (props) {
for (let i = 0; i < rawOffers.length; i++) {
const offer = rawOffers[i]
const processedOffer = await processTokenData(offer)
processedOffers.push(processedOffer)
const processedOfferMetadata = await processOfferMetadata(processedOffer)
processedOffers.push(processedOfferMetadata)
}
setOffersAreLoaded(true)
@@ -95,7 +116,7 @@ function NftsForSale (props) {
setOffersAreLoaded(true)
throw err
}
}, [processTokenData])
}, [processTokenData, processOfferMetadata])
// This function loads the token data .
const lazyLoadTokenData = useCallback(async (tokens) => {
@@ -164,6 +185,7 @@ function NftsForSale (props) {
const thisToken = tokens[i]
// Incon does not need to be downloaded, so continue with the next one
console.log('Icon already downloaded ', thisToken.iconAlreadyDownloaded)
if (thisToken.iconAlreadyDownloaded) continue
// Try to get token icon url from mutable data.
@@ -172,7 +194,7 @@ function NftsForSale (props) {
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
thisToken.tokenData.userData = userData
thisToken.userData = userData
}
// Mark token to prevent fetch token icon again.
@@ -244,17 +266,23 @@ function NftsForSale (props) {
// Effect to load NFTs on component mount
useEffect(() => {
console.log('loading nfts for sale')
loadNftOffers()
// Peevent to run multiple times
if (!componentMountRef.current) {
console.log('loading nfts for sale')
loadNftOffers()
componentMountRef.current = true
}
}, [loadNftOffers])
// Effect to reload data when page changes
useEffect(() => {
if (currentPage >= 0) {
// Validate to prevent load same page again
if (currentPage >= 0 && currentPage !== lastLoadedPage) {
console.log('page changed, reloading nfts for sale')
loadNftOffers(currentPage)
setLastLoadedPage(currentPage)
}
}, [currentPage, loadNftOffers])
}, [currentPage, loadNftOffers, lastLoadedPage])
// Handler for refresh button
const handleRefresh = useCallback(() => {
@@ -273,8 +301,34 @@ function NftsForSale (props) {
return url
}
const updateOffer = useCallback(async (offer) => {
try {
if (!offer) return
setOffersAreLoaded(false)
const processedOffer = await processTokenData(offer)
const processedOfferMetadata = await processOfferMetadata(processedOffer)
setOffers(offers => {
// find offer
const index = offers.findIndex((val) => { return val.tokenId === offer.tokenId })
// Save existing token data
processedOfferMetadata.tokenData = offers[index].tokenData
// replace with the new data
offers[index] = processedOfferMetadata
return offers
})
// Re-render tokens cards , to load the new data.
setTimeout(() => {
setOffersAreLoaded(true)
}, 500)
} catch (error) {
console.warn(error)
}
}, [processOfferMetadata, processTokenData])
// This function generates a Token Card for each token in the wallet.
function generateCards (offers) {
const generateCards = useCallback(() => {
console.log('generateCards() offerData: ', offers)
const tokens = offers
@@ -290,13 +344,14 @@ function NftsForSale (props) {
token={thisToken}
handleRefresh={handleRefresh}
key={`${thisToken.tokenId + i}`}
updateOffer={updateOffer}
/>
)
tokenCards.push(thisTokenCard)
}
return tokenCards
}
}, [offers, appData, handleRefresh, updateOffer])
return (
<Container>
@@ -304,6 +359,9 @@ function NftsForSale (props) {
<Col>
<h1>NFTs for Sale</h1>
</Col>
<Col className='d-flex justify-content-end'>
<FilterDropdown selectedFilter={selectedFilter} setSelectedFilter={setSelectedFilter} />
</Col>
</Row>
<Row>
<Col xs={12} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
@@ -339,7 +397,7 @@ function NftsForSale (props) {
{/* Pagination Controls */}
{offersAreLoaded && totalPages > 1 && (
<div
<div
style={{
position: 'fixed',
bottom: '70px',
@@ -395,7 +453,7 @@ function NftsForSale (props) {
Previous
</Button>
<div
<div
style={{
display: 'flex',
alignItems: 'center',
@@ -7,11 +7,18 @@
// Global npm libraries
import React, { useEffect, useState } from 'react'
import { Button, Modal, Container, Row, Col } from 'react-bootstrap'
import { Button, Modal, Container, Row, Col, Spinner } from 'react-bootstrap'
import axios from 'axios'
// Local libraries
import config from '../../../config'
// Global variables and constants
const SERVER = config.dexServer
function InfoButton (props) {
const [show, setShow] = useState(false)
const [mutableDataCid, setMutableDataCid] = useState(null)
const [loading, setLoading] = useState(false)
const handleClose = () => {
console.log('handleClose()')
@@ -36,7 +43,7 @@ function InfoButton (props) {
// Get token user data if it exists and verify if it contains media or markdown
useEffect(() => {
try {
const userDataStr = props.token.tokenData.userData
const userDataStr = props.token.userData
if (userDataStr) {
const userData = JSON.parse(userDataStr)
@@ -50,6 +57,22 @@ function InfoButton (props) {
}
}, [props.token, show])
// Update offer data
const updateOffer = async () => {
try {
setLoading(true)
const inputObj = { tokenId: props.token.tokenId }
const result = await axios.post(`${SERVER}/offer/mutable/sync/`, inputObj)
const offerData = result.data
console.log('offerData: ', offerData)
if (props.updateOffer) await props.updateOffer(offerData)
setLoading(false)
} catch (error) {
setLoading(false)
}
}
// Replace with dummy button until token data is loaded.
if (!props.token.tokenData) {
return (
@@ -105,6 +128,7 @@ function InfoButton (props) {
href={`/user-data/${props.token.tokenId}#single-view`}
target='_blank'
rel='noopener noreferrer'
variant='success'
>
View User Data
</Button>
@@ -114,7 +138,10 @@ function InfoButton (props) {
</Container>
</Modal.Body>
<Modal.Footer />
<Modal.Footer style={{ justifyContent: 'center' }}>
{!loading && <Button style={{ width: '135px' }} onClick={updateOffer}>Update</Button>}
{loading && <Spinner />}
</Modal.Footer>
</Modal>
</>
)
@@ -20,6 +20,7 @@ function TokenCard (props) {
// Update icon state every token.icon and token.tokenData changes
useEffect(() => {
console.log('setting icon')
setIcon(token.icon)
setTokenData(token.tokenData)
}, [token.icon, token.tokenData])
@@ -66,7 +67,7 @@ function TokenCard (props) {
<Row className='text-center'>
<Col>
<InfoButton token={token} disabled={!token.tokenData} />
<InfoButton token={token} disabled={!token.tokenData} {...props} />
</Col>
{!hideBuyBtn && (
+10 -2
View File
@@ -12,6 +12,8 @@ import ChatSidebar from './chat-sidebar'
import ChatMain from './chat-main'
import config from '../../../config'
// Global variables and constants
function NostrChat (props) {
const { appData } = props
const { nostrQueries, bchWalletState, startChannelChat } = appData
@@ -28,6 +30,8 @@ function NostrChat (props) {
const [selectedChannel, setSelectedChannel] = useState(null)
const [selectedChannelIsDm, setSelectedChannelIsDm] = useState(false)
const [deletedChats] = useState(appData.deletedChats)
const profilesRef = useRef({})
const dmChannelsRef = useRef([])
@@ -182,6 +186,9 @@ function NostrChat (props) {
// fetch messages when channel selected and channel metadata are loaded
if (!selectedChannel || !channelsLoaded || selectedChannelIsDm) return
// wait for deleted chats
if (!deletedChats || !Array.isArray(deletedChats)) return
// Load messages for group channel
const relays = nostrQueries.relays
if (relays.length === 0) {
@@ -204,7 +211,8 @@ function NostrChat (props) {
pool.on('event', (relay, subId, ev) => {
console.log('Group post retrieved from ', relay.url, ev.content)
const onBlackList = nostrQueries.blackList.find((val) => { return val === ev.pubkey })
if (!onBlackList)onMsgRead(ev)
const isDeleted = deletedChats.find((val) => { return val.eventId === ev.id })
if (!onBlackList && !isDeleted)onMsgRead(ev)
})
return () => {
@@ -212,7 +220,7 @@ function NostrChat (props) {
console.log('Close existing pool for group channel')
pool.close()
}
}, [onMsgRead, selectedChannel, selectedChannelIsDm, nostrQueries, channelsLoaded])
}, [onMsgRead, selectedChannel, selectedChannelIsDm, nostrQueries, channelsLoaded, deletedChats])
// Handle nostr pool for dm channels
useEffect(() => {
@@ -11,14 +11,13 @@
*/
import React, { useState, useEffect, useCallback } from 'react'
import { Container, Spinner, Dropdown } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFilter } from '@fortawesome/free-solid-svg-icons'
import { Container, Spinner } from 'react-bootstrap'
import axios from 'axios'
// Local libraries
import config from '../../../../config'
import ContentCard from './content-card'
import FilterDropdown from './filter-dropdown'
// Global variables and constants
const SERVER = config.dexServer
@@ -101,24 +100,10 @@ function ContentCreators (props) {
</div>
<div className='mb-4 d-flex justify-content-end'>
<Dropdown>
<Dropdown.Toggle variant='outline-secondary' className='d-flex align-items-center gap-2'>
<FontAwesomeIcon icon={faFilter} />
<span>{selectedFilter}</span>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onClick={() => setSelectedFilter('Most Followers')}>
Most Followers
</Dropdown.Item>
<Dropdown.Item onClick={() => setSelectedFilter('Most Tokens')}>
Most Tokens
</Dropdown.Item>
<Dropdown.Item onClick={() => setSelectedFilter('Most Likes')}>
Most Likes
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<FilterDropdown
selectedFilter={selectedFilter}
setSelectedFilter={setSelectedFilter}
/>
</div>
{!loaded && (
@@ -0,0 +1,31 @@
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFilter } from '@fortawesome/free-solid-svg-icons'
function FilterDropdown (props) {
const { selectedFilter, setSelectedFilter } = props
return (
<Dropdown>
<Dropdown.Toggle variant='outline-secondary' className='d-flex align-items-center gap-2'>
<FontAwesomeIcon icon={faFilter} />
<span>{selectedFilter}</span>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onClick={() => setSelectedFilter('Most Followers')}>
Most Followers
</Dropdown.Item>
<Dropdown.Item onClick={() => setSelectedFilter('Most Tokens')}>
Most Tokens
</Dropdown.Item>
<Dropdown.Item onClick={() => setSelectedFilter('Most Likes')}>
Most Likes
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
}
export default FilterDropdown
@@ -145,7 +145,7 @@ function NFTForSale (props) {
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
thisToken.tokenData.userData = userData
thisToken.userData = userData
}
// Mark token to prevent fetch token icon again.
+2 -2
View File
@@ -14,8 +14,8 @@ function SignMessage (props) {
const [sign, setSign] = useState('')
const [msg, setMsg] = useState('')
const [bchAddr] = useState(wallet.walletInfo.cashAddress)
const [slpAddr] = useState(wallet.walletInfo.slpAddress)
const [bchAddr] = useState(wallet?.walletInfo?.cashAddress)
const [slpAddr] = useState(wallet?.walletInfo?.slpAddress)
const [err, setErr] = useState('')
const [copied, setCopied] = useState(false)
+47 -25
View File
@@ -13,12 +13,17 @@ import TokenCard from './token-card'
import RefreshTokenBalance from './refresh-tokens'
const SlpTokens = (props) => {
const [appData, setAppData] = useState(props.appData)
const { appData } = props
const [iconsAreLoaded, setIconsAreLoaded] = useState(false)
const [dataAreLoaded, setDataAreLoaded] = useState(false)
const [tokens, setTokens] = useState([])
const refreshTokenButtonRef = React.useRef()
const { slpInitLoaded, asyncBackgroundFinished } = props.appData.asyncBackGroundInitState
// Background bch data loaded finished
const backgroundDataLoaded = slpInitLoaded || asyncBackgroundFinished
const backgroundDataError = !slpInitLoaded && asyncBackgroundFinished
// Update the tokens state when the appData changes
useEffect(() => {
@@ -31,8 +36,7 @@ const SlpTokens = (props) => {
// within the wallet app.
// This function triggers the on-click function within the refresh-tokens.js button.
const refreshTokens = async () => {
const newAppData = await refreshTokenButtonRef.current.handleRefreshTokenBalance()
setAppData(newAppData)
await refreshTokenButtonRef.current.handleRefreshTokenBalance()
}
// Get Cid from url
@@ -120,7 +124,7 @@ const SlpTokens = (props) => {
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
thisToken.tokenData.userData = userData
thisToken.userData = userData
}
// Mark token to prevent fetch token icon again.
@@ -135,6 +139,7 @@ const SlpTokens = (props) => {
const loadData = useCallback(async () => {
const tokens = appData.bchWalletState.slpTokens
console.log('tokens', tokens)
setTokens(tokens)
await lazyLoadTokenData(tokens)
await lazyLoadMutableData(tokens)
@@ -142,8 +147,10 @@ const SlpTokens = (props) => {
// Start to load the token icons when the component is mounted
useEffect(() => {
loadData()
}, [loadData])
if (slpInitLoaded) {
loadData()
}
}, [loadData, slpInitLoaded])
// Generate the token cards for each token in the wallet.
const generateCards = () => {
@@ -176,25 +183,35 @@ const SlpTokens = (props) => {
</Col>
</Row>
<Row>
<Col xs={12} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{/** Show spinner info if tokens are loaded but data is not loaded */
!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' }}>
<span style={{ marginRight: '10px' }}>Loading Token Icons </span>
<Spinner animation='border' />
</div>
)
}
{appData.asyncInitSucceeded && (
<Col xs={12} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{/** Show spinner info if tokens are loaded but data is not loaded */
!backgroundDataLoaded && !backgroundDataError && (
<div style={{ borderRadius: '10px', backgroundColor: '#f0f0f0', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center', width: 'fit-content' }}>
<span style={{ marginRight: '10px' }}>Loading Tokens </span>
<Spinner animation='border' />
</div>
)
}
{/** Show spinner info if tokens are loaded but data is not loaded */
!backgroundDataError && !dataAreLoaded && tokens.length > 0 && (
<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 */
backgroundDataLoaded && dataAreLoaded && !iconsAreLoaded && (
<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>
<Spinner animation='border' />
</div>
)
}
</Col>
</Col>
)}
</Row>
<br />
@@ -202,11 +219,16 @@ const SlpTokens = (props) => {
{generateCards()}
</Row>
{/** Display a message if no tokens are found */}
{tokens.length === 0 && (
{backgroundDataLoaded && !backgroundDataError && tokens.length === 0 && (
<Row className='text-center'>
<span> No tokens found in wallet </span>
</Row>
)}
{backgroundDataError && (
<Row style={{ color: 'red' }} className='text-center'>
<span>Tokens could not be loaded! </span>
</Row>
)}
</Container>
</>
@@ -59,7 +59,7 @@ function InfoButton (props) {
useEffect(() => {
try {
console.log('props token', props.token)
const userDataStr = props.token.tokenData.userData
const userDataStr = props.token.userData
if (userDataStr) {
const userData = JSON.parse(userDataStr)
@@ -18,6 +18,10 @@ function RefreshTokenBalance ({ appData: initialAppData, ref, lazyLoadTokenIcons
const [modalBody, setModalBody] = useState([])
const [hideSpinner, setHideSpinner] = useState(false)
const [hideWaitingModal, setHideWaitingModal] = useState(true)
const { slpInitLoaded, asyncBackgroundFinished } = initialAppData.asyncBackGroundInitState
// Background bch data loaded finished
const backgroundDataLoaded = slpInitLoaded || asyncBackgroundFinished
// Add a new line to the waiting modal.
const addToModal = (inStr) => {
@@ -58,6 +62,10 @@ function RefreshTokenBalance ({ appData: initialAppData, ref, lazyLoadTokenIcons
appData.updateBchWalletState({ walletObj: walletState, appData })
const newAppData = { ...appData, bchWalletState: walletState }
// if slpInitLoaded is 'false', them set as true , to show the new balance.
appData.updateBackGroundInitState({ slpInitLoaded: true })
// Update state
setHideWaitingModal(true)
setAppData(newAppData)
@@ -82,7 +90,7 @@ function RefreshTokenBalance ({ appData: initialAppData, ref, lazyLoadTokenIcons
return (
<>
<Button variant='success' onClick={handleRefreshTokenBalance}>
<Button variant='success' onClick={handleRefreshTokenBalance} disabled={!backgroundDataLoaded}>
<FontAwesomeIcon icon={faRedo} size='lg' /> Refresh
</Button>
+1 -1
View File
@@ -21,7 +21,7 @@ export function UninitializedView (props = {}) {
/>
{
appData.asyncInitFinished
? <AppBody menuState={100} wallet={appData.wallet} appData={appData} />
? <> <br /><AppBody menuState={100} wallet={appData.wallet} appData={appData} /></>
: null
}
</>
+34 -1
View File
@@ -47,6 +47,14 @@ function useAppState () {
const [denyClose, setDenyClose] = useState(false)
const [isSingleView, setIsSingleView] = useState(false)
// Deleted nostr chats.
const [deletedChats, setDeletedChats] = useState([])
// Background process state
const [asyncBackGroundInitState, setAsyncBackGroundInitState] = useState({
bchInitLoaded: false, slpInitLoaded: false, asyncBackgroundFinished: false
})
// NFTs for sale stored data to improve performance
const [nftForSaleCacheData, setNftForSaleCacheData] = useState(lsState.nftData || {})
@@ -130,6 +138,7 @@ function useAppState () {
setNftForSaleCacheData(allCacheData) // Update the state
updateLocalStorage({ nftData: allCacheData }) // Update the local storage
}
// Update relays data
function updateRelaysData (relaysData) {
setRelaysData(relaysData)
@@ -144,6 +153,7 @@ function useAppState () {
console.log('writeRelays: ', writeRelays)
nostrQueriesRef.current = new NostrQueries({ relays: readRelays })
}
// Restore relays data
function restoreRelaysData () {
const relaysData = [...localStorageDefault.relays] // Create a new array in order to detect changes
@@ -160,6 +170,25 @@ function useAppState () {
nostrQueriesRef.current = new NostrQueries({ relays: readRelays })
}
// Update background state
function updateBackGroundInitState (inObj = {}) {
try {
setAsyncBackGroundInitState(oldState => {
// console.log('background old state: ', oldState)
const state = Object.assign({}, oldState, inObj)
// console.log('background state: ', state)
return state
})
// console.log(`New wallet state: ${JSON.stringify(bchWalletState, null, 2)}`)
} catch (err) {
console.error('Error in App.js updateBackGroundInitState()')
throw err
}
}
return {
serverUrl,
setServerUrl,
@@ -210,7 +239,11 @@ function useAppState () {
readRelays,
writeRelays,
startChannelChat,
setStartChannelChat
setStartChannelChat,
asyncBackGroundInitState,
updateBackGroundInitState,
deletedChats,
setDeletedChats
}
}
+28
View File
@@ -14,6 +14,9 @@ import { base58_to_binary as base58ToBinary } from 'base58-js'
import { bytesToHex } from '@noble/hashes/utils' // already an installed dependency
import { getPublicKey } from 'nostr-tools/pure'
import * as nip19 from 'nostr-tools/nip19'
import config from '../config'
const SERVER = `${config.dexServer}/`
class AsyncLoad {
constructor () {
@@ -90,6 +93,11 @@ class AsyncLoad {
// Get the BCH balance of the wallet.
async getWalletBchBalance (wallet, updateBchWalletState, appData) {
try {
/* // Force error for development
await sleep(6000)
throw new Error('getWalletBchBalance error')
*/
// Get the BCH balance of the wallet.
const bchBalance = await wallet.getBalance({ bchAddress: wallet.walletInfo.cashAddress })
@@ -125,6 +133,10 @@ class AsyncLoad {
// Get a list of SLP tokens held by the wallet.
async getSlpTokenBalances (wallet, updateBchWalletState, appData) {
try {
/* // Force error for development
await sleep(6000)
throw new Error('getSlpTokenBalances error')
*/
// Get token information from the wallet. This will also initialize the UTXO store.
const slpTokens = await wallet.listTokens(wallet.walletInfo.cashAddress)
// console.log('slpTokens: ', slpTokens)
@@ -324,6 +336,22 @@ class AsyncLoad {
throw error
}
}
// Get all deleted chats
async fetchDeletedChats () {
try {
const options = {
method: 'GET',
url: `${SERVER}nostr/deletedChat`
}
const result = await axios.request(options)
const { deletedChats } = result.data
return deletedChats
} catch (error) {
console.error('Error deletedChats: ', error)
throw error
}
}
}
function sleep (ms) {