mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-07-05 04:54:53 +02:00
note pinning
This commit is contained in:
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 Download01 from "./icons/download-01";
|
||||||
import Repeat01 from "./icons/repeat-01";
|
import Repeat01 from "./icons/repeat-01";
|
||||||
import ReverseLeft from "./icons/reverse-left";
|
import ReverseLeft from "./icons/reverse-left";
|
||||||
|
import Pin01 from "./icons/pin-01";
|
||||||
|
|
||||||
const defaultProps: IconProps = { boxSize: 4 };
|
const defaultProps: IconProps = { boxSize: 4 };
|
||||||
|
|
||||||
@ -89,6 +90,7 @@ export const ChevronRightIcon = ChevronRight;
|
|||||||
export const LightningIcon = Zap;
|
export const LightningIcon = Zap;
|
||||||
export const RelayIcon = Server04;
|
export const RelayIcon = Server04;
|
||||||
export const BroadcastEventIcon = Share07;
|
export const BroadcastEventIcon = Share07;
|
||||||
|
export const PinIcon = Pin01;
|
||||||
|
|
||||||
export const ExternalLinkIcon = Share04;
|
export const ExternalLinkIcon = Share04;
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ import NostrPublishAction from "../../../classes/nostr-publish-action";
|
|||||||
import clientRelaysService from "../../../services/client-relays";
|
import clientRelaysService from "../../../services/client-relays";
|
||||||
import { useSigningContext } from "../../../providers/signing-provider";
|
import { useSigningContext } from "../../../providers/signing-provider";
|
||||||
import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from "../../icons";
|
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 useCurrentAccount from "../../../hooks/use-current-account";
|
||||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||||
import { createCoordinate } from "../../../services/replaceable-event-requester";
|
import { createCoordinate } from "../../../services/replaceable-event-requester";
|
||||||
@ -54,7 +54,7 @@ export default function RepostModal({
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { requestSignature } = useSigningContext();
|
const { requestSignature } = useSigningContext();
|
||||||
const showCommunities = useDisclosure();
|
const showCommunities = useDisclosure();
|
||||||
const { pointers } = useJoinedCommunitiesList(account?.pubkey);
|
const { pointers } = useUserCommunitiesList(account?.pubkey);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const repost = async (communityPointer?: AddressPointer) => {
|
const repost = async (communityPointer?: AddressPointer) => {
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
import { MenuItem, useDisclosure, useToast } from "@chakra-ui/react";
|
||||||
import { useCopyToClipboard } from "react-use";
|
import { useCopyToClipboard } from "react-use";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
|
import dayjs from "dayjs";
|
||||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
|
||||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BroadcastEventIcon,
|
BroadcastEventIcon,
|
||||||
@ -17,7 +14,11 @@ import {
|
|||||||
RepostIcon,
|
RepostIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
UnmuteIcon,
|
UnmuteIcon,
|
||||||
|
PinIcon,
|
||||||
} from "../icons";
|
} 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 NoteReactionsModal from "./note-zaps-modal";
|
||||||
import NoteDebugModal from "../debug-modals/note-debug-modal";
|
import NoteDebugModal from "../debug-modals/note-debug-modal";
|
||||||
import useCurrentAccount from "../../hooks/use-current-account";
|
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 { useMuteModalContext } from "../../providers/mute-modal-provider";
|
||||||
import NoteTranslationModal from "../note-translation-modal";
|
import NoteTranslationModal from "../note-translation-modal";
|
||||||
import Translate01 from "../icons/translate-01";
|
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">) {
|
export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
@ -90,6 +134,7 @@ export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Om
|
|||||||
<MenuItem onClick={broadcast} icon={<BroadcastEventIcon />}>
|
<MenuItem onClick={broadcast} icon={<BroadcastEventIcon />}>
|
||||||
Broadcast
|
Broadcast
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<PinNoteItem event={event} />
|
||||||
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
||||||
View Raw
|
View Raw
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { Select, SelectProps } from "@chakra-ui/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 useCurrentAccount from "../../hooks/use-current-account";
|
||||||
import { getCommunityName } from "../../helpers/nostr/communities";
|
import { getCommunityName } from "../../helpers/nostr/communities";
|
||||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
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 CommunitySelect = forwardRef<HTMLSelectElement, Omit<SelectProps, "children">>(({ ...props }, ref) => {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const { pointers } = useJoinedCommunitiesList(account?.pubkey);
|
const { pointers } = useUserCommunitiesList(account?.pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select placeholder="Select community" {...props} ref={ref}>
|
<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 useCurrentAccount from "./use-current-account";
|
||||||
import useReplaceableEvent from "./use-replaceable-event";
|
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 account = useCurrentAccount();
|
||||||
const key = pubkey ?? account?.pubkey;
|
const key = pubkey ?? account?.pubkey;
|
||||||
|
|
||||||
@ -22,10 +22,9 @@ export default function useJoinedCommunitiesList(pubkey?: string, opts?: Request
|
|||||||
[],
|
[],
|
||||||
opts,
|
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;
|
let useList = list || oldList;
|
||||||
console.log(list, oldList);
|
|
||||||
|
|
||||||
// if both exist, use the newest one
|
// if both exist, use the newest one
|
||||||
if (list && oldList) {
|
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 { Button, ButtonProps, useToast } from "@chakra-ui/react";
|
||||||
|
|
||||||
import { DraftNostrEvent, NostrEvent, isDTag } from "../../../types/nostr-event";
|
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 useCurrentAccount from "../../../hooks/use-current-account";
|
||||||
import { getCommunityName } from "../../../helpers/nostr/communities";
|
import { getCommunityName } from "../../../helpers/nostr/communities";
|
||||||
import { COMMUNITIES_LIST_KIND, listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists";
|
import { COMMUNITIES_LIST_KIND, listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists";
|
||||||
@ -16,10 +16,10 @@ export default function CommunityJoinButton({
|
|||||||
community,
|
community,
|
||||||
...props
|
...props
|
||||||
}: Omit<ButtonProps, "children"> & { community: NostrEvent }) {
|
}: Omit<ButtonProps, "children"> & { community: NostrEvent }) {
|
||||||
const account = useCurrentAccount();
|
|
||||||
const { list, pointers } = useJoinedCommunitiesList(account?.pubkey);
|
|
||||||
const { requestSignature } = useSigningContext();
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
const { list, pointers } = useUserCommunitiesList(account?.pubkey);
|
||||||
|
const { requestSignature } = useSigningContext();
|
||||||
|
|
||||||
const isSubscribed = pointers.find(
|
const isSubscribed = pointers.find(
|
||||||
(cord) => cord.identifier === getCommunityName(community) && cord.pubkey === community.pubkey,
|
(cord) => cord.identifier === getCommunityName(community) && cord.pubkey === community.pubkey,
|
||||||
@ -29,7 +29,7 @@ export default function CommunityJoinButton({
|
|||||||
try {
|
try {
|
||||||
const favList = {
|
const favList = {
|
||||||
kind: COMMUNITIES_LIST_KIND,
|
kind: COMMUNITIES_LIST_KIND,
|
||||||
content: "",
|
content: list?.content ?? "",
|
||||||
created_at: dayjs().unix(),
|
created_at: dayjs().unix(),
|
||||||
tags: list?.tags.filter((t) => !isDTag(t)) ?? [],
|
tags: list?.tags.filter((t) => !isDTag(t)) ?? [],
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,7 @@ import dayjs from "dayjs";
|
|||||||
|
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import { ErrorBoundary } from "../../components/error-boundary";
|
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 useCurrentAccount from "../../hooks/use-current-account";
|
||||||
import CommunityCard from "./components/community-card";
|
import CommunityCard from "./components/community-card";
|
||||||
import CommunityCreateModal, { FormValues } from "./components/community-create-modal";
|
import CommunityCreateModal, { FormValues } from "./components/community-create-modal";
|
||||||
@ -62,7 +62,9 @@ function CommunitiesHomePage() {
|
|||||||
const createModal = useDisclosure();
|
const createModal = useDisclosure();
|
||||||
|
|
||||||
const readRelays = useReadRelayUrls();
|
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(
|
const communities = useReplaceableEvents(communityCoordinates, readRelays).sort(
|
||||||
(a, b) => b.created_at - a.created_at,
|
(a, b) => b.created_at - a.created_at,
|
||||||
);
|
);
|
||||||
|
@ -1,35 +1,11 @@
|
|||||||
import { useOutletContext, Link as RouterLink } from "react-router-dom";
|
import { useOutletContext, Link as RouterLink } from "react-router-dom";
|
||||||
import {
|
import { Box, Button, Flex, Heading, IconButton, Image, Link, Text, useDisclosure } from "@chakra-ui/react";
|
||||||
Accordion,
|
import { nip19 } from "nostr-tools";
|
||||||
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 { readablizeSats } from "../../../helpers/bolt11";
|
|
||||||
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
||||||
import { getLudEndpoint } from "../../../helpers/lnurl";
|
import { getLudEndpoint } from "../../../helpers/lnurl";
|
||||||
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
|
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
|
||||||
import { truncatedId } from "../../../helpers/nostr/events";
|
import { truncatedId } from "../../../helpers/nostr/events";
|
||||||
import trustedUserStatsService from "../../../services/trusted-user-stats";
|
|
||||||
import { parseAddress } from "../../../services/dns-identity";
|
import { parseAddress } from "../../../services/dns-identity";
|
||||||
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
|
||||||
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
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 UserZapButton from "../components/user-zap-button";
|
||||||
import { UserProfileMenu } from "../components/user-profile-menu";
|
import { UserProfileMenu } from "../components/user-profile-menu";
|
||||||
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
|
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 UserProfileBadges from "./user-profile-badges";
|
||||||
import useEventCount from "../../../hooks/use-event-count";
|
import UserJoinedCommunities from "./user-joined-communities";
|
||||||
import useJoinedCommunitiesList from "../../../hooks/use-communities-joined-list";
|
import UserPinnedEvents from "./user-pinned-events";
|
||||||
import { PointerCommunityCard } from "../../communities/components/community-card";
|
import UserStatsAccordion from "./user-stats-accordion";
|
||||||
import { ErrorBoundary } from "../../../components/error-boundary";
|
|
||||||
import { useBreakpointValue } from "../../../providers/breakpoint-provider";
|
|
||||||
|
|
||||||
function buildDescriptionContent(description: string) {
|
function buildDescriptionContent(description: string) {
|
||||||
let content: EmbedableContent = [description.trim()];
|
let content: EmbedableContent = [description.trim()];
|
||||||
@ -70,153 +41,6 @@ function buildDescriptionContent(description: string) {
|
|||||||
return content;
|
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() {
|
export default function UserAboutTab() {
|
||||||
const expanded = useDisclosure();
|
const expanded = useDisclosure();
|
||||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||||
@ -366,6 +190,7 @@ export default function UserAboutTab() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
<UserPinnedEvents pubkey={pubkey} />
|
||||||
<UserJoinedCommunities pubkey={pubkey} />
|
<UserJoinedCommunities pubkey={pubkey} />
|
||||||
</Flex>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user