import { useRef, useState, useReducer, Reducer, useEffect, useCallback, } from "react"; import { attributes } from "../utils/attributes"; import { PrettyBytes } from "../utils/bytes"; import { Toast } from "../Toast/Toast"; import { UploadStatus } from "./types"; import { CircleCheck, TriangleAlert, CircleX, CircleStop } from "lucide-react"; import { Spinner } from "../Spinner/Spinner"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { CodexData } from "@codex-storage/sdk-js"; import { WebFileIcon } from "../WebFileIcon/WebFileIcon"; import { ButtonIcon } from "../ButtonIcon/ButtonIcon"; import { SimpleText } from "../SimpleText/SimpleText"; type UploadFileProps = { file: File; onClose: (id: string) => void; id: string; onSuccess: ((cid: string, file: File) => void) | undefined; codexData: CodexData; // useWorker: boolean; }; type State = { progress: { loaded: number; total: number }; cid: string; preview: string; status: UploadStatus; error: string; }; type Action = | { type: "reset"; } | { type: "progress"; loaded: number; total: number; } | { type: "preview"; preview: string; } | { type: "completed"; cid: string; } | { type: "cancel"; } | { type: "error"; error: string; }; function reducer(state: State, action: Action) { switch (action.type) { case "progress": { const { loaded, total } = action; return { ...state, progress: { loaded, total }, status: loaded === total ? "done" : state.status, }; } case "preview": { return { ...state, preview: action.preview, }; } case "completed": { return { ...state, status: "done" as UploadStatus, cid: action.cid, }; } case "cancel": { if (state.status === "progress") { return { ...state, status: "error" as UploadStatus, error: "The upload has been cancelled.", }; } return { progress: { loaded: 0, total: 0 }, cid: "", preview: "", status: "progress" as UploadStatus, error: "", }; } case "error": { return { ...state, error: action.error, status: "error" as UploadStatus }; } default: { return state; } } } const isImage = (type: string) => type.startsWith("image"); export function UploadFile({ file, onClose, id, onSuccess, codexData, // useWorker, }: UploadFileProps) { const abort = useRef<(() => void) | null>(null); const queryClient = useQueryClient(); const worker = useRef(null); const [toast, setToast] = useState({ time: 0, message: "" }); const [state, dispatch] = useReducer>(reducer, { progress: { loaded: 0, total: 0 }, cid: "", preview: "", status: "progress" as UploadStatus, error: "", }); const { mutateAsync } = useMutation({ mutationKey: ["upload"], mutationFn: (file: File) => { const res = codexData.upload(file, onProgress); abort.current = res.abort; return res.result.then((safe) => safe.error ? Promise.reject(safe.data.message) : Promise.resolve(safe.data) ); }, onError: (error) => { // worker.current?.terminate(); dispatch({ type: "error", error: error.message }); }, onSuccess: (cid: string) => { onInternalSuccess(cid); }, }); const init = useRef(false); const onInternalSuccess = useCallback( (cid: string) => { worker.current?.terminate(); queryClient.invalidateQueries({ queryKey: ["cids"], }); if (onSuccess) { onSuccess(cid, file); dispatch({ type: "reset" }); } else { dispatch({ type: "completed", cid }); } }, [onSuccess, dispatch, queryClient, file] ); const onProgress = (loaded: number, total: number) => { dispatch({ type: "progress", loaded, total, }); }; useEffect(() => { if (init.current) { return; } init.current = true; if (isImage(file.type)) { const reader = new FileReader(); reader.onload = () => { const preview = reader.result?.toString(); if (preview) { dispatch({ type: "preview", preview }); } }; reader.readAsDataURL(file); } mutateAsync(file); // if (useWorker) { // worker.current = new Worker(new URL("./worker", import.meta.url), { // type: "module", // }); // provider().then(() => { // worker.current?.postMessage({ type: "init", upload: "" }); // }); // worker.current.onmessage = function (e) { // const data = e.data; // if (e.data.type === "progress") { // onProgress(data.loaded, data.total); // } else if (e.data.type === "completed") { // onInternalSuccess(e.data.value.data); // } else if (e.data.error) { // // TODO report with sentry // dispatch({ type: "error", error: e.data.error }); // } // }; // worker.current.onerror = function (e) { // // TODO report to sentry // console.error("Error in worker:", e); // dispatch({ type: "error", error: e.message }); // worker.current?.terminate(); // }; // worker.current.postMessage({ type: "file", file }); // } else { // mutateAsync(file); // } }, [file, mutateAsync, onInternalSuccess, codexData]); const onCancel = () => { if (worker.current) { worker.current.postMessage({ type: "abort" }); } else { abort.current?.(); } dispatch({ type: "cancel" }); }; const onInternalClose = () => { if (worker.current) { worker.current.postMessage({ type: "abort" }); } else { abort.current?.(); } onClose(id); }; const onCopy = () => { if (cid) { navigator.clipboard.writeText(cid); setToast({ time: Date.now(), message: "The CID has been copied to your clipboard.", }); } }; const parts = file.name.split("."); const extension = parts.pop(); const filename = parts.join("."); const { cid, error, preview, progress, status } = state; const onAction = state.status === "progress" ? onCancel : onInternalClose; const percent = progress.total > 0 ? (progress.loaded / progress.total) * 100 : 0; const ActionIcon = () => ; return (
{preview ? ( Preview ) : ( )}
{filename} .{extension}
{PrettyBytes(file.size)}
{percent.toFixed(2)} %
{!!cid && ( <>
Success ! Click on the CID to copy it to your clipboard.
{cid} )} {error && {error}}
); } type UploadStatusIconProps = { status: UploadStatus; }; export function UploadStatusIcon({ status }: UploadStatusIconProps) { switch (status) { case "done": return ( ); case "error": return ( ); case "progress": return ; } } function UploadActionIcon({ status }: UploadStatusIconProps) { switch (status) { case "error": case "done": return ; case "progress": return ; } }