Show reposts in note details modal

This commit is contained in:
hzrd149 2023-11-23 11:12:13 -06:00
parent cf1ab354ad
commit b372edab27
16 changed files with 268 additions and 241 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Show reposts in note details modal

View File

@ -1,3 +1,3 @@
{
"cSpell.words": ["Bech", "Chakra", "Msat", "nostr", "noStrudel", "Npub", "pubkeys", "Sats", "webln"]
"cSpell.words": ["Bech", "Chakra", "lnurl", "Msat", "nostr", "noStrudel", "Npub", "pubkeys", "Sats", "webln"]
}

View File

@ -0,0 +1,104 @@
import React, { useState } from "react";
import {
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalCloseButton,
Button,
ModalProps,
Text,
Flex,
ButtonGroup,
Spacer,
ModalHeader,
} from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
import UserAvatarLink from "../user-avatar-link";
import { UserLink } from "../user-link";
import { LightningIcon } from "../icons";
import { ParsedZap } from "../../helpers/nostr/zaps";
import { readablizeSats } from "../../helpers/bolt11";
import useEventReactions from "../../hooks/use-event-reactions";
import useEventZaps from "../../hooks/use-event-zaps";
import Timestamp from "../timestamp";
import { getEventUID } from "../../helpers/nostr/events";
import ReactionDetails from "./reaction-details";
import RepostDetails from "./repost-details";
const ZapEvent = React.memo(({ zap }: { zap: ParsedZap }) => {
if (!zap.payment.amount) return null;
return (
<>
<Flex gap="2" alignItems="center">
<UserAvatarLink pubkey={zap.request.pubkey} size="xs" mr="2" />
<UserLink pubkey={zap.request.pubkey} />
<Timestamp timestamp={zap.event.created_at} />
<Spacer />
<LightningIcon color="yellow.500" />
<Text fontWeight="bold">{readablizeSats(zap.payment.amount / 1000)}</Text>
</Flex>
<Text>{zap.request.content}</Text>
</>
);
});
export default function EventInteractionDetailsModal({
isOpen,
onClose,
event,
size = "2xl",
...props
}: Omit<ModalProps, "children"> & { event: NostrEvent }) {
const uuid = getEventUID(event);
const zaps = useEventZaps(uuid, [], true) ?? [];
const reactions = useEventReactions(uuid, [], true) ?? [];
const [tab, setTab] = useState(zaps.length > 0 ? "zaps" : "reactions");
const renderTab = () => {
switch (tab) {
case "reposts":
return <RepostDetails event={event} />;
case "reactions":
return <ReactionDetails reactions={reactions} />;
case "zaps":
return (
<>
{zaps
.sort((a, b) => b.request.created_at - a.request.created_at)
.map((zap) => (
<ZapEvent key={zap.request.id} zap={zap} />
))}
</>
);
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} size={size} {...props}>
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<ModalHeader p={["2", "4"]}>
<ButtonGroup>
<Button size="sm" variant={tab === "zaps" ? "solid" : "outline"} onClick={() => setTab("zaps")}>
Zaps ({zaps.length})
</Button>
<Button size="sm" variant={tab === "reactions" ? "solid" : "outline"} onClick={() => setTab("reactions")}>
Reactions ({reactions.length})
</Button>
<Button size="sm" variant={tab === "reposts" ? "solid" : "outline"} onClick={() => setTab("reposts")}>
Reposts
</Button>
</ButtonGroup>
</ModalHeader>
<ModalBody px={["2", "4"]} pt="0" pb={["2", "4"]} display="flex" flexDirection="column" gap="2">
{renderTab()}
</ModalBody>
</ModalContent>
</Modal>
);
}

View File

@ -0,0 +1,55 @@
import { Box, Button, Divider, Flex, SimpleGrid, SimpleGridProps, useDisclosure } from "@chakra-ui/react";
import { useMemo } from "react";
import { NostrEvent } from "../../types/nostr-event";
import { groupReactions } from "../../helpers/nostr/reactions";
import UserAvatarLink from "../user-avatar-link";
import { UserLink } from "../user-link";
import ReactionIcon from "../event-reactions/reaction-icon";
function ShowMoreGrid({
pubkeys,
cutoff,
...props
}: Omit<SimpleGridProps, "children"> & { pubkeys: string[]; cutoff: number }) {
const showMore = useDisclosure();
const limited = pubkeys.length > cutoff && !showMore.isOpen ? pubkeys.slice(0, cutoff) : pubkeys;
return (
<>
<SimpleGrid spacing="1" {...props}>
{limited.map((pubkey) => (
<Flex gap="2" key={pubkey} alignItems="center" overflow="hidden">
<UserAvatarLink pubkey={pubkey} size="xs" />
<UserLink pubkey={pubkey} isTruncated />
</Flex>
))}
</SimpleGrid>
{limited.length !== pubkeys.length && (
<Button variant="link" size="md" onClick={showMore.onOpen}>
Show {pubkeys.length - limited.length} more
</Button>
)}
</>
);
}
export default function ReactionDetails({ reactions }: { reactions: NostrEvent[] }) {
const groups = useMemo(() => groupReactions(reactions), [reactions]);
return (
<Flex gap="2" direction="column">
{groups.map((group) => (
<Flex key={group.emoji} direction="column" gap="2">
<Flex gap="2" alignItems="center">
<Box fontSize="lg" borderWidth={1} w="8" h="8" borderRadius="md" p="1">
<ReactionIcon emoji={group.emoji} url={group.url} />
</Box>
<Divider />
</Flex>
<ShowMoreGrid pubkeys={group.pubkeys} columns={{ base: 2, sm: 3, md: 4 }} cutoff={12} />
</Flex>
))}
</Flex>
);
}

View File

@ -0,0 +1,30 @@
import { Button, Flex, SimpleGrid, SimpleGridProps, Text, useDisclosure } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { NostrEvent } from "../../types/nostr-event";
import UserAvatarLink from "../user-avatar-link";
import { UserLink } from "../user-link";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useSubject from "../../hooks/use-subject";
import Timestamp from "../timestamp";
export default function RepostDetails({ event }: { event: NostrEvent }) {
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(`${event.id}-reposts`, readRelays, { kinds: [Kind.Repost], "#e": [event.id] });
const reposts = useSubject(timeline.timeline);
return (
<>
{reposts.map((repost) => (
<Flex key={repost.id} gap="2" alignItems="center">
<UserAvatarLink pubkey={repost.pubkey} size="sm" />
<UserLink pubkey={repost.pubkey} fontWeight="bold" />
<Text>Shared</Text>
<Timestamp timestamp={repost.created_at} />
</Flex>
))}
</>
);
}

View File

@ -1,17 +1,14 @@
import { useMemo } from "react";
import { Button, useDisclosure } from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
import useEventReactions from "../../hooks/use-event-reactions";
import { groupReactions } from "../../helpers/nostr/reactions";
import ReactionDetailsModal from "../reaction-details-modal";
import useCurrentAccount from "../../hooks/use-current-account";
import ReactionGroupButton from "./reaction-group-button";
import { useAddReaction } from "./common-hooks";
export default function EventReactionButtons({ event, max }: { event: NostrEvent; max?: number }) {
const account = useCurrentAccount();
const detailsModal = useDisclosure();
const reactions = useEventReactions(event.id) ?? [];
const grouped = useMemo(() => groupReactions(reactions), [reactions]);
@ -34,8 +31,6 @@ export default function EventReactionButtons({ event, max }: { event: NostrEvent
colorScheme={account && group.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
/>
))}
<Button onClick={detailsModal.onOpen}>Show all</Button>
{detailsModal.isOpen && <ReactionDetailsModal isOpen onClose={detailsModal.onClose} reactions={reactions} />}
</>
);
}

View File

@ -0,0 +1,20 @@
import { IconButton, IconButtonProps } from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import InfoCircle from "../../icons/info-circle";
import useEventReactions from "../../../hooks/use-event-reactions";
import { getEventUID } from "../../../helpers/nostr/events";
import useEventZaps from "../../../hooks/use-event-zaps";
export function NoteDetailsButton({
event,
...props
}: { event: NostrEvent } & Omit<IconButtonProps, "icon" | "aria-label">) {
const uuid = getEventUID(event);
const reactions = useEventReactions(uuid) ?? [];
const zaps = useEventZaps(uuid);
if (reactions.length === 0 && zaps.length === 0) return null;
return <IconButton icon={<InfoCircle />} aria-label="Note Details" title="Note Details" {...props} />;
}

View File

@ -17,10 +17,11 @@ import NostrPublishAction from "../../../classes/nostr-publish-action";
import { AddReactionIcon } from "../../icons";
import ReactionPicker from "../../reaction-picker";
import { draftEventReaction } from "../../../helpers/nostr/reactions";
import { getEventUID } from "../../../helpers/nostr/events";
export default function ReactionButton({ event, ...props }: { event: NostrEvent } & Omit<ButtonProps, "children">) {
const { requestSignature } = useSigningContext();
const reactions = useEventReactions(event.id) ?? [];
const reactions = useEventReactions(getEventUID(event)) ?? [];
const addReaction = async (emoji = "+", url?: string) => {
const draft = draftEventReaction(event, emoji, url);

View File

@ -48,6 +48,8 @@ import NoteCommunityMetadata from "./note-community-metadata";
import useSingleEvent from "../../hooks/use-single-event";
import { InlineNoteContent } from "./inline-note-content";
import NoteProxyLink from "./components/note-proxy-link";
import { NoteDetailsButton } from "./components/note-details-button";
import EventInteractionDetailsModal from "../event-interactions-modal";
export type NoteProps = Omit<CardProps, "children"> & {
event: NostrEvent;
@ -72,6 +74,7 @@ export const Note = React.memo(
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);
@ -80,9 +83,9 @@ export const Note = React.memo(
const refs = getReferences(event);
const repliedTo = useSingleEvent(refs.replyId);
const showReactionsOnNewLine = useBreakpointValue({ base: true, md: false });
const showReactionsOnNewLine = useBreakpointValue({ base: true, lg: false });
const reactionButtons = showReactions && <NoteReactions event={event} flexWrap="wrap" variant="ghost" size="xs" />;
const reactionButtons = showReactions && <NoteReactions event={event} flexWrap="wrap" variant="ghost" size="sm" />;
return (
<TrustProvider event={event}>
@ -126,7 +129,7 @@ export const Note = React.memo(
<CardFooter padding="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
{showReactionsOnNewLine && reactionButtons}
<Flex gap="2" w="full" alignItems="center">
<ButtonGroup size="xs" variant="ghost" isDisabled={account?.readonly ?? true}>
<ButtonGroup size="sm" variant="ghost" isDisabled={account?.readonly ?? true}>
{showReplyButton && (
<IconButton icon={<ReplyIcon />} aria-label="Reply" title="Reply" onClick={replyForm.onOpen} />
)}
@ -136,10 +139,12 @@ export const Note = React.memo(
</ButtonGroup>
{!showReactionsOnNewLine && reactionButtons}
<Box flexGrow={1} />
<NoteProxyLink event={event} size="xs" variant="ghost" />
<EventRelays event={event} />
<BookmarkButton event={event} aria-label="Bookmark note" size="xs" variant="ghost" />
<NoteMenu event={event} size="xs" variant="ghost" aria-label="More Options" />
<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>
@ -147,6 +152,7 @@ export const Note = React.memo(
{replyForm.isOpen && (
<ReplyForm item={{ event, replies: [], refs }} onCancel={replyForm.onClose} onSubmitted={replyForm.onClose} />
)}
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={event} />}
</TrustProvider>
);
},

View File

@ -9,7 +9,6 @@ import {
CopyToClipboardIcon,
CodeIcon,
ExternalLinkIcon,
LikeIcon,
MuteIcon,
RepostIcon,
TrashIcon,
@ -19,7 +18,6 @@ import {
import { getSharableEventAddress } from "../../helpers/nip19";
import { DraftNostrEvent, NostrEvent, isETag } from "../../types/nostr-event";
import { CustomMenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
import NoteReactionsModal from "./note-zaps-modal";
import NoteDebugModal from "../debug-modals/note-debug-modal";
import useCurrentAccount from "../../hooks/use-current-account";
import { buildAppSelectUrl } from "../../helpers/nostr/apps";
@ -34,6 +32,7 @@ import Translate01 from "../icons/translate-01";
import useUserPinList from "../../hooks/use-user-pin-list";
import { useSigningContext } from "../../providers/signing-provider";
import { PIN_LIST_KIND, listAddEvent, listRemoveEvent } from "../../helpers/nostr/lists";
import InfoCircle from "../icons/info-circle";
function PinNoteItem({ event }: { event: NostrEvent }) {
const toast = useToast();
@ -75,10 +74,13 @@ function PinNoteItem({ event }: { event: NostrEvent }) {
);
}
export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
export default function NoteMenu({
event,
detailsClick,
...props
}: { event: NostrEvent; detailsClick?: () => void } & Omit<MenuIconButtonProps, "children">) {
const account = useCurrentAccount();
const infoModal = useDisclosure();
const reactionsModal = useDisclosure();
const debugModal = useDisclosure();
const translationsModal = useDisclosure();
const { isMuted, mute, unmute } = useUserMuteFunctions(event.pubkey);
const { openModal } = useMuteModalContext();
@ -101,6 +103,11 @@ export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Om
return (
<>
<CustomMenuIconButton {...props}>
{detailsClick && (
<MenuItem onClick={detailsClick} icon={<InfoCircle />}>
Details
</MenuItem>
)}
{address && (
<MenuItem onClick={() => window.open(buildAppSelectUrl(address), "_blank")} icon={<ExternalLinkIcon />}>
View in app...
@ -135,20 +142,13 @@ export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Om
Broadcast
</MenuItem>
<PinNoteItem event={event} />
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
<MenuItem onClick={debugModal.onOpen} icon={<CodeIcon />}>
View Raw
</MenuItem>
<MenuItem onClick={reactionsModal.onOpen} icon={<LikeIcon />}>
Zaps/Reactions
</MenuItem>
</CustomMenuIconButton>
{infoModal.isOpen && (
<NoteDebugModal event={event} isOpen={infoModal.isOpen} onClose={infoModal.onClose} size="6xl" />
)}
{reactionsModal.isOpen && (
<NoteReactionsModal noteId={event.id} isOpen={reactionsModal.isOpen} onClose={reactionsModal.onClose} />
{debugModal.isOpen && (
<NoteDebugModal event={event} isOpen={debugModal.isOpen} onClose={debugModal.onClose} size="6xl" />
)}
{translationsModal.isOpen && <NoteTranslationModal isOpen onClose={translationsModal.onClose} note={event} />}

View File

@ -1,112 +0,0 @@
import React, { useState } from "react";
import {
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalCloseButton,
Button,
ModalProps,
Text,
Flex,
ButtonGroup,
Box,
} from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
import UserAvatarLink from "../user-avatar-link";
import { UserLink } from "../user-link";
import { DislikeIcon, LightningIcon, LikeIcon } from "../icons";
import { ParsedZap } from "../../helpers/nostr/zaps";
import { readablizeSats } from "../../helpers/bolt11";
import useEventReactions from "../../hooks/use-event-reactions";
import useEventZaps from "../../hooks/use-event-zaps";
import Timestamp from "../timestamp";
function getReactionIcon(content: string) {
switch (content) {
case "+":
return <LikeIcon />;
case "-":
return <DislikeIcon />;
default:
return content;
}
}
const ReactionEvent = React.memo(({ event }: { event: NostrEvent }) => (
<Flex gap="2">
<Text>{getReactionIcon(event.content)}</Text>
<Flex overflow="hidden" gap="2">
<UserAvatarLink pubkey={event.pubkey} size="xs" />
<UserLink pubkey={event.pubkey} />
</Flex>
<Text ml="auto" flexShrink={0}>
<Timestamp timestamp={event.created_at} />
</Text>
</Flex>
));
const ZapEvent = React.memo(({ zap }: { zap: ParsedZap }) => {
if (!zap.payment.amount) return null;
return (
<Box borderWidth="1px" borderRadius="lg" py="2" px={["2", "4"]}>
<Flex gap="2" justifyContent="space-between">
<Box>
<UserAvatarLink pubkey={zap.request.pubkey} size="xs" mr="2" />
<UserLink pubkey={zap.request.pubkey} />
</Box>
<Text fontWeight="bold">
{readablizeSats(zap.payment.amount / 1000)} <LightningIcon color="yellow.500" />
</Text>
</Flex>
<Text>{zap.request.content}</Text>
</Box>
);
});
function sortEvents(a: NostrEvent, b: NostrEvent) {
return b.created_at - a.created_at;
}
export default function NoteReactionsModal({
isOpen,
onClose,
noteId,
}: { noteId: string } & Omit<ModalProps, "children">) {
const zaps = useEventZaps(noteId, [], true) ?? [];
const reactions = useEventReactions(noteId, [], true) ?? [];
const [selected, setSelected] = useState("zaps");
return (
<Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<ModalBody p={["2", "4"]}>
<Flex direction="column" gap="2">
<ButtonGroup>
<Button size="sm" variant={selected === "zaps" ? "solid" : "outline"} onClick={() => setSelected("zaps")}>
Zaps ({zaps.length})
</Button>
<Button
size="sm"
variant={selected === "reactions" ? "solid" : "outline"}
onClick={() => setSelected("reactions")}
>
Reactions ({reactions.length})
</Button>
</ButtonGroup>
{selected === "reactions" &&
reactions.sort(sortEvents).map((event) => <ReactionEvent key={event.id} event={event} />)}
{selected === "zaps" &&
zaps
.sort((a, b) => b.request.created_at - a.request.created_at)
.map((zap) => <ZapEvent key={zap.request.id} zap={zap} />)}
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
}

View File

@ -1,83 +0,0 @@
import {
Box,
Button,
Divider,
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
ModalProps,
SimpleGrid,
SimpleGridProps,
useDisclosure,
} from "@chakra-ui/react";
import { useMemo } from "react";
import { NostrEvent } from "../types/nostr-event";
import { groupReactions } from "../helpers/nostr/reactions";
import UserAvatarLink from "./user-avatar-link";
import { UserLink } from "./user-link";
import ReactionIcon from "./event-reactions/reaction-icon";
export type ReactionDetailsModalProps = Omit<ModalProps, "children"> & {
reactions: NostrEvent[];
};
function ShowMoreGrid({
pubkeys,
cutoff,
...props
}: Omit<SimpleGridProps, "children"> & { pubkeys: string[]; cutoff: number }) {
const showMore = useDisclosure();
const limited = pubkeys.length > cutoff && !showMore.isOpen ? pubkeys.slice(0, cutoff) : pubkeys;
return (
<>
<SimpleGrid spacing="1" {...props}>
{limited.map((pubkey) => (
<Flex gap="2" key={pubkey} alignItems="center" overflow="hidden">
<UserAvatarLink pubkey={pubkey} size="xs" />
<UserLink pubkey={pubkey} isTruncated />
</Flex>
))}
</SimpleGrid>
{limited.length !== pubkeys.length && (
<Button variant="link" size="md" onClick={showMore.onOpen}>
Show {pubkeys.length - limited.length} more
</Button>
)}
</>
);
}
export default function ReactionDetailsModal({ reactions, onClose, ...props }: ReactionDetailsModalProps) {
const groups = useMemo(() => groupReactions(reactions), [reactions]);
return (
<Modal onClose={onClose} size="2xl" {...props}>
<ModalOverlay />
<ModalContent>
<ModalHeader px="4" pb="0">
Reactions
</ModalHeader>
<ModalCloseButton />
<ModalBody display="flex" gap="2" px="4" pt="0" flexDirection="column">
{groups.map((group) => (
<Flex key={group.emoji} direction="column" gap="2">
<Flex gap="2" alignItems="center">
<Box fontSize="lg" borderWidth={1} w="8" h="8" borderRadius="md" p="1">
<ReactionIcon emoji={group.emoji} url={group.url} />
</Box>
<Divider />
</Flex>
<ShowMoreGrid pubkeys={group.pubkeys} columns={{ base: 2, sm: 3, md: 4 }} cutoff={12} />
</Flex>
))}
</ModalBody>
</ModalContent>
</Modal>
);
}

View File

@ -28,7 +28,7 @@ function EmojiPack({ cord, onSelect }: { cord: string; onSelect: ReactionPickerP
icon={<Image src={emoji.url} height="1.2rem" />}
aria-label={emoji.name}
title={emoji.name}
variant="outline"
variant="ghost"
size="sm"
onClick={() => onSelect(emoji.name, emoji.url)}
/>
@ -46,11 +46,11 @@ export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
return (
<Flex direction="column" gap="2">
<Flex wrap="wrap" gap="2">
<IconButton icon={<LikeIcon />} aria-label="Like" variant="outline" size="sm" onClick={() => onSelect("+")} />
<IconButton icon={<LikeIcon />} aria-label="Like" variant="ghost" size="sm" onClick={() => onSelect("+")} />
<IconButton
icon={<DislikeIcon />}
aria-label="Dislike"
variant="outline"
variant="ghost"
size="sm"
onClick={() => onSelect("-")}
/>
@ -58,7 +58,7 @@ export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
<IconButton
icon={<span>{emoji}</span>}
aria-label="Shaka"
variant="outline"
variant="ghost"
size="sm"
onClick={() => onSelect(emoji)}
/>

View File

@ -32,6 +32,7 @@ export function createCoordinate(kind: number, pubkey: string, d?: string) {
return `${kind}:${pubkey}${d ? ":" + d : ""}`;
}
/** This class is ued to batch requests to a single relay */
class ReplaceableEventRelayLoader {
private subscription: NostrSubscription;
private events = new SuperMap<Pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());

View File

@ -52,7 +52,7 @@ function HomePage() {
});
const header = (
<Flex gap="2" wrap="wrap" px={["2", 0]} alignItems="center">
<Flex gap="2" wrap="wrap" alignItems="center">
<PeopleListSelection />
<NoteFilterTypeButtons showReplies={showReplies} showReposts={showReposts} />
<RelaySelectionButton ml="auto" />

View File

@ -1,4 +1,4 @@
import { useState } from "react";
import { memo, useState } from "react";
import {
Alert,
AlertIcon,
@ -37,6 +37,8 @@ import BookmarkButton from "../../../components/note/components/bookmark-button"
import NoteCommunityMetadata from "../../../components/note/note-community-metadata";
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
import NoteProxyLink from "../../../components/note/components/note-proxy-link";
import { NoteDetailsButton } from "../../../components/note/components/note-details-button";
import EventInteractionDetailsModal from "../../../components/event-interactions-modal";
const LEVEL_COLORS = ["green", "blue", "red", "purple", "yellow", "cyan", "pink"];
@ -47,11 +49,12 @@ export type ThreadItemProps = {
level?: number;
};
export const ThreadPost = ({ post, initShowReplies, focusId, level = -1 }: ThreadItemProps) => {
export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }: ThreadItemProps) => {
const { showReactions } = useSubject(appSettings);
const [expanded, setExpanded] = useState(initShowReplies ?? (level < 2 || post.replies.length <= 1));
const toggle = () => setExpanded((v) => !v);
const showReplyForm = useDisclosure();
const replyForm = useDisclosure();
const detailsModal = useDisclosure();
const muteFilter = useClientSideMuteFilter();
@ -114,14 +117,14 @@ export const ThreadPost = ({ post, initShowReplies, focusId, level = -1 }: Threa
);
};
const showReactionsOnNewLine = useBreakpointValue({ base: true, md: false });
const showReactionsOnNewLine = useBreakpointValue({ base: true, lg: false });
const reactionButtons = showReactions && (
<NoteReactions event={post.event} flexWrap="wrap" variant="ghost" size="sm" />
);
const footer = (
<Flex gap="2" alignItems="center">
<ButtonGroup variant="ghost" size="sm">
<IconButton aria-label="Reply" title="Reply" onClick={showReplyForm.onToggle} icon={<ReplyIcon />} />
<IconButton aria-label="Reply" title="Reply" onClick={replyForm.onToggle} icon={<ReplyIcon />} />
<RepostButton event={post.event} />
<QuoteRepostButton event={post.event} />
@ -129,9 +132,12 @@ export const ThreadPost = ({ post, initShowReplies, focusId, level = -1 }: Threa
</ButtonGroup>
{!showReactionsOnNewLine && reactionButtons}
<Spacer />
<NoteProxyLink event={post.event} variant="ghost" size="sm" />
<BookmarkButton event={post.event} variant="ghost" aria-label="Bookmark" size="sm" />
<NoteMenu event={post.event} variant="ghost" size="sm" aria-label="More Options" />
<ButtonGroup size="sm" variant="ghost">
<NoteProxyLink event={post.event} />
<NoteDetailsButton event={post.event} onClick={detailsModal.onOpen} />
<BookmarkButton event={post.event} aria-label="Bookmark" />
<NoteMenu event={post.event} aria-label="More Options" detailsClick={detailsModal.onOpen} />
</ButtonGroup>
</Flex>
);
@ -151,9 +157,7 @@ export const ThreadPost = ({ post, initShowReplies, focusId, level = -1 }: Threa
{expanded && showReactionsOnNewLine && reactionButtons}
{expanded && footer}
</Flex>
{showReplyForm.isOpen && (
<ReplyForm item={post} onCancel={showReplyForm.onClose} onSubmitted={showReplyForm.onClose} />
)}
{replyForm.isOpen && <ReplyForm item={post} onCancel={replyForm.onClose} onSubmitted={replyForm.onClose} />}
{post.replies.length > 0 && expanded && (
<Flex direction="column" gap="2" pl={{ base: 2, md: 4 }}>
{post.replies.map((child) => (
@ -161,6 +165,7 @@ export const ThreadPost = ({ post, initShowReplies, focusId, level = -1 }: Threa
))}
</Flex>
)}
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={post.event} />}
</>
);
};
});