mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-09 20:29:17 +02:00
note pinning
This commit is contained in:
parent
87cf0198b0
commit
7cbffb96a4
5
.changeset/proud-forks-fry.md
Normal file
5
.changeset/proud-forks-fry.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add option to pin notes
|
5
.changeset/spicy-flowers-march.md
Normal file
5
.changeset/spicy-flowers-march.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Show pinned notes on user profile
|
@ -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;
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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) {
|
15
src/hooks/use-user-pin-list.ts
Normal file
15
src/hooks/use-user-pin-list.ts
Normal 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 };
|
||||
}
|
@ -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)) ?? [],
|
||||
};
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
36
src/views/user/about/user-joined-communities.tsx
Normal file
36
src/views/user/about/user-joined-communities.tsx
Normal 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>
|
||||
);
|
||||
}
|
32
src/views/user/about/user-pinned-events.tsx
Normal file
32
src/views/user/about/user-pinned-events.tsx
Normal 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>
|
||||
);
|
||||
}
|
144
src/views/user/about/user-stats-accordion.tsx
Normal file
144
src/views/user/about/user-stats-accordion.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user