sign and send reaction event

This commit is contained in:
hzrd149 2023-02-21 20:09:49 -06:00
parent 3db2c49cfb
commit c382d85981
8 changed files with 145 additions and 103 deletions

View File

@ -1,7 +1,20 @@
import React, { useContext } from "react";
import { Link as RouterLink } from "react-router-dom";
import moment from "moment";
import { Box, Card, CardBody, CardFooter, CardHeader, Flex, Heading, IconButton, Link, Text } from "@chakra-ui/react";
import {
Box,
Button,
ButtonGroup,
Card,
CardBody,
CardFooter,
CardHeader,
Flex,
Heading,
IconButton,
Link,
Text,
} from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
import { UserAvatarLink } from "../user-avatar-link";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
@ -9,17 +22,17 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
import { NoteContents } from "./note-contents";
import { NoteMenu } from "./note-menu";
import { useUserContacts } from "../../hooks/use-user-contacts";
import { UserTipButton } from "../user-tip-button";
import { NoteRelays } from "./note-relays";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { UserLink } from "../user-link";
import { ReplyIcon, ShareIcon } from "../icons";
import { LightningIcon, LikeIcon, ReplyIcon, ShareIcon } from "../icons";
import { PostModalContext } from "../../providers/post-modal-provider";
import { buildReply, buildShare } from "../../helpers/nostr-event";
import { UserDnsIdentityIcon } from "../user-dns-identity";
import { convertTimestampToDate } from "../../helpers/date";
import { useCurrentAccount } from "../../hooks/use-current-account";
import NoteReactions from "./note-reactions";
import NoteLikeButton from "./note-like-button";
import NoteZapButton from "./note-zap-button";
export type NoteProps = {
event: NostrEvent;
@ -56,7 +69,6 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
<NoteContents event={event} trusted={following.includes(event.pubkey)} maxHeight={maxHeight} />
</CardBody>
<CardFooter padding="2" display="flex" gap="2">
<UserTipButton pubkey={event.pubkey} eventId={event.id} size="xs" />
<IconButton
icon={<ReplyIcon />}
title="Reply"
@ -73,7 +85,10 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
size="xs"
isDisabled={account.readonly}
/>
<NoteReactions noteId={event.id} />
<ButtonGroup size="xs" isAttached>
<NoteZapButton note={event} size="xs" />
<NoteLikeButton note={event} size="xs" />
</ButtonGroup>
<Box flexGrow={1} />
<NoteRelays event={event} size="xs" />
<NoteMenu event={event} />

View File

@ -0,0 +1,80 @@
import {
Button,
ButtonProps,
Flex,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from "@chakra-ui/react";
import moment from "moment";
import { Kind } from "nostr-tools";
import { useState } from "react";
import { nostrPostAction } from "../../classes/nostr-post-action";
import { random } from "../../helpers/array";
import { useSigningContext } from "../../providers/signing-provider";
import clientRelaysService from "../../services/client-relays";
import { getEventRelays } from "../../services/event-relays";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import { DislikeIcon, LikeIcon } from "../icons";
export default function NoteLikeButton({ note, ...props }: { note: NostrEvent } & Omit<ButtonProps, "children">) {
const { requestSignature } = useSigningContext();
const [loading, setLoading] = useState(false);
const handleClick = async (reaction = "+") => {
const eventRelays = getEventRelays(note.id).value;
const event: DraftNostrEvent = {
kind: Kind.Reaction,
content: reaction,
tags: [
["e", note.id, random(eventRelays)],
["p", note.pubkey], // TODO: pick a relay for the user
],
created_at: moment().unix(),
};
const signed = await requestSignature(event);
if (signed) {
const writeRelays = clientRelaysService.getWriteUrls();
nostrPostAction(writeRelays, signed);
}
setLoading(false);
};
const customReaction = () => {
const input = window.prompt("Enter Reaction");
if (!input || [...input].length !== 1) return;
handleClick(input);
};
return (
<Popover placement="bottom" trigger="hover" openDelay={500}>
<PopoverTrigger>
<Button
leftIcon={<LikeIcon />}
aria-label="Like Note"
title="Like Note"
onClick={() => handleClick("+")}
isLoading={loading}
{...props}
>
0
</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<Flex gap="2">
<IconButton icon={<LikeIcon />} onClick={() => handleClick("+")} aria-label="like" />
<IconButton icon={<DislikeIcon />} onClick={() => handleClick("-")} aria-label="dislike" />
<IconButton icon={<span>🤙</span>} onClick={() => handleClick("🤙")} aria-label="different like" />
<IconButton icon={<span></span>} onClick={() => handleClick("❤️")} aria-label="different like" />
<Button onClick={customReaction}>Custom</Button>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
}

View File

@ -1,5 +1,4 @@
import {
Avatar,
MenuItem,
useDisclosure,
Modal,
@ -15,52 +14,33 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
import { NostrEvent } from "../../types/nostr-event";
import { MenuIconButton } from "../menu-icon-button";
import { ClipboardIcon, CodeIcon, IMAGE_ICONS } from "../icons";
import { ClipboardIcon, CodeIcon, LikeIcon } from "../icons";
import { getReferences } from "../../helpers/nostr-event";
import NoteReactionsModal from "./note-reactions-modal";
export const NoteMenu = ({ event }: { event: NostrEvent }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const infoModal = useDisclosure();
const reactionsModal = useDisclosure();
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
const noteId = normalizeToBech32(event.id, Bech32Prefix.Note);
return (
<>
<MenuIconButton>
<MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />}
href={`https://www.nostr.guru/e/${event.id}`}
target="_blank"
>
Open in Nostr.guru
</MenuItem>
<MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.brbIcon} size="xs" />}
href={`https://brb.io/n/${noteId}`}
target="_blank"
>
Open in BRB
</MenuItem>
<MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.snortSocialIcon} size="xs" />}
href={`https://snort.social/e/${noteId}`}
target="_blank"
>
Open in snort.social
<MenuItem onClick={reactionsModal.onOpen} icon={<LikeIcon />}>
Reactions
</MenuItem>
{noteId && (
<MenuItem onClick={() => copyToClipboard(noteId)} icon={<ClipboardIcon />}>
Copy Note ID
</MenuItem>
)}
<MenuItem onClick={onOpen} icon={<CodeIcon />}>
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
View Raw
</MenuItem>
</MenuIconButton>
{isOpen && (
<Modal isOpen={isOpen} onClose={onClose} size="6xl">
{infoModal.isOpen && (
<Modal isOpen={infoModal.isOpen} onClose={infoModal.onClose} size="6xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>Raw Event</ModalHeader>
@ -74,6 +54,9 @@ export const NoteMenu = ({ event }: { event: NostrEvent }) => {
</ModalContent>
</Modal>
)}
{reactionsModal.isOpen && (
<NoteReactionsModal noteId={event.id} isOpen={reactionsModal.isOpen} onClose={reactionsModal.onClose} />
)}
</>
);
};

View File

@ -3,21 +3,18 @@ import {
Modal,
ModalOverlay,
ModalContent,
ModalFooter,
ModalBody,
ModalCloseButton,
Button,
ModalProps,
Text,
useDisclosure,
Flex,
ButtonGroup,
IconButton,
} from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { NostrRequest } from "../../classes/nostr-request";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import { NostrEvent } from "../../types/nostr-event";
import { UserAvatarLink } from "../user-avatar-link";
import { UserLink } from "../user-link";
import moment from "moment";
@ -98,22 +95,14 @@ function sortEvents(a: NostrEvent, b: NostrEvent) {
return b.created_at - a.created_at;
}
export const NoteReactionsModal = ({ isOpen, onClose, noteId }: { noteId: string } & Omit<ModalProps, "children">) => {
export default function NoteReactionsModal({
isOpen,
onClose,
noteId,
}: { noteId: string } & Omit<ModalProps, "children">) {
const { reactions, zaps } = useEventReactions(noteId);
const [selected, setSelected] = useState("reactions");
const [sending, setSending] = useState(false);
const sendReaction = async (content: string) => {
setSending(true);
const event: DraftNostrEvent = {
kind: Kind.Reaction,
content,
created_at: moment().unix(),
tags: [["e", noteId]],
};
setSending(false);
};
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
@ -134,51 +123,7 @@ export const NoteReactionsModal = ({ isOpen, onClose, noteId }: { noteId: string
{selected === "zaps" && zaps.sort(sortEvents).map((event) => <ZapEvent key={event.id} event={event} />)}
</Flex>
</ModalBody>
<ModalFooter display="flex" gap="2">
<IconButton
icon={<LikeIcon />}
aria-label="Like Note"
title="Like Note"
size="sm"
variant="outline"
isDisabled
/>
<IconButton
icon={<DislikeIcon />}
aria-label="Dislike Note"
title="Dislike Note"
size="sm"
variant="outline"
isDisabled
/>
<Button size="sm" variant="outline" isDisabled>
🤙
</Button>
<Button size="sm" variant="outline" mr="auto" isDisabled>
Custom
</Button>
<Button colorScheme="blue" onClick={onClose} size="sm">
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
const NoteReactions = ({ noteId }: { noteId: string }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<ButtonGroup size="xs" isAttached>
<IconButton icon={<LikeIcon />} aria-label="Like Note" title="Like Note" />
<Button onClick={onOpen}>Reactions</Button>
</ButtonGroup>
{isOpen && <NoteReactionsModal noteId={noteId} isOpen={isOpen} onClose={onClose} />}
</>
);
};
export default NoteReactions;
}

View File

@ -0,0 +1,20 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { NostrEvent } from "../../types/nostr-event";
import { LightningIcon } from "../icons";
export default function NoteZapButton({ note, ...props }: { note: NostrEvent } & Omit<ButtonProps, "children">) {
const metadata = useUserMetadata(note.pubkey);
return (
<Button
leftIcon={<LightningIcon color="yellow.500" />}
aria-label="Zap Note"
title="Zap Note"
{...props}
isDisabled
>
0
</Button>
);
}

View File

@ -4,11 +4,7 @@ import { LightningIcon } from "./icons";
import { useState } from "react";
import { encodeText } from "../helpers/bech32";
export const UserTipButton = ({
pubkey,
eventId,
...props
}: { pubkey: string; eventId?: string } & Omit<IconButtonProps, "aria-label">) => {
export const UserTipButton = ({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "aria-label">) => {
const metadata = useUserMetadata(pubkey);
const [loading, setLoading] = useState(false);
const toast = useToast();

View File

@ -1,3 +1,6 @@
export function unique<T>(arr: T[]): T[] {
return Array.from(new Set(arr));
}
export function random<T>(arr: T[]): T {
return arr[Math.round(Math.random() * (arr.length - 1))];
}

View File

@ -1,7 +1,7 @@
import { useAsync } from "react-use";
import singleEventService from "../services/single-event";
export default function useSingleEvent(id: string, relays: string[]) {
export default function useSingleEvent(id: string, relays: string[] = []) {
const { loading, value: event } = useAsync(() => singleEventService.requestEvent(id, relays), [id, relays.join("|")]);
return {