mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-28 04:27:35 +02:00
Add details tabs under thread post
Show individual zaps on notes
This commit is contained in:
5
.changeset/early-shoes-admire.md
Normal file
5
.changeset/early-shoes-admire.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Show individual zaps on notes
|
5
.changeset/famous-walls-warn.md
Normal file
5
.changeset/famous-walls-warn.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add details tabs under thread post
|
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -7,7 +7,6 @@ import { NostrEvent } from "../../types/nostr-event";
|
|||||||
import { DotsMenuButton, MenuIconButtonProps } from "../dots-menu-button";
|
import { DotsMenuButton, MenuIconButtonProps } from "../dots-menu-button";
|
||||||
import NoteTranslationModal from "../../views/tools/transform-note/translation";
|
import NoteTranslationModal from "../../views/tools/transform-note/translation";
|
||||||
import Translate01 from "../icons/translate-01";
|
import Translate01 from "../icons/translate-01";
|
||||||
import InfoCircle from "../icons/info-circle";
|
|
||||||
import PinNoteMenuItem from "../common-menu-items/pin-note";
|
import PinNoteMenuItem from "../common-menu-items/pin-note";
|
||||||
import CopyShareLinkMenuItem from "../common-menu-items/copy-share-link";
|
import CopyShareLinkMenuItem from "../common-menu-items/copy-share-link";
|
||||||
import OpenInAppMenuItem from "../common-menu-items/open-in-app";
|
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 { usePublishEvent } from "../../providers/global/publish-provider";
|
||||||
import DebugEventMenuItem from "../debug-modal/debug-event-menu-item";
|
import DebugEventMenuItem from "../debug-modal/debug-event-menu-item";
|
||||||
|
|
||||||
export default function NoteMenu({
|
export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
event,
|
|
||||||
detailsClick,
|
|
||||||
...props
|
|
||||||
}: { event: NostrEvent; detailsClick?: () => void } & Omit<MenuIconButtonProps, "children">) {
|
|
||||||
const translationsModal = useDisclosure();
|
const translationsModal = useDisclosure();
|
||||||
const publish = usePublishEvent();
|
const publish = usePublishEvent();
|
||||||
|
|
||||||
@@ -59,11 +54,6 @@ export default function NoteMenu({
|
|||||||
Broadcast
|
Broadcast
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<PinNoteMenuItem event={event} />
|
<PinNoteMenuItem event={event} />
|
||||||
{detailsClick && (
|
|
||||||
<MenuItem onClick={detailsClick} icon={<InfoCircle />}>
|
|
||||||
Details
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<DebugEventMenuItem event={event} />
|
<DebugEventMenuItem event={event} />
|
||||||
</DotsMenuButton>
|
</DotsMenuButton>
|
||||||
|
|
||||||
|
@@ -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} />;
|
|
||||||
}
|
|
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
37
src/components/note/timeline-note/components/zap-bubbles.tsx
Normal file
37
src/components/note/timeline-note/components/zap-bubbles.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -11,7 +11,6 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Link,
|
Link,
|
||||||
LinkBox,
|
LinkBox,
|
||||||
Text,
|
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
@@ -35,73 +34,20 @@ import BookmarkButton from "../bookmark-button";
|
|||||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||||
import NoteReactions from "./components/note-reactions";
|
import NoteReactions from "./components/note-reactions";
|
||||||
import ReplyForm from "../../../views/thread/components/reply-form";
|
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 Timestamp from "../../timestamp";
|
||||||
import OpenInDrawerButton from "../open-in-drawer-button";
|
import OpenInDrawerButton from "../open-in-drawer-button";
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||||
import HoverLinkOverlay from "../../hover-link-overlay";
|
import HoverLinkOverlay from "../../hover-link-overlay";
|
||||||
import NoteCommunityMetadata from "./note-community-metadata";
|
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 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 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 POWIcon from "../../pow/pow-icon";
|
||||||
|
import ReplyContext from "./components/reply-context";
|
||||||
|
import ZapBubbles from "./components/zap-bubbles";
|
||||||
|
|
||||||
function ReplyToE({ pointer }: { pointer: EventPointer }) {
|
export type TimelineNoteProps = Omit<CardProps, "children"> & {
|
||||||
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"> & {
|
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
variant?: CardProps["variant"];
|
variant?: CardProps["variant"];
|
||||||
showReplyButton?: boolean;
|
showReplyButton?: boolean;
|
||||||
@@ -119,11 +65,10 @@ export function TimelineNote({
|
|||||||
registerIntersectionEntity = true,
|
registerIntersectionEntity = true,
|
||||||
clickable = true,
|
clickable = true,
|
||||||
...props
|
...props
|
||||||
}: NoteProps) {
|
}: TimelineNoteProps) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const { showReactions, showSignatureVerification } = useSubject(appSettings);
|
const { showReactions, showSignatureVerification } = useSubject(appSettings);
|
||||||
const replyForm = useDisclosure();
|
const replyForm = useDisclosure();
|
||||||
const detailsModal = useDisclosure();
|
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useRegisterIntersectionEntity(ref, event.id);
|
useRegisterIntersectionEntity(ref, event.id);
|
||||||
@@ -169,12 +114,13 @@ export function TimelineNote({
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<NoteCommunityMetadata event={event} />
|
<NoteCommunityMetadata event={event} />
|
||||||
{showReplyLine && <ReplyLine event={event} />}
|
{showReplyLine && <ReplyContext event={event} />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody p="0">
|
<CardBody p="0">
|
||||||
<NoteContentWithWarning event={event} />
|
<NoteContentWithWarning event={event} />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter padding="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
|
<CardFooter padding="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
|
||||||
|
<ZapBubbles event={event} />
|
||||||
{showReactionsOnNewLine && reactionButtons}
|
{showReactionsOnNewLine && reactionButtons}
|
||||||
<Flex gap="2" w="full" alignItems="center">
|
<Flex gap="2" w="full" alignItems="center">
|
||||||
<ButtonGroup size="sm" variant="ghost" isDisabled={account?.readonly ?? true}>
|
<ButtonGroup size="sm" variant="ghost" isDisabled={account?.readonly ?? true}>
|
||||||
@@ -189,9 +135,8 @@ export function TimelineNote({
|
|||||||
<Box flexGrow={1} />
|
<Box flexGrow={1} />
|
||||||
<ButtonGroup size="sm" variant="ghost">
|
<ButtonGroup size="sm" variant="ghost">
|
||||||
<NoteProxyLink event={event} />
|
<NoteProxyLink event={event} />
|
||||||
<NoteDetailsButton event={event} onClick={detailsModal.onOpen} />
|
|
||||||
<BookmarkButton event={event} aria-label="Bookmark note" />
|
<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>
|
</ButtonGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -204,7 +149,6 @@ export function TimelineNote({
|
|||||||
onSubmitted={replyForm.onClose}
|
onSubmitted={replyForm.onClose}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={event} />}
|
|
||||||
</TrustProvider>
|
</TrustProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -41,7 +41,7 @@ export const UserAvatar = forwardRef<HTMLDivElement, UserAvatarProps>(
|
|||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
noProxy={noProxy}
|
noProxy={noProxy}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
borderColor={color}
|
borderColor={size !== "xs" ? color : undefined}
|
||||||
borderStyle="none"
|
borderStyle="none"
|
||||||
size={size}
|
size={size}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -63,7 +63,7 @@ export const UserAvatar = forwardRef<HTMLDivElement, UserAvatarProps>(
|
|||||||
);
|
);
|
||||||
UserAvatar.displayName = "UserAvatar";
|
UserAvatar.displayName = "UserAvatar";
|
||||||
|
|
||||||
const StyledAvatar = styled(Avatar)`
|
const SquareAvatar = styled(Avatar)`
|
||||||
img {
|
img {
|
||||||
border-radius: var(--chakra-radii-lg);
|
border-radius: var(--chakra-radii-lg);
|
||||||
border-width: 0.18rem;
|
border-width: 0.18rem;
|
||||||
@@ -75,9 +75,10 @@ export type MetadataAvatarProps = Omit<AvatarProps, "src"> & {
|
|||||||
metadata?: Kind0ParsedContent;
|
metadata?: Kind0ParsedContent;
|
||||||
pubkey?: string;
|
pubkey?: string;
|
||||||
noProxy?: boolean;
|
noProxy?: boolean;
|
||||||
|
square?: boolean;
|
||||||
};
|
};
|
||||||
export const MetadataAvatar = forwardRef<HTMLDivElement, MetadataAvatarProps>(
|
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 { imageProxy, proxyUserMedia, hideUsernames } = useAppSettings();
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const picture = useMemo(() => {
|
const picture = useMemo(() => {
|
||||||
@@ -95,8 +96,10 @@ export const MetadataAvatar = forwardRef<HTMLDivElement, MetadataAvatarProps>(
|
|||||||
}
|
}
|
||||||
}, [metadata?.picture, imageProxy, proxyUserMedia, hideUsernames, account]);
|
}, [metadata?.picture, imageProxy, proxyUserMedia, hideUsernames, account]);
|
||||||
|
|
||||||
|
const AvatarComponent = square ? SquareAvatar : Avatar;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAvatar
|
<AvatarComponent
|
||||||
src={picture}
|
src={picture}
|
||||||
icon={pubkey ? <UserIdenticon pubkey={pubkey} /> : undefined}
|
icon={pubkey ? <UserIdenticon pubkey={pubkey} /> : undefined}
|
||||||
// overflow="hidden"
|
// overflow="hidden"
|
||||||
@@ -105,10 +108,9 @@ export const MetadataAvatar = forwardRef<HTMLDivElement, MetadataAvatarProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledAvatar>
|
</AvatarComponent>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
UserAvatar.displayName = "UserAvatar";
|
|
||||||
|
|
||||||
export default memo(UserAvatar);
|
export default memo(UserAvatar);
|
||||||
|
93
src/views/thread/components/details-tabs.tsx
Normal file
93
src/views/thread/components/details-tabs.tsx
Normal 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()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
30
src/views/thread/components/tabs/quotes.tsx
Normal file
30
src/views/thread/components/tabs/quotes.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,11 +1,13 @@
|
|||||||
import { Box, Button, Divider, Flex, SimpleGrid, SimpleGridProps, useDisclosure } from "@chakra-ui/react";
|
|
||||||
import { useMemo } from "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 useEventReactions from "../../../../hooks/use-event-reactions";
|
||||||
import { groupReactions } from "../../helpers/nostr/reactions";
|
import { ThreadItem } from "../../../../helpers/thread";
|
||||||
import UserAvatarLink from "../user/user-avatar-link";
|
import { groupReactions } from "../../../../helpers/nostr/reactions";
|
||||||
import UserLink from "../user/user-link";
|
import ReactionIcon from "../../../../components/event-reactions/reaction-icon";
|
||||||
import ReactionIcon from "../event-reactions/reaction-icon";
|
import UserLink from "../../../../components/user/user-link";
|
||||||
|
import UserAvatarLink from "../../../../components/user/user-avatar-link";
|
||||||
|
|
||||||
function ShowMoreGrid({
|
function ShowMoreGrid({
|
||||||
pubkeys,
|
pubkeys,
|
||||||
@@ -19,9 +21,9 @@ function ShowMoreGrid({
|
|||||||
<>
|
<>
|
||||||
<SimpleGrid spacing="1" {...props}>
|
<SimpleGrid spacing="1" {...props}>
|
||||||
{limited.map((pubkey) => (
|
{limited.map((pubkey) => (
|
||||||
<Flex gap="2" key={pubkey} alignItems="center" overflow="hidden">
|
<Flex gap="2" key={pubkey} alignItems="center">
|
||||||
<UserAvatarLink pubkey={pubkey} size="xs" />
|
<UserAvatarLink pubkey={pubkey} size="sm" />
|
||||||
<UserLink pubkey={pubkey} isTruncated />
|
<UserLink pubkey={pubkey} isTruncated fontWeight="bold" />
|
||||||
</Flex>
|
</Flex>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</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]);
|
const groups = useMemo(() => groupReactions(reactions), [reactions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap="2" direction="column">
|
<Flex gap="2" direction="column" px="2">
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<Flex key={group.emoji} direction="column" gap="2">
|
<Flex key={group.emoji} direction="column" gap="2">
|
||||||
<Flex gap="2" alignItems="center">
|
<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} />
|
<ReactionIcon emoji={group.emoji} url={group.url} />
|
||||||
</Box>
|
</Box>
|
||||||
|
{group.url ? group.emoji : ""}
|
||||||
<Divider />
|
<Divider />
|
||||||
</Flex>
|
</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>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
33
src/views/thread/components/tabs/reposts.tsx
Normal file
33
src/views/thread/components/tabs/reposts.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
44
src/views/thread/components/tabs/zaps.tsx
Normal file
44
src/views/thread/components/tabs/zaps.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -14,7 +14,6 @@ import Expand01 from "../../../components/icons/expand-01";
|
|||||||
import Minus from "../../../components/icons/minus";
|
import Minus from "../../../components/icons/minus";
|
||||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||||
import UserDnsIdentity from "../../../components/user/user-dns-identity";
|
import UserDnsIdentity from "../../../components/user/user-dns-identity";
|
||||||
import EventInteractionDetailsModal from "../../../components/event-interactions-modal";
|
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
|
||||||
import useAppSettings from "../../../hooks/use-app-settings";
|
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 QuoteRepostButton from "../../../components/note/quote-repost-button";
|
||||||
import NoteZapButton from "../../../components/note/note-zap-button";
|
import NoteZapButton from "../../../components/note/note-zap-button";
|
||||||
import NoteProxyLink from "../../../components/note/timeline-note/components/note-proxy-link";
|
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 BookmarkButton from "../../../components/note/bookmark-button";
|
||||||
import NoteMenu from "../../../components/note/note-menu";
|
import NoteMenu from "../../../components/note/note-menu";
|
||||||
import NoteCommunityMetadata from "../../../components/note/timeline-note/note-community-metadata";
|
import NoteCommunityMetadata from "../../../components/note/timeline-note/note-community-metadata";
|
||||||
import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents";
|
import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents";
|
||||||
import NoteReactions from "../../../components/note/timeline-note/components/note-reactions";
|
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 = {
|
export type ThreadItemProps = {
|
||||||
post: ThreadItem;
|
post: ThreadItem;
|
||||||
@@ -42,7 +42,6 @@ export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }:
|
|||||||
const { showReactions } = useAppSettings();
|
const { showReactions } = useAppSettings();
|
||||||
const expanded = useDisclosure({ defaultIsOpen: initShowReplies ?? (level < 2 || post.replies.length <= 1) });
|
const expanded = useDisclosure({ defaultIsOpen: initShowReplies ?? (level < 2 || post.replies.length <= 1) });
|
||||||
const replyForm = useDisclosure();
|
const replyForm = useDisclosure();
|
||||||
const detailsModal = useDisclosure();
|
|
||||||
|
|
||||||
const muteFilter = useClientSideMuteFilter();
|
const muteFilter = useClientSideMuteFilter();
|
||||||
|
|
||||||
@@ -117,9 +116,8 @@ export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }:
|
|||||||
<Spacer />
|
<Spacer />
|
||||||
<ButtonGroup size="sm" variant="ghost">
|
<ButtonGroup size="sm" variant="ghost">
|
||||||
<NoteProxyLink event={post.event} />
|
<NoteProxyLink event={post.event} />
|
||||||
<NoteDetailsButton event={post.event} onClick={detailsModal.onOpen} />
|
|
||||||
<BookmarkButton event={post.event} aria-label="Bookmark" />
|
<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>
|
</ButtonGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@@ -141,19 +139,28 @@ export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }:
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{header}
|
{header}
|
||||||
{expanded.isOpen && renderContent()}
|
{expanded.isOpen && (
|
||||||
{expanded.isOpen && showReactionsOnNewLine && reactionButtons}
|
<>
|
||||||
{expanded.isOpen && footer}
|
{renderContent()}
|
||||||
|
<ZapBubbles event={post.event} />
|
||||||
|
{showReactionsOnNewLine && reactionButtons}
|
||||||
|
{footer}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{replyForm.isOpen && <ReplyForm item={post} onCancel={replyForm.onClose} onSubmitted={replyForm.onClose} />}
|
{replyForm.isOpen && <ReplyForm item={post} onCancel={replyForm.onClose} onSubmitted={replyForm.onClose} />}
|
||||||
{post.replies.length > 0 && expanded.isOpen && (
|
{level === -1 ? (
|
||||||
|
<DetailsTabs post={post} />
|
||||||
|
) : (
|
||||||
|
expanded.isOpen &&
|
||||||
|
post.replies.length > 0 && (
|
||||||
<Flex direction="column" gap="2" pl={{ base: 2, md: 4 }}>
|
<Flex direction="column" gap="2" pl={{ base: 2, md: 4 }}>
|
||||||
{post.replies.map((child) => (
|
{post.replies.map((child) => (
|
||||||
<ThreadPost key={child.event.id} post={child} focusId={focusId} level={level + 1} />
|
<ThreadPost key={child.event.id} post={child} focusId={focusId} level={level + 1} />
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={post.event} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -9,9 +9,8 @@ import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu
|
|||||||
|
|
||||||
export default function TorrentCommentMenu({
|
export default function TorrentCommentMenu({
|
||||||
comment,
|
comment,
|
||||||
detailsClick,
|
|
||||||
...props
|
...props
|
||||||
}: { comment: NostrEvent; detailsClick?: () => void } & Omit<MenuIconButtonProps, "children">) {
|
}: { comment: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DotsMenuButton {...props}>
|
<DotsMenuButton {...props}>
|
||||||
|
@@ -31,7 +31,6 @@ import Expand01 from "../../../components/icons/expand-01";
|
|||||||
import { TrustProvider } from "../../../providers/local/trust";
|
import { TrustProvider } from "../../../providers/local/trust";
|
||||||
import { ReplyIcon } from "../../../components/icons";
|
import { ReplyIcon } from "../../../components/icons";
|
||||||
import ReplyForm from "../../thread/components/reply-form";
|
import ReplyForm from "../../thread/components/reply-form";
|
||||||
import EventInteractionDetailsModal from "../../../components/event-interactions-modal";
|
|
||||||
import useThreadColorLevelProps from "../../../hooks/use-thread-color-level-props";
|
import useThreadColorLevelProps from "../../../hooks/use-thread-color-level-props";
|
||||||
import TorrentCommentMenu from "./torrent-comment-menu";
|
import TorrentCommentMenu from "./torrent-comment-menu";
|
||||||
import NoteReactions from "../../../components/note/timeline-note/components/note-reactions";
|
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 { showReactions } = useAppSettings();
|
||||||
const expanded = useDisclosure({ defaultIsOpen: level < 2 || post.replies.length <= 1 });
|
const expanded = useDisclosure({ defaultIsOpen: level < 2 || post.replies.length <= 1 });
|
||||||
const replyForm = useDisclosure();
|
const replyForm = useDisclosure();
|
||||||
const detailsModal = useDisclosure();
|
|
||||||
|
|
||||||
const muteFilter = useClientSideMuteFilter();
|
const muteFilter = useClientSideMuteFilter();
|
||||||
|
|
||||||
@@ -110,7 +108,7 @@ export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level?
|
|||||||
{!showReactionsOnNewLine && reactionButtons}
|
{!showReactionsOnNewLine && reactionButtons}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ButtonGroup size="sm" variant="ghost">
|
<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>
|
</ButtonGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@@ -151,7 +149,6 @@ export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level?
|
|||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={post.event} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -6,11 +6,7 @@ import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-em
|
|||||||
import MuteUserMenuItem from "../../../components/common-menu-items/mute-user";
|
import MuteUserMenuItem from "../../../components/common-menu-items/mute-user";
|
||||||
import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item";
|
import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item";
|
||||||
|
|
||||||
export default function TrackMenu({
|
export default function TrackMenu({ track, ...props }: { track: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
track,
|
|
||||||
detailsClick,
|
|
||||||
...props
|
|
||||||
}: { track: NostrEvent; detailsClick?: () => void } & Omit<MenuIconButtonProps, "children">) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DotsMenuButton {...props}>
|
<DotsMenuButton {...props}>
|
||||||
|
Reference in New Issue
Block a user