313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
import { PostageBatch } from '@ethersphere/bee-js'
|
|
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
|
|
import { ReactElement, useContext, useEffect, useState } from 'react'
|
|
import Add from 'remixicon-react/AddLineIcon'
|
|
import ArrowDown from 'remixicon-react/ArrowDownSLineIcon'
|
|
import ArrowRight from 'remixicon-react/ArrowRightSLineIcon'
|
|
import DeleteFill from 'remixicon-react/DeleteBin6FillIcon'
|
|
import Delete from 'remixicon-react/DeleteBin6LineIcon'
|
|
import FolderFill from 'remixicon-react/Folder3FillIcon'
|
|
import Folder from 'remixicon-react/Folder3LineIcon'
|
|
import HistoryFill from 'remixicon-react/HistoryFillIcon'
|
|
import History from 'remixicon-react/HistoryLineIcon'
|
|
|
|
import { useView } from '../../../../pages/filemanager/ViewContext'
|
|
import { Context as FMContext } from '../../../../providers/FileManager'
|
|
import { Context as SettingsContext } from '../../../../providers/Settings'
|
|
import { FILE_MANAGER_EVENTS } from '../../constants/common'
|
|
import { ViewType } from '../../constants/transfers'
|
|
import { getUsableStamps } from '../../utils/bee'
|
|
import { truncateNameMiddle } from '../../utils/common'
|
|
import { CreateDriveModal } from '../CreateDriveModal/CreateDriveModal'
|
|
|
|
import { DriveItem } from './DriveItem/DriveItem'
|
|
import { ExpiredDriveItem } from './DriveItem/ExpiredDriveItem'
|
|
|
|
import './Sidebar.scss'
|
|
|
|
interface SidebarProps {
|
|
loading: boolean
|
|
errorMessage?: string
|
|
setErrorMessage?: (error: string) => void
|
|
}
|
|
|
|
export function Sidebar({ setErrorMessage, loading }: SidebarProps): ReactElement {
|
|
const [hovered, setHovered] = useState<string | null>(null)
|
|
const [isMyDrivesOpen, setIsMyDriveOpen] = useState(true)
|
|
const [isTrashOpen, setIsTrashOpen] = useState(false)
|
|
const [isCreateDriveOpen, setIsCreateDriveOpen] = useState(false)
|
|
const [usableStamps, setUsableStamps] = useState<PostageBatch[]>([])
|
|
const [isDriveCreationInProgress, setIsDriveCreationInProgress] = useState(false)
|
|
const [creatingDriveName, setCreatingDriveName] = useState<string | null>(null)
|
|
const [isExpiredOpen, setIsExpiredOpen] = useState(false)
|
|
|
|
const { beeApi } = useContext(SettingsContext)
|
|
const { setView, view } = useView()
|
|
const {
|
|
fm,
|
|
currentDrive,
|
|
currentStamp,
|
|
drives,
|
|
expiredDrives,
|
|
setCurrentDrive,
|
|
setCurrentStamp,
|
|
setShowError,
|
|
syncDrives,
|
|
} = useContext(FMContext)
|
|
|
|
useEffect(() => {
|
|
let isMounted = true
|
|
|
|
const getStamps = async () => {
|
|
if (!beeApi) {
|
|
return
|
|
}
|
|
|
|
const stamps = await getUsableStamps(beeApi)
|
|
|
|
if (isMounted) {
|
|
setUsableStamps([...stamps])
|
|
}
|
|
}
|
|
|
|
if (beeApi) {
|
|
getStamps()
|
|
}
|
|
|
|
const handleUpgradeEnd = async () => {
|
|
if (isMounted && beeApi) {
|
|
await getStamps()
|
|
}
|
|
}
|
|
|
|
window.addEventListener(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_END, handleUpgradeEnd as EventListener)
|
|
|
|
return () => {
|
|
isMounted = false
|
|
window.removeEventListener(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_END, handleUpgradeEnd as EventListener)
|
|
}
|
|
}, [beeApi, drives])
|
|
|
|
useEffect(() => {
|
|
if (!fm || drives.length === 0) {
|
|
return
|
|
}
|
|
|
|
if (!currentDrive) {
|
|
const firstDrive = drives[0]
|
|
setCurrentDrive(firstDrive)
|
|
setView(ViewType.File)
|
|
}
|
|
|
|
if (currentDrive && usableStamps.length > 0) {
|
|
const correspondingStamp = usableStamps.find(s => s.batchID.toString() === currentDrive.batchId.toString())
|
|
|
|
if (correspondingStamp) {
|
|
setCurrentStamp(correspondingStamp)
|
|
}
|
|
}
|
|
}, [fm, drives, currentDrive, usableStamps, setCurrentDrive, setCurrentStamp, setView, beeApi])
|
|
|
|
const handleCreateNewDrive = () => {
|
|
if (isDriveCreationInProgress) {
|
|
return
|
|
}
|
|
|
|
setIsCreateDriveOpen(true)
|
|
}
|
|
|
|
const isCurrent = (di: DriveInfo) => currentDrive?.id.toString() === di.id.toString()
|
|
|
|
return (
|
|
<div className="fm-sidebar">
|
|
<div className="fm-sidebar-content">
|
|
{!loading && (
|
|
<>
|
|
<div
|
|
className={`fm-sidebar-item ${isDriveCreationInProgress ? 'disabled' : ''}`}
|
|
onClick={() => handleCreateNewDrive()}
|
|
>
|
|
<div className="fm-sidebar-item-icon">
|
|
<Add size="16px" />
|
|
</div>
|
|
<div>Create new drive</div>
|
|
</div>
|
|
{isDriveCreationInProgress && (
|
|
<div className="fm-sidebar-item-description">
|
|
{truncateNameMiddle(creatingDriveName || 'Your Drive', 35, 8, 8)} is currently being created.
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{isCreateDriveOpen && (
|
|
<CreateDriveModal
|
|
onCancelClick={() => setIsCreateDriveOpen(false)}
|
|
onDriveCreated={() => {
|
|
setIsCreateDriveOpen(false)
|
|
setIsDriveCreationInProgress(false)
|
|
setCreatingDriveName(null)
|
|
}}
|
|
onCreationStarted={(driveName: string) => {
|
|
setIsDriveCreationInProgress(true)
|
|
setCreatingDriveName(driveName)
|
|
}}
|
|
onCreationError={(name: string) => {
|
|
setIsDriveCreationInProgress(false)
|
|
setErrorMessage?.(
|
|
`Error creating drive ${name}. Please try again. Possible causes include insufficient xDAI balance or a lost connection to the RPC.`,
|
|
)
|
|
setShowError(true)
|
|
setCreatingDriveName(null)
|
|
|
|
return
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<div
|
|
className="fm-sidebar-item"
|
|
onMouseEnter={() => setHovered('my-drives')}
|
|
onMouseLeave={() => setHovered(null)}
|
|
onClick={() => setIsMyDriveOpen(!isMyDrivesOpen)}
|
|
>
|
|
<div className="fm-sidebar-item-icon">
|
|
{isMyDrivesOpen ? <ArrowDown size="16px" /> : <ArrowRight size="16px" />}
|
|
</div>
|
|
<div className="fm-sidebar-item-icon" style={{ opacity: hovered === 'my-drives' ? 1 : 1 }}>
|
|
{hovered === 'my-drives' ? <FolderFill size="16px" /> : <Folder size="16px" />}
|
|
</div>
|
|
<div>My Drives</div>
|
|
</div>
|
|
|
|
{isMyDrivesOpen && isDriveCreationInProgress && (
|
|
<div className="fm-drive-item-container fm-drive-item-creating" aria-live="polite">
|
|
<div className="fm-drive-item-info">
|
|
<div className="fm-drive-item-header">
|
|
<div className="fm-drive-item-icon">
|
|
<Folder size="16px" />
|
|
</div>
|
|
</div>
|
|
<div className="fm-drive-item-content">
|
|
<div className="fm-drive-item-capacity">Initializing drive metadata</div>
|
|
</div>
|
|
</div>
|
|
<div className="fm-drive-item-actions" />
|
|
<div className="fm-drive-item-creating-overlay">
|
|
<div className="fm-mini-spinner" />
|
|
<span>Please wait…</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{isMyDrivesOpen &&
|
|
drives.map(d => {
|
|
const isSelected = isCurrent(d) && view === ViewType.File
|
|
const localStamp = usableStamps.find(s => s.batchID.toString() === d.batchId.toString() && !d.isAdmin)
|
|
const stamp = isSelected && currentStamp ? currentStamp : localStamp
|
|
|
|
return (
|
|
stamp && (
|
|
<div
|
|
key={d.id.toString()}
|
|
onClick={() => {
|
|
setCurrentDrive(d)
|
|
setCurrentStamp(stamp)
|
|
setView(ViewType.File)
|
|
}}
|
|
>
|
|
<DriveItem drive={d} stamp={stamp} isSelected={isSelected} setErrorMessage={setErrorMessage} />
|
|
</div>
|
|
)
|
|
)
|
|
})}
|
|
|
|
{expiredDrives.length > 0 && (
|
|
<>
|
|
<div
|
|
className="fm-sidebar-item"
|
|
onMouseEnter={() => setHovered('expired')}
|
|
onMouseLeave={() => setHovered(null)}
|
|
onClick={() => setIsExpiredOpen(prev => !prev)}
|
|
>
|
|
<div className="fm-sidebar-item-icon">
|
|
{isExpiredOpen ? <ArrowDown size="16px" /> : <ArrowRight size="16px" />}
|
|
</div>
|
|
<div className="fm-sidebar-item-icon">
|
|
{hovered === 'expired' ? <HistoryFill size="16px" /> : <History size="16px" />}
|
|
</div>
|
|
<div>Expired drives</div>
|
|
</div>
|
|
|
|
{isExpiredOpen && (
|
|
<div className="fm-drive-items-container fm-drive-items-container-open">
|
|
{expiredDrives.map(d => (
|
|
<div
|
|
key={`${d.id.toString()}-expired`}
|
|
onClick={() => {
|
|
setCurrentDrive(d)
|
|
setView(ViewType.Expired)
|
|
}}
|
|
>
|
|
<ExpiredDriveItem
|
|
drive={d}
|
|
onForgot={async () => {
|
|
await syncDrives()
|
|
setCurrentDrive(drives.length > 0 ? drives[0] : undefined)
|
|
setView(ViewType.File)
|
|
}}
|
|
setErrorMessage={setErrorMessage}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<div
|
|
className="fm-sidebar-item"
|
|
onMouseEnter={() => setHovered(ViewType.Trash)}
|
|
onMouseLeave={() => setHovered(null)}
|
|
onClick={() => setIsTrashOpen(!isTrashOpen)}
|
|
>
|
|
<div className="fm-sidebar-item-icon">
|
|
{isTrashOpen ? <ArrowDown size="16px" /> : <ArrowRight size="16px" />}
|
|
</div>
|
|
<div className="fm-sidebar-item-icon">
|
|
{hovered === ViewType.Trash ? <DeleteFill size="16px" /> : <Delete size="16px" />}
|
|
</div>
|
|
<div>Trash</div>
|
|
</div>
|
|
|
|
{isTrashOpen && (
|
|
<div className="fm-drive-items-container fm-drive-items-container-open">
|
|
{drives.map(d => {
|
|
const selected = isCurrent(d) && view === ViewType.Trash
|
|
const stamp = usableStamps.find(s => s.batchID.toString() === d.batchId.toString() && !d.isAdmin)
|
|
|
|
return (
|
|
<div
|
|
key={`${d.id.toString()}-trash`}
|
|
className={`fm-sidebar-item fm-trash-item${selected ? ' is-selected' : ''}`}
|
|
onClick={() => {
|
|
setCurrentDrive(d)
|
|
setCurrentStamp(stamp)
|
|
setView(ViewType.Trash)
|
|
}}
|
|
title={`${d.name} Trash`}
|
|
>
|
|
{truncateNameMiddle(d.name, 35, 8, 8)} Trash
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{isDriveCreationInProgress && (
|
|
<div className="fm-sidebar-drive-creation">Creating drive, please do not reload</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|