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

- drive capacity display with stamp polling
- download/upload progress handling
- overlay and tooltip issues
- FileMaganger readme
- ultra-light mode handling
- account feed view page
- download media files
- remove not found syncing link
- fix ultra light node wallet page
- tooltip issues
---------
Co-authored-by: Andrei Mitrea <andrei.mitrea.hq@gmail.com>
Co-authored-by: nidishk <nidishkrishnan45@gmail.com>
Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
Co-authored-by: Nándor Komlódi <nandor.komlodi@gmail.com>
Co-authored-by: rolandlor <33499567+rolandlor@users.noreply.github.com>
This commit is contained in:
Bálint Ujvári
2026-01-26 12:57:14 +01:00
committed by GitHub
parent ecadafd21d
commit 0d5138f5bc
78 changed files with 3961 additions and 1194 deletions
+274 -107
View File
@@ -1,8 +1,9 @@
import { useCallback, useState, useContext, useRef, useEffect } 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 } from '../utils/common'
import { formatBytes, safeSetState, truncateNameMiddle } from '../utils/common'
import {
DownloadProgress,
DownloadState,
@@ -10,13 +11,17 @@ import {
TrackDownloadProps,
TransferStatus,
} from '../constants/transfers'
import { calculateStampCapacityMetrics } from '../utils/bee'
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'
const SAMPLE_WINDOW_MS = 500
const ETA_SMOOTHING = 0.3
const MAX_UPLOAD_FILES = 10
const ABORT_EVENT = 'abort'
type ResolveResult = {
cancelled: boolean
@@ -27,6 +32,7 @@ type ResolveResult = {
}
type TransferItem = {
uuid: string
name: string
size?: string
percent: number
@@ -47,12 +53,15 @@ type ETAState = {
type UploadMeta = Record<string, string | number>
type UploadTask = {
uuid: string
file: File
finalName: string
prettySize?: string
isReplace: boolean
replaceTopic?: string
replaceHistory?: string
driveId: string
driveName: string
}
const normalizeCustomMetadata = (meta: UploadMeta): Record<string, string> => {
@@ -62,15 +71,21 @@ const normalizeCustomMetadata = (meta: UploadMeta): Record<string, string> => {
return out
}
const buildUploadMeta = (files: File[] | FileList, path?: string): UploadMeta => {
const buildUploadMeta = (files: File[] | FileList, path?: string, existingFile?: FileInfo): UploadMeta => {
const arr = Array.from(files as File[])
const totalSize = arr.reduce((acc, f) => acc + (f.size || 0), 0)
const primary = arr[0]
const previousAccumulated = existingFile
? Number(existingFile.customMetadata?.accumulatedSize || existingFile.customMetadata?.size || 0)
: 0
const accumulatedSize = previousAccumulated + totalSize
const meta: UploadMeta = {
size: String(totalSize),
fileCount: String(arr.length),
mime: primary?.type || 'application/octet-stream',
accumulatedSize: String(accumulatedSize),
}
if (path) meta.path = path
@@ -120,17 +135,23 @@ const calculateETA = (
}
}
const updateTransferItems = <T extends TransferItem>(items: T[], name: string, update: Partial<T>): T[] => {
return items.map(item => (item.name === name ? { ...item, ...update } : item))
const updateTransferItems = <T extends TransferItem>(items: T[], uuid: string, update: Partial<T>): T[] => {
return items.map(item => {
const matches = item.uuid === uuid
return matches ? { ...item, ...update } : item
})
}
const createTransferItem = (
uuid: string,
name: string,
size: string | undefined,
kind: FileTransferType,
driveName?: string,
status: TransferStatus = TransferStatus.Uploading,
): TransferItem => ({
uuid,
name,
size,
percent: 0,
@@ -147,13 +168,14 @@ interface TransferProps {
}
export function useTransfers({ setErrorMessage }: TransferProps) {
const { fm, currentDrive, currentStamp, files, setShowError, refreshStamp } = useContext(FMContext)
const { fm, adminDrive, currentDrive, currentStamp, files, setShowError, refreshStamp } = useContext(FMContext)
const { beeApi } = useContext(SettingsContext)
const [openConflict, conflictPortal] = useUploadConflictDialog()
const isMountedRef = useRef(true)
const uploadAbortsRef = useRef<AbortManager>(new AbortManager())
const queueRef = useRef<UploadTask[]>([])
const runningRef = useRef(false)
const cancelledNamesRef = useRef<Set<string>>(new Set())
const cancelledQueuedRef = useRef<Set<string>>(new Set())
const cancelledUploadingRef = useRef<Set<string>>(new Set())
const cancelledDownloadingRef = useRef<Set<string>>(new Set())
@@ -167,23 +189,27 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
i => i.status !== TransferStatus.Done && i.status !== TransferStatus.Error && i.status !== TransferStatus.Cancelled,
)
const clearAllFlagsFor = useCallback((name: string) => {
cancelledNamesRef.current.delete(name)
cancelledUploadingRef.current.delete(name)
uploadAbortsRef.current.abort(name)
queueRef.current = queueRef.current.filter(t => t.finalName !== name)
const clearAllFlagsFor = useCallback((uuid: string) => {
cancelledQueuedRef.current.delete(uuid)
cancelledUploadingRef.current.delete(uuid)
uploadAbortsRef.current.abort(uuid)
queueRef.current = queueRef.current.filter(t => {
return t.uuid !== uuid
})
}, [])
const ensureQueuedRow = useCallback(
(name: string, kind: FileTransferType, size?: string, driveName?: string) => {
clearAllFlagsFor(name)
(uuid: string, name: string, kind: FileTransferType, size?: string, driveName?: string) => {
safeSetState(
isMountedRef,
setUploadItems,
)(prev => {
const idx = prev.findIndex(p => p.name === name)
const base = createTransferItem(name, size, kind, driveName, TransferStatus.Queued)
const idx = prev.findIndex(p => p.uuid === uuid && p.status !== TransferStatus.Done)
const base = createTransferItem(uuid, name, size, kind, driveName, TransferStatus.Queued)
if (idx !== -1) {
clearAllFlagsFor(uuid)
}
if (idx === -1) return [...prev, base]
const copy = [...prev]
@@ -211,6 +237,10 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
const existing = taken[0]
const isTrashedExisting = existing ? isTrashed(existing) : false
if (!existing && allTakenNames.has(originalName)) {
return { cancelled: false, finalName: originalName, isReplace: false }
}
const choice = await openConflict({
originalName,
existingNames: allTakenNames,
@@ -237,14 +267,19 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
)
const trackUpload = useCallback(
(name: string, size?: string, kind: FileTransferType = FileTransferType.Upload) => {
(
uuid: string,
name: string,
size?: string,
kind: FileTransferType = FileTransferType.Upload,
driveName?: string,
) => {
if (!isMountedRef.current) {
return () => {
// no-op
}
}
const driveName = currentDrive?.name
const startedAt = Date.now()
let etaState: ETAState = {
@@ -254,8 +289,11 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
}
setUploadItems(prev => {
const idx = prev.findIndex(p => p.name === name)
const base = createTransferItem(name, size, kind, driveName, TransferStatus.Uploading)
const existing = prev.find(p => p.uuid === uuid)
const actualDriveName = existing?.driveName || driveName
const idx = prev.findIndex(p => p.uuid === uuid)
const base = createTransferItem(uuid, name, size, kind, actualDriveName, TransferStatus.Uploading)
if (idx === -1) return [...prev, base]
const copy = [...prev]
@@ -265,7 +303,9 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
})
const onProgress = (progress: UploadProgress) => {
if (cancelledUploadingRef.current.has(name) || !isMountedRef.current) return
const signal = uploadAbortsRef.current.getSignal(uuid)
if (cancelledUploadingRef.current.has(uuid) || !isMountedRef.current || signal?.aborted) return
if (progress.total > 0) {
const now = Date.now()
@@ -274,91 +314,134 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
const { etaSec, updatedState } = calculateETA(etaState, progress, startedAt, now)
etaState = updatedState
setUploadItems(prev =>
updateTransferItems(prev, name, {
percent: Math.max(prev.find(it => it.name === name)?.percent || 0, chunkPercentage),
kind,
etaSec,
}),
)
const isComplete = progress.processed >= progress.total
if (progress.processed >= progress.total) {
const finishedAt = Date.now()
setUploadItems(prev =>
updateTransferItems(prev, name, {
setUploadItems(prev => {
const existing = prev.find(it => it.uuid === uuid && it.status === TransferStatus.Uploading)
if (!existing || existing.status === TransferStatus.Done) return prev
if (isComplete) {
return updateTransferItems(prev, uuid, {
percent: 100,
status: TransferStatus.Done,
kind,
etaSec: 0,
elapsedSec: Math.round((finishedAt - startedAt) / 1000),
}),
)
}
elapsedSec: Math.round((now - startedAt) / 1000),
})
}
return updateTransferItems(prev, uuid, {
percent: Math.max(existing.percent, chunkPercentage),
kind,
etaSec,
})
})
}
}
return onProgress
},
[currentDrive?.name],
[],
)
const processUploadTask = useCallback(
async (task: UploadTask) => {
if (!fm || !currentDrive) return
if (!fm) return
const taskDrive = fm.driveList.find(d => d.id.toString() === task.driveId)
if (!taskDrive) {
return
}
const existingFile = task.isReplace ? files.find(f => f.topic.toString() === task.replaceTopic) : undefined
const info: FileInfoOptions = {
name: task.finalName,
files: [task.file],
customMetadata: normalizeCustomMetadata(buildUploadMeta([task.file])),
customMetadata: normalizeCustomMetadata(buildUploadMeta([task.file], undefined, existingFile)),
topic: task.isReplace ? task.replaceTopic : undefined,
}
const progressCb = trackUpload(
task.uuid,
task.finalName,
task.prettySize,
task.isReplace ? FileTransferType.Update : FileTransferType.Upload,
taskDrive.name,
)
safeSetState(
isMountedRef,
setUploadItems,
)(prev =>
updateTransferItems(prev, task.finalName, {
updateTransferItems(prev, task.uuid, {
status: TransferStatus.Uploading,
kind: task.isReplace ? FileTransferType.Update : FileTransferType.Upload,
startedAt: Date.now(),
}),
)
uploadAbortsRef.current.create(task.finalName)
uploadAbortsRef.current.create(task.uuid)
const signal = uploadAbortsRef.current.getSignal(task.uuid)
let reject: (reason?: Error) => void
const abortHandler = () => {
reject(new Error('Upload cancelled'))
}
const checkCancellation = new Promise<never>((_, rej) => {
reject = rej
signal?.addEventListener(ABORT_EVENT, abortHandler)
})
try {
await fm.upload(
currentDrive,
if (signal?.aborted) {
throw new Error('Upload cancelled')
}
const uploadPromise = fm.upload(
taskDrive,
{ ...info, onUploadProgress: progressCb },
{ actHistoryAddress: task.isReplace ? task.replaceHistory : undefined },
)
await Promise.race([uploadPromise, checkCancellation])
if (currentStamp) {
await refreshStamp(currentStamp.batchID.toString())
}
} catch {
const wasCancelled = cancelledUploadingRef.current.has(task.finalName)
} catch (error) {
const wasCancelled = cancelledUploadingRef.current.has(task.uuid) || signal?.aborted
if (!wasCancelled) {
const errorMsg = error instanceof Error ? error.message : String(error)
setErrorMessage?.(`Upload failed: ${errorMsg}`)
setShowError(true)
}
safeSetState(
isMountedRef,
setUploadItems,
)(prev =>
updateTransferItems(prev, task.finalName, {
updateTransferItems(prev, task.uuid, {
status: wasCancelled ? TransferStatus.Cancelled : TransferStatus.Error,
}),
)
} finally {
uploadAbortsRef.current.abort(task.finalName)
cancelledUploadingRef.current.delete(task.finalName)
cancelledNamesRef.current.delete(task.finalName)
signal?.removeEventListener(ABORT_EVENT, abortHandler)
const wasCancelled = cancelledUploadingRef.current.has(task.uuid) || signal?.aborted
if (!wasCancelled) {
uploadAbortsRef.current.abort(task.uuid)
cancelledUploadingRef.current.delete(task.uuid)
cancelledQueuedRef.current.delete(task.uuid)
}
}
},
[fm, currentDrive, currentStamp, trackUpload, refreshStamp],
[fm, files, currentStamp, trackUpload, refreshStamp, setShowError, setErrorMessage],
)
const trackDownload = useCallback(
@@ -369,7 +452,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
}
}
const driveName = currentDrive?.name
const driveName = props.driveName ?? currentDrive?.name
let startedAt: number | undefined
let etaState: ETAState = {
@@ -380,14 +463,15 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
setDownloadItems(prev => {
const row = createTransferItem(
props.uuid,
props.name,
props.size,
FileTransferType.Download,
driveName,
TransferStatus.Downloading,
)
row.startedAt = undefined // Downloads start timing when first progress is received
const idx = prev.findIndex(p => p.name === props.name)
row.startedAt = undefined
const idx = prev.findIndex(p => p.uuid === props.uuid)
if (idx === -1) return [...prev, row]
const out = [...prev]
@@ -417,10 +501,10 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
}
setDownloadItems(prev =>
updateTransferItems(prev, props.name, {
percent: Math.max(prev.find(it => it.name === props.name)?.percent || 0, percent),
updateTransferItems(prev, props.uuid, {
percent: Math.max(prev.find(it => it.uuid === props.uuid)?.percent || 0, percent),
etaSec,
startedAt: prev.find(it => it.name === props.name)?.startedAt ?? startedAt,
startedAt: prev.find(it => it.uuid === props.uuid)?.startedAt ?? startedAt,
}),
)
@@ -428,16 +512,16 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
const finishedAt = Date.now()
setDownloadItems(prev => {
const currentItem = prev.find(it => it.name === props.name)
const currentItem = prev.find(it => it.uuid === props.uuid)
const elapsedSec = currentItem?.startedAt ? Math.round((finishedAt - currentItem.startedAt) / 1000) : 0
if (dp.state === DownloadState.Cancelled || dp.state === DownloadState.Error) {
const wasCancelled =
dp.state === DownloadState.Cancelled || cancelledDownloadingRef.current.has(props.name)
dp.state === DownloadState.Cancelled || cancelledDownloadingRef.current.has(props.uuid)
cancelledDownloadingRef.current.delete(props.name)
cancelledDownloadingRef.current.delete(props.uuid)
return updateTransferItems(prev, props.name, {
return updateTransferItems(prev, props.uuid, {
status: wasCancelled ? TransferStatus.Cancelled : TransferStatus.Error,
etaSec: undefined,
elapsedSec: 0,
@@ -445,7 +529,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
})
}
return updateTransferItems(prev, props.name, {
return updateTransferItems(prev, props.uuid, {
percent: 100,
status: TransferStatus.Done,
etaSec: 0,
@@ -457,23 +541,48 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
return onProgress
},
[currentDrive?.name],
// currentDrive casues rerenders and flickering during the progress tracking
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
)
const uploadFiles = useCallback(
(picked: FileList | File[]): void => {
const filesArr = Array.from(picked)
if (filesArr.length === 0 || !fm || !currentDrive) return
if (filesArr.length === 0 || !fm || !currentDrive || !currentStamp) return
const currentlyQueued = queueRef.current.length
const newFilesCount = filesArr.length
const totalAfterAdd = currentlyQueued + newFilesCount
if (totalAfterAdd > MAX_UPLOAD_FILES) {
setErrorMessage?.(
`Youre trying to upload ${totalAfterAdd} files, but the limit is ${MAX_UPLOAD_FILES}. Please upload fewer files.`,
)
setShowError(true)
return
}
// TODO: move out this function from the cb and use as a util for better readaility
const preflight = async (): Promise<UploadTask[]> => {
const progressNames = new Set<string>(uploadItems.map(u => u.name))
const progressNames = new Set<string>(
uploadItems.filter(u => u.driveName === currentDrive.name).map(u => u.name),
)
const sameDrive = collectSameDrive(currentDrive.id.toString())
const onDiskNames = new Set<string>(sameDrive.map((fi: FileInfo) => fi.name))
const reserved = new Set<string>()
const tasks: UploadTask[] = []
let remainingBytes = calculateStampCapacityMetrics(currentStamp || null, currentDrive).remainingBytes
const allTaken = new Set<string>([
...Array.from(onDiskNames),
...Array.from(reserved),
...Array.from(progressNames),
])
// Track cumulative file sizes for capacity verification
let fileSizeSum = 0
let fileCount = 0
const processFile = async (file: File): Promise<UploadTask | null> => {
if (!currentStamp || !currentStamp.usable) {
@@ -483,28 +592,30 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
return null
}
const uuid = uuidV4()
const meta = buildUploadMeta([file])
const prettySize = formatBytes(meta.size)
const allTaken = new Set<string>([
...Array.from(onDiskNames),
...Array.from(reserved),
...Array.from(progressNames),
])
fileSizeSum += file.size
fileCount += 1
if (file.size > remainingBytes) {
// eslint-disable-next-line no-console
console.log(
'Skipping upload - insufficient space:',
file.name,
'size:',
file.size,
'remaining:',
remainingBytes,
)
setErrorMessage?.('There is not enough space to upload: ' + file.name)
setShowError(true)
const { ok } = verifyDriveSpace({
fm,
redundancyLevel: currentDrive.redundancyLevel,
stamp: currentStamp,
useInfoSize: true,
driveId: currentDrive.id.toString(),
adminRedundancy: adminDrive?.redundancyLevel,
fileSize: fileSizeSum,
fileCount,
cb: err => {
setErrorMessage?.(err + ' (' + truncateNameMiddle(file.name) + ')')
setShowError(true)
},
})
if (!ok) {
return null
}
@@ -533,9 +644,9 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
if (!retryInvalidCombo && !retryInvalidName) {
reserved.add(finalName)
remainingBytes -= file.size
ensureQueuedRow(
uuid,
finalName,
isReplace ? FileTransferType.Update : FileTransferType.Upload,
prettySize,
@@ -543,12 +654,15 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
)
return {
uuid,
file,
finalName,
prettySize,
isReplace: Boolean(isReplace),
replaceTopic,
replaceHistory,
driveId: currentDrive.id.toString(),
driveName: currentDrive.name,
}
}
}
@@ -561,7 +675,8 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
if (task) {
tasks.push(task)
} else if (file.size > remainingBytes) {
} else {
// Stop processing remaining files if capacity check failed
break
}
}
@@ -579,14 +694,14 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
if (!task) break
const isCancelled = cancelledNamesRef.current.has(task.finalName)
const isCancelled = cancelledQueuedRef.current.has(task.uuid)
if (isCancelled) {
safeSetState(
isMountedRef,
setUploadItems,
)(prev => updateTransferItems(prev, task.finalName, { status: TransferStatus.Cancelled }))
cancelledNamesRef.current.delete(task.finalName)
)(prev => updateTransferItems(prev, task.uuid, { status: TransferStatus.Cancelled }))
cancelledQueuedRef.current.delete(task.uuid)
queueRef.current.shift()
} else {
await processUploadTask(task)
@@ -595,10 +710,34 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
}
} finally {
runningRef.current = false
if (queueRef.current.length > 0) {
runQueue()
}
}
}
void (async () => {
if (!currentStamp || !currentStamp.usable) {
setErrorMessage?.('Stamp is not usable.')
setShowError(true)
return
}
if (beeApi) {
const stampValid = await validateStampStillExists(beeApi, currentStamp.batchID)
if (!stampValid) {
setErrorMessage?.(
'The selected stamp is no longer valid or has been deleted. Please select a different stamp.',
)
setShowError(true)
return
}
}
const tasks = await preflight()
queueRef.current = queueRef.current.concat(tasks)
runQueue()
@@ -613,69 +752,73 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
ensureQueuedRow,
processUploadTask,
uploadItems,
adminDrive,
setShowError,
setErrorMessage,
beeApi,
],
)
const cancelOrDismissUpload = useCallback(
(name: string) => {
(uuid: string) => {
safeSetState(
isMountedRef,
setUploadItems,
)(prev => {
const row = prev.find(r => r.name === name)
const row = prev.find(r => r.uuid === uuid)
if (!row) return prev
if (row.status === TransferStatus.Queued) {
cancelledNamesRef.current.add(name)
queueRef.current = queueRef.current.filter(t => t.finalName !== name)
cancelledQueuedRef.current.add(row.uuid)
queueRef.current = queueRef.current.filter(t => t.uuid !== row.uuid)
return prev.map(r => (r.name === name ? { ...r, status: TransferStatus.Cancelled } : r))
return prev.map(r => (r.uuid === row.uuid ? { ...r, status: TransferStatus.Cancelled } : r))
}
if (row.status === TransferStatus.Uploading) {
cancelledUploadingRef.current.add(name)
uploadAbortsRef.current.abort(name)
cancelledUploadingRef.current.add(row.uuid)
uploadAbortsRef.current.abort(row.uuid)
return prev.map(r => (r.name === name ? { ...r, status: TransferStatus.Cancelled } : r))
return prev.map(r => (r.uuid === uuid ? { ...r, status: TransferStatus.Cancelled } : r))
}
clearAllFlagsFor(name)
clearAllFlagsFor(row.uuid)
return prev.filter(r => r.name !== name)
return prev.filter(r => r.uuid !== uuid)
})
},
[clearAllFlagsFor],
)
const cancelOrDismissDownload = useCallback((name: string) => {
const cancelOrDismissDownload = useCallback((uuid: string) => {
safeSetState(
isMountedRef,
setDownloadItems,
)(prev => {
const row = prev.find(r => r.name === name)
const row = prev.find(r => r.uuid === uuid)
if (!row) return prev
if (row.status === TransferStatus.Downloading) {
cancelledDownloadingRef.current.add(name)
abortDownload(name)
cancelledDownloadingRef.current.add(uuid)
abortDownload(uuid)
return prev.map(r => (r.name === name ? { ...r, status: TransferStatus.Cancelled } : r))
return prev.map(r => (r.uuid === uuid ? { ...r, status: TransferStatus.Cancelled } : r))
}
cancelledDownloadingRef.current.delete(name)
cancelledDownloadingRef.current.delete(uuid)
return prev.filter(r => r.name !== name)
return prev.filter(r => r.uuid !== uuid)
})
}, [])
const dismissAllUploads = useCallback(() => {
setUploadItems([])
cancelledNamesRef.current.clear()
uploadAbortsRef.current.clear()
queueRef.current = []
cancelledQueuedRef.current.clear()
cancelledUploadingRef.current.clear()
setUploadItems([])
}, [])
const dismissAllDownloads = useCallback(() => {
@@ -684,7 +827,31 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
}, [])
useEffect(() => {
const handleFileUploaded = (e: Event) => {
const { fileInfo } = (e as CustomEvent).detail || {}
if (!fileInfo) return
setUploadItems(prev => {
const item = prev.find(it => it.name === fileInfo.name && it.status === TransferStatus.Uploading)
if (!item) return prev
const elapsedSec = item.startedAt ? Math.round((Date.now() - item.startedAt) / 1000) : 0
return updateTransferItems(prev, item.uuid, {
percent: 100,
status: TransferStatus.Done,
etaSec: 0,
elapsedSec,
})
})
}
window.addEventListener(FILE_MANAGER_EVENTS.FILE_UPLOADED, handleFileUploaded as EventListener)
return () => {
window.removeEventListener(FILE_MANAGER_EVENTS.FILE_UPLOADED, handleFileUploaded as EventListener)
isMountedRef.current = false
}
}, [])