refactor: expired file message

This commit is contained in:
Tristan Yang
2023-06-09 16:35:51 +08:00
parent 10ccfc588b
commit 5db3adfef8
10 changed files with 111 additions and 30 deletions
+29 -3
View File
@@ -1,7 +1,8 @@
import { FC, ReactElement } from "react"; import { FC, ReactElement, useEffect, useState } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import useExpiredResMap from "@/hooks/useExpiredResMap";
import { formatBytes, fromNowTime, getFileIcon } from "@/utils"; import { formatBytes, fromNowTime, getFileIcon } from "@/utils";
import IconDownload from "@/assets/icons/download.svg"; import IconDownload from "@/assets/icons/download.svg";
import { import {
@@ -79,11 +80,36 @@ const FileBox: FC<Props> = ({
from_uid, from_uid,
content content
}) => { }) => {
const [fetchError, setFetchError] = useState(false);
const { isExpired, setExpired } = useExpiredResMap();
const fromUser = useAppSelector((store) => store.users.byId[from_uid]); const fromUser = useAppSelector((store) => store.users.byId[from_uid]);
const icon = getFileIcon(file_type, name, "icon w-9 h-12"); const icon = getFileIcon(file_type, name, "icon w-9 h-12");
if (!content || !name) return null; const expired = isExpired(content);
useEffect(() => {
const tryFetch = async (url: string) => {
try {
let statusNum = (await fetch(url)).status;
if (statusNum >= 400) {
setFetchError(true);
}
} catch (e) {
setFetchError(true);
}
};
if (!expired && content) {
tryFetch(content);
}
}, [content, expired]);
useEffect(() => {
if (fetchError) {
setExpired(content);
}
}, [fetchError, content]);
if (!content || expired || fetchError) return null;
const previewContent = renderPreview({ file_type, content, name }); const previewContent = renderPreview({ file_type, content, name });
const withPreview = preview && previewContent; const withPreview = preview && previewContent && !fetchError && !expired;
return ( return (
<div <div
+1 -1
View File
@@ -23,7 +23,7 @@ const AudioMessage = ({ url, name, size, download }: Props) => {
setError(true); setError(true);
}; };
const _size = formatBytes(size); const _size = formatBytes(size);
if (error) return <ExpiredMessage type="audio" />; if (error) return <ExpiredMessage type="audio" url={url} />;
return ( return (
<div <div
className={clsx( className={clsx(
+11 -2
View File
@@ -1,6 +1,7 @@
import React from "react"; import React, { useEffect } from "react";
import clsx from "clsx"; import clsx from "clsx";
import useExpiredResMap from "@/hooks/useExpiredResMap";
import IconAudio from "@/assets/icons/file.audio.svg"; import IconAudio from "@/assets/icons/file.audio.svg";
import IconImage from "@/assets/icons/file.image.svg"; import IconImage from "@/assets/icons/file.image.svg";
import IconUnknown from "@/assets/icons/file.unknown.svg"; import IconUnknown from "@/assets/icons/file.unknown.svg";
@@ -9,6 +10,7 @@ import IconInfo from "@/assets/icons/info.svg";
type Props = { type Props = {
type?: "file" | "audio" | "image" | "video"; type?: "file" | "audio" | "image" | "video";
url?: string;
}; };
const InfoMap = { const InfoMap = {
file: { file: {
@@ -32,8 +34,15 @@ const InfoMap = {
icon: <IconVideo className="w-9 shrink-0 h-auto grayscale" /> icon: <IconVideo className="w-9 shrink-0 h-auto grayscale" />
} }
}; };
const ExpiredMessage = ({ type = "file" }: Props) => { const ExpiredMessage = ({ type = "file", url = "" }: Props) => {
const { setExpired } = useExpiredResMap();
const { title, desc, icon } = InfoMap[type]; const { title, desc, icon } = InfoMap[type];
useEffect(() => {
if (url) {
setExpired(url);
}
}, [url]);
return ( return (
<div <div
className={clsx( className={clsx(
+1 -1
View File
@@ -37,7 +37,7 @@ const ImageMessage: FC<Props> = ({
}; };
img.src = url; img.src = url;
}, [url]); }, [url]);
if (status == "error") return <ExpiredMessage type="image" />; if (status == "error") return <ExpiredMessage type="image" url={url} />;
return ( return (
<div <div
className={`relative overflow-hidden`} className={`relative overflow-hidden`}
@@ -48,7 +48,7 @@ const OtherFileMessage = ({
}); });
} }
}, [content]); }, [content]);
if (error) return <ExpiredMessage />; if (error) return <ExpiredMessage url={content} />;
return ( return (
<div <div
className={clsx( className={clsx(
+1 -1
View File
@@ -38,7 +38,7 @@ const VideoMessage = ({ url, name, size, download }: Props) => {
setError(true); setError(true);
}; };
const tipClass = "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"; const tipClass = "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2";
if (error) return <ExpiredMessage type="video" />; if (error) return <ExpiredMessage type="video" url={url} />;
return ( return (
<div <div
className={clsx( className={clsx(
+12 -4
View File
@@ -2,11 +2,13 @@ import { FC, useEffect, useState } from "react";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import { ChatContext } from "@/types/common"; import { ChatContext } from "@/types/common";
import useExpiredResMap from "@/hooks/useExpiredResMap";
import useRemoveLocalMessage from "@/hooks/useRemoveLocalMessage"; import useRemoveLocalMessage from "@/hooks/useRemoveLocalMessage";
import useSendMessage from "@/hooks/useSendMessage"; import useSendMessage from "@/hooks/useSendMessage";
import useUploadFile from "@/hooks/useUploadFile"; import useUploadFile from "@/hooks/useUploadFile";
import { getImageSize, isImage } from "@/utils"; import { getImageSize, isImage } from "@/utils";
import AudioMessage from "./AudioMessage"; import AudioMessage from "./AudioMessage";
import ExpiredMessage from "./ExpiredMessage";
import ImageMessage from "./ImageMessage"; import ImageMessage from "./ImageMessage";
import OtherFileMessage from "./OtherFileMessage"; import OtherFileMessage from "./OtherFileMessage";
import VideoMessage from "./VideoMessage"; import VideoMessage from "./VideoMessage";
@@ -41,6 +43,7 @@ const FileMessage: FC<Props> = ({
thumbnail = "", thumbnail = "",
properties = { local_id: 0, name: "", size: 0, content_type: "" } properties = { local_id: 0, name: "", size: 0, content_type: "" }
}) => { }) => {
const { isExpired } = useExpiredResMap();
const [imageSize, setImageSize] = useState(null); const [imageSize, setImageSize] = useState(null);
const [uploadingFile, setUploadingFile] = useState(false); const [uploadingFile, setUploadingFile] = useState(false);
const removeLocalMessage = useRemoveLocalMessage({ context, id: to }); const removeLocalMessage = useRemoveLocalMessage({ context, id: to });
@@ -136,7 +139,9 @@ const FileMessage: FC<Props> = ({
const sending = uploadingFile || isSending; const sending = uploadingFile || isSending;
// image // image
if (isImage(content_type, size)) if (isImage(content_type, size))
return ( return isExpired(thumbnail || content) ? (
<ExpiredMessage type="image" />
) : (
<ImageMessage <ImageMessage
key={properties?.local_id} key={properties?.local_id}
uploading={sending} uploading={sending}
@@ -147,12 +152,15 @@ const FileMessage: FC<Props> = ({
thumbnail={thumbnail} thumbnail={thumbnail}
/> />
); );
const isVideo = content_type.startsWith("video");
const isAudio = content_type.startsWith("audio");
if (isExpired(content) && !sending)
return <ExpiredMessage type={isAudio ? "audio" : isVideo ? "video" : "file"} />;
// video // video
if (content_type.startsWith("video") && !sending) if (isVideo && !sending)
return <VideoMessage size={size} url={content} name={name} download={download} />; return <VideoMessage size={size} url={content} name={name} download={download} />;
// audio // audio
if (content_type.startsWith("audio") && !sending) if (isAudio && !sending)
return <AudioMessage size={size} url={content} name={name} download={download} />; return <AudioMessage size={size} url={content} name={name} download={download} />;
return ( return (
<OtherFileMessage <OtherFileMessage
+30
View File
@@ -0,0 +1,30 @@
import { useLocalstorageState } from "rooks";
const LOCAL_KEY = `EXPIRED_FILES_MAP`;
const useExpiredResMap = () => {
const [map, setMap, remove] = useLocalstorageState(LOCAL_KEY, {} as Record<string, number>);
const setExpired = (url: string) => {
const key = new URL(url).searchParams.get("file_path");
if (!key || map[key]) return;
setMap((prev) => {
return { ...prev, [key]: Date.now() };
});
};
const isExpired = (url: string) => {
const key = new URL(url).searchParams.get("file_path");
if (!key || !map[key]) return false;
return true;
};
// const removeFromExpired = (key: string) => {
// setMap((prev) => {
// const { [key]: _, ...rest } = prev;
// return rest;
// });
// };
const clearExpired = () => {
remove();
};
return { setExpired, isExpired, clearExpired };
};
export default useExpiredResMap;
+3 -4
View File
@@ -40,10 +40,9 @@ const HomePage = lazy(() => import("./home"));
let toastId: string; let toastId: string;
const PageRoutes = () => { const PageRoutes = () => {
const { const {
ui: { online }, ui: { online }
fileMessages
} = useAppSelector((store) => { } = useAppSelector((store) => {
return { ui: store.ui, fileMessages: store.fileMessage }; return { ui: store.ui };
}, isEqual); }, isEqual);
// 提前获取device token // 提前获取device token
useDeviceToken(vapidKey); useDeviceToken(vapidKey);
@@ -283,7 +282,7 @@ const PageRoutes = () => {
path="files" path="files"
element={ element={
<LazyIt> <LazyIt>
<ResourceManagement fileMessages={fileMessages} /> <ResourceManagement />
</LazyIt> </LazyIt>
} }
></Route> ></Route>
+22 -13
View File
@@ -1,10 +1,11 @@
// @ts-nocheck // @ts-nocheck
import { memo, useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import clsx from "clsx"; import clsx from "clsx";
import Masonry from "masonry-layout"; import Masonry from "masonry-layout";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import FileBox from "@/components/FileBox"; import FileBox from "@/components/FileBox";
import useExpiredResMap from "@/hooks/useExpiredResMap";
import Filter from "./Filter"; import Filter from "./Filter";
import Search from "./Search"; import Search from "./Search";
import View from "./View"; import View from "./View";
@@ -37,15 +38,16 @@ const checkFilter = (data, filter, channelMessage) => {
}; };
let msnry: Masonry | null; let msnry: Masonry | null;
function ResourceManagement({ fileMessages }) { function ResourceManagement() {
const { isExpired } = useExpiredResMap();
const listContainerRef = useRef<HTMLDivElement>(); const listContainerRef = useRef<HTMLDivElement>();
const [filter, setFilter] = useState({}); const [filter, setFilter] = useState({});
const { message, view, channelMessage, userData } = useAppSelector((store) => { const { message, view, channelMessage, fileMsgs } = useAppSelector((store) => {
return { return {
fileMsgs: store.fileMessage,
message: store.message, message: store.message,
channelMessage: store.channelMessage, channelMessage: store.channelMessage,
view: store.ui.fileListView, view: store.ui.fileListView
userData: store.users.byId
}; };
}); });
@@ -83,6 +85,15 @@ function ResourceManagement({ fileMessages }) {
} }
} }
}, [view, filter]); }, [view, filter]);
// const fileMessages = fileMsgs.filter((id) => {
// const data = message[id];
// if (!data) return false;
// const { content, thumbnail } = data;
// let url = thumbnail || content;
// console.log("ff fitler", url, isExpired(url));
// return !isExpired(url);
// });
return ( return (
<div className="h-screen md:overflow-y-scroll flex flex-col items-start my-2 mr-6 rounded-2xl bg-white dark:bg-gray-700"> <div className="h-screen md:overflow-y-scroll flex flex-col items-start my-2 mr-6 rounded-2xl bg-white dark:bg-gray-700">
@@ -99,15 +110,17 @@ function ResourceManagement({ fileMessages }) {
)} )}
ref={listContainerRef} ref={listContainerRef}
> >
{fileMessages.map((id) => { {fileMsgs.map((id) => {
const data = message[id]; const data = message[id];
if (!data) return null; if (!data) return null;
const isSelected = checkFilter(data, filter, channelMessage); const isSelected = checkFilter(data, filter, channelMessage);
// 过滤掉不存在的用户 // 过滤掉不存在的用户
// const fromUser = userData[data.from_uid]; // const fromUser = userData[data.from_uid];
if (!isSelected) return null; if (!isSelected) return null;
const { mid, content, created_at, from_uid, properties } = data; const { mid, thumbnail, content, created_at, from_uid, properties } = data;
const { name, content_type, size } = properties ?? {}; const { name, content_type, size } = properties ?? {};
const url = thumbnail || content;
if (isExpired(url)) return null;
return ( return (
<div key={mid} className="grid-box mb-2"> <div key={mid} className="grid-box mb-2">
<FileBox <FileBox
@@ -115,7 +128,7 @@ function ResourceManagement({ fileMessages }) {
flex={view == "item"} flex={view == "item"}
key={mid} key={mid}
file_type={content_type} file_type={content_type}
content={content} content={thumbnail || content}
created_at={created_at} created_at={created_at}
from_uid={from_uid} from_uid={from_uid}
size={size} size={size}
@@ -129,8 +142,4 @@ function ResourceManagement({ fileMessages }) {
); );
} }
const equals = (a, b) => a.length === b.length && a.every((v, i) => v == b[i]); export default ResourceManagement;
export default memo(ResourceManagement, (prevs, nexts) => {
return equals(prevs.fileMessages, nexts.fileMessages);
});