add support for kind 6 reposts

This commit is contained in:
hzrd149 2023-04-11 06:51:48 -05:00
parent af1693cefe
commit e75ac1ba35
22 changed files with 319 additions and 97 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add support for kind 6 reposts

View File

@ -21,7 +21,7 @@
"light-bolt11-decoder": "^2.1.0",
"moment": "^2.29.4",
"noble-secp256k1": "^1.2.14",
"nostr-tools": "^1.7.4",
"nostr-tools": "^1.8.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",

View File

@ -0,0 +1,27 @@
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Flex } from "@chakra-ui/react";
import { ModalProps } from "@chakra-ui/react";
import { Bech32Prefix, hexToBech32 } from "../../helpers/nip19";
import { getReferences } from "../../helpers/nostr-event";
import { NostrEvent } from "../../types/nostr-event";
import RawJson from "./raw-block";
import RawValue from "./raw-value";
export default function NoteDebugModal({ event, ...props }: { event: NostrEvent } & Omit<ModalProps, "children">) {
return (
<Modal {...props}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Event Debug</ModalHeader>
<ModalCloseButton />
<ModalBody overflow="auto" fontSize="sm" padding="2">
<Flex gap="2" direction="column">
<RawValue heading="Event Id" value={event.id} />
<RawValue heading="Encoded id (NIP-19)" value={hexToBech32(event.id, Bech32Prefix.Note) ?? "failed"} />
<RawJson heading="Raw" json={event} />
<RawJson heading="References" json={getReferences(event)} />
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
}

View File

@ -0,0 +1,16 @@
import { Box, Code, Flex, Heading } from "@chakra-ui/react";
export default function RawJson({ json, heading }: { heading: string; json: any }) {
return (
<Box>
<Heading size="sm" mb="2">
{heading}
</Heading>
<Flex gap="2">
<Code whiteSpace="pre" overflowX="auto" width="100%">
{JSON.stringify(json, null, 2)}
</Code>
</Flex>
</Box>
);
}

View File

@ -0,0 +1,18 @@
import { Box, Code, Flex, Heading } from "@chakra-ui/react";
import { CopyIconButton } from "../copy-icon-button";
export default function RawValue({ value, heading }: { heading: string; value: string }) {
return (
<Box>
<Heading size="sm" mb="2">
{heading}
</Heading>
<Flex gap="2">
<Code fontSize="md" wordBreak="break-all">
{value}
</Code>
<CopyIconButton text={value} size="xs" aria-label="copy" />
</Flex>
</Box>
);
}

View File

@ -0,0 +1,28 @@
import { useMemo } from "react";
import { Flex, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay } from "@chakra-ui/react";
import { ModalProps } from "@chakra-ui/react";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import RawValue from "./raw-value";
import RawJson from "./raw-block";
export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } & Omit<ModalProps, "children">) {
const npub = useMemo(() => normalizeToBech32(pubkey, Bech32Prefix.Pubkey), [pubkey]);
const metadata = useUserMetadata(pubkey);
return (
<Modal {...props}>
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<ModalBody overflow="auto" fontSize="sm" padding="2">
<Flex gap="2" direction="column">
<RawValue heading="Hex pubkey" value={pubkey} />
{npub && <RawValue heading="Encoded pubkey (NIP-19)" value={npub} />}
<RawJson heading="Metadata (kind 0)" json={metadata} />
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
}

View File

@ -2,12 +2,12 @@ import React from "react";
import { ErrorBoundary as ErrorBoundaryHelper, FallbackProps } from "react-error-boundary";
import { Alert, AlertIcon, AlertTitle, AlertDescription } from "@chakra-ui/react";
export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
export function ErrorFallback({ error, resetErrorBoundary }: Partial<FallbackProps>) {
return (
<Alert status="error">
<AlertIcon />
<AlertTitle>Something went wrong</AlertTitle>
<AlertDescription>{error.message}</AlertDescription>
<AlertDescription>{error?.message}</AlertDescription>
</Alert>
);
}

View File

@ -130,15 +130,21 @@ export const SearchIcon = createIcon({
defaultProps,
});
export const ShareIcon = createIcon({
displayName: "ShareIcon",
export const RepostIcon = createIcon({
displayName: "RepostIcon",
d: "M13.12 17.023l-4.199-2.29a4 4 0 1 1 0-5.465l4.2-2.29a4 4 0 1 1 .959 1.755l-4.2 2.29a4.008 4.008 0 0 1 0 1.954l4.199 2.29a4 4 0 1 1-.959 1.755zM6 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm11-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4z",
defaultProps,
});
export const ReplyIcon = createIcon({
displayName: "ReplyIcon",
d: "M5.763 17H20V5H4v13.385L5.763 17zm.692 2L2 22.5V4a1 1 0 0 1 1-1h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6.455z",
d: "M11 20L1 12L11 4V9C16.5228 9 21 13.4772 21 19C21 19.2727 20.9891 19.5428 20.9677 19.81C19.5055 17.0364 16.6381 15.119 13.313 15.0053L13 15H10.9999L11 20ZM8.99986 13H10.9999L13.0341 13.0003L13.3814 13.0065C14.6657 13.0504 15.9053 13.3165 17.0568 13.7734C15.5898 12.0749 13.4204 11 11 11H9V8.16125L4.20156 12L8.99992 15.8387L8.99986 13Z",
defaultProps,
});
export const QuoteRepostIcon = createIcon({
displayName: "QuoteRepostIcon",
d: "M19.4167 6.67891C20.4469 7.77257 21.0001 9 21.0001 10.9897C21.0001 14.4891 18.5436 17.6263 14.9695 19.1768L14.0768 17.7992C17.4121 15.9946 18.0639 13.6539 18.3245 12.178C17.7875 12.4557 17.0845 12.5533 16.3954 12.4895C14.591 12.3222 13.1689 10.8409 13.1689 9C13.1689 7.067 14.7359 5.5 16.6689 5.5C17.742 5.5 18.7681 5.99045 19.4167 6.67891ZM9.41669 6.67891C10.4469 7.77257 11.0001 9 11.0001 10.9897C11.0001 14.4891 8.54359 17.6263 4.96951 19.1768L4.07682 17.7992C7.41206 15.9946 8.06392 13.6539 8.32447 12.178C7.78747 12.4557 7.08452 12.5533 6.39539 12.4895C4.59102 12.3222 3.16895 10.8409 3.16895 9C3.16895 7.067 4.73595 5.5 6.66895 5.5C7.742 5.5 8.76814 5.99045 9.41669 6.67891Z",
defaultProps,
});

View File

@ -0,0 +1,24 @@
import { useContext } from "react";
import { IconButton } from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import { QuoteRepostIcon } from "../../icons";
import { PostModalContext } from "../../../providers/post-modal-provider";
import { buildQuoteRepost } from "../../../helpers/nostr-event";
import { useCurrentAccount } from "../../../hooks/use-current-account";
export function QuoteRepostButton({ event }: { event: NostrEvent }) {
const account = useCurrentAccount();
const { openModal } = useContext(PostModalContext);
const handleClick = () => openModal(buildQuoteRepost(event));
return (
<IconButton
icon={<QuoteRepostIcon />}
onClick={handleClick}
aria-label="Quote repost"
title="Quote repost"
isDisabled={account.readonly}
/>
);
}

View File

@ -1,29 +1,19 @@
import {
Button,
ButtonProps,
Flex,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from "@chakra-ui/react";
import { Button, ButtonProps } from "@chakra-ui/react";
import moment from "moment";
import { Kind } from "nostr-tools";
import { useState } from "react";
import { nostrPostAction } from "../../classes/nostr-post-action";
import { random } from "../../helpers/array";
import { useCurrentAccount } from "../../hooks/use-current-account";
import useEventReactions from "../../hooks/use-event-reactions";
import { useSigningContext } from "../../providers/signing-provider";
import clientRelaysService from "../../services/client-relays";
import eventReactionsService from "../../services/event-reactions";
import { getEventRelays } from "../../services/event-relays";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import { DislikeIcon, LikeIcon } from "../icons";
import { nostrPostAction } from "../../../classes/nostr-post-action";
import { random } from "../../../helpers/array";
import { useCurrentAccount } from "../../../hooks/use-current-account";
import useEventReactions from "../../../hooks/use-event-reactions";
import { useSigningContext } from "../../../providers/signing-provider";
import clientRelaysService from "../../../services/client-relays";
import eventReactionsService from "../../../services/event-reactions";
import { getEventRelays } from "../../../services/event-relays";
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
import { DislikeIcon, LikeIcon } from "../../icons";
export default function NoteLikeButton({ note, ...props }: { note: NostrEvent } & Omit<ButtonProps, "children">) {
export default function ReactionButton({ note, ...props }: { note: NostrEvent } & Omit<ButtonProps, "children">) {
const { requestSignature } = useSigningContext();
const account = useCurrentAccount();

View File

@ -0,0 +1,18 @@
import { useContext } from "react";
import { IconButton } from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import { ReplyIcon } from "../../icons";
import { PostModalContext } from "../../../providers/post-modal-provider";
import { buildReply } from "../../../helpers/nostr-event";
import { useCurrentAccount } from "../../../hooks/use-current-account";
export function ReplyButton({ event }: { event: NostrEvent }) {
const account = useCurrentAccount();
const { openModal } = useContext(PostModalContext);
const reply = () => openModal(buildReply(event));
return (
<IconButton icon={<ReplyIcon />} title="Reply" aria-label="Reply" onClick={reply} isDisabled={account.readonly} />
);
}

View File

@ -0,0 +1,40 @@
import { useState } from "react";
import { IconButton, useToast } from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import { RepostIcon } from "../../icons";
import { buildRepost } from "../../../helpers/nostr-event";
import { useCurrentAccount } from "../../../hooks/use-current-account";
import { nostrPostAction } from "../../../classes/nostr-post-action";
import clientRelaysService from "../../../services/client-relays";
import signingService from "../../../services/signing";
export function RepostButton({ event }: { event: NostrEvent }) {
const account = useCurrentAccount();
const [loading, setLoading] = useState(false);
const toast = useToast();
const handleClick = async () => {
try {
setLoading(true);
const draftRepost = buildRepost(event);
const repost = await signingService.requestSignature(draftRepost, account);
await nostrPostAction(clientRelaysService.getWriteUrls(), repost);
} catch (e) {
if (e instanceof Error) {
toast({ status: "error", description: e.message });
}
}
setLoading(false);
};
return (
<IconButton
icon={<RepostIcon />}
onClick={handleClick}
aria-label="Repost Note"
title="Repost Note"
isDisabled={account.readonly}
isLoading={loading}
/>
);
}

View File

@ -1,4 +1,4 @@
import React, { useContext } from "react";
import React from "react";
import { Link as RouterLink } from "react-router-dom";
import moment from "moment";
import {
@ -11,7 +11,6 @@ import {
CardProps,
Flex,
Heading,
IconButton,
Link,
} from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
@ -24,18 +23,18 @@ import { useUserContacts } from "../../hooks/use-user-contacts";
import { NoteRelays } from "./note-relays";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { UserLink } from "../user-link";
import { ReplyIcon, ShareIcon } from "../icons";
import { PostModalContext } from "../../providers/post-modal-provider";
import { buildReply, buildShare } from "../../helpers/nostr-event";
import { UserDnsIdentityIcon } from "../user-dns-identity";
import { convertTimestampToDate } from "../../helpers/date";
import { useCurrentAccount } from "../../hooks/use-current-account";
import NoteLikeButton from "./note-like-button";
import ReactionButton from "./buttons/reaction-button";
import NoteZapButton from "./note-zap-button";
import { ExpandProvider } from "./expanded";
import useSubject from "../../hooks/use-subject";
import settings from "../../services/settings";
import EventVerificationIcon from "../event-verification-icon";
import { ReplyButton } from "./buttons/reply-button";
import { RepostButton } from "./buttons/repost-button";
import { QuoteRepostButton } from "./buttons/quote-repost-button";
export type NoteProps = {
event: NostrEvent;
@ -45,16 +44,12 @@ export type NoteProps = {
export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteProps) => {
const isMobile = useIsMobile();
const account = useCurrentAccount();
const { openModal } = useContext(PostModalContext);
const showReactions = useSubject(settings.showReactions);
const showSignatureVerification = useSubject(settings.showSignatureVerification);
const contacts = useUserContacts(account.pubkey);
const following = contacts?.contacts || [];
const reply = () => openModal(buildReply(event));
const share = () => openModal(buildShare(event));
return (
<ExpandProvider>
<Card variant={variant}>
@ -81,27 +76,12 @@ export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteP
/>
</CardBody>
<CardFooter padding="2" display="flex" gap="2">
<IconButton
variant="link"
icon={<ReplyIcon />}
title="Reply"
aria-label="Reply"
onClick={reply}
size="sm"
isDisabled={account.readonly}
/>
<IconButton
variant="link"
icon={<ShareIcon />}
onClick={share}
aria-label="Share Note"
title="Share Note"
size="sm"
isDisabled={account.readonly}
/>
<ButtonGroup size="sm" variant="link">
<ReplyButton event={event} />
<RepostButton event={event} />
<QuoteRepostButton event={event} />
<NoteZapButton note={event} size="sm" />
{showReactions && <NoteLikeButton note={event} size="sm" />}
{showReactions && <ReactionButton note={event} size="sm" />}
</ButtonGroup>
<Box flexGrow={1} />
<NoteRelays event={event} size="sm" variant="link" />

View File

@ -15,11 +15,12 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
import { NostrEvent } from "../../types/nostr-event";
import { MenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
import { ClipboardIcon, CodeIcon, LikeIcon, ShareIcon } from "../icons";
import { ClipboardIcon, CodeIcon, LikeIcon, RepostIcon } from "../icons";
import { getReferences } from "../../helpers/nostr-event";
import NoteReactionsModal from "./note-zaps-modal";
import { getEventRelays } from "../../services/event-relays";
import relayScoreboardService from "../../services/relay-scoreboard";
import NoteDebugModal from "../debug-modals/note-debug-modal";
function getShareLink(eventId: string) {
const relays = getEventRelays(eventId).value;
@ -43,7 +44,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
<MenuItem onClick={reactionsModal.onOpen} icon={<LikeIcon />}>
Zaps/Reactions
</MenuItem>
<MenuItem onClick={() => copyToClipboard("nostr:" + getShareLink(event.id))} icon={<ShareIcon />}>
<MenuItem onClick={() => copyToClipboard("nostr:" + getShareLink(event.id))} icon={<RepostIcon />}>
Copy Share Link
</MenuItem>
{noteId && (
@ -56,19 +57,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
</MenuItem>
</MenuIconButton>
{infoModal.isOpen && (
<Modal isOpen={infoModal.isOpen} onClose={infoModal.onClose} size="6xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>Raw Event</ModalHeader>
<ModalCloseButton />
<ModalBody overflow="auto" fontSize="sm" padding="2">
Raw JSON:
<pre>{JSON.stringify(event, null, 2)}</pre>
Parsed Refs:
<pre>{JSON.stringify(getReferences(event), null, 2)}</pre>
</ModalBody>
</ModalContent>
</Modal>
<NoteDebugModal event={event} isOpen={infoModal.isOpen} onClose={infoModal.onClose} size="6xl" />
)}
{reactionsModal.isOpen && (
<NoteReactionsModal noteId={event.id} isOpen={reactionsModal.isOpen} onClose={reactionsModal.onClose} />

View File

@ -0,0 +1,47 @@
import { Box, Flex, Heading, SkeletonText } from "@chakra-ui/react";
import { useAsync } from "react-use";
import clientRelaysService from "../services/client-relays";
import singleEventService from "../services/single-event";
import { isETag, NostrEvent } from "../types/nostr-event";
import { ErrorFallback } from "./error-boundary";
import { Note } from "./note";
import { NoteMenu } from "./note/note-menu";
import { UserAvatar } from "./user-avatar";
import { UserDnsIdentityIcon } from "./user-dns-identity";
import { UserLink } from "./user-link";
export default function RepostNote({ event, maxHeight }: { event: NostrEvent; maxHeight?: number }) {
const {
value: repostNote,
loading,
error,
} = useAsync(async () => {
const [_, eventId, relay] = event.tags.find(isETag) ?? [];
if (eventId) {
return singleEventService.requestEvent(eventId, relay ? [relay] : clientRelaysService.getReadUrls());
}
return null;
}, [event]);
return (
<Flex gap="2" direction="column">
<Flex gap="2" alignItems="center" pl="1">
<UserAvatar pubkey={event.pubkey} size="xs" />
<Heading size="sm" display="inline">
<UserLink pubkey={event.pubkey} />
</Heading>
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<span>Shared note</span>
<Box flex={1} />
{import.meta.env.DEV && <NoteMenu event={event} size="sm" variant="link" aria-label="note options" />}
</Flex>
{loading ? (
<SkeletonText />
) : repostNote ? (
<Note event={repostNote} maxHeight={maxHeight} />
) : (
<ErrorFallback error={error} />
)}
</Flex>
);
}

View File

@ -74,7 +74,7 @@ export default function ZapModal({
} = useForm<FormValues>({
mode: "onBlur",
defaultValues: {
amount: initialAmount ?? 10,
amount: initialAmount ?? zapAmounts[0],
comment: initialComment ?? "",
},
});

View File

@ -6,11 +6,11 @@ import accountService from "../services/account";
import { Kind } from "nostr-tools";
export function isReply(event: NostrEvent | DraftNostrEvent) {
return !!getReferences(event).replyId;
return event.kind === 1 && !!getReferences(event).replyId;
}
export function isNote(event: NostrEvent | DraftNostrEvent) {
return !isReply(event);
export function isRepost(event: NostrEvent | DraftNostrEvent) {
return event.kind === 6;
}
export function truncatedId(id: string, keep = 6) {
@ -112,7 +112,22 @@ export function buildReply(event: NostrEvent, account = accountService.current.v
};
}
export function buildShare(event: NostrEvent): DraftNostrEvent {
export function buildRepost(event: NostrEvent): DraftNostrEvent {
const relay = getEventRelays(event.id).value?.[0] ?? "";
const tags: NostrEvent["tags"] = [];
tags.push(["e", event.id, relay]);
tags.push(["p", event.pubkey]);
return {
kind: 6, //Kind.Repost
tags,
content: "",
created_at: moment().unix(),
};
}
export function buildQuoteRepost(event: NostrEvent): DraftNostrEvent {
const relay = getEventRelays(event.id).value?.[0] ?? "";
const tags: NostrEvent["tags"] = [];

View File

@ -3,7 +3,7 @@ import { Button, Flex, Spinner } from "@chakra-ui/react";
import moment from "moment";
import { Note } from "../../components/note";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import { isNote } from "../../helpers/nostr-event";
import { isReply } from "../../helpers/nostr-event";
import { useAppTitle } from "../../hooks/use-app-title";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import { useCurrentAccount } from "../../hooks/use-current-account";
@ -75,7 +75,7 @@ export default function DiscoverTab() {
{ pageSize: moment.duration(1, "hour").asSeconds(), enabled: throttledPubkeys.length > 0 }
);
const timeline = events.filter(isNote);
const timeline = events.filter((e) => !isReply(e));
return (
<Flex direction="column" gap="2">

View File

@ -2,7 +2,7 @@ import { Button, Flex, FormControl, FormLabel, Spinner, Switch } from "@chakra-u
import { useSearchParams } from "react-router-dom";
import moment from "moment";
import { Note } from "../../components/note";
import { isNote } from "../../helpers/nostr-event";
import { isReply } from "../../helpers/nostr-event";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import { useUserContacts } from "../../hooks/use-user-contacts";
import { AddIcon } from "@chakra-ui/icons";
@ -10,6 +10,7 @@ import { useContext } from "react";
import { PostModalContext } from "../../providers/post-modal-provider";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import { useCurrentAccount } from "../../hooks/use-current-account";
import RepostNote from "../../components/repost-note";
export default function FollowingTab() {
const account = useCurrentAccount();
@ -26,11 +27,11 @@ export default function FollowingTab() {
const { events, loading, loadMore } = useTimelineLoader(
`${account.pubkey}-following-posts`,
relays,
{ authors: following, kinds: [1], since: moment().subtract(2, "hour").unix() },
{ authors: following, kinds: [1, 6], since: moment().subtract(2, "hour").unix() },
{ pageSize: moment.duration(2, "hour").asSeconds(), enabled: following.length > 0 }
);
const timeline = showReplies ? events : events.filter(isNote);
const timeline = showReplies ? events : events.filter((e) => !isReply(e));
return (
<Flex direction="column" gap="2">
@ -43,9 +44,13 @@ export default function FollowingTab() {
</FormLabel>
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} />
</FormControl>
{timeline.map((event) => (
<Note key={event.id} event={event} maxHeight={600} />
))}
{timeline.map((event) =>
event.kind === 6 ? (
<RepostNote key={event.id} event={event} maxHeight={600} />
) : (
<Note key={event.id} event={event} maxHeight={600} />
)
)}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>
);

View File

@ -3,7 +3,7 @@ import moment from "moment";
import { useSearchParams } from "react-router-dom";
import { Note } from "../../components/note";
import { unique } from "../../helpers/array";
import { isNote } from "../../helpers/nostr-event";
import { isReply } from "../../helpers/nostr-event";
import { useAppTitle } from "../../hooks/use-app-title";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
@ -29,7 +29,7 @@ export default function GlobalTab() {
{ pageSize: moment.duration(5, "minutes").asSeconds() }
);
const timeline = showReplies ? events : events.filter(isNote);
const timeline = showReplies ? events : events.filter((e) => !isReply(e));
return (
<Flex direction="column" gap="2">

View File

@ -21,7 +21,8 @@ import { useOutletContext } from "react-router-dom";
import { RelayMode } from "../../classes/relay";
import { RelayIcon } from "../../components/icons";
import { Note } from "../../components/note";
import { isNote, truncatedId } from "../../helpers/nostr-event";
import RepostNote from "../../components/repost-note";
import { isReply, isRepost, truncatedId } from "../../helpers/nostr-event";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useFallbackUserRelays from "../../hooks/use-fallback-user-relays";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
@ -39,22 +40,31 @@ const UserNotesTab = () => {
const relays = userRelays.length === 0 ? readRelays : relayScoreboardService.getRankedRelays(userRelays).slice(0, 4);
const { isOpen: showReplies, onToggle: toggleReplies } = useDisclosure();
const { isOpen: hideReposts, onToggle: toggleReposts } = useDisclosure();
const { events, loading, loadMore } = useTimelineLoader(
`${truncatedId(pubkey)}-notes`,
relays,
{ authors: [pubkey], kinds: [1] },
{ authors: [pubkey], kinds: [1, 6] },
{ pageSize: moment.duration(2, "day").asSeconds(), startLimit: 20 }
);
const timeline = showReplies ? events : events.filter(isNote);
const timeline = events.filter((event) => {
if (!showReplies && isReply(event)) return false;
if (hideReposts && isRepost(event)) return false;
return true;
});
return (
<Flex direction="column" gap="2" pr="2" pl="2">
<FormControl display="flex" alignItems="center">
<FormLabel htmlFor="show-replies" mb="0">
Show Replies
<Switch id="replies" mr="2" isChecked={showReplies} onChange={toggleReplies} />
<FormLabel htmlFor="replies" mb="0">
Replies
</FormLabel>
<Switch id="reposts" mr="2" isChecked={!hideReposts} onChange={toggleReposts} />
<FormLabel htmlFor="reposts" mb="0">
Reposts
</FormLabel>
<Switch id="show-replies" isChecked={showReplies} onChange={toggleReplies} />
<Box flexGrow={1} />
<Popover>
<PopoverTrigger>
@ -75,9 +85,13 @@ const UserNotesTab = () => {
</PopoverContent>
</Popover>
</FormControl>
{timeline.map((event) => (
<Note key={event.id} event={event} maxHeight={1200} />
))}
{timeline.map((event) =>
event.kind === 6 ? (
<RepostNote key={event.id} event={event} maxHeight={1200} />
) : (
<Note key={event.id} event={event} maxHeight={1200} />
)
)}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>
);

View File

@ -4207,7 +4207,7 @@ normalize-package-data@^2.5.0:
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
nostr-tools@^1.7.4:
nostr-tools@^1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.8.3.tgz#10ddb8ed5d9ca3bf6c1e8fdd1961bb6584b8e1f2"
integrity sha512-0giVDk0ElhqlGY032ma/8Q8NsIyFL53fCCkndFCpuLabZ2E134Kth0sbnIIIFXLqm7VnYIlgLVtCna8+dUiZUg==