mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-29 11:12:12 +01:00
sign and send reaction event
This commit is contained in:
parent
3db2c49cfb
commit
c382d85981
@ -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} />
|
||||
|
80
src/components/note/note-like-button.tsx
Normal file
80
src/components/note/note-like-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
20
src/components/note/note-zap-button.tsx
Normal file
20
src/components/note/note-zap-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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();
|
||||
|
@ -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))];
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user