feat: sync and update with all changes from fork (#720)

* feat: sync and update with all changes from fork
* refactor: extract clipboard copy logic into custom hook
* fix: correct spelling of DEFAULT_REFRESH_FREQUENCY_MS in Stamps and WalletBalance providers
* refactor(ui-tests): replace fixed sleeps with condition-based waits
* fix: handle null values for size and granteeCount in infoGroups
* fix(lint): add newline at end of file in useClipboardCopy hook
* fix(ui-tests): page.goto URL
* refactor: update import paths for useClipboardCopy

---------

Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
This commit is contained in:
Bálint Ujvári
2026-03-02 11:34:39 +01:00
committed by GitHub
parent b0f00a624a
commit 519c411db0
303 changed files with 16609 additions and 29415 deletions
@@ -1,18 +1,20 @@
import { ReactElement, useState, useMemo, useEffect, useContext, useCallback } from 'react'
import './AdminStatusBar.scss'
import { ProgressBar } from '../ProgressBar/ProgressBar'
import { Tooltip } from '../Tooltip/Tooltip'
import { PostageBatch } from '@ethersphere/bee-js'
import { DriveInfo, estimateDriveListMetadataSize } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { Context as FMContext } from '../../../../providers/FileManager'
import { ConfirmModal } from '../ConfirmModal/ConfirmModal'
import { calculateStampCapacityMetrics } from '../../utils/bee'
import { getHumanReadableFileSize } from '../../../../utils/file'
import { FILE_MANAGER_EVENTS, POLLING_TIMEOUT_MS } from '../../constants/common'
import { TOOLTIPS } from '../../constants/tooltips'
import { useStampPolling } from '../../hooks/useStampPolling'
import { calculateStampCapacityMetrics } from '../../utils/bee'
import { ConfirmModal } from '../ConfirmModal/ConfirmModal'
import { ProgressBar } from '../ProgressBar/ProgressBar'
import { Tooltip } from '../Tooltip/Tooltip'
import { UpgradeDriveModal } from '../UpgradeDriveModal/UpgradeDriveModal'
import { UpgradeTimeoutModal } from '../UpgradeTimeoutModal/UpgradeTimeoutModal'
import { FILE_MANAGER_EVENTS, POLLING_TIMEOUT_MS } from '../../constants/common'
import { useStampPolling } from '../../hooks/useStampPolling'
import { TOOLTIPS } from '../../constants/tooltips'
import './AdminStatusBar.scss'
interface AdminStatusBarProps {
adminStamp: PostageBatch | null
@@ -1,4 +1,5 @@
import { ReactElement } from 'react'
import './Button.scss'
interface ButtonProps {
@@ -1,8 +1,10 @@
import { ReactElement } from 'react'
import React, { ReactElement } from 'react'
import { createPortal } from 'react-dom'
import { Button } from '../Button/Button'
import '../../styles/global.scss'
import './ConfirmModal.scss'
import { Button } from '../Button/Button'
import { createPortal } from 'react-dom'
interface ConfirmModalProps {
title?: React.ReactNode
@@ -1,20 +1,21 @@
import { BeeModes, BZZ, DAI, Duration, RedundancyLevel, Size, Utils } from '@ethersphere/bee-js'
import { ReactElement, useContext, useEffect, useRef, useState } from 'react'
import { BeeModes, BZZ, DAI, Duration, RedundancyLevel, Size, Utils } from '@ethersphere/bee-js'
import './CreateDriveModal.scss'
import { CustomDropdown } from '../CustomDropdown/CustomDropdown'
import { Button } from '../Button/Button'
import { fmFetchCost, handleCreateDrive } from '../../utils/bee'
import { getExpiryDateByLifetime } from '../../utils/common'
import { Context as BeeContext } from '../../../../providers/Bee'
import { Context as FMContext } from '../../../../providers/FileManager'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { getHumanReadableFileSize } from '../../../../utils/file'
import { erasureCodeMarks } from '../../constants/common'
import { desiredLifetimeOptions } from '../../constants/stamps'
import { Context as BeeContext } from '../../../../providers/Bee'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { FMSlider } from '../Slider/Slider'
import { Context as FMContext } from '../../../../providers/FileManager'
import { getHumanReadableFileSize } from '../../../../utils/file'
import { Tooltip } from '../Tooltip/Tooltip'
import { TOOLTIPS } from '../../constants/tooltips'
import { fmFetchCost, handleCreateDrive } from '../../utils/bee'
import { getExpiryDateByLifetime } from '../../utils/common'
import { Button } from '../Button/Button'
import { CustomDropdown } from '../CustomDropdown/CustomDropdown'
import { FMSlider } from '../Slider/Slider'
import { Tooltip } from '../Tooltip/Tooltip'
import './CreateDriveModal.scss'
const minMarkValue = Math.min(...erasureCodeMarks.map(mark => mark.value))
const maxMarkValue = Math.max(...erasureCodeMarks.map(mark => mark.value))
@@ -26,7 +27,7 @@ interface CreateDriveModalProps {
onCreationStarted: (driveName: string) => void
onCreationError: (name: string) => void
}
// TODO: select existing batch id or create a new one - just like in InitialModal
export function CreateDriveModal({
onCancelClick,
onDriveCreated,
@@ -72,7 +73,7 @@ export function CreateDriveModal({
}
}, [duplicate, nameExists])
const handleCapacityChange = (value: number, index: number) => {
const handleCapacityChange = (_: number, index: number) => {
setCapacityIndex(index)
}
@@ -1,9 +1,11 @@
import { useState, useRef } from 'react'
import './CustomDropdown.scss'
import { useRef, useState } from 'react'
import ArrowDropdown from 'remixicon-react/ArrowDropDownLineIcon'
import { useClickOutside } from '../../hooks/useClickOutside'
import { Tooltip } from '../Tooltip/Tooltip'
import './CustomDropdown.scss'
interface Option {
value: number
label: string
@@ -31,7 +33,7 @@ export function CustomDropdown({
infoText,
}: CustomDropdownProps) {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const ref = useRef<HTMLDivElement | null>(null)
useClickOutside(ref, () => setOpen(false), open)
@@ -1,17 +1,17 @@
import FormControl from '@mui/material/FormControl'
import FormControlLabel from '@mui/material/FormControlLabel'
import Radio from '@mui/material/Radio'
import { ReactElement, useState } from 'react'
import './DeleteFileModal.scss'
import { Button } from '../Button/Button'
import { createPortal } from 'react-dom'
import TrashIcon from 'remixicon-react/DeleteBin6LineIcon'
import AlertIcon from 'remixicon-react/AlertLineIcon'
import TrashIcon from 'remixicon-react/DeleteBin6LineIcon'
import Radio from '@material-ui/core/Radio'
import FormControlLabel from '@material-ui/core/FormControlLabel'
import FormControl from '@material-ui/core/FormControl'
import { TOOLTIPS } from '../../constants/tooltips'
import { FileAction } from '../../constants/transfers'
import { Button } from '../Button/Button'
import { Tooltip } from '../Tooltip/Tooltip'
import { FileAction } from '../../constants/transfers'
import { TOOLTIPS } from '../../constants/tooltips'
import './DeleteFileModal.scss'
interface DeleteFileModalProps {
name?: string
@@ -1,9 +1,11 @@
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useState } from 'react'
import { createPortal } from 'react-dom'
import { Button } from '../Button/Button'
import '../../styles/global.scss'
import './DestroyDriveModal.scss'
import { Button } from '../Button/Button'
import { createPortal } from 'react-dom'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
interface DestroyDriveModalProps {
drive: DriveInfo
@@ -1,7 +1,9 @@
import { ReactElement } from 'react'
import './ErrorModal.scss'
import { Button } from '../Button/Button'
import './ErrorModal.scss'
interface ErrorModalProps {
label: string
onClick: () => void
@@ -1,20 +1,20 @@
import { ReactElement, useState, useMemo, useEffect } from 'react'
import { Warning } from '@material-ui/icons'
import { PostageBatch } from '@ethersphere/bee-js'
import { Warning } from '@mui/icons-material'
import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import AlertIcon from 'remixicon-react/AlertLineIcon'
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
import DriveIcon from 'remixicon-react/HardDrive2LineIcon'
import { calculateStampCapacityMetrics } from '../../utils/bee'
import { getDaysLeft } from '../../utils/common'
import { Button } from '../Button/Button'
import { UpgradeDriveModal } from '../UpgradeDriveModal/UpgradeDriveModal'
import './ExpiringNotificationModal.scss'
import '../../styles/global.scss'
import { Button } from '../Button/Button'
import { createPortal } from 'react-dom'
import DriveIcon from 'remixicon-react/HardDrive2LineIcon'
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
import AlertIcon from 'remixicon-react/AlertLineIcon'
import { UpgradeDriveModal } from '../UpgradeDriveModal/UpgradeDriveModal'
import { getDaysLeft } from '../../utils/common'
import { PostageBatch } from '@ethersphere/bee-js'
import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import { calculateStampCapacityMetrics } from '../../utils/bee'
const EXPIRING_ITEMS_PAGE_SIZE = 3
interface ExpiringNotificationModalProps {
@@ -52,7 +52,10 @@ export function ExpiringNotificationModal({
const paginatedStamps = sortedStamps.slice(startIndex, startIndex + EXPIRING_ITEMS_PAGE_SIZE)
useEffect(() => {
setCurrentPage(0)
if (currentPage !== 0) {
setCurrentPage(0)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stamps])
if (stamps.length === 0) return <></>
@@ -1,52 +1,69 @@
import { ReactElement, useEffect, useLayoutEffect, useRef, useState, useContext, useMemo, useCallback } from 'react'
import './FileBrowser.scss'
import { FileBrowserHeader } from './FileBrowserHeader/FileBrowserHeader'
import { FileBrowserContent } from './FileBrowserContent/FileBrowserContent'
import { useContextMenu } from '../../hooks/useContextMenu'
import { NotificationBar } from '../NotificationBar/NotificationBar'
import { FileAction, FileTransferType, TransferStatus, ViewType } from '../../constants/transfers'
import { FileProgressNotification } from '../FileProgressNotification/FileProgressNotification'
import { PostageBatch } from '@ethersphere/bee-js'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import React, {
ReactElement,
useCallback,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useSearch } from '../../../../pages/filemanager/SearchContext'
import { useView } from '../../../../pages/filemanager/ViewContext'
import { Context as FMContext } from '../../../../providers/FileManager'
import { useTransfers } from '../../hooks/useTransfers'
import { useSearch } from '../../../../pages/filemanager/SearchContext'
import { useFileFiltering } from '../../hooks/useFileFiltering'
import { useDragAndDrop } from '../../hooks/useDragAndDrop'
import { useBulkActions } from '../../hooks/useBulkActions'
import { SortKey, SortDir, useSorting } from '../../hooks/useSorting'
import { Point, Dir, safeSetState, getFileId } from '../../utils/common'
import { computeContextMenuPosition } from '../../utils/ui'
import { FileBrowserTopBar } from './FileBrowserTopBar/FileBrowserTopBar'
import { handleDestroyAndForgetDrive } from '../../utils/bee'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { ErrorModal } from '../ErrorModal/ErrorModal'
import { FileBrowserModals } from './FileBrowserModals'
import { FileBrowserContextMenu } from './FileBrowserMenu/FileBrowserContextMenu'
import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import { FileAction, FileTransferType, TransferStatus, ViewType } from '../../constants/transfers'
import { useBulkActions } from '../../hooks/useBulkActions'
import { useContextMenu } from '../../hooks/useContextMenu'
import { useDragAndDrop } from '../../hooks/useDragAndDrop'
import { useFileFiltering } from '../../hooks/useFileFiltering'
import { SortDir, SortKey, useSorting } from '../../hooks/useSorting'
import { useTransfers } from '../../hooks/useTransfers'
import { handleDestroyAndForgetDrive } from '../../utils/bee'
import { Dir, getFileId, Point, safeSetState } from '../../utils/common'
import { computeContextMenuPosition } from '../../utils/ui'
import { ProgressDestroyModal } from '../DestroyDriveModal/DestroyDriveModal'
import { ErrorModal } from '../ErrorModal/ErrorModal'
import { FileProgressNotification } from '../FileProgressNotification/FileProgressNotification'
import { NotificationBar } from '../NotificationBar/NotificationBar'
const renderDestroySpinner = (
isDestroying: boolean,
isProgressModalOpen: boolean,
currentDrive: DriveInfo | undefined,
setter: () => void,
) => {
import { FileBrowserContent } from './FileBrowserContent/FileBrowserContent'
import { FileBrowserHeader } from './FileBrowserHeader/FileBrowserHeader'
import { FileBrowserContextMenu } from './FileBrowserMenu/FileBrowserContextMenu'
import { FileBrowserTopBar } from './FileBrowserTopBar/FileBrowserTopBar'
import { FileBrowserModals } from './FileBrowserModals'
import './FileBrowser.scss'
function DestroyProgressModal({
isDestroying,
isProgressModalOpen,
currentDrive,
onMinimize,
}: {
isDestroying: boolean
isProgressModalOpen: boolean
currentDrive?: DriveInfo
onMinimize: () => void
}) {
if (isProgressModalOpen && isDestroying && currentDrive) {
return <ProgressDestroyModal drive={currentDrive} onMinimize={setter} />
return <ProgressDestroyModal drive={currentDrive} onMinimize={onMinimize} />
}
return null
}
const showDestroyModal = (isDestroying: boolean, setter: () => void) => {
function DestroyingOverlay({ isDestroying, onClick }: { isDestroying: boolean; onClick: () => void }) {
if (!isDestroying) return null
return (
<div className="fm-refresh-overlay" aria-busy="true" aria-live="polite">
<div
className="fm-refresh-content"
onClick={setter}
onClick={onClick}
style={{ cursor: 'pointer' }}
title="Click to show progress modal"
>
@@ -57,6 +74,22 @@ const showDestroyModal = (isDestroying: boolean, setter: () => void) => {
)
}
function ErrorModalBlock({
showError,
label,
onOk,
}: {
showError: boolean
label: string
onOk: () => void
}): ReactElement | null {
if (!showError) {
return null
}
return <ErrorModal label={label} onClick={onOk} />
}
const extractFilesFromClipboardEvent = (e: React.ClipboardEvent): File[] => {
const out: File[] = []
const items = e.clipboardData?.items ?? []
@@ -78,6 +111,67 @@ interface FileBrowserProps {
setErrorMessage?: (error: string) => void
}
type FileBrowserContextMenuBlockProps = {
showContext: boolean
contextRef: React.RefObject<HTMLDivElement | null>
safePos: { x: number; y: number }
dropDir: Dir
drives: DriveInfo[]
view: ViewType
bulk: ReturnType<typeof useBulkActions>
adminStamp: PostageBatch | undefined
doRefresh: () => void
onContextUploadFile: () => void
setConfirmBulkRestore: (b: boolean) => void
setShowBulkDeleteModal: (b: boolean) => void
setShowDestroyDriveModal: (b: boolean) => void
}
function FileBrowserContextMenuBlock({
showContext,
contextRef,
safePos,
dropDir,
drives,
view,
bulk,
adminStamp,
doRefresh,
onContextUploadFile,
setConfirmBulkRestore,
setShowBulkDeleteModal,
setShowDestroyDriveModal,
}: FileBrowserContextMenuBlockProps): ReactElement | null {
if (!showContext) {
return null
}
return (
<div
ref={contextRef}
className="fm-file-browser-context-menu fm-context-menu"
style={{ top: safePos.y, left: safePos.x }}
data-drop={dropDir}
onMouseDown={e => e.stopPropagation()}
onClick={e => e.stopPropagation()}
>
<FileBrowserContextMenu
drives={drives}
view={view}
selectedFilesCount={bulk.selectedFiles.length}
onRefresh={doRefresh}
enableRefresh={Boolean(adminStamp)}
onUploadFile={onContextUploadFile}
onBulkDownload={() => bulk.bulkDownload(bulk.selectedFiles)}
onBulkRestore={() => setConfirmBulkRestore(true)}
onBulkDelete={() => setShowBulkDeleteModal(true)}
onBulkDestroy={() => setShowDestroyDriveModal(true)}
onBulkForget={() => bulk.bulkForget(bulk.selectedFiles)}
/>
</div>
)
}
export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps): ReactElement {
const { showContext, pos, contextRef, handleContextMenu, handleCloseContext } = useContextMenu<HTMLDivElement>()
const { view, setActualItemView } = useView()
@@ -120,11 +214,14 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
const q = query.trim().toLowerCase()
const isSearchMode = q.length > 0
const getDriveName = (fi: FileInfo): string => {
const match = drives.find(d => d.id.toString() === fi.driveId.toString())
const getDriveName = useCallback(
(driveId: string): string => {
const match = drives.find(d => d.id.toString() === driveId)
return match?.name ?? ''
}
return match?.name ?? ''
},
[drives],
)
const openTopbarMenu = (anchorEl: HTMLElement) => {
const r = anchorEl.getBoundingClientRect()
@@ -133,9 +230,7 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
const minY = (bodyRect?.top ?? 0) + 8
const clickY = Math.max(Math.round(r.bottom + 6), minY)
const fakeEvt = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
preventDefault: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
stopPropagation: () => {},
clientX: clickX,
clientY: clickY,
@@ -379,7 +474,6 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
const showDragOverlay = isDragging && Boolean(currentDrive)
const fileCountText = bulk.selectedFiles.length === 1 ? 'file' : 'files'
// Memoize onBulk object to prevent FileBrowserContent rerenders
const onBulk = useMemo(
() => ({
download: () => bulk.bulkDownload(bulk.selectedFiles),
@@ -445,42 +539,32 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
onBulk={onBulk}
setErrorMessage={setErrorMessage}
/>
{showError && (
<ErrorModal
label={errorMessage || 'An error occurred'}
onClick={() => {
setShowError(false)
setErrorMessage?.('')
<ErrorModalBlock
showError={Boolean(showError)}
label={errorMessage || 'An error occurred'}
onOk={() => {
setShowError(false)
setErrorMessage?.('')
return
}}
/>
)}
return
}}
/>
{showContext && (
<div
ref={contextRef}
className="fm-file-browser-context-menu fm-context-menu"
style={{ top: safePos.y, left: safePos.x }}
data-drop={dropDir}
onMouseDown={e => e.stopPropagation()}
onClick={e => e.stopPropagation()}
>
<FileBrowserContextMenu
drives={drives}
view={view}
selectedFilesCount={bulk.selectedFiles.length}
onRefresh={doRefresh}
enableRefresh={Boolean(fm?.adminStamp)}
onUploadFile={onContextUploadFile}
onBulkDownload={() => bulk.bulkDownload(bulk.selectedFiles)}
onBulkRestore={() => setConfirmBulkRestore(true)}
onBulkDelete={() => setShowBulkDeleteModal(true)}
onBulkDestroy={() => setShowDestroyDriveModal(true)}
onBulkForget={() => bulk.bulkForget(bulk.selectedFiles)}
/>
</div>
)}
<FileBrowserContextMenuBlock
showContext={showContext}
contextRef={contextRef}
safePos={safePos}
dropDir={dropDir}
drives={drives}
view={view}
bulk={bulk}
adminStamp={fm?.adminStamp}
doRefresh={doRefresh}
onContextUploadFile={onContextUploadFile}
setConfirmBulkRestore={setConfirmBulkRestore}
setShowBulkDeleteModal={setShowBulkDeleteModal}
setShowDestroyDriveModal={setShowDestroyDriveModal}
/>
</div>
{showDragOverlay && (
@@ -537,9 +621,13 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
</div>
)}
{showDestroyModal(isDestroying, () => setIsProgressModalOpen(true))}
{renderDestroySpinner(isDestroying, isProgressModalOpen, currentDrive, () => setIsProgressModalOpen(false))}
<DestroyingOverlay isDestroying={isDestroying} onClick={() => setIsProgressModalOpen(true)} />
<DestroyProgressModal
isDestroying={isDestroying}
isProgressModalOpen={isProgressModalOpen}
currentDrive={currentDrive}
onMinimize={() => setIsProgressModalOpen(false)}
/>
</div>
<div className="fm-file-browser-footer">
@@ -556,7 +644,7 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
type={FileTransferType.Download}
open={isDownloading}
items={downloadItems}
onRowClose={name => cancelOrDismissDownload(name)}
onRowClose={(name: string) => cancelOrDismissDownload(name)}
onCloseAll={() => dismissAllDownloads()}
/>
<NotificationBar setErrorMessage={setErrorMessage} />
@@ -1,8 +1,9 @@
import { ReactElement, useCallback, memo } from 'react'
import { FileItem } from '../FileItem/FileItem'
import { FileInfo, DriveInfo } from '@solarpunkltd/file-manager-lib'
import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import { memo, ReactElement, useCallback } from 'react'
import { DownloadProgress, TrackDownloadProps, ViewType } from '../../../constants/transfers'
import { getFileId } from '../../../utils/common'
import { FileItem } from '../FileItem/FileItem'
interface FileBrowserContentProps {
listToRender: FileInfo[]
@@ -1,8 +1,10 @@
import { ReactElement } from 'react'
import DownIcon from 'remixicon-react/ArrowDownSLineIcon'
import { useBulkActions } from '../../../hooks/useBulkActions'
import { SortDir, SortKey } from '../../../hooks/useSorting'
import { capitalizeFirstLetter } from '../../../../../../src/modules/filemanager/utils/common'
import { capitalizeFirstLetter } from '@/modules/filemanager/utils/common'
interface FileBrowserHeaderProps {
isSearchMode: boolean
@@ -1,10 +1,12 @@
import { ContextMenu } from '../../ContextMenu/ContextMenu'
import { ReactElement } from 'react'
import '../FileBrowser.scss'
import { ViewType } from '../../../constants/transfers'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement } from 'react'
import { ViewType } from '../../../constants/transfers'
import { ContextMenu } from '../../ContextMenu/ContextMenu'
import { Tooltip } from '../../Tooltip/Tooltip'
import '../FileBrowser.scss'
interface FileBrowserContextMenuProps {
drives: DriveInfo[]
view: ViewType
@@ -1,11 +1,12 @@
import type { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement } from 'react'
import type { FileInfo, DriveInfo } from '@solarpunkltd/file-manager-lib'
import { TOOLTIPS } from '../../constants/tooltips'
import { FileAction } from '../../constants/transfers'
import { ConfirmModal } from '../ConfirmModal/ConfirmModal'
import { Tooltip } from '../Tooltip/Tooltip'
import { DeleteFileModal } from '../DeleteFileModal/DeleteFileModal'
import { DestroyDriveModal } from '../DestroyDriveModal/DestroyDriveModal'
import { FileAction } from '../../constants/transfers'
import { TOOLTIPS } from '../../constants/tooltips'
import { Tooltip } from '../Tooltip/Tooltip'
interface FileBrowserModalsProps {
showDeleteModal: boolean
@@ -1,8 +1,10 @@
import { ReactElement } from 'react'
import './FileBrowserTopBar.scss'
import { useView } from '../../../../../pages/filemanager/ViewContext'
import { ViewType } from '../../../constants/transfers'
import './FileBrowserTopBar.scss'
type Props = {
onOpenMenu?: (anchorEl: HTMLElement) => void
canOpen?: boolean
@@ -1,33 +1,62 @@
import { ReactElement, useContext, useLayoutEffect, useMemo, useState, useRef, useEffect, useCallback } from 'react'
import { PostageBatch } from '@ethersphere/bee-js'
import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import React, {
ReactElement,
useCallback,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import './FileItem.scss'
import { GetIconElement } from '../../../utils/GetIconElement'
import { ContextMenu } from '../../ContextMenu/ContextMenu'
import { useContextMenu } from '../../../hooks/useContextMenu'
import { Context as SettingsContext } from '../../../../../providers/Settings'
import { DownloadProgress, TrackDownloadProps, ViewType } from '../../../constants/transfers'
import { GetInfoModal } from '../../GetInfoModal/GetInfoModal'
import { VersionHistoryModal } from '../../VersionHistoryModal/VersionHistoryModal'
import { DeleteFileModal } from '../../DeleteFileModal/DeleteFileModal'
import { RenameFileModal } from '../../RenameFileModal/RenameFileModal'
import { buildGetInfoGroups } from '../../../utils/infoGroups'
import type { FilePropertyGroup } from '../../../utils/infoGroups'
import { useView } from '../../../../../pages/filemanager/ViewContext'
import { Context as FMContext } from '../../../../../providers/FileManager'
import { DestroyDriveModal } from '../../DestroyDriveModal/DestroyDriveModal'
import { ConfirmModal } from '../../ConfirmModal/ConfirmModal'
import { Tooltip } from '../../Tooltip/Tooltip'
import { Dir, formatBytes, isTrashed, safeSetState, truncateNameMiddle } from '../../../utils/common'
import { FileAction } from '../../../constants/transfers'
import { TOOLTIPS } from '../../../constants/tooltips'
import { startDownloadingQueue, createDownloadAbort } from '../../../utils/download'
import { computeContextMenuPosition } from '../../../utils/ui'
import { getUsableStamps, handleDestroyAndForgetDrive, verifyDriveSpace } from '../../../utils/bee'
import { guessMime } from '../../../utils/view'
import { performFileOperation, FileOperation } from '../../../utils/fileOperations'
import { Context as SettingsContext } from '../../../../../providers/Settings'
import { uuidV4 } from '../../../../../utils'
import { TOOLTIPS } from '../../../constants/tooltips'
import { DownloadProgress, FileAction, TrackDownloadProps, ViewType } from '../../../constants/transfers'
import { useContextMenu } from '../../../hooks/useContextMenu'
import { getUsableStamps, handleDestroyAndForgetDrive, verifyDriveSpace } from '../../../utils/bee'
import { Dir, formatBytes, isTrashed, safeSetState, truncateNameMiddle } from '../../../utils/common'
import { createDownloadAbort, startDownloadingQueue } from '../../../utils/download'
import { FileOperation, performFileOperation } from '../../../utils/fileOperations'
import { GetIconElement } from '../../../utils/GetIconElement'
import type { FilePropertyGroup } from '../../../utils/infoGroups'
import { buildGetInfoGroups } from '../../../utils/infoGroups'
import { computeContextMenuPosition } from '../../../utils/ui'
import { ConfirmModal } from '../../ConfirmModal/ConfirmModal'
import { ContextMenu } from '../../ContextMenu/ContextMenu'
import { DeleteFileModal } from '../../DeleteFileModal/DeleteFileModal'
import { DestroyDriveModal } from '../../DestroyDriveModal/DestroyDriveModal'
import { GetInfoModal } from '../../GetInfoModal/GetInfoModal'
import { RenameFileModal } from '../../RenameFileModal/RenameFileModal'
import { Tooltip } from '../../Tooltip/Tooltip'
import { VersionHistoryModal } from '../../VersionHistoryModal/VersionHistoryModal'
import './FileItem.scss'
const MenuItem = ({
disabled,
danger,
onClick,
children,
}: {
disabled?: boolean
danger?: boolean
onClick?: () => void
children: React.ReactNode
}) => (
<div
className={`fm-context-item${danger ? ' red' : ''}`}
aria-disabled={disabled ? 'true' : 'false'}
style={disabled ? { opacity: 0.5, pointerEvents: 'none' } : undefined}
onClick={disabled ? undefined : onClick}
>
{children}
</div>
)
interface FileItemProps {
fileInfo: FileInfo
@@ -131,12 +160,6 @@ export function FileItem({
return out
}, [files, currentDrive, fileInfo.topic])
const handleItemContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.shiftKey) return
handleContextMenu(e)
}
// TODO: handleOpen shall only be available for images, videos etc... -> do not download 10GB into memory
const handleDownload = useCallback(
async (isNewWindow?: boolean) => {
if (!fm || !beeApi) return
@@ -241,7 +264,7 @@ export function FileItem({
)
refreshStamp(driveStamp.batchID.toString())
} catch (e: unknown) {
} catch {
setErrorMessage?.(`Error renaming file ${latestFileInfo.name}`)
setShowError(true)
}
@@ -250,41 +273,16 @@ export function FileItem({
[fm, driveStamp, currentDrive, latestFileInfo, takenNames, refreshStamp, setErrorMessage, setShowError],
)
const MenuItem = ({
disabled,
danger,
onClick,
children,
}: {
disabled?: boolean
danger?: boolean
onClick?: () => void
children: React.ReactNode
}) => (
<div
className={`fm-context-item${danger ? ' red' : ''}`}
aria-disabled={disabled ? 'true' : 'false'}
style={disabled ? { opacity: 0.5, pointerEvents: 'none' } : undefined}
onClick={disabled ? undefined : onClick}
>
{children}
</div>
)
const isBulk = (bulkSelectedCount ?? 0) > 1
const renderContextMenuItems = useCallback(() => {
const isBulk = (bulkSelectedCount ?? 0) > 1
const viewItem = (
<MenuItem disabled={isBulk} onClick={() => handleDownload(true)}>
View / Open
</MenuItem>
)
const downloadItem = isBulk ? (
<MenuItem onClick={onBulk.download}>Download</MenuItem>
) : (
<MenuItem onClick={() => handleDownload(false)}>Download</MenuItem>
)
const downloadItem = <MenuItem onClick={isBulk ? onBulk.download : () => handleDownload(false)}>Download</MenuItem>
const getInfoItem = (
<MenuItem
@@ -405,15 +403,15 @@ export function FileItem({
</>
)
}, [
isBulk,
view,
currentDrive,
drives,
bulkSelectedCount,
onBulk,
fileInfo.driveId,
handleDownload,
handleCloseContext,
openGetInfo,
onBulk,
currentDrive,
drives,
fileInfo.driveId,
setErrorMessage,
setShowError,
])
@@ -463,11 +461,15 @@ export function FileItem({
return <div className="fm-file-item-content">Error</div>
}
const { mime } = guessMime(fileInfo.name, fileInfo.customMetadata)
const mimeType = mime.split('/')[0]?.toLowerCase() || 'file'
return (
<div className="fm-file-item-content" onContextMenu={handleItemContextMenu} onClick={handleCloseContext}>
<div
className="fm-file-item-content"
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) => {
if (e.shiftKey) return
handleContextMenu(e)
}}
onClick={handleCloseContext}
>
<div className="fm-file-item-content-item fm-checkbox">
<input
type="checkbox"
@@ -478,7 +480,7 @@ export function FileItem({
</div>
<div className="fm-file-item-content-item fm-name" onDoubleClick={() => handleDownload(true)}>
<GetIconElement icon={mimeType} />
<GetIconElement name={fileInfo.name} metadata={fileInfo.customMetadata} />
{truncateNameMiddle(fileInfo.name)}
</div>
@@ -1,9 +1,11 @@
import { ReactElement, useEffect, useMemo, useRef, useState } from 'react'
import './FileProgressNotification.scss'
import UpIcon from 'remixicon-react/ArrowUpSLineIcon'
import DownIcon from 'remixicon-react/ArrowDownSLineIcon'
import UpIcon from 'remixicon-react/ArrowUpSLineIcon'
import { FileTransferType, ProgressItem, TransferStatus } from '../../constants/transfers'
import { FileProgressWindow } from '../FileProgressWindow/FileProgressWindow'
import { FileTransferType, TransferStatus, ProgressItem } from '../../constants/transfers'
import './FileProgressNotification.scss'
interface FileProgressNotificationProps {
label?: string
@@ -14,6 +16,8 @@ interface FileProgressNotificationProps {
onCloseAll?: () => void
}
const HIDER_TIMEOUT_MS = 3000
export function FileProgressNotification({
label,
type,
@@ -49,7 +53,7 @@ export function FileProgressNotification({
autoHideTimer.current = window.setTimeout(() => {
setShowFileProgressWindow(false)
autoHideTimer.current = null
}, 3000) as unknown as number
}, HIDER_TIMEOUT_MS) as unknown as number
}
return () => {
@@ -1,12 +1,13 @@
import { ReactElement, useLayoutEffect, useRef } from 'react'
import CloseIcon from 'remixicon-react/CloseLineIcon'
import ArrowDownIcon from 'remixicon-react/ArrowDownSLineIcon'
import './FileProgressWindow.scss'
import CloseIcon from 'remixicon-react/CloseLineIcon'
import { FileTransferType, ProgressItem, TransferBarColor, TransferStatus } from '../../constants/transfers'
import { capitalizeFirstLetter, truncateNameMiddle } from '../../utils/common'
import { GetIconElement } from '../../utils/GetIconElement'
import { ProgressBar } from '../ProgressBar/ProgressBar'
import { FileTransferType, TransferBarColor, TransferStatus, ProgressItem } from '../../constants/transfers'
import { capitalizeFirstLetter, truncateNameMiddle } from '../../utils/common'
import { guessMime } from '../../utils/view'
import './FileProgressWindow.scss'
interface FileProgressWindowProps {
items?: ProgressItem[]
@@ -138,9 +139,6 @@ export function FileProgressWindow({
const centerDisplay = getCenterText() || '\u00A0'
const { mime } = guessMime(item.name)
const mimeType = mime.split('/')[0].toLowerCase() || 'file'
return (
<div
className="fm-file-progress-window-file-item"
@@ -148,7 +146,7 @@ export function FileProgressWindow({
ref={idx === 0 ? firstRowRef : undefined}
>
<div className="fm-file-progress-window-file-type-icon">
<GetIconElement size="14" icon={mimeType} color="black" />
<GetIconElement size="14" name={item.name} color="black" />
</div>
<div className="fm-file-progress-window-file-datas">
@@ -1,10 +1,12 @@
import { useEffect, useRef, useCallback } from 'react'
import { useLocation } from 'react-router-dom'
import formbricks from '@formbricks/js'
import { useCallback, useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import { LocalStorageKeys } from '../../../../utils/localStorage'
const FM_CLICK_STORAGE_KEY = 'fm_click_count_v1'
const FM_SURVEY_TRIGGERED_KEY = 'fm_survey_triggered_v1'
const FM_CLICK_THRESHOLD = 25
const FM_FORMBRICKS_TRACK_CODE = 'file_manager_engagement_25_clicks'
const FORMBRICKS_INIT_TIMEOUT_MS = 1000
interface FormbricksIntegrationProps {
isActive: boolean
@@ -16,14 +18,14 @@ export function FormbricksIntegration({ isActive }: FormbricksIntegrationProps)
const formbricksReadyRef = useRef(false)
const pendingEventRef = useRef(false)
const environmentId = process.env.REACT_APP_FORMBRICKS_ENV_ID
const appUrl = process.env.REACT_APP_FORMBRICKS_APP_URL
const environmentId = import.meta.env.VITE_FORMBRICKS_ENV_ID
const appUrl = import.meta.env.VITE_FORMBRICKS_APP_URL
const flushPendingEvent = useCallback(() => {
if (pendingEventRef.current && localStorage.getItem(FM_SURVEY_TRIGGERED_KEY) !== 'true') {
if (pendingEventRef.current && localStorage.getItem(LocalStorageKeys.fmSurveyTriggered) !== 'true') {
try {
formbricks.track('file_manager_engagement_25_clicks')
localStorage.setItem(FM_SURVEY_TRIGGERED_KEY, 'true')
formbricks.track(FM_FORMBRICKS_TRACK_CODE)
localStorage.setItem(LocalStorageKeys.fmSurveyTriggered, 'true')
pendingEventRef.current = false
} catch {
// no-op
@@ -45,7 +47,7 @@ export function FormbricksIntegration({ isActive }: FormbricksIntegrationProps)
appUrl,
})
await new Promise(resolve => setTimeout(resolve, 1000))
await new Promise(resolve => setTimeout(resolve, FORMBRICKS_INIT_TIMEOUT_MS))
if (!cancelled) {
formbricksReadyRef.current = true
@@ -79,11 +81,11 @@ export function FormbricksIntegration({ isActive }: FormbricksIntegrationProps)
if (!isActive) return
const handleClick = async () => {
if (localStorage.getItem(FM_SURVEY_TRIGGERED_KEY) === 'true') return
if (localStorage.getItem(LocalStorageKeys.fmSurveyTriggered) === 'true') return
let count = 0
try {
const stored = localStorage.getItem(FM_CLICK_STORAGE_KEY)
const stored = localStorage.getItem(LocalStorageKeys.fmClickStorage)
if (stored) count = parseInt(stored, 10) || 0
} catch {
@@ -92,7 +94,7 @@ export function FormbricksIntegration({ isActive }: FormbricksIntegrationProps)
count += 1
try {
localStorage.setItem(FM_CLICK_STORAGE_KEY, String(count))
localStorage.setItem(LocalStorageKeys.fmClickStorage, String(count))
} catch {
// no-op
}
@@ -111,8 +113,8 @@ export function FormbricksIntegration({ isActive }: FormbricksIntegrationProps)
}
try {
await formbricks.track('file_manager_engagement_25_clicks')
localStorage.setItem(FM_SURVEY_TRIGGERED_KEY, 'true')
await formbricks.track(FM_FORMBRICKS_TRACK_CODE)
localStorage.setItem(LocalStorageKeys.fmSurveyTriggered, 'true')
} catch {
// no-op
}
@@ -1,11 +1,12 @@
import { ReactElement, useState, useEffect } from 'react'
import './GetInfoModal.scss'
import { Button } from '../Button/Button'
import { ReactElement, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import InfoIcon from 'remixicon-react/InformationLineIcon'
import ClipboardIcon from 'remixicon-react/FileCopyLineIcon'
import InfoIcon from 'remixicon-react/InformationLineIcon'
import type { FileProperty, FilePropertyGroup } from '../../utils/infoGroups'
import { Button } from '../Button/Button'
import './GetInfoModal.scss'
interface GetInfoModalProps {
name: string
@@ -13,31 +14,35 @@ interface GetInfoModalProps {
onCancelClick: () => void
}
const COPY_TIMEOUT_MS = 2000
export function GetInfoModal({ name, onCancelClick, properties }: GetInfoModalProps): ReactElement {
const modalRoot = document.querySelector('.fm-main') || document.body
const [copiedKey, setCopiedKey] = useState<string | null>(null)
const timeoutRef = useState<{ [key: string]: NodeJS.Timeout }>({})[0]
const timeoutRef = useRef<Record<string, NodeJS.Timeout>>({})
useEffect(() => {
return () => {
Object.values(timeoutRef).forEach(timeout => clearTimeout(timeout))
// eslint-disable-next-line react-hooks/exhaustive-deps
Object.values(timeoutRef.current).forEach(clearTimeout)
}
}, [timeoutRef])
}, [])
const handleCopy = async (prop: FileProperty) => {
try {
await navigator.clipboard.writeText(prop.raw ?? prop.value)
if (timeoutRef[prop.key]) {
clearTimeout(timeoutRef[prop.key])
if (timeoutRef.current[prop.key]) {
clearTimeout(timeoutRef.current[prop.key])
}
setCopiedKey(prop.key)
timeoutRef[prop.key] = setTimeout(() => {
timeoutRef.current[prop.key] = setTimeout(() => {
setCopiedKey(prev => (prev === prop.key ? null : prev))
delete timeoutRef[prop.key]
}, 2000)
delete timeoutRef.current[prop.key]
}, COPY_TIMEOUT_MS)
} catch {
/* noop */
}
@@ -1,3 +1,5 @@
@use 'sass:color';
$bg-900: #212121;
$bg-800: #262626;
$bg-700: #3e3e3e;
@@ -13,7 +15,7 @@ $accent: #ed8131;
height: 60px;
padding: 10px 16px;
background: $bg-900;
border-bottom: 1px solid rgba(255,255,255,0.04);
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.fm-header-left {
@@ -22,51 +24,73 @@ $accent: #ed8131;
gap: 12px;
}
.fm-header-logo {
width: 40px; height: 40px;
width: 40px;
height: 40px;
border-radius: 6px;
background: $accent;
color: $text-100;
display: grid; place-items: center;
display: grid;
place-items: center;
font-weight: 700;
svg { width: 18px; height: 18px; }
svg {
width: 18px;
height: 18px;
}
}
.fm-header-title {
color: $text-100;
font-weight: 600;
letter-spacing: .2px;
letter-spacing: 0.2px;
}
.fm-header-search {
flex: 1 1 auto;
max-width: 900px;
display: flex; align-items: center; gap: 8px;
display: flex;
align-items: center;
gap: 8px;
background: $bg-700;
border: 1px solid $border-400;
color: $text-300;
height: 36px; padding: 0 10px;
height: 36px;
padding: 0 10px;
border-radius: 8px;
&:focus-within {
border-color: $accent;
box-shadow: 0 0 0 2px rgba(237,129,49,0.25);
box-shadow: 0 0 0 2px rgba(237, 129, 49, 0.25);
}
.fm-header-search-icon { flex: 0 0 auto; }
.fm-header-search-icon {
flex: 0 0 auto;
}
input {
flex: 1 1 auto;
background: transparent; border: none; outline: none;
background: transparent;
border: none;
outline: none;
height: 100%;
color: $text-100; font-size: 14px;
color: $text-100;
font-size: 14px;
&::placeholder { color: $text-300; }
&::placeholder {
color: $text-300;
}
}
.fm-header-search-clear {
appearance: none; border: none; background: transparent;
color: $text-300; font-size: 18px; line-height: 1;
padding: 0 2px; cursor: pointer;
&:hover { color: $text-100; }
appearance: none;
border: none;
background: transparent;
color: $text-300;
font-size: 18px;
line-height: 1;
padding: 0 2px;
cursor: pointer;
&:hover {
color: $text-100;
}
}
}
@@ -76,7 +100,9 @@ $accent: #ed8131;
}
.fm-filter-btn {
display: inline-flex; align-items: center; gap: 6px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid $border-400;
@@ -85,59 +111,83 @@ $accent: #ed8131;
font-size: 13px;
cursor: pointer;
&:hover { background: mix($bg-800, #fff, 92%); }
&:focus-visible { outline: 2px solid rgba(237,129,49,0.4); outline-offset: 2px; }
&:hover {
background: color.mix($bg-800, #fff, 92%);
}
&:focus-visible {
outline: 2px solid rgba(237, 129, 49, 0.4);
outline-offset: 2px;
}
&[aria-expanded="true"] {
&[aria-expanded='true'] {
border-color: $accent;
box-shadow: 0 0 0 2px rgba(237,129,49,0.25);
box-shadow: 0 0 0 2px rgba(237, 129, 49, 0.25);
}
}
.fm-filter-menu {
position: absolute;
right: 0; top: calc(100% + 6px);
right: 0;
top: calc(100% + 6px);
min-width: 260px;
background: $bg-800;
border: 1px solid $border-400;
border-radius: 10px;
box-shadow: 0 10px 24px rgba(0,0,0,0.25);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.25);
padding: 10px;
z-index: 2000;
color: $text-100;
}
.fm-filter-group + .fm-filter-group { margin-top: 10px; }
.fm-filter-group + .fm-filter-group {
margin-top: 10px;
}
.fm-filter-group-title {
font-size: 12px;
color: $text-300;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: .04em;
letter-spacing: 0.04em;
}
.fm-filter-row {
display: flex; align-items: center; gap: 8px;
padding: 6px 4px; border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 4px;
border-radius: 6px;
cursor: default;
color: $text-100;
input[type="checkbox"], input[type="radio"] {
width: 14px; height: 14px; margin: 0;
input[type='checkbox'],
input[type='radio'] {
width: 14px;
height: 14px;
margin: 0;
accent-color: $accent;
}
&:hover { background: rgba(255,255,255,0.05); }
&:hover {
background: rgba(255, 255, 255, 0.05);
}
}
.fm-filter-sep {
height: 1px;
background: rgba(255,255,255,0.08);
background: rgba(255, 255, 255, 0.08);
margin: 8px 0;
}
.fm-header-filters { display: none; }
.fm-header-filters-label { display: none; }
.fm-header-chip-group { display: none; }
.fm-chip { display: none; }
.fm-header-filters {
display: none;
}
.fm-header-filters-label {
display: none;
}
.fm-header-chip-group {
display: none;
}
.fm-chip {
display: none;
}
@@ -1,11 +1,13 @@
import { ReactElement, useMemo, useState, useEffect, useRef, useContext } from 'react'
import SearchIcon from 'remixicon-react/SearchLineIcon'
import { ReactElement, useContext, useEffect, useMemo, useRef, useState } from 'react'
import FileIcon from 'remixicon-react/File2LineIcon'
import FilterIcon from 'remixicon-react/FilterLineIcon'
import './Header.scss'
import SearchIcon from 'remixicon-react/SearchLineIcon'
import { useSearch } from '../../../../pages/filemanager/SearchContext'
import { Context as FMContext } from '../../../../providers/FileManager'
import './Header.scss'
// Defaults used to determine “active filters”
const DEFAULT_FILTERS = {
scope: 'selected' as 'selected' | 'all',
@@ -1,22 +1,22 @@
import { BeeModes, BZZ, DAI, Duration, PostageBatch, RedundancyLevel, Size, Utils } from '@ethersphere/bee-js'
import { ADMIN_STAMP_LABEL } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { BeeModes, BZZ, DAI, Duration, PostageBatch, RedundancyLevel, Size, Utils } from '@ethersphere/bee-js'
import './InitialModal.scss'
import { CustomDropdown } from '../CustomDropdown/CustomDropdown'
import { Button } from '../Button/Button'
import { calculateStampCapacityMetrics, fmFetchCost, getUsableStamps, handleCreateDrive } from '../../utils/bee'
import { getExpiryDateByLifetime, safeSetState } from '../../utils/common'
import { Context as BeeContext } from '../../../../providers/Bee'
import { Context as FMContext } from '../../../../providers/FileManager'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { erasureCodeMarks } from '../../constants/common'
import { desiredLifetimeOptions } from '../../constants/stamps'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { Context as BeeContext } from '../../../../providers/Bee'
import { FMSlider } from '../Slider/Slider'
import { Context as FMContext } from '../../../../providers/FileManager'
import { ADMIN_STAMP_LABEL } from '@solarpunkltd/file-manager-lib'
import { ProgressBar } from '../ProgressBar/ProgressBar'
import { Tooltip } from '../Tooltip/Tooltip'
import { TOOLTIPS } from '../../constants/tooltips'
import { calculateStampCapacityMetrics, fmFetchCost, getUsableStamps, handleCreateDrive } from '../../utils/bee'
import { getExpiryDateByLifetime, safeSetState } from '../../utils/common'
import { Button } from '../Button/Button'
import { CustomDropdown } from '../CustomDropdown/CustomDropdown'
import { ProgressBar } from '../ProgressBar/ProgressBar'
import { FMSlider } from '../Slider/Slider'
import { Tooltip } from '../Tooltip/Tooltip'
import './InitialModal.scss'
interface InitialModalProps {
resetState: boolean
@@ -1,13 +1,15 @@
import { ReactElement, useContext, useEffect, useState } from 'react'
import './NotificationBar.scss'
import UpIcon from 'remixicon-react/ArrowUpSLineIcon'
import { ExpiringNotificationModal } from '../ExpiringNotificationModal/ExpiringNotificationModal'
import { getUsableStamps } from '../../utils/bee'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { PostageBatch } from '@ethersphere/bee-js'
import { Context as FMContext } from '../../../../providers/FileManager'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useContext, useEffect, useState } from 'react'
import UpIcon from 'remixicon-react/ArrowUpSLineIcon'
import { Context as FMContext } from '../../../../providers/FileManager'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { FILE_MANAGER_EVENTS } from '../../constants/common'
import { getUsableStamps } from '../../utils/bee'
import { ExpiringNotificationModal } from '../ExpiringNotificationModal/ExpiringNotificationModal'
import './NotificationBar.scss'
const NUMBER_OF_DAYS_WARNING = 7
const DAYS_TO_MILLISECONDS_MULTIPLIER = 24 * 60 * 60 * 1000
@@ -1,26 +1,32 @@
import { useState, ReactElement, useEffect } from 'react'
import './PrivateKeyModal.scss'
import { Button } from '../Button/Button'
import { setSignerPk, getSigner } from '../../utils/common'
import { uuidV4 } from '../../../../utils'
import { PrivateKey } from '@ethersphere/bee-js'
import ClipboardIcon from 'remixicon-react/FileCopyLineIcon'
import { ReactElement, useState } from 'react'
import CheckDoubleLineIcon from 'remixicon-react/CheckDoubleLineIcon'
import { Tooltip } from '../Tooltip/Tooltip'
import ClipboardIcon from 'remixicon-react/FileCopyLineIcon'
import { TOOLTIPS } from '../../constants/tooltips'
import { getSigner, setSignerPk } from '../../utils/common'
import { Button } from '../Button/Button'
import { Tooltip } from '../Tooltip/Tooltip'
import './PrivateKeyModal.scss'
import { uuidV4 } from '@/utils'
type Props = { onSaved: () => void }
const generateNewPrivateKey = (): string => {
const id = uuidV4()
const signer = getSigner(id)
return signer.toHex()
}
export function PrivateKeyModal({ onSaved }: Props): ReactElement {
const [value, setValue] = useState('')
const [value, setValue] = useState(generateNewPrivateKey())
const [confirmValue, setConfirmValue] = useState('')
const [showError, setShowError] = useState(false)
const [copied, setCopied] = useState(false)
useEffect(() => {
handleGenerateNew()
}, [])
const handleCopyPrivateKey = async () => {
try {
await navigator.clipboard.writeText(value)
@@ -32,11 +38,7 @@ export function PrivateKeyModal({ onSaved }: Props): ReactElement {
}
const handleGenerateNew = () => {
const id = uuidV4()
const signer = getSigner(id)
const privKey = signer.toHex()
setValue(privKey)
setValue(generateNewPrivateKey())
setConfirmValue('')
setCopied(false)
setShowError(false)
@@ -1,12 +1,13 @@
import { ReactElement, useEffect, useMemo, useRef, useState } from 'react'
import React, { ReactElement, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import EditIcon from 'remixicon-react/EditLineIcon'
import { safeSetState } from '../../utils/common'
import { Button } from '../Button/Button'
import '../../styles/global.scss'
import './RenameFileModal.scss'
import { Button } from '../Button/Button'
import EditIcon from 'remixicon-react/EditLineIcon'
import { createPortal } from 'react-dom'
import { safeSetState } from '../../utils/common'
const maxFileNameLength = 60
interface RenameFileModalProps {
@@ -1,28 +1,30 @@
import { ReactElement, useState, useContext, useEffect, useMemo, useCallback, useRef, memo } from 'react'
import { createPortal } from 'react-dom'
import Drive from 'remixicon-react/HardDrive2LineIcon'
import DriveFill from 'remixicon-react/HardDrive2FillIcon'
import MoreFill from 'remixicon-react/MoreFillIcon'
import './DriveItem.scss'
import { ProgressBar } from '../../ProgressBar/ProgressBar'
import { ContextMenu } from '../../ContextMenu/ContextMenu'
import { useContextMenu } from '../../../hooks/useContextMenu'
import { Button } from '../../Button/Button'
import { DestroyDriveModal, ProgressDestroyModal } from '../../DestroyDriveModal/DestroyDriveModal'
import { UpgradeDriveModal } from '../../UpgradeDriveModal/UpgradeDriveModal'
import { UpgradeTimeoutModal } from '../../UpgradeTimeoutModal/UpgradeTimeoutModal'
import { ViewType } from '../../../constants/transfers'
import { useView } from '../../../../../pages/filemanager/ViewContext'
import { Context as FMContext } from '../../../../../providers/FileManager'
import { PostageBatch } from '@ethersphere/bee-js'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import { calculateStampCapacityMetrics, handleDestroyAndForgetDrive } from '../../../utils/bee'
import React, { memo, ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import DriveFill from 'remixicon-react/HardDrive2FillIcon'
import Drive from 'remixicon-react/HardDrive2LineIcon'
import MoreFill from 'remixicon-react/MoreFillIcon'
import { useView } from '../../../../../pages/filemanager/ViewContext'
import { Context as FMContext } from '../../../../../providers/FileManager'
import { Context as SettingsContext } from '../../../../../providers/Settings'
import { truncateNameMiddle } from '../../../utils/common'
import { Tooltip } from '../../Tooltip/Tooltip'
import { TOOLTIPS } from '../../../constants/tooltips'
import { FILE_MANAGER_EVENTS, UPLOAD_POLLING_TIMEOUT_MS } from '../../../constants/common'
import { TOOLTIPS } from '../../../constants/tooltips'
import { ViewType } from '../../../constants/transfers'
import { useContextMenu } from '../../../hooks/useContextMenu'
import { useStampPolling } from '../../../hooks/useStampPolling'
import { calculateStampCapacityMetrics, handleDestroyAndForgetDrive } from '../../../utils/bee'
import { truncateNameMiddle } from '../../../utils/common'
import { Button } from '../../Button/Button'
import { ContextMenu } from '../../ContextMenu/ContextMenu'
import { DestroyDriveModal, ProgressDestroyModal } from '../../DestroyDriveModal/DestroyDriveModal'
import { ProgressBar } from '../../ProgressBar/ProgressBar'
import { Tooltip } from '../../Tooltip/Tooltip'
import { UpgradeDriveModal } from '../../UpgradeDriveModal/UpgradeDriveModal'
import { UpgradeTimeoutModal } from '../../UpgradeTimeoutModal/UpgradeTimeoutModal'
import './DriveItem.scss'
function useDriveEventListeners(
driveId: string,
@@ -1,16 +1,18 @@
import { ReactElement, useState, useContext } from 'react'
import { createPortal } from 'react-dom'
import Drive from 'remixicon-react/HardDrive2LineIcon'
import DriveFill from 'remixicon-react/HardDrive2FillIcon'
import MoreFill from 'remixicon-react/MoreFillIcon'
import { ContextMenu } from '../../ContextMenu/ContextMenu'
import { useContextMenu } from '../../../hooks/useContextMenu'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import React, { ReactElement, useContext, useState } from 'react'
import { createPortal } from 'react-dom'
import DriveFill from 'remixicon-react/HardDrive2FillIcon'
import Drive from 'remixicon-react/HardDrive2LineIcon'
import MoreFill from 'remixicon-react/MoreFillIcon'
import { Context as FMContext } from '../../../../../providers/FileManager'
import { useContextMenu } from '../../../hooks/useContextMenu'
import { handleDestroyAndForgetDrive } from '../../../utils/bee'
import { ConfirmModal } from '../../ConfirmModal/ConfirmModal'
import './DriveItem.scss'
import { truncateNameMiddle } from '../../../utils/common'
import { ConfirmModal } from '../../ConfirmModal/ConfirmModal'
import { ContextMenu } from '../../ContextMenu/ContextMenu'
import './DriveItem.scss'
interface Props {
drive: DriveInfo
@@ -1,27 +1,29 @@
import { ReactElement, useContext, useEffect, useState } from 'react'
import './Sidebar.scss'
import Add from 'remixicon-react/AddLineIcon'
import Folder from 'remixicon-react/Folder3LineIcon'
import FolderFill from 'remixicon-react/Folder3FillIcon'
import ArrowRight from 'remixicon-react/ArrowRightSLineIcon'
import ArrowDown from 'remixicon-react/ArrowDownSLineIcon'
import Delete from 'remixicon-react/DeleteBin6LineIcon'
import DeleteFill from 'remixicon-react/DeleteBin6FillIcon'
import History from 'remixicon-react/HistoryLineIcon'
import HistoryFill from 'remixicon-react/HistoryFillIcon'
import { DriveItem } from './DriveItem/DriveItem'
import { ExpiredDriveItem } from './DriveItem/ExpiredDriveItem'
import { CreateDriveModal } from '../CreateDriveModal/CreateDriveModal'
import { ViewType } from '../../constants/transfers'
import { PostageBatch } from '@ethersphere/bee-js'
import { Context as SettingsContext } from '../../../../providers/Settings'
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 { getUsableStamps } from '../../utils/bee'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import { truncateNameMiddle } from '../../utils/common'
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
@@ -1,9 +1,10 @@
import Slider from '@mui/material/Slider'
import { ReactElement, useState } from 'react'
import './Slider.scss'
import Slider from '@material-ui/core/Slider'
import { makeStyles } from '@material-ui/core/styles'
import { makeStyles } from 'tss-react/mui'
const useStyles = makeStyles({
import './Slider.scss'
const useStyles = makeStyles()({
root: {
width: '98%',
marginLeft: '-3px',
@@ -50,7 +51,7 @@ export function FMSlider({
step,
}: FMSliderProps): ReactElement {
const [value, setValue] = useState(defaultValue || 0)
const classes = useStyles()
const { classes } = useStyles()
return (
<>
@@ -1,5 +1,6 @@
import { ReactElement, useState, useRef, useCallback } from 'react'
import React, { ReactElement, useCallback, useRef, useState } from 'react'
import InfoIcon from 'remixicon-react/InformationLineIcon'
import './Tooltip.scss'
interface TooltipProps {
@@ -82,7 +83,7 @@ export function Tooltip({
}
: undefined
}
// eslint-disable-next-line react/no-danger
// Safe: label is always from static TOOLTIPS constant or hardcoded strings, never user input
dangerouslySetInnerHTML={{ __html: label }}
/>
</span>
@@ -1,13 +1,3 @@
import { ReactElement, useCallback, useContext, useEffect, useRef, useState } from 'react'
import './UpgradeDriveModal.scss'
import '../../styles/global.scss'
import { Warning } from '@material-ui/icons'
import { createPortal } from 'react-dom'
import DriveIcon from 'remixicon-react/HardDrive2LineIcon'
import DatabaseIcon from 'remixicon-react/Database2LineIcon'
import WalletIcon from 'remixicon-react/Wallet3LineIcon'
import ExternalLinkIcon from 'remixicon-react/ExternalLinkLineIcon'
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
import {
BatchId,
BeeRequestOptions,
@@ -19,18 +9,29 @@ import {
Size,
Utils,
} from '@ethersphere/bee-js'
import { Warning } from '@mui/icons-material'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useCallback, useContext, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
import DatabaseIcon from 'remixicon-react/Database2LineIcon'
import ExternalLinkIcon from 'remixicon-react/ExternalLinkLineIcon'
import DriveIcon from 'remixicon-react/HardDrive2LineIcon'
import WalletIcon from 'remixicon-react/Wallet3LineIcon'
import { CustomDropdown } from '../CustomDropdown/CustomDropdown'
import { Button } from '../Button/Button'
import { desiredLifetimeOptions } from '../../constants/stamps'
import { Context as BeeContext } from '../../../../providers/Bee'
import { fromBytesConversion, getExpiryDateByLifetime, truncateNameMiddle } from '../../utils/common'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { Context as FMContext } from '../../../../providers/FileManager'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { getHumanReadableFileSize } from '../../../../utils/file'
import { useStampPolling } from '../../hooks/useStampPolling'
import { FILE_MANAGER_EVENTS, POLLING_TIMEOUT_MS } from '../../constants/common'
import { desiredLifetimeOptions } from '../../constants/stamps'
import { useStampPolling } from '../../hooks/useStampPolling'
import { fromBytesConversion, getExpiryDateByLifetime, truncateNameMiddle } from '../../utils/common'
import { Button } from '../Button/Button'
import { CustomDropdown } from '../CustomDropdown/CustomDropdown'
import './UpgradeDriveModal.scss'
import '../../styles/global.scss'
interface UpgradeDriveModalProps {
stamp: PostageBatch
@@ -117,7 +118,7 @@ export function UpgradeDriveModal({
try {
cost = await beeApi?.getExtensionCost(batchId, capacity, duration, options, encryption, erasureCodeLevel)
} catch (e) {
} catch {
setErrorMessage?.('Failed to calculate extension cost')
setShowError(true)
@@ -281,7 +282,7 @@ export function UpgradeDriveModal({
icon={<CalendarIcon size="14px" color="rgb(237, 129, 49)" />}
options={desiredLifetimeOptions}
value={lifetimeIndex}
onChange={(value, index) => {
onChange={(value, _) => {
setLifetimeIndex(value)
}}
/>
@@ -1,6 +1,8 @@
import { ReactElement } from 'react'
import { createPortal } from 'react-dom'
import { Button } from '../Button/Button'
import '../../styles/global.scss'
import './UpgradeTimeoutModal.scss'
@@ -1,8 +1,10 @@
import { ReactElement, useMemo, useState } from 'react'
import WarningIcon from 'remixicon-react/ErrorWarningLineIcon'
import { Button } from '../Button/Button'
import './UploadConflictModal.scss'
import '../../styles/global.scss'
import { Button } from '../Button/Button'
import WarningIcon from 'remixicon-react/ErrorWarningLineIcon'
interface Props {
filename: string
@@ -1,24 +1,24 @@
import { ReactElement, useEffect, useMemo, useState, useCallback, useContext } from 'react'
import './VersionHistoryModal.scss'
import '../../styles/global.scss'
import { Button } from '../Button/Button'
import { FeedIndex } from '@ethersphere/bee-js'
import { FileInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import HistoryIcon from 'remixicon-react/HistoryLineIcon'
import { Context as FMContext } from '../../../../providers/FileManager'
import { FileInfo } from '@solarpunkltd/file-manager-lib'
import { FeedIndex } from '@ethersphere/bee-js'
import { ConflictAction, useUploadConflictDialog } from '../../hooks/useUploadConflictDialog'
import { ConfirmModal } from '../ConfirmModal/ConfirmModal'
import { Tooltip } from '../Tooltip/Tooltip'
import { TOOLTIPS } from '../../constants/tooltips'
import { verifyDriveSpace } from '../../utils/bee'
import { indexStrToBigint, truncateNameMiddle } from '../../utils/common'
import { VersionsList } from './VersionList/VersionList'
import { ActionTag, DownloadProgress, TrackDownloadProps } from '../../constants/transfers'
import { useTransfers } from '../../hooks/useTransfers'
import { ConflictAction, useUploadConflictDialog } from '../../hooks/useUploadConflictDialog'
import { verifyDriveSpace } from '../../utils/bee'
import { indexStrToBigint, truncateNameMiddle } from '../../utils/common'
import { Button } from '../Button/Button'
import { ConfirmModal } from '../ConfirmModal/ConfirmModal'
import { Tooltip } from '../Tooltip/Tooltip'
import { VersionsList } from './VersionList/VersionList'
import './VersionHistoryModal.scss'
import '../../styles/global.scss'
const VERSION_HISTORY_PAGE_SIZE = 5
@@ -1,24 +1,23 @@
import './VersionList.scss'
import '../../../styles/global.scss'
import { memo, useState, useCallback, useContext } from 'react'
import { Button } from '../../Button/Button'
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
import UserIcon from 'remixicon-react/UserLineIcon'
import DownloadIcon from 'remixicon-react/Download2LineIcon'
import { FileInfo } from '@solarpunkltd/file-manager-lib'
import { memo, useCallback, useContext, useState } from 'react'
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
import DownloadIcon from 'remixicon-react/Download2LineIcon'
import UserIcon from 'remixicon-react/UserLineIcon'
import { Context as FMContext } from '../../../../../providers/FileManager'
import { Context as SettingsContext } from '../../../../../providers/Settings'
import { uuidV4 } from '../../../../../utils'
import { TOOLTIPS } from '../../../constants/tooltips'
import { ActionTag, DownloadProgress, TrackDownloadProps } from '../../../constants/transfers'
import { useContextMenu } from '../../../hooks/useContextMenu'
import { capitalizeFirstLetter, formatBytes, indexStrToBigint, truncateNameMiddle } from '../../../utils/common'
import { startDownloadingQueue } from '../../../utils/download'
import { ActionTag, DownloadProgress, TrackDownloadProps } from '../../../constants/transfers'
import { Context as SettingsContext } from '../../../../../providers/Settings'
import { useContextMenu } from '../../../hooks/useContextMenu'
import { uuidV4 } from '../../../../../utils'
import { Button } from '../../Button/Button'
import { ConfirmModal } from '../../ConfirmModal/ConfirmModal'
import { Tooltip } from '../../Tooltip/Tooltip'
import { TOOLTIPS } from '../../../constants/tooltips'
import './VersionList.scss'
import '../../../styles/global.scss'
interface VersionListProps {
versions: FileInfo[]
+2 -1
View File
@@ -1,4 +1,5 @@
import { FeedIndex, RedundancyLevel } from '@ethersphere/bee-js'
import { capitalizeFirstLetter } from '../utils/common'
export const FEED_INDEX_ZERO = FeedIndex.fromBigInt(BigInt(0))
@@ -17,7 +18,7 @@ export const FILE_MANAGER_EVENTS = {
DRIVE_UPGRADE_TIMEOUT: 'fm:drive-upgrade-timeout',
} as const
export type FileManagerEventName = typeof FILE_MANAGER_EVENTS[keyof typeof FILE_MANAGER_EVENTS]
export type FileManagerEventName = (typeof FILE_MANAGER_EVENTS)[keyof typeof FILE_MANAGER_EVENTS]
export const POLLING_TIMEOUT_MS = 90000
export const UPLOAD_POLLING_TIMEOUT_MS = 10000
@@ -55,8 +55,6 @@ This action will do two things:
It will create the Admin Drive.`,
ADMIN_STATUS_WARNING:
'The File Manager works only while your storage remains valid. If it expires, all catalogue metadata is permanently lost.',
// Drive Creation
DRIVE_NAME: `${getTitleWithStyle('About Drive Name')}
Set a human-readable label for this drive (e.g. Personal files). This name is stored as metadata.`,
@@ -1,14 +1,15 @@
import { useCallback, useMemo, useRef, useState, useContext, useEffect } from 'react'
import type { FileInfo } from '@solarpunkltd/file-manager-lib'
import { PostageBatch } from '@ethersphere/bee-js'
import type { FileInfo } from '@solarpunkltd/file-manager-lib'
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { Context as FMContext } from '../../../providers/FileManager'
import { Context as SettingsContext } from '../../../providers/Settings'
import { startDownloadingQueue } from '../utils/download'
import { formatBytes, getFileId, safeSetState } from '../utils/common'
import { uuidV4 } from '../../../utils'
import { DownloadProgress, TrackDownloadProps } from '../constants/transfers'
import { getUsableStamps } from '../utils/bee'
import { performBulkFileOperation, FileOperation } from '../utils/fileOperations'
import { uuidV4 } from '../../../utils'
import { formatBytes, getFileId, safeSetState } from '../utils/common'
import { startDownloadingQueue } from '../utils/download'
import { FileOperation, performBulkFileOperation } from '../utils/fileOperations'
interface BulkOptions {
listToRender: FileInfo[]
@@ -1,11 +1,15 @@
import { useEffect } from 'react'
import React, { useEffect } from 'react'
export function useClickOutside<T extends Element>(ref: React.RefObject<T>, onClickOutside: () => void, active = true) {
export function useClickOutside<T extends HTMLDivElement>(
ref: React.RefObject<T | null>,
onClickOutside: () => void,
active = true,
) {
useEffect(() => {
if (!active) return
function handleDocumentClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
if (ref?.current && !ref.current.contains(e.target as Node)) {
onClickOutside()
}
}
@@ -1,11 +1,13 @@
import { useState, useRef } from 'react'
import { useClickOutside } from './useClickOutside'
import React, { useRef, useState } from 'react'
import { Point } from '../utils/common'
import { useClickOutside } from './useClickOutside'
export function useContextMenu<T extends Element = HTMLDivElement>() {
const [showContext, setShowContext] = useState(false)
const [pos, setPos] = useState<Point>({ x: 0, y: 0 })
const contextRef = useRef<T | null>(null)
const contextRef = useRef<HTMLDivElement | null>(null)
function handleContextMenu(e: React.MouseEvent<T> | MouseEvent) {
e.preventDefault()
@@ -1,5 +1,5 @@
import { useCallback, useRef, useState } from 'react'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import React, { useCallback, useRef, useState } from 'react'
interface UseDragAndDropProps {
onFilesDropped: (files: FileList) => void
@@ -1,5 +1,6 @@
import { useMemo, useCallback } from 'react'
import { FileInfo, DriveInfo } from '@solarpunkltd/file-manager-lib'
import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import { useCallback, useMemo } from 'react'
import { ViewType } from '../constants/transfers'
import { indexStrToBigint, isTrashed } from '../utils/common'
+7 -6
View File
@@ -1,5 +1,7 @@
import { useEffect, useMemo, useState } from 'react'
import type { FileInfo } from '@solarpunkltd/file-manager-lib'
import { useEffect, useMemo, useState } from 'react'
import { LocalStorageKeys } from '../../../utils/localStorage'
export enum SortKey {
Name = 'name',
@@ -19,10 +21,9 @@ type Options = {
persist?: boolean
defaultState?: SortState
storageKey?: string
getDriveName?: (fi: FileInfo) => string
getDriveName?: (driveId: string) => string
}
const STORAGE_KEY = 'fm.sort.v1'
const DEFAULT_STATE: SortState = { key: SortKey.Timestamp, dir: SortDir.Desc }
const coerceNumber = (v: unknown): number => {
@@ -61,7 +62,7 @@ export function useSorting(
toggle: (key: SortKey) => void
reset: () => void
} {
const { persist = true, defaultState = DEFAULT_STATE, storageKey = STORAGE_KEY, getDriveName } = opts
const { persist = true, defaultState = DEFAULT_STATE, storageKey = LocalStorageKeys.fmSortKey, getDriveName } = opts
const [sort, setSort] = useState<SortState>(() => {
if (!persist) return defaultState
@@ -127,8 +128,8 @@ export function useSorting(
}
if (sort.key === SortKey.Drive) {
const ad = (getDriveName?.(a) ?? '').toLocaleLowerCase()
const bd = (getDriveName?.(b) ?? '').toLocaleLowerCase()
const ad = (getDriveName?.(a.driveId.toString()) ?? '').toLocaleLowerCase()
const bd = (getDriveName?.(b.driveId.toString()) ?? '').toLocaleLowerCase()
if (ad < bd) return -1 * mul
@@ -1,5 +1,6 @@
import { useRef, useCallback } from 'react'
import { PostageBatch } from '@ethersphere/bee-js'
import { useCallback, useRef } from 'react'
import { POLLING_INTERVAL_MS } from '../constants/common'
interface UseStampPollingOptions {
+10 -9
View File
@@ -1,9 +1,10 @@
import { useCallback, useState, useContext, useRef, useEffect } from 'react'
import type { FileInfo, FileInfoOptions, UploadProgress } from '@solarpunkltd/file-manager-lib'
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
import { Context as FMContext } from '../../../providers/FileManager'
import { Context as SettingsContext } from '../../../providers/Settings'
import type { FileInfo, FileInfoOptions, UploadProgress } from '@solarpunkltd/file-manager-lib'
import { ConflictAction, useUploadConflictDialog } from './useUploadConflictDialog'
import { formatBytes, safeSetState, truncateNameMiddle } from '../utils/common'
import { uuidV4 } from '../../../utils'
import { FILE_MANAGER_EVENTS } from '../constants/common'
import {
DownloadProgress,
DownloadState,
@@ -11,12 +12,12 @@ import {
TrackDownloadProps,
TransferStatus,
} from '../constants/transfers'
import { validateStampStillExists, verifyDriveSpace } from '../utils/bee'
import { isTrashed } from '../utils/common'
import { abortDownload } from '../utils/download'
import { AbortManager } from '../utils/abortManager'
import { uuidV4 } from '../../../utils'
import { FILE_MANAGER_EVENTS } from '../constants/common'
import { validateStampStillExists, verifyDriveSpace } from '../utils/bee'
import { formatBytes, isTrashed, safeSetState, truncateNameMiddle } from '../utils/common'
import { abortDownload } from '../utils/download'
import { ConflictAction, useUploadConflictDialog } from './useUploadConflictDialog'
const SAMPLE_WINDOW_MS = 500
const ETA_SMOOTHING = 0.3
@@ -1,5 +1,6 @@
import { ReactPortal, useCallback, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import { UploadConflictModal } from '../components/UploadConflictModal/UploadConflictModal'
export enum ConflictAction {
@@ -1,15 +1,22 @@
import { ReactElement } from 'react'
import ImageIcon from 'remixicon-react/Image2LineIcon'
import FileIcon from 'remixicon-react/FileTextLineIcon'
import ImageIcon from 'remixicon-react/Image2LineIcon'
import { guessMime } from './view'
interface ContextMenuProps {
icon: string
name: string
metadata?: Record<string, string>
size?: string
color?: string
}
export function GetIconElement({ icon, size = '21px', color = '#ed8131' }: ContextMenuProps): ReactElement {
switch (icon) {
export function GetIconElement({ name, metadata, size = '21px', color = '#ed8131' }: ContextMenuProps): ReactElement {
const { mime } = guessMime(name, metadata)
const iconType = mime.split('/')[0]?.toLowerCase() || 'file'
switch (iconType) {
case 'image':
return <ImageIcon size={size} color={color} />
default:
+4 -3
View File
@@ -1,11 +1,13 @@
import { BatchId, Bee, BZZ, Duration, PostageBatch, RedundancyLevel, Size } from '@ethersphere/bee-js'
import {
FileManagerBase,
DriveInfo,
estimateDriveListMetadataSize,
estimateFileInfoMetadataSize,
FileInfo,
FileManagerBase,
} from '@solarpunkltd/file-manager-lib'
import React from 'react'
import { getHumanReadableFileSize } from '../../../utils/file'
import { ActionTag } from '../constants/transfers'
@@ -57,7 +59,7 @@ export const fmGetStorageCost = async (
}
return undefined
} catch (e) {
} catch {
return undefined
}
}
@@ -172,7 +174,6 @@ export const handleCreateDrive = async (options: CreateDriveOptions): Promise<vo
batchId = await beeApi.buyStorage(size, duration, { label }, undefined, encryption, redundancyLevel)
} else {
// TODO: redundant, fm checks for stamp validtiy
const isValid = await validateStampStillExists(beeApi, existingBatch.batchID)
if (!isValid) {
+11 -12
View File
@@ -1,7 +1,8 @@
import { PrivateKey } from '@ethersphere/bee-js'
import { Bytes, PrivateKey } from '@ethersphere/bee-js'
import { FileInfo, FileStatus } from '@solarpunkltd/file-manager-lib'
import { keccak256 } from '@ethersproject/keccak256'
import { toUtf8Bytes } from '@ethersproject/strings'
import React from 'react'
import { LocalStorageKeys } from '../../../utils/localStorage'
import { lifetimeAdjustments } from '../constants/stamps'
export function getDaysLeft(expiryDate: Date): number {
@@ -83,19 +84,17 @@ export function getFileId(fi: FileInfo): string {
return fi.topic.toString()
}
export const KEY_STORAGE = 'privateKey'
export function getSigner(input: string): PrivateKey {
const normalized = input.trim().toLowerCase()
const hash = keccak256(toUtf8Bytes(normalized))
const privateKeyHex = hash.slice(2)
const inputBytes = Bytes.fromUtf8(normalized)
const privateKeyHex = Bytes.keccak256(inputBytes).toHex()
return new PrivateKey(privateKeyHex)
}
export function getSignerPk(): PrivateKey | undefined {
try {
const fromLocalPk = localStorage.getItem(KEY_STORAGE)
const fromLocalPk = localStorage.getItem(LocalStorageKeys.fmPrivateKey)
if (!fromLocalPk) {
// eslint-disable-next-line no-console
@@ -107,24 +106,24 @@ export function getSignerPk(): PrivateKey | undefined {
return new PrivateKey(fromLocalPk)
} catch (err) {
// eslint-disable-next-line no-console
console.error(`Private key error in localStorage under key "${KEY_STORAGE}": `, err)
console.error(`Private key error in localStorage under key "${LocalStorageKeys.fmPrivateKey}": `, err)
return undefined
}
}
export function setSignerPk(pk: string): void {
localStorage.setItem(KEY_STORAGE, pk)
localStorage.setItem(LocalStorageKeys.fmPrivateKey, pk)
}
export function removeSignerPk(): void {
localStorage.removeItem(KEY_STORAGE)
localStorage.removeItem(LocalStorageKeys.fmPrivateKey)
}
export const capitalizeFirstLetter = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1)
export const safeSetState =
<T>(ref: React.MutableRefObject<boolean>, setter: React.Dispatch<React.SetStateAction<T>>) =>
<T>(ref: React.RefObject<boolean>, setter: React.Dispatch<React.SetStateAction<T>>) =>
(value: React.SetStateAction<T>) => {
if (ref.current) setter(value)
}
+9 -2
View File
@@ -1,8 +1,10 @@
import { FileInfo, FileManager } from '@solarpunkltd/file-manager-lib'
import { guessMime, VIEWERS } from './view'
import { AbortManager } from './abortManager'
import { DownloadProgress, DownloadState } from '../constants/transfers'
import { AbortManager } from './abortManager'
import { guessMime, VIEWERS } from './view'
const downloadAborts = new AbortManager()
enum Errors {
@@ -54,6 +56,7 @@ const processStream = async (
onDownloadProgress?.({ progress, isDownloading: false, state: DownloadState.Cancelled })
} else {
onDownloadProgress?.({ progress, isDownloading: false, state: DownloadState.Error })
// eslint-disable-next-line no-console
console.error('Failed to process stream: ', e)
}
@@ -70,6 +73,7 @@ const processStream = async (
}
} catch (e: unknown) {
/* no-op */
// eslint-disable-next-line no-console
console.error('filehandle close/abort error: ', e)
}
@@ -106,6 +110,7 @@ const streamToBlob = async (
onDownloadProgress?.({ progress, isDownloading: false, state: DownloadState.Cancelled })
} else {
onDownloadProgress?.({ progress, isDownloading: false, state: DownloadState.Error })
// eslint-disable-next-line no-console
console.error('Error during stream processing: ', error)
}
@@ -133,6 +138,7 @@ interface FileInfoWithHandle {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isPickerSupported = (): boolean => typeof (window as any).showSaveFilePicker === 'function'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isDirectoryPickerSupported = (): boolean => typeof (window as any).showDirectoryPicker === 'function'
@@ -384,6 +390,7 @@ export const startDownloadingQueue = async (
if (!isAbortError) {
tracker?.({ progress: 0, isDownloading: false, state: DownloadState.Error })
// eslint-disable-next-line no-console
console.error('download queue error: ', error)
} else {
@@ -1,9 +1,10 @@
import type { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import type { FileManagerBase } from '@solarpunkltd/file-manager-lib'
import type { PostageBatch, RedundancyLevel } from '@ethersphere/bee-js'
import type { DriveInfo, FileInfo, FileManagerBase } from '@solarpunkltd/file-manager-lib'
import { ActionTag } from '../constants/transfers'
import { verifyDriveSpace } from './bee'
import { capitalizeFirstLetter } from './common'
import { ActionTag } from '../constants/transfers'
export enum FileOperation {
Trash = 'trash',
+14 -9
View File
@@ -1,13 +1,14 @@
import type { ReactElement } from 'react'
import { FileStatus, FileInfo, FileManagerBase } from '@solarpunkltd/file-manager-lib'
import { GetGranteesResult, PostageBatch } from '@ethersphere/bee-js'
import GeneralIcon from 'remixicon-react/FileTextLineIcon'
import { FileInfo, FileManagerBase, FileStatus } from '@solarpunkltd/file-manager-lib'
import type { ReactElement } from 'react'
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
import AccessIcon from 'remixicon-react/ShieldKeyholeLineIcon'
import GeneralIcon from 'remixicon-react/FileTextLineIcon'
import HardDriveIcon from 'remixicon-react/HardDrive2LineIcon'
import AccessIcon from 'remixicon-react/ShieldKeyholeLineIcon'
import { erasureCodeMarks, FEED_INDEX_ZERO } from '../constants/common'
import { indexStrToBigint, truncateNameMiddle } from './common'
import { FEED_INDEX_ZERO, erasureCodeMarks } from '../constants/common'
export type FileProperty = { key: string; label: string; value: string; raw?: string }
export type FilePropertyGroup = { title: string; icon?: ReactElement; properties: FileProperty[] }
@@ -98,7 +99,7 @@ function buildGeneralGroup(
icon: <GeneralIcon size="14px" color="rgb(237, 129, 49)" />,
properties: [
{ key: 'type', label: 'Type', value: mime ?? dash },
{ key: 'size', label: 'Size', value: size != null ? formatBytes(size) : dash },
{ key: 'size', label: 'Size', value: size !== undefined && size !== null ? formatBytes(size) : dash },
{ key: 'count', label: 'Items', value: fileCount ?? '1' },
{ key: 'path', label: 'Location', value: truncateNameMiddle(path || dash, 35, 10, 10) },
{
@@ -137,7 +138,11 @@ function buildAccessGroup(fi: FileInfo, granteeCount?: number): FilePropertyGrou
raw: fi.owner.toString(),
},
{ key: 'shared', label: 'Sharing', value: fi.shared ? 'Shared' : 'Private' },
{ key: 'grantees', label: 'Grantees', value: granteeCount != null ? `${granteeCount}` : dash },
{
key: 'grantees',
label: 'Grantees',
value: granteeCount !== undefined && granteeCount !== null ? `${granteeCount}` : dash,
},
{
key: 'actpub',
label: 'ACT Publisher',
@@ -167,7 +172,7 @@ function buildStorageGroup(fi: FileInfo, driveName: string, stamp?: PostageBatch
const redundancyLabel =
fi.redundancyLevel !== undefined
? erasureCodeMarks.find(mark => mark.value === fi.redundancyLevel)?.label ?? fi.redundancyLevel.toString()
? (erasureCodeMarks.find(mark => mark.value === fi.redundancyLevel)?.label ?? fi.redundancyLevel.toString())
: dash
return {
+1 -1
View File
@@ -1,4 +1,4 @@
import { Point, Dir } from './common'
import { Dir, Point } from './common'
export function computeContextMenuPosition(args: {
clickPos: Point