feat: click to preview in widget
This commit is contained in:
@@ -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 |
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user