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:
@@ -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>
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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 <></>
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user