Files
ColdBreeze-chat-web/src/hooks/useUploadFile.ts
T
2023-08-29 11:33:23 +08:00

214 lines
6.4 KiB
TypeScript

import { useRef, useState } from "react";
import toast from "react-hot-toast";
import heic2any from "heic2any";
import BASE_URL, { FILE_SLICE_SIZE } from "@/app/config";
import { usePrepareUploadFileMutation, useUploadFileMutation } from "@/app/services/message";
import { updateUploadFiles } from "@/app/slices/ui";
import { useAppDispatch, useAppSelector } from "@/app/store";
import { Message } from "@/types/channel";
import { ChatContext } from "@/types/common";
import { UploadFileResponse } from "@/types/message";
import { shallowEqual } from "react-redux";
export type UploadFileData = {
name: string;
type: string;
size: number;
url: string;
converting?: boolean;
};
interface IProps {
context: ChatContext;
id: number;
}
const convertHeic2Jpg = async (file: { name: string; type: string; size: number; url: string }) => {
const res = await fetch(file.url);
const blob = await res.blob();
const jpgBlob = (await heic2any({
blob,
toType: "image/jpeg",
quality: 0.8
})) as Blob;
const newName = file.name.replace(/\.hei\w$/i, ".jpg");
return { ...file, name: newName, converting: false, url: URL.createObjectURL(jpgBlob) };
};
const useUploadFile = (props?: IProps) => {
const { context, id } = props ? props : { context: "channel", id: 0 };
const dispatch = useAppDispatch();
const stageFiles = useAppSelector(
(store) => store.ui.uploadFiles[`${context}_${id}`] || [],
shallowEqual
);
const replying = useAppSelector(
(store) => store.message.replying[`${context}_${id}`],
shallowEqual
);
const [data, setData] = useState<Message | null>(null);
const canceledRef = useRef(false);
const sliceUploadedCountRef = useRef(0);
const totalSliceCountRef = useRef(1);
const [prepareUploadFile, { isLoading: isPreparing, isError: prepareFileError }] =
usePrepareUploadFileMutation();
const [uploadFileFn, { isLoading: isUploading, isError: uploadFileError }] =
useUploadFileMutation();
const uploadChunk = (data: { file_id: string; chunk: Blob; is_last: boolean }) => {
const { file_id, chunk, is_last } = data;
const formData = new FormData();
formData.append("file_id", file_id);
formData.append("chunk_data", chunk);
formData.append("chunk_is_last", `${is_last}`);
return uploadFileFn(formData);
};
const uploadFile = async (file?: File) => {
if (!file) return;
console.log("up file", file);
setData(null);
const {
name = `-${+new Date()}.${file.type.split("/")[1]}`,
type: file_type,
size: file_size
} = file;
console.log("file type", file_type);
// 生成 file id
const resp = await prepareUploadFile({
content_type: file_type,
filename: name
});
console.log("prepareUploadFile", resp);
if ("error" in resp) {
// toast.error("Prepare Upload File Error");
return;
}
const file_id = resp.data;
let uploadResult = null;
canceledRef.current = false;
totalSliceCountRef.current = 1;
sliceUploadedCountRef.current = 0;
// setUploadingFile(true);
// 2MB
if (file_size <= FILE_SLICE_SIZE) {
// 一次性上传文件
uploadResult = await uploadChunk({ file_id, chunk: file, is_last: true });
sliceUploadedCountRef.current = 1;
} else {
// 分片上传文件
totalSliceCountRef.current = Math.ceil(file_size / FILE_SLICE_SIZE);
const totalSliceCount = totalSliceCountRef.current;
const _arr = new Array(totalSliceCount);
// const chunk=file.slice(block_size * index, block_size * (index + 1));
for await (const [idx] of _arr.entries()) {
// 退出循环
if (canceledRef.current) break;
try {
const chunk = file.slice(FILE_SLICE_SIZE * idx, FILE_SLICE_SIZE * (idx + 1), file_type);
uploadResult = await uploadChunk({
file_id,
chunk,
// 如果是最后一个chunk,标记下
is_last: idx == _arr.length - 1
});
sliceUploadedCountRef.current++;
} catch (error) {
console.error("upload file error", error);
canceledRef.current = true;
return;
}
}
}
// 出错 则返回
if (!uploadResult || "error" in uploadResult || !uploadResult.data) {
console.error("upload file error uploadResult:", uploadResult);
return;
}
const { path, size, hash } = uploadResult.data as UploadFileResponse;
const encodedPath = encodeURIComponent(path);
const res = {
name,
file_type,
path,
size,
hash,
url: `${BASE_URL}/resource/file?file_path=${encodedPath}`,
thumbnail: file_type.startsWith("image")
? `${BASE_URL}/resource/file?file_path=${encodedPath}&thumbnail=true`
: "",
download: `${BASE_URL}/resource/file?file_path=${encodedPath}&download=true`
};
setData(res);
return res;
};
const stopUploading = () => {
canceledRef.current = true;
};
const removeStageFile = (idx: number) => {
dispatch(updateUploadFiles({ context, id, operation: "remove", index: idx }));
};
const addStageFile = (filesData: UploadFileData[]) => {
if (replying) {
toast.error("Only text is supported when replying a message");
return;
}
const heifs: number[] = [];
filesData.forEach((f, idx) => {
if (f.type.startsWith("image/hei")) {
f.converting = true;
heifs.push(idx);
}
});
dispatch(updateUploadFiles({ context, id, data: filesData }));
if (heifs.length) {
heifs.forEach((idx) => {
convertHeic2Jpg(filesData[idx]).then((res) => {
console.log("heif2jpg", res);
dispatch(updateUploadFiles({ context, id, data: res, operation: "replace", idx }));
});
});
}
};
const resetStageFiles = () => {
dispatch(updateUploadFiles({ context, id, operation: "reset" }));
};
const updateStageFile = (idx: number, data = {}) => {
dispatch(
updateUploadFiles({
context,
id,
operation: "update",
index: idx,
...data
})
);
};
return {
stopUploading,
data,
isUploading: isPreparing || isUploading,
progress: +Number((sliceUploadedCountRef.current / totalSliceCountRef.current) * 100).toFixed(
2
),
uploadFile,
isError: uploadFileError || prepareFileError,
isSuccess: !!data,
stageFiles,
addStageFile,
resetStageFiles,
removeStageFile,
updateStageFile
};
};
export default useUploadFile;