note pinning

This commit is contained in:
hzrd149 2023-11-17 14:04:57 -06:00
parent 87cf0198b0
commit 7cbffb96a4
14 changed files with 311 additions and 201 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add option to pin notes

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Show pinned notes on user profile

View File

@ -60,6 +60,7 @@ import Wallet02 from "./icons/wallet-02";
import Download01 from "./icons/download-01";
import Repeat01 from "./icons/repeat-01";
import ReverseLeft from "./icons/reverse-left";
import Pin01 from "./icons/pin-01";
const defaultProps: IconProps = { boxSize: 4 };
@ -89,6 +90,7 @@ export const ChevronRightIcon = ChevronRight;
export const LightningIcon = Zap;
export const RelayIcon = Server04;
export const BroadcastEventIcon = Share07;
export const PinIcon = Pin01;
export const ExternalLinkIcon = Share04;

View File

@ -24,7 +24,7 @@ import NostrPublishAction from "../../../classes/nostr-publish-action";
import clientRelaysService from "../../../services/client-relays";
import { useSigningContext } from "../../../providers/signing-provider";
import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from "../../icons";
import useJoinedCommunitiesList from "../../../hooks/use-communities-joined-list";
import useUserCommunitiesList from "../../../hooks/use-user-communities-list";
import useCurrentAccount from "../../../hooks/use-current-account";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { createCoordinate } from "../../../services/replaceable-event-requester";
@ -54,7 +54,7 @@ export default function RepostModal({
const toast = useToast();
const { requestSignature } = useSigningContext();
const showCommunities = useDisclosure();
const { pointers } = useJoinedCommunitiesList(account?.pubkey);
const { pointers } = useUserCommunitiesList(account?.pubkey);
const [loading, setLoading] = useState(false);
const repost = async (communityPointer?: AddressPointer) => {

View File

@ -1,11 +1,8 @@
import { useCallback } from "react";
import { MenuItem, useDisclosure } from "@chakra-ui/react";
import { useCallback, useState } from "react";
import { MenuItem, useDisclosure, useToast } from "@chakra-ui/react";
import { useCopyToClipboard } from "react-use";
import { nip19 } from "nostr-tools";
import { getSharableEventAddress } from "../../helpers/nip19";
import { NostrEvent } from "../../types/nostr-event";
import { CustomMenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
import dayjs from "dayjs";
import {
BroadcastEventIcon,
@ -17,7 +14,11 @@ import {
RepostIcon,
TrashIcon,
UnmuteIcon,
PinIcon,
} from "../icons";
import { getSharableEventAddress } from "../../helpers/nip19";
import { DraftNostrEvent, NostrEvent, isETag } from "../../types/nostr-event";
import { CustomMenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
import NoteReactionsModal from "./note-zaps-modal";
import NoteDebugModal from "../debug-modals/note-debug-modal";
import useCurrentAccount from "../../hooks/use-current-account";
@ -30,6 +31,49 @@ import useUserMuteFunctions from "../../hooks/use-user-mute-functions";
import { useMuteModalContext } from "../../providers/mute-modal-provider";
import NoteTranslationModal from "../note-translation-modal";
import Translate01 from "../icons/translate-01";
import useUserPinList from "../../hooks/use-user-pin-list";
import { useSigningContext } from "../../providers/signing-provider";
import { PIN_LIST_KIND, listAddEvent, listRemoveEvent } from "../../helpers/nostr/lists";
function PinNoteItem({ event }: { event: NostrEvent }) {
const toast = useToast();
const account = useCurrentAccount();
const { requestSignature } = useSigningContext();
const { list } = useUserPinList(account?.pubkey);
const isPinned = list?.tags.some((t) => isETag(t) && t[1] === event.id) ?? false;
const label = isPinned ? "Unpin Note" : "Pin Note";
const [loading, setLoading] = useState(false);
const togglePin = useCallback(async () => {
try {
setLoading(true);
let draft: DraftNostrEvent = {
kind: PIN_LIST_KIND,
created_at: dayjs().unix(),
content: list?.content ?? "",
tags: list?.tags ? Array.from(list.tags) : [],
};
if (isPinned) draft = listRemoveEvent(draft, event.id);
else draft = listAddEvent(draft, event.id);
const signed = await requestSignature(draft);
new NostrPublishAction(label, clientRelaysService.getWriteUrls(), signed);
setLoading(false);
} catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message });
}
}, [list, isPinned]);
if (event.pubkey !== account?.pubkey) return null;
return (
<MenuItem onClick={togglePin} icon={<PinIcon />} isDisabled={loading || account.readonly}>
{label}
</MenuItem>
);
}
export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
const account = useCurrentAccount();
@ -90,6 +134,7 @@ export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Om
<MenuItem onClick={broadcast} icon={<BroadcastEventIcon />}>
Broadcast
</MenuItem>
<PinNoteItem event={event} />
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
View Raw
</MenuItem>

View File

@ -1,7 +1,7 @@
import { forwardRef } from "react";
import { Select, SelectProps } from "@chakra-ui/react";
import useJoinedCommunitiesList from "../../hooks/use-communities-joined-list";
import useUserCommunitiesList from "../../hooks/use-user-communities-list";
import useCurrentAccount from "../../hooks/use-current-account";
import { getCommunityName } from "../../helpers/nostr/communities";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
@ -17,7 +17,7 @@ function CommunityOption({ pointer }: { pointer: AddressPointer }) {
const CommunitySelect = forwardRef<HTMLSelectElement, Omit<SelectProps, "children">>(({ ...props }, ref) => {
const account = useCurrentAccount();
const { pointers } = useJoinedCommunitiesList(account?.pubkey);
const { pointers } = useUserCommunitiesList(account?.pubkey);
return (
<Select placeholder="Select community" {...props} ref={ref}>

View File

@ -4,7 +4,7 @@ import { RequestOptions } from "../services/replaceable-event-requester";
import useCurrentAccount from "./use-current-account";
import useReplaceableEvent from "./use-replaceable-event";
export default function useJoinedCommunitiesList(pubkey?: string, opts?: RequestOptions) {
export default function useUserCommunitiesList(pubkey?: string, relays: string[] = [], opts?: RequestOptions) {
const account = useCurrentAccount();
const key = pubkey ?? account?.pubkey;
@ -22,10 +22,9 @@ export default function useJoinedCommunitiesList(pubkey?: string, opts?: Request
[],
opts,
);
const list = useReplaceableEvent(key ? { kind: COMMUNITIES_LIST_KIND, pubkey: key } : undefined, [], opts);
const list = useReplaceableEvent(key ? { kind: COMMUNITIES_LIST_KIND, pubkey: key } : undefined, relays, opts);
let useList = list || oldList;
console.log(list, oldList);
// if both exist, use the newest one
if (list && oldList) {

View File

@ -0,0 +1,15 @@
import { PIN_LIST_KIND, getEventsFromList } from "../helpers/nostr/lists";
import { RequestOptions } from "../services/replaceable-event-requester";
import useCurrentAccount from "./use-current-account";
import useReplaceableEvent from "./use-replaceable-event";
export default function useUserPinList(pubkey?: string, relays: string[] = [], opts?: RequestOptions) {
const account = useCurrentAccount();
const key = pubkey ?? account?.pubkey;
const list = useReplaceableEvent(key ? { kind: PIN_LIST_KIND, pubkey: key } : undefined, relays, opts);
const events = list ? getEventsFromList(list) : [];
return { list, events };
}

View File

@ -3,7 +3,7 @@ import dayjs from "dayjs";
import { Button, ButtonProps, useToast } from "@chakra-ui/react";
import { DraftNostrEvent, NostrEvent, isDTag } from "../../../types/nostr-event";
import useJoinedCommunitiesList from "../../../hooks/use-communities-joined-list";
import useUserCommunitiesList from "../../../hooks/use-user-communities-list";
import useCurrentAccount from "../../../hooks/use-current-account";
import { getCommunityName } from "../../../helpers/nostr/communities";
import { COMMUNITIES_LIST_KIND, listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists";
@ -16,10 +16,10 @@ export default function CommunityJoinButton({
community,
...props
}: Omit<ButtonProps, "children"> & { community: NostrEvent }) {
const account = useCurrentAccount();
const { list, pointers } = useJoinedCommunitiesList(account?.pubkey);
const { requestSignature } = useSigningContext();
const toast = useToast();
const account = useCurrentAccount();
const { list, pointers } = useUserCommunitiesList(account?.pubkey);
const { requestSignature } = useSigningContext();
const isSubscribed = pointers.find(
(cord) => cord.identifier === getCommunityName(community) && cord.pubkey === community.pubkey,
@ -29,7 +29,7 @@ export default function CommunityJoinButton({
try {
const favList = {
kind: COMMUNITIES_LIST_KIND,
content: "",
content: list?.content ?? "",
created_at: dayjs().unix(),
tags: list?.tags.filter((t) => !isDTag(t)) ?? [],
};

View File

@ -25,7 +25,7 @@ import dayjs from "dayjs";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { ErrorBoundary } from "../../components/error-boundary";
import useJoinedCommunitiesList from "../../hooks/use-communities-joined-list";
import useUserCommunitiesList from "../../hooks/use-user-communities-list";
import useCurrentAccount from "../../hooks/use-current-account";
import CommunityCard from "./components/community-card";
import CommunityCreateModal, { FormValues } from "./components/community-create-modal";
@ -62,7 +62,9 @@ function CommunitiesHomePage() {
const createModal = useDisclosure();
const readRelays = useReadRelayUrls();
const { pointers: communityCoordinates } = useJoinedCommunitiesList(account.pubkey, { alwaysRequest: true });
const { pointers: communityCoordinates } = useUserCommunitiesList(account.pubkey, readRelays, {
alwaysRequest: true,
});
const communities = useReplaceableEvents(communityCoordinates, readRelays).sort(
(a, b) => b.created_at - a.created_at,
);

View File

@ -1,35 +1,11 @@
import { useOutletContext, Link as RouterLink } from "react-router-dom";
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Button,
Flex,
Heading,
IconButton,
Image,
Link,
SimpleGrid,
Stat,
StatGroup,
StatHelpText,
StatLabel,
StatNumber,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { useAsync } from "react-use";
import { Kind, nip19 } from "nostr-tools";
import { Box, Button, Flex, Heading, IconButton, Image, Link, Text, useDisclosure } from "@chakra-ui/react";
import { nip19 } from "nostr-tools";
import { readablizeSats } from "../../../helpers/bolt11";
import { getUserDisplayName } from "../../../helpers/user-metadata";
import { getLudEndpoint } from "../../../helpers/lnurl";
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
import { truncatedId } from "../../../helpers/nostr/events";
import trustedUserStatsService from "../../../services/trusted-user-stats";
import { parseAddress } from "../../../services/dns-identity";
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
@ -51,15 +27,10 @@ import { UserFollowButton } from "../../../components/user-follow-button";
import UserZapButton from "../components/user-zap-button";
import { UserProfileMenu } from "../components/user-profile-menu";
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
import useUserContactList from "../../../hooks/use-user-contact-list";
import { getPubkeysFromList } from "../../../helpers/nostr/lists";
import Timestamp from "../../../components/timestamp";
import UserProfileBadges from "./user-profile-badges";
import useEventCount from "../../../hooks/use-event-count";
import useJoinedCommunitiesList from "../../../hooks/use-communities-joined-list";
import { PointerCommunityCard } from "../../communities/components/community-card";
import { ErrorBoundary } from "../../../components/error-boundary";
import { useBreakpointValue } from "../../../providers/breakpoint-provider";
import UserJoinedCommunities from "./user-joined-communities";
import UserPinnedEvents from "./user-pinned-events";
import UserStatsAccordion from "./user-stats-accordion";
function buildDescriptionContent(description: string) {
let content: EmbedableContent = [description.trim()];
@ -70,153 +41,6 @@ function buildDescriptionContent(description: string) {
return content;
}
function UserStatsAccordion({ pubkey }: { pubkey: string }) {
const contextRelays = useAdditionalRelayContext();
const contacts = useUserContactList(pubkey, contextRelays);
const { value: stats } = useAsync(() => trustedUserStatsService.getUserStats(pubkey), [pubkey]);
const followerCount = useEventCount({ "#p": [pubkey], kinds: [Kind.Contacts] });
return (
<Accordion allowMultiple>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Network Stats
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb="2">
<StatGroup gap="4" whiteSpace="pre">
<Stat>
<StatLabel>Following</StatLabel>
<StatNumber>{contacts ? readablizeSats(getPubkeysFromList(contacts).length) : "Unknown"}</StatNumber>
{contacts && (
<StatHelpText>
Updated <Timestamp timestamp={contacts.created_at} />
</StatHelpText>
)}
</Stat>
{stats && (
<>
<Stat>
<StatLabel>Followers</StatLabel>
<StatNumber>{readablizeSats(followerCount ?? 0) || 0}</StatNumber>
</Stat>
<Stat>
<StatLabel>Notes & replies</StatLabel>
<StatNumber>{readablizeSats(stats.pub_note_count) || 0}</StatNumber>
</Stat>
<Stat>
<StatLabel>Reactions</StatLabel>
<StatNumber>{readablizeSats(stats.pub_reaction_count) || 0}</StatNumber>
</Stat>
</>
)}
</StatGroup>
</AccordionPanel>
</AccordionItem>
{(stats?.zaps_sent || stats?.zaps_received) && (
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Zap Stats
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb="2">
<StatGroup gap="4" whiteSpace="pre">
{stats.zaps_sent && (
<>
<Stat>
<StatLabel>Zap Sent</StatLabel>
<StatNumber>{stats.zaps_sent.count}</StatNumber>
</Stat>
<Stat>
<StatLabel>Total Sats Sent</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_sent.msats / 1000)}</StatNumber>
</Stat>
<Stat>
<StatLabel>Avg Zap Sent</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_sent.avg_msats / 1000)}</StatNumber>
</Stat>
<Stat>
<StatLabel>Biggest Zap Sent</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_sent.max_msats / 1000)}</StatNumber>
</Stat>
</>
)}
{stats.zaps_received && (
<>
<Stat>
<StatLabel>Zap Received</StatLabel>
<StatNumber>{stats.zaps_received.count}</StatNumber>
</Stat>
<Stat>
<StatLabel>Total Sats Received</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_received.msats / 1000)}</StatNumber>
</Stat>
<Stat>
<StatLabel>Avg Zap Received</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_received.avg_msats / 1000)}</StatNumber>
</Stat>
<Stat>
<StatLabel>Biggest Zap Received</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_received.max_msats / 1000)}</StatNumber>
</Stat>
</>
)}
</StatGroup>
<Text color="slategrey">
Stats from{" "}
<Link href="https://nostr.band" isExternal color="blue.500">
nostr.band
</Link>
</Text>
</AccordionPanel>
</AccordionItem>
)}
</Accordion>
);
}
function UserJoinedCommunities({ pubkey }: { pubkey: string }) {
const { pointers: communities } = useJoinedCommunitiesList(pubkey);
const columns = useBreakpointValue({ base: 1, lg: 2, xl: 3 }) ?? 1;
const showAllCommunities = useDisclosure();
if (communities.length === 0) return null;
return (
<Flex direction="column" px="2">
<Heading size="md" my="2">
Joined Communities ({communities.length})
</Heading>
<SimpleGrid spacing="4" columns={columns}>
{(showAllCommunities.isOpen ? communities : communities.slice(0, columns * 2)).map((pointer) => (
<ErrorBoundary key={pointer.identifier + pointer.pubkey}>
<PointerCommunityCard pointer={pointer} />
</ErrorBoundary>
))}
</SimpleGrid>
{!showAllCommunities.isOpen && communities.length > columns * 2 && (
<Button variant="link" py="4" onClick={showAllCommunities.onOpen}>
Show All
</Button>
)}
</Flex>
);
}
export default function UserAboutTab() {
const expanded = useDisclosure();
const { pubkey } = useOutletContext() as { pubkey: string };
@ -366,6 +190,7 @@ export default function UserAboutTab() {
</Button>
</Flex>
<UserPinnedEvents pubkey={pubkey} />
<UserJoinedCommunities pubkey={pubkey} />
</Flex>
);

View File

@ -0,0 +1,36 @@
import { Button, Flex, Heading, SimpleGrid, useDisclosure } from "@chakra-ui/react";
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
import useUserCommunitiesList from "../../../hooks/use-user-communities-list";
import { PointerCommunityCard } from "../../communities/components/community-card";
import { ErrorBoundary } from "../../../components/error-boundary";
import { useBreakpointValue } from "../../../providers/breakpoint-provider";
export default function UserJoinedCommunities({ pubkey }: { pubkey: string }) {
const contextRelays = useAdditionalRelayContext();
const { pointers: communities } = useUserCommunitiesList(pubkey, contextRelays, { alwaysRequest: true });
const columns = useBreakpointValue({ base: 1, lg: 2, xl: 3 }) ?? 1;
const showAllCommunities = useDisclosure();
if (communities.length === 0) return null;
return (
<Flex direction="column" px="2">
<Heading size="md" my="2">
Joined Communities ({communities.length})
</Heading>
<SimpleGrid spacing="4" columns={columns}>
{(showAllCommunities.isOpen ? communities : communities.slice(0, columns * 2)).map((pointer) => (
<ErrorBoundary key={pointer.identifier + pointer.pubkey}>
<PointerCommunityCard pointer={pointer} />
</ErrorBoundary>
))}
</SimpleGrid>
{!showAllCommunities.isOpen && communities.length > columns * 2 && (
<Button variant="link" pt="4" onClick={showAllCommunities.onOpen}>
Show All
</Button>
)}
</Flex>
);
}

View File

@ -0,0 +1,32 @@
import { Button, Flex, Heading, useDisclosure } from "@chakra-ui/react";
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
import useUserPinList from "../../../hooks/use-user-pin-list";
import { EmbedEventPointer } from "../../../components/embed-event";
export default function UserPinnedEvents({ pubkey }: { pubkey: string }) {
const contextRelays = useAdditionalRelayContext();
const { events, list } = useUserPinList(pubkey, contextRelays);
const showAll = useDisclosure();
if (events.length === 0) return null;
return (
<Flex direction="column" gap="2">
<Heading size="md" my="2">
Pinned
</Heading>
{(showAll.isOpen ? events : events.slice(0, 2)).map((event) => (
<EmbedEventPointer
key={event.id}
pointer={{ type: "nevent", data: { id: event.id, relays: event.relay ? [event.relay] : [] } }}
/>
))}
{!showAll.isOpen && events.length > 2 && (
<Button variant="link" pt="4" onClick={showAll.onOpen}>
Show All
</Button>
)}
</Flex>
);
}

View File

@ -0,0 +1,144 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Link,
Stat,
StatGroup,
StatHelpText,
StatLabel,
StatNumber,
Text,
} from "@chakra-ui/react";
import { useAsync } from "react-use";
import { Kind } from "nostr-tools";
import { readablizeSats } from "../../../helpers/bolt11";
import trustedUserStatsService from "../../../services/trusted-user-stats";
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
import useUserContactList from "../../../hooks/use-user-contact-list";
import { getPubkeysFromList } from "../../../helpers/nostr/lists";
import Timestamp from "../../../components/timestamp";
import useEventCount from "../../../hooks/use-event-count";
export default function UserStatsAccordion({ pubkey }: { pubkey: string }) {
const contextRelays = useAdditionalRelayContext();
const contacts = useUserContactList(pubkey, contextRelays);
const { value: stats } = useAsync(() => trustedUserStatsService.getUserStats(pubkey), [pubkey]);
const followerCount = useEventCount({ "#p": [pubkey], kinds: [Kind.Contacts] });
return (
<Accordion allowMultiple>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Network Stats
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb="2">
<StatGroup gap="4" whiteSpace="pre">
<Stat>
<StatLabel>Following</StatLabel>
<StatNumber>{contacts ? readablizeSats(getPubkeysFromList(contacts).length) : "Unknown"}</StatNumber>
{contacts && (
<StatHelpText>
Updated <Timestamp timestamp={contacts.created_at} />
</StatHelpText>
)}
</Stat>
{stats && (
<>
<Stat>
<StatLabel>Followers</StatLabel>
<StatNumber>{readablizeSats(followerCount ?? 0) || 0}</StatNumber>
</Stat>
<Stat>
<StatLabel>Notes & replies</StatLabel>
<StatNumber>{readablizeSats(stats.pub_note_count) || 0}</StatNumber>
</Stat>
<Stat>
<StatLabel>Reactions</StatLabel>
<StatNumber>{readablizeSats(stats.pub_reaction_count) || 0}</StatNumber>
</Stat>
</>
)}
</StatGroup>
</AccordionPanel>
</AccordionItem>
{(stats?.zaps_sent || stats?.zaps_received) && (
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Zap Stats
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb="2">
<StatGroup gap="4" whiteSpace="pre">
{stats.zaps_sent && (
<>
<Stat>
<StatLabel>Zap Sent</StatLabel>
<StatNumber>{stats.zaps_sent.count}</StatNumber>
</Stat>
<Stat>
<StatLabel>Total Sats Sent</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_sent.msats / 1000)}</StatNumber>
</Stat>
<Stat>
<StatLabel>Avg Zap Sent</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_sent.avg_msats / 1000)}</StatNumber>
</Stat>
<Stat>
<StatLabel>Biggest Zap Sent</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_sent.max_msats / 1000)}</StatNumber>
</Stat>
</>
)}
{stats.zaps_received && (
<>
<Stat>
<StatLabel>Zap Received</StatLabel>
<StatNumber>{stats.zaps_received.count}</StatNumber>
</Stat>
<Stat>
<StatLabel>Total Sats Received</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_received.msats / 1000)}</StatNumber>
</Stat>
<Stat>
<StatLabel>Avg Zap Received</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_received.avg_msats / 1000)}</StatNumber>
</Stat>
<Stat>
<StatLabel>Biggest Zap Received</StatLabel>
<StatNumber>{readablizeSats(stats.zaps_received.max_msats / 1000)}</StatNumber>
</Stat>
</>
)}
</StatGroup>
<Text color="slategrey">
Stats from{" "}
<Link href="https://nostr.band" isExternal color="blue.500">
nostr.band
</Link>
</Text>
</AccordionPanel>
</AccordionItem>
)}
</Accordion>
);
}