diff --git a/packages/react-chat/src/components/Chat/ChatMessageContent.tsx b/packages/react-chat/src/components/Chat/ChatMessageContent.tsx index 4f4e8441..fc1815fe 100644 --- a/packages/react-chat/src/components/Chat/ChatMessageContent.tsx +++ b/packages/react-chat/src/components/Chat/ChatMessageContent.tsx @@ -4,6 +4,7 @@ import styled from "styled-components"; import { ChatMessage } from "../../models/ChatMessage"; import { Metadata } from "../../models/Metadata"; +import { ImageMenu } from "../Form/ImageMenu"; /* eslint-disable no-useless-escape */ const regEx = @@ -13,14 +14,12 @@ const regEx = type ChatMessageContentProps = { message: ChatMessage; fetchMetadata?: (url: string) => Promise; - setImage: (image: string) => void; }; export function ChatMessageContent({ message, fetchMetadata, - setImage, }: ChatMessageContentProps) { const { content, image } = useMemo(() => message, [message]); @@ -72,16 +71,20 @@ export function ChatMessageContent({ }; updatePreview(); }, [link]); + return (
{elements.map((el) => el)}
{image && ( - { - setImage(image); - }} - > - + + { + setImage(image); + }} + /> + )} {openGraph && ( @@ -103,6 +106,7 @@ const MessageImageWrapper = styled.div` width: 147px; height: 196px; margin-top: 8px; + position: relative; `; const MessageImage = styled.img` @@ -110,6 +114,7 @@ const MessageImage = styled.img` height: 100%; object-fit: cover; border-radius: 16px; + cursor; pointer; `; const PreviewSiteNameWrapper = styled.div` diff --git a/packages/react-chat/src/components/Form/ImageMenu.tsx b/packages/react-chat/src/components/Form/ImageMenu.tsx new file mode 100644 index 00000000..38028766 --- /dev/null +++ b/packages/react-chat/src/components/Form/ImageMenu.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import styled from "styled-components"; + +import { useContextMenu } from "../../hooks/useContextMenu"; +import { copyImg } from "../../utils/copyImg"; +import { downloadImg } from "../../utils/downloadImg"; +import { CopyIcon } from "../Icons/CopyIcon"; +import { DownloadIcon } from "../Icons/DownloadIcon"; +import { textSmallStyles } from "../Text"; + +interface ImageMenuProps { + imageId: string; +} + +export const ImageMenu = ({ imageId }: ImageMenuProps) => { + const { showMenu } = useContextMenu(imageId); + + return showMenu ? ( + + + copyImg(imageId)}> + Copy imageId + + downloadImg(imageId)}> + + Download imageId + + + + ) : ( + <> + ); +}; + +const MenuBlock = styled.div` + width: 176px; + height: 84px; + background: ${({ theme }) => theme.bodyBackgroundColor}; + box-shadow: 0px 2px 4px rgba(0, 34, 51, 0.16), + 0px 4px 12px rgba(0, 34, 51, 0.08); + border-radius: 8px; + padding: 8px 0; + position: absolute; + left: 120px; + top: 46px; + z-index: 2; +`; + +const MenuList = styled.ul` + list-style: none; +`; + +const MenuItem = styled.li` + width: 100%; + display: flex; + align-items: center; + padding: 8px 14px; + cursor: pointer; +`; + +const MenuText = styled.span` + margin-left: 6px; + color: ${({ theme }) => theme.primary}; + + ${textSmallStyles} +`; diff --git a/packages/react-chat/src/components/Icons/CopyIcon.tsx b/packages/react-chat/src/components/Icons/CopyIcon.tsx new file mode 100644 index 00000000..3455faa4 --- /dev/null +++ b/packages/react-chat/src/components/Icons/CopyIcon.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import styled from "styled-components"; + +export const CopyIcon = () => { + return ( + + + + + ); +}; + +const Icon = styled.svg` + & > path { + fill: ${({ theme }) => theme.tertiary}; + } +`; diff --git a/packages/react-chat/src/components/Icons/DownloadIcon.tsx b/packages/react-chat/src/components/Icons/DownloadIcon.tsx new file mode 100644 index 00000000..c1afdaa8 --- /dev/null +++ b/packages/react-chat/src/components/Icons/DownloadIcon.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import styled from "styled-components"; + +export const DownloadIcon = () => { + return ( + + + + + ); +}; + +const Icon = styled.svg` + & > path { + fill: ${({ theme }) => theme.tertiary}; + } +`; diff --git a/packages/react-chat/src/hooks/useContextMenu.ts b/packages/react-chat/src/hooks/useContextMenu.ts new file mode 100644 index 00000000..45b47b44 --- /dev/null +++ b/packages/react-chat/src/hooks/useContextMenu.ts @@ -0,0 +1,32 @@ +import { useCallback, useEffect, useState } from "react"; + +export const useContextMenu = (elementId: string) => { + const [showMenu, setShowMenu] = useState(false); + const element = document.getElementById(elementId) || document; + + const handleContextMenu = useCallback( + (event) => { + event.preventDefault(); + setShowMenu(true); + }, + [setShowMenu] + ); + + const handleClick = useCallback( + () => (showMenu ? setShowMenu(false) : null), + [showMenu] + ); + + useEffect(() => { + element.addEventListener("click", handleClick); + element.addEventListener("contextmenu", handleContextMenu); + document.addEventListener("click", () => setShowMenu(false)); + return () => { + element.removeEventListener("click", handleClick); + element.removeEventListener("contextmenu", handleContextMenu); + document.removeEventListener("click", () => setShowMenu(false)); + }; + }); + + return { showMenu }; +}; diff --git a/packages/react-chat/src/utils/copyImg.ts b/packages/react-chat/src/utils/copyImg.ts new file mode 100644 index 00000000..c8827cd3 --- /dev/null +++ b/packages/react-chat/src/utils/copyImg.ts @@ -0,0 +1,38 @@ +import { createImg } from "./createImg"; + +const copyToClipboard = async (pngBlob: any) => { + try { + await navigator.clipboard.write([ + new ClipboardItem({ + [pngBlob.type]: pngBlob, + }), + ]); + } catch (error) { + console.error(error); + } +}; + +const convertToPng = (imgBlob: any) => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const imageEl = createImg({ src: window.URL.createObjectURL(imgBlob) }); + imageEl.onload = (e: any) => { + canvas.width = e.target.width; + canvas.height = e.target.height; + if (ctx) ctx.drawImage(e.target, 0, 0, e.target.width, e.target.height); + canvas.toBlob(copyToClipboard, "image/png", 1); + }; +}; + +export const copyImg = async (image: string) => { + const img = await fetch(image); + const imgBlob = await img.blob(); + const extension = image.split(".").pop(); + const supportedToBeConverted = ["jpeg", "jpg", "gif"]; + if (extension && supportedToBeConverted.indexOf(extension.toLowerCase())) { + return convertToPng(imgBlob); + } else if (extension && extension.toLowerCase() === "png") { + return copyToClipboard(imgBlob); + } + return; +}; diff --git a/packages/react-chat/src/utils/createImg.ts b/packages/react-chat/src/utils/createImg.ts new file mode 100644 index 00000000..893be2ff --- /dev/null +++ b/packages/react-chat/src/utils/createImg.ts @@ -0,0 +1,8 @@ +export const createImg = (options: any) => { + options = options || {}; + const img = document.createElement("img"); + if (options.src) { + img.src = options.src; + } + return img; +}; diff --git a/packages/react-chat/src/utils/downloadImg.ts b/packages/react-chat/src/utils/downloadImg.ts new file mode 100644 index 00000000..34691958 --- /dev/null +++ b/packages/react-chat/src/utils/downloadImg.ts @@ -0,0 +1,27 @@ +import { createImg } from "./createImg"; + +const createLink = (name: string, imgBlob: any) => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const imageEl = createImg({ src: window.URL.createObjectURL(imgBlob) }); + imageEl.onload = (e: any) => { + canvas.width = e.target.width; + canvas.height = e.target.height; + if (ctx) ctx.drawImage(e.target, 0, 0, e.target.width, e.target.height); + const a = document.createElement("a"); + a.download = `${name}.png`; + a.href = canvas.toDataURL("image/png"); + a.click(); + }; +}; + +export const downloadImg = async (image: string) => { + const img = await fetch(image); + console.log(img); + const imgBlob = await img.blob(); + const name = image.split("/").pop(); + if (name) { + return createLink(name, imgBlob); + } + return; +};