Fix: file-manager and swarm-desktop bugs (#714)

- drive capacity display with stamp polling
- download/upload progress handling
- overlay and tooltip issues
- FileMaganger readme
- ultra-light mode handling
- account feed view page
- download media files
- remove not found syncing link
- fix ultra light node wallet page
- tooltip issues
---------
Co-authored-by: Andrei Mitrea <andrei.mitrea.hq@gmail.com>
Co-authored-by: nidishk <nidishkrishnan45@gmail.com>
Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
Co-authored-by: Nándor Komlódi <nandor.komlodi@gmail.com>
Co-authored-by: rolandlor <33499567+rolandlor@users.noreply.github.com>
This commit is contained in:
Bálint Ujvári
2026-01-26 12:57:14 +01:00
committed by GitHub
parent ecadafd21d
commit 0d5138f5bc
78 changed files with 3961 additions and 1194 deletions
+6 -4
View File
@@ -8,7 +8,6 @@ import Link from 'remixicon-react/LinkIcon'
import ExpandableListItem from '../../../components/ExpandableListItem'
import ExpandableListItemActions from '../../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../../components/ExpandableListItemKey'
import { Loading } from '../../../components/Loading'
import { SwarmButton } from '../../../components/SwarmButton'
import TroubleshootConnectionCard from '../../../components/TroubleshootConnectionCard'
import { Context as BeeContext, CheckState } from '../../../providers/Bee'
@@ -16,6 +15,7 @@ import { Context as SettingsContext } from '../../../providers/Settings'
import { ROUTES } from '../../../routes'
import { AccountNavigation } from '../AccountNavigation'
import { Header } from '../Header'
import { WalletInfoCard } from '../../../pages/info/WalletInfoCard'
export function AccountWallet(): ReactElement {
const { nodeAddresses, nodeInfo, status, walletBalance } = useContext(BeeContext)
@@ -44,7 +44,7 @@ export function AccountWallet(): ReactElement {
<Box mb={4}>
<Grid container direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="h2">Wallet balance</Typography>
{isDesktop && (
{isDesktop && walletBalance && (
<SwarmButton onClick={onDeposit} iconType={Download}>
Top up wallet
</SwarmButton>
@@ -70,8 +70,10 @@ export function AccountWallet(): ReactElement {
</Box>
</>
) : (
<Box mb={8}>
<Loading />
<Box mb={8} alignItems="center" display="flex" justifyContent="center">
<Box maxWidth={400}>
<WalletInfoCard />
</Box>
</Box>
)}
<ExpandableListItemActions>
+1 -1
View File
@@ -48,7 +48,7 @@ export default function CreateNewFeed(): ReactElement {
return
}
const wallet = generateWallet()
const stamps = await beeApi.getAllPostageBatch()
const stamps = await beeApi.getPostageBatches()
if (!stamps || !stamps.length) {
enqueueSnackbar(<span>No stamp available</span>, { variant: 'error' })
+3 -3
View File
@@ -27,6 +27,8 @@ export function FeedSubpage(): ReactElement {
useEffect(() => {
if (!identity || !identity.feedHash) {
navigate(ROUTES.ACCOUNT_FEEDS, { replace: true })
return
}
@@ -35,11 +37,9 @@ export function FeedSubpage(): ReactElement {
} catch {
setAvailable(false)
}
}, [beeApi, uuid, identity])
}, [beeApi, uuid, identity, navigate])
if (!identity || !status.all) {
navigate(ROUTES.ACCOUNT_FEEDS, { replace: true })
return <></>
}
+33 -13
View File
@@ -8,6 +8,7 @@ import { AdminStatusBar } from '../../modules/filemanager/components/AdminStatus
import { FileBrowser } from '../../modules/filemanager/components/FileBrowser/FileBrowser'
import { InitialModal } from '../../modules/filemanager/components/InitialModal/InitialModal'
import { Context as FMContext } from '../../providers/FileManager'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context as BeeContext, CheckState } from '../../providers/Bee'
import { PrivateKeyModal } from '../../modules/filemanager/components/PrivateKeyModal/PrivateKeyModal'
import { getSignerPk, removeSignerPk } from '../../../src/modules/filemanager/utils/common'
@@ -26,8 +27,10 @@ export function FileManagerPage(): ReactElement {
const [errorMessage, setErrorMessage] = useState<string>('')
const [showResetModal, setShowResetModal] = useState<boolean>(false)
const [isCreationInProgress, setIsCreationInProgress] = useState<boolean>(false)
const [showConnectionError, setShowConnectionError] = useState<boolean>(false)
const { status } = useContext(BeeContext)
const { beeApi } = useContext(SettingsContext)
const { fm, shallReset, adminDrive, initializationError, init } = useContext(FMContext)
useEffect(() => {
@@ -39,6 +42,18 @@ export function FileManagerPage(): ReactElement {
}, [])
useEffect(() => {
if (status.all !== CheckState.OK) {
setShowConnectionError(true)
} else {
setShowConnectionError(false)
}
}, [status.all])
useEffect(() => {
if (!beeApi) {
return
}
if (!hasPk) {
setIsLoading(false)
@@ -71,7 +86,7 @@ export function FileManagerPage(): ReactElement {
}
setIsLoading(true)
}, [fm, hasPk, initializationError, adminDrive, shallReset])
}, [fm, beeApi, hasPk, initializationError, adminDrive, shallReset])
const handlePrivateKeySaved = useCallback(async () => {
if (!isMountedRef.current) return
@@ -111,16 +126,6 @@ export function FileManagerPage(): ReactElement {
const isFormbricksActive = Boolean(fm && fm.adminStamp && adminDrive && !showInitialModal && !loading)
if (status.all !== CheckState.OK) {
return (
<div className="fm-main">
<div className="fm-loading">
<div className="fm-loading-title">Bee node error - cannot load File Manager</div>
</div>
</div>
)
}
if (!hasPk) {
return (
<div className="fm-main">
@@ -174,7 +179,13 @@ export function FileManagerPage(): ReactElement {
<InitialModal
resetState={shallReset}
handleVisibility={(isVisible: boolean) => setShowInitialModal(isVisible)}
handleShowError={(flag: boolean) => setShowErrorModal(flag)}
handleShowError={(flag: boolean, error?: string) => {
setShowErrorModal(flag)
if (error) {
setErrorMessage(error)
}
}}
setIsCreationInProgress={(isCreating: boolean) => setIsCreationInProgress(isCreating)}
/>
</div>
@@ -196,10 +207,13 @@ export function FileManagerPage(): ReactElement {
if (showErrorModal) {
return (
<ErrorModal
label={'Error during admin state creation, try again'}
label={
'Error creating Admin Drive. Please try again. Possible causes include insufficient xDAI balance or a lost connection to the RPC.'
}
onClick={() => {
setShowErrorModal(false)
setShowInitialModal(true)
setErrorMessage('')
}}
/>
)
@@ -209,6 +223,12 @@ export function FileManagerPage(): ReactElement {
<SearchProvider>
<ViewProvider>
<div className="fm-main">
{showConnectionError && fm && (
<ErrorModal
label="Bee node connection error. Please check your node status. File Manager will continue when connection is restored."
onClick={() => setShowConnectionError(false)}
/>
)}
<FormbricksIntegration isActive={isFormbricksActive} />
<Header />
<div className="fm-main-content">
+5
View File
@@ -9,6 +9,7 @@ import { getHumanReadableFileSize } from '../../utils/file'
import { shortenHash } from '../../utils/hash'
import { AssetIcon } from './AssetIcon'
import { FitVideo } from '../../components/FitVideo'
import { FitAudio } from '../../components/FitAudio'
interface Props {
previewUri?: string
@@ -21,6 +22,10 @@ const getPreviewComponent = (previewUri?: string, metadata?: Metadata) => {
return () => <FitVideo src={previewUri} maxWidth="250px" maxHeight="175px" />
}
if (metadata?.isAudio) {
return () => <FitAudio src={previewUri} maxWidth="250px" />
}
if (metadata?.isImage) {
return () => <FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} />
}
+2 -1
View File
@@ -83,7 +83,8 @@ export function AssetSyncing({ reference }: Props): ReactElement {
<DocumentationText>
Files are not immediately accessible on the Swarm network. Please wait until your upload is synced to the
network.{' '}
<a href="https://docs.ethswarm.org/docs/develop/access-the-swarm/syncing">Learn more about syncing</a>.
{/* TODO: syncing article was removed, now the only available doc. is at https://docs.ethswarm.org/api/#tag/Tag */}
{/* <a href="https://docs.ethswarm.org/docs/develop/access-the-swarm/syncing">Learn more about syncing</a>. */}
</DocumentationText>
</Box>
<Box mb={4}>
+71 -7
View File
@@ -1,9 +1,9 @@
import { Box, Typography } from '@material-ui/core'
import { MantarayNode, NULL_ADDRESS } from '@ethersphere/bee-js'
import { Bytes, MantarayNode, NULL_ADDRESS } from '@ethersphere/bee-js'
import { saveAs } from 'file-saver'
import JSZip from 'jszip'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react'
import { ReactElement, useContext, useEffect, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { HistoryHeader } from '../../components/HistoryHeader'
import { Loading } from '../../components/Loading'
@@ -36,6 +36,8 @@ export function Share(): ReactElement {
const [preview, setPreview] = useState<string | undefined>(undefined)
const [metadata, setMetadata] = useState<Metadata | undefined>()
const isMountedRef = useRef(true)
async function prepare() {
if (!beeApi || !status.all) {
return
@@ -56,37 +58,53 @@ export function Share(): ReactElement {
const entries = manifest.collectAndMap()
delete entries[META_FILE_NAME]
if (!isMountedRef.current) return
setSwarmEntries(entries)
const docsMetadata = manifest.getDocsMetadata()
// needed in catch block, shadows the outer variable
const indexDocument = docsMetadata.indexDocument
if (!isMountedRef.current) return
setIndexDocument(indexDocument)
try {
const remoteMetadata = await beeApi.downloadFile(reference, META_FILE_NAME)
const formattedMetadata = remoteMetadata.data.toJSON() as Metadata
if (formattedMetadata.isVideo || formattedMetadata.isImage) {
if (formattedMetadata.isVideo || formattedMetadata.isAudio || formattedMetadata.isImage) {
if (!isMountedRef.current) return
setPreview(`${apiUrl}/bzz/${reference}`)
}
if (!isMountedRef.current) return
setMetadata({ ...formattedMetadata, hash })
} catch (e) {
// if metadata is not available or invalid go with the default one
const count = Object.keys(entries).length
if (!isMountedRef.current) return
setMetadata({
hash,
type: count > 1 ? 'folder' : 'unknown',
name: reference,
count,
isWebsite: Boolean(indexDocument && /.*\.html?$/i.test(indexDocument)),
isVideo: Boolean(indexDocument && /.*\.(mp4|webm|ogg|mp3|ogg|wav)$/i.test(indexDocument)),
isVideo: Boolean(indexDocument && /.*\.(mp4|webm|ogv)$/i.test(indexDocument)),
isAudio: Boolean(indexDocument && /.*\.(mp3|ogg|oga|wav|webm|m4a|aac|flac)$/i.test(indexDocument)),
isImage: Boolean(indexDocument && /.*\.(jpg|jpeg|png|gif|webp|svg)$/i.test(indexDocument)),
// naive assumption based on indexDocument, we don't want to download the whole manifest
})
}
} catch {
if (!isMountedRef.current) return
setNotFound(true)
enqueueSnackbar('The specified hash does not contain valid content.', { variant: 'error' })
@@ -112,9 +130,16 @@ export function Share(): ReactElement {
navigate(ROUTES.ACCOUNT_FEEDS_UPDATE.replace(':hash', reference))
}
useEffect(() => {
return () => {
isMountedRef.current = false
}
}, [])
useEffect(() => {
setLoading(true)
prepare().finally(() => {
if (!isMountedRef.current) return
setLoading(false)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -124,19 +149,58 @@ export function Share(): ReactElement {
if (!beeApi) {
return
}
putHistory(HISTORY_KEYS.DOWNLOAD_HISTORY, reference, determineHistoryName(reference, indexDocument))
setDownloading(true)
if (Object.keys(swarmEntries).length === 1) {
window.open(`${apiUrl}/bzz/${reference}/`, '_blank')
const singleFileName = Object.keys(swarmEntries)[0]
const singleFileHash = Object.values(swarmEntries)[0]
let fileData: Bytes
try {
fileData = await beeApi.downloadData(singleFileHash)
} catch (err) {
// eslint-disable-next-line no-console
console.error('Failed to download file: ', err)
return
}
const dataArray = fileData.toUint8Array()
const arrayBuffer = new ArrayBuffer(dataArray.length)
const view = new Uint8Array(arrayBuffer)
view.set(dataArray)
const blob = new Blob([arrayBuffer], { type: metadata?.type || 'application/octet-stream' })
saveAs(blob, metadata?.name || singleFileName || reference)
} else {
const zip = new JSZip()
for (const [path, hash] of Object.entries(swarmEntries)) {
zip.file(path, (await beeApi.downloadData(hash)).toUint8Array())
try {
zip.file(path, (await beeApi.downloadData(hash)).toUint8Array())
} catch (err) {
// eslint-disable-next-line no-console
console.error('Failed to download files: ', err)
return
}
}
const content = await zip.generateAsync({ type: 'blob' })
let content: Blob
try {
content = await zip.generateAsync({ type: 'blob' })
} catch (err) {
// eslint-disable-next-line no-console
console.error('Failed to compress file: ', err)
return
}
saveAs(content, reference + '.zip')
}
if (!isMountedRef.current) return
setDownloading(false)
}
+41 -21
View File
@@ -9,8 +9,10 @@ import { useNavigate } from 'react-router-dom'
import { DocumentationText } from '../../components/DocumentationText'
import { SwarmButton } from '../../components/SwarmButton'
import { Context, UploadOrigin } from '../../providers/File'
import { Context as BeeContext } from '../../providers/Bee'
import { ROUTES } from '../../routes'
import { detectIndexHtml } from '../../utils/file'
import { BeeModes } from '@ethersphere/bee-js'
interface Props {
uploadOrigin: UploadOrigin
@@ -51,6 +53,7 @@ const useStyles = makeStyles((theme: Theme) =>
export function UploadArea({ uploadOrigin, showHelp }: Props): ReactElement {
const { setFiles, setUploadOrigin } = useContext(Context)
const { nodeInfo } = useContext(BeeContext)
const classes = useStyles()
const navigate = useNavigate()
const { enqueueSnackbar } = useSnackbar()
@@ -121,35 +124,52 @@ export function UploadArea({ uploadOrigin, showHelp }: Props): ReactElement {
}
}
const isUploadEnabled = nodeInfo?.beeMode !== BeeModes.ULTRA_LIGHT
return (
<>
<div className={classes.areaWrapper}>
<DropzoneArea
key={version}
dropzoneClass={classes.dropzone}
onChange={handleChange}
filesLimit={1e9}
maxFileSize={MAX_FILE_SIZE}
showPreviews={false}
/>
<div className={classes.buttonWrapper}>
<SwarmButton className={classes.button} onClick={onUploadFileClick} iconType={FilePlus}>
Add File
</SwarmButton>
<SwarmButton className={classes.button} onClick={onUploadFolderClick} iconType={FolderPlus}>
Add Folder
</SwarmButton>
<SwarmButton className={classes.button} onClick={onUploadWebsiteClick} iconType={PlusCircle}>
Add Website
</SwarmButton>
{isUploadEnabled && (
<div className={classes.areaWrapper}>
<DropzoneArea
key={version}
dropzoneClass={classes.dropzone}
onChange={handleChange}
filesLimit={1e9}
maxFileSize={MAX_FILE_SIZE}
showPreviews={false}
/>
<div className={classes.buttonWrapper}>
<SwarmButton className={classes.button} onClick={onUploadFileClick} iconType={FilePlus}>
Add File
</SwarmButton>
<SwarmButton className={classes.button} onClick={onUploadFolderClick} iconType={FolderPlus}>
Add Folder
</SwarmButton>
<SwarmButton className={classes.button} onClick={onUploadWebsiteClick} iconType={PlusCircle}>
Add Website
</SwarmButton>
</div>
</div>
</div>
{showHelp && (
)}
{isUploadEnabled && showHelp && (
<DocumentationText>
You can click the buttons above or simply drag and drop to add a file or folder. To upload a website to Swarm,
make sure that your folder contains an index.html file.
</DocumentationText>
)}
{!isUploadEnabled && (
<DocumentationText>
Uploading files requires running a light node. Please{' '}
<a
href="https://docs.ethswarm.org/docs/desktop/configuration/#upgrading-from-an-ultra-light-to-a-light-node"
target="_blank"
rel="noreferrer"
>
upgrade
</a>{' '}
to continue.
</DocumentationText>
)}
</>
)
}
+14
View File
@@ -0,0 +1,14 @@
import { ReactElement } from 'react'
import { HistoryHeader } from '../../components/HistoryHeader'
import { Typography } from '@material-ui/core'
export default function PageNotFound(): ReactElement {
return (
<div>
<HistoryHeader>Page not found</HistoryHeader>
<Typography>
The given url is invalid. Please go back or <a href="/">navigate to Home screen.</a>
</Typography>
</div>
)
}