diff --git a/web/src/components/MemoResourceListView.tsx b/web/src/components/MemoResourceListView.tsx index 538f0ac3..8ffb2772 100644 --- a/web/src/components/MemoResourceListView.tsx +++ b/web/src/components/MemoResourceListView.tsx @@ -1,4 +1,4 @@ -import classNames from "classnames"; +import { memo } from "react"; import { absolutifyLink } from "@/helpers/utils"; import { Resource } from "@/types/proto/api/v2/resource_service"; import { getResourceType, getResourceUrl } from "@/utils/resource"; @@ -6,106 +6,102 @@ import MemoResource from "./MemoResource"; import showPreviewImageDialog from "./PreviewImageDialog"; import SquareDiv from "./kit/SquareDiv"; -interface Props { - resourceList: Resource[]; - className?: string; -} +const MemoResourceListView = ({ resourceList = [] }: { resourceList: Resource[] }) => { + const mediaResources: Resource[] = []; + const otherResources: Resource[] = []; -const getDefaultProps = (): Props => { - return { - className: "", - resourceList: [], - }; -}; + resourceList.forEach((resource) => { + const type = getResourceType(resource); + if (type === "image/*" || type === "video/*") { + mediaResources.push(resource); + return; + } -const MemoResourceListView: React.FC<Props> = (props: Props) => { - const { className, resourceList } = { - ...getDefaultProps(), - ...props, - }; - const imageResourceList = resourceList.filter((resource) => getResourceType(resource).startsWith("image")); - const videoResourceList = resourceList.filter((resource) => resource.type.startsWith("video")); - const otherResourceList = resourceList.filter( - (resource) => !imageResourceList.includes(resource) && !videoResourceList.includes(resource) - ); - - const imgUrls = imageResourceList.map((resource) => { - return getResourceUrl(resource); + otherResources.push(resource); }); const handleImageClick = (imgUrl: string) => { + const imgUrls = mediaResources + .filter((resource) => getResourceType(resource) === "image/*") + .map((resource) => getResourceUrl(resource)); const index = imgUrls.findIndex((url) => url === imgUrl); showPreviewImageDialog(imgUrls, index); }; + const MediaCard = ({ resource, thumbnail }: { resource: Resource; thumbnail?: boolean }) => { + const type = getResourceType(resource); + const url = getResourceUrl(resource); + if (type === "image/*") { + return ( + <img + className="cursor-pointer min-h-full w-auto object-cover" + src={resource.externalLink ? url : `${url}${thumbnail ? "?thumbnail=1" : ""}`} + onClick={() => handleImageClick(url)} + decoding="async" + /> + ); + } + + if (type === "video/*") { + return ( + <video + className="cursor-pointer w-full h-full object-contain bg-zinc-100 dark:bg-zinc-800" + preload="metadata" + crossOrigin="anonymous" + src={absolutifyLink(url)} + controls + /> + ); + } + + return <></>; + }; + + const MediaList = ({ resources = [] }: { resources: Resource[] }) => { + if (resources.length === 0) return <></>; + + if (resources.length === 1) { + return ( + <div className="mt-2 max-w-full max-h-72 flex justify-center items-center border dark:border-zinc-800 rounded overflow-hidden hide-scrollbar hover:shadow-md"> + <MediaCard resource={mediaResources[0]} /> + </div> + ); + } + + const cards = resources.map((resource) => ( + <SquareDiv + key={resource.id} + className="flex justify-center items-center border dark:border-zinc-900 rounded overflow-hidden hide-scrollbar hover:shadow-md" + > + <MediaCard resource={resource} thumbnail /> + </SquareDiv> + )); + + if (resources.length === 2 || resources.length === 4) { + return <div className="w-full mt-2 grid gap-2 grid-cols-2">{cards}</div>; + } + + return <div className="w-full mt-2 grid gap-2 grid-cols-2 sm:grid-cols-3">{cards}</div>; + }; + + const OtherList = ({ resources = [] }: { resources: Resource[] }) => { + if (resources.length === 0) return <></>; + + return ( + <div className="w-full flex flex-row justify-start flex-wrap mt-2"> + {otherResources.map((resource) => ( + <MemoResource key={resource.id} className="my-1 mr-2" resource={resource} /> + ))} + </div> + ); + }; + return ( <> - {imageResourceList.length > 0 && - (imageResourceList.length === 1 ? ( - <div className="mt-2 max-w-full max-h-72 flex justify-center items-center border dark:border-zinc-800 rounded overflow-hidden hide-scrollbar hover:shadow-md"> - <img - className="cursor-pointer min-h-full w-auto object-cover" - src={getResourceUrl(imageResourceList[0])} - onClick={() => handleImageClick(getResourceUrl(imageResourceList[0]))} - decoding="async" - /> - </div> - ) : ( - <div - className={classNames( - "w-full mt-2 grid gap-2 grid-cols-2", - imageResourceList.length === 4 ? "sm:grid-cols-2" : "sm:grid-cols-3" - )} - > - {imageResourceList.map((resource) => { - const url = getResourceUrl(resource); - return ( - <SquareDiv - key={resource.id} - className="flex justify-center items-center border dark:border-zinc-900 rounded overflow-hidden hide-scrollbar hover:shadow-md" - > - <img - className="cursor-pointer min-h-full w-auto object-cover" - src={resource.externalLink ? url : url + "?thumbnail=1"} - onClick={() => handleImageClick(url)} - decoding="async" - /> - </SquareDiv> - ); - })} - </div> - ))} - - <div className={`w-full flex flex-col justify-start items-start ${className || ""}`}> - {videoResourceList.length > 0 && ( - <div className="w-full grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2"> - {videoResourceList.map((resource) => { - const url = getResourceUrl(resource); - return ( - <SquareDiv key={resource.id} className="shadow rounded overflow-hidden hide-scrollbar"> - <video - className="cursor-pointer w-full h-full object-contain bg-zinc-100 dark:bg-zinc-800" - preload="metadata" - crossOrigin="anonymous" - src={absolutifyLink(url)} - controls - ></video> - </SquareDiv> - ); - })} - </div> - )} - </div> - - {otherResourceList.length > 0 && ( - <div className="w-full flex flex-row justify-start flex-wrap mt-2"> - {otherResourceList.map((resource) => { - return <MemoResource key={resource.id} className="my-1 mr-2" resource={resource} />; - })} - </div> - )} + <MediaList resources={mediaResources} /> + <OtherList resources={otherResources} /> </> ); }; -export default MemoResourceListView; +export default memo(MemoResourceListView); diff --git a/web/src/components/ShareMemoDialog.tsx b/web/src/components/ShareMemoDialog.tsx index 6ab547a7..593b64d2 100644 --- a/web/src/components/ShareMemoDialog.tsx +++ b/web/src/components/ShareMemoDialog.tsx @@ -113,7 +113,7 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => { <span className="w-full px-6 pt-5 pb-2 text-sm text-gray-500">{memo.displayTsStr}</span> <div className="w-full px-6 text-base pb-4"> <MemoContent content={memo.content} /> - <MemoResourceListView className="!grid-cols-2" resourceList={memo.resourceList} /> + <MemoResourceListView resourceList={memo.resourceList} /> </div> <div className="flex flex-row justify-between items-center w-full bg-gray-100 dark:bg-zinc-700 py-4 px-6"> <div className="flex flex-row justify-start items-center">