Files
nostrudel/src/components/note/index.tsx
hzrd149 d0a510261e rebuild event tag reference helpers
DMs: refocus input after sending
2024-01-01 15:35:38 -06:00

212 lines
7.9 KiB
TypeScript

import React, { useRef } from "react";
import {
Box,
ButtonGroup,
Card,
CardBody,
CardFooter,
CardHeader,
CardProps,
Flex,
IconButton,
Link,
LinkBox,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
import UserAvatarLink from "../user-avatar-link";
import { Link as RouterLink } from "react-router-dom";
import NoteMenu from "./note-menu";
import UserLink from "../user-link";
import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
import NoteZapButton from "./note-zap-button";
import { ExpandProvider } from "../../providers/local/expanded";
import useSubject from "../../hooks/use-subject";
import appSettings from "../../services/settings/app-settings";
import EventVerificationIcon from "../event-verification-icon";
import RepostButton from "./components/repost-button";
import QuoteRepostButton from "./components/quote-repost-button";
import { ReplyIcon } from "../icons";
import NoteContentWithWarning from "./note-content-with-warning";
import { TrustProvider } from "../../providers/local/trust";
import { useRegisterIntersectionEntity } from "../../providers/local/intersection-observer";
import BookmarkButton from "./components/bookmark-button";
import useCurrentAccount from "../../hooks/use-current-account";
import NoteReactions from "./components/note-reactions";
import ReplyForm from "../../views/thread/components/reply-form";
import { getReferences, truncatedId } from "../../helpers/nostr/events";
import Timestamp from "../timestamp";
import OpenInDrawerButton from "../open-in-drawer-button";
import { getSharableEventAddress } from "../../helpers/nip19";
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
import HoverLinkOverlay from "../hover-link-overlay";
import NoteCommunityMetadata from "./note-community-metadata";
import useSingleEvent from "../../hooks/use-single-event";
import { CompactNoteContent } from "../compact-note-content";
import NoteProxyLink from "./components/note-proxy-link";
import { NoteDetailsButton } from "./components/note-details-button";
import EventInteractionDetailsModal from "../event-interactions-modal";
import singleEventService from "../../services/single-event";
import { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19";
import { nip19 } from "nostr-tools";
function ReplyToE({ pointer }: { pointer: EventPointer }) {
const event = useSingleEvent(pointer.id, pointer.relays);
if (!event) {
const nevent = nip19.neventEncode(pointer);
return (
<Text>
Replying to{" "}
<Link as={RouterLink} to={`/l/${nevent}`} color="blue.500">
{truncatedId(nevent)}
</Link>
</Text>
);
}
return (
<>
<Text>
Replying to <UserLink pubkey={event.pubkey} fontWeight="bold" />
</Text>
<CompactNoteContent event={event} maxLength={96} isTruncated textOnly />
</>
);
}
function ReplyToA({ pointer }: { pointer: AddressPointer }) {
const naddr = nip19.naddrEncode(pointer);
return (
<Text>
Replying to{" "}
<Link as={RouterLink} to={`/l/${naddr}`} color="blue.500">
{truncatedId(naddr)}
</Link>
</Text>
);
}
function ReplyLine({ event }: { event: NostrEvent }) {
const refs = getReferences(event);
if (!refs.reply) return null;
return (
<Flex gap="2" fontStyle="italic" alignItems="center" whiteSpace="nowrap">
<ReplyIcon />
{refs.reply.type === "nevent" ? <ReplyToE pointer={refs.reply.data} /> : <ReplyToA pointer={refs.reply.data} />}
</Flex>
);
}
export type NoteProps = Omit<CardProps, "children"> & {
event: NostrEvent;
variant?: CardProps["variant"];
showReplyButton?: boolean;
showReplyLine?: boolean;
hideDrawerButton?: boolean;
registerIntersectionEntity?: boolean;
clickable?: boolean;
};
export const Note = React.memo(
({
event,
variant = "outline",
showReplyButton,
showReplyLine = true,
hideDrawerButton,
registerIntersectionEntity = true,
clickable = true,
...props
}: NoteProps) => {
const account = useCurrentAccount();
const { showReactions, showSignatureVerification } = useSubject(appSettings);
const replyForm = useDisclosure();
const detailsModal = useDisclosure();
// if there is a parent intersection observer, register this card
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, event.id);
const showReactionsOnNewLine = useBreakpointValue({ base: true, lg: false });
const reactionButtons = showReactions && <NoteReactions event={event} flexWrap="wrap" variant="ghost" size="sm" />;
return (
<TrustProvider event={event}>
<ExpandProvider>
<Card
as={LinkBox}
variant={variant}
ref={registerIntersectionEntity ? ref : undefined}
data-event-id={event.id}
{...props}
>
{clickable && (
<HoverLinkOverlay
as={RouterLink}
to={`/n/${getSharableEventAddress(event)}`}
onClick={() => singleEventService.handleEvent(event)}
/>
)}
<CardHeader p="2">
<Flex flex="1" gap="2" alignItems="center">
<UserAvatarLink pubkey={event.pubkey} size={["xs", "sm"]} />
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<Flex grow={1} />
{showSignatureVerification && <EventVerificationIcon event={event} />}
{!hideDrawerButton && (
<OpenInDrawerButton
to={`/n/${getSharableEventAddress(event)}`}
size="sm"
variant="ghost"
onClick={() => singleEventService.handleEvent(event)}
/>
)}
<Link as={RouterLink} whiteSpace="nowrap" color="current" to={`/n/${getSharableEventAddress(event)}`}>
<Timestamp timestamp={event.created_at} />
</Link>
</Flex>
<NoteCommunityMetadata event={event} />
{showReplyLine && <ReplyLine event={event} />}
</CardHeader>
<CardBody p="0">
<NoteContentWithWarning event={event} />
</CardBody>
<CardFooter padding="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
{showReactionsOnNewLine && reactionButtons}
<Flex gap="2" w="full" alignItems="center">
<ButtonGroup size="sm" variant="ghost" isDisabled={account?.readonly ?? true}>
{showReplyButton && (
<IconButton icon={<ReplyIcon />} aria-label="Reply" title="Reply" onClick={replyForm.onOpen} />
)}
<RepostButton event={event} />
<QuoteRepostButton event={event} />
<NoteZapButton event={event} />
</ButtonGroup>
{!showReactionsOnNewLine && reactionButtons}
<Box flexGrow={1} />
<ButtonGroup size="sm" variant="ghost">
<NoteProxyLink event={event} />
<NoteDetailsButton event={event} onClick={detailsModal.onOpen} />
<BookmarkButton event={event} aria-label="Bookmark note" />
<NoteMenu event={event} aria-label="More Options" detailsClick={detailsModal.onOpen} />
</ButtonGroup>
</Flex>
</CardFooter>
</Card>
</ExpandProvider>
{replyForm.isOpen && (
<ReplyForm item={{ event, replies: [], refs: getReferences(event) }} onCancel={replyForm.onClose} onSubmitted={replyForm.onClose} />
)}
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={event} />}
</TrustProvider>
);
},
);
export default Note;