Context menu (#62)
This commit is contained in:
parent
296f6856fc
commit
fab6113163
|
@ -4,6 +4,7 @@ import styled from "styled-components";
|
||||||
|
|
||||||
import { ChatMessage } from "../../models/ChatMessage";
|
import { ChatMessage } from "../../models/ChatMessage";
|
||||||
import { Metadata } from "../../models/Metadata";
|
import { Metadata } from "../../models/Metadata";
|
||||||
|
import { ImageMenu } from "../Form/ImageMenu";
|
||||||
|
|
||||||
/* eslint-disable no-useless-escape */
|
/* eslint-disable no-useless-escape */
|
||||||
const regEx =
|
const regEx =
|
||||||
|
@ -13,14 +14,12 @@ const regEx =
|
||||||
type ChatMessageContentProps = {
|
type ChatMessageContentProps = {
|
||||||
message: ChatMessage;
|
message: ChatMessage;
|
||||||
fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
|
fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
|
||||||
|
|
||||||
setImage: (image: string) => void;
|
setImage: (image: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ChatMessageContent({
|
export function ChatMessageContent({
|
||||||
message,
|
message,
|
||||||
fetchMetadata,
|
fetchMetadata,
|
||||||
|
|
||||||
setImage,
|
setImage,
|
||||||
}: ChatMessageContentProps) {
|
}: ChatMessageContentProps) {
|
||||||
const { content, image } = useMemo(() => message, [message]);
|
const { content, image } = useMemo(() => message, [message]);
|
||||||
|
@ -72,16 +71,20 @@ export function ChatMessageContent({
|
||||||
};
|
};
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}, [link]);
|
}, [link]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentWrapper>
|
<ContentWrapper>
|
||||||
<div>{elements.map((el) => el)}</div>
|
<div>{elements.map((el) => el)}</div>
|
||||||
{image && (
|
{image && (
|
||||||
<MessageImageWrapper
|
<MessageImageWrapper>
|
||||||
onClick={() => {
|
<MessageImage
|
||||||
setImage(image);
|
src={image}
|
||||||
}}
|
id={image}
|
||||||
>
|
onClick={() => {
|
||||||
<MessageImage src={image} />
|
setImage(image);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ImageMenu imageId={image} />
|
||||||
</MessageImageWrapper>
|
</MessageImageWrapper>
|
||||||
)}
|
)}
|
||||||
{openGraph && (
|
{openGraph && (
|
||||||
|
@ -103,6 +106,7 @@ const MessageImageWrapper = styled.div`
|
||||||
width: 147px;
|
width: 147px;
|
||||||
height: 196px;
|
height: 196px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MessageImage = styled.img`
|
const MessageImage = styled.img`
|
||||||
|
@ -110,6 +114,7 @@ const MessageImage = styled.img`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
cursor; pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const PreviewSiteNameWrapper = styled.div`
|
const PreviewSiteNameWrapper = styled.div`
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
<MenuBlock>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={() => copyImg(imageId)}>
|
||||||
|
<CopyIcon /> <MenuText>Copy imageId</MenuText>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => downloadImg(imageId)}>
|
||||||
|
<DownloadIcon />
|
||||||
|
<MenuText> Download imageId</MenuText>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</MenuBlock>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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}
|
||||||
|
`;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export const CopyIcon = () => {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M4.16683 7.00065C4.44297 7.00065 4.66683 6.77679 4.66683 6.50065C4.66683 6.22451 4.44297 6.00065 4.16683 6.00065H4.00016C2.5274 6.00065 1.3335 7.19456 1.3335 8.66732V12.0007C1.3335 13.4734 2.5274 14.6673 4.00016 14.6673H7.3335C8.80626 14.6673 10.0002 13.4734 10.0002 12.0007V11.834C10.0002 11.5578 9.77631 11.334 9.50016 11.334C9.22402 11.334 9.00016 11.5578 9.00016 11.834V12.0007C9.00016 12.9211 8.25397 13.6673 7.3335 13.6673H4.00016C3.07969 13.6673 2.3335 12.9211 2.3335 12.0007V8.66732C2.3335 7.74684 3.07969 7.00065 4.00016 7.00065H4.16683Z" />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M6.00016 4.00065C6.00016 2.52789 7.19407 1.33398 8.66683 1.33398H12.0002C13.4729 1.33398 14.6668 2.52789 14.6668 4.00065V7.33398C14.6668 8.80674 13.4729 10.0007 12.0002 10.0007H8.66683C7.19407 10.0007 6.00016 8.80674 6.00016 7.33398V4.00065ZM8.66683 2.33398H12.0002C12.9206 2.33398 13.6668 3.08018 13.6668 4.00065V7.33398C13.6668 8.25446 12.9206 9.00065 12.0002 9.00065H8.66683C7.74636 9.00065 7.00016 8.25446 7.00016 7.33398V4.00065C7.00016 3.08018 7.74636 2.33398 8.66683 2.33398Z"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Icon = styled.svg`
|
||||||
|
& > path {
|
||||||
|
fill: ${({ theme }) => theme.tertiary};
|
||||||
|
}
|
||||||
|
`;
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export const DownloadIcon = () => {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M10.6668 8.83399C10.6668 8.55784 10.8924 8.33835 11.1647 8.29286C12.5846 8.05568 13.6668 6.82121 13.6668 5.33399C13.6668 3.67713 12.3237 2.33399 10.6668 2.33399L5.3335 2.33399C3.67664 2.33399 2.3335 3.67713 2.3335 5.33399C2.3335 6.82121 3.4157 8.05568 4.83559 8.29286C5.10796 8.33835 5.3335 8.55784 5.3335 8.83399C5.3335 9.11013 5.10852 9.33726 4.83449 9.30316C2.86085 9.05755 1.3335 7.37414 1.3335 5.33399C1.3335 3.12485 3.12436 1.33398 5.3335 1.33398L10.6668 1.33399C12.876 1.33399 14.6668 3.12485 14.6668 5.33399C14.6668 7.37414 13.1395 9.05755 11.1658 9.30316C10.8918 9.33726 10.6668 9.11013 10.6668 8.83399Z" />
|
||||||
|
<path d="M8.00016 4.83399C8.27631 4.83399 8.50016 5.05784 8.50016 5.33399V11.9888C8.50016 12.2858 8.85921 12.4345 9.0692 12.2245L10.3133 10.9804C10.5085 10.7852 10.8251 10.7852 11.0204 10.9804C11.2156 11.1757 11.2156 11.4923 11.0204 11.6875L8.35372 14.3542C8.15845 14.5495 7.84187 14.5495 7.64661 14.3542L4.97994 11.6875C4.78468 11.4923 4.78468 11.1757 4.97994 10.9804C5.1752 10.7852 5.49179 10.7852 5.68705 10.9804L6.93113 12.2245C7.14112 12.4345 7.50016 12.2858 7.50016 11.9888V5.33399C7.50016 5.05784 7.72402 4.83399 8.00016 4.83399Z" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Icon = styled.svg`
|
||||||
|
& > path {
|
||||||
|
fill: ${({ theme }) => theme.tertiary};
|
||||||
|
}
|
||||||
|
`;
|
|
@ -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 };
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
Loading…
Reference in New Issue