feat: add media storage

This commit is contained in:
2026-09-17 05:23:28 +08:00
parent e3d55e4b7e
commit de0666beb1
8 changed files with 282 additions and 59 deletions
+140
View File
@@ -0,0 +1,140 @@
import { useWAClient } from "@/app/context";
import { storage } from "@/lib/storage";
import type { MessageDTO } from "@shared/dto";
import { DownloadIcon, Loader2Icon, PaperclipIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
type Props = {
data: Omit<MessageDTO, "id" | "from"> & { id: string };
};
export default function MessageMedia({ data }: Props) {
const { type } = data;
const { client } = useWAClient();
const loadingRef = useRef(false);
const [isLoading, setLoading] = useState(false);
const [isDownloaded, setDownloaded] = useState(false);
const [body, setBody] = useState(
type === "image" ? "data:image/webp;base64," + data.body : data.body,
);
useEffect(() => {
if (loadingRef.current) return;
(async function () {
try {
loadingRef.current = true;
if (type === "image" || type === "document") {
const res = await storage.get<string>(data.id);
if (res) {
setBody(res);
setDownloaded(true);
}
}
if (type === "sticker" && !body) {
const res = await storage.get<string>(data.id);
if (res) {
setBody(res);
setDownloaded(true);
} else {
await fetchMedia();
}
}
} catch (err) {
console.error(err);
} finally {
loadingRef.current = false;
}
})();
}, [data.id]);
async function fetchMedia() {
try {
setLoading(true);
const res = await client.getMedia(data.id);
setBody(res);
setDownloaded(true);
storage.set(data.id, res);
return res;
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
}
async function openMedia() {
if (isLoading) return;
if (isDownloaded) {
// download image
const blob = await fetch(body!).then((res) => res.blob());
const url = URL.createObjectURL(blob);
window.open(url);
return;
} else {
fetchMedia();
}
}
if (type === "image") {
return (
<a
href="#"
onClick={() => openMedia()}
className="w-full max-w-40 block relative overflow-hidden"
>
<img src={body || ""} alt="" className="w-full" />
{isLoading && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<Loader2Icon className="animate-spin size-8" />
</div>
)}
</a>
);
}
if (type === "sticker" && body) {
return (
<img
src={body}
alt=""
className="w-full max-w-40 block relative overflow-hidden"
/>
);
}
if (type === "document") {
return (
<button
onClick={() => openMedia()}
className="flex gap-2 items-center rounded-md bg-accent px-3 py-2 mb-2 truncate w-full cursor-pointer"
disabled={isLoading}
>
{isLoading ? (
<Loader2Icon className="animate-spin size-4" />
) : isDownloaded ? (
<PaperclipIcon className="size-4" />
) : (
<DownloadIcon className="size-4" />
)}
<span className="text-xs truncate">{data.filename}</span>
<span className="text-xs">{readableBytes(data.size || 0)}</span>
</button>
);
}
return null;
}
function readableBytes(bytes: number) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
if (bytes < 1024 * 1024 * 1024)
return (bytes / 1024 / 1024).toFixed(1) + " MB";
return (bytes / 1024 / 1024 / 1024).toFixed(1) + " GB";
}