Context menu (#62)

This commit is contained in:
Maria Rushkova 2021-10-11 17:02:31 +02:00 committed by GitHub
parent 296f6856fc
commit fab6113163
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 232 additions and 8 deletions

View File

@ -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>
<MessageImage
src={image}
id={image}
onClick={() => { onClick={() => {
setImage(image); setImage(image);
}} }}
> />
<MessageImage src={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`

View File

@ -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}
`;

View File

@ -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};
}
`;

View File

@ -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};
}
`;

View File

@ -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 };
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};