mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-10 12:53:14 +02:00
sign and send reaction event
This commit is contained in:
@@ -1,7 +1,20 @@
|
|||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import moment from "moment";
|
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 { NostrEvent } from "../../types/nostr-event";
|
||||||
import { UserAvatarLink } from "../user-avatar-link";
|
import { UserAvatarLink } from "../user-avatar-link";
|
||||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
||||||
@@ -9,17 +22,17 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
|||||||
import { NoteContents } from "./note-contents";
|
import { NoteContents } from "./note-contents";
|
||||||
import { NoteMenu } from "./note-menu";
|
import { NoteMenu } from "./note-menu";
|
||||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||||
import { UserTipButton } from "../user-tip-button";
|
|
||||||
import { NoteRelays } from "./note-relays";
|
import { NoteRelays } from "./note-relays";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
import { UserLink } from "../user-link";
|
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 { PostModalContext } from "../../providers/post-modal-provider";
|
||||||
import { buildReply, buildShare } from "../../helpers/nostr-event";
|
import { buildReply, buildShare } from "../../helpers/nostr-event";
|
||||||
import { UserDnsIdentityIcon } from "../user-dns-identity";
|
import { UserDnsIdentityIcon } from "../user-dns-identity";
|
||||||
import { convertTimestampToDate } from "../../helpers/date";
|
import { convertTimestampToDate } from "../../helpers/date";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
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 = {
|
export type NoteProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
@@ -56,7 +69,6 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
|
|||||||
<NoteContents event={event} trusted={following.includes(event.pubkey)} maxHeight={maxHeight} />
|
<NoteContents event={event} trusted={following.includes(event.pubkey)} maxHeight={maxHeight} />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter padding="2" display="flex" gap="2">
|
<CardFooter padding="2" display="flex" gap="2">
|
||||||
<UserTipButton pubkey={event.pubkey} eventId={event.id} size="xs" />
|
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<ReplyIcon />}
|
icon={<ReplyIcon />}
|
||||||
title="Reply"
|
title="Reply"
|
||||||
@@ -73,7 +85,10 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
|
|||||||
size="xs"
|
size="xs"
|
||||||
isDisabled={account.readonly}
|
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} />
|
<Box flexGrow={1} />
|
||||||
<NoteRelays event={event} size="xs" />
|
<NoteRelays event={event} size="xs" />
|
||||||
<NoteMenu event={event} />
|
<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 {
|
import {
|
||||||
Avatar,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -15,52 +14,33 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
|||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { MenuIconButton } from "../menu-icon-button";
|
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 { getReferences } from "../../helpers/nostr-event";
|
||||||
|
import NoteReactionsModal from "./note-reactions-modal";
|
||||||
|
|
||||||
export const NoteMenu = ({ event }: { event: NostrEvent }) => {
|
export const NoteMenu = ({ event }: { event: NostrEvent }) => {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const infoModal = useDisclosure();
|
||||||
|
const reactionsModal = useDisclosure();
|
||||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
||||||
const noteId = normalizeToBech32(event.id, Bech32Prefix.Note);
|
const noteId = normalizeToBech32(event.id, Bech32Prefix.Note);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuIconButton>
|
<MenuIconButton>
|
||||||
<MenuItem
|
<MenuItem onClick={reactionsModal.onOpen} icon={<LikeIcon />}>
|
||||||
as="a"
|
Reactions
|
||||||
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>
|
</MenuItem>
|
||||||
{noteId && (
|
{noteId && (
|
||||||
<MenuItem onClick={() => copyToClipboard(noteId)} icon={<ClipboardIcon />}>
|
<MenuItem onClick={() => copyToClipboard(noteId)} icon={<ClipboardIcon />}>
|
||||||
Copy Note ID
|
Copy Note ID
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={onOpen} icon={<CodeIcon />}>
|
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
||||||
View Raw
|
View Raw
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuIconButton>
|
</MenuIconButton>
|
||||||
{isOpen && (
|
{infoModal.isOpen && (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="6xl">
|
<Modal isOpen={infoModal.isOpen} onClose={infoModal.onClose} size="6xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>Raw Event</ModalHeader>
|
<ModalHeader>Raw Event</ModalHeader>
|
||||||
@@ -74,6 +54,9 @@ export const NoteMenu = ({ event }: { event: NostrEvent }) => {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
{reactionsModal.isOpen && (
|
||||||
|
<NoteReactionsModal noteId={event.id} isOpen={reactionsModal.isOpen} onClose={reactionsModal.onClose} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -3,21 +3,18 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalCloseButton,
|
ModalCloseButton,
|
||||||
Button,
|
Button,
|
||||||
ModalProps,
|
ModalProps,
|
||||||
Text,
|
Text,
|
||||||
useDisclosure,
|
|
||||||
Flex,
|
Flex,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
IconButton,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
import { NostrRequest } from "../../classes/nostr-request";
|
import { NostrRequest } from "../../classes/nostr-request";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
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 { UserAvatarLink } from "../user-avatar-link";
|
||||||
import { UserLink } from "../user-link";
|
import { UserLink } from "../user-link";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -98,22 +95,14 @@ function sortEvents(a: NostrEvent, b: NostrEvent) {
|
|||||||
return b.created_at - a.created_at;
|
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 { reactions, zaps } = useEventReactions(noteId);
|
||||||
const [selected, setSelected] = useState("reactions");
|
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 (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose}>
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
<ModalOverlay />
|
<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} />)}
|
{selected === "zaps" && zaps.sort(sortEvents).map((event) => <ZapEvent key={event.id} event={event} />)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalBody>
|
</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>
|
</ModalContent>
|
||||||
</Modal>
|
</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 { useState } from "react";
|
||||||
import { encodeText } from "../helpers/bech32";
|
import { encodeText } from "../helpers/bech32";
|
||||||
|
|
||||||
export const UserTipButton = ({
|
export const UserTipButton = ({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "aria-label">) => {
|
||||||
pubkey,
|
|
||||||
eventId,
|
|
||||||
...props
|
|
||||||
}: { pubkey: string; eventId?: string } & Omit<IconButtonProps, "aria-label">) => {
|
|
||||||
const metadata = useUserMetadata(pubkey);
|
const metadata = useUserMetadata(pubkey);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
export function unique<T>(arr: T[]): T[] {
|
export function unique<T>(arr: T[]): T[] {
|
||||||
return Array.from(new Set(arr));
|
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 { useAsync } from "react-use";
|
||||||
import singleEventService from "../services/single-event";
|
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("|")]);
|
const { loading, value: event } = useAsync(() => singleEventService.requestEvent(id, relays), [id, relays.join("|")]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Reference in New Issue
Block a user