Add details tabs under thread post

Show individual zaps on notes
This commit is contained in:
hzrd149
2024-04-02 17:07:22 -05:00
parent fbcfa42740
commit 4c3d04140e
19 changed files with 364 additions and 277 deletions

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Show individual zaps on notes

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add details tabs under thread post

View File

@@ -1,104 +0,0 @@
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/user-avatar-link";
import UserLink from "../user/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/event";
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

@@ -1,33 +0,0 @@
import { Flex, Text } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { NostrEvent } from "../../types/nostr-event";
import UserAvatarLink from "../user/user-avatar-link";
import UserLink from "../user/user-link";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelays } from "../../hooks/use-client-relays";
import useSubject from "../../hooks/use-subject";
import Timestamp from "../timestamp";
export default function RepostDetails({ event }: { event: NostrEvent }) {
const readRelays = useReadRelays();
const timeline = useTimelineLoader(`${event.id}-reposts`, readRelays, {
kinds: [kinds.Repost, kinds.GenericRepost],
"#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

@@ -7,7 +7,6 @@ import { NostrEvent } from "../../types/nostr-event";
import { DotsMenuButton, MenuIconButtonProps } from "../dots-menu-button";
import NoteTranslationModal from "../../views/tools/transform-note/translation";
import Translate01 from "../icons/translate-01";
import InfoCircle from "../icons/info-circle";
import PinNoteMenuItem from "../common-menu-items/pin-note";
import CopyShareLinkMenuItem from "../common-menu-items/copy-share-link";
import OpenInAppMenuItem from "../common-menu-items/open-in-app";
@@ -19,11 +18,7 @@ import Recording02 from "../icons/recording-02";
import { usePublishEvent } from "../../providers/global/publish-provider";
import DebugEventMenuItem from "../debug-modal/debug-event-menu-item";
export default function NoteMenu({
event,
detailsClick,
...props
}: { event: NostrEvent; detailsClick?: () => void } & Omit<MenuIconButtonProps, "children">) {
export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
const translationsModal = useDisclosure();
const publish = usePublishEvent();
@@ -59,11 +54,6 @@ export default function NoteMenu({
Broadcast
</MenuItem>
<PinNoteMenuItem event={event} />
{detailsClick && (
<MenuItem onClick={detailsClick} icon={<InfoCircle />}>
Details
</MenuItem>
)}
<DebugEventMenuItem event={event} />
</DotsMenuButton>

View File

@@ -1,20 +0,0 @@
import { IconButton, IconButtonProps } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import InfoCircle from "../../../icons/info-circle";
import useEventReactions from "../../../../hooks/use-event-reactions";
import { getEventUID } from "../../../../helpers/nostr/event";
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

@@ -0,0 +1,58 @@
import { NostrEvent, nip19 } from "nostr-tools";
import { Flex, Link, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { getThreadReferences, truncatedId } from "../../../../helpers/nostr/event";
import UserLink from "../../../user/user-link";
import useSingleEvent from "../../../../hooks/use-single-event";
import { CompactNoteContent } from "../../../compact-note-content";
import { ReplyIcon } from "../../../icons";
function ReplyToE({ pointer }: { pointer: nip19.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: nip19.AddressPointer }) {
const naddr = nip19.naddrEncode(pointer);
return (
<Text>
Replying to{" "}
<Link as={RouterLink} to={`/l/${naddr}`} color="blue.500">
{truncatedId(naddr)}
</Link>
</Text>
);
}
export default function ReplyContext({ event }: { event: NostrEvent }) {
const refs = getThreadReferences(event);
if (!refs.reply) return null;
return (
<Flex gap="2" fontStyle="italic" alignItems="center" whiteSpace="nowrap">
<ReplyIcon />
{refs.reply.e ? <ReplyToE pointer={refs.reply.e} /> : <ReplyToA pointer={refs.reply.a} />}
</Flex>
);
}

View File

@@ -0,0 +1,37 @@
import { Flex, Tag, TagLabel } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { getEventUID } from "nostr-idb";
import styled from "@emotion/styled";
import useEventZaps from "../../../../hooks/use-event-zaps";
import UserAvatar from "../../../user/user-avatar";
import { readablizeSats } from "../../../../helpers/bolt11";
import { LightningIcon } from "../../../icons";
const HiddenScrollbar = styled(Flex)`
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
display: none;
}
`;
export default function ZapBubbles({ event }: { event: NostrEvent }) {
const zaps = useEventZaps(getEventUID(event));
if (zaps.length === 0) return null;
const sorted = zaps.sort((a, b) => (b.payment.amount ?? 0) - (a.payment.amount ?? 0));
return (
<HiddenScrollbar overflowY="hidden" overflowX="auto" gap="2">
{sorted.map((zap) => (
<Tag borderRadius="full" py="1" flexShrink={0} variant="outline">
<LightningIcon mr="1" />
<TagLabel fontWeight="bold">{readablizeSats((zap.payment.amount ?? 0) / 1000)}</TagLabel>
<UserAvatar pubkey={zap.request.pubkey} size="xs" square={false} ml="2" />
</Tag>
))}
</HiddenScrollbar>
);
}

View File

@@ -11,7 +11,6 @@ import {
IconButton,
Link,
LinkBox,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
@@ -35,73 +34,20 @@ import BookmarkButton from "../bookmark-button";
import useCurrentAccount from "../../../hooks/use-current-account";
import NoteReactions from "./components/note-reactions";
import ReplyForm from "../../../views/thread/components/reply-form";
import { getThreadReferences, truncatedId } from "../../../helpers/nostr/event";
import { getThreadReferences } from "../../../helpers/nostr/event";
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";
import POWIcon from "../../pow/pow-icon";
import ReplyContext from "./components/reply-context";
import ZapBubbles from "./components/zap-bubbles";
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 = getThreadReferences(event);
if (!refs.reply) return null;
return (
<Flex gap="2" fontStyle="italic" alignItems="center" whiteSpace="nowrap">
<ReplyIcon />
{refs.reply.e ? <ReplyToE pointer={refs.reply.e} /> : <ReplyToA pointer={refs.reply.a} />}
</Flex>
);
}
export type NoteProps = Omit<CardProps, "children"> & {
export type TimelineNoteProps = Omit<CardProps, "children"> & {
event: NostrEvent;
variant?: CardProps["variant"];
showReplyButton?: boolean;
@@ -119,11 +65,10 @@ export function TimelineNote({
registerIntersectionEntity = true,
clickable = true,
...props
}: NoteProps) {
}: TimelineNoteProps) {
const account = useCurrentAccount();
const { showReactions, showSignatureVerification } = useSubject(appSettings);
const replyForm = useDisclosure();
const detailsModal = useDisclosure();
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, event.id);
@@ -169,12 +114,13 @@ export function TimelineNote({
)}
</Flex>
<NoteCommunityMetadata event={event} />
{showReplyLine && <ReplyLine event={event} />}
{showReplyLine && <ReplyContext event={event} />}
</CardHeader>
<CardBody p="0">
<NoteContentWithWarning event={event} />
</CardBody>
<CardFooter padding="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
<ZapBubbles event={event} />
{showReactionsOnNewLine && reactionButtons}
<Flex gap="2" w="full" alignItems="center">
<ButtonGroup size="sm" variant="ghost" isDisabled={account?.readonly ?? true}>
@@ -189,9 +135,8 @@ export function TimelineNote({
<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} />
<NoteMenu event={event} aria-label="More Options" />
</ButtonGroup>
</Flex>
</CardFooter>
@@ -204,7 +149,6 @@ export function TimelineNote({
onSubmitted={replyForm.onClose}
/>
)}
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={event} />}
</TrustProvider>
);
}

View File

@@ -41,7 +41,7 @@ export const UserAvatar = forwardRef<HTMLDivElement, UserAvatarProps>(
metadata={metadata}
noProxy={noProxy}
ref={ref}
borderColor={color}
borderColor={size !== "xs" ? color : undefined}
borderStyle="none"
size={size}
{...props}
@@ -63,7 +63,7 @@ export const UserAvatar = forwardRef<HTMLDivElement, UserAvatarProps>(
);
UserAvatar.displayName = "UserAvatar";
const StyledAvatar = styled(Avatar)`
const SquareAvatar = styled(Avatar)`
img {
border-radius: var(--chakra-radii-lg);
border-width: 0.18rem;
@@ -75,9 +75,10 @@ export type MetadataAvatarProps = Omit<AvatarProps, "src"> & {
metadata?: Kind0ParsedContent;
pubkey?: string;
noProxy?: boolean;
square?: boolean;
};
export const MetadataAvatar = forwardRef<HTMLDivElement, MetadataAvatarProps>(
({ pubkey, metadata, noProxy, children, ...props }, ref) => {
({ pubkey, metadata, noProxy, children, square = true, ...props }, ref) => {
const { imageProxy, proxyUserMedia, hideUsernames } = useAppSettings();
const account = useCurrentAccount();
const picture = useMemo(() => {
@@ -95,8 +96,10 @@ export const MetadataAvatar = forwardRef<HTMLDivElement, MetadataAvatarProps>(
}
}, [metadata?.picture, imageProxy, proxyUserMedia, hideUsernames, account]);
const AvatarComponent = square ? SquareAvatar : Avatar;
return (
<StyledAvatar
<AvatarComponent
src={picture}
icon={pubkey ? <UserIdenticon pubkey={pubkey} /> : undefined}
// overflow="hidden"
@@ -105,10 +108,9 @@ export const MetadataAvatar = forwardRef<HTMLDivElement, MetadataAvatarProps>(
{...props}
>
{children}
</StyledAvatar>
</AvatarComponent>
);
},
);
UserAvatar.displayName = "UserAvatar";
export default memo(UserAvatar);

View File

@@ -0,0 +1,93 @@
import { useState } from "react";
import { Button, Flex } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { getEventUID } from "nostr-idb";
import { ThreadItem } from "../../../helpers/thread";
import useEventCount from "../../../hooks/use-event-count";
import PostZapsTab from "./tabs/zaps";
import { ThreadPost } from "./thread-post";
import useEventZaps from "../../../hooks/use-event-zaps";
import PostReactionsTab from "./tabs/reactions";
import useEventReactions from "../../../hooks/use-event-reactions";
import PostRepostsTab from "./tabs/reposts";
import PostQuotesTab from "./tabs/quotes";
export default function DetailsTabs({ post }: { post: ThreadItem }) {
const [selected, setSelected] = useState("replies");
const repostCount = useEventCount({ "#e": [post.event.id], kinds: [kinds.Repost, kinds.GenericRepost] });
const zaps = useEventZaps(getEventUID(post.event));
const reactions = useEventReactions(getEventUID(post.event)) ?? [];
const renderContent = () => {
switch (selected) {
case "replies":
return (
<Flex direction="column" gap="2" pl={{ base: 2, md: 4 }}>
{post.replies.map((child) => (
<ThreadPost key={child.event.id} post={child} focusId={undefined} level={0} />
))}
</Flex>
);
case "quotes":
return <PostQuotesTab post={post} />;
case "reactions":
return <PostReactionsTab post={post} />;
case "reposts":
return <PostRepostsTab post={post} />;
case "zaps":
return <PostZapsTab post={post} />;
}
return null;
};
return (
<>
<Flex gap="4" px="2" overflowX="auto">
<Button
size="sm"
flexShrink={0}
variant={selected === "replies" ? "solid" : "outline"}
onClick={() => setSelected("replies")}
>
Replies{post.replies.length > 0 ? ` (${post.replies.length})` : ""}
</Button>
<Button
size="sm"
flexShrink={0}
variant={selected === "quotes" ? "solid" : "outline"}
onClick={() => setSelected("quotes")}
>
Quotes
</Button>
<Button
size="sm"
flexShrink={0}
variant={selected === "reposts" ? "solid" : "outline"}
onClick={() => setSelected("reposts")}
>
Reposts{repostCount && repostCount > 0 ? ` (${repostCount})` : ""}
</Button>
<Button
size="sm"
flexShrink={0}
variant={selected === "zaps" ? "solid" : "outline"}
onClick={() => setSelected("zaps")}
>
Zaps{zaps.length > 0 ? ` (${zaps.length})` : ""}
</Button>
<Button
size="sm"
flexShrink={0}
variant={selected === "reactions" ? "solid" : "outline"}
onClick={() => setSelected("reactions")}
>
Reactions{reactions.length > 0 ? ` (${reactions.length})` : ""}
</Button>
</Flex>
{renderContent()}
</>
);
}

View File

@@ -0,0 +1,30 @@
import { kinds } from "nostr-tools";
import { Flex } from "@chakra-ui/react";
import { ThreadItem } from "../../../../helpers/thread";
import { useReadRelays } from "../../../../hooks/use-client-relays";
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
import useSubject from "../../../../hooks/use-subject";
import { getContentTagRefs } from "../../../../helpers/nostr/event";
import { TimelineNote } from "../../../../components/note/timeline-note";
export default function PostQuotesTab({ post }: { post: ThreadItem }) {
const readRelays = useReadRelays();
const timeline = useTimelineLoader(`${post.event.id}-quotes`, readRelays, {
kinds: [kinds.ShortTextNote],
"#e": [post.event.id],
});
const events = useSubject(timeline.timeline);
const quotes = events.filter((e) => {
return getContentTagRefs(e.content, e.tags).some((t) => t[0] === "e" && t[1] === post.event.id);
});
return (
<Flex gap="2" direction="column">
{quotes.map((quote) => (
<TimelineNote key={quote.id} event={quote} />
))}
</Flex>
);
}

View File

@@ -1,11 +1,13 @@
import { Box, Button, Divider, Flex, SimpleGrid, SimpleGridProps, useDisclosure } from "@chakra-ui/react";
import { useMemo } from "react";
import { getEventUID } from "nostr-idb";
import { Box, Button, Divider, Flex, SimpleGrid, SimpleGridProps, useDisclosure } from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
import { groupReactions } from "../../helpers/nostr/reactions";
import UserAvatarLink from "../user/user-avatar-link";
import UserLink from "../user/user-link";
import ReactionIcon from "../event-reactions/reaction-icon";
import useEventReactions from "../../../../hooks/use-event-reactions";
import { ThreadItem } from "../../../../helpers/thread";
import { groupReactions } from "../../../../helpers/nostr/reactions";
import ReactionIcon from "../../../../components/event-reactions/reaction-icon";
import UserLink from "../../../../components/user/user-link";
import UserAvatarLink from "../../../../components/user/user-avatar-link";
function ShowMoreGrid({
pubkeys,
@@ -19,9 +21,9 @@ function ShowMoreGrid({
<>
<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 gap="2" key={pubkey} alignItems="center">
<UserAvatarLink pubkey={pubkey} size="sm" />
<UserLink pubkey={pubkey} isTruncated fontWeight="bold" />
</Flex>
))}
</SimpleGrid>
@@ -34,20 +36,22 @@ function ShowMoreGrid({
);
}
export default function ReactionDetails({ reactions }: { reactions: NostrEvent[] }) {
export default function PostReactionsTab({ post }: { post: ThreadItem }) {
const reactions = useEventReactions(getEventUID(post.event)) ?? [];
const groups = useMemo(() => groupReactions(reactions), [reactions]);
return (
<Flex gap="2" direction="column">
<Flex gap="2" direction="column" px="2">
{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">
<Box fontSize="xl" borderWidth={1} w="10" h="10" borderRadius="md" p="2" flexShrink={0}>
<ReactionIcon emoji={group.emoji} url={group.url} />
</Box>
{group.url ? group.emoji : ""}
<Divider />
</Flex>
<ShowMoreGrid pubkeys={group.pubkeys} columns={{ base: 2, sm: 3, md: 4 }} cutoff={12} />
<ShowMoreGrid pubkeys={group.pubkeys} columns={{ base: 1, sm: 2, md: 4, lg: 5, xl: 6 }} cutoff={12} />
</Flex>
))}
</Flex>

View File

@@ -0,0 +1,33 @@
import { Flex, Text } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import UserAvatarLink from "../../../../components/user/user-avatar-link";
import UserLink from "../../../../components/user/user-link";
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
import { useReadRelays } from "../../../../hooks/use-client-relays";
import useSubject from "../../../../hooks/use-subject";
import Timestamp from "../../../../components/timestamp";
import { ThreadItem } from "../../../../helpers/thread";
export default function PostRepostsTab({ post }: { post: ThreadItem }) {
const readRelays = useReadRelays();
const timeline = useTimelineLoader(`${post.event.id}-reposts`, readRelays, {
kinds: [kinds.Repost, kinds.GenericRepost],
"#e": [post.event.id],
});
const reposts = useSubject(timeline.timeline);
return (
<Flex direction="column" gap="2" px="2">
{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>
))}
</Flex>
);
}

View File

@@ -0,0 +1,44 @@
import { memo } from "react";
import { Box, Flex, Text } from "@chakra-ui/react";
import { ThreadItem } from "../../../../helpers/thread";
import { ParsedZap } from "../../../../helpers/nostr/zaps";
import UserAvatarLink from "../../../../components/user/user-avatar-link";
import UserLink from "../../../../components/user/user-link";
import Timestamp from "../../../../components/timestamp";
import { LightningIcon } from "../../../../components/icons";
import { readablizeSats } from "../../../../helpers/bolt11";
import useEventZaps from "../../../../hooks/use-event-zaps";
import { getEventUID } from "nostr-idb";
const ZapEvent = memo(({ zap }: { zap: ParsedZap }) => {
if (!zap.payment.amount) return null;
return (
<>
<Flex gap="2">
<UserAvatarLink pubkey={zap.request.pubkey} size="sm" />
<Box>
<UserLink pubkey={zap.request.pubkey} fontWeight="bold" />
<Text>
<LightningIcon color="yellow.500" /> {readablizeSats(zap.payment.amount / 1000)}
</Text>
</Box>
<Timestamp timestamp={zap.event.created_at} />
</Flex>
{zap.request.content && <Text>{zap.request.content}</Text>}
</>
);
});
export default function PostZapsTab({ post }: { post: ThreadItem }) {
const zaps = useEventZaps(getEventUID(post.event));
return (
<Flex px="2" direction="column" gap="2" mb="2">
{zaps.map((zap) => (
<ZapEvent key={zap.event.id} zap={zap} />
))}
</Flex>
);
}

View File

@@ -14,7 +14,6 @@ import Expand01 from "../../../components/icons/expand-01";
import Minus from "../../../components/icons/minus";
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
import UserDnsIdentity from "../../../components/user/user-dns-identity";
import EventInteractionDetailsModal from "../../../components/event-interactions-modal";
import { getSharableEventAddress } from "../../../helpers/nip19";
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
import useAppSettings from "../../../hooks/use-app-settings";
@@ -24,12 +23,13 @@ import RepostButton from "../../../components/note/timeline-note/components/repo
import QuoteRepostButton from "../../../components/note/quote-repost-button";
import NoteZapButton from "../../../components/note/note-zap-button";
import NoteProxyLink from "../../../components/note/timeline-note/components/note-proxy-link";
import { NoteDetailsButton } from "../../../components/note/timeline-note/components/note-details-button";
import BookmarkButton from "../../../components/note/bookmark-button";
import NoteMenu from "../../../components/note/note-menu";
import NoteCommunityMetadata from "../../../components/note/timeline-note/note-community-metadata";
import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents";
import NoteReactions from "../../../components/note/timeline-note/components/note-reactions";
import ZapBubbles from "../../../components/note/timeline-note/components/zap-bubbles";
import DetailsTabs from "./details-tabs";
export type ThreadItemProps = {
post: ThreadItem;
@@ -42,7 +42,6 @@ export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }:
const { showReactions } = useAppSettings();
const expanded = useDisclosure({ defaultIsOpen: initShowReplies ?? (level < 2 || post.replies.length <= 1) });
const replyForm = useDisclosure();
const detailsModal = useDisclosure();
const muteFilter = useClientSideMuteFilter();
@@ -117,9 +116,8 @@ export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }:
<Spacer />
<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} />
<NoteMenu event={post.event} aria-label="More Options" />
</ButtonGroup>
</Flex>
);
@@ -141,19 +139,28 @@ export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }:
ref={ref}
>
{header}
{expanded.isOpen && renderContent()}
{expanded.isOpen && showReactionsOnNewLine && reactionButtons}
{expanded.isOpen && footer}
{expanded.isOpen && (
<>
{renderContent()}
<ZapBubbles event={post.event} />
{showReactionsOnNewLine && reactionButtons}
{footer}
</>
)}
</Flex>
{replyForm.isOpen && <ReplyForm item={post} onCancel={replyForm.onClose} onSubmitted={replyForm.onClose} />}
{post.replies.length > 0 && expanded.isOpen && (
<Flex direction="column" gap="2" pl={{ base: 2, md: 4 }}>
{post.replies.map((child) => (
<ThreadPost key={child.event.id} post={child} focusId={focusId} level={level + 1} />
))}
</Flex>
{level === -1 ? (
<DetailsTabs post={post} />
) : (
expanded.isOpen &&
post.replies.length > 0 && (
<Flex direction="column" gap="2" pl={{ base: 2, md: 4 }}>
{post.replies.map((child) => (
<ThreadPost key={child.event.id} post={child} focusId={focusId} level={level + 1} />
))}
</Flex>
)
)}
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={post.event} />}
</>
);
});

View File

@@ -9,9 +9,8 @@ import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu
export default function TorrentCommentMenu({
comment,
detailsClick,
...props
}: { comment: NostrEvent; detailsClick?: () => void } & Omit<MenuIconButtonProps, "children">) {
}: { comment: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
return (
<>
<DotsMenuButton {...props}>

View File

@@ -31,7 +31,6 @@ import Expand01 from "../../../components/icons/expand-01";
import { TrustProvider } from "../../../providers/local/trust";
import { ReplyIcon } from "../../../components/icons";
import ReplyForm from "../../thread/components/reply-form";
import EventInteractionDetailsModal from "../../../components/event-interactions-modal";
import useThreadColorLevelProps from "../../../hooks/use-thread-color-level-props";
import TorrentCommentMenu from "./torrent-comment-menu";
import NoteReactions from "../../../components/note/timeline-note/components/note-reactions";
@@ -42,7 +41,6 @@ export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level?
const { showReactions } = useAppSettings();
const expanded = useDisclosure({ defaultIsOpen: level < 2 || post.replies.length <= 1 });
const replyForm = useDisclosure();
const detailsModal = useDisclosure();
const muteFilter = useClientSideMuteFilter();
@@ -110,7 +108,7 @@ export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level?
{!showReactionsOnNewLine && reactionButtons}
<Spacer />
<ButtonGroup size="sm" variant="ghost">
<TorrentCommentMenu comment={post.event} aria-label="More Options" detailsClick={detailsModal.onOpen} />
<TorrentCommentMenu comment={post.event} aria-label="More Options" />
</ButtonGroup>
</Flex>
);
@@ -151,7 +149,6 @@ export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level?
))}
</Flex>
)}
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={post.event} />}
</>
);
});

View File

@@ -6,11 +6,7 @@ import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-em
import MuteUserMenuItem from "../../../components/common-menu-items/mute-user";
import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item";
export default function TrackMenu({
track,
detailsClick,
...props
}: { track: NostrEvent; detailsClick?: () => void } & Omit<MenuIconButtonProps, "children">) {
export default function TrackMenu({ track, ...props }: { track: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
return (
<>
<DotsMenuButton {...props}>