192 lines
5.6 KiB
TypeScript
192 lines
5.6 KiB
TypeScript
import { FC, useEffect, useState } from "react";
|
|
import ImageMessage from "./ImageMessage";
|
|
import useRemoveLocalMessage from "../../hook/useRemoveLocalMessage";
|
|
import useUploadFile from "../../hook/useUploadFile";
|
|
import useSendMessage from "../../hook/useSendMessage";
|
|
import Progress from "./Progress";
|
|
import { getFileIcon, formatBytes, isImage, getImageSize, fromNowTime } from "../../utils";
|
|
import { useAppSelector } from "../../../app/store";
|
|
import IconDownload from "../../../assets/icons/download.svg";
|
|
import IconClose from "../../../assets/icons/close.circle.svg";
|
|
import VideoMessage from "./VideoMessage";
|
|
import AudioMessage from "./AudioMessage";
|
|
import clsx from "clsx";
|
|
|
|
const isLocalFile = (content: string) => {
|
|
return content.startsWith("blob:");
|
|
};
|
|
|
|
interface Props {
|
|
context: "user" | "channel";
|
|
to: number;
|
|
created_at: number;
|
|
from_uid: number;
|
|
content: string;
|
|
download: string;
|
|
thumbnail: string;
|
|
properties: {
|
|
local_id: number;
|
|
name: string;
|
|
size: number;
|
|
content_type: string;
|
|
};
|
|
}
|
|
|
|
const FileMessage: FC<Props> = ({
|
|
context,
|
|
to,
|
|
created_at,
|
|
from_uid,
|
|
content = "",
|
|
download = "",
|
|
thumbnail = "",
|
|
properties = { local_id: 0, name: "", size: 0, content_type: "" }
|
|
}) => {
|
|
const [imageSize, setImageSize] = useState(null);
|
|
const [uploadingFile, setUploadingFile] = useState(false);
|
|
const removeLocalMessage = useRemoveLocalMessage({ context, id: to });
|
|
const {
|
|
sendMessage,
|
|
isSuccess: sendMessageSuccess,
|
|
isSending
|
|
} = useSendMessage({
|
|
context,
|
|
from: from_uid,
|
|
to
|
|
});
|
|
const { stopUploading, data, uploadFile, progress, isSuccess: uploadSuccess } = useUploadFile();
|
|
const fromUser = useAppSelector((store) => store.users.byId[from_uid]);
|
|
const { size = 0, name, content_type } = properties ?? {};
|
|
useEffect(() => {
|
|
const handleUpSend = async ({
|
|
url,
|
|
name,
|
|
type
|
|
}: {
|
|
url: string;
|
|
name: string;
|
|
type: string;
|
|
}) => {
|
|
try {
|
|
setUploadingFile(true);
|
|
if (type.startsWith("image")) {
|
|
const size = await getImageSize(url);
|
|
setImageSize(size);
|
|
}
|
|
let file = await fetch(url)
|
|
.then((r) => r.blob())
|
|
.then((blobFile) => new File([blobFile], name, { type }));
|
|
|
|
await uploadFile(file);
|
|
setUploadingFile(false);
|
|
} catch (error) {
|
|
setUploadingFile(false);
|
|
console.error("fetch local file error", error);
|
|
}
|
|
};
|
|
// local file
|
|
if (isLocalFile(content)) {
|
|
handleUpSend({ url: content, name, type: content_type });
|
|
}
|
|
}, [content, name, content_type]);
|
|
useEffect(() => {
|
|
const props = properties ?? {};
|
|
const propsV2 = imageSize ? { ...props, ...imageSize } : props;
|
|
// 本地文件 并且上传成功
|
|
if (uploadSuccess && isLocalFile(content)) {
|
|
// 把已经上传的东西当做消息发出去
|
|
const { path } = data;
|
|
sendMessage({
|
|
ignoreLocal: true,
|
|
type: "file",
|
|
content: { path },
|
|
properties: propsV2
|
|
});
|
|
}
|
|
}, [uploadSuccess, data, properties, content]);
|
|
useEffect(() => {
|
|
if (sendMessageSuccess) {
|
|
// 回收本地资源
|
|
URL.revokeObjectURL(content);
|
|
}
|
|
}, [sendMessageSuccess, content]);
|
|
const handleCancel = () => {
|
|
stopUploading();
|
|
URL.revokeObjectURL(content);
|
|
removeLocalMessage(properties.local_id);
|
|
};
|
|
if (!properties) return null;
|
|
const icon = getFileIcon(content_type, name, "w-9 h-auto");
|
|
|
|
if (!content || !name) return null;
|
|
|
|
const sending = uploadingFile || isSending;
|
|
// image
|
|
if (isImage(content_type, size))
|
|
return (
|
|
<ImageMessage
|
|
key={properties?.local_id}
|
|
uploading={sending}
|
|
progress={progress}
|
|
properties={{ ...imageSize, ...properties }}
|
|
content={content}
|
|
download={download}
|
|
thumbnail={thumbnail}
|
|
/>
|
|
);
|
|
// video
|
|
if (content_type.startsWith("video") && !sending)
|
|
return (
|
|
<VideoMessage
|
|
size={size}
|
|
url={content}
|
|
name={name}
|
|
download={download}
|
|
/>
|
|
);
|
|
// audio
|
|
if (content_type.startsWith("audio") && !sending)
|
|
return (
|
|
<AudioMessage
|
|
size={size}
|
|
url={content}
|
|
name={name}
|
|
download={download}
|
|
/>
|
|
);
|
|
return (
|
|
<div className={clsx(`file_message bg-slate-50 dark:bg-slate-900 border border-solid border-gray-300 dark:border-gray-500 box-border w-[370px] rounded-md`, sending && "opacity-90")}>
|
|
<div className="px-2 py-3 flex items-center justify-between gap-2">
|
|
{icon}
|
|
<div className="flex flex-col gap-1 w-full overflow-hidden">
|
|
<span className="font-semibold text-sm text-gray-800 dark:text-gray-100 truncate">{name}</span>
|
|
<span className="whitespace-nowrap text-xs text-gray-500 dark:text-gray-300 flex gap-4">
|
|
{sending ? (
|
|
<Progress value={progress} width={"80%"} />
|
|
) : (
|
|
<>
|
|
<strong>{formatBytes(size)}</strong>
|
|
<strong>{fromNowTime(created_at)}</strong>
|
|
{fromUser && (
|
|
<strong>
|
|
by <strong className="font-bold">{fromUser.name}</strong>
|
|
</strong>
|
|
)}
|
|
</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
{sending ? (
|
|
<IconClose className="cursor-pointer" onClick={handleCancel} />
|
|
) : (
|
|
<a className="whitespace-nowrap" download={name} href={`${content}&download=true`}>
|
|
<IconDownload className="fill-gray-500 dark:fill-gray-400" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FileMessage;
|