add verify button to images

This commit is contained in:
hzrd149 2024-04-27 19:57:19 -05:00
parent 794193b575
commit 9efbb2eb58
3 changed files with 127 additions and 10 deletions

View File

@ -1,31 +1,40 @@
import { Button, ButtonGroup, ButtonGroupProps, Link } from "@chakra-ui/react";
import { ChevronUpIcon, ChevronDownIcon } from "../icons";
import { useCallback, useState } from "react";
export default function EmbedActions({
open,
url,
label,
onToggle,
children,
...props
}: Omit<ButtonGroupProps, "children"> & {
}: ButtonGroupProps & {
open: boolean;
onToggle: (open: boolean) => void;
url?: string | URL;
label: string;
}) {
const [copied, setCopied] = useState(false);
const copy = useCallback(() => {
if (!url) return;
navigator.clipboard.writeText(url.toString());
setCopied(true);
setTimeout(() => setCopied(false), 1000);
}, [url]);
return (
<ButtonGroup variant="link" size="sm" {...props}>
<Button onClick={() => onToggle(!open)}>
[ {label} {open ? <ChevronDownIcon /> : <ChevronUpIcon />} ]
</Button>
{navigator.clipboard && url && (
<Button onClick={() => navigator.clipboard.writeText(url.toString())}>[ Copy ]</Button>
)}
{navigator.clipboard && url && <Button onClick={copy}>{copied ? "[ Copied ]" : "[ Copy ]"}</Button>}
{open && url && (
<Button as={Link} href={url.toString()} isExternal>
[ Open ]
</Button>
)}
{children}
</ButtonGroup>
);
}

View File

@ -1,4 +1,4 @@
import { PropsWithChildren } from "react";
import { PropsWithChildren, ReactNode } from "react";
import EmbedActions from "./embed-actions";
import { Link, useDisclosure } from "@chakra-ui/react";
@ -10,7 +10,14 @@ export default function ExpandableEmbed({
url,
urls,
hideOnDefaultOpen,
}: PropsWithChildren<{ label: string; url?: string | URL; urls?: string[] | URL[]; hideOnDefaultOpen?: boolean }>) {
actions,
}: PropsWithChildren<{
label: string;
url?: string | URL;
urls?: string[] | URL[];
hideOnDefaultOpen?: boolean;
actions?: ReactNode;
}>) {
const { autoShowMedia } = useAppSettings();
const expanded = useDisclosure({ defaultIsOpen: autoShowMedia });
const showActions = hideOnDefaultOpen && autoShowMedia ? false : true;
@ -26,7 +33,9 @@ export default function ExpandableEmbed({
display="flex"
mt="2"
mb="1"
/>
>
{actions}
</EmbedActions>
)}
{expanded.isOpen
? children

View File

@ -1,5 +1,39 @@
import { MouseEventHandler, MutableRefObject, forwardRef, useCallback, useMemo, useRef } from "react";
import { Image, ImageProps, Link, LinkProps } from "@chakra-ui/react";
import {
MouseEvent,
MouseEventHandler,
MutableRefObject,
forwardRef,
useCallback,
useMemo,
useRef,
useState,
} from "react";
import {
Button,
Code,
Image,
ImageProps,
Link,
LinkProps,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverHeader,
PopoverTrigger,
Text,
Tooltip,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { sha256 } from "@noble/hashes/sha256";
import { EmbedableContent, defaultGetLocation } from "../../../helpers/embeds";
import { getMatchLink } from "../../../helpers/regexp";
@ -12,6 +46,7 @@ import { useBreakpointValue } from "../../../providers/global/breakpoint-provide
import useElementTrustBlur from "../../../hooks/use-element-trust-blur";
import { buildImageProxyURL } from "../../../helpers/image";
import ExpandableEmbed from "../expandable-embed";
import { bytesToHex } from "@noble/hashes/utils";
export type TrustImageProps = ImageProps;
@ -60,6 +95,8 @@ export const EmbeddedImage = forwardRef<HTMLImageElement, EmbeddedImageProps>(
[show],
);
const sha256 = src?.match(/[0-9a-f]{64}/i);
// NOTE: the parent <div> has display=block and and <a> has inline-block
// this is so that the <a> element can act like a block without being full width
return (
@ -183,12 +220,74 @@ export function embedImageGallery(content: EmbedableContent, event?: NostrEvent)
.flat();
}
function VerifyImageButton({ src, original }: { src: URL; original: string }) {
const toast = useToast();
const [loading, setLoading] = useState(false);
const modal = useDisclosure();
const [downloaded, setDownloaded] = useState<string>();
const [matches, setMatches] = useState<boolean>();
const verify = useCallback(
async (e: MouseEvent<HTMLButtonElement>) => {
if (matches !== undefined) return modal.onOpen();
setLoading(true);
try {
const buff = await fetch(src).then((res) => res.arrayBuffer());
const downloaded = bytesToHex(sha256.create().update(new Uint8Array(buff)).digest());
setDownloaded(downloaded);
setMatches(original === downloaded);
} catch (error) {
if (error instanceof Error) toast({ status: "error", description: error.message });
}
setLoading(false);
},
[src, matches],
);
return (
<>
<Button
onClick={verify}
isLoading={loading}
colorScheme={matches === undefined ? undefined : matches ? "green" : "red"}
>
[ {matches === undefined ? "Verify" : matches ? "Valid" : "Invalid!"} ]
</Button>
{modal.isOpen && downloaded && (
<Modal isOpen={modal.isOpen} onClose={modal.onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader p="4">Invalid Hash</ModalHeader>
<ModalCloseButton />
<ModalBody px="4" pb="4" pt="0">
<Text fontWeight="bold">Original:</Text>
<Code>{original}</Code>
<Text fontWeight="bold">Downloaded:</Text>
<Code>{downloaded}</Code>
</ModalBody>
</ModalContent>
</Modal>
)}
</>
);
}
// nostr:nevent1qqsfhafvv705g5wt8rcaytkj6shsshw3dwgamgfe3za8knk0uq4yesgpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqsrnltk
export function renderImageUrl(match: URL) {
if (!isImageURL(match)) return null;
const hash = match.pathname.match(/[0-9a-f]{64}/)?.[0];
return (
<ExpandableEmbed label="Image" url={match} hideOnDefaultOpen>
<ExpandableEmbed
label="Image"
url={match}
actions={hash ? <VerifyImageButton src={match} original={hash} /> : undefined}
hideOnDefaultOpen={!hash}
>
<EmbeddedImage src={match.toString()} imageProps={{ maxH: ["initial", "35vh"] }} />
</ExpandableEmbed>
);