mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-19 20:11:31 +02:00
add verify button to images
This commit is contained in:
@@ -1,31 +1,40 @@
|
|||||||
import { Button, ButtonGroup, ButtonGroupProps, Link } from "@chakra-ui/react";
|
import { Button, ButtonGroup, ButtonGroupProps, Link } from "@chakra-ui/react";
|
||||||
import { ChevronUpIcon, ChevronDownIcon } from "../icons";
|
import { ChevronUpIcon, ChevronDownIcon } from "../icons";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
export default function EmbedActions({
|
export default function EmbedActions({
|
||||||
open,
|
open,
|
||||||
url,
|
url,
|
||||||
label,
|
label,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: Omit<ButtonGroupProps, "children"> & {
|
}: ButtonGroupProps & {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onToggle: (open: boolean) => void;
|
onToggle: (open: boolean) => void;
|
||||||
url?: string | URL;
|
url?: string | URL;
|
||||||
label: string;
|
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 (
|
return (
|
||||||
<ButtonGroup variant="link" size="sm" {...props}>
|
<ButtonGroup variant="link" size="sm" {...props}>
|
||||||
<Button onClick={() => onToggle(!open)}>
|
<Button onClick={() => onToggle(!open)}>
|
||||||
[ {label} {open ? <ChevronDownIcon /> : <ChevronUpIcon />} ]
|
[ {label} {open ? <ChevronDownIcon /> : <ChevronUpIcon />} ]
|
||||||
</Button>
|
</Button>
|
||||||
{navigator.clipboard && url && (
|
{navigator.clipboard && url && <Button onClick={copy}>{copied ? "[ Copied ]" : "[ Copy ]"}</Button>}
|
||||||
<Button onClick={() => navigator.clipboard.writeText(url.toString())}>[ Copy ]</Button>
|
|
||||||
)}
|
|
||||||
{open && url && (
|
{open && url && (
|
||||||
<Button as={Link} href={url.toString()} isExternal>
|
<Button as={Link} href={url.toString()} isExternal>
|
||||||
[ Open ]
|
[ Open ]
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{children}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren, ReactNode } from "react";
|
||||||
import EmbedActions from "./embed-actions";
|
import EmbedActions from "./embed-actions";
|
||||||
import { Link, useDisclosure } from "@chakra-ui/react";
|
import { Link, useDisclosure } from "@chakra-ui/react";
|
||||||
|
|
||||||
@@ -10,7 +10,14 @@ export default function ExpandableEmbed({
|
|||||||
url,
|
url,
|
||||||
urls,
|
urls,
|
||||||
hideOnDefaultOpen,
|
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 { autoShowMedia } = useAppSettings();
|
||||||
const expanded = useDisclosure({ defaultIsOpen: autoShowMedia });
|
const expanded = useDisclosure({ defaultIsOpen: autoShowMedia });
|
||||||
const showActions = hideOnDefaultOpen && autoShowMedia ? false : true;
|
const showActions = hideOnDefaultOpen && autoShowMedia ? false : true;
|
||||||
@@ -26,7 +33,9 @@ export default function ExpandableEmbed({
|
|||||||
display="flex"
|
display="flex"
|
||||||
mt="2"
|
mt="2"
|
||||||
mb="1"
|
mb="1"
|
||||||
/>
|
>
|
||||||
|
{actions}
|
||||||
|
</EmbedActions>
|
||||||
)}
|
)}
|
||||||
{expanded.isOpen
|
{expanded.isOpen
|
||||||
? children
|
? children
|
||||||
|
@@ -1,5 +1,39 @@
|
|||||||
import { MouseEventHandler, MutableRefObject, forwardRef, useCallback, useMemo, useRef } from "react";
|
import {
|
||||||
import { Image, ImageProps, Link, LinkProps } from "@chakra-ui/react";
|
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 { EmbedableContent, defaultGetLocation } from "../../../helpers/embeds";
|
||||||
import { getMatchLink } from "../../../helpers/regexp";
|
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 useElementTrustBlur from "../../../hooks/use-element-trust-blur";
|
||||||
import { buildImageProxyURL } from "../../../helpers/image";
|
import { buildImageProxyURL } from "../../../helpers/image";
|
||||||
import ExpandableEmbed from "../expandable-embed";
|
import ExpandableEmbed from "../expandable-embed";
|
||||||
|
import { bytesToHex } from "@noble/hashes/utils";
|
||||||
|
|
||||||
export type TrustImageProps = ImageProps;
|
export type TrustImageProps = ImageProps;
|
||||||
|
|
||||||
@@ -60,6 +95,8 @@ export const EmbeddedImage = forwardRef<HTMLImageElement, EmbeddedImageProps>(
|
|||||||
[show],
|
[show],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sha256 = src?.match(/[0-9a-f]{64}/i);
|
||||||
|
|
||||||
// NOTE: the parent <div> has display=block and and <a> has inline-block
|
// 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
|
// this is so that the <a> element can act like a block without being full width
|
||||||
return (
|
return (
|
||||||
@@ -183,12 +220,74 @@ export function embedImageGallery(content: EmbedableContent, event?: NostrEvent)
|
|||||||
.flat();
|
.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
|
// nostr:nevent1qqsfhafvv705g5wt8rcaytkj6shsshw3dwgamgfe3za8knk0uq4yesgpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqsrnltk
|
||||||
export function renderImageUrl(match: URL) {
|
export function renderImageUrl(match: URL) {
|
||||||
if (!isImageURL(match)) return null;
|
if (!isImageURL(match)) return null;
|
||||||
|
|
||||||
|
const hash = match.pathname.match(/[0-9a-f]{64}/)?.[0];
|
||||||
|
|
||||||
return (
|
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"] }} />
|
<EmbeddedImage src={match.toString()} imageProps={{ maxH: ["initial", "35vh"] }} />
|
||||||
</ExpandableEmbed>
|
</ExpandableEmbed>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user