214 lines
6.4 KiB
TypeScript
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;
|