refactor: preview image modal

This commit is contained in:
Tristan Yang
2025-08-10 18:11:55 +08:00
parent 880ef4f2cc
commit db525ea222
7 changed files with 1603 additions and 1903 deletions
+34 -32
View File
@@ -1,11 +1,11 @@
{ {
"name": "vocechat-web", "name": "vocechat-web",
"version": "0.9.11", "version": "0.9.15",
"homepage": "https://voce.chat", "homepage": "https://voce.chat",
"dependencies": { "dependencies": {
"@metamask/onboarding": "^1.0.1", "@metamask/onboarding": "^1.0.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.6.1",
"@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-popover": "^1.1.14",
"@react-oauth/google": "^0.12.2", "@react-oauth/google": "^0.12.2",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
@@ -24,50 +24,50 @@
"@udecode/plate-paragraph": "^33.0.0", "@udecode/plate-paragraph": "^33.0.0",
"@udecode/plate-trailing-block": "^33.0.0", "@udecode/plate-trailing-block": "^33.0.0",
"@uiball/loaders": "^1.3.1", "@uiball/loaders": "^1.3.1",
"agora-rtc-sdk-ng": "^4.23.3", "agora-rtc-sdk-ng": "^4.24.0",
"broadcast-channel": "^7.1.0", "broadcast-channel": "^7.1.0",
"browserslist": "^4.24.5", "browserslist": "^4.25.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"copy-image-clipboard": "^2.1.2", "copy-image-clipboard": "^2.1.2",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.2", "css-minimizer-webpack-plugin": "^7.0.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"firebase": "^11.7.3", "firebase": "^11.7.3",
"framer-motion": "12.11.3", "framer-motion": "12.23.12",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.1",
"heic2any": "^0.0.4", "heic2any": "^0.0.4",
"html-webpack-plugin": "^5.6.3", "html-webpack-plugin": "^5.6.3",
"i18next": "^24.2.2", "i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.1.0", "i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"linkify-plugin-mention": "^4.2.0", "linkify-plugin-mention": "^4.3.2",
"linkify-react": "^4.3.1", "linkify-react": "^4.3.2",
"linkify-string": "^4.3.1", "linkify-string": "^4.3.2",
"linkifyjs": "^4.3.1", "linkifyjs": "^4.3.2",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"localforage-setitems": "^1.4.0", "localforage-setitems": "^1.4.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"masonry-layout": "^4.2.2", "masonry-layout": "^4.2.2",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.3",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.1.0", "react": "^19.1.1",
"react-dev-utils": "^12.0.1", "react-dev-utils": "^12.0.1",
"react-dnd": "16.0.1", "react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1", "react-dnd-html5-backend": "16.0.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.1",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-i18next": "^15.5.1", "react-i18next": "^15.6.1",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-refresh": "0.17.0", "react-refresh": "0.17.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.8.0",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9", "react-textarea-autosize": "^8.5.9",
"react-use-wizard": "^2.3.0", "react-use-wizard": "^2.3.0",
"react-viewport-list": "^7.1.2", "react-viewport-list": "^7.1.2",
"react-virtuoso": "^4.12.7", "react-virtuoso": "^4.13.0",
"rooks": "^7.14.1", "rooks": "^8.4.0",
"slate": "^0.112.0", "slate": "^0.112.0",
"slate-history": "^0.110.3", "slate-history": "^0.110.3",
"slate-hyperscript": "^0.100.0", "slate-hyperscript": "^0.100.0",
@@ -77,8 +77,8 @@
"tailwind-merge": "2.5.4", "tailwind-merge": "2.5.4",
"terser-webpack-plugin": "^5.3.14", "terser-webpack-plugin": "^5.3.14",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"wavesurfer.js": "^7.9.5", "wavesurfer.js": "^7.10.1",
"webpack": "^5.99.8", "webpack": "^5.101.0",
"webpack-dev-server": "^4.15.1", "webpack-dev-server": "^4.15.1",
"webpack-manifest-plugin": "^5.0.1", "webpack-manifest-plugin": "^5.0.1",
"workbox-core": "^7.3.0", "workbox-core": "^7.3.0",
@@ -86,7 +86,8 @@
"workbox-precaching": "^7.3.0", "workbox-precaching": "^7.3.0",
"workbox-routing": "^7.3.0", "workbox-routing": "^7.3.0",
"workbox-strategies": "^7.3.0", "workbox-strategies": "^7.3.0",
"workbox-webpack-plugin": "^7.3.0" "workbox-webpack-plugin": "^7.3.0",
"yet-another-react-lightbox": "^3.25.0"
}, },
"scripts": { "scripts": {
"start": "REACT_APP_BUILD_TIME=$(date +%s) node scripts/start.js", "start": "REACT_APP_BUILD_TIME=$(date +%s) node scripts/start.js",
@@ -113,29 +114,30 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.27.1", "@babel/core": "^7.28.0",
"@eslint/js": "^9.26.0", "@eslint/js": "^9.32.0",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.20",
"@types/masonry-layout": "^4.2.8", "@types/masonry-layout": "^4.2.8",
"@types/node": "^22.15.18", "@types/node": "^22.15.18",
"@types/react": "18.3.18", "@types/react": "19.1.9",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^19.1.7",
"@types/react-helmet": "^6.1.11", "@types/react-helmet": "^6.1.11",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"babel-loader": "^9.2.1", "babel-loader": "^9.2.1",
"babel-plugin-named-asset-import": "^0.3.8", "babel-plugin-named-asset-import": "^0.3.8",
"babel-preset-react-app": "^10.1.0", "babel-preset-react-app": "^10.1.0",
"eslint": "^9.26.0", "eslint": "^9.32.0",
"gh-pages": "^6.3.0", "gh-pages": "^6.3.0",
"md5-file": "^5.0.0", "md5-file": "^5.0.0",
"postcss": "^8.5.3", "postcss": "^8.5.6",
"postcss-loader": "^8.1.1", "postcss-loader": "^8.1.1",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"typescript": "^5.8.3", "typescript": "^5.9.2",
"typescript-eslint": "^8.32.1", "typescript-eslint": "^8.39.0",
"webpack-bundle-analyzer": "^4.10.2" "webpack-bundle-analyzer": "^4.10.2"
}, },
"packageManager": "pnpm@10.14.0",
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"slate-react": "patches/slate-react.patch", "slate-react": "patches/slate-react.patch",
+1514 -1782
View File
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -187,13 +187,20 @@ function build(previousFileSizes) {
} }
function copyPublicFolder() { function copyPublicFolder() {
const ignoreFiles = ["widget_demo.html", "CNAME", "email.tpl.html", "robots.txt"];
fs.copySync(paths.appPublic, paths.appBuild, { fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true, dereference: true,
filter: (file) => { filter: (file) => file !== paths.appHtml,
const [fileName] = file.split("/").slice(-1);
const filters = file !== paths.appHtml && !ignoreFiles.includes(fileName);
return filters;
},
}); });
} }
// function copyPublicFolder() {
// const ignoreFiles = ["widget_demo.html", "CNAME", "email.tpl.html", "robots.txt"];
// fs.copySync(paths.appPublic, paths.appBuild, {
// dereference: true,
// filter: (file) => {
// const [fileName] = file.split("/").slice(-1);
// const filters = file !== paths.appHtml && !ignoreFiles.includes(fileName);
// return filters;
// },
// });
// }
+2 -2
View File
@@ -12,8 +12,8 @@ const prices: Price[] = [
}, },
]; ];
const official_dev = `https://dev.voce.chat`; const official_dev = `https://dev.voce.chat`;
const local_dev = `http://localhost:3000`; const local_dev = `http://localhost:3333`;
// const local_dev = `http://chat.jcdl369.top:3009`; // const local_dev = `https://dev.voce.chat`;
const dev_origin = process.env.REACT_APP_OFFICIAL_DEMO ? official_dev : local_dev; const dev_origin = process.env.REACT_APP_OFFICIAL_DEMO ? official_dev : local_dev;
// const local_dev = `https://im.ttt.td`; // const local_dev = `https://im.ttt.td`;
+31 -67
View File
@@ -1,10 +1,11 @@
import { FC, useEffect, useRef, useState } from "react"; import { FC } from "react";
import { useTranslation } from "react-i18next"; import { Lightbox } from "yet-another-react-lightbox";
import { Ring } from "@uiball/loaders"; import "yet-another-react-lightbox/styles.css";
import { useKey, useOutsideClick } from "rooks"; import Fullscreen from "yet-another-react-lightbox/plugins/fullscreen";
import Download from "yet-another-react-lightbox/plugins/download";
import Modal from "./Modal"; import Captions from "yet-another-react-lightbox/plugins/captions";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import "yet-another-react-lightbox/plugins/captions.css";
export interface PreviewImageData { export interface PreviewImageData {
originUrl: string; originUrl: string;
thumbnail?: string; thumbnail?: string;
@@ -20,69 +21,32 @@ interface Props {
} }
const ImagePreviewModal: FC<Props> = ({ download = true, data, closeModal }) => { const ImagePreviewModal: FC<Props> = ({ download = true, data, closeModal }) => {
const { t } = useTranslation();
const [url, setUrl] = useState(data?.thumbnail);
const [loading, setLoading] = useState(true);
const wrapperRef = useRef<HTMLDivElement>(null);
useOutsideClick(wrapperRef, closeModal);
useKey(
"Escape",
() => {
closeModal();
},
{ eventTypes: ["keyup"] }
);
useEffect(() => {
if (data) {
const { originUrl } = data;
if (!originUrl) setLoading(false);
const img = new Image();
img.src = originUrl;
img.onload = () => {
setUrl(originUrl);
setLoading(false);
};
img.onerror = () => {
setLoading(false);
};
}
}, [data]);
if (!data) return null; if (!data) return null;
const { originUrl, downloadLink, name, type } = data; const { originUrl, downloadLink, name, type } = data;
return ( return (
<Modal> <Lightbox
<div className="w-screen h-screen flex-center bg-black/90"> open
<div className={`relative flex flex-col justify-start gap-3`} ref={wrapperRef}> close={closeModal}
<div className="overflow-auto"> styles={{
<img navigationNext: {
className={`max-w-[70vw] max-h-[80vh] ${loading ? "blur-sm" : ""}`} display: "none",
src={url} },
alt="preview" navigationPrev: {
/> display: "none",
</div> },
{download && ( }}
<a slides={[
// onClick={handleDownload} {
className="font-semibold text-sm text-gray-600 hover:underline hover:text-white" src: originUrl,
download={name} alt: name,
type={type} title: name,
href={downloadLink || originUrl} description: name,
rel="noreferrer" download: downloadLink || originUrl,
> },
{t("action.download_origin")} ]}
</a> plugins={download ? [Captions, Fullscreen, Zoom, Download] : [Captions, Fullscreen, Zoom]}
)} captions={{ descriptionTextAlign: "center" }}
{loading && ( />
<div className="absolute top-[40%] left-1/2 -translate-x-1/2">
<Ring />
</div>
)}
</div>
</div>
</Modal>
); );
}; };
@@ -1,17 +1,14 @@
import { forwardRef } from "react";
import { PlateElement, PlateElementProps } from "@udecode/plate-common"; import { PlateElement, PlateElementProps } from "@udecode/plate-common";
import { cn } from "@/utils"; import { cn } from "@/utils";
const ParagraphElement = forwardRef<React.ElementRef<typeof PlateElement>, PlateElementProps>( const ParagraphElement = ({ className, children, ref, ...props }: PlateElementProps) => {
({ className, children, ...props }: PlateElementProps, ref) => { return (
return ( <PlateElement ref={ref} className={cn("m-0 px-0 py-1", className)} {...props}>
<PlateElement ref={ref} className={cn("m-0 px-0 py-1", className)} {...props}> {children}
{children} </PlateElement>
</PlateElement> );
); };
}
);
ParagraphElement.displayName = "ParagraphElement"; ParagraphElement.displayName = "ParagraphElement";
export { ParagraphElement }; export { ParagraphElement };
@@ -1,9 +1,7 @@
import { forwardRef } from "react";
// @ts-ignore // @ts-ignore
const CustomList = forwardRef(({ style, ...props }, ref) => { const CustomList = ({ style, ref, ...props }) => {
// @ts-ignore // @ts-ignore
return <div style={{ ...style, width: `calc(100% - 2rem)` }} {...props} ref={ref} />; return <div style={{ ...style, width: `calc(100% - 2rem)` }} {...props} ref={ref} />;
}); };
export default CustomList; export default CustomList;