mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-07 03:18:02 +02:00
Show reposts in note details modal
This commit is contained in:
parent
cf1ab354ad
commit
b372edab27
5
.changeset/sixty-comics-search.md
Normal file
5
.changeset/sixty-comics-search.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Show reposts in note details modal
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -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"]
|
||||
}
|
||||
|
104
src/components/event-interactions-modal/index.tsx
Normal file
104
src/components/event-interactions-modal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
55
src/components/event-interactions-modal/reaction-details.tsx
Normal file
55
src/components/event-interactions-modal/reaction-details.tsx
Normal 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>
|
||||
);
|
||||
}
|
30
src/components/event-interactions-modal/repost-details.tsx
Normal file
30
src/components/event-interactions-modal/repost-details.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
20
src/components/note/components/note-details-button.tsx
Normal file
20
src/components/note/components/note-details-button.tsx
Normal 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} />;
|
||||
}
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
@ -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} />}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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)}
|
||||
/>
|
||||
|
@ -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>());
|
||||
|
@ -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" />
|
||||
|
@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user