feat: click to preview in widget

This commit is contained in:
Tristan Yang
2023-07-24 19:22:23 +08:00
parent 4328dc11fb
commit ccb5e93172
7 changed files with 155 additions and 29 deletions
+4 -4
View File
@@ -14,7 +14,7 @@
"@toast-ui/editor": "^3.2.2",
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
"@toast-ui/react-editor": "^3.2.3",
"@types/node": "^20.4.2",
"@types/node": "^20.4.4",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@udecode/plate": "16.8",
@@ -67,7 +67,7 @@
"react-textarea-autosize": "^8.5.2",
"react-use-wizard": "^2.2.1",
"react-viewport-list": "^7.1.1",
"react-virtuoso": "^4.4.1",
"react-virtuoso": "^4.4.2",
"resolve": "^1.22.3",
"rooks": "^7.14.1",
"slate": "^0.94.1",
@@ -79,7 +79,7 @@
"terser-webpack-plugin": "^5.3.9",
"tippy.js": "^6.3.7",
"typescript": "^5.1.6",
"wavesurfer.js": "^7.0.3",
"wavesurfer.js": "^7.0.6",
"webpack": "^5.88.2",
"webpack-dev-server": "^4.15.1",
"webpack-manifest-plugin": "^5.0.0",
@@ -147,7 +147,7 @@
"lint-staged": "^13.2.3",
"md5-file": "^5.0.0",
"patch-package": "^7.0.2",
"postcss": "^8.4.26",
"postcss": "^8.4.27",
"postcss-loader": "^7.3.3",
"postinstall-postinstall": "^2.1.0",
"prettier": "^3.0.0",
+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#667085" xmlns="http://www.w3.org/2000/svg">
<path
d="M5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21ZM5 21L16 10L21 15M10 8.5C10 9.32843 9.32843 10 8.5 10C7.67157 10 7 9.32843 7 8.5C7 7.67157 7.67157 7 8.5 7C9.32843 7 10 7.67157 10 8.5Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 472 B

+30
View File
@@ -0,0 +1,30 @@
import { useState } from "react";
import IconClose from "@/assets/icons/close.circle.svg";
type Props = {
thumbnail?: string;
content: string;
};
const Image = ({ thumbnail, content }: Props) => {
const [originalVisible, setOriginalVisible] = useState(false);
const toggleVisible = () => {
setOriginalVisible((prev) => !prev);
};
return (
<>
{originalVisible && (
<div className="fixed top-0 left-0 w-full h-full bg-black/50 flex items-center justify-center">
<div className="relative">
<img className="max-w-full" src={content || thumbnail} />
<button className="absolute -top-2 -right-2" onClick={toggleVisible}>
<IconClose className="w-5 h-5 dark:fill-gray-300" />
</button>
</div>
</div>
)}
<img onClick={toggleVisible} className="max-w-xs cursor-pointer" src={thumbnail || content} />
</>
);
};
export default Image;
+14 -3
View File
@@ -5,6 +5,8 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
import { useAppSelector } from "../../../app/store";
import Text from "./Text";
import Image from "./Image";
import { ContentType } from "@/types/message";
dayjs.extend(localizedFormat);
@@ -12,7 +14,8 @@ export interface IWidgetMessage {
mid: number;
uid: number;
host?: boolean;
type?: "text";
type?: ContentType;
thumbnail?: string;
content: string;
create_time: number;
sending: boolean;
@@ -30,12 +33,20 @@ const Time = ({ time }: { time: number }) => {
};
const Index = (props: IWidgetMessage) => {
const { logo } = useAppSelector((store) => store.server);
const { host = false, type = "text", content, uid, create_time, sending } = props;
const { host = false, type, content, thumbnail = "", uid, create_time, sending } = props;
let contentContainer = null;
console.log("render message", type, content);
switch (type) {
case "text":
case "text/plain":
contentContainer = <Text sending={sending} content={content} host={host} uid={uid} />;
break;
case "vocechat/file":
{
console.log("image file", content);
contentContainer = <Image thumbnail={thumbnail} content={content} />;
}
break;
default:
break;
+12 -4
View File
@@ -53,10 +53,17 @@ const MessageFeed = ({ hostId }: Props) => {
{mids.map((mid) => {
const currMsg = messageMap[mid];
if (!currMsg) return null;
const lastMsg = messageMap[mids[mids.length - 1]];
console.log("lll", lastMsg);
// const lastMsg = messageMap[mids[mids.length - 1]];
const { content, created_at = 0, from_uid = 0, sending = false } = currMsg;
const {
content_type,
thumbnail = "",
content,
created_at = 0,
from_uid = 0,
sending = false
} = currMsg;
console.log("lll", content, content_type);
return (
<Message
uid={from_uid}
@@ -64,8 +71,9 @@ const MessageFeed = ({ hostId }: Props) => {
sending={sending}
key={mid}
mid={mid}
type="text"
type={content_type}
content={content as string}
thumbnail={thumbnail}
create_time={created_at}
/>
);
+66 -8
View File
@@ -1,10 +1,14 @@
import { memo, useRef, useState } from "react";
import { ChangeEvent, memo, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import IconSend from "@/assets/icons/send.svg";
import IconImage from "@/assets/icons/image.svg";
import useSendMessage from "../../hooks/useSendMessage";
import { useWidget } from "../WidgetContext";
import useUploadFile from "@/hooks/useUploadFile";
import { Wobble } from "@uiball/loaders";
import { getImageSize } from "@/utils";
type Props = {
from: number;
@@ -12,6 +16,8 @@ type Props = {
};
let isComposing = false;
const MessageInput = (props: Props) => {
const inputRef = useRef<HTMLInputElement>(null);
const { uploadFile, isUploading, isSuccess: uploadSuccess } = useUploadFile();
const { t } = useTranslation("widget");
const { color } = useWidget();
const { from, to } = props;
@@ -36,6 +42,36 @@ const MessageInput = (props: Props) => {
});
setContent("");
};
const handleImageSelect = () => {
if (inputRef?.current) {
inputRef.current.click();
}
};
const handleFileChange = async (evt: ChangeEvent<HTMLInputElement>) => {
const files = evt.target.files;
if (!files || files?.length == 0) return;
const [file] = Array.from(files);
const result = await uploadFile(file);
if (result) {
// send message
const properties: any = await getImageSize(URL.createObjectURL(file));
console.log("uploaded", result, properties);
const { path } = result;
sendMessage({
ignoreLocal: true,
type: "file",
content: { path },
properties
});
}
};
// useEffect(() => {
// if(uploadSuccess){
// inputRef.current.value="";
// }
// }, [uploadSuccess])
return (
<div className="relative border-t border-gray-300 dark:border-gray-600 w-full">
<div className={"px-3 py-2 min-h-[48px] flex items-center gap-2"}>
@@ -69,13 +105,35 @@ const MessageInput = (props: Props) => {
}
}}
/>
<button
onClick={handleSend}
disabled={content.trim().length === 0}
className="px-2 py-1 disabled:opacity-60"
>
<IconSend className="dark:stroke-white w-4 h-4" />
</button>
<div className="flex items-center gap-2">
<button
onClick={handleImageSelect}
disabled={isUploading}
className="p-1 disabled:opacity-60"
>
{isUploading ? (
<Wobble size={16} />
) : (
<IconImage className="dark:stroke-gray-100 w-4 h-4" />
)}
<input
onChange={handleFileChange}
accept="image/*"
ref={inputRef}
type="file"
name="image"
id="image"
hidden
/>
</button>
<button
onClick={handleSend}
disabled={content.trim().length === 0}
className="p-1 disabled:opacity-60"
>
<IconSend className="dark:fill-gray-100 w-4 h-4" />
</button>
</div>
</div>
</div>
);
+24 -10
View File
@@ -2880,11 +2880,16 @@
resolved "https://mirrors.cloud.tencent.com/npm/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^20.4.2":
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
version "20.4.2"
resolved "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9"
integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==
"@types/node@^20.4.4":
version "20.4.4"
resolved "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-20.4.4.tgz#c79c7cc22c9d0e97a7944954c9e663bcbd92b0cb"
integrity sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://mirrors.cloud.tencent.com/npm/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -10150,7 +10155,7 @@ postcss@^7.0.35:
picocolors "^0.2.1"
source-map "^0.6.1"
postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.4:
postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.4:
version "8.4.26"
resolved "https://mirrors.cloud.tencent.com/npm/postcss/-/postcss-8.4.26.tgz#1bc62ab19f8e1e5463d98cf74af39702a00a9e94"
integrity sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==
@@ -10159,6 +10164,15 @@ postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.27:
version "8.4.27"
resolved "https://mirrors.cloud.tencent.com/npm/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postinstall-postinstall@^2.1.0:
version "2.1.0"
resolved "https://mirrors.cloud.tencent.com/npm/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3"
@@ -10739,10 +10753,10 @@ react-viewport-list@^7.1.1:
resolved "https://mirrors.cloud.tencent.com/npm/react-viewport-list/-/react-viewport-list-7.1.1.tgz#2f36ad1db8124e8a960bc006b95228b696cc81d6"
integrity sha512-O3gxykg3DgpcyYH+/X2kFwWFaZjckJ0FK3UBb4vFhZe+CsGLcX8OVJb0VSYI+IupEuQ9pl8dvJak8JRkIuvNjw==
react-virtuoso@^4.4.1:
version "4.4.1"
resolved "https://mirrors.cloud.tencent.com/npm/react-virtuoso/-/react-virtuoso-4.4.1.tgz#43d7ac35346c4eba947b40858b375d5844b5ae9f"
integrity sha512-QrZ0JLnZFH8ltMw6q+S7U1+V2vUcSHzoIfLRzQKSv4nMJhEdjiZ+e9PqWCI7xJiy2AmSCAgo7g1V5osuurJo2Q==
react-virtuoso@^4.4.2:
version "4.4.2"
resolved "https://mirrors.cloud.tencent.com/npm/react-virtuoso/-/react-virtuoso-4.4.2.tgz#752438e5031a9f72722371a52ae5ac7265f3e1c0"
integrity sha512-9uChSRSK5bTbKTxzuta4RqhfrlZSVHI/v8fHwmv3jmImS8FjdZ4Mzgqz9JKUp/yQo3dIHkR2QwidQeqxgYzPhQ==
react@^18.2.0:
version "18.2.0"
@@ -12500,10 +12514,10 @@ watchpack@^2.4.0:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
wavesurfer.js@^7.0.3:
version "7.0.3"
resolved "https://mirrors.cloud.tencent.com/npm/wavesurfer.js/-/wavesurfer.js-7.0.3.tgz#d3e0b07056c08d43db19d4950d2de68681f48606"
integrity sha512-gJ3P+Bd3Q4E8qETjjg0pneaVqm2J7jegG2Cc6vqEF5YDDKQ3m8sKsvVfgVhJkacKkO9jFAGDu58Hw4zLr7xD0A==
wavesurfer.js@^7.0.6:
version "7.0.6"
resolved "https://mirrors.cloud.tencent.com/npm/wavesurfer.js/-/wavesurfer.js-7.0.6.tgz#e0a214df7497f0503e18fb67959e4bca356c388a"
integrity sha512-Pkg0iBnIsgl9k4F938S9/RBEqklt9jPeeo59mmKimbUoxcKr11l7rsu9qHkGJvkl1hhx33lcSz0cA9F/qwFngQ==
wbuf@^1.1.0, wbuf@^1.7.3:
version "1.7.3"